Vuo  2.0.0
VuoRendererComment.cc
Go to the documentation of this file.
1 
10 #include "VuoRendererComment.hh"
12 #include "VuoRendererSignaler.hh"
13 #include "VuoRendererFonts.hh"
14 #include "VuoStringUtilities.hh"
15 #include "VuoComment.hh"
16 
17 const qreal VuoRendererComment::cornerRadius = 10 /*VuoRendererFonts::thickPenWidth/2.0*/;
18 const qreal VuoRendererComment::borderWidth = 5.5 /*VuoRendererPort::portBarrierWidth*/;
19 const 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 
66 QPainterPath 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 
82 QRectF 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 
96 QRectF 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 
122 QPainterPath 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 
145 void 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 
165 void VuoRendererComment::drawCommentFrame(QPainter *painter, VuoRendererColors *colors) const
166 {
167  // Filled rounded rectangle
168  painter->fillPath(commentFrame, colors->commentFill());
169 }
170 
174 void 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 
187 void 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 
198 QPainterPath 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 
210 void 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 
220 QPainterPath 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 
231 QVariant 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  {
281  updateGeometry();
282  updateColor();
283  }
284 
285  return QGraphicsItem::itemChange(change, newValue);
286 }
287 
291 bool 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 
374 void VuoRendererComment::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
375 {
376  hoverMoveEvent(event);
377 }
378 
382 void 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 
396 void 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 
409 void VuoRendererComment::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
410 {
411  if (event->modifiers() & Qt::ShiftModifier)
413  else
415 }
416 
420 void 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 
436 void 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 
465  updateGeometry();
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  {
476  updateGeometry();
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 
498 void 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 
520 void VuoRendererComment::setContent(string content)
521 {
522  QGraphicsItem::CacheMode normalCacheMode = cacheMode();
523  setCacheMode(QGraphicsItem::NoCache);
524  updateGeometry();
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 
549 void VuoRendererComment::setBodySelectable(bool bodySelectable)
550 {
551  QGraphicsItem::CacheMode normalCacheMode = cacheMode();
552  setCacheMode(QGraphicsItem::NoCache);
553  updateGeometry();
554 
555  this->bodySelectable = bodySelectable;
556 
557  setCacheMode(normalCacheMode);
558 }
559 
564 void 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 
586 void 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)
611  alignedFrameHeight += VuoRendererComposition::minorGridLineSpacing;
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 
654 bool VuoRendererComment::titleHandleActiveForEventPos(QPointF pos)
655 {
656  return (titleHandleHoveredForEventPos(pos) || (textItem->boundingRect().contains(pos)) || isSelected());
657 }
658 
662 bool VuoRendererComment::titleHandleHoveredForEventPos(QPointF pos)
663 {
664  return (extendedTitleHandleBoundingRect().contains(pos) && !dragHandleHoveredForEventPos(pos));
665 }
666 
670 bool VuoRendererComment::dragHandleHoveredForEventPos(QPointF pos)
671 {
672  return extendedDragHandleBoundingRect().contains(pos);
673 }
674 
678 QString 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() ? "#383838" : "#ececec");
702 }