Live mode: open a saved project, switch between open projects

open_project opens a .qet and makes it current, as File > Open does but
with no file dialog and no error windows: a missing, unreadable or
foreign file is refused in the answer. A project already open is only
made current. switch_project makes another open project current, by its
index or its file. status now lists every open project (index, title,
file, folios, unsaved changes, current), so the assistant sees what it
can switch to. MCP: qet_live_open_project, qet_live_switch_project; both
paths go through the workspace policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-05 19:00:13 +13:00
parent 42b1e43e4a
commit ad20eb94ba
4 changed files with 174 additions and 2 deletions
+94
View File
@@ -256,6 +256,10 @@ void LiveServer::handle(const QJsonObject &request)
answer = command(request.value(QStringLiteral("action")).toString());
} else if (cmd == QLatin1String("new_project")) {
answer = newProject(request);
} else if (cmd == QLatin1String("open_project")) {
answer = openProject(request.value(QStringLiteral("path")).toString());
} else if (cmd == QLatin1String("switch_project")) {
answer = switchProject(request);
} else if (cmd == QLatin1String("show_folio")) {
answer = showFolio(request.value(QStringLiteral("folio")).toInt(-1));
} else if (cmd == QLatin1String("undo_last")) {
@@ -318,6 +322,7 @@ QJsonObject LiveServer::status()
{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("projects"), openProjects());
answer.insert(QStringLiteral("macro_recorder"), QJsonObject{
{QStringLiteral("recording"), MacroRecorder::instance().isRecording()},
{QStringLiteral("steps"), MacroRecorder::instance().stepCount()},
@@ -502,6 +507,95 @@ QJsonObject LiveServer::newProject(const QJsonObject &request)
return answer;
}
/**
@brief LiveServer::openProjects
The projects open in the editor window, in tab order: the index
switch_project takes, title, file, folios, unsaved changes, and which
one is current.
*/
QJsonArray LiveServer::openProjects() const
{
QJsonArray list;
QETDiagramEditor *e = editor();
if (!e) return list;
const QETProject *current = e->currentProject();
const QList<ProjectView *> views = e->openedProjects();
for (int i = 0; i < views.count(); ++i) {
QETProject *p = views.at(i)->project();
list.append(QJsonObject{
{QStringLiteral("index"), i},
{QStringLiteral("title"), p->title()},
{QStringLiteral("path"), p->filePath()},
{QStringLiteral("folios"), int(p->diagrams().count())},
{QStringLiteral("modified"), p->projectWasModified()},
{QStringLiteral("read_only"), p->isReadOnly()},
{QStringLiteral("current"), p == current}});
}
return list;
}
/**
@brief LiveServer::openProject
Open a saved project and make it current, as File > Open does but with
no file dialog and no error windows: what is wrong comes back in the
answer. A project already open is only made current.
*/
QJsonObject LiveServer::openProject(const QString &raw_path)
{
QETDiagramEditor *e = editor();
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
const QString path = raw_path.trimmed();
if (path.isEmpty()) return failure(QStringLiteral("no path given"));
const QFileInfo info(path);
if (info.isRelative()) return failure(QStringLiteral("path must be absolute: %1").arg(path));
if (!info.isFile()) return failure(QStringLiteral("%1 does not exist").arg(path));
if (!info.isReadable()) return failure(QStringLiteral("%1 cannot be read (permissions)").arg(path));
QJsonObject extra{{QStringLiteral("opened"), true}};
if (ProjectView *pv = e->viewForFile(info.absoluteFilePath())) {
e->activateProject(pv);
extra = {{QStringLiteral("opened"), false}, {QStringLiteral("already_open"), true}};
} else if (QETApp::diagramEditorForFile(info.absoluteFilePath())) {
return failure(QStringLiteral("%1 is open in another QElectroTech window; "
"switch to it there").arg(path));
} else {
if (!e->openAndAddProject(info.absoluteFilePath(), false))
return failure(QStringLiteral("QElectroTech could not open %1 (not a "
"QElectroTech project, or a newer format)").arg(path));
if (ProjectView *pv = e->viewForFile(info.absoluteFilePath())) e->activateProject(pv);
}
QJsonObject answer = status();
for (auto it = extra.begin(); it != extra.end(); ++it) answer.insert(it.key(), it.value());
return answer;
}
/**
@brief LiveServer::switchProject
Make another open project current, by its index in "projects" (status)
or by its file path.
*/
QJsonObject LiveServer::switchProject(const QJsonObject &request)
{
QETDiagramEditor *e = editor();
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
const QList<ProjectView *> views = e->openedProjects();
ProjectView *target = nullptr;
if (request.contains(QStringLiteral("index"))) {
const int index = request.value(QStringLiteral("index")).toInt(-1);
if (index < 0 || index >= views.count())
return failure(QStringLiteral("no open project %1: %2 are open, counted from 0")
.arg(index).arg(views.count()));
target = views.at(index);
} else {
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
if (path.isEmpty()) return failure(QStringLiteral("give \"index\" (from \"projects\") or \"path\""));
target = e->viewForFile(QFileInfo(path).absoluteFilePath());
if (!target) return failure(QStringLiteral("%1 is not open; open_project opens it").arg(path));
}
e->activateProject(target);
return status();
}
/**
@brief LiveServer::undoLast
Undo the newest step, only if the assistant made it: what the user did