300 lines
20 KiB
C++
300 lines
20 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#pragma once
|
|
#include "WebViewCommands.h"
|
|
#include "WebMacroCommands.h"
|
|
#include "WebGroupCommand.h"
|
|
#include "WebStructureCommands.h"
|
|
#include "WebOpenCommand.h"
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QSet>
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <sstream>
|
|
#include <App/DocumentObject.h>
|
|
#include <vector>
|
|
|
|
static QString nativeDocumentView(const QJsonObject& request, const QString& owner)
|
|
{
|
|
const auto args = request.value("args").toObject();
|
|
const auto document = args.value("document").toString();
|
|
const auto command = args.value("command").toString();
|
|
const QSet<QString> commands{"Std_Test1", "Std_Test2", "Std_Test3", "Std_Test4", "Std_Test5", "Std_Test6", "Std_TestCmdFuncs", "Std_Open", "Std_Part", "Std_VarSet", "Std_ToggleFreeze", "Std_ToggleSkipRecompute", "Std_Group", "Std_AlignToSelection", "Std_BoxElementSelection", "Std_BoxSelection", "Std_DrawStyle",
|
|
"Std_ActivateNextWindow", "Std_ActivatePrevWindow", "Std_CascadeWindows", "Std_CloseActiveWindow",
|
|
"Std_CloseAllWindows", "Std_DockViewMenu", "Std_TileWindows", "Std_ToggleToolBarLock",
|
|
"Std_ToolBarMenu", "Std_UserInterface", "Std_ViewStatusBar", "Std_Windows", "Std_WindowsMenu",
|
|
"Std_FreezeViews", "Std_HideObjects", "Std_HideSelection", "Std_MainFullscreen",
|
|
"Std_OrthographicCamera", "Std_PerspectiveCamera", "Std_SceneInspector", "Std_SelBack",
|
|
"Std_SelBoundingBox", "Std_SelForward", "Std_SelectVisibleObjects", "Std_ShowObjects",
|
|
"Std_ShowSelection", "Std_TreeSingleDocument", "Std_TreeSyncPlacement", "Std_TreeSyncSelection",
|
|
"Std_TreeSyncView", "Std_TreeViewActions", "Std_ViewBoxZoom", "Std_ViewCreate", "Std_ViewDock", "Std_ViewDockUndockFullscreen", "Std_ViewFitSelection", "Std_ViewFullscreen", "Std_ViewGroup", "Std_ViewHome", "Std_ViewIvIssueCamPos", "Std_ViewIvStereoInterleavedColumns", "Std_ViewIvStereoInterleavedRows", "Std_ViewIvStereoOff", "Std_ViewIvStereoQuadBuff", "Std_ViewIvStereoRedGreen", "Std_ViewLoadImage", "Std_ViewRestoreCamera", "Std_ViewSaveCamera", "Std_ViewScreenShot",
|
|
"Std_Delete", "Std_DependencyGraph", "Std_DuplicateSelection", "Std_Edit",
|
|
"Std_ExportDependencyGraph", "Std_About", "Std_AboutQt", "Std_AnnotationLabel", "Std_CommandLine",
|
|
"Recompute", "Std_Alignment", "Std_Copy", "Std_Cut", "Std_Export", "Std_Expressions", "Std_Import",
|
|
"Std_MergeProjects", "Std_New", "Std_Paste", "Std_Placement", "Std_Print", "Std_PrintPdf",
|
|
"Std_PrintPreview", "Std_ProjectInfo", "Std_ProjectUtil", "Std_Properties", "Std_Quit", "Std_Redo",
|
|
"Std_PythonHelp", "Std_DevHandbook", "Std_DlgCustomize", "Std_DlgParameter", "Std_DlgPreferences",
|
|
"Std_FreeCADDonation", "Std_FreeCADForum", "Std_FreeCADUserHub", "Std_FreeCADWebsite",
|
|
"Std_FreeCADWebsite", "Std_OnlineHelp", "Std_OnlineHelpWebsite", "Std_ReportBug", "Std_TextDocument",
|
|
"Std_RecentFiles", "Std_RecentMacros", "Std_ReloadStyleSheet", "Std_RestartInSafeMode",
|
|
"Std_UnitsCalculator", "Std_UserEditMode", "Std_WhatsThis", "Std_Workbench", "NaviCubeDraggableCmd",
|
|
"Std_LinkActions", "Std_LinkImport", "Std_LinkImportAll", "Std_LinkMake", "Std_LinkMakeGroup",
|
|
"Std_LinkMakeRelative", "Std_LinkReplace", "Std_LinkUnlink",
|
|
"Std_LinkSelectActions", "Std_LinkSelectAllLinks", "Std_LinkSelectLinked", "Std_LinkSelectLinkedFinal",
|
|
"Std_DlgMacroExecute", "Std_DlgMacroExecuteDirect", "Std_DlgMacroRecord", "Std_MacroAttachDebugger",
|
|
"Std_MacroStepInto", "Std_MacroStepOver", "Std_MacroStopDebug", "Std_OpenMacrosFolder", "Std_ToggleBreakpoint",
|
|
"Std_Refresh", "Std_Revert", "Std_Save", "Std_SaveAll", "Std_SaveAs", "Std_SaveCopy",
|
|
"Std_SelectAll", "Std_Transform", "Std_TransformManip", "Std_Undo"};
|
|
const auto failure = [&](const QString& code, const QString& message) {
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", false}, {"code", code}, {"error", message}}).toJson(QJsonDocument::Compact));
|
|
};
|
|
if (args.value("owner").toString() != owner || document.isEmpty() || !commands.contains(command)
|
|
|| !args.value("parameters").isObject())
|
|
return failure("invalid_view_request", "View request requires a matching native owner, document, command and parameters");
|
|
auto parameters = args.value("parameters").toObject();
|
|
if (parameters.contains("documentId") && parameters.value("documentId").toString() != document)
|
|
return failure("document_mismatch", "View parameters belong to another document");
|
|
parameters.insert("documentId", document);
|
|
if (command == "Std_Test1" || command == "Std_Test2"
|
|
|| command == "Std_Test3" || command == "Std_Test4"
|
|
|| command == "Std_Test5" || command == "Std_Test6"
|
|
|| command == "Std_TestCmdFuncs") {
|
|
const QJsonObject record{{"name", command}, {"args", parameters}};
|
|
const auto input = QJsonDocument(QJsonObject{{"records", QJsonArray{record}}})
|
|
.toJson(QJsonDocument::Compact);
|
|
std::string output;
|
|
if (!executeWebSourceTestCommand(input.constData(), output))
|
|
return failure("native_command_failed", "Native source command did not complete");
|
|
const auto batch = QJsonDocument::fromJson(QByteArray::fromStdString(output)).object();
|
|
const auto completed = batch.value("records").toArray().first().toObject();
|
|
if (!batch.value("ok").toBool())
|
|
return failure(completed.value("code").toString(), completed.value("message").toString());
|
|
auto result = completed.value("result").toObject();
|
|
result.insert("documentId", document);
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true},
|
|
{"schema", "freecad-web/document-view/v1"}, {"owner", owner},
|
|
{"document", document}, {"command", command}, {"result", result}})
|
|
.toJson(QJsonDocument::Compact));
|
|
}
|
|
if (command == "Std_LinkSelectActions" || command == "Std_LinkSelectAllLinks"
|
|
|| command == "Std_LinkSelectLinked" || command == "Std_LinkSelectLinkedFinal") {
|
|
auto& app = App::GetApplication();
|
|
if (!app.getDocument(document.toUtf8().constData()))
|
|
return failure("document_not_found", "Requested document is not open in this runtime");
|
|
QString operation = command;
|
|
if (command == "Std_LinkSelectActions") {
|
|
const auto index = parameters.value("iMsg");
|
|
if (!index.isDouble() || index.toDouble() != index.toInt(-1)
|
|
|| index.toInt(-1) < 0 || index.toInt(-1) > 2)
|
|
return failure("invalid_arguments", "Select a link navigation action");
|
|
const QString actions[]{"Std_LinkSelectLinked", "Std_LinkSelectLinkedFinal", "Std_LinkSelectAllLinks"};
|
|
operation = actions[index.toInt()];
|
|
}
|
|
const auto entries = parameters.value("selection").toArray();
|
|
if (entries.isEmpty()) return failure("empty_selection", "Select an object for link navigation");
|
|
const auto first = entries.first().toObject();
|
|
auto* sourceDoc = app.getDocument(first.value("documentId").toString(document).toUtf8().constData());
|
|
auto* root = sourceDoc ? sourceDoc->getObject(first.value("object").toString().toUtf8().constData()) : nullptr;
|
|
if (!root) return failure("invalid_selection", "Selected object is not open in this runtime");
|
|
const auto subs = first.value("subelements").toArray();
|
|
const auto path = subs.isEmpty() ? std::string() : subs.first().toString().toStdString();
|
|
auto* selected = root->getSubObject(path.c_str());
|
|
if (!selected) return failure("invalid_selection", "Selected sub-object no longer exists");
|
|
QJsonArray selection;
|
|
QString activeDocument = document;
|
|
const auto identity = [](App::DocumentObject* obj, const std::string& sub = std::string()) {
|
|
QJsonObject item{{"documentId", QString::fromUtf8(obj->getDocument()->getName())},
|
|
{"object", QString::fromUtf8(obj->getNameInDocument())}};
|
|
if (!sub.empty()) item.insert("subelements", QJsonArray{QString::fromStdString(sub)});
|
|
return item;
|
|
};
|
|
try {
|
|
if (operation == "Std_LinkSelectAllLinks") {
|
|
for (auto* link : app.getLinksTo(selected, 0))
|
|
if (link && link->isAttachedToDocument()) selection.append(identity(link));
|
|
if (selection.isEmpty()) return failure("inactive_command", "Selected object has no incoming links");
|
|
} else {
|
|
const bool finalLink = operation == "Std_LinkSelectLinkedFinal";
|
|
auto* linked = selected->getLinkedObject(finalLink);
|
|
std::string subname;
|
|
if (!linked || linked == selected) {
|
|
if (selected->getDocument() == root->getDocument())
|
|
return failure("inactive_command", "Selected object has no linked target");
|
|
linked = nullptr;
|
|
for (auto dot = path.find('.'); dot != std::string::npos; dot = path.find('.', dot + 1)) {
|
|
auto* parent = root->getSubObject(path.substr(0, dot + 1).c_str());
|
|
if (!parent) break;
|
|
parent = parent->getLinkedObject(true);
|
|
if (parent && parent->getDocument() != root->getDocument()) {
|
|
linked = finalLink ? (selected == parent ? nullptr : selected) : parent;
|
|
if (!finalLink) subname = path.substr(dot + 1);
|
|
break;
|
|
}
|
|
}
|
|
if (!linked) return failure("inactive_command", "Selected object has no further linked target");
|
|
} else if (finalLink && linked == selected->getLinkedObject(false)) {
|
|
return failure("inactive_command", "Selected link has no deeper linked target");
|
|
}
|
|
if (!linked->isAttachedToDocument())
|
|
return failure("invalid_selection", "Linked object is no longer attached to a document");
|
|
activeDocument = QString::fromUtf8(linked->getDocument()->getName());
|
|
app.setActiveDocument(activeDocument.toUtf8().constData());
|
|
selection.append(identity(linked, subname));
|
|
}
|
|
} catch (const std::exception& error) {
|
|
return failure("native_link_failed", QString::fromUtf8(error.what()));
|
|
}
|
|
const QJsonObject result{{"command", command}, {"documentId", document},
|
|
{"operation", operation}, {"activeDocument", activeDocument},
|
|
{"changed", false}, {"selection", selection}};
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true},
|
|
{"schema", "freecad-web/document-view/v1"}, {"owner", owner},
|
|
{"document", document}, {"command", command}, {"result", result}}).toJson(QJsonDocument::Compact));
|
|
}
|
|
if (command == "Std_ToggleFreeze" || command == "Std_ToggleSkipRecompute") {
|
|
auto* doc = App::GetApplication().getDocument(document.toUtf8().constData());
|
|
if (!doc) return failure("document_not_found", "Requested document is not open in this runtime");
|
|
const auto operation = parameters.value("operation").toString(command == "Std_ToggleFreeze" ? "apply" : "read");
|
|
if (operation != "read" && operation != "apply")
|
|
return failure("invalid_arguments", "Expected read or apply operation");
|
|
if (command == "Std_ToggleSkipRecompute" && operation == "apply" && !parameters.value("checked").isBool())
|
|
return failure("invalid_arguments", "Skip recomputes requires a boolean checked state");
|
|
std::vector<App::DocumentObject*> objects;
|
|
QSet<App::DocumentObject*> seen;
|
|
if (command == "Std_ToggleFreeze") {
|
|
if (!parameters.value("selection").isArray())
|
|
return failure("invalid_selection", "Select objects to freeze or unfreeze");
|
|
for (const auto& entry : parameters.value("selection").toArray()) {
|
|
const auto item = entry.toObject();
|
|
auto* selectedDoc = App::GetApplication().getDocument(item.value("documentId").toString(document).toUtf8().constData());
|
|
auto* obj = selectedDoc ? selectedDoc->getObject(item.value("object").toString().toUtf8().constData()) : nullptr;
|
|
if (!obj) return failure("invalid_selection", "Selected object is not open in this runtime");
|
|
if (!seen.contains(obj)) { seen.insert(obj); objects.push_back(obj); }
|
|
}
|
|
if (objects.empty()) return failure("empty_selection", "Select objects to freeze or unfreeze");
|
|
}
|
|
std::vector<App::Document*> transactions;
|
|
QJsonArray selection, frozen, changedDocuments;
|
|
bool changed = false;
|
|
try {
|
|
if (operation == "apply") {
|
|
QSet<App::Document*> owners;
|
|
if (command == "Std_ToggleSkipRecompute") owners.insert(doc);
|
|
for (auto* obj : objects) owners.insert(obj->getDocument());
|
|
for (auto* target : owners) {
|
|
target->openTransaction(command == "Std_ToggleFreeze" ? "Toggle freeze" : "Skip recomputes");
|
|
transactions.push_back(target);
|
|
}
|
|
if (command == "Std_ToggleSkipRecompute") {
|
|
const bool checked = parameters.value("checked").toBool();
|
|
changed = doc->testStatus(App::Document::SkipRecompute) != checked;
|
|
doc->setStatus(App::Document::SkipRecompute, checked);
|
|
} else {
|
|
for (auto* obj : objects) {
|
|
if (obj->isFreezed()) obj->unfreeze(); else obj->freeze();
|
|
}
|
|
changed = true;
|
|
}
|
|
for (auto* target : transactions) {
|
|
target->commitTransaction();
|
|
if (changed) changedDocuments.append(QString::fromUtf8(target->getName()));
|
|
}
|
|
transactions.clear();
|
|
}
|
|
for (auto* obj : objects) {
|
|
QJsonObject identity{{"documentId", QString::fromUtf8(obj->getDocument()->getName())},
|
|
{"object", QString::fromUtf8(obj->getNameInDocument())},
|
|
{"label", QString::fromUtf8(obj->Label.getValue())}};
|
|
selection.append(identity);
|
|
identity.insert("frozen", obj->isFreezed());
|
|
frozen.append(identity);
|
|
}
|
|
} catch (const std::exception& error) {
|
|
for (auto* target : transactions) target->abortTransaction();
|
|
return failure("native_feature_failed", QString::fromUtf8(error.what()));
|
|
} catch (...) {
|
|
for (auto* target : transactions) target->abortTransaction();
|
|
return failure("native_feature_failed", "Native feature state change failed");
|
|
}
|
|
const QJsonObject result{{"command", command}, {"documentId", document},
|
|
{"changed", changed}, {"selection", selection}, {"frozenObjects", frozen},
|
|
{"skipRecompute", doc->testStatus(App::Document::SkipRecompute)}, {"changedDocuments", changedDocuments}};
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true},
|
|
{"schema", "freecad-web/document-view/v1"}, {"owner", owner},
|
|
{"document", document}, {"command", command}, {"result", result}}).toJson(QJsonDocument::Compact));
|
|
}
|
|
if (command == QStringLiteral("Std_ExportDependencyGraph")) {
|
|
auto* doc = App::GetApplication().getDocument(document.toUtf8().constData());
|
|
if (!doc)
|
|
return failure("document_not_found", "Requested document is not open in the selected native runtime");
|
|
std::ostringstream graph;
|
|
doc->exportGraphviz(graph);
|
|
if (!graph)
|
|
return failure("graph_export_failed", "Native dependency graph serialization failed");
|
|
const QJsonObject result{{"documentId", document},
|
|
{"graphviz", QString::fromStdString(graph.str())}};
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true},
|
|
{"schema", "freecad-web/document-view/v1"}, {"owner", owner},
|
|
{"document", document}, {"command", command}, {"result", result}}).toJson(QJsonDocument::Compact));
|
|
}
|
|
const auto apiId = "Web:desktop:" + command;
|
|
const QJsonObject record{{"apiId", apiId}, {"name", command}, {"args", parameters}};
|
|
const auto input = QJsonDocument(QJsonObject{{"documentId", document}, {"records", QJsonArray{record}}}).toJson(QJsonDocument::Compact);
|
|
std::string output;
|
|
const auto gil = PyGILState_Ensure();
|
|
const bool macroCommand = command == QStringLiteral("Std_DlgMacroRecord")
|
|
|| command == QStringLiteral("Std_DlgMacroExecute")
|
|
|| command == QStringLiteral("Std_DlgMacroExecuteDirect")
|
|
|| command == QStringLiteral("Std_MacroAttachDebugger");
|
|
const bool handled = command == QStringLiteral("Std_Open") ? executeWebOpenCommand(input.constData(), output)
|
|
: command == QStringLiteral("Std_Part") || command == QStringLiteral("Std_VarSet")
|
|
? executeWebStructureCommand(input.constData(), output) : command == QStringLiteral("Std_Group")
|
|
? executeWebGroupCommand(input.constData(), output) : macroCommand
|
|
? executeWebMacroCommand(input.constData(), output)
|
|
: executeDocumentCommand(input.constData(), output);
|
|
if (!handled || PyErr_Occurred()) {
|
|
PyObject *type = nullptr, *value = nullptr, *trace = nullptr;
|
|
PyErr_Fetch(&type, &value, &trace); PyErr_NormalizeException(&type, &value, &trace);
|
|
PyObject* message = value ? PyObject_Str(value) : nullptr;
|
|
const char* text = message ? PyUnicode_AsUTF8(message) : nullptr;
|
|
const auto result = failure("native_view_failed", text ? QString::fromUtf8(text) : QStringLiteral("Native view command returned no result"));
|
|
Py_XDECREF(message); Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(trace); PyErr_Clear();
|
|
PyGILState_Release(gil);
|
|
return result;
|
|
}
|
|
PyGILState_Release(gil);
|
|
const auto response = QJsonDocument::fromJson(QByteArray::fromStdString(output)).object();
|
|
const auto records = response.value("records").toArray();
|
|
if (records.size() != 1 || records.first().toObject().value("apiId").toString() != apiId)
|
|
return failure("native_protocol_error", "Native view result identity mismatch");
|
|
const auto result = records.first().toObject();
|
|
if (!response.value("ok").toBool() || result.value("status").toString() != "success")
|
|
return failure(result.value("code").toString("native_view_failed"), result.value("message").toString("View operation failed"));
|
|
if (command == QStringLiteral("Std_ProjectUtil")
|
|
|| command == QStringLiteral("Std_Properties")
|
|
|| command == QStringLiteral("Std_Quit")
|
|
|| command == QStringLiteral("Std_Redo")) {
|
|
const auto value = result.value("result").toObject();
|
|
if (value.value("command").toString() != command
|
|
|| value.value("documentId").toString() != document)
|
|
return failure("native_protocol_error", "Native document command result identity mismatch");
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true},
|
|
{"schema", "freecad-web/document-view/v1"}, {"owner", owner},
|
|
{"document", document}, {"command", command}, {"result", value}})
|
|
.toJson(QJsonDocument::Compact));
|
|
}
|
|
if (command == QStringLiteral("Std_About") || command == QStringLiteral("Std_AboutQt")
|
|
|| command == QStringLiteral("Std_AnnotationLabel") || command == QStringLiteral("Std_CommandLine")
|
|
|| command == QStringLiteral("Std_RecentFiles") || command == QStringLiteral("Std_RecentMacros")
|
|
|| command == QStringLiteral("Std_ReloadStyleSheet")) {
|
|
const auto value = result.value("result").toObject();
|
|
if (result.value("name").toString() != command
|
|
|| value.value("command").toString() != command
|
|
|| value.value("documentId").toString() != document)
|
|
return failure("native_protocol_error", "Native standard command result identity mismatch");
|
|
}
|
|
return QString::fromUtf8(QJsonDocument(QJsonObject{{"ok", true}, {"schema", "freecad-web/document-view/v1"},
|
|
{"owner", owner}, {"document", document}, {"command", command}, {"result", result.value("result")}}).toJson(QJsonDocument::Compact));
|
|
}
|