// 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 #include #include #include #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 contexts; // sorted by descending priority QList overlays; EditingContext current; // Additional commands injected into context toolbars // Key: contextId -> toolbarName -> list of command names QMap> 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(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("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("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(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(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(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"