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