Skip to content

Commit

Permalink
App: introduce DialogExecScript to let user control script execution
Browse files Browse the repository at this point in the history
  • Loading branch information
HuguesDelorme committed Jun 27, 2024
1 parent a392fbd commit 073bea6
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 62 deletions.
74 changes: 26 additions & 48 deletions src/app/commands_tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@
#include "../gui/gui_application.h"
#include "../gui/gui_document.h"
#include "../qtcommon/filepath_conv.h"
#include "../qtscripting/script_global.h"
#include "app_module.h"
#include "dialog_exec_script.h"
#include "dialog_inspect_xde.h"
#include "dialog_options.h"
#include "dialog_save_image_view.h"
#include "qtwidgets_utils.h"
#include "theme.h"

#include <QtWidgets/QWidget>
#include <QtWidgets/QMenu>

#include <QtCore/QDateTime>
#include <QtCore/QFile>
#include <QtCore/QtDebug>
#include <QtQml/QJSEngine>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMenu>

namespace Mayo {

Expand Down Expand Up @@ -106,38 +104,11 @@ void CommandEditOptions::execute()
QtWidgetsUtils::asyncDialogExec(dlg);
}

namespace {

void evaluateScript(QJSEngine* jsEngine, const FilePath& filepathScript)
{
if (!jsEngine)
return;

auto fnJsEvaluate = [](QJSEngine* jsEngine, const QString& program) {
auto jsVal = jsEngine->evaluate(program);
if (jsVal.isError()) {
qCritical() << "Error at line"
<< jsVal.property("lineNumber").toInt()
<< ":" << jsVal.toString();
}

return jsVal;
};

QFile jsFile(filepathTo<QString>(filepathScript));
if (jsFile.open(QIODevice::ReadOnly))
fnJsEvaluate(jsEngine, jsFile.readAll());
}

} // namespace

CommandExecScript::CommandExecScript(IAppContext* context, QJSEngine* jsEngine)
: Command(context),
m_jsEngine(jsEngine)
CommandExecScript::CommandExecScript(IAppContext* context)
: Command(context)
{
auto action = new QAction(this);
action->setText(Command::tr("Execute Script..."));
//action->setToolTip(Command::tr("Options"));
this->setAction(action);
}

Expand All @@ -149,28 +120,36 @@ void CommandExecScript::execute()
QString(/*dir*/),
Command::tr("Script files(*.js)")
);
if (strFilePath.isEmpty())
return;
if (!strFilePath.isEmpty())
CommandExecScript::runScript(this->context(), filepathFrom(strFilePath));
}

evaluateScript(m_jsEngine, filepathFrom(strFilePath));
AppModule::get()->prependRecentScript(filepathFrom(strFilePath));
void CommandExecScript::runScript(IAppContext* context, const FilePath& scriptFilePath)
{
auto dlg = new DialogExecScript(context->widgetMain());
dlg->setScriptEngineCreator([=](QObject* parent) {
return createScriptEngine(context->guiApp()->application(), parent);
});
dlg->setScriptFilePath(scriptFilePath);
QtWidgetsUtils::asyncDialogExec(dlg);
dlg->startScript();
AppModule::get()->prependRecentScript(scriptFilePath);
}

CommandExecRecentScript::CommandExecRecentScript(IAppContext* context, QJSEngine* jsEngine)
: Command(context),
m_jsEngine(jsEngine)
CommandExecRecentScript::CommandExecRecentScript(IAppContext* context)
: Command(context)
{
auto action = new QAction(this);
action->setText(Command::tr("Execute Recent Script"));
this->setAction(action);
}

CommandExecRecentScript::CommandExecRecentScript(
IAppContext* context, QMenu* containerMenu, QJSEngine* jsEngine
)
: CommandExecRecentScript(context, jsEngine)
CommandExecRecentScript::CommandExecRecentScript(IAppContext* context, QMenu* containerMenu)
: CommandExecRecentScript(context)
{
QObject::connect(containerMenu, &QMenu::aboutToShow, this, &CommandExecRecentScript::recreateEntries);
QObject::connect(
containerMenu, &QMenu::aboutToShow, this, &CommandExecRecentScript::recreateEntries
);
}

void CommandExecRecentScript::execute()
Expand All @@ -192,8 +171,7 @@ void CommandExecRecentScript::recreateEntries()
const QString strFileName = filepathTo<QString>(recentScript.filepath.filename());
const QString strEntryRecentScript = Command::tr("%1 | %2").arg(++idFile).arg(strFileName);
auto action = menu->addAction(strEntryRecentScript, this, [=]{
evaluateScript(m_jsEngine, recentScript.filepath);
AppModule::get()->prependRecentScript(recentScript.filepath);
CommandExecScript::runScript(this->context(), recentScript.filepath);
});
QDateTime dateTimeLastExec;
dateTimeLastExec.setTimeSpec(Qt::UTC);
Expand Down
14 changes: 5 additions & 9 deletions src/app/commands_tools.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,22 @@ class CommandEditOptions : public Command {

class CommandExecScript : public Command {
public:
CommandExecScript(IAppContext* context, QJSEngine* jsEngine);
CommandExecScript(IAppContext* context);
void execute() override;

static constexpr std::string_view Name = "exec-script";
static void runScript(IAppContext* context, const FilePath& scriptFilePath);

private:
QJSEngine* m_jsEngine = nullptr;
static constexpr std::string_view Name = "exec-script";
};

class CommandExecRecentScript : public Command {
public:
CommandExecRecentScript(IAppContext* context, QJSEngine* jsEngine);
CommandExecRecentScript(IAppContext* context, QMenu* containerMenu, QJSEngine* jsEngine);
CommandExecRecentScript(IAppContext* context);
CommandExecRecentScript(IAppContext* context, QMenu* containerMenu);
void execute() override;
void recreateEntries();

static constexpr std::string_view Name = "exec-script-recent";

private:
QJSEngine* m_jsEngine = nullptr;
};

} // namespace Mayo
190 changes: 190 additions & 0 deletions src/app/dialog_exec_script.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/****************************************************************************
** Copyright (c) 2024, Fougue Ltd. <https://www.fougue.pro>
** All rights reserved.
** See license at https://github.com/fougue/mayo/blob/master/LICENSE.txt
****************************************************************************/

#include "dialog_exec_script.h"

#include "qtwidgets_utils.h"
#include "../qtcommon/filepath_conv.h"
#include "../qtcommon/log_message_handler.h"
#include "../qtscripting/script_global.h"
#include "ui_dialog_exec_script.h"

#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileinfo>
#include <QtCore/QTimer>
#include <QtQml/QJSEngine>
#include <gsl/util>

namespace Mayo {

namespace {

QString scriptProgram(const QString& strFilepath)
{
QFile file(strFilepath);
if (file.open(QIODevice::ReadOnly))
return file.readAll();

return {};
}

} // namespace

DialogExecScript::DialogExecScript(QWidget* parent)
: QDialog(parent),
m_ui(new Ui_DialogExecScript)
{
m_ui->setupUi(this);
// TODO Don't forget to disconnect those slots in destructor(maybe not needed as m_taskMgr will be out of scope)
m_taskMgr.signalStarted.connectSlot(&DialogExecScript::onTaskStarted, this);
m_taskMgr.signalEnded.connectSlot(&DialogExecScript::onTaskEnded, this);
QObject::connect(
m_ui->btn_restartStop, &QAbstractButton::clicked, this, &DialogExecScript::restartOrStopScriptExec
);
QObject::connect(
m_ui->buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &QDialog::accept
);
}

DialogExecScript::~DialogExecScript()
{
delete m_ui;
if (m_jsEngine)
m_jsEngine->deleteLater();
}

void DialogExecScript::setScriptEngineCreator(ScriptEngineCreator fn)
{
m_fnScriptEngineCreator = std::move(fn);
}

void DialogExecScript::setScriptFilePath(const FilePath& scriptFilePath)
{
m_scriptFilePath = QDir::toNativeSeparators(filepathTo<QString>(scriptFilePath));
}

void DialogExecScript::startScript()
{
m_scriptExecTaskId = m_taskMgr.newTask([=](TaskProgress*) {
// Override the "console output" handler of LogMessageHandler, first keep the current handler
// so it can be restored before exiting
auto& logMsgHandler = LogMessageHandler::instance();
auto onEntryJsConsoleOutputHandler = logMsgHandler.jsConsoleOutputHandler();
auto _ = gsl::finally([&]{
logMsgHandler.setJsConsoleOutputHandler(onEntryJsConsoleOutputHandler);
});
auto fnAddConsoleOutput = [=](QtMsgType type, const QString& text, const QString& file, int line) {
const Message msg = { type, text, file, line };
QTimer::singleShot(0, this, [=]{ this->addConsoleOutput(msg); });
};
logMsgHandler.setJsConsoleOutputHandler(
[=](QtMsgType type, const QMessageLogContext& context, const QString& text) {
const QString strFile = context.file ? context.file : "";
fnAddConsoleOutput(type, text, strFile, context.line);
}
);

// Evaluate script program
this->recreateScriptEngine();
auto jsVal = m_jsEngine->evaluate(scriptProgram(m_scriptFilePath), m_scriptFilePath);
if (jsVal.isError()) {
const QString name = jsVal.property("name").toString();
const QString message = jsVal.property("message").toString();
fnAddConsoleOutput(
QtCriticalMsg,
tr("%1: %2").arg(name, message),
jsVal.property("fileName").toString(),
jsVal.property("lineNumber").toInt()
);
}
});
m_taskMgr.run(m_scriptExecTaskId);
}

void DialogExecScript::done(int resultCode)
{
if (m_scriptExecIsRunning) {
// TODO Ask if script execution should be stopped
}
else {
QDialog::done(resultCode);
}
}

void DialogExecScript::onTaskStarted(TaskId taskId)
{
if (m_scriptExecTaskId != taskId)
return;

m_scriptExecIsRunning = true;
m_wasScriptExecInterrupted = false;
m_ui->label_Status->setText(tr("Executing '%1'...").arg(m_scriptFilePath));
m_ui->btn_restartStop->setText(tr("Stop"));
m_ui->progressBar_Execution->setRange(0, 0);
m_ui->progressBar_Execution->setValue(-1);
m_ui->treeWidget_Output->clear();
}

void DialogExecScript::onTaskEnded(TaskId taskId)
{
if (m_scriptExecTaskId != taskId)
return;

m_scriptExecIsRunning = false;
if (!m_wasScriptExecInterrupted)
m_ui->label_Status->setText(tr("Finished '%1'").arg(m_scriptFilePath));
else
m_ui->label_Status->setText(tr("Stopped '%1'").arg(m_scriptFilePath));

m_ui->btn_restartStop->setText(tr("Restart"));
m_ui->progressBar_Execution->setRange(0, 100);
m_ui->progressBar_Execution->setValue(!m_wasScriptExecInterrupted ? 100 : 0);
m_jsEngine->setInterrupted(false);

for (int col = 0; col < m_ui->treeWidget_Output->columnCount(); ++col)
m_ui->treeWidget_Output->resizeColumnToContents(col);
}

void DialogExecScript::restartOrStopScriptExec()
{
if (m_scriptExecIsRunning) {
m_wasScriptExecInterrupted = true;
m_jsEngine->setInterrupted(true);
}
else {
this->startScript();
}
}

void DialogExecScript::recreateScriptEngine()
{
delete m_jsEngine;
m_jsEngine = m_fnScriptEngineCreator(nullptr);
}

void DialogExecScript::addConsoleOutput(const Message& msg)
{
auto fnStrMsgType = [](QtMsgType type) -> QString {
switch (type) {
case QtDebugMsg: return tr("debug");
case QtWarningMsg: return tr("warning");
case QtCriticalMsg: return tr("critical");
case QtFatalMsg: return tr("fatal");
case QtInfoMsg: return tr("info");
default: return tr("?");
}
};

auto item = new QTreeWidgetItem;
item->setText(0, fnStrMsgType(msg.type));
item->setText(1, msg.text);
item->setText(2, QFileInfo(msg.contextFile).fileName());
item->setText(3, QString::number(msg.contextLine));
m_ui->treeWidget_Output->addTopLevelItem(item);
}

} // namespace Mayo
Loading

0 comments on commit 073bea6

Please sign in to comment.