diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 2cd30d8b8a..e56c3db80c 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -551,6 +551,13 @@ Application::Application(bool GUIenabled) {"AxisCross", SoFCPlacementIndicatorKit::AxisCross}, }); + Base::PyRegisterEnum(module, "IconPosition", { + {"TopLeft", Gui::BitmapFactoryInst::TopLeft}, + {"TopRight", Gui::BitmapFactoryInst::TopRight}, + {"BottomLeft", Gui::BitmapFactoryInst::BottomLeft}, + {"BottomRight", Gui::BitmapFactoryInst::BottomRight} + }); + CommandActionPy::init_type(); Base::Interpreter().addType(CommandActionPy::type_object(), module, "CommandAction"); diff --git a/src/Gui/ViewProviderFeaturePython.cpp b/src/Gui/ViewProviderFeaturePython.cpp index f92045caef..7edac104a5 100644 --- a/src/Gui/ViewProviderFeaturePython.cpp +++ b/src/Gui/ViewProviderFeaturePython.cpp @@ -158,6 +158,64 @@ QIcon ViewProviderFeaturePythonImp::getIcon() const return {}; } +std::map +ViewProviderFeaturePythonImp::getOverlayIcons() const +{ + std::map overlays; + _FC_PY_CALL_CHECK(getOverlayIcons, return overlays); + + Base::PyGILStateLocker lock; + try { + Py::Object ret(Base::pyCall(py_getOverlayIcons.ptr())); + if (ret.isNone()) { + return overlays; + } + + // Expect a dictionary (dict) from Python + if (!PyDict_Check(ret.ptr())) { + return overlays; + } + + Py::Dict dict(ret); + PyObject *key, *value; + Py_ssize_t pos = 0; + + // Iterate over the dictionary items + while (PyDict_Next(dict.ptr(), &pos, &key, &value)) { + // Key should be an integer (from the enum) + if (!PyLong_Check(key)) { + continue; + } + // Value should be a string + if (!PyUnicode_Check(value)) { + continue; + } + + long position_val = PyLong_AsLong(key); + // Basic validation for the enum range + if (position_val >= BitmapFactoryInst::TopLeft + && position_val <= BitmapFactoryInst::BottomRight) { + auto position = static_cast(position_val); + std::string iconName = Py::String(value).as_std_string("utf-8"); + if (!iconName.empty()) { + overlays[position] = iconName; + } + } + } + } + catch (Py::Exception&) { + if (PyErr_ExceptionMatches(PyExc_NotImplementedError)) { + PyErr_Clear(); + } + else { + Base::PyException e; + e.reportException(); + } + } + + return overlays; +} + bool ViewProviderFeaturePythonImp::claimChildren(std::vector &children) const { _FC_PY_CALL_CHECK(claimChildren,return(false)); diff --git a/src/Gui/ViewProviderFeaturePython.h b/src/Gui/ViewProviderFeaturePython.h index cc2737db8f..96a598c3c8 100644 --- a/src/Gui/ViewProviderFeaturePython.h +++ b/src/Gui/ViewProviderFeaturePython.h @@ -30,6 +30,7 @@ #include "ViewProviderGeometryObject.h" #include "Document.h" +#include "BitmapFactory.h" class SoSensor; class SoDragger; @@ -55,6 +56,8 @@ public: // Returns the icon QIcon getIcon() const; + // returns a map of position -> icon name. + std::map getOverlayIcons() const; bool claimChildren(std::vector&) const; ValueT useNewSelectionModel() const; void onSelectionChanged(const SelectionChanges&); @@ -138,6 +141,7 @@ private: #define FC_PY_VIEW_OBJECT \ FC_PY_ELEMENT(getIcon) \ + FC_PY_ELEMENT(getOverlayIcons) \ FC_PY_ELEMENT(claimChildren) \ FC_PY_ELEMENT(useNewSelectionModel) \ FC_PY_ELEMENT(getElementPicked) \ @@ -224,6 +228,29 @@ public: return icon; } + QIcon mergeColorfulOverlayIcons(const QIcon& orig) const override + { + QIcon currentIcon = orig; + + // Get the map of overlay names from the Python implementation + std::map overlayMap = imp->getOverlayIcons(); + + if (!overlayMap.empty()) { + // Use the static instance of BitmapFactory to perform the merge + for (const auto& [position, name] : overlayMap) { + static const QSize overlayIconSize { 10, 10 }; + QPixmap overlayPixmap = + Gui::BitmapFactory().pixmapFromSvg(name.c_str(), overlayIconSize); + if (!overlayPixmap.isNull()) { + currentIcon = + Gui::BitmapFactoryInst::mergePixmap(currentIcon, overlayPixmap, position); + } + } + } + + return ViewProviderT::mergeColorfulOverlayIcons(currentIcon); + } + std::vector claimChildren() const override { std::vector res; if(!imp->claimChildren(res)) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 6b47a6d70e..a705ed5e97 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -939,23 +939,17 @@ void AssemblyObject::removeUnconnectedJoints(std::vector& } // Filter out unconnected joints - joints.erase( - std::remove_if( - joints.begin(), - joints.end(), - [&](App::DocumentObject* joint) { - App::DocumentObject* obj1 = getMovingPartFromRef(this, joint, "Reference1"); - App::DocumentObject* obj2 = getMovingPartFromRef(this, joint, "Reference2"); - if (!isObjInSetOfObjRefs(obj1, connectedParts) - || !isObjInSetOfObjRefs(obj2, connectedParts)) { - Base::Console().warning( - "%s is unconnected to a grounded part so it is ignored.\n", - joint->getFullName()); - return true; // Remove joint if any connected object is not in connectedParts - } - return false; - }), - joints.end()); + joints.erase(std::remove_if(joints.begin(), + joints.end(), + [&](App::DocumentObject* joint) { + App::DocumentObject* obj1 = + getMovingPartFromRef(this, joint, "Reference1"); + App::DocumentObject* obj2 = + getMovingPartFromRef(this, joint, "Reference2"); + return (!isObjInSetOfObjRefs(obj1, connectedParts) + || !isObjInSetOfObjRefs(obj2, connectedParts)); + }), + joints.end()); } void AssemblyObject::traverseAndMarkConnectedParts(App::DocumentObject* currentObj, diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 20624ed245..c76c713927 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -212,6 +212,52 @@ bool ViewProviderAssembly::canDragObjectToTarget(App::DocumentObject* obj, return true; } +void ViewProviderAssembly::updateData(const App::Property* prop) +{ + auto* obj = static_cast(pcObject); + if (prop == &obj->Group) { + // Defer the icon update until the event loop is idle. + // This ensures the assembly has had a chance to recompute its + // connectivity state before we query it. + + // We can't capture the raw 'obj' pointer because it may be deleted + // by the time the timer fires. Instead, we capture the names of the + // document and the object, and look them up again. + if (!obj->getDocument()) { + return; // Should not happen, but a good safeguard + } + const std::string docName = obj->getDocument()->getName(); + const std::string objName = obj->getNameInDocument(); + + QTimer::singleShot(0, [docName, objName]() { + // Re-acquire the document and the object safely. + App::Document* doc = App::GetApplication().getDocument(docName.c_str()); + if (!doc) { + return; // Document was closed + } + + auto* pcObj = doc->getObject(objName.c_str()); + auto* obj = static_cast(pcObj); + + // Now we can safely check if the object still exists and is attached. + if (!obj || !obj->isAttachedToDocument()) { + return; + } + + std::vector joints = obj->getJoints(false); + for (auto* joint : joints) { + Gui::ViewProvider* jointVp = Gui::Application::Instance->getViewProvider(joint); + if (jointVp) { + jointVp->signalChangeIcon(); + } + } + }); + } + else { + Gui::ViewProviderPart::updateData(prop); + } +} + bool ViewProviderAssembly::setEdit(int mode) { if (mode == ViewProvider::Default) { diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 9e37913a57..6cb10070e5 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -107,6 +107,8 @@ public: bool onDelete(const std::vector& subNames) override; bool canDelete(App::DocumentObject* obj) const override; + void updateData(const App::Property*) override; + /** @name enter/exit edit mode */ //@{ bool setEdit(int ModNum) override; diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index e654f73142..cb8dc73440 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -573,6 +573,9 @@ class Joint: for obj in joint.InList: if obj.isDerivedFrom("Assembly::AssemblyObject"): return obj + elif obj.isDerivedFrom("Assembly::AssemblyLink"): + return self.getAssembly(obj) + return None def setJointType(self, joint, newType): @@ -946,6 +949,24 @@ class ViewProviderJoint: return ":/icons/Assembly_CreateJoint.svg" + def getOverlayIcons(self): + """ + Return a dictionary of overlay icons. + Keys are positions from Gui.IconPosition. + Values are the icon resource names. + """ + + overlays = {} + + assembly = self.app_obj.Proxy.getAssembly(self.app_obj) + # Assuming Reference1 corresponds to the first part link + if hasattr(self.app_obj, "Reference1"): + part = UtilsAssembly.getMovingPart(assembly, self.app_obj.Reference1) + if part is not None and not assembly.isPartConnected(part): + overlays[Gui.IconPosition.BottomLeft] = "Part_Detached" + + return overlays + def dumps(self): """When saving the document this object gets stored using Python's json module.\ Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\