diff options
Diffstat (limited to 'src')
64 files changed, 10843 insertions, 0 deletions
diff --git a/src/data/edge.cpp b/src/data/edge.cpp new file mode 100644 index 0000000..0ae566b --- /dev/null +++ b/src/data/edge.cpp @@ -0,0 +1,424 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "edge.h"
+#include "tikzit.h"
+#include "util.h"
+
+#include <QDebug>
+#include <QPointF>
+
+Edge::Edge(Node *s, Node *t, QObject *parent) :
+ QObject(parent), _source(s), _target(t)
+{
+ _data = new GraphElementData();
+ _edgeNode = 0;
+ _dirty = true;
+
+ if (s != t) {
+ _basicBendMode = true;
+ _bend = 0;
+ _inAngle = 0;
+ _outAngle = 0;
+ _weight = 0.4f;
+ } else {
+ _basicBendMode = false;
+ _bend = 0;
+ _inAngle = 135;
+ _outAngle = 45;
+ _weight = 1.0f;
+ }
+ _style = noneEdgeStyle;
+ updateControls();
+}
+
+Edge::~Edge()
+{
+ delete _data;
+ delete _edgeNode;
+}
+
+/*!
+ * @brief Edge::copy makes a deep copy of an edge.
+ * @param nodeTable is an optional pointer to a table mapping the old source/target
+ * node pointers to their new, copied versions. This is used when making a copy of
+ * an entire (sub)graph.
+ * @return a copy of the edge
+ */
+Edge *Edge::copy(QMap<Node*,Node*> *nodeTable)
+{
+ Edge *e;
+ if (nodeTable == 0) e = new Edge(_source, _target);
+ else e = new Edge(nodeTable->value(_source), nodeTable->value(_target));
+ e->setData(_data->copy());
+ e->setBasicBendMode(_basicBendMode);
+ e->setBend(_bend);
+ e->setInAngle(_inAngle);
+ e->setOutAngle(_outAngle);
+ e->setWeight(_weight);
+ e->attachStyle();
+ e->updateControls();
+ return e;
+}
+
+Node *Edge::source() const
+{
+ return _source;
+}
+
+Node *Edge::target() const
+{
+ return _target;
+}
+
+bool Edge::isSelfLoop()
+{
+ return (_source == _target);
+}
+
+bool Edge::isStraight()
+{
+ return (_basicBendMode && _bend == 0);
+}
+
+GraphElementData *Edge::data() const
+{
+ return _data;
+}
+
+void Edge::setData(GraphElementData *data)
+{
+ delete _data;
+ _data = data;
+ setAttributesFromData();
+}
+
+QString Edge::styleName() const
+{
+ QString nm = _data->property("style");
+ if (nm.isNull()) return "none";
+ else return nm;
+}
+
+void Edge::setStyleName(const QString &styleName)
+{
+ if (!styleName.isNull() && styleName != "none") _data->setProperty("style", styleName);
+ else _data->unsetProperty("style");
+}
+
+QString Edge::sourceAnchor() const
+{
+ return _sourceAnchor;
+}
+
+void Edge::setSourceAnchor(const QString &sourceAnchor)
+{
+ _sourceAnchor = sourceAnchor;
+}
+
+QString Edge::targetAnchor() const
+{
+ return _targetAnchor;
+}
+
+void Edge::setTargetAnchor(const QString &targetAnchor)
+{
+ _targetAnchor = targetAnchor;
+}
+
+Node *Edge::edgeNode() const
+{
+ return _edgeNode;
+}
+
+void Edge::setEdgeNode(Node *edgeNode)
+{
+ if (_edgeNode != 0) delete _edgeNode;
+ _edgeNode = edgeNode;
+}
+
+bool Edge::hasEdgeNode()
+{
+ return _edgeNode != 0;
+}
+
+void Edge::updateControls() {
+ //if (_dirty) {
+ QPointF src = _source->point();
+ QPointF targ = _target->point();
+
+ float dx = (targ.x() - src.x());
+ float dy = (targ.y() - src.y());
+
+ float outAngleR = 0.0f;
+ float inAngleR = 0.0f;
+
+ if (_basicBendMode) {
+ float angle = std::atan2(dy, dx);
+ float bnd = (float)_bend * (M_PI / 180.0f);
+ outAngleR = angle - bnd;
+ inAngleR = M_PI + angle + bnd;
+ _outAngle = outAngleR * (180.f / M_PI);
+ _inAngle = inAngleR * (180.f / M_PI);
+ } else {
+ outAngleR = (float)_outAngle * (M_PI / 180.0f);
+ inAngleR = (float)_inAngle * (M_PI / 180.0f);
+ }
+
+ // TODO: calculate head and tail properly, not just for circles
+ if (_source->style()->isNone()) {
+ _tail = src;
+ } else {
+ _tail = QPointF(src.x() + std::cos(outAngleR) * 0.2,
+ src.y() + std::sin(outAngleR) * 0.2);
+ }
+
+ if (_target->style()->isNone()) {
+ _head = targ;
+ } else {
+ _head = QPointF(targ.x() + std::cos(inAngleR) * 0.2,
+ targ.y() + std::sin(inAngleR) * 0.2);
+ }
+
+ // give a default distance for self-loops
+ _cpDist = (dx==0.0f && dy==0.0f) ? _weight : std::sqrt(dx*dx + dy*dy) * _weight;
+
+ _cp1 = QPointF(src.x() + (_cpDist * std::cos(outAngleR)),
+ src.y() + (_cpDist * std::sin(outAngleR)));
+
+ _cp2 = QPointF(targ.x() + (_cpDist * std::cos(inAngleR)),
+ targ.y() + (_cpDist * std::sin(inAngleR)));
+
+ _mid = bezierInterpolateFull (0.5f, _tail, _cp1, _cp2, _head);
+ _tailTangent = bezierTangent(0.0f, 0.1f);
+ _headTangent = bezierTangent(1.0f, 0.9f);
+}
+
+void Edge::setAttributesFromData()
+{
+ _basicBendMode = true;
+ bool ok = true;
+
+ if (_data->atom("bend left")) {
+ _bend = -30;
+ } else if (_data->atom("bend right")) {
+ _bend = 30;
+ } else if (_data->property("bend left") != 0) {
+ _bend = -_data->property("bend left").toInt(&ok);
+ if (!ok) _bend = -30;
+ } else if (_data->property("bend right") != 0) {
+ _bend = _data->property("bend right").toInt(&ok);
+ if (!ok) _bend = 30;
+ } else {
+ _bend = 0;
+
+ if (_data->property("in") != 0 && _data->property("out") != 0) {
+ _basicBendMode = false;
+ _inAngle = _data->property("in").toInt(&ok);
+ if (!ok) _inAngle = 0;
+ _outAngle = _data->property("out").toInt(&ok);
+ if (!ok) _outAngle = 180;
+ }
+ }
+
+ if (!_data->property("looseness").isNull()) {
+ _weight = _data->property("looseness").toFloat(&ok) / 2.5f;
+ if (!ok) _weight = 0.4f;
+ } else {
+ _weight = (isSelfLoop()) ? 1.0f : 0.4f;
+ }
+
+ //qDebug() << "bend: " << _bend << " in: " << _inAngle << " out: " << _outAngle;
+ _dirty = true;
+}
+
+void Edge::updateData()
+{
+ _data->unsetAtom("loop");
+ _data->unsetProperty("in");
+ _data->unsetProperty("out");
+ _data->unsetAtom("bend left");
+ _data->unsetAtom("bend right");
+ _data->unsetProperty("bend left");
+ _data->unsetProperty("bend right");
+ _data->unsetProperty("looseness");
+
+ // TODO: style handling?
+
+ if (_basicBendMode) {
+ if (_bend != 0) {
+ QString bendKey;
+ int b;
+ if (_bend < 0) {
+ bendKey = "bend left";
+ b = -_bend;
+ } else {
+ bendKey = "bend right";
+ b = _bend;
+ }
+
+ if (b == 30) {
+ _data->setAtom(bendKey);
+ } else {
+ _data->setProperty(bendKey, QString::number(b));
+ }
+ }
+ } else {
+ _data->setProperty("in", QString::number(_inAngle));
+ _data->setProperty("out", QString::number(_outAngle));
+ }
+
+ if (_source == _target) _data->setAtom("loop");
+ if (!isSelfLoop() && !isStraight() && _weight != 0.4f)
+ _data->setProperty("looseness", QString::number(_weight*2.5f, 'f', 2));
+ if (_source->isBlankNode()) _sourceAnchor = "center";
+ else _sourceAnchor = "";
+ if (_target->isBlankNode()) _targetAnchor = "center";
+ else _targetAnchor = "";
+
+}
+
+
+QPointF Edge::head() const
+{
+ return _head;
+}
+
+QPointF Edge::tail() const
+{
+ return _tail;
+}
+
+QPointF Edge::cp1() const
+{
+ return _cp1;
+}
+
+QPointF Edge::cp2() const
+{
+ return _cp2;
+}
+
+int Edge::bend() const
+{
+ return _bend;
+}
+
+int Edge::inAngle() const
+{
+ return _inAngle;
+}
+
+int Edge::outAngle() const
+{
+ return _outAngle;
+}
+
+float Edge::weight() const
+{
+ return _weight;
+}
+
+bool Edge::basicBendMode() const
+{
+ return _basicBendMode;
+}
+
+float Edge::cpDist() const
+{
+ return _cpDist;
+}
+
+void Edge::setBasicBendMode(bool mode)
+{
+ _basicBendMode = mode;
+}
+
+void Edge::setBend(int bend)
+{
+ _bend = bend;
+}
+
+void Edge::setInAngle(int inAngle)
+{
+ _inAngle = inAngle;
+}
+
+void Edge::setOutAngle(int outAngle)
+{
+ _outAngle = outAngle;
+}
+
+void Edge::setWeight(float weight)
+{
+ _weight = weight;
+}
+
+int Edge::tikzLine() const
+{
+ return _tikzLine;
+}
+
+void Edge::setTikzLine(int tikzLine)
+{
+ _tikzLine = tikzLine;
+}
+
+QPointF Edge::mid() const
+{
+ return _mid;
+}
+
+QPointF Edge::headTangent() const
+{
+ return _headTangent;
+}
+
+QPointF Edge::tailTangent() const
+{
+ return _tailTangent;
+}
+
+void Edge::attachStyle()
+{
+ QString nm = styleName();
+ if (nm.isNull()) _style = noneEdgeStyle;
+ else _style = tikzit->styles()->edgeStyle(nm);
+}
+
+Style *Edge::style() const
+{
+ return _style;
+}
+
+QPointF Edge::bezierTangent(float start, float end) const
+{
+ float dx = bezierInterpolate(end, _tail.x(), _cp1.x(), _cp2.x(), _head.x()) -
+ bezierInterpolate(start, _tail.x(), _cp1.x(), _cp2.x(), _head.x());
+ float dy = bezierInterpolate(end, _tail.y(), _cp1.y(), _cp2.y(), _head.y()) -
+ bezierInterpolate(start, _tail.y(), _cp1.y(), _cp2.y(), _head.y());
+
+ // normalise
+ float len = sqrt(dx*dx + dy * dy);
+ if (len != 0) {
+ dx = (dx / len) * 0.1f;
+ dy = (dy / len) * 0.1f;
+ }
+
+ return QPointF(dx, dy);
+}
diff --git a/src/data/edge.h b/src/data/edge.h new file mode 100644 index 0000000..ad71364 --- /dev/null +++ b/src/data/edge.h @@ -0,0 +1,130 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef EDGE_H
+#define EDGE_H
+
+#include "graphelementdata.h"
+#include "node.h"
+#include "style.h"
+
+#include <QObject>
+#include <QPointF>
+
+class Edge : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Edge(Node *s, Node *t, QObject *parent = 0);
+ ~Edge();
+ Edge *copy(QMap<Node *, Node *> *nodeTable = 0);
+
+ Node *source() const;
+ Node *target() const;
+
+ bool isSelfLoop();
+ bool isStraight();
+
+ GraphElementData *data() const;
+ void setData(GraphElementData *data);
+
+ QString sourceAnchor() const;
+ void setSourceAnchor(const QString &sourceAnchor);
+
+ QString targetAnchor() const;
+ void setTargetAnchor(const QString &targetAnchor);
+
+ Node *edgeNode() const;
+ void setEdgeNode(Node *edgeNode);
+ bool hasEdgeNode();
+
+ void updateControls();
+ void setAttributesFromData();
+ void updateData();
+
+ QPointF head() const;
+ QPointF tail() const;
+ QPointF cp1() const;
+ QPointF cp2() const;
+ QPointF mid() const;
+ QPointF headTangent() const;
+ QPointF tailTangent() const;
+
+ int bend() const;
+ int inAngle() const;
+ int outAngle() const;
+ float weight() const;
+ bool basicBendMode() const;
+ float cpDist() const;
+
+ void setBasicBendMode(bool mode);
+ void setBend(int bend);
+ void setInAngle(int inAngle);
+ void setOutAngle(int outAngle);
+ void setWeight(float weight);
+
+ int tikzLine() const;
+ void setTikzLine(int tikzLine);
+
+
+ void attachStyle();
+ QString styleName() const;
+ void setStyleName(const QString & styleName);
+ Style *style() const;
+
+signals:
+
+public slots:
+
+private:
+ QPointF bezierTangent(float start, float end) const;
+ QString _sourceAnchor;
+ QString _targetAnchor;
+
+ // owned
+ Node *_edgeNode;
+ GraphElementData *_data;
+
+ // referenced
+ Node *_source;
+ Node *_target;
+
+
+ Style *_style;
+
+ bool _dirty;
+ bool _basicBendMode;
+ int _bend;
+ int _inAngle;
+ int _outAngle;
+ float _weight;
+ float _cpDist;
+
+ QPointF _head;
+ QPointF _tail;
+ QPointF _cp1;
+ QPointF _cp2;
+ QPointF _mid;
+
+ QPointF _headTangent;
+ QPointF _tailTangent;
+
+ int _tikzLine;
+};
+
+#endif // EDGE_H
diff --git a/src/data/graph.cpp b/src/data/graph.cpp new file mode 100644 index 0000000..bba2061 --- /dev/null +++ b/src/data/graph.cpp @@ -0,0 +1,383 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "graph.h"
+#include "util.h"
+
+#include <QTextStream>
+#include <QSet>
+#include <QtAlgorithms>
+#include <QDebug>
+#include <algorithm>
+
+Graph::Graph(QObject *parent) : QObject(parent)
+{
+ _data = new GraphElementData(this);
+ _bbox = QRectF(0,0,0,0);
+}
+
+Graph::~Graph()
+{
+}
+
+// add a node. The graph claims ownership.
+void Graph::addNode(Node *n) {
+ n->setParent(this);
+ _nodes << n;
+}
+
+void Graph::addNode(Node *n, int index)
+{
+ n->setParent(this);
+ _nodes.insert(index, n);
+}
+
+void Graph::removeNode(Node *n) {
+ // the node itself is not deleted, as it may still be referenced in an undo command. It will
+ // be deleted when graph is, via QObject memory management.
+ _nodes.removeOne(n);
+}
+
+
+void Graph::addEdge(Edge *e)
+{
+ e->setParent(this);
+ _edges << e;
+}
+
+void Graph::addEdge(Edge *e, int index)
+{
+ e->setParent(this);
+ _edges.insert(index, e);
+}
+
+void Graph::removeEdge(Edge *e)
+{
+ // the edge itself is not deleted, as it may still be referenced in an undo command. It will
+ // be deleted when graph is, via QObject memory management.
+ _edges.removeOne(e);
+}
+
+int Graph::maxIntName()
+{
+ int max = -1;
+ int i;
+ bool ok;
+ foreach (Node *n, _nodes) {
+ i = n->name().toInt(&ok);
+ if (ok && i > max) max = i;
+ }
+ return max;
+}
+
+void Graph::reorderNodes(const QVector<Node *> &newOrder)
+{
+ _nodes = newOrder;
+}
+
+void Graph::reorderEdges(const QVector<Edge *> &newOrder)
+{
+ _edges = newOrder;
+}
+
+QRectF Graph::realBbox()
+{
+ //float maxX = 0.0f;
+ QRectF rect = bbox();
+ foreach (Node *n, _nodes) {
+ rect = rect.united(QRectF(n->point().x()-0.5f,
+ n->point().y()-0.5f,
+ 1.0f, 1.0f));
+ }
+
+ return rect;
+}
+
+QRectF Graph::boundsForNodes(QSet<Node*>nds) {
+ QPointF p;
+ QPointF tl;
+ QPointF br;
+ bool hasPoints = false;
+ foreach (Node *n, nds) {
+ p = n->point();
+ if (!hasPoints) {
+ hasPoints = true;
+ tl = p;
+ br = p;
+ } else {
+ if (p.x() < tl.x()) tl.setX(p.x());
+ if (p.y() > tl.y()) tl.setY(p.y());
+ if (p.x() > br.x()) br.setX(p.x());
+ if (p.y() < br.y()) br.setY(p.y());
+ }
+ }
+
+ QRectF rect(tl, br);
+ return rect;
+}
+
+QString Graph::freshNodeName()
+{
+ return QString::number(maxIntName() + 1);
+}
+
+void Graph::renameApart(Graph *graph)
+{
+ int i = graph->maxIntName() + 1;
+ foreach (Node *n, _nodes) {
+ n->setName(QString::number(i));
+ i++;
+ }
+}
+
+GraphElementData *Graph::data() const
+{
+ return _data;
+}
+
+void Graph::setData(GraphElementData *data)
+{
+ delete _data;
+ _data = data;
+}
+
+const QVector<Node*> &Graph::nodes()
+{
+ return _nodes;
+}
+
+const QVector<Edge*> &Graph::edges()
+{
+ return _edges;
+}
+
+QRectF Graph::bbox() const
+{
+ return _bbox;
+}
+
+bool Graph::hasBbox() {
+ return !(_bbox == QRectF(0,0,0,0));
+}
+
+void Graph::clearBbox() {
+ _bbox = QRectF(0,0,0,0);
+}
+
+QString Graph::tikz()
+{
+ QString str;
+ QTextStream code(&str);
+ int line = 0;
+
+ code << "\\begin{tikzpicture}" << _data->tikz() << "\n";
+ line++;
+ if (hasBbox()) {
+ code << "\t\\path [use as bounding box] ("
+ << _bbox.topLeft().x() << "," << _bbox.topLeft().y()
+ << ") rectangle ("
+ << _bbox.bottomRight().x() << "," << _bbox.bottomRight().y()
+ << ");\n";
+ line++;
+ }
+
+ if (!_nodes.isEmpty()) {
+ code << "\t\\begin{pgfonlayer}{nodelayer}\n";
+ line++;
+ }
+
+ Node *n;
+ foreach (n, _nodes) {
+ n->setTikzLine(line);
+ code << "\t\t\\node ";
+
+ if (!n->data()->isEmpty())
+ code << n->data()->tikz() << " ";
+
+ code << "(" << n->name() << ") at ("
+ << floatToString(n->point().x())
+ << ", "
+ << floatToString(n->point().y())
+ << ") {" << n->label() << "};\n";
+ line++;
+ }
+
+ if (!_nodes.isEmpty()) {
+ code << "\t\\end{pgfonlayer}\n";
+ line++;
+ }
+
+ if (!_edges.isEmpty()) {
+ code << "\t\\begin{pgfonlayer}{edgelayer}\n";
+ line++;
+ }
+
+
+ Edge *e;
+ foreach (e, _edges) {
+ e->setTikzLine(line);
+ e->updateData();
+ code << "\t\t\\draw ";
+
+ if (!e->data()->isEmpty())
+ code << e->data()->tikz() << " ";
+
+ code << "(" << e->source()->name();
+ if (e->sourceAnchor() != "")
+ code << "." << e->sourceAnchor();
+ code << ") to ";
+
+ if (e->hasEdgeNode()) {
+ code << "node ";
+ if (!e->edgeNode()->data()->isEmpty())
+ code << e->edgeNode()->data()->tikz() << " ";
+ code << "{" << e->edgeNode()->label() << "} ";
+ }
+
+ if (e->source() == e->target()) {
+ code << "()";
+ } else {
+ code << "(" << e->target()->name();
+ if (e->targetAnchor() != "")
+ code << "." << e->targetAnchor();
+ code << ")";
+ }
+
+ code << ";\n";
+ line++;
+ }
+
+ if (!_edges.isEmpty()) {
+ code << "\t\\end{pgfonlayer}\n";
+ line++;
+ }
+
+ code << "\\end{tikzpicture}\n";
+ line++;
+
+ code.flush();
+ return str;
+}
+
+Graph *Graph::copyOfSubgraphWithNodes(QSet<Node *> nds)
+{
+ Graph *g = new Graph();
+ g->setData(_data->copy());
+ QMap<Node*,Node*> nodeTable;
+ foreach (Node *n, nodes()) {
+ if (nds.contains(n)) {
+ Node *n1 = n->copy();
+ nodeTable.insert(n, n1);
+ g->addNode(n1);
+ }
+ }
+ foreach (Edge *e, edges()) {
+ if (nds.contains(e->source()) && nds.contains(e->target())) {
+ g->addEdge(e->copy(&nodeTable));
+ }
+ }
+
+ return g;
+}
+
+void Graph::insertGraph(Graph *graph)
+{
+ QMap<Node*,Node*> nodeTable;
+ foreach (Node *n, graph->nodes()) addNode(n);
+ foreach (Edge *e, graph->edges()) addEdge(e);
+}
+
+void Graph::reflectNodes(QSet<Node*> nds, bool horizontal)
+{
+ QRectF bds = boundsForNodes(nds);
+ float ctr;
+ if (horizontal) ctr = bds.center().x();
+ else ctr = bds.center().y();
+
+ QPointF p;
+ foreach(Node *n, nds) {
+ p = n->point();
+ if (horizontal) p.setX(2 * ctr - p.x());
+ else p.setY(2 * ctr - p.y());
+ n->setPoint(p);
+ }
+
+ foreach (Edge *e, _edges) {
+ if (nds.contains(e->source()) && nds.contains(e->target())) {
+ if (!e->basicBendMode()) {
+ if (horizontal) {
+ if (e->inAngle() < 0) e->setInAngle(-180 - e->inAngle());
+ else e->setInAngle(180 - e->inAngle());
+
+ if (e->outAngle() < 0) e->setOutAngle(-180 - e->outAngle());
+ else e->setOutAngle(180 - e->outAngle());
+ }
+ else {
+ e->setInAngle(-e->inAngle());
+ e->setOutAngle(-e->outAngle());
+ }
+ }
+ else {
+ e->setBend(-e->bend());
+ }
+ }
+ }
+}
+
+void Graph::rotateNodes(QSet<Node*> nds, bool clockwise)
+{
+ //QRectF bds = boundsForNodes(nds);
+ // QPointF ctr = bds.center();
+ // ctr.setX((float)floor(ctr.x() * 4.0f) / 4.0f);
+ // ctr.setY((float)floor(ctr.y() * 4.0f) / 4.0f);
+ float sign = (clockwise) ? 1.0f : -1.0f;
+
+ QPointF p;
+ // float dx, dy;
+ foreach(Node *n, nds) {
+ p = n->point();
+ // dx = p.x() - ctr.x();
+ // dy = p.y() - ctr.y();
+ n->setPoint(QPointF(sign * p.y(), -sign * p.x()));
+ }
+
+ int newIn, newOut;
+ foreach (Edge *e, _edges) {
+ if (nds.contains(e->source()) && nds.contains(e->target())) {
+ // update angles if necessary. Note that "basic" bends are computed based
+ // on node position, so they don't need to be updated.
+ if (!e->basicBendMode()) {
+ newIn = e->inAngle() - sign * 90;
+ newOut = e->outAngle() - sign * 90;
+
+ // normalise the angle to be within (-180,180]
+ if (newIn > 180) newIn -= 360;
+ else if (newIn <= -180) newIn += 360;
+ if (newOut > 180) newOut -= 360;
+ else if (newOut <= -180) newOut += 360;
+ e->setInAngle(newIn);
+ e->setOutAngle(newOut);
+ }
+ }
+ }
+}
+
+void Graph::setBbox(const QRectF &bbox)
+{
+ _bbox = bbox;
+}
diff --git a/src/data/graph.h b/src/data/graph.h new file mode 100644 index 0000000..286ccdc --- /dev/null +++ b/src/data/graph.h @@ -0,0 +1,130 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+/*!
+ * A graph defined by tikz code.
+ */
+
+#ifndef GRAPH_H
+#define GRAPH_H
+
+#include "node.h"
+#include "edge.h"
+#include "graphelementdata.h"
+
+#include <QObject>
+#include <QVector>
+#include <QMultiHash>
+#include <QRectF>
+#include <QString>
+#include <QMap>
+
+class Graph : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Graph(QObject *parent = 0);
+ ~Graph();
+ void addNode(Node *n);
+ void addNode(Node *n, int index);
+ void removeNode(Node *n);
+ void addEdge(Edge *e);
+ void addEdge(Edge *e, int index);
+ void removeEdge(Edge *e);
+ int maxIntName();
+ void reorderNodes(const QVector<Node*> &newOrder);
+ void reorderEdges(const QVector<Edge*> &newOrder);
+ QRectF boundsForNodes(QSet<Node*> ns);
+ QString freshNodeName();
+
+ /*!
+ * \brief renameApart assigns fresh names to all of the nodes in "this",
+ * with respect to the given graph
+ * \param graph
+ */
+ void renameApart(Graph *graph);
+
+ GraphElementData *data() const;
+ void setData(GraphElementData *data);
+
+ const QVector<Node *> &nodes();
+ const QVector<Edge*> &edges();
+
+ QRectF bbox() const;
+ void setBbox(const QRectF &bbox);
+ bool hasBbox();
+ void clearBbox();
+
+ /*!
+ * \brief realBbox computes the union of the user-defined
+ * bounding box, and the bounding boxes of the graph's
+ * contents.
+ *
+ * \return
+ */
+ QRectF realBbox();
+
+ QString tikz();
+
+ /*!
+ * \brief copyOfSubgraphWithNodes produces a copy of the full subgraph
+ * with the given nodes. Used for cutting and copying to clipboard.
+ * \param nds
+ * \return
+ */
+ Graph *copyOfSubgraphWithNodes(QSet<Node*> nds);
+
+ /*!
+ * \brief insertGraph inserts the given graph into "this". Prior to calling this
+ * method, the node names in the given graph should be made fresh via
+ * "renameApart". Note that the parameter "graph" relinquishes ownership of its
+ * nodes and edges, so it should be not be allowed to exist longer than "this".
+ * \param graph
+ */
+ void insertGraph(Graph *graph);
+
+ /*!
+ * \brief reflectNodes flips the given set of nodes horizontally or vertically,
+ * depending on the value of the second parameter.
+ * \param nds a set of nodes to flip
+ * \param horizontal a boolean determining whether to flip horizontally or
+ * vertically
+ */
+ void reflectNodes(QSet<Node*> nds, bool horizontal);
+
+ /*!
+ * \brief rotateNodes rotates the given set of nodes clockwise or counter-clockwise,
+ * depending on the value of the second parameter.
+ * \param nds a set of nodes to flip
+ * \param clockwose a boolean determining whether to rotate clockwise or counter-clockwise
+ */
+ void rotateNodes(QSet<Node*> nds, bool clockwise);
+signals:
+
+public slots:
+
+private:
+ QVector<Node*> _nodes;
+ QVector<Edge*> _edges;
+ //QMultiHash<Node*,Edge*> inEdges;
+ //QMultiHash<Node*,Edge*> outEdges;
+ GraphElementData *_data;
+ QRectF _bbox;
+};
+
+#endif // GRAPH_H
diff --git a/src/data/graphelementdata.cpp b/src/data/graphelementdata.cpp new file mode 100644 index 0000000..810ebd6 --- /dev/null +++ b/src/data/graphelementdata.cpp @@ -0,0 +1,264 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "graphelementdata.h"
+
+#include <QDebug>
+#include <QTextStream>
+
+GraphElementData::GraphElementData(QVector<GraphElementProperty> init, QObject *parent) : QAbstractItemModel(parent)
+{
+ root = new GraphElementProperty();
+ _properties = init;
+}
+
+GraphElementData::GraphElementData(QObject *parent) : QAbstractItemModel(parent) {
+ root = new GraphElementProperty();
+}
+
+GraphElementData::~GraphElementData()
+{
+ delete root;
+}
+
+GraphElementData *GraphElementData::copy()
+{
+ return new GraphElementData(_properties);
+}
+
+void GraphElementData::setProperty(QString key, QString value)
+{
+ int i = indexOfKey(key);
+ if (i != -1) {
+ _properties[i].setValue(value);
+ } else {
+ GraphElementProperty p(key, value);
+ _properties << p;
+ }
+}
+
+void GraphElementData::unsetProperty(QString key)
+{
+ int i = indexOfKey(key);
+ if (i != -1)
+ _properties.remove(i);
+}
+
+void GraphElementData::add(GraphElementProperty p)
+{
+ int i = _properties.size();
+ beginInsertRows(QModelIndex(), i, i);
+ _properties << p;
+ endInsertRows();
+}
+
+void GraphElementData::operator <<(GraphElementProperty p)
+{
+ add(p);
+}
+
+void GraphElementData::setAtom(QString atom)
+{
+ int i = indexOfKey(atom);
+ if (i == -1)
+ _properties << GraphElementProperty(atom);
+}
+
+void GraphElementData::unsetAtom(QString atom)
+{
+ int i = indexOfKey(atom);
+ if (i != -1)
+ _properties.remove(i);
+}
+
+QString GraphElementData::property(QString key)
+{
+ int i = indexOfKey(key);
+ if (i != -1) {
+ return _properties[i].value();
+ } else {
+ return QString(); // null QString
+ }
+}
+
+bool GraphElementData::hasProperty(QString key)
+{
+ return (indexOfKey(key) != -1);
+}
+
+bool GraphElementData::atom(QString atom)
+{
+ return (indexOfKey(atom) != -1);
+}
+
+int GraphElementData::indexOfKey(QString key)
+{
+ for (int i = 0; i < _properties.size(); ++i) {
+ QString key1 = _properties[i].key();
+ if (key1 == key) return i;
+ }
+ return -1;
+}
+
+bool GraphElementData::removeRows(int row, int /*count*/, const QModelIndex &parent)
+{
+ if (row >= 0 && row < _properties.length()) {
+ beginRemoveRows(parent, row, row+1);
+ _properties.remove(row);
+ endRemoveRows();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool GraphElementData::moveRows(const QModelIndex &sourceParent,
+ int sourceRow,
+ int /*count*/,
+ const QModelIndex &destinationParent,
+ int destinationRow)
+{
+ if (sourceRow >= 0 && sourceRow < _properties.length() &&
+ destinationRow >= 0 && destinationRow <= _properties.length())
+ {
+ beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, destinationRow);
+ GraphElementProperty p = _properties[sourceRow];
+ _properties.remove(sourceRow);
+ if (sourceRow < destinationRow) {
+ _properties.insert(destinationRow - 1, p);
+ } else {
+ _properties.insert(destinationRow, p);
+ }
+ endMoveRows();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+QVariant GraphElementData::data(const QModelIndex &index, int role) const
+{
+ if (role == Qt::DisplayRole || role == Qt::EditRole) {
+ if (index.row() >= 0 && index.row() < _properties.length()) {
+ const GraphElementProperty &p = _properties[index.row()];
+ QString s = (index.column() == 0) ? p.key() : p.value();
+ return QVariant(s);
+ }
+ }
+
+ return QVariant();
+}
+
+QVariant GraphElementData::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
+ if (section == 0) return QVariant("Key/Atom");
+ else return QVariant("Value");
+ }
+
+ return QVariant();
+}
+
+QModelIndex GraphElementData::index(int row, int column, const QModelIndex &) const
+{
+ return createIndex(row, column, (void*)0);
+}
+
+QModelIndex GraphElementData::parent(const QModelIndex &) const
+{
+ // there is no nesting, so always return an invalid index
+ return QModelIndex();
+}
+
+int GraphElementData::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ } else {
+ return _properties.size();
+ }
+}
+
+int GraphElementData::columnCount(const QModelIndex &) const
+{
+ return 2;
+}
+
+Qt::ItemFlags GraphElementData::flags(const QModelIndex &index) const
+{
+ if (index.row() >= 0 && index.row() < _properties.length()) {
+ if (index.column() == 0 ||
+ (!_properties[index.row()].atom() && index.column() == 1))
+ {
+ return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
+ }
+ }
+ return QAbstractItemModel::flags(index);
+}
+
+bool GraphElementData::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ bool success = false;
+ if (index.row() >= 0 && index.row() < _properties.length()) {
+ if (index.column() == 0) {
+ _properties[index.row()].setKey(value.toString());
+ success = true;
+ } else if (index.column() == 1 && !_properties[index.row()].atom()) {
+ _properties[index.row()].setValue(value.toString());
+ success = true;
+ }
+ }
+
+ if (success) {
+ QVector<int> roles;
+ roles << role;
+ emit dataChanged(index, index, roles);
+ }
+
+ return success;
+}
+
+QString GraphElementData::tikz() {
+ if (_properties.length() == 0) return "";
+ QString str;
+ QTextStream code(&str);
+ code << "[";
+
+ GraphElementProperty p;
+ bool first = true;
+ foreach(p, _properties) {
+ if (!first) code << ", ";
+ code << p.tikz();
+ first = false;
+ }
+
+ code << "]";
+
+ code.flush();
+ return str;
+}
+
+bool GraphElementData::isEmpty()
+{
+ return _properties.isEmpty();
+}
+
+QVector<GraphElementProperty> GraphElementData::properties() const
+{
+ return _properties;
+}
diff --git a/src/data/graphelementdata.h b/src/data/graphelementdata.h new file mode 100644 index 0000000..23f0466 --- /dev/null +++ b/src/data/graphelementdata.h @@ -0,0 +1,84 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef GRAPHELEMENTDATA_H
+#define GRAPHELEMENTDATA_H
+
+#include "graphelementproperty.h"
+
+#include <QAbstractItemModel>
+#include <QString>
+#include <QVariant>
+#include <QModelIndex>
+#include <QVector>
+
+class GraphElementData : public QAbstractItemModel
+{
+ Q_OBJECT
+public:
+ explicit GraphElementData(QVector<GraphElementProperty> init,
+ QObject *parent = 0);
+ explicit GraphElementData(QObject *parent = 0);
+ ~GraphElementData();
+ GraphElementData *copy();
+ void setProperty(QString key, QString value);
+ void unsetProperty(QString key);
+ void setAtom(QString atom);
+ void unsetAtom(QString atom);
+ QString property(QString key);
+ bool hasProperty(QString key);
+ bool atom(QString atom);
+ int indexOfKey(QString key);
+ bool removeRows(int row, int count, const QModelIndex &parent) override;
+ bool moveRows(const QModelIndex &sourceParent,
+ int sourceRow, int,
+ const QModelIndex &destinationParent,
+ int destinationRow) override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+
+ QModelIndex index(int row, int column, const QModelIndex &) const override;
+ QModelIndex parent(const QModelIndex &) const override;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole) override;
+
+ void operator <<(GraphElementProperty p);
+ void add(GraphElementProperty p);
+
+ QString tikz();
+ bool isEmpty();
+ QVector<GraphElementProperty> properties() const;
+
+signals:
+
+public slots:
+
+private:
+ QVector<GraphElementProperty> _properties;
+ GraphElementProperty *root;
+};
+
+#endif // GRAPHELEMENTDATA_H
diff --git a/src/data/graphelementproperty.cpp b/src/data/graphelementproperty.cpp new file mode 100644 index 0000000..aa1bfc8 --- /dev/null +++ b/src/data/graphelementproperty.cpp @@ -0,0 +1,73 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "graphelementproperty.h"
+
+#include <QRegExp>
+
+GraphElementProperty::GraphElementProperty ():
+ _key(""), _value(""), _atom(false)
+{}
+
+GraphElementProperty::GraphElementProperty(QString key, QString value, bool atom) :
+ _key(key), _value(value), _atom(atom)
+{}
+
+GraphElementProperty::GraphElementProperty(QString key, QString value) :
+ _key(key), _value(value), _atom(false)
+{}
+
+GraphElementProperty::GraphElementProperty(QString key) :
+ _key(key), _value(""), _atom(true)
+{}
+
+QString GraphElementProperty::key() const
+{ return _key; }
+
+QString GraphElementProperty::value() const
+{ return _value; }
+
+void GraphElementProperty::setValue(const QString &value)
+{ _value = value; }
+
+bool GraphElementProperty::atom() const
+{ return _atom; }
+
+
+bool GraphElementProperty::operator==(const GraphElementProperty &p)
+{
+ if (_atom) return p.atom() && p.key() == _key;
+ else return !p.atom() && p.key() == _key && p.value() == _value;
+}
+
+QString GraphElementProperty::tikzEscape(QString str)
+{
+ QRegExp re("[0-9a-zA-Z<> \\-'.]*");
+ if (re.exactMatch(str)) return str;
+ else return "{" + str + "}";
+}
+
+QString GraphElementProperty::tikz() {
+ if (_atom) return tikzEscape(_key);
+ return tikzEscape(_key) + "=" + tikzEscape(_value);
+}
+
+void GraphElementProperty::setKey(const QString &key)
+{
+ _key = key;
+}
diff --git a/src/data/graphelementproperty.h b/src/data/graphelementproperty.h new file mode 100644 index 0000000..4ebe104 --- /dev/null +++ b/src/data/graphelementproperty.h @@ -0,0 +1,58 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef GRAPHELEMENTPROPERTY_H
+#define GRAPHELEMENTPROPERTY_H
+
+#include <QObject>
+
+class GraphElementProperty
+{
+public:
+ GraphElementProperty();
+
+ // full constructor
+ GraphElementProperty(QString key, QString value, bool atom);
+
+ // construct a proper property
+ GraphElementProperty(QString key, QString value);
+
+ // construct an atom
+ GraphElementProperty(QString key);
+
+ QString key() const;
+ void setKey(const QString &key);
+ QString value() const;
+ void setValue(const QString &value);
+ bool atom() const;
+ bool operator==(const GraphElementProperty &p);
+
+ static QString tikzEscape(QString str);
+ QString tikz();
+
+signals:
+
+public slots:
+
+private:
+ QString _key;
+ QString _value;
+ bool _atom;
+};
+
+#endif // GRAPHELEMENTPROPERTY_H
diff --git a/src/data/node.cpp b/src/data/node.cpp new file mode 100644 index 0000000..75acd00 --- /dev/null +++ b/src/data/node.cpp @@ -0,0 +1,123 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "node.h"
+#include "tikzit.h"
+
+#include <QDebug>
+
+Node::Node(QObject *parent) : QObject(parent), _tikzLine(-1)
+{
+ _data = new GraphElementData();
+ _style = noneStyle;
+ _data->setProperty("style", "none");
+}
+
+Node::~Node()
+{
+ delete _data;
+}
+
+Node *Node::copy() {
+ Node *n1 = new Node();
+ n1->setName(name());
+ n1->setData(data()->copy());
+ n1->setPoint(point());
+ n1->setLabel(label());
+ n1->attachStyle();
+ n1->setTikzLine(tikzLine());
+ return n1;
+}
+
+QPointF Node::point() const
+{
+ return _point;
+}
+
+void Node::setPoint(const QPointF &point)
+{
+ _point = point;
+}
+
+QString Node::name() const
+{
+ return _name;
+}
+
+void Node::setName(const QString &name)
+{
+ _name = name;
+}
+
+QString Node::label() const
+{
+ return _label;
+}
+
+void Node::setLabel(const QString &label)
+{
+ _label = label;
+}
+
+GraphElementData *Node::data() const
+{
+ return _data;
+}
+
+void Node::setData(GraphElementData *data)
+{
+ delete _data;
+ _data = data;
+}
+
+QString Node::styleName() const
+{
+ return _data->property("style");
+}
+
+void Node::setStyleName(const QString &styleName)
+{
+ _data->setProperty("style", styleName);
+}
+
+void Node::attachStyle()
+{
+ QString nm = styleName();
+ if (nm == "none") _style = noneStyle;
+ else _style = tikzit->styles()->nodeStyle(nm);
+}
+
+Style *Node::style() const
+{
+ return _style;
+}
+
+bool Node::isBlankNode()
+{
+ return styleName() == "none";
+}
+
+int Node::tikzLine() const
+{
+ return _tikzLine;
+}
+
+void Node::setTikzLine(int tikzLine)
+{
+ _tikzLine = tikzLine;
+}
diff --git a/src/data/node.h b/src/data/node.h new file mode 100644 index 0000000..490393d --- /dev/null +++ b/src/data/node.h @@ -0,0 +1,74 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef NODE_H
+#define NODE_H
+
+#include "graphelementdata.h"
+#include "style.h"
+
+#include <QObject>
+#include <QPointF>
+#include <QString>
+
+class Node : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Node(QObject *parent = 0);
+ ~Node();
+
+ Node *copy();
+
+ QPointF point() const;
+ void setPoint(const QPointF &point);
+
+ QString name() const;
+ void setName(const QString &name);
+
+ QString label() const;
+ void setLabel(const QString &label);
+
+ GraphElementData *data() const;
+ void setData(GraphElementData *data);
+
+ QString styleName() const;
+ void setStyleName(const QString &styleName);
+
+ void attachStyle();
+ Style *style() const;
+
+ bool isBlankNode();
+
+ int tikzLine() const;
+ void setTikzLine(int tikzLine);
+
+signals:
+
+public slots:
+
+private:
+ QPointF _point;
+ QString _name;
+ QString _label;
+ Style *_style;
+ GraphElementData *_data;
+ int _tikzLine;
+};
+
+#endif // NODE_H
diff --git a/src/data/style.cpp b/src/data/style.cpp new file mode 100644 index 0000000..d0f011d --- /dev/null +++ b/src/data/style.cpp @@ -0,0 +1,273 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "style.h"
+#include "tikzit.h"
+
+Style *noneStyle = new Style("none", new GraphElementData());
+Style *unknownStyle = new Style("unknown", new GraphElementData({GraphElementProperty("tikzit fill", "blue")}));
+Style *noneEdgeStyle = new Style("none", new GraphElementData({GraphElementProperty("-")}));
+
+Style::Style() : _name("none")
+{
+ _data = new GraphElementData(this);
+}
+
+Style::Style(QString name, GraphElementData *data) : _name(name), _data(data)
+{
+ _data->setParent(this);
+}
+
+bool Style::isNone() const
+{
+ return _name == "none";
+}
+
+GraphElementData *Style::data() const
+{
+ return _data;
+}
+
+QString Style::name() const
+{
+ return _name;
+}
+
+QColor Style::strokeColor(bool tikzitOverride) const
+{
+ QString col = propertyWithDefault("draw", "black", tikzitOverride);
+ return tikzit->colorByName(col);
+}
+
+QColor Style::fillColor(bool tikzitOverride) const
+{
+ QString col = propertyWithDefault("fill", "white", tikzitOverride);
+ return tikzit->colorByName(col);
+}
+
+QBrush Style::brush() const
+{
+ return QBrush(fillColor());
+}
+
+QString Style::shape(bool tikzitOverride) const
+{
+ return propertyWithDefault("shape", "circle", tikzitOverride);
+}
+
+
+// TODO
+int Style::strokeThickness() const
+{
+ return 1;
+}
+
+bool Style::isEdgeStyle() const
+{
+ if (_data->atom("-") || _data->atom("->") || _data->atom("-|") ||
+ _data->atom("<-") || _data->atom("<->") || _data->atom("<-|") ||
+ _data->atom("|-") || _data->atom("|->") || _data->atom("|-|")) return true;
+ else return false;
+}
+
+
+
+QString Style::propertyWithDefault(QString prop, QString def, bool tikzitOverride) const
+{
+ if (_data == 0) return def;
+ QString val;
+ if (tikzitOverride) {
+ val = _data->property("tikzit " + prop);
+ if (val.isNull()) val = _data->property(prop);
+ } else {
+ val = _data->property(prop);
+ }
+ if (val.isNull()) val = def;
+ return val;
+}
+
+QString Style::tikz() const
+{
+ return "\\tikzstyle{" + _name + "}=" + _data->tikz();
+}
+
+void Style::setArrowAtom(QString atom)
+{
+ _data->unsetAtom("-");
+ _data->unsetAtom("->");
+ _data->unsetAtom("-|");
+
+ _data->unsetAtom("<-");
+ _data->unsetAtom("<->");
+ _data->unsetAtom("<-|");
+
+ _data->unsetAtom("|-");
+ _data->unsetAtom("|->");
+ _data->unsetAtom("|-|");
+
+ _data->setAtom(atom);
+}
+
+void Style::setName(const QString &name)
+{
+ _name = name;
+}
+
+Style::ArrowTipStyle Style::arrowHead() const
+{
+ if (_data->atom("->") || _data->atom("<->") || _data->atom("|->")) return Pointer;
+ if (_data->atom("-|") || _data->atom("<-|") || _data->atom("|-|")) return Flat;
+ return NoTip;
+}
+
+Style::ArrowTipStyle Style::arrowTail() const
+{
+ if (_data->atom("<-") || _data->atom("<->") || _data->atom("<-|")) return Pointer;
+ if (_data->atom("|-") || _data->atom("|->") || _data->atom("|-|")) return Flat;
+ return NoTip;
+}
+
+Style::DrawStyle Style::drawStyle() const
+{
+ if (_data->atom("dashed")) return Dashed;
+ if (_data->atom("dotted")) return Dotted;
+ return Solid;
+}
+
+
+QPen Style::pen() const
+{
+ QPen p(strokeColor());
+ p.setWidthF((float)strokeThickness() * 2.0f);
+
+ QVector<qreal> pat;
+ switch (drawStyle()) {
+ case Dashed:
+ pat << 3.0 << 3.0;
+ p.setDashPattern(pat);
+ break;
+ case Dotted:
+ pat << 1.0 << 1.0;
+ p.setDashPattern(pat);
+ break;
+ case Solid:
+ break;
+ }
+
+ return p;
+}
+
+QPainterPath Style::path() const
+{
+ QPainterPath pth;
+ QString sh = shape();
+
+ if (sh == "rectangle") {
+ pth.addRect(-30.0f, -30.0f, 60.0f, 60.0f);
+ } else { // default is 'circle'
+ pth.addEllipse(QPointF(0.0f,0.0f), 30.0f, 30.0f);
+ }
+ return pth;
+}
+
+QIcon Style::icon() const
+{
+ if (!isEdgeStyle()) {
+ // draw an icon matching the style
+ QImage px(100,100,QImage::Format_ARGB32_Premultiplied);
+ px.fill(Qt::transparent);
+
+
+ QPainter painter(&px);
+ painter.setRenderHint(QPainter::Antialiasing);
+ QPainterPath pth = path();
+ pth.translate(50.0f, 50.0f);
+
+ if (isNone()) {
+ QColor c(180,180,200);
+ painter.setPen(QPen(c));
+ painter.setBrush(QBrush(c));
+ painter.drawEllipse(QPointF(50.0f,50.0f), 3,3);
+
+ QPen pen(QColor(180,180,220));
+ pen.setWidth(3);
+ QVector<qreal> p;
+ p << 2.0 << 2.0;
+ pen.setDashPattern(p);
+ painter.setPen(pen);
+ painter.setBrush(Qt::NoBrush);
+ painter.drawPath(pth);
+ } else {
+ painter.setPen(pen());
+ painter.setBrush(brush());
+ painter.drawPath(pth);
+ }
+
+ return QIcon(QPixmap::fromImage(px));
+ } else {
+ // draw an icon matching the style
+ QPixmap px(100,100);
+ px.fill(Qt::transparent);
+ QPainter painter(&px);
+
+ if (_data == 0) {
+ QPen pen(Qt::black);
+ pen.setWidth(3);
+ } else {
+ painter.setPen(pen());
+ }
+
+ painter.drawLine(10, 50, 90, 50);
+
+ QPen pn = pen();
+ pn.setStyle(Qt::SolidLine);
+ painter.setPen(pn);
+
+ switch (arrowHead()) {
+ case Pointer:
+ painter.drawLine(90,50,80,40);
+ painter.drawLine(90,50,80,60);
+ break;
+ case Flat:
+ painter.drawLine(90,40,90,60);
+ break;
+ case NoTip:
+ break;
+ }
+
+ switch (arrowTail()) {
+ case Pointer:
+ painter.drawLine(10,50,20,40);
+ painter.drawLine(10,50,20,60);
+ break;
+ case Flat:
+ painter.drawLine(10,40,10,60);
+ break;
+ case NoTip:
+ break;
+ }
+
+
+ return QIcon(px);
+ }
+}
+
+QString Style::category() const
+{
+ return propertyWithDefault("tikzit category", "", false);
+}
diff --git a/src/data/style.h b/src/data/style.h new file mode 100644 index 0000000..78e11dc --- /dev/null +++ b/src/data/style.h @@ -0,0 +1,82 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef STYLE_H
+#define STYLE_H
+
+
+#include "graphelementdata.h"
+
+#include <QObject>
+#include <QColor>
+#include <QPen>
+#include <QBrush>
+#include <QPainterPath>
+#include <QIcon>
+
+class Style : public QObject
+{
+ Q_OBJECT
+public:
+ enum ArrowTipStyle {
+ Flat, Pointer, NoTip
+ };
+
+ enum DrawStyle {
+ Solid, Dotted, Dashed
+ };
+
+ Style();
+ Style(QString name, GraphElementData *data);
+ bool isNone() const;
+ bool isEdgeStyle() const;
+
+ // for node and edge styles
+ GraphElementData *data() const;
+ QString name() const;
+ QColor strokeColor(bool tikzitOverride=true) const;
+ int strokeThickness() const;
+ QPen pen() const;
+ QPainterPath path() const;
+ QIcon icon() const;
+ void setName(const QString &name);
+ QString propertyWithDefault(QString prop, QString def, bool tikzitOverride=true) const;
+ QString tikz() const;
+ void setArrowAtom(QString atom);
+
+ // only relevant for node styles
+ QColor fillColor(bool tikzitOverride=true) const;
+ QBrush brush() const;
+ QString shape(bool tikzitOverride=true) const;
+
+ // only relevant for edge styles
+ Style::ArrowTipStyle arrowHead() const;
+ Style::ArrowTipStyle arrowTail() const;
+ Style::DrawStyle drawStyle() const;
+ QString category() const;
+
+protected:
+ QString _name;
+ GraphElementData *_data;
+};
+
+extern Style *noneStyle;
+extern Style *unknownStyle;
+extern Style *noneEdgeStyle;
+
+#endif // STYLE_H
diff --git a/src/data/stylelist.cpp b/src/data/stylelist.cpp new file mode 100644 index 0000000..bc2ef37 --- /dev/null +++ b/src/data/stylelist.cpp @@ -0,0 +1,163 @@ +#include "stylelist.h" + +#include <QTextStream> + +StyleList::StyleList(bool edgeStyles, QObject *parent) : QAbstractListModel(parent), _edgeStyles(edgeStyles) +{ + if (edgeStyles) { + _styles << noneEdgeStyle; + } else { + _styles << noneStyle; + } +} + +Style *StyleList::style(QString name) +{ + foreach (Style *s, _styles) + if (s->name() == name) return s; + return nullptr; +} + +Style *StyleList::style(int i) +{ + return _styles[i]; +} + +int StyleList::length() const +{ + return _styles.length(); +} + +void StyleList::addStyle(Style *s) +{ + s->setParent(this); + if (s->category() == _category) { + int n = numInCategory(); + beginInsertRows(QModelIndex(), n, n); + _styles << s; + endInsertRows(); + } else { + _styles << s; + } +} + +void StyleList::removeNthStyle(int n) +{ + beginRemoveRows(QModelIndex(), n, n); + _styles.remove(nthInCategory(n)); + endRemoveRows(); +} + +void StyleList::clear() +{ + int n = numInCategory(); + if (n > 1) { + beginRemoveRows(QModelIndex(), 1, n - 1); + _styles.clear(); + if (_edgeStyles) _styles << noneEdgeStyle; + else _styles << noneStyle; + endRemoveRows(); + } else { + _styles.clear(); + if (_edgeStyles) _styles << noneEdgeStyle; + else _styles << noneStyle; + } + + _category = ""; +} + +QString StyleList::tikz() +{ + QString str; + QTextStream code(&str); + for (int i = 1; i < _styles.length(); ++i) + code << _styles[i]->tikz() << "\n"; + code.flush(); + return str; +} + +int StyleList::numInCategory() const +{ + int c = 0; + foreach (Style *s, _styles) { + if (_category == "" || s->isNone() || s->category() == _category) { + ++c; + } + } + return c; +} + +int StyleList::nthInCategory(int n) const +{ + int c = 0; + for (int j = 0; j < _styles.length(); ++j) { + if (_category == "" || _styles[j]->isNone() || _styles[j]->category() == _category) { + if (c == n) return j; + else ++c; + } + } + return -1; +} + +Style *StyleList::styleInCategory(int n) const +{ + return _styles[nthInCategory(n)]; +} + +QVariant StyleList::data(const QModelIndex &index, int role) const +{ + if (role == Qt::DisplayRole) { + return QVariant(styleInCategory(index.row())->name()); + } else if (role == Qt::DecorationRole) { + return QVariant(styleInCategory(index.row())->icon()); + } else { + return QVariant(); + } +} + +int StyleList::rowCount(const QModelIndex &/*parent*/) const +{ + return numInCategory(); +} + +bool StyleList::moveRows(const QModelIndex &sourceParent, + int sourceRow, + int /*count*/, + const QModelIndex &destinationParent, + int destinationRow) +{ + if (sourceRow >= 1 && sourceRow < numInCategory() && + destinationRow >= 1 && destinationRow <= numInCategory()) + { + beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, destinationRow); + int sourceIndex = nthInCategory(sourceRow); + int destinationIndex = nthInCategory(destinationRow); + if (destinationIndex == -1) + destinationIndex = _styles.length(); + Style *s = _styles[sourceIndex]; + _styles.remove(sourceIndex); + if (sourceIndex < destinationIndex) { + _styles.insert(destinationIndex - 1, s); + } else { + _styles.insert(destinationIndex, s); + } + endMoveRows(); + return true; + } else { + return false; + } +} + +QString StyleList::category() const +{ + return _category; +} + +void StyleList::setCategory(const QString &category) +{ + if (category != _category) { + beginResetModel(); + _category = category; + endResetModel(); + } +} diff --git a/src/data/stylelist.h b/src/data/stylelist.h new file mode 100644 index 0000000..cf86c06 --- /dev/null +++ b/src/data/stylelist.h @@ -0,0 +1,47 @@ +#ifndef NODESTYLELIST_H +#define NODESTYLELIST_H + +#include "style.h" + +#include <QAbstractListModel> + +class StyleList : public QAbstractListModel +{ + Q_OBJECT +public: + explicit StyleList(bool edgeStyles = false, QObject *parent = nullptr); + Style *style(QString name); + Style *style(int i); + int length() const; + void addStyle(Style *s); + void removeNthStyle(int n); + void clear(); + QString tikz(); + + int numInCategory() const; + int nthInCategory(int n) const; + Style *styleInCategory(int n) const; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &/*parent*/) const override; + bool moveRows(const QModelIndex &sourceParent, + int sourceRow, + int /*count*/, + const QModelIndex &destinationParent, + int destinationChild) override; + + + QString category() const; + void setCategory(const QString &category); + +signals: + +public slots: + +private: + QVector<Style*> _styles; + QString _category; + bool _edgeStyles; +}; + +#endif // NODESTYLELIST_H diff --git a/src/data/tikzassembler.cpp b/src/data/tikzassembler.cpp new file mode 100644 index 0000000..cd0b517 --- /dev/null +++ b/src/data/tikzassembler.cpp @@ -0,0 +1,72 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "tikzassembler.h"
+
+#include "tikzparserdefs.h"
+#include "tikzparser.parser.hpp"
+#include "tikzlexer.h"
+
+int yyparse(void *scanner);
+
+TikzAssembler::TikzAssembler(Graph *graph, QObject *parent) :
+ QObject(parent), _graph(graph), _tikzStyles(0)
+{
+ yylex_init(&scanner);
+ yyset_extra(this, scanner);
+}
+
+TikzAssembler::TikzAssembler(TikzStyles *tikzStyles, QObject *parent) :
+ QObject(parent), _graph(0), _tikzStyles(tikzStyles)
+{
+ yylex_init(&scanner);
+ yyset_extra(this, scanner);
+}
+
+void TikzAssembler::addNodeToMap(Node *n) { _nodeMap.insert(n->name(), n); }
+Node *TikzAssembler::nodeWithName(QString name) { return _nodeMap[name]; }
+
+bool TikzAssembler::parse(const QString &tikz)
+{
+ yy_scan_string(tikz.toLatin1().data(), scanner);
+ int result = yyparse(scanner);
+
+ if (result == 0) return true;
+ else return false;
+}
+
+Graph *TikzAssembler::graph() const
+{
+ return _graph;
+}
+
+TikzStyles *TikzAssembler::tikzStyles() const
+{
+ return _tikzStyles;
+}
+
+bool TikzAssembler::isGraph() const
+{
+ return _graph != 0;
+}
+
+bool TikzAssembler::isTikzStyles() const
+{
+ return _tikzStyles != 0;
+}
+
diff --git a/src/data/tikzassembler.h b/src/data/tikzassembler.h new file mode 100644 index 0000000..7b32224 --- /dev/null +++ b/src/data/tikzassembler.h @@ -0,0 +1,60 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+/*!
+ * Convenience class to hold the parser state while loading tikz graphs or projects.
+ */
+
+#ifndef TIKZASSEMBLER_H
+#define TIKZASSEMBLER_H
+
+#include "node.h"
+#include "graph.h"
+#include "tikzstyles.h"
+
+#include <QObject>
+#include <QHash>
+
+class TikzAssembler : public QObject
+{
+ Q_OBJECT
+public:
+ explicit TikzAssembler(Graph *graph, QObject *parent = 0);
+ explicit TikzAssembler(TikzStyles *tikzStyles, QObject *parent = 0);
+ void addNodeToMap(Node *n);
+ Node *nodeWithName(QString name);
+ bool parse(const QString &tikz);
+
+ Graph *graph() const;
+ TikzStyles *tikzStyles() const;
+ bool isGraph() const;
+ bool isTikzStyles() const;
+
+
+signals:
+
+public slots:
+
+private:
+ QHash<QString,Node*> _nodeMap;
+ Graph *_graph;
+ TikzStyles *_tikzStyles;
+ void *scanner;
+};
+
+#endif // TIKZASSEMBLER_H
diff --git a/src/data/tikzdocument.cpp b/src/data/tikzdocument.cpp new file mode 100644 index 0000000..24a793b --- /dev/null +++ b/src/data/tikzdocument.cpp @@ -0,0 +1,213 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include <QFile>
+#include <QFileInfo>
+#include <QSettings>
+#include <QTextStream>
+#include <QMessageBox>
+#include <QFileDialog>
+
+#include "tikzit.h"
+#include "tikzdocument.h"
+#include "tikzassembler.h"
+#include "mainwindow.h"
+
+TikzDocument::TikzDocument(QObject *parent) : QObject(parent)
+{
+ _graph = new Graph(this);
+ _parseSuccess = true;
+ _fileName = "";
+ _shortName = "";
+ _undoStack = new QUndoStack();
+ _undoStack->setClean();
+}
+
+TikzDocument::~TikzDocument()
+{
+ delete _graph;
+ delete _undoStack;
+}
+
+QUndoStack *TikzDocument::undoStack() const
+{
+ return _undoStack;
+}
+
+Graph *TikzDocument::graph() const
+{
+ return _graph;
+}
+
+QString TikzDocument::tikz() const
+{
+ return _tikz;
+}
+
+void TikzDocument::open(QString fileName)
+{
+ _fileName = fileName;
+ QFile file(fileName);
+ QFileInfo fi(file);
+ _shortName = fi.fileName();
+ QSettings settings("tikzit", "tikzit");
+ settings.setValue("previous-file-path", fi.absolutePath());
+
+ if (!file.open(QIODevice::ReadOnly)) {
+// QMessageBox::critical(this, tr("Error"),
+// tr("Could not open file"));
+ _parseSuccess = false;
+ return;
+ }
+
+ QTextStream in(&file);
+ _tikz = in.readAll();
+ file.close();
+
+ Graph *newGraph = new Graph(this);
+ TikzAssembler ass(newGraph);
+ if (ass.parse(_tikz)) {
+ delete _graph;
+ _graph = newGraph;
+ foreach (Node *n, _graph->nodes()) n->attachStyle();
+ foreach (Edge *e, _graph->edges()) {
+ e->attachStyle();
+ e->updateControls();
+ }
+ _parseSuccess = true;
+ refreshTikz();
+ setClean();
+ } else {
+ delete newGraph;
+ _parseSuccess = false;
+ }
+}
+
+bool TikzDocument::save() {
+ if (_fileName == "") {
+ return saveAs();
+ } else {
+ MainWindow *win = tikzit->activeWindow();
+ if (win != 0 && !win->tikzScene()->enabled()) {
+ win->tikzScene()->parseTikz(win->tikzSource());
+ if (!win->tikzScene()->enabled()) {
+ auto resp = QMessageBox::question(0,
+ tr("Tikz failed to parse"),
+ tr("Cannot save file with invalid TiKZ source. Revert changes and save?"));
+ if (resp == QMessageBox::Yes) win->tikzScene()->setEnabled(true);
+ else return false; // ABORT the save
+ }
+ }
+
+ refreshTikz();
+ QFile file(_fileName);
+ QFileInfo fi(file);
+ _shortName = fi.fileName();
+ QSettings settings("tikzit", "tikzit");
+ settings.setValue("previous-file-path", fi.absolutePath());
+
+ if (file.open(QIODevice::WriteOnly)) {
+ QTextStream stream(&file);
+ stream << _tikz;
+ file.close();
+ setClean();
+ return true;
+ } else {
+ QMessageBox::warning(0, "Save Failed", "Could not open file: '" + _fileName + "' for writing.");
+ }
+ }
+
+ return false;
+}
+
+bool TikzDocument::isClean() const
+{
+ return _undoStack->isClean();
+}
+
+void TikzDocument::setClean()
+{
+ _undoStack->setClean();
+}
+
+void TikzDocument::setGraph(Graph *graph)
+{
+ _graph = graph;
+ refreshTikz();
+}
+
+bool TikzDocument::saveAs() {
+ MainWindow *win = tikzit->activeWindow();
+ if (win != 0 && !win->tikzScene()->enabled()) {
+ win->tikzScene()->parseTikz(win->tikzSource());
+ if (!win->tikzScene()->enabled()) {
+ auto resp = QMessageBox::question(0,
+ tr("Tikz failed to parse"),
+ tr("Cannot save file with invalid TiKZ source. Revert changes and save?"));
+ if (resp == QMessageBox::Yes) win->tikzScene()->setEnabled(true);
+ else return false; // ABORT the save
+ }
+ }
+
+ QSettings settings("tikzit", "tikzit");
+
+ QFileDialog dialog;
+ dialog.setDefaultSuffix("tikz");
+ dialog.setWindowTitle(tr("Save File As"));
+ dialog.setAcceptMode(QFileDialog::AcceptSave);
+ dialog.setNameFilter(tr("TiKZ Files (*.tikz)"));
+ dialog.setFileMode(QFileDialog::AnyFile);
+ dialog.setDirectory(settings.value("previous-file-path").toString());
+ dialog.setOption(QFileDialog::DontUseNativeDialog);
+
+// QString fileName = QFileDialog::getSaveFileName(tikzit->activeWindow(),
+// tr("Save File As"),
+// settings.value("previous-file-path").toString(),
+// tr("TiKZ Files (*.tikz)"),
+// nullptr,
+// QFileDialog::DontUseNativeDialog);
+
+ if (dialog.exec() && !dialog.selectedFiles().isEmpty()) {
+ QString fileName = dialog.selectedFiles()[0];
+ _fileName = fileName;
+ if (save()) {
+ // clean state might not change, so update title bar manually
+ tikzit->activeWindow()->updateFileName();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+QString TikzDocument::shortName() const
+{
+ return _shortName;
+}
+
+bool TikzDocument::parseSuccess() const
+{
+ return _parseSuccess;
+}
+
+void TikzDocument::refreshTikz()
+{
+ _tikz = _graph->tikz();
+ if (MainWindow *w = dynamic_cast<MainWindow*>(parent()))
+ w->refreshTikz();
+}
diff --git a/src/data/tikzdocument.h b/src/data/tikzdocument.h new file mode 100644 index 0000000..fca5434 --- /dev/null +++ b/src/data/tikzdocument.h @@ -0,0 +1,69 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+/*!
+ * This class contains a tikz Graph, source code, file info, and undo stack. It serves as the model
+ * in the MVC triple (TikzDocument, TikzView, TikzScene).
+ */
+
+#ifndef TIKZDOCUMENT_H
+#define TIKZDOCUMENT_H
+
+#include "graph.h"
+
+#include <QObject>
+#include <QUndoStack>
+
+class TikzDocument : public QObject
+{
+ Q_OBJECT
+public:
+ explicit TikzDocument(QObject *parent = 0);
+ ~TikzDocument();
+
+ Graph *graph() const;
+ void setGraph(Graph *graph);
+ QString tikz() const;
+ QUndoStack *undoStack() const;
+ bool parseSuccess() const;
+ void refreshTikz();
+
+ void open(QString fileName);
+
+ QString shortName() const;
+
+ bool saveAs();
+ bool save();
+
+ bool isClean() const;
+ void setClean();
+
+private:
+ Graph *_graph;
+ QString _tikz;
+ QString _fileName;
+ QString _shortName;
+ QUndoStack *_undoStack;
+ bool _parseSuccess;
+
+signals:
+
+public slots:
+};
+
+#endif // TIKZDOCUMENT_H
diff --git a/src/data/tikzlexer.l b/src/data/tikzlexer.l new file mode 100644 index 0000000..0d80467 --- /dev/null +++ b/src/data/tikzlexer.l @@ -0,0 +1,193 @@ +%{
+/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger, Chris Heunen,
+ K. Johan Paulsson, Alex Merry
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+
+/*!
+ * \file tikzlexer.l
+ *
+ * The lexer for tikz input.
+ */
+
+#include "tikzparserdefs.h"
+#include "tikzparser.parser.hpp"
+
+#include <sstream>
+
+
+#define YY_USER_ACTION \
+ yylloc->first_line = yylloc->last_line; \
+ yylloc->first_column = yylloc->last_column + 1; \
+ yylloc->last_column = yylloc->first_column + yyleng - 1;
+
+%}
+
+%option reentrant bison-bridge bison-locations 8bit
+%option bison-locations 8bit
+%option nounput
+%option yylineno
+%option noyywrap
+%option header-file="tikzlexer.h"
+%option extra-type="TikzAssembler *"
+
+%s props
+%s xcoord
+%s ycoord
+%s noderef
+
+FLOAT \-?[0-9]*(\.[0-9]+)?
+
+%%
+
+ /* whitespace is ignored, except for position counting; we don't
+ count formfeed and vtab as whitespace, because it's not obvious
+ how they should be dealt with and no-one actually uses them */
+
+ /* lex will take the longest-matching string */
+<INITIAL,xcoord,ycoord,props,noderef>\r\n|\r|\n {
+ yylloc->first_line += 1;
+ yylloc->last_line = yylloc->first_line;
+ yylloc->first_column = yylloc->last_column = 0;
+}
+<INITIAL,xcoord,ycoord,props,noderef>[\t ]+ { }
+<INITIAL,xcoord,ycoord,props,noderef>%.*$ { }
+
+\\begin\{tikzpicture\} { return BEGIN_TIKZPICTURE_CMD; }
+\\end\{tikzpicture\} { return END_TIKZPICTURE_CMD; }
+\\tikzstyle { return TIKZSTYLE_CMD; }
+\\begin\{pgfonlayer\} { return BEGIN_PGFONLAYER_CMD; }
+\\end\{pgfonlayer\} { return END_PGFONLAYER_CMD; }
+\\draw { return DRAW_CMD; }
+\\node { return NODE_CMD; }
+\\path { return PATH_CMD; }
+; { return SEMICOLON; }
+= { return EQUALS; }
+<INITIAL>rectangle { return RECTANGLE; }
+<INITIAL>node { return NODE; }
+<INITIAL>at { return AT; }
+<INITIAL>to { return TO; }
+
+\([ ]*{FLOAT}[ ]*,[ ]*{FLOAT}[ ]*\) {
+ yylloc->last_column = yylloc->first_column + 1;
+ yyless(1);
+ BEGIN(xcoord);
+}
+<xcoord>{FLOAT} {
+ yylval->pt = new QPointF();
+ QString s(yytext);
+ yylval->pt->setX(s.toDouble());
+ BEGIN(ycoord);
+}
+<ycoord>, { }
+<ycoord>{FLOAT} {
+ QString s(yytext);
+ yylval->pt->setY(s.toDouble());
+}
+<ycoord>\) {
+ BEGIN(INITIAL);
+ return TCOORD;
+}
+
+ /* when we see "[", change parsing mode */
+\[ /*syntaxhlfix]*/ {
+ BEGIN(props);
+ return LEFTBRACKET;
+}
+<props>= { return EQUALS; }
+<props>, { return COMMA; }
+ /* technically, it is possible to have newlines in the middle of
+ property names or values, but in practice this is unlikely and
+ screws up our line counting */
+<props>[^=,\{\] \t\n]([^=,\{\]\n]*[^=,\{\] \t\n])? {
+ char *str = (char*)malloc(sizeof(char)*yyleng + 1);
+ strncpy(str, yytext, yyleng + 1);
+ yylval->str = str;
+ return PROPSTRING;
+}
+<props>\] {
+ BEGIN(INITIAL);
+ return RIGHTBRACKET;
+}
+
+\( {
+ BEGIN(noderef);
+ return LEFTPARENTHESIS;
+}
+<noderef>\. {
+ return FULLSTOP;
+}
+ /* we assume node names (and anchor names) never contain
+ newlines */
+<noderef>[^\.\{\)\n]+ {
+ //qDebug() << "nodename: " << yytext << " size: " << strlen(yytext);
+ char *str = (char*)malloc(sizeof(char)*yyleng + 1);
+ strncpy(str, yytext, yyleng+1);
+ yylval->str = str;
+ return REFSTRING;
+}
+<noderef>\) {
+ BEGIN(INITIAL);
+ return RIGHTPARENTHESIS;
+}
+
+<INITIAL,props>\{ {
+ std::stringstream buf;
+ unsigned int brace_depth = 1;
+ unsigned int escape = 0;
+ while (1) {
+ char c = yyinput(yyscanner);
+ // eof reached before closing brace
+ if (c == '\0' || c == EOF) {
+ return UNCLOSED_DELIM_STR;
+ }
+
+ yylloc->last_column += 1;
+ yyleng += 1;
+ if (escape) {
+ escape = 0;
+ } else if (c == '\\') {
+ escape = 1;
+ } else if (c == '{') {
+ brace_depth++;
+ } else if (c == '}') {
+ brace_depth--;
+ if (brace_depth == 0) break;
+ } else if (c == '\n') {
+ yylloc->last_line += 1;
+ yylloc->last_column = 0;
+ }
+ buf << c;
+ }
+
+ char *str = (char*)malloc(sizeof(char) * yyleng + 1);
+ strncpy(str, buf.str().c_str(), yyleng + 1);
+ //str[len] = 0;
+ yylval->str = str;
+ //qDebug() << "got delim string: " << str;
+ return DELIMITEDSTRING;
+}
+
+\\begin { return UNKNOWN_BEGIN_CMD; }
+\\end { return UNKNOWN_END_CMD; }
+\\[a-zA-Z0-9]+ { return UNKNOWN_CMD; }
+[a-zA-Z0-9]+ { return UNKNOWN_STR; }
+<INITIAL,xcoord,ycoord,props,noderef>. { return UNKNOWN_STR; }
+
+ /* vi:ft=lex:noet:ts=4:sts=4:sw=4:
+ */
diff --git a/src/data/tikzparser.y b/src/data/tikzparser.y new file mode 100644 index 0000000..4473107 --- /dev/null +++ b/src/data/tikzparser.y @@ -0,0 +1,284 @@ +%{
+/*!
+ * \file tikzparser.y
+ *
+ * The parser for tikz input.
+ */
+
+/*
+ * Copyright 2010 Chris Heunen
+ * Copyright 2010-2017 Aleks Kissinger
+ * Copyright 2013 K. Johan Paulsson
+ * Copyright 2013 Alex Merry <dev@randomguy3.me.uk>
+ *
+ * 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.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include "tikzparserdefs.h"
+%}
+
+/* we use features added to bison 2.4 */
+%require "2.3"
+
+%error-verbose
+/* enable maintaining locations for better error messages */
+%locations
+/* the name of the header file */
+/*%defines "common/tikzparser.h"*/
+/* make it re-entrant (no global variables) */
+%pure-parser
+/* We use a pure (re-entrant) lexer. This means yylex
+ will take a void* (opaque) type to maintain its state */
+%lex-param {void *scanner}
+/* Since this parser is also pure, yyparse needs to take
+ that lexer state as an argument */
+%parse-param {void *scanner}
+
+/* possible data types for semantic values */
+%union {
+ char *str;
+ GraphElementProperty *prop;
+ GraphElementData *data;
+ Node *node;
+ QPointF *pt;
+ struct noderef noderef;
+}
+
+%{
+#include "node.h"
+#include "edge.h"
+#include "graphelementdata.h"
+#include "graphelementproperty.h"
+
+#include "tikzlexer.h"
+#include "tikzassembler.h"
+/* the assembler (used by this parser) is stored in the lexer
+ state as "extra" data */
+#define assembler yyget_extra(scanner)
+
+/* pass errors off to the assembler */
+void yyerror(YYLTYPE *yylloc, void * /*scanner*/, const char *str) {
+ // TODO: implement reportError()
+ //assembler->reportError(str, yylloc);
+ qDebug() << "\nparse error: " << str << " line:" << yylloc->first_line;
+}
+%}
+
+/* yyloc is set up with first_column = last_column = 1 by default;
+ however, it makes more sense to think of us being "before the
+ start of the line" before we parse anything */
+%initial-action {
+ yylloc.first_column = yylloc.last_column = 0;
+}
+
+
+%token BEGIN_TIKZPICTURE_CMD "\\begin{tikzpicture}"
+%token END_TIKZPICTURE_CMD "\\end{tikzpicture}"
+%token TIKZSTYLE_CMD "\\tikzstyle"
+%token BEGIN_PGFONLAYER_CMD "\\begin{pgfonlayer}"
+%token END_PGFONLAYER_CMD "\\end{pgfonlayer}"
+%token DRAW_CMD "\\draw"
+%token NODE_CMD "\\node"
+%token PATH_CMD "\\path"
+%token RECTANGLE "rectangle"
+%token NODE "node"
+%token AT "at"
+%token TO "to"
+%token SEMICOLON ";"
+%token COMMA ","
+
+%token LEFTPARENTHESIS "("
+%token RIGHTPARENTHESIS ")"
+%token LEFTBRACKET "["
+%token RIGHTBRACKET "]"
+%token FULLSTOP "."
+%token EQUALS "="
+%token <pt> TCOORD "coordinate"
+%token <str> PROPSTRING "key/value string"
+%token <str> REFSTRING "string"
+%token <str> DELIMITEDSTRING "{-delimited string"
+
+%token UNKNOWN_BEGIN_CMD "unknown \\begin command"
+%token UNKNOWN_END_CMD "unknown \\end command"
+%token UNKNOWN_CMD "unknown latex command"
+%token UNKNOWN_STR "unknown string"
+%token UNCLOSED_DELIM_STR "unclosed {-delimited string"
+
+%type<str> nodename
+%type<str> optanchor
+%type<str> val
+%type<prop> property
+%type<data> extraproperties
+%type<data> properties
+%type<data> optproperties
+%type<node> optedgenode
+%type<noderef> noderef
+%type<noderef> optnoderef
+
+%%
+
+
+tikz: tikzstyles | tikzpicture;
+
+tikzstyles: tikzstyles tikzstyle | ;
+tikzstyle: "\\tikzstyle" DELIMITEDSTRING "=" "[" properties "]"
+ {
+ if (assembler->isTikzStyles()) {
+ assembler->tikzStyles()->addStyle(QString($2), $5);
+ }
+ }
+
+tikzpicture: "\\begin{tikzpicture}" optproperties tikzcmds "\\end{tikzpicture}"
+ {
+ if (assembler->isGraph() && $2) {
+ assembler->graph()->setData($2);
+ }
+ };
+tikzcmds: tikzcmds tikzcmd | ;
+tikzcmd: node | edge | boundingbox | ignore;
+
+ignore: "\\begin{pgfonlayer}" DELIMITEDSTRING | "\\end{pgfonlayer}";
+
+optproperties:
+ "[" "]"
+ { $$ = 0; }
+ | "[" properties "]"
+ { $$ = $2; }
+ | { $$ = 0; };
+properties: extraproperties property
+ {
+ $1->add(*$2);
+ delete $2;
+ $$ = $1;
+ };
+extraproperties:
+ extraproperties property ","
+ {
+ $1->add(*$2);
+ delete $2;
+ $$ = $1;
+ }
+ | { $$ = new GraphElementData(); };
+property:
+ val "=" val
+ {
+ GraphElementProperty *p = new GraphElementProperty(QString($1),QString($3));
+ free($1);
+ free($3);
+ $$ = p;
+ }
+ | val
+ {
+ GraphElementProperty *a = new GraphElementProperty(QString($1));
+ free($1);
+ $$ = a;
+ };
+val: PROPSTRING { $$ = $1; } | DELIMITEDSTRING { $$ = $1; };
+
+nodename: "(" REFSTRING ")" { $$ = $2; };
+node: "\\node" optproperties nodename "at" TCOORD DELIMITEDSTRING ";"
+ {
+ Node *node = new Node();
+
+ if ($2) {
+ node->setData($2);
+ }
+ //qDebug() << "node name: " << $3;
+ node->setName(QString($3));
+ node->setLabel(QString($6));
+ free($3);
+ free($6);
+
+ node->setPoint(*$5);
+ delete $5;
+
+ assembler->graph()->addNode(node);
+ assembler->addNodeToMap(node);
+ };
+
+optanchor: { $$ = 0; } | "." REFSTRING { $$ = $2; };
+noderef: "(" REFSTRING optanchor ")"
+ {
+ $$.node = assembler->nodeWithName(QString($2));
+ free($2);
+ $$.anchor = $3;
+ };
+optnoderef:
+ noderef { $$ = $1; }
+ | "(" ")" { $$.node = 0; $$.anchor = 0; }
+optedgenode:
+ { $$ = 0; }
+ | "node" optproperties DELIMITEDSTRING
+ {
+ $$ = new Node();
+ if ($2)
+ $$->setData($2);
+ $$->setLabel(QString($3));
+ free($3);
+ }
+edge: "\\draw" optproperties noderef "to" optedgenode optnoderef ";"
+ {
+ Node *s;
+ Node *t;
+
+ s = $3.node;
+
+ if ($6.node) {
+ t = $6.node;
+ } else {
+ t = s;
+ }
+
+ // if the source or the target of the edge doesn't exist, quietly ignore it.
+ if (s != 0 && t != 0) {
+ Edge *edge = new Edge(s, t);
+ if ($2) {
+ edge->setData($2);
+ edge->setAttributesFromData();
+ }
+
+ if ($5)
+ edge->setEdgeNode($5);
+ if ($3.anchor) {
+ edge->setSourceAnchor(QString($3.anchor));
+ free($3.anchor);
+ }
+
+ if ($6.node) {
+ if ($6.anchor) {
+ edge->setTargetAnchor(QString($6.anchor));
+ free($6.anchor);
+ }
+ } else {
+ edge->setTargetAnchor(edge->sourceAnchor());
+ }
+
+ assembler->graph()->addEdge(edge);
+ }
+ };
+
+ignoreprop: val | val "=" val;
+ignoreprops: ignoreprop ignoreprops | ;
+optignoreprops: "[" ignoreprops "]";
+boundingbox:
+ "\\path" optignoreprops TCOORD "rectangle" TCOORD ";"
+ {
+ assembler->graph()->setBbox(QRectF(*$3, *$5));
+ delete $3;
+ delete $5;
+ };
+
+/* vi:ft=yacc:noet:ts=4:sts=4:sw=4
+*/
diff --git a/src/data/tikzparserdefs.h b/src/data/tikzparserdefs.h new file mode 100644 index 0000000..02743fe --- /dev/null +++ b/src/data/tikzparserdefs.h @@ -0,0 +1,40 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef TIKZPARSERDEFS_H
+#define TIKZPARSERDEFS_H
+
+#define YY_NO_UNISTD_H 1
+
+#include "graphelementproperty.h"
+#include "graphelementdata.h"
+#include "node.h"
+#include "tikzassembler.h"
+
+#include <QString>
+#include <QRectF>
+#include <QDebug>
+
+struct noderef {
+ Node *node;
+ char *anchor;
+};
+
+inline int isatty(int) { return 0; }
+
+#endif // TIKZPARSERDEFS_H
diff --git a/src/data/tikzstyles.cpp b/src/data/tikzstyles.cpp new file mode 100644 index 0000000..522b3f5 --- /dev/null +++ b/src/data/tikzstyles.cpp @@ -0,0 +1,181 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#include "tikzstyles.h"
+#include "tikzassembler.h"
+
+#include <QDebug>
+#include <QColorDialog>
+#include <QFile>
+#include <QFileInfo>
+
+TikzStyles::TikzStyles(QObject *parent) : QObject(parent)
+{
+ _nodeStyles = new StyleList(false, this);
+ _edgeStyles = new StyleList(true, this);
+}
+
+Style *TikzStyles::nodeStyle(QString name) const
+{
+ Style *s = _nodeStyles->style(name);
+
+ return (s == nullptr) ? unknownStyle : s;
+}
+
+Style *TikzStyles::edgeStyle(QString name) const
+{
+ Style *s = _edgeStyles->style(name);
+ return (s == nullptr) ? noneEdgeStyle : s;
+}
+
+
+void TikzStyles::clear()
+{
+ _nodeStyles->clear();
+ _edgeStyles->clear();
+}
+
+bool TikzStyles::loadStyles(QString fileName)
+{
+ QFile file(fileName);
+ if (file.open(QIODevice::ReadOnly)) {
+ QTextStream in(&file);
+ QString styleTikz = in.readAll();
+ file.close();
+
+ clear();
+ TikzAssembler ass(this);
+ return ass.parse(styleTikz);
+ } else {
+ return false;
+ }
+}
+
+bool TikzStyles::saveStyles(QString fileName)
+{
+ QFile file(fileName);
+ if (file.open(QIODevice::WriteOnly)) {
+ QTextStream stream(&file);
+ stream << tikz();
+ file.close();
+ return true;
+ }
+ return false;
+}
+
+void TikzStyles::refreshModels(QStandardItemModel *nodeModel, QStandardItemModel *edgeModel, QString category, bool includeNone)
+{
+ nodeModel->clear();
+ edgeModel->clear();
+
+
+ //QString f = tikzit->styleFile();
+ //ui->styleFile->setText(f);
+
+ QStandardItem *it;
+
+ if (includeNone) {
+ it = new QStandardItem(noneStyle->icon(), noneStyle->name());
+ it->setEditable(false);
+ it->setData(noneStyle->name());
+ nodeModel->appendRow(it);
+ it->setTextAlignment(Qt::AlignCenter);
+ it->setSizeHint(QSize(48,48));
+ }
+
+ Style *s;
+ for (int i = 0; i < _nodeStyles->length(); ++i) {
+ s = _nodeStyles->style(i);
+ if (category == "" || category == s->propertyWithDefault("tikzit category", "", false))
+ {
+ it = new QStandardItem(s->icon(), s->name());
+ it->setEditable(false);
+ it->setData(s->name());
+ it->setSizeHint(QSize(48,48));
+ nodeModel->appendRow(it);
+ }
+ }
+
+ if (includeNone) {
+ it = new QStandardItem(noneEdgeStyle->icon(), noneEdgeStyle->name());
+ it->setEditable(false);
+ it->setData(noneEdgeStyle->name());
+ edgeModel->appendRow(it);
+ }
+
+ for (int i = 0; i < _edgeStyles->length(); ++i) {
+ s = _edgeStyles->style(i);
+ it = new QStandardItem(s->icon(), s->name());
+ it->setEditable(false);
+ it->setData(s->name());
+ it->setSizeHint(QSize(48,48));
+ edgeModel->appendRow(it);
+ }
+}
+
+StyleList *TikzStyles::nodeStyles() const
+{
+ return _nodeStyles;
+}
+
+StyleList *TikzStyles::edgeStyles() const
+{
+ return _edgeStyles;
+}
+
+QStringList TikzStyles::categories() const
+{
+ QMap<QString,bool> cats; // use a QMap to keep keys sorted
+ cats.insert("", true);
+ Style *ns;
+ for (int i = 0; i < _nodeStyles->length(); ++i) {
+ ns = _nodeStyles->style(i);
+ cats.insert(ns->propertyWithDefault("tikzit category", "", false), true);
+ }
+ //foreach (EdgeStyle *s, _edgeStyles) cats << s->propertyWithDefault("tikzit category", "", false);
+ return QStringList(cats.keys());
+}
+
+QString TikzStyles::tikz() const
+{
+ QString str;
+ QTextStream code(&str);
+
+ code << "% TiKZ style file generated by TikZiT. You may edit this file manually,\n";
+ code << "% but some things (e.g. comments) may be overwritten. To be readable in\n";
+ code << "% TikZiT, the only non-comment lines must be of the form:\n";
+ code << "% \\tikzstyle{NAME}=[PROPERTY LIST]\n\n";
+
+ code << "% Node styles\n";
+ code << _nodeStyles->tikz();
+
+ code << "\n% Edge styles\n";
+ code << _edgeStyles->tikz();
+
+ code.flush();
+ return str;
+}
+
+void TikzStyles::addStyle(QString name, GraphElementData *data)
+{
+ Style *s = new Style(name, data);
+ if (s->isEdgeStyle()) _edgeStyles->addStyle(s);
+ else _nodeStyles->addStyle(s);
+}
+
+
diff --git a/src/data/tikzstyles.h b/src/data/tikzstyles.h new file mode 100644 index 0000000..5f372ab --- /dev/null +++ b/src/data/tikzstyles.h @@ -0,0 +1,65 @@ +/*
+ TikZiT - a GUI diagram editor for TikZ
+ Copyright (C) 2018 Aleks Kissinger
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+#ifndef PROJECT_H
+#define PROJECT_H
+
+#include "graphelementdata.h"
+#include "stylelist.h"
+#include "style.h"
+
+#include <QObject>
+#include <QString>
+#include <QColor>
+#include <QStandardItemModel>
+
+class TikzStyles : public QObject
+{
+ Q_OBJECT
+public:
+ explicit TikzStyles(QObject *parent = 0);
+ void addStyle(QString name, GraphElementData *data);
+
+ Style *nodeStyle(QString name) const;
+ Style *edgeStyle(QString name) const;
+ QStringList categories() const;
+ QString tikz() const;
+ void clear();
+
+ bool loadStyles(QString fileName);
+ bool saveStyles(QString fileName);
+ void refreshModels(QStandardItemModel *nodeModel,
+ QStandardItemModel *edgeModel,
+ QString category="",
+ bool includeNone=true);
+
+ StyleList *nodeStyles() const;
+ StyleList *edgeStyles() const;
+
+signals:
+
+public slots:
+
+private:
+ StyleList *_nodeStyles;
+ StyleList* _edgeStyles;
+ QStringList _colNames;
+ QVector<QColor> _cols;
+};
+
+#endif // PROJECT_H
diff --git a/src/gui/commands.cpp b/src/gui/commands.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/gui/commands.cpp diff --git a/src/gui/commands.h b/src/gui/commands.h new file mode 100644 index 0000000..73bfaa7 --- /dev/null +++ b/src/gui/commands.h @@ -0,0 +1,4 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +#endif // COMMANDS_H diff --git a/src/gui/edgeitem.cpp b/src/gui/edgeitem.cpp new file mode 100644 index 0000000..48f321e --- /dev/null +++ b/src/gui/edgeitem.cpp @@ -0,0 +1,230 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "tikzit.h" +#include "edgeitem.h" + +#include <QPainterPath> +#include <QPen> + +EdgeItem::EdgeItem(Edge *edge) +{ + _edge = edge; + setFlag(QGraphicsItem::ItemIsSelectable); + + _cp1Item = new QGraphicsEllipseItem(this); + _cp1Item->setParentItem(this); + _cp1Item->setRect(GLOBAL_SCALEF * (-0.1), GLOBAL_SCALEF * (-0.1), + GLOBAL_SCALEF * 0.2, GLOBAL_SCALEF * 0.2); + _cp1Item->setVisible(false); + + _cp2Item = new QGraphicsEllipseItem(this); + _cp2Item->setParentItem(this); + _cp2Item->setRect(GLOBAL_SCALEF * (-0.1), GLOBAL_SCALEF * (-0.1), + GLOBAL_SCALEF * 0.2, GLOBAL_SCALEF * 0.2); + _cp2Item->setVisible(false); + + readPos(); +} + +void EdgeItem::readPos() +{ + //_edge->setAttributesFromData(); + _edge->updateControls(); + QPainterPath path; + + path.moveTo (toScreen(_edge->tail())); + + if (_edge->bend() != 0 || !_edge->basicBendMode()) { + path.cubicTo(toScreen(_edge->cp1()), + toScreen(_edge->cp2()), + toScreen(_edge->head())); + } + else { + path.lineTo(toScreen(_edge->head())); + } + + setPath(path); + + _cp1Item->setPos(toScreen(_edge->cp1())); + _cp2Item->setPos(toScreen(_edge->cp2())); +} + +void EdgeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + //QGraphicsPathItem::paint(painter, option, widget); + QPen pen = _edge->style()->pen(); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path()); + + QPointF ht = _edge->headTangent(); + QPointF hLeft(-ht.y(), ht.x()); + QPointF hRight(ht.y(), -ht.x()); + QPointF tt = _edge->tailTangent(); + QPointF tLeft(-ht.y(), ht.x()); + QPointF tRight(ht.y(), -ht.x()); + + pen.setStyle(Qt::SolidLine); + painter->setPen(pen); + + + + switch (_edge->style()->arrowHead()) { + case Style::Flat: + { + painter->drawLine( + toScreen(_edge->head() + hLeft), + toScreen(_edge->head() + hRight)); + break; + } + case Style::Pointer: + { + QPainterPath pth; + pth.moveTo(toScreen(_edge->head() + ht + hLeft)); + pth.lineTo(toScreen(_edge->head())); + pth.lineTo(toScreen(_edge->head() + ht + hRight)); + painter->drawPath(pth); + break; + } + case Style::NoTip: + break; + } + + //QPen outline = QPen(Qt::red); + //painter->setPen(outline); + //painter->drawPath(_expPath); + //painter->setPen(pen); + + switch (_edge->style()->arrowTail()) { + case Style::Flat: + { + painter->drawLine( + toScreen(_edge->tail() + tLeft), + toScreen(_edge->tail() + tRight)); + break; + } + case Style::Pointer: + { + QPainterPath pth; + pth.moveTo(toScreen(_edge->tail() + tt + tLeft)); + pth.lineTo(toScreen(_edge->tail())); + pth.lineTo(toScreen(_edge->tail() + tt + tRight)); + painter->drawPath(pth); + break; + } + case Style::NoTip: + break; + } + + if (isSelected()) { + QColor draw; + QColor draw1; + QColor fill; + + if (_edge->basicBendMode()) { + draw = Qt::blue; + draw1 = QColor(100,100,255,100); + fill = QColor(200,200,255,50); + } else { + draw = Qt::darkGreen; + draw1 = QColor(0, 150, 0, 50); + fill = QColor(200,255,200,150); + } + + painter->setPen(QPen(draw1)); + + float r = GLOBAL_SCALEF * _edge->cpDist(); + painter->drawEllipse(toScreen(_edge->source()->point()), r, r); + painter->drawEllipse(toScreen(_edge->target()->point()), r, r); + + painter->setPen(QPen(draw)); + painter->setBrush(QBrush(fill)); + + painter->drawLine(toScreen(_edge->tail()), toScreen(_edge->cp1())); + painter->drawLine(toScreen(_edge->head()), toScreen(_edge->cp2())); + + //painter->drawEllipse(toScreen(_edge->cp1()), r, r); + //painter->drawEllipse(toScreen(_edge->cp2()), r, r); + + _cp1Item->setPen(QPen(draw)); + _cp1Item->setBrush(QBrush(fill)); + _cp1Item->setVisible(true); + + _cp2Item->setPen(QPen(draw)); + _cp2Item->setBrush(QBrush(fill)); + _cp2Item->setVisible(true); + + r = GLOBAL_SCALEF * 0.05; + painter->setPen(QPen(Qt::black)); + painter->setBrush(QBrush(QColor(255,255,255,200))); + painter->drawEllipse(toScreen(_edge->mid()), r, r); + } else { + _cp1Item->setVisible(false); + _cp2Item->setVisible(false); + } +} + +QRectF EdgeItem::boundingRect() const +{ + return _boundingRect; +} + +QPainterPath EdgeItem::shape() const +{ + return _expPath; +} + +Edge *EdgeItem::edge() const +{ + return _edge; +} + +QGraphicsEllipseItem *EdgeItem::cp1Item() const +{ + return _cp1Item; +} + +QGraphicsEllipseItem *EdgeItem::cp2Item() const +{ + return _cp2Item; +} + +QPainterPath EdgeItem::path() const +{ + return _path; +} + +void EdgeItem::setPath(const QPainterPath &path) +{ + prepareGeometryChange(); + + _path = path; + + // get the shape of the edge, and expand a bit to make selection easier + QPainterPathStroker stroker; + stroker.setWidth(8); + stroker.setJoinStyle(Qt::MiterJoin); + _expPath = stroker.createStroke(_path).simplified(); + + float r = GLOBAL_SCALEF * (_edge->cpDist() + 0.2); + _boundingRect = _path.boundingRect().adjusted(-r,-r,r,r); + + update(); +} + diff --git a/src/gui/edgeitem.h b/src/gui/edgeitem.h new file mode 100644 index 0000000..c03edd6 --- /dev/null +++ b/src/gui/edgeitem.h @@ -0,0 +1,62 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * A QGraphicsItem that handles drawing a single edge. + */ + +#ifndef EDGEITEM_H +#define EDGEITEM_H + +#include "edge.h" + +#include <QObject> +#include <QGraphicsPathItem> +#include <QPainter> +#include <QStyleOptionGraphicsItem> +#include <QWidget> +#include <QGraphicsEllipseItem> +#include <QString> + +class EdgeItem : public QGraphicsItem +{ +public: + EdgeItem(Edge *edge); + void readPos(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; + QRectF boundingRect() const override; + QPainterPath shape() const override; + Edge *edge() const; + QGraphicsEllipseItem *cp1Item() const; + QGraphicsEllipseItem *cp2Item() const; + + + QPainterPath path() const; + void setPath(const QPainterPath &path); + + +private: + Edge *_edge; + QPainterPath _path; + QPainterPath _expPath; + QRectF _boundingRect; + QGraphicsEllipseItem *_cp1Item; + QGraphicsEllipseItem *_cp2Item; +}; + +#endif // EDGEITEM_H diff --git a/src/gui/mainmenu.cpp b/src/gui/mainmenu.cpp new file mode 100644 index 0000000..8166c59 --- /dev/null +++ b/src/gui/mainmenu.cpp @@ -0,0 +1,264 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "mainmenu.h" +#include "tikzit.h" + +#include <QDebug> +#include <QSettings> +#include <QMessageBox> + +MainMenu::MainMenu() +{ + QSettings settings("tikzit", "tikzit"); + ui.setupUi(this); + + if (!settings.value("check-for-updates").isNull()) { + ui.actionCheck_for_updates_automatically->blockSignals(true); + ui.actionCheck_for_updates_automatically->setChecked(settings.value("check-for-updates").toBool()); + ui.actionCheck_for_updates_automatically->blockSignals(false); + } +} + +void MainMenu::addDocks(QMenu *m) +{ + ui.menuView->addSeparator(); + foreach (QAction *a, m->actions()) { + if (!a->isSeparator()) ui.menuView->addAction(a); + } +} + +QAction *MainMenu::updatesAction() +{ + return ui.actionCheck_for_updates_automatically; +} + +// File +void MainMenu::on_actionNew_triggered() +{ + tikzit->newDoc(); +} + +void MainMenu::on_actionOpen_triggered() +{ + tikzit->open(); +} + +void MainMenu::on_actionClose_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->close(); +} + +void MainMenu::on_actionSave_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzDocument()->save(); +} + +void MainMenu::on_actionSave_As_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzDocument()->saveAs(); +} + +void MainMenu::on_actionExit_triggered() +{ + tikzit->quit(); +} + + +// Edit +void MainMenu::on_actionUndo_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzDocument()->undoStack()->undo(); +} + +void MainMenu::on_actionRedo_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzDocument()->undoStack()->redo(); +} + +void MainMenu::on_actionCut_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->cutToClipboard(); +} + +void MainMenu::on_actionCopy_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->copyToClipboard(); +} + +void MainMenu::on_actionPaste_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->pasteFromClipboard(); +} + +void MainMenu::on_actionDelete_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->deleteSelectedItems(); +} + +void MainMenu::on_actionSelect_All_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->selectAllNodes(); +} + +void MainMenu::on_actionDeselect_All_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->deselectAll(); +} + +void MainMenu::on_actionReflectHorizontal_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->reflectNodes(true); +} + +void MainMenu::on_actionReflectVertical_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->reflectNodes(false); +} + +void MainMenu::on_actionRotateCW_triggered() { + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->rotateNodes(true); +} + +void MainMenu::on_actionRotateCCW_triggered() { + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->rotateNodes(false); +} + +void MainMenu::on_actionBring_to_Front_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->reorderSelection(true); +} + +void MainMenu::on_actionSend_to_Back_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->reorderSelection(false); +} + +void MainMenu::on_actionExtendUp_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->extendSelectionUp(); +} + +void MainMenu::on_actionExtendDown_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->extendSelectionDown(); +} + +void MainMenu::on_actionExtendLeft_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->extendSelectionLeft(); +} + +void MainMenu::on_actionExtendRight_triggered() +{ + if (tikzit->activeWindow() != 0) + tikzit->activeWindow()->tikzScene()->extendSelectionRight(); +} + + +// Tikz +void MainMenu::on_actionParse_triggered() +{ + MainWindow *win = tikzit->activeWindow(); + if (win != 0) { + if (win->tikzScene()->parseTikz(win->tikzSource())) { + QList<int> sz = win->splitter()->sizes(); + sz[0] = sz[0] + sz[1]; + sz[1] = 0; + win->splitter()->setSizes(sz); + } + } +} + +void MainMenu::on_actionRevert_triggered() +{ + MainWindow *win = tikzit->activeWindow(); + if (win != 0) { + win->tikzDocument()->refreshTikz(); + win->tikzScene()->setEnabled(true); + } +} + +void MainMenu::on_actionJump_to_Selection_triggered() +{ + MainWindow *win = tikzit->activeWindow(); + if (win != 0) { + //qDebug() << "jump to selection on line:" << win->tikzScene()->lineNumberForSelection(); + QList<int> sz = win->splitter()->sizes(); + if (sz[1] == 0) { + sz[1] = 200; + win->splitter()->setSizes(sz); + } + win->setSourceLine(win->tikzScene()->lineNumberForSelection()); + } +} + + +// View +void MainMenu::on_actionZoom_In_triggered() +{ + if (tikzit->activeWindow() != 0) tikzit->activeWindow()->tikzView()->zoomIn(); +} + +void MainMenu::on_actionZoom_Out_triggered() +{ + if (tikzit->activeWindow() != 0) tikzit->activeWindow()->tikzView()->zoomOut(); +} + +void MainMenu::on_actionAbout_triggered() +{ + QMessageBox::about(this, + "TikZiT", + "<h2><b>TikZiT</b></h2>" + "<p><i>version " TIKZIT_VERSION "</i></p>" + "<p>TikZiT is a GUI diagram editor for PGF/TikZ. It is licensed under the " + "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\">GNU General " + "Public License, version 3.0</a>.</p>" + "<p>For more info and updates, visit: " + "<a href=\"https://tikzit.github.io\">tikzit.github.io</a></p>"); +} + +void MainMenu::on_actionCheck_for_updates_automatically_triggered() +{ + tikzit->setCheckForUpdates(ui.actionCheck_for_updates_automatically->isChecked()); +} + +void MainMenu::on_actionCheck_now_triggered() +{ + tikzit->checkForUpdates(); +} diff --git a/src/gui/mainmenu.h b/src/gui/mainmenu.h new file mode 100644 index 0000000..c14a284 --- /dev/null +++ b/src/gui/mainmenu.h @@ -0,0 +1,81 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#ifndef MAINMENU_H +#define MAINMENU_H + +#include "ui_mainmenu.h" + +#include <QMenuBar> + +class MainMenu : public QMenuBar +{ + Q_OBJECT +public: + MainMenu(); + void addDocks(QMenu *m); + QAction *updatesAction(); + +private: + Ui::MainMenu ui; + +public slots: + // File + void on_actionNew_triggered(); + void on_actionOpen_triggered(); + void on_actionClose_triggered(); + void on_actionSave_triggered(); + void on_actionSave_As_triggered(); + void on_actionExit_triggered(); + + // Edit + void on_actionUndo_triggered(); + void on_actionRedo_triggered(); + void on_actionCut_triggered(); + void on_actionCopy_triggered(); + void on_actionPaste_triggered(); + void on_actionDelete_triggered(); + void on_actionSelect_All_triggered(); + void on_actionDeselect_All_triggered(); + void on_actionReflectHorizontal_triggered(); + void on_actionReflectVertical_triggered(); + void on_actionRotateCW_triggered(); + void on_actionRotateCCW_triggered(); + void on_actionBring_to_Front_triggered(); + void on_actionSend_to_Back_triggered(); + void on_actionExtendUp_triggered(); + void on_actionExtendDown_triggered(); + void on_actionExtendLeft_triggered(); + void on_actionExtendRight_triggered(); + + // Tikz + void on_actionParse_triggered(); + void on_actionRevert_triggered(); + void on_actionJump_to_Selection_triggered(); + + // View + void on_actionZoom_In_triggered(); + void on_actionZoom_Out_triggered(); + + // Help + void on_actionAbout_triggered(); + void on_actionCheck_for_updates_automatically_triggered(); + void on_actionCheck_now_triggered(); +}; + +#endif // MAINMENU_H diff --git a/src/gui/mainmenu.ui b/src/gui/mainmenu.ui new file mode 100644 index 0000000..0481c1d --- /dev/null +++ b/src/gui/mainmenu.ui @@ -0,0 +1,346 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainMenu</class> + <widget class="QMenuBar" name="MainMenu"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>476</width> + <height>22</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionNew"/> + <addaction name="actionOpen"/> + <addaction name="separator"/> + <addaction name="actionClose"/> + <addaction name="actionSave"/> + <addaction name="actionSave_As"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuEdit"> + <property name="title"> + <string>Edit</string> + </property> + <widget class="QMenu" name="menuSelect"> + <property name="title"> + <string>Extend Selection</string> + </property> + <addaction name="actionExtendLeft"/> + <addaction name="actionExtendRight"/> + <addaction name="actionExtendUp"/> + <addaction name="actionExtendDown"/> + </widget> + <widget class="QMenu" name="menuReorder"> + <property name="title"> + <string>Reorder</string> + </property> + <addaction name="actionBring_to_Front"/> + <addaction name="actionSend_to_Back"/> + </widget> + <widget class="QMenu" name="menuTransform"> + <property name="title"> + <string>Transform</string> + </property> + <addaction name="actionReflectHorizontal"/> + <addaction name="actionReflectVertical"/> + <addaction name="actionRotateCW"/> + <addaction name="actionRotateCCW"/> + </widget> + <addaction name="actionUndo"/> + <addaction name="actionRedo"/> + <addaction name="separator"/> + <addaction name="actionCut"/> + <addaction name="actionCopy"/> + <addaction name="actionPaste"/> + <addaction name="actionDelete"/> + <addaction name="separator"/> + <addaction name="actionSelect_All"/> + <addaction name="actionDeselect_All"/> + <addaction name="menuSelect"/> + <addaction name="separator"/> + <addaction name="menuReorder"/> + <addaction name="menuTransform"/> + </widget> + <widget class="QMenu" name="menuTikz"> + <property name="title"> + <string>Tikz</string> + </property> + <addaction name="actionParse"/> + <addaction name="actionRevert"/> + <addaction name="actionJump_to_Selection"/> + </widget> + <widget class="QMenu" name="menuView"> + <property name="title"> + <string>View</string> + </property> + <addaction name="actionZoom_In"/> + <addaction name="actionZoom_Out"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>Help</string> + </property> + <addaction name="actionAbout"/> + <addaction name="separator"/> + <addaction name="actionCheck_for_updates_automatically"/> + <addaction name="actionCheck_now"/> + </widget> + <action name="actionNew"> + <property name="text"> + <string>New...</string> + </property> + <property name="shortcut"> + <string>Ctrl+N</string> + </property> + </action> + <action name="actionOpen"> + <property name="text"> + <string>Open...</string> + </property> + <property name="shortcut"> + <string>Ctrl+O</string> + </property> + </action> + <action name="actionClose"> + <property name="text"> + <string>Close</string> + </property> + <property name="shortcut"> + <string>Ctrl+W</string> + </property> + </action> + <action name="actionSave"> + <property name="text"> + <string>Save</string> + </property> + <property name="shortcut"> + <string>Ctrl+S</string> + </property> + </action> + <action name="actionSave_As"> + <property name="text"> + <string>Save As...</string> + </property> + <property name="shortcut"> + <string>Ctrl+Shift+S</string> + </property> + </action> + <action name="actionUndo"> + <property name="text"> + <string>Undo</string> + </property> + <property name="shortcut"> + <string>Ctrl+Z</string> + </property> + </action> + <action name="actionRedo"> + <property name="text"> + <string>Redo</string> + </property> + <property name="shortcut"> + <string>Ctrl+Shift+Z</string> + </property> + </action> + <action name="actionCut"> + <property name="text"> + <string>Cut</string> + </property> + <property name="shortcut"> + <string>Ctrl+X</string> + </property> + </action> + <action name="actionCopy"> + <property name="text"> + <string>Copy</string> + </property> + <property name="shortcut"> + <string>Ctrl+C</string> + </property> + </action> + <action name="actionPaste"> + <property name="text"> + <string>Paste</string> + </property> + <property name="shortcut"> + <string>Ctrl+V</string> + </property> + </action> + <action name="actionDelete"> + <property name="text"> + <string>Delete</string> + </property> + <property name="shortcut"> + <string>Backspace</string> + </property> + </action> + <action name="actionSelect_All"> + <property name="text"> + <string>Select All</string> + </property> + <property name="shortcut"> + <string>Ctrl+A</string> + </property> + </action> + <action name="actionDeselect_All"> + <property name="text"> + <string>Deselect All</string> + </property> + <property name="shortcut"> + <string>Ctrl+D</string> + </property> + </action> + <action name="actionParse"> + <property name="text"> + <string>Parse Tikz</string> + </property> + <property name="shortcut"> + <string>Ctrl+T</string> + </property> + </action> + <action name="actionZoom_In"> + <property name="text"> + <string>Zoom In</string> + </property> + <property name="shortcut"> + <string>Ctrl+=</string> + </property> + </action> + <action name="actionZoom_Out"> + <property name="text"> + <string>Zoom Out</string> + </property> + <property name="shortcut"> + <string>Ctrl+-</string> + </property> + </action> + <action name="actionExit"> + <property name="text"> + <string>Exit</string> + </property> + </action> + <action name="actionRevert"> + <property name="text"> + <string>Revert Tikz</string> + </property> + </action> + <action name="actionJump_to_Selection"> + <property name="text"> + <string>Jump to Selection</string> + </property> + <property name="shortcut"> + <string>Ctrl+J</string> + </property> + </action> + <action name="actionReflectHorizontal"> + <property name="text"> + <string>Reflect Horizontally</string> + </property> + <property name="shortcut"> + <string>Alt+Right</string> + </property> + </action> + <action name="actionReflectVertical"> + <property name="text"> + <string>Reflect Vertically</string> + </property> + <property name="shortcut"> + <string>Alt+Down</string> + </property> + </action> + <action name="actionRotateCW"> + <property name="text"> + <string>Rotate 90' CW</string> + </property> + <property name="shortcut"> + <string>Alt+Shift+Right</string> + </property> + </action> + <action name="actionRotateCCW"> + <property name="text"> + <string>Rotate 90' CCW</string> + </property> + <property name="shortcut"> + <string>Alt+Shift+Left</string> + </property> + </action> + <action name="actionExtendLeft"> + <property name="text"> + <string>To the left</string> + </property> + <property name="shortcut"> + <string>Shift+Left</string> + </property> + </action> + <action name="actionExtendRight"> + <property name="text"> + <string>To the right</string> + </property> + <property name="shortcut"> + <string>Shift+Right</string> + </property> + </action> + <action name="actionExtendUp"> + <property name="text"> + <string>Upward</string> + </property> + <property name="shortcut"> + <string>Shift+Up</string> + </property> + </action> + <action name="actionExtendDown"> + <property name="text"> + <string>Downward</string> + </property> + <property name="shortcut"> + <string>Shift+Down</string> + </property> + </action> + <action name="actionBring_to_Front"> + <property name="text"> + <string>Bring to Front</string> + </property> + <property name="shortcut"> + <string>Ctrl+]</string> + </property> + </action> + <action name="actionSend_to_Back"> + <property name="text"> + <string>Send to Back</string> + </property> + <property name="shortcut"> + <string>Ctrl+[</string> + </property> + </action> + <action name="actionCheck_for_updates_automatically"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Check for updates automatically</string> + </property> + </action> + <action name="actionCheck_now"> + <property name="text"> + <string>Check now</string> + </property> + </action> + <action name="actionAbout"> + <property name="text"> + <string>About</string> + </property> + </action> + <addaction name="menuFile"/> + <addaction name="menuEdit"/> + <addaction name="menuView"/> + <addaction name="menuTikz"/> + <addaction name="menuHelp"/> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp new file mode 100644 index 0000000..c450b5b --- /dev/null +++ b/src/gui/mainwindow.cpp @@ -0,0 +1,217 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "mainmenu.h" +#include "tikzassembler.h" +#include "toolpalette.h" +#include "tikzit.h" + +#include <QDebug> +#include <QFile> +#include <QList> +#include <QSettings> +#include <QMessageBox> +#include <QFileDialog> +#include <QTextEdit> +#include <QTextBlock> +#include <QIcon> +#include <QPushButton> + +int MainWindow::_numWindows = 0; + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + QSettings settings("tikzit", "tikzit"); + _windowId = _numWindows; + _numWindows++; + ui->setupUi(this); + + setWindowIcon(QIcon(":/images/tikzit.png")); + + setAttribute(Qt::WA_DeleteOnClose, true); + _tikzDocument = new TikzDocument(this); + + _toolPalette = new ToolPalette(this); + addToolBar(_toolPalette); + + _stylePalette = new StylePalette(this); + + _tikzScene = new TikzScene(_tikzDocument, _toolPalette, _stylePalette, this); + ui->tikzView->setScene(_tikzScene); + + // TODO: check if each window should have a menu + _menu = new MainMenu(); + _menu->setParent(this); + setMenuBar(_menu); + + QVariant geom = settings.value("geometry-main"); + QVariant state = settings.value("windowState-main"); + + if (geom.isValid()) { + restoreGeometry(geom.toByteArray()); + } + + if (state.isValid()) { + restoreState(state.toByteArray(), 2); + } else { + addDockWidget(Qt::RightDockWidgetArea, _stylePalette); + resizeDocks({_stylePalette}, {130}, Qt::Horizontal); + } + + // initially, the source view should be collapsed + QList<int> sz = ui->splitter->sizes(); + sz[0] = sz[0] + sz[1]; + sz[1] = 0; + ui->splitter->setSizes(sz); + + _tikzDocument->refreshTikz(); + + connect(_tikzDocument->undoStack(), SIGNAL(cleanChanged(bool)), this, SLOT(updateFileName())); + _menu->addDocks(createPopupMenu()); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + ui->tikzSource->setTabStopDistance(20.0); +#else + ui->tikzSource->setTabStopWidth(20); +#endif +} + +MainWindow::~MainWindow() +{ + tikzit->removeWindow(this); + delete ui; +} + +void MainWindow::open(QString fileName) +{ + _tikzDocument->open(fileName); + + //ui->tikzSource->setText(_tikzDocument->tikz()); + + + if (_tikzDocument->parseSuccess()) { + statusBar()->showMessage("TiKZ parsed successfully", 2000); + //setWindowTitle("TiKZiT - " + _tikzDocument->shortName()); + _tikzScene->setTikzDocument(_tikzDocument); + updateFileName(); + } else { + statusBar()->showMessage("Cannot read TiKZ source"); + } + +} + +QSplitter *MainWindow::splitter() const { + return ui->splitter; +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + //qDebug() << "got close event"; + + QSettings settings("tikzit", "tikzit"); + settings.setValue("geometry-main", saveGeometry()); + settings.setValue("windowState-main", saveState(2)); + + if (!_tikzDocument->isClean()) { + QString nm = _tikzDocument->shortName(); + if (nm.isEmpty()) nm = "untitled"; + QMessageBox::StandardButton resBtn = QMessageBox::question( + this, "Save Changes", + "Do you wish to save changes to " + nm + "?", + QMessageBox::Cancel | QMessageBox::No | QMessageBox::Yes, + QMessageBox::Yes); + + if (resBtn == QMessageBox::Yes && _tikzDocument->save()) { + event->accept(); + } else if (resBtn == QMessageBox::No) { + event->accept(); + } else { + event->ignore(); + } + } else { + event->accept(); + } +} + +void MainWindow::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::ActivationChange && isActiveWindow()) { + tikzit->setActiveWindow(this); + //tikzit->stylePalette()->raise(); + } + QMainWindow::changeEvent(event); +} + +MainMenu *MainWindow::menu() const +{ + return _menu; +} + +StylePalette *MainWindow::stylePalette() const +{ + return _stylePalette; +} + +QString MainWindow::tikzSource() +{ + return ui->tikzSource->toPlainText(); +} + +void MainWindow::setSourceLine(int line) +{ + QTextCursor cursor(ui->tikzSource->document()->findBlockByLineNumber(line)); + cursor.movePosition(QTextCursor::EndOfLine); + //ui->tikzSource->moveCursor(QTextCursor::End); + ui->tikzSource->setTextCursor(cursor); + ui->tikzSource->setFocus(); +} + +void MainWindow::updateFileName() +{ + QString nm = _tikzDocument->shortName(); + if (nm.isEmpty()) nm = "untitled"; + if (!_tikzDocument->isClean()) nm += "*"; + setWindowTitle(nm + " - TikZiT"); +} + +void MainWindow::refreshTikz() +{ + // don't emit textChanged() when we update the tikz + ui->tikzSource->blockSignals(true); + ui->tikzSource->setText(_tikzDocument->tikz()); + ui->tikzSource->blockSignals(false); +} + +ToolPalette *MainWindow::toolPalette() const +{ + return _toolPalette; +} + +TikzDocument *MainWindow::tikzDocument() const +{ + return _tikzDocument; +} + +TikzScene *MainWindow::tikzScene() const +{ + return _tikzScene; +} + +int MainWindow::windowId() const +{ + return _windowId; +} + +TikzView *MainWindow::tikzView() const +{ + return ui->tikzView; +} + +void MainWindow::on_tikzSource_textChanged() +{ + if (_tikzScene->enabled()) _tikzScene->setEnabled(false); +} + + diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h new file mode 100644 index 0000000..21fbd5a --- /dev/null +++ b/src/gui/mainwindow.h @@ -0,0 +1,64 @@ +/*! + * A top-level window, which contains a single TikzDocument. + */ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "tikzscene.h" +#include "tikzview.h" +#include "graph.h" +#include "tikzdocument.h" +#include "mainmenu.h" +#include "toolpalette.h" +#include "stylepalette.h" + +#include <QMainWindow> +#include <QGraphicsView> +#include <QSplitter> + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + + void open(QString fileName); + int windowId() const; + TikzView *tikzView() const; + TikzScene *tikzScene() const; + TikzDocument *tikzDocument() const; + ToolPalette *toolPalette() const; + StylePalette *stylePalette() const; + QSplitter *splitter() const; + QString tikzSource(); + void setSourceLine(int line); + + MainMenu *menu() const; + +public slots: + void on_tikzSource_textChanged(); + void updateFileName(); + void refreshTikz(); +protected: + void closeEvent(QCloseEvent *event) override; + void changeEvent(QEvent *event) override; + +private: + TikzScene *_tikzScene; + TikzDocument *_tikzDocument; + MainMenu *_menu; + ToolPalette *_toolPalette; + StylePalette *_stylePalette; + Ui::MainWindow *ui; + int _windowId; + static int _numWindows; +}; + +#endif // MAINWINDOW_H diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui new file mode 100644 index 0000000..bedc695 --- /dev/null +++ b/src/gui/mainwindow.ui @@ -0,0 +1,208 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>640</width> + <height>580</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>untitled - TikZiT</string> + </property> + <widget class="QWidget" name="centralWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="TikzView" name="tikzView"/> + <widget class="QTextEdit" name="tikzSource"> + <property name="font"> + <font> + <family>Courier New</family> + <pointsize>10</pointsize> + </font> + </property> + <property name="lineWrapMode"> + <enum>QTextEdit::NoWrap</enum> + </property> + <property name="html"> + <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Courier New'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.SF NS Text'; font-size:13pt;"><br /></p></body></html></string> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <action name="actionNew"> + <property name="text"> + <string>New...</string> + </property> + </action> + <action name="actionOpen"> + <property name="text"> + <string>Open...</string> + </property> + </action> + <action name="actionClose"> + <property name="text"> + <string>Close</string> + </property> + </action> + <action name="actionSave"> + <property name="text"> + <string>Save</string> + </property> + </action> + <action name="actionSave_As"> + <property name="text"> + <string>Save As...</string> + </property> + </action> + <action name="actionUndo"> + <property name="text"> + <string>Undo</string> + </property> + </action> + <action name="actionRedo"> + <property name="text"> + <string>Redo</string> + </property> + </action> + <action name="actionCut"> + <property name="text"> + <string>Cut</string> + </property> + </action> + <action name="actionCopy"> + <property name="text"> + <string>Copy</string> + </property> + </action> + <action name="actionPase"> + <property name="text"> + <string>Paste</string> + </property> + </action> + <action name="actionDelete"> + <property name="text"> + <string>Delete</string> + </property> + </action> + <action name="actionSelect_All"> + <property name="text"> + <string>Select All</string> + </property> + </action> + <action name="actionDeselect_All"> + <property name="text"> + <string>Deselect All</string> + </property> + </action> + <action name="actionParse"> + <property name="text"> + <string>Parse</string> + </property> + <property name="shortcut"> + <string>Ctrl+T</string> + </property> + </action> + <action name="actionZoom_In"> + <property name="text"> + <string>Zoom In</string> + </property> + <property name="shortcut"> + <string>Ctrl+=</string> + </property> + </action> + <action name="actionZoom_Out"> + <property name="text"> + <string>Zoom Out</string> + </property> + <property name="shortcut"> + <string>Ctrl+-</string> + </property> + </action> + </widget> + <layoutdefault spacing="6" margin="11"/> + <customwidgets> + <customwidget> + <class>TikzView</class> + <extends>QGraphicsView</extends> + <header>tikzview.h</header> + <slots> + <slot>zoomIn()</slot> + <slot>zoomOut()</slot> + </slots> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>actionZoom_In</sender> + <signal>triggered()</signal> + <receiver>tikzView</receiver> + <slot>zoomIn()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>237</x> + <y>103</y> + </hint> + </hints> + </connection> + <connection> + <sender>actionZoom_Out</sender> + <signal>triggered()</signal> + <receiver>tikzView</receiver> + <slot>zoomOut()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>237</x> + <y>103</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/gui/nodeitem.cpp b/src/gui/nodeitem.cpp new file mode 100644 index 0000000..18ff43c --- /dev/null +++ b/src/gui/nodeitem.cpp @@ -0,0 +1,164 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "tikzit.h" +#include "nodeitem.h" +#include "tikzscene.h" +#include <cmath> + +#include <QPen> +#include <QApplication> +#include <QBrush> +#include <QDebug> +#include <QFont> +#include <QFontMetrics> +#include <QPainterPathStroker> + +NodeItem::NodeItem(Node *node) +{ + _node = node; + setFlag(QGraphicsItem::ItemIsSelectable); + //setFlag(QGraphicsItem::ItemIsMovable); + //setFlag(QGraphicsItem::ItemSendsGeometryChanges); + readPos(); + updateBounds(); +} + +void NodeItem::readPos() +{ + setPos(toScreen(_node->point())); +} + +void NodeItem::writePos() +{ + _node->setPoint(fromScreen(pos())); +} + +QRectF NodeItem::labelRect() const { + QString label = _node->label(); + //QFont f("Courier", 9); + QFontMetrics fm(Tikzit::LABEL_FONT); + + QRectF rect = fm.boundingRect(label); + //rect.adjust(-2,-2,2,2); + rect.moveCenter(QPointF(0,0)); + return rect; +} + +void NodeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (_node->style()->isNone()) { + QColor c(180,180,200); + painter->setPen(QPen(c)); + painter->setBrush(QBrush(c)); + painter->drawEllipse(QPointF(0,0), 1,1); + + QPen pen(QColor(180,180,220)); + QVector<qreal> p; + p << 1.0 << 2.0; + pen.setDashPattern(p); + pen.setWidthF(2.0f); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(shape()); + } else { + QPen pen(_node->style()->strokeColor()); + pen.setWidth(_node->style()->strokeThickness()); + painter->setPen(pen); + painter->setBrush(QBrush(_node->style()->fillColor())); + painter->drawPath(shape()); + } + + if (_node->label() != "") { + QRectF rect = labelRect(); + QPen pen(QColor(200,0,0,120)); + QVector<qreal> d; + d << 2.0 << 2.0; + pen.setDashPattern(d); + painter->setPen(pen); + painter->setBrush(QBrush(QColor(255,255,100,120))); + painter->drawRect(rect); + + painter->setPen(QPen(Qt::black)); + painter->setFont(Tikzit::LABEL_FONT); + painter->drawText(rect, Qt::AlignCenter, _node->label()); + } + + if (isSelected()) { + QPainterPath sh = shape(); + QPainterPathStroker stroker; + stroker.setWidth(4); + QPainterPath outline = (stroker.createStroke(sh) + sh).simplified(); + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(QColor(150,200,255,100))); + painter->drawPath(outline); + } + +} + +QPainterPath NodeItem::shape() const +{ + QPainterPath path; + + if (_node->style()->shape() == "rectangle") { + path.addRect(-0.2 * GLOBAL_SCALEF, -0.2 * GLOBAL_SCALEF, 0.4 * GLOBAL_SCALEF, 0.4 * GLOBAL_SCALEF); + } else { + path.addEllipse(QPointF(0, 0), GLOBAL_SCALEF * 0.2, GLOBAL_SCALEF * 0.2); + } + return path; +} + +// TODO: nodeitem should sync boundingRect()-relevant stuff (label etc) explicitly, +// to allow prepareGeometryChange() +QRectF NodeItem::boundingRect() const +{ + return _boundingRect; +} + +void NodeItem::updateBounds() +{ + prepareGeometryChange(); + QString label = _node->label(); + if (label != "") { + QFontMetrics fm(Tikzit::LABEL_FONT); + QRectF labelRect = fm.boundingRect(label); + labelRect.moveCenter(QPointF(0, 0)); + _boundingRect = labelRect.united(shape().boundingRect()).adjusted(-4, -4, 4, 4); + } else { + _boundingRect = shape().boundingRect().adjusted(-4, -4, 4, 4); + } +} + +Node *NodeItem::node() const +{ + return _node; +} + +//QVariant NodeItem::itemChange(GraphicsItemChange change, const QVariant &value) +//{ +// if (change == ItemPositionChange) { +// QPointF newPos = value.toPointF(); +// int gridSize = GLOBAL_SCALE / 8; +// QPointF gridPos(round(newPos.x()/gridSize)*gridSize, round(newPos.y()/gridSize)*gridSize); +// _node->setPoint(fromScreen(gridPos)); +// +// return gridPos; +// } else { +// return QGraphicsItem::itemChange(change, value); +// } +//} diff --git a/src/gui/nodeitem.h b/src/gui/nodeitem.h new file mode 100644 index 0000000..5be4f3e --- /dev/null +++ b/src/gui/nodeitem.h @@ -0,0 +1,51 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * A QGraphicsItem that handles drawing a single node. + */ + +#ifndef NODEITEM_H +#define NODEITEM_H + +#include "node.h" + +#include <QObject> +#include <QGraphicsItem> +#include <QPainterPath> +#include <QRectF> + +class NodeItem : public QGraphicsItem +{ +public: + NodeItem(Node *node); + void readPos(); + void writePos(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; + QPainterPath shape() const override; + QRectF boundingRect() const override; + void updateBounds(); + Node *node() const; + +private: + Node *_node; + QRectF labelRect() const; + QRectF _boundingRect; +}; + +#endif // NODEITEM_H diff --git a/src/gui/propertypalette.cpp b/src/gui/propertypalette.cpp new file mode 100644 index 0000000..c27b8b2 --- /dev/null +++ b/src/gui/propertypalette.cpp @@ -0,0 +1,61 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "propertypalette.h" +#include "graphelementdata.h" +#include "ui_propertypalette.h" + +#include <QModelIndex> +#include <QDebug> +#include <QCloseEvent> +#include <QSettings> + +PropertyPalette::PropertyPalette(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::PropertyPalette) +{ + setWindowFlags(Qt::Window + | Qt::CustomizeWindowHint + | Qt::WindowTitleHint); + //setFocusPolicy(Qt::NoFocus); + ui->setupUi(this); + GraphElementData *d = new GraphElementData(); + d->setProperty("key 1", "value 1"); + d->setAtom("atom 1"); + d->setProperty("key 2", "value 2"); + + //QModelIndex i = d->index(0,0); + //ui->treeView->setModel(d); + + QSettings settings("tikzit", "tikzit"); + QVariant geom = settings.value("property-palette-geometry"); + if (geom != QVariant()) { + restoreGeometry(geom.toByteArray()); + } +} + +PropertyPalette::~PropertyPalette() +{ + delete ui; +} + +void PropertyPalette::closeEvent(QCloseEvent *event) { + QSettings settings("tikzit", "tikzit"); + settings.setValue("property-palette-geometry", saveGeometry()); + QDockWidget::closeEvent(event); +} diff --git a/src/gui/propertypalette.h b/src/gui/propertypalette.h new file mode 100644 index 0000000..69f4515 --- /dev/null +++ b/src/gui/propertypalette.h @@ -0,0 +1,46 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * Enables the user to edit properties of the graph, as well as the selected node/edge. + */ + +#ifndef PROPERTYPALETTE_H +#define PROPERTYPALETTE_H + +#include <QDockWidget> + +namespace Ui { +class PropertyPalette; +} + +class PropertyPalette : public QDockWidget +{ + Q_OBJECT + +public: + explicit PropertyPalette(QWidget *parent = 0); + ~PropertyPalette(); + +protected: + void closeEvent(QCloseEvent *event) override; +private: + Ui::PropertyPalette *ui; +}; + +#endif // PROPERTYPALETTE_H diff --git a/src/gui/propertypalette.ui b/src/gui/propertypalette.ui new file mode 100644 index 0000000..a8ba5d2 --- /dev/null +++ b/src/gui/propertypalette.ui @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PropertyPalette</class> + <widget class="QDockWidget" name="PropertyPalette"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>194</width> + <height>341</height> + </rect> + </property> + <property name="floating"> + <bool>false</bool> + </property> + <property name="windowTitle"> + <string>Properties</string> + </property> + <widget class="QWidget" name="dockWidgetContents"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeView" name="treeView"> + <property name="indentation"> + <number>0</number> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/styleeditor.cpp b/src/gui/styleeditor.cpp new file mode 100644 index 0000000..29192d6 --- /dev/null +++ b/src/gui/styleeditor.cpp @@ -0,0 +1,842 @@ +#include <QColorDialog> +#include <QDebug> +#include <QMessageBox> + +#include "tikzit.h" +#include "styleeditor.h" +#include "ui_styleeditor.h" + +StyleEditor::StyleEditor(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::StyleEditor) +{ + ui->setupUi(this); + _formWidgets << ui->name << ui->category << + ui->fillColor << ui->hasTikzitFillColor << ui->tikzitFillColor << + ui->drawColor << ui->hasTikzitDrawColor << ui->tikzitDrawColor << + ui->shape << ui->hasTikzitShape << ui->tikzitShape << + ui->leftArrow << ui->rightArrow << + ui->properties; + + setWindowIcon(QIcon(":/images/tikzit.png")); + _styles = nullptr; + _activeStyle = nullptr; + + ui->styleListView->setViewMode(QListView::IconMode); + ui->styleListView->setMovement(QListView::Static); + ui->styleListView->setGridSize(QSize(48,48)); + + ui->edgeStyleListView->setViewMode(QListView::IconMode); + ui->edgeStyleListView->setMovement(QListView::Static); + ui->edgeStyleListView->setGridSize(QSize(48,48)); + + connect(ui->category->lineEdit(), + SIGNAL(editingFinished()), + this, SLOT(categoryChanged())); + connect(ui->category, + SIGNAL(currentIndexChanged(int)), + this, SLOT(categoryChanged())); + + connect(ui->shape->lineEdit(), + SIGNAL(editingFinished()), + this, SLOT(shapeChanged())); + connect(ui->shape, + SIGNAL(currentIndexChanged(int)), + this, SLOT(shapeChanged())); + + // setup the color dialog to display only the named colors that tikzit/xcolor knows + // about as "standard colors". + for (int i = 0; i < 48; ++i) { + QColorDialog::setStandardColor(i, QColor(Qt::white)); + } + + // grayscale in column 1 + int pos = 0; + for (int i=0; i < 5; ++i) { + QColorDialog::setStandardColor(pos, tikzit->colorByIndex(i)); + pos += 1; + } + + // rainbow in column 2 + pos = 6; + for (int i=5; i < 11; ++i) { + QColorDialog::setStandardColor(pos, tikzit->colorByIndex(i)); + pos += 1; + } + + // brown/green/teal spectrum in column 3 + pos = 12; + for (int i=11; i < 16; ++i) { + QColorDialog::setStandardColor(pos, tikzit->colorByIndex(i)); + pos += 1; + } + + // pinks in column 4 + pos = 18; + for (int i=16; i < 19; ++i) { + QColorDialog::setStandardColor(pos, tikzit->colorByIndex(i)); + pos += 1; + } + + refreshDisplay(); +} + +StyleEditor::~StyleEditor() +{ + delete ui; +} + +void StyleEditor::open() { + if (_styles != nullptr) delete _styles; + _styles = new TikzStyles; + _activeStyle = nullptr; + ui->styleListView->setModel(_styles->nodeStyles()); + ui->edgeStyleListView->setModel(_styles->edgeStyles()); + connect(ui->styleListView->selectionModel(), + SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(nodeItemChanged(QModelIndex))); + connect(ui->edgeStyleListView->selectionModel(), + SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(edgeItemChanged(QModelIndex))); + + if (_styles->loadStyles(tikzit->styleFilePath())) { + setDirty(false); + refreshCategories(); + refreshDisplay(); + show(); + } else { + QMessageBox::warning(0, + "Bad style file.", + "Bad style file: '" + tikzit->styleFile() + "'. Check that the file exists and is properly formatted."); + } +} + +void StyleEditor::closeEvent(QCloseEvent *event) +{ + if (dirty()) { + QMessageBox::StandardButton resBtn = QMessageBox::question( + this, "Save Changes", + "Do you wish to save changes to " + tikzit->styleFile() + "?", + QMessageBox::Cancel | QMessageBox::No | QMessageBox::Yes, + QMessageBox::Yes); + + if (resBtn == QMessageBox::Yes) { + save(); + event->accept(); + } else if (resBtn == QMessageBox::No) { + event->accept(); + } else { + event->ignore(); + } + } else { + event->accept(); + } +} + +void StyleEditor::nodeItemChanged(QModelIndex sel) +{ + //qDebug() << "nodeItemChanged, new index:" << sel.row(); + if (sel.isValid()) { + ui->edgeStyleListView->selectionModel()->clear(); + _activeStyle = _styles->nodeStyles()->styleInCategory(sel.row()); + } + _nodeStyleIndex = sel; + refreshDisplay(); +} + +void StyleEditor::edgeItemChanged(QModelIndex sel) +{ + if (sel.isValid()) { + ui->styleListView->selectionModel()->clear(); + _activeStyle = _styles->edgeStyles()->styleInCategory(sel.row()); + } + _edgeStyleIndex = sel; + refreshDisplay(); +} + +void StyleEditor::categoryChanged() +{ + Style *s = activeStyle(); + QString cat = ui->category->currentText(); + //qDebug() << "got category: " << cat; + + if (s != nullptr && s->data()->property("tikzit category") != cat) { + if (cat.isEmpty()) s->data()->unsetProperty("tikzit category"); + else s->data()->setProperty("tikzit category", cat); + setDirty(true); + refreshCategories(); + + if (_styles->nodeStyles()->category() != "") { + ui->currentCategory->setCurrentText(cat); + //qDebug() << "after cat change, cat reports:" << _styles->nodeStyles()->category(); + } + //refreshDisplay(); + } +} + +void StyleEditor::currentCategoryChanged() +{ + if (_styles != nullptr) { + QString cat = ui->currentCategory->currentText(); + if (cat != _styles->nodeStyles()->category()) { + ui->styleListView->selectionModel()->clear(); + _styles->nodeStyles()->setCategory(cat); + + if (_activeStyle != nullptr && !_activeStyle->isEdgeStyle()) { + for (int i = 0; i < _styles->nodeStyles()->numInCategory(); ++i) { + if (_styles->nodeStyles()->styleInCategory(i) == _activeStyle) { + ui->styleListView->selectionModel()->setCurrentIndex( + _styles->nodeStyles()->index(i), + QItemSelectionModel::ClearAndSelect); + break; + } + } + if (!_nodeStyleIndex.isValid()) _activeStyle = nullptr; + } + } + } +} + +void StyleEditor::shapeChanged() +{ + Style *s = activeStyle(); + if (s != 0) { + s->data()->setProperty("shape", ui->shape->currentText()); + refreshActiveStyle(); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::refreshCategories() +{ + ui->currentCategory->blockSignals(true); + ui->category->blockSignals(true); + QString curCat = ui->currentCategory->currentText(); + QString cat = ui->category->currentText(); + ui->currentCategory->clear(); + ui->category->clear(); + + if (_styles != nullptr) { + foreach(QString c, _styles->categories()) { + ui->category->addItem(c); + ui->currentCategory->addItem(c); + } + } + + ui->currentCategory->setCurrentText(curCat); + ui->category->setCurrentText(cat); + ui->currentCategory->blockSignals(false); + ui->category->blockSignals(false); +} + +void StyleEditor::propertyChanged() +{ + if (_nodeStyleIndex.isValid()) { + emit _styles->nodeStyles()->dataChanged(_nodeStyleIndex, _nodeStyleIndex); + + if (_activeStyle->category() != _styles->nodeStyles()->category()) { + refreshCategories(); + if (_styles->nodeStyles()->category() != "") + ui->currentCategory->setCurrentText(_activeStyle->category()); + } + } else if (_edgeStyleIndex.isValid()) { + emit _styles->edgeStyles()->dataChanged(_edgeStyleIndex, _edgeStyleIndex); + } + setDirty(true); + refreshDisplay(); +} + +void StyleEditor::refreshDisplay() +{ + // enable all fields and block signals while we set their values + foreach (QWidget *w, _formWidgets) { + w->blockSignals(true); + } + + // set to default values + ui->name->setText("none"); + ui->category->setCurrentText(""); + //ui->category->clear(); + + setColor(ui->fillColor, QColor(Qt::gray)); + setColor(ui->drawColor, QColor(Qt::gray)); + setColor(ui->tikzitFillColor, QColor(Qt::gray)); + setColor(ui->tikzitDrawColor, QColor(Qt::gray)); + ui->hasTikzitDrawColor->setChecked(false); + ui->hasTikzitFillColor->setChecked(false); + ui->shape->setCurrentText(""); + ui->hasTikzitShape->setChecked(false); + ui->tikzitShape->setCurrentText(""); + ui->leftArrow->setCurrentText(""); + ui->rightArrow->setCurrentText(""); + ui->properties->setModel(0); + + Style *s = activeStyle(); + +// qDebug() << "style" << s; + if (s != nullptr && !s->isNone()) { +// qDebug() << "non-null style update"; + + // name + ui->name->setEnabled(true); + ui->name->setText(s->name()); + + // property list + ui->properties->setEnabled(true); + setPropertyModel(s->data()); + + // draw + QColor realDraw = s->strokeColor(false); + QColor draw = s->strokeColor(); + ui->drawColor->setEnabled(true); + setColor(ui->drawColor, realDraw); + + // tikzit draw + bool drawOverride = s->data()->hasProperty("tikzit draw"); + ui->hasTikzitDrawColor->setEnabled(true); + ui->hasTikzitDrawColor->setChecked(drawOverride); + + ui->tikzitDrawColor->setEnabled(drawOverride); + if (drawOverride) setColor(ui->tikzitDrawColor, draw); + + if (!s->isEdgeStyle()) { +// qDebug() << "node style update"; + // category + ui->category->setEnabled(true); + ui->category->setCurrentText( + s->propertyWithDefault("tikzit category", "", false)); + + // fill + QColor realFill = s->fillColor(false); + QColor fill = s->fillColor(); + ui->fillColor->setEnabled(true); + setColor(ui->fillColor, realFill); + + // tikzit fill + bool fillOverride = s->data()->hasProperty("tikzit fill"); + ui->hasTikzitFillColor->setEnabled(true); + ui->hasTikzitFillColor->setChecked(fillOverride); + ui->tikzitFillColor->setEnabled(fillOverride); + if (fillOverride) setColor(ui->tikzitFillColor, fill); + + // shape + QString realShape = s->propertyWithDefault("shape", "", false); + QString shape = s->propertyWithDefault("tikzit shape", "", false); + ui->shape->setEnabled(true); + ui->shape->setCurrentText(realShape); + + // tikzit shape + bool shapeOverride = s->data()->hasProperty("tikzit shape"); + ui->hasTikzitShape->setEnabled(true); + ui->hasTikzitShape->setChecked(shapeOverride); + ui->tikzitShape->setEnabled(shapeOverride); + if (shapeOverride) ui->tikzitShape->setCurrentText(shape); + } else { +// qDebug() << "edge style update"; + + // set fill to gray (disabled) + ui->fillColor->setEnabled(false); + ui->tikzitFillColor->setEnabled(false); + ui->hasTikzitFillColor->setEnabled(false); + + ui->shape->setEnabled(false); + ui->tikzitShape->setEnabled(false); + ui->hasTikzitShape->setEnabled(false); + + + // arrow tail + ui->leftArrow->setEnabled(true); + + switch (s->arrowTail()) { + case Style::NoTip: + ui->leftArrow->setCurrentText(""); + break; + case Style::Pointer: + ui->leftArrow->setCurrentText("<"); + break; + case Style::Flat: + ui->leftArrow->setCurrentText("|"); + break; + } + + // arrow head + ui->rightArrow->setEnabled(true); + switch (s->arrowHead()) { + case Style::NoTip: + ui->rightArrow->setCurrentText(""); + break; + case Style::Pointer: + ui->rightArrow->setCurrentText(">"); + break; + case Style::Flat: + ui->rightArrow->setCurrentText("|"); + break; + } + } + + } else { +// qDebug() << "null style update"; + + foreach (QWidget *w, _formWidgets) { + w->setEnabled(false); + } + } + + // unblock signals so we are ready for user input + foreach (QWidget *w, _formWidgets) { + w->blockSignals(false); + } +} + +void StyleEditor::on_fillColor_clicked() +{ + updateColor(ui->fillColor, "Fill Color", "fill"); +} + +void StyleEditor::on_drawColor_clicked() +{ + updateColor(ui->drawColor, "Draw Color", "draw"); +} + +void StyleEditor::on_tikzitFillColor_clicked() +{ + updateColor(ui->tikzitFillColor, "TikZiT Fill Color", "tikzit fill"); +} + +void StyleEditor::on_tikzitDrawColor_clicked() +{ + updateColor(ui->tikzitDrawColor, "TikZiT Draw Color", "tikzit draw"); +} + +void StyleEditor::on_hasTikzitFillColor_stateChanged(int state) +{ + Style *s = activeStyle(); + if (s != nullptr) { + if (state == Qt::Checked) s->data()->setProperty("tikzit fill", s->data()->property("fill")); + else s->data()->unsetProperty("tikzit fill"); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_hasTikzitDrawColor_stateChanged(int state) +{ + Style *s = activeStyle(); + if (s != nullptr) { + if (state == Qt::Checked) s->data()->setProperty("tikzit draw", s->data()->property("draw")); + else s->data()->unsetProperty("tikzit draw"); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_hasTikzitShape_stateChanged(int state) +{ + Style *s = activeStyle(); + if (s != nullptr) { + if (state == Qt::Checked) s->data()->setProperty("tikzit shape", s->data()->property("shape")); + else s->data()->unsetProperty("tikzit shape"); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_tikzitShape_currentIndexChanged(int) +{ + Style *s = activeStyle(); + if (s != nullptr) { + s->data()->setProperty("tikzit shape", ui->tikzitShape->currentText()); + refreshActiveStyle(); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_leftArrow_currentIndexChanged(int) +{ + Style *s = activeStyle(); + if (s != nullptr) { + s->setArrowAtom(ui->leftArrow->currentText() + "-" + + ui->rightArrow->currentText()); + refreshActiveStyle(); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_rightArrow_currentIndexChanged(int) +{ + Style *s = activeStyle(); + if (s != nullptr) { + s->setArrowAtom(ui->leftArrow->currentText() + "-" + + ui->rightArrow->currentText()); + refreshActiveStyle(); + refreshDisplay(); + setDirty(true); + } +} + +void StyleEditor::on_addProperty_clicked() +{ + Style *s = activeStyle(); + if (s != nullptr) { + s->data()->add(GraphElementProperty("new property", "")); + setDirty(true); + } +} + +void StyleEditor::on_addAtom_clicked() +{ + Style *s = activeStyle(); + if (s != 0) { + s->data()->add(GraphElementProperty("new atom")); + setDirty(true); + } +} + +void StyleEditor::on_removeProperty_clicked() +{ + Style *s = activeStyle(); + if (s != 0) { + QModelIndexList sel = ui->properties->selectionModel()->selectedRows(); + if (!sel.isEmpty()) { + s->data()->removeRows(sel[0].row(), 1, sel[0].parent()); + setDirty(true); + } + } +} + +void StyleEditor::on_propertyUp_clicked() +{ + Style *s = activeStyle(); + if (s != 0) { + QModelIndexList sel = ui->properties->selectionModel()->selectedRows(); + if (!sel.isEmpty()) { + s->data()->moveRows( + sel[0].parent(), + sel[0].row(), 1, + sel[0].parent(), + sel[0].row() - 1); + setDirty(true); + } + } +} + +void StyleEditor::on_propertyDown_clicked() +{ + Style *s = activeStyle(); + if (s != 0) { + QModelIndexList sel = ui->properties->selectionModel()->selectedRows(); + if (!sel.isEmpty()) { + s->data()->moveRows( + sel[0].parent(), + sel[0].row(), 1, + sel[0].parent(), + sel[0].row() + 2); + setDirty(true); + } + } +} + +void StyleEditor::on_addStyle_clicked() +{ + int i = 0; + + // get a fresh name + QString name; + while (true) { + name = QString("new style ") + QString::number(i); + if (_styles->nodeStyles()->style(name) == nullptr) break; + ++i; + } + + // add the style to the current category + Style *s; + if (_styles->nodeStyles()->category() == "") { + s = new Style(name, new GraphElementData({ + GraphElementProperty("fill", "white"), + GraphElementProperty("draw", "black"), + GraphElementProperty("shape", "circle") + })); + } else { + s = new Style(name, new GraphElementData({ + GraphElementProperty("fill", "white"), + GraphElementProperty("draw", "black"), + GraphElementProperty("shape", "circle"), + GraphElementProperty("category", _styles->nodeStyles()->category()), + })); + } + _styles->nodeStyles()->addStyle(s); + + // set dirty flag and select the newly-added style + setDirty(true); + selectNodeStyle(_styles->nodeStyles()->numInCategory()-1); +} + +void StyleEditor::on_removeStyle_clicked() +{ + if (_nodeStyleIndex.isValid()) { + int i = _nodeStyleIndex.row(); + if (i > 0) { + ui->styleListView->selectionModel()->clear(); + _styles->nodeStyles()->removeNthStyle(i); + setDirty(true); + if (i < _styles->nodeStyles()->numInCategory()) { + selectNodeStyle(i); + } + } + } +} + +void StyleEditor::on_styleUp_clicked() +{ + if (_nodeStyleIndex.isValid()) { + int r = _nodeStyleIndex.row(); + if (_styles->nodeStyles()->moveRows( + _nodeStyleIndex.parent(), + r, 1, + _nodeStyleIndex.parent(), + r - 1)) + { + setDirty(true); + nodeItemChanged(_styles->nodeStyles()->index(r - 1)); + } + } +} + +void StyleEditor::on_styleDown_clicked() +{ + if (_nodeStyleIndex.isValid()) { + int r = _nodeStyleIndex.row(); + if (_styles->nodeStyles()->moveRows( + _nodeStyleIndex.parent(), + r, 1, + _nodeStyleIndex.parent(), + r + 2)) + { + setDirty(true); + nodeItemChanged(_styles->nodeStyles()->index(r + 1)); + } + } +} + +void StyleEditor::on_addEdgeStyle_clicked() +{ + int i = 0; + + // get a fresh name + QString name; + while (true) { + name = QString("new edge style ") + QString::number(i); + if (_styles->edgeStyles()->style(name) == nullptr) break; + ++i; + } + + // add the style (edge styles only have one category: "") + Style *s = new Style(name, new GraphElementData({GraphElementProperty("-")})); + _styles->edgeStyles()->addStyle(s); + + // set dirty flag and select the newly-added style + setDirty(true); + selectEdgeStyle(_styles->edgeStyles()->numInCategory()-1); +} + +void StyleEditor::on_removeEdgeStyle_clicked() +{ + if (_edgeStyleIndex.isValid()) { + int i = _edgeStyleIndex.row(); + if (i > 0) { + ui->edgeStyleListView->selectionModel()->clear(); + _styles->edgeStyles()->removeNthStyle(i); + setDirty(true); + if (i < _styles->edgeStyles()->numInCategory()) { + selectEdgeStyle(i); + } + } + } +} + +void StyleEditor::on_edgeStyleUp_clicked() +{ + if (_edgeStyleIndex.isValid()) { + int r = _edgeStyleIndex.row(); + if (_styles->edgeStyles()->moveRows( + _edgeStyleIndex.parent(), + r, 1, + _edgeStyleIndex.parent(), + r - 1)) + { + setDirty(true); + edgeItemChanged(_styles->edgeStyles()->index(r - 1)); + } + } +} + +void StyleEditor::on_edgeStyleDown_clicked() +{ + if (_edgeStyleIndex.isValid()) { + int r = _edgeStyleIndex.row(); + if (_styles->edgeStyles()->moveRows( + _edgeStyleIndex.parent(), + r, 1, + _edgeStyleIndex.parent(), + r + 2)) + { + setDirty(true); + edgeItemChanged(_styles->edgeStyles()->index(r + 1)); + } + } +} + +void StyleEditor::on_save_clicked() +{ + save(); + close(); +} + +void StyleEditor::on_currentCategory_currentIndexChanged(int) +{ + currentCategoryChanged(); +} + + +void StyleEditor::save() +{ + QString p = tikzit->styleFilePath(); + + if (_styles->saveStyles(p)) { + setDirty(false); + tikzit->loadStyles(p); + } else { + QMessageBox::warning(0, + "Unabled to save style file", + "Unable to write to file: '" + tikzit->styleFile() + "'."); + } +} + +void StyleEditor::on_styleListView_clicked() +{ +} + +void StyleEditor::on_edgeStyleListView_clicked() +{ +} + +void StyleEditor::on_name_editingFinished() +{ + Style *s = activeStyle(); + + if (s != 0) { + s->setName(ui->name->text()); + refreshActiveStyle(); +// refreshDisplay(); + setDirty(true); + } +} + + +void StyleEditor::setColor(QPushButton *btn, QColor col) +{ + QPalette pal = btn->palette(); + pal.setColor(QPalette::Button, col); + btn->setPalette(pal); + btn->update(); +} + +void StyleEditor::setPropertyModel(GraphElementData *d) +{ + if (ui->properties->model() != 0) { + disconnect(ui->properties->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), + this, SLOT(propertyChanged())); + } + ui->properties->setModel(d); + connect(d, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), + this, SLOT(propertyChanged())); +} + +QColor StyleEditor::color(QPushButton *btn) +{ + QPalette pal = btn->palette(); + return pal.color(QPalette::Button); +} + +Style *StyleEditor::activeStyle() +{ +// if (_styles != nullptr) { +// if (_nodeStyleIndex.isValid()) +// return _styles->nodeStyles()->styleInCategory(_nodeStyleIndex.row()); + +// if (_edgeStyleIndex.isValid()) +// return _styles->edgeStyles()->styleInCategory(_edgeStyleIndex.row()); +// } + + return _activeStyle; +} + +void StyleEditor::refreshActiveStyle() +{ + if (_styles != nullptr) { + if (_nodeStyleIndex.isValid()) { + emit _styles->nodeStyles()->dataChanged(_nodeStyleIndex, _nodeStyleIndex); + + // force a re-layout + ui->styleListView->setGridSize(QSize(48,48)); + } + + if (_edgeStyleIndex.isValid()) { + emit _styles->edgeStyles()->dataChanged(_edgeStyleIndex, _edgeStyleIndex); + + // force a re-layout + ui->edgeStyleListView->setGridSize(QSize(48,48)); + } + } +} + +void StyleEditor::updateColor(QPushButton *btn, QString name, QString propName) +{ + QColor col = QColorDialog::getColor( + color(btn), + this, + name, + QColorDialog::DontUseNativeDialog); + if (col.isValid()) { + setColor(btn, col); + Style *s = activeStyle(); + if (s != nullptr) { + s->data()->setProperty(propName, tikzit->nameForColor(col)); + refreshActiveStyle(); + refreshDisplay(); + setDirty(true); + } + } +} + +void StyleEditor::selectNodeStyle(int i) +{ + ui->styleListView->selectionModel()->setCurrentIndex( + _styles->nodeStyles()->index(i), + QItemSelectionModel::ClearAndSelect); +} + +void StyleEditor::selectEdgeStyle(int i) +{ + ui->edgeStyleListView->selectionModel()->setCurrentIndex( + _styles->edgeStyles()->index(i), + QItemSelectionModel::ClearAndSelect); +} + +bool StyleEditor::dirty() const +{ + return _dirty; +} + +void StyleEditor::setDirty(bool dirty) +{ + _dirty = dirty; + if (dirty) { + setWindowTitle("Style Editor* - TikZiT"); + } else { + setWindowTitle("Style Editor - TikZiT"); + } +} diff --git a/src/gui/styleeditor.h b/src/gui/styleeditor.h new file mode 100644 index 0000000..4bae7db --- /dev/null +++ b/src/gui/styleeditor.h @@ -0,0 +1,98 @@ +#ifndef STYLEEDITOR_H +#define STYLEEDITOR_H + +#include "style.h" +#include "tikzstyles.h" + +#include <QMainWindow> +#include <QPushButton> +#include <QStandardItemModel> + +namespace Ui { +class StyleEditor; +} + +class StyleEditor : public QMainWindow +{ + Q_OBJECT + +public: + explicit StyleEditor(QWidget *parent = 0); + ~StyleEditor(); + + void open(); + void save(); + void closeEvent(QCloseEvent *event) override; + + bool dirty() const; + void setDirty(bool dirty); + +public slots: + void refreshDisplay(); + void nodeItemChanged(QModelIndex sel); + void edgeItemChanged(QModelIndex sel); + void categoryChanged(); + void currentCategoryChanged(); + void shapeChanged(); + void refreshCategories(); + void propertyChanged(); + void on_styleListView_clicked(); + void on_edgeStyleListView_clicked(); + + void on_name_editingFinished(); + void on_fillColor_clicked(); + void on_drawColor_clicked(); + void on_tikzitFillColor_clicked(); + void on_tikzitDrawColor_clicked(); + void on_hasTikzitFillColor_stateChanged(int state); + void on_hasTikzitDrawColor_stateChanged(int state); + + void on_hasTikzitShape_stateChanged(int state); + void on_tikzitShape_currentIndexChanged(int); + + void on_leftArrow_currentIndexChanged(int); + void on_rightArrow_currentIndexChanged(int); + + void on_addProperty_clicked(); + void on_addAtom_clicked(); + void on_removeProperty_clicked(); + void on_propertyUp_clicked(); + void on_propertyDown_clicked(); + + void on_addStyle_clicked(); + void on_removeStyle_clicked(); + void on_styleUp_clicked(); + void on_styleDown_clicked(); + + void on_addEdgeStyle_clicked(); + void on_removeEdgeStyle_clicked(); + void on_edgeStyleUp_clicked(); + void on_edgeStyleDown_clicked(); + + void on_save_clicked(); + + void on_currentCategory_currentIndexChanged(int); + + +private: + Ui::StyleEditor *ui; + void setColor(QPushButton *btn, QColor col); + void setPropertyModel(GraphElementData *d); + QColor color(QPushButton *btn); + //QString _activeCategory; + Style *activeStyle(); + void refreshActiveStyle(); + TikzStyles *_styles; + void updateColor(QPushButton *btn, QString name, QString propName); + void selectNodeStyle(int i); + void selectEdgeStyle(int i); + + QVector<QWidget*> _formWidgets; + bool _dirty; + + QModelIndex _nodeStyleIndex; + QModelIndex _edgeStyleIndex; + Style *_activeStyle; +}; + +#endif // STYLEEDITOR_H diff --git a/src/gui/styleeditor.ui b/src/gui/styleeditor.ui new file mode 100644 index 0000000..9c06894 --- /dev/null +++ b/src/gui/styleeditor.ui @@ -0,0 +1,829 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>StyleEditor</class> + <widget class="QMainWindow" name="StyleEditor"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>608</width> + <height>517</height> + </rect> + </property> + <property name="windowTitle"> + <string>Style Editor - TikZiT</string> + </property> + <widget class="QWidget" name="centralwidget"> + <widget class="QComboBox" name="currentCategory"> + <property name="geometry"> + <rect> + <x>10</x> + <y>10</y> + <width>181</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QLabel" name="labelName"> + <property name="geometry"> + <rect> + <x>200</x> + <y>50</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Name</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QLabel" name="labelCategory"> + <property name="geometry"> + <rect> + <x>200</x> + <y>80</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Category</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QLabel" name="labelFillColor"> + <property name="geometry"> + <rect> + <x>200</x> + <y>110</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Fill Color</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QLabel" name="labelDrawColor"> + <property name="geometry"> + <rect> + <x>200</x> + <y>140</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Draw Color</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QLineEdit" name="name"> + <property name="geometry"> + <rect> + <x>300</x> + <y>50</y> + <width>301</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + </widget> + <widget class="QComboBox" name="category"> + <property name="geometry"> + <rect> + <x>300</x> + <y>80</y> + <width>301</width> + <height>22</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + <widget class="QPushButton" name="fillColor"> + <property name="geometry"> + <rect> + <x>300</x> + <y>110</y> + <width>31</width> + <height>18</height> + </rect> + </property> + <property name="autoFillBackground"> + <bool>true</bool> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string/> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + <widget class="QPushButton" name="drawColor"> + <property name="geometry"> + <rect> + <x>300</x> + <y>140</y> + <width>31</width> + <height>18</height> + </rect> + </property> + <property name="autoFillBackground"> + <bool>true</bool> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string/> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + <widget class="QCheckBox" name="hasTikzitFillColor"> + <property name="geometry"> + <rect> + <x>420</x> + <y>110</y> + <width>91</width> + <height>17</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + <italic>false</italic> + </font> + </property> + <property name="text"> + <string>in TikZiT:</string> + </property> + </widget> + <widget class="QCheckBox" name="hasTikzitDrawColor"> + <property name="geometry"> + <rect> + <x>420</x> + <y>140</y> + <width>91</width> + <height>17</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + <italic>false</italic> + </font> + </property> + <property name="text"> + <string>in TikZiT:</string> + </property> + </widget> + <widget class="QPushButton" name="tikzitFillColor"> + <property name="geometry"> + <rect> + <x>510</x> + <y>110</y> + <width>31</width> + <height>18</height> + </rect> + </property> + <property name="autoFillBackground"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + <widget class="QPushButton" name="tikzitDrawColor"> + <property name="geometry"> + <rect> + <x>510</x> + <y>140</y> + <width>31</width> + <height>18</height> + </rect> + </property> + <property name="autoFillBackground"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + <widget class="QLabel" name="labelShape"> + <property name="geometry"> + <rect> + <x>200</x> + <y>170</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Shape</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QComboBox" name="shape"> + <property name="geometry"> + <rect> + <x>300</x> + <y>170</y> + <width>231</width> + <height>22</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string/> + </property> + </item> + <item> + <property name="text"> + <string>circle</string> + </property> + </item> + <item> + <property name="text"> + <string>rectangle</string> + </property> + </item> + </widget> + <widget class="QCheckBox" name="hasTikzitShape"> + <property name="geometry"> + <rect> + <x>320</x> + <y>200</y> + <width>91</width> + <height>17</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + <italic>false</italic> + </font> + </property> + <property name="text"> + <string>in TikZiT:</string> + </property> + </widget> + <widget class="QComboBox" name="tikzitShape"> + <property name="geometry"> + <rect> + <x>420</x> + <y>200</y> + <width>111</width> + <height>22</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <item> + <property name="text"> + <string/> + </property> + </item> + <item> + <property name="text"> + <string>circle</string> + </property> + </item> + <item> + <property name="text"> + <string>rectangle</string> + </property> + </item> + </widget> + <widget class="QLabel" name="labelArrowhead"> + <property name="geometry"> + <rect> + <x>200</x> + <y>230</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Arrowhead</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QComboBox" name="leftArrow"> + <property name="geometry"> + <rect> + <x>300</x> + <y>230</y> + <width>41</width> + <height>22</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="editable"> + <bool>false</bool> + </property> + <item> + <property name="text"> + <string/> + </property> + </item> + <item> + <property name="text"> + <string><</string> + </property> + </item> + <item> + <property name="text"> + <string>|</string> + </property> + </item> + </widget> + <widget class="QLabel" name="labelDash"> + <property name="geometry"> + <rect> + <x>350</x> + <y>230</y> + <width>16</width> + <height>21</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>16</pointsize> + </font> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + <widget class="QComboBox" name="rightArrow"> + <property name="geometry"> + <rect> + <x>370</x> + <y>230</y> + <width>41</width> + <height>22</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="editable"> + <bool>false</bool> + </property> + <item> + <property name="text"> + <string/> + </property> + </item> + <item> + <property name="text"> + <string>></string> + </property> + </item> + <item> + <property name="text"> + <string>|</string> + </property> + </item> + </widget> + <widget class="QLabel" name="labelProperties"> + <property name="geometry"> + <rect> + <x>200</x> + <y>270</y> + <width>91</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Properties</string> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + <widget class="QToolButton" name="addProperty"> + <property name="geometry"> + <rect> + <x>300</x> + <y>460</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + <widget class="QToolButton" name="addAtom"> + <property name="geometry"> + <rect> + <x>330</x> + <y>460</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>+a</string> + </property> + </widget> + <widget class="QToolButton" name="removeProperty"> + <property name="geometry"> + <rect> + <x>360</x> + <y>460</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + <widget class="QToolButton" name="propertyUp"> + <property name="geometry"> + <rect> + <x>390</x> + <y>460</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>^</string> + </property> + </widget> + <widget class="QToolButton" name="propertyDown"> + <property name="geometry"> + <rect> + <x>420</x> + <y>460</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>v</string> + </property> + </widget> + <widget class="QListView" name="styleListView"> + <property name="geometry"> + <rect> + <x>10</x> + <y>40</y> + <width>181</width> + <height>251</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + <italic>true</italic> + </font> + </property> + </widget> + <widget class="QListView" name="edgeStyleListView"> + <property name="geometry"> + <rect> + <x>10</x> + <y>330</y> + <width>181</width> + <height>151</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + <italic>true</italic> + </font> + </property> + </widget> + <widget class="QToolButton" name="addStyle"> + <property name="geometry"> + <rect> + <x>10</x> + <y>300</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + <widget class="QToolButton" name="removeStyle"> + <property name="geometry"> + <rect> + <x>40</x> + <y>300</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + <widget class="QToolButton" name="styleUp"> + <property name="geometry"> + <rect> + <x>70</x> + <y>300</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>^</string> + </property> + </widget> + <widget class="QToolButton" name="styleDown"> + <property name="geometry"> + <rect> + <x>100</x> + <y>300</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>v</string> + </property> + </widget> + <widget class="QToolButton" name="removeEdgeStyle"> + <property name="geometry"> + <rect> + <x>40</x> + <y>490</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + <widget class="QToolButton" name="addEdgeStyle"> + <property name="geometry"> + <rect> + <x>10</x> + <y>490</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + <widget class="QToolButton" name="edgeStyleUp"> + <property name="geometry"> + <rect> + <x>70</x> + <y>490</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>^</string> + </property> + </widget> + <widget class="QToolButton" name="edgeStyleDown"> + <property name="geometry"> + <rect> + <x>100</x> + <y>490</y> + <width>23</width> + <height>18</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>v</string> + </property> + </widget> + <widget class="QPushButton" name="save"> + <property name="geometry"> + <rect> + <x>479</x> + <y>490</y> + <width>121</width> + <height>20</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Save and Close</string> + </property> + </widget> + <widget class="QTreeView" name="properties"> + <property name="geometry"> + <rect> + <x>300</x> + <y>270</y> + <width>301</width> + <height>181</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + </font> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>false</bool> + </property> + <attribute name="headerVisible"> + <bool>true</bool> + </attribute> + </widget> + </widget> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/stylepalette.cpp b/src/gui/stylepalette.cpp new file mode 100644 index 0000000..af096c7 --- /dev/null +++ b/src/gui/stylepalette.cpp @@ -0,0 +1,195 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "stylepalette.h" +#include "ui_stylepalette.h" +#include "tikzit.h" + +#include <QDebug> +#include <QIcon> +#include <QSize> +#include <QSettings> +#include <QPainter> +#include <QPixmap> +#include <QPainterPath> +#include <QMessageBox> + +StylePalette::StylePalette(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::StylePalette) +{ + ui->setupUi(this); + +// QSettings settings("tikzit", "tikzit"); +// QVariant geom = settings.value("style-palette-geometry"); +// if (geom != QVariant()) { +// restoreGeometry(geom.toByteArray()); +// } + +// _nodeModel = new QStandardItemModel(this); +// _edgeModel = new QStandardItemModel(this); + + ui->styleListView->setModel(tikzit->styles()->nodeStyles()); + ui->styleListView->setViewMode(QListView::IconMode); + ui->styleListView->setMovement(QListView::Static); + ui->styleListView->setGridSize(QSize(48,48)); + + + ui->edgeStyleListView->setModel(tikzit->styles()->edgeStyles()); + ui->edgeStyleListView->setViewMode(QListView::IconMode); + ui->edgeStyleListView->setMovement(QListView::Static); + ui->edgeStyleListView->setGridSize(QSize(48,48)); + + reloadStyles(); + + connect(ui->styleListView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT( nodeStyleDoubleClicked(const QModelIndex&)) ); + connect(ui->edgeStyleListView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(edgeStyleDoubleClicked(const QModelIndex&))); +} + +StylePalette::~StylePalette() +{ + delete ui; +} + +void StylePalette::reloadStyles() +{ + QString f = tikzit->styleFile(); + ui->styleFile->setText(f); + + ui->styleListView->setModel(tikzit->styles()->nodeStyles()); + ui->edgeStyleListView->setModel(tikzit->styles()->edgeStyles()); + + QString cat = ui->currentCategory->currentText(); + ui->currentCategory->clear(); + + // TODO: styleFile() should return invalid string if no style file loaded + if (f != "[no styles]") { + ui->currentCategory->addItems(tikzit->styles()->categories()); + ui->currentCategory->setCurrentText(cat); + } +} + +void StylePalette::changeNodeStyle(int increment) +{ + QModelIndexList i = ui->styleListView->selectionModel()->selectedIndexes(); + int row = 0; + if (!i.isEmpty()) { + int row = (i[0].row()+increment)% tikzit->styles()->nodeStyles()->numInCategory(); + if (row < 0) row += tikzit->styles()->nodeStyles()->numInCategory(); + } + + //QModelIndex i1 = ui->styleListView->rootIndex().child(row, 0); + QModelIndex i1 =tikzit->styles()->nodeStyles()->index(row,0); + ui->styleListView->selectionModel()->select(i1, QItemSelectionModel::ClearAndSelect); + ui->styleListView->scrollTo(i1); +} + +void StylePalette::nextNodeStyle() +{ + changeNodeStyle(1); +} + +void StylePalette::previousNodeStyle() +{ + changeNodeStyle(-1); +} + +QString StylePalette::activeNodeStyleName() +{ + const QModelIndexList i = ui->styleListView->selectionModel()->selectedIndexes(); + + if (i.isEmpty()) { + return "none"; + } else { + return i[0].data().toString(); + } +} + +QString StylePalette::activeEdgeStyleName() +{ + const QModelIndexList i = ui->edgeStyleListView->selectionModel()->selectedIndexes(); + + if (i.isEmpty()) { + return "none"; + } else { + return i[0].data().toString(); + } +} + +void StylePalette::nodeStyleDoubleClicked(const QModelIndex &) +{ + tikzit->activeWindow()->tikzScene()->applyActiveStyleToNodes(); +} + +void StylePalette::edgeStyleDoubleClicked(const QModelIndex &) +{ + tikzit->activeWindow()->tikzScene()->applyActiveStyleToEdges(); +} + +void StylePalette::on_buttonNewTikzstyles_clicked() +{ + tikzit->newTikzStyles(); +} + +void StylePalette::on_buttonOpenTikzstyles_clicked() +{ + tikzit->openTikzStyles(); +} + +void StylePalette::on_buttonEditTikzstyles_clicked() +{ + if (tikzit->styleFile() != "[no styles]") { + tikzit->showStyleEditor(); + } else { + QMessageBox::warning(0, + "No style file", + "You cannot edit styles until a style file is loaded. Either create a new style file or load an existing one."); + } +} + +void StylePalette::on_buttonRefreshTikzstyles_clicked() +{ + QSettings settings("tikzit", "tikzit"); + QString path = settings.value("previous-tikzstyles-file").toString(); + if (!path.isEmpty()) tikzit->loadStyles(path); +} + +void StylePalette::on_currentCategory_currentTextChanged(const QString &cat) +{ + //tikzit->styles()->refreshModels(_nodeModel, _edgeModel, cat); + tikzit->styles()->nodeStyles()->setCategory(cat); +} + +//void StylePalette::on_buttonApplyNodeStyle_clicked() +//{ +// if (tikzit->activeWindow() != 0) tikzit->activeWindow()->tikzScene()->applyActiveStyleToNodes(); +//} + +void StylePalette::closeEvent(QCloseEvent *event) +{ + QSettings settings("tikzit", "tikzit"); + settings.setValue("style-palette-geometry", saveGeometry()); + QDockWidget::closeEvent(event); +} + +void StylePalette::resizeEvent(QResizeEvent *event) +{ + QDockWidget::resizeEvent(event); + ui->styleListView->setGridSize(QSize(48,48)); + ui->edgeStyleListView->setGridSize(QSize(48,48)); +} diff --git a/src/gui/stylepalette.h b/src/gui/stylepalette.h new file mode 100644 index 0000000..e83f961 --- /dev/null +++ b/src/gui/stylepalette.h @@ -0,0 +1,63 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#ifndef STYLEPALETTE_H +#define STYLEPALETTE_H + +#include <QDockWidget> +#include <QStandardItemModel> + +namespace Ui { +class StylePalette; +} + +class StylePalette : public QDockWidget +{ + Q_OBJECT + +public: + explicit StylePalette(QWidget *parent = 0); + ~StylePalette(); + void reloadStyles(); + void nextNodeStyle(); + void previousNodeStyle(); + QString activeNodeStyleName(); + QString activeEdgeStyleName(); + + +public slots: + void nodeStyleDoubleClicked(const QModelIndex &); + void edgeStyleDoubleClicked(const QModelIndex &); + void on_buttonNewTikzstyles_clicked(); + void on_buttonOpenTikzstyles_clicked(); + void on_buttonEditTikzstyles_clicked(); + void on_buttonRefreshTikzstyles_clicked(); + void on_currentCategory_currentTextChanged(const QString &cat); + //void on_buttonApplyNodeStyle_clicked(); + +private: + void changeNodeStyle(int increment); + + Ui::StylePalette *ui; + +protected: + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; +}; + +#endif // STYLEPALETTE_H diff --git a/src/gui/stylepalette.ui b/src/gui/stylepalette.ui new file mode 100644 index 0000000..94e8a7c --- /dev/null +++ b/src/gui/stylepalette.ui @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>StylePalette</class> + <widget class="QDockWidget" name="StylePalette"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>130</width> + <height>506</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>130</width> + <height>268</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>400</width> + <height>524287</height> + </size> + </property> + <property name="floating"> + <bool>false</bool> + </property> + <property name="windowTitle"> + <string>Styles</string> + </property> + <widget class="QWidget" name="dockWidgetContents"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="styleFile"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>[no style file]</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>2</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolButton" name="buttonNewTikzstyles"> + <property name="toolTip"> + <string>New Style File</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../tikzit.qrc"> + <normaloff>:/images/document-new.svg</normaloff>:/images/document-new.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="buttonOpenTikzstyles"> + <property name="toolTip"> + <string>Load Style File</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../tikzit.qrc"> + <normaloff>:/images/document-open.svg</normaloff>:/images/document-open.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="buttonEditTikzstyles"> + <property name="toolTip"> + <string>Edit Styles</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../tikzit.qrc"> + <normaloff>:/images/text-x-generic_with_pencil.svg</normaloff>:/images/text-x-generic_with_pencil.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="buttonRefreshTikzstyles"> + <property name="toolTip"> + <string>Refresh Styles</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../tikzit.qrc"> + <normaloff>:/images/refresh.svg</normaloff>:/images/refresh.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QComboBox" name="currentCategory"/> + </item> + <item> + <widget class="QListView" name="styleListView"> + <property name="font"> + <font> + <pointsize>8</pointsize> + <italic>true</italic> + </font> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOn</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="resizeMode"> + <enum>QListView::Adjust</enum> + </property> + </widget> + </item> + <item> + <widget class="QListView" name="edgeStyleListView"> + <property name="font"> + <font> + <pointsize>8</pointsize> + <italic>true</italic> + </font> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <resources> + <include location="../../tikzit.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/gui/tikzscene.cpp b/src/gui/tikzscene.cpp new file mode 100644 index 0000000..c061221 --- /dev/null +++ b/src/gui/tikzscene.cpp @@ -0,0 +1,924 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "tikzit.h" +#include "util.h" +#include "tikzscene.h" +#include "undocommands.h" +#include "tikzassembler.h" + +#include <QPen> +#include <QBrush> +#include <QDebug> +#include <QClipboard> +#include <QInputDialog> +#include <cmath> + + +TikzScene::TikzScene(TikzDocument *tikzDocument, ToolPalette *tools, + StylePalette *styles, QObject *parent) : + QGraphicsScene(parent), _tikzDocument(tikzDocument), _tools(tools), _styles(styles) +{ + _modifyEdgeItem = 0; + _edgeStartNodeItem = 0; + _drawEdgeItem = new QGraphicsLineItem(); + _rubberBandItem = new QGraphicsRectItem(); + _enabled = true; + //setSceneRect(-310,-230,620,450); + setSceneRect(-1000,-1000,2000,2000); + + QPen pen; + pen.setColor(QColor::fromRgbF(0.5f, 0.0f, 0.5f)); + //pen.setWidth(3.0f); + pen.setCosmetic(true); + _drawEdgeItem->setPen(pen); + _drawEdgeItem->setLine(0,0,0,0); + _drawEdgeItem->setVisible(false); + addItem(_drawEdgeItem); + + pen.setColor(QColor::fromRgbF(0.6f, 0.6f, 0.8f)); + //pen.setWidth(3.0f); + //QVector<qreal> dash; + //dash << 4.0 << 4.0; + pen.setStyle(Qt::DashLine); + //pen.setDashPattern(dash); + _rubberBandItem->setPen(pen); + + QBrush brush(QColor::fromRgbF(0.6,0.6,0.8,0.2)); + _rubberBandItem->setBrush(brush); + + _rubberBandItem->setVisible(false); + addItem(_rubberBandItem); +} + +TikzScene::~TikzScene() { +} + +Graph *TikzScene::graph() +{ + return _tikzDocument->graph(); +} + +void TikzScene::graphReplaced() +{ + + foreach (NodeItem *ni, _nodeItems) { + removeItem(ni); + delete ni; + } + _nodeItems.clear(); + + foreach (EdgeItem *ei, _edgeItems) { + removeItem(ei); + delete ei; + } + _edgeItems.clear(); + + foreach (Edge *e, graph()->edges()) { + //e->attachStyle(); + //e->updateControls(); + EdgeItem *ei = new EdgeItem(e); + _edgeItems.insert(e, ei); + addItem(ei); + } + + foreach (Node *n, graph()->nodes()) { + //n->attachStyle(); + NodeItem *ni = new NodeItem(n); + _nodeItems.insert(n, ni); + addItem(ni); + } + + refreshZIndices(); +} + +void TikzScene::extendSelectionUp() +{ + bool found = false; + float m = 0.0f; + foreach (Node *n, getSelectedNodes()) { + if (!found) { + m = n->point().y(); + found = true; + } else { + if (n->point().y() > m) m = n->point().y(); + } + } + + foreach (NodeItem *ni, nodeItems().values()) { + if (ni->node()->point().y() >= m) ni->setSelected(true); + } +} + +void TikzScene::extendSelectionDown() +{ + bool found = false; + float m = 0.0f; + foreach (Node *n, getSelectedNodes()) { + if (!found) { + m = n->point().y(); + found = true; + } else { + if (n->point().y() < m) m = n->point().y(); + } + } + + foreach (NodeItem *ni, nodeItems().values()) { + if (ni->node()->point().y() <= m) ni->setSelected(true); + } +} + +void TikzScene::extendSelectionLeft() +{ + bool found = false; + float m = 0.0f; + foreach (Node *n, getSelectedNodes()) { + if (!found) { + m = n->point().x(); + found = true; + } else { + if (n->point().x() < m) m = n->point().x(); + } + } + + foreach (NodeItem *ni, nodeItems().values()) { + if (ni->node()->point().x() <= m) ni->setSelected(true); + } +} + +void TikzScene::extendSelectionRight() +{ + bool found = false; + float m = 0.0f; + foreach (Node *n, getSelectedNodes()) { + if (!found) { + m = n->point().x(); + found = true; + } else { + if (n->point().x() < m) m = n->point().x(); + } + } + + foreach (NodeItem *ni, nodeItems().values()) { + if (ni->node()->point().x() >= m) ni->setSelected(true); + } +} + +void TikzScene::reorderSelection(bool toFront) +{ + QVector<Node*> nodeOrd, nodeOrd1; + QVector<Edge*> edgeOrd, edgeOrd1; + QSet<Node*> selNodes; + QSet<Edge*> selEdges; + getSelection(selNodes, selEdges); + foreach (Node *n, graph()->nodes()) { + if (selNodes.contains(n)) nodeOrd1 << n; + else nodeOrd << n; + } + + foreach (Edge *e, graph()->edges()) { + if (selEdges.contains(e)) edgeOrd1 << e; + else edgeOrd << e; + } + + if (toFront) { + nodeOrd += nodeOrd1; + edgeOrd += edgeOrd1; + } else { + nodeOrd = nodeOrd1 + nodeOrd; + edgeOrd = edgeOrd1 + edgeOrd; + } + + ReorderCommand *cmd = new ReorderCommand(this, graph()->nodes(), nodeOrd, graph()->edges(), edgeOrd); + _tikzDocument->undoStack()->push(cmd); +} + +void TikzScene::refreshZIndices() +{ + qreal z = 0.0; + foreach (Edge *e, graph()->edges()) { + edgeItems()[e]->setZValue(z); + z += 1.0; + } + + foreach (Node *n, graph()->nodes()) { + nodeItems()[n]->setZValue(z); + z += 1.0; + } +} + +void TikzScene::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (!_enabled) return; + + // current mouse position, in scene coordinates + _mouseDownPos = event->scenePos(); + + _draggingNodes = false; + + // disable rubber band drag, which will clear the selection. Only re-enable it + // for the SELECT tool, and when no control point has been clicked. + //views()[0]->setDragMode(QGraphicsView::NoDrag); + + // radius of a control point for bezier edges, in scene coordinates + qreal cpR = GLOBAL_SCALEF * (0.1); + qreal cpR2 = cpR * cpR; + + switch (_tools->currentTool()) { + case ToolPalette::SELECT: + // check if we grabbed a control point of an edge + foreach (QGraphicsItem *gi, selectedItems()) { + if (EdgeItem *ei = dynamic_cast<EdgeItem*>(gi)) { + qreal dx, dy; + + dx = ei->cp1Item()->pos().x() - _mouseDownPos.x(); + dy = ei->cp1Item()->pos().y() - _mouseDownPos.y(); + + if (dx*dx + dy*dy <= cpR2) { + _modifyEdgeItem = ei; + _firstControlPoint = true; + break; + } + + dx = ei->cp2Item()->pos().x() - _mouseDownPos.x(); + dy = ei->cp2Item()->pos().y() - _mouseDownPos.y(); + + if (dx*dx + dy*dy <= cpR2) { + _modifyEdgeItem = ei; + _firstControlPoint = false; + break; + } + } + } + + if (_modifyEdgeItem != 0) { + // store for undo purposes + Edge *e = _modifyEdgeItem->edge(); + _oldBend = e->bend(); + _oldInAngle = e->inAngle(); + _oldOutAngle = e->outAngle(); + _oldWeight = e->weight(); + } else { + // since we are not dragging a control point, process the click normally + //views()[0]->setDragMode(QGraphicsView::RubberBandDrag); + QGraphicsScene::mousePressEvent(event); + + if (items(_mouseDownPos).isEmpty()) { + _rubberBandItem->setRect(QRectF(_mouseDownPos,_mouseDownPos)); + _rubberBandItem->setVisible(true); + //qDebug() << "starting rubber band drag"; + } + +// foreach (QGraphicsItem *gi, items()) { +// if (EdgeItem *ei = dynamic_cast<EdgeItem*>(gi)) { +// //qDebug() << "got an edge item: " << ei; +// ei->setFlag(QGraphicsItem::ItemIsSelectable, false); +// //ei->setSelected(true); +// } +// } + + // save current node positions for undo support + _oldNodePositions.clear(); + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) { + _oldNodePositions.insert(ni->node(), ni->node()->point()); + } + } + + auto its = items(_mouseDownPos); + if (!its.isEmpty() && dynamic_cast<NodeItem*>(its[0])) + _draggingNodes = true; + } + + break; + case ToolPalette::VERTEX: + break; + case ToolPalette::EDGE: + foreach (QGraphicsItem *gi, items(_mouseDownPos)) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)){ + _edgeStartNodeItem = ni; + _edgeEndNodeItem = ni; + QLineF line(toScreen(ni->node()->point()), _mouseDownPos); + _drawEdgeItem->setLine(line); + _drawEdgeItem->setVisible(true); + break; + } + } + break; + case ToolPalette::CROP: + break; + } +} + +void TikzScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (!_enabled) return; + + // current mouse position, in scene coordinates + QPointF mousePos = event->scenePos(); + //QRectF rb = views()[0]->rubberBandRect(); + //invalidate(-800,-800,1600,1600); + //invalidate(QRectF(), QGraphicsScene::BackgroundLayer); + + switch (_tools->currentTool()) { + case ToolPalette::SELECT: + if (_modifyEdgeItem != 0) { + Edge *e = _modifyEdgeItem->edge(); + + // dragging a control point + QPointF src = toScreen(e->source()->point()); + QPointF targ = toScreen(e->target()->point()); + float dx1 = targ.x() - src.x(); + float dy1 = targ.y() - src.y(); + float dx2, dy2; + if (_firstControlPoint) { + dx2 = mousePos.x() - src.x(); + dy2 = mousePos.y() - src.y(); + } else { + dx2 = mousePos.x() - targ.x(); + dy2 = mousePos.y() - targ.y(); + } + + float baseDist = sqrt(dx1*dx1 + dy1*dy1); + float handleDist = sqrt(dx2*dx2 + dy2*dy2); + float wcoarseness = 0.1f; + + if (!e->isSelfLoop()) { + if (baseDist != 0) { + e->setWeight(roundToNearest(wcoarseness, handleDist/baseDist)); + } else { + e->setWeight(roundToNearest(wcoarseness, handleDist/GLOBAL_SCALEF)); + } + } + + float control_angle = atan2(-dy2, dx2); + + int bcoarseness = 15; + + if(e->basicBendMode()) { + float bnd; + float base_angle = atan2(-dy1, dx1); + if (_firstControlPoint) { + bnd = base_angle - control_angle; + } else { + bnd = control_angle - base_angle + M_PI; + if (bnd > M_PI) bnd -= 2*M_PI; + } + + e->setBend(round(bnd * (180.0f / M_PI) * (1.0f / (float)bcoarseness)) * bcoarseness); + + } else { + int bnd = round(control_angle * (180.0f / M_PI) * + (1.0f / (float)bcoarseness)) * + bcoarseness; + if (_firstControlPoint) { + // TODO: enable moving both control points +// if ([theEvent modifierFlags] & NSAlternateKeyMask) { +// if ([modifyEdge isSelfLoop]) { +// [modifyEdge setInAngle:[modifyEdge inAngle] + +// (bnd - [modifyEdge outAngle])]; +// } else { +// [modifyEdge setInAngle:[modifyEdge inAngle] - +// (bnd - [modifyEdge outAngle])]; +// } +// } + + e->setOutAngle(bnd); + } else { +// if (theEvent.modifierFlags & NSAlternateKeyMask) { +// if ([modifyEdge isSelfLoop]) { +// [modifyEdge setOutAngle:[modifyEdge outAngle] + +// (bnd - [modifyEdge inAngle])]; +// } else { +// [modifyEdge setOutAngle:[modifyEdge outAngle] - +// (bnd - [modifyEdge inAngle])]; +// } +// } + + e->setInAngle(bnd); + } + } + + _modifyEdgeItem->readPos(); + + } else if (_draggingNodes) { // nodes being dragged + QGraphicsScene::mouseMoveEvent(event); + + // apply the same offset to all nodes, otherwise we get odd rounding behaviour with + // multiple selection. + QPointF shift = mousePos - _mouseDownPos; + shift = QPointF(round(shift.x()/GRID_SEP)*GRID_SEP, round(shift.y()/GRID_SEP)*GRID_SEP); + + foreach (Node *n, _oldNodePositions.keys()) { + NodeItem *ni = _nodeItems[n]; + + // in (rare) cases, the graph can change while we are dragging + if (ni != 0) { + ni->setPos(toScreen(_oldNodePositions[n]) + shift); + ni->writePos(); + } + } + + refreshAdjacentEdges(_oldNodePositions.keys()); + } else { + // otherwise, process mouse move normally + QGraphicsScene::mouseMoveEvent(event); + + if (_rubberBandItem->isVisible()) { + qreal left = std::min(_mouseDownPos.x(), mousePos.x()); + qreal top = std::min(_mouseDownPos.y(), mousePos.y()); + qreal w = std::abs(_mouseDownPos.x() - mousePos.x()); + qreal h = std::abs(_mouseDownPos.y() - mousePos.y()); + + _rubberBandItem->setRect(QRectF(left, top, w, h)); + } + } + + break; + case ToolPalette::VERTEX: + break; + case ToolPalette::EDGE: + if (_drawEdgeItem->isVisible()) { + _edgeEndNodeItem = 0; + foreach (QGraphicsItem *gi, items(mousePos)) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)){ + _edgeEndNodeItem = ni; + break; + } + } + QPointF p1 = _drawEdgeItem->line().p1(); + QPointF p2 = (_edgeEndNodeItem != 0) ? toScreen(_edgeEndNodeItem->node()->point()) : mousePos; + QLineF line(p1, p2); + + _drawEdgeItem->setLine(line); + } + break; + case ToolPalette::CROP: + break; + } +} + +void TikzScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (!_enabled) return; + + // current mouse position, in scene coordinates + QPointF mousePos = event->scenePos(); + + switch (_tools->currentTool()) { + case ToolPalette::SELECT: + if (_modifyEdgeItem != 0) { + // finished dragging a control point + Edge *e = _modifyEdgeItem->edge(); + + if (_oldWeight != e->weight() || + _oldBend != e->bend() || + _oldInAngle != e->inAngle() || + _oldOutAngle != e->outAngle()) + { + EdgeBendCommand *cmd = new EdgeBendCommand(this, e, _oldWeight, _oldBend, _oldInAngle, _oldOutAngle); + _tikzDocument->undoStack()->push(cmd); + } + + _modifyEdgeItem = 0; + } else { + // otherwise, process mouse move normally + QGraphicsScene::mouseReleaseEvent(event); + + if (_rubberBandItem->isVisible()) { + QPainterPath sel; + sel.addRect(_rubberBandItem->rect()); + foreach (QGraphicsItem *gi, items()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) { + if (sel.contains(toScreen(ni->node()->point()))) ni->setSelected(true); + } + } + //setSelectionArea(sel); + } + + _rubberBandItem->setVisible(false); + + if (!_oldNodePositions.empty()) { + QPointF shift = mousePos - _mouseDownPos; + shift = QPointF(round(shift.x()/GRID_SEP)*GRID_SEP, round(shift.y()/GRID_SEP)*GRID_SEP); + + if (shift.x() != 0 || shift.y() != 0) { + QMap<Node*,QPointF> newNodePositions; + + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) { + ni->writePos(); + newNodePositions.insert(ni->node(), ni->node()->point()); + } + } + + //qDebug() << _oldNodePositions; + //qDebug() << newNodePositions; + + _tikzDocument->undoStack()->push(new MoveCommand(this, _oldNodePositions, newNodePositions)); + } + + _oldNodePositions.clear(); + } + } + + break; + case ToolPalette::VERTEX: + { + QPointF gridPos(round(mousePos.x()/GRID_SEP)*GRID_SEP, round(mousePos.y()/GRID_SEP)*GRID_SEP); + Node *n = new Node(_tikzDocument); + n->setName(graph()->freshNodeName()); + n->setPoint(fromScreen(gridPos)); + n->setStyleName(_styles->activeNodeStyleName()); + + QRectF grow(gridPos.x() - GLOBAL_SCALEF, gridPos.y() - GLOBAL_SCALEF, 2 * GLOBAL_SCALEF, 2 * GLOBAL_SCALEF); + QRectF newBounds = sceneRect().united(grow); + //qDebug() << grow; + //qDebug() << newBounds; + + AddNodeCommand *cmd = new AddNodeCommand(this, n, newBounds); + _tikzDocument->undoStack()->push(cmd); + } + break; + case ToolPalette::EDGE: + // add an edge + if (_edgeStartNodeItem != 0 && _edgeEndNodeItem != 0) { + Edge *e = new Edge(_edgeStartNodeItem->node(), _edgeEndNodeItem->node(), _tikzDocument); + e->setStyleName(_styles->activeEdgeStyleName()); + AddEdgeCommand *cmd = new AddEdgeCommand(this, e); + _tikzDocument->undoStack()->push(cmd); + } + _edgeStartNodeItem = 0; + _edgeEndNodeItem = 0; + _drawEdgeItem->setVisible(false); + break; + case ToolPalette::CROP: + break; + } + + // clear artefacts from rubber band selection + invalidate(QRect(), QGraphicsScene::BackgroundLayer); +} + + + +void TikzScene::keyReleaseEvent(QKeyEvent *event) +{ + if (!_enabled) return; + + if (event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete) { + deleteSelectedItems(); + } else if (event->modifiers() == Qt::NoModifier) { + switch(event->key()) { + case Qt::Key_S: + tikzit->activeWindow()->toolPalette()->setCurrentTool(ToolPalette::SELECT); + break; + case Qt::Key_V: + case Qt::Key_N: + tikzit->activeWindow()->toolPalette()->setCurrentTool(ToolPalette::VERTEX); + break; + case Qt::Key_E: + tikzit->activeWindow()->toolPalette()->setCurrentTool(ToolPalette::EDGE); + break; + case Qt::Key_B: + tikzit->activeWindow()->toolPalette()->setCurrentTool(ToolPalette::CROP); + break; + } + } +} + +void TikzScene::keyPressEvent(QKeyEvent *event) +{ + bool capture = false; + + if (event->key() == Qt::Key_QuoteLeft) { + capture = true; + _styles->nextNodeStyle(); + } + + if (event->modifiers() & Qt::ControlModifier) { + QPointF delta(0,0); + float shift = (event->modifiers() & Qt::ShiftModifier) ? 1.0f : 10.0f; + switch(event->key()) { + case Qt::Key_Left: + delta.setX(-0.025f * shift); + break; + case Qt::Key_Right: + delta.setX(0.025f * shift); + break; + case Qt::Key_Up: + delta.setY(0.025f * shift); + break; + case Qt::Key_Down: + delta.setY(-0.025f * shift); + break; + } + + if (!delta.isNull()) { + capture = true; + QMap<Node*,QPointF> oldNodePositions; + QMap<Node*,QPointF> newNodePositions; + QPointF pos; + + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) { + pos = ni->node()->point(); + oldNodePositions.insert(ni->node(), pos); + newNodePositions.insert(ni->node(), pos + delta); + } + } + + MoveCommand *cmd = new MoveCommand(this, oldNodePositions, newNodePositions); + _tikzDocument->undoStack()->push(cmd); + } + } + + if (!capture) QGraphicsScene::keyPressEvent(event); +} + +void TikzScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (!_enabled) return; + + QPointF mousePos = event->scenePos(); + + foreach (QGraphicsItem *it, items(mousePos)) { + if (EdgeItem *ei = dynamic_cast<EdgeItem*>(it)) { + if (!ei->edge()->isSelfLoop()) { + ChangeEdgeModeCommand *cmd = new ChangeEdgeModeCommand(this, ei->edge()); + _tikzDocument->undoStack()->push(cmd); + } + break; + } else if (NodeItem *ni = dynamic_cast<NodeItem*>(it)) { + bool ok; + QString newLabel = QInputDialog::getText(views()[0], tr("Node label"), + tr("Label:"), QLineEdit::Normal, + ni->node()->label(), &ok); + if (ok) { + QMap<Node*,QString> oldLabels; + oldLabels.insert(ni->node(), ni->node()->label()); + ChangeLabelCommand *cmd = new ChangeLabelCommand(this, oldLabels, newLabel); + _tikzDocument->undoStack()->push(cmd); + } + break; + } + } +} + +bool TikzScene::enabled() const +{ + return _enabled; +} + +void TikzScene::setEnabled(bool enabled) +{ + _enabled = enabled; + update(); +} + +int TikzScene::lineNumberForSelection() +{ + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) return ni->node()->tikzLine(); + if (EdgeItem *ei = dynamic_cast<EdgeItem*>(gi)) return ei->edge()->tikzLine(); + } + return 0; +} + + +void TikzScene::applyActiveStyleToNodes() { + ApplyStyleToNodesCommand *cmd = new ApplyStyleToNodesCommand(this, _styles->activeNodeStyleName()); + _tikzDocument->undoStack()->push(cmd); +} + +void TikzScene::applyActiveStyleToEdges() { + ApplyStyleToEdgesCommand *cmd = new ApplyStyleToEdgesCommand(this, _styles->activeEdgeStyleName()); + _tikzDocument->undoStack()->push(cmd); +} + +void TikzScene::deleteSelectedItems() +{ + QSet<Node*> selNodes; + QSet<Edge*> selEdges; + getSelection(selNodes, selEdges); + + QMap<int,Node*> deleteNodes; + QMap<int,Edge*> deleteEdges; + + for (int i = 0; i < _tikzDocument->graph()->nodes().length(); ++i) { + Node *n = _tikzDocument->graph()->nodes()[i]; + if (selNodes.contains(n)) deleteNodes.insert(i, n); + } + + for (int i = 0; i < _tikzDocument->graph()->edges().length(); ++i) { + Edge *e = _tikzDocument->graph()->edges()[i]; + if (selEdges.contains(e) || + selNodes.contains(e->source()) || + selNodes.contains(e->target())) deleteEdges.insert(i, e); + } + + //qDebug() << "nodes:" << deleteNodes; + //qDebug() << "edges:" << deleteEdges; + DeleteCommand *cmd = new DeleteCommand(this, deleteNodes, deleteEdges, selEdges); + _tikzDocument->undoStack()->push(cmd); +} + +void TikzScene::copyToClipboard() +{ + Graph *g = graph()->copyOfSubgraphWithNodes(getSelectedNodes()); + //qDebug() << g->tikz(); + QGuiApplication::clipboard()->setText(g->tikz()); + delete g; +} + +void TikzScene::cutToClipboard() +{ + copyToClipboard(); + deleteSelectedItems(); +} + + +void TikzScene::pasteFromClipboard() +{ + QString tikz = QGuiApplication::clipboard()->text(); + Graph *g = new Graph(); + TikzAssembler ass(g); + + // attempt to parse whatever's on the clipboard, if we get a + // non-empty tikz graph, insert it. + if (ass.parse(tikz) && !g->nodes().isEmpty()) { + // make sure names in the new subgraph are fresh + g->renameApart(graph()); + + QRectF srcRect = g->realBbox(); + QRectF tgtRect = graph()->realBbox(); + QPointF shift(tgtRect.right() - srcRect.left(), 0.0f); + + if (shift.x() > 0) { + foreach (Node *n, g->nodes()) { + n->setPoint(n->point() + shift); + } + } + + PasteCommand *cmd = new PasteCommand(this, g); + _tikzDocument->undoStack()->push(cmd); + } +} + +void TikzScene::selectAllNodes() +{ + foreach (NodeItem *ni, _nodeItems.values()) { + ni->setSelected(true); + } +} + +void TikzScene::deselectAll() +{ + selectedItems().clear(); +} + +bool TikzScene::parseTikz(QString tikz) +{ + Graph *newGraph = new Graph(this); + TikzAssembler ass(newGraph); + if (ass.parse(tikz)) { + ReplaceGraphCommand *cmd = new ReplaceGraphCommand(this, graph(), newGraph); + tikzDocument()->undoStack()->push(cmd); + setEnabled(true); + views()[0]->setFocus(); + return true; + } else { + return false; + } +} + +void TikzScene::reflectNodes(bool horizontal) +{ + ReflectNodesCommand *cmd = new ReflectNodesCommand(this, getSelectedNodes(), horizontal); + tikzDocument()->undoStack()->push(cmd); +} + +void TikzScene::rotateNodes(bool clockwise) +{ + RotateNodesCommand *cmd = new RotateNodesCommand(this, getSelectedNodes(), clockwise); + tikzDocument()->undoStack()->push(cmd); +} + + +void TikzScene::getSelection(QSet<Node *> &selNodes, QSet<Edge *> &selEdges) +{ + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) selNodes << ni->node(); + if (EdgeItem *ei = dynamic_cast<EdgeItem*>(gi)) selEdges << ei->edge(); + } +} + +QSet<Node *> TikzScene::getSelectedNodes() +{ + QSet<Node*> selNodes; + foreach (QGraphicsItem *gi, selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(gi)) selNodes << ni->node(); + } + return selNodes; +} + + +TikzDocument *TikzScene::tikzDocument() const +{ + return _tikzDocument; +} + +void TikzScene::setTikzDocument(TikzDocument *tikzDocument) +{ + _tikzDocument = tikzDocument; + graphReplaced(); +} + +void TikzScene::reloadStyles() +{ + _styles->reloadStyles(); + foreach(EdgeItem *ei, _edgeItems) { + ei->edge()->attachStyle(); + ei->readPos(); // trigger a repaint + } + + foreach (NodeItem *ni, _nodeItems) { + ni->node()->attachStyle(); + ni->readPos(); // trigger a repaint + } +} + +// void TikzScene::refreshSceneBounds() +// { +// // if (!views().empty()) { +// // QGraphicsView *v = views().first(); +// // QRectF viewB = v->mapToScene(v->viewport()->rect()).boundingRect(); +// // //QPointF tl = v->mapToScene(viewB.topLeft()); +// // //viewB.setTopLeft(tl); +// +// // QRectF bounds = viewB.united(rectToScreen(graph()->realBbox().adjusted(-1.0f, -1.0f, 1.0f, 1.0f))); +// // //qDebug() << viewB; +// +// // if (bounds != sceneRect()) { +// // QPointF c = viewB.center(); +// // setSceneRect(bounds); +// // v->centerOn(c); +// // } +// // } +// //setBounds(graphB); +// } + +void TikzScene::refreshAdjacentEdges(QList<Node*> nodes) +{ + if (nodes.empty()) return; + foreach (Edge *e, _edgeItems.keys()) { + EdgeItem *ei = _edgeItems[e]; + + // the list "nodes" can be out of date, e.g. if the graph changes while dragging + if (ei != 0) { + if (nodes.contains(ei->edge()->source()) || nodes.contains(ei->edge()->target())) { + ei->edge()->updateControls(); + ei->readPos(); + } + } + } +} + +//void TikzScene::setBounds(QRectF bounds) +//{ +// if (bounds != sceneRect()) { +// if (!views().empty()) { +// QGraphicsView *v = views().first(); +// QPointF c = v->mapToScene(v->viewport()->rect().center()); +// setSceneRect(bounds); +// v->centerOn(c); +// } else { +// setSceneRect(bounds); +// } +// } +//} + +QMap<Node*,NodeItem *> &TikzScene::nodeItems() +{ + return _nodeItems; +} + +QMap<Edge*,EdgeItem*> &TikzScene::edgeItems() +{ + return _edgeItems; +} diff --git a/src/gui/tikzscene.h b/src/gui/tikzscene.h new file mode 100644 index 0000000..2a3e988 --- /dev/null +++ b/src/gui/tikzscene.h @@ -0,0 +1,118 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * Manage the scene, which contains a single Graph, and respond to user input. This serves as + * the controller for the MVC (TikzDocument, TikzView, TikzScene). + */ + +#ifndef TIKZSCENE_H +#define TIKZSCENE_H + +#include "graph.h" +#include "nodeitem.h" +#include "edgeitem.h" +#include "tikzdocument.h" +#include "toolpalette.h" +#include "stylepalette.h" + +#include <QWidget> +#include <QGraphicsScene> +#include <QPainter> +#include <QRectF> +#include <QVector> +#include <QGraphicsEllipseItem> +#include <QGraphicsSceneMouseEvent> + +class TikzScene : public QGraphicsScene +{ + Q_OBJECT +public: + TikzScene(TikzDocument *tikzDocument, ToolPalette *tools, StylePalette *styles, QObject *parent); + ~TikzScene(); + Graph *graph(); + QMap<Node*,NodeItem*> &nodeItems(); + QMap<Edge*,EdgeItem*> &edgeItems(); + void refreshAdjacentEdges(QList<Node*> nodes); +// void setBounds(QRectF bounds); + + TikzDocument *tikzDocument() const; + void setTikzDocument(TikzDocument *tikzDocument); + void reloadStyles(); + //void refreshSceneBounds(); + void applyActiveStyleToNodes(); + void applyActiveStyleToEdges(); + void deleteSelectedItems(); + void copyToClipboard(); + void cutToClipboard(); + void pasteFromClipboard(); + void selectAllNodes(); + void deselectAll(); + bool parseTikz(QString tikz); + void reflectNodes(bool horizontal); + void rotateNodes(bool clockwise); + bool enabled() const; + void setEnabled(bool enabled); + int lineNumberForSelection(); + + void extendSelectionUp(); + void extendSelectionDown(); + void extendSelectionLeft(); + void extendSelectionRight(); + + void reorderSelection(bool toFront); + + + void getSelection(QSet<Node*> &selNodes, QSet<Edge*> &selEdges); + QSet<Node*> getSelectedNodes(); + +public slots: + void graphReplaced(); + void refreshZIndices(); + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; +private: + TikzDocument *_tikzDocument; + ToolPalette *_tools; + StylePalette *_styles; + QMap<Node*,NodeItem*> _nodeItems; + QMap<Edge*,EdgeItem*> _edgeItems; + QGraphicsLineItem *_drawEdgeItem; + QGraphicsRectItem *_rubberBandItem; + EdgeItem *_modifyEdgeItem; + NodeItem *_edgeStartNodeItem; + NodeItem *_edgeEndNodeItem; + bool _firstControlPoint; + QPointF _mouseDownPos; + bool _draggingNodes; + + QMap<Node*,QPointF> _oldNodePositions; + float _oldWeight; + int _oldBend; + int _oldInAngle; + int _oldOutAngle; + bool _enabled; +}; + +#endif // TIKZSCENE_H diff --git a/src/gui/tikzview.cpp b/src/gui/tikzview.cpp new file mode 100644 index 0000000..52a32cf --- /dev/null +++ b/src/gui/tikzview.cpp @@ -0,0 +1,146 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "tikzview.h" +#include "tikzit.h" + +#include <QDebug> +#include <QScrollBar> + +TikzView::TikzView(QWidget *parent) : QGraphicsView(parent) +{ + setRenderHint(QPainter::Antialiasing); + setResizeAnchor(QGraphicsView::AnchorViewCenter); + //setDragMode(QGraphicsView::RubberBandDrag); + + setBackgroundBrush(QBrush(Qt::white)); + + _scale = 1.0f; +} + +void TikzView::zoomIn() +{ + _scale *= 1.6f; + scale(1.6,1.6); +} + +void TikzView::zoomOut() +{ + _scale *= 0.625f; + scale(0.625,0.625); +} + +void TikzView::setScene(QGraphicsScene *scene) +{ + QGraphicsView::setScene(scene); + centerOn(QPointF(0.0f,0.0f)); +} + +void TikzView::drawBackground(QPainter *painter, const QRectF &rect) +{ + QGraphicsView::drawBackground(painter, rect); + // draw a gray background if disabled + TikzScene *sc = static_cast<TikzScene*>(scene()); + if (!sc->enabled()) painter->fillRect(rect, QBrush(QColor(240,240,240))); + + // draw the grid + + QPen pen1; + //pen1.setWidthF(0.5); + pen1.setCosmetic(true); + pen1.setColor(QColor(250,250,255)); + + QPen pen2 = pen1; + pen2.setColor(QColor(240,240,250)); + + QPen pen3 = pen1; + pen3.setColor(QColor(220,220,240)); + + painter->setPen(pen1); + + if (_scale > 0.2f) { + for (int x = -GRID_SEP; x > rect.left(); x -= GRID_SEP) { + if (x % (GRID_SEP * GRID_N) != 0) { + qreal xf = (qreal)x; + painter->drawLine(xf, rect.top(), xf, rect.bottom()); + } + } + + for (int x = GRID_SEP; x < rect.right(); x += GRID_SEP) { + if (x % (GRID_SEP * GRID_N) != 0) { + qreal xf = (qreal)x; + painter->drawLine(xf, rect.top(), xf, rect.bottom()); + } + } + + for (int y = -GRID_SEP; y > rect.top(); y -= GRID_SEP) { + if (y % (GRID_SEP * GRID_N) != 0) { + qreal yf = (qreal)y; + painter->drawLine(rect.left(), yf, rect.right(), yf); + } + } + + for (int y = GRID_SEP; y < rect.bottom(); y += GRID_SEP) { + if (y % (GRID_SEP * GRID_N) != 0) { + qreal yf = (qreal)y; + painter->drawLine(rect.left(), yf, rect.right(), yf); + } + } + } + + painter->setPen(pen2); + + for (int x = -GRID_SEP*GRID_N; x > rect.left(); x -= GRID_SEP*GRID_N) { + qreal xf = (qreal)x; + painter->drawLine(xf, rect.top(), xf, rect.bottom()); + } + + for (int x = GRID_SEP*GRID_N; x < rect.right(); x += GRID_SEP*GRID_N) { + qreal xf = (qreal)x; + painter->drawLine(xf, rect.top(), xf, rect.bottom()); + } + + for (int y = -GRID_SEP*GRID_N; y > rect.top(); y -= GRID_SEP*GRID_N) { + qreal yf = (qreal)y; + painter->drawLine(rect.left(), yf, rect.right(), yf); + } + + for (int y = GRID_SEP*GRID_N; y < rect.bottom(); y += GRID_SEP*GRID_N) { + qreal yf = (qreal)y; + painter->drawLine(rect.left(), yf, rect.right(), yf); + } + + painter->setPen(pen3); + painter->drawLine(rect.left(), 0, rect.right(), 0); + painter->drawLine(0, rect.top(), 0, rect.bottom()); +} + +void TikzView::wheelEvent(QWheelEvent *event) +{ + if (event->modifiers() & Qt::ShiftModifier) { + event->setModifiers(Qt::NoModifier); + QGraphicsView::wheelEvent(event); + } else if (event->modifiers() & Qt::ControlModifier) { + if (event->angleDelta().y() > 0) { + zoomIn(); + } else if (event->angleDelta().y() < 0) { + zoomOut(); + } + } +} + diff --git a/src/gui/tikzview.h b/src/gui/tikzview.h new file mode 100644 index 0000000..60c5841 --- /dev/null +++ b/src/gui/tikzview.h @@ -0,0 +1,53 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * Display a Graph, and manage any user input that purely changes the view (e.g. Zoom). This + * serves as the view in the MVC (TikzDocument, TikzView, TikzScene). + */ + +#ifndef TIKZVIEW_H +#define TIKZVIEW_H + +#include <QObject> +#include <QWidget> +#include <QGraphicsView> +#include <QPainter> +#include <QGraphicsItem> +#include <QStyleOptionGraphicsItem> +#include <QRectF> +#include <QMouseEvent> + +class TikzView : public QGraphicsView +{ + Q_OBJECT +public: + explicit TikzView(QWidget *parent = 0); + +public slots: + void zoomIn(); + void zoomOut(); + void setScene(QGraphicsScene *scene); +protected: + void drawBackground(QPainter *painter, const QRectF &rect) override; + void wheelEvent(QWheelEvent *event) override; +private: + float _scale; +}; + +#endif // TIKZVIEW_H diff --git a/src/gui/toolpalette.cpp b/src/gui/toolpalette.cpp new file mode 100644 index 0000000..d9ea05b --- /dev/null +++ b/src/gui/toolpalette.cpp @@ -0,0 +1,96 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "toolpalette.h" + +#include <QVector> +#include <QLayout> +#include <QVBoxLayout> +#include <QDebug> + +ToolPalette::ToolPalette(QWidget *parent) : + QToolBar(parent) +{ + setWindowFlags(Qt::Window + | Qt::CustomizeWindowHint + | Qt::WindowDoesNotAcceptFocus); + setOrientation(Qt::Vertical); + setFocusPolicy(Qt::NoFocus); + setWindowTitle("Tools"); + setObjectName("toolPalette"); + //setGeometry(100,200,30,195); + + tools = new QActionGroup(this); + + // select = new QAction(QIcon(":/images/Inkscape_icons_edit_select_all.svg"), "Select"); + // vertex = new QAction(QIcon(":/images/Inkscape_icons_draw_ellipse.svg"), "Add Vertex"); + // edge = new QAction(QIcon(":/images/Inkscape_icons_draw_path.svg"), "Add Edge"); + // crop = new QAction(QIcon(":/images/crop.svg"), "Bounding Box"); + + select = new QAction("Select (s)", this); + select->setIcon(QIcon(":/images/tikzit-tool-select.svg")); + vertex = new QAction("Add Vertex (v)", this); + vertex->setIcon(QIcon(":/images/tikzit-tool-node.svg")); + edge = new QAction("Add Edge (e)", this); + edge->setIcon(QIcon(":/images/tikzit-tool-edge.svg")); + + + tools->addAction(select); + tools->addAction(vertex); + tools->addAction(edge); + //tools->addAction(crop); + + select->setCheckable(true); + vertex->setCheckable(true); + edge->setCheckable(true); + //crop->setCheckable(true); + select->setChecked(true); + + addAction(select); + addAction(vertex); + addAction(edge); + //addAction(crop); +} + +ToolPalette::Tool ToolPalette::currentTool() const +{ + QAction *a = tools->checkedAction(); + if (a == vertex) return VERTEX; + else if (a == edge) return EDGE; + else if (a == crop) return CROP; + else return SELECT; +} + +void ToolPalette::setCurrentTool(ToolPalette::Tool tool) +{ + switch(tool) { + case SELECT: + select->setChecked(true); + break; + case VERTEX: + vertex->setChecked(true); + break; + case EDGE: + edge->setChecked(true); + break; + case CROP: + crop->setChecked(true); + break; + } +} + diff --git a/src/gui/toolpalette.h b/src/gui/toolpalette.h new file mode 100644 index 0000000..a001055 --- /dev/null +++ b/src/gui/toolpalette.h @@ -0,0 +1,53 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * A small window that lets the user select the current editing tool. + */ + +#ifndef TOOLPALETTE_H +#define TOOLPALETTE_H + +#include <QObject> +#include <QToolBar> +#include <QAction> +#include <QActionGroup> + +class ToolPalette : public QToolBar +{ + Q_OBJECT +public: + ToolPalette(QWidget *parent = 0); + enum Tool { + SELECT, + VERTEX, + EDGE, + CROP + }; + + Tool currentTool() const; + void setCurrentTool(Tool tool); +private: + QActionGroup *tools; + QAction *select; + QAction *vertex; + QAction *edge; + QAction *crop; +}; + +#endif // TOOLPALETTE_H diff --git a/src/gui/undocommands.cpp b/src/gui/undocommands.cpp new file mode 100644 index 0000000..f713582 --- /dev/null +++ b/src/gui/undocommands.cpp @@ -0,0 +1,543 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "undocommands.h" +#include "nodeitem.h" +#include "edgeitem.h" + +#include <QGraphicsView> + +GraphUpdateCommand::GraphUpdateCommand(TikzScene *scene, QUndoCommand *parent) : QUndoCommand(parent), _scene(scene) +{ +} + +void GraphUpdateCommand::undo() +{ + _scene->tikzDocument()->refreshTikz(); + //refreshSceneBounds does nothing + //_scene->refreshSceneBounds(); + _scene->invalidate(); +} + +void GraphUpdateCommand::redo() +{ + _scene->tikzDocument()->refreshTikz(); + //refreshSceneBounds does nothing + //_scene->refreshSceneBounds(); + _scene->invalidate(); +} + + +MoveCommand::MoveCommand(TikzScene *scene, + QMap<Node*, QPointF> oldNodePositions, + QMap<Node*, QPointF> newNodePositions, + QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), + _oldNodePositions(oldNodePositions), + _newNodePositions(newNodePositions) +{} + + +void MoveCommand::undo() +{ + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_oldNodePositions.contains(ni->node())) { + ni->node()->setPoint(_oldNodePositions.value(ni->node())); + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_oldNodePositions.keys()); + GraphUpdateCommand::undo(); +} + +void MoveCommand::redo() +{ + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_newNodePositions.contains(ni->node())) { + ni->node()->setPoint(_newNodePositions.value(ni->node())); + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_newNodePositions.keys()); + GraphUpdateCommand::redo(); +} + +EdgeBendCommand::EdgeBendCommand(TikzScene *scene, Edge *edge, + float oldWeight, int oldBend, + int oldInAngle, int oldOutAngle, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), + _edge(edge), + _oldWeight(oldWeight), _oldBend(oldBend), + _oldInAngle(oldInAngle), _oldOutAngle(oldOutAngle) +{ + _newWeight = edge->weight(); + _newBend = edge->bend(); + _newInAngle = edge->inAngle(); + _newOutAngle = edge->outAngle(); +} + +void EdgeBendCommand::undo() +{ + _edge->setWeight(_oldWeight); + _edge->setBend(_oldBend); + _edge->setInAngle(_oldInAngle); + _edge->setOutAngle(_oldOutAngle); + + foreach(EdgeItem *ei, _scene->edgeItems()) { + if (ei->edge() == _edge) { + ei->readPos(); + break; + } + } + GraphUpdateCommand::undo(); +} + +void EdgeBendCommand::redo() +{ + _edge->setWeight(_newWeight); + _edge->setBend(_newBend); + _edge->setInAngle(_newInAngle); + _edge->setOutAngle(_newOutAngle); + + foreach(EdgeItem *ei, _scene->edgeItems()) { + if (ei->edge() == _edge) { + ei->readPos(); + break; + } + } + + GraphUpdateCommand::redo(); +} + +DeleteCommand::DeleteCommand(TikzScene *scene, + QMap<int, Node *> deleteNodes, + QMap<int, Edge *> deleteEdges, + QSet<Edge *> selEdges, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), + _deleteNodes(deleteNodes), _deleteEdges(deleteEdges), _selEdges(selEdges) +{} + +void DeleteCommand::undo() +{ + for (auto it = _deleteNodes.begin(); it != _deleteNodes.end(); ++it) { + Node *n = it.value(); + n->attachStyle(); // in case styles have changed + _scene->graph()->addNode(n, it.key()); + NodeItem *ni = new NodeItem(n); + _scene->nodeItems().insert(n, ni); + _scene->addItem(ni); + ni->setSelected(true); + } + + for (auto it = _deleteEdges.begin(); it != _deleteEdges.end(); ++it) { + Edge *e = it.value(); + e->attachStyle(); + _scene->graph()->addEdge(e, it.key()); + EdgeItem *ei = new EdgeItem(e); + _scene->edgeItems().insert(e, ei); + _scene->addItem(ei); + + if (_selEdges.contains(e)) ei->setSelected(true); + } + + _scene->refreshZIndices(); + GraphUpdateCommand::undo(); +} + +void DeleteCommand::redo() +{ + foreach (Edge *e, _deleteEdges.values()) { + EdgeItem *ei = _scene->edgeItems()[e]; + _scene->edgeItems().remove(e); + _scene->removeItem(ei); + delete ei; + + _scene->graph()->removeEdge(e); + } + + foreach (Node *n, _deleteNodes.values()) { + NodeItem *ni = _scene->nodeItems()[n]; + _scene->nodeItems().remove(n); + _scene->removeItem(ni); + delete ni; + + _scene->graph()->removeNode(n); + } + + _scene->refreshZIndices(); + GraphUpdateCommand::redo(); +} + +AddNodeCommand::AddNodeCommand(TikzScene *scene, Node *node, QRectF newBounds, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _node(node), _oldBounds(_scene->sceneRect()), _newBounds(newBounds) +{ +} + +void AddNodeCommand::undo() +{ + NodeItem *ni = _scene->nodeItems()[_node]; + _scene->removeItem(ni); + _scene->nodeItems().remove(_node); + delete ni; + + _scene->graph()->removeNode(_node); + + //_scene->setBounds(_oldBounds); + + _scene->refreshZIndices(); + GraphUpdateCommand::undo(); +} + +void AddNodeCommand::redo() +{ + _node->attachStyle(); // do for every redo, in case styles have changed + _scene->graph()->addNode(_node); + NodeItem *ni = new NodeItem(_node); + _scene->nodeItems().insert(_node, ni); + _scene->addItem(ni); + + //_scene->setBounds(_newBounds); + + _scene->refreshZIndices(); + GraphUpdateCommand::redo(); +} + +AddEdgeCommand::AddEdgeCommand(TikzScene *scene, Edge *edge, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _edge(edge) +{ +} + +void AddEdgeCommand::undo() +{ + EdgeItem *ei = _scene->edgeItems()[_edge]; + _scene->removeItem(ei); + _scene->edgeItems().remove(_edge); + delete ei; + + _scene->graph()->removeEdge(_edge); + _scene->refreshZIndices(); + GraphUpdateCommand::undo(); +} + +void AddEdgeCommand::redo() +{ + _edge->attachStyle(); // do for every redo, in case styles have changed + _scene->graph()->addEdge(_edge); + EdgeItem *ei = new EdgeItem(_edge); + _scene->edgeItems().insert(_edge, ei); + _scene->addItem(ei); + + // TODO: deal consistently with stacking order + // edges should always be stacked below nodes + if (!_scene->graph()->nodes().isEmpty()) { + ei->stackBefore(_scene->nodeItems()[_scene->graph()->nodes().first()]); + } + + _scene->refreshZIndices(); + GraphUpdateCommand::redo(); +} + +ChangeEdgeModeCommand::ChangeEdgeModeCommand(TikzScene *scene, Edge *edge, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _edge(edge) +{ +} + +void ChangeEdgeModeCommand::undo() +{ + // FIXME: this act strangely sometimes + _edge->setBasicBendMode(!_edge->basicBendMode()); + _scene->edgeItems()[_edge]->readPos(); + GraphUpdateCommand::undo(); +} + +void ChangeEdgeModeCommand::redo() +{ + _edge->setBasicBendMode(!_edge->basicBendMode()); + _scene->edgeItems()[_edge]->readPos(); + GraphUpdateCommand::redo(); +} + +ApplyStyleToNodesCommand::ApplyStyleToNodesCommand(TikzScene *scene, QString style, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _style(style), _oldStyles() +{ + foreach (QGraphicsItem *it, scene->selectedItems()) { + if (NodeItem *ni = dynamic_cast<NodeItem*>(it)) { + _oldStyles.insert(ni->node(), ni->node()->styleName()); + } + } +} + +void ApplyStyleToNodesCommand::undo() +{ + foreach (Node *n, _oldStyles.keys()) { + n->setStyleName(_oldStyles[n]); + n->attachStyle(); + } + _scene->refreshAdjacentEdges(_oldStyles.keys()); + + GraphUpdateCommand::undo(); +} + +void ApplyStyleToNodesCommand::redo() +{ + foreach (Node *n, _oldStyles.keys()) { + n->setStyleName(_style); + n->attachStyle(); + } + _scene->refreshAdjacentEdges(_oldStyles.keys()); + + GraphUpdateCommand::redo(); +} + + +ApplyStyleToEdgesCommand::ApplyStyleToEdgesCommand(TikzScene * scene, QString style, QUndoCommand * parent) : + GraphUpdateCommand(scene, parent), _style(style), _oldStyles() +{ + foreach(QGraphicsItem *it, scene->selectedItems()) { + if (EdgeItem *ei = dynamic_cast<EdgeItem*>(it)) { + _oldStyles.insert(ei->edge(), ei->edge()->styleName()); + } + } +} + +void ApplyStyleToEdgesCommand::undo() +{ + foreach(Edge *e, _oldStyles.keys()) { + e->setStyleName(_oldStyles[e]); + e->attachStyle(); + } + + GraphUpdateCommand::undo(); +} + +void ApplyStyleToEdgesCommand::redo() +{ + foreach(Edge *e, _oldStyles.keys()) { + e->setStyleName(_style); + e->attachStyle(); + } + GraphUpdateCommand::redo(); +} + +PasteCommand::PasteCommand(TikzScene *scene, Graph *graph, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _graph(graph) +{ + scene->getSelection(_oldSelectedNodes, _oldSelectedEdges); +} + +void PasteCommand::undo() +{ + _scene->clearSelection(); + + foreach (Edge *e, _graph->edges()) { + EdgeItem *ei = _scene->edgeItems()[e]; + _scene->edgeItems().remove(e); + _scene->removeItem(ei); + delete ei; + + _scene->graph()->removeEdge(e); + } + + foreach (Node *n, _graph->nodes()) { + NodeItem *ni = _scene->nodeItems()[n]; + _scene->nodeItems().remove(n); + _scene->removeItem(ni); + delete ni; + + _scene->graph()->removeNode(n); + } + + foreach(Node *n, _oldSelectedNodes) _scene->nodeItems()[n]->setSelected(true); + foreach(Edge *e, _oldSelectedEdges) _scene->edgeItems()[e]->setSelected(true); + + _scene->refreshZIndices(); + GraphUpdateCommand::undo(); +} + +void PasteCommand::redo() +{ + _scene->clearSelection(); + _scene->graph()->insertGraph(_graph); + + foreach (Edge *e, _graph->edges()) { + e->attachStyle(); // in case styles have changed + EdgeItem *ei = new EdgeItem(e); + _scene->edgeItems().insert(e, ei); + _scene->addItem(ei); + } + + foreach (Node *n, _graph->nodes()) { + n->attachStyle(); // in case styles have changed + NodeItem *ni = new NodeItem(n); + _scene->nodeItems().insert(n, ni); + _scene->addItem(ni); + ni->setSelected(true); + } + + _scene->refreshZIndices(); + GraphUpdateCommand::redo(); +} + +ChangeLabelCommand::ChangeLabelCommand(TikzScene *scene, QMap<Node *, QString> oldLabels, QString newLabel, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _oldLabels(oldLabels), _newLabel(newLabel) +{ +} + +void ChangeLabelCommand::undo() +{ + foreach (Node *n, _oldLabels.keys()) { + n->setLabel(_oldLabels[n]); + NodeItem *ni = _scene->nodeItems()[n]; + if (ni != 0) ni->updateBounds(); + } + + GraphUpdateCommand::undo(); +} + +void ChangeLabelCommand::redo() +{ + foreach (Node *n, _oldLabels.keys()) { + n->setLabel(_newLabel); + NodeItem *ni = _scene->nodeItems()[n]; + if (ni != 0) ni->updateBounds(); + } + + GraphUpdateCommand::redo(); +} + +ReplaceGraphCommand::ReplaceGraphCommand(TikzScene *scene, Graph *oldGraph, Graph *newGraph, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _oldGraph(oldGraph), _newGraph(newGraph) +{ +} + +void ReplaceGraphCommand::undo() +{ + foreach (Node *n, _oldGraph->nodes()) n->attachStyle(); + foreach (Edge *e, _oldGraph->edges()) { + e->attachStyle(); + e->updateControls(); + } + _scene->tikzDocument()->setGraph(_oldGraph); + _scene->graphReplaced(); + GraphUpdateCommand::undo(); +} + +void ReplaceGraphCommand::redo() +{ + foreach (Node *n, _newGraph->nodes()) n->attachStyle(); + foreach (Edge *e, _newGraph->edges()) { + e->attachStyle(); + e->updateControls(); + } + _scene->tikzDocument()->setGraph(_newGraph); + _scene->graphReplaced(); + GraphUpdateCommand::redo(); +} + +ReflectNodesCommand::ReflectNodesCommand(TikzScene *scene, QSet<Node*> nodes, bool horizontal, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _nodes(nodes), _horizontal(horizontal) +{ +} + +void ReflectNodesCommand::undo() +{ + _scene->graph()->reflectNodes(_nodes, _horizontal); + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_nodes.contains(ni->node())) { + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_nodes.toList()); + GraphUpdateCommand::undo(); +} + +void ReflectNodesCommand::redo() +{ + _scene->graph()->reflectNodes(_nodes, _horizontal); + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_nodes.contains(ni->node())) { + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_nodes.toList()); + GraphUpdateCommand::redo(); +} + + +RotateNodesCommand::RotateNodesCommand(TikzScene *scene, QSet<Node*> nodes, bool clockwise, QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), _nodes(nodes), _clockwise(clockwise) +{ +} + +void RotateNodesCommand::undo() +{ + _scene->graph()->rotateNodes(_nodes, !_clockwise); + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_nodes.contains(ni->node())) { + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_nodes.toList()); + GraphUpdateCommand::undo(); +} + +void RotateNodesCommand::redo() +{ + _scene->graph()->rotateNodes(_nodes, _clockwise); + foreach (NodeItem *ni, _scene->nodeItems()) { + if (_nodes.contains(ni->node())) { + ni->readPos(); + } + } + + _scene->refreshAdjacentEdges(_nodes.toList()); + GraphUpdateCommand::redo(); +} + +ReorderCommand::ReorderCommand(TikzScene *scene, + const QVector<Node *> &oldNodeOrder, + const QVector<Node *> &newNodeOrder, + const QVector<Edge *> &oldEdgeOrder, + const QVector<Edge *> &newEdgeOrder, + QUndoCommand *parent) : + GraphUpdateCommand(scene, parent), + _oldNodeOrder(oldNodeOrder), _newNodeOrder(newNodeOrder), + _oldEdgeOrder(oldEdgeOrder), _newEdgeOrder(newEdgeOrder) +{ +} + +void ReorderCommand::undo() +{ + _scene->graph()->reorderNodes(_oldNodeOrder); + _scene->graph()->reorderEdges(_oldEdgeOrder); + _scene->refreshZIndices(); + GraphUpdateCommand::undo(); +} + +void ReorderCommand::redo() +{ + _scene->graph()->reorderNodes(_newNodeOrder); + _scene->graph()->reorderEdges(_newEdgeOrder); + _scene->refreshZIndices(); + GraphUpdateCommand::redo(); +} diff --git a/src/gui/undocommands.h b/src/gui/undocommands.h new file mode 100644 index 0000000..dc60549 --- /dev/null +++ b/src/gui/undocommands.h @@ -0,0 +1,238 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * \file undocommands.h + * + * All changes to a TikzDocument are done via subclasses of QUndoCommand. When a controller + * (e.g. TikzScene) gets input from the user to change the document, it will push one of + * these commands onto the TikzDocument's undo stack, which automatically calls the redo() + * method of the command. + */ + +#ifndef UNDOCOMMANDS_H +#define UNDOCOMMANDS_H + +#include "tikzscene.h" + +#include <QUndoCommand> +#include <QSet> + +class GraphUpdateCommand : public QUndoCommand { +public: + explicit GraphUpdateCommand(TikzScene *scene, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +protected: + TikzScene *_scene; +}; + +class MoveCommand : public GraphUpdateCommand +{ +public: + explicit MoveCommand(TikzScene *scene, + QMap<Node*,QPointF> oldNodePositions, + QMap<Node*,QPointF> newNodePositions, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QMap<Node*,QPointF> _oldNodePositions; + QMap<Node*,QPointF> _newNodePositions; +}; + +class EdgeBendCommand : public GraphUpdateCommand +{ +public: + explicit EdgeBendCommand(TikzScene *scene, Edge *edge, + float oldWeight, int oldBend, + int oldInAngle, int oldOutAngle, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Edge *_edge; + float _oldWeight; + int _oldBend; + int _oldInAngle; + int _oldOutAngle; + float _newWeight; + int _newBend; + int _newInAngle; + int _newOutAngle; +}; + +class DeleteCommand : public GraphUpdateCommand +{ +public: + explicit DeleteCommand(TikzScene *scene, + QMap<int,Node*> deleteNodes, + QMap<int,Edge*> deleteEdges, + QSet<Edge*> selEdges, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QMap<int,Node*> _deleteNodes; + QMap<int,Edge*> _deleteEdges; + QSet<Edge*> _selEdges; +}; + +class AddNodeCommand : public GraphUpdateCommand +{ +public: + explicit AddNodeCommand(TikzScene *scene, Node *node, QRectF newBounds, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Node *_node; + QRectF _oldBounds; + QRectF _newBounds; +}; + +class AddEdgeCommand : public GraphUpdateCommand +{ +public: + explicit AddEdgeCommand(TikzScene *scene, Edge *edge, QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Edge *_edge; +}; + +class ChangeEdgeModeCommand : public GraphUpdateCommand +{ +public: + explicit ChangeEdgeModeCommand(TikzScene *scene, Edge *edge, QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Edge *_edge; +}; + +class ApplyStyleToNodesCommand : public GraphUpdateCommand +{ +public: + explicit ApplyStyleToNodesCommand(TikzScene *scene, QString style, QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QString _style; + QMap<Node*,QString> _oldStyles; +}; + +class ApplyStyleToEdgesCommand : public GraphUpdateCommand +{ +public: + explicit ApplyStyleToEdgesCommand(TikzScene *scene, QString style, QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QString _style; + QMap<Edge*, QString> _oldStyles; +}; + +class PasteCommand : public GraphUpdateCommand +{ +public: + explicit PasteCommand(TikzScene *scene, Graph *graph, QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Graph *_graph; + QSet<Node*> _oldSelectedNodes; + QSet<Edge*> _oldSelectedEdges; +}; + +class ChangeLabelCommand : public GraphUpdateCommand +{ +public: + explicit ChangeLabelCommand(TikzScene *scene, + QMap<Node*,QString> oldLabels, + QString newLabel, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QMap<Node*,QString> _oldLabels; + QString _newLabel; +}; + +class ReplaceGraphCommand : public GraphUpdateCommand +{ +public: + explicit ReplaceGraphCommand(TikzScene *scene, + Graph *oldGraph, + Graph *newGraph, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + Graph *_oldGraph; + Graph *_newGraph; +}; + +class ReflectNodesCommand : public GraphUpdateCommand +{ +public: + explicit ReflectNodesCommand(TikzScene *scene, + QSet<Node*> nodes, + bool horizontal, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QSet<Node*> _nodes; + bool _horizontal; +}; + +class RotateNodesCommand : public GraphUpdateCommand +{ +public: + explicit RotateNodesCommand(TikzScene *scene, + QSet<Node*> nodes, + bool clockwise, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QSet<Node*> _nodes; + bool _clockwise; +}; + +class ReorderCommand : public GraphUpdateCommand +{ +public: + explicit ReorderCommand(TikzScene *scene, + const QVector<Node*> &oldNodeOrder, + const QVector<Node*> &newNodeOrder, + const QVector<Edge*> &oldEdgeOrder, + const QVector<Edge*> &newEdgeOrder, + QUndoCommand *parent = 0); + void undo() override; + void redo() override; +private: + QVector<Node*> _oldNodeOrder; + QVector<Node*> _newNodeOrder; + QVector<Edge*> _oldEdgeOrder; + QVector<Edge*> _newEdgeOrder; +}; + +#endif // UNDOCOMMANDS_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..99d23e9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,74 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + + +/*! + * \file main.cpp + * + * The main entry point for the TikZiT executable. + */ + +#include "tikzit.h" + +#include <QApplication> +#include <QMenuBar> +#include <QDesktopWidget> +#include <QDebug> +#include <QScreen> + +// #ifdef Q_OS_WIN +// #include <Windows.h> +// #endif + +int main(int argc, char *argv[]) +{ + // #ifdef Q_OS_WIN + // SetProcessDPIAware(); + // #endif +// QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + + // dummy application for detecting DPI + QApplication *a0 = new QApplication(argc, argv); +// qDebug() << "physical DPI" << QApplication::screens()[0]->physicalDotsPerInch(); + + if (QApplication::screens()[0]->physicalDotsPerInch() >= 100) { + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + } else { + QApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + } + + delete a0; + + QApplication a(argc, argv); + a.setQuitOnLastWindowClosed(false); + + + + tikzit = new Tikzit(); + tikzit->init(); + + qDebug() << a.arguments().length(); + + + if (a.arguments().length() > 1) { + tikzit->open(a.arguments()[1]); + } + + return a.exec(); +} diff --git a/src/test/testmain.cpp b/src/test/testmain.cpp new file mode 100644 index 0000000..30e5f6d --- /dev/null +++ b/src/test/testmain.cpp @@ -0,0 +1,22 @@ +#include "testtest.h"
+#include "testparser.h"
+#include "testtikzoutput.h"
+
+#include <QTest>
+#include <QDebug>
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+ TestTest test;
+ TestParser parser;
+ TestTikzOutput tikzOutput;
+ int r = QTest::qExec(&test, argc, argv) |
+ QTest::qExec(&parser, argc, argv) |
+ QTest::qExec(&tikzOutput, argc, argv);
+
+ if (r == 0) std::cout << "***************** All tests passed! *****************\n";
+ else std::cout << "***************** Some tests failed. *****************\n";
+
+ return r;
+}
diff --git a/src/test/testparser.cpp b/src/test/testparser.cpp new file mode 100644 index 0000000..85afe95 --- /dev/null +++ b/src/test/testparser.cpp @@ -0,0 +1,163 @@ +#include "testparser.h"
+#include "graph.h"
+#include "tikzassembler.h"
+
+#include <QTest>
+#include <QVector>
+
+//void TestParser::initTestCase()
+//{
+
+//}
+
+//void TestParser::cleanupTestCase()
+//{
+
+//}
+
+void TestParser::parseEmptyGraph()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse("\\begin{tikzpicture}\n\\end{tikzpicture}");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 0);
+ QVERIFY(g->edges().size() == 0);
+ delete g;
+}
+
+void TestParser::parseNodeGraph()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse(
+ "\\begin{tikzpicture}\n"
+ " \\node (node0) at (1.1, -2.2) {};\n"
+ " \\node (node1) at (3, 4) {test};\n"
+ "\\end{tikzpicture}");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 2);
+ QVERIFY(g->edges().size() == 0);
+ QVERIFY(g->nodes()[0]->name() == "node0");
+ QVERIFY(g->nodes()[0]->label() == "");
+ QVERIFY(g->nodes()[0]->point() == QPointF(1.1,-2.2));
+ QVERIFY(g->nodes()[1]->name() == "node1");
+ QVERIFY(g->nodes()[1]->label() == "test");
+ QVERIFY(g->nodes()[1]->point() == QPointF(3,4));
+ delete g;
+}
+
+void TestParser::parseEdgeGraph()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse(
+ "\\begin{tikzpicture}\n"
+ " \\begin{pgfonlayer}{nodelayer}\n"
+ " \\node [style=x, {foo++}] (0) at (-1, -1) {};\n"
+ " \\node [style=y] (1) at (0, 1) {};\n"
+ " \\node [style=z] (2) at (1, -1) {};\n"
+ " \\end{pgfonlayer}\n"
+ " \\begin{pgfonlayer}{edgelayer}\n"
+ " \\draw [style=a] (1.center) to (2);\n"
+ " \\draw [style=b, foo] (2) to (0.west);\n"
+ " \\draw [style=c] (0) to (1);\n"
+ " \\end{pgfonlayer}\n"
+ "\\end{tikzpicture}\n");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 3);
+ QVERIFY(g->edges().size() == 3);
+ QVERIFY(g->nodes()[0]->data()->atom("foo++"));
+ QVERIFY(g->edges()[0]->data()->property("style") == "a");
+ QVERIFY(!g->edges()[0]->data()->atom("foo"));
+ QVERIFY(g->edges()[1]->data()->property("style") == "b");
+ QVERIFY(g->edges()[1]->data()->atom("foo"));
+ QVERIFY(g->edges()[2]->data()->property("style") == "c");
+ Node *en = g->edges()[0]->edgeNode();
+ QVERIFY(en == 0);
+ delete g;
+}
+
+void TestParser::parseEdgeNode()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse(
+ "\\begin{tikzpicture}\n"
+ " \\begin{pgfonlayer}{nodelayer}\n"
+ " \\node [style=none] (0) at (-1, 0) {};\n"
+ " \\node [style=none] (1) at (1, 0) {};\n"
+ " \\end{pgfonlayer}\n"
+ " \\begin{pgfonlayer}{edgelayer}\n"
+ " \\draw [style=diredge] (0.center) to node[foo, bar=baz baz]{test} (1.center);\n"
+ " \\end{pgfonlayer}\n"
+ "\\end{tikzpicture}\n");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 2);
+ QVERIFY(g->edges().size() == 1);
+ Node *en = g->edges()[0]->edgeNode();
+ QVERIFY(en != 0);
+ QVERIFY(en->label() == "test");
+ QVERIFY(en->data()->atom("foo"));
+ QVERIFY(en->data()->property("bar") == "baz baz");
+ delete g;
+}
+
+void TestParser::parseEdgeBends()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse(
+ "\\begin{tikzpicture}\n"
+ " \\begin{pgfonlayer}{nodelayer}\n"
+ " \\node [style=white] (0) at (-1, 0) {};\n"
+ " \\node [style=black] (1) at (1, 0) {};\n"
+ " \\end{pgfonlayer}\n"
+ " \\begin{pgfonlayer}{edgelayer}\n"
+ " \\draw [style=diredge,bend left] (0) to (1);\n"
+ " \\draw [style=diredge,bend right] (0) to (1);\n"
+ " \\draw [style=diredge,bend left=20] (0) to (1);\n"
+ " \\draw [style=diredge,bend right=80] (0) to (1);\n"
+ " \\draw [style=diredge,in=10,out=150,looseness=2] (0) to (1);\n"
+ " \\end{pgfonlayer}\n"
+ "\\end{tikzpicture}\n");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 2);
+ QVERIFY(g->edges().size() == 5);
+ QVERIFY(g->edges()[0]->bend() == -30);
+ QVERIFY(g->edges()[1]->bend() == 30);
+ QVERIFY(g->edges()[2]->bend() == -20);
+ QVERIFY(g->edges()[3]->bend() == 80);
+ QVERIFY(g->edges()[4]->inAngle() == 10);
+ QVERIFY(g->edges()[4]->outAngle() == 150);
+ QVERIFY(g->edges()[4]->weight() == 2.0f/2.5f);
+}
+
+void TestParser::parseBbox()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+ bool res = ga.parse(
+ "\\begin{tikzpicture}\n"
+ " \\path [use as bounding box] (-1.5,-1.5) rectangle (1.5,1.5);\n"
+ " \\begin{pgfonlayer}{nodelayer}\n"
+ " \\node [style=white dot] (0) at (-1, -1) {};\n"
+ " \\node [style=white dot] (1) at (0, 1) {};\n"
+ " \\node [style=white dot] (2) at (1, -1) {};\n"
+ " \\end{pgfonlayer}\n"
+ " \\begin{pgfonlayer}{edgelayer}\n"
+ " \\draw [style=diredge] (1) to (2);\n"
+ " \\draw [style=diredge] (2) to (0);\n"
+ " \\draw [style=diredge] (0) to (1);\n"
+ " \\end{pgfonlayer}\n"
+ "\\end{tikzpicture}\n");
+ QVERIFY(res);
+ QVERIFY(g->nodes().size() == 3);
+ QVERIFY(g->edges().size() == 3);
+ QVERIFY(g->hasBbox());
+ QVERIFY(g->bbox() == QRectF(QPointF(-1.5,-1.5), QPointF(1.5,1.5)));
+
+ delete g;
+}
+
+
diff --git a/src/test/testparser.h b/src/test/testparser.h new file mode 100644 index 0000000..a59647d --- /dev/null +++ b/src/test/testparser.h @@ -0,0 +1,18 @@ +#ifndef TESTPARSER_H
+#define TESTPARSER_H
+
+#include <QObject>
+
+class TestParser : public QObject
+{
+ Q_OBJECT
+private slots:
+ void parseEmptyGraph();
+ void parseNodeGraph();
+ void parseEdgeGraph();
+ void parseEdgeNode();
+ void parseEdgeBends();
+ void parseBbox();
+};
+
+#endif // TESTPARSER_H
diff --git a/src/test/testtest.cpp b/src/test/testtest.cpp new file mode 100644 index 0000000..7f8d8cb --- /dev/null +++ b/src/test/testtest.cpp @@ -0,0 +1,10 @@ +#include "testtest.h"
+
+#include <QObject>
+#include <QTest>
+
+void TestTest::initTestCase() { qDebug("initialising test"); }
+void TestTest::myFirstTest() { QVERIFY(1 == 1); }
+void TestTest::mySecondTest() { QVERIFY(1 != 2); }
+void TestTest::cleanupTestCase() { qDebug("cleaning up test"); }
+
diff --git a/src/test/testtest.h b/src/test/testtest.h new file mode 100644 index 0000000..a94dd41 --- /dev/null +++ b/src/test/testtest.h @@ -0,0 +1,17 @@ +#ifndef TESTTEST_H
+#define TESTTEST_H
+
+#include <QObject>
+#include <QTest>
+
+class TestTest: public QObject
+{
+ Q_OBJECT
+private slots:
+ void initTestCase();
+ void myFirstTest();
+ void mySecondTest();
+ void cleanupTestCase();
+};
+
+#endif // TESTTEST_H
diff --git a/src/test/testtikzoutput.cpp b/src/test/testtikzoutput.cpp new file mode 100644 index 0000000..d7ec32a --- /dev/null +++ b/src/test/testtikzoutput.cpp @@ -0,0 +1,97 @@ +#include "testtikzoutput.h"
+#include "graphelementproperty.h"
+#include "graphelementdata.h"
+#include "graph.h"
+#include "tikzassembler.h"
+
+#include <QTest>
+#include <QRectF>
+#include <QPointF>
+
+void TestTikzOutput::escape()
+{
+ QVERIFY(GraphElementProperty::tikzEscape("foo") == "foo");
+ QVERIFY(GraphElementProperty::tikzEscape("foo'") == "foo'");
+ QVERIFY(GraphElementProperty::tikzEscape("foo bar") == "foo bar");
+ QVERIFY(GraphElementProperty::tikzEscape("foo.bar") == "foo.bar");
+ QVERIFY(GraphElementProperty::tikzEscape("foo-bar") == "foo-bar");
+ QVERIFY(GraphElementProperty::tikzEscape("foo >") == "foo >");
+ QVERIFY(GraphElementProperty::tikzEscape("foo <") == "foo <");
+ QVERIFY(GraphElementProperty::tikzEscape("foo+") == "{foo+}");
+ QVERIFY(GraphElementProperty::tikzEscape("foo{bar}") == "{foo{bar}}");
+}
+
+void TestTikzOutput::data()
+{
+ GraphElementData d;
+ QVERIFY(d.tikz() == "");
+ d.setAtom("foo");
+ QVERIFY(d.tikz() == "[foo]");
+ d.setAtom("bar");
+ QVERIFY(d.tikz() == "[foo, bar]");
+ d.setProperty("foo","bar");
+ QVERIFY(d.tikz() == "[foo, bar, foo=bar]");
+ d.setAtom("foo+");
+ QVERIFY(d.tikz() == "[foo, bar, foo=bar, {foo+}]");
+ d.unsetAtom("foo");
+ QVERIFY(d.tikz() == "[bar, foo=bar, {foo+}]");
+ d.unsetProperty("foo");
+ QVERIFY(d.tikz() == "[bar, {foo+}]");
+ d.unsetAtom("foo+");
+ QVERIFY(d.tikz() == "[bar]");
+ d.unsetAtom("bar");
+ QVERIFY(d.tikz() == "");
+}
+
+void TestTikzOutput::graphEmpty()
+{
+ Graph *g = new Graph();
+
+ QString tikz =
+ "\\begin{tikzpicture}\n"
+ "\\end{tikzpicture}\n";
+ QVERIFY(g->tikz() == tikz);
+
+ delete g;
+}
+
+void TestTikzOutput::graphFromTikz()
+{
+ Graph *g = new Graph();
+ TikzAssembler ga(g);
+
+ QString tikz =
+ "\\begin{tikzpicture}\n"
+ "\t\\path [use as bounding box] (-1.5,-1.5) rectangle (1.5,1.5);\n"
+ "\t\\begin{pgfonlayer}{nodelayer}\n"
+ "\t\t\\node [style=white dot] (0) at (-1, -1) {};\n"
+ "\t\t\\node [style=white dot] (1) at (0, 1) {};\n"
+ "\t\t\\node [style=white dot] (2) at (1, -1) {};\n"
+ "\t\\end{pgfonlayer}\n"
+ "\t\\begin{pgfonlayer}{edgelayer}\n"
+ "\t\t\\draw [style=diredge] (1) to (2);\n"
+ "\t\t\\draw [style=diredge] (2.center) to (0);\n"
+ "\t\t\\draw [style=diredge] (0) to ();\n"
+ "\t\\end{pgfonlayer}\n"
+ "\\end{tikzpicture}\n";
+ bool res = ga.parse(tikz);
+ QVERIFY2(res, "parsed successfully");
+ QVERIFY2(g->tikz() == tikz, "produced matching tikz");
+
+ delete g;
+}
+
+void TestTikzOutput::graphBbox()
+{
+ Graph *g = new Graph();
+ g->setBbox(QRectF(QPointF(-0.75, -0.5), QPointF(0.25, 1)));
+
+ QString tikz =
+ "\\begin{tikzpicture}\n"
+ "\t\\path [use as bounding box] (-0.75,-0.5) rectangle (0.25,1);\n"
+ "\\end{tikzpicture}\n";
+ QVERIFY(g->tikz() == tikz);
+
+
+ delete g;
+}
diff --git a/src/test/testtikzoutput.h b/src/test/testtikzoutput.h new file mode 100644 index 0000000..f4949f5 --- /dev/null +++ b/src/test/testtikzoutput.h @@ -0,0 +1,17 @@ +#ifndef TESTTIKZOUTPUT_H
+#define TESTTIKZOUTPUT_H
+
+#include <QObject>
+
+class TestTikzOutput : public QObject
+{
+ Q_OBJECT
+private slots:
+ void escape();
+ void data();
+ void graphBbox();
+ void graphEmpty();
+ void graphFromTikz();
+};
+
+#endif // TESTTIKZOUTPUT_H
diff --git a/src/tikzit.cpp b/src/tikzit.cpp new file mode 100644 index 0000000..c9286c9 --- /dev/null +++ b/src/tikzit.cpp @@ -0,0 +1,431 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "tikzit.h" +#include "tikzassembler.h" +#include "tikzstyles.h" + +#include <QFile> +#include <QFileDialog> +#include <QSettings> +#include <QDebug> +#include <QMessageBox> +#include <QRegularExpression> +#include <QVersionNumber> +#include <QNetworkAccessManager> + +// application-level instance of Tikzit +Tikzit *tikzit; + +// font to use for node labels +QFont Tikzit::LABEL_FONT("Courrier", 9); + +Tikzit::Tikzit() : _styleFile("[no styles]"), _activeWindow(0) +{ +} + +void Tikzit::init() +{ + QSettings settings("tikzit", "tikzit"); + + // 19 standard xcolor colours + _colNames << + "black" << + "darkgray" << + "gray" << + "lightgray" << + "white" << + + "red" << + "orange" << + "yellow" << + "green" << + "blue" << + "purple" << + + "brown" << + "olive" << + "lime" << + "cyan" << + "teal" << + + "magenta" << + "violet" << + "pink"; + + _cols << + QColor::fromRgbF(0,0,0) << + QColor::fromRgbF(0.25,0.25,0.25) << + QColor::fromRgbF(0.5,0.5,0.5) << + QColor::fromRgbF(0.75,0.75,0.75) << + QColor::fromRgbF(1,1,1) << + + QColor::fromRgbF(1,0,0) << + QColor::fromRgbF(1,0.5,0) << + QColor::fromRgbF(1,1,0) << + QColor::fromRgbF(0,1,0) << + QColor::fromRgbF(0,0,1) << + QColor::fromRgbF(0.75,0,0.25) << + + QColor::fromRgbF(0.75,0.5,0.25) << + QColor::fromRgbF(0.5,0.5,0) << + QColor::fromRgbF(0.75,1,0) << + QColor::fromRgbF(0,1,1) << + QColor::fromRgbF(0,0.5,0.5) << + + QColor::fromRgbF(1,0,1) << + QColor::fromRgbF(0.5,0,0.5) << + QColor::fromRgbF(1,0.75,0.75); + + _mainMenu = new MainMenu(); + QMainWindow *dummy = new QMainWindow(); + + _toolPalette = new ToolPalette(dummy); + _propertyPalette = new PropertyPalette(dummy); + //_stylePalette = new StylePalette(dummy); + _styles = new TikzStyles(this); + + _styleEditor = new StyleEditor(); + + //_stylePalette->show(); + _windows << new MainWindow(); + _windows[0]->show(); + + QString styleFile = settings.value("previous-tikzstyles-file").toString(); + if (!styleFile.isEmpty()) loadStyles(styleFile); + + QVariant check = settings.value("check-for-updates"); + if (check.isNull()) { + int resp = QMessageBox::question(0, + tr("Check for updates"), + tr("Would you like TikZiT to check for updates automatically?" + " (You can always change this later in the Help menu.)"), + QMessageBox::Yes | QMessageBox::Default, + QMessageBox::No, + QMessageBox::NoButton); + check.setValue(resp == QMessageBox::Yes); + } + + setCheckForUpdates(check.toBool()); + + if (check.toBool()) { + checkForUpdates(); + } +} + +//QMenuBar *Tikzit::mainMenu() const +//{ +// return _mainMenu; +//} + +QColor Tikzit::colorByIndex(int i) +{ + return _cols[i]; +} + +QColor Tikzit::colorByName(QString name) +{ + for (int i = 0; i < _colNames.length(); ++i) { + if (_colNames[i] == name) return _cols[i]; + } + + QRegExp re( + "rgb\\s*,\\s*255\\s*:\\s*" + "red\\s*,\\s*([0-9]+)\\s*;\\s*" + "green\\s*,\\s*([0-9]+)\\s*;\\s*" + "blue\\s*,\\s*([0-9]+)\\s*" + ); + + if (re.exactMatch(name)) { + QStringList cap = re.capturedTexts(); + //qDebug() << cap; + return QColor( + cap[1].toInt(), + cap[2].toInt(), + cap[3].toInt()); + } + + return QColor(); +} + +QString Tikzit::nameForColor(QColor col) +{ + for (int i = 0; i < _colNames.length(); ++i) { + if (_cols[i] == col) return _colNames[i]; + } + + // if the color is not recognised, return it in tikz-readable RBG format + return "rgb,255: red,"+ QString::number(col.red()) + + "; green," + QString::number(col.green()) + + "; blue," + QString::number(col.blue()); +} + +void Tikzit::newTikzStyles() +{ + QSettings settings("tikzit", "tikzit"); + QFileDialog dialog; + dialog.setDefaultSuffix("tikzstyles"); + dialog.setWindowTitle(tr("Create TikZ Style File")); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setLabelText(QFileDialog::Accept, "Create"); + dialog.setNameFilter(tr("TiKZ Style File (*.tikzstyles)")); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setDirectory(settings.value("previous-file-path").toString()); + dialog.setOption(QFileDialog::DontUseNativeDialog); + + if (dialog.exec() && !dialog.selectedFiles().isEmpty()) { + QString fileName = dialog.selectedFiles()[0]; + TikzStyles *st = new TikzStyles; + + if (st->saveStyles(fileName)) { + QFileInfo fi(fileName); + _styleFile = fi.fileName(); + _styleFilePath = fi.absoluteFilePath(); + settings.setValue("previous-tikzstyles-file", fileName); + settings.setValue("previous-tikzstyles-path", fi.absolutePath()); + delete _styles; + _styles = st; + + foreach (MainWindow *w, _windows) { + w->tikzScene()->reloadStyles(); + } + } else { + QMessageBox::warning(0, + "Could not write to style file.", + "Could not write to: '" + fileName + "'. Check file permissions or choose a new location."); + } + } +} + +ToolPalette *Tikzit::toolPalette() const +{ + return _toolPalette; +} + +PropertyPalette *Tikzit::propertyPalette() const +{ + return _propertyPalette; +} + +void Tikzit::newDoc() +{ + MainWindow *w = new MainWindow(); + w->show(); + _windows << w; +} + +MainWindow *Tikzit::activeWindow() const +{ + return _activeWindow; +} + +void Tikzit::setActiveWindow(MainWindow *activeWindow) +{ + _activeWindow = activeWindow; +} + +void Tikzit::removeWindow(MainWindow *w) +{ + _windows.removeAll(w); + if (_activeWindow == w) { + if (_windows.isEmpty()) { + _activeWindow = 0; + // TODO: check if we should quit when last window closed + quit(); + } else _activeWindow = _windows[0]; + } +} + +void Tikzit::open() +{ + QSettings settings("tikzit", "tikzit"); + QString fileName = QFileDialog::getOpenFileName(0, + tr("Open File"), + settings.value("previous-file-path").toString(), + tr("TiKZ Files (*.tikz)"), + nullptr, + QFileDialog::DontUseNativeDialog); + + open(fileName); +} + +void Tikzit::open(QString fileName) +{ + if (!fileName.isEmpty()) { + if (_windows.size() == 1 && + _windows[0]->tikzDocument()->isClean() && + _windows[0]->tikzDocument()->shortName().isEmpty()) + { + _windows[0]->open(fileName); + _windows[0]->show(); + } + else { + MainWindow *w = new MainWindow(); + w->show(); + w->open(fileName); + _windows << w; + } + } +} + +void Tikzit::openTikzStyles() { + QSettings settings("tikzit", "tikzit"); + QString fileName = QFileDialog::getOpenFileName(0, + tr("Open File"), + settings.value("previous-tikzstyles-path").toString(), + tr("TiKZ Style Files (*.tikzstyles)"), + nullptr, + QFileDialog::DontUseNativeDialog); + + if (!fileName.isEmpty()) { + QFileInfo fi(fileName); + if (fi.exists() && loadStyles(fileName)) { + QSettings settings("tikzit", "tikzit"); + settings.setValue("previous-tikzstyles-path", fi.absolutePath()); + settings.setValue("previous-tikzstyles-file", fileName); + } + } +} + +bool Tikzit::loadStyles(QString fileName) +{ + QFileInfo fi(fileName); + if (fi.exists()) { + TikzStyles *st = new TikzStyles(this); + if (st->loadStyles(fileName)) { + _styleFile = fi.fileName(); + _styleFilePath = fi.absoluteFilePath(); + delete _styles; + _styles = st; + + foreach (MainWindow *w, _windows) { + w->tikzScene()->reloadStyles(); + } + return true; + } else { + QMessageBox::warning(0, + "Bad style file.", + "Bad style file: '" + fileName + "'. Check the file is properly formatted and try to load it again."); + return false; + } + + } else { + //settings.setValue("previous-tikzstyles-file", ""); + QMessageBox::warning(0, "Style file not found.", "Could not open style file: '" + fileName + "'."); + return false; + } +} + +void Tikzit::showStyleEditor() +{ + _styleEditor->open(); +} + +QString Tikzit::styleFile() const +{ + return _styleFile; +} + +QString Tikzit::styleFilePath() const +{ + return _styleFilePath; +} + +void Tikzit::setCheckForUpdates(bool check) +{ + QSettings settings("tikzit", "tikzit"); + settings.setValue("check-for-updates", check); + foreach (MainWindow *w, _windows) { + w->menu()->updatesAction()->blockSignals(true); + w->menu()->updatesAction()->setChecked(check); + w->menu()->updatesAction()->blockSignals(false); + } +} + +void Tikzit::checkForUpdates() +{ + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + connect(manager, SIGNAL(finished(QNetworkReply*)), + this, SLOT(updateReply(QNetworkReply*))); + + manager->get(QNetworkRequest(QUrl("https://tikzit.github.io/latest-version.txt"))); +} + +void Tikzit::updateReply(QNetworkReply *reply) +{ + if (!reply->isReadable()) return; + + QByteArray data = reply->read(200); + QString strLatest = QString::fromUtf8(data).simplified(); + + // check for valid version string and capture optional RC suffix + QRegularExpression re("^[1-9]+(\\.[0-9]+)*(-[rR][cC]([0-9]+))?$"); + QRegularExpressionMatch m; + m = re.match(TIKZIT_VERSION); + + // any non-RC versions are considered later than RC versions. + int rcCurrent = (!m.captured(3).isEmpty()) ? m.captured(3).toInt() : 1000; + + m = re.match(strLatest); + + if (m.hasMatch()) { + QVersionNumber current = QVersionNumber::fromString(TIKZIT_VERSION).normalized(); + QVersionNumber latest = QVersionNumber::fromString(strLatest).normalized(); + + int rcLatest = (!m.captured(3).isEmpty()) ? m.captured(3).toInt() : 1000; + + if (latest > current || (latest == current && rcLatest > rcCurrent)) { + // give the version string in standard format + strLatest = QString::number(latest.majorVersion()) + "." + + QString::number(latest.minorVersion()) + "." + + QString::number(latest.microVersion()); + if (rcLatest != 1000) strLatest += "-rc" + QString::number(rcLatest); + QMessageBox::information(0, + tr("Update available"), + "<p><b>A new version of TikZiT is available!</b></p>" + "<p><i>current version: " TIKZIT_VERSION "<br />" + "latest version: " + strLatest + "</i></p>" + "<p>Download it now from: " + "<a href=\"https://tikzit.github.io\">tikzit.github.io</a>.</p>"); + } + } else { + QMessageBox::warning(0, + tr("Invalid response"), + "<p>Got invalid version response from " + "<a href=\"https://tikzit.github.io\">tikzit.github.io</a>.</p>"); + } +} + +//StylePalette *Tikzit::stylePalette() const +//{ +// return _stylePalette; +//} + + +TikzStyles *Tikzit::styles() const +{ + return _styles; +} + +void Tikzit::quit() +{ + //_stylePalette->close(); + QApplication::quit(); +} + + diff --git a/src/tikzit.h b/src/tikzit.h new file mode 100644 index 0000000..d36a940 --- /dev/null +++ b/src/tikzit.h @@ -0,0 +1,159 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + + +/*! + * + * \mainpage TikZiT Documentation + * + * This is the source code documentation for TikZiT. The global entry point + * for the TikZiT executable is in main.cpp, whereas the class Tikzit maintains + * the global application state. + * + * The TikZ parser is implemented in flex/bison in the files tikzlexer.l and tikzparser.y. + * + * Most of the interesting code for handling user input is in the class TikzScene. Anything + * that makes a change to the tikz file should be implemented as a QUndoCommand. Currently, + * these are all in undocommands.h. + * + * I've basically been adding documentation as I go. Other bits and pieces can be accessed + * by searching, or via the class list/class hierarchy links in the menu above. + * + */ + +/*! + * + * \class Tikzit + * + * Tikzit is the top-level class which maintains the global application state. For convenience, + * it also holds an instance of the main menu for macOS (or Ubuntu unity) style GUIs which only + * have one, application-level menu. + * + */ + +#ifndef TIKZIT_H +#define TIKZIT_H + +#define TIKZIT_VERSION "2.0.0" + +#include "mainwindow.h" +#include "mainmenu.h" +#include "ui_mainmenu.h" + +#include "styleeditor.h" +#include "toolpalette.h" +#include "propertypalette.h" +#include "stylepalette.h" +#include "tikzstyles.h" + +#include <QObject> +#include <QVector> +#include <QStringList> +#include <QPointF> +#include <QMenuBar> +#include <QMainWindow> +#include <QFont> +#include <QColor> +#include <QNetworkReply> + +// Number of pixels between (0,0) and (1,0) at 100% zoom level. This should be +// divisible by 8 to avoid rounding errors with e.g. grid-snapping. +#define GLOBAL_SCALE 40 +#define GLOBAL_SCALEF 40.0f +#define GLOBAL_SCALEF_INV 0.025f +#define GRID_N 4 +#define GRID_SEP 10 +#define GRID_SEPF 10.0f + +inline QPointF toScreen(QPointF src) +{ src.setY(-src.y()); src *= GLOBAL_SCALEF; return src; } + +inline QPointF fromScreen(QPointF src) +{ src.setY(-src.y()); src *= GLOBAL_SCALEF_INV; return src; } + +inline QRectF rectToScreen(QRectF src) +{ return QRectF(src.x() * GLOBAL_SCALEF, + -(src.y()+src.height()) * GLOBAL_SCALEF, + src.width() * GLOBAL_SCALEF, + src.height() * GLOBAL_SCALEF); } + +inline QRectF rectFromScreen(QRectF src) +{ return QRectF(src.x() * GLOBAL_SCALEF_INV, + -(src.y()+src.height()) * GLOBAL_SCALEF_INV, + src.width() * GLOBAL_SCALEF_INV, + src.height() * GLOBAL_SCALEF_INV); } + +class Tikzit : public QObject { + Q_OBJECT +public: + Tikzit(); + ToolPalette *toolPalette() const; + PropertyPalette *propertyPalette() const; + + MainWindow *activeWindow() const; + void setActiveWindow(MainWindow *activeWindow); + void removeWindow(MainWindow *w); + + static QFont LABEL_FONT; + + void newDoc(); + void open(); + void open(QString fileName); + void quit(); + void init(); + + // convenience functions for named colors + QColor colorByIndex(int i); + QColor colorByName(QString name); + QString nameForColor(QColor col); + + void newTikzStyles(); + void openTikzStyles(); + bool loadStyles(QString fileName); + void showStyleEditor(); + TikzStyles *styles() const; + QString styleFile() const; + //StylePalette *stylePalette() const; + + QString styleFilePath() const; + +public slots: + void setCheckForUpdates(bool check); + void checkForUpdates(); + void updateReply(QNetworkReply *reply); + +private: + // void createMenu(); + + MainMenu *_mainMenu; + ToolPalette *_toolPalette; + PropertyPalette *_propertyPalette; + //StylePalette *_stylePalette; + TikzStyles *_styles; + QString _styleFile; + QString _styleFilePath; + QVector<MainWindow*> _windows; + MainWindow *_activeWindow; + StyleEditor *_styleEditor; + QStringList _colNames; + QVector<QColor> _cols; +}; + +extern Tikzit *tikzit; + +#endif // TIKZIT_H diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..d5e2b96 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,73 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "util.h" + + +float bezierInterpolate(float dist, float c0, float c1, float c2, float c3) { + float distp = 1 - dist; + return (distp*distp*distp) * c0 + + 3 * (distp*distp) * dist * c1 + + 3 * (dist*dist) * distp * c2 + + (dist*dist*dist) * c3; +} + +QPointF bezierInterpolateFull (float dist, QPointF c0, QPointF c1, QPointF c2, QPointF c3) { + return QPointF(bezierInterpolate (dist, c0.x(), c1.x(), c2.x(), c3.x()), + bezierInterpolate (dist, c0.y(), c1.y(), c2.y(), c3.y())); +} + + +float roundToNearest(float stepSize, float val) { + if (stepSize==0.0f) return val; + else return round(val/stepSize)*stepSize; +} + +float radiansToDegrees (float radians) { + return (radians * 180.0f) / M_PI; +} + +float degreesToRadians(float degrees) { + return (degrees * M_PI) / 180.0f; +} + +int normaliseAngleDeg (int degrees) { + while (degrees > 180) { + degrees -= 360; + } + while (degrees <= -180) { + degrees += 360; + } + return degrees; +} + +float normaliseAngleRad (float rads) { + while (rads > M_PI) { + rads -= 2 * M_PI; + } + while (rads <= -M_PI) { + rads += 2 * M_PI; + } + return rads; +} + +// convert float to string, squashing very small floats to zero +QString floatToString(float f) { + if (f >= -0.000001 && f <= 0.000001) return "0"; + else return QString::number(f); +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..89d0c5b --- /dev/null +++ b/src/util.h @@ -0,0 +1,49 @@ +/* + TikZiT - a GUI diagram editor for TikZ + Copyright (C) 2018 Aleks Kissinger + + 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 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +/*! + * Various utility functions, mostly for mathematical calculation. + */ + +#ifndef UTIL_H +#define UTIL_H + +#include <QPoint> +#include <QString> +#include <QColor> +#include <cmath> + +#ifndef M_PI +#define M_PI 3.14159265358979323846264338327950288 +#endif + +// interpolate on a cubic bezier curve +float bezierInterpolate(float dist, float c0, float c1, float c2, float c3); +QPointF bezierInterpolateFull (float dist, QPointF c0, QPointF c1, QPointF c2, QPointF c3); + +// rounding +float roundToNearest(float stepSize, float val); +float radiansToDegrees (float radians); +QString floatToString(float f); + +// angles +float degreesToRadians(float degrees); +int normaliseAngleDeg (int degrees); +float normaliseAngleRad (float rads); + +#endif // UTIL_H |