1
/***************************************************************************
2
* This program is free software; you can redistribute it and/or modify *
3
* it under the terms of the GNU General Public License as published by *
4
* the Free Software Foundation; either version 2 of the License, or *
5
* (at your option) any later version. *
8
* Umbrello UML Modeller Authors <uml-devel@uml.sf.net> *
9
***************************************************************************/
11
#ifndef LAYOUTGENERATOR_H
13
#include "associationwidget.h"
14
#include "debug_utils.h"
15
#include "dotgenerator.h"
16
#include "floatingtextwidget.h"
17
#include "umlwidget.h"
20
#include <KStandardDirs>
29
#include <QTemporaryFile>
30
#include <QTextStream>
33
#include <KConfigGroup>
34
#include <KDesktopFile>
36
#define LAYOUTGENERATOR_DEBUG
37
//#define LAYOUTGENERATOR_DATA_DEBUG
39
#ifdef LAYOUTGENERATOR_DEBUG
41
static QString pngViewer()
54
static QString textViewer()
69
* The class LayoutGenerator provides calculated layouts of diagrams.
71
* It uses the dot executable from the graphviz package for calculation
72
* of widget positions.
74
* The implementation calls dot with information from the displayed
75
* widgets and associations by creating a temporary dot file based on a
76
* layout configure file, which is located in the umbrello/layouts subdir of
77
* the "data" resource type. The config file is determined from the
78
* type of the currently displayed diagram and the layout chosen by the user.
80
* Dot creates a file containing the calculated widget positions.
81
* The widget positions are retrieved from this file and used to move
82
* widgets on the provided diagram. Additional points in association lines
85
* @author Ralf Habacker <ralf.habacker@freenet.de>
87
class LayoutGenerator : public DotGenerator
90
typedef QHash<QString,QRectF> NodeType;
91
typedef QList<QPointF> EdgePoints;
92
typedef QHash<QString,EdgePoints> EdgeType;
99
setUseFullNodeLabels(false);
103
* Return state if layout generator is enabled.
104
* It is enabled when the dot application has been found.
106
* @return true if enabled
110
if (m_executable.isEmpty())
111
m_executable = KStandardDirs::findExe("dot");
112
return !m_executable.isEmpty();
116
* generate layout and apply it to the given diagram.
118
* @return true if generating succeded
120
bool generate(UMLScene *scene, const QString &variant = QString())
124
QTemporaryFile xdotOut;
126
uWarning() << "Could not apply autolayout because graphviz installation has not been found.";
130
#ifdef LAYOUTGENERATOR_DEBUG
131
in.setAutoRemove(false);
132
out.setAutoRemove(false);
133
xdotOut.setAutoRemove(false);
136
// generate filenames
144
#ifdef LAYOUTGENERATOR_DEBUG
145
qDebug() << textViewer() << in.fileName();
146
qDebug() << textViewer() << out.fileName();
147
qDebug() << textViewer() << xdotOut.fileName();
150
if (!createDotFile(scene, in.fileName(), variant))
154
if (!m_generator.isEmpty()) {
155
QFileInfo fi(m_executable);
156
QString path = fi.absolutePath();
157
executable = path + "/" + m_generator;
160
executable = m_executable;
164
args << "-o" << out.fileName() << "-Tplain-ext" << in.fileName();
165
p.start(executable, args);
169
args << "-o" << xdotOut.fileName() << "-Txdot" << in.fileName();
170
p.start(executable, args);
173
#ifdef LAYOUTGENERATOR_DEBUG
174
QTemporaryFile pngFile;
175
pngFile.setAutoRemove(false);
176
pngFile.setFileTemplate(QDir::tempPath() + "/umbrello-layoutgenerator-XXXXXX.png");
179
qDebug() << pngViewer() << pngFile.fileName();
181
args << "-o" << pngFile.fileName() << "-Tpng" << in.fileName();
182
p.start(executable, args);
186
if (!readGeneratedDotFile(out.fileName()))
188
if (!readGeneratedDotFile(xdotOut.fileName()))
196
* apply auto layout to the given scene
198
* @return true if autolayout has been applied
200
bool apply(UMLScene *scene)
202
foreach(AssociationWidget *assoc, scene->associationList()) {
203
AssociationLine *path = assoc->associationLine();
204
QString type = assoc->associationType().toString().toLower();
205
QString key = "type::" + type;
208
if (m_edgeParameters.contains("id::" + key) && m_edgeParameters["id::" + key] == "swap")
209
id = fixID(ID2STR(assoc->widgetIDForRole(Uml::A)) + ID2STR(assoc->widgetIDForRole(Uml::B)));
211
id = fixID(ID2STR(assoc->widgetIDForRole(Uml::B)) + ID2STR(assoc->widgetIDForRole(Uml::A)));
213
// adjust associations not used in the dot file
214
if (!m_edges.contains(id)) {
216
AssociationLine *path = assoc->associationLine();
217
if (path->count() > 2 && assoc->widgetIDForRole(Uml::A) != assoc->widgetIDForRole(Uml::B)) {
218
while(path->count() > 2)
219
path->removePoint(1);
224
EdgePoints &p = m_edges[id];
227
while(path->count() > 1) {
228
path->removePoint(0);
230
path->setEndPoints(QPoint(p[0].x() + m_origin.x(), m_boundingRect.height() - p[0].y() + m_origin.y()), QPoint(p[len-1].x() + m_origin.x(), m_boundingRect.height() - p[len-1].y() + m_origin.y()));
232
// set label position
233
QPointF &l = m_edgeLabelPosition[id];
234
FloatingTextWidget *tw = assoc->nameWidget();
236
tw->setX((int)(l.x() + m_origin.x()));
237
tw->setY(int(m_boundingRect.height() - l.y() + m_origin.y()));
239
// FIXME: set remaining association line points
241
for(int i = 1; i < len-1; i++) {
242
path->insertPoint(i, QPoint(p[i].x()+ m_origin.x(), m_boundingRect.height() - p[i].y() + m_origin.y()));
246
* here stuff could be added to add more points from informations returned by dot.
250
foreach(UMLWidget *widget, scene->widgetList()) {
251
QString id = ID2STR(widget->id());
252
if (!m_nodes.contains(id))
254
QPoint p = origin(id);
256
widget->setY(p.y()-widget->height());
257
widget->adjustAssocs(widget->x(), widget->y()); // adjust assoc lines
260
foreach(AssociationWidget *assoc, scene->associationList()) {
261
assoc->calculateEndingPoints();
262
if (assoc->associationLine())
263
assoc->associationLine()->update();
264
assoc->resetTextPositions();
270
* Return a list of available templates for a given scene type
272
* @param scene The diagram
273
* @param configFiles will contain the collected list of config files
274
* @return true if collecting succeeds
276
static bool availableConfigFiles(UMLScene *scene, QHash<QString,QString> &configFiles)
278
QString diagramType = scene->type().toString().toLower();
281
QStringList fileNames = dirs.findAllResources("data", QString("umbrello/layouts/%1*.desktop").arg(diagramType));
282
foreach(const QString &fileName, fileNames) {
283
QFileInfo fi(fileName);
285
if (fi.baseName().contains("-"))
286
baseName = fi.baseName().remove(diagramType + "-");
287
else if (fi.baseName() == diagramType)
288
baseName = fi.baseName();
290
baseName = "default";
291
KDesktopFile desktopFile(fileName);
292
configFiles[baseName] = desktopFile.readName();
299
* Return the origin of node based on the bottom/left corner
301
* @param id The widget id to fetch the origin from
302
* @return QPoint instance with the coordinates
304
QPoint origin(const QString &id)
306
QString key = fixID(id);
307
if (!m_nodes.contains(key)) {
308
#ifdef LAYOUTGENERATOR_DATA_DEBUG
313
QRectF &r = m_nodes[key];
314
QPoint p(m_origin.x() + r.x() - r.width()/2, m_boundingRect.height() - r.y() + r.height()/2 + m_origin.y());
315
#ifdef LAYOUTGENERATOR_DATA_DEBUG
322
* Read generated dot file and extract positions
323
* of the contained widgets.
325
* @return true if extracting succeeded
327
bool readGeneratedDotFile(const QString &fileName)
329
QFile file(fileName);
330
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
333
QTextStream in(&file);
334
while (!in.atEnd()) {
335
QString line = in.readLine();
336
while(line.endsWith(','))
337
line += in.readLine();
345
* Parse line from dot generated plain-ext output format
347
* The format is documented at http://graphviz.org/content/output-formats#dplain-ext and looks like:
349
* graph 1 28.083 10.222
350
* node ITfDmJvJE00m 8.0833 8.7361 0.86111 0.45833 QObject solid box black lightgrey
351
* edge sL4cKPpHnJkU sL4cKPpHnJkU 7 8.1253 7.2568 8.2695 7.2687 8.375 7.3127 8.375 7.3889 8.375 7.4377 8.3317 7.4733 8.2627 7.4957 Aggregation 8.8472 7.3889 solid black
353
* @param line line in dot plain-ext output format
354
* @return true if line could be parsed successfully
356
bool parseLine(const QString &line)
358
QStringList a = line.split(' ');
359
if (a[0] == "graph") {
360
m_boundingRect = QRectF(0, 0, a[2].toDouble()*m_scale, a[3].toDouble()*m_scale);
362
} else if (a[0] == "node") {
363
QString key = fixID(a[1]);
364
m_nodes[key] = QRectF(a[2].toDouble()*m_scale, a[3].toDouble()*m_scale, a[4].toDouble()*m_scale, a[5].toDouble()*m_scale);
366
} else if (a[0] == "edge") {
367
QString key = fixID(a[1]+a[2]);
369
int len = a[3].toInt();
370
for(int i = 0; i < len; i++)
371
p.append(QPointF(a[i*2+4].toDouble()*m_scale, a[i*2+5].toDouble()*m_scale));
376
double x = a[b+1].toDouble(&ok);
379
double y = a[b+2].toDouble(&ok);
382
m_edgeLabelPosition[key] = QPointF(x*m_scale, y*m_scale);
385
} else if (a[0] == "stop") {
392
typedef QMap<QString,QStringList> ParameterList;
394
bool splitParameters(QMap<QString,QStringList> &map, const QString &s)
396
// FIXME: add shape=box without '"'
397
static QRegExp rx("([a-zA-Z_]+)=\"([a-zA-Z0-9.- #]+)\"");
398
static QRegExp rx2("([a-zA-Z_]+)=([a-zA-Z0-9.- #]+)");
402
* while ((pos = rx2.indexIn(s, pos)) != -1) {
403
* QString key = rx2.cap(1);
404
* QString value = rx2.cap(2);
406
* pos += rx2.matchedLength();
407
* //qDebug() << key << value;
408
* if (map.contains(key))
411
* map[key] = QStringList() << value;
415
while ((pos = rx.indexIn(s, pos)) != -1) {
416
QString key = rx.cap(1);
417
QString value = rx.cap(2);
419
pos += rx.matchedLength();
420
//qDebug() << key << value;
425
data = value.split(' ');
426
} else if (key.startsWith('_')) {
427
data = value.split(' ');
429
else if (key == "label")
430
data = QStringList() << value;
432
data = value.split(',');
434
if (map.contains(key))
445
graph [splines=polyline, rankdir=BT, outputorder=nodesfirst, ranksep="0.5", nodesep="0.5"];
447
graph [bb="0,0,2893,638",
448
_draw_="c 9 -#ffffffff C 9 -#ffffffff P 4 0 -1 0 638 2894 638 2894 -1 ",
450
XC0weWhArzOJ [label=note, shape=box, width="2.5833", height="0.86111", pos="93,31", _draw_="c 9 -#000000ff p 4 186 62 0 62 0 0 186 0 ", _ldraw_="F 14.000000 11 -Times-Roman c 9 -#000000ff T 93 27 0 24 4 -note "];
451
sL4cKPpHnJkU -> ITfDmJvJE00m [arrowhead=normal, weight="1.0", label=" ", pos="e,2326.3,600.47 2299.7,543.57 2306.1,557.22 2314.9,575.99 2322.1,591.39", lp="2319,572", _draw_="c 9 -#000000ff B 4 2300 544 2306 557 2315 576 2322 591 ", _hdraw_="S 5 -solid c 9 -#000000ff C 9 -#000000ff P 3 2319 593 2326 600 2325 590 ", _ldraw_="F 14.000000 11 -Times-Roman c 9 -#000000ff T 2319 568 0 4 1 - "];
452
sL4cKPpHnJkU -> sL4cKPpHnJkU [label=" ", arrowtail=odiamond, dir=back, constraint=false, pos="s,2339.3,516.43 2351.5,516.59 2365.1,517.35 2375,520.16 2375,525 2375,531.2 2358.7,534.06 2339.3,533.57", lp="2377,525", _draw_="c 9 -#000000ff B 7 2351 517 2365 517 2375 520 2375 525 2375 531 2359 534 2339 534 ", _tdraw_="S 5 -solid c 9 -#000000ff p 4 2351 517 2345 521 2339 516 2345 513 ", _ldraw_="F 14.000000 11 -Times-Roman c 9 -#000000ff T 2377 521 0 4 1 - "];
455
bool parseLine(const QString &line)
457
static QRegExp m_cols("^[\t ]*(.*)[\t ]*\\[(.*)\\]");
458
static int m_level = -1;
460
if (line.contains('{')) {
464
else if (line.contains('}')) {
469
if (m_cols.indexIn(line, pos) == -1)
472
QString keyword = m_cols.cap(1).trimmed();
473
QString attributeString = m_cols.cap(2);
474
uDebug() << keyword << attributeString;
475
ParameterList attributes;
476
splitParameters(attributes, attributeString);
477
uDebug() << attributes;
479
if (keyword == "graph") {
480
if (attributes.contains("bb")) {
481
QStringList &a = attributes["bb"];
482
m_boundingRect.setLeft(a[0].toDouble());
483
m_boundingRect.setTop(a[1].toDouble());
484
m_boundingRect.setRight(a[2].toDouble());
485
m_boundingRect.setBottom(a[3].toDouble());
487
} else if (keyword == "node") {
489
} else if (keyword == "edge") {
492
} else if (line.contains("->")) {
493
QStringList k = keyword.split(" ");
496
QString key = fixID(k[0]+k[2]);
498
if (attributes.contains("pos")) {
499
QStringList &a = attributes["pos"];
502
for(int i = 1; i < a.size(); i++) {
503
QStringList b = a[i].split(',');
504
QPointF p(b[0].toDouble(), b[1].toDouble());
507
QStringList b = a[0].split(',');
508
QPointF p(b[0].toDouble(), b[1].toDouble());
511
m_edges[key] = points;
513
if (0 && attributes.contains("_draw_")) {
514
QStringList &a = attributes["_draw_"];
515
if (a.size() < 5 || (a[3] != "L" && a[3] != "p"))
517
int size = a[4].toInt();
520
for(int i = 0; i < size; i++) {
521
QPointF p(a[i*2+5].toDouble(), a[i*2+6].toDouble());
524
m_edges[key] = points;
530
QRectF f(0, 0, 0, 0);
531
QString id = fixID(keyword);
532
if (attributes.contains("pos")) {
533
QStringList &a = attributes["pos"];
534
QStringList b = a[0].split(",");
535
f.setLeft(b[0].toDouble());
536
f.setTop(b[1].toDouble());
538
if (attributes.contains("height")) {
539
QStringList &a = attributes["height"];
540
f.setHeight(a[0].toDouble()*scale);
543
if (attributes.contains("width")) {
544
QStringList &a = attributes["width"];
545
f.setWidth(a[0].toDouble()*scale);
547
uDebug() << "adding" << id << f;
554
QRectF m_boundingRect;
555
NodeType m_nodes; ///< list of nodes found in parsed dot file
556
EdgeType m_edges; ///< list of edges found in parsed dot file
557
QHash<QString, QPointF> m_edgeLabelPosition; ///< contains global node parameters
559
friend QDebug operator<<(QDebug out, LayoutGenerator &c);
563
static QDebug operator<<(QDebug out, LayoutGenerator &c)
565
out << "LayoutGenerator:"
566
<< "m_boundingRect:" << c.m_boundingRect
567
<< "m_nodes:" << c.m_nodes
568
<< "m_edges:" << c.m_edges
569
<< "m_scale:" << c.m_scale
570
<< "m_executable:" << c.m_executable;