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