Files
create/src/Gui/Selection/SelectionView.cpp
Markus Reitböck a72a0d6405 Gui: use CMake to generate precompiled headers on all platforms
"Professional CMake" book suggest the following:

"Targets should build successfully with or without compiler support for precompiled headers. It
should be considered an optimization, not a requirement. In particular, do not explicitly include a
precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically
generated precompile header on the compiler command line instead. This is more portable across
the major compilers and is likely to be easier to maintain. It will also avoid warnings being
generated from certain code checking tools like iwyu (include what you use)."

Therefore, removed the "#include <PreCompiled.h>" from sources, also
there is no need for the "#ifdef _PreComp_" anymore
2025-09-14 09:47:03 +02:00

1139 lines
41 KiB
C++

/***************************************************************************
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <QCheckBox>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMenu>
#include <QTextStream>
#include <QToolButton>
#include <QVBoxLayout>
#include <set>
#include <App/ComplexGeoData.h>
#include <App/Document.h>
#include <App/ElementNamingUtils.h>
#include <App/GeoFeature.h>
#include <App/IndexedName.h>
#include <Base/Console.h>
#include "SelectionView.h"
#include "Application.h"
#include "BitmapFactory.h"
#include "Command.h"
#include "Document.h"
#include "ViewProvider.h"
FC_LOG_LEVEL_INIT("Selection", true, true, true)
using namespace Gui;
using namespace Gui::DockWnd;
/* TRANSLATOR Gui::DockWnd::SelectionView */
SelectionView::SelectionView(Gui::Document* pcDocument, QWidget* parent)
: DockWindow(pcDocument, parent)
, SelectionObserver(true, ResolveMode::NoResolve)
, x(0.0f)
, y(0.0f)
, z(0.0f)
, openedAutomatically(false)
{
setWindowTitle(tr("Selection View"));
QVBoxLayout* vLayout = new QVBoxLayout(this);
vLayout->setSpacing(0);
vLayout->setContentsMargins(0, 0, 0, 0);
QLineEdit* searchBox = new QLineEdit(this);
searchBox->setPlaceholderText(tr("Search"));
searchBox->setToolTip(tr("Searches object labels"));
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setSpacing(2);
QToolButton* clearButton = new QToolButton(this);
clearButton->setFixedSize(18, 21);
clearButton->setCursor(Qt::ArrowCursor);
clearButton->setStyleSheet(QStringLiteral("QToolButton {margin-bottom:1px}"));
clearButton->setIcon(BitmapFactory().pixmap(":/icons/edit-cleartext.svg"));
clearButton->setToolTip(tr("Clears the search field"));
clearButton->setAutoRaise(true);
countLabel = new QLabel(this);
countLabel->setText(QStringLiteral("0"));
countLabel->setToolTip(tr("The number of selected items"));
hLayout->addWidget(searchBox);
hLayout->addWidget(clearButton, 0, Qt::AlignRight);
hLayout->addWidget(countLabel, 0, Qt::AlignRight);
vLayout->addLayout(hLayout);
selectionView = new QListWidget(this);
selectionView->setContextMenuPolicy(Qt::CustomContextMenu);
vLayout->addWidget(selectionView);
enablePickList = new QCheckBox(this);
enablePickList->setText(tr("Picked object list"));
vLayout->addWidget(enablePickList);
pickList = new QListWidget(this);
pickList->setVisible(false);
vLayout->addWidget(pickList);
selectionView->setMouseTracking(true); // needed for itemEntered() to work
pickList->setMouseTracking(true);
resize(200, 200);
// clang-format off
connect(clearButton, &QToolButton::clicked, searchBox, &QLineEdit::clear);
connect(searchBox, &QLineEdit::textChanged, this, &SelectionView::search);
connect(searchBox, &QLineEdit::editingFinished, this, &SelectionView::validateSearch);
connect(selectionView, &QListWidget::itemDoubleClicked, this, &SelectionView::toggleSelect);
connect(selectionView, &QListWidget::itemEntered, this, &SelectionView::preselect);
connect(pickList, &QListWidget::itemDoubleClicked, this, &SelectionView::toggleSelect);
connect(pickList, &QListWidget::itemEntered, this, &SelectionView::preselect);
connect(selectionView, &QListWidget::customContextMenuRequested, this, &SelectionView::onItemContextMenu);
#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
connect(enablePickList, &QCheckBox::checkStateChanged, this, &SelectionView::onEnablePickList);
#else
connect(enablePickList, &QCheckBox::stateChanged, this, &SelectionView::onEnablePickList);
#endif
// clang-format on
}
SelectionView::~SelectionView() = default;
void SelectionView::leaveEvent(QEvent*)
{
Selection().rmvPreselect();
}
/// @cond DOXERR
void SelectionView::onSelectionChanged(const SelectionChanges& Reason)
{
ParameterGrp::handle hGrp = App::GetApplication()
.GetUserParameter()
.GetGroup("BaseApp")
->GetGroup("Preferences")
->GetGroup("Selection");
bool autoShow = hGrp->GetBool("AutoShowSelectionView", false);
hGrp->SetBool("AutoShowSelectionView",
autoShow); // Remove this line once the preferences window item is implemented
if (autoShow) {
if (!parentWidget()->isVisible() && Selection().hasSelection()) {
parentWidget()->show();
openedAutomatically = true;
}
else if (openedAutomatically && !Selection().hasSelection()) {
parentWidget()->hide();
openedAutomatically = false;
}
}
QString selObject;
QTextStream str(&selObject);
auto getSelectionName = [](QTextStream& str,
const char* docName,
const char* objName,
const char* subName,
App::DocumentObject* obj) {
str << docName;
str << "#";
str << objName;
if (subName != 0 && subName[0] != 0) {
str << ".";
/* Original code doesn't take account of histories in subelement names and displays
* them inadvertently. Let's not do that.
str << subName;
*/
/* Remove the history from the displayed subelement name */
App::ElementNamePair elementName;
App::GeoFeature::resolveElement(obj, subName, elementName);
str << elementName.oldName.c_str(); // Use the shortened element name not the full one.
/* Mark it visually if there was a history as a "tell" for if a given selection has TNP
* fixes in it. */
if (elementName.newName.size() > 0) {
str << " []";
}
auto subObj = obj->getSubObject(subName);
if (subObj) {
obj = subObj;
}
}
str << " (";
str << QString::fromUtf8(obj->Label.getValue());
str << ")";
};
if (Reason.Type == SelectionChanges::AddSelection) {
// save as user data
QStringList list;
list << QString::fromLatin1(Reason.pDocName);
list << QString::fromLatin1(Reason.pObjectName);
App::Document* doc = App::GetApplication().getDocument(Reason.pDocName);
App::DocumentObject* obj = doc->getObject(Reason.pObjectName);
getSelectionName(str, Reason.pDocName, Reason.pObjectName, Reason.pSubName, obj);
// insert the selection as item
QListWidgetItem* item = new QListWidgetItem(selObject, selectionView);
item->setData(Qt::UserRole, list);
}
else if (Reason.Type == SelectionChanges::ClrSelection) {
if (!Reason.pDocName[0]) {
// remove all items
selectionView->clear();
}
else {
// build name
str << Reason.pDocName;
str << "#";
// remove all items
const auto items = selectionView->findItems(selObject, Qt::MatchStartsWith);
for (auto item : items) {
delete item;
}
}
}
else if (Reason.Type == SelectionChanges::RmvSelection) {
App::Document* doc = App::GetApplication().getDocument(Reason.pDocName);
App::DocumentObject* obj = doc->getObject(Reason.pObjectName);
getSelectionName(str, Reason.pDocName, Reason.pObjectName, Reason.pSubName, obj);
// remove all items
QList<QListWidgetItem*> l = selectionView->findItems(selObject, Qt::MatchStartsWith);
if (l.size() == 1) {
delete l[0];
}
}
else if (Reason.Type == SelectionChanges::SetSelection) {
// remove all items
selectionView->clear();
std::vector<SelectionSingleton::SelObj> objs =
Gui::Selection().getSelection(Reason.pDocName, ResolveMode::NoResolve);
for (const auto& it : objs) {
// save as user data
QStringList list;
list << QString::fromLatin1(it.DocName);
list << QString::fromLatin1(it.FeatName);
App::Document* doc = App::GetApplication().getDocument(it.DocName);
App::DocumentObject* obj = doc->getObject(it.FeatName);
getSelectionName(str, it.DocName, it.FeatName, it.SubName, obj);
QListWidgetItem* item = new QListWidgetItem(selObject, selectionView);
item->setData(Qt::UserRole, list);
selObject.clear();
}
}
else if (Reason.Type == SelectionChanges::PickedListChanged) {
bool picking = Selection().needPickedList();
enablePickList->setChecked(picking);
pickList->setVisible(picking);
pickList->clear();
if (picking) {
const auto& sels = Selection().getPickedList(Reason.pDocName);
for (const auto& sel : sels) {
App::Document* doc = App::GetApplication().getDocument(sel.DocName);
if (!doc) {
continue;
}
App::DocumentObject* obj = doc->getObject(sel.FeatName);
if (!obj) {
continue;
}
QString selObject;
QTextStream str(&selObject);
getSelectionName(str, sel.DocName, sel.FeatName, sel.SubName, obj);
this->x = sel.x;
this->y = sel.y;
this->z = sel.z;
new QListWidgetItem(selObject, pickList);
}
}
}
countLabel->setText(QString::number(selectionView->count()));
}
void SelectionView::search(const QString& text)
{
if (!text.isEmpty()) {
searchList.clear();
App::Document* doc = App::GetApplication().getActiveDocument();
std::vector<App::DocumentObject*> objects;
if (doc) {
objects = doc->getObjects();
selectionView->clear();
for (auto it : objects) {
QString label = QString::fromUtf8(it->Label.getValue());
if (label.contains(text, Qt::CaseInsensitive)) {
searchList.push_back(it);
// save as user data
QString selObject;
QTextStream str(&selObject);
QStringList list;
list << QString::fromLatin1(doc->getName());
list << QString::fromLatin1(it->getNameInDocument());
// build name
str << QString::fromUtf8(doc->Label.getValue());
str << "#";
str << it->getNameInDocument();
str << " (";
str << label;
str << ")";
QListWidgetItem* item = new QListWidgetItem(selObject, selectionView);
item->setData(Qt::UserRole, list);
}
}
countLabel->setText(QString::number(selectionView->count()));
}
}
}
void SelectionView::validateSearch()
{
if (!searchList.empty()) {
App::Document* doc = App::GetApplication().getActiveDocument();
if (doc) {
Gui::Selection().clearSelection();
for (auto it : searchList) {
Gui::Selection().addSelection(doc->getName(), it->getNameInDocument(), nullptr);
}
}
}
}
void SelectionView::select(QListWidgetItem* item)
{
if (!item) {
item = selectionView->currentItem();
}
if (!item) {
return;
}
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.size() < 2) {
return;
}
try {
// Gui::Selection().clearSelection();
Gui::Command::runCommand(Gui::Command::Gui, "Gui.Selection.clearSelection()");
// Gui::Selection().addSelection(elements[0].toLatin1(),elements[1].toLatin1(),0);
QString cmd = QStringLiteral(
R"(Gui.Selection.addSelection(App.getDocument("%1").getObject("%2")))")
.arg(elements[0], elements[1]);
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::deselect()
{
QListWidgetItem* item = selectionView->currentItem();
if (!item) {
return;
}
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.size() < 2) {
return;
}
// Gui::Selection().rmvSelection(elements[0].toLatin1(),elements[1].toLatin1(),0);
QString cmd = QStringLiteral(
R"(Gui.Selection.removeSelection(App.getDocument("%1").getObject("%2")))")
.arg(elements[0], elements[1]);
try {
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::toggleSelect(QListWidgetItem* item)
{
if (!item) {
return;
}
std::string name = item->text().toLatin1().constData();
char* docname = &name.at(0);
char* objname = std::strchr(docname, '#');
if (!objname) {
return;
}
*objname++ = 0;
char* subname = std::strchr(objname, '.');
if (subname) {
*subname++ = 0;
char* end = std::strchr(subname, ' ');
if (end) {
*end = 0;
}
}
else {
char* end = std::strchr(objname, ' ');
if (end) {
*end = 0;
}
}
QString cmd;
if (Gui::Selection().isSelected(docname, objname, subname)) {
cmd = QStringLiteral("Gui.Selection.removeSelection("
"App.getDocument('%1').getObject('%2'),'%3')")
.arg(QString::fromLatin1(docname),
QString::fromLatin1(objname),
QString::fromLatin1(subname));
}
else {
cmd = QStringLiteral("Gui.Selection.addSelection("
"App.getDocument('%1').getObject('%2'),'%3',%4,%5,%6)")
.arg(QString::fromLatin1(docname),
QString::fromLatin1(objname),
QString::fromLatin1(subname))
.arg(x)
.arg(y)
.arg(z);
}
try {
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::preselect(QListWidgetItem* item)
{
if (!item) {
return;
}
std::string name = item->text().toLatin1().constData();
char* docname = &name.at(0);
char* objname = std::strchr(docname, '#');
if (!objname) {
return;
}
*objname++ = 0;
char* subname = std::strchr(objname, '.');
if (subname) {
*subname++ = 0;
char* end = std::strchr(subname, ' ');
if (end) {
*end = 0;
}
}
else {
char* end = std::strchr(objname, ' ');
if (end) {
*end = 0;
}
}
QString cmd = QStringLiteral("Gui.Selection.setPreselection("
"App.getDocument('%1').getObject('%2'),'%3',tp=2)")
.arg(QString::fromLatin1(docname),
QString::fromLatin1(objname),
QString::fromLatin1(subname));
try {
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::zoom()
{
select();
try {
Gui::Command::runCommand(Gui::Command::Gui, "Gui.SendMsgToActiveView(\"ViewSelection\")");
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::treeSelect()
{
select();
try {
Gui::Command::runCommand(Gui::Command::Gui, "Gui.runCommand(\"Std_TreeSelection\")");
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::touch()
{
QListWidgetItem* item = selectionView->currentItem();
if (!item) {
return;
}
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.size() < 2) {
return;
}
QString cmd = QStringLiteral(R"(App.getDocument("%1").getObject("%2").touch())")
.arg(elements[0], elements[1]);
try {
Gui::Command::runCommand(Gui::Command::Doc, cmd.toLatin1());
}
catch (Base::Exception& e) {
e.reportException();
}
}
void SelectionView::toPython()
{
QListWidgetItem* item = selectionView->currentItem();
if (!item) {
return;
}
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.size() < 2) {
return;
}
try {
QString cmd = QStringLiteral(R"(obj = App.getDocument("%1").getObject("%2"))")
.arg(elements[0], elements[1]);
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
if (elements.length() > 2) {
App::Document* doc = App::GetApplication().getDocument(elements[0].toLatin1());
App::DocumentObject* obj = doc->getObject(elements[1].toLatin1());
QString property = getProperty(obj);
cmd = QStringLiteral(R"(shp = App.getDocument("%1").getObject("%2").%3)")
.arg(elements[0], elements[1], property);
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
if (supportPart(obj, elements[2])) {
cmd = QStringLiteral(R"(elt = App.getDocument("%1").getObject("%2").%3.%4)")
.arg(elements[0], elements[1], property, elements[2]);
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
}
}
catch (const Base::Exception& e) {
e.reportException();
}
}
void SelectionView::showPart()
{
QListWidgetItem* item = selectionView->currentItem();
if (!item) {
return;
}
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.length() > 2) {
App::Document* doc = App::GetApplication().getDocument(elements[0].toLatin1());
App::DocumentObject* obj = doc->getObject(elements[1].toLatin1());
QString module = getModule(obj->getTypeId().getName());
QString property = getProperty(obj);
if (!module.isEmpty() && !property.isEmpty() && supportPart(obj, elements[2])) {
try {
Gui::Command::addModule(Gui::Command::Gui, module.toLatin1());
QString cmd =
QStringLiteral(R"(%1.show(App.getDocument("%2").getObject("%3").%4.%5))")
.arg(module, elements[0], elements[1], property, elements[2]);
Gui::Command::runCommand(Gui::Command::Gui, cmd.toLatin1());
}
catch (const Base::Exception& e) {
e.reportException();
}
}
}
}
QString SelectionView::getModule(const char* type) const
{
// go up the inheritance tree and find the module name of the first
// sub-class that has not the prefix "App::"
std::string prefix;
Base::Type typeId = Base::Type::fromName(type);
while (!typeId.isBad()) {
std::string temp(typeId.getName());
std::string::size_type pos = temp.find_first_of("::");
std::string module;
if (pos != std::string::npos) {
module = std::string(temp, 0, pos);
}
if (module != "App") {
prefix = module;
}
else {
break;
}
typeId = typeId.getParent();
}
return QString::fromStdString(prefix);
}
QString SelectionView::getProperty(App::DocumentObject* obj) const
{
QString property;
if (obj->isDerivedFrom<App::GeoFeature>()) {
App::GeoFeature* geo = static_cast<App::GeoFeature*>(obj);
const App::PropertyComplexGeoData* data = geo->getPropertyOfGeometry();
const char* name = data ? data->getName() : nullptr;
if (App::Property::isValidName(name)) {
property = QString::fromLatin1(name);
}
}
return property;
}
bool SelectionView::supportPart(App::DocumentObject* obj, const QString& part) const
{
if (obj->isDerivedFrom<App::GeoFeature>()) {
App::GeoFeature* geo = static_cast<App::GeoFeature*>(obj);
const App::PropertyComplexGeoData* data = geo->getPropertyOfGeometry();
if (data) {
const Data::ComplexGeoData* geometry = data->getComplexData();
std::vector<const char*> types = geometry->getElementTypes();
for (auto it : types) {
if (part.startsWith(QString::fromLatin1(it))) {
return true;
}
}
}
}
return false;
}
void SelectionView::onItemContextMenu(const QPoint& point)
{
QListWidgetItem* item = selectionView->itemAt(point);
if (!item) {
return;
}
QMenu menu;
QAction* selectAction = menu.addAction(tr("Select Only"), this, [&] {
this->select(nullptr);
});
selectAction->setIcon(QIcon::fromTheme(QStringLiteral("view-select")));
selectAction->setToolTip(tr("Selects only this object"));
QAction* deselectAction = menu.addAction(tr("Deselect"), this, &SelectionView::deselect);
deselectAction->setIcon(QIcon::fromTheme(QStringLiteral("view-unselectable")));
deselectAction->setToolTip(tr("Deselects this object"));
QAction* zoomAction = menu.addAction(tr("Zoom Fit"), this, &SelectionView::zoom);
zoomAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best")));
zoomAction->setToolTip(tr("Selects and fits this object in the 3D window"));
QAction* gotoAction = menu.addAction(tr("Go to Selection"), this, &SelectionView::treeSelect);
gotoAction->setToolTip(tr("Selects and locates this object in the tree view"));
QAction* touchAction = menu.addAction(tr("Mark to Recompute"), this, &SelectionView::touch);
touchAction->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
touchAction->setToolTip(tr("Marks this object to be recomputed"));
QAction* toPythonAction =
menu.addAction(tr("To Python Console"), this, &SelectionView::toPython);
toPythonAction->setIcon(QIcon::fromTheme(QStringLiteral("applications-python")));
toPythonAction->setToolTip(
tr("Reveals this object and its subelements in the Python console."));
QStringList elements = item->data(Qt::UserRole).toStringList();
if (elements.length() > 2) {
// subshape-specific entries
QAction* showPart =
menu.addAction(tr("Duplicate Subshape"), this, &SelectionView::showPart);
showPart->setIcon(QIcon(QStringLiteral(":/icons/ClassBrowser/member.svg")));
showPart->setToolTip(tr("Creates a standalone copy of this subshape in the document"));
}
menu.exec(selectionView->mapToGlobal(point));
}
void SelectionView::onUpdate()
{}
bool SelectionView::onMsg(const char* /*pMsg*/, const char** /*ppReturn*/)
{
return false;
}
void SelectionView::hideEvent(QHideEvent* ev)
{
DockWindow::hideEvent(ev);
}
void SelectionView::showEvent(QShowEvent* ev)
{
enablePickList->setChecked(Selection().needPickedList());
Gui::DockWindow::showEvent(ev);
}
void SelectionView::onEnablePickList()
{
bool enabled = enablePickList->isChecked();
Selection().enablePickedList(enabled);
pickList->setVisible(enabled);
}
/// @endcond
// SelectionMenu implementation
SelectionMenu::SelectionMenu(QWidget *parent)
: QMenu(parent)
{
connect(this, &QMenu::hovered, this, &SelectionMenu::onHover);
}
struct ElementInfo {
QMenu *menu = nullptr;
QIcon icon;
std::vector<int> indices;
};
struct SubMenuInfo {
QMenu *menu = nullptr;
// Map from sub-object label to map from object path to element info
std::map<std::string, std::map<std::string, ElementInfo>> items;
};
PickData SelectionMenu::doPick(const std::vector<PickData> &sels, const QPoint& pos)
{
clear();
Gui::Selection().setClarifySelectionActive(true);
currentSelections = sels;
std::map<std::string, SubMenuInfo> menus;
processSelections(currentSelections, menus);
buildMenuStructure(menus, currentSelections);
QAction* picked = this->exec(pos);
return onPicked(picked, currentSelections);
}
void SelectionMenu::processSelections(std::vector<PickData> &selections, std::map<std::string, SubMenuInfo> &menus)
{
std::map<App::DocumentObject*, QIcon> icons;
std::set<std::string> createdElementTypes;
std::set<std::string> 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<std::string, SubMenuInfo> &menus, const std::vector<PickData> &selections)
{
std::vector<std::string> preferredOrder = {"Object", "Solid", "Face", "Edge", "Vertex", "Wire", "Shell", "Compound", "CompSolid"};
std::vector<std::map<std::string, SubMenuInfo>::iterator> menuArray;
menuArray.reserve(menus.size());
for (const auto& category : preferredOrder) {
if (auto it = menus.find(category); 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 elementTypeIterator : menuArray) {
auto &elementTypeEntry = *elementTypeIterator;
auto &subMenuInfo = elementTypeEntry.second;
const std::string &elementType = elementTypeEntry.first;
if (subMenuInfo.items.empty()) {
continue;
}
subMenuInfo.menu = addMenu(QString::fromUtf8(elementType.c_str()));
// 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;
for (auto &objectPathEntry : objectLabelEntry.second) {
auto &elementInfo = objectPathEntry.second;
if (!groupMenu) {
createFlatMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections);
} else {
createGroupedMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections);
}
}
}
}
}
PickData SelectionMenu::onPicked(QAction *picked, const std::vector<PickData> &sels)
{
// Clear the ClarifySelection active flag when menu is done
Gui::Selection().setClarifySelectionActive(false);
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 || currentSelections.empty())
return;
// 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;
// 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)
{
return QMenu::eventFilter(obj, event);
}
void SelectionMenu::leaveEvent(QEvent *e)
{
Gui::Selection().rmvPreselect();
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;
} 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);
}
}
}
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<App::DocumentObject*, QIcon> &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<std::string, SubMenuInfo> &menus, std::set<std::string> &createdTypes)
{
auto geoFeature = freecad_cast<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> 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<PickData> &selections, std::map<std::string, SubMenuInfo> &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) {
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<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> types = geoFeature->getElementTypes(true);
if (types.size() > 1) {
shouldAdd = true;
}
}
}
} else {
// 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<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> types = geoFeature->getElementTypes(true);
if (!types.empty()) {
shouldAdd = true;
}
}
}
}
}
if (shouldAdd) {
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);
}
}
}
}
bool SelectionMenu::shouldGroupMenu(const SubMenuInfo &info)
{
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 > MAX_SELECTION_COUNT_BEFORE_GROUPING && objCount > 1) {
return true;
}
}
return false;
}
void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &selections)
{
for (int idx : elementInfo.indices) {
const auto &sel = selections[idx];
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]() {
onHover(action);
});
}
}
void SelectionMenu::createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &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 = 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]() {
onHover(action);
});
}
}
#include "moc_SelectionView.cpp"