diff --git a/src/Gui/ApplicationPy.cpp b/src/Gui/ApplicationPy.cpp index 5686d8f52a..7fd1cc367d 100644 --- a/src/Gui/ApplicationPy.cpp +++ b/src/Gui/ApplicationPy.cpp @@ -67,6 +67,7 @@ #include "WorkbenchManager.h" #include "WorkbenchManipulatorPython.h" #include "FileOriginPython.h" +#include "EditingContext.h" #include "OriginManager.h" #include "Inventor/MarkerBitmaps.h" #include "Language/Translator.h" @@ -596,6 +597,68 @@ PyMethodDef ApplicationPy::Methods[] = { "Set the active origin by ID.\n" "\n" "id : str\n The origin ID to activate."}, + {"registerEditingContext", + (PyCFunction)ApplicationPy::sRegisterEditingContext, + METH_VARARGS, + "registerEditingContext(id, label, color, toolbars, match, priority=50) -> None\n" + "\n" + "Register an editing context for dynamic toolbar management.\n" + "\n" + "id : str\n Unique context identifier (e.g. 'fem.analysis').\n" + "label : str\n Display label template. Use {name} for object name.\n" + "color : str\n Hex color for breadcrumb (e.g. '#f38ba8').\n" + "toolbars : list of str\n Toolbar names to show when active.\n" + "match : callable\n Function returning True if context is active.\n" + "priority : int\n Higher values checked first (default 50)."}, + {"unregisterEditingContext", + (PyCFunction)ApplicationPy::sUnregisterEditingContext, + METH_VARARGS, + "unregisterEditingContext(id) -> None\n" + "\n" + "Unregister an editing context.\n" + "\n" + "id : str\n Context identifier to remove."}, + {"registerEditingOverlay", + (PyCFunction)ApplicationPy::sRegisterEditingOverlay, + METH_VARARGS, + "registerEditingOverlay(id, toolbars, match) -> None\n" + "\n" + "Register an overlay context that adds toolbars to any active context.\n" + "\n" + "id : str\n Unique overlay identifier.\n" + "toolbars : list of str\n Toolbar names to add.\n" + "match : callable\n Function returning True if overlay is active."}, + {"unregisterEditingOverlay", + (PyCFunction)ApplicationPy::sUnregisterEditingOverlay, + METH_VARARGS, + "unregisterEditingOverlay(id) -> None\n" + "\n" + "Unregister an editing overlay.\n" + "\n" + "id : str\n Overlay identifier to remove."}, + {"injectEditingCommands", + (PyCFunction)ApplicationPy::sInjectEditingCommands, + METH_VARARGS, + "injectEditingCommands(contextId, toolbarName, commands) -> None\n" + "\n" + "Inject additional commands into a context's toolbar.\n" + "\n" + "contextId : str\n Context to inject into.\n" + "toolbarName : str\n Toolbar within that context.\n" + "commands : list of str\n Command names to add."}, + {"currentEditingContext", + (PyCFunction)ApplicationPy::sCurrentEditingContext, + METH_VARARGS, + "currentEditingContext() -> dict\n" + "\n" + "Get the current editing context as a dict with keys:\n" + "id, label, color, toolbars, breadcrumb, breadcrumbColors."}, + {"refreshEditingContext", + (PyCFunction)ApplicationPy::sRefreshEditingContext, + METH_VARARGS, + "refreshEditingContext() -> None\n" + "\n" + "Force re-resolution of the editing context."}, {nullptr, nullptr, 0, nullptr} /* Sentinel */ }; @@ -2139,3 +2202,185 @@ PyObject* ApplicationPy::sSetActiveOrigin(PyObject* /*self*/, PyObject* args) } PY_CATCH; } + +// --------------------------------------------------------------------------- +// Editing Context Python API +// --------------------------------------------------------------------------- + +static QStringList pyListToQStringList(PyObject* listObj) +{ + QStringList result; + if (!listObj || !PyList_Check(listObj)) { + return result; + } + Py_ssize_t size = PyList_Size(listObj); + for (Py_ssize_t i = 0; i < size; ++i) { + PyObject* item = PyList_GetItem(listObj, i); + if (PyUnicode_Check(item)) { + result << QString::fromUtf8(PyUnicode_AsUTF8(item)); + } + } + return result; +} + +static PyObject* qStringListToPyList(const QStringList& list) +{ + Py::List pyList; + for (const auto& s : list) { + pyList.append(Py::String(s.toUtf8().constData())); + } + return Py::new_reference_to(pyList); +} + +PyObject* ApplicationPy::sRegisterEditingContext(PyObject* /*self*/, PyObject* args) +{ + const char* id = nullptr; + const char* label = nullptr; + const char* color = nullptr; + PyObject* toolbarsList = nullptr; + PyObject* matchCallable = nullptr; + int priority = 50; + + if (!PyArg_ParseTuple(args, "sssOO|i", &id, &label, &color, &toolbarsList, &matchCallable, &priority)) { + return nullptr; + } + + if (!PyCallable_Check(matchCallable)) { + PyErr_SetString(PyExc_TypeError, "match must be callable"); + return nullptr; + } + + QStringList toolbars = pyListToQStringList(toolbarsList); + + // Hold a reference to the Python callable + Py::Callable pyMatch(matchCallable); + pyMatch.increment_reference_count(); + + ContextDefinition def; + def.id = QString::fromUtf8(id); + def.labelTemplate = QString::fromUtf8(label); + def.color = QString::fromUtf8(color); + def.toolbars = toolbars; + def.priority = priority; + def.match = [pyMatch]() -> bool { + Base::PyGILStateLocker lock; + try { + Py::Callable fn(pyMatch); + Py::Object result = fn.apply(Py::TupleN()); + return result.isTrue(); + } + catch (Py::Exception&) { + Base::PyException e; + e.ReportException(); + return false; + } + }; + + EditingContextResolver::instance()->registerContext(def); + Py_Return; +} + +PyObject* ApplicationPy::sUnregisterEditingContext(PyObject* /*self*/, PyObject* args) +{ + const char* id = nullptr; + if (!PyArg_ParseTuple(args, "s", &id)) { + return nullptr; + } + EditingContextResolver::instance()->unregisterContext(QString::fromUtf8(id)); + Py_Return; +} + +PyObject* ApplicationPy::sRegisterEditingOverlay(PyObject* /*self*/, PyObject* args) +{ + const char* id = nullptr; + PyObject* toolbarsList = nullptr; + PyObject* matchCallable = nullptr; + + if (!PyArg_ParseTuple(args, "sOO", &id, &toolbarsList, &matchCallable)) { + return nullptr; + } + + if (!PyCallable_Check(matchCallable)) { + PyErr_SetString(PyExc_TypeError, "match must be callable"); + return nullptr; + } + + QStringList toolbars = pyListToQStringList(toolbarsList); + + Py::Callable pyMatch(matchCallable); + pyMatch.increment_reference_count(); + + OverlayDefinition def; + def.id = QString::fromUtf8(id); + def.toolbars = toolbars; + def.match = [pyMatch]() -> bool { + Base::PyGILStateLocker lock; + try { + Py::Callable fn(pyMatch); + Py::Object result = fn.apply(Py::TupleN()); + return result.isTrue(); + } + catch (Py::Exception&) { + Base::PyException e; + e.ReportException(); + return false; + } + }; + + EditingContextResolver::instance()->registerOverlay(def); + Py_Return; +} + +PyObject* ApplicationPy::sUnregisterEditingOverlay(PyObject* /*self*/, PyObject* args) +{ + const char* id = nullptr; + if (!PyArg_ParseTuple(args, "s", &id)) { + return nullptr; + } + EditingContextResolver::instance()->unregisterOverlay(QString::fromUtf8(id)); + Py_Return; +} + +PyObject* ApplicationPy::sInjectEditingCommands(PyObject* /*self*/, PyObject* args) +{ + const char* contextId = nullptr; + const char* toolbarName = nullptr; + PyObject* commandsList = nullptr; + + if (!PyArg_ParseTuple(args, "ssO", &contextId, &toolbarName, &commandsList)) { + return nullptr; + } + + QStringList commands = pyListToQStringList(commandsList); + EditingContextResolver::instance() + ->injectCommands(QString::fromUtf8(contextId), QString::fromUtf8(toolbarName), commands); + Py_Return; +} + +PyObject* ApplicationPy::sCurrentEditingContext(PyObject* /*self*/, PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + EditingContext ctx = EditingContextResolver::instance()->currentContext(); + + Py::Dict result; + result.setItem("id", Py::String(ctx.id.toUtf8().constData())); + result.setItem("label", Py::String(ctx.label.toUtf8().constData())); + result.setItem("color", Py::String(ctx.color.toUtf8().constData())); + result.setItem("toolbars", Py::Object(qStringListToPyList(ctx.toolbars), true)); + result.setItem("breadcrumb", Py::Object(qStringListToPyList(ctx.breadcrumb), true)); + result.setItem("breadcrumbColors", Py::Object(qStringListToPyList(ctx.breadcrumbColors), true)); + + return Py::new_reference_to(result); +} + +PyObject* ApplicationPy::sRefreshEditingContext(PyObject* /*self*/, PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + EditingContextResolver::instance()->refresh(); + Py_Return; +} diff --git a/src/Gui/ApplicationPy.h b/src/Gui/ApplicationPy.h index e92211e8db..cd88de0cbc 100644 --- a/src/Gui/ApplicationPy.h +++ b/src/Gui/ApplicationPy.h @@ -123,6 +123,15 @@ public: static PyObject* sSuspendWaitCursor (PyObject *self, PyObject *args); static PyObject* sResumeWaitCursor (PyObject *self, PyObject *args); + // Editing context management + static PyObject* sRegisterEditingContext (PyObject *self, PyObject *args); + static PyObject* sUnregisterEditingContext (PyObject *self, PyObject *args); + static PyObject* sRegisterEditingOverlay (PyObject *self, PyObject *args); + static PyObject* sUnregisterEditingOverlay (PyObject *self, PyObject *args); + static PyObject* sInjectEditingCommands (PyObject *self, PyObject *args); + static PyObject* sCurrentEditingContext (PyObject *self, PyObject *args); + static PyObject* sRefreshEditingContext (PyObject *self, PyObject *args); + static PyMethodDef Methods[]; // clang-format on }; diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index ca461d7fbb..d46967af3d 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1318,7 +1318,9 @@ SOURCE_GROUP("View" FILES ${View_SRCS}) # The workbench sources SET(Workbench_CPP_SRCS + BreadcrumbToolBar.cpp DockWindowManager.cpp + EditingContext.cpp OverlayManager.cpp OverlayWidgets.cpp MenuManager.cpp @@ -1336,7 +1338,9 @@ SET(Workbench_CPP_SRCS ) SET(Workbench_SRCS ${Workbench_CPP_SRCS} + BreadcrumbToolBar.h DockWindowManager.h + EditingContext.h OverlayManager.h OverlayWidgets.h MenuManager.h diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index 4272adba2b..159cf60cf7 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -105,6 +105,8 @@ #include "SelectionView.h" #include "SplashScreen.h" #include "StatusBarLabel.h" +#include "BreadcrumbToolBar.h" +#include "EditingContext.h" #include "ToolBarManager.h" #include "ToolBoxManager.h" #include "Tree.h" @@ -305,6 +307,7 @@ struct MainWindowP QLabel* actionLabel; InputHintWidget* hintLabel; QLabel* rightSideLabel; + BreadcrumbToolBar* breadcrumbBar = nullptr; QTimer* actionTimer; QTimer* statusTimer; QTimer* activityTimer; @@ -489,6 +492,20 @@ MainWindow::MainWindow(QWidget* parent, Qt::WindowFlags f) #endif connect(d->mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::onWindowActivated); + // Breadcrumb toolbar for editing context display + d->breadcrumbBar = new BreadcrumbToolBar(this); + addToolBar(Qt::TopToolBarArea, d->breadcrumbBar); + insertToolBarBreak(d->breadcrumbBar); + + // Initialize the editing context resolver and connect to breadcrumb + auto* ctxResolver = EditingContextResolver::instance(); + connect( + ctxResolver, + &EditingContextResolver::contextChanged, + d->breadcrumbBar, + &BreadcrumbToolBar::updateContext + ); + setupDockWindows(); // accept drops on the window, get handled in dropEvent, dragEnterEvent diff --git a/src/Gui/PythonWorkbenchPyImp.cpp b/src/Gui/PythonWorkbenchPyImp.cpp index 8835095f27..c22b4b8b44 100644 --- a/src/Gui/PythonWorkbenchPyImp.cpp +++ b/src/Gui/PythonWorkbenchPyImp.cpp @@ -225,7 +225,8 @@ PyObject* PythonWorkbenchPy::appendToolbar(PyObject* args) { PyObject* pObject; char* psToolBar; - if (!PyArg_ParseTuple(args, "sO", &psToolBar, &pObject)) { + const char* psVisibility = nullptr; + if (!PyArg_ParseTuple(args, "sO|s", &psToolBar, &pObject, &psVisibility)) { return nullptr; } if (!PyList_Check(pObject)) { @@ -233,6 +234,17 @@ PyObject* PythonWorkbenchPy::appendToolbar(PyObject* args) return nullptr; } + auto vis = Gui::ToolBarItem::DefaultVisibility::Visible; + if (psVisibility) { + std::string visStr(psVisibility); + if (visStr == "Unavailable") { + vis = Gui::ToolBarItem::DefaultVisibility::Unavailable; + } + else if (visStr == "Hidden") { + vis = Gui::ToolBarItem::DefaultVisibility::Hidden; + } + } + std::list items; int nSize = PyList_Size(pObject); for (int i = 0; i < nSize; ++i) { @@ -245,7 +257,7 @@ PyObject* PythonWorkbenchPy::appendToolbar(PyObject* args) continue; } } - getPythonBaseWorkbenchPtr()->appendToolbar(psToolBar, items); + getPythonBaseWorkbenchPtr()->appendToolbar(psToolBar, items, vis); Py_Return; } diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 93e500fd50..2c1dd94179 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -1246,11 +1246,15 @@ void PythonBaseWorkbench::clearContextMenu() _contextMenu->clear(); } -void PythonBaseWorkbench::appendToolbar(const std::string& bar, const std::list& items) const +void PythonBaseWorkbench::appendToolbar( + const std::string& bar, + const std::list& items, + ToolBarItem::DefaultVisibility vis +) const { ToolBarItem* item = _toolBar->findItem(bar); if (!item) { - item = new ToolBarItem(_toolBar); + item = new ToolBarItem(_toolBar, vis); item->setCommand(bar); } diff --git a/src/Gui/Workbench.h b/src/Gui/Workbench.h index e3152bdce8..65c0353098 100644 --- a/src/Gui/Workbench.h +++ b/src/Gui/Workbench.h @@ -277,7 +277,11 @@ public: void clearContextMenu(); /// Appends a new toolbar - void appendToolbar(const std::string& bar, const std::list& items) const; + void appendToolbar( + const std::string& bar, + const std::list& items, + ToolBarItem::DefaultVisibility vis = ToolBarItem::DefaultVisibility::Visible + ) const; /// Removes a toolbar void removeToolbar(const std::string& bar) const; diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 62394c1c90..afcfbb2739 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -106,8 +106,11 @@ class AssemblyWorkbench(Workbench): "Assembly_CreateJointGearBelt", ] - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdList) - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly Joints"), cmdListJoints) + # Unavailable — EditingContextResolver controls visibility + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdList, "Unavailable") + self.appendToolbar( + QT_TRANSLATE_NOOP("Workbench", "Assembly Joints"), cmdListJoints, "Unavailable" + ) self.appendMenu( [QT_TRANSLATE_NOOP("Workbench", "&Assembly")], diff --git a/src/Mod/PartDesign/Gui/Workbench.cpp b/src/Mod/PartDesign/Gui/Workbench.cpp index 7de7d24864..5b8bd00b41 100644 --- a/src/Mod/PartDesign/Gui/Workbench.cpp +++ b/src/Mod/PartDesign/Gui/Workbench.cpp @@ -580,7 +580,9 @@ Gui::MenuItem* Workbench::setupMenuBar() const Gui::ToolBarItem* Workbench::setupToolBars() const { Gui::ToolBarItem* root = StdWorkbench::setupToolBars(); - Gui::ToolBarItem* part = new Gui::ToolBarItem(root); + // All PartDesign toolbars use Unavailable — EditingContextResolver controls visibility + Gui::ToolBarItem* part + = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); part->setCommand("Part Design Helper Features"); *part << "PartDesign_Body" @@ -590,7 +592,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "PartDesign_SubShapeBinder" << "PartDesign_Clone"; - part = new Gui::ToolBarItem(root); + part = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); part->setCommand("Part Design Modeling Features"); *part << "PartDesign_Pad" @@ -610,15 +612,14 @@ Gui::ToolBarItem* Workbench::setupToolBars() const << "Separator" << "PartDesign_Boolean"; - part = new Gui::ToolBarItem(root); - + part = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); part->setCommand("Part Design Dress-Up Features"); *part << "PartDesign_Fillet" << "PartDesign_Chamfer" << "PartDesign_Draft" << "PartDesign_Thickness"; - part = new Gui::ToolBarItem(root); + part = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); part->setCommand("Part Design Transformation Features"); *part << "PartDesign_Mirrored" diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index aaf1066acc..8302b95979 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -3665,7 +3665,7 @@ bool ViewProviderSketch::setEdit(int ModNum) Gui::getMainWindow()->installEventFilter(listener.get()); - Workbench::enterEditMode(); + // Toolbar visibility is now managed by EditingContextResolver. // Give focus to the MDI so that keyboard events are caught after starting edit. // Else pressing ESC right after starting edit will not be caught to exit edit mode. @@ -3815,7 +3815,7 @@ void ViewProviderSketch::unsetEdit(int ModNum) auto gridnode = getGridNode(); pcRoot->removeChild(gridnode); - Workbench::leaveEditMode(); + // Toolbar visibility is now managed by EditingContextResolver. if (listener) { Gui::getMainWindow()->removeEventFilter(listener.get()); diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index a03a41802f..47ba6e5e50 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -176,80 +176,19 @@ inline const QStringList nonEditModeToolbarNames() void Workbench::activated() { - /* When the workbench is activated, it may happen that we are in edit mode or not. - * If we are not in edit mode, the function enterEditMode (called by the ViewProvider) takes - * care to save the state of toolbars outside of edit mode. We cannot do it here, as we are - * coming from another WB. - * - * If we moved to another WB from edit mode, the new WB was activated before deactivating this. - * Therefore we had no chance to tidy up the save state. We assume a loss of any CHANGE to - * toolbar configuration since last entering edit mode in this case (for any change in - * configuration to be stored, the edit mode must be left while the selected Workbench is the - * sketcher WB). - * - * However, now that we are back (from another WB), we need to make the toolbars available. - * These correspond to the last saved state. - */ - Gui::Document* doc = Gui::Application::Instance->activeDocument(); - if (isSketchInEdit(doc)) { - Gui::ToolBarManager::getInstance()->setState( - editModeToolbarNames(), - Gui::ToolBarManager::State::ForceAvailable - ); - - Gui::ToolBarManager::getInstance()->setState( - nonEditModeToolbarNames(), - Gui::ToolBarManager::State::ForceHidden - ); - } + // Toolbar visibility is now managed by EditingContextResolver. + // The context system handles showing/hiding Sketcher toolbars + // based on whether a sketch is in edit mode. } void Workbench::enterEditMode() { - /* Ensure the state left by the non-edit mode toolbars is saved (in case of changing to edit - * mode) without changing workbench - */ - Gui::ToolBarManager::getInstance()->setState( - nonEditModeToolbarNames(), - Gui::ToolBarManager::State::SaveState - ); - - Gui::ToolBarManager::getInstance()->setState( - editModeToolbarNames(), - Gui::ToolBarManager::State::ForceAvailable - ); - Gui::ToolBarManager::getInstance()->setState( - nonEditModeToolbarNames(), - Gui::ToolBarManager::State::ForceHidden - ); + // Toolbar visibility is now managed by EditingContextResolver. } void Workbench::leaveEditMode() { - /* Ensure the state left by the edit mode toolbars is saved (in case of changing to edit mode) - * without changing workbench. - * - * However, do not save state if the current workbench is not the Sketcher WB, because otherwise - * we would be saving the state of the currently activated workbench, and the toolbars would - * disappear (as the toolbars of that other WB are only visible). - */ - auto* workbench = Gui::WorkbenchManager::instance()->active(); - - if (workbench->name() == "SketcherWorkbench") { - Gui::ToolBarManager::getInstance()->setState( - editModeToolbarNames(), - Gui::ToolBarManager::State::SaveState - ); - } - - Gui::ToolBarManager::getInstance()->setState( - editModeToolbarNames(), - Gui::ToolBarManager::State::RestoreDefault - ); - Gui::ToolBarManager::getInstance()->setState( - nonEditModeToolbarNames(), - Gui::ToolBarManager::State::RestoreDefault - ); + // Toolbar visibility is now managed by EditingContextResolver. } namespace SketcherGui diff --git a/src/Mod/Spreadsheet/Gui/Workbench.cpp b/src/Mod/Spreadsheet/Gui/Workbench.cpp index 8395679abd..82d5a79cb3 100644 --- a/src/Mod/Spreadsheet/Gui/Workbench.cpp +++ b/src/Mod/Spreadsheet/Gui/Workbench.cpp @@ -238,7 +238,9 @@ Gui::MenuItem* Workbench::setupMenuBar() const Gui::ToolBarItem* Workbench::setupToolBars() const { Gui::ToolBarItem* root = StdWorkbench::setupToolBars(); - Gui::ToolBarItem* part = new Gui::ToolBarItem(root); + // Unavailable — EditingContextResolver controls visibility + Gui::ToolBarItem* part + = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); part->setCommand("Spreadsheet"); *part << "Spreadsheet_CreateSheet" << "Separator"