mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-08 04:44:13 +02:00
Merge pull request #1312 from ispyisail/feature/live-new-project
Add live-mode commands to create, open and switch projects
This commit is contained in:
+10
-1
@@ -324,7 +324,7 @@ general (on the selected elements, say, not on these exact ones),
|
||||
## Live mode: working in the QElectroTech you have open
|
||||
|
||||
Every tool above works on files, with no QElectroTech window involved. The
|
||||
three `qet_live_*` tools instead act on the project open in **your**
|
||||
`qet_live_*` tools instead act on the project open in **your**
|
||||
QElectroTech, in front of you, so you can watch, stop or undo:
|
||||
|
||||
| | |
|
||||
@@ -336,6 +336,15 @@ QElectroTech, in front of you, so you can watch, stop or undo:
|
||||
| `qet_live_show_folio` | show another folio |
|
||||
| `qet_live_undo_last` | undo the newest step, only if the assistant made it |
|
||||
| `qet_live_screenshot` | a picture of the folio on screen, as an MCP image, cropped to the folio |
|
||||
| `qet_live_new_project` | a new project, as File > New makes it, made current; optional title, number of folios and a file to save it to (never over an existing one) |
|
||||
| `qet_live_open_project` | open a saved project and make it current, with no dialog; one already open is only made current |
|
||||
| `qet_live_switch_project` | make another open project current, by its index in `qet_live_status`'s `projects` or its file |
|
||||
|
||||
Every `qet_live_*` call works on the current project. Pages and links
|
||||
between pages need no tool of their own: in `qet_live_run_script`,
|
||||
`qet.addFolio()` and `qet.setFolioTitle()` add and name folios, and
|
||||
`qet.linkElements()` links a folio report arrow, or a coil and its
|
||||
contacts, across folios.
|
||||
|
||||
A script the assistant writes on the spot is shown to you first, with
|
||||
*Run*, *Decline* or *Always this session*; the Assistant
|
||||
|
||||
+105
-1
@@ -4085,6 +4085,45 @@ def tool_live_show_folio(folio: int) -> dict:
|
||||
return _live_call({"cmd": "show_folio", "folio": folio})
|
||||
|
||||
|
||||
def tool_live_new_project(title: str = "", folios: int = 1, path: str = "") -> dict:
|
||||
_require_script_consent()
|
||||
if not isinstance(folios, int) or isinstance(folios, bool) or not 1 <= folios <= 100:
|
||||
raise ValueError("'folios' must be a whole number from 1 to 100")
|
||||
if not isinstance(title, str) or not isinstance(path, str):
|
||||
raise ValueError("'title' and 'path' must be text")
|
||||
if path and not Path(path).expanduser().is_absolute():
|
||||
raise ValueError("'path' must be absolute, e.g. /home/me/projects/pump.qet")
|
||||
request = {"cmd": "new_project", "folios": folios}
|
||||
if title:
|
||||
request["title"] = title
|
||||
if path:
|
||||
request["path"] = str(Path(path).expanduser())
|
||||
return _live_call(request)
|
||||
|
||||
|
||||
def tool_live_open_project(path: str) -> dict:
|
||||
if not isinstance(path, str) or not path.strip():
|
||||
raise ValueError("'path' must be the project's file, e.g. /home/me/projects/pump.qet")
|
||||
p = Path(path).expanduser()
|
||||
if not p.is_absolute():
|
||||
raise ValueError("'path' must be absolute, e.g. /home/me/projects/pump.qet")
|
||||
return _live_call({"cmd": "open_project", "path": str(p)})
|
||||
|
||||
|
||||
def tool_live_switch_project(index: int | None = None, path: str = "") -> dict:
|
||||
if (index is None) == (not path):
|
||||
raise ValueError("give exactly one of 'index' (from qet_live_status's "
|
||||
"\"projects\") or 'path'")
|
||||
if index is not None:
|
||||
if not isinstance(index, int) or isinstance(index, bool) or index < 0:
|
||||
raise ValueError("'index' must be a whole number counted from 0")
|
||||
return _live_call({"cmd": "switch_project", "index": index})
|
||||
p = Path(path).expanduser()
|
||||
if not p.is_absolute():
|
||||
raise ValueError("'path' must be absolute")
|
||||
return _live_call({"cmd": "switch_project", "path": str(p)})
|
||||
|
||||
|
||||
def tool_live_undo_last() -> dict:
|
||||
_require_script_consent()
|
||||
return _live_call({"cmd": "undo_last"})
|
||||
@@ -5091,7 +5130,9 @@ TOOLS = [
|
||||
"name": "qet_live_status",
|
||||
"description": "LIVE MODE. Ask the QElectroTech the user has open what is on "
|
||||
"screen: the project, the folio shown (index and title), the "
|
||||
"selected elements, the last undo step and the stored scripts. "
|
||||
"selected elements, the last undo step, the stored scripts, and "
|
||||
"\"projects\": every project open in the window (index, title, "
|
||||
"file, folios, unsaved changes, which is current). "
|
||||
"Works only if the user switched live mode on in QElectroTech "
|
||||
"and accepted its warning at this start; the error says which "
|
||||
"step is missing. Changes nothing.",
|
||||
@@ -5164,6 +5205,58 @@ TOOLS = [
|
||||
},
|
||||
"handler": lambda a: tool_live_show_folio(a["folio"]),
|
||||
},
|
||||
{
|
||||
"name": "qet_live_new_project",
|
||||
"description": "LIVE MODE. Create a new project in the QElectroTech the "
|
||||
"user has open, as File > New does (this installation's "
|
||||
"new-folio defaults), and make it the current project: every "
|
||||
"following qet_live_* call works on it. Optional title, number "
|
||||
"of empty folios (pages, 1-100) and an absolute path to save it "
|
||||
"to at once -- never over an existing file. Then, in "
|
||||
"qet_live_run_script: qet.addFolio() / qet.insertFolio(i) add "
|
||||
"pages, qet.setFolioTitle(i, text) names them, and "
|
||||
"qet.linkElements(folioA, a, folioB, b) links across pages -- "
|
||||
"a going folio report arrow to a coming one, or a coil to its "
|
||||
"contacts on another page.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "the project's title"},
|
||||
"folios": {"type": "integer", "default": 1, "minimum": 1, "maximum": 100},
|
||||
"path": {"type": "string",
|
||||
"description": "absolute .qet path to save to now; must not exist"},
|
||||
},
|
||||
},
|
||||
"handler": lambda a: tool_live_new_project(a.get("title", ""), a.get("folios", 1),
|
||||
a.get("path", "")),
|
||||
},
|
||||
{
|
||||
"name": "qet_live_open_project",
|
||||
"description": "LIVE MODE. Open a saved project (.qet, absolute path) in the "
|
||||
"QElectroTech the user has open and make it current, as File > "
|
||||
"Open does but with no dialog; errors come back as text. A "
|
||||
"project already open is only made current. Every following "
|
||||
"qet_live_* call works on it.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string", "description": "absolute .qet path"}},
|
||||
"required": ["path"],
|
||||
},
|
||||
"handler": lambda a: tool_live_open_project(a["path"]),
|
||||
},
|
||||
{
|
||||
"name": "qet_live_switch_project",
|
||||
"description": "LIVE MODE. Make another project that is already open the "
|
||||
"current one, by its index in qet_live_status's \"projects\" or "
|
||||
"by its file path. Every following qet_live_* call works on it. "
|
||||
"Changes no project.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"index": {"type": "integer", "minimum": 0},
|
||||
"path": {"type": "string"}},
|
||||
},
|
||||
"handler": lambda a: tool_live_switch_project(a.get("index"), a.get("path", "")),
|
||||
},
|
||||
{
|
||||
"name": "qet_live_undo_last",
|
||||
"description": "LIVE MODE. Undo the newest step in the open project, only if "
|
||||
@@ -5298,6 +5391,11 @@ _DATA_PATHS = {
|
||||
"qet_check": {"read": ("project",)},
|
||||
"qet_layout_check": {"read": ("project",)},
|
||||
"qet_project_new": {"write": ("output",)},
|
||||
# QElectroTech writes this file, but where is the client's choice:
|
||||
# held to the same workspace as every other file a tool writes.
|
||||
"qet_live_new_project": {"write": ("path",)},
|
||||
"qet_live_open_project": {"read": ("path",)},
|
||||
"qet_live_switch_project": {"read": ("path",)},
|
||||
"qet_element_build": {"write": ("output",)},
|
||||
# The scripts folder is chosen by scripts_dir(), never by the client,
|
||||
# so only the project a script is tried on is a data path here.
|
||||
@@ -5318,6 +5416,9 @@ _LAUNCHES_QET_WITH = {"qet_script_install": "test_project"}
|
||||
# Tools whose "overwrite" guards a file the server names itself (the
|
||||
# stored script, in scripts_dir()), not a client-chosen output path.
|
||||
_OVERWRITE_OWN_FILE = {"qet_script_install"}
|
||||
# Tools that create a file and never replace one, whatever the client asks:
|
||||
# no "overwrite" in their schema, and the flag is ignored if sent anyway.
|
||||
_NEVER_OVERWRITE = {"qet_live_new_project"}
|
||||
|
||||
# qet_edit operations that name a file of their own.
|
||||
_DATA_PATH_OPS = {"add_image": "file", "add_pdf_page": "file"}
|
||||
@@ -5517,6 +5618,9 @@ def enforce_path_policy(tool_name: str, arguments: dict) -> None:
|
||||
# Writing over something that is already there is the one step this
|
||||
# server cannot undo, so it is the one step it will not take on its
|
||||
# own. qet_project_new already had this flag; the others now match it.
|
||||
if out.exists() and tool_name in _NEVER_OVERWRITE:
|
||||
raise ValueError(f"{arg!r} already exists: {out}. This tool only "
|
||||
"creates new files; choose another name.")
|
||||
if out.exists() and not arguments.get("overwrite"):
|
||||
raise ValueError(
|
||||
f"{arg!r} already exists: {out}. Pass \"overwrite\": true to "
|
||||
|
||||
@@ -184,7 +184,8 @@ class ToolRegistry(unittest.TestCase):
|
||||
"qet_live_run_stored", "qet_live_command", "qet_live_show_folio",
|
||||
"qet_live_undo_last", "qet_live_screenshot", "qet_about",
|
||||
"qet_recording_list", "qet_recording_read", "qet_recording_check",
|
||||
"qet_recording_remove", "qet_layout_check"})
|
||||
"qet_recording_remove", "qet_layout_check", "qet_live_new_project",
|
||||
"qet_live_open_project", "qet_live_switch_project"})
|
||||
|
||||
|
||||
class EditValidation(unittest.TestCase):
|
||||
@@ -2829,7 +2830,20 @@ class PathPolicy(unittest.TestCase):
|
||||
guarded = {name for name, spec in m._DATA_PATHS.items() if spec.get("write")}
|
||||
advertised = {t["name"] for t in m.TOOLS
|
||||
if "overwrite" in t["inputSchema"].get("properties", {})}
|
||||
self.assertEqual(guarded, advertised - m._OVERWRITE_OWN_FILE)
|
||||
self.assertEqual(guarded - m._NEVER_OVERWRITE, advertised - m._OVERWRITE_OWN_FILE)
|
||||
self.assertFalse(advertised & m._NEVER_OVERWRITE)
|
||||
|
||||
def test_never_overwrite_tools_ignore_the_flag(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
target = Path(d) / "exists.qet"
|
||||
target.write_text("x")
|
||||
with mock.patch.dict(os.environ, {"QET_MCP_WORKSPACE": d}):
|
||||
for args in ({"path": str(target)}, {"path": str(target), "overwrite": True}):
|
||||
with self.assertRaisesRegex(ValueError, "only creates new files"):
|
||||
m.enforce_path_policy("qet_live_new_project", args)
|
||||
m.enforce_path_policy("qet_live_new_project", {"path": str(Path(d) / "new.qet")})
|
||||
with self.assertRaises(ValueError):
|
||||
m.enforce_path_policy("qet_live_new_project", {"path": "/etc/new.qet"})
|
||||
|
||||
def test_every_data_path_argument_is_guarded(self):
|
||||
"""The other direction: a tool whose schema takes a data path must be
|
||||
@@ -3533,6 +3547,48 @@ class LiveClient(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
m.tool_live_show_folio("2")
|
||||
|
||||
def test_new_project_sends_only_what_was_given(self):
|
||||
self.session()
|
||||
m.tool_live_new_project()
|
||||
m.tool_live_new_project("Pump station", 3, "/tmp/x/pump.qet")
|
||||
self.assertEqual([{k: v for k, v in r.items() if k not in ("token", "id")}
|
||||
for r in self.seen],
|
||||
[{"cmd": "new_project", "folios": 1},
|
||||
{"cmd": "new_project", "folios": 3, "title": "Pump station",
|
||||
"path": "/tmp/x/pump.qet"}])
|
||||
for bad in (0, 101, "2", True):
|
||||
with self.assertRaises(ValueError):
|
||||
m.tool_live_new_project(folios=bad)
|
||||
with self.assertRaisesRegex(ValueError, "absolute"):
|
||||
m.tool_live_new_project(path="pump.qet")
|
||||
self.assertEqual(len(self.seen), 2)
|
||||
|
||||
def test_open_and_switch_send_only_what_was_given(self):
|
||||
self.session()
|
||||
m.tool_live_open_project("/tmp/x/pump.qet")
|
||||
m.tool_live_switch_project(1)
|
||||
m.tool_live_switch_project(path="/tmp/x/pump.qet")
|
||||
self.assertEqual([{k: v for k, v in r.items() if k not in ("token", "id")}
|
||||
for r in self.seen],
|
||||
[{"cmd": "open_project", "path": "/tmp/x/pump.qet"},
|
||||
{"cmd": "switch_project", "index": 1},
|
||||
{"cmd": "switch_project", "path": "/tmp/x/pump.qet"}])
|
||||
for bad in ({}, {"index": 0, "path": "/a.qet"}, {"index": -1},
|
||||
{"index": True}, {"path": "rel.qet"}):
|
||||
with self.assertRaises(ValueError):
|
||||
m.tool_live_switch_project(**bad)
|
||||
for bad in ("", "rel.qet"):
|
||||
with self.assertRaises(ValueError):
|
||||
m.tool_live_open_project(bad)
|
||||
self.assertEqual(len(self.seen), 3)
|
||||
|
||||
def test_new_project_needs_script_consent(self):
|
||||
self.session()
|
||||
with mock.patch.dict(os.environ, {"QET_ENABLE_SCRIPTING": ""}):
|
||||
with self.assertRaises(ValueError):
|
||||
m.tool_live_new_project()
|
||||
self.assertEqual(self.seen, [])
|
||||
|
||||
def test_stale_session_file(self):
|
||||
self.info({"socket": self.sock_path + "-gone", "token": "T0K"})
|
||||
with self.assertRaisesRegex(ValueError, "could not reach"):
|
||||
|
||||
@@ -1856,8 +1856,10 @@ bool QETDiagramEditor::openAndAddProject(
|
||||
box.exec();
|
||||
}
|
||||
|
||||
//Not when nobody is there to answer: an assistant opening a
|
||||
//project in live mode would wait on a window it cannot see
|
||||
BackupDialog backup_dialog(this);
|
||||
if (backup_dialog.exec() == QDialog::Accepted)
|
||||
if (interactive && backup_dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
QString backup_path = filepath_info.absolutePath() + QDir::separator() +
|
||||
QDateTime::currentDateTime().toString("yyyy-MM-dd-hh-mm") + "_" +
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMdiSubWindow>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include "../projectview.h"
|
||||
@@ -251,6 +254,12 @@ void LiveServer::handle(const QJsonObject &request)
|
||||
answer = runStored(request.value(QStringLiteral("script")).toString());
|
||||
} else if (cmd == QLatin1String("command")) {
|
||||
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")) {
|
||||
@@ -313,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()},
|
||||
@@ -441,6 +451,151 @@ QJsonObject LiveServer::showFolio(int folio)
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("folio"), folio}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::newProject
|
||||
A new project, as File > New makes it: the new-folio defaults of this
|
||||
QElectroTech, opened and made the current project, so the next script
|
||||
works on it. Optional: "title", "folios" (how many empty folios, 1 to
|
||||
100, default 1), and "path" to save it to at once -- never over an
|
||||
existing file, so an assistant cannot replace the user's work.
|
||||
*/
|
||||
QJsonObject LiveServer::newProject(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
|
||||
|
||||
const int folios = request.value(QStringLiteral("folios")).toInt(1);
|
||||
if (folios < 1 || folios > 100)
|
||||
return failure(QStringLiteral("folios must be between 1 and 100, not %1").arg(folios));
|
||||
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
|
||||
if (!path.isEmpty()) {
|
||||
const QFileInfo info(path);
|
||||
if (info.isRelative())
|
||||
return failure(QStringLiteral("path must be absolute: %1").arg(path));
|
||||
if (info.exists())
|
||||
return failure(QStringLiteral("%1 already exists; a new project is never "
|
||||
"saved over a file").arg(path));
|
||||
if (!info.dir().exists())
|
||||
return failure(QStringLiteral("the folder %1 does not exist").arg(info.absolutePath()));
|
||||
}
|
||||
|
||||
auto project = new QETProject(e);
|
||||
for (int i = 0; i < folios; ++i) project->addNewDiagram();
|
||||
const QString title = request.value(QStringLiteral("title")).toString().trimmed();
|
||||
if (!title.isEmpty()) project->setTitle(title);
|
||||
if (!e->addProject(project)) return failure(QStringLiteral("QElectroTech refused the new project"));
|
||||
|
||||
//Current at once, whether or not QElectroTech is the active
|
||||
//application: the next request runs on it (see editor())
|
||||
for (ProjectView *pv : e->openedProjects()) {
|
||||
if (pv->project() == project) {
|
||||
for (QMdiSubWindow *w : e->m_workspace.subWindowList())
|
||||
if (w->widget() == pv) e->m_workspace.setActiveSubWindow(w);
|
||||
}
|
||||
}
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
project->setFilePath(path);
|
||||
const QETResult result = project->write();
|
||||
if (!result.isOk())
|
||||
return failure(QStringLiteral("the project was created but not saved to %1: %2")
|
||||
.arg(path, result.errorMessage()));
|
||||
}
|
||||
|
||||
QJsonObject answer = status();
|
||||
answer.insert(QStringLiteral("created"), true);
|
||||
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
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#ifndef LIVESERVER_H
|
||||
#define LIVESERVER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
@@ -84,6 +85,10 @@ class LiveServer : public QObject
|
||||
QJsonObject runStored(const QString &id);
|
||||
QJsonObject command(const QString &id);
|
||||
QJsonObject showFolio(int folio);
|
||||
QJsonObject newProject(const QJsonObject &request);
|
||||
QJsonObject openProject(const QString &path);
|
||||
QJsonObject switchProject(const QJsonObject &request);
|
||||
QJsonArray openProjects() const;
|
||||
QJsonObject undoLast();
|
||||
QJsonObject screenshot();
|
||||
bool confirm(const QString &name, const QString &source);
|
||||
|
||||
Reference in New Issue
Block a user