/*************************************************************************** * Copyright (c) 2004 Jürgen Riegel * * * * 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 "PreCompiled.h" #ifndef _PreComp_ # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include "Tree.h" #include "BitmapFactory.h" #include "Command.h" #include "Document.h" #include "ExpressionCompleter.h" #include "Macro.h" #include "MainWindow.h" #include "MenuManager.h" #include "TreeParams.h" #include "View3DInventor.h" #include "ViewProviderDocumentObject.h" #include "Widgets.h" #include "Workbench.h" FC_LOG_LEVEL_INIT("Tree", false, true, true) #define _TREE_PRINT(_level,_func,_msg) \ _FC_PRINT(FC_LOG_INSTANCE,_level,_func, '['<,_msg) #define TREE_WARN(_msg) _TREE_PRINT(FC_LOGLEVEL_WARN,Notify,_msg) #define TREE_ERR(_msg) _TREE_PRINT(FC_LOGLEVEL_ERR,Notify,_msg) #define TREE_LOG(_msg) _TREE_PRINT(FC_LOGLEVEL_LOG,Notify,_msg) #define TREE_TRACE(_msg) _TREE_PRINT(FC_LOGLEVEL_TRACE,Notify,_msg) using namespace Gui; namespace sp = std::placeholders; ///////////////////////////////////////////////////////////////////////////////// std::unique_ptr TreeWidget::documentPixmap; std::unique_ptr TreeWidget::documentPartialPixmap; static QBrush _TreeItemBackground; std::set TreeWidget::Instances; static TreeWidget* _LastSelectedTreeWidget; const int TreeWidget::DocumentType = 1000; const int TreeWidget::ObjectType = 1001; static bool _DraggingActive; static bool _DragEventFilter; static bool isVisibilityIconEnabled() { return TreeParams::getVisibilityIcon(); } static bool isOnlyNameColumnDisplayed() { return TreeParams::getHideInternalNames() && TreeParams::getHideColumn(); } static bool isSelectionCheckBoxesEnabled() { return TreeParams::getCheckBoxesSelection(); } void TreeParams::onItemBackgroundChanged() { if (getItemBackground()) { Base::Color color; color.setPackedValue(getItemBackground()); QColor col; col.setRedF(color.r); col.setGreenF(color.g); col.setBlueF(color.b); col.setAlphaF(color.a); _TreeItemBackground = QBrush(col); } else _TreeItemBackground = QBrush(); refreshTreeViews(); } ////////////////////////////////////////////////////////////////////////////////////// struct Stats { #define DEFINE_STATS \ DEFINE_STAT(testStatus1) \ DEFINE_STAT(testStatus2) \ DEFINE_STAT(testStatus3) \ DEFINE_STAT(getIcon) \ DEFINE_STAT(setIcon) \ #define DEFINE_STAT(_name) \ FC_DURATION_DECLARE(_name);\ int _name##_count; DEFINE_STATS void init() { #undef DEFINE_STAT #define DEFINE_STAT(_name) \ FC_DURATION_INIT(_name);\ _name##_count = 0; DEFINE_STATS } void print() { #undef DEFINE_STAT #define DEFINE_STAT(_name) FC_DURATION_MSG(_name, #_name " count: " << _name##_count); DEFINE_STATS } #undef DEFINE_STAT #define DEFINE_STAT(_name) \ void time_##_name(FC_TIME_POINT &t) {\ ++_name##_count;\ FC_DURATION_PLUS(_name,t);\ } DEFINE_STATS }; //static Stats _Stats; struct TimingInfo { bool timed = false; FC_TIME_POINT t; FC_DURATION& d; explicit TimingInfo(FC_DURATION& d) :d(d) { _FC_TIME_INIT(t); } ~TimingInfo() { stop(); } void stop() { if (!timed) { timed = true; FC_DURATION_PLUS(d, t); } } void reset() { stop(); _FC_TIME_INIT(t); } }; // #define DO_TIMING #ifdef DO_TIMING #define _Timing(_idx,_name) ++_Stats._name##_count; TimingInfo _tt##_idx(_Stats._name) #define Timing(_name) _Timing(0,_name) #define _TimingStop(_idx,_name) _tt##_idx.stop(); #define TimingStop(_name) _TimingStop(0,_name); #define TimingInit() _Stats.init(); #define TimingPrint() _Stats.print(); #else #define _Timing(...) do{}while(0) #define Timing(...) do{}while(0) #define TimingInit() do{}while(0) #define TimingPrint() do{}while(0) #define _TimingStop(...) do{}while(0); #define TimingStop(...) do{}while(0); #endif // --------------------------------------------------------------------------- using DocumentObjectItems = std::set; class Gui::DocumentObjectData { public: bool dirtyFlag {}; DocumentItem* docItem; DocumentObjectItems items; ViewProviderDocumentObject* viewObject; DocumentObjectItem* rootItem{nullptr}; std::vector children; std::set childSet; bool removeChildrenFromRoot; bool itemHidden; std::string label; std::string label2; std::string internalName; using Connection = boost::signals2::scoped_connection; Connection connectIcon; Connection connectTool; Connection connectStat; Connection connectHl; DocumentObjectData(DocumentItem* docItem, ViewProviderDocumentObject* vpd) : docItem(docItem) , viewObject(vpd) { //NOLINTBEGIN // Setup connections connectIcon = viewObject->signalChangeIcon.connect( std::bind(&DocumentObjectData::slotChangeIcon, this)); connectTool = viewObject->signalChangeToolTip.connect( std::bind(&DocumentObjectData::slotChangeToolTip, this, sp::_1)); connectStat = viewObject->signalChangeStatusTip.connect( std::bind(&DocumentObjectData::slotChangeStatusTip, this, sp::_1)); connectHl = viewObject->signalChangeHighlight.connect( std::bind(&DocumentObjectData::slotChangeHighlight, this, sp::_1, sp::_2)); //NOLINTEND removeChildrenFromRoot = viewObject->canRemoveChildrenFromRoot(); itemHidden = !viewObject->showInTree(); label = viewObject->getObject()->Label.getValue(); label2 = viewObject->getObject()->Label2.getValue(); internalName = viewObject->getObject()->getNameInDocument(); } void insertItem(DocumentObjectItem* item) { items.insert(item); dirtyFlag = true; } void removeItem(DocumentObjectItem* item) { auto it = items.find(item); if (it == items.end()) { assert(0); } else { items.erase(it); dirtyFlag = true; } } const char* getTreeName() const { return docItem->getTreeName(); } void updateChildren(DocumentObjectDataPtr other) { children = other->children; childSet = other->childSet; } bool updateChildren(bool checkVisibility) { auto newChildren = viewObject->claimChildren(); auto obj = viewObject->getObject(); std::set newSet; bool updated = false; for (auto child : newChildren) { auto childVp = docItem->getViewProvider(child); if (!childVp) continue; if (child && child->isAttachedToDocument()) { if (!newSet.insert(child).second) { TREE_WARN("duplicate child item " << obj->getFullName() << '.' << child->getNameInDocument()); } else if (!childSet.erase(child)) { // this means new child detected updated = true; if (child->getDocument() == obj->getDocument() && child->getDocument() == docItem->document()->getDocument()) { auto& parents = docItem->_ParentMap[child]; if (parents.insert(obj).second && child->Visibility.getValue()) { bool showable = false; for (auto parent : parents) { if (!parent->hasChildElement() && parent->getLinkedObject(false) == parent) { showable = true; break; } } childVp->setShowable(showable); } } } } } for (auto child : childSet) { if (newSet.find(child) == newSet.end()) { // this means old child removed updated = true; auto mapIt = docItem->_ParentMap.find(child); // If 'child' is not part of the map then it has already been deleted // in _slotDeleteObject. if (mapIt != docItem->_ParentMap.end()) { docItem->_ParentMap[child].erase(obj); auto childVp = docItem->getViewProvider(child); if (childVp && child->getDocument() == obj->getDocument()) childVp->setShowable(docItem->isObjectShowable(child)); } } } // We still need to check the order of the children updated = updated || children != newChildren; children.swap(newChildren); childSet.swap(newSet); if (updated && checkVisibility) { for (auto child : children) { auto childVp = docItem->getViewProvider(child); if (childVp && child->getDocument() == obj->getDocument()) childVp->setShowable(docItem->isObjectShowable(child)); } } return updated; } void testStatus(bool resetStatus = false) { QIcon icon, icon2; for (auto item : items) item->testStatus(resetStatus, icon, icon2); } void slotChangeIcon() { testStatus(true); } void slotChangeToolTip(const QString& tip) { for (auto item : items) item->setToolTip(0, tip); } void slotChangeStatusTip(const QString& tip) { for (auto item : items) item->setStatusTip(0, tip); } void slotChangeHighlight(bool set, Gui::HighlightMode mode) { for (auto item : items) item->setHighlight(set, mode); } }; // --------------------------------------------------------------------------- class DocumentItem::ExpandInfo : public std::unordered_map { public: void restore(Base::XMLReader& reader) { int level = reader.level(); int count = reader.getAttributeAsInteger("count"); for (int i = 0; i < count; ++i) { reader.readElement("Expand"); auto& entry = (*this)[reader.getAttribute("name")]; if (!reader.hasAttribute("count")) continue; entry.reset(new ExpandInfo); entry->restore(reader); } reader.readEndElement("Expand", level - 1); } }; // --------------------------------------------------------------------------- namespace Gui { /** * TreeWidget item delegate for editing */ class TreeWidgetItemDelegate: public QStyledItemDelegate { typedef QStyledItemDelegate inherited; // Beware, big scary hack incoming! // // This is artificial QTreeWidget that is not rendered and its sole goal is to be the source // of style information that can be manipulated using QSS. From Qt6.5 tree branches also // have rendered background using ::item sub-control. Whole row also gets background from // the same sub-control. Only way to prevent this is to disable background of ::item, // this however limits our ability to style tree items. As solution we create this widget // that will be for painter to read information and draw proper backgrounds only when asked. // // More information: https://github.com/FreeCAD/FreeCAD/pull/13807 QTreeView *artificial; QRect calculateItemRect(const QStyleOptionViewItem &option) const; public: explicit TreeWidgetItemDelegate(QObject* parent=nullptr); virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const; virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const; virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; } // namespace Gui TreeWidgetItemDelegate::TreeWidgetItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { artificial = new QTreeView(qobject_cast(parent)); artificial->setObjectName(QStringLiteral("DocumentTreeItems")); artificial->setFixedSize(0, 0); // ensure that it does not render } QRect TreeWidgetItemDelegate::calculateItemRect(const QStyleOptionViewItem &option) const { auto tree = static_cast(parent()); auto style = tree->style(); QRect rect = option.rect; const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, artificial) + 1; // 2 margin for text, 2 margin for decoration (icon) = 4 times margin int width = 4 * margin + option.fontMetrics.boundingRect(option.text).width() + option.decorationSize.width() + TreeParams::getItemBackgroundPadding() ; if (TreeParams::getCheckBoxesSelection()) { // another 2 margin for checkbox width += 2 * margin + style->pixelMetric(QStyle::PM_IndicatorWidth) + style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); } if (width < rect.width()) { rect.setWidth(width); } return rect; } void TreeWidgetItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); auto tree = static_cast(parent()); auto style = tree->style(); // If only the first column is shown, we'll trim the color background when // rendering as transparent overlay. bool trimColumnSize = isOnlyNameColumnDisplayed(); if (index.column() == 0) { if (tree->testAttribute(Qt::WA_NoSystemBackground) && (trimColumnSize || (opt.backgroundBrush.style() == Qt::NoBrush && _TreeItemBackground.style() != Qt::NoBrush))) { QRect rect = calculateItemRect(option); if (trimColumnSize && opt.backgroundBrush.style() == Qt::NoBrush) { painter->fillRect(rect, _TreeItemBackground); } else if (!opt.state.testFlag(QStyle::State_Selected)) { painter->fillRect(rect, _TreeItemBackground); } } } style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, artificial); } void TreeWidgetItemDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const { inherited::initStyleOption(option, index); auto tree = static_cast(parent()); auto item = tree->itemFromIndex(index); if (!item) { return; } auto mousePos = option->widget->mapFromGlobal(QCursor::pos()); auto isHovered = option->rect.contains(mousePos); if (!isHovered) { option->state &= ~QStyle::State_MouseOver; } QSize size = option->icon.actualSize(QSize(0xffff, 0xffff)); if (size.height() > 0) { option->decorationSize = QSize( size.width() * TreeWidget::iconSize() / size.height(), TreeWidget::iconSize() ); } if (isOnlyNameColumnDisplayed()) { option->rect = calculateItemRect(*option); // we need to extend this shape a bit, 3px on each side // this value was obtained experimentally option->rect.setWidth(option->rect.width() + 3 * 2); } } class DynamicQLineEdit : public ExpLineEdit { public: DynamicQLineEdit(QWidget *parent = nullptr) : ExpLineEdit(parent) {} QSize sizeHint() const override { QSize size = QLineEdit::sizeHint(); QFontMetrics fm(font()); int availableWidth = parentWidget()->width() - geometry().x(); // Calculate available width int margin = 2 * (style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) + 2 * style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing) + TreeParams::getItemBackgroundPadding(); size.setWidth(std::min(fm.horizontalAdvance(text()) + margin , availableWidth)); return size; } // resize on key presses void keyPressEvent(QKeyEvent *event) override { ExpLineEdit::keyPressEvent(event); setMinimumWidth(sizeHint().width()); } }; QWidget* TreeWidgetItemDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const { auto ti = static_cast(index.internalPointer()); if (ti->type() != TreeWidget::ObjectType || index.column() > 1) return nullptr; auto item = static_cast(ti); App::DocumentObject* obj = item->object()->getObject(); auto& prop = index.column() ? obj->Label2 : obj->Label; std::ostringstream str; str << "Change " << obj->getNameInDocument() << '.' << prop.getName(); App::GetApplication().setActiveTransaction(str.str().c_str()); FC_LOG("create editor transaction " << App::GetApplication().getActiveTransaction()); DynamicQLineEdit *editor; if(TreeParams::getLabelExpression()) { DynamicQLineEdit *le = new DynamicQLineEdit(parent); le->setAutoApply(true); le->setFrame(false); le->bind(App::ObjectIdentifier(prop)); editor = le; } else { editor = new DynamicQLineEdit(parent); } editor->setReadOnly(prop.isReadOnly()); return editor; } QSize TreeWidgetItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); int spacing = std::max(0, static_cast(TreeParams::getItemSpacing())); size.setHeight(size.height() + spacing); return size; } // --------------------------------------------------------------------------- TreeWidget::TreeWidget(const char* name, QWidget* parent) : QTreeWidget(parent), SelectionObserver(true, ResolveMode::NoResolve) , contextItem(nullptr) , searchObject(nullptr) , searchDoc(nullptr) , searchContextDoc(nullptr) , editingItem(nullptr) , currentDocItem(nullptr) , myName(name) { Instances.insert(this); if (!_LastSelectedTreeWidget) _LastSelectedTreeWidget = this; this->setDragEnabled(true); this->setAcceptDrops(true); this->setColumnCount(3); this->setItemDelegate(new TreeWidgetItemDelegate(this)); this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); this->showHiddenAction = new QAction(this); this->showHiddenAction->setCheckable(true); connect(this->showHiddenAction, &QAction::triggered, this, &TreeWidget::onShowHidden); this->toggleVisibilityInTreeAction = new QAction(this); connect(this->toggleVisibilityInTreeAction, &QAction::triggered, this, &TreeWidget::onToggleVisibilityInTree); this->createGroupAction = new QAction(this); connect(this->createGroupAction, &QAction::triggered, this, &TreeWidget::onCreateGroup); this->relabelObjectAction = new QAction(this); #ifndef Q_OS_MAC this->relabelObjectAction->setShortcut(Qt::Key_F2); #endif connect(this->relabelObjectAction, &QAction::triggered, this, &TreeWidget::onRelabelObject); this->finishEditingAction = new QAction(this); connect(this->finishEditingAction, &QAction::triggered, this, &TreeWidget::onFinishEditing); this->selectDependentsAction = new QAction(this); connect(this->selectDependentsAction, &QAction::triggered, this, &TreeWidget::onSelectDependents); this->closeDocAction = new QAction(this); connect(this->closeDocAction, &QAction::triggered, this, &TreeWidget::onCloseDoc); this->reloadDocAction = new QAction(this); connect(this->reloadDocAction, &QAction::triggered, this, &TreeWidget::onReloadDoc); this->skipRecomputeAction = new QAction(this); this->skipRecomputeAction->setCheckable(true); connect(this->skipRecomputeAction, &QAction::toggled, this, &TreeWidget::onSkipRecompute); this->allowPartialRecomputeAction = new QAction(this); this->allowPartialRecomputeAction->setCheckable(true); connect(this->allowPartialRecomputeAction, &QAction::toggled, this, &TreeWidget::onAllowPartialRecompute); this->markRecomputeAction = new QAction(this); connect(this->markRecomputeAction, &QAction::triggered, this, &TreeWidget::onMarkRecompute); this->recomputeObjectAction = new QAction(this); connect(this->recomputeObjectAction, &QAction::triggered, this, &TreeWidget::onRecomputeObject); this->searchObjectsAction = new QAction(this); this->searchObjectsAction->setText(tr("Search...")); this->searchObjectsAction->setStatusTip(tr("Search for objects")); connect(this->searchObjectsAction, &QAction::triggered, this, &TreeWidget::onSearchObjects); this->openFileLocationAction = new QAction(this); connect(this->openFileLocationAction, &QAction::triggered, this, &TreeWidget::onOpenFileLocation); //NOLINTBEGIN // Setup connections connectNewDocument = Application::Instance->signalNewDocument.connect(std::bind(&TreeWidget::slotNewDocument, this, sp::_1, sp::_2)); connectDelDocument = Application::Instance->signalDeleteDocument.connect(std::bind(&TreeWidget::slotDeleteDocument, this, sp::_1)); connectRenDocument = Application::Instance->signalRenameDocument.connect(std::bind(&TreeWidget::slotRenameDocument, this, sp::_1)); connectActDocument = Application::Instance->signalActiveDocument.connect(std::bind(&TreeWidget::slotActiveDocument, this, sp::_1)); connectRelDocument = Application::Instance->signalRelabelDocument.connect(std::bind(&TreeWidget::slotRelabelDocument, this, sp::_1)); connectShowHidden = Application::Instance->signalShowHidden.connect(std::bind(&TreeWidget::slotShowHidden, this, sp::_1)); // Gui::Document::signalChangedObject informs the App::Document property // change, not view provider's own property, which is what the signal below // for connectChangedViewObj = Application::Instance->signalChangedObject.connect( std::bind(&TreeWidget::slotChangedViewObject, this, sp::_1, sp::_2)); //NOLINTEND setupResizableColumn(this); this->header()->setStretchLastSection(true); QObject::connect(this->header(), &QHeaderView::sectionResized, [](int idx, int, int newSize) { if (idx == 1) TreeParams::setColumnSize2(newSize); else if (idx == 2) TreeParams::setColumnSize3(newSize); else TreeParams::setColumnSize1(newSize); }); // Add the first main label this->rootItem = invisibleRootItem(); this->expandItem(this->rootItem); this->setSelectionMode(QAbstractItemView::ExtendedSelection); this->setMouseTracking(true); // needed for itemEntered() to work this->preselectTimer = new QTimer(this); this->preselectTimer->setSingleShot(true); this->statusTimer = new QTimer(this); this->statusTimer->setSingleShot(false); this->selectTimer = new QTimer(this); this->selectTimer->setSingleShot(true); connect(this->statusTimer, &QTimer::timeout, this, &TreeWidget::onUpdateStatus); connect(this, &QTreeWidget::itemEntered, this, &TreeWidget::onItemEntered); connect(this, &QTreeWidget::itemCollapsed, this, &TreeWidget::onItemCollapsed); connect(this, &QTreeWidget::itemExpanded, this, &TreeWidget::onItemExpanded); connect(this, &QTreeWidget::itemSelectionChanged, this, &TreeWidget::onItemSelectionChanged); connect(this, &QTreeWidget::itemChanged, this, &TreeWidget::onItemChanged); connect(this->preselectTimer, &QTimer::timeout, this, &TreeWidget::onPreSelectTimer); connect(this->selectTimer, &QTimer::timeout, this, &TreeWidget::onSelectTimer); preselectTime.start(); setupText(); if (!documentPixmap) { documentPixmap = std::make_unique(Gui::BitmapFactory().pixmap("Document")); QIcon icon(*documentPixmap); documentPartialPixmap = std::make_unique(icon.pixmap(documentPixmap->size(), QIcon::Disabled)); } setColumnHidden(1, TreeParams::getHideColumn()); setColumnHidden(2, TreeParams::getHideInternalNames()); header()->setVisible(!TreeParams::getHideColumn() || !TreeParams::getHideInternalNames()); TreeParams::onFontSizeChanged(); } TreeWidget::~TreeWidget() { connectNewDocument.disconnect(); connectDelDocument.disconnect(); connectRenDocument.disconnect(); connectActDocument.disconnect(); connectRelDocument.disconnect(); connectShowHidden.disconnect(); connectChangedViewObj.disconnect(); Instances.erase(this); if (_LastSelectedTreeWidget == this) _LastSelectedTreeWidget = nullptr; } const char* TreeWidget::getTreeName() const { return myName.c_str(); } // reimpelement to select only objects in the active document void TreeWidget::selectAll() { auto gdoc = Application::Instance->getDocument( App::GetApplication().getActiveDocument()); if (!gdoc) return; auto itDoc = DocumentMap.find(gdoc); if (itDoc == DocumentMap.end()) return; if (TreeParams::getRecordSelection()) Gui::Selection().selStackPush(); Gui::Selection().clearSelection(); Gui::Selection().setSelection(gdoc->getDocument()->getName(), gdoc->getDocument()->getObjects()); } bool TreeWidget::isObjectShowable(App::DocumentObject* obj) { if (!obj || !obj->isAttachedToDocument()) return true; Gui::Document* doc = Application::Instance->getDocument(obj->getDocument()); if (!doc) return true; if (Instances.empty()) return true; auto tree = *Instances.begin(); auto it = tree->DocumentMap.find(doc); if (it != tree->DocumentMap.end()) return it->second->isObjectShowable(obj); return true; } static bool _DisableCheckTopParent; void TreeWidget::checkTopParent(App::DocumentObject*& obj, std::string& subname) { if (_DisableCheckTopParent) return; if (!Instances.empty() && obj && obj->isAttachedToDocument()) { auto tree = *Instances.begin(); auto it = tree->DocumentMap.find(Application::Instance->getDocument(obj->getDocument())); if (it != tree->DocumentMap.end()) { if (tree->statusTimer->isActive()) { bool locked = tree->blockSelection(true); tree->_updateStatus(false); tree->blockSelection(locked); } auto parent = it->second->getTopParent(obj, subname); if (parent) obj = parent; } } } void TreeWidget::resetItemSearch() { if (!searchObject) return; auto it = ObjectTable.find(searchObject); if (it != ObjectTable.end()) { for (auto& data : it->second) { if (!data) continue; for (auto item : data->items) static_cast(item)->restoreBackground(); } } searchObject = nullptr; } void TreeWidget::startItemSearch(QLineEdit* edit) { resetItemSearch(); searchDoc = nullptr; searchContextDoc = nullptr; auto sels = selectedItems(); if (sels.size() == 1) { if (sels.front()->type() == DocumentType) { searchDoc = static_cast(sels.front())->document(); } else if (sels.front()->type() == ObjectType) { auto item = static_cast(sels.front()); searchDoc = item->object()->getDocument(); searchContextDoc = item->getOwnerDocument()->document(); } } else searchDoc = Application::Instance->activeDocument(); App::DocumentObject* obj = nullptr; if (searchContextDoc && !searchContextDoc->getDocument()->getObjects().empty()) obj = searchContextDoc->getDocument()->getObjects().front(); else if (searchDoc && !searchDoc->getDocument()->getObjects().empty()) obj = searchDoc->getDocument()->getObjects().front(); if (obj) static_cast(edit)->setDocumentObject(obj); } void TreeWidget::itemSearch(const QString& text, bool select) { resetItemSearch(); auto docItem = getDocumentItem(searchDoc); if (!docItem) { docItem = getDocumentItem(Application::Instance->activeDocument()); if (!docItem) { FC_TRACE("item search no document"); resetItemSearch(); return; } } auto doc = docItem->document()->getDocument(); const auto& objs = doc->getObjects(); if (objs.empty()) { FC_TRACE("item search no objects"); return; } std::string txt(text.toUtf8().constData()); try { if (txt.empty()) return; if (txt.find("<<") == std::string::npos) { auto pos = txt.find('.'); if (pos == std::string::npos) txt += '.'; else if (pos != txt.size() - 1) { txt.insert(pos + 1, "<<"); if (txt.back() != '.') txt += '.'; txt += ">>."; } } else if (txt.back() != '.') txt += '.'; txt += "_self"; auto path = App::ObjectIdentifier::parse(objs.front(), txt); if (path.getPropertyName() != "_self") { FC_TRACE("Object " << txt << " not found in " << doc->getName()); return; } auto obj = path.getDocumentObject(); if (!obj) { FC_TRACE("Object " << txt << " not found in " << doc->getName()); return; } std::string subname = path.getSubObjectName(); App::DocumentObject* parent = nullptr; if (searchContextDoc) { auto it = DocumentMap.find(searchContextDoc); if (it != DocumentMap.end()) { parent = it->second->getTopParent(obj, subname); if (parent) { obj = parent; docItem = it->second; doc = docItem->document()->getDocument(); } } } if (!parent) { parent = docItem->getTopParent(obj, subname); while (!parent) { if (docItem->document()->getDocument() == obj->getDocument()) { // this shouldn't happen FC_LOG("Object " << txt << " not found in " << doc->getName()); return; } auto it = DocumentMap.find(Application::Instance->getDocument(obj->getDocument())); if (it == DocumentMap.end()) return; docItem = it->second; parent = docItem->getTopParent(obj, subname); } obj = parent; } auto item = docItem->findItemByObject(true, obj, subname.c_str()); if (!item) { FC_TRACE("item " << txt << " not found in " << doc->getName()); return; } scrollToItem(item); Selection().setPreselect(obj->getDocument()->getName(), obj->getNameInDocument(), subname.c_str(), 0, 0, 0, SelectionChanges::MsgSource::TreeView); if (select) { Gui::Selection().selStackPush(); Gui::Selection().clearSelection(); Gui::Selection().addSelection(obj->getDocument()->getName(), obj->getNameInDocument(), subname.c_str()); Gui::Selection().selStackPush(); } else { searchObject = item->object()->getObject(); item->setBackground(0, QColor(255, 255, 0, 100)); } FC_TRACE("found item " << txt); } catch (...) { FC_TRACE("item " << txt << " search exception in " << doc->getName()); } } Gui::Document* TreeWidget::selectedDocument() { for (auto tree : Instances) { if (!tree->isVisible()) continue; auto sels = tree->selectedItems(); if (sels.size() == 1 && sels[0]->type() == DocumentType) return static_cast(sels[0])->document(); } return nullptr; } void TreeWidget::updateStatus(bool delay) { for (auto tree : Instances) tree->_updateStatus(delay); } void TreeWidget::_updateStatus(bool delay) { // When running from a different thread Qt will raise a warning // when trying to start the QTimer if (Q_UNLIKELY(thread() != QThread::currentThread())) { return; } if (!delay) { if (!ChangedObjects.empty() || !NewObjects.empty()) onUpdateStatus(); return; } int timeout = TreeParams::getStatusTimeout(); if (timeout < 0) timeout = 1; statusTimer->start(timeout); } void TreeWidget::contextMenuEvent(QContextMenuEvent* e) { // ask workbenches and view provider, ... MenuItem view; Gui::Application::Instance->setupContextMenu("Tree", &view); view << "Std_Properties" << "Separator" << "Std_Expressions"; Workbench::createLinkMenu(&view); QMenu contextMenu; QMenu subMenu; QMenu editMenu; QActionGroup subMenuGroup(&subMenu); subMenuGroup.setExclusive(true); connect(&subMenuGroup, &QActionGroup::triggered, this, &TreeWidget::onActivateDocument); MenuManager::getInstance()->setupContextMenu(&view, contextMenu); // get the current item this->contextItem = itemAt(e->pos()); if (this->contextItem && this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); // It's better to let user decide whether and how to activate // the current document, such as by double-clicking. // App::GetApplication().setActiveDocument(doc); showHiddenAction->setChecked(docitem->showHidden()); contextMenu.addAction(this->showHiddenAction); contextMenu.addAction(this->openFileLocationAction); contextMenu.addAction(this->searchObjectsAction); contextMenu.addAction(this->closeDocAction); if (doc->testStatus(App::Document::PartialDoc)) contextMenu.addAction(this->reloadDocAction); else { for (auto d : doc->getDependentDocuments()) { if (d->testStatus(App::Document::PartialDoc)) { contextMenu.addAction(this->reloadDocAction); break; } } contextMenu.addAction(this->selectDependentsAction); this->skipRecomputeAction->setChecked(doc->testStatus(App::Document::SkipRecompute)); contextMenu.addAction(this->skipRecomputeAction); this->allowPartialRecomputeAction->setChecked(doc->testStatus(App::Document::AllowPartialRecompute)); if (doc->testStatus(App::Document::SkipRecompute)) contextMenu.addAction(this->allowPartialRecomputeAction); contextMenu.addAction(this->markRecomputeAction); contextMenu.addAction(this->createGroupAction); } contextMenu.addSeparator(); } else if (this->contextItem && this->contextItem->type() == ObjectType) { auto objitem = static_cast (this->contextItem); // check that the selection is not across several documents bool acrossDocuments = false; auto SelectedObjectsList = Selection().getCompleteSelection(); // get the object's document as reference App::Document* doc = objitem->object()->getObject()->getDocument(); for (auto it = SelectedObjectsList.begin(); it != SelectedObjectsList.end(); ++it) { if ((*it).pDoc != doc) { acrossDocuments = true; break; } } showHiddenAction->setChecked(doc->ShowHidden.getValue()); contextMenu.addAction(this->showHiddenAction); contextMenu.addAction(this->toggleVisibilityInTreeAction); if (!acrossDocuments) { // is only sensible for selections within one document if (objitem->object()->getObject()->isDerivedFrom()) contextMenu.addAction(this->createGroupAction); // if there are dependent objects in the selection, add context menu to add them to selection if (CheckForDependents()) contextMenu.addAction(this->selectDependentsAction); } contextMenu.addSeparator(); contextMenu.addAction(this->markRecomputeAction); contextMenu.addAction(this->recomputeObjectAction); contextMenu.addSeparator(); // relabeling is only possible for a single selected document if (SelectedObjectsList.size() == 1) contextMenu.addAction(this->relabelObjectAction); auto selItems = this->selectedItems(); // if only one item is selected, setup the edit menu if (selItems.size() == 1) { objitem->object()->setupContextMenu(&editMenu, this, SLOT(onStartEditing())); QList editAct = editMenu.actions(); if (!editAct.isEmpty()) { QAction* topact = contextMenu.actions().constFirst(); for (QList::iterator it = editAct.begin(); it != editAct.end(); ++it) contextMenu.insertAction(topact, *it); QAction* first = editAct.front(); contextMenu.setDefaultAction(first); if (objitem->object()->isEditing()) contextMenu.insertAction(topact, this->finishEditingAction); contextMenu.insertSeparator(topact); } } } // add a submenu to active a document if two or more exist std::vector docs = App::GetApplication().getDocuments(); if (docs.size() >= 2) { contextMenu.addSeparator(); App::Document* activeDoc = App::GetApplication().getActiveDocument(); subMenu.setTitle(tr("Activate document")); contextMenu.addMenu(&subMenu); QAction* active = nullptr; for (auto it = docs.begin(); it != docs.end(); ++it) { QString label = QString::fromUtf8((*it)->Label.getValue()); QAction* action = subMenuGroup.addAction(label); action->setCheckable(true); action->setStatusTip(tr("Activate document %1").arg(label)); action->setData(QByteArray((*it)->getName())); if (*it == activeDoc) active = action; } if (active) active->setChecked(true); subMenu.addActions(subMenuGroup.actions()); } // add a submenu to present the settings of the tree. QMenu settingsMenu; settingsMenu.setTitle(tr("Tree settings")); contextMenu.addSeparator(); contextMenu.addMenu(&settingsMenu); QAction* action = new QAction(tr("Show description"), this); QAction* internalNameAction = new QAction(tr("Show internal name"), this); action->setStatusTip(tr("Show a description column for items. An item's description can be set by pressing F2 (or your OS's edit button) or by editing the 'label2' property.")); action->setCheckable(true); ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView"); action->setChecked(!hGrp->GetBool("HideColumn", true)); settingsMenu.addAction(action); QObject::connect(action, &QAction::triggered, this, [this, action, internalNameAction, hGrp]() { bool show = action->isChecked(); hGrp->SetBool("HideColumn", !show); setColumnHidden(1, !show); header()->setVisible(action->isChecked()||internalNameAction->isChecked()); }); internalNameAction->setStatusTip(tr("Show an internal name column for items.")); internalNameAction->setCheckable(true); internalNameAction->setChecked(!hGrp->GetBool("HideInternalNames", true)); settingsMenu.addAction(internalNameAction); QObject::connect(internalNameAction, &QAction::triggered, this, [this, action, internalNameAction, hGrp]() { bool show = internalNameAction->isChecked(); hGrp->SetBool("HideInternalNames", !show); setColumnHidden(2, !show); header()->setVisible(action->isChecked()||internalNameAction->isChecked()); }); if (contextMenu.actions().count() > 0) { try { contextMenu.exec(QCursor::pos()); } catch (Base::Exception& e) { e.ReportException(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); } catch (...) { FC_ERR("Unknown exception"); } contextItem = nullptr; } } void TreeWidget::hideEvent(QHideEvent* ev) { QTreeWidget::hideEvent(ev); } void TreeWidget::showEvent(QShowEvent* ev) { QTreeWidget::showEvent(ev); } void TreeWidget::onCreateGroup() { QString name = tr("Group"); App::AutoTransaction trans("Create group"); if (this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); QString cmd = QStringLiteral("App.getDocument(\"%1\").addObject" "(\"App::DocumentObjectGroup\",\"Group\").Label=\"%2\"") .arg(QString::fromLatin1(doc->getName()), name); Gui::Command::runCommand(Gui::Command::App, cmd.toUtf8()); } else if (this->contextItem->type() == ObjectType) { auto objitem = static_cast (this->contextItem); App::DocumentObject* obj = objitem->object()->getObject(); App::Document* doc = obj->getDocument(); QString cmd = QStringLiteral("App.getDocument(\"%1\").getObject(\"%2\")" ".newObject(\"App::DocumentObjectGroup\",\"Group\").Label=\"%3\"") .arg(QString::fromLatin1(doc->getName()), QString::fromLatin1(obj->getNameInDocument()), name); Gui::Command::runCommand(Gui::Command::App, cmd.toUtf8()); } } void TreeWidget::onRelabelObject() { QTreeWidgetItem* item = currentItem(); if (item) editItem(item); } void TreeWidget::onStartEditing() { auto action = qobject_cast(sender()); if (action) { if (this->contextItem && this->contextItem->type() == ObjectType) { auto objitem = static_cast (this->contextItem); int edit = action->data().toInt(); App::DocumentObject* obj = objitem->object()->getObject(); if (!obj || !obj->isAttachedToDocument()) return; auto doc = const_cast(objitem->getOwnerDocument()->document()); MDIView* view = doc->getActiveView(); if (view) getMainWindow()->setActiveWindow(view); editingItem = objitem; if (!doc->setEdit(objitem->object(), edit)) editingItem = nullptr; } } } void TreeWidget::onFinishEditing() { if (this->contextItem && this->contextItem->type() == ObjectType) { auto objitem = static_cast (this->contextItem); App::DocumentObject* obj = objitem->object()->getObject(); if (!obj) return; Gui::Document* doc = Gui::Application::Instance->getDocument(obj->getDocument()); doc->commitCommand(); doc->resetEdit(); doc->getDocument()->recompute(); } } // check if selection has dependent objects bool TreeWidget::CheckForDependents() { // if the selected object is a document if (this->contextItem && this->contextItem->type() == DocumentType) { return true; } // it can be an object else { QList items = this->selectedItems(); for (QList::iterator it = items.begin(); it != items.end(); ++it) { if ((*it)->type() == ObjectType) { auto objitem = static_cast(*it); App::DocumentObject* obj = objitem->object()->getObject(); // get dependents auto subObjectList = obj->getOutList(); if (!subObjectList.empty()) return true; } } } return false; } // adds an App::DocumentObject* and its dependent objects to the selection void TreeWidget::addDependentToSelection(App::Document* doc, App::DocumentObject* docObject) { // add the docObject to the selection Selection().addSelection(doc->getName(), docObject->getNameInDocument()); // get the dependent auto subObjectList = docObject->getOutList(); // the dependent can in turn have dependents, thus add them recursively for (auto itDepend = subObjectList.begin(); itDepend != subObjectList.end(); ++itDepend) addDependentToSelection(doc, (*itDepend)); } // add dependents of the selected tree object to selection void TreeWidget::onSelectDependents() { // We only have this context menu entry if the selection is within one document but it // might be not the active document. Therefore get the document not here but later by casting. App::Document* doc; // if the selected object is a document if (this->contextItem && this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); doc = docitem->document()->getDocument(); std::vector obj = doc->getObjects(); for (auto it = obj.begin(); it != obj.end(); ++it) Selection().addSelection(doc->getName(), (*it)->getNameInDocument()); } // it can be an object else { QList items = this->selectedItems(); for (QList::iterator it = items.begin(); it != items.end(); ++it) { if ((*it)->type() == ObjectType) { auto objitem = static_cast(*it); doc = objitem->object()->getObject()->getDocument(); App::DocumentObject* obj = objitem->object()->getObject(); // the dependents can also have dependents, thus add them recursively via a separate void addDependentToSelection(doc, obj); } } } } void TreeWidget::onSkipRecompute(bool on) { // if a document item is selected then touch all objects if (this->contextItem && this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); doc->setStatus(App::Document::SkipRecompute, on); } } void TreeWidget::onAllowPartialRecompute(bool on) { // if a document item is selected then touch all objects if (this->contextItem && this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); doc->setStatus(App::Document::AllowPartialRecompute, on); } } void TreeWidget::onMarkRecompute() { // if a document item is selected then touch all objects if (this->contextItem && this->contextItem->type() == DocumentType) { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); std::vector obj = doc->getObjects(); for (auto it = obj.begin(); it != obj.end(); ++it) (*it)->enforceRecompute(); } // mark all selected objects else { QList items = this->selectedItems(); for (QList::iterator it = items.begin(); it != items.end(); ++it) { if ((*it)->type() == ObjectType) { auto objitem = static_cast(*it); App::DocumentObject* obj = objitem->object()->getObject(); obj->enforceRecompute(); } } } } void TreeWidget::onRecomputeObject() { std::vector objs; const auto items = selectedItems(); for (auto ti : items) { if (ti->type() == ObjectType) { auto objitem = static_cast(ti); objs.push_back(objitem->object()->getObject()); objs.back()->enforceRecompute(); } } if (objs.empty()) return; App::AutoTransaction committer("Recompute object"); objs.front()->getDocument()->recompute(objs, true); } DocumentItem* TreeWidget::getDocumentItem(const Gui::Document* doc) const { auto it = DocumentMap.find(doc); if (it != DocumentMap.end()) return it->second; return nullptr; } void TreeWidget::selectAllInstances(const ViewProviderDocumentObject& vpd) { if (!isSelectionAttached()) return; if (selectTimer->isActive()) onSelectTimer(); else _updateStatus(false); for (const auto& v : DocumentMap) v.second->selectAllInstances(vpd); } static int &treeIconSize() { static int _treeIconSize = -1; if (_treeIconSize < 0) _treeIconSize = TreeParams::getIconSize(); return _treeIconSize; } int TreeWidget::iconHeight() const { return treeIconSize(); } void TreeWidget::setIconHeight(int height) { if (treeIconSize() == height) return; treeIconSize() = height; if (treeIconSize() <= 0) treeIconSize() = std::max(10, iconSize()); for(auto tree : Instances) tree->setIconSize(QSize(treeIconSize(), treeIconSize())); } int TreeWidget::iconSize() { static int defaultSize; if (defaultSize == 0) { auto tree = instance(); if(tree) { #if QT_VERSION < QT_VERSION_CHECK(6,0,0) defaultSize = tree->viewOptions().decorationSize.width(); #else QStyleOptionViewItem opt; tree->initViewItemOption(&opt); defaultSize = opt.decorationSize.width(); #endif } else { defaultSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); } } if (treeIconSize() > 0) return std::max(10, treeIconSize()); return defaultSize; } TreeWidget* TreeWidget::instance() { auto res = _LastSelectedTreeWidget; if (res && res->isVisible()) return res; for (auto inst : Instances) { if (!res) res = inst; if (inst->isVisible()) return inst; } return res; } void TreeWidget::setupResizableColumn(TreeWidget *tree) { auto mode = TreeParams::getResizableColumn()? QHeaderView::Interactive : QHeaderView::ResizeToContents; for(auto inst : Instances) { if(!tree || tree==inst) { inst->header()->setSectionResizeMode(0, mode); inst->header()->setSectionResizeMode(1, mode); inst->header()->setSectionResizeMode(2, mode); if (TreeParams::getResizableColumn()) { QSignalBlocker blocker(inst); if (TreeParams::getColumnSize1() > 0) inst->header()->resizeSection(0, TreeParams::getColumnSize1()); if (TreeParams::getColumnSize2() > 0) inst->header()->resizeSection(1, TreeParams::getColumnSize2()); if (TreeParams::getColumnSize3() > 0) inst->header()->resizeSection(2, TreeParams::getColumnSize3()); } } } } std::vector TreeWidget::getSelection(App::Document* doc) { std::vector ret; TreeWidget* tree = instance(); if (!tree || !tree->isSelectionAttached()) { for (auto pTree : Instances) if (pTree->isSelectionAttached()) { tree = pTree; break; } } if (!tree) return ret; if (tree->selectTimer->isActive()) tree->onSelectTimer(); else tree->_updateStatus(false); const auto items = tree->selectedItems(); for (auto ti : items) { if (ti->type() != ObjectType) continue; auto item = static_cast(ti); auto vp = item->object(); auto obj = vp->getObject(); if (!obj || !obj->isAttachedToDocument()) { FC_WARN("skip invalid object"); continue; } if (doc && obj->getDocument() != doc) { FC_LOG("skip objects not from current document"); continue; } ViewProviderDocumentObject* parentVp = nullptr; auto parent = item->getParentItem(); if (parent) { parentVp = parent->object(); if (!parentVp->getObject()->isAttachedToDocument()) { FC_WARN("skip '" << obj->getFullName() << "' with invalid parent"); continue; } } ret.emplace_back(); auto& sel = ret.back(); sel.topParent = nullptr; std::ostringstream ss; item->getSubName(ss, sel.topParent); if (!sel.topParent) sel.topParent = obj; else ss << obj->getNameInDocument() << '.'; sel.subname = ss.str(); sel.parentVp = parentVp; sel.vp = vp; } return ret; } void TreeWidget::selectAllLinks(App::DocumentObject* obj) { if (!isSelectionAttached()) return; if (!obj || !obj->isAttachedToDocument()) { TREE_ERR("invalid object"); return; } if (selectTimer->isActive()) onSelectTimer(); else _updateStatus(false); for (auto link : App::GetApplication().getLinksTo(obj, App::GetLinkRecursive)) { if (!link || !link->isAttachedToDocument()) { TREE_ERR("invalid linked object"); continue; } auto vp = dynamic_cast( Application::Instance->getViewProvider(link)); if (!vp) { TREE_ERR("invalid view provider of the linked object"); continue; } for (auto& v : DocumentMap) v.second->selectAllInstances(*vp); } } void TreeWidget::onSearchObjects() { Q_EMIT emitSearchObjects(); } void TreeWidget::onActivateDocument(QAction* active) { // activate the specified document QByteArray docname = active->data().toByteArray(); Gui::Document* doc = Application::Instance->getDocument((const char*)docname); if (doc && !doc->setActiveView()) doc->setActiveView(nullptr, View3DInventor::getClassTypeId()); } Qt::DropActions TreeWidget::supportedDropActions() const { return Qt::LinkAction | Qt::CopyAction | Qt::MoveAction; } bool TreeWidget::event(QEvent* e) { return QTreeWidget::event(e); } bool TreeWidget::eventFilter(QObject*, QEvent* ev) { switch (ev->type()) { case QEvent::KeyPress: case QEvent::KeyRelease: { auto ke = static_cast(ev); if (ke->key() != Qt::Key_Escape) { // Qt 5 only recheck key modifier on mouse move, so generate a fake // event to trigger drag cursor change auto mouseEvent = new QMouseEvent(QEvent::MouseMove, mapFromGlobal(QCursor::pos()), QCursor::pos(), Qt::NoButton, QApplication::mouseButtons(), QApplication::queryKeyboardModifiers()); QApplication::postEvent(this, mouseEvent); } break; } default: break; } return false; } namespace Gui { bool isTreeViewDragging() { return _DraggingActive; } } // namespace Gui void TreeWidget::keyPressEvent(QKeyEvent* event) { if (event->matches(QKeySequence::Find)) { event->accept(); onSearchObjects(); return; } else if (event->modifiers() == Qt::AltModifier) { if (event->key() == Qt::Key_Left) { for (auto& item : selectedItems()) { item->setExpanded(false); } event->accept(); return; } else if (event->key() == Qt::Key_Right) { for (auto& item : selectedItems()) { item->setExpanded(true); } event->accept(); return; } else if (event->key() == Qt::Key_Up) { for (auto& item : selectedItems()) { item->setExpanded(true); for (auto& child : childrenOfItem(*item)) { child->setExpanded(false); } } event->accept(); return; } else if (event->key() == Qt::Key_Down) { for (auto& item : selectedItems()) { item->setExpanded(true); for (auto& child : childrenOfItem(*item)) { child->setExpanded(true); } } event->accept(); return; } } QTreeWidget::keyPressEvent(event); } void TreeWidget::mousePressEvent(QMouseEvent* event) { if (isVisibilityIconEnabled()) { QTreeWidgetItem* item = itemAt(event->pos()); if (item && item->type() == TreeWidget::ObjectType && event->button() == Qt::LeftButton) { auto objitem = static_cast(item); // Mouse position relative to viewport auto mousePos = event->pos(); // Rect occupied by the item relative to viewport auto iconRect = visualItemRect(objitem); auto style = this->style(); // If the checkboxes are visible, these are displayed before the icon // and we have to compensate for its width. if (isSelectionCheckBoxesEnabled()) { int checkboxWidth = style->pixelMetric(QStyle::PM_IndicatorWidth) + style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); iconRect.adjust(checkboxWidth, 0, 0, 0); } int const margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; iconRect.adjust(margin, 0, 0, 0); // We are interested in the first icon (visibility icon) iconRect.setWidth(iconSize()); // If the visibility icon was clicked, toggle the DocumentObject visibility if (iconRect.contains(mousePos)) { auto obj = objitem->object()->getObject(); char const* objname = obj->getNameInDocument(); App::DocumentObject* parent = nullptr; std::ostringstream subName; objitem->getSubName(subName, parent); // Try the ElementVisible API, if that is not supported toggle the Visibility property int visible = -1; if (parent) { visible = parent->isElementVisible(objname); } if (parent && visible >= 0) { parent->setElementVisible(objname, !visible); } else { visible = obj->Visibility.getValue(); obj->Visibility.setValue(!visible); } // to prevent selection of the item via QTreeWidget::mousePressEvent event->accept(); return; } } } QTreeWidget::mousePressEvent(event); } void TreeWidget::mouseDoubleClickEvent(QMouseEvent* event) { QTreeWidgetItem* item = itemAt(event->pos()); if (!item) return; try { if (item->type() == TreeWidget::DocumentType) { Gui::Document* doc = static_cast(item)->document(); if (!doc) return; if (doc->getDocument()->testStatus(App::Document::PartialDoc)) { contextItem = item; onReloadDoc(); return; } if (!doc->setActiveView()) doc->setActiveView(nullptr, View3DInventor::getClassTypeId()); } else if (item->type() == TreeWidget::ObjectType) { auto objitem = static_cast(item); ViewProviderDocumentObject* vp = objitem->object(); objitem->getOwnerDocument()->document()->setActiveView(vp); auto manager = Application::Instance->macroManager(); auto lines = manager->getLines(); std::ostringstream ss; ss << Command::getObjectCmd(vp->getObject()) << ".ViewObject.doubleClicked()"; const char* commandText = vp->getTransactionText(); if (commandText) { auto editDoc = Application::Instance->editDocument(); App::AutoTransaction committer(commandText, true); if (!vp->doubleClicked()) QTreeWidget::mouseDoubleClickEvent(event); else if (lines == manager->getLines()) manager->addLine(MacroManager::Gui, ss.str().c_str()); // If the double click starts an editing, let the transaction persist if (!editDoc && Application::Instance->editDocument()) committer.setEnable(false); } else { if (!vp->doubleClicked()) QTreeWidget::mouseDoubleClickEvent(event); else if (lines == manager->getLines()) manager->addLine(MacroManager::Gui, ss.str().c_str()); } } } catch (Base::Exception& e) { e.ReportException(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); } catch (...) { FC_ERR("Unknown exception"); } } void TreeWidget::startDragging() { if (state() != NoState) return; if (selectedItems().empty()) return; setState(DraggingState); startDrag(model()->supportedDragActions()); setState(NoState); stopAutoScroll(); } void TreeWidget::startDrag(Qt::DropActions supportedActions) { Base::StateLocker guard(_DraggingActive); QTreeWidget::startDrag(supportedActions); if (_DragEventFilter) { _DragEventFilter = false; qApp->removeEventFilter(this); } } bool TreeWidget::dropMimeData(QTreeWidgetItem* parent, int index, const QMimeData* data, Qt::DropAction action) { return QTreeWidget::dropMimeData(parent, index, data, action); } void TreeWidget::dragEnterEvent(QDragEnterEvent* event) { QTreeWidget::dragEnterEvent(event); } void TreeWidget::dragLeaveEvent(QDragLeaveEvent* event) { QTreeWidget::dragLeaveEvent(event); } struct ItemInfo { std::string doc; std::string obj; std::string parentDoc; std::string parent; std::string ownerDoc; std::string owner; std::string subname; std::string topDoc; std::string topObj; std::string topSubname; std::vector subs; bool dragging = false; }; struct ItemInfo2 { std::string doc; std::string obj; std::string parentDoc; std::string parent; std::string topDoc; std::string topObj; std::string topSubname; }; namespace { class DropHandler { public: static std::vector > > filterItems(const QList& sels, QTreeWidgetItem* targetItem) { std::vector > > items; items.reserve(sels.size()); for (auto ti : sels) { if (ti->type() != TreeWidget::ObjectType) continue; // ignore child elements if the parent is selected if (sels.contains(ti->parent())) continue; if (ti == targetItem) continue; auto item = static_cast(ti); items.emplace_back(); auto& info = items.back(); info.first = item; const auto& subnames = item->getSubNames(); info.second.insert(info.second.end(), subnames.begin(), subnames.end()); } return items; } static App::PropertyPlacement* getPlacement(const ItemInfo& info, const App::DocumentObject* obj, Base::Matrix4D& mat) { App::PropertyPlacement* propPlacement = nullptr; if (!info.topObj.empty()) { auto doc = App::GetApplication().getDocument(info.topDoc.c_str()); if (doc) { auto topObj = doc->getObject(info.topObj.c_str()); if (topObj) { auto sobj = topObj->getSubObject(info.topSubname.c_str(), nullptr, &mat); if (sobj == obj) { propPlacement = freecad_cast( obj->getPropertyByName("Placement")); } } } } else { propPlacement = freecad_cast( obj->getPropertyByName("Placement")); if (propPlacement) mat = propPlacement->getValue().toMatrix(); } return propPlacement; } }; QPoint getPos(QEvent* event) { if (auto* dragMoveEvent = dynamic_cast(event)) { #if QT_VERSION < QT_VERSION_CHECK(6,0,0) return dragMoveEvent->pos(); #else return dragMoveEvent->position().toPoint(); #endif } else if (auto* dropEvent = dynamic_cast(event)) { #if QT_VERSION < QT_VERSION_CHECK(6,0,0) return dropEvent->pos(); #else return dropEvent->position().toPoint(); #endif } // For unsupported event types or if casting fails return QPoint(-1, -1); } Qt::DropAction getDropAction(int size, const int type) { if (QApplication::keyboardModifiers() == Qt::ControlModifier) { return Qt::CopyAction; } else if (QApplication::keyboardModifiers() == Qt::AltModifier && (size == 1 || type == TreeWidget::DocumentType)) { return Qt::LinkAction; } else { return Qt::MoveAction; } } } void TreeWidget::dragMoveEvent(QDragMoveEvent* event) { // Qt5 does not change drag cursor in response to modifier key press, // because QDrag installs a event filter that eats up key event. We install // a filter after Qt and generate fake mouse move event in response to key // press event, which triggers QDrag to update its cursor if (!_DragEventFilter) { _DragEventFilter = true; qApp->installEventFilter(this); } QTreeWidget::dragMoveEvent(event); if (!event->isAccepted()) { //return; // QTreeWidget::dragMoveEvent is rejecting the event when in between items // at DocumentItem root level. Which is preventing reordering. To work around // we accept for now, then reject below if targetItem not found. event->accept(); } TargetItemInfo targetInfo = getTargetInfo(event); QTreeWidgetItem* targetItem = targetInfo.targetItem; if (!targetItem) { event->ignore(); return; } auto items = selectedItems(); auto da = getDropAction(items.size(), targetItem->type()); event->setDropAction(da); if (targetItem->type() == TreeWidget::DocumentType) { leaveEvent(nullptr); } else if (targetItem->type() == TreeWidget::ObjectType) { onItemEntered(targetItem); auto targetItemObj = static_cast(targetItem); Gui::ViewProviderDocumentObject* vp = targetItemObj->object(); // if we are in between or if target doesn't accept drops then the target is the parent if (da == Qt::MoveAction && (targetInfo.inThresholdZone || !vp->canDropObjects())) { targetInfo.targetItem = targetInfo.targetItem->parent(); if (targetInfo.targetItem->type() == TreeWidget::DocumentType) { leaveEvent(nullptr); return; } targetItemObj = static_cast(targetInfo.targetItem); vp = targetItemObj->object(); if (!vp) { TREE_TRACE("cannot drop"); return; } } try { if (da != Qt::LinkAction && !vp->canDropObjects()) { if (!(event->possibleActions() & Qt::LinkAction) || items.size() != 1) { TREE_TRACE("Cannot drop here"); event->ignore(); return; } } for (auto ti : items) { if (ti->type() != TreeWidget::ObjectType) { TREE_TRACE("cannot drop"); event->ignore(); return; } auto item = static_cast(ti); auto obj = item->object()->getObject(); if (da == Qt::MoveAction) { // Check if item can be dragged from his parent auto parentItem = item->getParentItem(); if (parentItem && !(parentItem->object()->canDragObjects() && parentItem->object()->canDragObject(obj))) { TREE_TRACE("Cannot drag object"); event->ignore(); return; } } std::ostringstream str; auto owner = item->getRelativeParent(str, targetItemObj); auto subname = str.str(); // let the view provider decide to accept the object or ignore it if (da != Qt::LinkAction && !vp->canDropObjectEx(obj, owner, subname.c_str(), item->mySubs)) { TREE_TRACE("cannot drop " << obj->getFullName() << ' ' << (owner ? owner->getFullName() : "") << '.' << subname); event->ignore(); return; } } } catch (Base::Exception& e) { e.ReportException(); event->ignore(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); event->ignore(); } catch (...) { FC_ERR("Unknown exception"); event->ignore(); } } else { leaveEvent(nullptr); event->ignore(); } } TreeWidget::TargetItemInfo TreeWidget::getTargetInfo(QEvent* ev) { TargetItemInfo targetInfo; QPoint pos = getPos(ev); if (pos == QPoint(-1, -1)) { return {}; // Return an empty struct } targetInfo.targetItem = itemAt(pos); // not dropped onto an item or one of the source items is also the destination item if (!targetInfo.targetItem || targetInfo.targetItem->isSelected()) { return {}; } targetInfo.underMouseItem = targetInfo.targetItem; if (targetInfo.targetItem->type() == TreeWidget::ObjectType) { auto targetItemObj = static_cast(targetInfo.targetItem); targetInfo.targetDoc = targetItemObj->getOwnerDocument()->document()->getDocument(); } else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) { auto targetDocItem = static_cast(targetInfo.targetItem); targetInfo.targetDoc = targetDocItem->document()->getDocument(); } else { return {}; } // Calculate the position of the mouse relative to the item's rectangle QRect itemRect = visualItemRect(targetInfo.targetItem); int mouseY = pos.y(); int itemMidPoint = itemRect.top() + itemRect.height() / 2; int threshold = itemRect.height() * 0.20; // 20% of the item's height as threshold targetInfo.inBottomHalf = mouseY > itemMidPoint; targetInfo.inThresholdZone = ((mouseY < itemRect.top() + threshold) && !targetInfo.targetItem->isExpanded()) || (mouseY > itemRect.top() + itemRect.height() - threshold); return targetInfo; } bool TreeWidget::dropInDocument(QDropEvent* event, TargetItemInfo& targetInfo, std::vector items) { std::string errMsg; auto da = event->dropAction(); bool touched = false; std::vector infos; infos.reserve(items.size()); bool syncPlacement = TreeParams::getSyncPlacement(); App::AutoTransaction committer( da == Qt::LinkAction ? "Link object" : da == Qt::CopyAction ? "Copy object" : "Move object"); // check if items can be dragged for (auto& v : items) { auto item = v.first; auto obj = item->object()->getObject(); auto parentItem = item->getParentItem(); if (parentItem) { bool allParentsOK = canDragFromParents(parentItem, obj, nullptr); if (!allParentsOK || !parentItem->object()->canDragObjects() || !parentItem->object()->canDragObject(obj)) { committer.close(true); TREE_ERR("'" << obj->getFullName() << "' cannot be dragged out of '" << parentItem->object()->getObject()->getFullName() << "'"); return false; } } else if (da != Qt::MoveAction || item->myOwner != targetInfo.targetItem) { // We will not drag item out of parent if either, 1) modifier // key is held, or 2) the dragging item is not inside the // dropping document tree. parentItem = nullptr; } infos.emplace_back(); auto& info = infos.back(); info.doc = obj->getDocument()->getName(); info.obj = obj->getNameInDocument(); if (parentItem) { auto parent = parentItem->object()->getObject(); info.parentDoc = parent->getDocument()->getName(); info.parent = parent->getNameInDocument(); } if (syncPlacement) { std::ostringstream ss; App::DocumentObject* topParent = nullptr; item->getSubName(ss, topParent); if (topParent) { info.topDoc = topParent->getDocument()->getName(); info.topObj = topParent->getNameInDocument(); ss << obj->getNameInDocument() << '.'; info.topSubname = ss.str(); } } } // Because the existence of subname, we must de-select the drag the // object manually. Just do a complete clear here for simplicity Selection().selStackPush(); Selection().clearCompleteSelection(); // Open command auto manager = Application::Instance->macroManager(); try { std::vector droppedObjs; for (auto& info : infos) { auto doc = App::GetApplication().getDocument(info.doc.c_str()); if (!doc) continue; auto obj = doc->getObject(info.obj.c_str()); auto vpc = dynamic_cast(Application::Instance->getViewProvider(obj)); if (!vpc) { FC_WARN("Cannot find dragging object " << info.obj); continue; } Base::Matrix4D mat; App::PropertyPlacement* propPlacement = nullptr; if (syncPlacement) { if (!info.topObj.empty()) { auto doc = App::GetApplication().getDocument(info.topDoc.c_str()); if (doc) { auto topObj = doc->getObject(info.topObj.c_str()); if (topObj) { auto sobj = topObj->getSubObject(info.topSubname.c_str(), nullptr, &mat); if (sobj == obj) { propPlacement = dynamic_cast(obj->getPropertyByName("Placement")); } } } } else { propPlacement = dynamic_cast(obj->getPropertyByName("Placement")); if (propPlacement) { mat = propPlacement->getValue().toMatrix(); } } } if (da == Qt::LinkAction) { std::string name = targetInfo.targetDoc->getUniqueObjectName("Link"); FCMD_DOC_CMD(targetInfo.targetDoc, "addObject('App::Link','" << name << "').setLink(" << Command::getObjectCmd(obj) << ")"); auto link = targetInfo.targetDoc->getObject(name.c_str()); if (!link) continue; FCMD_OBJ_CMD(link, "Label='" << obj->getLinkedObject(true)->Label.getValue() << "'"); propPlacement = dynamic_cast(link->getPropertyByName("Placement")); if (propPlacement) propPlacement->setValueIfChanged(Base::Placement(mat)); droppedObjs.push_back(link); } else if (!info.parent.empty()) { auto parentDoc = App::GetApplication().getDocument(info.parentDoc.c_str()); if (!parentDoc) { FC_WARN("Canont find document " << info.parentDoc); continue; } auto parent = parentDoc->getObject(info.parent.c_str()); auto vpp = dynamic_cast(Application::Instance->getViewProvider(parent)); if (!vpp) { FC_WARN("Cannot find dragging object's parent " << info.parent); continue; } std::ostringstream ss; ss << Command::getObjectCmd(vpp->getObject()) << ".ViewObject.dragObject(" << Command::getObjectCmd(obj) << ')'; auto lines = manager->getLines(); vpp->dragObject(obj); if (manager->getLines() == lines) { manager->addLine(MacroManager::Gui, ss.str().c_str()); } //make sure it is not part of a geofeaturegroup anymore. //When this has happen we need to handle all removed //objects auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(obj); if (grp) { FCMD_OBJ_CMD(grp, "removeObject(" << Command::getObjectCmd(obj) << ")"); } // check if the object has been deleted obj = doc->getObject(info.obj.c_str()); if (!obj || !obj->isAttachedToDocument()) { continue; } droppedObjs.push_back(obj); if (propPlacement) { propPlacement->setValueIfChanged(Base::Placement(mat)); } } else { std::ostringstream ss; ss << "App.getDocument('" << targetInfo.targetDoc->getName() << "')." << (da == Qt::CopyAction ? "copyObject(" : "moveObject(") << Command::getObjectCmd(obj) << ", True)"; App::DocumentObject* res = nullptr; if (da == Qt::CopyAction) { auto copied = targetInfo.targetDoc->copyObject({ obj }, true); if (!copied.empty()) { res = copied.back(); } } else if (da == Qt::MoveAction && obj->getDocument() == targetInfo.targetDoc) { // Moving a object within the document root. // Do not set 'res' as changing the placement is not desired: #13690 droppedObjs.push_back(obj); } else { // Moving a object from another document. res = targetInfo.targetDoc->moveObject(obj, true); } if (res) { propPlacement = dynamic_cast( res->getPropertyByName("Placement")); if (propPlacement) { propPlacement->setValueIfChanged(Base::Placement(mat)); } droppedObjs.push_back(res); } manager->addLine(MacroManager::App, ss.str().c_str()); } } touched = true; Base::FlagToggler<> guard(_DisableCheckTopParent); Selection().setSelection(targetInfo.targetDoc->getName(), droppedObjs); // If moved, then we sort objects properly. if (da == Qt::MoveAction) { sortDroppedObjects(targetInfo, droppedObjs); } } catch (const Base::Exception& e) { e.ReportException(); errMsg = e.what(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); errMsg = e.what(); } catch (...) { FC_ERR("Unknown exception"); errMsg = "Unknown exception"; } if (!errMsg.empty()) { committer.close(true); QMessageBox::critical(getMainWindow(), QObject::tr("Drag & drop failed"), QString::fromUtf8(errMsg.c_str())); return false; } return touched; } bool TreeWidget::dropInObject(QDropEvent* event, TargetItemInfo& targetInfo, std::vector items) { std::string errMsg; auto da = event->dropAction(); bool touched = false; // add object to group auto targetItemObj = static_cast(targetInfo.targetItem); Gui::ViewProviderDocumentObject* vp = targetItemObj->object(); if (!vp || !vp->getObject() || !vp->getObject()->isAttachedToDocument()) { TREE_TRACE("invalid object"); return false; } // if we are in between or if target doesn't accept drops then the target is the parent if (da == Qt::MoveAction && (targetInfo.inThresholdZone || !vp->canDropObjects())) { targetInfo.targetItem = targetInfo.targetItem->parent(); if (targetInfo.targetItem->type() == TreeWidget::DocumentType) { return dropInDocument(event, targetInfo, items); } targetItemObj = static_cast(targetInfo.targetItem); vp = targetItemObj->object(); if (!vp || !vp->getObject() || !vp->getObject()->isAttachedToDocument()) { TREE_TRACE("invalid object"); return false; } } if (da != Qt::LinkAction && !vp->canDropObjects()) { if (!(event->possibleActions() & Qt::LinkAction) || items.size() != 1) { TREE_TRACE("Cannot drop objects"); return false; // no group like object } } App::DocumentObject* targetObj = targetItemObj->object()->getObject(); std::ostringstream targetSubname; App::DocumentObject* targetParent = nullptr; targetItemObj->getSubName(targetSubname, targetParent); Selection().selStackPush(); Selection().clearCompleteSelection(); if (targetParent) { targetSubname << vp->getObject()->getNameInDocument() << '.'; Selection().addSelection(targetParent->getDocument()->getName(), targetParent->getNameInDocument(), targetSubname.str().c_str()); } else { targetParent = targetObj; Selection().addSelection(targetParent->getDocument()->getName(), targetParent->getNameInDocument()); } // Open command App::AutoTransaction committer("Drop object"); bool syncPlacement = TreeParams::getSyncPlacement() && targetItemObj->isGroup(); bool setSelection = true; std::vector draggedObjects; std::vector > droppedObjects; std::vector infos; // Only keep text names here, because you never know when doing drag // and drop some object may delete other objects. infos.reserve(items.size()); for (auto& v : items) { infos.emplace_back(); auto& info = infos.back(); auto item = v.first; App::DocumentObject* obj = item->object()->getObject(); std::ostringstream str; App::DocumentObject* topParent = nullptr; auto owner = item->getRelativeParent(str, targetItemObj, &topParent, &info.topSubname); if (syncPlacement && topParent) { info.topDoc = topParent->getDocument()->getName(); info.topObj = topParent->getNameInDocument(); } info.subname = str.str(); info.doc = obj->getDocument()->getName(); info.obj = obj->getNameInDocument(); if (owner) { info.ownerDoc = owner->getDocument()->getName(); info.owner = owner->getNameInDocument(); } info.subs.swap(v.second); // check if items can be dragged if (da == Qt::MoveAction && item->myOwner == targetItemObj->myOwner && vp->canDragAndDropObject(obj)) { auto parentItem = item->getParentItem(); if (!parentItem) { info.dragging = true; } else { bool allParentsOK = canDragFromParents(parentItem, obj, targetObj); if (allParentsOK) { auto vpp = parentItem->object(); info.dragging = true; info.parent = vpp->getObject()->getNameInDocument(); info.parentDoc = vpp->getObject()->getDocument()->getName(); } else { committer.close(true); return false; } } } if (da != Qt::LinkAction && !vp->canDropObjectEx(obj, owner, info.subname.c_str(), item->mySubs)) { if (event->possibleActions() & Qt::LinkAction) { if (items.size() > 1) { committer.close(true); TREE_TRACE("Cannot replace with more than one object"); return false; } auto ext = vp->getObject()->getExtensionByType(true); if ((!ext || !ext->getLinkedObjectProperty()) && !targetItemObj->getParentItem()) { committer.close(true); TREE_TRACE("Cannot replace without parent"); return false; } da = Qt::LinkAction; } } } try { std::set inList; auto parentObj = targetObj; if (da == Qt::LinkAction && targetItemObj->getParentItem()) { parentObj = targetItemObj->getParentItem()->object()->getObject(); } inList = parentObj->getInListEx(true); inList.insert(parentObj); std::string target = targetObj->getNameInDocument(); auto targetDoc = targetObj->getDocument(); for (auto& info : infos) { auto& subname = info.subname; targetObj = targetDoc->getObject(target.c_str()); vp = freecad_cast( Application::Instance->getViewProvider(targetObj)); if (!vp) { FC_ERR("Cannot find drop target object " << target); break; } auto doc = App::GetApplication().getDocument(info.doc.c_str()); if (!doc) { FC_WARN("Cannot find document " << info.doc); continue; } auto obj = doc->getObject(info.obj.c_str()); auto vpc = dynamic_cast(Application::Instance->getViewProvider(obj)); if (!vpc) { FC_WARN("Cannot find dragging object " << info.obj); continue; } ViewProviderDocumentObject* vpp = nullptr; if (da != Qt::LinkAction && !info.parentDoc.empty()) { auto parentDoc = App::GetApplication().getDocument(info.parentDoc.c_str()); if (parentDoc) { auto parent = parentDoc->getObject(info.parent.c_str()); vpp = dynamic_cast(Application::Instance->getViewProvider(parent)); } if (!vpp) { FC_WARN("Cannot find dragging object's parent " << info.parent); continue; } } App::DocumentObject* owner = nullptr; if (!info.ownerDoc.empty()) { auto ownerDoc = App::GetApplication().getDocument(info.ownerDoc.c_str()); if (ownerDoc) owner = ownerDoc->getObject(info.owner.c_str()); if (!owner) { FC_WARN("Cannot find dragging object's top parent " << info.owner); continue; } } Base::Matrix4D mat; App::PropertyPlacement* propPlacement = nullptr; if (syncPlacement) { propPlacement = DropHandler::getPlacement(info, obj, mat); } auto dropParent = targetParent; auto manager = Application::Instance->macroManager(); std::ostringstream ss; if (vpp) { auto lines = manager->getLines(); ss << Command::getObjectCmd(vpp->getObject()) << ".ViewObject.dragObject(" << Command::getObjectCmd(obj) << ')'; vpp->dragObject(obj); if (manager->getLines() == lines) manager->addLine(MacroManager::Gui, ss.str().c_str()); owner = nullptr; subname.clear(); ss.str(""); obj = doc->getObject(info.obj.c_str()); if (!obj || !obj->isAttachedToDocument()) { FC_WARN("Dropping object deleted: " << info.doc << '#' << info.obj); continue; } } if (da == Qt::MoveAction) { // Try to adjust relative links to avoid cyclic dependency, may // throw exception if failed ss.str(""); ss << Command::getObjectCmd(obj) << ".adjustRelativeLinks(" << Command::getObjectCmd(targetObj) << ")"; manager->addLine(MacroManager::Gui, ss.str().c_str()); std::set visited; if (obj->adjustRelativeLinks(inList, &visited)) { inList = parentObj->getInListEx(true); inList.insert(parentObj); // TODO: link adjustment and placement adjustment does // not work together at the moment. propPlacement = nullptr; } } if (inList.count(obj)) { FC_THROWM(Base::RuntimeError, "Dependency loop detected for " << obj->getFullName()); } std::string dropName; ss.str(""); if (da == Qt::LinkAction) { auto parentItem = targetItemObj->getParentItem(); if (parentItem) { ss << Command::getObjectCmd( parentItem->object()->getObject(), nullptr, ".replaceObject(", true) << Command::getObjectCmd(targetObj) << "," << Command::getObjectCmd(obj) << ")"; std::ostringstream ss; dropParent = nullptr; parentItem->getSubName(ss, dropParent); if (dropParent) ss << parentItem->object()->getObject()->getNameInDocument() << '.'; else dropParent = parentItem->object()->getObject(); ss << obj->getNameInDocument() << '.'; dropName = ss.str(); } else { TREE_WARN("ignore replace operation without parent"); continue; } Gui::Command::runCommand(Gui::Command::App, ss.str().c_str()); } else { ss << Command::getObjectCmd(vp->getObject()) << ".ViewObject.dropObject(" << Command::getObjectCmd(obj); if (owner) { ss << "," << Command::getObjectCmd(owner) << ",'" << subname << "',["; } else ss << ",None,'',["; for (auto& sub : info.subs) ss << "'" << sub << "',"; ss << "])"; auto lines = manager->getLines(); dropName = vp->dropObjectEx(obj, owner, subname.c_str(), info.subs); if (manager->getLines() == lines) manager->addLine(MacroManager::Gui, ss.str().c_str()); if (!dropName.empty()) dropName = targetSubname.str() + dropName; } touched = true; // Construct the subname pointing to the dropped object if (dropName.empty()) { auto pos = targetSubname.tellp(); targetSubname << obj->getNameInDocument() << '.' << std::ends; dropName = targetSubname.str(); targetSubname.seekp(pos); } Base::Matrix4D newMat; auto sobj = dropParent->getSubObject(dropName.c_str(), nullptr, &newMat); if (!sobj) { FC_LOG("failed to find dropped object " << dropParent->getFullName() << '.' << dropName); setSelection = false; continue; } if (da != Qt::CopyAction && propPlacement) { // try to adjust placement if ((info.dragging && sobj == obj) || (!info.dragging && sobj->getLinkedObject(false) == obj)) { if (!info.dragging) propPlacement = freecad_cast( sobj->getPropertyByName("Placement")); if (propPlacement) { newMat *= propPlacement->getValue().inverse().toMatrix(); newMat.inverseGauss(); Base::Placement pla(newMat * mat); propPlacement->setValueIfChanged(pla); } } } droppedObjects.emplace_back(dropParent, dropName); draggedObjects.push_back(obj); } Base::FlagToggler<> guard(_DisableCheckTopParent); if (setSelection && !droppedObjects.empty()) { Selection().selStackPush(); Selection().clearCompleteSelection(); for (auto& v : droppedObjects) { Selection().addSelection(v.first->getDocument()->getName(), v.first->getNameInDocument(), v.second.c_str()); } Selection().selStackPush(); } // If moved, then we sort objects properly. if (da == Qt::MoveAction && vp->acceptReorderingObjects()) { sortDroppedObjects(targetInfo, draggedObjects); } } catch (const Base::Exception& e) { e.ReportException(); errMsg = e.what(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); errMsg = e.what(); } catch (...) { FC_ERR("Unknown exception"); errMsg = "Unknown exception"; } if (!errMsg.empty()) { committer.close(true); QMessageBox::critical(getMainWindow(), QObject::tr("Drag & drop failed"), QString::fromUtf8(errMsg.c_str())); return false; } return touched; } bool TreeWidget::canDragFromParents(DocumentObjectItem* parentItem, App::DocumentObject* obj, App::DocumentObject* target) { // We query all the parents recursively. (for cases like assembly/group/part) bool allParentsOK = true; while (parentItem) { if (!parentItem->object()->canDragObjectToTarget(obj, target)) { allParentsOK = false; break; } parentItem = parentItem->getParentItem(); } return allParentsOK; } void TreeWidget::dropEvent(QDropEvent* event) { //FIXME: This should actually be done inside dropMimeData TargetItemInfo targetInfo = getTargetInfo(event); if (!targetInfo.targetItem) { return; } // filter out the selected items we cannot handle std::vector items; items = DropHandler::filterItems(selectedItems(), targetInfo.targetItem); if (items.empty()) { return; // nothing needs to be done } event->setDropAction(getDropAction(items.size(), targetInfo.targetItem->type())); bool touched = false; if (targetInfo.targetItem->type() == TreeWidget::ObjectType) { touched = dropInObject(event, targetInfo, items); } else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) { touched = dropInDocument(event, targetInfo, items); } if (touched && TreeParams::getRecomputeOnDrop()) { targetInfo.targetDoc->recompute(); } if (touched && TreeParams::getSyncView()) { auto gdoc = Application::Instance->getDocument(targetInfo.targetDoc); if (gdoc) gdoc->setActiveView(); } } void TreeWidget::sortDroppedObjects(TargetItemInfo& targetInfo, std::vector draggedObjects) { if (targetInfo.targetItem == targetInfo.underMouseItem) { return; } auto underMouseItemObj = static_cast(targetInfo.underMouseItem); auto underMouseObj = underMouseItemObj->object()->getObject(); std::vector sortedObjList; std::vector objList; auto sortIntoList = [&sortedObjList, &draggedObjects, underMouseObj, &targetInfo](const std::vector& objects) { for (auto* obj : objects) { if (obj == underMouseObj) { if (targetInfo.inBottomHalf) { sortedObjList.push_back(obj); } for (auto* draggedObj : draggedObjects) { sortedObjList.push_back(draggedObj); } if (!targetInfo.inBottomHalf) { sortedObjList.push_back(obj); } } else { if (std::ranges::find(draggedObjects, obj) == draggedObjects.end()) { sortedObjList.push_back(obj); } } } }; if (targetInfo.targetItem->type() == TreeWidget::ObjectType) { // To update the order of items of groups such as App::Part, we just need to change the order in the Group property auto targetItemObj = static_cast(targetInfo.targetItem); App::DocumentObject* targetObj = targetItemObj->object()->getObject(); auto propGroup = freecad_cast(targetObj->getPropertyByName("Group")); if (!propGroup) { return; } objList = propGroup->getValue(); sortIntoList(objList); // Move dropped objects to correct position propGroup->setValue(sortedObjList); } else if (targetInfo.targetItem->type() == TreeWidget::DocumentType) { Gui::Document* guiDoc = Gui::Application::Instance->getDocument(targetInfo.targetDoc->getName()); objList = guiDoc->getTreeRootObjects(); // First we need to sort objList by treeRank. std::sort(objList.begin(), objList.end(), [](App::DocumentObject* a, App::DocumentObject* b) { auto vpA = dynamic_cast(Gui::Application::Instance->getViewProvider(a)); auto vpB = dynamic_cast(Gui::Application::Instance->getViewProvider(b)); if (vpA && vpB) { return vpA->getTreeRank() < vpB->getTreeRank(); } return false; // Keep the original order if either vpA or vpB is nullptr }); // Then we move dropped objects to their correct position sortIntoList(objList); // Then we set the tree rank for (size_t i = 0; i < sortedObjList.size(); ++i) { auto vp = dynamic_cast(Application::Instance->getViewProvider(sortedObjList[i])); vp->setTreeRank(i); } // Lastly we refresh the tree static_cast(targetInfo.targetItem)->sortObjectItems(); } } void TreeWidget::drawRow(QPainter* painter, const QStyleOptionViewItem& options, const QModelIndex& index) const { QTreeWidget::drawRow(painter, options, index); } void TreeWidget::slotNewDocument(const Gui::Document& Doc, bool isMainDoc) { if (Doc.getDocument()->testStatus(App::Document::TempDoc)) return; auto item = new DocumentItem(&Doc, this->rootItem); if (isMainDoc) this->expandItem(item); item->setIcon(0, *documentPixmap); item->setText(0, QString::fromUtf8(Doc.getDocument()->Label.getValue())); DocumentMap[&Doc] = item; } void TreeWidget::onReloadDoc() { if (!this->contextItem || this->contextItem->type() != DocumentType) return; auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); std::string name = doc->FileName.getValue(); Application::Instance->reopen(doc); for (auto& v : DocumentMap) { if (name == v.first->getDocument()->FileName.getValue()) { scrollToItem(v.second); App::GetApplication().setActiveDocument(v.first->getDocument()); break; } } } void TreeWidget::onCloseDoc() { if (!this->contextItem || this->contextItem->type() != DocumentType) return; try { auto docitem = static_cast(this->contextItem); Gui::Document* gui = docitem->document(); App::Document* doc = gui->getDocument(); if (gui->canClose(true, true)) Command::doCommand(Command::Doc, "App.closeDocument(\"%s\")", doc->getName()); } catch (const Base::Exception& e) { e.ReportException(); } catch (std::exception& e) { FC_ERR("C++ exception: " << e.what()); } catch (...) { FC_ERR("Unknown exception"); } } void TreeWidget::onOpenFileLocation() { auto docitem = static_cast(this->contextItem); App::Document* doc = docitem->document()->getDocument(); std::string name = doc->FileName.getValue(); const QFileInfo fileInfo(QString::fromStdString(name)); if (!fileInfo.exists()) { QMessageBox::warning(this, tr("Error"), tr("File does not exist.")); return; } const QString filePath = fileInfo.canonicalPath(); bool success = false; #if defined(Q_OS_MAC) success = QProcess::startDetached(QStringLiteral("open"), {filePath}); #elif defined(Q_OS_WIN) QStringList param; if (!fileInfo.isDir()) { param += QStringLiteral("/select,"); } param += QDir::toNativeSeparators(filePath); success = QProcess::startDetached(QStringLiteral("explorer.exe"), param); #else success = QProcess::startDetached(QStringLiteral("xdg-open"), {filePath}); #endif if (!success) { QMessageBox::warning(this, tr("Error"), tr("Failed to open directory.")); } } void TreeWidget::slotRenameDocument(const Gui::Document& Doc) { // do nothing here Q_UNUSED(Doc); } void TreeWidget::slotChangedViewObject(const Gui::ViewProvider& vp, const App::Property& prop) { if (!App::GetApplication().isRestoring() && vp.isDerivedFrom()) { const auto& vpd = static_cast(vp); if (&prop == &vpd.ShowInTree) { ChangedObjects.emplace(vpd.getObject(), 0); _updateStatus(); } } } void TreeWidget::slotTouchedObject(const App::DocumentObject& obj) { ChangedObjects.emplace(const_cast(&obj), 0); _updateStatus(); } void TreeWidget::slotShowHidden(const Gui::Document& Doc) { auto it = DocumentMap.find(&Doc); if (it != DocumentMap.end()) it->second->updateItemsVisibility(it->second, it->second->showHidden()); } void TreeWidget::slotRelabelDocument(const Gui::Document& Doc) { auto it = DocumentMap.find(&Doc); if (it != DocumentMap.end()) { it->second->setText(0, QString::fromUtf8(Doc.getDocument()->Label.getValue())); } } void TreeWidget::slotActiveDocument(const Gui::Document& Doc) { auto jt = DocumentMap.find(&Doc); if (jt == DocumentMap.end()) return; // signal is emitted before the item gets created int displayMode = TreeParams::getDocumentMode(); for (auto it = DocumentMap.begin(); it != DocumentMap.end(); ++it) { QFont f = it->second->font(0); f.setBold(it == jt); it->second->setHidden(0 == displayMode && it != jt); if (2 == displayMode) { it->second->setExpanded(it == jt); } // this must be done as last step it->second->setFont(0, f); } } struct UpdateDisabler { QWidget& widget; int& blocked; bool visible{false}; bool focus{false}; // Note! DO NOT block signal here, or else // QTreeWidgetItem::setChildIndicatorPolicy() does not work UpdateDisabler(QWidget& w, int& blocked) : widget(w), blocked(blocked) { if (++blocked > 1) return; focus = widget.hasFocus(); visible = widget.isVisible(); if (visible) { // setUpdatesEnabled(false) does not seem to speed up anything. // setVisible(false) on the other hand makes QTreeWidget::setData // (i.e. any change to QTreeWidgetItem) faster by 10+ times. // // widget.setUpdatesEnabled(false); widget.setVisible(false); } } ~UpdateDisabler() { if (blocked <= 0 || --blocked != 0) return; if (visible) { widget.setVisible(true); if (focus) widget.setFocus(); } } }; void TreeWidget::onUpdateStatus() { if (this->state() == DraggingState || App::GetApplication().isRestoring()) { _updateStatus(); return; } for (auto& v : DocumentMap) { if (v.first->isPerformingTransaction()) { // We have to delay item creation until undo/redo is done, because the // object re-creation while in transaction may break tree view item // update logic. For example, a parent object re-created before its // children, but the parent's link property already contains all the // (detached) children. _updateStatus(); return; } } FC_LOG("begin update status"); UpdateDisabler disabler(*this, updateBlocked); std::vector errors; // Use a local copy in case of nested calls auto localNewObjects = NewObjects; NewObjects.clear(); // Checking for new objects for (auto& v : localNewObjects) { auto doc = App::GetApplication().getDocument(v.first.c_str()); if (!doc) continue; auto gdoc = Application::Instance->getDocument(doc); if (!gdoc) continue; auto docItem = getDocumentItem(gdoc); if (!docItem) continue; for (auto id : v.second) { auto obj = doc->getObjectByID(id); if (!obj) continue; if (obj->isError()) errors.push_back(obj); if (docItem->ObjectMap.count(obj)) continue; auto vpd = freecad_cast(gdoc->getViewProvider(obj)); if (vpd) docItem->createNewItem(*vpd); } } // Use a local copy in case of nested calls auto localChangedObjects = ChangedObjects; ChangedObjects.clear(); // Update children of changed objects for (auto& v : localChangedObjects) { auto obj = v.first; auto iter = ObjectTable.find(obj); if (iter == ObjectTable.end()) continue; if (v.second.test(CS_Error) && obj->isError()) errors.push_back(obj); if (!iter->second.empty()) { auto data = *iter->second.begin(); bool itemHidden = !data->viewObject->showInTree(); if (data->itemHidden != itemHidden) { for (auto& data : iter->second) { data->itemHidden = itemHidden; if (data->docItem->showHidden()) continue; for (auto item : data->items) item->setHidden(itemHidden); } } } updateChildren(iter->first, iter->second, v.second.test(CS_Output), false); } FC_LOG("update item status"); TimingInit(); for (auto pos = DocumentMap.begin(); pos != DocumentMap.end(); ++pos) { pos->second->testStatus(); } TimingPrint(); // Checking for just restored documents for (auto& v : DocumentMap) { auto docItem = v.second; for (auto obj : docItem->PopulateObjects) docItem->populateObject(obj); docItem->PopulateObjects.clear(); auto doc = v.first->getDocument(); if (!docItem->connectChgObject.connected()) { //NOLINTBEGIN docItem->connectChgObject = docItem->document()->signalChangedObject.connect( std::bind(&TreeWidget::slotChangeObject, this, sp::_1, sp::_2)); docItem->connectTouchedObject = doc->signalTouchedObject.connect( std::bind(&TreeWidget::slotTouchedObject, this, sp::_1)); //NOLINTEND } if (doc->testStatus(App::Document::PartialDoc)) docItem->setIcon(0, *documentPartialPixmap); else if (docItem->_ExpandInfo) { for (auto& entry : *docItem->_ExpandInfo) { const char* name = entry.first.c_str(); bool legacy = name[0] == '*'; if (legacy) ++name; auto obj = doc->getObject(name); if (!obj) continue; auto iter = docItem->ObjectMap.find(obj); if (iter == docItem->ObjectMap.end()) continue; if (iter->second->rootItem) docItem->restoreItemExpansion(entry.second, iter->second->rootItem); else if (legacy && !iter->second->items.empty()) { auto item = *iter->second->items.begin(); item->setExpanded(true); } } } docItem->_ExpandInfo.reset(); } if (Selection().hasSelection() && !selectTimer->isActive() && !this->isSelectionBlocked()) { this->blockSelection(true); currentDocItem = nullptr; for (auto& v : DocumentMap) { v.second->setSelected(false); v.second->selectItems(); } this->blockSelection(false); } auto activeDocItem = getDocumentItem(Application::Instance->activeDocument()); QTreeWidgetItem* errItem = nullptr; for (auto obj : errors) { DocumentObjectDataPtr data; if (activeDocItem) { auto it = activeDocItem->ObjectMap.find(obj); if (it != activeDocItem->ObjectMap.end()) data = it->second; } if (!data) { auto docItem = getDocumentItem( Application::Instance->getDocument(obj->getDocument())); if (docItem) { auto it = docItem->ObjectMap.find(obj); if (it != docItem->ObjectMap.end()) data = it->second; } } if (data) { auto item = data->rootItem; if (!item && !data->items.empty()) { item = *data->items.begin(); data->docItem->showItem(item, false, true); } if (!errItem) errItem = item; } } if (errItem) scrollToItem(errItem); updateGeometries(); statusTimer->stop(); FC_LOG("done update status"); } void TreeWidget::onItemEntered(QTreeWidgetItem* item) { // object item selected if (item && item->type() == TreeWidget::ObjectType) { auto objItem = static_cast(item); objItem->displayStatusInfo(); if (TreeParams::getPreSelection()) { int timeout = TreeParams::getPreSelectionDelay(); if (timeout < 0) timeout = 1; if (preselectTime.elapsed() < timeout) onPreSelectTimer(); else { timeout = TreeParams::getPreSelectionTimeout(); if (timeout < 0) timeout = 1; preselectTimer->start(timeout); Selection().rmvPreselect(); } } } else if (TreeParams::getPreSelection()) Selection().rmvPreselect(); } void TreeWidget::leaveEvent(QEvent* event) { Q_UNUSED(event) if (!updateBlocked && TreeParams::getPreSelection()) { preselectTimer->stop(); Selection().rmvPreselect(); } } void TreeWidget::onPreSelectTimer() { if (!TreeParams::getPreSelection()) return; auto item = itemAt(viewport()->mapFromGlobal(QCursor::pos())); if (!item || item->type() != TreeWidget::ObjectType) return; preselectTime.restart(); auto objItem = static_cast(item); auto vp = objItem->object(); auto obj = vp->getObject(); std::ostringstream ss; App::DocumentObject* parent = nullptr; objItem->getSubName(ss, parent); if (!parent) parent = obj; else if (!obj->redirectSubName(ss, parent, nullptr)) ss << obj->getNameInDocument() << '.'; Selection().setPreselect(parent->getDocument()->getName(), parent->getNameInDocument(), ss.str().c_str(), 0, 0, 0, SelectionChanges::MsgSource::TreeView); } void TreeWidget::onItemCollapsed(QTreeWidgetItem* item) { // object item collapsed if (item && item->type() == TreeWidget::ObjectType) { static_cast(item)->setExpandedStatus(false); } } void TreeWidget::onItemExpanded(QTreeWidgetItem* item) { // object item expanded if (item && item->type() == TreeWidget::ObjectType) { auto objItem = static_cast(item); objItem->setExpandedStatus(true); objItem->getOwnerDocument()->populateItem(objItem, false, false); } } void TreeWidget::scrollItemToTop() { auto doc = Application::Instance->activeDocument(); for (auto tree : Instances) { if (!tree->isSelectionAttached() || tree->isSelectionBlocked()) continue; tree->_updateStatus(false); if (doc && Gui::Selection().hasSelection(doc->getDocument()->getName(), ResolveMode::NoResolve)) { auto it = tree->DocumentMap.find(doc); if (it != tree->DocumentMap.end()) { bool lock = tree->blockSelection(true); it->second->selectItems(DocumentItem::SR_FORCE_EXPAND); tree->blockSelection(lock); } } else { tree->blockSelection(true); for (int i = 0; i < tree->rootItem->childCount(); i++) { auto docItem = dynamic_cast(tree->rootItem->child(i)); if (!docItem) continue; auto doc = docItem->document()->getDocument(); if (Gui::Selection().hasSelection(doc->getName())) { tree->currentDocItem = docItem; docItem->selectItems(DocumentItem::SR_FORCE_EXPAND); tree->currentDocItem = nullptr; break; } } tree->blockSelection(false); } tree->selectTimer->stop(); tree->_updateStatus(false); } } void TreeWidget::expandSelectedItems(TreeItemMode mode) { if (!isSelectionAttached()) return; const auto items = selectedItems(); for (auto item : items) { switch (mode) { case TreeItemMode::ExpandPath: { QTreeWidgetItem* parentItem = item->parent(); while (parentItem) { parentItem->setExpanded(true); parentItem = parentItem->parent(); } item->setExpanded(true); break; } case TreeItemMode::ExpandItem: item->setExpanded(true); break; case TreeItemMode::CollapseItem: item->setExpanded(false); break; case TreeItemMode::ToggleItem: if (item->isExpanded()) item->setExpanded(false); else item->setExpanded(true); break; } } } void TreeWidget::setupText() { this->headerItem()->setText(0, tr("Labels & Attributes")); this->headerItem()->setText(1, tr("Description")); this->headerItem()->setText(2, tr("Internal name")); this->showHiddenAction->setText(tr("Show items hidden in tree view")); this->showHiddenAction->setStatusTip(tr("Show items that are marked as 'hidden' in the tree view")); this->toggleVisibilityInTreeAction->setText(tr("Toggle visibility in tree view")); this->toggleVisibilityInTreeAction->setStatusTip(tr("Toggles the visibility of selected items in the tree view")); this->createGroupAction->setText(tr("Create group")); this->createGroupAction->setStatusTip(tr("Create a group")); this->relabelObjectAction->setText(tr("Rename")); this->relabelObjectAction->setStatusTip(tr("Rename object")); this->finishEditingAction->setText(tr("Finish editing")); this->finishEditingAction->setStatusTip(tr("Finish editing object")); this->selectDependentsAction->setText(tr("Add dependent objects to selection")); this->selectDependentsAction->setStatusTip(tr("Adds all dependent objects to the selection")); this->closeDocAction->setText(tr("Close document")); this->closeDocAction->setStatusTip(tr("Close the document")); #ifdef Q_OS_MAC this->openFileLocationAction->setText(tr("Reveal in Finder")); this->openFileLocationAction->setStatusTip(tr("Reveal the current file location in Finder")); #else this->openFileLocationAction->setText(tr("Open File Location")); this->openFileLocationAction->setStatusTip(tr("Open the current file location")); #endif this->reloadDocAction->setText(tr("Reload document")); this->reloadDocAction->setStatusTip(tr("Reload a partially loaded document")); this->skipRecomputeAction->setText(tr("Skip recomputes")); this->skipRecomputeAction->setStatusTip(tr("Enable or disable recomputations of document")); this->allowPartialRecomputeAction->setText(tr("Allow partial recomputes")); this->allowPartialRecomputeAction->setStatusTip( tr("Enable or disable recomputating editing object when 'skip recomputation' is enabled")); this->markRecomputeAction->setText(tr("Mark to recompute")); this->markRecomputeAction->setStatusTip(tr("Mark this object to be recomputed")); this->markRecomputeAction->setIcon(BitmapFactory().iconFromTheme("Std_MarkToRecompute")); this->recomputeObjectAction->setText(tr("Recompute object")); this->recomputeObjectAction->setStatusTip(tr("Recompute the selected object")); this->recomputeObjectAction->setIcon(BitmapFactory().iconFromTheme("view-refresh")); } void TreeWidget::syncView(ViewProviderDocumentObject* vp) { if (currentDocItem && TreeParams::getSyncView()) { bool focus = hasFocus(); currentDocItem->document()->setActiveView(vp); if (focus) setFocus(); } } void TreeWidget::onShowHidden() { if (!this->contextItem) return; DocumentItem* docItem = nullptr; if (this->contextItem->type() == DocumentType) docItem = static_cast(contextItem); else if (this->contextItem->type() == ObjectType) docItem = static_cast(contextItem)->getOwnerDocument(); if (docItem) docItem->setShowHidden(showHiddenAction->isChecked()); } void TreeWidget::onToggleVisibilityInTree() { const auto items = selectedItems(); for (auto item : items) { if (item->type() == ObjectType) { auto objectItem = static_cast(item); auto object = objectItem->object(); // toggle value bool showInTree = !object->showInTree(); // update object object->ShowInTree.setValue(showInTree); // update GUI auto ownerDocument = objectItem->getOwnerDocument(); bool hidden = !ownerDocument->showHidden() && !showInTree; objectItem->setHidden(hidden); if (hidden) { objectItem->setSelected(false); } } } } void TreeWidget::changeEvent(QEvent* e) { if (e->type() == QEvent::LanguageChange) setupText(); QTreeWidget::changeEvent(e); } void TreeWidget::onItemSelectionChanged() { if (!this->isSelectionAttached() || this->isSelectionBlocked() || updateBlocked) return; _LastSelectedTreeWidget = this; // block tmp. the connection to avoid to notify us ourself bool lock = this->blockSelection(true); if (selectTimer->isActive()) onSelectTimer(); else _updateStatus(false); auto selItems = selectedItems(); // do not allow document item multi-selection if (!selItems.empty()) { auto firstType = selItems.back()->type(); for (auto it = selItems.begin(); it != selItems.end();) { auto item = *it; if ((firstType == ObjectType && item->type() != ObjectType) || (firstType == DocumentType && item != selItems.back())) { item->setSelected(false); it = selItems.erase(it); } else ++it; } } if (selItems.size() <= 1) { if (TreeParams::getRecordSelection()) Gui::Selection().selStackPush(); // This special handling to deal with possible discrepancy of // Gui.Selection and Tree view selection because of newly added // DocumentObject::redirectSubName() Selection().clearCompleteSelection(); DocumentObjectItem* item = nullptr; if (!selItems.empty()) { if (selItems.front()->type() == ObjectType) item = static_cast(selItems.front()); else if (selItems.front()->type() == DocumentType) { auto ditem = static_cast(selItems.front()); if (TreeParams::getSyncView()) { bool focus = hasFocus(); ditem->document()->setActiveView(); if (focus) setFocus(); } // For triggering property editor refresh Gui::Selection().signalSelectionChanged(SelectionChanges()); } } for (auto& v : DocumentMap) { currentDocItem = v.second; v.second->clearSelection(item); currentDocItem = nullptr; } if (TreeParams::getRecordSelection()) Gui::Selection().selStackPush(); } else { for (auto pos = DocumentMap.begin(); pos != DocumentMap.end(); ++pos) { currentDocItem = pos->second; pos->second->updateSelection(pos->second); currentDocItem = nullptr; } if (TreeParams::getRecordSelection()) Gui::Selection().selStackPush(true, true); } this->blockSelection(lock); } void TreeWidget::synchronizeSelectionCheckBoxes() { const bool useCheckBoxes = isSelectionCheckBoxesEnabled(); for (auto tree : TreeWidget::Instances) { QSignalBlocker blocker(tree); for (QTreeWidgetItemIterator it(tree); *it; ++it) { auto item = *it; if (item->type() == ObjectType) { if (useCheckBoxes) item->setCheckState(0, item->isSelected() ? Qt::Checked : Qt::Unchecked); else item->setData(0, Qt::CheckStateRole, QVariant()); } } tree->resizeColumnToContents(0); } } void TreeWidget::updateVisibilityIcons() { for (auto tree : TreeWidget::Instances) { QSignalBlocker blocker(tree); for (QTreeWidgetItemIterator it(tree); *it; ++it) { auto item = *it; if (item->type() == ObjectType) { auto objitem = static_cast(item); objitem->testStatus(true); } } tree->resizeColumnToContents(0); } } QList TreeWidget::childrenOfItem(const QTreeWidgetItem& item) const { QList children = QList(); // check item is in this tree if (!this->indexFromItem(&item).isValid()) return children; for (int i = 0; i < item.childCount(); i++) { children.append(item.child(i)); } return children; } void TreeWidget::onItemChanged(QTreeWidgetItem* item, int column) { if (column == 0 && isSelectionCheckBoxesEnabled()) { bool selected = item->isSelected(); bool checked = item->checkState(0) == Qt::Checked; if (checked != selected) { item->setSelected(checked); } } } void TreeWidget::onSelectTimer() { _updateStatus(false); bool syncSelect = TreeParams::getSyncSelection(); bool locked = this->blockSelection(true); if (Selection().hasSelection()) { for (auto& v : DocumentMap) { v.second->setSelected(false); currentDocItem = v.second; v.second->selectItems(syncSelect ? DocumentItem::SR_EXPAND : DocumentItem::SR_SELECT); currentDocItem = nullptr; } } else { for (auto& v : DocumentMap) v.second->clearSelection(); } this->blockSelection(locked); selectTimer->stop(); return; } void TreeWidget::onSelectionChanged(const SelectionChanges& msg) { // When running from a different thread Qt will raise a warning // when trying to start the QTimer if (Q_UNLIKELY(thread() != QThread::currentThread())) { return; } switch (msg.Type) { case SelectionChanges::AddSelection: case SelectionChanges::RmvSelection: case SelectionChanges::SetSelection: case SelectionChanges::ClrSelection: { int timeout = TreeParams::getSelectionTimeout(); if (timeout <= 0) timeout = 1; selectTimer->start(timeout); break; } default: break; } } // ---------------------------------------------------------------------------- /* TRANSLATOR Gui::TreePanel */ TreePanel::TreePanel(const char* name, QWidget* parent) : QWidget(parent) { this->treeWidget = new TreeWidget(name, this); int indent = TreeParams::getIndentation(); if (indent) this->treeWidget->setIndentation(indent); auto pLayout = new QVBoxLayout(this); pLayout->setSpacing(0); pLayout->setContentsMargins(0, 0, 0, 0); pLayout->addWidget(this->treeWidget); connect(this->treeWidget, &TreeWidget::emitSearchObjects, this, &TreePanel::showEditor); this->searchBox = new Gui::ExpressionLineEdit(this, true); static_cast(this->searchBox)->setExactMatch(Gui::ExpressionParameter::instance()->isExactMatch()); pLayout->addWidget(this->searchBox); this->searchBox->hide(); this->searchBox->installEventFilter(this); this->searchBox->setPlaceholderText(tr("Search")); connect(this->searchBox, &QLineEdit::returnPressed, this, &TreePanel::accept); connect(this->searchBox, &QLineEdit::textChanged, this, &TreePanel::itemSearch); } TreePanel::~TreePanel() = default; void TreePanel::accept() { QString text = this->searchBox->text(); hideEditor(); this->treeWidget->setFocus(); this->treeWidget->itemSearch(text, true); } bool TreePanel::eventFilter(QObject* obj, QEvent* ev) { if (obj != this->searchBox) return false; if (ev->type() == QEvent::KeyPress) { bool consumed = false; int key = static_cast(ev)->key(); switch (key) { case Qt::Key_Escape: hideEditor(); consumed = true; treeWidget->setFocus(); break; default: break; } return consumed; } return false; } void TreePanel::showEditor() { this->searchBox->show(); this->searchBox->setFocus(); this->treeWidget->startItemSearch(searchBox); } void TreePanel::hideEditor() { static_cast(this->searchBox)->setDocumentObject(nullptr); this->searchBox->clear(); this->searchBox->hide(); this->treeWidget->resetItemSearch(); auto sels = this->treeWidget->selectedItems(); if (!sels.empty()) this->treeWidget->scrollToItem(sels.front()); } void TreePanel::itemSearch(const QString& text) { this->treeWidget->itemSearch(text, false); } // ---------------------------------------------------------------------------- /* TRANSLATOR Gui::TreeDockWidget */ TreeDockWidget::TreeDockWidget(Gui::Document* pcDocument, QWidget* parent) : DockWindow(pcDocument, parent) { setWindowTitle(tr("Tree view")); auto panel = new TreePanel("TreeView", this); auto pLayout = new QGridLayout(this); pLayout->setSpacing(0); pLayout->setContentsMargins(0, 0, 0, 0); pLayout->addWidget(panel, 0, 0); } TreeDockWidget::~TreeDockWidget() = default; void TreeWidget::selectLinkedObject(App::DocumentObject* linked) { if (!isSelectionAttached() || isSelectionBlocked()) return; auto linkedVp = freecad_cast( Application::Instance->getViewProvider(linked)); if (!linkedVp) { TREE_ERR("invalid linked view provider"); return; } auto linkedDoc = getDocumentItem(linkedVp->getDocument()); if (!linkedDoc) { TREE_ERR("cannot find document of linked object"); return; } if (selectTimer->isActive()) onSelectTimer(); else _updateStatus(false); auto it = linkedDoc->ObjectMap.find(linked); if (it == linkedDoc->ObjectMap.end()) { TREE_ERR("cannot find tree item of linked object"); return; } auto linkedItem = it->second->rootItem; if (!linkedItem) linkedItem = *it->second->items.begin(); if (linkedDoc->showItem(linkedItem, true)) scrollToItem(linkedItem); if (linkedDoc->document()->getDocument() != App::GetApplication().getActiveDocument()) { bool focus = hasFocus(); linkedDoc->document()->setActiveView(linkedItem->object()); if (focus) setFocus(); } } // ---------------------------------------------------------------------------- DocumentItem::DocumentItem(const Gui::Document* doc, QTreeWidgetItem* parent) : QTreeWidgetItem(parent, TreeWidget::DocumentType), pDocument(const_cast(doc)) { //NOLINTBEGIN // Setup connections connectNewObject = doc->signalNewObject.connect(std::bind(&DocumentItem::slotNewObject, this, sp::_1)); connectDelObject = doc->signalDeletedObject.connect( std::bind(&TreeWidget::slotDeleteObject, getTree(), sp::_1)); if (!App::GetApplication().isRestoring()) { connectChgObject = doc->signalChangedObject.connect( std::bind(&TreeWidget::slotChangeObject, getTree(), sp::_1, sp::_2)); connectTouchedObject = doc->getDocument()->signalTouchedObject.connect( std::bind(&TreeWidget::slotTouchedObject, getTree(), sp::_1)); } connectEdtObject = doc->signalInEdit.connect(std::bind(&DocumentItem::slotInEdit, this, sp::_1)); connectResObject = doc->signalResetEdit.connect(std::bind(&DocumentItem::slotResetEdit, this, sp::_1)); connectHltObject = doc->signalHighlightObject.connect( std::bind(&DocumentItem::slotHighlightObject, this, sp::_1, sp::_2, sp::_3, sp::_4, sp::_5)); connectExpObject = doc->signalExpandObject.connect( std::bind(&DocumentItem::slotExpandObject, this, sp::_1, sp::_2, sp::_3, sp::_4)); connectScrObject = doc->signalScrollToObject.connect(std::bind(&DocumentItem::slotScrollToObject, this, sp::_1)); auto adoc = doc->getDocument(); connectRecomputed = adoc->signalRecomputed.connect(std::bind(&DocumentItem::slotRecomputed, this, sp::_1, sp::_2)); connectRecomputedObj = adoc->signalRecomputedObject.connect( std::bind(&DocumentItem::slotRecomputedObject, this, sp::_1)); //NOLINTEND setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable/*|Qt::ItemIsEditable*/); treeName = getTree()->getTreeName(); } DocumentItem::~DocumentItem() { connectNewObject.disconnect(); connectDelObject.disconnect(); connectChgObject.disconnect(); connectTouchedObject.disconnect(); connectEdtObject.disconnect(); connectResObject.disconnect(); connectHltObject.disconnect(); connectExpObject.disconnect(); connectScrObject.disconnect(); connectRecomputed.disconnect(); connectRecomputedObj.disconnect(); } TreeWidget* DocumentItem::getTree() const { return static_cast(treeWidget()); } const char* DocumentItem::getTreeName() const { return treeName; } #define FOREACH_ITEM(_item, _obj) \ auto _it = ObjectMap.end();\ if(_obj.getObject() && _obj.getObject()->isAttachedToDocument())\ _it = ObjectMap.find(_obj.getObject());\ if(_it != ObjectMap.end()) {\ for(auto _item : _it->second->items) { #define FOREACH_ITEM_ALL(_item) \ for(const auto& _v : ObjectMap) {\ for(auto _item : _v.second->items) { #define END_FOREACH_ITEM }} void DocumentItem::slotInEdit(const Gui::ViewProviderDocumentObject& v) { (void)v; ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/TreeView"); unsigned long col = hGrp->GetUnsigned("TreeEditColor", 563609599); QColor color(Base::Color::fromPackedRGB(col)); if (!getTree()->editingItem) { auto doc = Application::Instance->editDocument(); if (!doc) return; ViewProviderDocumentObject* parentVp = nullptr; std::string subname; auto vp = doc->getInEdit(&parentVp, &subname); if (!parentVp) parentVp = dynamic_cast(vp); if (parentVp) getTree()->editingItem = findItemByObject(true, parentVp->getObject(), subname.c_str()); } if (getTree()->editingItem) getTree()->editingItem->setBackground(0, color); else { FOREACH_ITEM(item, v) item->setBackground(0, color); END_FOREACH_ITEM } } void DocumentItem::slotResetEdit(const Gui::ViewProviderDocumentObject& v) { auto tree = getTree(); FOREACH_ITEM_ALL(item) if (tree->editingItem) { if (item == tree->editingItem) { item->setData(0, Qt::BackgroundRole, QVariant()); break; } } else if (item->object() == &v) item->setData(0, Qt::BackgroundRole, QVariant()); END_FOREACH_ITEM tree->editingItem = nullptr; } void DocumentItem::slotNewObject(const Gui::ViewProviderDocumentObject& obj) { if (!obj.getObject() || !obj.getObject()->isAttachedToDocument()) { FC_ERR("view provider not attached"); return; } getTree()->NewObjects[pDocument->getDocument()->getName()].push_back(obj.getObject()->getID()); getTree()->_updateStatus(); } bool DocumentItem::createNewItem(const Gui::ViewProviderDocumentObject& obj, QTreeWidgetItem* parent, int index, DocumentObjectDataPtr data) { if (!obj.getObject() || !obj.getObject()->isAttachedToDocument() || obj.getObject()->testStatus(App::PartialObject)) return false; if (!data) { auto& pdata = ObjectMap[obj.getObject()]; if (!pdata) { pdata = std::make_shared( this, const_cast(&obj)); auto& entry = getTree()->ObjectTable[obj.getObject()]; if (!entry.empty()) pdata->updateChildren(*entry.begin()); else pdata->updateChildren(true); entry.insert(pdata); } else if (pdata->rootItem && !parent) { Base::Console().Warning("DocumentItem::slotNewObject: Cannot add view provider twice.\n"); return false; } data = pdata; } auto item = new DocumentObjectItem(this, data); if (!parent || parent == this) { parent = this; data->rootItem = item; if (index < 0) index = findRootIndex(obj.getObject()); } if (index < 0) parent->addChild(item); else parent->insertChild(index, item); assert(item->parent() == parent); item->setText(0, QString::fromUtf8(data->label.c_str())); if (!data->label2.empty()) item->setText(1, QString::fromUtf8(data->label2.c_str())); item->setText(2, QString::fromUtf8(data->internalName.c_str())); if (!obj.showInTree() && !showHidden()) item->setHidden(true); item->testStatus(true); populateItem(item); return true; } ViewProviderDocumentObject* DocumentItem::getViewProvider(App::DocumentObject* obj) { return freecad_cast( Application::Instance->getViewProvider(obj)); } void TreeWidget::slotDeleteDocument(const Gui::Document& Doc) { NewObjects.erase(Doc.getDocument()->getName()); auto it = DocumentMap.find(&Doc); if (it != DocumentMap.end()) { UpdateDisabler disabler(*this, updateBlocked); auto docItem = it->second; for (auto& v : docItem->ObjectMap) { for (auto item : v.second->items) item->myOwner = nullptr; auto obj = v.second->viewObject->getObject(); if (obj->getDocument() == Doc.getDocument()) { _slotDeleteObject(*v.second->viewObject, docItem); continue; } auto it = ObjectTable.find(obj); assert(it != ObjectTable.end()); assert(it->second.size() > 1); it->second.erase(v.second); } this->rootItem->takeChild(this->rootItem->indexOfChild(docItem)); delete docItem; DocumentMap.erase(it); } } void TreeWidget::slotDeleteObject(const Gui::ViewProviderDocumentObject& view) { _slotDeleteObject(view, nullptr); } void TreeWidget::_slotDeleteObject(const Gui::ViewProviderDocumentObject& view, DocumentItem* deletingDoc) { auto obj = view.getObject(); auto itEntry = ObjectTable.find(obj); if (itEntry == ObjectTable.end()) return; if (itEntry->second.empty()) { ObjectTable.erase(itEntry); return; } TREE_LOG("delete object " << obj->getFullName()); bool needUpdate = false; for (const auto& data : itEntry->second) { DocumentItem* docItem = data->docItem; if (docItem == deletingDoc) continue; auto doc = docItem->document()->getDocument(); auto& items = data->items; if (obj->getDocument() == doc) docItem->_ParentMap.erase(obj); bool lock = blockSelection(true); for (auto cit = items.begin(), citNext = cit; cit != items.end(); cit = citNext) { ++citNext; (*cit)->myOwner = nullptr; delete* cit; } blockSelection(lock); // Check for any child of the deleted object that is not in the tree, and put it // under document item. for (auto child : data->children) { auto childVp = docItem->getViewProvider(child); if (!childVp || child->getDocument() != doc) continue; docItem->_ParentMap[child].erase(obj); auto cit = docItem->ObjectMap.find(child); if (cit == docItem->ObjectMap.end() || cit->second->items.empty()) { if (docItem->createNewItem(*childVp)) needUpdate = true; } else { auto childItem = *cit->second->items.begin(); if (childItem->requiredAtRoot(false)) { if (docItem->createNewItem(*childItem->object(), docItem, -1, childItem->myData)) needUpdate = true; } } childVp->setShowable(docItem->isObjectShowable(child)); } docItem->ObjectMap.erase(obj); } ObjectTable.erase(itEntry); if (needUpdate) _updateStatus(); } bool DocumentItem::populateObject(App::DocumentObject* obj) { // make sure at least one of the item corresponding to obj is populated auto it = ObjectMap.find(obj); if (it == ObjectMap.end()) return false; auto& items = it->second->items; if (items.empty()) return false; for (auto item : items) { if (item->populated) return true; } TREE_LOG("force populate object " << obj->getFullName()); auto item = *items.begin(); item->populated = true; populateItem(item, true); return true; } void DocumentItem::populateItem(DocumentObjectItem* item, bool refresh, bool delay) { (void)delay; if (item->populated && !refresh) return; // Lazy loading policy: We will create an item for each children object if // a) the item is expanded, or b) there is at least one free child, i.e. // child originally located at root. item->setChildIndicatorPolicy(item->myData->children.empty() ? QTreeWidgetItem::DontShowIndicator : QTreeWidgetItem::ShowIndicator); if (!item->populated && !item->isExpanded()) { bool doPopulate = false; bool external = item->object()->getDocument() != item->getOwnerDocument()->document(); if (external) return; auto obj = item->object()->getObject(); auto linked = obj->getLinkedObject(true); if (linked && linked->getDocument() != obj->getDocument()) return; for (auto child : item->myData->children) { auto it = ObjectMap.find(child); if (it == ObjectMap.end() || it->second->items.empty()) { auto vp = getViewProvider(child); if (!vp) continue; doPopulate = true; break; } if (item->myData->removeChildrenFromRoot) { if (it->second->rootItem) { doPopulate = true; break; } } } if (!doPopulate) return; } item->populated = true; bool checkHidden = !showHidden(); bool updated = false; int i = -1; // iterate through the claimed children, and try to synchronize them with the // children tree item with the same order of appearance. int childCount = item->childCount(); for (auto child : item->myData->children) { ++i; // the current index of the claimed child bool found = false; for (int j = i; j < childCount; ++j) { QTreeWidgetItem* ci = item->child(j); if (ci->type() != TreeWidget::ObjectType) continue; auto childItem = static_cast(ci); if (childItem->object()->getObject() != child) continue; found = true; if (j != i) { // fix index if it is changed childItem->setHighlight(false); item->removeChild(ci); item->insertChild(i, ci); assert(ci->parent() == item); if (checkHidden) updateItemsVisibility(ci, false); } // Check if the item just changed its policy of whether to remove // children item from the root. if (item->myData->removeChildrenFromRoot) { if (childItem->myData->rootItem) { assert(childItem != childItem->myData->rootItem); bool lock = getTree()->blockSelection(true); delete childItem->myData->rootItem; getTree()->blockSelection(lock); } } else if (childItem->requiredAtRoot()) { createNewItem(*childItem->object(), this, -1, childItem->myData); updated = true; } break; } if (found) continue; // This algo will be recursively applied to newly created child items // through slotNewObject -> populateItem auto it = ObjectMap.find(child); if (it == ObjectMap.end() || it->second->items.empty()) { auto vp = getViewProvider(child); if (!vp || !createNewItem(*vp, item, i, it == ObjectMap.end() ? DocumentObjectDataPtr() : it->second)) --i; else updated = true; continue; } if (!item->myData->removeChildrenFromRoot || !it->second->rootItem) { DocumentObjectItem* childItem = *it->second->items.begin(); if (!createNewItem(*childItem->object(), item, i, it->second)) --i; else updated = true; } else { DocumentObjectItem* childItem = it->second->rootItem; if (item == childItem || item->isChildOfItem(childItem)) { TREE_ERR("Cyclic dependency in " << item->object()->getObject()->getFullName() << '.' << childItem->object()->getObject()->getFullName()); --i; continue; } it->second->rootItem = nullptr; childItem->setHighlight(false); this->removeChild(childItem); item->insertChild(i, childItem); assert(childItem->parent() == item); if (checkHidden) updateItemsVisibility(childItem, false); } } for (++i; item->childCount() > i;) { QTreeWidgetItem* ci = item->child(i); if (ci->type() == TreeWidget::ObjectType) { auto childItem = static_cast(ci); if (childItem->requiredAtRoot()) { item->removeChild(childItem); auto index = findRootIndex(childItem->object()->getObject()); if (index >= 0) this->insertChild(index, childItem); else this->addChild(childItem); assert(childItem->parent() == this); if (checkHidden) updateItemsVisibility(childItem, false); childItem->myData->rootItem = childItem; continue; } } bool lock = getTree()->blockSelection(true); delete ci; getTree()->blockSelection(lock); } if (updated) getTree()->_updateStatus(); } int DocumentItem::findRootIndex(App::DocumentObject* childObj) { if (!TreeParams::getKeepRootOrder() || !childObj || !childObj->isAttachedToDocument()) return -1; // Use view provider's tree rank to find correct place at the root level. int count = this->childCount(); if (!count) return -1; int first, last; auto getTreeRank = [](Gui::ViewProviderDocumentObject* vp) -> int { if (vp->getTreeRank() == -1) { vp->setTreeRank(vp->getObject()->getID()); } return vp->getTreeRank(); }; auto vpc = dynamic_cast(Application::Instance->getViewProvider(childObj)); int childTreeRank = getTreeRank(vpc); // find the last item for (last = count - 1; last >= 0; --last) { auto citem = this->child(last); if (citem->type() == TreeWidget::ObjectType) { auto vp = static_cast(citem)->object(); if (getTreeRank(vp) <= childTreeRank) { return last + 1; } break; } } // find the first item for (first = 0; first < count; ++first) { auto citem = this->child(first); if (citem->type() == TreeWidget::ObjectType) { auto vp = static_cast(citem)->object(); if (getTreeRank(vp) > childTreeRank) { return first; } break; } } // now do a binary search to find the lower bound, assuming the root level // object is already in order count = last - first; int pos; while (count > 0) { int step = count / 2; pos = first + step; for (; pos <= last; ++pos) { auto citem = this->child(pos); if (citem->type() != TreeWidget::ObjectType) continue; auto vp = static_cast(citem)->object(); if (vp->getTreeRank() < childTreeRank) { first = ++pos; count -= step + 1; } else count = step; break; } if (pos > last) return -1; } if (first > last) return -1; return first; } void DocumentItem::sortObjectItems() { QSignalBlocker guard(getTree()); std::vector sortedItems; sortedItems.reserve(this->childCount()); for (int i = 0; i < this->childCount(); ++i) { QTreeWidgetItem* treeItem = this->child(i); if (treeItem->type() == TreeWidget::ObjectType) { sortedItems.push_back(static_cast(treeItem)); } } std::stable_sort(sortedItems.begin(), sortedItems.end(), [](DocumentObjectItem* a, DocumentObjectItem* b) { return a->object()->getTreeRank() < b->object()->getTreeRank(); }); int sortedIndex = 0; std::vector expansion; for (int i = 0; i < this->childCount(); ++i) { QTreeWidgetItem* treeItem = this->child(i); if (treeItem->type() != TreeWidget::ObjectType) { continue; } DocumentObjectItem* sortedItem = sortedItems[sortedIndex++]; if (sortedItem == treeItem) { continue; } expansion.clear(); sortedItem->getExpandedSnapshot(expansion); this->removeChild(sortedItem); this->insertChild(i, sortedItem); if (!showHidden()) { updateItemsVisibility(sortedItem, false); } std::vector::const_iterator expFrom = expansion.cbegin(); sortedItem->applyExpandedSnapshot(expansion, expFrom); } } void TreeWidget::slotChangeObject( const Gui::ViewProviderDocumentObject& view, const App::Property& prop) { auto obj = view.getObject(); if (!obj || !obj->isAttachedToDocument()) return; auto itEntry = ObjectTable.find(obj); if (itEntry == ObjectTable.end() || itEntry->second.empty()) return; _updateStatus(); // Let's not waste time on the newly added Visibility property in // DocumentObject. if (&prop == &obj->Visibility) return; if (&prop == &obj->Label) { const char* label = obj->Label.getValue(); auto firstData = *itEntry->second.begin(); if (firstData->label != label) { for (const auto& data : itEntry->second) { data->label = label; auto displayName = QString::fromUtf8(label); for (auto item : data->items) item->setText(0, displayName); } } return; } if (&prop == &obj->Label2) { const char* label = obj->Label2.getValue(); auto firstData = *itEntry->second.begin(); if (firstData->label2 != label) { for (const auto& data : itEntry->second) { data->label2 = label; auto displayName = QString::fromUtf8(label); for (auto item : data->items) item->setText(1, displayName); } } return; } auto& s = ChangedObjects[obj]; if (prop.testStatus(App::Property::Output) || prop.testStatus(App::Property::NoRecompute)) { s.set(CS_Output); } } void TreeWidget::updateChildren(App::DocumentObject* obj, const std::set& dataSet, bool propOutput, bool force) { bool childrenChanged = false; std::vector children; bool removeChildrenFromRoot = true; DocumentObjectDataPtr found; for (auto data : dataSet) { if (!found) { found = data; childrenChanged = found->updateChildren(force); removeChildrenFromRoot = found->viewObject->canRemoveChildrenFromRoot(); if (!childrenChanged && found->removeChildrenFromRoot == removeChildrenFromRoot) return; } else if (childrenChanged) data->updateChildren(found); data->removeChildrenFromRoot = removeChildrenFromRoot; DocumentItem* docItem = data->docItem; for (auto item : data->items) docItem->populateItem(item, true); } if (force) return; if (childrenChanged && propOutput) { // When a property is marked as output, it will not touch its object, // and thus, its property change will not be propagated through // recomputation. So we have to manually check for each links here. for (auto link : App::GetApplication().getLinksTo(obj, App::GetLinkRecursive)) { if (ChangedObjects.count(link)) continue; std::vector linkedChildren; DocumentObjectDataPtr found; auto it = ObjectTable.find(link); if (it == ObjectTable.end()) continue; for (auto data : it->second) { if (!found) { found = data; if (!found->updateChildren(false)) break; } data->updateChildren(found); DocumentItem* docItem = data->docItem; for (auto item : data->items) docItem->populateItem(item, true); } } } if (childrenChanged) { if (!selectTimer->isActive()) onSelectionChanged(SelectionChanges()); //if the item is in a GeoFeatureGroup we may need to update that too, as the claim children //of the geofeaturegroup depends on what the childs claim auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(obj); if (grp && !ChangedObjects.count(grp)) { auto iter = ObjectTable.find(grp); if (iter != ObjectTable.end()) updateChildren(grp, iter->second, true, false); } } } void DocumentItem::slotHighlightObject(const Gui::ViewProviderDocumentObject& obj, const Gui::HighlightMode& high, bool set, const App::DocumentObject* parent, const char* subname) { getTree()->_updateStatus(false); if (parent && parent->getDocument() != document()->getDocument()) { auto it = getTree()->DocumentMap.find(Application::Instance->getDocument(parent->getDocument())); if (it != getTree()->DocumentMap.end()) it->second->slotHighlightObject(obj, high, set, parent, subname); return; } FOREACH_ITEM(item, obj) if (parent) { App::DocumentObject* topParent = nullptr; std::ostringstream ss; item->getSubName(ss, topParent); if (!topParent) { if (parent != obj.getObject()) continue; } } item->setHighlight(set, high); if (parent) return; END_FOREACH_ITEM } static unsigned int countExpandedItem(const QTreeWidgetItem* item) { unsigned int size = 0; for (int i = 0, count = item->childCount(); i < count; ++i) { auto citem = item->child(i); if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded()) continue; auto obj = static_cast(citem)->object()->getObject(); if (obj->isAttachedToDocument()) size += strlen(obj->getNameInDocument()) + countExpandedItem(citem); } return size; } unsigned int DocumentItem::getMemSize() const { return countExpandedItem(this); } static void saveExpandedItem(Base::Writer& writer, const QTreeWidgetItem* item) { int itemCount = 0; for (int i = 0, count = item->childCount(); i < count; ++i) { auto citem = item->child(i); if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded()) continue; auto obj = static_cast(citem)->object()->getObject(); if (obj->isAttachedToDocument()) ++itemCount; } if (!itemCount) { writer.Stream() << "/>" << std::endl; return; } writer.Stream() << " count=\"" << itemCount << "\">" << std::endl; writer.incInd(); for (int i = 0, count = item->childCount(); i < count; ++i) { auto citem = item->child(i); if (citem->type() != TreeWidget::ObjectType || !citem->isExpanded()) continue; auto obj = static_cast(citem)->object()->getObject(); if (obj->isAttachedToDocument()) { writer.Stream() << writer.ind() << "getNameInDocument() << "\""; saveExpandedItem(writer, static_cast(citem)); } } writer.decInd(); writer.Stream() << writer.ind() << "" << std::endl; } void DocumentItem::Save(Base::Writer& writer) const { writer.Stream() << writer.ind() << "restore(reader); for (auto inst : TreeWidget::Instances) { if (inst != getTree()) { auto docItem = inst->getDocumentItem(document()); if (docItem) docItem->_ExpandInfo = _ExpandInfo; } } } void DocumentItem::restoreItemExpansion(const ExpandInfoPtr& info, DocumentObjectItem* item) { item->setExpanded(true); if (!info) return; for (int i = 0, count = item->childCount(); i < count; ++i) { auto citem = item->child(i); if (citem->type() != TreeWidget::ObjectType) continue; auto obj = static_cast(citem)->object()->getObject(); if (!obj->isAttachedToDocument()) continue; auto it = info->find(obj->getNameInDocument()); if (it != info->end()) restoreItemExpansion(it->second, static_cast(citem)); } } void DocumentItem::slotExpandObject(const Gui::ViewProviderDocumentObject& obj, const Gui::TreeItemMode& mode, const App::DocumentObject* parent, const char* subname) { getTree()->_updateStatus(false); if ((mode == TreeItemMode::ExpandItem || mode == TreeItemMode::ExpandPath) && obj.getDocument()->getDocument()->testStatus(App::Document::Restoring)) { if (!_ExpandInfo) _ExpandInfo.reset(new ExpandInfo); _ExpandInfo->emplace(std::string("*") + obj.getObject()->getNameInDocument(), ExpandInfoPtr()); return; } if (parent && parent->getDocument() != document()->getDocument()) { auto it = getTree()->DocumentMap.find(Application::Instance->getDocument(parent->getDocument())); if (it != getTree()->DocumentMap.end()) it->second->slotExpandObject(obj, mode, parent, subname); return; } FOREACH_ITEM(item, obj) // All document object items must always have a parent, either another // object item or document item. If not, then there is a bug somewhere // else. assert(item->parent()); switch (mode) { case TreeItemMode::ExpandPath: if (!parent) { QTreeWidgetItem* parentItem = item->parent(); while (parentItem) { parentItem->setExpanded(true); parentItem = parentItem->parent(); } item->setExpanded(true); break; } // fall through case TreeItemMode::ExpandItem: if (!parent) { if (item->parent()->isExpanded()) item->setExpanded(true); } else { App::DocumentObject* topParent = nullptr; std::ostringstream ss; item->getSubName(ss, topParent); if (!topParent) { if (parent != obj.getObject()) continue; } else if (topParent != parent) continue; showItem(item, false, true); item->setExpanded(true); } break; case TreeItemMode::CollapseItem: item->setExpanded(false); break; case TreeItemMode::ToggleItem: if (item->isExpanded()) item->setExpanded(false); else item->setExpanded(true); break; default: break; } if (item->isExpanded()) populateItem(item); if (parent) return; END_FOREACH_ITEM } void DocumentItem::slotScrollToObject(const Gui::ViewProviderDocumentObject& obj) { if (!obj.getObject() || !obj.getObject()->isAttachedToDocument()) return; auto it = ObjectMap.find(obj.getObject()); if (it == ObjectMap.end() || it->second->items.empty()) return; auto item = it->second->rootItem; if (!item) item = *it->second->items.begin(); getTree()->_updateStatus(false); getTree()->scrollToItem(item); } void DocumentItem::slotRecomputedObject(const App::DocumentObject& obj) { if (obj.isValid()) return; slotRecomputed(*obj.getDocument(), { const_cast(&obj) }); } void DocumentItem::slotRecomputed(const App::Document&, const std::vector& objs) { auto tree = getTree(); for (auto obj : objs) { if (!obj->isValid()) tree->ChangedObjects[obj].set(TreeWidget::CS_Error); } if (!tree->ChangedObjects.empty()) tree->_updateStatus(); } Gui::Document* DocumentItem::document() const { return this->pDocument; } void DocumentItem::testStatus() { for (const auto& v : ObjectMap) v.second->testStatus(); } void DocumentItem::setData(int column, int role, const QVariant& value) { if (role == Qt::EditRole) { QString label = value.toString(); pDocument->getDocument()->Label.setValue((const char*)label.toUtf8()); } QTreeWidgetItem::setData(column, role, value); } void DocumentItem::clearSelection(DocumentObjectItem* exclude) { // Block signals here otherwise we get a recursion and quadratic runtime bool ok = treeWidget()->blockSignals(true); FOREACH_ITEM_ALL(item); _v.second->dirtyFlag = false; if (item == exclude) { if (item->selected > 0) item->selected = -1; else item->selected = 0; updateItemSelection(item); // The set has been changed while calling updateItemSelection // so that the iterator has become invalid -> Abort if (_v.second->dirtyFlag) { break; } } else { item->selected = 0; item->mySubs.clear(); item->setSelected(false); item->setCheckState(false); } END_FOREACH_ITEM; treeWidget()->blockSignals(ok); } void DocumentItem::updateSelection(QTreeWidgetItem* ti, bool unselect) { for (int i = 0, count = ti->childCount(); i < count; ++i) { auto child = ti->child(i); if (child && child->type() == TreeWidget::ObjectType) { auto childItem = static_cast(child); if (unselect) { childItem->setSelected(false); childItem->setCheckState(false); } updateItemSelection(childItem); if (unselect && childItem->isGroup()) { // If the child item being force unselected by its group parent // is itself a group, propagate the unselection to its own // children updateSelection(childItem, true); } } } if (unselect) return; for (int i = 0, count = ti->childCount(); i < count; ++i) updateSelection(ti->child(i)); } void DocumentItem::updateItemSelection(DocumentObjectItem* item) { // Note: In several places of this function the selection is altered and the notification of // the selection observers can trigger a recreation of all DocumentObjectItem so that the // passed 'item' can become a dangling pointer. // Thus,'item' mustn't be accessed any more after altering the selection. // For further details see the bug analysis of #13107 bool selected = item->isSelected(); bool checked = item->checkState(0) == Qt::Checked; if (selected && !checked) item->setCheckState(true); if (!selected && checked) item->setCheckState(false); if ((selected && item->selected > 0) || (!selected && !item->selected)) { return; } if (item->selected != -1) item->mySubs.clear(); item->selected = selected; auto obj = item->object()->getObject(); if (!obj || !obj->isAttachedToDocument()) return; std::ostringstream str; App::DocumentObject* topParent = nullptr; item->getSubName(str, topParent); if (topParent) { if (!obj->redirectSubName(str, topParent, nullptr)) str << obj->getNameInDocument() << '.'; obj = topParent; } const char* objname = obj->getNameInDocument(); const char* docname = obj->getDocument()->getName(); const auto& subname = str.str(); #ifdef FC_DEBUG if (!subname.empty()) { assert(item->getParentItem()); } #endif if (!selected) { Gui::Selection().rmvSelection(docname, objname, subname.c_str()); return; } auto vobj = item->object(); selected = false; if (!item->mySubs.empty()) { for (auto& sub : item->mySubs) { if (Gui::Selection().addSelection(docname, objname, (subname + sub).c_str())) { selected = true; } } } if (!selected) { item->mySubs.clear(); if (!Gui::Selection().addSelection(docname, objname, subname.c_str())) { // Safely re-access the item DocumentObjectItem* item2 = findItem(vobj->getObject(), subname); if (item2) { item2->selected = 0; item2->setSelected(false); item2->setCheckState(false); } return; } } getTree()->syncView(vobj); } App::DocumentObject* DocumentItem::getTopParent(App::DocumentObject* obj, std::string& subname) { auto it = ObjectMap.find(obj); if (it == ObjectMap.end() || it->second->items.empty()) return nullptr; // already a top parent if (it->second->rootItem) return obj; for (auto item : it->second->items) { // non group object do not provide a coordinate system, hence its // claimed child is still in the global coordinate space, so the // child can still be considered a top level object if (!item->isParentGroup()) return obj; } // If no top level item, find an item that is closest to the top level std::multimap items; for (auto item : it->second->items) { int i = 0; for (auto parent = item->parent(); parent; ++i, parent = parent->parent()) { if (parent->isHidden()) i += 1000; ++i; } items.emplace(i, item); } App::DocumentObject* topParent = nullptr; std::ostringstream ss; items.begin()->second->getSubName(ss, topParent); if (!topParent) { // this shouldn't happen FC_WARN("No top parent for " << obj->getFullName() << '.' << subname); return obj; } ss << obj->getNameInDocument() << '.' << subname; FC_LOG("Subname correction " << obj->getFullName() << '.' << subname << " -> " << topParent->getFullName() << '.' << ss.str()); subname = ss.str(); return topParent; } DocumentObjectItem *DocumentItem::findItem(App::DocumentObject* obj, const std::string& subname) const { auto it = ObjectMap.find(obj); if (it == ObjectMap.end()) { return nullptr; } // There is only one instance in the tree view if (it->second->items.size() == 1) { return *(it->second->items.begin()); } // If there are multiple instances use the one with the same subname DocumentObjectItem* item {}; for (auto jt : it->second->items) { std::ostringstream str; App::DocumentObject* topParent = nullptr; jt->getSubName(str, topParent); if (topParent) { if (!obj->redirectSubName(str, topParent, nullptr)) { str << obj->getNameInDocument() << '.'; } } if (subname == str.str()) { item = jt; break; } } return item; } DocumentObjectItem* DocumentItem::findItemByObject( bool sync, App::DocumentObject* obj, const char* subname, bool select) { if (!subname) subname = ""; auto it = ObjectMap.find(obj); if (it == ObjectMap.end() || it->second->items.empty()) return nullptr; // prefer top level item of this object if (it->second->rootItem) return findItem(sync, it->second->rootItem, subname, select); for (auto item : it->second->items) { // non group object do not provide a coordinate system, hence its // claimed child is still in the global coordinate space, so the // child can still be considered a top level object if (!item->isParentGroup()) return findItem(sync, item, subname, select); } // If no top level item, find an item that is closest to the top level std::multimap items; for (auto item : it->second->items) { int i = 0; for (auto parent = item->parent(); parent; ++i, parent = parent->parent()) ++i; items.emplace(i, item); } for (auto& v : items) { auto item = findItem(sync, v.second, subname, select); if (item) return item; } return nullptr; } DocumentObjectItem* DocumentItem::findItem( bool sync, DocumentObjectItem* item, const char* subname, bool select) { if (item->isHidden()) item->setHidden(false); if (!subname || *subname == 0) { if (select) { item->selected += 2; item->mySubs.clear(); } return item; } TREE_TRACE("find next " << subname); // try to find the next level object name const char* nextsub = nullptr; const char* dot = nullptr; if ((dot = strchr(subname, '.'))) nextsub = dot + 1; else { if (select) { item->selected += 2; if (std::ranges::find(item->mySubs, subname) == item->mySubs.end()) { item->mySubs.emplace_back(subname); } } return item; } std::string name(subname, nextsub - subname); auto obj = item->object()->getObject(); auto subObj = obj->getSubObject(name.c_str()); if (!subObj || subObj == obj) { if (!subObj && !getTree()->searchDoc) TREE_LOG("sub object not found " << item->getName() << '.' << name.c_str()); if (select) { item->selected += 2; if (std::ranges::find(item->mySubs, subname) == item->mySubs.end()) item->mySubs.emplace_back(subname); } return item; } if (select) item->mySubs.clear(); if (!item->populated && sync) { //force populate the item item->populated = true; populateItem(item, true); } for (int i = 0, count = item->childCount(); i < count; ++i) { auto ti = item->child(i); if (!ti || ti->type() != TreeWidget::ObjectType) continue; auto child = static_cast(ti); if (child->object()->getObject() == subObj) return findItem(sync, child, nextsub, select); } // The sub object is not found. This could happen for geo group, since its // children may be in more than one hierarchy down. bool found = false; DocumentObjectItem* res = nullptr; auto it = ObjectMap.find(subObj); if (it != ObjectMap.end()) { for (auto child : it->second->items) { if (child->isChildOfItem(item)) { found = true; res = findItem(sync, child, nextsub, select); if (!select) return res; } } } if (select && !found) { // The sub object is still not found. Maybe it is a non-object sub-element. // Select the current object instead. TREE_TRACE("element " << subname << " not found"); item->selected += 2; if (std::ranges::find(item->mySubs, subname) == item->mySubs.end()) item->mySubs.emplace_back(subname); } return res; } void DocumentItem::selectItems(SelectionReason reason) { const auto& sels = Selection().getSelection(pDocument->getDocument()->getName(), ResolveMode::NoResolve); bool sync = (sels.size() > 50 || reason == SR_SELECT) ? false : true; for (const auto& sel : sels) findItemByObject(sync, sel.pObject, sel.SubName, true); DocumentObjectItem* newSelect = nullptr; DocumentObjectItem* oldSelect = nullptr; FOREACH_ITEM_ALL(item) if (item->selected == 1) { // this means it is the old selection and is not in the current // selection item->selected = 0; item->mySubs.clear(); item->setSelected(false); item->setCheckState(false); } else if (item->selected) { if (sync) { if (item->selected == 2 && showItem(item, false, reason == SR_FORCE_EXPAND)) { // This means newly selected and can auto expand if (!newSelect) newSelect = item; } if (!newSelect && !oldSelect && !item->isHidden()) { bool visible = true; for (auto parent = item->parent(); parent; parent = parent->parent()) { if (!parent->isExpanded() || parent->isHidden()) { visible = false; break; } } if (visible) oldSelect = item; } } item->selected = 1; item->setSelected(true); item->setCheckState(true); } END_FOREACH_ITEM; if (sync) { if (!newSelect) newSelect = oldSelect; else getTree()->syncView(newSelect->object()); if (newSelect) getTree()->scrollToItem(newSelect); } } void DocumentItem::populateParents(const ViewProvider* vp, ViewParentMap& parentMap) { auto it = parentMap.find(vp); if (it == parentMap.end()) return; for (auto parent : it->second) { auto it = ObjectMap.find(parent->getObject()); if (it == ObjectMap.end()) continue; populateParents(parent, parentMap); for (auto item : it->second->items) { if (!item->isHidden() && !item->populated) { item->populated = true; populateItem(item, true); } } } } void DocumentItem::selectAllInstances(const ViewProviderDocumentObject& vpd) { ViewParentMap parentMap; auto pObject = vpd.getObject(); if (ObjectMap.find(pObject) == ObjectMap.end()) return; bool lock = getTree()->blockSelection(true); // We are trying to select all items corresponding to a given view // provider, i.e. all appearance of the object inside all its parent items // // Build a map of object to all its parent for (auto& v : ObjectMap) { if (v.second->viewObject == &vpd) continue; for (auto child : v.second->viewObject->claimChildren()) { auto vp = getViewProvider(child); if (!vp) continue; parentMap[vp].push_back(v.second->viewObject); } } // now make sure all parent items are populated. In order to do that, we // need to populate the oldest parent first populateParents(&vpd, parentMap); DocumentObjectItem* first = nullptr; FOREACH_ITEM(item, vpd); if (showItem(item, true) && !first) first = item; END_FOREACH_ITEM; getTree()->blockSelection(lock); if (first) { treeWidget()->scrollToItem(first); updateSelection(); } } bool DocumentItem::showHidden() const { return pDocument->getDocument()->ShowHidden.getValue(); } void DocumentItem::setShowHidden(bool show) { pDocument->getDocument()->ShowHidden.setValue(show); } bool DocumentItem::showItem(DocumentObjectItem* item, bool select, bool force) { auto parent = item->parent(); if (item->isHidden()) { if (!force) return false; item->setHidden(false); } if (parent->type() == TreeWidget::ObjectType) { if (!showItem(static_cast(parent), false)) return false; auto pitem = static_cast(parent); if (force || !pitem->object()->getObject()->testStatus(App::NoAutoExpand)) parent->setExpanded(true); else if (!select) return false; } else parent->setExpanded(true); if (select) { item->setSelected(true); item->setCheckState(true); } return true; } void DocumentItem::updateItemsVisibility(QTreeWidgetItem* item, bool show) { if (item->type() == TreeWidget::ObjectType) { auto objitem = static_cast(item); objitem->setHidden(!show && !objitem->object()->showInTree()); } for (int i = 0; i < item->childCount(); ++i) updateItemsVisibility(item->child(i), show); } void DocumentItem::updateSelection() { bool lock = getTree()->blockSelection(true); updateSelection(this, false); getTree()->blockSelection(lock); } // ---------------------------------------------------------------------------- static int countItems; DocumentObjectItem::DocumentObjectItem(DocumentItem* ownerDocItem, DocumentObjectDataPtr data) : QTreeWidgetItem(TreeWidget::ObjectType) , myOwner(ownerDocItem), myData(data), previousStatus(-1), selected(0), populated(false) { setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsUserCheckable); setCheckState(false); myData->insertItem(this); ++countItems; TREE_LOG("Create item: " << countItems << ", " << object()->getObject()->getFullName()); } DocumentObjectItem::~DocumentObjectItem() { --countItems; TREE_LOG("Delete item: " << countItems << ", " << object()->getObject()->getFullName()); myData->removeItem(this); if (myData->rootItem == this) myData->rootItem = nullptr; if (myOwner && myData->items.empty()) { auto it = myOwner->_ParentMap.find(object()->getObject()); if (it != myOwner->_ParentMap.end() && !it->second.empty()) { myOwner->PopulateObjects.push_back(*it->second.begin()); myOwner->getTree()->_updateStatus(); } } } void DocumentObjectItem::restoreBackground() { this->setBackground(0, this->bgBrush); } void DocumentObjectItem::setHighlight(bool set, Gui::HighlightMode high) { QFont f = this->font(0); auto highlight = [this, set](const QColor& col) { if (set) this->setBackground(0, col); else this->setBackground(0, QBrush()); this->bgBrush = this->background(0); }; switch (high) { case HighlightMode::Bold: f.setBold(set); break; case HighlightMode::Italic: f.setItalic(set); break; case HighlightMode::Underlined: f.setUnderline(set); break; case HighlightMode::Overlined: f.setOverline(set); break; case HighlightMode::StrikeOut: f.setStrikeOut(set); break; case HighlightMode::Blue: highlight(QColor(200, 200, 255)); break; case HighlightMode::LightBlue: highlight(QColor(230, 230, 255)); break; case HighlightMode::UserDefined: { QColor color(230, 230, 255); if (set) { ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/TreeView"); bool bold = hGrp->GetBool("TreeActiveBold", true); bool italic = hGrp->GetBool("TreeActiveItalic", false); bool underlined = hGrp->GetBool("TreeActiveUnderlined", false); bool overlined = hGrp->GetBool("TreeActiveOverlined", false); f.setBold(bold); f.setItalic(italic); f.setUnderline(underlined); f.setOverline(overlined); unsigned long col = hGrp->GetUnsigned("TreeActiveColor", 1538528255); color = Base::Color::fromPackedRGB(col); } else { f.setBold(false); f.setItalic(false); f.setUnderline(false); f.setOverline(false); } highlight(color); } break; default: break; } this->setFont(0, f); } const char* DocumentObjectItem::getTreeName() const { return myData->getTreeName(); } Gui::ViewProviderDocumentObject* DocumentObjectItem::object() const { return myData->viewObject; } void DocumentObjectItem::testStatus(bool resetStatus) { QIcon icon, icon2; testStatus(resetStatus, icon, icon2); } namespace { enum Status { Visible = 1 << 0, Recompute = 1 << 1, Error = 1 << 2, Hidden = 1 << 3, External = 1 << 4, Freezed = 1 << 5 }; } void DocumentObjectItem::testStatus(bool resetStatus, QIcon& icon1, QIcon& icon2) { App::DocumentObject* pObject = object()->getObject(); int visible = -1; auto parentItem = getParentItem(); if (parentItem) { Timing(testStatus1); auto parent = parentItem->object()->getObject(); auto ext = parent->getExtensionByType(true, false); if (!ext) visible = parent->isElementVisible(pObject->getNameInDocument()); else { // We are dealing with a plain group. It has special handling when // linked, which allows it to have indpenedent visibility control. // We need to go up the hierarchy and see if there is any link to // it. for (auto pp = parentItem->getParentItem(); pp; pp = pp->getParentItem()) { auto obj = pp->object()->getObject(); if (!obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) { visible = pp->object()->getObject()->isElementVisible(pObject->getNameInDocument()); break; } } } } Timing(testStatus2); if (visible < 0) visible = object()->isShow() ? 1 : 0; auto obj = object()->getObject(); auto linked = obj->getLinkedObject(false); bool external = object()->getDocument() != getOwnerDocument()->document() || (linked && linked->getDocument() != obj->getDocument()); bool freezed = pObject->isFreezed(); int currentStatus = ((freezed ? 1 : 0) << 5) | ((external ? 1 : 0) << 4) | ((object()->showInTree() ? 0 : 1) << 3) | ((pObject->isError() ? 1 : 0) << 2) | ((pObject->isTouched() || pObject->mustExecute() == 1 ? 1 : 0) << 1) | (visible ? 1 : 0); TimingStop(testStatus2); if (!resetStatus && previousStatus == currentStatus) return; _Timing(1, testStatus3); previousStatus = currentStatus; QIcon::Mode mode = QIcon::Normal; if (currentStatus & Status::Visible) { // Note: By default the foreground, i.e. text color is invalid // to make use of the default color of the tree widget's palette. // If we temporarily set this color to dark and reset to an invalid // color again we cannot do it with setTextColor() or setForeground(), // respectively, because for any reason the color would always switch // to black which will lead to unreadable text if the system background // hss already a dark color. // However, it works if we set the appropriate role to an empty QVariant(). for (int column = 0; column < this->columnCount(); ++column) { this->setData(column, Qt::ForegroundRole, QVariant()); } } else { // invisible QStyleOptionViewItem opt; // it can happen that a tree item is not attached to the tree widget (#0003025) if (this->treeWidget()) opt.initFrom(this->treeWidget()); for (int column = 0; column < this->columnCount(); ++column) { this->setForeground(column, opt.palette.color(QPalette::Disabled, QPalette::Text)); } mode = QIcon::Disabled; } _TimingStop(1, testStatus3); QIcon& icon = mode == QIcon::Normal ? icon1 : icon2; if (icon.isNull()) { Timing(getIcon); QPixmap px; if (currentStatus & Status::Error) { static QPixmap pxError; if (pxError.isNull()) { // object is in error state pxError = Gui::BitmapFactory().pixmapFromSvg("overlay_error", QSize(10, 10)); } px = pxError; } else if (currentStatus & Status::Recompute) { static QPixmap pxRecompute; if (pxRecompute.isNull()) { // object must be recomputed pxRecompute = Gui::BitmapFactory().pixmapFromSvg("overlay_recompute", QSize(10, 10)); } px = pxRecompute; } // get the original icon set QIcon icon_org = object()->getIcon(); #if QT_VERSION < QT_VERSION_CHECK(6,0,0) int w = getTree()->viewOptions().decorationSize.width(); #else QStyleOptionViewItem opt; getTree()->initViewItemOption(&opt); int w = opt.decorationSize.width(); #endif QPixmap pxOn, pxOff; // if needed show small pixmap inside if (!px.isNull()) { pxOff = BitmapFactory().merge(icon_org.pixmap(w, w, mode, QIcon::Off), px, BitmapFactoryInst::TopRight); pxOn = BitmapFactory().merge(icon_org.pixmap(w, w, mode, QIcon::On), px, BitmapFactoryInst::TopRight); } else { pxOff = icon_org.pixmap(w, w, mode, QIcon::Off); pxOn = icon_org.pixmap(w, w, mode, QIcon::On); } if (currentStatus & Status::Hidden) { static QPixmap pxHidden; if (pxHidden.isNull()) { pxHidden = Gui::BitmapFactory().pixmapFromSvg("TreeItemVisible", QSize(10, 10)); } pxOff = BitmapFactory().merge(pxOff, pxHidden, BitmapFactoryInst::TopLeft); pxOn = BitmapFactory().merge(pxOn, pxHidden, BitmapFactoryInst::TopLeft); } if (currentStatus & Status::External) { static QPixmap pxExternal; constexpr int px = 12; if (pxExternal.isNull()) { pxExternal = Gui::BitmapFactory().pixmapFromSvg("LinkOverlay", QSize(px, px)); } pxOff = BitmapFactory().merge(pxOff, pxExternal, BitmapFactoryInst::BottomRight); pxOn = BitmapFactory().merge(pxOn, pxExternal, BitmapFactoryInst::BottomRight); } if (currentStatus & Status::Freezed) { static QPixmap pxFreeze; if (pxFreeze.isNull()) { // object is in freezed state pxFreeze = Gui::BitmapFactory().pixmapFromSvg("Std_ToggleFreeze", QSize(16, 16)); } pxOff = BitmapFactory().merge(pxOff, pxFreeze, BitmapFactoryInst::TopLeft); pxOn = BitmapFactory().merge(pxOn, pxFreeze, BitmapFactoryInst::TopLeft); } icon.addPixmap(pxOn, QIcon::Normal, QIcon::On); icon.addPixmap(pxOff, QIcon::Normal, QIcon::Off); icon = object()->mergeColorfulOverlayIcons(icon); if (isVisibilityIconEnabled()) { static QPixmap pxVisible, pxInvisible; if (pxVisible.isNull()) { pxVisible = BitmapFactory().pixmap("TreeItemVisible"); } if (pxInvisible.isNull()) { pxInvisible = BitmapFactory().pixmap("TreeItemInvisible"); } // Prepend the visibility pixmap to the final icon pixmaps and use these as the icon. QIcon new_icon; auto style = this->getTree()->style(); int const spacing = style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); for (auto state: {QIcon::On, QIcon::Off}) { QPixmap px_org = icon.pixmap(0xFFFF, 0xFFFF, QIcon::Normal, state); QPixmap px(2*px_org.width() + spacing, px_org.height()); px.fill(Qt::transparent); QPainter pt; pt.begin(&px); pt.setPen(Qt::NoPen); pt.drawPixmap(0, 0, px_org.width(), px_org.height(), (currentStatus & Status::Visible) ? pxVisible : pxInvisible); pt.drawPixmap(px_org.width() + spacing, 0, px_org.width(), px_org.height(), px_org); pt.end(); new_icon.addPixmap(px, QIcon::Normal, state); } icon = new_icon; } } _Timing(2, setIcon); this->setIcon(0, icon); } void DocumentObjectItem::displayStatusInfo() { App::DocumentObject* Obj = object()->getObject(); QString info = QApplication::translate(Obj->getTypeId().getName(), Obj->getStatusString()); if (Obj->mustExecute() == 1 && !Obj->isError()) info += TreeWidget::tr(" (but must be executed)"); QString status = TreeWidget::tr("%1, Internal name: %2") .arg(info, QString::fromLatin1(Obj->getNameInDocument())); if (!Obj->isError()) getMainWindow()->showMessage(status); else { getMainWindow()->showStatus(MainWindow::Err, status); QTreeWidget* tree = this->treeWidget(); QPoint pos = tree->visualItemRect(this).topRight(); QToolTip::showText(tree->mapToGlobal(pos), info); } } void DocumentObjectItem::setExpandedStatus(bool on) { if (getOwnerDocument()->document() == object()->getDocument()) object()->getObject()->setStatus(App::Expand, on); } void DocumentObjectItem::setData(int column, int role, const QVariant& value) { QVariant myValue(value); if (role == Qt::EditRole && column <= 1) { auto obj = object()->getObject(); auto& label = column ? obj->Label2 : obj->Label; std::ostringstream str; str << TreeWidget::tr("Rename").toStdString() << ' ' << getName() << '.' << label.getName(); // Explicitly open and commit a transaction since this is a single change here // For more details: https://forum.freecad.org/viewtopic.php?f=3&t=72351 App::Document* doc = obj->getDocument(); doc->openTransaction(str.str().c_str()); label.setValue(value.toString().toUtf8().constData()); doc->commitTransaction(); myValue = QString::fromUtf8(label.getValue()); } QTreeWidgetItem::setData(column, role, myValue); } bool DocumentObjectItem::isChildOfItem(DocumentObjectItem* item) { for (auto pitem = parent(); pitem; pitem = pitem->parent()) if (pitem == item) return true; return false; } bool DocumentObjectItem::requiredAtRoot(bool excludeSelf) const { if (myData->rootItem || object()->getDocument() != getOwnerDocument()->document()) return false; bool checkMap = true; for (auto item : myData->items) { if (excludeSelf && item == this) continue; auto pi = item->getParentItem(); if (!pi || pi->myData->removeChildrenFromRoot) return false; checkMap = false; } if (checkMap && myOwner) { auto it = myOwner->_ParentMap.find(object()->getObject()); if (it != myOwner->_ParentMap.end()) { // Reaching here means all items of this corresponding object is // going to be deleted, but the object itself is not deleted and // still being referred to by some parent item that is not expanded // yet. So, we force populate at least one item of the parent // object to make sure that there is at least one corresponding // item for each object. // // PS: practically speaking, it won't hurt much to delete all the // items, because the item will be auto created once the user // expand its parent item. It only causes minor problems, such as, // tree scroll to object command won't work properly. for (auto parent : it->second) { if (getOwnerDocument()->populateObject(parent)) return false; } } } return true; } bool DocumentObjectItem::isLink() const { auto obj = object()->getObject(); auto linked = obj->getLinkedObject(false); return linked && obj != linked; } bool DocumentObjectItem::isLinkFinal() const { auto obj = object()->getObject(); auto linked = obj->getLinkedObject(false); return linked && linked == linked->getLinkedObject(true); } bool DocumentObjectItem::isParentLink() const { auto pi = getParentItem(); return pi && pi->isLink(); } enum GroupType { NotGroup = 0, LinkGroup = 1, PartGroup = 2, SuperGroup = 3, //reversed for future }; int DocumentObjectItem::isGroup() const { auto obj = object()->getObject(); auto linked = obj->getLinkedObject(true); if (linked && linked->hasExtension( App::GeoFeatureGroupExtension::getExtensionClassTypeId())) return PartGroup; if (obj->hasChildElement()) return LinkGroup; if (obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) { for (auto parent = getParentItem(); parent; parent = parent->getParentItem()) { auto pobj = parent->object()->getObject(); if (pobj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) continue; if (pobj->isElementVisible(obj->getNameInDocument()) >= 0) return LinkGroup; } } return NotGroup; } bool DocumentItem::isObjectShowable(App::DocumentObject* obj) { auto itParents = _ParentMap.find(obj); if (itParents == _ParentMap.end() || itParents->second.empty()) return true; bool showable = true; for (auto parent : itParents->second) { if (parent->getDocument() != obj->getDocument()) continue; if (!parent->hasChildElement() && parent->getLinkedObject(false) == parent) return true; showable = false; } return showable; } int DocumentObjectItem::isParentGroup() const { auto pi = getParentItem(); return pi ? pi->isGroup() : 0; } DocumentObjectItem* DocumentObjectItem::getParentItem() const { if (parent()->type() != TreeWidget::ObjectType) return nullptr; return static_cast(parent()); } DocumentObjectItem* DocumentObjectItem::getNextSibling() const { QTreeWidgetItem* parent = this->parent(); if (parent) { int index = parent->indexOfChild(const_cast(this)); if (index >= 0) { while (++index < parent->childCount()) { QTreeWidgetItem* sibling = parent->child(index); if (sibling->type() == TreeWidget::ObjectType) { return static_cast(sibling); } } } } return nullptr; } DocumentObjectItem* DocumentObjectItem::getPreviousSibling() const { QTreeWidgetItem* parent = this->parent(); if (parent) { int index = parent->indexOfChild(const_cast(this)); while (index > 0) { QTreeWidgetItem* sibling = parent->child(--index); if (sibling->type() == TreeWidget::ObjectType) { return static_cast(sibling); } } } return nullptr; } const char* DocumentObjectItem::getName() const { const char* name = object()->getObject()->getNameInDocument(); return name ? name : ""; } int DocumentObjectItem::getSubName(std::ostringstream& str, App::DocumentObject*& topParent) const { auto parent = getParentItem(); if (!parent) return NotGroup; int ret = parent->getSubName(str, topParent); if (ret != SuperGroup) { int group = parent->isGroup(); if (group == NotGroup) { if (ret != PartGroup) { // Handle this situation, // // LinkGroup // |--PartExtrude // |--Sketch // // This function traverse from top down, so, when seeing a // non-group object 'PartExtrude', its following children should // not be grouped, so must reset any previous parents here. topParent = nullptr; str.str(""); //reset the current subname return NotGroup; } group = PartGroup; } ret = group; } auto obj = parent->object()->getObject(); if (!obj || !obj->isAttachedToDocument()) { topParent = nullptr; str.str(""); return NotGroup; } if (!topParent) topParent = obj; else if (!obj->redirectSubName(str, topParent, object()->getObject())) str << obj->getNameInDocument() << '.'; return ret; } App::DocumentObject* DocumentObjectItem::getFullSubName( std::ostringstream& str, DocumentObjectItem* parent) const { auto pi = getParentItem(); if (this == parent || !pi || (!parent && !pi->isGroup())) return object()->getObject(); auto ret = pi->getFullSubName(str, parent); str << getName() << '.'; return ret; } App::DocumentObject* DocumentObjectItem::getRelativeParent( std::ostringstream& str, DocumentObjectItem* cousin, App::DocumentObject** topParent, std::string* topSubname) const { std::ostringstream str2; App::DocumentObject* top = nullptr, * top2 = nullptr; getSubName(str, top); if (topParent) *topParent = top; if (!top) return nullptr; if (topSubname) *topSubname = str.str() + getName() + '.'; cousin->getSubName(str2, top2); if (top != top2) { str << getName() << '.'; return top; } auto subname = str.str(); auto subname2 = str2.str(); const char* sub = subname.c_str(); const char* sub2 = subname2.c_str(); while (true) { const char* dot = strchr(sub, '.'); if (!dot) { str.str(""); return nullptr; } const char* dot2 = strchr(sub2, '.'); if (!dot2 || dot - sub != dot2 - sub2 || strncmp(sub, sub2, dot - sub) != 0) { auto substr = subname.substr(0, dot - subname.c_str() + 1); auto ret = top->getSubObject(substr.c_str()); if (!top) { FC_ERR("invalid subname " << top->getFullName() << '.' << substr); str.str(""); return nullptr; } str.str(""); str << dot + 1 << getName() << '.'; return ret; } sub = dot + 1; sub2 = dot2 + 1; } str.str(""); return nullptr; } void DocumentObjectItem::setCheckState(bool checked) { if (isSelectionCheckBoxesEnabled()) QTreeWidgetItem::setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); else setData(0, Qt::CheckStateRole, QVariant()); } DocumentItem* DocumentObjectItem::getParentDocument() const { return getTree()->getDocumentItem(object()->getDocument()); } DocumentItem* DocumentObjectItem::getOwnerDocument() const { return myOwner; } TreeWidget* DocumentObjectItem::getTree() const { return static_cast(treeWidget()); } void DocumentObjectItem::getExpandedSnapshot(std::vector& snapshot) const { snapshot.push_back(isExpanded()); for (int i = 0; i < childCount(); ++i) { static_cast(child(i))->getExpandedSnapshot(snapshot); } } void DocumentObjectItem::applyExpandedSnapshot(const std::vector& snapshot, std::vector::const_iterator& from) { setExpanded(*from++); for (int i = 0; i < childCount(); ++i) { static_cast(child(i))->applyExpandedSnapshot(snapshot, from); } } #include "moc_Tree.cpp"