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 "node.hpp"
17
18 #include "constants.hpp"
19 #include "edge.hpp"
20 #include "graphics_factory.hpp"
21 #include "image.hpp"
22 #include "layers.hpp"
23 #include "test_mode.hpp"
24 #include "text_edit.hpp"
25
26 #include "simple_logger.hpp"
27
28 #include <QFont>
29 #include <QGraphicsEffect>
30 #include <QGraphicsScene>
31 #include <QGraphicsSceneHoverEvent>
32 #include <QImage>
33 #include <QPainter>
34 #include <QPen>
35 #include <QTextCursor>
36 #include <QVector2D>
37
38 #include <algorithm>
39 #include <cmath>
40
41 Node * Node::m_lastHoveredNode = nullptr;
42
Node()43 Node::Node()
44 : m_textEdit(new TextEdit(this))
45 {
46 setAcceptHoverEvents(true);
47
48 setGraphicsEffect(GraphicsFactory::createDropShadowEffect());
49
50 m_size = QSize(Constants::Node::MIN_WIDTH, Constants::Node::MIN_HEIGHT);
51
52 setZValue(static_cast<int>(Layers::Node));
53
54 createEdgePoints();
55
56 createHandles();
57
58 initTextField();
59
60 setSelected(false);
61
62 connect(m_textEdit, &TextEdit::textChanged, [=](const QString & text) {
63 setText(text);
64 adjustSize();
65 });
66
67 connect(m_textEdit, &TextEdit::undoPointRequested, this, &Node::undoPointRequested);
68
69 // Set the background transparent as the TextEdit background will be rendered in Node::paint().
70 // The reason for this is that TextEdit's background affects only the area that includes letters
71 // and we want to render a larger area.
72 m_textEdit->setBackgroundColor({ 0, 0, 0, 0 });
73 }
74
Node(const Node & other)75 Node::Node(const Node & other)
76 : Node()
77 {
78 setColor(other.m_color);
79
80 setCornerRadius(other.m_cornerRadius);
81
82 setImageRef(other.m_imageRef);
83
84 setIndex(other.m_index);
85
86 setLocation(other.m_location);
87
88 m_size = other.m_size;
89
90 setText(other.text());
91
92 setTextColor(other.m_textColor);
93
94 setTextSize(other.m_textSize);
95
96 setFont(other.m_font);
97 }
98
addGraphicsEdge(Edge & edge)99 void Node::addGraphicsEdge(Edge & edge)
100 {
101 if (!TestMode::enabled()) {
102 m_graphicsEdges.push_back(&edge);
103 } else {
104 TestMode::logDisabledCode("addGraphicsEdge");
105 }
106 }
107
removeGraphicsEdge(Edge & edge)108 void Node::removeGraphicsEdge(Edge & edge)
109 {
110 if (!TestMode::enabled()) {
111 if (const auto iter = std::find(m_graphicsEdges.begin(), m_graphicsEdges.end(), &edge); iter != m_graphicsEdges.end()) {
112 m_graphicsEdges.erase(iter);
113 }
114 } else {
115 TestMode::logDisabledCode("removeGraphicsEdge");
116 }
117 }
118
removeHandles()119 void Node::removeHandles()
120 {
121 for (auto && handle : m_handles) {
122 if (handle.second->scene()) {
123 handle.second->hide();
124 scene()->removeItem(handle.second);
125 }
126 }
127 }
128
adjustSize()129 void Node::adjustSize()
130 {
131 prepareGeometryChange();
132
133 const auto margin = Constants::Node::MARGIN * 2;
134 const auto newSize = QSize {
135 std::max(Constants::Node::MIN_WIDTH, static_cast<int>(m_textEdit->boundingRect().width() + margin)),
136 std::max(Constants::Node::MIN_HEIGHT, static_cast<int>(m_textEdit->boundingRect().height() + margin))
137 };
138
139 m_size = newSize;
140
141 createEdgePoints();
142
143 updateEdgeLines();
144
145 updateHandlePositions();
146
147 initTextField();
148
149 update();
150 }
151
boundingRect() const152 QRectF Node::boundingRect() const
153 {
154 return { -m_size.width() / 2, -m_size.height() / 2, m_size.width(), m_size.height() };
155 }
156
createAndAddGraphicsEdge(NodePtr targetNode)157 EdgePtr Node::createAndAddGraphicsEdge(NodePtr targetNode)
158 {
159 const auto edge = std::make_shared<Edge>(*this, *targetNode);
160 edge->updateLine();
161 m_graphicsEdges.push_back(edge.get());
162 return edge;
163 }
164
createEdgePoints()165 void Node::createEdgePoints()
166 {
167 const double w2 = m_size.width() * 0.5;
168 const double h2 = m_size.height() * 0.5;
169 const double bias = 0.1;
170
171 m_edgePoints = {
172 { { -w2, h2 }, true },
173 { { 0, h2 + bias }, false },
174 { { w2, h2 }, true },
175 { { w2 + bias, 0 }, false },
176 { { w2, -h2 }, true },
177 { { 0, -h2 - bias }, false },
178 { { -w2, -h2 }, true },
179 { { -w2 - bias, 0 }, false }
180 };
181 }
182
createHandles()183 void Node::createHandles()
184 {
185 m_handles[NodeHandle::Role::Add] = new NodeHandle(*this, NodeHandle::Role::Add, Constants::Node::HANDLE_RADIUS);
186
187 m_handles[NodeHandle::Role::Color] = new NodeHandle(*this, NodeHandle::Role::Color, Constants::Node::HANDLE_RADIUS_SMALL);
188
189 m_handles[NodeHandle::Role::TextColor] = new NodeHandle(*this, NodeHandle::Role::TextColor, Constants::Node::HANDLE_RADIUS_SMALL);
190
191 m_handles[NodeHandle::Role::Drag] = new NodeHandle(*this, NodeHandle::Role::Drag, Constants::Node::HANDLE_RADIUS_MEDIUM);
192
193 updateHandlePositions();
194 }
195
expandedTextEditRect() const196 QRectF Node::expandedTextEditRect() const
197 {
198 auto textEditRect = QRectF {};
199 textEditRect.setX(m_textEdit->pos().x());
200 textEditRect.setY(m_textEdit->pos().y());
201 textEditRect.setWidth(m_size.width() - Constants::Node::MARGIN * 2);
202 textEditRect.setHeight(m_textEdit->boundingRect().height());
203 return textEditRect;
204 }
205
getDistance(const Node & node1,const EdgePoint & point1,const Node & node2,const EdgePoint & point2)206 inline double getDistance(const Node & node1, const EdgePoint & point1, const Node & node2, const EdgePoint & point2)
207 {
208 return std::pow(node1.pos().x() + point1.location.x() - node2.pos().x() - point2.location.x(), 2) + //
209 std::pow(node1.pos().y() + point1.location.y() - node2.pos().y() - point2.location.y(), 2);
210 }
211
getNearestEdgePoints(const Node & node1,const Node & node2)212 std::pair<EdgePoint, EdgePoint> Node::getNearestEdgePoints(const Node & node1, const Node & node2)
213 {
214 double bestDistance = std::numeric_limits<double>::max();
215 std::pair<EdgePoint, EdgePoint> bestPair = { EdgePoint(), EdgePoint() };
216
217 // This is O(n^2) but fine as there are not many points
218 for (auto && point1 : node1.m_edgePoints) {
219 for (auto && point2 : node2.m_edgePoints) {
220 if (const auto distance = getDistance(node1, point1, node2, point2); distance < bestDistance) {
221 bestDistance = distance;
222 bestPair = { point1, point2 };
223 }
224 }
225 }
226
227 return bestPair;
228 }
229
highlightText(const QString & text)230 void Node::highlightText(const QString & text)
231 {
232 if (!TestMode::enabled()) {
233 auto cursor(m_textEdit->textCursor());
234 cursor.clearSelection();
235 if (!text.isEmpty()) {
236 if (const auto index = static_cast<int>(m_textEdit->text().toLower().indexOf(text.toLower())); index >= 0) {
237 cursor.setPosition(index);
238 cursor.movePosition(QTextCursor::MoveOperation::Right, QTextCursor::MoveMode::KeepAnchor, static_cast<int>(text.length()));
239 }
240 }
241 m_textEdit->setTextCursor(cursor);
242 } else {
243 TestMode::logDisabledCode("highlightText");
244 }
245 }
246
hoverEnterEvent(QGraphicsSceneHoverEvent * event)247 void Node::hoverEnterEvent(QGraphicsSceneHoverEvent * event)
248 {
249 // This is to more quickly hide the handles of the previous node when
250 // hovering on another node.
251 if (Node::m_lastHoveredNode && Node::m_lastHoveredNode != this) {
252 Node::m_lastHoveredNode->setHandlesVisible(false);
253 }
254 Node::m_lastHoveredNode = this;
255
256 setHandlesVisible(true);
257
258 QGraphicsItem::hoverEnterEvent(event);
259 }
260
hoverMoveEvent(QGraphicsSceneHoverEvent * event)261 void Node::hoverMoveEvent(QGraphicsSceneHoverEvent * event)
262 {
263 setHandlesVisible(true);
264
265 QGraphicsItem::hoverMoveEvent(event);
266 }
267
mousePressEvent(QGraphicsSceneMouseEvent * event)268 void Node::mousePressEvent(QGraphicsSceneMouseEvent * event)
269 {
270 // Prevent left-click on the drag node
271 if (event && index() != -1) {
272 if (expandedTextEditRect().contains(event->pos())) {
273 m_textEdit->setFocus();
274 }
275 QGraphicsItem::mousePressEvent(event);
276 }
277 }
278
initTextField()279 void Node::initTextField()
280 {
281 if (!TestMode::enabled()) {
282 m_textEdit->setTextWidth(-1);
283 m_textEdit->setPos(-m_size.width() * 0.5 + Constants::Node::MARGIN, -m_size.height() * 0.5 + Constants::Node::MARGIN);
284 } else {
285 TestMode::logDisabledCode("initTestField");
286 }
287 }
288
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)289 void Node::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
290 {
291 Q_UNUSED(widget)
292 Q_UNUSED(option)
293
294 for (auto && handle : m_handles) {
295 if (!handle.second->scene())
296 scene()->addItem(handle.second);
297 }
298
299 painter->save();
300
301 // Background
302
303 QPainterPath path;
304 const QRectF rect(-m_size.width() / 2, -m_size.height() / 2, m_size.width(), m_size.height());
305 path.addRoundedRect(rect, m_cornerRadius, m_cornerRadius);
306 painter->setRenderHint(QPainter::Antialiasing);
307
308 if (!m_pixmap.isNull()) {
309 QPixmap scaledPixmap(static_cast<int>(m_size.width()), static_cast<int>(m_size.height()));
310 scaledPixmap.fill(Qt::transparent);
311 QPainter pixmapPainter(&scaledPixmap);
312 QPainterPath scaledPath;
313 const QRectF scaledRect(0, 0, m_size.width(), m_size.height());
314 scaledPath.addRoundedRect(scaledRect, m_cornerRadius, m_cornerRadius);
315 const auto pixmapAspect = static_cast<double>(m_pixmap.width()) / m_pixmap.height();
316 if (const auto nodeAspect = m_size.width() / m_size.height(); nodeAspect > 1.0) {
317 if (pixmapAspect > nodeAspect) {
318 pixmapPainter.fillPath(scaledPath, QBrush(m_pixmap.scaledToHeight(static_cast<int>(m_size.height()))));
319 } else {
320 pixmapPainter.fillPath(scaledPath, QBrush(m_pixmap.scaledToWidth(static_cast<int>(m_size.width()))));
321 }
322 } else {
323 if (pixmapAspect < nodeAspect) {
324 pixmapPainter.fillPath(scaledPath, QBrush(m_pixmap.scaledToWidth(static_cast<int>(m_size.width()))));
325 } else {
326 pixmapPainter.fillPath(scaledPath, QBrush(m_pixmap.scaledToHeight(static_cast<int>(m_size.height()))));
327 }
328 }
329
330 painter->drawPixmap(rect, scaledPixmap, scaledRect);
331 } else {
332 const QPen pen(QColor { 2 * m_color.red() / 3, 2 * m_color.green() / 3, 2 * m_color.blue() / 3 }, 1);
333 painter->fillPath(path, QBrush(m_color));
334 painter->strokePath(path, pen);
335 }
336
337 // Patch for TextEdit
338
339 painter->fillRect(expandedTextEditRect(), Constants::Node::TEXT_EDIT_BACKGROUND_COLOR);
340
341 painter->restore();
342 }
343
setColor(const QColor & color)344 void Node::setColor(const QColor & color)
345 {
346 m_color = color;
347 if (!TestMode::enabled()) {
348 update();
349 } else {
350 TestMode::logDisabledCode("update() on setColor");
351 }
352 }
353
cornerRadius() const354 int Node::cornerRadius() const
355 {
356 return m_cornerRadius;
357 }
358
setCornerRadius(int value)359 void Node::setCornerRadius(int value)
360 {
361 m_cornerRadius = value;
362
363 updateEdgeLines();
364
365 // Needed to redraw immediately on e.g. a new design. Otherwise updates start to work only after
366 // the first mouse over, which is a bit weird.
367 prepareGeometryChange();
368
369 update();
370 }
371
setHandlesVisible(bool visible)372 void Node::setHandlesVisible(bool visible)
373 {
374 for (auto && handle : m_handles) {
375 handle.second->setVisible(visible && index() != -1);
376 }
377 }
378
location() const379 QPointF Node::location() const
380 {
381 return m_location;
382 }
383
setLocation(QPointF newLocation)384 void Node::setLocation(QPointF newLocation)
385 {
386 m_location = newLocation;
387 setPos(newLocation);
388
389 updateEdgeLines();
390
391 updateHandlePositions();
392 }
393
placementBoundingRect() const394 QRectF Node::placementBoundingRect() const
395 {
396 return { -m_size.width() / 2, -m_size.height() / 2, m_size.width(), m_size.height() };
397 }
398
selected() const399 bool Node::selected() const
400 {
401 return m_selected;
402 }
403
setSelected(bool selected)404 void Node::setSelected(bool selected)
405 {
406 m_selected = selected;
407 GraphicsFactory::setSelected(graphicsEffect(), selected);
408 update();
409 }
410
setTextInputActive(bool active)411 void Node::setTextInputActive(bool active)
412 {
413 m_textEdit->setActive(active);
414 if (active) {
415 m_textEdit->setFocus();
416 } else {
417 m_textEdit->clearFocus();
418 }
419 }
420
text() const421 QString Node::text() const
422 {
423 if (!TestMode::enabled()) {
424 return m_textEdit->text();
425 } else {
426 TestMode::logDisabledCode("return widget text");
427 return m_text;
428 }
429 }
430
setText(const QString & text)431 void Node::setText(const QString & text)
432 {
433 if (text != m_text) {
434 m_text = text;
435 m_textEdit->setText(text);
436 adjustSize();
437 }
438 }
439
textColor() const440 QColor Node::textColor() const
441 {
442 return m_textColor;
443 }
444
setTextColor(const QColor & color)445 void Node::setTextColor(const QColor & color)
446 {
447 m_textColor = color;
448 if (!TestMode::enabled()) {
449 m_textEdit->setDefaultTextColor(color);
450 m_textEdit->update();
451 } else {
452 TestMode::logDisabledCode("set widget color");
453 }
454 }
455
setFont(const QFont & font)456 void Node::setFont(const QFont & font)
457 {
458 m_font = font;
459 if (!TestMode::enabled()) {
460 // Handle size and family separately to maintain backwards compatibility
461 QFont newFont(font);
462 newFont.setPointSize(m_textSize);
463 m_textEdit->setFont(newFont);
464 adjustSize();
465 } else {
466 TestMode::logDisabledCode("set node font");
467 }
468 }
469
setTextSize(int textSize)470 void Node::setTextSize(int textSize)
471 {
472 m_textSize = textSize;
473 if (!TestMode::enabled()) {
474 m_textEdit->setTextSize(textSize);
475 adjustSize();
476 } else {
477 TestMode::logDisabledCode("set node text size");
478 }
479 }
480
setImageRef(size_t imageRef)481 void Node::setImageRef(size_t imageRef)
482 {
483 if (imageRef) {
484 m_imageRef = imageRef;
485 emit imageRequested(imageRef, *this);
486 } else if (m_imageRef) {
487 m_imageRef = imageRef;
488 applyImage({});
489 }
490 }
491
applyImage(const Image & image)492 void Node::applyImage(const Image & image)
493 {
494 m_pixmap = QPixmap::fromImage(image.image());
495
496 update();
497 }
498
updateEdgeLines()499 void Node::updateEdgeLines()
500 {
501 for (auto && edge : m_graphicsEdges) {
502 edge->updateLine();
503 }
504 }
505
updateHandlePositions()506 void Node::updateHandlePositions()
507 {
508 m_handles[NodeHandle::Role::Add]->setPos(pos() + QPointF { 0, m_size.height() * 0.5 });
509
510 m_handles[NodeHandle::Role::Color]->setPos(pos() + QPointF { m_size.width() * 0.5, m_size.height() * 0.5 - Constants::Node::HANDLE_RADIUS_SMALL * 0.5 });
511
512 m_handles[NodeHandle::Role::TextColor]->setPos(pos() + QPointF { m_size.width() * 0.5, -m_size.height() * 0.5 + Constants::Node::HANDLE_RADIUS_SMALL * 0.5 });
513
514 m_handles[NodeHandle::Role::Drag]->setPos(pos() + QPointF { -m_size.width() * 0.5 - Constants::Node::HANDLE_RADIUS_SMALL * 0.15, -m_size.height() * 0.5 - Constants::Node::HANDLE_RADIUS_SMALL * 0.15 });
515 }
516
lastHoveredNode()517 Node * Node::lastHoveredNode()
518 {
519 return m_lastHoveredNode;
520 }
521
size() const522 QSizeF Node::size() const
523 {
524 return m_size;
525 }
526
setSize(const QSizeF & size)527 void Node::setSize(const QSizeF & size)
528 {
529 m_size = size;
530 }
531
imageRef() const532 size_t Node::imageRef() const
533 {
534 return m_imageRef;
535 }
536
color() const537 QColor Node::color() const
538 {
539 return m_color;
540 }
541
containsText(const QString & text) const542 bool Node::containsText(const QString & text) const
543 {
544 return m_text.toLower().contains(text.toLower());
545 }
546
index() const547 int Node::index() const
548 {
549 return m_index;
550 }
551
setIndex(int index)552 void Node::setIndex(int index)
553 {
554 m_index = index;
555 }
556
unselectText()557 void Node::unselectText()
558 {
559 auto cursor(m_textEdit->textCursor());
560 cursor.clearSelection();
561 m_textEdit->setTextCursor(cursor);
562 }
563
~Node()564 Node::~Node()
565 {
566 if (Node::m_lastHoveredNode == this) {
567 Node::m_lastHoveredNode = nullptr;
568 }
569
570 juzzlin::L().debug() << "Deleting handles of node " << index();
571
572 for (auto && handle : m_handles) {
573 delete handle.second;
574 }
575
576 juzzlin::L().debug() << "Deleting Node " << index();
577 }
578