FreeCAD / src /Gui /DAGView /DAGModel.cpp
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2015 Thomas Anderson <blobfish[at]gmx.com> *
* *
* 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 <boost/graph/topological_sort.hpp>
#include <boost_graph_reverse_graph.hpp>
#include <memory>
#include <QBrush>
#include <QColor>
#include <QGraphicsPixmapItem>
#include <QGraphicsProxyWidget>
#include <QGraphicsSceneContextMenuEvent>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QMenu>
#include <QPainter>
#include <QPen>
#include <QString>
#include <QTimer>
#include <Base/Console.h>
#include <Base/TimeInfo.h>
#include <App/Document.h>
#include <Gui/Application.h>
#include <Gui/BitmapFactory.h>
#include <Gui/Document.h>
#include <Gui/MainWindow.h>
#include <Gui/MenuManager.h>
#include <Gui/Selection/Selection.h>
#include <Gui/ViewProviderDocumentObject.h>
#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<Graph>();
graphLink = std::make_shared<GraphLinkContainer>();
setupViewConstants();
graphDirty = false;
currentPrehighlight = nullptr;
ParameterGrp::handle group = App::GetApplication()
.GetUserParameter()
.GetGroup("BaseApp")
->GetGroup("Preferences")
->GetGroup("DAGView");
selectionMode = static_cast<SelectionMode>(group->GetInt("SelectionMode", 0));
group->SetInt("SelectionMode", static_cast<int>(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<Gui::ViewProviderDocumentObject*>(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<Gui::ViewProviderDocumentObject&>(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<QGraphicsPixmapItem> 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<App::PropertyLinkBase>()) {
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<Edge> 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<App::DocumentObject*> 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<QGraphicsPathItem>();
(*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<int>(
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<int>(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<qreal>(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<int>(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<int>(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<Graph>(*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<RectItem*>(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<QGraphicsItem*> selection = collidingItems(&intersectionLine);
for (auto currentItem : selection) {
auto rect = dynamic_cast<RectItem*>(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<QGraphicsPixmapItem*>(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<ViewProviderDocumentObject*>(record.VPDObject)->show(); // const hack
}
else {
const_cast<ViewProviderDocumentObject*>(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<ViewProviderDocumentObject*>(record.VPDObject)->doubleClicked();
}
QGraphicsScene::mouseDoubleClickEvent(event);
}
std::vector<Gui::DAG::Vertex> Model::getAllSelected()
{
std::vector<Gui::DAG::Vertex> 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<QGraphicsPixmapItem*>(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<Gui::DAG::Vertex> 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<Gui::ViewProviderDocumentObject*>(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<Gui::DAG::Vertex> 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<Gui::DAG::Vertex> selections = getAllSelected();
assert(selections.size() == 1);
const GraphLinkRecord& record = findRecord(selections.front(), *graphLink);
auto lineEdit = qobject_cast<LineEdit*>(proxy->widget());
assert(lineEdit);
const_cast<App::DocumentObject*>(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<QAction*>(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<ViewProviderDocumentObject*>(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<Base::Type> 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<Base::Type>& 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<Base::Type> skipTypes = buildSkipTypes();
for (const auto& currentVertex : connectedVertices) {
const GraphLinkRecord& record = findRecord(currentVertex, *graphLink);
if (testSkipType(record.DObject, skipTypes)) {
continue;
}
const_cast<ViewProviderDocumentObject*>(record.VPDObject)->hide(); // const hack
}
const GraphLinkRecord& sourceRecord = findRecord(sourceIn, *graphLink);
if (!testSkipType(sourceRecord.DObject, skipTypes)) {
const_cast<ViewProviderDocumentObject*>(sourceRecord.VPDObject)->show(); // const hack
}
}
#include <moc_DAGModel.cpp>