diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 8b725210fb..b6b858bc82 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -3684,6 +3684,116 @@ void StdTreeDrag::activated(int) } } +//=========================================================================== +// Std_GroupMoveUp +//=========================================================================== +DEF_STD_CMD_A(StdGroupMoveUp) + +StdGroupMoveUp::StdGroupMoveUp() + : Command("Std_GroupMoveUp") +{ + sGroup = QT_TR_NOOP("TreeView"); + sMenuText = QT_TR_NOOP("Move up in group"); + sToolTipText = QT_TR_NOOP("Move object one place higher in its group"); + sStatusTip = sToolTipText; + sWhatsThis = "Std_GroupMoveUp"; + sPixmap = "button_up"; + sAccel = "Alt+Up"; + eType = 0; +} + +void StdGroupMoveUp::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + TreeWidget *tree = TreeWidget::instance(); + if (!tree) { + return; + } + + std::vector selected; + if (!tree->getSelectedSiblingObjectItems(selected)) { + return; + } + + DocumentObjectItem *previous; + if (!tree->allowMoveUpInGroup(selected, &previous)) { + return; + } + + TreeWidget::moveSiblings(selected, previous, -1); +} + +bool StdGroupMoveUp::isActive(void) +{ + TreeWidget *tree = TreeWidget::instance(); + if (!tree) { + return false; + } + + std::vector selected; + if (!tree->getSelectedSiblingObjectItems(selected)) { + return false; + } + + return tree->allowMoveUpInGroup(selected); +} + +//=========================================================================== +// Std_GroupMoveDown +//=========================================================================== +DEF_STD_CMD_A(StdGroupMoveDown) + +StdGroupMoveDown::StdGroupMoveDown() + : Command("Std_GroupMoveDown") +{ + sGroup = QT_TR_NOOP("TreeView"); + sMenuText = QT_TR_NOOP("Move down in group"); + sToolTipText = QT_TR_NOOP("Move object one place lower in its group"); + sStatusTip = sToolTipText; + sWhatsThis = "Std_GroupMoveDown"; + sPixmap = "button_down"; + sAccel = "Alt+Down"; + eType = 0; +} + +void StdGroupMoveDown::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + TreeWidget *tree = TreeWidget::instance(); + if (!tree) { + return; + } + + std::vector selected; + if (!tree->getSelectedSiblingObjectItems(selected)) { + return; + } + + DocumentObjectItem *next ; + if (!tree->allowMoveDownInGroup(selected, &next)) { + return; + } + + TreeWidget::moveSiblings(selected, next, +1); +} + +bool StdGroupMoveDown::isActive(void) +{ + TreeWidget *tree = TreeWidget::instance(); + if (!tree) { + return false; + } + + std::vector selected; + if (!tree->getSelectedSiblingObjectItems(selected)) { + return false; + } + + return tree->allowMoveDownInGroup(selected); +} + //====================================================================== // Std_TreeViewActions //=========================================================================== @@ -3718,6 +3828,11 @@ public: addCommand(new StdTreeDrag(),cmds.size()); addCommand(new StdTreeSelection(),cmds.size()); + + addCommand(); + + addCommand(new StdGroupMoveUp()); + addCommand(new StdGroupMoveDown()); }; virtual const char* className() const {return "StdCmdTreeViewActions";} }; diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp index 813e84a69f..d5110459e2 100644 --- a/src/Gui/Document.cpp +++ b/src/Gui/Document.cpp @@ -727,6 +727,9 @@ void Document::slotNewObject(const App::DocumentObject& Obj) activeView->getViewer()->addViewProvider(pcProvider); } + // If no tree rank was assigned, do it now, otherwise keep the current one + pcProvider->TreeRank.setValue(pcProvider->TreeRank.getValue()); + // adding to the tree signalNewObject(*pcProvider); pcProvider->pcDocument = this; @@ -1429,13 +1432,29 @@ void Document::RestoreDocFile(Base::Reader &reader) expanded = true; } } + + int rank = 0; + if (localreader->hasAttribute("rank")) { + rank = localreader->getAttributeAsInteger("rank"); + } + ViewProvider* pObj = getViewProviderByName(name.c_str()); if (pObj) // check if this feature has been registered pObj->Restore(*localreader); - if (pObj && expanded) { - Gui::ViewProviderDocumentObject* vp = static_cast(pObj); - this->signalExpandObject(*vp, TreeItemMode::ExpandItem,0,0); + + ViewProviderDocumentObject *vpdo = dynamic_cast(pObj); + if (vpdo) { + if (rank <= 0 ) { + // For backward compatibility, use object ID as tree rank + rank = vpdo->getObject()->getID(); + } + vpdo->TreeRank.setValue(rank); + + if (expanded) { + this->signalExpandObject(*vpdo, TreeItemMode::ExpandItem, 0, 0); + } } + localreader->readEndElement("ViewProvider"); } localreader->readEndElement("ViewProviderData"); @@ -1555,6 +1574,11 @@ void Document::SaveDocFile (Base::Writer &writer) const if (obj->hasExtensions()) writer.Stream() << " Extensions=\"True\""; + ViewProviderDocumentObject *vpdo = dynamic_cast(obj); + if (vpdo && vpdo->TreeRank.getValue()) { + writer.Stream() << " rank=\"" << vpdo->TreeRank.getValue() << "\""; + } + writer.Stream() << ">" << std::endl; obj->Save(writer); writer.Stream() << writer.ind() << "" << std::endl; diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 6d400dbf66..53545268cb 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -340,8 +340,18 @@ public: } } } - // We still need to check the order of the children + + // Sort the child items by their tree rank + std::stable_sort(newChildren.begin(), newChildren.end(), + [this](App::DocumentObject *a, App::DocumentObject *b) { + ViewProviderDocumentObject *vpa = this->docItem->getViewProvider(a); + ViewProviderDocumentObject *vpb = this->docItem->getViewProvider(b); + return vpa->TreeRank.getValue() < vpb->TreeRank.getValue(); + }); + + // Mark updated in case the order of the children did change updated = updated || children!=newChildren; + children.swap(newChildren); childSet.swap(newSet); @@ -555,6 +565,9 @@ TreeWidget::TreeWidget(const char *name, QWidget* parent) this, SLOT(onItemSelectionChanged())); connect(this, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(onItemChanged(QTreeWidgetItem*, int))); + connect(MainWindow::getInstance(), SIGNAL(tabifiedDockWidgetActivated(QDockWidget *)), + this, SLOT(onTabifiedDockWidgetActivated(QDockWidget *))); + connect(this->preselectTimer, SIGNAL(timeout()), this, SLOT(onPreSelectTimer())); connect(this->selectTimer, SIGNAL(timeout()), @@ -1387,6 +1400,17 @@ void TreeWidget::keyPressEvent(QKeyEvent *event) return; } } + + if (event->modifiers() == Qt::AltModifier + && (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down)) { + // Consume the Alt+Up/Down keypresses. This is because if the "Move Up/Down in Group" + // key shortcut is inactive, they are interpreted as ordinary up/down single selection + // move, which results in confusing behavior on item group borders crossing + event->accept(); + return; + } + + QTreeWidget::keyPressEvent(event); } @@ -2337,6 +2361,10 @@ void TreeWidget::slotChangedViewObject(const Gui::ViewProvider& vp, const App::P ChangedObjects.emplace(vpd.getObject(),0); _updateStatus(); } + else if (&prop == &vpd.TreeRank) { + ReorderedObjects.insert(vpd.getObject()); + _updateStatus(); + } } } @@ -2498,6 +2526,47 @@ void TreeWidget::onUpdateStatus(void) } ChangedObjects.clear(); + // Sort parents of object items with adjusted order + std::set reorderParents; + for (auto &obj : ReorderedObjects) { + ViewProvider *vp = Application::Instance->getViewProvider(obj); + if (!vp || !vp->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId())) { + continue; + } + + auto docIt = DocumentMap.find(static_cast(vp)->getDocument()); + if (docIt == DocumentMap.end() || !docIt->second) { + continue; + } + + DocumentItem *docItem = docIt->second; + auto parentIt = docItem->_ParentMap.find(obj); + if (parentIt != docItem->_ParentMap.end() && parentIt->second.size() > 0) { + for (App::DocumentObject *parent : parentIt->second) { + auto dataIt = docItem->ObjectMap.find(parent); + if (dataIt == docItem->ObjectMap.end()) { + continue; + } + + for (DocumentObjectItem *parentItem : dataIt->second->items) { + reorderParents.insert(parentItem); + } + } + } + else { + reorderParents.insert(docItem); + } + } + ReorderedObjects.clear(); + + if (!reorderParents.empty()) { + for (QTreeWidgetItem *parentItem : reorderParents) { + sortObjectItems(parentItem, [](const DocumentObjectItem *a, const DocumentObjectItem *b) + { return a->object()->TreeRank.getValue() < b->object()->TreeRank.getValue(); }); + } + reorderParents.clear(); + } + FC_LOG("update item status"); TimingInit(); for (auto pos = DocumentMap.begin();pos!=DocumentMap.end();++pos) { @@ -2586,6 +2655,7 @@ void TreeWidget::onUpdateStatus(void) errItem = item; } } + if(errItem) scrollToItem(errItem); @@ -2620,6 +2690,27 @@ void TreeWidget::onItemEntered(QTreeWidgetItem * item) Selection().rmvPreselect(); } +void TreeWidget::onTabifiedDockWidgetActivated(QDockWidget *dockWidget) { + + QWidget *parent = this->parentWidget(); + while (parent) { + if (parent == dockWidget) { + this->setFocus(); + return; + } + + parent = parent->parentWidget(); + } +} + +void TreeWidget::focusInEvent(QFocusEvent *event) { + + _LastSelectedTreeWidget = this; + Application::Instance->updateActions(true); + + QTreeWidget::focusInEvent(event); +} + void TreeWidget::leaveEvent(QEvent *) { if(!updateBlocked && TreeParams::Instance()->PreSelection()) { preselectTimer->stop(); @@ -2966,6 +3057,209 @@ void TreeWidget::onSelectionChanged(const SelectionChanges& msg) } } +bool TreeWidget::getSelectedSiblingObjectItems(std::vector &items) const +{ + QList selected = this->selectedItems(); + if (selected.isEmpty()) { + return false; + } + + if (selected.first()->type() != TreeWidget::ObjectType) { + return false; + } + + QTreeWidgetItem *parentItem = selected.first()->parent(); + for (int i = 1; i < selected.size(); ++i) { + if (selected[i]->type() != TreeWidget::ObjectType || selected[i]->parent() != parentItem) { + return false; + } + } + + items.resize(items.size() + selected.size(), 0); + std::transform(selected.begin(), selected.end(), items.end() - selected.size(), + [](QTreeWidgetItem *i) { return static_cast(i); }); + std::sort(items.begin(), items.end(), + [parentItem](DocumentObjectItem *a, DocumentObjectItem *b) + { return parentItem->indexOfChild(a) < parentItem->indexOfChild(b); }); + + return true; +} + +bool TreeWidget::allowMoveUpInGroup(const std::vector &items, DocumentObjectItem **preceding) const +{ + if (!items.size()) { + return false; + } + + DocumentObjectItem *previous = items.front()->getPreviousSibling(); + if (!previous) { + return false; + } + + if (preceding) { + *preceding = previous; + } + + QTreeWidgetItem *parent = items.front()->parent(); + if (parent->type() == ObjectType) { + ViewProviderDocumentObject *docObj = static_cast(parent)->object(); + if (!docObj) { + return false; + } + + for (DocumentObjectItem *item : items) { + if (!docObj->allowTreeOrderSwap(item->object()->getObject(), previous->object()->getObject())) { + return false; + } + } + } + + return true; +} + +bool TreeWidget::allowMoveDownInGroup(const std::vector &items, DocumentObjectItem **succeeding) const +{ + if (!items.size()) { + return false; + } + + DocumentObjectItem *next = items.back()->getNextSibling(); + if (!next) { + return false; + } + + if (succeeding) { + *succeeding = next; + } + + QTreeWidgetItem *parent = items.back()->parent(); + if (parent->type() == ObjectType) { + ViewProviderDocumentObject *docObj = static_cast(parent)->object(); + if (!docObj) { + return false; + } + + for (DocumentObjectItem *item : items) { + if (!docObj->allowTreeOrderSwap(item->object()->getObject(), next->object()->getObject())) { + return false; + } + } + } + + return true; +} + +bool TreeWidget::moveSiblings(const std::vector &items, DocumentObjectItem *pivot, int direction) +{ + // Some sanity checks + make sure pivot is not within the items to be moved + if (!items.size() || !pivot || !direction || std::find(items.begin(), items.end(), pivot) != items.end()) { + return false; + } + + QTreeWidgetItem *parent = pivot->parent(); + if (!parent) { + return false; + } + + Gui::Document *doc = pivot->object()->getDocument(); + if (!doc) { + return false; + } + + int childCount = parent->childCount(); + std::vector groupItems(childCount, 0); + std::vector ranks(childCount); + + for (int i = 0; i < childCount; ++i) { + QTreeWidgetItem *treeItem = parent->child(i); + if (treeItem->type() != ObjectType) { + return false; + } + + groupItems[i] = static_cast(treeItem); + ranks[i] = static_cast(treeItem)->object()->TreeRank.getValue(); + } + + int insertIndex = parent->indexOfChild(pivot) + (direction > 0); + for (DocumentObjectItem *item : items) { + std::vector::iterator it = std::find(groupItems.begin(), groupItems.end(), item); + if (it == groupItems.end()) { + continue; + } + + int index = it - groupItems.begin(); + groupItems.erase(it); + + if (index < insertIndex) { + --insertIndex; + } + groupItems.insert(groupItems.begin() + insertIndex, item); + ++insertIndex; + } + + doc->openCommand(QT_TRANSLATE_NOOP("Command", direction > 0 ? "Move down in group" : "Move down in group")); + + int changes = 0; + for (int i = 0; i < childCount; ++i) { + App::PropertyInteger &rank = groupItems[i]->object()->TreeRank; + if (rank.getValue() != ranks[i]) { + rank.setValue(ranks[i]); + ++changes; + } + } + + return changes > 0; +} + +bool TreeWidget::sortObjectItems(QTreeWidgetItem *node, DocumentObjectItemComparator comparator) +{ + if (!node || !comparator || node->childCount() <= 0) { + return false; + } + + bool lock = blockConnection(true); + + std::vector sortedItems; + sortedItems.reserve(node->childCount()); + + for (int i = 0; i < node->childCount(); ++i) { + QTreeWidgetItem *treeItem = node->child(i); + if (treeItem->type() == TreeWidget::ObjectType) { + sortedItems.push_back(static_cast(treeItem)); + } + } + + std::stable_sort(sortedItems.begin(), sortedItems.end(), comparator); + + int sortedIndex = 0; + int swaps = 0; + for (int i = 0; i < node->childCount(); ++i) { + QTreeWidgetItem *treeItem = node->child(i); + if (treeItem->type() != TreeWidget::ObjectType) { + continue; + } + + DocumentObjectItem *sortedItem = sortedItems[sortedIndex++]; + if (sortedItem == treeItem) { + continue; + } + + std::vector expansion; + sortedItem->getExpandedSnapshot(expansion); + + node->removeChild(sortedItem); + node->insertChild(i, sortedItem); + ++swaps; + + std::vector::const_iterator expFrom = expansion.cbegin(); + sortedItem->applyExpandedSnapshot(expansion, expFrom); + } + + blockConnection(lock); + + return swaps > 0; +} + // ---------------------------------------------------------------------------- /* TRANSLATOR Gui::TreePanel */ @@ -3272,7 +3566,7 @@ bool DocumentItem::createNewItem(const Gui::ViewProviderDocumentObject& obj, parent = this; data->rootItem = item; if(index<0) - index = findRootIndex(obj.getObject()); + index = findRootIndex(&obj); } if(index<0) parent->addChild(item); @@ -3571,7 +3865,7 @@ void DocumentItem::populateItem(DocumentObjectItem *item, bool refresh, bool del DocumentObjectItem* childItem = static_cast(ci); if(childItem->requiredAtRoot()) { item->removeChild(childItem); - auto index = findRootIndex(childItem->object()->getObject()); + auto index = findRootIndex(childItem->object()); if(index>=0) this->insertChild(index,childItem); else @@ -3592,13 +3886,13 @@ void DocumentItem::populateItem(DocumentObjectItem *item, bool refresh, bool del getTree()->_updateStatus(); } -int DocumentItem::findRootIndex(App::DocumentObject *childObj) { - if(!TreeParams::Instance()->KeepRootOrder() || !childObj || !childObj->getNameInDocument()) +int DocumentItem::findRootIndex(const ViewProviderDocumentObject *childObj) const { + if (!TreeParams::Instance()->KeepRootOrder() || !childObj || !childObj->getObject() + || !childObj->getObject()->getNameInDocument()) { return -1; + } - // object id is monotonically increasing, so use this as a hint to insert - // object back so that we can have a stable order in root level. - + // Use view provider's tree rank to find correct place at the root level. int count = this->childCount(); if(!count) return -1; @@ -3609,9 +3903,10 @@ int DocumentItem::findRootIndex(App::DocumentObject *childObj) { for(last=count-1;last>=0;--last) { auto citem = this->child(last); if(citem->type() == TreeWidget::ObjectType) { - auto obj = static_cast(citem)->object()->getObject(); - if(obj->getID()<=childObj->getID()) - return last+1; + auto obj = static_cast(citem)->object(); + if (obj->TreeRank.getValue() <= childObj->TreeRank.getValue()) { + return last + 1; + } break; } } @@ -3620,9 +3915,10 @@ int DocumentItem::findRootIndex(App::DocumentObject *childObj) { for(first=0;firstchild(first); if(citem->type() == TreeWidget::ObjectType) { - auto obj = static_cast(citem)->object()->getObject(); - if(obj->getID()>=childObj->getID()) + auto obj = static_cast(citem)->object(); + if (obj->TreeRank.getValue() > childObj->TreeRank.getValue()) { return first; + } break; } } @@ -3638,8 +3934,8 @@ int DocumentItem::findRootIndex(App::DocumentObject *childObj) { auto citem = this->child(pos); if(citem->type() != TreeWidget::ObjectType) continue; - auto obj = static_cast(citem)->object()->getObject(); - if(obj->getID()getID()) { + auto obj = static_cast(citem)->object(); + if (obj->TreeRank.getValue() < childObj->TreeRank.getValue()) { first = ++pos; count -= step+1; } else @@ -4970,6 +5266,40 @@ DocumentObjectItem *DocumentObjectItem::getParentItem() const{ 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 0; +} + +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 0; +} + const char *DocumentObjectItem::getName() const { const char *name = object()->getObject()->getNameInDocument(); return name?name:""; @@ -5096,4 +5426,22 @@ 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" diff --git a/src/Gui/Tree.h b/src/Gui/Tree.h index f644f81a64..6997af4456 100644 --- a/src/Gui/Tree.h +++ b/src/Gui/Tree.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -131,6 +132,13 @@ public: void synchronizeSelectionCheckBoxes(); + bool getSelectedSiblingObjectItems(std::vector &items) const; + + bool allowMoveUpInGroup(const std::vector &items, DocumentObjectItem **preceding = 0) const; + bool allowMoveDownInGroup(const std::vector &items, DocumentObjectItem **succeeding = 0) const; + + static bool moveSiblings(const std::vector &items, DocumentObjectItem *pivot, int direction); + protected: /// Observer message from the Selection void onSelectionChanged(const SelectionChanges& msg) override; @@ -155,6 +163,7 @@ protected: protected: void showEvent(QShowEvent *) override; void hideEvent(QHideEvent *) override; + void focusInEvent(QFocusEvent *event) override; void leaveEvent(QEvent *) override; void _updateStatus(bool delay=true); @@ -183,6 +192,7 @@ private Q_SLOTS: void onItemEntered(QTreeWidgetItem * item); void onItemCollapsed(QTreeWidgetItem * item); void onItemExpanded(QTreeWidgetItem * item); + void onTabifiedDockWidgetActivated(QDockWidget *dockWidget); void onUpdateStatus(void); Q_SIGNALS: @@ -212,6 +222,9 @@ private: bool CheckForDependents(); void addDependentToSelection(App::Document* doc, App::DocumentObject* docObject); + typedef bool (*DocumentObjectItemComparator)(const DocumentObjectItem *a, const DocumentObjectItem *b); + bool sortObjectItems(QTreeWidgetItem *node, DocumentObjectItemComparator comparator); + private: QAction* createGroupAction; QAction* relabelObjectAction; @@ -250,6 +263,8 @@ private: std::unordered_map > NewObjects; + std::set ReorderedObjects; + static std::set Instances; std::string myName; // for debugging purpose @@ -341,7 +356,7 @@ protected: QTreeWidgetItem *parent=0, int index=-1, DocumentObjectDataPtr ptrs = DocumentObjectDataPtr()); - int findRootIndex(App::DocumentObject *childObj); + int findRootIndex(const ViewProviderDocumentObject *childObj) const; DocumentObjectItem *findItemByObject(bool sync, App::DocumentObject *obj, const char *subname, bool select=false); @@ -441,8 +456,14 @@ public: int isParentGroup() const; DocumentObjectItem *getParentItem() const; + DocumentObjectItem *getNextSibling() const; + DocumentObjectItem *getPreviousSibling() const; + TreeWidget *getTree() const; + void getExpandedSnapshot(std::vector &snapshot) const; + void applyExpandedSnapshot(const std::vector &snapshot, std::vector::const_iterator &from); + private: void setCheckState(bool checked); diff --git a/src/Gui/ViewProviderDocumentObject.cpp b/src/Gui/ViewProviderDocumentObject.cpp index e4cc2bbb2b..4c18218c57 100644 --- a/src/Gui/ViewProviderDocumentObject.cpp +++ b/src/Gui/ViewProviderDocumentObject.cpp @@ -66,6 +66,8 @@ FC_LOG_LEVEL_INIT("Gui",true,true) using namespace Gui; +int ViewProviderDocumentObject::lastTreeRank = 0; + PROPERTY_SOURCE(Gui::ViewProviderDocumentObject, Gui::ViewProvider) ViewProviderDocumentObject::ViewProviderDocumentObject() @@ -90,6 +92,8 @@ ViewProviderDocumentObject::ViewProviderDocumentObject() "Element: On top only if some sub-element of the object is selected"); OnTopWhenSelected.setEnums(OnTopEnum); + ADD_PROPERTY_TYPE(TreeRank, (0), dogroup, App::PropertyType(App::Prop_Hidden|App::Prop_NoPersist), "Tree view item ordering key"); + sPixmap = "Feature"; } @@ -221,6 +225,14 @@ void ViewProviderDocumentObject::onChanged(const App::Property* prop) ? SoFCSelectionRoot::Box : SoFCSelectionRoot::Full; } } + else if (prop == &TreeRank) { + if (this->TreeRank.getValue() <= 0) { + this->TreeRank.setValue(++ViewProviderDocumentObject::lastTreeRank); + } + else if (this->TreeRank.getValue() > ViewProviderDocumentObject::lastTreeRank) { + ViewProviderDocumentObject::lastTreeRank = this->TreeRank.getValue(); + } + } if (prop && !prop->testStatus(App::Property::NoModify) && pcDocument @@ -677,3 +689,15 @@ std::string ViewProviderDocumentObject::getFullName() const { return pcObject->getFullName() + ".ViewObject"; return std::string(); } + +bool ViewProviderDocumentObject::allowTreeOrderSwap(const App::DocumentObject *child1, const App::DocumentObject *child2) const +{ + std::vector extensions = getExtensionsDerivedFromType(); + for (ViewProviderExtension *ext : extensions) { + if (!ext->extensionAllowTreeOrderSwap(child1, child2)) { + return false; + } + } + + return true; +} diff --git a/src/Gui/ViewProviderDocumentObject.h b/src/Gui/ViewProviderDocumentObject.h index 3af4173d13..4d5a54df1d 100644 --- a/src/Gui/ViewProviderDocumentObject.h +++ b/src/Gui/ViewProviderDocumentObject.h @@ -64,6 +64,9 @@ public: App::PropertyEnumeration OnTopWhenSelected; App::PropertyEnumeration SelectionStyle; + // Hidden properties + App::PropertyInteger TreeRank; + virtual void attach(App::DocumentObject *pcObject); virtual void reattach(App::DocumentObject *); virtual void update(const App::Property*) override; @@ -155,6 +158,8 @@ public: void setShowable(bool enable); bool isShowable() const; + virtual bool allowTreeOrderSwap(const App::DocumentObject *child1, const App::DocumentObject *child2) const; + protected: /*! Get the active mdi view of the document this view provider is part of. @note The returned mdi view doesn't need to be a 3d view but can be e.g. @@ -207,6 +212,8 @@ protected: App::DocumentObject *pcObject; Gui::Document* pcDocument; + static int lastTreeRank; + private: bool _Showable = true; diff --git a/src/Gui/ViewProviderExtension.h b/src/Gui/ViewProviderExtension.h index 27af83db83..fa96fd3c2d 100644 --- a/src/Gui/ViewProviderExtension.h +++ b/src/Gui/ViewProviderExtension.h @@ -110,6 +110,8 @@ public: virtual bool extensionGetElementPicked(const SoPickedPoint *, std::string &) const {return false;} virtual bool extensionGetDetailPath(const char *, SoFullPath *, SoDetail *&) const {return false;} + virtual bool extensionAllowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return true; } + private: bool m_ignoreOverlayIcon = false; //Gui::ViewProviderDocumentObject* m_viewBase = nullptr; diff --git a/src/Gui/ViewProviderOrigin.h b/src/Gui/ViewProviderOrigin.h index e9fd870706..e46bdad450 100644 --- a/src/Gui/ViewProviderOrigin.h +++ b/src/Gui/ViewProviderOrigin.h @@ -74,6 +74,8 @@ public: return false; } + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + /// Returns default size. Use this if it is not possible to determine appropriate size by other means static double defaultSize(); protected: diff --git a/src/Gui/ViewProviderOriginGroupExtension.h b/src/Gui/ViewProviderOriginGroupExtension.h index 1a28f7fe4e..a204fd920a 100644 --- a/src/Gui/ViewProviderOriginGroupExtension.h +++ b/src/Gui/ViewProviderOriginGroupExtension.h @@ -48,6 +48,8 @@ public: void updateOriginSize(); + virtual bool extensionAllowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + protected: void slotChangedObjectApp ( const App::DocumentObject& obj ); void slotChangedObjectGui ( const Gui::ViewProviderDocumentObject& obj ); diff --git a/src/Mod/Part/Gui/ViewProviderBoolean.h b/src/Mod/Part/Gui/ViewProviderBoolean.h index 3c379309ab..ac97433d90 100644 --- a/src/Mod/Part/Gui/ViewProviderBoolean.h +++ b/src/Mod/Part/Gui/ViewProviderBoolean.h @@ -44,6 +44,9 @@ public: QIcon getIcon(void) const; void updateData(const App::Property*); bool onDelete(const std::vector &); + + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + }; /// ViewProvider for the MultiFuse feature diff --git a/src/Mod/Part/Gui/ViewProviderMirror.h b/src/Mod/Part/Gui/ViewProviderMirror.h index 1753a66291..1664528a41 100644 --- a/src/Mod/Part/Gui/ViewProviderMirror.h +++ b/src/Mod/Part/Gui/ViewProviderMirror.h @@ -126,6 +126,8 @@ public: /// grouping handling std::vector claimChildren(void)const; bool onDelete(const std::vector &); + + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } }; class ViewProviderSweep : public ViewProviderPart @@ -141,6 +143,8 @@ public: /// grouping handling std::vector claimChildren(void)const; bool onDelete(const std::vector &); + + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } }; class ViewProviderOffset : public ViewProviderPart @@ -188,6 +192,8 @@ public: void setupContextMenu(QMenu*, QObject*, const char*); bool onDelete(const std::vector &); + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + protected: virtual bool setEdit(int ModNum); virtual void unsetEdit(int ModNum); diff --git a/src/Mod/PartDesign/Gui/ViewProviderLoft.h b/src/Mod/PartDesign/Gui/ViewProviderLoft.h index 01f24e76c5..a043d87261 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderLoft.h +++ b/src/Mod/PartDesign/Gui/ViewProviderLoft.h @@ -44,7 +44,9 @@ public: virtual bool onDelete(const std::vector &); void highlightReferences(const bool on, bool auxiliary); - + + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + protected: virtual bool setEdit(int ModNum); virtual void unsetEdit(int ModNum); diff --git a/src/Mod/PartDesign/Gui/ViewProviderPipe.h b/src/Mod/PartDesign/Gui/ViewProviderPipe.h index e3efe3a695..3215cdfd29 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderPipe.h +++ b/src/Mod/PartDesign/Gui/ViewProviderPipe.h @@ -52,7 +52,9 @@ public: virtual bool onDelete(const std::vector &); void highlightReferences(Reference mode, bool on); - + + virtual bool allowTreeOrderSwap(const App::DocumentObject *, const App::DocumentObject *) const { return false; } + protected: virtual QIcon getIcon(void) const; virtual bool setEdit(int ModNum); diff --git a/src/Mod/Test/CMakeLists.txt b/src/Mod/Test/CMakeLists.txt index 81d1422ed8..741a12e451 100644 --- a/src/Mod/Test/CMakeLists.txt +++ b/src/Mod/Test/CMakeLists.txt @@ -13,6 +13,7 @@ SET(Test_SRCS unittestgui.py testmakeWireString.py TestPythonSyntax.py + TreeView.py ) SOURCE_GROUP("" FILES ${Test_SRCS}) diff --git a/src/Mod/Test/InitGui.py b/src/Mod/Test/InitGui.py index cbb18f8588..2bd25e6850 100644 --- a/src/Mod/Test/InitGui.py +++ b/src/Mod/Test/InitGui.py @@ -77,4 +77,5 @@ Gui.addWorkbench(TestWorkbench()) FreeCAD.__unit_test__ += [ "Workbench", "Menu", "Menu.MenuDeleteCases", - "Menu.MenuCreateCases" ] + "Menu.MenuCreateCases", + "TreeView"] diff --git a/src/Mod/Test/TreeView.py b/src/Mod/Test/TreeView.py new file mode 100644 index 0000000000..019309ae1a --- /dev/null +++ b/src/Mod/Test/TreeView.py @@ -0,0 +1,244 @@ + +# TreeView test module + +import os +import time +import tempfile +import unittest +import FreeCAD + +from PySide import QtCore, QtGui +import FreeCADGui + + +class TreeViewTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def getTreeWidget(self): + mainWnd = FreeCADGui.getMainWindow() + treeDock = mainWnd.findChild(QtGui.QDockWidget, "Tree View") + if treeDock is None: + treeDock = mainWnd.findChild(QtGui.QDockWidget, "Combo View") + if not treeDock is None: + tabWidget = treeDock.findChild(QtGui.QTabWidget, "combiTab") + if not tabWidget is None: + tabWidget.setCurrentIndex(0) + self.assertTrue(not treeDock is None, "No Tree View docks available") + + treeView = treeDock.findChild(QtGui.QTreeWidget) + self.assertTrue(not treeView is None, "No Tree View widget found") + + return treeView + + def waitForTreeViewSync(self): + start = time.time() + while time.time() < start + 0.5: + FreeCADGui.updateGui() + + def selectDocItem(self, docItem): + + treeView = self.getTreeWidget() + + if docItem.TypeId == "App::Document": + appNode = treeView.topLevelItem(0) + self.assertTrue(not appNode is None, "No Application top level node") + + docNode = next((appNode.child(i) for i in range(appNode.childCount()) + if appNode.child(i).text(0) == docItem.Label), None) + self.assertTrue(not docNode is None, "No test Document node") + treeView.setCurrentItem(docNode) + else: + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(docItem) + self.waitForTreeViewSync() + + def trySwapOuterTreeViewItems(self, docItem, transposable): + + self.selectDocItem(docItem) + treeView = self.getTreeWidget() + + selected = treeView.selectedItems() + self.assertTrue(len(selected) == 1, + "Unexpected count of selected items: " + str(len(selected))) + selected = selected[0] + + originalState = [ selected.child(i).text(0) for i in range(selected.childCount()) ] + self.assertTrue(len(originalState) >= 1, + "No children found in item " + selected.text(0)) + + targetState = originalState.copy() + if transposable: + targetState[0], targetState[-1] = targetState[-1], targetState[0] + + treeView.setCurrentItem(selected.child(0)) + self.waitForTreeViewSync() + + # One move down attempt more to test boundary behaviour + for i in range(len(originalState)): + FreeCADGui.runCommand("Std_GroupMoveDown") + self.waitForTreeViewSync() + + treeView.setCurrentItem(selected.child(len(originalState) - 2 if len(originalState) > 1 else 0)) + self.waitForTreeViewSync() + + # One move up attempt more to test boundary behaviour + for i in range(len(originalState) - 1): + FreeCADGui.runCommand("Std_GroupMoveUp") + self.waitForTreeViewSync() + + finalState = [ selected.child(i).text(0) for i in range(selected.childCount()) ] + self.assertTrue(targetState == finalState, + "Unexpected final state: %s\nExpected: %s" % (finalState, targetState)) + + def getChildrenOf(self, docItem): + + self.selectDocItem(docItem) + treeView = self.getTreeWidget() + + selected = treeView.selectedItems() + self.assertTrue(len(selected) == 1, + "Unexpected count of selected items: " + str(len(selected))) + selected = selected[0] + + return [ selected.child(i).text(0) for i in range(selected.childCount()) ] + + + def testMoveTransposableItems(self): + # Makes sense only if Gui is shown + if not FreeCAD.GuiUp: + return + + FreeCAD.TestEnvironment = True + + doc = FreeCAD.newDocument("TreeViewTest1") + FreeCAD.setActiveDocument(doc.Name) + + box = doc.addObject("Part::Box", "Box") + cyl = doc.addObject("Part::Cylinder", "Cylinder") + sph = doc.addObject("Part::Sphere", "Sphere") + con = doc.addObject("Part::Cone", "Cone") + doc.recompute() + + self.trySwapOuterTreeViewItems(doc, True) + + grp = doc.addObject("App::DocumentObjectGroup", "Group") + grp.addObjects([ box, cyl, sph, con ]) + doc.recompute() + + self.trySwapOuterTreeViewItems(grp, True) + + del FreeCAD.TestEnvironment + + + def testMoveUnmovableItems(self): + # Makes sense only if Gui is shown + if not FreeCAD.GuiUp: + return + + FreeCAD.TestEnvironment = True + + doc = FreeCAD.newDocument("TreeViewTest2") + FreeCAD.setActiveDocument(doc.Name) + + sph = doc.addObject("Part::Sphere", "Sphere") + con = doc.addObject("Part::Cone", "Cone") + doc.recompute() + + cut = doc.addObject("Part::Cut", "Cut") + cut.Base = sph + cut.Tool = con + doc.recompute() + + self.trySwapOuterTreeViewItems(cut, False) + + bdy = doc.addObject("PartDesign::Body", "Body") + box = doc.addObject("PartDesign::AdditiveBox", "Box") + bdy.addObject(box) + doc.recompute() + + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(bdy) + self.waitForTreeViewSync() + + treeView = self.getTreeWidget() + treeView.selectedItems()[0].setExpanded(True) + self.waitForTreeViewSync() + + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(doc.Name, bdy.Name, box.Name + ".Face6") + self.waitForTreeViewSync() + + cha = bdy.newObject("PartDesign::Chamfer", "Chamfer") + cha.Base = (box, ["Face6"]) + doc.recompute() + + cyl = doc.addObject("PartDesign::SubtractiveCylinder", "Cylinder") + bdy.addObject(cyl) + doc.recompute() + + self.trySwapOuterTreeViewItems(bdy, False) + + del FreeCAD.TestEnvironment + + + def testItemOrderSaveAndRestore(self): + # Makes sense only if Gui is shown + if not FreeCAD.GuiUp: + return + + FreeCAD.TestEnvironment = True + + doc = FreeCAD.newDocument("TreeViewTest3") + FreeCAD.setActiveDocument(doc.Name) + + grp = doc.addObject("App::DocumentObjectGroup", "Group") + box = doc.addObject("Part::Box", "Box") + cyl = doc.addObject("Part::Cylinder", "Cylinder") + sph = doc.addObject("Part::Sphere", "Sphere") + con = doc.addObject("Part::Cone", "Cone") + doc.recompute() + + origOrder = self.getChildrenOf(doc) + self.assertTrue(origOrder == ["Group", "Box", "Cylinder", "Sphere", "Cone"]) + + origOrderFile = tempfile.gettempdir() + os.sep + "TreeViewTest3_1.fcstd" + doc.saveAs(origOrderFile) + + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(con) + FreeCADGui.Selection.addSelection(cyl) + self.waitForTreeViewSync() + + FreeCADGui.runCommand("Std_GroupMoveUp") + self.waitForTreeViewSync() + + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection(grp) + FreeCADGui.Selection.addSelection(box) + self.waitForTreeViewSync() + + FreeCADGui.runCommand("Std_GroupMoveDown") + self.waitForTreeViewSync() + + newOrder = self.getChildrenOf(doc) + self.assertTrue(newOrder == ["Cylinder", "Cone", "Sphere", "Group", "Box"]) + + newOrderFile = tempfile.gettempdir() + os.sep + "TreeViewTest3_2.fcstd" + doc.saveAs(newOrderFile) + + FreeCAD.closeDocument(doc.Name) + self.waitForTreeViewSync() + + doc = FreeCAD.open(origOrderFile) + order = self.getChildrenOf(doc) + self.assertTrue(order == origOrder) + + doc = FreeCAD.open(newOrderFile) + order = self.getChildrenOf(doc) + self.assertTrue(order == newOrder) + + del FreeCAD.TestEnvironment