mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-20 15:24:14 +02:00
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>
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
|
||||
|
||||
@@ -849,6 +849,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"
|
||||
@@ -549,6 +552,15 @@ void QETDiagramEditor::setUpActions()
|
||||
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this);
|
||||
connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering);
|
||||
|
||||
#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]() {
|
||||
@@ -1002,6 +1014,9 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_project -> addAction(m_project_export_wiring_list);
|
||||
menu_project -> addAction(m_project_wiring_list_view);
|
||||
menu_project -> addAction(m_terminal_numbering);
|
||||
#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);
|
||||
@@ -1856,6 +1871,9 @@ void QETDiagramEditor::slot_updateActions()
|
||||
m_project_export_wiring_list -> setEnabled(opened_project);
|
||||
m_project_wiring_list_view -> setEnabled(opened_project);
|
||||
m_terminal_numbering -> setEnabled(editable_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
|
||||
@@ -2970,3 +2988,29 @@ void QETDiagramEditor::slot_terminalNumbering() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -136,6 +136,9 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void editProjectProperties(ProjectView *);
|
||||
void editProjectProperties(QETProject *);
|
||||
void slot_terminalNumbering();
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
void slot_runScript();
|
||||
#endif
|
||||
void editDiagramProperties(DiagramView *);
|
||||
void editDiagramProperties(Diagram *);
|
||||
void addDiagramToProject(QETProject *);
|
||||
@@ -211,6 +214,9 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_project_export_wiring_list, ///< Action to export the wiring list
|
||||
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
|
||||
*m_terminal_numbering, ///< Action to launch terminal numbering
|
||||
#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,170 @@
|
||||
/*
|
||||
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 "../cli_export.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QTextStream>
|
||||
|
||||
QetScriptApi::QetScriptApi(QETProject *project, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_project(project)
|
||||
{
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
bool QetScriptApi::save(const QString &output)
|
||||
{
|
||||
return runFlag(QStringLiteral("--resave"), {output});
|
||||
}
|
||||
|
||||
void QetScriptApi::log(const QString &message)
|
||||
{
|
||||
QTextStream(stderr) << message << "\n";
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
/**
|
||||
@brief The QetScriptApi class
|
||||
The object a script sees as `qet` (bugtracker #162): a small,
|
||||
deliberately read-mostly surface over an open project, for scripted
|
||||
batch/CI work and human-written macros.
|
||||
|
||||
@b Scope, on purpose: this reads the model (folio count, titles, element
|
||||
and conductor counts -- the same data the `--info` CLI export already
|
||||
reports) and can trigger the same export/save operations the `--export-*`
|
||||
CLI flags already do. It does not create or edit diagram geometry, does
|
||||
not touch the undo stack, and does not drive the GUI. Those are all
|
||||
explicitly out of scope for this first version; see the discussion on
|
||||
bugtracker #162.
|
||||
|
||||
Export/save methods are thin wrappers around CLIExport::run() -- the
|
||||
exact same, already-tested code path the `--export-*` flags use -- built
|
||||
from the project's own file path, not the live in-memory 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 for each call: fine for
|
||||
the batch/CI use case this targets, and for the headless `--run` entry
|
||||
point it's exactly what a second `--export-*` invocation would have
|
||||
done anyway. A macro acting on unsaved GUI edits should call save()
|
||||
first.
|
||||
*/
|
||||
class QetScriptApi : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QetScriptApi(QETProject *project, 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);
|
||||
|
||||
// -- 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);
|
||||
|
||||
QETProject *m_project;
|
||||
};
|
||||
|
||||
#endif // QET_SCRIPT_API_H
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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 "../qetproject.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTextStream>
|
||||
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
#include <QJSEngine>
|
||||
#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) ? 0 : 1;
|
||||
}
|
||||
|
||||
bool runOnProject(const QString &scriptPath, QETProject *project)
|
||||
{
|
||||
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, &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);
|
||||
|
||||
QJSValue result = engine.evaluate(source, scriptPath);
|
||||
if (result.isError()) {
|
||||
err << "Script error: " << scriptPath << ":"
|
||||
<< result.property(QStringLiteral("lineNumber")).toInt() << ": "
|
||||
<< result.toString() << "\n";
|
||||
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 *)
|
||||
{
|
||||
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,61 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
/**
|
||||
@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.
|
||||
*/
|
||||
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.
|
||||
@return true if the script ran without throwing.
|
||||
*/
|
||||
bool runOnProject(const QString &scriptPath, QETProject *project);
|
||||
|
||||
}
|
||||
|
||||
#endif // QET_SCRIPTING_H
|
||||
Reference in New Issue
Block a user