mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 09:14:12 +02:00
Add a macro recorder: record a task by hand, for an assistant to script
Projet > Scripts > Enregistrer une macro (also in command search and the shortcut bar) records what is done on the current project until clicked again. Under <data folder>/recordings/<id>/ it saves the whole project before and after, the folio and selection at the start, and each step from the undo history -- its name, the commands inside it, the folio, what was selected, and the folio after it. No editing command is taught to the recorder, and nothing new walks the scene: the files are QElectroTech's own serialisation. While recording, the status bar shows "● Enregistrement : N étapes" with Arrêter. At the end a box says where it went and offers "Copier la demande pour l'assistant": a ready-made request naming the recording, to paste into the assistant's chat, since QElectroTech cannot send it anything itself. qet-assistant.json lists the recordings, and live status says whether one is under way and which was last. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -930,6 +930,8 @@ list(APPEND QET_SRC_FILES
|
||||
${QET_DIR}/sources/scripting/scriptmanagerdialog.h
|
||||
${QET_DIR}/sources/scripting/liveserver.cpp
|
||||
${QET_DIR}/sources/scripting/liveserver.h
|
||||
${QET_DIR}/sources/scripting/macrorecorder.cpp
|
||||
${QET_DIR}/sources/scripting/macrorecorder.h
|
||||
${QET_DIR}/sources/scripting/assistantinfo.cpp
|
||||
${QET_DIR}/sources/scripting/assistantinfo.h
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "scripting/scriptlibrary.h"
|
||||
#include "scripting/scriptmanagerdialog.h"
|
||||
#include "scripting/liveserver.h"
|
||||
#include "scripting/macrorecorder.h"
|
||||
#include "scripting/assistantinfo.h"
|
||||
#endif
|
||||
#include <QCoreApplication>
|
||||
@@ -87,6 +88,11 @@
|
||||
#include <QDir>
|
||||
#include <QDesktopServices>
|
||||
#include <QTimer>
|
||||
#include <QGuiApplication>
|
||||
#include <QPushButton>
|
||||
#include <QMessageBox>
|
||||
#include <QClipboard>
|
||||
#include <QPainter>
|
||||
#include <QJsonArray>
|
||||
#include <QVBoxLayout>
|
||||
#include <QListWidget>
|
||||
@@ -180,6 +186,7 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
|
||||
readSettingsState(); // restoreState() must be called after show() in Qt6
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
setUpLiveIndicator();
|
||||
setUpMacroRecorder();
|
||||
//Live mode asks once per run, from the first window to open, and
|
||||
//only once that window is on screen to anchor its warning.
|
||||
QTimer::singleShot(0, this, [this]() { LiveServer::instance().askAndStart(this); });
|
||||
@@ -727,6 +734,31 @@ void QETDiagramEditor::setUpActions()
|
||||
" dans le script pour l'API disponible)"));
|
||||
connect(m_run_script, &QAction::triggered, this, &QETDiagramEditor::slot_runScript);
|
||||
|
||||
//Record what the user does, for an assistant to turn into a script
|
||||
{
|
||||
QPixmap dot(32, 32);
|
||||
dot.fill(Qt::transparent);
|
||||
QPainter painter(&dot);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(0xd0, 0x30, 0x30));
|
||||
painter.drawEllipse(QRectF(6, 6, 20, 20));
|
||||
painter.end();
|
||||
m_record_macro = new QAction(QIcon(dot), tr("Enregistrer une macro"), this);
|
||||
}
|
||||
m_record_macro->setCheckable(true);
|
||||
m_record_macro->setStatusTip(tr("Enregistre ce que vous faites sur le projet, pour qu'un "
|
||||
"assistant IA en fasse un script ; recliquez pour arrêter"));
|
||||
connect(m_record_macro, &QAction::triggered, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
if (!MacroRecorder::instance().start(this)) m_record_macro->setChecked(false);
|
||||
} else {
|
||||
MacroRecorder::instance().stop();
|
||||
}
|
||||
});
|
||||
ShortcutManager::instance().registerAction(m_record_macro, "diagrameditor.record_macro",
|
||||
tr("Éditeur de schémas"), QKeySequence());
|
||||
|
||||
//Write, try, give an icon to and delete stored scripts
|
||||
m_manage_scripts = new QAction(tr("Gérer les scripts…"), this);
|
||||
m_manage_scripts->setStatusTip(tr("Écrire un script et en faire un bouton avec une icône"));
|
||||
@@ -3902,6 +3934,7 @@ void QETDiagramEditor::rebuildScriptActions()
|
||||
}
|
||||
|
||||
m_scripts_menu->addSeparator();
|
||||
m_scripts_menu->addAction(m_record_macro);
|
||||
m_scripts_menu->addAction(m_manage_scripts);
|
||||
m_scripts_menu->addAction(m_run_script);
|
||||
m_scripts_menu->addAction(m_open_scripts_folder);
|
||||
@@ -3915,6 +3948,68 @@ void QETDiagramEditor::rebuildScriptActions()
|
||||
updateScriptActions();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::setUpMacroRecorder
|
||||
While recording: the action checked, and "● Enregistrement : N étapes"
|
||||
with Arrêter on the status bar. At the end, where it was saved and the
|
||||
request to paste into the assistant.
|
||||
*/
|
||||
void QETDiagramEditor::setUpMacroRecorder()
|
||||
{
|
||||
auto *box = new QWidget(this);
|
||||
auto *layout = new QHBoxLayout(box);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
auto *label = new QLabel(box);
|
||||
label->setStyleSheet(QStringLiteral("color: #d03030; font-weight: bold"));
|
||||
auto *stop = new QToolButton(box);
|
||||
stop->setText(tr("Arrêter"));
|
||||
stop->setToolTip(tr("Arrêter l'enregistrement de la macro"));
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(stop);
|
||||
statusBar()->addPermanentWidget(box);
|
||||
box->hide();
|
||||
connect(stop, &QToolButton::clicked, this, []() { MacroRecorder::instance().stop(); });
|
||||
|
||||
connect(&MacroRecorder::instance(), &MacroRecorder::stateChanged, box,
|
||||
[this, box, label](bool recording, int steps) {
|
||||
box->setVisible(recording);
|
||||
label->setText(tr("● Enregistrement : %n étape(s)", nullptr, steps));
|
||||
const QSignalBlocker blocker(m_record_macro);
|
||||
m_record_macro->setChecked(recording);
|
||||
updateScriptActions();
|
||||
});
|
||||
connect(&MacroRecorder::instance(), &MacroRecorder::finished, this,
|
||||
[this](const QJsonObject &recording, QETDiagramEditor *editor) {
|
||||
if (editor == this) macroRecorded(recording);
|
||||
});
|
||||
}
|
||||
|
||||
void QETDiagramEditor::macroRecorded(const QJsonObject &recording)
|
||||
{
|
||||
const int steps = recording.value(QStringLiteral("steps")).toArray().size();
|
||||
const QString dir = recording.value(QStringLiteral("folder")).toString();
|
||||
QMessageBox box(QMessageBox::Information, tr("Macro enregistrée"),
|
||||
tr("« %1 » : %n étape(s).\n\nPour en faire un script, demandez-le à "
|
||||
"votre assistant IA : le bouton ci-dessous copie la demande, il "
|
||||
"suffit de la coller dans sa fenêtre.", nullptr, steps)
|
||||
.arg(recording.value(QStringLiteral("name")).toString()),
|
||||
QMessageBox::NoButton, this);
|
||||
box.setInformativeText(QDir::toNativeSeparators(dir)
|
||||
+ (recording.value(QStringLiteral("complete")).toBool()
|
||||
? QString() : QStringLiteral("\n") + recording.value(QStringLiteral("note")).toString()));
|
||||
QPushButton *copy = box.addButton(tr("&Copier la demande pour l'assistant"), QMessageBox::AcceptRole);
|
||||
QPushButton *open = box.addButton(tr("&Ouvrir le dossier"), QMessageBox::ActionRole);
|
||||
box.addButton(tr("&Fermer"), QMessageBox::RejectRole);
|
||||
box.setDefaultButton(copy);
|
||||
box.exec();
|
||||
if (box.clickedButton() == copy) {
|
||||
QGuiApplication::clipboard()->setText(MacroRecorder::assistantRequest(recording));
|
||||
statusBar()->showMessage(tr("Demande copiée : collez-la dans la fenêtre de l'assistant"), 8000);
|
||||
} else if (box.clickedButton() == open) {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::setUpLiveIndicator
|
||||
"Assistant connecté" and a Stop button on the status bar, shown
|
||||
@@ -4027,6 +4122,7 @@ void QETDiagramEditor::updateScriptActions()
|
||||
Diagram *diagram = dv ? dv->diagram() : nullptr;
|
||||
const bool selection = diagram && !diagram->selectedItems().isEmpty();
|
||||
const bool conductor = diagram && !diagram->selectedConductors().isEmpty();
|
||||
m_record_macro->setEnabled(currentProject() || MacroRecorder::instance().isRecording());
|
||||
for (QAction *action : std::as_const(m_script_actions)) {
|
||||
const QString context = action->property("qet_script_context").toString();
|
||||
bool enabled = currentProject() != nullptr;
|
||||
|
||||
@@ -62,6 +62,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
|
||||
friend class TerminalStripEditorWindow;
|
||||
friend class LiveServer;
|
||||
friend class MacroRecorder;
|
||||
|
||||
public:
|
||||
QETDiagramEditor(
|
||||
@@ -160,6 +161,8 @@ class QETDiagramEditor : public QETMainWindow
|
||||
bool ensureScriptingEnabled(const QString &title);
|
||||
void runStoredScript(const QString &path, const QString &name);
|
||||
void setUpLiveIndicator();
|
||||
void setUpMacroRecorder();
|
||||
void macroRecorded(const QJsonObject &recording);
|
||||
#endif
|
||||
void editDiagramProperties(DiagramView *);
|
||||
void editDiagramProperties(Diagram *);
|
||||
@@ -253,6 +256,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_run_script, ///< Action to run a JavaScript macro against the current project
|
||||
*m_open_scripts_folder, ///< Action to open the folder stored scripts are read from
|
||||
*m_manage_scripts, ///< Action to open the script manager
|
||||
*m_record_macro, ///< Action to start / stop recording a macro
|
||||
#endif
|
||||
*m_export_project_db, ///Export to file the internal database of the current project
|
||||
*m_tile_window, ///< Show MDI subwindows as tile
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "qetscriptapi.h"
|
||||
#include "scriptlibrary.h"
|
||||
#include "macrorecorder.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetversion.h"
|
||||
#include "../utils/qetsettings.h"
|
||||
@@ -89,6 +90,7 @@ void write()
|
||||
{QStringLiteral("data"), QETApp::dataDir()},
|
||||
{QStringLiteral("config"), QETApp::configDir()},
|
||||
{QStringLiteral("scripts"), ScriptLibrary::folder()},
|
||||
{QStringLiteral("recordings"), MacroRecorder::folder()},
|
||||
{QStringLiteral("elements_common"), QETApp::commonElementsDir()},
|
||||
{QStringLiteral("elements_company"), QETApp::companyElementsDir()},
|
||||
{QStringLiteral("elements_custom"), QETApp::customElementsDir()},
|
||||
@@ -103,10 +105,12 @@ void write()
|
||||
#endif
|
||||
{QStringLiteral("scripting_enabled"), QetSettings::scriptingEnabled()},
|
||||
{QStringLiteral("live_mode_setting"), QetSettings::liveAssistantEnabled()},
|
||||
{QStringLiteral("live_mode_open"), !s_live.isEmpty()}}},
|
||||
{QStringLiteral("live_mode_open"), !s_live.isEmpty()},
|
||||
{QStringLiteral("macro_recording"), MacroRecorder::instance().isRecording()}}},
|
||||
{QStringLiteral("script_api"), QJsonArray::fromStringList(QetScriptApi::signatures())},
|
||||
{QStringLiteral("stored_scripts"), scripts},
|
||||
{QStringLiteral("refused_scripts"), refused},
|
||||
{QStringLiteral("recordings"), MacroRecorder::recordings()},
|
||||
{QStringLiteral("live"), s_live.isEmpty() ? QJsonValue() : QJsonValue(s_live)}};
|
||||
|
||||
const QString file_path = path();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "liveserver.h"
|
||||
|
||||
#include "assistantinfo.h"
|
||||
#include "macrorecorder.h"
|
||||
#include "qetscripting.h"
|
||||
#include "scriptlibrary.h"
|
||||
#include "../diagram.h"
|
||||
@@ -310,6 +311,11 @@ QJsonObject LiveServer::status()
|
||||
scripts.append(QJsonObject{{QStringLiteral("id"), s.header.id},
|
||||
{QStringLiteral("name"), s.header.name}});
|
||||
answer.insert(QStringLiteral("stored_scripts"), scripts);
|
||||
//So an assistant can notice a recording the user just made
|
||||
answer.insert(QStringLiteral("macro_recorder"), QJsonObject{
|
||||
{QStringLiteral("recording"), MacroRecorder::instance().isRecording()},
|
||||
{QStringLiteral("steps"), MacroRecorder::instance().stepCount()},
|
||||
{QStringLiteral("last_recording"), MacroRecorder::instance().lastId()}});
|
||||
|
||||
QETDiagramEditor *e = editor();
|
||||
QETProject *project = e ? e->currentProject() : nullptr;
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
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 "macrorecorder.h"
|
||||
|
||||
#include "assistantinfo.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <QUndoStack>
|
||||
|
||||
namespace {
|
||||
bool writeFile(const QString &path, const QByteArray &data)
|
||||
{
|
||||
QSaveFile file(path);
|
||||
return file.open(QIODevice::WriteOnly) && file.write(data) >= 0 && file.commit();
|
||||
}
|
||||
}
|
||||
|
||||
MacroRecorder &MacroRecorder::instance()
|
||||
{
|
||||
static MacroRecorder recorder;
|
||||
return recorder;
|
||||
}
|
||||
|
||||
QString MacroRecorder::folder()
|
||||
{
|
||||
return QETApp::dataDir() + QStringLiteral("/recordings");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MacroRecorder::recordings
|
||||
What qet-assistant.json lists: every saved recording, newest first.
|
||||
*/
|
||||
QJsonArray MacroRecorder::recordings()
|
||||
{
|
||||
QJsonArray list;
|
||||
QDir dir(folder());
|
||||
const QStringList ids = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot,
|
||||
QDir::Name | QDir::Reversed);
|
||||
for (const QString &id : ids) {
|
||||
QFile file(dir.filePath(id + QStringLiteral("/recording.json")));
|
||||
if (!file.open(QIODevice::ReadOnly)) continue;
|
||||
const QJsonObject r = QJsonDocument::fromJson(file.readAll()).object();
|
||||
list.append(QJsonObject{
|
||||
{QStringLiteral("id"), id},
|
||||
{QStringLiteral("name"), r.value(QStringLiteral("name"))},
|
||||
{QStringLiteral("steps"), r.value(QStringLiteral("steps")).toArray().size()},
|
||||
{QStringLiteral("started"), r.value(QStringLiteral("started"))},
|
||||
{QStringLiteral("complete"), r.value(QStringLiteral("complete"))},
|
||||
{QStringLiteral("folder"), dir.filePath(id)}});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
bool MacroRecorder::start(QETDiagramEditor *editor)
|
||||
{
|
||||
QETProject *project = editor ? editor->currentProject() : nullptr;
|
||||
if (!project || isRecording()) return false;
|
||||
|
||||
m_id = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd-HHmmss"));
|
||||
m_dir = folder() + QLatin1Char('/') + m_id;
|
||||
if (!QDir().mkpath(m_dir + QStringLiteral("/steps"))) return false;
|
||||
if (!writeFile(m_dir + QStringLiteral("/before.qet"), project->toXml().toByteArray(4)))
|
||||
return false;
|
||||
|
||||
m_project = project;
|
||||
m_editor = editor;
|
||||
m_started = QDateTime::currentDateTime().toString(Qt::ISODate);
|
||||
m_project_title = project->title();
|
||||
m_project_path = project->filePath();
|
||||
m_steps = QJsonArray();
|
||||
m_start = where();
|
||||
m_last_index = project->undoStack()->index();
|
||||
connect(project->undoStack(), &QUndoStack::indexChanged,
|
||||
this, &MacroRecorder::undoIndexChanged);
|
||||
//Closed while recording: keep what was recorded
|
||||
connect(project, &QObject::destroyed, this, [this]() { finish(true); });
|
||||
|
||||
emit stateChanged(true, 0);
|
||||
AssistantInfo::write();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroRecorder::undoIndexChanged(int index)
|
||||
{
|
||||
if (!m_project) return;
|
||||
QUndoStack *stack = m_project->undoStack();
|
||||
if (index > m_last_index) {
|
||||
for (int i = m_last_index; i < index; ++i) {
|
||||
const QUndoCommand *cmd = stack->command(i);
|
||||
if (!cmd) continue;
|
||||
QStringList parts;
|
||||
for (int c = 0; c < cmd->childCount(); ++c) parts << cmd->child(c)->text();
|
||||
addStep(QStringLiteral("do"), cmd->text(), parts);
|
||||
}
|
||||
} else if (index < m_last_index) {
|
||||
for (int i = m_last_index - 1; i >= index; --i) {
|
||||
const QUndoCommand *cmd = stack->command(i);
|
||||
addStep(QStringLiteral("undo"), cmd ? cmd->text() : QString(), {});
|
||||
}
|
||||
}
|
||||
m_last_index = index;
|
||||
}
|
||||
|
||||
void MacroRecorder::addStep(const QString &kind, const QString &text, const QStringList &parts)
|
||||
{
|
||||
const int n = int(m_steps.size()) + 1;
|
||||
QJsonObject step{
|
||||
{QStringLiteral("n"), n},
|
||||
{QStringLiteral("kind"), kind},
|
||||
{QStringLiteral("undo_text"), text},
|
||||
{QStringLiteral("parts"), QJsonArray::fromStringList(parts)}};
|
||||
|
||||
const QJsonObject here = where();
|
||||
for (auto it = here.begin(); it != here.end(); ++it) step.insert(it.key(), it.value());
|
||||
DiagramView *view = m_editor ? m_editor->currentDiagramView() : nullptr;
|
||||
Diagram *diagram = view ? view->diagram() : nullptr;
|
||||
if (diagram && diagram->project() == m_project) {
|
||||
const QString file = QStringLiteral("steps/%1.xml").arg(n, 3, 10, QLatin1Char('0'));
|
||||
if (writeFile(m_dir + QLatin1Char('/') + file, diagram->toXml(true).toByteArray(4)))
|
||||
step.insert(QStringLiteral("folio_file"), file);
|
||||
}
|
||||
m_steps.append(step);
|
||||
emit stateChanged(true, n);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MacroRecorder::where
|
||||
The folio on screen and the elements selected on it: where the person
|
||||
was working, which a script that acts "on the selection" needs to be
|
||||
checked against.
|
||||
*/
|
||||
QJsonObject MacroRecorder::where() const
|
||||
{
|
||||
DiagramView *view = m_editor ? m_editor->currentDiagramView() : nullptr;
|
||||
Diagram *diagram = view ? view->diagram() : nullptr;
|
||||
if (!diagram || diagram->project() != m_project) return {};
|
||||
QJsonArray selected;
|
||||
for (QGraphicsItem *item : diagram->selectedItems()) {
|
||||
if (item->type() != Element::Type) continue;
|
||||
auto *element = static_cast<Element *>(item);
|
||||
selected.append(QJsonObject{
|
||||
{QStringLiteral("uuid"), element->uuid().toString()},
|
||||
{QStringLiteral("label"), element->actualLabel()},
|
||||
{QStringLiteral("x"), element->pos().x()},
|
||||
{QStringLiteral("y"), element->pos().y()}});
|
||||
}
|
||||
return {{QStringLiteral("folio"), int(m_project->diagrams().indexOf(diagram))},
|
||||
{QStringLiteral("folio_title"), diagram->title()},
|
||||
{QStringLiteral("selected_elements"), selected}};
|
||||
}
|
||||
|
||||
void MacroRecorder::stop()
|
||||
{
|
||||
if (isRecording()) finish(false);
|
||||
}
|
||||
|
||||
void MacroRecorder::finish(bool project_closed)
|
||||
{
|
||||
if (m_dir.isEmpty()) return;
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (m_project) {
|
||||
disconnect(m_project->undoStack(), nullptr, this, nullptr);
|
||||
disconnect(m_project, nullptr, this, nullptr);
|
||||
}
|
||||
bool complete = false;
|
||||
if (!project_closed && m_project)
|
||||
complete = writeFile(m_dir + QStringLiteral("/after.qet"),
|
||||
m_project->toXml().toByteArray(4));
|
||||
|
||||
const QJsonObject recording{
|
||||
{QStringLiteral("format"), 1},
|
||||
{QStringLiteral("id"), m_id},
|
||||
{QStringLiteral("name"), tr("Macro du %1").arg(
|
||||
QDateTime::fromString(m_started, Qt::ISODate).toString(QStringLiteral("dd/MM/yyyy HH:mm")))},
|
||||
{QStringLiteral("project_title"), m_project_title},
|
||||
{QStringLiteral("project_path"), m_project_path},
|
||||
{QStringLiteral("started"), m_started},
|
||||
{QStringLiteral("stopped"), QDateTime::currentDateTime().toString(Qt::ISODate)},
|
||||
{QStringLiteral("complete"), complete},
|
||||
{QStringLiteral("note"), complete ? QString()
|
||||
: tr("projet fermé pendant l'enregistrement : pas de after.qet")},
|
||||
{QStringLiteral("before"), QStringLiteral("before.qet")},
|
||||
{QStringLiteral("after"), complete ? QJsonValue(QStringLiteral("after.qet")) : QJsonValue()},
|
||||
{QStringLiteral("folder"), m_dir},
|
||||
{QStringLiteral("start"), m_start},
|
||||
{QStringLiteral("steps"), m_steps}};
|
||||
writeFile(m_dir + QStringLiteral("/recording.json"),
|
||||
QJsonDocument(recording).toJson(QJsonDocument::Indented));
|
||||
|
||||
m_last_id = m_id;
|
||||
m_project = nullptr;
|
||||
m_editor = nullptr;
|
||||
m_dir.clear();
|
||||
emit stateChanged(false, int(m_steps.size()));
|
||||
AssistantInfo::write();
|
||||
emit finished(recording, editor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MacroRecorder::assistantRequest
|
||||
What "Copier la demande pour l'assistant" puts on the clipboard: the
|
||||
recording named, and the round the assistant should follow.
|
||||
*/
|
||||
QString MacroRecorder::assistantRequest(const QJsonObject &recording)
|
||||
{
|
||||
const int steps = int(recording.value(QStringLiteral("steps")).toArray().size());
|
||||
return tr("J'ai enregistré une macro dans QElectroTech : « %1 », %n étape(s) "
|
||||
"(identifiant %2, dossier %3).\n"
|
||||
"Avec le serveur MCP qet : lis-la avec qet_recording_read, écris un "
|
||||
"script qui fait la même chose de façon générale (par exemple sur les "
|
||||
"éléments sélectionnés plutôt que sur ceux-là précisément), vérifie-le "
|
||||
"avec qet_recording_check jusqu'à ce qu'il corresponde, puis propose-le "
|
||||
"comme bouton avec qet_script_install.", nullptr, steps)
|
||||
.arg(recording.value(QStringLiteral("name")).toString())
|
||||
.arg(recording.value(QStringLiteral("id")).toString(),
|
||||
recording.value(QStringLiteral("folder")).toString());
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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 MACRORECORDER_H
|
||||
#define MACRORECORDER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
class QETDiagramEditor;
|
||||
class QETProject;
|
||||
|
||||
/**
|
||||
@brief The MacroRecorder class
|
||||
Records what a person does by hand on one project, so an AI assistant
|
||||
can turn it into a script (see MACRO-RECORDER-SCOPE.md):
|
||||
|
||||
@code
|
||||
<data folder>/recordings/<id>/
|
||||
recording.json name, project, times, the steps
|
||||
before.qet the whole project when recording started
|
||||
after.qet the whole project when it stopped
|
||||
steps/003.xml the folio a step happened on, after the step
|
||||
@endcode
|
||||
|
||||
Steps come from the project's undo history -- each new entry is one
|
||||
step, with its name and the names of the commands inside it -- so no
|
||||
editing command has to be taught to the recorder. Before and after are
|
||||
exact whatever was done; the steps give the order. Nothing new walks
|
||||
the scene: the files are QElectroTech's own serialisation.
|
||||
|
||||
QElectroTech cannot send anything to an assistant; the assistant fetches
|
||||
recordings through the qet MCP server, and qet-assistant.json lists them.
|
||||
*/
|
||||
class MacroRecorder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static MacroRecorder &instance();
|
||||
static QString folder();
|
||||
static QJsonArray recordings();
|
||||
|
||||
bool start(QETDiagramEditor *editor);
|
||||
void stop();
|
||||
bool isRecording() const { return !m_project.isNull(); }
|
||||
int stepCount() const { return int(m_steps.size()); }
|
||||
QString lastId() const { return m_last_id; }
|
||||
static QString assistantRequest(const QJsonObject &recording);
|
||||
|
||||
signals:
|
||||
void stateChanged(bool recording, int steps);
|
||||
/// A recording was saved; @a recording is its recording.json
|
||||
void finished(const QJsonObject &recording, QETDiagramEditor *editor);
|
||||
|
||||
private:
|
||||
MacroRecorder() = default;
|
||||
void undoIndexChanged(int index);
|
||||
void addStep(const QString &kind, const QString &text, const QStringList &parts);
|
||||
void finish(bool project_closed);
|
||||
QJsonObject where() const;
|
||||
|
||||
QPointer<QETProject> m_project;
|
||||
QPointer<QETDiagramEditor> m_editor;
|
||||
QString m_id;
|
||||
QString m_dir;
|
||||
QString m_started;
|
||||
QString m_project_title;
|
||||
QString m_project_path;
|
||||
QJsonArray m_steps;
|
||||
QJsonObject m_start; ///< folio and selection when recording started
|
||||
QString m_last_id;
|
||||
int m_last_index = 0;
|
||||
};
|
||||
|
||||
#endif // MACRORECORDER_H
|
||||
Reference in New Issue
Block a user