From ce8ba746020f410a8d548ad93a0552031e94e921 Mon Sep 17 00:00:00 2001 From: realthunder Date: Tue, 17 Jun 2025 01:47:47 +0200 Subject: [PATCH 01/34] Gui: Add a context menu to select obstructed items (from RT fork) As the title says, this is from RT's fork. I only adjusted it a little bit and removed pie selection which was previously in the implementation. To activate context menu - "G, G", then if it can't resolve edges vs faces, it adds QMenus on top to let user decide which one to select. Co-authored-by: tetektoza --- src/Gui/CommandView.cpp | 112 ++++++++++++++++++++++ src/Gui/Navigation/NavigationStyle.cpp | 63 ++++++++++++- src/Gui/Selection/SelectionView.cpp | 124 +++++++++++++++++++++++++ src/Gui/Selection/SelectionView.h | 40 ++++++++ src/Gui/Workbench.cpp | 3 +- 5 files changed, 340 insertions(+), 2 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 65e1502aa0..e54dc65c74 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -28,6 +28,7 @@ # include # include # include +# include # include # include # include @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +75,7 @@ #include "OverlayManager.h" #include "SceneInspector.h" #include "Selection.h" +#include "Selection/SelectionView.h" #include "SelectionObject.h" #include "SoFCOffscreenRenderer.h" #include "TextureMapping.h" @@ -3953,6 +3956,114 @@ bool StdCmdAlignToSelection::isActive() return getGuiApplication()->sendHasMsgToActiveView("AlignToSelection"); } +//=========================================================================== +// Std_PickGeometry +//=========================================================================== + +DEF_STD_CMD_A(StdCmdPickGeometry) + +StdCmdPickGeometry::StdCmdPickGeometry() + : Command("Std_PickGeometry") +{ + sGroup = "View"; + sMenuText = QT_TR_NOOP("Pick geometry"); + sToolTipText = QT_TR_NOOP("Pick hidden geometries under the mouse cursor in 3D view.\n" + "This command is supposed to be activated by keyboard shortcut."); + sWhatsThis = "Std_PickGeometry"; + sStatusTip = sToolTipText; + sAccel = "G, G"; + eType = NoTransaction | AlterSelection; +} + +void StdCmdPickGeometry::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + // Get the active view + auto view = Application::Instance->activeView(); + if (!view || !view->isDerivedFrom(View3DInventor::getClassTypeId())) { + return; + } + + auto view3d = static_cast(view); + auto viewer = view3d->getViewer(); + if (!viewer) { + return; + } + + // Get cursor position in the viewer + QPoint pos = QCursor::pos(); + QWidget* widget = viewer->getGLWidget(); + if (!widget) { + return; + } + + QPoint local = widget->mapFromGlobal(pos); + SbVec2s point; + point[0] = local.x(); + point[1] = widget->height() - local.y() - 1; + + // Use ray picking to get all objects under cursor + SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion()); + pickAction.setPoint(point); + pickAction.setRadius(viewer->getPickRadius()); + pickAction.setPickAll(true); // Get all objects under cursor + pickAction.apply(viewer->getSoRenderManager()->getSceneGraph()); + + const SoPickedPointList& pplist = pickAction.getPickedPointList(); + if (pplist.getLength() == 0) { + return; + } + + // Convert picked points to PickData list + std::vector selections; + + for (int i = 0; i < pplist.getLength(); ++i) { + SoPickedPoint* pp = pplist[i]; + if (!pp || !pp->getPath()) + continue; + + ViewProvider* vp = viewer->getViewProviderByPath(pp->getPath()); + if (!vp) + continue; + + // Cast to ViewProviderDocumentObject to get the object + auto vpDoc = dynamic_cast(vp); + if (!vpDoc) + continue; + + App::DocumentObject* obj = vpDoc->getObject(); + if (!obj) + continue; + + std::string elementName = vp->getElement(pp->getDetail()); + + // Create PickData with selection information + PickData pickData; + pickData.obj = obj; + pickData.element = elementName; + pickData.docName = obj->getDocument()->getName(); + pickData.objName = obj->getNameInDocument(); + pickData.subName = elementName; + + selections.push_back(pickData); + } + + if (selections.empty()) { + return; + } + + // Use SelectionMenu to display and handle the pick menu + SelectionMenu contextMenu(widget); + contextMenu.doPick(selections); +} + +bool StdCmdPickGeometry::isActive() +{ + auto view = qobject_cast(getMainWindow()->activeWindow()); + return view != nullptr; +} + //=========================================================================== // Instantiation //=========================================================================== @@ -3985,6 +4096,7 @@ void CreateViewStdCommands() rcCmdMgr.addCommand(new StdRecallWorkingView()); rcCmdMgr.addCommand(new StdCmdViewGroup()); rcCmdMgr.addCommand(new StdCmdAlignToSelection()); + rcCmdMgr.addCommand(new StdCmdPickGeometry()); rcCmdMgr.addCommand(new StdCmdViewExample1()); rcCmdMgr.addCommand(new StdCmdViewExample2()); diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 9a5761bb82..d70b3ceec1 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -48,6 +48,8 @@ #include "Navigation/NavigationStyle.h" #include "Navigation/NavigationStylePy.h" #include "Application.h" +#include "Command.h" +#include "Action.h" #include "Inventor/SoMouseWheelEvent.h" #include "MenuManager.h" #include "MouseSelection.h" @@ -1953,6 +1955,8 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) QAction *item = navMenuGroup->addAction(name); navMenu->addAction(item); item->setCheckable(true); + QByteArray data(style.first.getName()); + item->setData(data); if (const Base::Type item_style = style.first; item_style != this->getTypeId()) { auto triggeredFun = [this, item_style](){ @@ -1970,7 +1974,64 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) item->setChecked(true); } - contextMenu->popup(QCursor::pos()); + // Add Pick Geometry option if there are objects under cursor + bool separator = false; + auto posAction = !contextMenu->actions().empty() ? contextMenu->actions().front() : nullptr; + + // Get picked objects at position + SoRayPickAction rp(viewer->getSoRenderManager()->getViewportRegion()); + rp.setPoint(position); + rp.setRadius(viewer->getPickRadius()); + rp.setPickAll(true); + rp.apply(viewer->getSoRenderManager()->getSceneGraph()); + + const SoPickedPointList& pplist = rp.getPickedPointList(); + QAction *pickAction = nullptr; + + if (pplist.getLength() > 0) { + separator = true; + auto cmd = Application::Instance->commandManager().getCommandByName("Std_PickGeometry"); + if (cmd) { + pickAction = new QAction(cmd->getAction()->text(), contextMenu); + pickAction->setShortcut(cmd->getAction()->shortcut()); + } else { + pickAction = new QAction(QObject::tr("Pick geometry"), contextMenu); + } + if (posAction) { + contextMenu->insertAction(posAction, pickAction); + contextMenu->insertSeparator(posAction); + } else { + contextMenu->addAction(pickAction); + } + } + + if (separator && posAction) + contextMenu->insertSeparator(posAction); + + QAction* used = contextMenu->exec(QCursor::pos()); + if (used && navMenuGroup->actions().indexOf(used) >= 0 && used->isChecked()) { + QByteArray type = used->data().toByteArray(); + QWidget* widget = viewer->getWidget(); + while (widget && !widget->inherits("Gui::View3DInventor")) + widget = widget->parentWidget(); + if (widget) { + // this is the widget where the viewer is embedded + Base::Type style = Base::Type::fromName((const char*)type); + if (style != this->getTypeId()) { + QEvent* event = new NavigationStyleEvent(style); + QApplication::postEvent(widget, event); + } + } + return; + } + + if (pickAction && used == pickAction) { + // Execute the Pick Geometry command at this position + auto cmd = Application::Instance->commandManager().getCommandByName("Std_PickGeometry"); + if (cmd && cmd->isActive()) { + cmd->invoke(0); + } + } } PyObject* NavigationStyle::getPyObject() diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index c40a17fa1b..a28d420d95 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -42,6 +42,7 @@ #include "BitmapFactory.h" #include "Command.h" #include "Document.h" +#include "ViewProvider.h" FC_LOG_LEVEL_INIT("Selection", true, true, true) @@ -704,4 +705,127 @@ void SelectionView::onEnablePickList() /// @endcond +// SelectionMenu implementation +SelectionMenu::SelectionMenu(QWidget *parent) + : QMenu(parent) +{ +} + +struct ElementInfo { + QMenu *menu = nullptr; + QIcon icon; + std::vector indices; +}; + +struct SubMenuInfo { + QMenu *menu = nullptr; + // Map from sub-object label to map from object path to element info + std::map > items; +}; + +PickData SelectionMenu::doPick(const std::vector &sels) +{ + clear(); + + std::map> typeGroups; + + // Group selections by element type + for (int i = 0; i < (int)sels.size(); ++i) { + const auto &sel = sels[i]; + std::string elementType = "Other"; + + // Extract element type from element name + if (!sel.element.empty()) { + if (sel.element.find("Face") == 0) elementType = "Face"; + else if (sel.element.find("Edge") == 0) elementType = "Edge"; + else if (sel.element.find("Vertex") == 0) elementType = "Vertex"; + else if (sel.element.find("Wire") == 0) elementType = "Wire"; + else if (sel.element.find("Shell") == 0) elementType = "Shell"; + else if (sel.element.find("Solid") == 0) elementType = "Solid"; + } + + typeGroups[elementType].push_back(i); + } + + // Create menu structure + for (const auto &typeGroup : typeGroups) { + const std::string &typeName = typeGroup.first; + const std::vector &indices = typeGroup.second; + + if (indices.empty()) continue; + + QMenu *typeMenu = nullptr; + if (typeGroups.size() > 1) { + typeMenu = addMenu(QString::fromUtf8(typeName.c_str())); + } + + for (int idx : indices) { + const auto &sel = sels[idx]; + + QString text = QString::fromUtf8(sel.obj->Label.getValue()); + if (!sel.element.empty()) { + text += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")"); + } + + // Get icon from view provider + QIcon icon; + auto vp = Application::Instance->getViewProvider(sel.obj); + if (vp) { + icon = vp->getIcon(); + } + + QAction *action; + if (typeMenu) { + action = typeMenu->addAction(icon, text); + } else { + action = addAction(icon, text); + } + action->setData(idx); + } + } + + QAction* picked = this->exec(QCursor::pos()); + return onPicked(picked, sels); +} + +PickData SelectionMenu::onPicked(QAction *picked, const std::vector &sels) +{ + Gui::Selection().rmvPreselect(); + if (!picked) + return PickData{}; + + int index = picked->data().toInt(); + if (index >= 0 && index < (int)sels.size()) { + const auto &sel = sels[index]; + if (sel.obj) { + Gui::Selection().addSelection(sel.docName.c_str(), + sel.objName.c_str(), + sel.subName.c_str()); + } + return sel; + } + return PickData{}; +} + +void SelectionMenu::onHover(QAction *action) +{ + if (!action) + return; + + // For now, just clear preselection on hover + // Could be enhanced to preselect the hovered item + Gui::Selection().rmvPreselect(); +} + +bool SelectionMenu::eventFilter(QObject *obj, QEvent *event) +{ + return QMenu::eventFilter(obj, event); +} + +void SelectionMenu::leaveEvent(QEvent *e) +{ + Gui::Selection().rmvPreselect(); + QMenu::leaveEvent(e); +} + #include "moc_SelectionView.cpp" diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index 318d67d6bc..28389691b7 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -25,6 +25,8 @@ #include "DockWindow.h" #include "Selection.h" +#include +#include class QListWidget; @@ -112,6 +114,44 @@ private: }; } // namespace DockWnd + +// Simple selection data structure +struct PickData { + App::DocumentObject* obj; + std::string element; + std::string docName; + std::string objName; + std::string subName; +}; + +// Add SelectionMenu class outside the DockWnd namespace +class GuiExport SelectionMenu : public QMenu { + Q_OBJECT +public: + SelectionMenu(QWidget *parent=nullptr); + + /** Populate and show the menu for picking geometry elements. + * + * @param sels: a list of geometry element references + * @return Return the picked geometry reference + * + * The menu will be divided into submenus that are grouped by element type. + */ + PickData doPick(const std::vector &sels); + +public Q_SLOTS: + void onHover(QAction *); + +protected: + bool eventFilter(QObject *, QEvent *) override; + void leaveEvent(QEvent *e) override; + PickData onPicked(QAction *, const std::vector &sels); + +private: + QPointer activeMenu; + QPointer activeAction; +}; + } // namespace Gui #endif // GUI_DOCKWND_SELECTIONVIEW_H diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index bf44bf0523..c244079487 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -737,7 +737,8 @@ MenuItem* StdWorkbench::setupMenuBar() const << "Separator" << "Std_ProjectUtil" << "Std_DlgParameter" - << "Std_DlgCustomize"; + << "Std_DlgCustomize" + << "Std_PickGeometry"; // Macro auto macro = new MenuItem( menuBar ); From 5d49b4b1082b6b5d0ff9ca695fb4125bf88ef8b0 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 21 Jun 2025 10:20:13 +0200 Subject: [PATCH 02/34] Core: Respect highlighting for preselection too Co-authored-by: realthunder --- src/Gui/View3DInventorViewer.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index f96ac8c28a..d1542de411 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -840,11 +840,10 @@ void View3DInventorViewer::onSelectionChanged(const SelectionChanges & reason) } if(Reason.Type == SelectionChanges::RmvPreselect || - Reason.Type == SelectionChanges::RmvPreselectSignal) + Reason.Type == SelectionChanges::RmvPreselectSignal || + Reason.Type == SelectionChanges::SetPreselect) { - //Hint: do not create a tmp. instance of SelectionChanges - SelectionChanges selChanges(SelectionChanges::RmvPreselect); - SoFCPreselectionAction preselectionAction(selChanges); + SoFCPreselectionAction preselectionAction(Reason); preselectionAction.apply(pcViewProviderRoot); } else { SoFCSelectionAction selectionAction(Reason); From 59b234028fc09c0f17b0b67e41e2c7fd86a400d5 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 21 Jun 2025 10:43:09 +0200 Subject: [PATCH 03/34] Core: Add preselect on hovered menu items in PickGeometry tool Co-authored-by: realthunder --- src/Gui/Selection/SelectionView.cpp | 34 +++++++++++++++++++++++++---- src/Gui/Selection/SelectionView.h | 1 + 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index a28d420d95..46ef5a4493 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -707,8 +707,9 @@ void SelectionView::onEnablePickList() // SelectionMenu implementation SelectionMenu::SelectionMenu(QWidget *parent) - : QMenu(parent) + : QMenu(parent), currentSelections(nullptr) { + connect(this, &QMenu::hovered, this, &SelectionMenu::onHover); } struct ElementInfo { @@ -727,6 +728,9 @@ PickData SelectionMenu::doPick(const std::vector &sels) { clear(); + // store reference to selections for use in onHover + currentSelections = &sels; + std::map> typeGroups; // Group selections by element type @@ -777,6 +781,8 @@ PickData SelectionMenu::doPick(const std::vector &sels) QAction *action; if (typeMenu) { action = typeMenu->addAction(icon, text); + // Connect submenu hovered signals as well + connect(typeMenu, &QMenu::hovered, this, &SelectionMenu::onHover); } else { action = addAction(icon, text); } @@ -809,12 +815,32 @@ PickData SelectionMenu::onPicked(QAction *picked, const std::vector &s void SelectionMenu::onHover(QAction *action) { - if (!action) + if (!action || !currentSelections) return; - // For now, just clear preselection on hover - // Could be enhanced to preselect the hovered item + // Clear previous preselection Gui::Selection().rmvPreselect(); + + // Get the selection index from the action data + bool ok; + int index = action->data().toInt(&ok); + if (!ok || index < 0 || index >= (int)currentSelections->size()) + return; + + const auto &sel = (*currentSelections)[index]; + if (!sel.obj) + return; + + // extract just the element name (e.g., "Face1") from subName for preselection + std::string elementName = sel.element; + if (!elementName.empty()) { + // use TreeView as message source for menu hover + Gui::Selection().setPreselect(sel.docName.c_str(), + sel.objName.c_str(), + elementName.c_str(), + 0, 0, 0, + SelectionChanges::MsgSource::TreeView); + } } bool SelectionMenu::eventFilter(QObject *obj, QEvent *event) diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index 28389691b7..a5584a0cce 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -150,6 +150,7 @@ protected: private: QPointer activeMenu; QPointer activeAction; + const std::vector* currentSelections; }; } // namespace Gui From c856c9ea01c0aca0a7e4114a1d9b545ec41c0297 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Wed, 25 Jun 2025 11:28:25 +0200 Subject: [PATCH 04/34] Core/Gui: Render primitives on top of the scene in PickGeometry + added some better grouping for items, which are assigned per object right now. For example, if we exceed 10 items per object it gets an additional group. Co-authored-by: realthunder --- src/Gui/CommandView.cpp | 10 +- src/Gui/Inventor/So3DAnnotation.cpp | 65 +++- src/Gui/Inventor/So3DAnnotation.h | 21 +- src/Gui/Selection/Selection.cpp | 10 + src/Gui/Selection/Selection.h | 5 + src/Gui/Selection/SelectionView.cpp | 363 +++++++++++++++++---- src/Gui/Selection/SelectionView.h | 23 ++ src/Gui/Selection/SoFCUnifiedSelection.cpp | 55 +++- src/Gui/View3DInventorViewer.cpp | 10 +- src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 38 ++- src/Mod/Part/Gui/SoBrepEdgeSet.h | 8 + src/Mod/Part/Gui/SoBrepFaceSet.cpp | 51 ++- src/Mod/Part/Gui/SoBrepFaceSet.h | 7 + src/Mod/Part/Gui/SoBrepPointSet.cpp | 23 +- src/Mod/Part/Gui/SoBrepPointSet.h | 7 + src/Mod/Part/Gui/ViewProviderExt.cpp | 5 +- src/Mod/Part/Gui/ViewProviderExt.h | 4 + 17 files changed, 614 insertions(+), 91 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index e54dc65c74..6764e67c83 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -4036,7 +4036,15 @@ void StdCmdPickGeometry::activated(int iMsg) if (!obj) continue; + // Get element information - handle sub-objects like Assembly parts std::string elementName = vp->getElement(pp->getDetail()); + std::string subName; + + // Try to get more detailed sub-object information + bool hasSubObject = false; + if (vp->getElementPicked(pp, subName)) { + hasSubObject = true; + } // Create PickData with selection information PickData pickData; @@ -4044,7 +4052,7 @@ void StdCmdPickGeometry::activated(int iMsg) pickData.element = elementName; pickData.docName = obj->getDocument()->getName(); pickData.objName = obj->getNameInDocument(); - pickData.subName = elementName; + pickData.subName = hasSubObject ? subName : elementName; selections.push_back(pickData); } diff --git a/src/Gui/Inventor/So3DAnnotation.cpp b/src/Gui/Inventor/So3DAnnotation.cpp index 50ba2b10ad..2a2fb91811 100644 --- a/src/Gui/Inventor/So3DAnnotation.cpp +++ b/src/Gui/Inventor/So3DAnnotation.cpp @@ -29,17 +29,23 @@ #include #endif #include +#include #endif #include "So3DAnnotation.h" +#include using namespace Gui; SO_ELEMENT_SOURCE(SoDelayedAnnotationsElement); +bool SoDelayedAnnotationsElement::isProcessingDelayedPaths = false; + void SoDelayedAnnotationsElement::init(SoState* state) { SoElement::init(state); + priorityPaths.clear(); + paths.truncate(0); } void SoDelayedAnnotationsElement::initClass() @@ -49,21 +55,72 @@ void SoDelayedAnnotationsElement::initClass() SO_ENABLE(SoGLRenderAction, SoDelayedAnnotationsElement); } -void SoDelayedAnnotationsElement::addDelayedPath(SoState* state, SoPath* path) +void SoDelayedAnnotationsElement::addDelayedPath(SoState* state, SoPath* path, int priority) { auto elt = static_cast(state->getElementNoPush(classStackIndex)); + // add to priority-aware storage if priority has been specified + if (priority > 0) { + elt->priorityPaths.emplace_back(path, priority); + return; + } + elt->paths.append(path); } SoPathList SoDelayedAnnotationsElement::getDelayedPaths(SoState* state) { auto elt = static_cast(state->getElementNoPush(classStackIndex)); - auto copy = elt->paths; - + + // if we don't have priority paths, just return normal delayed paths + if (elt->priorityPaths.empty()) { + auto copy = elt->paths; + elt->paths.truncate(0); + return copy; + } + + // sort by priority (lower numbers render first) + std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(), + [](const PriorityPath& a, const PriorityPath& b) { + return a.priority < b.priority; + }); + + SoPathList sortedPaths; + for (const auto& priorityPath : elt->priorityPaths) { + sortedPaths.append(priorityPath.path); + } + + // Clear storage + elt->priorityPaths.clear(); elt->paths.truncate(0); + + return sortedPaths; +} - return copy; +void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state, SoGLRenderAction* action) +{ + auto elt = static_cast(state->getElementNoPush(classStackIndex)); + + if (elt->priorityPaths.empty()) return; + + std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(), + [](const PriorityPath& a, const PriorityPath& b) { + return a.priority < b.priority; + }); + + isProcessingDelayedPaths = true; + + for (const auto& priorityPath : elt->priorityPaths) { + SoPathList singlePath; + singlePath.append(priorityPath.path); + + action->apply(singlePath, TRUE); + } + + isProcessingDelayedPaths = false; + + elt->priorityPaths.clear(); + elt->paths.truncate(0); } SO_NODE_SOURCE(So3DAnnotation); diff --git a/src/Gui/Inventor/So3DAnnotation.h b/src/Gui/Inventor/So3DAnnotation.h index 0e2a62c42a..dbd29e5e4d 100644 --- a/src/Gui/Inventor/So3DAnnotation.h +++ b/src/Gui/Inventor/So3DAnnotation.h @@ -27,6 +27,7 @@ #include #include #include +#include namespace Gui { @@ -43,6 +44,15 @@ protected: SoDelayedAnnotationsElement& operator=(const SoDelayedAnnotationsElement& other) = default; SoDelayedAnnotationsElement& operator=(SoDelayedAnnotationsElement&& other) noexcept = default; + // internal structure to hold path with it's rendering + // priority (lower renders first) + struct PriorityPath { + SoPath* path; + int priority; + + PriorityPath(SoPath* p, int pr = 0) : path(p), priority(pr) {} + }; + public: SoDelayedAnnotationsElement(const SoDelayedAnnotationsElement& other) = delete; SoDelayedAnnotationsElement(SoDelayedAnnotationsElement&& other) noexcept = delete; @@ -51,8 +61,13 @@ public: static void initClass(); - static void addDelayedPath(SoState* state, SoPath* path); + static void addDelayedPath(SoState* state, SoPath* path, int priority = 0); + static SoPathList getDelayedPaths(SoState* state); + + static void processDelayedPathsWithPriority(SoState* state, SoGLRenderAction* action); + + static bool isProcessingDelayedPaths; SbBool matches([[maybe_unused]] const SoElement* element) const override { @@ -64,6 +79,10 @@ public: return nullptr; } +private: + // priority-aware paths + std::vector priorityPaths; + SoPathList paths; }; diff --git a/src/Gui/Selection/Selection.cpp b/src/Gui/Selection/Selection.cpp index baa7b87669..11d0c14082 100644 --- a/src/Gui/Selection/Selection.cpp +++ b/src/Gui/Selection/Selection.cpp @@ -2604,3 +2604,13 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje } PY_CATCH; } + +bool SelectionSingleton::pickGeometryActive = false; + +bool SelectionSingleton::isPickGeometryActive() { + return pickGeometryActive; +} + +void SelectionSingleton::setPickGeometryActive(bool active) { + pickGeometryActive = active; +} diff --git a/src/Gui/Selection/Selection.h b/src/Gui/Selection/Selection.h index f207474f26..2faf778350 100644 --- a/src/Gui/Selection/Selection.h +++ b/src/Gui/Selection/Selection.h @@ -291,6 +291,8 @@ private: class GuiExport SelectionSingleton : public Base::Subject { public: + static bool pickGeometryActive; + struct SelObj { const char* DocName; const char* FeatName; @@ -409,6 +411,9 @@ public: */ void setVisible(VisibleState visible); + static bool isPickGeometryActive(); + static void setPickGeometryActive(bool active); + /// signal on new object boost::signals2::signal signalSelectionChanged; diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 46ef5a4493..ad000310e3 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -31,11 +31,15 @@ #include #include #include +#include #endif #include #include +#include #include +#include +#include #include "SelectionView.h" #include "Application.h" @@ -727,75 +731,100 @@ struct SubMenuInfo { PickData SelectionMenu::doPick(const std::vector &sels) { clear(); + Gui::Selection().setPickGeometryActive(true); - // store reference to selections for use in onHover - currentSelections = &sels; + std::vector selsCopy = sels; + currentSelections = &selsCopy; - std::map> typeGroups; - - // Group selections by element type - for (int i = 0; i < (int)sels.size(); ++i) { - const auto &sel = sels[i]; - std::string elementType = "Other"; - - // Extract element type from element name - if (!sel.element.empty()) { - if (sel.element.find("Face") == 0) elementType = "Face"; - else if (sel.element.find("Edge") == 0) elementType = "Edge"; - else if (sel.element.find("Vertex") == 0) elementType = "Vertex"; - else if (sel.element.find("Wire") == 0) elementType = "Wire"; - else if (sel.element.find("Shell") == 0) elementType = "Shell"; - else if (sel.element.find("Solid") == 0) elementType = "Solid"; - } - - typeGroups[elementType].push_back(i); - } - - // Create menu structure - for (const auto &typeGroup : typeGroups) { - const std::string &typeName = typeGroup.first; - const std::vector &indices = typeGroup.second; - - if (indices.empty()) continue; - - QMenu *typeMenu = nullptr; - if (typeGroups.size() > 1) { - typeMenu = addMenu(QString::fromUtf8(typeName.c_str())); - } - - for (int idx : indices) { - const auto &sel = sels[idx]; - - QString text = QString::fromUtf8(sel.obj->Label.getValue()); - if (!sel.element.empty()) { - text += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")"); - } - - // Get icon from view provider - QIcon icon; - auto vp = Application::Instance->getViewProvider(sel.obj); - if (vp) { - icon = vp->getIcon(); - } - - QAction *action; - if (typeMenu) { - action = typeMenu->addAction(icon, text); - // Connect submenu hovered signals as well - connect(typeMenu, &QMenu::hovered, this, &SelectionMenu::onHover); - } else { - action = addAction(icon, text); - } - action->setData(idx); - } - } + std::map menus; + processSelections(selsCopy, menus); + buildMenuStructure(menus, selsCopy); QAction* picked = this->exec(QCursor::pos()); - return onPicked(picked, sels); + return onPicked(picked, selsCopy); +} + +void SelectionMenu::processSelections(std::vector &selections, std::map &menus) +{ + std::map icons; + std::set createdElementTypes; + std::set processedItems; + + for (int i = 0; i < (int)selections.size(); ++i) { + const auto &sel = selections[i]; + + App::DocumentObject* sobj = getSubObject(sel); + std::string elementType = extractElementType(sel); + std::string objKey = createObjectKey(sel); + std::string itemId = elementType + "|" + std::string(sobj->Label.getValue()) + "|" + sel.subName; + + if (processedItems.find(itemId) != processedItems.end()) { + continue; + } + processedItems.insert(itemId); + + QIcon icon = getOrCreateIcon(sobj, icons); + + auto &elementInfo = menus[elementType].items[sobj->Label.getValue()][objKey]; + elementInfo.icon = icon; + elementInfo.indices.push_back(i); + + addGeoFeatureTypes(sobj, menus, createdElementTypes); + addWholeObjectSelection(sel, sobj, selections, menus, icon); + } +} + +void SelectionMenu::buildMenuStructure(std::map &menus, const std::vector &selections) +{ + std::vector preferredOrder = {"Object", "Solid", "Face", "Edge", "Vertex", "Wire", "Shell", "Compound", "CompSolid"}; + std::vector::iterator> menuArray; + menuArray.reserve(menus.size()); + + for (const auto& category : preferredOrder) { + auto it = menus.find(category); + if (it != menus.end()) { + menuArray.push_back(it); + } + } + + for (auto it = menus.begin(); it != menus.end(); ++it) { + if (std::find(preferredOrder.begin(), preferredOrder.end(), it->first) == preferredOrder.end()) { + menuArray.push_back(it); + } + } + + for (auto it : menuArray) { + auto &v = *it; + auto &info = v.second; + + if (info.items.empty()) { + continue; + } + + info.menu = addMenu(QString::fromUtf8(v.first.c_str())); + bool groupMenu = shouldGroupMenu(info); + + for (auto &vv : info.items) { + const std::string &label = vv.first; + + for (auto &vvv : vv.second) { + auto &elementInfo = vvv.second; + + if (!groupMenu) { + createFlatMenu(elementInfo, info.menu, label, v.first, selections); + } else { + createGroupedMenu(elementInfo, info.menu, label, v.first, selections); + } + } + } + } } PickData SelectionMenu::onPicked(QAction *picked, const std::vector &sels) { + // Clear the PickGeometry active flag when menu is done + Gui::Selection().setPickGeometryActive(false); + Gui::Selection().rmvPreselect(); if (!picked) return PickData{}; @@ -831,13 +860,14 @@ void SelectionMenu::onHover(QAction *action) if (!sel.obj) return; - // extract just the element name (e.g., "Face1") from subName for preselection - std::string elementName = sel.element; - if (!elementName.empty()) { - // use TreeView as message source for menu hover + // For hover preselection, use the whole object path like solids do + // This provides consistent behavior and better highlighting for assembly elements + if (!sel.subName.empty()) { + // Always use the full original path for consistent behavior + // This matches how solids behave and provides better highlighting Gui::Selection().setPreselect(sel.docName.c_str(), sel.objName.c_str(), - elementName.c_str(), + sel.subName.c_str(), 0, 0, 0, SelectionChanges::MsgSource::TreeView); } @@ -854,4 +884,205 @@ void SelectionMenu::leaveEvent(QEvent *e) QMenu::leaveEvent(e); } +App::DocumentObject* SelectionMenu::getSubObject(const PickData &sel) +{ + App::DocumentObject* sobj = sel.obj; + if (!sel.subName.empty()) { + sobj = sel.obj->getSubObject(sel.subName.c_str()); + if (!sobj) { + sobj = sel.obj; + } + } + return sobj; +} + +std::string SelectionMenu::extractElementType(const PickData &sel) +{ + std::string actualElement; + + if (!sel.element.empty()) { + actualElement = sel.element; + } else if (!sel.subName.empty()) { + const char *elementName = Data::findElementName(sel.subName.c_str()); + if (elementName && elementName[0]) { + actualElement = elementName; + } + } + + if (!actualElement.empty()) { + std::size_t pos = actualElement.find_first_of("0123456789"); + if (pos != std::string::npos) { + return actualElement.substr(0, pos); + } + return actualElement; + } + + return "Other"; +} + +std::string SelectionMenu::createObjectKey(const PickData &sel) +{ + std::string objKey = std::string(sel.objName); + if (!sel.subName.empty()) { + std::string subNameNoElement = sel.subName; + const char *elementName = Data::findElementName(sel.subName.c_str()); + if (elementName && elementName[0]) { + std::string elementStr = elementName; + std::size_t elementPos = subNameNoElement.rfind(elementStr); + if (elementPos != std::string::npos) { + subNameNoElement = subNameNoElement.substr(0, elementPos); + } + } + objKey += "." + subNameNoElement; + } + return objKey; +} + +QIcon SelectionMenu::getOrCreateIcon(App::DocumentObject* sobj, std::map &icons) +{ + auto &icon = icons[sobj]; + if (icon.isNull()) { + auto vp = Application::Instance->getViewProvider(sobj); + if (vp) + icon = vp->getIcon(); + } + return icon; +} + +void SelectionMenu::addGeoFeatureTypes(App::DocumentObject* sobj, std::map &menus, std::set &createdTypes) +{ + auto geoFeature = dynamic_cast(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector types = geoFeature->getElementTypes(true); + for (const char* type : types) { + if (type && type[0] && createdTypes.find(type) == createdTypes.end()) { + menus[type]; + createdTypes.insert(type); + } + } + } +} + +void SelectionMenu::addWholeObjectSelection(const PickData &sel, App::DocumentObject* sobj, + std::vector &selections, std::map &menus, const QIcon &icon) +{ + if (sel.subName.empty()) return; + + std::string actualElement = extractElementType(sel) != "Other" ? sel.element : ""; + if (actualElement.empty() && !sel.subName.empty()) { + const char *elementName = Data::findElementName(sel.subName.c_str()); + if (elementName) actualElement = elementName; + } + if (actualElement.empty()) return; + + bool shouldAdd = false; + if (sobj && sobj != sel.obj) { + std::string typeName = sobj->getTypeId().getName(); + if (typeName == "App::Part" || typeName == "PartDesign::Body") { + shouldAdd = true; + } else { + auto geoFeature = dynamic_cast(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector types = geoFeature->getElementTypes(true); + if (types.size() > 1) { + shouldAdd = true; + } + } + } + } + + if (shouldAdd) { + std::string subNameStr = sel.subName; + std::size_t lastDot = subNameStr.find_last_of('.'); + if (lastDot != std::string::npos && lastDot > 0) { + std::size_t prevDot = subNameStr.find_last_of('.', lastDot - 1); + std::string subObjName; + if (prevDot != std::string::npos) { + subObjName = subNameStr.substr(prevDot + 1, lastDot - prevDot - 1); + } else { + subObjName = subNameStr.substr(0, lastDot); + } + + if (!subObjName.empty()) { + std::string wholeObjKey = std::string(sel.objName) + "." + subObjName + "."; + auto &objItems = menus["Object"].items[sobj->Label.getValue()]; + if (objItems.find(wholeObjKey) == objItems.end()) { + PickData wholeObjSel = sel; + wholeObjSel.subName = subObjName + "."; + wholeObjSel.element = ""; + + selections.push_back(wholeObjSel); + + auto &wholeObjInfo = objItems[wholeObjKey]; + wholeObjInfo.icon = icon; + wholeObjInfo.indices.push_back(selections.size() - 1); + } + } + } + } +} + +bool SelectionMenu::shouldGroupMenu(const SubMenuInfo &info) +{ + if (info.items.size() > 20) { + return true; + } + + std::size_t objCount = 0; + std::size_t count = 0; + for (auto &vv : info.items) { + objCount += vv.second.size(); + for (auto &vvv : vv.second) + count += vvv.second.indices.size(); + if (count > 5 && objCount > 1) { + return true; + } + } + return false; +} + +void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label, + const std::string &elementType, const std::vector &selections) +{ + for (int idx : elementInfo.indices) { + const auto &sel = selections[idx]; + QString text = QString::fromUtf8(label.c_str()); + if (!sel.element.empty()) { + text += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")"); + } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { + text += QString::fromLatin1(" (Whole Object)"); + } + QAction *action = parentMenu->addAction(elementInfo.icon, text); + action->setData(idx); + connect(action, &QAction::hovered, this, [this, action]() { + onHover(action); + }); + } +} + +void SelectionMenu::createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label, + const std::string &elementType, const std::vector &selections) +{ + if (!elementInfo.menu) { + elementInfo.menu = parentMenu->addMenu(elementInfo.icon, QString::fromUtf8(label.c_str())); + } + + for (int idx : elementInfo.indices) { + const auto &sel = selections[idx]; + QString text; + if (!sel.element.empty()) { + text = QString::fromUtf8(sel.element.c_str()); + } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { + text = QString::fromLatin1("Whole Object"); + } else { + text = QString::fromUtf8(sel.subName.c_str()); + } + QAction *action = elementInfo.menu->addAction(text); + action->setData(idx); + connect(action, &QAction::hovered, this, [this, action]() { + onHover(action); + }); + } +} + #include "moc_SelectionView.cpp" diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index a5584a0cce..e08c700027 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -27,6 +27,10 @@ #include "Selection.h" #include #include +#include +#include +#include +#include class QListWidget; @@ -38,6 +42,9 @@ namespace App { class DocumentObject; } +struct ElementInfo; +struct SubMenuInfo; + namespace Gui { namespace DockWnd { @@ -148,6 +155,22 @@ protected: PickData onPicked(QAction *, const std::vector &sels); private: + void processSelections(std::vector &selections, std::map &menus); + void buildMenuStructure(std::map &menus, const std::vector &selections); + + App::DocumentObject* getSubObject(const PickData &sel); + std::string extractElementType(const PickData &sel); + std::string createObjectKey(const PickData &sel); + QIcon getOrCreateIcon(App::DocumentObject* sobj, std::map &icons); + void addGeoFeatureTypes(App::DocumentObject* sobj, std::map &menus, std::set &createdTypes); + void addWholeObjectSelection(const PickData &sel, App::DocumentObject* sobj, std::vector &selections, + std::map &menus, const QIcon &icon); + bool shouldGroupMenu(const SubMenuInfo &info); + void createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label, + const std::string &elementType, const std::vector &selections); + void createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label, + const std::string &elementType, const std::vector &selections); + QPointer activeMenu; QPointer activeAction; const std::vector* currentSelections; diff --git a/src/Gui/Selection/SoFCUnifiedSelection.cpp b/src/Gui/Selection/SoFCUnifiedSelection.cpp index 88c6aa619d..def60c048e 100644 --- a/src/Gui/Selection/SoFCUnifiedSelection.cpp +++ b/src/Gui/Selection/SoFCUnifiedSelection.cpp @@ -367,20 +367,51 @@ void SoFCUnifiedSelection::doAction(SoAction *action) App::Document* doc = App::GetApplication().getDocument(preselectAction->SelChange.pDocName); App::DocumentObject* obj = doc->getObject(preselectAction->SelChange.pObjectName); ViewProvider*vp = Application::Instance->getViewProvider(obj); - SoDetail* detail = vp->getDetail(preselectAction->SelChange.pSubName); + + // use getDetailPath() like selection does, instead of just getDetail() + SoDetail* detail = nullptr; + detailPath->truncate(0); + auto subName = preselectAction->SelChange.pSubName; + + SoFullPath* pathToHighlight = nullptr; + if (vp && vp->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId()) && + (useNewSelection.getValue() || vp->useNewSelectionModel()) && vp->isSelectable()) { + + // get proper detail path for sub-objects (like Assembly parts) + if (!subName || !subName[0] || vp->getDetailPath(subName, detailPath, true, detail)) { + if (detailPath->getLength()) { + pathToHighlight = detailPath; + } else { + // fallback to ViewProvider root if no specific path + pathToHighlight = static_cast(new SoPath(2)); + pathToHighlight->ref(); + pathToHighlight->append(vp->getRoot()); + } + } + } else { + detail = vp->getDetail(subName); + pathToHighlight = static_cast(new SoPath(2)); + pathToHighlight->ref(); + pathToHighlight->append(vp->getRoot()); + } - SoHighlightElementAction highlightAction; - highlightAction.setHighlighted(true); - highlightAction.setColor(this->colorHighlight.getValue()); - highlightAction.setElement(detail); - highlightAction.apply(vp->getRoot()); + if (pathToHighlight) { + SoHighlightElementAction highlightAction; + highlightAction.setHighlighted(true); + highlightAction.setColor(this->colorHighlight.getValue()); + highlightAction.setElement(detail); + highlightAction.apply(pathToHighlight); + + currentHighlightPath = static_cast(pathToHighlight->copy()); + currentHighlightPath->ref(); + + // clean up temporary path if we created one + if (pathToHighlight != detailPath) { + pathToHighlight->unref(); + } + } + delete detail; - - SoSearchAction sa; - sa.setNode(vp->getRoot()); - sa.apply(vp->getRoot()); - currentHighlightPath = static_cast(sa.getPath()->copy()); - currentHighlightPath->ref(); } if (useNewSelection.getValue()) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index d1542de411..aaf6c5c66d 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -2473,7 +2473,15 @@ void View3DInventorViewer::renderScene() So3DAnnotation::render = true; glClear(GL_DEPTH_BUFFER_BIT); - glra->apply(SoDelayedAnnotationsElement::getDelayedPaths(state)); + + // process delayed paths with priority support + if (Gui::Selection().isPickGeometryActive()) { + Gui::SoDelayedAnnotationsElement::processDelayedPathsWithPriority(state, glra); + } else { + // standard processing for normal delayed annotations + glra->apply(Gui::SoDelayedAnnotationsElement::getDelayedPaths(state)); + } + So3DAnnotation::render = false; } catch (const Base::MemoryException&) { diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 934ae99343..7a1a6f4d3b 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -44,10 +44,18 @@ # include # include # include +# include +# include #endif #include +#include +#include #include "SoBrepEdgeSet.h" +#include "SoBrepFaceSet.h" +#include "ViewProviderExt.h" + +#include using namespace PartGui; @@ -79,6 +87,20 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext(this,selContext,ctx2); if(ctx2 && ctx2->selectionIndex.empty()) return; + + + if (Gui::Selection() + .isPickGeometryActive() && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths + && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive())) { + // if we are using pickgeometry - add this to delayed paths with priority + // as we want to get this rendered on top of everything + viewProvider->setFaceHighlightActive(true); + Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), + action->getCurPath()->copy(), + 200); + return; + } + if(selContext2->checkGlobal(ctx)) { if(selContext2->isSelectAll()) { selContext2->sl.clear(); @@ -132,9 +154,23 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) } if(ctx2 && !ctx2->selectionIndex.empty()) renderSelection(action,ctx2,false); - else + else if (Gui::Selection().isPickGeometryActive() + && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive()) + && Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { + state->push(); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(false); + glDisable(GL_DEPTH_TEST); + inherited::GLRender(action); + state->pop(); + } + else { + inherited::GLRender(action); + } + // Workaround for #0000433 //#if !defined(FC_OS_WIN32) if(!action->isRenderingDelayedPaths()) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.h b/src/Mod/Part/Gui/SoBrepEdgeSet.h index c63674af0d..a341c027dd 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.h +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.h @@ -36,6 +36,8 @@ class SoTextureCoordinateBundle; namespace PartGui { +class ViewProviderPartExt; + class PartGuiExport SoBrepEdgeSet : public SoIndexedLineSet { using inherited = SoIndexedLineSet; @@ -44,6 +46,8 @@ class PartGuiExport SoBrepEdgeSet : public SoIndexedLineSet { public: static void initClass(); SoBrepEdgeSet(); + + void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; } protected: ~SoBrepEdgeSet() override = default; @@ -68,11 +72,15 @@ private: void renderSelection(SoGLRenderAction *action, SelContextPtr, bool push=true); bool validIndexes(const SoCoordinateElement*, const std::vector&) const; + private: SelContextPtr selContext; SelContextPtr selContext2; Gui::SoFCSelectionCounter selCounter; uint32_t packedColor{0}; + + // backreference to viewprovider that owns this node + ViewProviderPartExt* viewProvider = nullptr; }; } // namespace PartGui diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.cpp b/src/Mod/Part/Gui/SoBrepFaceSet.cpp index e8ada70029..b1a006e03c 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.cpp +++ b/src/Mod/Part/Gui/SoBrepFaceSet.cpp @@ -73,8 +73,12 @@ #include #include #include +#include #include "SoBrepFaceSet.h" +#include "ViewProviderExt.h" +#include "SoBrepEdgeSet.h" + using namespace PartGui; @@ -188,6 +192,9 @@ void SoBrepFaceSet::doAction(SoAction* action) ctx->highlightIndex = -1; touch(); } + if (viewProvider) { + viewProvider->setFaceHighlightActive(false); + } return; } @@ -204,6 +211,9 @@ void SoBrepFaceSet::doAction(SoAction* action) ctx->highlightIndex = -1; touch(); } + if (viewProvider) { + viewProvider->setFaceHighlightActive(false); + } }else { int index = static_cast(detail)->getPartIndex(); SelContextPtr ctx = Gui::SoFCSelectionRoot::getActionContext(action,this,selContext); @@ -521,6 +531,40 @@ void SoBrepFaceSet::GLRender(SoGLRenderAction *action) auto state = action->getState(); selCounter.checkRenderCache(state); + + // for the tool add this node to delayed paths as we want to render it on top of the scene + if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted() + && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 + && ctx->highlightIndex < partIndex.getNum()) { + + if (!Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { + if (viewProvider) { + viewProvider->setFaceHighlightActive(true); + } + + const SoPath* currentPath = action->getCurPath(); + Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), + currentPath->copy(), + 100); + return; + } else { + // during priority delayed paths processing: + // render base faces normally first, then render highlight on top + + inherited::GLRender(action); + + state->push(); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(false); + glDisable(GL_DEPTH_TEST); + + renderHighlight(action, ctx); + + state->pop(); + return; + } + } // override material binding to PER_PART_INDEX to achieve // preselection/selection with transparency @@ -730,7 +774,7 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext singleColor = ctx?-1:1; } - bool partialRender = ctx2 && !ctx2->isSelectAll(); + bool partialRender = (ctx2 && !ctx2->isSelectAll()); if(singleColor>0 && !partialRender) { //optimization for single color non-partial rendering @@ -772,7 +816,8 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext packedColors.push_back(ctx->highlightColor.getPackedValue(trans0)); matIndex[ctx->highlightIndex] = packedColors.size()-1; } - }else{ + } + else{ if(partialRender) { packedColors.push_back(SbColor(1.0,1.0,1.0).getPackedValue(1.0)); matIndex.resize(partIndex.getNum(),0); @@ -848,7 +893,7 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext SoLazyElement::setPacked(state, this, packedColors.size(), packedColors.data(), hasTransparency); SoTextureEnabledElement::set(state,this,false); - if(hasTransparency && action->isRenderingDelayedPaths()) { + if (hasTransparency && action->isRenderingDelayedPaths()) { // rendering delayed paths means we are doing annotation (e.g. // always on top rendering). To render transparency correctly in // this case, we shall use openGL transparency blend. Override diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.h b/src/Mod/Part/Gui/SoBrepFaceSet.h index bab9e49414..17e33c7aed 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.h +++ b/src/Mod/Part/Gui/SoBrepFaceSet.h @@ -38,6 +38,8 @@ class SoTextureCoordinateBundle; namespace PartGui { +class ViewProviderPartExt; + /** * First some words to the history and the reason why we have this class: * In older FreeCAD versions we had an own Inventor node for each sub-element of a shape with its own highlight node. @@ -79,6 +81,8 @@ class PartGuiExport SoBrepFaceSet : public SoIndexedFaceSet { public: static void initClass(); SoBrepFaceSet(); + + void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; } SoMFInt32 partIndex; @@ -154,6 +158,9 @@ private: // Define some VBO pointer for the current mesh class VBO; std::unique_ptr pimpl; + + // backreference to viewprovider that owns this node + ViewProviderPartExt* viewProvider = nullptr; }; } // namespace PartGui diff --git a/src/Mod/Part/Gui/SoBrepPointSet.cpp b/src/Mod/Part/Gui/SoBrepPointSet.cpp index d674011d3f..9dec883f45 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.cpp +++ b/src/Mod/Part/Gui/SoBrepPointSet.cpp @@ -44,7 +44,9 @@ #endif #include +#include +#include "ViewProviderExt.h" #include "SoBrepPointSet.h" @@ -81,6 +83,18 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action) return; if(selContext2->checkGlobal(ctx)) ctx = selContext2; + + // for pickgeometry, add this node to delayed path if it is highlighted and render it on + // top of everything else (highest priority) + if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted() + && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 + && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { + viewProvider->setFaceHighlightActive(true); + Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), + action->getCurPath()->copy(), + 300); + return; + } if(ctx && ctx->highlightIndex == std::numeric_limits::max()) { if(ctx->selectionIndex.empty() || ctx->isSelectAll()) { @@ -121,8 +135,15 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action) } if(ctx2 && !ctx2->selectionIndex.empty()) renderSelection(action,ctx2,false); - else + else if (Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { + glPushAttrib(GL_DEPTH_BUFFER_BIT); + glDepthFunc(GL_ALWAYS); inherited::GLRender(action); + glPopAttrib(); + } + else { + inherited::GLRender(action); + } // Workaround for #0000433 //#if !defined(FC_OS_WIN32) diff --git a/src/Mod/Part/Gui/SoBrepPointSet.h b/src/Mod/Part/Gui/SoBrepPointSet.h index 1dd87e8c55..d9ff7f9f2a 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.h +++ b/src/Mod/Part/Gui/SoBrepPointSet.h @@ -36,6 +36,8 @@ class SoTextureCoordinateBundle; namespace PartGui { +class ViewProviderPartExt; + class PartGuiExport SoBrepPointSet : public SoPointSet { using inherited = SoPointSet; @@ -44,6 +46,8 @@ class PartGuiExport SoBrepPointSet : public SoPointSet { public: static void initClass(); SoBrepPointSet(); + + void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; } protected: ~SoBrepPointSet() override = default; @@ -64,6 +68,9 @@ private: SelContextPtr selContext2; Gui::SoFCSelectionCounter selCounter; uint32_t packedColor{0}; + + // backreference to viewprovider that owns this node + ViewProviderPartExt* viewProvider = nullptr; }; } // namespace PartGui diff --git a/src/Mod/Part/Gui/ViewProviderExt.cpp b/src/Mod/Part/Gui/ViewProviderExt.cpp index 3de944e65f..c2c326b808 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.cpp +++ b/src/Mod/Part/Gui/ViewProviderExt.cpp @@ -191,6 +191,7 @@ ViewProviderPartExt::ViewProviderPartExt() coords = new SoCoordinate3(); coords->ref(); faceset = new SoBrepFaceSet(); + faceset->setViewProvider(this); faceset->ref(); norm = new SoNormal; norm->ref(); @@ -198,8 +199,10 @@ ViewProviderPartExt::ViewProviderPartExt() normb->value = SoNormalBinding::PER_VERTEX_INDEXED; normb->ref(); lineset = new SoBrepEdgeSet(); + lineset->setViewProvider(this); lineset->ref(); nodeset = new SoBrepPointSet(); + nodeset->setViewProvider(this); nodeset->ref(); pcFaceBind = new SoMaterialBinding(); @@ -447,9 +450,9 @@ void ViewProviderPartExt::attach(App::DocumentObject *pcFeat) // normal viewing with edges and points pcNormalRoot->addChild(pcPointsRoot); - pcNormalRoot->addChild(wireframe); pcNormalRoot->addChild(offset); pcNormalRoot->addChild(pcFlatRoot); + pcNormalRoot->addChild(wireframe); // just faces with no edges or points pcFlatRoot->addChild(pShapeHints); diff --git a/src/Mod/Part/Gui/ViewProviderExt.h b/src/Mod/Part/Gui/ViewProviderExt.h index 1f6950d50f..b5d62dfcdb 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.h +++ b/src/Mod/Part/Gui/ViewProviderExt.h @@ -156,6 +156,9 @@ public: bool allowOverride(const App::DocumentObject &) const override; + void setFaceHighlightActive(bool active) { faceHighlightActive = active; } + bool isFaceHighlightActive() const { return faceHighlightActive; } + /** @name Edit methods */ //@{ void setupContextMenu(QMenu*, QObject*, const char*) override; @@ -213,6 +216,7 @@ protected: bool VisualTouched; bool NormalsFromUV; + bool faceHighlightActive = false; private: Gui::ViewProviderFaceTexture texture; From 3c5131e0512022f5049aac8d0b460d293d413e4a Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:01:25 +0200 Subject: [PATCH 05/34] Gui: Add support for toggling PickGeometry with long-press LMB Co-authored-by: realthunder --- src/Gui/View3DInventorViewer.cpp | 47 +++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index aaf6c5c66d..2f185fe7b2 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -106,6 +106,7 @@ #include "View3DInventorViewer.h" #include "Application.h" +#include "Command.h" #include "Document.h" #include "GLPainter.h" #include "Inventor/SoAxisCrossKit.h" @@ -231,9 +232,27 @@ while the progress bar is running. class Gui::ViewerEventFilter : public QObject { public: - ViewerEventFilter() = default; + ViewerEventFilter() : longPressTimer(new QTimer(this)) { + longPressTimer->setSingleShot(true); + longPressTimer->setInterval(1000); // after 1s of LMB press on viewport it gets toggled + connect(longPressTimer, &QTimer::timeout, [this]() { + if (currentViewer) { + triggerPickGeometry(); + } + }); + } ~ViewerEventFilter() override = default; +private: + void triggerPickGeometry() { + Gui::Command::runCommand(Gui::Command::Gui, "Gui.runCommand('Std_PickGeometry')"); + } + + QTimer* longPressTimer; + QPoint pressPosition; + View3DInventorViewer* currentViewer = nullptr; + +public: bool eventFilter(QObject* obj, QEvent* event) override { // Bug #0000607: Some mice also support horizontal scrolling which however might // lead to some unwanted zooming when pressing the MMB for panning. @@ -276,6 +295,32 @@ public: } } + if (event->type() == QEvent::MouseButtonPress) { + auto mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + currentViewer = static_cast(obj); + pressPosition = mouseEvent->pos(); + longPressTimer->start(); + } + } + else if (event->type() == QEvent::MouseButtonRelease) { + auto mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + longPressTimer->stop(); + currentViewer = nullptr; + } + } + else if (event->type() == QEvent::MouseMove) { + if (longPressTimer->isActive()) { + auto mouseEvent = static_cast(event); + // cancel long press if mouse moved too far (more than 5 pixels) + if ((mouseEvent->pos() - pressPosition).manhattanLength() > 5) { + longPressTimer->stop(); + currentViewer = nullptr; + } + } + } + return false; } }; From b780cf923f22ecafbebd2e9736c5823625cde3a5 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:26:11 +0200 Subject: [PATCH 06/34] Part/Gui: Rename Pick Geometry tool to Clarify Selection Co-authored-by: realthunder --- src/Gui/CommandView.cpp | 22 +++++++++++----------- src/Gui/Navigation/NavigationStyle.cpp | 10 +++++----- src/Gui/Selection/Selection.cpp | 10 +++++----- src/Gui/Selection/Selection.h | 6 +++--- src/Gui/Selection/SelectionView.cpp | 6 +++--- src/Gui/View3DInventorViewer.cpp | 8 ++++---- src/Gui/Workbench.cpp | 2 +- src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 6 +++--- src/Mod/Part/Gui/SoBrepFaceSet.cpp | 2 +- src/Mod/Part/Gui/SoBrepPointSet.cpp | 4 ++-- 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 6764e67c83..2e3d14e2ba 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3957,25 +3957,25 @@ bool StdCmdAlignToSelection::isActive() } //=========================================================================== -// Std_PickGeometry +// Std_ClarifySelection //=========================================================================== -DEF_STD_CMD_A(StdCmdPickGeometry) +DEF_STD_CMD_A(StdCmdClarifySelection) -StdCmdPickGeometry::StdCmdPickGeometry() - : Command("Std_PickGeometry") +StdCmdClarifySelection::StdCmdClarifySelection() + : Command("Std_ClarifySelection") { sGroup = "View"; - sMenuText = QT_TR_NOOP("Pick geometry"); - sToolTipText = QT_TR_NOOP("Pick hidden geometries under the mouse cursor in 3D view.\n" - "This command is supposed to be activated by keyboard shortcut."); - sWhatsThis = "Std_PickGeometry"; + sMenuText = QT_TR_NOOP("Clarify Selection"); + sToolTipText = QT_TR_NOOP("Displays a context menu at the mouse cursor to select overlapping " + "or obstructed geometry in the 3D view.\n"); + sWhatsThis = "Std_ClarifySelection"; sStatusTip = sToolTipText; sAccel = "G, G"; eType = NoTransaction | AlterSelection; } -void StdCmdPickGeometry::activated(int iMsg) +void StdCmdClarifySelection::activated(int iMsg) { Q_UNUSED(iMsg); @@ -4066,7 +4066,7 @@ void StdCmdPickGeometry::activated(int iMsg) contextMenu.doPick(selections); } -bool StdCmdPickGeometry::isActive() +bool StdCmdClarifySelection::isActive() { auto view = qobject_cast(getMainWindow()->activeWindow()); return view != nullptr; @@ -4104,7 +4104,7 @@ void CreateViewStdCommands() rcCmdMgr.addCommand(new StdRecallWorkingView()); rcCmdMgr.addCommand(new StdCmdViewGroup()); rcCmdMgr.addCommand(new StdCmdAlignToSelection()); - rcCmdMgr.addCommand(new StdCmdPickGeometry()); + rcCmdMgr.addCommand(new StdCmdClarifySelection()); rcCmdMgr.addCommand(new StdCmdViewExample1()); rcCmdMgr.addCommand(new StdCmdViewExample2()); diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index d70b3ceec1..a5b853d9d4 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -1974,7 +1974,7 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) item->setChecked(true); } - // Add Pick Geometry option if there are objects under cursor + // Add Clarify Selection option if there are objects under cursor bool separator = false; auto posAction = !contextMenu->actions().empty() ? contextMenu->actions().front() : nullptr; @@ -1990,12 +1990,12 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) if (pplist.getLength() > 0) { separator = true; - auto cmd = Application::Instance->commandManager().getCommandByName("Std_PickGeometry"); + auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection"); if (cmd) { pickAction = new QAction(cmd->getAction()->text(), contextMenu); pickAction->setShortcut(cmd->getAction()->shortcut()); } else { - pickAction = new QAction(QObject::tr("Pick geometry"), contextMenu); + pickAction = new QAction(QObject::tr("Clarify Selection"), contextMenu); } if (posAction) { contextMenu->insertAction(posAction, pickAction); @@ -2026,8 +2026,8 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) } if (pickAction && used == pickAction) { - // Execute the Pick Geometry command at this position - auto cmd = Application::Instance->commandManager().getCommandByName("Std_PickGeometry"); + // Execute the Clarify Selection command at this position + auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection"); if (cmd && cmd->isActive()) { cmd->invoke(0); } diff --git a/src/Gui/Selection/Selection.cpp b/src/Gui/Selection/Selection.cpp index 11d0c14082..566ca86242 100644 --- a/src/Gui/Selection/Selection.cpp +++ b/src/Gui/Selection/Selection.cpp @@ -2605,12 +2605,12 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje PY_CATCH; } -bool SelectionSingleton::pickGeometryActive = false; +bool SelectionSingleton::clarifySelectionActive = false; -bool SelectionSingleton::isPickGeometryActive() { - return pickGeometryActive; +bool SelectionSingleton::isClarifySelectionActive() { + return clarifySelectionActive; } -void SelectionSingleton::setPickGeometryActive(bool active) { - pickGeometryActive = active; +void SelectionSingleton::setClarifySelectionActive(bool active) { + clarifySelectionActive = active; } diff --git a/src/Gui/Selection/Selection.h b/src/Gui/Selection/Selection.h index 2faf778350..aa0eb9576c 100644 --- a/src/Gui/Selection/Selection.h +++ b/src/Gui/Selection/Selection.h @@ -291,7 +291,7 @@ private: class GuiExport SelectionSingleton : public Base::Subject { public: - static bool pickGeometryActive; + static bool clarifySelectionActive; struct SelObj { const char* DocName; @@ -411,8 +411,8 @@ public: */ void setVisible(VisibleState visible); - static bool isPickGeometryActive(); - static void setPickGeometryActive(bool active); + static bool isClarifySelectionActive(); + static void setClarifySelectionActive(bool active); /// signal on new object boost::signals2::signal signalSelectionChanged; diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index ad000310e3..d6b0ee8bf7 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -731,7 +731,7 @@ struct SubMenuInfo { PickData SelectionMenu::doPick(const std::vector &sels) { clear(); - Gui::Selection().setPickGeometryActive(true); + Gui::Selection().setClarifySelectionActive(true); std::vector selsCopy = sels; currentSelections = &selsCopy; @@ -822,8 +822,8 @@ void SelectionMenu::buildMenuStructure(std::map &menus PickData SelectionMenu::onPicked(QAction *picked, const std::vector &sels) { - // Clear the PickGeometry active flag when menu is done - Gui::Selection().setPickGeometryActive(false); + // Clear the ClarifySelection active flag when menu is done + Gui::Selection().setClarifySelectionActive(false); Gui::Selection().rmvPreselect(); if (!picked) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index 2f185fe7b2..f6ac7667ac 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -237,15 +237,15 @@ public: longPressTimer->setInterval(1000); // after 1s of LMB press on viewport it gets toggled connect(longPressTimer, &QTimer::timeout, [this]() { if (currentViewer) { - triggerPickGeometry(); + triggerClarifySelection(); } }); } ~ViewerEventFilter() override = default; private: - void triggerPickGeometry() { - Gui::Command::runCommand(Gui::Command::Gui, "Gui.runCommand('Std_PickGeometry')"); + void triggerClarifySelection() { + Gui::Command::runCommand(Gui::Command::Gui, "Gui.runCommand('Std_ClarifySelection')"); } QTimer* longPressTimer; @@ -2520,7 +2520,7 @@ void View3DInventorViewer::renderScene() glClear(GL_DEPTH_BUFFER_BIT); // process delayed paths with priority support - if (Gui::Selection().isPickGeometryActive()) { + if (Gui::Selection().isClarifySelectionActive()) { Gui::SoDelayedAnnotationsElement::processDelayedPathsWithPriority(state, glra); } else { // standard processing for normal delayed annotations diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index c244079487..41e53d9aa5 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -738,7 +738,7 @@ MenuItem* StdWorkbench::setupMenuBar() const << "Std_ProjectUtil" << "Std_DlgParameter" << "Std_DlgCustomize" - << "Std_PickGeometry"; + << "Std_ClarifySelection"; // Macro auto macro = new MenuItem( menuBar ); diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 7a1a6f4d3b..0f2345d2eb 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -90,9 +90,9 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) if (Gui::Selection() - .isPickGeometryActive() && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths + .isClarifySelectionActive() && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive())) { - // if we are using pickgeometry - add this to delayed paths with priority + // if we are using clarifyselection - add this to delayed paths with priority // as we want to get this rendered on top of everything viewProvider->setFaceHighlightActive(true); Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), @@ -154,7 +154,7 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) } if(ctx2 && !ctx2->selectionIndex.empty()) renderSelection(action,ctx2,false); - else if (Gui::Selection().isPickGeometryActive() + else if (Gui::Selection().isClarifySelectionActive() && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive()) && Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { state->push(); diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.cpp b/src/Mod/Part/Gui/SoBrepFaceSet.cpp index b1a006e03c..449bd9439a 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.cpp +++ b/src/Mod/Part/Gui/SoBrepFaceSet.cpp @@ -533,7 +533,7 @@ void SoBrepFaceSet::GLRender(SoGLRenderAction *action) selCounter.checkRenderCache(state); // for the tool add this node to delayed paths as we want to render it on top of the scene - if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted() + if (Gui::Selection().isClarifySelectionActive() && ctx && ctx->isHighlighted() && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 && ctx->highlightIndex < partIndex.getNum()) { diff --git a/src/Mod/Part/Gui/SoBrepPointSet.cpp b/src/Mod/Part/Gui/SoBrepPointSet.cpp index 9dec883f45..bfb028e8c9 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.cpp +++ b/src/Mod/Part/Gui/SoBrepPointSet.cpp @@ -84,9 +84,9 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action) if(selContext2->checkGlobal(ctx)) ctx = selContext2; - // for pickgeometry, add this node to delayed path if it is highlighted and render it on + // for clarifyselection, add this node to delayed path if it is highlighted and render it on // top of everything else (highest priority) - if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted() + if (Gui::Selection().isClarifySelectionActive() && ctx && ctx->isHighlighted() && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { viewProvider->setFaceHighlightActive(true); From de8b9f2dbf9f565ea06d108c8269a251dee02282 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:33:11 +0200 Subject: [PATCH 07/34] Gui: Initialize SbVec2s point directly in CommandView --- src/Gui/CommandView.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 2e3d14e2ba..36b86e3eed 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3999,9 +3999,8 @@ void StdCmdClarifySelection::activated(int iMsg) } QPoint local = widget->mapFromGlobal(pos); - SbVec2s point; - point[0] = local.x(); - point[1] = widget->height() - local.y() - 1; + SbVec2s point(static_cast(local.x()), + static_cast(widget->height() - local.y() - 1)); // Use ray picking to get all objects under cursor SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion()); From 80aca3168404fc627e01b7f590244c38c43322c6 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:34:58 +0200 Subject: [PATCH 08/34] Gui: Use designated initializer in CommandView --- src/Gui/CommandView.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 36b86e3eed..8391f5def5 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -4046,12 +4046,11 @@ void StdCmdClarifySelection::activated(int iMsg) } // Create PickData with selection information - PickData pickData; - pickData.obj = obj; - pickData.element = elementName; - pickData.docName = obj->getDocument()->getName(); - pickData.objName = obj->getNameInDocument(); - pickData.subName = hasSubObject ? subName : elementName; + PickData pickData {.obj = obj, + .element = elementName, + .docName = obj->getDocument()->getName(), + .objName = obj->getNameInDocument(), + .subName = hasSubObject ? subName : elementName}; selections.push_back(pickData); } From 8c004fc98bd5ff117e1b1444f0620152531e1065 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:38:26 +0200 Subject: [PATCH 09/34] Gui: Use freecad_cast where possible in CommandView for ClarifySelection --- src/Gui/CommandView.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 8391f5def5..9726e2e6b2 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3980,12 +3980,11 @@ void StdCmdClarifySelection::activated(int iMsg) Q_UNUSED(iMsg); // Get the active view - auto view = Application::Instance->activeView(); - if (!view || !view->isDerivedFrom(View3DInventor::getClassTypeId())) { + auto view3d = freecad_cast(Application::Instance->activeView()); + if (!view3d) { return; } - auto view3d = static_cast(view); auto viewer = view3d->getViewer(); if (!viewer) { return; @@ -4027,7 +4026,7 @@ void StdCmdClarifySelection::activated(int iMsg) continue; // Cast to ViewProviderDocumentObject to get the object - auto vpDoc = dynamic_cast(vp); + auto vpDoc = freecad_cast(vp); if (!vpDoc) continue; From 178996a80aad1c484f8638235ad31fc26b2c7e3a Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:40:30 +0200 Subject: [PATCH 10/34] Gui: Check for nullptr in activeWindow in CommandView directly --- src/Gui/CommandView.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 9726e2e6b2..2895397c51 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -4065,8 +4065,7 @@ void StdCmdClarifySelection::activated(int iMsg) bool StdCmdClarifySelection::isActive() { - auto view = qobject_cast(getMainWindow()->activeWindow()); - return view != nullptr; + return qobject_cast(getMainWindow()->activeWindow()) != nullptr; } //=========================================================================== From 85526db81aab60808db25104ee086e1ab3d57c84 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:46:05 +0200 Subject: [PATCH 11/34] Gui: Fix typo in SelectionView --- src/Gui/Selection/SelectionView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index d6b0ee8bf7..30d82cc699 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -725,7 +725,7 @@ struct ElementInfo { struct SubMenuInfo { QMenu *menu = nullptr; // Map from sub-object label to map from object path to element info - std::map > items; + std::map> items; }; PickData SelectionMenu::doPick(const std::vector &sels) From 93297670392e1f8fc644f956ba17367031dccf5d Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:48:03 +0200 Subject: [PATCH 12/34] Gui: Use QStringLiteral for selected element --- src/Gui/Selection/SelectionView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 30d82cc699..dad0f0c422 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -1048,7 +1048,7 @@ void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const auto &sel = selections[idx]; QString text = QString::fromUtf8(label.c_str()); if (!sel.element.empty()) { - text += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")"); + text += QStringLiteral(" (%1)").arg(QString::fromUtf8(sel.element.c_str())); } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { text += QString::fromLatin1(" (Whole Object)"); } From 873aab3b2a41a67e8c9d4b9c57e3ab156c1e7c23 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 21:53:33 +0200 Subject: [PATCH 13/34] Gui: Extract helper methods for navigation context menu --- src/Gui/Navigation/NavigationStyle.cpp | 49 ++++++++++++++++++-------- src/Gui/Navigation/NavigationStyle.h | 8 +++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index a5b853d9d4..94c9e6e6b5 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -1934,6 +1934,34 @@ SbBool NavigationStyle::isPopupMenuEnabled() const return this->menuenabled; } +bool NavigationStyle::isNavigationStyleAction(QAction* action, QActionGroup* navMenuGroup) const +{ + return action && navMenuGroup->actions().indexOf(action) >= 0 && action->isChecked(); +} + +QWidget* NavigationStyle::findView3DInventorWidget() const +{ + QWidget* widget = viewer->getWidget(); + while (widget && !widget->inherits("Gui::View3DInventor")) { + widget = widget->parentWidget(); + } + return widget; +} + +void NavigationStyle::applyNavigationStyleChange(QAction* selectedAction) +{ + QByteArray navigationStyleTypeName = selectedAction->data().toByteArray(); + QWidget* view3DWidget = findView3DInventorWidget(); + + if (view3DWidget) { + Base::Type newNavigationStyle = Base::Type::fromName(navigationStyleTypeName.constData()); + if (newNavigationStyle != this->getTypeId()) { + QEvent* navigationChangeEvent = new NavigationStyleEvent(newNavigationStyle); + QApplication::postEvent(view3DWidget, navigationChangeEvent); + } + } +} + void NavigationStyle::openPopupMenu(const SbVec2s& position) { Q_UNUSED(position); @@ -2008,24 +2036,15 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) if (separator && posAction) contextMenu->insertSeparator(posAction); - QAction* used = contextMenu->exec(QCursor::pos()); - if (used && navMenuGroup->actions().indexOf(used) >= 0 && used->isChecked()) { - QByteArray type = used->data().toByteArray(); - QWidget* widget = viewer->getWidget(); - while (widget && !widget->inherits("Gui::View3DInventor")) - widget = widget->parentWidget(); - if (widget) { - // this is the widget where the viewer is embedded - Base::Type style = Base::Type::fromName((const char*)type); - if (style != this->getTypeId()) { - QEvent* event = new NavigationStyleEvent(style); - QApplication::postEvent(widget, event); - } - } + QAction* selectedAction = contextMenu->exec(QCursor::pos()); + + // handle navigation style change if user selected a navigation style option + if (selectedAction && isNavigationStyleAction(selectedAction, navMenuGroup)) { + applyNavigationStyleChange(selectedAction); return; } - if (pickAction && used == pickAction) { + if (pickAction && selectedAction == pickAction) { // Execute the Clarify Selection command at this position auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection"); if (cmd && cmd->isActive()) { diff --git a/src/Gui/Navigation/NavigationStyle.h b/src/Gui/Navigation/NavigationStyle.h index 348bbea95c..ffa907d7e5 100644 --- a/src/Gui/Navigation/NavigationStyle.h +++ b/src/Gui/Navigation/NavigationStyle.h @@ -36,6 +36,7 @@ #include #include +#include #include #include #include @@ -239,6 +240,13 @@ protected: void syncWithEvent(const SoEvent * const ev); virtual void openPopupMenu(const SbVec2s& position); +private: + bool isNavigationStyleAction(QAction* action, QActionGroup* navMenuGroup) const; + QWidget* findView3DInventorWidget() const; + void applyNavigationStyleChange(QAction* selectedAction); + +protected: + void clearLog(); void addToLog(const SbVec2s pos, const SbTime time); From 31accb32dfe32efaf2a9b2e63cefb241dee15a4f Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 22:01:55 +0200 Subject: [PATCH 14/34] Gui: Add a comment explaining invoke(0) call in NavigationStyle --- src/Gui/Navigation/NavigationStyle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 94c9e6e6b5..322d924824 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -2048,7 +2048,7 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) // Execute the Clarify Selection command at this position auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection"); if (cmd && cmd->isActive()) { - cmd->invoke(0); + cmd->invoke(0); // required placeholder value - we don't use group command } } } From 2190d7dd3b446f75d62c30cea1e86e35f2e2ce50 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 22:07:20 +0200 Subject: [PATCH 15/34] Gui: Use copy for currentSelections in Clarify Selection tool --- src/Gui/Selection/SelectionView.cpp | 17 ++++++++--------- src/Gui/Selection/SelectionView.h | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index dad0f0c422..b225d80895 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -711,7 +711,7 @@ void SelectionView::onEnablePickList() // SelectionMenu implementation SelectionMenu::SelectionMenu(QWidget *parent) - : QMenu(parent), currentSelections(nullptr) + : QMenu(parent) { connect(this, &QMenu::hovered, this, &SelectionMenu::onHover); } @@ -733,15 +733,14 @@ PickData SelectionMenu::doPick(const std::vector &sels) clear(); Gui::Selection().setClarifySelectionActive(true); - std::vector selsCopy = sels; - currentSelections = &selsCopy; + currentSelections = sels; std::map menus; - processSelections(selsCopy, menus); - buildMenuStructure(menus, selsCopy); + processSelections(currentSelections, menus); + buildMenuStructure(menus, currentSelections); QAction* picked = this->exec(QCursor::pos()); - return onPicked(picked, selsCopy); + return onPicked(picked, currentSelections); } void SelectionMenu::processSelections(std::vector &selections, std::map &menus) @@ -844,7 +843,7 @@ PickData SelectionMenu::onPicked(QAction *picked, const std::vector &s void SelectionMenu::onHover(QAction *action) { - if (!action || !currentSelections) + if (!action || currentSelections.empty()) return; // Clear previous preselection @@ -853,10 +852,10 @@ void SelectionMenu::onHover(QAction *action) // Get the selection index from the action data bool ok; int index = action->data().toInt(&ok); - if (!ok || index < 0 || index >= (int)currentSelections->size()) + if (!ok || index < 0 || index >= (int)currentSelections.size()) return; - const auto &sel = (*currentSelections)[index]; + const auto &sel = currentSelections[index]; if (!sel.obj) return; diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index e08c700027..dfd365bb70 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -173,7 +173,7 @@ private: QPointer activeMenu; QPointer activeAction; - const std::vector* currentSelections; + std::vector currentSelections; }; } // namespace Gui From c73525ff6c6d32754e57b6a6e3b45a878c2aa6c7 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 22:10:01 +0200 Subject: [PATCH 16/34] Gui: Use variable assignment directly in if statement --- src/Gui/Navigation/NavigationStyle.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 322d924824..1706796a14 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -2018,8 +2018,8 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) if (pplist.getLength() > 0) { separator = true; - auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection"); - if (cmd) { + if (auto cmd = + Application::Instance->commandManager().getCommandByName("Std_ClarifySelection")) { pickAction = new QAction(cmd->getAction()->text(), contextMenu); pickAction->setShortcut(cmd->getAction()->shortcut()); } else { From 7d99d69fc71487ef5d626941c8c77a6297b325e2 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Tue, 2 Sep 2025 22:10:55 +0200 Subject: [PATCH 17/34] Gui: Use QByteArray directly in setData without intermediate variable --- src/Gui/Navigation/NavigationStyle.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 1706796a14..03c9447438 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -1983,8 +1983,7 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) QAction *item = navMenuGroup->addAction(name); navMenu->addAction(item); item->setCheckable(true); - QByteArray data(style.first.getName()); - item->setData(data); + item->setData(QByteArray(style.first.getName())); if (const Base::Type item_style = style.first; item_style != this->getTypeId()) { auto triggeredFun = [this, item_style](){ From 02962686f132e82907bc66c8d09957abc1b97275 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Thu, 4 Sep 2025 23:42:11 +0200 Subject: [PATCH 18/34] Gui: Merge standard delayed paths with delayed priority paths --- src/Gui/Inventor/So3DAnnotation.cpp | 35 ++++++++++------------------- src/Gui/Inventor/So3DAnnotation.h | 5 +---- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/Gui/Inventor/So3DAnnotation.cpp b/src/Gui/Inventor/So3DAnnotation.cpp index 2a2fb91811..87f54d86e9 100644 --- a/src/Gui/Inventor/So3DAnnotation.cpp +++ b/src/Gui/Inventor/So3DAnnotation.cpp @@ -44,8 +44,7 @@ bool SoDelayedAnnotationsElement::isProcessingDelayedPaths = false; void SoDelayedAnnotationsElement::init(SoState* state) { SoElement::init(state); - priorityPaths.clear(); - paths.truncate(0); + paths.clear(); } void SoDelayedAnnotationsElement::initClass() @@ -58,41 +57,32 @@ void SoDelayedAnnotationsElement::initClass() void SoDelayedAnnotationsElement::addDelayedPath(SoState* state, SoPath* path, int priority) { auto elt = static_cast(state->getElementNoPush(classStackIndex)); - - // add to priority-aware storage if priority has been specified - if (priority > 0) { - elt->priorityPaths.emplace_back(path, priority); - return; - } - elt->paths.append(path); + // add to unified storage with specified priority (default = 0) + elt->paths.emplace_back(path, priority); } SoPathList SoDelayedAnnotationsElement::getDelayedPaths(SoState* state) { auto elt = static_cast(state->getElementNoPush(classStackIndex)); - // if we don't have priority paths, just return normal delayed paths - if (elt->priorityPaths.empty()) { - auto copy = elt->paths; - elt->paths.truncate(0); - return copy; + if (elt->paths.empty()) { + return SoPathList(); } // sort by priority (lower numbers render first) - std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(), + std::stable_sort(elt->paths.begin(), elt->paths.end(), [](const PriorityPath& a, const PriorityPath& b) { return a.priority < b.priority; }); SoPathList sortedPaths; - for (const auto& priorityPath : elt->priorityPaths) { + for (const auto& priorityPath : elt->paths) { sortedPaths.append(priorityPath.path); } // Clear storage - elt->priorityPaths.clear(); - elt->paths.truncate(0); + elt->paths.clear(); return sortedPaths; } @@ -101,16 +91,16 @@ void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state { auto elt = static_cast(state->getElementNoPush(classStackIndex)); - if (elt->priorityPaths.empty()) return; + if (elt->paths.empty()) return; - std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(), + std::stable_sort(elt->paths.begin(), elt->paths.end(), [](const PriorityPath& a, const PriorityPath& b) { return a.priority < b.priority; }); isProcessingDelayedPaths = true; - for (const auto& priorityPath : elt->priorityPaths) { + for (const auto& priorityPath : elt->paths) { SoPathList singlePath; singlePath.append(priorityPath.path); @@ -119,8 +109,7 @@ void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state isProcessingDelayedPaths = false; - elt->priorityPaths.clear(); - elt->paths.truncate(0); + elt->paths.clear(); } SO_NODE_SOURCE(So3DAnnotation); diff --git a/src/Gui/Inventor/So3DAnnotation.h b/src/Gui/Inventor/So3DAnnotation.h index dbd29e5e4d..fee78372d4 100644 --- a/src/Gui/Inventor/So3DAnnotation.h +++ b/src/Gui/Inventor/So3DAnnotation.h @@ -80,10 +80,7 @@ public: } private: - // priority-aware paths - std::vector priorityPaths; - - SoPathList paths; + std::vector paths; }; /*! @brief 3D Annotation Node - Annotation with depth buffer From 6b3b3d230f6c0de6292d6c53b1ece5363ac92651 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 00:48:02 +0200 Subject: [PATCH 19/34] Gui: Convert Clarify Selection usage in SelectionSingleton to instance --- src/Gui/Selection/Selection.cpp | 2 -- src/Gui/Selection/Selection.h | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Gui/Selection/Selection.cpp b/src/Gui/Selection/Selection.cpp index 566ca86242..5bb307917b 100644 --- a/src/Gui/Selection/Selection.cpp +++ b/src/Gui/Selection/Selection.cpp @@ -2605,8 +2605,6 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje PY_CATCH; } -bool SelectionSingleton::clarifySelectionActive = false; - bool SelectionSingleton::isClarifySelectionActive() { return clarifySelectionActive; } diff --git a/src/Gui/Selection/Selection.h b/src/Gui/Selection/Selection.h index aa0eb9576c..70cb93f388 100644 --- a/src/Gui/Selection/Selection.h +++ b/src/Gui/Selection/Selection.h @@ -291,8 +291,6 @@ private: class GuiExport SelectionSingleton : public Base::Subject { public: - static bool clarifySelectionActive; - struct SelObj { const char* DocName; const char* FeatName; @@ -411,8 +409,8 @@ public: */ void setVisible(VisibleState visible); - static bool isClarifySelectionActive(); - static void setClarifySelectionActive(bool active); + bool isClarifySelectionActive(); + void setClarifySelectionActive(bool active); /// signal on new object boost::signals2::signal signalSelectionChanged; @@ -713,6 +711,7 @@ protected: int logDisabled = 0; bool logHasSelection = false; + bool clarifySelectionActive = false; SelectionStyle selectionStyle; }; From a19d6f0fee0916374a857814f9ad35401f5f76dd Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 00:49:24 +0200 Subject: [PATCH 20/34] Gui: Use assignment in if statement for menus iterator --- src/Gui/Selection/SelectionView.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index b225d80895..78c8cfbdd3 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -780,8 +780,7 @@ void SelectionMenu::buildMenuStructure(std::map &menus menuArray.reserve(menus.size()); for (const auto& category : preferredOrder) { - auto it = menus.find(category); - if (it != menus.end()) { + if (auto it = menus.find(category); it != menus.end()) { menuArray.push_back(it); } } From 9b4e21287bad8bbb04ec3293d4ba711ee78075e0 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 01:00:37 +0200 Subject: [PATCH 21/34] Gui: Use cleaner variable naming for menu items in SelectionView --- src/Gui/Selection/SelectionView.cpp | 33 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 78c8cfbdd3..b356645db4 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -791,27 +791,28 @@ void SelectionMenu::buildMenuStructure(std::map &menus } } - for (auto it : menuArray) { - auto &v = *it; - auto &info = v.second; + for (auto elementTypeIterator : menuArray) { + auto &elementTypeEntry = *elementTypeIterator; + auto &subMenuInfo = elementTypeEntry.second; + const std::string &elementType = elementTypeEntry.first; - if (info.items.empty()) { + if (subMenuInfo.items.empty()) { continue; } - info.menu = addMenu(QString::fromUtf8(v.first.c_str())); - bool groupMenu = shouldGroupMenu(info); + subMenuInfo.menu = addMenu(QString::fromUtf8(elementType.c_str())); + bool groupMenu = shouldGroupMenu(subMenuInfo); - for (auto &vv : info.items) { - const std::string &label = vv.first; + for (auto &objectLabelEntry : subMenuInfo.items) { + const std::string &objectLabel = objectLabelEntry.first; - for (auto &vvv : vv.second) { - auto &elementInfo = vvv.second; + for (auto &objectPathEntry : objectLabelEntry.second) { + auto &elementInfo = objectPathEntry.second; if (!groupMenu) { - createFlatMenu(elementInfo, info.menu, label, v.first, selections); + createFlatMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections); } else { - createGroupedMenu(elementInfo, info.menu, label, v.first, selections); + createGroupedMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections); } } } @@ -1028,10 +1029,10 @@ bool SelectionMenu::shouldGroupMenu(const SubMenuInfo &info) std::size_t objCount = 0; std::size_t count = 0; - for (auto &vv : info.items) { - objCount += vv.second.size(); - for (auto &vvv : vv.second) - count += vvv.second.indices.size(); + for (auto &objectLabelEntry : info.items) { + objCount += objectLabelEntry.second.size(); + for (auto &objectPathEntry : objectLabelEntry.second) + count += objectPathEntry.second.indices.size(); if (count > 5 && objCount > 1) { return true; } From 91a77e89bac9d28ea7dd3166fcc6a6ffe31fd92c Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 01:03:50 +0200 Subject: [PATCH 22/34] Gui: Replace usage of dynamic_cast with freecad_cast in SelectionView --- src/Gui/Selection/SelectionView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index b356645db4..1517a5d4a2 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -950,7 +950,7 @@ QIcon SelectionMenu::getOrCreateIcon(App::DocumentObject* sobj, std::map &menus, std::set &createdTypes) { - auto geoFeature = dynamic_cast(sobj->getLinkedObject(true)); + auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); if (geoFeature) { std::vector types = geoFeature->getElementTypes(true); for (const char* type : types) { @@ -980,7 +980,7 @@ void SelectionMenu::addWholeObjectSelection(const PickData &sel, App::DocumentOb if (typeName == "App::Part" || typeName == "PartDesign::Body") { shouldAdd = true; } else { - auto geoFeature = dynamic_cast(sobj->getLinkedObject(true)); + auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); if (geoFeature) { std::vector types = geoFeature->getElementTypes(true); if (types.size() > 1) { From 350eb6d3d4131e65686eed5d5b772bd3c99e23d0 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 01:41:51 +0200 Subject: [PATCH 23/34] Gui: Add variable names for grouping thresholds in SelectionView --- src/Gui/Selection/SelectionView.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 1517a5d4a2..eedf9fa925 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -1023,17 +1023,19 @@ void SelectionMenu::addWholeObjectSelection(const PickData &sel, App::DocumentOb bool SelectionMenu::shouldGroupMenu(const SubMenuInfo &info) { - if (info.items.size() > 20) { + constexpr std::size_t MAX_MENU_ITEMS_BEFORE_GROUPING = 20; + if (info.items.size() > MAX_MENU_ITEMS_BEFORE_GROUPING) { return true; } std::size_t objCount = 0; std::size_t count = 0; + constexpr std::size_t MAX_SELECTION_COUNT_BEFORE_GROUPING = 5; for (auto &objectLabelEntry : info.items) { objCount += objectLabelEntry.second.size(); for (auto &objectPathEntry : objectLabelEntry.second) count += objectPathEntry.second.indices.size(); - if (count > 5 && objCount > 1) { + if (count > MAX_SELECTION_COUNT_BEFORE_GROUPING && objCount > 1) { return true; } } From c67d9c529c24fb4cfcfee636cf74a0ddddf4fef4 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 01:52:16 +0200 Subject: [PATCH 24/34] Gui: Align to use translated strings in SelectionView --- src/Gui/Selection/SelectionView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index eedf9fa925..56f0221a80 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -1051,7 +1051,7 @@ void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, if (!sel.element.empty()) { text += QStringLiteral(" (%1)").arg(QString::fromUtf8(sel.element.c_str())); } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { - text += QString::fromLatin1(" (Whole Object)"); + text += QStringLiteral(" (%1)").arg(tr("Whole Object")); } QAction *action = parentMenu->addAction(elementInfo.icon, text); action->setData(idx); @@ -1074,7 +1074,7 @@ void SelectionMenu::createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMen if (!sel.element.empty()) { text = QString::fromUtf8(sel.element.c_str()); } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { - text = QString::fromLatin1("Whole Object"); + text = tr("Whole Object"); } else { text = QString::fromUtf8(sel.subName.c_str()); } From d3ce061d5e80c20f20d7fcb95d3608c7c6ad59c0 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 02:05:52 +0200 Subject: [PATCH 25/34] Gui: Allow long press timeout for Clarify Selection to be configurable --- src/Gui/View3DInventorViewer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index f6ac7667ac..1d9bcbbe61 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -234,7 +234,6 @@ class Gui::ViewerEventFilter : public QObject public: ViewerEventFilter() : longPressTimer(new QTimer(this)) { longPressTimer->setSingleShot(true); - longPressTimer->setInterval(1000); // after 1s of LMB press on viewport it gets toggled connect(longPressTimer, &QTimer::timeout, [this]() { if (currentViewer) { triggerClarifySelection(); @@ -300,6 +299,11 @@ public: if (mouseEvent->button() == Qt::LeftButton) { currentViewer = static_cast(obj); pressPosition = mouseEvent->pos(); + + int longPressTimeout = App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/View") + ->GetInt("LongPressTimeout", 1000); + longPressTimer->setInterval(longPressTimeout); longPressTimer->start(); } } From 233470155d22a0daee38a5182ffdea9f6732cb3d Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 02:09:40 +0200 Subject: [PATCH 26/34] Gui: Break up condition for Clarify Selection to separate variables --- src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 0f2345d2eb..88e1fedf23 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -89,16 +89,20 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) return; - if (Gui::Selection() - .isClarifySelectionActive() && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths - && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive())) { - // if we are using clarifyselection - add this to delayed paths with priority - // as we want to get this rendered on top of everything - viewProvider->setFaceHighlightActive(true); - Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), - action->getCurPath()->copy(), - 200); - return; + bool hasContextHighlight = ctx && !ctx->hl.empty(); + bool hasFaceHighlight = viewProvider->isFaceHighlightActive(); + bool hasAnyHighlight = hasContextHighlight || hasFaceHighlight; + + if (Gui::Selection().isClarifySelectionActive() + && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths + && hasAnyHighlight) { + // if we are using clarifyselection - add this to delayed paths with priority + // as we want to get this rendered on top of everything + viewProvider->setFaceHighlightActive(true); + Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), + action->getCurPath()->copy(), + 200); + return; } if(selContext2->checkGlobal(ctx)) { From 44a4825edf181306fab5472ef6551b811ae161a2 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 02:24:53 +0200 Subject: [PATCH 27/34] Gui: Add solid selection spport for top-lvl objects in pick menu --- src/Gui/Selection/SelectionView.cpp | 112 +++++++++++++++++----------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 56f0221a80..ddfedad372 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -859,17 +859,12 @@ void SelectionMenu::onHover(QAction *action) if (!sel.obj) return; - // For hover preselection, use the whole object path like solids do - // This provides consistent behavior and better highlighting for assembly elements - if (!sel.subName.empty()) { - // Always use the full original path for consistent behavior - // This matches how solids behave and provides better highlighting - Gui::Selection().setPreselect(sel.docName.c_str(), - sel.objName.c_str(), - sel.subName.c_str(), - 0, 0, 0, - SelectionChanges::MsgSource::TreeView); - } + // set preselection for both sub-objects and whole objects + Gui::Selection().setPreselect(sel.docName.c_str(), + sel.objName.c_str(), + !sel.subName.empty() ? sel.subName.c_str() : "", + 0, 0, 0, + SelectionChanges::MsgSource::TreeView); } bool SelectionMenu::eventFilter(QObject *obj, QEvent *event) @@ -975,47 +970,76 @@ void SelectionMenu::addWholeObjectSelection(const PickData &sel, App::DocumentOb if (actualElement.empty()) return; bool shouldAdd = false; - if (sobj && sobj != sel.obj) { - std::string typeName = sobj->getTypeId().getName(); - if (typeName == "App::Part" || typeName == "PartDesign::Body") { - shouldAdd = true; + if (sobj) { + if (sobj != sel.obj) { + // sub-objects + std::string typeName = sobj->getTypeId().getName(); + if (typeName == "App::Part" || typeName == "PartDesign::Body") { + shouldAdd = true; + } else { + auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector types = geoFeature->getElementTypes(true); + if (types.size() > 1) { + shouldAdd = true; + } + } + } } else { - auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); - if (geoFeature) { - std::vector types = geoFeature->getElementTypes(true); - if (types.size() > 1) { - shouldAdd = true; + // top-level objects (sobj == sel.obj) + // check if subName is just an element name + if (sel.subName.find('.') == std::string::npos) { + auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector types = geoFeature->getElementTypes(true); + if (!types.empty()) { + shouldAdd = true; + } } } } } if (shouldAdd) { - std::string subNameStr = sel.subName; - std::size_t lastDot = subNameStr.find_last_of('.'); - if (lastDot != std::string::npos && lastDot > 0) { - std::size_t prevDot = subNameStr.find_last_of('.', lastDot - 1); - std::string subObjName; - if (prevDot != std::string::npos) { - subObjName = subNameStr.substr(prevDot + 1, lastDot - prevDot - 1); - } else { - subObjName = subNameStr.substr(0, lastDot); - } - - if (!subObjName.empty()) { - std::string wholeObjKey = std::string(sel.objName) + "." + subObjName + "."; - auto &objItems = menus["Object"].items[sobj->Label.getValue()]; - if (objItems.find(wholeObjKey) == objItems.end()) { - PickData wholeObjSel = sel; - wholeObjSel.subName = subObjName + "."; - wholeObjSel.element = ""; - - selections.push_back(wholeObjSel); - - auto &wholeObjInfo = objItems[wholeObjKey]; - wholeObjInfo.icon = icon; - wholeObjInfo.indices.push_back(selections.size() - 1); + std::string wholeObjKey; + std::string wholeObjSubName; + + if (sobj != sel.obj) { + // sub-objects + std::string subNameStr = sel.subName; + std::size_t lastDot = subNameStr.find_last_of('.'); + if (lastDot != std::string::npos && lastDot > 0) { + std::size_t prevDot = subNameStr.find_last_of('.', lastDot - 1); + std::string subObjName; + if (prevDot != std::string::npos) { + subObjName = subNameStr.substr(prevDot + 1, lastDot - prevDot - 1); + } else { + subObjName = subNameStr.substr(0, lastDot); } + + if (!subObjName.empty()) { + wholeObjKey = std::string(sel.objName) + "." + subObjName + "."; + wholeObjSubName = subObjName + "."; + } + } + } else { + // top-level objects (sobj == sel.obj) + wholeObjKey = std::string(sel.objName) + "."; + wholeObjSubName = ""; // empty subName for top-level whole object + } + + if (!wholeObjKey.empty()) { + auto &objItems = menus["Object"].items[sobj->Label.getValue()]; + if (objItems.find(wholeObjKey) == objItems.end()) { + PickData wholeObjSel = sel; + wholeObjSel.subName = wholeObjSubName; + wholeObjSel.element = ""; + + selections.push_back(wholeObjSel); + + auto &wholeObjInfo = objItems[wholeObjKey]; + wholeObjInfo.icon = icon; + wholeObjInfo.indices.push_back(selections.size() - 1); } } } From 527f18adcdbfb642adebc6b995d02a102fc334f1 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 5 Sep 2025 02:28:59 +0200 Subject: [PATCH 28/34] Tests: Correct testcases after scene graph reordering in Clarify Select --- src/Mod/Import/TestImportGui.py | 4 ++-- src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 9 +++++---- src/Mod/Part/Gui/SoBrepFaceSet.cpp | 7 ++++--- src/Mod/Part/Gui/SoBrepPointSet.cpp | 12 ++++++++---- src/Mod/Part/parttests/ColorPerFaceTest.py | 16 ++++++++-------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Mod/Import/TestImportGui.py b/src/Mod/Import/TestImportGui.py index 9f2d6eb25a..8a0cfdb707 100644 --- a/src/Mod/Import/TestImportGui.py +++ b/src/Mod/Import/TestImportGui.py @@ -77,7 +77,7 @@ class ExportImportTest(unittest.TestCase): sa.apply(feature.ViewObject.RootNode) paths = sa.getPaths() - bind = paths.get(2).getTail() + bind = paths.get(1).getTail() self.assertEqual(bind.value.getValue(), bind.PER_PART) sa = coin.SoSearchAction() @@ -87,5 +87,5 @@ class ExportImportTest(unittest.TestCase): sa.apply(feature.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 6) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 88e1fedf23..14da9ed8e9 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -90,7 +90,7 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) bool hasContextHighlight = ctx && !ctx->hl.empty(); - bool hasFaceHighlight = viewProvider->isFaceHighlightActive(); + bool hasFaceHighlight = viewProvider && viewProvider->isFaceHighlightActive(); bool hasAnyHighlight = hasContextHighlight || hasFaceHighlight; if (Gui::Selection().isClarifySelectionActive() @@ -98,7 +98,9 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) && hasAnyHighlight) { // if we are using clarifyselection - add this to delayed paths with priority // as we want to get this rendered on top of everything - viewProvider->setFaceHighlightActive(true); + if (viewProvider) { + viewProvider->setFaceHighlightActive(true); + } Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), action->getCurPath()->copy(), 200); @@ -159,8 +161,7 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) if(ctx2 && !ctx2->selectionIndex.empty()) renderSelection(action,ctx2,false); else if (Gui::Selection().isClarifySelectionActive() - && ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive()) - && Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { + && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths && hasAnyHighlight) { state->push(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.cpp b/src/Mod/Part/Gui/SoBrepFaceSet.cpp index 449bd9439a..8ef68c572a 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.cpp +++ b/src/Mod/Part/Gui/SoBrepFaceSet.cpp @@ -532,10 +532,11 @@ void SoBrepFaceSet::GLRender(SoGLRenderAction *action) auto state = action->getState(); selCounter.checkRenderCache(state); + bool hasContextHighlight = ctx && ctx->isHighlighted() && !ctx->isHighlightAll() + && ctx->highlightIndex >= 0 && ctx->highlightIndex < partIndex.getNum(); + // for the tool add this node to delayed paths as we want to render it on top of the scene - if (Gui::Selection().isClarifySelectionActive() && ctx && ctx->isHighlighted() - && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 - && ctx->highlightIndex < partIndex.getNum()) { + if (Gui::Selection().isClarifySelectionActive() && hasContextHighlight) { if (!Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { if (viewProvider) { diff --git a/src/Mod/Part/Gui/SoBrepPointSet.cpp b/src/Mod/Part/Gui/SoBrepPointSet.cpp index bfb028e8c9..8d8b8d6009 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.cpp +++ b/src/Mod/Part/Gui/SoBrepPointSet.cpp @@ -83,13 +83,17 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action) return; if(selContext2->checkGlobal(ctx)) ctx = selContext2; - + + + bool hasContextHighlight = + ctx && ctx->isHighlighted() && !ctx->isHighlightAll() && ctx->highlightIndex >= 0; // for clarifyselection, add this node to delayed path if it is highlighted and render it on // top of everything else (highest priority) - if (Gui::Selection().isClarifySelectionActive() && ctx && ctx->isHighlighted() - && !ctx->isHighlightAll() && ctx->highlightIndex >= 0 + if (Gui::Selection().isClarifySelectionActive() && hasContextHighlight && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) { - viewProvider->setFaceHighlightActive(true); + if (viewProvider) { + viewProvider->setFaceHighlightActive(true); + } Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(), action->getCurPath()->copy(), 300); diff --git a/src/Mod/Part/parttests/ColorPerFaceTest.py b/src/Mod/Part/parttests/ColorPerFaceTest.py index 3086efa738..85cdf8a858 100644 --- a/src/Mod/Part/parttests/ColorPerFaceTest.py +++ b/src/Mod/Part/parttests/ColorPerFaceTest.py @@ -49,7 +49,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(box.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 6) def testBoxAndLink(self): @@ -83,7 +83,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(box.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 6) def testTransparency(self): @@ -110,7 +110,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(box.ViewObject.RootNode) paths = sa.getPaths() - bind = paths.get(2).getTail() + bind = paths.get(1).getTail() self.assertEqual(bind.value.getValue(), bind.PER_PART) sa = coin.SoSearchAction() @@ -120,7 +120,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(box.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 6) def testMultiFuse(self): @@ -146,7 +146,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(fuse.ViewObject.RootNode) paths = sa.getPaths() - bind = paths.get(2).getTail() + bind = paths.get(1).getTail() self.assertEqual(bind.value.getValue(), bind.PER_PART) sa = coin.SoSearchAction() @@ -156,7 +156,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(fuse.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 11) self.assertEqual(len(fuse.Shape.Faces), 11) @@ -195,7 +195,7 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(fuse.ViewObject.RootNode) paths = sa.getPaths() - bind = paths.get(2).getTail() + bind = paths.get(1).getTail() self.assertEqual(bind.value.getValue(), bind.PER_PART) sa = coin.SoSearchAction() @@ -205,5 +205,5 @@ class ColorPerFaceTest(unittest.TestCase): sa.apply(fuse.ViewObject.RootNode) paths = sa.getPaths() - mat = paths.get(2).getTail() + mat = paths.get(1).getTail() self.assertEqual(mat.diffuseColor.getNum(), 11) From 4cfe200e23f54669f765666ca6452ffe21f726db Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 12:09:36 +0200 Subject: [PATCH 29/34] Gui: Add Clarify Selection radius multiplier option --- src/Gui/CommandView.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 2895397c51..b292d152fb 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -4004,7 +4004,12 @@ void StdCmdClarifySelection::activated(int iMsg) // Use ray picking to get all objects under cursor SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion()); pickAction.setPoint(point); - pickAction.setRadius(viewer->getPickRadius()); + + float clarifyRadiusMultiplier = App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/View") + ->GetFloat("ClarifySelectionRadiusMultiplier", 5.0f); + + pickAction.setRadius(viewer->getPickRadius() * clarifyRadiusMultiplier); pickAction.setPickAll(true); // Get all objects under cursor pickAction.apply(viewer->getSoRenderManager()->getSceneGraph()); From dd171311547a46d7601a06bfed323a273737ad71 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 13:55:17 +0200 Subject: [PATCH 30/34] Gui: Move Clarify Selection higher in Tools submenu hierarchy --- src/Gui/Workbench.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 41e53d9aa5..8f84d3111e 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -723,6 +723,7 @@ MenuItem* StdWorkbench::setupMenuBar() const << "Separator"; #endif *tool << "Std_Measure" + << "Std_ClarifySelection" << "Std_QuickMeasure" << "Std_UnitsCalculator" << "Separator" @@ -737,8 +738,7 @@ MenuItem* StdWorkbench::setupMenuBar() const << "Separator" << "Std_ProjectUtil" << "Std_DlgParameter" - << "Std_DlgCustomize" - << "Std_ClarifySelection"; + << "Std_DlgCustomize"; // Macro auto macro = new MenuItem( menuBar ); From 7c090f6348f5ac81606b71bab2bcc5681e62dd88 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 14:43:20 +0200 Subject: [PATCH 31/34] Gui: Remember right click position before menu pop-up for ClarifySelect --- src/Gui/CommandView.cpp | 25 +++++++++++++++++++------ src/Gui/Navigation/NavigationStyle.cpp | 12 +++++++++++- src/Gui/Navigation/NavigationStyle.h | 7 +++++++ src/Gui/Selection/SelectionView.cpp | 4 ++-- src/Gui/Selection/SelectionView.h | 3 ++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index b292d152fb..e63e66e250 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3990,16 +3990,22 @@ void StdCmdClarifySelection::activated(int iMsg) return; } - // Get cursor position in the viewer - QPoint pos = QCursor::pos(); QWidget* widget = viewer->getGLWidget(); if (!widget) { return; } - QPoint local = widget->mapFromGlobal(pos); - SbVec2s point(static_cast(local.x()), - static_cast(widget->height() - local.y() - 1)); + // check if we have a stored right-click position (context menu) or should use current cursor position (keyboard shortcut) + SbVec2s point; + auto& storedPosition = viewer->navigationStyle()->getRightClickPosition(); + if (storedPosition.has_value()) { + point = storedPosition.value(); + } else { + QPoint pos = QCursor::pos(); + QPoint local = widget->mapFromGlobal(pos); + point = SbVec2s(static_cast(local.x()), + static_cast(widget->height() - local.y() - 1)); + } // Use ray picking to get all objects under cursor SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion()); @@ -4063,9 +4069,16 @@ void StdCmdClarifySelection::activated(int iMsg) return; } + QPoint globalPos; + if (storedPosition.has_value()) { + globalPos = widget->mapToGlobal(QPoint(point[0], widget->height() - point[1] - 1)); + } else { + globalPos = QCursor::pos(); + } + // Use SelectionMenu to display and handle the pick menu SelectionMenu contextMenu(widget); - contextMenu.doPick(selections); + contextMenu.doPick(selections, globalPos); } bool StdCmdClarifySelection::isActive() diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 03c9447438..9c97445e34 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -1024,6 +1024,11 @@ SbVec3f NavigationStyle::getRotationCenter(SbBool& found) const return this->rotationCenter; } +std::optional& NavigationStyle::getRightClickPosition() +{ + return rightClickPosition; +} + void NavigationStyle::setRotationCenter(const SbVec3f& cnt) { this->rotationCenter = cnt; @@ -1964,7 +1969,9 @@ void NavigationStyle::applyNavigationStyleChange(QAction* selectedAction) void NavigationStyle::openPopupMenu(const SbVec2s& position) { - Q_UNUSED(position); + // store the right-click position for potential use by Clarify Selection + rightClickPosition = position; + // ask workbenches and view provider, ... MenuItem view; Gui::Application::Instance->setupContextMenu("View", &view); @@ -2040,6 +2047,7 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) // handle navigation style change if user selected a navigation style option if (selectedAction && isNavigationStyleAction(selectedAction, navMenuGroup)) { applyNavigationStyleChange(selectedAction); + rightClickPosition.reset(); return; } @@ -2050,6 +2058,8 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position) cmd->invoke(0); // required placeholder value - we don't use group command } } + + rightClickPosition.reset(); } PyObject* NavigationStyle::getPyObject() diff --git a/src/Gui/Navigation/NavigationStyle.h b/src/Gui/Navigation/NavigationStyle.h index ffa907d7e5..4faf708175 100644 --- a/src/Gui/Navigation/NavigationStyle.h +++ b/src/Gui/Navigation/NavigationStyle.h @@ -42,6 +42,7 @@ #include #include #include +#include // forward declarations class SoEvent; @@ -196,6 +197,8 @@ public: SbVec3f getRotationCenter(SbBool&) const; + std::optional& getRightClickPosition(); + PyObject *getPyObject() override; protected: @@ -298,6 +301,10 @@ protected: Py::SmartPtr pythonObject; + // store the position where right-click occurred just before + // the menu popped up + std::optional rightClickPosition; + private: friend class NavigationAnimator; diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index ddfedad372..93a5f1f986 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -728,7 +728,7 @@ struct SubMenuInfo { std::map> items; }; -PickData SelectionMenu::doPick(const std::vector &sels) +PickData SelectionMenu::doPick(const std::vector &sels, const QPoint& pos) { clear(); Gui::Selection().setClarifySelectionActive(true); @@ -739,7 +739,7 @@ PickData SelectionMenu::doPick(const std::vector &sels) processSelections(currentSelections, menus); buildMenuStructure(menus, currentSelections); - QAction* picked = this->exec(QCursor::pos()); + QAction* picked = this->exec(pos); return onPicked(picked, currentSelections); } diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index dfd365bb70..f051c14ebd 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -140,11 +140,12 @@ public: /** Populate and show the menu for picking geometry elements. * * @param sels: a list of geometry element references + * @param pos: optional position to show the menu (defaults to current cursor position) * @return Return the picked geometry reference * * The menu will be divided into submenus that are grouped by element type. */ - PickData doPick(const std::vector &sels); + PickData doPick(const std::vector &sels, const QPoint& pos = QCursor::pos()); public Q_SLOTS: void onHover(QAction *); From 0e15b6520e1968cf4a213cfecd983263e89e7898 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 15:10:08 +0200 Subject: [PATCH 32/34] Gui: Skip creating submenu for Object and Other types in Clarify Select --- src/Gui/Selection/SelectionView.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 93a5f1f986..17ce93bd25 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -801,7 +801,9 @@ void SelectionMenu::buildMenuStructure(std::map &menus } subMenuInfo.menu = addMenu(QString::fromUtf8(elementType.c_str())); - bool groupMenu = shouldGroupMenu(subMenuInfo); + + // for "Object" type, and "Other", always use flat menu (no submenus for individual objects) + bool groupMenu = (elementType != "Object" && elementType != "Other") && shouldGroupMenu(subMenuInfo); for (auto &objectLabelEntry : subMenuInfo.items) { const std::string &objectLabel = objectLabelEntry.first; @@ -1074,8 +1076,6 @@ void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, QString text = QString::fromUtf8(label.c_str()); if (!sel.element.empty()) { text += QStringLiteral(" (%1)").arg(QString::fromUtf8(sel.element.c_str())); - } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { - text += QStringLiteral(" (%1)").arg(tr("Whole Object")); } QAction *action = parentMenu->addAction(elementInfo.icon, text); action->setData(idx); From 9565589f89461c1da4660c095b437b731986460b Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 15:44:33 +0200 Subject: [PATCH 33/34] Gui: Handle proper naming for Link objects in Clarify Selection --- src/Gui/Selection/SelectionView.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index 17ce93bd25..08dac85922 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -902,6 +902,13 @@ std::string SelectionMenu::extractElementType(const PickData &sel) const char *elementName = Data::findElementName(sel.subName.c_str()); if (elementName && elementName[0]) { actualElement = elementName; + } else { + // for link objects like "Bucket.Edge222", extract "Edge222" + std::string subName = sel.subName; + std::size_t lastDot = subName.find_last_of('.'); + if (lastDot != std::string::npos && lastDot + 1 < subName.length()) { + actualElement = subName.substr(lastDot + 1); + } } } @@ -1076,7 +1083,17 @@ void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, QString text = QString::fromUtf8(label.c_str()); if (!sel.element.empty()) { text += QStringLiteral(" (%1)").arg(QString::fromUtf8(sel.element.c_str())); + } else if (!sel.subName.empty() && elementType != "Object" && elementType != "Other") { + // For link objects, extract element name from subName + // For "Bucket.Face74", we want to show "Bucket001 (Face74)" + std::string subName = sel.subName; + std::size_t lastDot = subName.find_last_of('.'); + if (lastDot != std::string::npos && lastDot + 1 < subName.length()) { + QString elementName = QString::fromUtf8(subName.substr(lastDot + 1).c_str()); + text += QStringLiteral(" (%1)").arg(elementName); + } } + QAction *action = parentMenu->addAction(elementInfo.icon, text); action->setData(idx); connect(action, &QAction::hovered, this, [this, action]() { @@ -1099,9 +1116,20 @@ void SelectionMenu::createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMen text = QString::fromUtf8(sel.element.c_str()); } else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') { text = tr("Whole Object"); + } else if (!sel.subName.empty()) { + // extract just the element name from subName for link objects + // for "Bucket.Edge222", we want just "Edge222" + std::string subName = sel.subName; + std::size_t lastDot = subName.find_last_of('.'); + if (lastDot != std::string::npos && lastDot + 1 < subName.length()) { + text = QString::fromUtf8(subName.substr(lastDot + 1).c_str()); + } else { + text = QString::fromUtf8(sel.subName.c_str()); + } } else { text = QString::fromUtf8(sel.subName.c_str()); } + QAction *action = elementInfo.menu->addAction(text); action->setData(idx); connect(action, &QAction::hovered, this, [this, action]() { From 0358d5bde61d3cb12835392564363eb1db37b231 Mon Sep 17 00:00:00 2001 From: tetektoza Date: Sat, 6 Sep 2025 17:13:05 +0200 Subject: [PATCH 34/34] Gui: Correct linter problems --- src/Gui/CommandView.cpp | 21 +++++++++++++-------- src/Gui/Inventor/So3DAnnotation.cpp | 6 ++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index e63e66e250..e9d0b72c21 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -4011,12 +4011,13 @@ void StdCmdClarifySelection::activated(int iMsg) SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion()); pickAction.setPoint(point); - float clarifyRadiusMultiplier = App::GetApplication() + constexpr double defaultMultiplier = 5.0F; + double clarifyRadiusMultiplier = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/View") - ->GetFloat("ClarifySelectionRadiusMultiplier", 5.0f); + ->GetFloat("ClarifySelectionRadiusMultiplier", defaultMultiplier); pickAction.setRadius(viewer->getPickRadius() * clarifyRadiusMultiplier); - pickAction.setPickAll(true); // Get all objects under cursor + pickAction.setPickAll(static_cast(true)); // Get all objects under cursor pickAction.apply(viewer->getSoRenderManager()->getSceneGraph()); const SoPickedPointList& pplist = pickAction.getPickedPointList(); @@ -4029,22 +4030,26 @@ void StdCmdClarifySelection::activated(int iMsg) for (int i = 0; i < pplist.getLength(); ++i) { SoPickedPoint* pp = pplist[i]; - if (!pp || !pp->getPath()) + if (!pp || !pp->getPath()) { continue; + } ViewProvider* vp = viewer->getViewProviderByPath(pp->getPath()); - if (!vp) + if (!vp) { continue; + } // Cast to ViewProviderDocumentObject to get the object auto vpDoc = freecad_cast(vp); - if (!vpDoc) + if (!vpDoc) { continue; + } App::DocumentObject* obj = vpDoc->getObject(); - if (!obj) + if (!obj) { continue; - + } + // Get element information - handle sub-objects like Assembly parts std::string elementName = vp->getElement(pp->getDetail()); std::string subName; diff --git a/src/Gui/Inventor/So3DAnnotation.cpp b/src/Gui/Inventor/So3DAnnotation.cpp index 87f54d86e9..a6766a3ff0 100644 --- a/src/Gui/Inventor/So3DAnnotation.cpp +++ b/src/Gui/Inventor/So3DAnnotation.cpp @@ -67,7 +67,7 @@ SoPathList SoDelayedAnnotationsElement::getDelayedPaths(SoState* state) auto elt = static_cast(state->getElementNoPush(classStackIndex)); if (elt->paths.empty()) { - return SoPathList(); + return {}; } // sort by priority (lower numbers render first) @@ -91,7 +91,9 @@ void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state { auto elt = static_cast(state->getElementNoPush(classStackIndex)); - if (elt->paths.empty()) return; + if (elt->paths.empty()) { + return; + } std::stable_sort(elt->paths.begin(), elt->paths.end(), [](const PriorityPath& a, const PriorityPath& b) {