// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2015 Thomas Anderson * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DAGModel.h" using namespace Gui; using namespace DAG; namespace sp = std::placeholders; LineEdit::LineEdit(QWidget* parentIn) : QLineEdit(parentIn) {} void LineEdit::keyPressEvent(QKeyEvent* eventIn) { if (eventIn->key() == Qt::Key_Escape) { Q_EMIT rejectedSignal(); eventIn->accept(); return; } if ((eventIn->key() == Qt::Key_Enter) || (eventIn->key() == Qt::Key_Return)) { Q_EMIT acceptedSignal(); eventIn->accept(); return; } QLineEdit::keyPressEvent(eventIn); } // I don't think I should have to call invalidate // and definitely not on the whole scene! // if we have performance problems, this will definitely // be something to re-visit. I am not wasting anymore time on // this right now. // this->scene()->invalidate(); // this->scene()->invalidate(this->sceneTransform().inverted().mapRect(this->boundingRect())); // update(boundingRect()); // note: I haven't tried this again since I turned BSP off. Model::Model(QObject* parentIn, const Gui::Document& documentIn) : QGraphicsScene(parentIn) { // turned off BSP as it was giving inconsistent discovery of items // underneath cursor. this->setItemIndexMethod(QGraphicsScene::NoIndex); theGraph = std::make_shared(); graphLink = std::make_shared(); setupViewConstants(); graphDirty = false; currentPrehighlight = nullptr; ParameterGrp::handle group = App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") ->GetGroup("Preferences") ->GetGroup("DAGView"); selectionMode = static_cast(group->GetInt("SelectionMode", 0)); group->SetInt("SelectionMode", static_cast(selectionMode)); // ensure entry exists. QIcon temp(Gui::BitmapFactory().iconFromTheme("dagViewVisible")); visiblePixmapEnabled = temp.pixmap(iconSize, iconSize, QIcon::Normal, QIcon::On); visiblePixmapDisabled = temp.pixmap(iconSize, iconSize, QIcon::Disabled, QIcon::Off); QIcon passIcon(Gui::BitmapFactory().iconFromTheme("dagViewPass")); passPixmap = passIcon.pixmap(iconSize, iconSize); QIcon failIcon(Gui::BitmapFactory().iconFromTheme("dagViewFail")); failPixmap = failIcon.pixmap(iconSize, iconSize); QIcon pendingIcon(Gui::BitmapFactory().iconFromTheme("dagViewPending")); pendingPixmap = pendingIcon.pixmap(iconSize, iconSize); renameAction = new QAction(this); renameAction->setText(tr("Rename")); renameAction->setStatusTip(tr("Renames the object")); #ifndef Q_OS_MAC renameAction->setShortcut(Qt::Key_F2); #endif connect(renameAction, &QAction::triggered, this, &Model::renameAcceptedSlot); editingFinishedAction = new QAction(this); editingFinishedAction->setText(tr("Finish Editing")); editingFinishedAction->setStatusTip(tr("Finishes editing the object")); connect(this->editingFinishedAction, &QAction::triggered, this, &Model::editingFinishedSlot); // NOLINTBEGIN connectNewObject = documentIn.signalNewObject.connect( std::bind(&Model::slotNewObject, this, sp::_1) ); connectDelObject = documentIn.signalDeletedObject.connect( std::bind(&Model::slotDeleteObject, this, sp::_1) ); connectChgObject = documentIn.signalChangedObject.connect( std::bind(&Model::slotChangeObject, this, sp::_1, sp::_2) ); connectEdtObject = documentIn.signalInEdit.connect(std::bind(&Model::slotInEdit, this, sp::_1)); connectResObject = documentIn.signalResetEdit.connect( std::bind(&Model::slotResetEdit, this, sp::_1) ); // NOLINTEND for (auto obj : documentIn.getDocument()->getObjects()) { auto vpd = freecad_cast(documentIn.getViewProvider(obj)); if (vpd) { slotNewObject(*vpd); } } } Model::~Model() { if (connectNewObject.connected()) { connectNewObject.disconnect(); } if (connectDelObject.connected()) { connectDelObject.disconnect(); } if (connectChgObject.connected()) { connectChgObject.disconnect(); } if (connectEdtObject.connected()) { connectEdtObject.disconnect(); } if (connectResObject.connected()) { connectResObject.disconnect(); } removeAllItems(); } void Model::setupViewConstants() { ParameterGrp::handle group = App::GetApplication() .GetUserParameter() .GetGroup("BaseApp") ->GetGroup("Preferences") ->GetGroup("DAGView"); // get font point size. int fontPointSize = group->GetInt("FontPointSize", 0); group->SetInt("FontPointSize", fontPointSize); // ensure entry exists. if (fontPointSize != 0) { QFont tempFont(this->font()); tempFont.setPointSize(fontPointSize); this->setFont(tempFont); } // get direction direction = group->GetFloat("Direction", 1.0); if (direction != -1.0 && direction != 1.0) { direction = 1.0; } group->SetFloat("Direction", direction); // ensure entry exists. QFontMetrics fontMetric(this->font()); fontHeight = fontMetric.height(); verticalSpacing = 1.0; rowHeight = (fontHeight + 2.0 * verticalSpacing) * direction; // pixel space top and bottom. iconSize = fontHeight; pointSize = fontHeight / 2.0; pointSpacing = pointSize; pointToIcon = iconSize; iconToIcon = iconSize * 0.25; iconToText = iconSize / 2.0; rowPadding = fontHeight; backgroundBrushes = {this->palette().base(), this->palette().alternateBase()}; forgroundBrushes = { QBrush(Qt::red), QBrush(Qt::darkRed), QBrush(Qt::green), QBrush(Qt::darkGreen), QBrush(Qt::blue), QBrush(Qt::darkBlue), QBrush(Qt::cyan), QBrush(Qt::darkCyan), QBrush(Qt::magenta), QBrush(Qt::darkMagenta), // QBrush(Qt::yellow), can't read QBrush(Qt::darkYellow), QBrush(Qt::gray), QBrush(Qt::darkGray), QBrush(Qt::lightGray) }; // reserve some of the these for highlight stuff. } void Model::slotNewObject(const ViewProviderDocumentObject& VPDObjectIn) { Vertex virginVertex = boost::add_vertex(*theGraph); addVertexItemsToScene(virginVertex); GraphLinkRecord virginRecord; virginRecord.DObject = VPDObjectIn.getObject(); virginRecord.VPDObject = &VPDObjectIn; virginRecord.rectItem = (*theGraph)[virginVertex].rectangle.get(); virginRecord.uniqueName = std::string(virginRecord.DObject->getNameInDocument()); virginRecord.vertex = virginVertex; graphLink->insert(virginRecord); // setup rectangle. auto rectangle = (*theGraph)[virginVertex].rectangle.get(); rectangle->setEditingBrush(QBrush(Qt::yellow)); auto icon = (*theGraph)[virginVertex].icon; icon->setPixmap(VPDObjectIn.getIcon().pixmap(iconSize, iconSize)); (*theGraph)[virginVertex].stateIcon->setPixmap(passPixmap); (*theGraph)[virginVertex].text->setFont(this->font()); // NOLINTBEGIN (*theGraph)[virginVertex].connChangeIcon = const_cast(VPDObjectIn) .signalChangeIcon.connect( std::bind(&Model::slotChangeIcon, this, boost::cref(VPDObjectIn), icon) ); // NOLINTEND graphDirty = true; lastAddedVertex = Graph::null_vertex(); } void Model::slotChangeIcon( const ViewProviderDocumentObject& VPDObjectIn, std::shared_ptr icon ) { icon->setPixmap(VPDObjectIn.getIcon().pixmap(iconSize, iconSize)); this->invalidate(); } void Model::slotDeleteObject(const ViewProviderDocumentObject& VPDObjectIn) { Vertex vertex = findRecord(&VPDObjectIn, *graphLink).vertex; // remove items from scene. removeVertexItemsFromScene(vertex); // remove connector items auto outRange = boost::out_edges(vertex, *theGraph); for (auto outEdgeIt = outRange.first; outEdgeIt != outRange.second; ++outEdgeIt) { this->removeItem((*theGraph)[*outEdgeIt].connector.get()); } auto inRange = boost::in_edges(vertex, *theGraph); for (auto inEdgeIt = inRange.first; inEdgeIt != inRange.second; ++inEdgeIt) { this->removeItem((*theGraph)[*inEdgeIt].connector.get()); } if (vertex == lastAddedVertex) { lastAddedVertex = Graph::null_vertex(); } (*theGraph)[vertex].connChangeIcon.disconnect(); // remove the actual vertex. boost::clear_vertex(vertex, *theGraph); boost::remove_vertex(vertex, *theGraph); eraseRecord(&VPDObjectIn, *graphLink); graphDirty = true; } void Model::slotChangeObject(const ViewProviderDocumentObject& VPDObjectIn, const App::Property& propertyIn) { std::string name("Empty Name"); if (propertyIn.hasName()) { name = propertyIn.getName(); } assert(!name.empty()); // std::cout << std::endl << "inside changed object." << std::endl << // "Property name is: " << name << std::endl << // "Property type is: " << propertyIn.getTypeId().getName() << std::endl << std::endl; // renaming of objects. if (std::string("Label") == name) { if (hasRecord(&VPDObjectIn, *graphLink)) { const GraphLinkRecord& record = findRecord(&VPDObjectIn, *graphLink); auto text = (*theGraph)[record.vertex].text.get(); text->setPlainText(QString::fromUtf8(record.DObject->Label.getValue())); } } else if (propertyIn.isDerivedFrom()) { if (hasRecord(&VPDObjectIn, *graphLink)) { const GraphLinkRecord& record = findRecord(&VPDObjectIn, *graphLink); boost::clear_vertex(record.vertex, *theGraph); graphDirty = true; } } } void Model::slotInEdit(const ViewProviderDocumentObject& VPDObjectIn) { RectItem* rect = (*theGraph)[findRecord(&VPDObjectIn, *graphLink).vertex].rectangle.get(); rect->editingStart(); this->invalidate(); } void Model::slotResetEdit(const ViewProviderDocumentObject& VPDObjectIn) { RectItem* rect = (*theGraph)[findRecord(&VPDObjectIn, *graphLink).vertex].rectangle.get(); rect->editingFinished(); this->invalidate(); } void Model::selectionChanged(const SelectionChanges& msg) { // TODO: note that treeview uses set selection which sends a message with just a document name // and no object name. Have to explore further. auto getAllEdges = [this](const Vertex& vertexIn) { // is there really no function to get both in and out edges? std::vector out; OutEdgeIterator outIt, outItEnd; for (boost::tie(outIt, outItEnd) = boost::out_edges(vertexIn, *theGraph); outIt != outItEnd; ++outIt) { out.push_back(*outIt); } InEdgeIterator inIt, inItEnd; for (boost::tie(inIt, inItEnd) = boost::in_edges(vertexIn, *theGraph); inIt != inItEnd; ++inIt) { out.push_back(*inIt); } return out; }; auto highlightConnectorOn = [this, getAllEdges](const Vertex& vertexIn) { QColor color = (*theGraph)[vertexIn].text->defaultTextColor(); QPen pen(color); pen.setWidth(3.0); auto edges = getAllEdges(vertexIn); for (auto edge : edges) { (*theGraph)[edge].connector->setPen(pen); (*theGraph)[edge].connector->setZValue(1.0); } }; auto highlightConnectorOff = [this, getAllEdges](const Vertex& vertexIn) { auto edges = getAllEdges(vertexIn); for (auto edge : edges) { (*theGraph)[edge].connector->setPen(QPen()); (*theGraph)[edge].connector->setZValue(0.0); } }; // lambda for clearing selections. auto clearSelection = [this, highlightConnectorOff]() { BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) { RectItem* rect = (*theGraph)[currentVertex].rectangle.get(); assert(rect); rect->selectionOff(); highlightConnectorOff(currentVertex); } }; // lambda for getting rectangle. auto getRectangle = [this](const char* in) { assert(in); std::string name(in); assert(!name.empty()); const GraphLinkRecord& record = findRecord(name, *graphLink); RectItem* rect = (*theGraph)[record.vertex].rectangle.get(); assert(rect); return rect; }; if (msg.Type == SelectionChanges::AddSelection) { if (msg.pObjectName) { RectItem* rect = getRectangle(msg.pObjectName); rect->selectionOn(); highlightConnectorOn(findRecord(std::string(msg.pObjectName), *graphLink).vertex); } } else if (msg.Type == SelectionChanges::RmvSelection) { if (msg.pObjectName) { RectItem* rect = getRectangle(msg.pObjectName); rect->selectionOff(); highlightConnectorOff(findRecord(std::string(msg.pObjectName), *graphLink).vertex); } } else if (msg.Type == SelectionChanges::SetSelection) { clearSelection(); auto selections = Gui::Selection().getSelection(msg.pDocName); for (const auto& selection : selections) { assert(selection.FeatName); RectItem* rect = getRectangle(selection.FeatName); rect->selectionOn(); highlightConnectorOn(findRecord(selection.FeatName, *graphLink).vertex); } } else if (msg.Type == SelectionChanges::ClrSelection) { clearSelection(); } this->invalidate(); } void Model::awake() { if (graphDirty) { updateSlot(); this->invalidate(); } updateStates(); } void Model::updateSlot() { // empty outList means it is a root. // empty inList means it is a leaf. // NOTE: some of the following loops can/should be combined // for speed. Not doing yet, as I want a simple algorithm until // a more complete picture is formed. Base::TimeElapsed startTime; // here we will cycle through the graph updating edges. // we have to do this first and in isolation because everything is dependent on an up to date graph. BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) { const App::DocumentObject* currentDObject = findRecord(currentVertex, *graphLink).DObject; std::vector otherDObjects = currentDObject->getOutList(); for (auto& currentOtherDObject : otherDObjects) { if (!hasRecord(currentOtherDObject, *graphLink)) { continue; } Vertex otherVertex = findRecord(currentOtherDObject, *graphLink).vertex; bool result; Edge edge; boost::tie(edge, result) = boost::add_edge(currentVertex, otherVertex, *theGraph); if (result) { (*theGraph)[edge].connector = std::make_shared(); (*theGraph)[edge].connector->setZValue(0.0); } } } BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) { if (!(*theGraph)[currentVertex].rectangle->scene()) { addVertexItemsToScene(currentVertex); } } // sync scene items for graph edge. BGL_FORALL_EDGES(currentEdge, *theGraph, Graph) { if (!(*theGraph)[currentEdge].connector->scene()) { this->addItem((*theGraph)[currentEdge].connector.get()); } } indexVerticesEdges(); Path sorted; try { boost::topological_sort(*theGraph, std::back_inserter(sorted)); } catch (const boost::not_a_dag&) { Base::Console().error("not a dag exception in DAGView::Model::updateSlot()\n"); // do not continuously report an error for cyclic graphs graphDirty = false; return; } // index the vertices in sort order. int tempIndex = 0; for (const auto& currentVertex : sorted) { (*theGraph)[currentVertex].topoSortIndex = tempIndex; tempIndex++; } // draw graph(nodes and connectors). int currentRow = 0; int currentColumn = -1; // we know first column is going to be root so will be kicked up to 0. int maxColumn = currentColumn; // used for determining offset of icons and text. qreal maxTextLength = 0; for (const auto& currentVertex : sorted) { if (boost::out_degree(currentVertex, *theGraph) == 0) { currentColumn = 0; } else { // loop parents and find an acceptable column. int farthestParentIndex = sorted.size(); ColumnMask columnMask; Path parentVertices; OutEdgeIterator it, itEnd; boost::tie(it, itEnd) = boost::out_edges(currentVertex, *theGraph); for (; it != itEnd; ++it) { // std::cout << std::endl << "name: " << findRecord(currentVertex, // *graphLink).DObject->Label.getValue() << std::endl; Vertex target = boost::target(*it, *theGraph); parentVertices.push_back(target); int currentParentIndex = (*theGraph)[target].topoSortIndex; if (currentParentIndex < farthestParentIndex) { Path::const_iterator start = sorted.begin() + currentParentIndex + 1; // 1 after Path::const_iterator end = sorted.begin() + (*theGraph)[currentVertex].topoSortIndex; // 1 before Path::const_iterator it; for (it = start; it != end; ++it) { // std::cout << " parent: " << findRecord(*it, // *graphLink).DObject->Label.getValue() << std::endl; columnMask |= (*theGraph)[*it].column; } farthestParentIndex = currentParentIndex; } } // have to create a smaller subset to get through std::cout. // std::bitset<8> testSet; // for (unsigned int index = 0; index < testSet.size(); ++index) // testSet[index]= columnMask[index]; // std::cout << "mask for " << findRecord(currentVertex, // *graphLink).DObject->Label.getValue() << " " << // testSet.to_string() << std::endl; // now we should have a mask representing the columns that are being used. // this is from the lowest parent, in the topo sort, to last entry. // try to use the same column as one of the parents.(*theGraph)[*it].column int destinationColumn = 0; // default to first column for (const auto& currentParent : parentVertices) { if (((*theGraph)[currentParent].column & columnMask).none()) { // go with first visible parent for now. destinationColumn = static_cast( columnFromMask((*theGraph)[currentParent].column) ); break; } } // if destination not valid look for the first open column. if (columnMask.test(destinationColumn)) { for (std::size_t index = 0; index < columnMask.size(); ++index) { if (!columnMask.test(index)) { destinationColumn = index; break; } } } currentColumn = destinationColumn; } assert(currentColumn < static_cast(ColumnMask().size())); // temp limitation. maxColumn = std::max(currentColumn, maxColumn); QBrush currentBrush(forgroundBrushes.at(currentColumn % forgroundBrushes.size())); auto rectangle = (*theGraph)[currentVertex].rectangle.get(); rectangle->setRect(-rowPadding, 0.0, rowPadding, rowHeight); // calculate actual length later. rectangle->setTransform(QTransform::fromTranslate(0, rowHeight * currentRow)); rectangle->setBackgroundBrush(backgroundBrushes[currentRow % backgroundBrushes.size()]); auto point = (*theGraph)[currentVertex].point.get(); point->setRect(0.0, 0.0, pointSize, pointSize); point->setTransform( QTransform::fromTranslate( pointSpacing * static_cast(currentColumn), rowHeight * currentRow + rowHeight / 2.0 - pointSize / 2.0 ) ); point->setBrush(currentBrush); qreal cheat = 0.0; if (direction == -1) { cheat = rowHeight; } auto visiblePixmap = (*theGraph)[currentVertex].visibleIcon.get(); visiblePixmap->setTransform( QTransform::fromTranslate(0.0, rowHeight * currentRow + cheat) ); // calculate x location later. auto statePixmap = (*theGraph)[currentVertex].stateIcon.get(); statePixmap->setTransform( QTransform::fromTranslate(0.0, rowHeight * currentRow + cheat) ); // calculate x location later. auto pixmap = (*theGraph)[currentVertex].icon.get(); pixmap->setTransform( QTransform::fromTranslate(0.0, rowHeight * currentRow + cheat) ); // calculate x location later. auto text = (*theGraph)[currentVertex].text.get(); text->setPlainText( QString::fromUtf8(findRecord(currentVertex, *graphLink).DObject->Label.getValue()) ); text->setDefaultTextColor(currentBrush.color()); maxTextLength = std::max(maxTextLength, text->boundingRect().width()); text->setTransform( QTransform::fromTranslate(0.0, rowHeight * currentRow - verticalSpacing * 2.0 + cheat) ); // calculate x location later. (*theGraph)[currentVertex].lastVisibleState = VisibilityState::None; // force visual update // for color. // store column and row int the graph. use for connectors later. (*theGraph)[currentVertex].row = currentRow; (*theGraph)[currentVertex].column.reset().set((currentColumn)); // our list is topo sorted so all dependents should be located, so we can build the // connectors. will have some more logic for connector path, simple for now. qreal currentX = pointSpacing * currentColumn + pointSize / 2.0; qreal currentY = rowHeight * currentRow + rowHeight / 2.0; OutEdgeIterator it, itEnd; boost::tie(it, itEnd) = boost::out_edges(currentVertex, *theGraph); for (; it != itEnd; ++it) { Vertex target = boost::target(*it, *theGraph); qreal dependentX = pointSpacing * static_cast(columnFromMask((*theGraph)[target].column)) + pointSize / 2.0; // on center. columnFromMask((*theGraph)[target].column); qreal dependentY = rowHeight * (*theGraph)[target].row + rowHeight / 2.0; QGraphicsPathItem* pathItem = (*theGraph)[*it].connector.get(); pathItem->setBrush(Qt::NoBrush); QPainterPath path; path.moveTo(currentX, currentY); if (currentColumn == static_cast(columnFromMask((*theGraph)[target].column))) { path.lineTo(currentX, dependentY); // straight connector in y. } else { // connector with bend. qreal radius = pointSpacing / 1.9; // no zero length line. path.lineTo(currentX, dependentY + radius * direction); qreal yPosition; if (direction == -1.0) { yPosition = dependentY - 2.0 * radius; } else { yPosition = dependentY; } qreal width = 2.0 * radius; qreal height = width; if (dependentX > currentX) // radius to the right. { QRectF arcRect(currentX, yPosition, width, height); path.arcTo(arcRect, 180.0, 90.0 * -direction); } else // radius to the left. { QRectF arcRect(currentX - 2.0 * radius, yPosition, width, height); path.arcTo(arcRect, 0.0, 90.0 * direction); } path.lineTo(dependentX, dependentY); } pathItem->setPath(path); } currentRow++; } // now that we have the graph drawn we know where to place icons and text. qreal columnSpacing = (maxColumn * pointSpacing); for (const auto& currentVertex : sorted) { qreal localCurrentX = columnSpacing; localCurrentX += pointToIcon; auto visiblePixmap = (*theGraph)[currentVertex].visibleIcon.get(); QTransform visibleIconTransform = QTransform::fromTranslate(localCurrentX, 0.0); visiblePixmap->setTransform(visiblePixmap->transform() * visibleIconTransform); localCurrentX += iconSize + iconToIcon; auto statePixmap = (*theGraph)[currentVertex].stateIcon.get(); QTransform stateIconTransform = QTransform::fromTranslate(localCurrentX, 0.0); statePixmap->setTransform(statePixmap->transform() * stateIconTransform); localCurrentX += iconSize + iconToIcon; auto pixmap = (*theGraph)[currentVertex].icon.get(); QTransform iconTransform = QTransform::fromTranslate(localCurrentX, 0.0); pixmap->setTransform(pixmap->transform() * iconTransform); localCurrentX += iconSize + iconToText; auto text = (*theGraph)[currentVertex].text.get(); QTransform textTransform = QTransform::fromTranslate(localCurrentX, 0.0); text->setTransform(text->transform() * textTransform); auto rectangle = (*theGraph)[currentVertex].rectangle.get(); QRectF rect = rectangle->rect(); rect.setWidth(localCurrentX + maxTextLength + 2.0 * rowPadding); rectangle->setRect(rect); } // Modeling_Challenge_Casting_ta4 with 59 features: "Initialize DAG View time: 0.007" // keeping algo simple with extra loops only added 0.002 to above number. // std::cout << "Initialize DAG View time: " << Base::TimeElapsed::diffTimeF(startTime, // Base::TimeElapsed()) << std::endl; // outputGraphviz(*theGraph, "./graphviz.dot"); graphDirty = false; } void Model::indexVerticesEdges() { std::size_t index = 0; // index vertices. VertexIterator it, itEnd; for (boost::tie(it, itEnd) = boost::vertices(*theGraph); it != itEnd; ++it) { boost::put(boost::vertex_index, *theGraph, *it, index); index++; } // index edges. didn't need this when I put it in. EdgeIterator eit, eitEnd; index = 0; for (boost::tie(eit, eitEnd) = boost::edges(*theGraph); eit != eitEnd; ++eit) { boost::put(boost::edge_index, *theGraph, *eit, index); index++; } } void Model::removeAllItems() { if (theGraph) { BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) removeVertexItemsFromScene(currentVertex); BGL_FORALL_EDGES(currentEdge, *theGraph, Graph) { if ((*theGraph)[currentEdge].connector->scene()) { this->removeItem((*theGraph)[currentEdge].connector.get()); } } } } void Model::addVertexItemsToScene(const Gui::DAG::Vertex& vertexIn) { // these are either all in or all out. so just test rectangle. if ((*theGraph)[vertexIn].rectangle->scene()) { // already in the scene. return; } this->addItem((*theGraph)[vertexIn].rectangle.get()); this->addItem((*theGraph)[vertexIn].point.get()); this->addItem((*theGraph)[vertexIn].visibleIcon.get()); this->addItem((*theGraph)[vertexIn].stateIcon.get()); this->addItem((*theGraph)[vertexIn].icon.get()); this->addItem((*theGraph)[vertexIn].text.get()); } void Model::removeVertexItemsFromScene(const Gui::DAG::Vertex& vertexIn) { // these are either all in or all out. so just test rectangle. if (!(*theGraph)[vertexIn].rectangle->scene()) { // not in the scene. return; } this->removeItem((*theGraph)[vertexIn].rectangle.get()); this->removeItem((*theGraph)[vertexIn].point.get()); this->removeItem((*theGraph)[vertexIn].visibleIcon.get()); this->removeItem((*theGraph)[vertexIn].stateIcon.get()); this->removeItem((*theGraph)[vertexIn].text.get()); this->removeItem((*theGraph)[vertexIn].icon.get()); } void Model::updateStates() { // not sure I want to use the same pixmap merge for failing feature icons. // thinking maybe red background or another column of icons for state? BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) { const GraphLinkRecord& record = findRecord(currentVertex, *graphLink); auto visiblePixmap = (*theGraph)[currentVertex].visibleIcon.get(); VisibilityState currentVisibilityState = (record.VPDObject->isShow()) ? (VisibilityState::On) : (VisibilityState::Off); if ((currentVisibilityState != (*theGraph)[currentVertex].lastVisibleState) || ((*theGraph)[currentVertex].lastVisibleState == VisibilityState::None)) { if (record.VPDObject->isShow()) { visiblePixmap->setPixmap(visiblePixmapEnabled); } else { visiblePixmap->setPixmap(visiblePixmapDisabled); } (*theGraph)[currentVertex].lastVisibleState = currentVisibilityState; } FeatureState currentFeatureState = FeatureState::Pass; if (record.DObject->isError()) { currentFeatureState = FeatureState::Fail; } else if ((record.DObject->mustExecute() == 1)) { currentFeatureState = FeatureState::Pending; } if (currentFeatureState != (*theGraph)[currentVertex].lastFeatureState) { if (currentFeatureState == FeatureState::Pass) { (*theGraph)[currentVertex].stateIcon->setPixmap(passPixmap); } else { if (currentFeatureState == FeatureState::Fail) { (*theGraph)[currentVertex].stateIcon->setPixmap(failPixmap); } else { (*theGraph)[currentVertex].stateIcon->setPixmap(pendingPixmap); } } (*theGraph)[currentVertex].stateIcon->setToolTip( QString::fromLatin1(record.DObject->getStatusString()) ); (*theGraph)[currentVertex].lastFeatureState = currentFeatureState; } } } std::size_t Model::columnFromMask(const ColumnMask& maskIn) { std::string maskString = maskIn.to_string(); return maskString.size() - maskString.find('1') - 1; } RectItem* Model::getRectFromPosition(const QPointF& position) { RectItem* rect = nullptr; auto theItems = this->items(position, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder); for (auto currentItem : theItems) { rect = dynamic_cast(currentItem); if (rect) { break; } } return rect; } void Model::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { auto clearPrehighlight = [this]() { if (currentPrehighlight) { currentPrehighlight->preHighlightOff(); currentPrehighlight = nullptr; } }; RectItem* rect = getRectFromPosition(event->scenePos()); if (!rect) { clearPrehighlight(); return; } if (rect == currentPrehighlight) { return; } clearPrehighlight(); rect->preHighlightOn(); currentPrehighlight = rect; invalidate(); QGraphicsScene::mouseMoveEvent(event); } void Model::mousePressEvent(QGraphicsSceneMouseEvent* event) { auto goShiftSelect = [this, event]() { QPointF currentPickPoint = event->scenePos(); QGraphicsLineItem intersectionLine(QLineF(lastPick, currentPickPoint)); QList selection = collidingItems(&intersectionLine); for (auto currentItem : selection) { auto rect = dynamic_cast(currentItem); if (!rect) { continue; } const GraphLinkRecord& selectionRecord = findRecord(rect, *graphLink); Gui::Selection().addSelection( selectionRecord.DObject->getDocument()->getName(), selectionRecord.DObject->getNameInDocument() ); } }; auto toggleSelect = [](const App::DocumentObject* dObjectIn, RectItem* rectIn) { if (rectIn->isSelected()) { Gui::Selection().rmvSelection( dObjectIn->getDocument()->getName(), dObjectIn->getNameInDocument() ); } else { Gui::Selection().addSelection( dObjectIn->getDocument()->getName(), dObjectIn->getNameInDocument() ); } }; if (proxy) { renameAcceptedSlot(); } if (event->button() == Qt::LeftButton) { RectItem* rect = getRectFromPosition(event->scenePos()); if (rect) { const GraphLinkRecord& record = findRecord(rect, *graphLink); // don't like that I am doing this again here after getRectFromPosition call. QGraphicsItem* item = itemAt(event->scenePos(), QTransform()); auto pixmapItem = dynamic_cast(item); if (pixmapItem && (pixmapItem == (*theGraph)[record.vertex].visibleIcon.get())) { // get all selections, but for now just the current pick. if ((*theGraph)[record.vertex].lastVisibleState == VisibilityState::Off) { const_cast(record.VPDObject)->show(); // const hack } else { const_cast(record.VPDObject)->hide(); // const hack } return; } const App::DocumentObject* dObject = record.DObject; if (selectionMode == SelectionMode::Single) { if (event->modifiers() & Qt::ControlModifier) { toggleSelect(dObject, rect); } else if ((event->modifiers() & Qt::ShiftModifier) && lastPickValid) { goShiftSelect(); } else { Gui::Selection().clearSelection(dObject->getDocument()->getName()); Gui::Selection().addSelection( dObject->getDocument()->getName(), dObject->getNameInDocument() ); } } if (selectionMode == SelectionMode::Multiple) { if ((event->modifiers() & Qt::ShiftModifier) && lastPickValid) { goShiftSelect(); } else { toggleSelect(dObject, rect); } } lastPickValid = true; lastPick = event->scenePos(); } else { lastPickValid = false; Gui::Selection().clearSelection(); // get document name? } } QGraphicsScene::mousePressEvent(event); } void Model::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (event->button() == Qt::LeftButton) { auto selections = getAllSelected(); if (selections.size() != 1) { return; } const GraphLinkRecord& record = findRecord(selections.front(), *graphLink); Gui::Document* doc = Gui::Application::Instance->getDocument(record.DObject->getDocument()); MDIView* view = doc->getActiveView(); if (view) { getMainWindow()->setActiveWindow(view); } const_cast(record.VPDObject)->doubleClicked(); } QGraphicsScene::mouseDoubleClickEvent(event); } std::vector Model::getAllSelected() { std::vector out; BGL_FORALL_VERTICES(currentVertex, *theGraph, Graph) { if ((*theGraph)[currentVertex].rectangle->isSelected()) { out.push_back(currentVertex); } } return out; } void Model::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { RectItem* rect = getRectFromPosition(event->scenePos()); if (rect) { const GraphLinkRecord& record = findRecord(rect, *graphLink); // don't like that I am doing this again here after getRectFromPosition call. QGraphicsItem* item = itemAt(event->scenePos(), QTransform()); auto pixmapItem = dynamic_cast(item); if (pixmapItem && (pixmapItem == (*theGraph)[record.vertex].visibleIcon.get())) { visiblyIsolate(record.vertex); return; } if (!rect->isSelected()) { Gui::Selection().clearSelection(record.DObject->getDocument()->getName()); Gui::Selection().addSelection( record.DObject->getDocument()->getName(), record.DObject->getNameInDocument() ); lastPickValid = true; lastPick = event->scenePos(); } MenuItem view; Gui::Application::Instance->setupContextMenu("Tree", &view); QMenu contextMenu; MenuManager::getInstance()->setupContextMenu(&view, contextMenu); // actions for only one selection. std::vector selections = getAllSelected(); if (selections.size() == 1) { contextMenu.addAction(renameAction); // when we have only one selection then we know it is rect from above. if (!rect->isEditing()) { const_cast(record.VPDObject) ->setupContextMenu(&contextMenu, this, SLOT(editingStartSlot())); // const hack. } else { contextMenu.addAction(editingFinishedAction); } } if (!contextMenu.actions().isEmpty()) { contextMenu.exec(event->screenPos()); } } QGraphicsScene::contextMenuEvent(event); } void Model::onRenameSlot() { assert(!proxy); std::vector selections = getAllSelected(); assert(selections.size() == 1); auto lineEdit = new LineEdit(); auto text = (*theGraph)[selections.front()].text.get(); lineEdit->setText(text->toPlainText()); connect(lineEdit, &LineEdit::acceptedSignal, this, &Model::renameAcceptedSlot); connect(lineEdit, &LineEdit::rejectedSignal, this, &Model::renameRejectedSlot); proxy = this->addWidget(lineEdit); proxy->setGeometry(text->sceneBoundingRect()); lineEdit->selectAll(); QTimer::singleShot(0, lineEdit, qOverload<>(&QLineEdit::setFocus)); } void Model::renameAcceptedSlot() { assert(proxy); std::vector selections = getAllSelected(); assert(selections.size() == 1); const GraphLinkRecord& record = findRecord(selections.front(), *graphLink); auto lineEdit = qobject_cast(proxy->widget()); assert(lineEdit); const_cast(record.DObject) ->Label.setValue(lineEdit->text().toUtf8().constData()); // const hack finishRename(); } void Model::renameRejectedSlot() { finishRename(); } void Model::finishRename() { assert(proxy); this->removeItem(proxy); proxy->deleteLater(); proxy = nullptr; this->invalidate(); } void Model::editingStartSlot() { auto action = qobject_cast(sender()); if (action) { int edit = action->data().toInt(); auto selections = getAllSelected(); assert(selections.size() == 1); const GraphLinkRecord& record = findRecord(selections.front(), *graphLink); Gui::Document* doc = Gui::Application::Instance->getDocument(record.DObject->getDocument()); MDIView* view = doc->getActiveView(); if (view) { getMainWindow()->setActiveWindow(view); } doc->setEdit(const_cast(record.VPDObject), edit); } } void Model::editingFinishedSlot() { auto selections = getAllSelected(); assert(selections.size() == 1); const GraphLinkRecord& record = findRecord(selections.front(), *graphLink); Gui::Document* doc = Gui::Application::Instance->getDocument(record.DObject->getDocument()); doc->commitCommand(); doc->resetEdit(); doc->getDocument()->recompute(); } void Model::visiblyIsolate(Gui::DAG::Vertex sourceIn) { auto buildSkipTypes = []() { std::vector out; Base::Type type; type = Base::Type::fromName("App::DocumentObjectGroup"); if (!type.isBad()) { out.push_back(type); } type = Base::Type::fromName("App::Part"); if (!type.isBad()) { out.push_back(type); } type = Base::Type::fromName("PartDesign::Body"); if (!type.isBad()) { out.push_back(type); } return out; }; auto testSkipType = [](const App::DocumentObject* dObject, const std::vector& types) { for (const auto& currentType : types) { if (dObject->isDerivedFrom(currentType)) { return true; } } return false; }; indexVerticesEdges(); Path connectedVertices; ConnectionVisitor visitor(connectedVertices); boost::breadth_first_search(*theGraph, sourceIn, boost::visitor(visitor)); boost::breadth_first_search(boost::make_reverse_graph(*theGraph), sourceIn, boost::visitor(visitor)); // note source vertex is added twice to Path. Once for each search. static std::vector skipTypes = buildSkipTypes(); for (const auto& currentVertex : connectedVertices) { const GraphLinkRecord& record = findRecord(currentVertex, *graphLink); if (testSkipType(record.DObject, skipTypes)) { continue; } const_cast(record.VPDObject)->hide(); // const hack } const GraphLinkRecord& sourceRecord = findRecord(sourceIn, *graphLink); if (!testSkipType(sourceRecord.DObject, skipTypes)) { const_cast(sourceRecord.VPDObject)->show(); // const hack } } #include