Commit 8678a034 authored by Ralf Habacker's avatar Ralf Habacker
Browse files

Move implementation of LayoutGenerator into cpp file.

parent c4fd97e6
......@@ -392,6 +392,7 @@ set(libumbrello_SRCS
foreignkeyconstraint.cpp
icon_utils.cpp
import_rose.cpp
layoutgenerator.cpp
listpopupmenu.cpp
main.cpp
model_utils.cpp
......
/***************************************************************************
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* copyright (C) 2012-2014 *
* Umbrello UML Modeller Authors <umbrello-devel@kde.org> *
***************************************************************************/
#include "layoutgenerator.h"
#include "associationline.h"
#include "associationwidget.h"
#include "debug_utils.h"
#include "floatingtextwidget.h"
#include "umlwidget.h"
// app includes
#include <KConfigGroup>
#include <KDesktopFile>
#include <KStandardDirs>
// qt includes
#include <QDir>
#include <QFile>
#include <QHash>
#include <QProcess>
#include <QRegExp>
#include <QString>
#include <QTemporaryFile>
//#include <QTextStream>
#define LAYOUTGENERATOR_DEBUG
//#define LAYOUTGENERATOR_DATA_DEBUG
#ifdef LAYOUTGENERATOR_DEBUG
static QString pngViewer()
{
#ifdef Q_WS_WIN
return "start";
#else
#ifdef Q_WS_MAC
return "unknown";
#else
return "okular";
#endif
#endif
}
static QString textViewer()
{
#ifdef Q_WS_WIN
return "start";
#else
#ifdef Q_WS_MAC
return "unknown";
#else
return "mcedit";
#endif
#endif
}
#endif
/**
* constructor
*/
LayoutGenerator::LayoutGenerator()
{
setUseFullNodeLabels(false);
}
/**
* Return state if layout generator is enabled.
* It is enabled when the dot application has been found.
*
* @return true if enabled
*/
bool LayoutGenerator::isEnabled()
{
Settings::OptionState& optionState = Settings::optionState();
if (optionState.autoLayoutState.autoDotPath) {
m_dotPath = currentDotPath();
}
else if (!optionState.autoLayoutState.dotPath.isEmpty()) {
m_dotPath = optionState.autoLayoutState.dotPath;
}
return !m_dotPath.isEmpty();
}
/**
* Return the path where dot is installed.
*
* @return string with dot path
*/
QString LayoutGenerator::currentDotPath()
{
QString executable = KStandardDirs::findExe("dot");
if (!executable.isEmpty()) {
QFileInfo fi(executable);
return fi.absolutePath();
}
#ifdef Q_OS_WIN
// search for dot installation
QString appDir(qgetenv("ProgramFiles"));
QDir dir(appDir);
dir.setFilter(QDir::Dirs);
dir.setNameFilters(QStringList() << "Graphviz*");
dir.setSorting(QDir::Reversed);
QFileInfoList list = dir.entryInfoList();
if (list.size() > 0) {
QString dotPath = list.at(0).absoluteFilePath();
QString exePath = QFile::exists(dotPath + "\\bin") ? dotPath + "\\bin" : dotPath;
return QFile::exists(exePath + "\\dot.exe") ? exePath : "";
}
#endif
return QString();
}
/**
* generate layout and apply it to the given diagram.
*
* @return true if generating succeeded
*/
bool LayoutGenerator::generate(UMLScene *scene, const QString &variant)
{
QTemporaryFile in;
QTemporaryFile out;
QTemporaryFile xdotOut;
if (!isEnabled()) {
uWarning() << "Could not apply autolayout because graphviz installation has not been found.";
return false;
}
#ifdef LAYOUTGENERATOR_DEBUG
in.setAutoRemove(false);
out.setAutoRemove(false);
xdotOut.setAutoRemove(false);
#endif
// generate filenames
in.open();
in.close();
out.open();
out.close();
xdotOut.open();
xdotOut.close();
#ifdef LAYOUTGENERATOR_DEBUG
qDebug() << textViewer() << in.fileName();
qDebug() << textViewer() << out.fileName();
qDebug() << textViewer() << xdotOut.fileName();
#endif
if (!createDotFile(scene, in.fileName(), variant))
return false;
QString executable = m_dotPath + "/" + m_generator;
QProcess p;
QStringList args;
args << "-o" << out.fileName() << "-Tplain-ext" << in.fileName();
p.start(executable, args);
p.waitForFinished();
args.clear();
args << "-o" << xdotOut.fileName() << "-Txdot" << in.fileName();
p.start(executable, args);
p.waitForFinished();
#ifdef LAYOUTGENERATOR_DEBUG
QTemporaryFile pngFile;
pngFile.setAutoRemove(false);
pngFile.setFileTemplate(QDir::tempPath() + "/umbrello-layoutgenerator-XXXXXX.png");
pngFile.open();
pngFile.close();
qDebug() << pngViewer() << pngFile.fileName();
args.clear();
args << "-o" << pngFile.fileName() << "-Tpng" << in.fileName();
p.start(executable, args);
p.waitForFinished();
#endif
#ifndef USE_XDOT
if (!readGeneratedDotFile(out.fileName()))
#else
if (!readGeneratedDotFile(xdotOut.fileName()))
#endif
return false;
return true;
}
/**
* apply auto layout to the given scene
* @param scene
* @return true if autolayout has been applied
*/
bool LayoutGenerator::apply(UMLScene *scene)
{
foreach(AssociationWidget *assoc, scene->associationList()) {
AssociationLine *path = assoc->associationLine();
QString type = Uml::AssociationType::toString(assoc->associationType()).toLower();
QString key = "type::" + type;
QString id;
if (m_edgeParameters.contains("id::" + key) && m_edgeParameters["id::" + key] == "swap")
id = fixID(Uml::ID::toString(assoc->widgetLocalIDForRole(Uml::RoleType::A)) + Uml::ID::toString(assoc->widgetLocalIDForRole(Uml::RoleType::B)));
else
id = fixID(Uml::ID::toString(assoc->widgetLocalIDForRole(Uml::RoleType::B)) + Uml::ID::toString(assoc->widgetLocalIDForRole(Uml::RoleType::A)));
// adjust associations not used in the dot file
if (!m_edges.contains(id)) {
// shorten line path
AssociationLine *path = assoc->associationLine();
if (path->count() > 2 && assoc->widgetLocalIDForRole(Uml::RoleType::A) != assoc->widgetLocalIDForRole(Uml::RoleType::B)) {
while(path->count() > 2)
path->removePoint(1);
}
continue;
}
EdgePoints &p = m_edges[id];
int len = p.size();
while(path->count() > 1) {
path->removePoint(0);
}
path->setEndPoints(mapToScene(p[0]), mapToScene(p[len-1]));
// set label position
QPointF &l = m_edgeLabelPosition[id];
FloatingTextWidget *tw = assoc->nameWidget();
if (tw) {
tw->setPos(mapToScene(l));
}
// FIXME: set remaining association line points
/*
for(int i = 1; i < len-1; i++) {
path->insertPoint(i, mapToScene((p[i]));
}
*/
/*
* here stuff could be added to add more points from information returned by dot.
*/
}
foreach(UMLWidget *widget, scene->widgetList()) {
QString id = Uml::ID::toString(widget->localID());
if (!m_nodes.contains(id))
continue;
QPoint p = origin(id);
widget->setX(p.x());
widget->setY(p.y()-widget->height());
widget->adjustAssocs(widget->x(), widget->y()); // adjust assoc lines
}
foreach(AssociationWidget *assoc, scene->associationList()) {
assoc->calculateEndingPoints();
if (assoc->associationLine())
assoc->associationLine()->update();
assoc->resetTextPositions();
}
return true;
}
/**
* Return a list of available templates for a given scene type
*
* @param scene The diagram
* @param configFiles will contain the collected list of config files
* @return true if collecting succeeds
*/
bool LayoutGenerator::availableConfigFiles(UMLScene *scene, QHash<QString,QString> &configFiles)
{
QString diagramType = Uml::DiagramType::toString(scene->type()).toLower();
KStandardDirs dirs;
QStringList fileNames = dirs.findAllResources("data", QString("umbrello/layouts/%1*.desktop").arg(diagramType));
foreach(const QString &fileName, fileNames) {
QFileInfo fi(fileName);
QString baseName;
if (fi.baseName().contains("-"))
baseName = fi.baseName().remove(diagramType + "-");
else if (fi.baseName() == diagramType)
baseName = fi.baseName();
else
baseName = "default";
KDesktopFile desktopFile(fileName);
configFiles[baseName] = desktopFile.readName();
}
return true;
}
/**
* Return the origin of node based on the bottom/left corner
*
* @param id The widget id to fetch the origin from
* @return QPoint instance with the coordinates
*/
QPoint LayoutGenerator::origin(const QString &id)
{
QString key = fixID(id);
if (!m_nodes.contains(key)) {
#ifdef LAYOUTGENERATOR_DATA_DEBUG
uDebug() << key;
#endif
return QPoint(0,0);
}
QRectF &r = m_nodes[key];
QPoint p(m_origin.x() + r.x() - r.width()/2, m_boundingRect.height() - r.y() + r.height()/2 + m_origin.y());
#ifdef LAYOUTGENERATOR_DATA_DEBUG
uDebug() << r << p;
#endif
return p;
}
/**
* Read generated dot file and extract positions
* of the contained widgets.
*
* @return true if extracting succeeded
*/
bool LayoutGenerator::readGeneratedDotFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
while(line.endsWith(','))
line += in.readLine();
parseLine(line);
}
return true;
}
#ifndef USE_XDOT
/**
* Parse line from dot generated plain-ext output format
*
* The format is documented at http://graphviz.org/content/output-formats#dplain-ext and looks like:
*
* graph 1 28.083 10.222
* node ITfDmJvJE00m 8.0833 8.7361 0.86111 0.45833 QObject solid box black lightgrey
* 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
*
* @param line line in dot plain-ext output format
* @return true if line could be parsed successfully
*/
bool LayoutGenerator::parseLine(const QString &line)
{
QStringList a = line.split(' ');
if (a[0] == "graph") {
m_boundingRect = QRectF(0, 0, a[2].toDouble()*m_scale, a[3].toDouble()*m_scale);
return true;
} else if (a[0] == "node") {
QString key = fixID(a[1]);
m_nodes[key] = QRectF(a[2].toDouble()*m_scale, a[3].toDouble()*m_scale, a[4].toDouble()*m_scale, a[5].toDouble()*m_scale);
return true;
} else if (a[0] == "edge") {
QString key = fixID(a[1]+a[2]);
EdgePoints p;
int len = a[3].toInt();
for(int i = 0; i < len; i++)
p.append(QPointF(a[i*2+4].toDouble()*m_scale, a[i*2+5].toDouble()*m_scale));
m_edges[key] = p;
int b = len*2 + 4;
bool ok;
double x = a[b+1].toDouble(&ok);
if (!ok)
return true;
double y = a[b+2].toDouble(&ok);
if (!ok)
return true;
m_edgeLabelPosition[key] = QPointF(x*m_scale, y*m_scale);
return true;
} else if (a[0] == "stop") {
return true;
}
return false;
}
#else
typedef QMap<QString,QStringList> ParameterList;
bool LayoutGenerator::splitParameters(QMap<QString,QStringList> &map, const QString &s)
{
// FIXME: add shape=box without '"'
static QRegExp rx("([a-zA-Z_]+)=\"([a-zA-Z0-9.- #]+)\"");
static QRegExp rx2("([a-zA-Z_]+)=([a-zA-Z0-9.- #]+)");
int pos = 0;
int count = 0;
/*
* while ((pos = rx2.indexIn(s, pos)) != -1) {
* QString key = rx2.cap(1);
* QString value = rx2.cap(2);
* ++count;
* pos += rx2.matchedLength();
* //qDebug() << key << value;
* if (map.contains(key))
* map[key] << value;
* else
* map[key] = QStringList() << value;
}
*/
pos = 0;
while ((pos = rx.indexIn(s, pos)) != -1) {
QString key = rx.cap(1);
QString value = rx.cap(2);
++count;
pos += rx.matchedLength();
//qDebug() << key << value;
QStringList data;
if (key == "pos") {
value.remove("e,");
data = value.split(' ');
} else if (key.startsWith('_')) {
data = value.split(' ');
}
else if (key == "label")
data = QStringList() << value;
else
data = value.split(',');
if (map.contains(key))
map[key] << data;
else
map[key] = data;
}
return true;
}
/**
*
digraph G {
graph [splines=polyline, rankdir=BT, outputorder=nodesfirst, ranksep="0.5", nodesep="0.5"];
node [label="\N"];
graph [bb="0,0,2893,638",
_draw_="c 9 -#ffffffff C 9 -#ffffffff P 4 0 -1 0 638 2894 638 2894 -1 ",
xdotversion="1.2"];
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 "];
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 - "];
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 - "];
*/
bool LayoutGenerator::parseLine(const QString &line)
{
static QRegExp m_cols("^[\t ]*(.*)[\t ]*\\[(.*)\\]");
static int m_level = -1;
if (line.contains('{')) {
m_level++;
return true;
}
else if (line.contains('}')) {
m_level--;
return true;
}
int pos = 0;
if (m_cols.indexIn(line, pos) == -1)
return false;
QString keyword = m_cols.cap(1).trimmed();
QString attributeString = m_cols.cap(2);
uDebug() << keyword << attributeString;
ParameterList attributes;
splitParameters(attributes, attributeString);
uDebug() << attributes;
if (keyword == "graph") {
if (attributes.contains("bb")) {
QStringList &a = attributes["bb"];
m_boundingRect.setLeft(a[0].toDouble());
m_boundingRect.setTop(a[1].toDouble());
m_boundingRect.setRight(a[2].toDouble());
m_boundingRect.setBottom(a[3].toDouble());
}
} else if (keyword == "node") {
return true;
} else if (keyword == "edge") {
return true;
// transistion
} else if (line.contains("->")) {
QStringList k = keyword.split(" ");
if (k.size() < 3)
return false;
QString key = fixID(k[0]+k[2]);
if (attributes.contains("pos")) {
QStringList &a = attributes["pos"];
EdgePoints points;
for(int i = 1; i < a.size(); i++) {
QStringList b = a[i].split(',');
QPointF p(b[0].toDouble(), b[1].toDouble());
points.append(p);
}
QStringList b = a[0].split(',');
QPointF p(b[0].toDouble(), b[1].toDouble());
points.append(p);
m_edges[key] = points;
}
if (0 && attributes.contains("_draw_")) {
QStringList &a = attributes["_draw_"];
if (a.size() < 5 || (a[3] != "L" && a[3] != "p"))
return false;
int size = a[4].toInt();
EdgePoints points;
for(int i = 0; i < size; i++) {
QPointF p(a[i*2+5].toDouble(), a[i*2+6].toDouble());
points.append(p);
}
m_edges[key] = points;
}
return true;
// single node
} else {
double scale = 72.0;
QRectF f(0, 0, 0, 0);
QString id = fixID(keyword);
if (attributes.contains("pos")) {
QStringList &a = attributes["pos"];
QStringList b = a[0].split(",");
f.setLeft(b[0].toDouble());
f.setTop(b[1].toDouble());
}
if (attributes.contains("height")) {
QStringList &a = attributes["height"];
f.setHeight(a[0].toDouble()*scale);
}
if (attributes.contains("width")) {
QStringList &a = attributes["width"];
f.setWidth(a[0].toDouble()*scale);
}
uDebug() << "adding" << id << f;
m_nodes[id] = f;
}
return true;
}
#endif
/**
* map dot coordinate to scene coordinate
* @param p dot point to map
* @return uml scene coordinate
*/
QPointF LayoutGenerator::mapToScene(const QPointF &p)
{
return QPointF(p.x()+ m_origin.x(), m_boundingRect.height() - p.y() + m_origin.y());
}
#if 0
static QDebug operator<<(QDebug out, LayoutGenerator &c)
{
out << "LayoutGenerator:"
<< "m_boundingRect:" << c.m_boundingRect
<< "m_nodes:" << c.m_nodes
<< "m_edges:" << c.m_edges
<< "m_scale:" << c.m_scale
<< "m_executable:" << c.m_executable;
return out;
}
#endif
......@@ -10,62 +10,11 @@
#ifndef LAYOUTGENERATOR_H
#include "associationline.h"
#include "associationwidget.h"
#include "debug_utils.h"
#include "dotgenerator.h"
#include "floatingtextwidget.h"
#include "optionstate.h"
#include "umlwidget.h"
// app includes
#include <KConfigGroup>
#include <KDesktopFile>
#include <KStandardDirs>
// qt includes
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QHash>
#include <QProcess>
//// qt includes
#include <QRectF>
#include <QRegExp>
#include <QString>
#include <QTemporaryFile>
#include <QTextStream>
#define LAYOUTGENERATOR_DEBUG
//#define LAYOUTGENERATOR_DATA_DEBUG
#ifdef LAYOUTGENERATOR_DEBUG