// 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 #include #include #include #include #include #include #include #include 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 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 objects; QSet 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 transactions; QJsonArray selection, frozen, changedDocuments; bool changed = false; try { if (operation == "apply") { QSet 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)); }