// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * Copyright (c) 2018 Zheng Lei (realthunder) * * * * 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 #include #include #include #include #include "Dialogs/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) using namespace Gui; /* TRANSLATOR Gui::DlgObjectSelection */ DlgObjectSelection::DlgObjectSelection( const std::vector& objs, QWidget* parent, Qt::WindowFlags fl ) : QDialog(parent, fl) { 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, &QCheckBox::toggled, this, &DlgObjectSelection::onAutoDeps); ui->checkBoxShowDeps->setChecked(hGrp->GetBool("ObjectSelectionShowDeps", false)); QObject::connect(ui->checkBoxShowDeps, &QCheckBox::toggled, [this](bool checked) { hGrp->SetBool("ObjectSelectionShowDeps", checked); onShowDeps(); }); QMetaObject::invokeMethod(this, "onShowDeps", Qt::QueuedConnection); // 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->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->headerItem()->setText(0, tr("Depending on")); ui->depList->headerItem()->setText(1, tr("Document")); ui->depList->headerItem()->setText(2, tr("Name")); 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); connect(ui->treeWidget, &QTreeWidget::itemExpanded, this, &DlgObjectSelection::onItemExpanded); 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(); /** * 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 Selection")); useOriginalsBtn->setToolTip( tr("Ignore dependencies and proceed with the objects\noriginally selected prior to opening " "this dialog") ); ui->buttonBox->addButton(useOriginalsBtn, QDialogButtonBox::ResetRole); connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &DlgObjectSelection::onObjItemChanged); connect(ui->depList, &QTreeWidget::itemChanged, this, &DlgObjectSelection::onDepItemChanged); connect(ui->inList, &QTreeWidget::itemChanged, this, &DlgObjectSelection::onDepItemChanged); connect( ui->treeWidget, &QTreeWidget::itemSelectionChanged, this, &DlgObjectSelection::onItemSelectionChanged ); connect(useOriginalsBtn, &QPushButton::clicked, this, &DlgObjectSelection::onUseOriginalsBtnClicked); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &DlgObjectSelection::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DlgObjectSelection::reject); timer.setSingleShot(true); connect(&timer, &QTimer::timeout, this, &DlgObjectSelection::checkItemChanged); } /** * Destroys the object and frees any allocated resources */ DlgObjectSelection::~DlgObjectSelection() { // no need to delete child widgets, Qt does it all for us delete ui; } 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.empty()) { return items[0]; } item = new QTreeWidgetItem(ui->treeWidget); auto vp = freecad_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.empty()) { 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); return item; } void DlgObjectSelection::onItemExpanded(QTreeWidgetItem* item) { if (item->childCount()) { return; } 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::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); } else if (!count) { allItem->setCheckState(0, Qt::Unchecked); } } 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; i < items->size(); ++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.contains(o) || itemChanged.contains(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); } } 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.contains(o) || itemChanged.contains(o)) { continue; } auto it = itemMap.find(o); if (it == itemMap.end() || it->second[0]->checkState(0) == state) { continue; } int count = 0; int selcount = 0; for (auto sibling : o->getOutList()) { if (!depSet.contains(sibling)) { continue; } ++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; } ++selcount; } 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(SelectionOptions options) const { if (returnOriginals) { return initSels; } std::vector res; 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()) { const auto items = depItem->treeWidget()->selectedItems(); for (auto item : items) { 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 { const auto items = ui->treeWidget->selectedItems(); for (auto item : items) { 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 (const 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.empty()) { // 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 (!itemChanged.contains(v.first) && 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() { ui->depList->clear(); depMap.clear(); ui->inList->clear(); inMap.clear(); std::vector sels; const auto items = ui->treeWidget->selectedItems(); for (auto item : items) { 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.empty()) { 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); } } } 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.empty() ? _deps : deps; for (auto it = objs.rbegin(); it != objs.rend(); ++it) { createDepItem(ui->depList, *it); } } std::set inlist; for (auto obj : sels) { obj->getInListEx(inlist, true); } for (auto it = inlist.begin(); it != inlist.end();) { if (!depSet.contains(*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::onUseOriginalsBtnClicked() { returnOriginals = true; QDialog::accept(); } void DlgObjectSelection::accept() { QDialog::accept(); } 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(); } void DlgObjectSelection::onShowDeps() { bool checked = ui->checkBoxShowDeps->isChecked(); auto sizes = ui->vsplitter->sizes(); if (!checked && sizes[1] > 0) { sizes[1] = 0; } else if (checked && (sizes[0] == 0 || sizes[1] == 0)) { sizes[0] = sizes[1] = this->width() / 2; } else { return; } ui->vsplitter->setSizes(sizes); } #include "moc_DlgObjectSelection.cpp"