mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 01:04:13 +02:00
Merge pull request #1223 from ispyisail/feature/script-live-ask-first
Add ask-first, an Assistant panel and safe commands to live mode
This commit is contained in:
@@ -87,6 +87,11 @@
|
||||
#include <QDir>
|
||||
#include <QDesktopServices>
|
||||
#include <QTimer>
|
||||
#include <QJsonArray>
|
||||
#include <QVBoxLayout>
|
||||
#include <QListWidget>
|
||||
#include <QCheckBox>
|
||||
#include <QDockWidget>
|
||||
#include <QJsonObject>
|
||||
#include <QTime>
|
||||
#include <QStatusBar>
|
||||
@@ -3939,6 +3944,55 @@ void QETDiagramEditor::setUpLiveIndicator()
|
||||
};
|
||||
connect(&LiveServer::instance(), &LiveServer::stateChanged, box, update);
|
||||
update(LiveServer::instance().state());
|
||||
|
||||
//The Assistant dock: every action, and whether to be asked first
|
||||
auto *dock = new QDockWidget(tr("Assistant"), this);
|
||||
dock->setObjectName(QStringLiteral("assistant_dock"));
|
||||
auto *content = new QWidget(dock);
|
||||
auto *dock_layout = new QVBoxLayout(content);
|
||||
auto *ask = new QCheckBox(tr("Demander avant d'exécuter un script écrit par l'assistant"), content);
|
||||
ask->setChecked(LiveServer::instance().askFirst());
|
||||
ask->setToolTip(tr("Pour cette session seulement : chaque démarrage redemande"));
|
||||
auto *log = new QListWidget(content);
|
||||
log->setWordWrap(true);
|
||||
dock_layout->addWidget(ask);
|
||||
dock_layout->addWidget(log);
|
||||
dock->setWidget(content);
|
||||
addDockWidget(Qt::RightDockWidgetArea, dock);
|
||||
dock->hide();
|
||||
connect(ask, &QCheckBox::toggled, &LiveServer::instance(), &LiveServer::setAskFirst);
|
||||
connect(&LiveServer::instance(), &LiveServer::askFirstChanged, ask, &QCheckBox::setChecked);
|
||||
connect(&LiveServer::instance(), &LiveServer::stateChanged, dock,
|
||||
[dock](LiveServer::State state) {
|
||||
if (state == LiveServer::Waiting) dock->show();
|
||||
else if (state == LiveServer::Off) dock->hide();
|
||||
});
|
||||
connect(&LiveServer::instance(), &LiveServer::handled, log,
|
||||
[log](const QJsonObject &request, const QJsonObject &answer) {
|
||||
const QString cmd = request.value(QStringLiteral("cmd")).toString();
|
||||
if (cmd == QLatin1String("status")) return;
|
||||
const bool ok = answer.value(QStringLiteral("ok")).toBool();
|
||||
QString what = answer.value(QStringLiteral("undo")).toString();
|
||||
for (const char *key : {"name", "script", "action"})
|
||||
if (what.isEmpty()) what = request.value(QLatin1String(key)).toString();
|
||||
if (what.isEmpty()) what = cmd;
|
||||
auto *item = new QListWidgetItem(QStringLiteral("%1 %2 %3")
|
||||
.arg(QTime::currentTime().toString(QStringLiteral("HH:mm:ss")),
|
||||
ok ? QStringLiteral("✓") : QStringLiteral("✗"), what));
|
||||
QStringList details;
|
||||
if (!ok) details << answer.value(QStringLiteral("error")).toString();
|
||||
const QJsonArray lines = answer.value(QStringLiteral("log")).toArray();
|
||||
for (const QJsonValue &l : lines) details << l.toString();
|
||||
const QString source = request.value(QStringLiteral("source")).toString();
|
||||
if (!source.isEmpty()) details << QString() << source;
|
||||
item->setToolTip(details.join(QLatin1Char('\n')));
|
||||
item->setData(Qt::UserRole, details.join(QLatin1Char('\n')));
|
||||
log->insertItem(0, item);
|
||||
});
|
||||
connect(log, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
|
||||
QET::QetMessageBox::information(this, tr("Assistant"),
|
||||
item->data(Qt::UserRole).toString());
|
||||
});
|
||||
//What the assistant just did, where the user is already looking
|
||||
connect(&LiveServer::instance(), &LiveServer::handled, box,
|
||||
[label](const QJsonObject &request, const QJsonObject &answer) {
|
||||
@@ -3949,6 +4003,10 @@ void QETDiagramEditor::setUpLiveIndicator()
|
||||
what = request.value(QStringLiteral("name")).toString();
|
||||
if (what.isEmpty())
|
||||
what = request.value(QStringLiteral("script")).toString();
|
||||
if (what.isEmpty())
|
||||
what = request.value(QStringLiteral("action")).toString();
|
||||
if (what.isEmpty())
|
||||
what = cmd;
|
||||
label->setText(tr("Mode direct : %1 %2 à %3")
|
||||
.arg(answer.value(QStringLiteral("ok")).toBool() ? QStringLiteral("✓")
|
||||
: QStringLiteral("✗"),
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "../utils/qetsettings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
@@ -40,6 +41,16 @@
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
#include <QUndoStack>
|
||||
#include <QAction>
|
||||
#include <QBuffer>
|
||||
#include <QPixmap>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include "../projectview.h"
|
||||
#include "../shortcutmanager.h"
|
||||
|
||||
namespace {
|
||||
QString randomHex(int bytes)
|
||||
@@ -225,10 +236,25 @@ void LiveServer::handle(const QJsonObject &request)
|
||||
if (cmd == QLatin1String("status")) {
|
||||
answer = status();
|
||||
} else if (cmd == QLatin1String("run_script")) {
|
||||
answer = runScript(request.value(QStringLiteral("name")).toString(),
|
||||
request.value(QStringLiteral("source")).toString());
|
||||
const QString name = request.value(QStringLiteral("name")).toString();
|
||||
const QString source = request.value(QStringLiteral("source")).toString();
|
||||
//A script the assistant just wrote: the user sees it first,
|
||||
//unless they said "always" this session. A stored script
|
||||
//is one the user already has, so it runs as a click would.
|
||||
if (m_ask_first && !confirm(name, source))
|
||||
answer = failure(QStringLiteral("refused by the user"));
|
||||
else
|
||||
answer = runScript(name, source);
|
||||
} else if (cmd == QLatin1String("run_stored")) {
|
||||
answer = runStored(request.value(QStringLiteral("script")).toString());
|
||||
} else if (cmd == QLatin1String("command")) {
|
||||
answer = command(request.value(QStringLiteral("action")).toString());
|
||||
} else if (cmd == QLatin1String("show_folio")) {
|
||||
answer = showFolio(request.value(QStringLiteral("folio")).toInt(-1));
|
||||
} else if (cmd == QLatin1String("undo_last")) {
|
||||
answer = undoLast();
|
||||
} else if (cmd == QLatin1String("screenshot")) {
|
||||
answer = screenshot();
|
||||
} else {
|
||||
answer = failure(QStringLiteral("unknown command: %1").arg(cmd));
|
||||
}
|
||||
@@ -342,3 +368,140 @@ QJsonObject LiveServer::runStored(const QString &id)
|
||||
}
|
||||
return failure(QStringLiteral("no stored script with id %1").arg(id));
|
||||
}
|
||||
|
||||
void LiveServer::setAskFirst(bool ask)
|
||||
{
|
||||
if (ask == m_ask_first) return;
|
||||
m_ask_first = ask;
|
||||
emit askFirstChanged(ask);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::allowedCommands
|
||||
The editor commands an assistant may trigger: ones that open no dialog
|
||||
(a dialog would wait for an answer nobody is there to give the
|
||||
assistant) and change nothing that undo cannot take back. Saving,
|
||||
printing, exporting, deleting, opening and closing are not in it: a
|
||||
script can delete, as one undo step, and the rest stay the user's.
|
||||
*/
|
||||
QStringList LiveServer::allowedCommands()
|
||||
{
|
||||
static const QStringList ids{
|
||||
QStringLiteral("diagrameditor.select_all"),
|
||||
QStringLiteral("diagrameditor.select_nothing"),
|
||||
QStringLiteral("diagrameditor.select_invert"),
|
||||
QStringLiteral("diagrameditor.select_all_conductors"),
|
||||
QStringLiteral("diagrameditor.select_all_text_fields"),
|
||||
QStringLiteral("diagrameditor.zoom_in"),
|
||||
QStringLiteral("diagrameditor.zoom_out"),
|
||||
QStringLiteral("diagrameditor.zoom_content"),
|
||||
QStringLiteral("diagrameditor.zoom_fit"),
|
||||
QStringLiteral("diagrameditor.zoom_reset"),
|
||||
QStringLiteral("diagrameditor.rotate_selection"),
|
||||
QStringLiteral("diagrameditor.rotate_texts"),
|
||||
QStringLiteral("diagrameditor.snap_selection_to_grid"),
|
||||
QStringLiteral("diagrameditor.group_selection"),
|
||||
QStringLiteral("diagrameditor.ungroup_selection"),
|
||||
QStringLiteral("diagrameditor.conductor_reset")};
|
||||
return ids;
|
||||
}
|
||||
|
||||
QJsonObject LiveServer::command(const QString &id)
|
||||
{
|
||||
if (!allowedCommands().contains(id))
|
||||
return failure(QStringLiteral("%1 is not a command an assistant may use; allowed: %2")
|
||||
.arg(id, allowedCommands().join(QStringLiteral(", "))));
|
||||
QETDiagramEditor *e = editor();
|
||||
QAction *action = e ? ShortcutManager::instance().action(id, e) : nullptr;
|
||||
if (!action) return failure(QStringLiteral("no editor window has %1").arg(id));
|
||||
if (!action->isEnabled())
|
||||
return failure(QStringLiteral("%1 is not available right now (nothing "
|
||||
"selected, or no project open)").arg(id));
|
||||
action->trigger();
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("action"), id}};
|
||||
}
|
||||
|
||||
QJsonObject LiveServer::showFolio(int folio)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
ProjectView *pv = e ? e->currentProjectView() : nullptr;
|
||||
if (!pv) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
const QList<Diagram *> diagrams = pv->project()->diagrams();
|
||||
if (folio < 0 || folio >= diagrams.count())
|
||||
return failure(QStringLiteral("no folio %1: the project has %2, counted from 0")
|
||||
.arg(folio).arg(diagrams.count()));
|
||||
pv->showDiagram(diagrams.at(folio));
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("folio"), folio}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::undoLast
|
||||
Undo the newest step, only if the assistant made it: what the user did
|
||||
by hand stays theirs to undo.
|
||||
*/
|
||||
QJsonObject LiveServer::undoLast()
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
QETProject *project = e ? e->currentProject() : nullptr;
|
||||
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
QUndoStack *stack = project->undoStack();
|
||||
const QString text = stack->text(stack->index() - 1);
|
||||
const QString prefix = tr("Assistant : %1").arg(QString());
|
||||
if (!stack->canUndo() || !text.startsWith(prefix))
|
||||
return failure(QStringLiteral("the last step is not the assistant's (\"%1\"); "
|
||||
"only the user undoes their own").arg(text));
|
||||
stack->undo();
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("undone"), text}};
|
||||
}
|
||||
|
||||
QJsonObject LiveServer::screenshot()
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
DiagramView *view = e ? e->currentDiagramView() : nullptr;
|
||||
if (!view) return failure(QStringLiteral("no folio is shown in QElectroTech"));
|
||||
const QPixmap pixmap = view->grab();
|
||||
QByteArray png;
|
||||
QBuffer buffer(&png);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
pixmap.save(&buffer, "PNG");
|
||||
return {{QStringLiteral("ok"), true},
|
||||
{QStringLiteral("width"), pixmap.width()},
|
||||
{QStringLiteral("height"), pixmap.height()},
|
||||
{QStringLiteral("png_base64"), QString::fromLatin1(png.toBase64())}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::confirm
|
||||
Show the user the script the assistant wants to run, and ask.
|
||||
@return true to run it
|
||||
*/
|
||||
bool LiveServer::confirm(const QString &name, const QString &source)
|
||||
{
|
||||
QDialog dialog(editor());
|
||||
dialog.setWindowTitle(tr("L'assistant veut exécuter un script"));
|
||||
auto *layout = new QVBoxLayout(&dialog);
|
||||
layout->addWidget(new QLabel(
|
||||
tr("« %1 » sur le projet ouvert. Une fois exécuté, Ctrl+Z l'annule.")
|
||||
.arg(name.isEmpty() ? tr("script") : name), &dialog));
|
||||
auto *text = new QPlainTextEdit(source, &dialog);
|
||||
text->setReadOnly(true);
|
||||
text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
text->setMinimumSize(520, 220);
|
||||
layout->addWidget(text);
|
||||
auto *buttons = new QDialogButtonBox(&dialog);
|
||||
QPushButton *run = buttons->addButton(tr("&Exécuter"), QDialogButtonBox::AcceptRole);
|
||||
buttons->addButton(tr("&Refuser"), QDialogButtonBox::RejectRole);
|
||||
QPushButton *always = buttons->addButton(tr("&Toujours pour cette session"),
|
||||
QDialogButtonBox::AcceptRole);
|
||||
layout->addWidget(buttons);
|
||||
QPushButton *clicked = nullptr;
|
||||
connect(buttons, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *b) {
|
||||
clicked = qobject_cast<QPushButton *>(b);
|
||||
});
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
run->setDefault(true);
|
||||
dialog.exec();
|
||||
if (clicked == always) setAskFirst(false);
|
||||
return clicked == run || clicked == always;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,15 @@ class LiveServer : public QObject
|
||||
void askAndStart(QWidget *parent);
|
||||
void stop();
|
||||
State state() const { return m_state; }
|
||||
bool askFirst() const { return m_ask_first; }
|
||||
void setAskFirst(bool ask);
|
||||
static QStringList allowedCommands();
|
||||
|
||||
signals:
|
||||
void stateChanged(LiveServer::State state);
|
||||
/// A request was handled: what was asked, and the answer sent
|
||||
void handled(const QJsonObject &request, const QJsonObject &answer);
|
||||
void askFirstChanged(bool ask);
|
||||
|
||||
private:
|
||||
LiveServer();
|
||||
@@ -78,6 +82,11 @@ class LiveServer : public QObject
|
||||
QJsonObject status();
|
||||
QJsonObject runScript(const QString &name, const QString &source);
|
||||
QJsonObject runStored(const QString &id);
|
||||
QJsonObject command(const QString &id);
|
||||
QJsonObject showFolio(int folio);
|
||||
QJsonObject undoLast();
|
||||
QJsonObject screenshot();
|
||||
bool confirm(const QString &name, const QString &source);
|
||||
QETDiagramEditor *editor() const;
|
||||
|
||||
QLocalServer *m_server = nullptr;
|
||||
@@ -87,6 +96,7 @@ class LiveServer : public QObject
|
||||
State m_state = Off;
|
||||
bool m_asked = false;
|
||||
bool m_busy = false;
|
||||
bool m_ask_first = true; ///< per session, never saved: every start asks again
|
||||
};
|
||||
|
||||
#endif // LIVESERVER_H
|
||||
|
||||
Reference in New Issue
Block a user