cherry-pick #32: MDI pre-document tab for Silo new item (70118201b0)

This commit is contained in:
forbes
2026-02-13 14:09:44 -06:00
parent 30cc226bf6
commit 54f8006e24
12 changed files with 321 additions and 81 deletions

View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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

View File

@@ -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

View File

@@ -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<std::string> 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;
}

View File

@@ -1246,11 +1246,15 @@ void PythonBaseWorkbench::clearContextMenu()
_contextMenu->clear();
}
void PythonBaseWorkbench::appendToolbar(const std::string& bar, const std::list<std::string>& items) const
void PythonBaseWorkbench::appendToolbar(
const std::string& bar,
const std::list<std::string>& items,
ToolBarItem::DefaultVisibility vis
) const
{
ToolBarItem* item = _toolBar->findItem(bar);
if (!item) {
item = new ToolBarItem(_toolBar);
item = new ToolBarItem(_toolBar, vis);
item->setCommand(bar);
}

View File

@@ -277,7 +277,11 @@ public:
void clearContextMenu();
/// Appends a new toolbar
void appendToolbar(const std::string& bar, const std::list<std::string>& items) const;
void appendToolbar(
const std::string& bar,
const std::list<std::string>& items,
ToolBarItem::DefaultVisibility vis = ToolBarItem::DefaultVisibility::Visible
) const;
/// Removes a toolbar
void removeToolbar(const std::string& bar) const;

View File

@@ -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")],

View File

@@ -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"

View File

@@ -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());

View File

@@ -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

View File

@@ -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"