feat(silo): replace modal new-item dialog with MDI pre-document tab #176

Merged
forbes merged 1 commits from feat/editing-context-system into main 2026-02-11 21:51:08 +00:00
24 changed files with 1376 additions and 111 deletions

View File

@@ -12,13 +12,22 @@ FreeCAD startup
└─ src/Mod/Create/InitGui.py
├─ setup_kindred_workbenches()
│ ├─ exec(mods/ztools/ztools/InitGui.py)
│ │ registers ZToolsWorkbench
│ │ └─ installs _ZToolsPartDesignManipulator (global)
│ │ schedules deferred _register() (2000ms)
│ │ ├─ imports ZTools commands
│ │ ├─ installs _ZToolsManipulator (global)
│ │ └─ injects commands into editing contexts
│ └─ exec(mods/silo/freecad/InitGui.py)
─ registers SiloWorkbench
─ registers SiloWorkbench
│ └─ schedules deferred Silo overlay registration (2500ms)
├─ EditingContextResolver singleton created (MainWindow constructor)
│ ├─ registers built-in contexts (PartDesign, Sketcher, Assembly, Spreadsheet)
│ ├─ connects to signalInEdit/signalResetEdit/signalActiveDocument/signalActivateView
│ └─ BreadcrumbToolBar connected to contextChanged signal
└─ Deferred setup (QTimer):
├─ 1500ms: _register_silo_origin() → registers Silo FileOrigin
├─ 2000ms: _setup_silo_auth_panel() → "Database Auth" dock
├─ 2000ms: ZTools _register() → commands + manipulator
├─ 2500ms: Silo overlay registration → "Silo Origin" toolbar overlay
├─ 3000ms: _check_silo_first_start() → settings prompt
├─ 4000ms: _setup_silo_activity_panel() → "Database Activity" dock (SSE)
└─ 10000ms: _check_for_updates() → update checker (Gitea API)
@@ -33,13 +42,15 @@ src/Mod/Create/ Kindred bootstrap module (Python)
├── version.py.in CMake template → version.py (build-time)
└── update_checker.py Checks Gitea releases API for updates
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
src/Gui/CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
src/Gui/OriginManager.h/.cpp Origin lifecycle management
src/Gui/OriginSelectorWidget.h/.cpp UI for origin selection
mods/ztools/ [submodule] ztools workbench
├── ztools/InitGui.py ZToolsWorkbench + PartDesign manipulator
mods/ztools/ [submodule] command provider (not a workbench)
├── ztools/InitGui.py Deferred command registration + _ZToolsManipulator
├── ztools/ztools/
│ ├── commands/ Datum, pattern, pocket, assembly, spreadsheet
│ ├── datums/core.py Datum creation via Part::AttachExtension
@@ -50,7 +61,7 @@ mods/silo/ [submodule -> silo-mod.git] FreeCAD workbench
├── silo-client/ [submodule -> silo-client.git] shared API client
│ └── silo_client/ SiloClient, SiloSettings, CATEGORY_NAMES
└── freecad/ FreeCAD workbench (Python)
├── InitGui.py SiloWorkbench
├── InitGui.py SiloWorkbench + Silo overlay context registration
├── silo_commands.py Commands + FreeCADSiloSettings adapter
└── silo_origin.py FileOrigin backend for Silo

View File

@@ -1,26 +1,29 @@
# Components
## ztools workbench
## ztools (command provider)
ZTools is no longer a standalone workbench. It registers as a command provider that
injects tools into the appropriate editing contexts via `WorkbenchManipulator` and
the `EditingContextResolver` system.
**Registered commands (9):**
| Command | Function |
|---------|----------|
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) |
| `ZTools_DatumManager` | Manage existing datum objects |
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) |
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation |
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly |
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis |
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles |
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment |
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation |
| Command | Function | Injected Into |
|---------|----------|---------------|
| `ZTools_DatumCreator` | Create datum planes, axes, points (16 modes) | PartDesign Helper Features |
| `ZTools_DatumManager` | Manage existing datum objects | PartDesign Helper Features |
| `ZTools_EnhancedPocket` | Flip-side pocket (cut outside sketch profile) | PartDesign Modeling Features |
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation | PartDesign Transformation Features |
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Assembly |
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Assembly |
| `ZTools_SpreadsheetStyle{Bold,Italic,Underline}` | Text style toggles | Spreadsheet |
| `ZTools_SpreadsheetAlign{Left,Center,Right}` | Cell alignment | Spreadsheet |
| `ZTools_Spreadsheet{BgColor,TextColor,QuickAlias}` | Colors and alias creation | Spreadsheet |
**PartDesign integration** via `_ZToolsPartDesignManipulator`:
- `ZTools_DatumCreator`, `ZTools_DatumManager` → "Part Design Helper Features" toolbar
- `ZTools_EnhancedPocket` → "Part Design Modeling Features" toolbar
- `ZTools_RotatedLinearPattern` → "Part Design Transformation Features" toolbar
- Same commands inserted into Part Design menu after `PartDesign_Boolean`
**Integration** via `_ZToolsManipulator` (WorkbenchManipulator) and `injectEditingCommands()`:
- Commands are injected into native workbench toolbars (PartDesign, Assembly, Spreadsheet)
- Context toolbar injections ensure commands appear when the relevant editing context is active
- PartDesign menu items inserted after `PartDesign_Boolean`
**Datum types (7):** offset_from_face, offset_from_plane, midplane, 3_points, normal_to_edge, angled, tangent_to_cylinder. All except tangent_to_cylinder use `Part::AttachExtension` for automatic parametric updates.

View File

@@ -76,7 +76,7 @@ Pure Python workbenches following FreeCAD's addon pattern. Self-contained with `
**Implementation:** `src/Mod/Create/Init.py` and `InitGui.py` load workbenches from `mods/` at startup using `exec()`. Addons degrade gracefully if submodule is absent.
**Default workbench:** `ZToolsWorkbench` (set in `resources/preferences/KindredCreate/KindredCreate.cfg`).
**Default workbench:** `PartDesignWorkbench` (set in `PreferencePacks/KindredCreate/KindredCreate.cfg`). ZTools is no longer a workbench — its commands are injected into native workbench toolbars via the `EditingContextResolver` and `WorkbenchManipulator` systems.
### Phase 2: Enhanced Pocket as C++ feature -- NOT STARTED

View File

@@ -6,7 +6,7 @@
1. ~~**QSS duplication.**~~ Resolved. The canonical QSS lives in `src/Gui/Stylesheets/KindredCreate.qss`. The PreferencePacks copy is now generated at build time via `configure_file()` in `src/Gui/PreferencePacks/CMakeLists.txt`. The unused `resources/preferences/KindredCreate/` directory has been removed.
2. **WorkbenchManipulator timing.** The `_ZToolsPartDesignManipulator` appends commands by name. If ZToolsWorkbench hasn't been activated when the user switches to PartDesign, the commands may not be registered. The manipulator API tolerates missing commands silently, but buttons won't appear.
2. ~~**WorkbenchManipulator timing.**~~ Resolved. ZTools is no longer a workbench. Commands are registered via a deferred `QTimer.singleShot(2000ms)` in `InitGui.py`, which activates dependent workbenches first, then imports ZTools commands and installs the `_ZToolsManipulator`. The `EditingContextResolver` handles toolbar visibility based on editing context.
3. ~~**Silo shortcut persistence.**~~ Resolved. `Silo_ToggleMode` removed; file operations now delegate to the selected origin via the unified origin system.

View File

@@ -33,8 +33,8 @@ See [Installation](./installation.md) for prebuilt package details and [Building
On first launch, Kindred Create:
1. Loads the **ztools** and **Silo** workbenches automatically via the Create bootstrap module
2. Opens the **ZToolsWorkbench** as the default workbench
1. Loads **ztools** commands and the **Silo** workbench via the Create bootstrap module
2. Opens the **PartDesign** workbench as the default (with context-driven toolbars)
3. Prompts for Silo server configuration if not yet set up
4. Checks for application updates in the background (after ~10 seconds)

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

@@ -0,0 +1,195 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include "BreadcrumbToolBar.h"
#include "EditingContext.h"
#include <QColor>
#include <QLabel>
#include <QToolButton>
#include "Application.h"
#include "Document.h"
using namespace Gui;
// ---------------------------------------------------------------------------
// Stylesheet template for breadcrumb chip buttons
// ---------------------------------------------------------------------------
static QString chipStyle(const QString& bgHex)
{
// Darken background slightly for contrast, use white text
QColor bg(bgHex);
QColor textColor(QStringLiteral("#cdd6f4")); // Catppuccin Text
return QStringLiteral(
"QToolButton {"
" background-color: %1;"
" color: %2;"
" border: none;"
" border-radius: 4px;"
" padding: 2px 8px;"
" font-weight: bold;"
" font-size: 11px;"
"}"
"QToolButton:hover {"
" background-color: %3;"
"}"
"QToolButton:pressed {"
" background-color: %4;"
"}"
)
.arg(bg.name(), textColor.name(), bg.lighter(120).name(), bg.darker(120).name());
}
static QString separatorStyle()
{
return QStringLiteral(
"QLabel {"
" color: #6c7086;" // Catppuccin Overlay0
" font-size: 11px;"
" padding: 0 2px;"
"}"
);
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
BreadcrumbToolBar::BreadcrumbToolBar(QWidget* parent)
: QToolBar(parent)
{
setObjectName(QStringLiteral("BreadcrumbToolBar"));
setWindowTitle(tr("Editing Context"));
setMovable(false);
setFloatable(false);
setIconSize(QSize(16, 16));
// Thin toolbar with minimal margins
setStyleSheet(QStringLiteral(
"QToolBar {"
" background: #1e1e2e;" // Catppuccin Base
" border: none;"
" spacing: 2px;"
" padding: 2px 4px;"
" max-height: 24px;"
"}"
));
}
// ---------------------------------------------------------------------------
// Update display from context
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::updateContext(const EditingContext& ctx)
{
clearSegments();
buildSegments(ctx);
}
// ---------------------------------------------------------------------------
// Build breadcrumb segments
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::clearSegments()
{
for (auto* btn : _segments) {
removeAction(btn->defaultAction());
delete btn;
}
_segments.clear();
for (auto* sep : _separators) {
delete sep;
}
_separators.clear();
clear();
}
void BreadcrumbToolBar::buildSegments(const EditingContext& ctx)
{
if (ctx.breadcrumb.isEmpty()) {
return;
}
for (int i = 0; i < ctx.breadcrumb.size(); ++i) {
// Add separator between segments
if (i > 0) {
auto* sep = new QLabel(QStringLiteral("\u203A")); // character
sep->setStyleSheet(separatorStyle());
addWidget(sep);
_separators.append(sep);
}
auto* btn = new QToolButton(this);
btn->setText(ctx.breadcrumb[i]);
btn->setAutoRaise(true);
// Apply color from breadcrumb colors
QString color;
if (i < ctx.breadcrumbColors.size()) {
color = ctx.breadcrumbColors[i];
}
else {
color = ctx.color;
}
btn->setStyleSheet(chipStyle(color));
// Make clickable: clicking a parent segment navigates up
int segmentIndex = i;
connect(btn, &QToolButton::clicked, this, [this, segmentIndex]() {
segmentClicked(segmentIndex);
});
addWidget(btn);
_segments.append(btn);
}
}
// ---------------------------------------------------------------------------
// Navigation: clicking a breadcrumb segment
// ---------------------------------------------------------------------------
void BreadcrumbToolBar::segmentClicked(int index)
{
// Clicking the last segment does nothing (already there)
if (index >= _segments.size() - 1) {
return;
}
// Clicking a parent segment exits the current edit mode
auto* guiDoc = Application::Instance->activeDocument();
if (guiDoc && guiDoc->getInEdit()) {
guiDoc->resetEdit();
}
}
#include "moc_BreadcrumbToolBar.cpp"

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#ifndef GUI_BREADCRUMBTOOLBAR_H
#define GUI_BREADCRUMBTOOLBAR_H
#include <QToolBar>
#include <QList>
#include <FCGlobal.h>
class QToolButton;
class QLabel;
namespace Gui
{
struct EditingContext;
/**
* A thin toolbar that displays the current editing context as color-coded
* breadcrumb segments. Each segment is a clickable QToolButton; clicking
* a parent segment navigates up (exits the current edit, etc.).
*/
class GuiExport BreadcrumbToolBar: public QToolBar
{
Q_OBJECT
public:
explicit BreadcrumbToolBar(QWidget* parent = nullptr);
public Q_SLOTS:
void updateContext(const EditingContext& ctx);
private Q_SLOTS:
void segmentClicked(int index);
private:
void clearSegments();
void buildSegments(const EditingContext& ctx);
QList<QToolButton*> _segments;
QList<QLabel*> _separators;
};
} // namespace Gui
#endif // GUI_BREADCRUMBTOOLBAR_H

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

614
src/Gui/EditingContext.cpp Normal file
View File

@@ -0,0 +1,614 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include "EditingContext.h"
#include <algorithm>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <Base/Type.h>
#include "Application.h"
#include "Document.h"
#include "MDIView.h"
#include "ToolBarManager.h"
#include "ViewProvider.h"
#include "ViewProviderDocumentObject.h"
using namespace Gui;
// Catppuccin Mocha palette
namespace CatppuccinMocha
{
constexpr const char* Surface0 = "#313244";
constexpr const char* Surface1 = "#45475a";
constexpr const char* Mauve = "#cba6f7";
constexpr const char* Green = "#a6e3a1";
constexpr const char* Blue = "#89b4fa";
constexpr const char* Yellow = "#f9e2af";
constexpr const char* Teal = "#94e2d5";
constexpr const char* Red = "#f38ba8";
constexpr const char* Peach = "#fab387";
constexpr const char* Text = "#cdd6f4";
} // namespace CatppuccinMocha
// ---------------------------------------------------------------------------
// Private data
// ---------------------------------------------------------------------------
struct EditingContextResolver::Private
{
QList<ContextDefinition> contexts; // sorted by descending priority
QList<OverlayDefinition> overlays;
EditingContext current;
// Additional commands injected into context toolbars
// Key: contextId -> toolbarName -> list of command names
QMap<QString, QMap<QString, QStringList>> injections;
void sortContexts()
{
std::sort(
contexts.begin(),
contexts.end(),
[](const ContextDefinition& a, const ContextDefinition& b) {
return a.priority > b.priority;
}
);
}
};
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
EditingContextResolver* EditingContextResolver::_instance = nullptr;
EditingContextResolver* EditingContextResolver::instance()
{
if (!_instance) {
_instance = new EditingContextResolver();
}
return _instance;
}
void EditingContextResolver::destruct()
{
delete _instance;
_instance = nullptr;
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
EditingContextResolver::EditingContextResolver()
: QObject(nullptr)
, d(new Private)
{
registerBuiltinContexts();
// Connect to application signals
auto& app = *Application::Instance;
app.signalInEdit.connect([this](const ViewProviderDocumentObject& vp) { onInEdit(vp); });
app.signalResetEdit.connect([this](const ViewProviderDocumentObject& vp) { onResetEdit(vp); });
app.signalActiveDocument.connect([this](const Document& doc) { onActiveDocument(doc); });
app.signalActivateView.connect([this](const MDIView* view) { onActivateView(view); });
app.signalActivateWorkbench.connect([this](const char*) { refresh(); });
}
EditingContextResolver::~EditingContextResolver()
{
delete d;
}
// ---------------------------------------------------------------------------
// Helper: check if an App::DocumentObject's type name matches (by string)
// ---------------------------------------------------------------------------
static bool objectIsDerivedFrom(App::DocumentObject* obj, const char* typeName)
{
if (!obj) {
return false;
}
Base::Type target = Base::Type::fromName(typeName);
if (target.isBad()) {
return false;
}
return obj->getTypeId().isDerivedFrom(target);
}
static bool vpObjectIsDerivedFrom(ViewProvider* vp, const char* typeName)
{
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (!vpd) {
return false;
}
return objectIsDerivedFrom(vpd->getObject(), typeName);
}
// ---------------------------------------------------------------------------
// Helper: get the active "part" object from the active view
// ---------------------------------------------------------------------------
static App::DocumentObject* getActivePartObject()
{
auto* guiDoc = Application::Instance->activeDocument();
if (!guiDoc) {
return nullptr;
}
auto* view = guiDoc->getActiveView();
if (!view) {
return nullptr;
}
return view->getActiveObject<App::DocumentObject*>("part");
}
// ---------------------------------------------------------------------------
// Helper: get the label of the active "part" object
// ---------------------------------------------------------------------------
static QString getActivePartLabel()
{
auto* obj = getActivePartObject();
if (!obj) {
return {};
}
return QString::fromUtf8(obj->Label.getValue());
}
// ---------------------------------------------------------------------------
// Helper: get label of the object currently in edit
// ---------------------------------------------------------------------------
static QString getInEditLabel()
{
auto* guiDoc = Application::Instance->activeDocument();
if (!guiDoc) {
return {};
}
auto* vp = guiDoc->getInEdit();
if (!vp) {
return {};
}
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (!vpd || !vpd->getObject()) {
return {};
}
return QString::fromUtf8(vpd->getObject()->Label.getValue());
}
// ---------------------------------------------------------------------------
// Built-in context registrations
// ---------------------------------------------------------------------------
void EditingContextResolver::registerBuiltinContexts()
{
// --- Sketcher edit (highest priority — VP in edit) ---
registerContext({
/*.id =*/QStringLiteral("sketcher.edit"),
/*.labelTemplate =*/QStringLiteral("Sketcher: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Green),
/*.toolbars =*/
{QStringLiteral("Edit Mode"),
QStringLiteral("Geometries"),
QStringLiteral("Constraints"),
QStringLiteral("Sketcher Tools"),
QStringLiteral("B-Spline Tools"),
QStringLiteral("Visual Helpers")},
/*.priority =*/90,
/*.match =*/
[]() {
auto* doc = Application::Instance->activeDocument();
if (!doc) {
return false;
}
auto* vp = doc->getInEdit();
return vp && vpObjectIsDerivedFrom(vp, "Sketcher::SketchObject");
},
});
// --- Assembly edit (VP in edit) ---
registerContext({
/*.id =*/QStringLiteral("assembly.edit"),
/*.labelTemplate =*/QStringLiteral("Assembly: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Blue),
/*.toolbars =*/
{QStringLiteral("Assembly"),
QStringLiteral("Assembly Joints"),
QStringLiteral("Assembly Management")},
/*.priority =*/90,
/*.match =*/
[]() {
auto* doc = Application::Instance->activeDocument();
if (!doc) {
return false;
}
auto* vp = doc->getInEdit();
return vp && vpObjectIsDerivedFrom(vp, "Assembly::AssemblyObject");
},
});
// --- PartDesign with features (active body has children) ---
registerContext({
/*.id =*/QStringLiteral("partdesign.feature"),
/*.labelTemplate =*/QStringLiteral("Body: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Mauve),
/*.toolbars =*/
{QStringLiteral("Part Design Helper Features"),
QStringLiteral("Part Design Modeling Features"),
QStringLiteral("Part Design Dress-Up Features"),
QStringLiteral("Part Design Transformation Features")},
/*.priority =*/40,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
if (!obj || !objectIsDerivedFrom(obj, "PartDesign::Body")) {
return false;
}
// Body has at least one child beyond the origin
auto children = obj->getOutList();
for (auto* child : children) {
if (child && !objectIsDerivedFrom(child, "App::Origin")) {
return true;
}
}
return false;
},
});
// --- PartDesign body (active body, empty or origin-only) ---
registerContext({
/*.id =*/QStringLiteral("partdesign.body"),
/*.labelTemplate =*/QStringLiteral("Body: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Mauve),
/*.toolbars =*/
{QStringLiteral("Part Design Helper Features"), QStringLiteral("Sketcher")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "PartDesign::Body");
},
});
// --- Assembly idle (assembly exists, active, but not in edit) ---
registerContext({
/*.id =*/QStringLiteral("assembly.idle"),
/*.labelTemplate =*/QStringLiteral("Assembly: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Blue),
/*.toolbars =*/
{QStringLiteral("Assembly")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "Assembly::AssemblyObject");
},
});
// --- Spreadsheet ---
registerContext({
/*.id =*/QStringLiteral("spreadsheet"),
/*.labelTemplate =*/QStringLiteral("Spreadsheet: {name}"),
/*.color =*/QLatin1String(CatppuccinMocha::Yellow),
/*.toolbars =*/
{QStringLiteral("Spreadsheet")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePartObject();
return obj && objectIsDerivedFrom(obj, "Spreadsheet::Sheet");
},
});
// --- Empty document ---
registerContext({
/*.id =*/QStringLiteral("empty_document"),
/*.labelTemplate =*/QStringLiteral("New Document"),
/*.color =*/QLatin1String(CatppuccinMocha::Surface1),
/*.toolbars =*/
{QStringLiteral("Structure")},
/*.priority =*/10,
/*.match =*/
[]() { return Application::Instance->activeDocument() != nullptr; },
});
// --- No document ---
registerContext({
/*.id =*/QStringLiteral("no_document"),
/*.labelTemplate =*/QStringLiteral("Kindred Create"),
/*.color =*/QLatin1String(CatppuccinMocha::Surface0),
/*.toolbars =*/ {},
/*.priority =*/0,
/*.match =*/[]() { return true; },
});
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void EditingContextResolver::registerContext(const ContextDefinition& def)
{
// Remove any existing context with the same id
unregisterContext(def.id);
d->contexts.append(def);
d->sortContexts();
}
void EditingContextResolver::unregisterContext(const QString& id)
{
d->contexts.erase(
std::remove_if(
d->contexts.begin(),
d->contexts.end(),
[&](const ContextDefinition& c) { return c.id == id; }
),
d->contexts.end()
);
}
void EditingContextResolver::registerOverlay(const OverlayDefinition& def)
{
unregisterOverlay(def.id);
d->overlays.append(def);
}
void EditingContextResolver::unregisterOverlay(const QString& id)
{
d->overlays.erase(
std::remove_if(
d->overlays.begin(),
d->overlays.end(),
[&](const OverlayDefinition& o) { return o.id == id; }
),
d->overlays.end()
);
}
void EditingContextResolver::injectCommands(
const QString& contextId,
const QString& toolbarName,
const QStringList& commands
)
{
d->injections[contextId][toolbarName].append(commands);
}
// ---------------------------------------------------------------------------
// Resolution
// ---------------------------------------------------------------------------
EditingContext EditingContextResolver::resolve() const
{
EditingContext ctx;
// Find the first matching primary context
for (const auto& def : d->contexts) {
if (def.match && def.match()) {
ctx.id = def.id;
ctx.color = def.color;
ctx.toolbars = def.toolbars;
// Expand label template
QString label = def.labelTemplate;
if (label.contains(QStringLiteral("{name}"))) {
// For edit-mode contexts, use the in-edit object name
QString name = getInEditLabel();
if (name.isEmpty()) {
name = getActivePartLabel();
}
if (name.isEmpty()) {
name = QStringLiteral("?");
}
label.replace(QStringLiteral("{name}"), name);
}
ctx.label = label;
break;
}
}
// Append overlay toolbars
for (const auto& overlay : d->overlays) {
if (overlay.match && overlay.match()) {
for (const auto& tb : overlay.toolbars) {
if (!ctx.toolbars.contains(tb)) {
ctx.toolbars.append(tb);
}
}
}
}
// Append any injected toolbar names that aren't already in the context
auto injIt = d->injections.find(ctx.id);
if (injIt != d->injections.end()) {
for (auto it = injIt->begin(); it != injIt->end(); ++it) {
if (!ctx.toolbars.contains(it.key())) {
ctx.toolbars.append(it.key());
}
}
}
// Build breadcrumb
ctx.breadcrumb = buildBreadcrumb(ctx);
ctx.breadcrumbColors = buildBreadcrumbColors(ctx);
return ctx;
}
EditingContext EditingContextResolver::currentContext() const
{
return d->current;
}
// ---------------------------------------------------------------------------
// Breadcrumb building
// ---------------------------------------------------------------------------
QStringList EditingContextResolver::buildBreadcrumb(const EditingContext& ctx) const
{
QStringList crumbs;
if (ctx.id == QStringLiteral("no_document") || ctx.id == QStringLiteral("empty_document")) {
crumbs << ctx.label;
return crumbs;
}
// Always start with the active part/body/assembly label
QString partLabel = getActivePartLabel();
if (!partLabel.isEmpty()) {
crumbs << partLabel;
}
// If in edit mode, add the edited object
auto* guiDoc = Application::Instance->activeDocument();
if (guiDoc) {
auto* vp = guiDoc->getInEdit();
if (vp) {
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (vpd && vpd->getObject()) {
QString editLabel = QString::fromUtf8(vpd->getObject()->Label.getValue());
// Don't duplicate if the part label IS the edited object
if (editLabel != partLabel) {
crumbs << editLabel;
}
crumbs << QStringLiteral("[editing]");
}
}
}
if (crumbs.isEmpty()) {
crumbs << ctx.label;
}
return crumbs;
}
QStringList EditingContextResolver::buildBreadcrumbColors(const EditingContext& ctx) const
{
QStringList colors;
if (ctx.breadcrumb.size() <= 1) {
colors << ctx.color;
return colors;
}
// For multi-segment breadcrumbs:
// - First segments (parent) use the parent context color
// - Last segments (active edit) use the current context color
// e.g., Body (mauve) > Sketch001 (green) > [editing] (green)
// Determine parent color
QString parentColor = QLatin1String(CatppuccinMocha::Mauve); // default for Body
auto* partObj = getActivePartObject();
if (partObj) {
if (objectIsDerivedFrom(partObj, "Assembly::AssemblyObject")) {
parentColor = QLatin1String(CatppuccinMocha::Blue);
}
}
for (int i = 0; i < ctx.breadcrumb.size(); ++i) {
if (i == 0 && ctx.breadcrumb.size() > 1) {
colors << parentColor;
}
else {
colors << ctx.color;
}
}
return colors;
}
// ---------------------------------------------------------------------------
// Apply context → toolbar state changes
// ---------------------------------------------------------------------------
void EditingContextResolver::applyContext(const EditingContext& ctx)
{
if (ctx.id == d->current.id && ctx.toolbars == d->current.toolbars) {
return; // No change
}
auto* tbm = ToolBarManager::getInstance();
if (!tbm) {
return;
}
// Hide previously active context toolbars
if (!d->current.toolbars.isEmpty()) {
tbm->setState(d->current.toolbars, ToolBarManager::State::RestoreDefault);
}
// Show new context toolbars
if (!ctx.toolbars.isEmpty()) {
tbm->setState(ctx.toolbars, ToolBarManager::State::ForceAvailable);
}
d->current = ctx;
Q_EMIT contextChanged(ctx);
}
// ---------------------------------------------------------------------------
// Signal handlers
// ---------------------------------------------------------------------------
void EditingContextResolver::onInEdit(const ViewProviderDocumentObject& /*vp*/)
{
refresh();
}
void EditingContextResolver::onResetEdit(const ViewProviderDocumentObject& /*vp*/)
{
refresh();
}
void EditingContextResolver::onActiveDocument(const Document& /*doc*/)
{
refresh();
}
void EditingContextResolver::onActivateView(const MDIView* /*view*/)
{
refresh();
}
void EditingContextResolver::refresh()
{
applyContext(resolve());
}
#include "moc_EditingContext.cpp"

134
src/Gui/EditingContext.h Normal file
View File

@@ -0,0 +1,134 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of Kindred Create. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#ifndef GUI_EDITINGCONTEXT_H
#define GUI_EDITINGCONTEXT_H
#include <functional>
#include <QObject>
#include <QString>
#include <QStringList>
#include <FCGlobal.h>
namespace Gui
{
class Document;
class MDIView;
class ViewProviderDocumentObject;
/// Snapshot of the resolved editing context.
struct GuiExport EditingContext
{
QString id; // e.g. "sketcher.edit", "partdesign.body"
QString label; // e.g. "Sketcher: Sketch001"
QString color; // Catppuccin Mocha hex, e.g. "#a6e3a1"
QStringList toolbars; // toolbar names to ForceAvailable
QStringList breadcrumb; // e.g. ["Body", "Sketch001", "[editing]"]
QStringList breadcrumbColors; // per-segment color hex values
};
/// Definition used when registering a context.
struct GuiExport ContextDefinition
{
QString id;
QString labelTemplate; // supports {name} placeholder
QString color;
QStringList toolbars;
int priority = 0; // higher = checked first during resolve
/// Return true if this context matches the current application state.
std::function<bool()> match;
};
/// Definition for an overlay context (additive, not exclusive).
struct GuiExport OverlayDefinition
{
QString id;
QStringList toolbars;
/// Return true if the overlay should be active.
std::function<bool()> match;
};
/**
* Singleton that resolves the current editing context from application state
* and drives toolbar visibility and the breadcrumb display.
*
* Built-in contexts (PartDesign, Sketcher, Assembly, Spreadsheet) are registered
* at construction. Third-party modules register via registerContext() /
* registerOverlay() from Python or C++.
*/
class GuiExport EditingContextResolver: public QObject
{
Q_OBJECT
public:
static EditingContextResolver* instance();
static void destruct();
void registerContext(const ContextDefinition& def);
void unregisterContext(const QString& id);
void registerOverlay(const OverlayDefinition& def);
void unregisterOverlay(const QString& id);
/// Inject additional commands into a context's toolbar.
void injectCommands(const QString& contextId, const QString& toolbarName, const QStringList& commands);
/// Force a re-resolve (e.g. after workbench switch).
void refresh();
EditingContext currentContext() const;
Q_SIGNALS:
void contextChanged(const EditingContext& ctx);
private:
EditingContextResolver();
~EditingContextResolver() override;
EditingContext resolve() const;
void applyContext(const EditingContext& ctx);
QStringList buildBreadcrumb(const EditingContext& ctx) const;
QStringList buildBreadcrumbColors(const EditingContext& ctx) const;
// Signal handlers
void onInEdit(const ViewProviderDocumentObject& vp);
void onResetEdit(const ViewProviderDocumentObject& vp);
void onActiveDocument(const Document& doc);
void onActivateView(const MDIView* view);
void registerBuiltinContexts();
struct Private;
Private* d;
static EditingContextResolver* _instance;
};
} // namespace Gui
#endif // GUI_EDITINGCONTEXT_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

@@ -107,7 +107,7 @@
<FCInt Name="MinimumOnScreenTime" Value="3" />
</FCParamGroup>
<FCParamGroup Name="General">
<FCText Name="AutoloadModule">ZToolsWorkbench</FCText>
<FCText Name="AutoloadModule">PartDesignWorkbench</FCText>
</FCParamGroup>
<FCParamGroup Name="MainWindow">
<FCText Name="StyleSheet">KindredCreate.qss</FCText>

View File

@@ -227,7 +227,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)) {
@@ -235,6 +236,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) {
@@ -247,7 +259,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

@@ -578,7 +578,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"
@@ -588,7 +590,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"
@@ -608,15 +610,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

@@ -3576,7 +3576,7 @@ bool ViewProviderSketch::setEdit(int ModNum)
Gui::getMainWindow()->installEventFilter(listener);
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.
@@ -3726,7 +3726,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);

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"