From 5947f91b177f698c0afbb23e8a421b124b6371df Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Tue, 15 Mar 2022 21:33:07 +0800 Subject: [PATCH] Gui: refactor Object Selection Dialog It is originally used for dependency selection when copying object. Modified to improve auto dependency selection, and for use by Link configuration object setup, which also involves dependency selection. --- src/App/DocumentObserver.cpp | 58 +++ src/App/DocumentObserver.h | 25 ++ src/Gui/DlgObjectSelection.cpp | 792 +++++++++++++++++++++------------ src/Gui/DlgObjectSelection.h | 102 ++++- src/Gui/DlgObjectSelection.ui | 181 ++++---- 5 files changed, 788 insertions(+), 370 deletions(-) diff --git a/src/App/DocumentObserver.cpp b/src/App/DocumentObserver.cpp index 7f3bdb1216..4e8fa83035 100644 --- a/src/App/DocumentObserver.cpp +++ b/src/App/DocumentObserver.cpp @@ -283,6 +283,11 @@ SubObjectT::SubObjectT(const DocumentObject *obj, const char *s) { } +SubObjectT::SubObjectT(const DocumentObject *obj) + :DocumentObjectT(obj) +{ +} + SubObjectT::SubObjectT(const DocumentObjectT& obj, const char *s) :DocumentObjectT(obj),subname(s?s:"") { @@ -327,6 +332,22 @@ SubObjectT &SubObjectT::operator=(SubObjectT &&other) return *this; } +SubObjectT &SubObjectT::operator=(const DocumentObjectT &other) +{ + if (this == &other) + return *this; + static_cast(*this) = other; + subname.clear(); + return *this; +} + +SubObjectT &SubObjectT::operator=(const DocumentObject *other) +{ + static_cast(*this) = other; + subname.clear(); + return *this; +} + bool SubObjectT::operator==(const SubObjectT &other) const { return static_cast(*this) == other && subname == other.subname; @@ -398,6 +419,43 @@ std::vector SubObjectT::getSubObjectList() const { return {}; } +std::string SubObjectT::getObjectFullName(const char *docName) const +{ + std::ostringstream ss; + if (!docName || getDocumentName() != docName) { + ss << getDocumentName(); + if (auto doc = getDocument()) { + if (doc->Label.getStrValue() != getDocumentName()) + ss << "(" << doc->Label.getValue() << ")"; + } + ss << "#"; + } + ss << getObjectName(); + if (getObjectLabel().size() && getObjectLabel() != getObjectName()) + ss << " (" << getObjectLabel() << ")"; + return ss.str(); +} + +std::string SubObjectT::getSubObjectFullName(const char *docName) const +{ + if (subname.empty()) + return getObjectFullName(docName); + std::ostringstream ss; + if (!docName || getDocumentName() != docName) { + ss << getDocumentName(); + if (auto doc = getDocument()) { + if (doc->Label.getStrValue() != getDocumentName()) + ss << "(" << doc->Label.getValue() << ")"; + } + ss << "#"; + } + ss << getObjectName() << "." << subname; + auto sobj = getSubObject(); + if (sobj && sobj->Label.getStrValue() != sobj->getNameInDocument()) + ss << " (" << sobj->Label.getValue() << ")"; + return ss.str(); +} + // ----------------------------------------------------------------------------- PropertyLinkT::PropertyLinkT() diff --git a/src/App/DocumentObserver.h b/src/App/DocumentObserver.h index 38f36a78e2..6630a497d3 100644 --- a/src/App/DocumentObserver.h +++ b/src/App/DocumentObserver.h @@ -176,6 +176,9 @@ public: /*! Constructor */ SubObjectT(const DocumentObject*, const char *subname); + /*! Constructor */ + SubObjectT(const DocumentObject*); + /*! Constructor */ SubObjectT(const char *docName, const char *objName, const char *subname); @@ -185,15 +188,37 @@ public: /*! Assignment operator */ SubObjectT &operator=(SubObjectT &&); + /*! Assignment operator */ + SubObjectT &operator=(const DocumentObjectT&); + + /*! Assignment operator */ + SubObjectT &operator=(const App::DocumentObject*); + /*! Equality operator */ bool operator==(const SubObjectT&) const; /// Set the subname path to the sub-object void setSubName(const char *subname); + /// Set the subname path to the sub-object + void setSubName(const std::string &subname) { + setSubName(subname.c_str()); + } + /// Return the subname path const std::string &getSubName() const; + /** Return docname#objname (label) + * @param docName: optional document name. The document prefix will only be printed + * if it is different then the given 'doc'. + */ + std::string getObjectFullName(const char *docName=nullptr) const; + + /** Return docname#objname.subname (label) + * @param doc: optional document name. The document prefix will only be printed + * if it is different then the given 'doc'. + */ + std::string getSubObjectFullName(const char *docName=nullptr) const; /// Return the subname path without sub-element std::string getSubNameNoElement() const; diff --git a/src/Gui/DlgObjectSelection.cpp b/src/Gui/DlgObjectSelection.cpp index 1526a8053c..f1783736db 100644 --- a/src/Gui/DlgObjectSelection.cpp +++ b/src/Gui/DlgObjectSelection.cpp @@ -24,6 +24,7 @@ #ifndef _PreComp_ # include # include +# include #endif #include @@ -32,8 +33,10 @@ #include "DlgObjectSelection.h" #include "ui_DlgObjectSelection.h" #include "Application.h" +#include "MainWindow.h" #include "ViewProviderDocumentObject.h" - +#include "MetaTypes.h" +#include "ViewParams.h" FC_LOG_LEVEL_INIT("Gui",true,true) @@ -43,90 +46,115 @@ using namespace Gui; DlgObjectSelection::DlgObjectSelection( const std::vector &objs, QWidget* parent, Qt::WindowFlags fl) - : QDialog(parent, fl), ui(new Ui_DlgObjectSelection) + : QDialog(parent, fl) { - /** - * make a copy of the originally selected objects - * so we can return them if the user clicks useOriginalsBtn - */ - this->originalSelections = objs; + init(objs, {}); +} +DlgObjectSelection::DlgObjectSelection( + const std::vector &objs, + const std::vector &excludes, + QWidget* parent, + Qt::WindowFlags fl) + : QDialog(parent, fl) +{ + init(objs, excludes); +} + +static bool inline setCheckState(QTreeWidgetItem *item, Qt::CheckState state, bool forced=true) +{ + if (!forced) { + if (item->isSelected()) { + if (state == Qt::Unchecked || item->checkState(0) == Qt::Unchecked) + return false; + } + if (item->checkState(0) == state) + return false; + } + // auto objT = qvariant_cast(item->data(0, Qt::UserRole)); + // FC_MSG(objT.getObjectFullName() << (state == Qt::Unchecked ? " unchecked" : + // (state == Qt::Checked ? " checked" : " partial"))); + item->setCheckState(0, state); + return true; +} + +void DlgObjectSelection::init(const std::vector &objs, + const std::vector &excludes) + +{ + initSels = objs; + std::sort(initSels.begin(), initSels.end()); + + deps = App::Document::getDependencyList(objs, App::Document::DepSort); + depSet.insert(deps.begin(), deps.end()); + + ui = new Ui_DlgObjectSelection; ui->setupUi(this); + hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); + ui->checkBoxAutoDeps->setChecked(hGrp->GetBool("ObjectSelectionAutoDeps", true)); + connect(ui->checkBoxAutoDeps, SIGNAL(toggled(bool)), this, SLOT(onAutoDeps(bool))); + // make sure to show a horizontal scrollbar if needed ui->depList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->depList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->depList->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); - ui->depList->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + ui->inList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->inList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->inList->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - ui->depList->header()->setStretchLastSection(false); - ui->depList->headerItem()->setText(0, tr("Dependency")); + ui->depList->headerItem()->setText(0, tr("Depending on")); ui->depList->headerItem()->setText(1, tr("Document")); ui->depList->headerItem()->setText(2, tr("Name")); - ui->depList->headerItem()->setText(3, tr("State")); - ui->treeWidget->headerItem()->setText(0, tr("Hierarchy")); + ui->inList->headerItem()->setText(0, tr("Depended by")); + ui->inList->headerItem()->setText(1, tr("Document")); + ui->inList->headerItem()->setText(2, tr("Name")); + + ui->treeWidget->headerItem()->setText(0, tr("Selections")); ui->treeWidget->header()->setStretchLastSection(false); - for(auto obj : App::Document::getDependencyList(objs)) { - auto &info = objMap[obj]; - info.depItem = new QTreeWidgetItem(ui->depList); - auto vp = Gui::Application::Instance->getViewProvider(obj); - if(vp) info.depItem->setIcon(0, vp->getIcon()); - info.depItem->setText(0, QString::fromUtf8((obj)->Label.getValue())); - info.depItem->setText(1, QString::fromUtf8(obj->getDocument()->getName())); - info.depItem->setText(2, QString::fromLatin1(obj->getNameInDocument())); - info.depItem->setText(3, tr("Selected")); - info.depItem->setFlags(Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); - info.depItem->setCheckState(0,Qt::Checked); - } - for(auto obj : objs) { - auto &info = objMap[obj]; - info.items.push_back(createItem(obj,nullptr)); - info.items.back()->setCheckState(0,Qt::Checked); - } - - for(auto &v : objMap) { - for(auto obj : v.first->getOutListRecursive()) { - if(obj == v.first) - continue; - auto it = objMap.find(obj); - if(it == objMap.end()) - continue; - v.second.outList[obj] = &it->second; - } - for(auto obj : v.first->getInListRecursive()) { - if(obj == v.first) - continue; - auto it = objMap.find(obj); - if(it == objMap.end()) - continue; - v.second.inList[obj] = &it->second; - } - } - /** - * create useOriginalsBtn and add to the button box - * tried adding to .ui file, but could never get the - * formatting exactly the way I wanted it. -- - */ - useOriginalsBtn = new QPushButton(tr("&Use Original Selections")); - useOriginalsBtn->setToolTip(tr("Ignore dependencies and proceed with objects\noriginally selected prior to opening this dialog")); - ui->buttonBox->addButton(useOriginalsBtn,QDialogButtonBox::ActionRole); - connect(ui->treeWidget, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(onItemExpanded(QTreeWidgetItem*))); + + allItem = new QTreeWidgetItem(ui->treeWidget); + allItem->setText(0, QStringLiteral("<%1>").arg(tr("All"))); + QFont font = allItem->font(0); + font.setBold(true); + allItem->setFont(0, font); + allItem->setFlags(Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); + allItem->setCheckState(0, Qt::Checked); + + for(auto obj : initSels) + getItem(obj)->setCheckState(0, Qt::Checked); + + for(auto obj : deps) + getItem(obj)->setCheckState(0, Qt::Checked); + + auto filter = excludes; + std::sort(filter.begin(), filter.end()); + for (auto obj : deps) { + auto it = std::lower_bound(filter.begin(), filter.end(), obj); + if (it != filter.end() && *it == obj) + setItemState(obj, Qt::Unchecked); + } + onItemSelectionChanged(); + connect(ui->treeWidget, SIGNAL(itemChanged(QTreeWidgetItem*,int)), - this, SLOT(onItemChanged(QTreeWidgetItem*,int))); + this, SLOT(onObjItemChanged(QTreeWidgetItem*,int))); connect(ui->depList, SIGNAL(itemChanged(QTreeWidgetItem*,int)), - this, SLOT(onItemChanged(QTreeWidgetItem*,int))); + this, SLOT(onDepItemChanged(QTreeWidgetItem*,int))); + connect(ui->inList, SIGNAL(itemChanged(QTreeWidgetItem*,int)), + this, SLOT(onDepItemChanged(QTreeWidgetItem*,int))); connect(ui->treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(onItemSelectionChanged())); - connect(ui->depList, SIGNAL(itemSelectionChanged()), - this, SLOT(onDepSelectionChanged())); + connect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(useOriginalsBtn, SIGNAL(clicked()), this, SLOT(onUseOriginalsBtnClicked())); + + timer.setSingleShot(true); + connect(&timer, SIGNAL(timeout()), this, SLOT(checkItemChanged())); } /** @@ -138,249 +166,427 @@ DlgObjectSelection::~DlgObjectSelection() delete ui; } -QTreeWidgetItem *DlgObjectSelection::createItem(App::DocumentObject *obj, QTreeWidgetItem *parent) { - QTreeWidgetItem* item; - if(parent) - item = new QTreeWidgetItem(parent); - else +QTreeWidgetItem *DlgObjectSelection::getItem(App::DocumentObject *obj, + std::vector **pitems, + QTreeWidgetItem *parent) +{ + auto &items = itemMap[App::SubObjectT(obj, "")]; + if (pitems) + *pitems = &items; + QTreeWidgetItem *item; + if (!parent) { + if (items.size()) + return items[0]; item = new QTreeWidgetItem(ui->treeWidget); - auto vp = Gui::Application::Instance->getViewProvider(obj); - if(vp) item->setIcon(0, vp->getIcon()); - item->setText(0, QString::fromUtf8((obj)->Label.getValue())); - item->setData(0, Qt::UserRole, QByteArray(obj->getDocument()->getName())); - item->setData(0, Qt::UserRole+1, QByteArray(obj->getNameInDocument())); + auto vp = Base::freecad_dynamic_cast( + Gui::Application::Instance->getViewProvider(obj)); + if (vp) item->setIcon(0, vp->getIcon()); + App::SubObjectT objT(obj, ""); + item->setText(0, QString::fromUtf8((obj)->Label.getValue())); + if (std::binary_search(initSels.begin(), initSels.end(), obj)) { + QFont font = item->font(0); + font.setBold(true); + font.setItalic(true); + item->setFont(0, font); + } + item->setToolTip(0, QString::fromUtf8(objT.getObjectFullName().c_str())); + item->setData(0, Qt::UserRole, QVariant::fromValue(objT)); + item->setChildIndicatorPolicy(obj->getOutList().empty() ? + QTreeWidgetItem::DontShowIndicator : QTreeWidgetItem::ShowIndicator); + } else if (items.size()) { + item = new QTreeWidgetItem(parent); + item->setIcon(0, items[0]->icon(0)); + item->setText(0, items[0]->text(0)); + item->setFont(0, items[0]->font(0)); + item->setToolTip(0, items[0]->toolTip(0)); + item->setData(0, Qt::UserRole, items[0]->data(0, Qt::UserRole)); + item->setChildIndicatorPolicy(items[0]->childIndicatorPolicy()); + item->setCheckState(0, items[0]->checkState(0)); + } else + return nullptr; + items.push_back(item); item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); - std::set outSet; - for(auto o : obj->getOutList()) { - if(objMap.count(o)) - outSet.insert(o); - } - if(outSet.empty()) - return item; - item->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); - if(!parent) { - bool populate = false; - for(auto o : outSet) { - if(objMap[o].items.empty()) { - populate = true; - break; - } - } - if(!populate) - return item; - for(auto o : outSet) { - auto &info = objMap[o]; - info.items.push_back(createItem(o,item)); - info.items.back()->setCheckState(0,info.checkState); - } - } return item; } -class SignalBlocker { -public: - SignalBlocker(QTreeWidget *treeWidget) - :treeWidget(treeWidget) - { - treeWidget->blockSignals(true); - } - ~SignalBlocker() { - treeWidget->blockSignals(false); - } - QTreeWidget *treeWidget; -}; - -App::DocumentObject *DlgObjectSelection::objFromItem(QTreeWidgetItem *item) { - std::string name; - std::string docName; - if(item->treeWidget() == ui->treeWidget) { - docName = item->data(0,Qt::UserRole).toByteArray().constData(); - name = item->data(0,Qt::UserRole+1).toByteArray().constData(); - }else{ - docName = qPrintable(item->text(1)); - name = qPrintable(item->text(2)); - } - auto doc = App::GetApplication().getDocument(docName.c_str()); - if(!doc) return nullptr; - return doc->getObject(name.c_str()); -} - -void DlgObjectSelection::onItemExpanded(QTreeWidgetItem * item) { - if(item->childCount()) +void DlgObjectSelection::onItemExpanded(QTreeWidgetItem *item) +{ + if (item->childCount()) return; - auto obj = objFromItem(item); - if(!obj) - return; - SignalBlocker blocker(ui->treeWidget); - std::set outSet; - for(auto o : obj->getOutList()) { - if(!objMap.count(obj) || !outSet.insert(o).second) - continue; - auto &info = objMap[o]; - info.items.push_back(createItem(o,item)); - info.items.back()->setCheckState(0,info.checkState); + if (auto obj = qvariant_cast(item->data(0, Qt::UserRole)).getObject()) { + QSignalBlocker blocker(ui->treeWidget); + std::set set; + for (auto child : obj->getOutList()) { + if (child && set.insert(child).second) + getItem(child, nullptr, item); + } } } -void DlgObjectSelection::onItemChanged(QTreeWidgetItem * item, int column) { - if(column) return; - auto obj = objFromItem(item); - if(!obj) return; - auto state = item->checkState(0); - auto it = objMap.find(obj); - if(it == objMap.end() || state == it->second.checkState) - return; - SignalBlocker blocker(ui->treeWidget); - SignalBlocker blocker2(ui->depList); - auto &info = it->second; - info.checkState = state; +void DlgObjectSelection::updateAllItemState() +{ + int count = 0; + for (const auto &v : itemMap) { + auto state = v.second[0]->checkState(0); + if (state == Qt::Unchecked) { + if (count) { + allItem->setCheckState(0, Qt::PartiallyChecked); + return; + } + } else { + if (state == Qt::PartiallyChecked) { + allItem->setCheckState(0, Qt::PartiallyChecked); + return; + } + ++count; + } + } + if (count && count == (int)itemMap.size()) + allItem->setCheckState(0, Qt::Checked); +} - if(item == info.depItem) { - for(auto item : info.items) - item->setCheckState(0,state); - }else{ - info.depItem->setCheckState(0,state); - info.depItem->setText(3,state==Qt::Checked?tr("Selected"):QString()); +void DlgObjectSelection::setItemState(App::DocumentObject *obj, + Qt::CheckState state, + bool forced) +{ + std::vector *items = nullptr; + auto item = getItem(obj, &items); + if (!setCheckState(item, state, forced)) + return; + + for (size_t i=1; isize(); ++i) + setCheckState(items->at(i), state, true); + + std::vector objs = {obj}; + + if (ui->checkBoxAutoDeps->isChecked() && state == Qt::Checked) { + // If an object is newly checked, check all its dependencies + for (auto o : obj->getOutListRecursive()) { + if (!depSet.count(o) || itemChanged.count(o)) + continue; + auto itItem = itemMap.find(o); + if (itItem == itemMap.end() || itItem->second[0]->checkState(0) == state) + continue; + + for (auto i : itItem->second) + setCheckState(i, state, true); + objs.push_back(o); + } } - if(state == Qt::Unchecked) { - for(auto &v : info.outList) { - if(info.inList.count(v.first)) { - // This indicates a dependency loop. The check here is so that - // object selection still works despite of the loop + for(auto obj : objs) { + auto it = inMap.find(obj); + if (it != inMap.end()) + setCheckState(it->second, state); + + auto itDep = depMap.find(obj); + if (itDep != depMap.end()) + setCheckState(itDep->second, state); + + // If an object toggles state, we need to revisit all its in-list + // object to update the partial/full checked state. + for (auto o : obj->getInList()) { + if (!depSet.count(o) ||itemChanged.count(o)) continue; - } - if(v.second->checkState == Qt::Unchecked) + auto it = itemMap.find(o); + if (it == itemMap.end() || it->second[0]->checkState(0) == state) continue; - v.second->checkState = Qt::Unchecked; - v.second->depItem->setText(3,QString()); - v.second->depItem->setCheckState(0,Qt::Unchecked); - for(auto item : v.second->items) - item->setCheckState(0,Qt::Unchecked); - } - for(auto &v : info.inList) { - if(v.second->checkState != Qt::Checked) - continue; - v.second->checkState = Qt::PartiallyChecked; - v.second->depItem->setText(3,tr("Partial")); - v.second->depItem->setCheckState(0,Qt::PartiallyChecked); - for(auto item : v.second->items) - item->setCheckState(0,Qt::PartiallyChecked); - } - return; - } else if(state == Qt::Checked) { - for(auto &v : info.outList) { - if(info.inList.count(v.first)) { - // This indicates a dependency loop. The check here is so that - // object selection still works despite of the loop - continue; - } - if(v.second->checkState == Qt::Checked) - continue; - v.second->checkState = Qt::Checked; - v.second->depItem->setText(3,tr("Selected")); - v.second->depItem->setCheckState(0,Qt::Checked); - for(auto item : v.second->items) - item->setCheckState(0,Qt::Checked); - } - bool touched; - do { - touched = false; - for(auto &v : info.inList) { - if(v.second->checkState != Qt::PartiallyChecked) + int count = 0; + int selcount = 0; + for (auto sibling : o->getOutList()) { + if (!depSet.count(sibling)) continue; - bool partial = false; - for(auto &vv : v.second->outList) { - if(vv.second->checkState != Qt::Checked) { - partial = true; - break; - } + ++count; + auto it = itemMap.find(sibling); + if (it == itemMap.end()) + continue; + auto s = it->second[0]->checkState(0); + if (s == Qt::Unchecked) + continue; + if (it->second[0]->checkState(0) == Qt::PartiallyChecked) { + selcount = -1; + break; } - if(partial) - continue; - touched = true; - v.second->checkState = Qt::Checked; - v.second->depItem->setText(3,tr("Selected")); - v.second->depItem->setCheckState(0,Qt::Checked); - for(auto item : v.second->items) - item->setCheckState(0,Qt::Checked); + ++selcount; } - }while(touched); + auto state = it->second[0]->checkState(0); + if (state == Qt::Checked && selcount != count) + setItemState(o, Qt::PartiallyChecked, true); + else if (state == Qt::PartiallyChecked && selcount == count) + setItemState(o, Qt::Checked, true); + } } } -std::vector DlgObjectSelection::getSelections() const { - - if (returnOriginals){ - return originalSelections; - } - +std::vector DlgObjectSelection::getSelections(SelectionOptions options) const { std::vector res; - for(auto &v : objMap) { - if(v.second.checkState != Qt::Unchecked) - res.push_back(v.first); + Base::Flags flags(options); + if (!flags.testFlag(SelectionOptions::Invert)) { + for (const auto &v : itemMap) { + if (v.second[0]->checkState(0) == Qt::Unchecked) + continue; + if (auto obj = v.first.getObject()) + res.push_back(obj); + } + } else { + for (auto obj : deps) { + auto it = itemMap.find(obj); + if (it == itemMap.end() || it->second[0]->checkState(0) == Qt::Unchecked) + res.push_back(obj); + } } + if (flags.testFlag(SelectionOptions::Sort)) + std::sort(res.begin(), res.end()); return res; } +void DlgObjectSelection::onDepItemChanged(QTreeWidgetItem * depItem, int column) { + if(column) return; + QSignalBlocker blocker(ui->depList); + QSignalBlocker blocker2(ui->inList); + QSignalBlocker blocker3(ui->treeWidget); + auto state = depItem->checkState(0); + if (depItem->isSelected()) { + for (auto item : depItem->treeWidget()->selectedItems()) { + auto objT = qvariant_cast(item->data(0, Qt::UserRole)); + auto it = itemMap.find(objT); + if (it == itemMap.end()) + continue; + setCheckState(item, state); + for (auto i : it->second) + setCheckState(i, state); + itemChanged[objT] = state; + } + } else { + auto objT = qvariant_cast(depItem->data(0, Qt::UserRole)); + auto it = itemMap.find(objT); + if (it != itemMap.end()) { + itemChanged[objT] = state; + for (auto i : it->second) + setCheckState(i, state); + } + } + timer.start(10); +} + +void DlgObjectSelection::onObjItemChanged(QTreeWidgetItem * objItem, int column) { + if(column != 0) + return; + + QSignalBlocker blocker3(ui->treeWidget); + auto state = objItem->checkState(0); + if (objItem == allItem) { + if (state == Qt::PartiallyChecked) + return; + ui->treeWidget->selectionModel()->clearSelection(); + itemChanged.clear(); + timer.stop(); + onItemSelectionChanged(); + if (state == Qt::Unchecked) { + for (const auto &v : itemMap) { + for (auto i : v.second) + setCheckState(i, Qt::Unchecked); + auto it = depMap.find(v.first); + if (it != depMap.end()) + setCheckState(it->second, Qt::Unchecked); + it = inMap.find(v.first); + if (it != inMap.end()) + setCheckState(it->second, Qt::Unchecked); + } + } else { + for (auto obj : initSels) + setCheckState(getItem(obj), Qt::Checked); + for (auto obj : deps) { + setCheckState(getItem(obj), Qt::Checked); + auto it = depMap.find(obj); + if (it != depMap.end()) + setCheckState(it->second, Qt::Checked); + it = inMap.find(obj); + if (it != inMap.end()) + setCheckState(it->second, Qt::Checked); + } + } + return; + } + + if (!objItem->isSelected()) { + ui->treeWidget->selectionModel()->clearSelection(); + objItem->setSelected(true); + // We treat selected item in tree widget specially in case of checking + // items in depList or inList. To simplify logic, we change selection + // here if an unselected item has been checked. + itemChanged[qvariant_cast(objItem->data(0, Qt::UserRole))] = state; + onItemSelectionChanged(); + } + else { + for (auto item : ui->treeWidget->selectedItems()) { + setCheckState(item, state); + itemChanged[qvariant_cast(item->data(0, Qt::UserRole))] = state; + } + } + timer.start(10); +} + + +static bool getOutList(App::DocumentObject *obj, + std::set &visited, + std::vector &result) +{ + if (!visited.insert(obj).second) + return false; + + for (auto o : obj->getOutList()) { + if (getOutList(o, visited, result)) + result.push_back(o); + } + return true; +} + +void DlgObjectSelection::checkItemChanged() { + + QSignalBlocker blocker(ui->depList); + QSignalBlocker blocker2(ui->inList); + QSignalBlocker blocker3(ui->treeWidget); + + std::set unchecked; + + for (auto &v : itemChanged) { + const auto &objT = v.first; + Qt::CheckState state = v.second; + if (auto obj = objT.getObject()) { + if (state == Qt::Unchecked) { + // We'll deal with unchecked item later + if (ui->checkBoxAutoDeps->isChecked()) + unchecked.insert(obj); + } else { + // For checked item, setItemState will auto select its + // dependency + setItemState(obj, state, true); + } + } + } + + if (unchecked.size()) { + // When some item is unchecked by the user, we need to re-check the + // recursive outlist of the initially selected object, excluding all + // currently unchecked object. And then uncheck any item that does not + // appear in the returned outlist. + + for (const auto &v : itemMap) { + auto item = v.second[0]; + if (item->checkState(0) == Qt::Unchecked) { + if (auto obj = qvariant_cast(item->data(0, Qt::UserRole)).getObject()) + unchecked.insert(obj); + } + } + + auto outlist = initSels; + for (auto obj : initSels) + getOutList(obj, unchecked, outlist); + std::sort(outlist.begin(), outlist.end()); + + for (const auto &v : itemMap) { + if (v.second[0]->checkState(0) == Qt::Unchecked) + continue; + if (auto obj = v.first.getObject()) { + if (!std::binary_search(outlist.begin(), outlist.end(), obj)) + setItemState(obj, Qt::Unchecked, true); + } + } + } + + itemChanged.clear(); + updateAllItemState(); +} + +QTreeWidgetItem *DlgObjectSelection::createDepItem(QTreeWidget *parent, App::DocumentObject *obj) +{ + auto item = new QTreeWidgetItem(parent); + if (parent == ui->depList) + depMap[obj] = item; + else + inMap[obj] = item; + App::SubObjectT objT(obj); + auto vp = Gui::Application::Instance->getViewProvider(obj); + if(vp) item->setIcon(0, vp->getIcon()); + item->setData(0, Qt::UserRole, QVariant::fromValue(objT)); + item->setToolTip(0, QString::fromUtf8(objT.getObjectFullName().c_str())); + item->setText(0, QString::fromUtf8((obj)->Label.getValue())); + if (std::binary_search(initSels.begin(), initSels.end(), obj)) { + QFont font = item->font(0); + font.setBold(true); + font.setItalic(true); + item->setFont(0, font); + } + item->setText(1, QString::fromUtf8(obj->getDocument()->getName())); + item->setText(2, QString::fromUtf8(obj->getNameInDocument())); + item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); + auto it = itemMap.find(obj); + if (it != itemMap.end()) + setCheckState(item, it->second[0]->checkState(0)); + return item; +} + void DlgObjectSelection::onItemSelectionChanged() { - SignalBlocker block2(ui->treeWidget); - SignalBlocker block(ui->depList); - QTreeWidgetItem *scroll=nullptr; - for(auto &v : objMap) { - auto &info = v.second; - auto it = sels.find(v.first); - auto selected = it==sels.end(); - for(auto item : info.items) { - if(selected == item->isSelected()) { - for(auto item : info.items) - item->setSelected(selected); - scroll = info.depItem; - info.depItem->setSelected(selected); - scroll = info.depItem; - if(!selected) - sels.erase(it); - else - sels.insert(v.first); - break; - } + ui->depList->clear(); + depMap.clear(); + ui->inList->clear(); + inMap.clear(); + + std::vector sels; + for (auto item : ui->treeWidget->selectedItems()) { + if (item == allItem) { + sels.clear(); + break; + } + auto obj = qvariant_cast(item->data(0, Qt::UserRole)).getObject(); + if (obj) + sels.push_back(obj); + } + + std::vector _deps; + if (sels.size()) { + std::sort(sels.begin(), sels.end()); + for (auto dep : App::Document::getDependencyList(sels, App::Document::DepSort)) { + if (!std::binary_search(sels.begin(), sels.end(), dep)) + _deps.push_back(dep); } } - if(scroll) - ui->depList->scrollToItem(scroll); -} -void DlgObjectSelection::onDepSelectionChanged() { - SignalBlocker block2(ui->treeWidget); - SignalBlocker block(ui->depList); - QTreeWidgetItem *scroll=nullptr; - for(auto &v : objMap) { - auto &info = v.second; - auto it = sels.find(v.first); - auto selected = it==sels.end(); - if(info.depItem->isSelected()==selected) { - for(auto item : info.items) { - scroll = item; - item->setSelected(selected); - } - if(!selected) - sels.erase(it); - else { - sels.insert(v.first); - for(auto item : info.items) { - for(auto parent=item->parent();parent;parent=parent->parent()) - parent->setExpanded(true); - } - } - } + bool enabled = ui->depList->isSortingEnabled(); + if (enabled) + ui->depList->setSortingEnabled(false); + + bool enabled2 = ui->inList->isSortingEnabled(); + if (enabled2) + ui->inList->setSortingEnabled(false); + + { + QSignalBlocker blocker(ui->depList); + auto &objs = sels.size() ? _deps : deps; + for (auto it = objs.rbegin(); it != objs.rend(); ++it) + createDepItem(ui->depList, *it); } - if(scroll) - ui->treeWidget->scrollToItem(scroll); -} -void DlgObjectSelection::onUseOriginalsBtnClicked(){ - returnOriginals = true; - QDialog::accept(); + std::set inlist; + for (auto obj : sels) + obj->getInListEx(inlist, true); + for (auto it = inlist.begin(); it != inlist.end();) { + if (!depSet.count(*it) || std::binary_search(sels.begin(), sels.end(), *it)) + it = inlist.erase(it); + else + ++it; + } + { + QSignalBlocker blocker2(ui->inList); + for (auto obj : inlist) + createDepItem(ui->inList, obj); + } + + if (enabled) + ui->depList->setSortingEnabled(true); + if (enabled2) + ui->inList->setSortingEnabled(true); } void DlgObjectSelection::accept() { @@ -391,4 +597,42 @@ void DlgObjectSelection::reject() { QDialog::reject(); } +void DlgObjectSelection::addCheckBox(QCheckBox *box) { + ui->horizontalLayout->insertWidget(0, box); +} + +void DlgObjectSelection::setMessage(const QString &msg) { + ui->label->setText(msg); +} + +void DlgObjectSelection::onAutoDeps(bool checked) +{ + hGrp->SetBool("ObjectSelectionAutoDeps", checked); + if (!checked) + return; + + QSignalBlocker blocker(ui->treeWidget); + for (auto obj : deps) { + auto it = itemMap.find(obj); + if (it == itemMap.end()) + continue; + auto item = it->second[0]; + if (item->checkState(0) == Qt::Unchecked) + continue; + Qt::CheckState state = Qt::Checked; + for (auto o : obj->getOutList()) { + auto it = itemMap.find(o); + if (it == itemMap.end()) + continue; + if (it->second[0]->checkState(0) != Qt::Checked) { + state = Qt::PartiallyChecked; + break; + } + } + for (auto i : it->second) + setCheckState(i, state); + } + onItemSelectionChanged(); +} + #include "moc_DlgObjectSelection.cpp" diff --git a/src/Gui/DlgObjectSelection.h b/src/Gui/DlgObjectSelection.h index 8c1adf4240..b1e5108dbc 100644 --- a/src/Gui/DlgObjectSelection.h +++ b/src/Gui/DlgObjectSelection.h @@ -23,52 +23,116 @@ #define GUI_DLGOBJECTSELECTION_H #include +#include +#include +#include +#include + +class QCheckBox; +class QTreeWidgetItem; +class QTreeWidget; namespace Gui { class Ui_DlgObjectSelection; + +/** Dialog for object dependency selection + */ class GuiExport DlgObjectSelection : public QDialog { Q_OBJECT public: + /** Constructor + * + * Creates a dialog for selecting the given objects and their dependent + * objects + * + * @param objs: initial objects + * @param parent: optional parent widget + * @param fl: optional window flags + */ DlgObjectSelection(const std::vector &objs, QWidget* parent = nullptr, Qt::WindowFlags fl = Qt::WindowFlags()); + + /** Constructor + * + * Creates a dialog for selecting the given objects and their dependent + * objects with exclusions + * + * @param objs: initial objects + * @param excludes: excluded objects. The objects and their dependents will + * still be in the list but unchecked. + * @param parent: optional parent widget + * @param fl: optional window flags + */ + DlgObjectSelection(const std::vector &objs, + const std::vector &excludes, + QWidget* parent = nullptr, Qt::WindowFlags fl = Qt::WindowFlags()); + + /// Destructor ~DlgObjectSelection(); - std::vector getSelections() const; + /// Options for getSelections() + enum class SelectionOptions { + /// Invert the selection, i.e. return the unselected objects + Invert = 1, + /// Sort the returned object in depending order + Sort = 2, + /// Return the unselected objects sorted in depending order + InvertSort = 3, + }; + /// Get the selected objects + std::vector getSelections(SelectionOptions options = SelectionOptions()) const; + + /// Add a user defined checkbox at the bottom of the dialog + void addCheckBox(QCheckBox *box); + + /// Override the prompt message + void setMessage(const QString &); + void accept(); void reject(); private Q_SLOTS: - void onItemExpanded(QTreeWidgetItem * item); - void onItemChanged(QTreeWidgetItem * item, int); + void onDepItemChanged(QTreeWidgetItem * item, int); + void onObjItemChanged(QTreeWidgetItem * item, int); void onItemSelectionChanged(); - void onDepSelectionChanged(); - void onUseOriginalsBtnClicked(); + void checkItemChanged(); + void onAutoDeps(bool); + void onItemExpanded(QTreeWidgetItem *item); private: - QTreeWidgetItem *createItem(App::DocumentObject *obj, QTreeWidgetItem *parent); - App::DocumentObject *objFromItem(QTreeWidgetItem *item); - QPushButton *useOriginalsBtn; - std::vector originalSelections; - bool returnOriginals = false; + QTreeWidgetItem *getItem(App::DocumentObject *obj, + std::vector **items = nullptr, + QTreeWidgetItem *parent = nullptr); + + QTreeWidgetItem *createDepItem(QTreeWidget *parent, App::DocumentObject *obj); + + void init(const std::vector &objs, + const std::vector &excludes); + + void setItemState(App::DocumentObject *obj, Qt::CheckState state, bool forced = false); + void updateAllItemState(); private: - struct Info { - std::map inList; - std::map outList; - std::vector items; - QTreeWidgetItem *depItem = nullptr; - Qt::CheckState checkState = Qt::Checked; - }; - std::map objMap; Ui_DlgObjectSelection* ui; - std::set sels; + std::vector initSels; + std::vector deps; + std::set depSet; + std::map> itemMap; + std::map depMap; + std::map inMap; + std::map itemChanged; + QTreeWidgetItem *allItem = nullptr; + + QTimer timer; + ParameterGrp::handle hGrp; }; } // namespace Gui +ENABLE_BITMASK_OPERATORS(Gui::DlgObjectSelection::SelectionOptions); #endif // GUI_DLGOBJECTSELECTION_H diff --git a/src/Gui/DlgObjectSelection.ui b/src/Gui/DlgObjectSelection.ui index 5c4ee9a40e..af4c6fdd1e 100644 --- a/src/Gui/DlgObjectSelection.ui +++ b/src/Gui/DlgObjectSelection.ui @@ -7,7 +7,7 @@ 0 0 621 - 383 + 375 @@ -23,7 +23,7 @@ - + 0 0 @@ -37,7 +37,7 @@ - + 0 @@ -54,37 +54,10 @@ 0 - - - 0 - 0 - - QAbstractItemView::ExtendedSelection - - true - - - true - - - false - - - 1 - - true - - - false - - - false - - false @@ -93,61 +66,115 @@ - - - - 1 - 0 - + + + Qt::Vertical - - QAbstractItemView::ExtendedSelection - - - false - - - true - - - 4 - - - true - - - - 1 + + + + 1 + 0 + - - - - 2 + + QAbstractItemView::ExtendedSelection - - - - 3 + + false - + + true + + + 3 + + + true + + + + 1 + + + + + 2 + + + + + 3 + + + + + + + 1 + 0 + + + + QAbstractItemView::ExtendedSelection + + + false + + + true + + + 3 + + + true + + + + 1 + + + + + 2 + + + + + 3 + + + - - - - 0 - 1 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - + + + + + Auto select depending objects + + + + + + + + 0 + 1 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + +