Vuo  2.4.0
VuoRendererComment.cc
Go to the documentation of this file.
1
10#include "VuoRendererComment.hh"
13#include "VuoRendererFonts.hh"
14#include "VuoStringUtilities.hh"
15#include "VuoComment.hh"
16
17const qreal VuoRendererComment::cornerRadius = 10 /*VuoRendererFonts::thickPenWidth/2.0*/;
18const qreal VuoRendererComment::borderWidth = 5.5 /*VuoRendererPort::portBarrierWidth*/;
19const qreal VuoRendererComment::textMargin = 5 /*VuoRendererFonts::thickPenWidth/4.0*/;
20
25 : VuoBaseDetail<VuoComment>("VuoRendererComment from VuoCompilerComment", baseComment)
26{
27 getBase()->setRenderer(this);
28 setZValue(commentZValue);
29 this->resizeDragInProgress = false;
30 this->resizeDeltaIgnored = QPointF(0,0);
31 this->dragHandleHovered = false;
32 this->titleHandleHovered = false;
33 this->bodySelectable = true;
34
35 this->textItem = new QGraphicsTextItem(this);
36
37 VuoRendererColors *colors = new VuoRendererColors(getBase()->getTintColor(), VuoRendererColors::noSelection);
38 textItem->setDefaultTextColor(colors->commentText());
39
40 textItem->setPos(QPoint(textMargin, textMargin));
41 textItem->setTextInteractionFlags(Qt::TextBrowserInteraction); // necessary for clickable links
42 textItem->setFlag(QGraphicsItem::ItemIsSelectable, false); // disable dotted border
43 textItem->setFlag(QGraphicsItem::ItemIsFocusable, false); // disable dotted border
44 textItem->setOpenExternalLinks(true);
45 updateFormattedCommentText();
46
47 // Set up signaler after comment has been positioned to avoid sending a spurious commentMoved signal.
48 this->signaler = NULL;
49
50 this->setFlags(QGraphicsItem::ItemIsMovable |
51 QGraphicsItem::ItemIsSelectable |
52 QGraphicsItem::ItemIsFocusable |
53 QGraphicsItem::ItemSendsGeometryChanges);
54
55 this->setAcceptHoverEvents(true);
56
57 setPos(baseComment->getX(), baseComment->getY());
58 updateFrameRect();
59
60 this->signaler = signaler;
61}
62
66QPainterPath VuoRendererComment::getCommentFrame(QRectF commentFrameRect) const
67{
68 // Rounded rectangle
69 QPainterPath frame;
70 frame.moveTo(commentFrameRect.right(), commentFrameRect.center().y());
71 addRoundedCorner(frame, true, commentFrameRect.bottomRight(), cornerRadius, false, false);
72 addRoundedCorner(frame, true, commentFrameRect.bottomLeft(), cornerRadius, false, true);
73 addRoundedCorner(frame, true, commentFrameRect.topLeft(), cornerRadius, true, true);
74 addRoundedCorner(frame, true, commentFrameRect.topRight(), cornerRadius, true, false);
75
76 return frame;
77}
78
82QRectF VuoRendererComment::extendedTitleHandleBoundingRect(void) const
83{
84 QRectF r = getTitleHandlePath(frameRect).boundingRect();
85
86 r.setLeft(boundingRect().left());
87 r.setRight(boundingRect().right());
88 r.adjust(0,0,0,textMargin-r.bottom());
89
90 return r.toAlignedRect();
91}
92
96QRectF VuoRendererComment::extendedDragHandleBoundingRect(void) const
97{
98 QRectF r = getDragHandlePath(frameRect).boundingRect();
99 r.adjust(-borderWidth/2.0, -borderWidth/2.0, borderWidth/2.0, borderWidth/2.0);
100 r.adjust(-10, -10, 10, 10);
101
102 return r.toAlignedRect();
103}
104
109{
110 QRectF r = this->frameRect;
111
112 // Antialiasing bleed
113 r.adjust(-1,-1,1,1);
114
115 return r.toAlignedRect();
116}
117
122QPainterPath VuoRendererComment::shape() const
123{
124 if (bodySelectable || isSelected())
125 {
126 QPainterPath p;
127 p.addRect(boundingRect());
128 return p;
129 }
130 else
131 {
132 QPainterPath p;
133 p.addPath(getTitleHandlePath(frameRect));
134
135 QPainterPathStroker s;
136 s.setWidth(textMargin);
137
138 return s.createStroke(p);
139 }
140}
141
145void VuoRendererComment::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
146{
147 painter->setRenderHint(QPainter::Antialiasing, true);
148 drawBoundingRect(painter);
149
150 VuoRendererColors::SelectionType selectionType = (isSelected()? VuoRendererColors::directSelection : VuoRendererColors::noSelection);
151 VuoRendererColors *colors = new VuoRendererColors(getBase()->getTintColor(), selectionType);
152
153 // @todo https://b33p.net/kosada/node/9986 Quantize ?
154 drawCommentFrame(painter, colors);
155 drawTextContent(painter);
156 drawTitleHandle(painter, colors);
157 drawDragHandle(painter, colors);
158
159 delete colors;
160}
161
165void VuoRendererComment::drawCommentFrame(QPainter *painter, VuoRendererColors *colors) const
166{
167 // Filled rounded rectangle
168 painter->fillPath(commentFrame, colors->commentFill());
169}
170
174void VuoRendererComment::drawTextContent(QPainter *painter) const
175{
176 // The actual QGraphicsTextItem child will paint itself.
177 QRectF textContentBoundingRect = QRectF(textMargin,
178 textMargin,
179 frameRect.width() - 2*textMargin,
180 frameRect.height() - 2*textMargin);
181 VuoRendererItem::drawRect(painter, textContentBoundingRect);
182}
183
187void VuoRendererComment::drawTitleHandle(QPainter *painter, VuoRendererColors *colors) const
188{
189 bool paintTitleHandle = (titleHandleHovered || isSelected());
190 painter->setPen(QPen(paintTitleHandle? colors->commentFrame() : colors->commentFill(), borderWidth, Qt::SolidLine, Qt::RoundCap));
191 painter->drawPath(getTitleHandlePath(frameRect));
192 VuoRendererItem::drawRect(painter, extendedTitleHandleBoundingRect());
193}
194
198QPainterPath VuoRendererComment::getTitleHandlePath(QRectF commentFrameRect) const
199{
200 QPainterPath p;
201 p.moveTo(commentFrameRect.topLeft()+QPointF(0.5*borderWidth+cornerRadius, 0.5*borderWidth));
202 p.lineTo(commentFrameRect.topRight()+QPointF(-0.5*borderWidth-cornerRadius, 0.5*borderWidth));
203
204 return p;
205}
206
210void VuoRendererComment::drawDragHandle(QPainter *painter, VuoRendererColors *colors) const
211{
212 painter->setPen(QPen(dragHandleHovered? colors->commentFrame() : colors->commentFill(), borderWidth, Qt::SolidLine, Qt::RoundCap));
213 painter->drawPath(getDragHandlePath(frameRect));
214 VuoRendererItem::drawRect(painter, extendedDragHandleBoundingRect());
215}
216
220QPainterPath VuoRendererComment::getDragHandlePath(QRectF commentFrameRect) const
221{
222 QPainterPath p;
223 addRoundedCorner(p, false, commentFrameRect.bottomRight()-QPointF(0.5*borderWidth, 0.5*borderWidth), cornerRadius, false, false);
224
225 return p;
226}
227
231QVariant VuoRendererComment::itemChange(GraphicsItemChange change, const QVariant &value)
232{
233 QVariant newValue = value;
234
235 if (change == QGraphicsItem::ItemSceneHasChanged)
236 {
237 // Scene event filters can only be installed on graphics items after they have been added to a scene.
238 if (scene())
239 textItem->installSceneEventFilter(this);
240 }
241
242 if (change == QGraphicsItem::ItemPositionChange)
243 {
244 if (getSnapToGrid())
245 {
246 // Quantize position to nearest minor gridline.
248 }
249 else
250 {
251 // Quantize position to whole pixels.
252 newValue = value.toPoint();
253 }
254 }
255
256 // Comment has moved within its parent
257 if (change == QGraphicsItem::ItemPositionHasChanged)
258 {
259 QPointF newPos = value.toPointF();
260 if ((getBase()->getX() != newPos.x()) || (getBase()->getY() != newPos.y()))
261 {
262 qreal dx = newPos.x() - this->getBase()->getX();
263 qreal dy = newPos.y() - this->getBase()->getY();
264
265 this->getBase()->setX(newPos.x());
266 this->getBase()->setY(newPos.y());
267
268 if (signaler)
269 {
270 set<VuoRendererComment *> movedComments;
271 movedComments.insert(this);
272 signaler->signalCommentsMoved(movedComments, dx, dy, true);
273 }
274 }
275
276 return newPos;
277 }
278
279 if (change == QGraphicsItem::ItemSelectedHasChanged)
280 {
282 updateColor();
283 }
284
285 return QGraphicsItem::itemChange(change, newValue);
286}
287
291bool VuoRendererComment::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
292{
293 if (watched == this->textItem)
294 {
295 // Respond to double-clicks in our own VuoRendererComment event handler even if the
296 // double-click occurred over the comment text.
297 if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
298 {
299 QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
300
301 // Map the event position from child coordinates to local coordinates.
302 mouseEvent->setPos(mapFromItem(this->textItem, mouseEvent->pos()));
303
304 mouseDoubleClickEvent(mouseEvent);
305 return true; // Comment text does not need to know about mouse double-click events.
306 }
307
308 // Disable text selection via mouse-drag within comment text.
309 {
310 if (event->type() == QEvent::GraphicsSceneMouseMove)
311 {
312 QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
313
314 // Map the event position from child coordinates to local coordinates.
315 mouseEvent->setPos(mapFromItem(this->textItem, mouseEvent->pos()));
316
317 mouseMoveEvent(mouseEvent);
318 return true; // Comment text does not need to know about mouse move events.
319 }
320 if (event->type() == QEvent::GraphicsSceneMousePress)
321 {
322 QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
323
324 // Map the event position from child coordinates to local coordinates.
325 mouseEvent->setPos(mapFromItem(this->textItem, mouseEvent->pos()));
326
327 // Remove Shift modifier from mouse-press events to prevent it from triggering unpredictable text selection.
328 Qt::KeyboardModifiers modifiersOtherThanShift = (mouseEvent->modifiers() & ~Qt::ShiftModifier);
329 mouseEvent->setModifiers(modifiersOtherThanShift);
330
331 mousePressEvent(mouseEvent);
332
333 // Remap the event position to child coordinates before passing it along to the child.
334 mouseEvent->setPos(mapToItem(this->textItem, mouseEvent->pos()));
335
336 return false; // Comment text needs to know about mouse presses on links.
337 }
338 if (event->type() == QEvent::GraphicsSceneMouseRelease)
339 {
340 QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
341
342 // Map the event position from child coordinates to local coordinates.
343 mouseEvent->setPos(mapFromItem(this->textItem, mouseEvent->pos()));
344
345 mouseReleaseEvent(mouseEvent);
346
347 // Remap the event position to child coordinates before passing it along to the child.
348 mouseEvent->setPos(mapToItem(this->textItem, mouseEvent->pos()));
349
350 return false; // Comment text needs to know about mouse releases on links.
351 }
352 }
353
354 // Make sure this comment's hover highlighting is updated when its child text item
355 // is receiving the hover move events.
356 if (event->type() == QEvent::GraphicsSceneHoverMove)
357 {
358 QGraphicsSceneHoverEvent *hoverEvent = static_cast<QGraphicsSceneHoverEvent *>(event);
359
360 // Map the event position from child coordinates to local coordinates.
361 hoverEvent->setPos(mapFromItem(this->textItem, hoverEvent->pos()));
362
363 hoverMoveEvent(hoverEvent);
364 return true; // Comment text does not need to know about hover events.
365 }
366 }
367
368 return false;
369}
370
374void VuoRendererComment::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
375{
376 hoverMoveEvent(event);
377}
378
382void VuoRendererComment::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
383{
384 // Update hover highlighting for the drag handle.
385 QGraphicsItem::CacheMode normalCacheMode = cacheMode();
386 setCacheMode(QGraphicsItem::NoCache);
387 prepareGeometryChange();
388 dragHandleHovered = dragHandleHoveredForEventPos(event->pos());
389 titleHandleHovered = titleHandleHoveredForEventPos(event->pos());
390 setCacheMode(normalCacheMode);
391}
392
396void VuoRendererComment::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
397{
398 QGraphicsItem::CacheMode normalCacheMode = cacheMode();
399 setCacheMode(QGraphicsItem::NoCache);
400 prepareGeometryChange();
401 dragHandleHovered = false;
402 titleHandleHovered = false;
403 setCacheMode(normalCacheMode);
404}
405
409void VuoRendererComment::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
410{
411 if (event->modifiers() & Qt::ShiftModifier)
413 else
415}
416
420void VuoRendererComment::mousePressEvent(QGraphicsSceneMouseEvent *event)
421{
422 if ((event->button() == Qt::LeftButton) && dragHandleHoveredForEventPos(event->pos()))
423 {
424 resizeDragInProgress = true;
425 resizeDeltaIgnored = QPointF(0,0);
426 }
427 else if ((event->button() == Qt::LeftButton) && titleHandleActiveForEventPos(event->pos()))
428 QGraphicsItem::mousePressEvent(event);
429 else
430 event->ignore();
431}
432
436void VuoRendererComment::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
437{
438 if ((event->buttons() & Qt::LeftButton) && resizeDragInProgress)
439 {
440 const qreal minSize = VuoRendererComposition::minorGridLineSpacing*2.0;
441 const QPointF eventDelta = event->scenePos().toPoint() - event->lastScenePos().toPoint();
442
443 QPointF requestedDelta = eventDelta;
444
445 // Wait to start expanding until the mouse passes back over the point where clamping began.
446 if ((requestedDelta.x() > 0) && (resizeDeltaIgnored.x() < 0))
447 {
448 requestedDelta.setX(requestedDelta.x() + resizeDeltaIgnored.x());
449 resizeDeltaIgnored.setX(0);
450 }
451
452 if ((requestedDelta.y() > 0) && (resizeDeltaIgnored.y() < 0))
453 {
454 requestedDelta.setY(requestedDelta.y() + resizeDeltaIgnored.y());
455 resizeDeltaIgnored.setY(0);
456 }
457
458 qreal widthUnderflow = minSize - (getBase()->getWidth()+requestedDelta.x());
459 qreal heightUnderflow = minSize - (getBase()->getHeight()+requestedDelta.y());
460
461 // Don't allow the user to resize the comment below the minimum comment dimensions.
462 QPointF adjustedDelta((widthUnderflow <= 0? requestedDelta.x() : requestedDelta.x()+widthUnderflow),
463 (heightUnderflow <= 0? requestedDelta.y() : requestedDelta.y()+heightUnderflow));
464
466 getBase()->setWidth(getBase()->getWidth()+adjustedDelta.x());
467 getBase()->setHeight(getBase()->getHeight()+adjustedDelta.y());
468 updateFrameRect();
469
470 resizeDeltaIgnored += requestedDelta - adjustedDelta;
471
472 // Don't allow the user to resize the comment so that it no longer encloses its text content.
473 int yTextOverflow = textItem->boundingRect().height()+2*textMargin - frameRect.height();
474 if ((yTextOverflow > 0) && ((adjustedDelta.y() < 0) || adjustedDelta.x() < 0))
475 {
477 getBase()->setWidth(getBase()->getWidth()-adjustedDelta.x());
478 getBase()->setHeight(getBase()->getHeight()-adjustedDelta.y());
479 updateFrameRect();
480
481 resizeDeltaIgnored += adjustedDelta;
482 }
483
484 else if (signaler)
485 signaler->signalCommentResized(this, adjustedDelta.x(), adjustedDelta.y());
486 }
487
488 else if ((event->buttons() & Qt::LeftButton) && titleHandleActiveForEventPos(event->buttonDownPos(Qt::LeftButton)))
489 QGraphicsItem::mouseMoveEvent(event);
490
491 else
492 event->ignore();
493}
494
498void VuoRendererComment::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
499{
500 if (event->button() == Qt::LeftButton)
501 resizeDragInProgress = false;
502
503 if ((event->button() == Qt::LeftButton) && titleHandleActiveForEventPos(event->buttonDownPos(Qt::LeftButton)))
504 QGraphicsItem::mouseReleaseEvent(event);
505 else
506 event->ignore();
507}
508
513{
514 this->prepareGeometryChange();
515}
516
521{
522 QGraphicsItem::CacheMode normalCacheMode = cacheMode();
523 setCacheMode(QGraphicsItem::NoCache);
525
526 getBase()->setContent(content);
527 updateFormattedCommentText();
528
529 // Expand as necessary to accommodate the modified text. Normally we will at most need
530 // to expand vertically, but some richtext content doesn't get word-wrapped
531 // by the QGraphicsTextItem, so in these cases we might need to expand horizontally as well.
532 QPointF minSize(max(getBase()->getWidth()*1.,
533 this->textItem->boundingRect().toAlignedRect().width()+2*textMargin),
534 max(getBase()->getHeight()*1.,
535 this->textItem->boundingRect().toAlignedRect().height()+2*textMargin));
536
537 getBase()->setWidth(minSize.toPoint().x());
538 getBase()->setHeight(minSize.toPoint().y());
539
540 updateFrameRect();
541 setCacheMode(normalCacheMode);
542}
543
550{
551 QGraphicsItem::CacheMode normalCacheMode = cacheMode();
552 setCacheMode(QGraphicsItem::NoCache);
554
555 this->bodySelectable = bodySelectable;
556
557 setCacheMode(normalCacheMode);
558}
559
564void VuoRendererComment::updateFormattedCommentText()
565{
566 string textWithoutQuotes = "";
567
568 // Unescape the JSON string.
569 json_object *js = json_tokener_parse(getBase()->getContent().c_str());
570 if (json_object_get_type(js) == json_type_string)
571 textWithoutQuotes = json_object_get_string(js);
572 json_object_put(js);
573
574 QString textContent = QString::fromUtf8(textWithoutQuotes.c_str());
575
576 // QPainter::drawText expects strings to be canonically composed,
577 // else it renders diacritics next to (instead of superimposed upon) their base glyphs.
578 textContent = textContent.normalized(QString::NormalizationForm_C);
579 this->textItem->setHtml(generateTextStyleString()
580 .append(VuoStringUtilities::generateHtmlFromMarkdown(textContent.toUtf8().constData()).c_str()));
581}
582
586void VuoRendererComment::updateFrameRect(void)
587{
588 QRectF updatedFrameRect;
589 int unalignedFrameWidth = floor(getBase()->getWidth());
590 int unalignedFrameHeight = floor(getBase()->getHeight());
591
592 // If in snap-to-grid mode, snap to the next-largest horizontal and vertical grid positions.
593 int alignedFrameWidth = (getSnapToGrid()?
595 unalignedFrameWidth);
596
597 if (alignedFrameWidth < unalignedFrameWidth)
599
600 int alignedFrameHeight = unalignedFrameHeight;
601 if (getSnapToGrid())
602 {
603 // Make it possible to enclose nodes inside comments with equal margins on the top and bottom by accounting for
604 // the little bit of extra space that all nodes extend vertically beyond minor gridline increments.
605 const int nodeHeightOverflow = floor(VuoRendererPort::portContainerMargin*2) + 1;
606
607 alignedFrameHeight = VuoRendererComposition::quantizeToNearestGridLine(QPointF(0, unalignedFrameHeight-nodeHeightOverflow), VuoRendererComposition::minorGridLineSpacing).y()
608 + nodeHeightOverflow;
609
610 if (alignedFrameHeight < unalignedFrameHeight)
612 }
613
614 updatedFrameRect.setWidth(alignedFrameWidth);
615 updatedFrameRect.setHeight(alignedFrameHeight);
616
617 // Correct for the fact that VuoRendererComposition::drawBackground() corrects for the fact that
618 // VuoRendererNode::paint() starts painting at (-1,0) rather than (0,0), to keep edges
619 // aligned with grid lines.
620 // @todo: Eliminate this correction after modifying VuoRendererNode::paint()
621 // for https://b33p.net/kosada/node/10210 .
622 const int xAlignmentCorrection = -1;
623 updatedFrameRect.adjust(xAlignmentCorrection, 0, xAlignmentCorrection, 0);
624
625 if (this->frameRect != updatedFrameRect)
626 {
627 this->frameRect = updatedFrameRect;
628 this->commentFrame = getCommentFrame(this->frameRect);
629 this->textItem->setTextWidth(frameRect.width() - 2*textMargin);
630 }
631}
632
637{
638 QGraphicsItem::CacheMode normalCacheMode = cacheMode();
639 setCacheMode(QGraphicsItem::NoCache);
640 prepareGeometryChange();
641
642 VuoRendererColors::SelectionType selectionType = (isSelected()? VuoRendererColors::directSelection : VuoRendererColors::noSelection);
643 VuoRendererColors *colors = new VuoRendererColors(getBase()->getTintColor(), selectionType);
644 this->textItem->setDefaultTextColor(colors->commentText());
645 updateFormattedCommentText();
646
647 setCacheMode(normalCacheMode);
648}
649
654bool VuoRendererComment::titleHandleActiveForEventPos(QPointF pos)
655{
656 return (titleHandleHoveredForEventPos(pos) || (textItem->boundingRect().contains(pos)) || isSelected());
657}
658
662bool VuoRendererComment::titleHandleHoveredForEventPos(QPointF pos)
663{
664 return (extendedTitleHandleBoundingRect().contains(pos) && !dragHandleHoveredForEventPos(pos));
665}
666
670bool VuoRendererComment::dragHandleHoveredForEventPos(QPointF pos)
671{
672 return extendedDragHandleBoundingRect().contains(pos);
673}
674
678QString VuoRendererComment::generateTextStyleString()
679{
681 return VUO_QSTRINGIFY(
682 <style>
683 * {
684 %2;
685 }
686 a {
687 color: %1;
688 }
689 h1,h2,h3,h4,h5,h6,b,strong,th {
690 font-weight: bold;
691 }
692 pre,code {
693 font-family: 'Monaco';
694 font-size: 12px;
695 background-color: %3;
696 white-space: pre-wrap;
697 }
698 </style>)
699 .arg(VuoRendererColors::isDark() ? "#88a2de" : "#74acec")
700 .arg(f->getCSS(f->commentFont()))
701 .arg(VuoRendererColors::isDark() ? "rgba(0,0,0,.25)" : "rgba(255,255,255,.6)");
702}