mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-20 07:14:13 +02:00
Merge pull request #891 from ispyisail/feature/162-js-scripting
Add JavaScript scripting: read, export, edit geometry, undo, navigate
This commit is contained in:
@@ -109,6 +109,22 @@ if(QT_VERSION_MAJOR GREATER_EQUAL 6)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# JavaScript scripting (bugtracker #162: `--run script.js`, and later a
|
||||
# "Run Script..." menu action) needs QJSEngine, in the Qml module. As with
|
||||
# QtPdf above, this is not fatal when missing: some minimal Qt6 packagings
|
||||
# may not ship it, and scripting is optional functionality nothing else in
|
||||
# the application depends on. When it's missing the feature is silently
|
||||
# disabled - see the QET_HAS_SCRIPTING guard in qetscripting.*.
|
||||
set(QET_HAS_SCRIPTING FALSE)
|
||||
find_package(Qt${QT_VERSION_MAJOR} QUIET COMPONENTS Qml)
|
||||
if(TARGET Qt${QT_VERSION_MAJOR}::Qml)
|
||||
set(QET_HAS_SCRIPTING TRUE)
|
||||
list(APPEND QET_PRIVATE_LIBRARIES Qt::Qml)
|
||||
add_compile_definitions(QET_HAS_SCRIPTING)
|
||||
else()
|
||||
message(STATUS "Qt Qml module not available: JavaScript scripting (--run) disabled")
|
||||
endif()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
# CMake < 4.3 only creates the SQLite::SQLite3 target (no SQLite3::SQLite3
|
||||
|
||||
@@ -851,6 +851,20 @@ if(QT_VERSION_MAJOR GREATER_EQUAL 6)
|
||||
)
|
||||
endif()
|
||||
|
||||
# JavaScript scripting (bugtracker #162). Unconditionally in the source
|
||||
# list, like the QtPdf files above: this file is included before the
|
||||
# QET_HAS_SCRIPTING probe runs in the top-level CMakeLists.txt, so the
|
||||
# variable isn't set yet here. Same pattern as QtPdf: always compiled, the
|
||||
# actual Qt::Qml dependent code is behind #ifdef QET_HAS_SCRIPTING inside
|
||||
# qetscripting.cpp/qetscriptapi.cpp themselves, compiling to a harmless
|
||||
# stub when the module wasn't found.
|
||||
list(APPEND QET_SRC_FILES
|
||||
${QET_DIR}/sources/scripting/qetscriptapi.cpp
|
||||
${QET_DIR}/sources/scripting/qetscriptapi.h
|
||||
${QET_DIR}/sources/scripting/qetscripting.cpp
|
||||
${QET_DIR}/sources/scripting/qetscripting.h
|
||||
)
|
||||
|
||||
set(TS_FILES
|
||||
${QET_DIR}/lang/qet_ar.ts
|
||||
${QET_DIR}/lang/qet_ca.ts
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
#include "scripting/qetscripting.h"
|
||||
#endif
|
||||
#include "logging/eventloopwatchdog.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "machine_info.h"
|
||||
@@ -135,6 +138,17 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
QET::QetMessageBox::setNonInteractive(true);
|
||||
return CLIExport::run(export_app.arguments());
|
||||
}
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
// Headless scripting: --run <script.js> <project.qet> (bugtracker
|
||||
// #162). Same reasoning as the export branch above for running
|
||||
// before SingleApplication and answering message boxes headlessly.
|
||||
if (QetScripting::isRunRequest(raw_args)) {
|
||||
QApplication script_app(argc, argv);
|
||||
QETProject::setBackupEnabled(false);
|
||||
QET::QetMessageBox::setNonInteractive(true);
|
||||
return QetScripting::run(script_app.arguments());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Resolve the logger's state (log directory, session filename, open
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetdiagrameditor.h"
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
#include "scripting/qetscripting.h"
|
||||
#endif
|
||||
#include <QCoreApplication>
|
||||
#include "ElementsCollection/elementscollectionwidget.h"
|
||||
#include "QWidgetAnimation/qwidgetanimation.h"
|
||||
@@ -558,6 +561,15 @@ void QETDiagramEditor::setUpActions()
|
||||
" sans avoir à fermer et rouvrir le projet (action non annulable)"));
|
||||
connect(m_reload_element_drawings, &QAction::triggered, this, &QETDiagramEditor::slot_reloadElementDrawings);
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
// Run a JavaScript macro against the current project (bugtracker #162).
|
||||
m_run_script = new QAction(tr("Exécuter un script..."), this);
|
||||
m_run_script->setStatusTip(
|
||||
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);
|
||||
#endif
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this);
|
||||
connect(m_export_project_db, &QAction::triggered, [this]() {
|
||||
@@ -1012,6 +1024,9 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_project -> addAction(m_project_wiring_list_view);
|
||||
menu_project -> addAction(m_terminal_numbering);
|
||||
menu_project -> addAction(m_reload_element_drawings);
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
menu_project -> addAction(m_run_script);
|
||||
#endif
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
menu_project -> addSeparator();
|
||||
menu_project -> addAction(m_export_project_db);
|
||||
@@ -1867,6 +1882,9 @@ void QETDiagramEditor::slot_updateActions()
|
||||
m_project_wiring_list_view -> setEnabled(opened_project);
|
||||
m_terminal_numbering -> setEnabled(editable_project);
|
||||
m_reload_element_drawings -> setEnabled(opened_project);
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
m_run_script -> setEnabled(opened_project);
|
||||
#endif
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
m_export_project_db -> setEnabled(editable_project);
|
||||
#endif
|
||||
@@ -3075,3 +3093,29 @@ void QETDiagramEditor::slot_reloadElementDrawings() {
|
||||
box.setDetailedText(geometry_changed.join(QLatin1Char('\n')));
|
||||
box.exec();
|
||||
}
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
/**
|
||||
@brief QETDiagramEditor::slot_runScript
|
||||
Run a JavaScript macro against the current project (bugtracker #162).
|
||||
See QetScriptApi for what a script can do -- read-mostly: folio/element/
|
||||
conductor counts and the same export operations the --export-* CLI
|
||||
flags provide. Export/save calls inside the script act on this
|
||||
project's file on disk, so unsaved edits in the open editor are not
|
||||
visible to the script; save first if that matters.
|
||||
*/
|
||||
void QETDiagramEditor::slot_runScript() {
|
||||
QETProject *project = currentProject();
|
||||
if (!project) return;
|
||||
|
||||
const QString script_path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Exécuter un script"),
|
||||
QString(),
|
||||
tr("Scripts JavaScript (*.js);;Tous les fichiers (*)")
|
||||
);
|
||||
if (script_path.isEmpty()) return;
|
||||
|
||||
QetScripting::runOnProject(script_path, project, currentDiagramView());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -137,6 +137,9 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void editProjectProperties(QETProject *);
|
||||
void slot_terminalNumbering();
|
||||
void slot_reloadElementDrawings();
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
void slot_runScript();
|
||||
#endif
|
||||
void editDiagramProperties(DiagramView *);
|
||||
void editDiagramProperties(Diagram *);
|
||||
void addDiagramToProject(QETProject *);
|
||||
@@ -213,6 +216,9 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
|
||||
*m_terminal_numbering, ///< Action to launch terminal numbering
|
||||
*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
|
||||
#endif
|
||||
*m_export_project_db, ///Export to file the internal database of the current project
|
||||
*m_tile_window, ///< Show MDI subwindows as tile
|
||||
*m_cascade_window, ///< Show MDI subwindows as cascade
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
#include "qetscriptapi.h"
|
||||
|
||||
#include "../ElementsCollection/elementslocation.h"
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../cli_export.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../factory/elementfactory.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetmessagebox.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetresult.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../undocommand/deleteqgraphicsitemcommand.h"
|
||||
|
||||
#include <QTextStream>
|
||||
#include <QUndoCommand>
|
||||
|
||||
QetScriptApi::QetScriptApi(QETProject *project, DiagramView *view, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_project(project),
|
||||
m_view(view)
|
||||
{
|
||||
}
|
||||
|
||||
QString QetScriptApi::projectTitle() const
|
||||
{
|
||||
return m_project ? m_project->title() : QString();
|
||||
}
|
||||
|
||||
QString QetScriptApi::filePath() const
|
||||
{
|
||||
return m_project ? m_project->filePath() : QString();
|
||||
}
|
||||
|
||||
int QetScriptApi::folioCount() const
|
||||
{
|
||||
return m_project ? m_project->diagrams().count() : 0;
|
||||
}
|
||||
|
||||
QString QetScriptApi::folioTitle(int index) const
|
||||
{
|
||||
if (!m_project) return QString();
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (index < 0 || index >= diagrams.count()) return QString();
|
||||
return diagrams.at(index)->title();
|
||||
}
|
||||
|
||||
int QetScriptApi::elementCount(int folioIndex) const
|
||||
{
|
||||
if (!m_project) return 0;
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
|
||||
DiagramContent content(diagrams.at(folioIndex), false);
|
||||
return content.m_elements.count();
|
||||
}
|
||||
|
||||
int QetScriptApi::conductorCount(int folioIndex) const
|
||||
{
|
||||
if (!m_project) return 0;
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
|
||||
DiagramContent content(diagrams.at(folioIndex), false);
|
||||
return content.conductors(DiagramContent::AnyConductor).count();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::runFlag
|
||||
Build a CLIExport::run() argument list from this project's own file
|
||||
path plus @p args, and run it. Reopens the project from disk -- see the
|
||||
class comment for why that trade-off was made.
|
||||
@param flag one of the --export-* / --resave / --set-titleblock flags
|
||||
@param args the flag's own positional arguments (output path, etc.)
|
||||
@return true if CLIExport::run() returned 0 (success)
|
||||
*/
|
||||
bool QetScriptApi::runFlag(const QString &flag, const QStringList &args)
|
||||
{
|
||||
if (!m_project) {
|
||||
log(QStringLiteral("qet.%1: no project").arg(flag));
|
||||
return false;
|
||||
}
|
||||
const QString path = m_project->filePath();
|
||||
if (path.isEmpty()) {
|
||||
log(QStringLiteral("qet.%1: project has no file path -- save it first").arg(flag));
|
||||
return false;
|
||||
}
|
||||
QStringList full_args;
|
||||
full_args << flag << path << args;
|
||||
|
||||
if (m_view)
|
||||
{
|
||||
// Interactive "Run Script...": CLIExport::run() opens a second,
|
||||
// temporary QETProject on the same file the GUI already has open.
|
||||
// Backups are only disabled on the headless --run path (main.cpp);
|
||||
// here that second, short-lived project would otherwise manage its
|
||||
// own KAutoSaveFile for the same path as the user's real, already
|
||||
// open project, racing it and risking a stale-restore prompt next
|
||||
// launch. Disable backups for just this one export and restore
|
||||
// them right after -- the headless path must never see this
|
||||
// change, since it depends on backups staying off for the whole
|
||||
// run (see the crash note next to the other setBackupEnabled(false)
|
||||
// call in main.cpp).
|
||||
QETProject::setBackupEnabled(false);
|
||||
const int result = CLIExport::run(full_args);
|
||||
QETProject::setBackupEnabled(true);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
return CLIExport::run(full_args) == 0;
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportPdf(const QString &output, bool showTerminals)
|
||||
{
|
||||
QStringList args{output};
|
||||
if (showTerminals) args << QStringLiteral("--show-terminals");
|
||||
return runFlag(QStringLiteral("--export-pdf"), args);
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportPng(const QString &outDir, bool showTerminals)
|
||||
{
|
||||
QStringList args{outDir};
|
||||
if (showTerminals) args << QStringLiteral("--show-terminals");
|
||||
return runFlag(QStringLiteral("--export-png"), args);
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportSvg(const QString &outDir, bool showTerminals)
|
||||
{
|
||||
QStringList args{outDir};
|
||||
if (showTerminals) args << QStringLiteral("--show-terminals");
|
||||
return runFlag(QStringLiteral("--export-svg"), args);
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportCables(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-cables"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportWires(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-wires"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportBom(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-bom"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportWiring(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-wiring"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportNets(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-nets"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportLinks(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--export-links"), {output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::exportInfo(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--info"), output.isEmpty() ? QStringList{} : QStringList{output});
|
||||
}
|
||||
|
||||
bool QetScriptApi::setTitleBlock(const QString &output, const QStringList &assignments)
|
||||
{
|
||||
QStringList args{output};
|
||||
args << assignments;
|
||||
return runFlag(QStringLiteral("--set-titleblock"), args);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::save
|
||||
Write this project's own current in-memory state -- unlike every export
|
||||
method above, this does NOT go through runFlag()/CLIExport::run(): that
|
||||
reopens the project fresh from its file on disk, which would never see
|
||||
any addElement()/setElementPosition()/moveElement()/deleteElement() this
|
||||
script made, only silently rewrite the file exactly as it already was.
|
||||
Caught by testing this against a script that added an element and
|
||||
called save(): the element counted correctly in memory but the saved
|
||||
file didn't have it, since the old implementation opened an unrelated,
|
||||
unmodified second copy of the project to write.
|
||||
|
||||
With no output path, this goes through QETProject::write() -- the same
|
||||
path "Enregistrer" uses -- so it honours read-only mode the same way,
|
||||
updates saveddate/savedtime, and clears the modified flag. A plain
|
||||
QFile write used to skip all of that and could tear the file on an
|
||||
interruption, where write() goes through QET::writeXmlFile()'s
|
||||
QSaveFile. With an explicit output path this is a save to a different
|
||||
file, so it goes through QET::writeXmlFile() directly without touching
|
||||
the project's own filePath() or read-only state, the same way "Save As"
|
||||
targeting a writable location is allowed even for a project opened
|
||||
read-only.
|
||||
@param output path to write to; the project's own file path if empty
|
||||
@return false if there is no output path to use, or the write failed
|
||||
*/
|
||||
bool QetScriptApi::save(const QString &output)
|
||||
{
|
||||
if (!m_project) return false;
|
||||
|
||||
if (output.isEmpty())
|
||||
{
|
||||
if (m_project->filePath().isEmpty()) {
|
||||
log(QStringLiteral("qet.save: no output path given and the project has none of its own -- pass one"));
|
||||
return false;
|
||||
}
|
||||
const QETResult result = m_project->write();
|
||||
if (!result.isOk()) {
|
||||
log(QStringLiteral("qet.save: %1").arg(result.errorMessage()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QDomDocument xml_doc(m_project->toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_doc, output, &error_message)) {
|
||||
log(QStringLiteral("qet.save: %1").arg(error_message));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void QetScriptApi::log(const QString &message)
|
||||
{
|
||||
QTextStream(stderr) << message << "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::findElement
|
||||
@param folioIndex
|
||||
@param elementUuid as returned by addElement(), or read from the
|
||||
element's own uuid attribute
|
||||
@return the matching element on that folio, or nullptr
|
||||
*/
|
||||
Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const
|
||||
{
|
||||
if (!m_project) return nullptr;
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return nullptr;
|
||||
DiagramContent content(diagrams.at(folioIndex), false);
|
||||
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||
if (elmt->uuid().toString() == elementUuid)
|
||||
return elmt;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::addElement
|
||||
Place a new element on a folio, through the same AddGraphicsObjectCommand
|
||||
the interactive drag-from-collection-panel path uses (see
|
||||
DiagramEventAddElement::addElement()) -- so Ctrl+Z undoes it exactly as
|
||||
it would a manually dropped element.
|
||||
|
||||
Like that path, the element is first imported into the project's own
|
||||
embedded collection (QETProject::importElement()): building straight
|
||||
from a common://custom:// location without embedding it left the saved
|
||||
.qet referencing a definition outside the project, missing on any
|
||||
machine that doesn't have that same collection installed.
|
||||
|
||||
importElement() can, on a name collision with a different, already
|
||||
embedded element, pop a modal dialog asking the user to choose -- with
|
||||
nobody there to answer it in a script, headless or interactive, that is
|
||||
exactly the hang class this whole API is built to avoid (see the class
|
||||
comment). Detected and refused before it can happen, rather than risked.
|
||||
|
||||
@param folioIndex
|
||||
@param locationPath an element collection path, e.g.
|
||||
"embed://some/path.elmt" or "common://10_electric/...elmt"
|
||||
@param x @param y target position, in the diagram's own coordinates
|
||||
@return the new element's uuid (empty string on failure -- read-only
|
||||
project, bad folio index, an import collision, or the location could
|
||||
not be resolved/built)
|
||||
*/
|
||||
QString QetScriptApi::addElement(int folioIndex, const QString &locationPath, double x, double y)
|
||||
{
|
||||
if (!m_project) return QString();
|
||||
if (m_project->isReadOnly()) {
|
||||
log(QStringLiteral("qet.addElement: project is read-only"));
|
||||
return QString();
|
||||
}
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
||||
Diagram *diagram = diagrams.at(folioIndex);
|
||||
|
||||
// ElementsLocation::setPath() forces ANY path to embed:// as soon as a
|
||||
// non-null project is given, common://custom:// included -- passing
|
||||
// m_project unconditionally here (as the pre-fix code did) silently
|
||||
// turned every non-embed:: locationPath into a lookup for an embedded
|
||||
// element that was never there. Caught by actually running this
|
||||
// against a common:// path: "does not resolve to an element" even
|
||||
// though the file plainly exists.
|
||||
ElementsLocation location = locationPath.startsWith(QStringLiteral("embed://"))
|
||||
? ElementsLocation(locationPath, m_project)
|
||||
: ElementsLocation(locationPath);
|
||||
if (!location.isElement() || !location.exist()) {
|
||||
log(QStringLiteral("qet.addElement: '%1' does not resolve to an element").arg(locationPath));
|
||||
return QString();
|
||||
}
|
||||
|
||||
ElementsLocation import_location = location;
|
||||
if (!(location.isProject() && location.project() == m_project))
|
||||
{
|
||||
const QString import_path = location.isFileSystem()
|
||||
? QStringLiteral("import/") + location.collectionPath(false)
|
||||
: location.collectionPath(false);
|
||||
const ElementsLocation existing(import_path, m_project);
|
||||
if (existing.exist() && existing.uuid() != location.uuid()) {
|
||||
log(QStringLiteral("qet.addElement: '%1' would collide with a different element "
|
||||
"already embedded under the same name -- refusing rather than "
|
||||
"risk the interactive import-conflict dialog").arg(locationPath));
|
||||
return QString();
|
||||
}
|
||||
|
||||
import_location = m_project->importElement(location);
|
||||
if (!import_location.exist()) {
|
||||
log(QStringLiteral("qet.addElement: could not import '%1' into the project").arg(locationPath));
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
int state = 0;
|
||||
Element *element = ElementFactory::Instance()->createElement(import_location, nullptr, &state);
|
||||
if (state) {
|
||||
delete element;
|
||||
log(QStringLiteral("qet.addElement: could not build element from '%1'").arg(locationPath));
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QPointF pos(x, y);
|
||||
element->setPos(pos);
|
||||
diagram->addItem(element);
|
||||
|
||||
auto *undo_group = new QUndoCommand(QObject::tr("Ajouter %1").arg(element->name()));
|
||||
new AddGraphicsObjectCommand(element, diagram, pos, undo_group);
|
||||
diagram->undoStack().push(undo_group);
|
||||
|
||||
return element->uuid().toString();
|
||||
}
|
||||
|
||||
bool QetScriptApi::setElementPosition(int folioIndex, const QString &elementUuid, double x, double y)
|
||||
{
|
||||
if (m_project && m_project->isReadOnly()) {
|
||||
log(QStringLiteral("qet.setElementPosition: project is read-only"));
|
||||
return false;
|
||||
}
|
||||
Element *element = findElement(folioIndex, elementUuid);
|
||||
if (!element) return false;
|
||||
|
||||
const QVariant old_value = element->pos();
|
||||
const QVariant new_value = QPointF(x, y);
|
||||
if (old_value == new_value) return true; // already there; nothing to push
|
||||
|
||||
auto *cmd = new QPropertyUndoCommand(element, "pos", old_value, new_value);
|
||||
cmd->setText(QObject::tr("Déplacer %1").arg(element->name()));
|
||||
m_project->undoStack()->push(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::moveElement(int folioIndex, const QString &elementUuid, double dx, double dy)
|
||||
{
|
||||
Element *element = findElement(folioIndex, elementUuid);
|
||||
if (!element) return false;
|
||||
const QPointF p = element->pos();
|
||||
return setElementPosition(folioIndex, elementUuid, p.x() + dx, p.y() + dy);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::deleteElement
|
||||
Through DeleteQGraphicsItemCommand -- the same command the Delete key
|
||||
uses -- so any conductors attached to the element's terminals are
|
||||
cleaned up the same way, not left dangling.
|
||||
*/
|
||||
bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid)
|
||||
{
|
||||
if (!m_project) return false;
|
||||
if (m_project->isReadOnly()) {
|
||||
log(QStringLiteral("qet.deleteElement: project is read-only"));
|
||||
return false;
|
||||
}
|
||||
Element *element = findElement(folioIndex, elementUuid);
|
||||
if (!element) return false;
|
||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
||||
|
||||
DiagramContent content;
|
||||
content.m_elements << element;
|
||||
if (DeleteQGraphicsItemCommand::hasNonDeletableTerminal(content)) {
|
||||
log(QStringLiteral("qet.deleteElement: %1 has a non-deletable terminal (linked master/slave?), refusing").arg(elementUuid));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *cmd = new DeleteQGraphicsItemCommand(diagram, content);
|
||||
diagram->undoStack().push(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::undo()
|
||||
{
|
||||
if (!m_project || !m_project->undoStack()->canUndo()) return false;
|
||||
m_project->undoStack()->undo();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::redo()
|
||||
{
|
||||
if (!m_project || !m_project->undoStack()->canRedo()) return false;
|
||||
m_project->undoStack()->redo();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::canUndo() const
|
||||
{
|
||||
return m_project && m_project->undoStack()->canUndo();
|
||||
}
|
||||
|
||||
bool QetScriptApi::canRedo() const
|
||||
{
|
||||
return m_project && m_project->undoStack()->canRedo();
|
||||
}
|
||||
|
||||
bool QetScriptApi::selectElement(const QString &elementUuid)
|
||||
{
|
||||
if (!m_project) return false;
|
||||
for (Diagram *diagram : m_project->diagrams()) {
|
||||
DiagramContent content(diagram, false);
|
||||
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||
if (elmt->uuid().toString() == elementUuid) {
|
||||
elmt->setSelected(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void QetScriptApi::deselectAll(int folioIndex)
|
||||
{
|
||||
if (!m_project) return;
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return;
|
||||
diagrams.at(folioIndex)->clearSelection();
|
||||
}
|
||||
|
||||
bool QetScriptApi::zoomFit()
|
||||
{
|
||||
if (!m_view) return false;
|
||||
m_view->zoomFit();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::zoomToContent()
|
||||
{
|
||||
if (!m_view) return false;
|
||||
m_view->zoomContent();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QetScriptApi::zoomReset()
|
||||
{
|
||||
if (!m_view) return false;
|
||||
m_view->zoomReset();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::showMessage
|
||||
A modal QET::QetMessageBox::information() -- safe to call headless
|
||||
because non-interactive mode is already set for the whole process
|
||||
before any script runs (main.cpp), which is exactly the mechanism
|
||||
that keeps --export-* and --resave from hanging on a warning dialog.
|
||||
Interactively, a real person is there to see and dismiss it.
|
||||
*/
|
||||
void QetScriptApi::showMessage(const QString &text)
|
||||
{
|
||||
QET::QetMessageBox::information(nullptr, QObject::tr("Script"), text);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
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. All four refuse on a read-only project,
|
||||
same as their GUI equivalents check Diagram::isReadOnly() before
|
||||
editing. addElement() also imports the element into the project's
|
||||
own embedded collection first (QETProject::importElement()), same
|
||||
as the drag-from-collection-panel path -- without it, the saved
|
||||
file referenced a definition outside the project and went missing
|
||||
on a machine without that same collection installed. 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 -- including addElement(), which detects an
|
||||
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.
|
||||
*/
|
||||
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
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
#include "qetscripting.h"
|
||||
|
||||
#include "qetscriptapi.h"
|
||||
#include "../qetmessagebox.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QObject>
|
||||
#include <QTextStream>
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
#include <QJSEngine>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
QTextStream out(stdout);
|
||||
QTextStream err(stderr);
|
||||
}
|
||||
|
||||
namespace QetScripting {
|
||||
|
||||
bool isRunRequest(const QStringList &args)
|
||||
{
|
||||
return args.contains(QStringLiteral("--run"));
|
||||
}
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
|
||||
int run(const QStringList &args)
|
||||
{
|
||||
const int idx = args.indexOf(QStringLiteral("--run"));
|
||||
const QString script_path = args.value(idx + 1);
|
||||
const QString project_path = args.value(idx + 2);
|
||||
if (script_path.isEmpty() || project_path.isEmpty()) {
|
||||
err << "Usage: qelectrotech --run <script.js> <project.qet>\n";
|
||||
return 2;
|
||||
}
|
||||
if (!QFileInfo::exists(script_path)) {
|
||||
err << "Script not found: " << script_path << "\n";
|
||||
return 2;
|
||||
}
|
||||
if (!QFileInfo::exists(project_path)) {
|
||||
err << "Project not found: " << project_path << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
QETProject project(project_path);
|
||||
if (project.state() != QETProject::Ok) {
|
||||
err << "Failed to open project: " << project_path
|
||||
<< " (state " << project.state() << ")\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return runOnProject(script_path, &project, nullptr) ? 0 : 1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A runaway script (an infinite loop, or just a very slow one) would
|
||||
// otherwise freeze the GUI forever, or hang a CI job running --run with
|
||||
// no way out. QJSEngine::setInterrupted() is documented callable from
|
||||
// another thread; the engine polls it during evaluation and returns an
|
||||
// error QJSValue, which the normal error-reporting path below already
|
||||
// handles.
|
||||
constexpr int kScriptTimeoutMs = 30000;
|
||||
}
|
||||
|
||||
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view)
|
||||
{
|
||||
QFile file(scriptPath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
err << "Cannot open script: " << scriptPath << "\n";
|
||||
return false;
|
||||
}
|
||||
const QString source = QString::fromUtf8(file.readAll());
|
||||
file.close();
|
||||
|
||||
QJSEngine engine;
|
||||
auto *api = new QetScriptApi(project, view, &engine);
|
||||
QJSValue qet_value = engine.newQObject(api);
|
||||
// newQObject() takes ownership by default (QJSEngine::JavaScriptOwnership),
|
||||
// which would delete api as soon as the engine's GC decides to -- api's
|
||||
// real owner is the engine itself via the parent-child relationship set
|
||||
// above, so keep the engine, not the GC, in charge of its lifetime.
|
||||
engine.setObjectOwnership(api, QJSEngine::CppOwnership);
|
||||
engine.globalObject().setProperty(QStringLiteral("qet"), qet_value);
|
||||
|
||||
// wait_for(), not sleep_for(): a script that finishes well inside the
|
||||
// timeout must let the watchdog thread wake immediately, not force
|
||||
// every run -- including a fast, successful one -- to block on join()
|
||||
// for the full budget. Caught by testing this against a one-line
|
||||
// script: it took the full 30 seconds to exit before this fix.
|
||||
bool finished = false;
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
std::thread watchdog([&]() {
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
cv.wait_for(lock, std::chrono::milliseconds(kScriptTimeoutMs), [&finished]{ return finished; });
|
||||
if (!finished) {
|
||||
engine.setInterrupted(true);
|
||||
}
|
||||
});
|
||||
|
||||
QJSValue result = engine.evaluate(source, scriptPath);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
finished = true;
|
||||
}
|
||||
cv.notify_one();
|
||||
watchdog.join();
|
||||
|
||||
if (result.isError()) {
|
||||
const QString message = QStringLiteral("Script error: %1:%2: %3")
|
||||
.arg(scriptPath)
|
||||
.arg(result.property(QStringLiteral("lineNumber")).toInt())
|
||||
.arg(result.toString());
|
||||
err << message << "\n";
|
||||
// Interactive "Run Script...": stderr is invisible to a user who
|
||||
// launched the GUI normally (nowhere on Windows, easy to miss
|
||||
// everywhere else). Headless --run has no GUI to show this in, and
|
||||
// no session for it to block.
|
||||
if (view) {
|
||||
QET::QetMessageBox::critical(nullptr, QObject::tr("Script"), message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#else // !QET_HAS_SCRIPTING
|
||||
|
||||
// Qt::Qml was not found at configure time (see the QET_HAS_SCRIPTING probe
|
||||
// in the top-level CMakeLists.txt). Compile to a clear, non-silent failure
|
||||
// rather than omitting these symbols -- the same "always compiled, the
|
||||
// disabled path says why" shape as the QtPdf feature guard.
|
||||
|
||||
int run(const QStringList &)
|
||||
{
|
||||
err << "This build of QElectroTech was compiled without the Qt Qml "
|
||||
"module, so JavaScript scripting (--run) is not available.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool runOnProject(const QString &, QETProject *, DiagramView *)
|
||||
{
|
||||
err << "This build of QElectroTech was compiled without the Qt Qml "
|
||||
"module, so JavaScript scripting is not available.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif // QET_HAS_SCRIPTING
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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_SCRIPTING_H
|
||||
#define QET_SCRIPTING_H
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
class QETProject;
|
||||
class DiagramView;
|
||||
|
||||
/**
|
||||
@brief JavaScript scripting entry points (bugtracker #162).
|
||||
|
||||
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 {
|
||||
|
||||
/**
|
||||
@brief True if @p args is a `--run <script.js> <project.qet>`
|
||||
invocation.
|
||||
*/
|
||||
bool isRunRequest(const QStringList &args);
|
||||
|
||||
/**
|
||||
@brief Run the script named in @p args against the project also
|
||||
named there, headless.
|
||||
Usage: qelectrotech --run <script.js> <project.qet>
|
||||
@return process exit code: 0 on success, 1 if the project failed to
|
||||
open or the script threw, 2 on a usage error.
|
||||
*/
|
||||
int run(const QStringList &args);
|
||||
|
||||
/**
|
||||
@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, DiagramView *view = nullptr);
|
||||
|
||||
}
|
||||
|
||||
#endif // QET_SCRIPTING_H
|
||||
Reference in New Issue
Block a user