18#include <config-kdsme.h>
34#include <graphviz/graph.h>
41#include <QPainterPath>
50using namespace ObjectHelper;
55const char *DEFAULT_LAYOUT_TOOL =
"dot";
63const qreal DOT_DEFAULT_DPI = 72.0;
64const qreal DISPLAY_DPI = 96.0;
65const qreal TO_DOT_DPI_RATIO = DISPLAY_DPI / DOT_DEFAULT_DPI;
67QVector<QPair<const char *, const char *>> attributesForState(
const State *state)
69 typedef QPair<const char *, const char *> EntryType;
70 typedef QVector<EntryType> ResultType;
72 if (
const auto *pseudoState = qobject_cast<const PseudoState *>(state)) {
73 switch (pseudoState->kind()) {
76 << EntryType(
"label",
"")
77 << EntryType(
"shape",
"circle")
78 << EntryType(
"fixedsize",
"true")
79 << EntryType(
"height",
"0.20")
80 << EntryType(
"width",
"0.20");
86 if (state->
type() == State::HistoryStateType) {
88 << EntryType(
"label",
"H*")
89 << EntryType(
"shape",
"circle")
90 << EntryType(
"fixedsize",
"true");
93 if (state->
type() == State::FinalStateType) {
95 << EntryType(
"shape",
"doublecircle")
96 << EntryType(
"label",
"")
97 << EntryType(
"style",
"filled")
98 << EntryType(
"fillcolor",
"black")
99 << EntryType(
"fixedsize",
"true")
100 << EntryType(
"height",
"0.15")
101 << EntryType(
"width",
"0.15");
106 << EntryType(
"shape",
"rectangle")
107 << EntryType(
"style",
"rounded");
110bool isAncestorCollapsed(
const State *current)
114 if (parent && !parent->isExpanded())
124struct GraphvizLayouterBackend::Private
129 void buildState(
State *state, Agraph_t *graph);
130 void buildTransitions(
const State *state, Agraph_t *graph);
131 void buildTransition(
Transition *transition, Agraph_t *graph);
134 void importItem(
Element *item,
void *obj)
const;
135 void importState(
State *state, Agnode_t *node)
const;
136 void importState(
State *state, Agraph_t *graph)
const;
137 void importTransition(
Transition *transition, Agedge_t *edge)
const;
140 void openContext(
const QString &
id);
147 QRectF boundingRectForGraph(Agraph_t *graph)
const;
151 QRectF labelRectForEdge(Agedge_t *edge)
const;
155 QPainterPath pathForEdge(Agedge_t *edge)
const;
157 inline Agnode_t *agnodeForState(
State *state)
const;
160 Agraph_t *m_graph =
nullptr;
162 GVC_t *m_context =
nullptr;
168 QPointer<State> m_root;
169 QHash<Element *, Agnode_t *> m_elementToDummyNodeMap;
170 QHash<Element *, void *> m_elementToPointerMap;
173GraphvizLayouterBackend::Private::Private()
181GraphvizLayouterBackend::Private::~Private()
185void GraphvizLayouterBackend::Private::buildState(
State *state, Agraph_t *graph)
190 IF_DEBUG(qCDebug(KDSME_CORE) << state->
label() << *state << graph);
195 if (m_layoutMode == RecursiveMode && !state->
childStates().isEmpty()) {
197 Agraph_t *newGraph =
_agsubg(graph, graphName);
199 m_elementToPointerMap[state] = newGraph;
200 _agset(newGraph, QStringLiteral(
"label"), state->
label().isEmpty() ? QObject::tr(
"<unnamed>") : state->label() + u
" ###");
202 auto dummyNode =
_agnode(newGraph, u
"dummynode_" + graphName);
203 _agset(dummyNode, QStringLiteral(
"shape"), QStringLiteral(
"point"));
204 _agset(dummyNode, QStringLiteral(
"style"), QStringLiteral(
"invis"));
205 m_elementToDummyNodeMap[state] = dummyNode;
207 if (!isAncestorCollapsed(state)) {
209 for (
State *childState : childStates) {
210 buildState(childState, newGraph);
214 if (m_layoutMode == RecursiveMode && isAncestorCollapsed(state)) {
219 m_elementToPointerMap[state] = newNode;
221 if (!qIsNull(state->
width()) && !qIsNull(state->
height())) {
222 _agset(newNode, QStringLiteral(
"width"), QString::number(state->
width() / DISPLAY_DPI));
223 _agset(newNode, QStringLiteral(
"height"), QString::number(state->
height() / DISPLAY_DPI));
224 _agset(newNode, QStringLiteral(
"fixedsize"), QStringLiteral(
"true"));
226 if (!state->
label().isEmpty()) {
227 _agset(newNode, QStringLiteral(
"label"), state->
label());
230 const auto attrs = attributesForState(qobject_cast<State *>(state));
231 for (
const auto &kv : attrs) {
232 _agset(newNode, QString::fromLatin1(kv.first), QString::fromLatin1(kv.second));
237void GraphvizLayouterBackend::Private::buildTransitions(
const State *state, Agraph_t *graph)
239 IF_DEBUG(qCDebug(KDSME_CORE) << state->
label() << *state << graph);
241 const auto stateTransitions = state->
transitions();
242 for (
Transition *transition : stateTransitions) {
243 buildTransition(transition, graph);
246 if (m_layoutMode == RecursiveMode) {
248 for (
const State *childState : childStates) {
249 buildTransitions(childState, graph);
254void GraphvizLayouterBackend::Private::buildTransition(
Transition *transition, Agraph_t *graph)
260 if (m_layoutMode == RecursiveMode && isAncestorCollapsed(transition->
sourceState())) {
264 IF_DEBUG(qCDebug(KDSME_CORE) << transition->
label() << *transition << graph);
266 const auto sourceState = transition->
sourceState();
267 const auto targetState = transition->
targetState();
269 Agnode_t *source = agnodeForState(sourceState);
272 Agnode_t *target = agnodeForState(targetState);
276 auto sourceDummyNode = m_elementToDummyNodeMap.value(sourceState);
277 auto targetDummyNode = m_elementToDummyNodeMap.value(targetState);
279 Agedge_t *edge =
_agedge(graph,
280 sourceDummyNode ? sourceDummyNode : source,
281 targetDummyNode ? targetDummyNode : target,
283 if (!transition->
label().isEmpty() && m_properties->showTransitionLabels()) {
284 _agset(edge, QStringLiteral(
"label"), transition->
label());
289 if (sourceDummyNode) {
291 _agset(edge, QStringLiteral(
"ltail"), graphName);
293 if (targetDummyNode) {
295 _agset(edge, QStringLiteral(
"lhead"), graphName);
297 m_elementToPointerMap[transition] = edge;
301void GraphvizLayouterBackend::Private::import()
303 IF_DEBUG(qCDebug(KDSME_CORE) << m_elementToPointerMap.keys();)
307 walker.walkItems(m_root, [
this](
Element *element) {
308 if (
auto obj = m_elementToPointerMap.value(element)) {
309 importItem(element, obj);
315void GraphvizLayouterBackend::Private::importItem(
Element *item,
void *obj)
const
317 if (
auto *state = qobject_cast<State *>(item)) {
318 if (m_layoutMode == RecursiveMode && !state->
childStates().isEmpty()) {
319 importState(state,
static_cast<Agraph_t *
>(obj));
321 importState(state,
static_cast<Agnode_t *
>(obj));
323 }
else if (
auto *transition = qobject_cast<Transition *>(item)) {
324 importTransition(transition,
static_cast<Agedge_t *
>(obj));
328void GraphvizLayouterBackend::Private::importState(
State *state, Agnode_t *node)
const
333 IF_DEBUG(qCDebug(KDSME_CORE) <<
"before" << state->
label() << *state << node);
337 const qreal x = ND_coord(node).x * TO_DOT_DPI_RATIO;
340 const qreal y = (GD_bb(m_graph).UR.y - ND_coord(node).y) * TO_DOT_DPI_RATIO;
344 state->
setWidth(ND_width(node) * DISPLAY_DPI);
345 state->
setHeight(ND_height(node) * DISPLAY_DPI);
348 const QPointF absolutePos = QPointF(x - state->
width() / 2, y - state->
height() / 2);
349 if (m_layoutMode == RecursiveMode) {
351 state->
setPos(relativePos);
353 state->
setPos(absolutePos);
356 IF_DEBUG(qCDebug(KDSME_CORE) <<
"after" << state->
label() << *state << node);
359void GraphvizLayouterBackend::Private::importState(
State *state, Agraph_t *graph)
const
364 IF_DEBUG(qCDebug(KDSME_CORE) <<
"before" << state->
label() << *state << graph);
366 const QRectF rect = boundingRectForGraph(graph);
371 const QPointF absolutePos = rect.topLeft();
372 if (m_layoutMode == RecursiveMode) {
374 state->
setPos(relativePos);
376 state->
setPos(absolutePos);
379 IF_DEBUG(qCDebug(KDSME_CORE) <<
"after" << state->
label() << *state << graph);
382void GraphvizLayouterBackend::Private::importTransition(
Transition *transition, Agedge_t *edge)
const
384 Q_ASSERT(transition);
387 IF_DEBUG(qCDebug(KDSME_CORE) <<
"before" << transition << edge);
389 const QPainterPath path = pathForEdge(edge);
390 const QRectF labelRect = labelRectForEdge(edge);
391 const QRectF
boundingRect = labelRect.united(path.boundingRect());
394 if (m_layoutMode == RecursiveMode) {
396 transition->
setPos(relativePos);
400 transition->
setShape(path.translated(-absolutePos));
402 IF_DEBUG(qCDebug(KDSME_CORE) <<
"after" << transition << edge);
406#if KDSME_STATIC_GRAPHVIZ
407GVC_t *gvContextWithStaticPlugins();
411void GraphvizLayouterBackend::Private::openContext(
const QString &
id)
415 m_elementToDummyNodeMap.clear();
416 m_elementToPointerMap.clear();
419 m_graph =
_agopen(
id, Agdirected, &AgDefaultDisc);
421 m_graph =
_agopen(
id, AGDIGRAPH);
425 if (m_layoutMode == RecursiveMode) {
426 _agset(m_graph, QStringLiteral(
"compound"), QStringLiteral(
"true"));
428 _agset(m_graph, QStringLiteral(
"overlap"), QStringLiteral(
"prism"));
429 _agset(m_graph, QStringLiteral(
"overlap_shrink"), QStringLiteral(
"true"));
430 _agset(m_graph, QStringLiteral(
"splines"), QStringLiteral(
"true"));
431 _agset(m_graph, QStringLiteral(
"pad"), QStringLiteral(
"0.0"));
432 _agset(m_graph, QStringLiteral(
"dpi"), QStringLiteral(
"96.0"));
433 _agset(m_graph, QStringLiteral(
"nodesep"), QStringLiteral(
"0.2"));
436void GraphvizLayouterBackend::Private::closeLayout()
449 m_properties =
nullptr;
454QRectF GraphvizLayouterBackend::Private::boundingRectForGraph(Agraph_t *graph)
const
457 const qreal left = GD_bb(graph).LL.x * TO_DOT_DPI_RATIO;
458 const qreal top = (GD_bb(m_graph).UR.y - GD_bb(graph).LL.y) * TO_DOT_DPI_RATIO;
459 const qreal right = GD_bb(graph).UR.x * TO_DOT_DPI_RATIO;
460 const qreal bottom = (GD_bb(m_graph).UR.y - GD_bb(graph).UR.y) * TO_DOT_DPI_RATIO;
462 return QRectF(left, top, right - left, bottom - top).normalized();
465QRectF GraphvizLayouterBackend::Private::labelRectForEdge(Agedge_t *edge)
const
473 const double posx = ED_label(edge)->pos.x;
474 const double posy = ED_label(edge)->pos.y;
475 const QRectF labelBoundingRect = QRectF(
476 (posx - ED_label(edge)->dimen.x / 2.0) * TO_DOT_DPI_RATIO,
477 ((GD_bb(m_graph).UR.y - posy) - ED_label(edge)->dimen.y / 2.0) * TO_DOT_DPI_RATIO,
478 ED_label(edge)->dimen.x * TO_DOT_DPI_RATIO,
479 ED_label(edge)->dimen.y * TO_DOT_DPI_RATIO);
481 return labelBoundingRect;
484QPainterPath GraphvizLayouterBackend::Private::pathForEdge(Agedge_t *edge)
const
491 if (ED_spl(edge) && (ED_spl(edge)->list !=
nullptr) && (ED_spl(edge)->list->size % 3 == 1)) {
493 if (ED_spl(edge)->list->sflag) {
494 path.moveTo(ED_spl(edge)->list->sp.x * TO_DOT_DPI_RATIO,
495 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->sp.y) * TO_DOT_DPI_RATIO);
496 path.lineTo(ED_spl(edge)->list->list[0].x * TO_DOT_DPI_RATIO,
497 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->list[0].y) * TO_DOT_DPI_RATIO);
499 path.moveTo(ED_spl(edge)->list->list[0].x * TO_DOT_DPI_RATIO,
500 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->list[0].y) * TO_DOT_DPI_RATIO);
504 for (
size_t i = 1; i < ED_spl(edge)->list->size; i += 3) {
505 path.cubicTo(ED_spl(edge)->list->list[i].x * TO_DOT_DPI_RATIO,
506 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->list[i].y) * TO_DOT_DPI_RATIO,
507 ED_spl(edge)->list->list[i + 1].x * TO_DOT_DPI_RATIO,
508 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->list[i + 1].y) * TO_DOT_DPI_RATIO,
509 ED_spl(edge)->list->list[i + 2].x * TO_DOT_DPI_RATIO,
510 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->list[i + 2].y) * TO_DOT_DPI_RATIO);
514 if (ED_spl(edge)->list->eflag) {
515 path.lineTo(ED_spl(edge)->list->ep.x * TO_DOT_DPI_RATIO,
516 (GD_bb(m_graph).UR.y - ED_spl(edge)->list->ep.y) * TO_DOT_DPI_RATIO);
524Agnode_t *GraphvizLayouterBackend::Private::agnodeForState(
State *state)
const
527 return static_cast<Agnode_t *
>(m_elementToPointerMap.value(state));
530#if !KDSME_STATIC_GRAPHVIZ && !defined(Q_OS_WINDOWS)
546#if KDSME_STATIC_GRAPHVIZ
547 d->m_context = gvContextWithStaticPlugins();
548#elif !defined(Q_OS_WINDOWS)
551 d->m_context = gvContext();
553 Q_ASSERT(d->m_context);
561 Q_ASSERT(d->m_context);
562 gvFreeContext(d->m_context);
563 d->m_context =
nullptr;
570 return d->m_layoutMode;
575 d->m_layoutMode = mode;
581 _gvLayout(d->m_context, d->m_graph, DEFAULT_LAYOUT_TOOL);
583 if (qEnvironmentVariableIsSet(
"KDSME_DEBUG_GRAPHVIZ")) {
584 const auto state = d->m_root;
585 const auto machine = state->
machine();
589 const QDir tmpDir = QDir::temp();
590 tmpDir.mkdir(QStringLiteral(
"kdsme_debug"));
591 const QString baseName = QStringLiteral(
"%1/%2_%3").arg(tmpDir.filePath(QStringLiteral(
"kdsme_debug"))).arg(machineName).arg(stateName);
593 saveToFile(baseName + u
".dot", QStringLiteral(
"dot"));
600 qCDebug(KDSME_CORE) <<
"Cannot render image, context not open:" << filePath;
605 QFile file(filePath);
606 if (file.open(QIODevice::WriteOnly)) {
607 const int rc = gvRenderFilename(d->m_context, d->m_graph, qPrintable(format), qPrintable(filePath));
609 qCDebug(KDSME_CORE) <<
"gvRenderFilename to" << filePath <<
"failed with return-code:" << rc;
612 qCDebug(KDSME_CORE) <<
"Cannot render image, cannot open:" << filePath;
620 d->m_properties = properties;
622 d->openContext(QStringLiteral(
"GraphvizLayouterBackend@%1").arg(
addressToString(
this)));
632 d->buildState(state, d->m_graph);
637 d->buildTransitions(state, d->m_graph);
642 d->buildTransition(transition, d->m_graph);
655 return d->boundingRectForGraph(d->m_graph);
void openLayout(KDSME::State *state, const KDSME::LayoutProperties *properties)
LayoutMode layoutMode() const
~GraphvizLayouterBackend()
void buildTransitions(const KDSME::State *state)
void buildState(KDSME::State *state)
void setLayoutMode(LayoutMode mode)
void buildTransition(KDSME::Transition *transition)
QRectF boundingRect() const
void saveToFile(const QString &filePath, const QString &format=QStringLiteral("png"))
GraphvizLayouterBackend()
@ RecursiveMode
Performs a recursive import of all state machine elements,.
void setPos(const QPointF &pos)
QPointF absolutePos() const
QPointF pos
The position of the element from the top-left corner.
void setWidth(qreal width)
void setHeight(qreal height)
Element * parentElement() const
QList< Transition * > transitions() const
QList< State * > childStates() const
Q_INVOKABLE KDSME::StateMachine * machine() const
Q_INVOKABLE KDSME::State * parentState() const
Type type() const override
KDSME::State * sourceState
KDSME::State * targetState
void setShape(const QPainterPath &shape)
void setLabelBoundingRect(const QRectF &rect)
@ RecursiveWalk
Traverse the children of this item.
lt_symlist_t lt_preloaded_symbols[]
gvplugin_library_t gvplugin_dot_layout_LTX_library
QRectF boundingRect(QSGGeometry *geometry)
int _agset(void *object, const QString &attr, const QString &value)
Directly use agsafeset which always works, contrarily to agset.
int _gvLayout(GVC_t *gvc, graph_t *g, const char *engine)
Agraph_t * _agopen(const QString &name, int kind)
The agopen method for opening a graph.
Agnode_t * _agnode(Agraph_t *graph, const QString &attr, bool create=true)
Agraph_t * _agsubg(Agraph_t *graph, const QString &attr, bool create=true)
Agedge_t * _agedge(Agraph_t *graph, Agnode_t *tail, Agnode_t *head, const QString &name=QString(), bool create=true)
KDSME_CORE_EXPORT QString addressToString(const void *p)