1 // This file is part of Heimer.
2 // Copyright (C) 2018 Jussi Lind <jussi.lind@iki.fi>
3 //
4 // Heimer is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 // Heimer is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Heimer. If not, see <http://www.gnu.org/licenses/>.
15 
16 #include "edge.hpp"
17 
18 #include "constants.hpp"
19 #include "edge_dot.hpp"
20 #include "edge_text_edit.hpp"
21 #include "graphics_factory.hpp"
22 #include "layers.hpp"
23 #include "node.hpp"
24 #include "settings_proxy.hpp"
25 #include "test_mode.hpp"
26 
27 #include "simple_logger.hpp"
28 
29 #include <QBrush>
30 #include <QFont>
31 #include <QGraphicsEllipseItem>
32 #include <QPen>
33 #include <QPropertyAnimation>
34 #include <QTimer>
35 #include <QVector2D>
36 
37 #include <QtMath>
38 
39 #include <cassert>
40 #include <cmath>
41 
Edge(Node & sourceNode,Node & targetNode,bool enableAnimations,bool enableLabel)42 Edge::Edge(Node & sourceNode, Node & targetNode, bool enableAnimations, bool enableLabel)
43   : m_sourceNode(&sourceNode)
44   , m_targetNode(&targetNode)
45   , m_reversed(SettingsProxy::instance().reversedEdgeDirection())
46   , m_arrowMode(SettingsProxy::instance().edgeArrowMode())
47   , m_enableAnimations(enableAnimations)
48   , m_enableLabel(enableLabel)
49   , m_sourceDot(enableAnimations ? new EdgeDot(this) : nullptr)
50   , m_targetDot(enableAnimations ? new EdgeDot(this) : nullptr)
51   , m_label(enableLabel ? new EdgeTextEdit(this) : nullptr)
52   , m_arrowheadL0(new QGraphicsLineItem(this))
53   , m_arrowheadR0(new QGraphicsLineItem(this))
54   , m_arrowheadL1(new QGraphicsLineItem(this))
55   , m_arrowheadR1(new QGraphicsLineItem(this))
56   , m_sourceDotSizeAnimation(enableAnimations ? new QPropertyAnimation(m_sourceDot, "scale", this) : nullptr)
57   , m_targetDotSizeAnimation(enableAnimations ? new QPropertyAnimation(m_targetDot, "scale", this) : nullptr)
58 {
59     setAcceptHoverEvents(true && enableAnimations);
60 
61     setGraphicsEffect(GraphicsFactory::createDropShadowEffect());
62 
63     setZValue(static_cast<int>(Layers::Edge));
64 
65     initDots();
66 
67     if (m_enableLabel) {
68         m_label->setZValue(static_cast<int>(Layers::EdgeLabel));
69         m_label->setBackgroundColor(Constants::Edge::LABEL_COLOR);
70 
71         connect(m_label, &TextEdit::textChanged, [=](const QString & text) {
72             updateLabel();
73             m_text = text;
74         });
75 
76         connect(m_label, &TextEdit::undoPointRequested, this, &Edge::undoPointRequested);
77 
78         m_labelVisibilityTimer.setSingleShot(true);
79         m_labelVisibilityTimer.setInterval(Constants::Edge::LABEL_DURATION);
80 
81         connect(&m_labelVisibilityTimer, &QTimer::timeout, [=] {
82             setLabelVisible(false);
83         });
84     }
85 }
86 
hoverEnterEvent(QGraphicsSceneHoverEvent * event)87 void Edge::hoverEnterEvent(QGraphicsSceneHoverEvent * event)
88 {
89     m_labelVisibilityTimer.stop();
90 
91     setLabelVisible(true);
92 
93     QGraphicsItem::hoverEnterEvent(event);
94 }
95 
hoverLeaveEvent(QGraphicsSceneHoverEvent * event)96 void Edge::hoverLeaveEvent(QGraphicsSceneHoverEvent * event)
97 {
98     m_labelVisibilityTimer.start();
99 
100     QGraphicsItem::hoverLeaveEvent(event);
101 }
102 
getPen() const103 QPen Edge::getPen() const
104 {
105     QPen pen { QBrush { QColor { m_color.red(), m_color.green(), m_color.blue() } }, m_width };
106     pen.setCapStyle(Qt::PenCapStyle::RoundCap);
107     return pen;
108 }
109 
initDots()110 void Edge::initDots()
111 {
112     if (m_enableAnimations) {
113         m_sourceDot->setPen(QPen(Constants::Edge::DOT_COLOR));
114         m_sourceDot->setBrush(QBrush(Constants::Edge::DOT_COLOR));
115         m_sourceDot->setZValue(zValue() + 10);
116 
117         m_targetDot->setPen(QPen(Constants::Edge::DOT_COLOR));
118         m_targetDot->setBrush(QBrush(Constants::Edge::DOT_COLOR));
119         m_targetDot->setZValue(zValue() + 10);
120 
121         m_sourceDotSizeAnimation->setDuration(Constants::Edge::DOT_DURATION);
122         m_sourceDotSizeAnimation->setStartValue(1.0);
123         m_sourceDotSizeAnimation->setEndValue(0.0);
124 
125         const QRectF rect(-Constants::Edge::DOT_RADIUS, -Constants::Edge::DOT_RADIUS, Constants::Edge::DOT_RADIUS * 2, Constants::Edge::DOT_RADIUS * 2);
126         m_sourceDot->setRect(rect);
127 
128         m_targetDotSizeAnimation->setDuration(Constants::Edge::DOT_DURATION);
129         m_targetDotSizeAnimation->setStartValue(1.0);
130         m_targetDotSizeAnimation->setEndValue(0.0);
131 
132         m_targetDot->setRect(rect);
133     }
134 }
135 
setArrowHeadPen(const QPen & pen)136 void Edge::setArrowHeadPen(const QPen & pen)
137 {
138     m_arrowheadL0->setPen(pen);
139     m_arrowheadL0->update();
140     m_arrowheadR0->setPen(pen);
141     m_arrowheadR0->update();
142     m_arrowheadL1->setPen(pen);
143     m_arrowheadL1->update();
144     m_arrowheadR1->setPen(pen);
145     m_arrowheadR1->update();
146 }
147 
setLabelVisible(bool visible,EdgeTextEdit::VisibilityChangeReason vcr)148 void Edge::setLabelVisible(bool visible, EdgeTextEdit::VisibilityChangeReason vcr)
149 {
150     if (m_label) {
151         m_label->setVisible(visible, vcr);
152     }
153 }
154 
setWidth(double width)155 void Edge::setWidth(double width)
156 {
157     m_width = width;
158 
159     setPen(getPen());
160     setArrowHeadPen(pen());
161     updateLine();
162 }
163 
setArrowMode(ArrowMode arrowMode)164 void Edge::setArrowMode(ArrowMode arrowMode)
165 {
166     m_arrowMode = arrowMode;
167     if (!TestMode::enabled()) {
168         updateLine();
169     } else {
170         TestMode::logDisabledCode("Update line after arrow mode change");
171     }
172 }
173 
setColor(const QColor & color)174 void Edge::setColor(const QColor & color)
175 {
176     m_color = color;
177 
178     setPen(getPen());
179     setArrowHeadPen(pen());
180     updateLine();
181 }
182 
setText(const QString & text)183 void Edge::setText(const QString & text)
184 {
185     m_text = text;
186     if (!TestMode::enabled()) {
187         if (m_label) {
188             m_label->setText(text);
189         }
190         setLabelVisible(!text.isEmpty());
191     } else {
192         TestMode::logDisabledCode("Set label text");
193     }
194 }
195 
setFont(const QFont & font)196 void Edge::setFont(const QFont & font)
197 {
198     if (m_label) {
199         // Handle size and family separately to maintain backwards compatibility
200         QFont newFont(font);
201         newFont.setPointSize(m_label->font().pointSize());
202         m_label->setFont(newFont);
203     }
204 }
205 
setTextSize(int textSize)206 void Edge::setTextSize(int textSize)
207 {
208     if (m_label) {
209         m_label->setTextSize(textSize);
210     }
211 }
212 
setReversed(bool reversed)213 void Edge::setReversed(bool reversed)
214 {
215     m_reversed = reversed;
216 
217     updateArrowhead();
218 }
219 
setSelected(bool selected)220 void Edge::setSelected(bool selected)
221 {
222     m_selected = selected;
223     GraphicsFactory::setSelected(graphicsEffect(), selected);
224     update();
225 }
226 
sourceNode() const227 Node & Edge::sourceNode() const
228 {
229     return *m_sourceNode;
230 }
231 
targetNode() const232 Node & Edge::targetNode() const
233 {
234     return *m_targetNode;
235 }
236 
updateArrowhead()237 void Edge::updateArrowhead()
238 {
239     const auto point0 = m_reversed ? this->line().p1() : this->line().p2();
240     const auto angle0 = m_reversed ? -this->line().angle() + 180 : -this->line().angle();
241     const auto point1 = m_reversed ? this->line().p2() : this->line().p1();
242     const auto angle1 = m_reversed ? -this->line().angle() : -this->line().angle() + 180;
243 
244     QLineF lineL0;
245     QLineF lineR0;
246     QLineF lineL1;
247     QLineF lineR1;
248 
249     switch (m_arrowMode) {
250     case ArrowMode::Single: {
251         lineL0.setP1(point0);
252         const auto angleL = qDegreesToRadians(angle0 + Constants::Edge::ARROW_OPENING);
253         lineL0.setP2(point0 + QPointF(std::cos(angleL), std::sin(angleL)) * Constants::Edge::ARROW_LENGTH);
254         lineR0.setP1(point0);
255         const auto angleR = qDegreesToRadians(angle0 - Constants::Edge::ARROW_OPENING);
256         lineR0.setP2(point0 + QPointF(std::cos(angleR), std::sin(angleR)) * Constants::Edge::ARROW_LENGTH);
257         m_arrowheadL0->setLine(lineL0);
258         m_arrowheadR0->setLine(lineR0);
259         m_arrowheadL0->show();
260         m_arrowheadR0->show();
261         m_arrowheadL1->hide();
262         m_arrowheadR1->hide();
263         break;
264     }
265     case ArrowMode::Double: {
266         lineL0.setP1(point0);
267         const auto angleL0 = qDegreesToRadians(angle0 + Constants::Edge::ARROW_OPENING);
268         lineL0.setP2(point0 + QPointF(std::cos(angleL0), std::sin(angleL0)) * Constants::Edge::ARROW_LENGTH);
269         lineR0.setP1(point0);
270         const auto angleR0 = qDegreesToRadians(angle0 - Constants::Edge::ARROW_OPENING);
271         lineR0.setP2(point0 + QPointF(std::cos(angleR0), std::sin(angleR0)) * Constants::Edge::ARROW_LENGTH);
272         lineL1.setP1(point1);
273         m_arrowheadL0->setLine(lineL0);
274         m_arrowheadR0->setLine(lineR0);
275         m_arrowheadL0->show();
276         m_arrowheadR0->show();
277         const auto angleL1 = qDegreesToRadians(angle1 + Constants::Edge::ARROW_OPENING);
278         lineL1.setP2(point1 + QPointF(std::cos(angleL1), std::sin(angleL1)) * Constants::Edge::ARROW_LENGTH);
279         lineR1.setP1(point1);
280         const auto angleR1 = qDegreesToRadians(angle1 - Constants::Edge::ARROW_OPENING);
281         lineR1.setP2(point1 + QPointF(std::cos(angleR1), std::sin(angleR1)) * Constants::Edge::ARROW_LENGTH);
282         m_arrowheadL1->setLine(lineL1);
283         m_arrowheadR1->setLine(lineR1);
284         m_arrowheadL1->show();
285         m_arrowheadR1->show();
286         break;
287     }
288     case ArrowMode::Hidden:
289         m_arrowheadL0->hide();
290         m_arrowheadR0->hide();
291         m_arrowheadL1->hide();
292         m_arrowheadR1->hide();
293         break;
294     }
295 }
296 
updateDots()297 void Edge::updateDots()
298 {
299     if (m_enableAnimations) {
300         // Trigger new animation if relative connection location has changed
301         const auto newRelativeSourcePos = line().p1() - sourceNode().pos();
302         if (m_previousRelativeSourcePos != newRelativeSourcePos) {
303             m_previousRelativeSourcePos = newRelativeSourcePos;
304             m_sourceDotSizeAnimation->stop();
305             m_sourceDotSizeAnimation->start();
306         }
307 
308         // Update location of possibly active animation
309         m_sourceDot->setPos(line().p1());
310 
311         // Trigger new animation if relative connection location has changed
312         const auto newRelativeTargetPos = line().p2() - targetNode().pos();
313         if (m_previousRelativeTargetPos != newRelativeTargetPos) {
314             m_previousRelativeTargetPos = newRelativeTargetPos;
315             m_targetDotSizeAnimation->stop();
316             m_targetDotSizeAnimation->start();
317         }
318 
319         // Update location of possibly active animation
320         m_targetDot->setPos(line().p2());
321     }
322 }
323 
updateLabel(LabelUpdateReason lur)324 void Edge::updateLabel(LabelUpdateReason lur)
325 {
326     if (m_label) {
327         m_label->setPos((line().p1() + line().p2()) * 0.5 - QPointF(m_label->boundingRect().width(), m_label->boundingRect().height()) * 0.5);
328 
329         // Toggle visibility according to space available if geometry changed
330         if (lur == LabelUpdateReason::EdgeGeometryChanged) {
331             setLabelVisible(!m_label->sceneBoundingRect().intersects(sourceNode().sceneBoundingRect()) && !m_label->sceneBoundingRect().intersects(targetNode().sceneBoundingRect()), EdgeTextEdit::VisibilityChangeReason::AvailableSpaceChanged);
332         }
333     }
334 }
335 
setTargetNode(Node & targetNode)336 void Edge::setTargetNode(Node & targetNode)
337 {
338     m_targetNode = &targetNode;
339 }
340 
setSourceNode(Node & sourceNode)341 void Edge::setSourceNode(Node & sourceNode)
342 {
343     m_sourceNode = &sourceNode;
344 }
345 
reversed() const346 bool Edge::reversed() const
347 {
348     return m_reversed;
349 }
350 
arrowMode() const351 Edge::ArrowMode Edge::arrowMode() const
352 {
353     return m_arrowMode;
354 }
355 
text() const356 QString Edge::text() const
357 {
358     return m_text;
359 }
360 
updateLine()361 void Edge::updateLine()
362 {
363     const auto nearestPoints = Node::getNearestEdgePoints(sourceNode(), targetNode());
364 
365     const auto p1 = nearestPoints.first.location + sourceNode().pos();
366     QVector2D direction1(sourceNode().pos() - p1);
367     direction1.normalize();
368 
369     const auto p2 = nearestPoints.second.location + targetNode().pos();
370     QVector2D direction2(targetNode().pos() - p2);
371     direction2.normalize();
372 
373     setLine(QLineF(
374       p1 + (nearestPoints.first.isCorner ? Constants::Edge::CORNER_RADIUS_SCALE * (direction1 * static_cast<float>(sourceNode().cornerRadius())).toPointF() : QPointF { 0, 0 }),
375       p2 + (nearestPoints.second.isCorner ? Constants::Edge::CORNER_RADIUS_SCALE * (direction2 * static_cast<float>(targetNode().cornerRadius())).toPointF() : QPointF { 0, 0 }) - //
376         (direction2 * static_cast<float>(m_width)).toPointF() * Constants::Edge::WIDTH_SCALE));
377 
378     updateDots();
379     updateLabel(LabelUpdateReason::EdgeGeometryChanged);
380     updateArrowhead();
381 }
382 
~Edge()383 Edge::~Edge()
384 {
385     if (!TestMode::enabled()) {
386         juzzlin::L().debug() << "Deleting edge " << sourceNode().index() << " -> " << targetNode().index();
387 
388         if (m_enableAnimations) {
389             m_sourceDotSizeAnimation->stop();
390             m_targetDotSizeAnimation->stop();
391         }
392 
393         sourceNode().removeGraphicsEdge(*this);
394         targetNode().removeGraphicsEdge(*this);
395     } else {
396         TestMode::logDisabledCode("Edge destructor");
397     }
398 }
399