Files
create/src/Gui/EditingContext.cpp
forbes 5883ac8a0d
Some checks failed
Build and Test / build (pull_request) Has been cancelled
fix(gui): resolve PartDesign toolbars and breadcrumb when editing body inside assembly
When double-clicking a PartDesign Body inside an Assembly, the editing
context resolver failed to show PartDesign toolbars or a proper
breadcrumb. Three root causes:

1. Priority preemption: assembly.edit (priority 90) always matched
   because the Assembly VP remained in edit mode, blocking all
   PartDesign contexts (priorities 30-40).

2. Wrong active-object key: PartDesign matchers only queried the
   "part" active object, but ViewProviderBody::toggleActiveBody()
   sets "part" to the containing App::Part (which is the Assembly
   itself). The Body is set under "pdbody", which was never queried.

3. Missing refresh trigger: ActiveObjectList::setObject() fires
   Document::signalActivatedViewProvider, but EditingContextResolver
   had no connection to it, so setActiveObject("pdbody", body) never
   triggered a context re-resolve.

Changes:
- Forward signalActivatedViewProvider from Gui::Document to
  Gui::Application (same pattern as signalInEdit/signalResetEdit)
- Connect EditingContextResolver to the new application-level signal
- Add getActivePdBodyObject() helper querying the "pdbody" key
- Add partdesign.in_assembly context (priority 95) that matches when
  a Body is active pdbody while an Assembly is in edit
- Update partdesign.body and partdesign.feature matchers to check
  pdbody before falling back to the part key
- Add Assembly > Body breadcrumb with Blue > Mauve coloring
- Update label resolution to prefer pdbody name
2026-02-21 09:43:51 -06:00

765 lines
25 KiB
C++

// 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"
#include "WorkbenchManager.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(); });
app.signalActivatedViewProvider.connect(
[this](const ViewProviderDocumentObject*, 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 active "pdbody" object from the active view
// ---------------------------------------------------------------------------
static App::DocumentObject* getActivePdBodyObject()
{
auto* guiDoc = Application::Instance->activeDocument();
if (!guiDoc) {
return nullptr;
}
auto* view = guiDoc->getActiveView();
if (!view) {
return nullptr;
}
return view->getActiveObject<App::DocumentObject*>("pdbody");
}
// ---------------------------------------------------------------------------
// 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()
{
// --- PartDesign body active inside an assembly (supersedes assembly.edit) ---
registerContext({
/*.id =*/QStringLiteral("partdesign.in_assembly"),
/*.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"),
QStringLiteral("Sketcher")},
/*.priority =*/95,
/*.match =*/
[]() {
auto* body = getActivePdBodyObject();
if (!body || !objectIsDerivedFrom(body, "PartDesign::Body")) {
return false;
}
// Only match when we're inside an assembly edit session
auto* doc = Application::Instance->activeDocument();
if (!doc) {
return false;
}
auto* vp = doc->getInEdit();
return vp && vpObjectIsDerivedFrom(vp, "Assembly::AssemblyObject");
},
});
// --- 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"),
QStringLiteral("Sketcher")},
/*.priority =*/40,
/*.match =*/
[]() {
auto* obj = getActivePdBodyObject();
if (!obj) {
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("Part Design Modeling Features"),
QStringLiteral("Part Design Dress-Up Features"),
QStringLiteral("Part Design Transformation Features"),
QStringLiteral("Sketcher")},
/*.priority =*/30,
/*.match =*/
[]() {
auto* obj = getActivePdBodyObject();
if (!obj) {
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"),
QStringLiteral("Assembly Joints"),
QStringLiteral("Assembly Management")},
/*.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");
},
});
// --- Workbench-level fallbacks (priority 20) ---
// Show basic workbench toolbars when the workbench is active but no
// specific editing context matches (e.g. no Body selected in PartDesign).
registerContext({
/*.id =*/QStringLiteral("partdesign.workbench"),
/*.labelTemplate =*/QStringLiteral("Part Design"),
/*.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"),
QStringLiteral("Sketcher")},
/*.priority =*/20,
/*.match =*/
[]() {
return WorkbenchManager::instance()->activeName() == "PartDesignWorkbench";
},
});
registerContext({
/*.id =*/QStringLiteral("sketcher.workbench"),
/*.labelTemplate =*/QStringLiteral("Sketcher"),
/*.color =*/QLatin1String(CatppuccinMocha::Green),
/*.toolbars =*/
{QStringLiteral("Sketcher"),
QStringLiteral("Sketcher Tools"),
QStringLiteral("Geometries"),
QStringLiteral("Constraints"),
QStringLiteral("B-Spline Tools"),
QStringLiteral("Visual Helpers")},
/*.priority =*/20,
/*.match =*/
[]() {
return WorkbenchManager::instance()->activeName() == "SketcherWorkbench";
},
});
registerContext({
/*.id =*/QStringLiteral("assembly.workbench"),
/*.labelTemplate =*/QStringLiteral("Assembly"),
/*.color =*/QLatin1String(CatppuccinMocha::Blue),
/*.toolbars =*/
{QStringLiteral("Assembly"),
QStringLiteral("Assembly Joints"),
QStringLiteral("Assembly Management")},
/*.priority =*/20,
/*.match =*/
[]() {
return WorkbenchManager::instance()->activeName() == "AssemblyWorkbench";
},
});
// --- 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()) {
// Try pdbody first for PartDesign contexts
auto* bodyObj = getActivePdBodyObject();
if (bodyObj) {
name = QString::fromUtf8(bodyObj->Label.getValue());
}
}
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;
}
// Assembly > Body breadcrumb for in-assembly part editing
if (ctx.id == QStringLiteral("partdesign.in_assembly")) {
auto* guiDoc = Application::Instance->activeDocument();
if (guiDoc) {
auto* vp = guiDoc->getInEdit();
if (vp) {
auto* vpd = dynamic_cast<ViewProviderDocumentObject*>(vp);
if (vpd && vpd->getObject()) {
crumbs << QString::fromUtf8(vpd->getObject()->Label.getValue());
}
}
}
auto* body = getActivePdBodyObject();
if (body) {
crumbs << QString::fromUtf8(body->Label.getValue());
}
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.id == QStringLiteral("partdesign.in_assembly")) {
for (int i = 0; i < ctx.breadcrumb.size(); ++i) {
colors << (i == 0 ? QLatin1String(CatppuccinMocha::Blue)
: QLatin1String(CatppuccinMocha::Mauve));
}
return 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"