Vuo  2.0.2
VuoNodePopover.cc
Go to the documentation of this file.
1 
10 #include "VuoNodePopover.hh"
11 
12 #include "VuoCompiler.hh"
13 #include "VuoComposition.hh"
15 #include "VuoException.hh"
17 #include "VuoRendererFonts.hh"
18 #include "VuoEditor.hh"
19 #include "VuoEditorComposition.hh"
20 #include "VuoEditorUtilities.hh"
21 #include "VuoEditorWindow.hh"
22 #include "VuoNodeClass.hh"
23 #include "VuoNodeClassList.hh"
24 #include "VuoNodeSet.hh"
25 #include "VuoStringUtilities.hh"
27 
28 const int VuoNodePopover::defaultPopoverTextWidth = 192;
29 const int VuoNodePopover::margin = 8;
30 
34 VuoNodePopover::VuoNodePopover(VuoNodeClass *nodeClass, VuoCompiler *compiler, QWidget *parent) :
35  VuoPanelDocumentation(parent)
36 {
37  this->node = NULL;
38  this->nodeClass = nodeClass;
39  this->displayModelNode = !VuoCompilerMakeListNodeClass::isMakeListNodeClassName(nodeClass->getClassName());
40  this->modelNode = NULL;
41  this->compiler = compiler;
42  initialize();
43 }
44 
48 void VuoNodePopover::initialize()
49 {
50  VuoEditor *editor = (VuoEditor *)qApp;
51  bool isDark = editor->isInterfaceDark();
52 
53 #if VUO_PRO
54  initialize_Pro();
55 #endif
56 
57  this->popoverHasBeenShown = false;
58 
59  if (nodeClass->hasCompiler())
60  {
61  VuoCompilerSpecializedNodeClass *specialized = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler());
62  this->nodeSet = (specialized ? specialized->getOriginalGenericNodeSet() : nodeClass->getNodeSet());
63  }
64  else
65  this->nodeSet = NULL;
66 
67  // Header content
68  this->headerLabel = new QLabel("", this);
69  headerLabel->setText(generateNodePopoverTextHeader());
70  headerLabel->setTextInteractionFlags(headerLabel->textInteractionFlags() | Qt::TextSelectableByMouse);
71 
72  // Text content
73  this->textLabel = new QLabel("", this);
74  textLabel->setText(generateNodePopoverText(isDark));
75  textLabel->setTextInteractionFlags(textLabel->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
76  textLabel->setOpenExternalLinks(false);
77  connect(textLabel, &QLabel::linkActivated, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
78 
79  QTextDocument *doc = textLabel->findChild<QTextDocument *>();
80  doc->setIndentWidth(20);
81 
82  // Model node
83  if (displayModelNode)
84  {
86  VuoNode *baseNode = (nodeClass->hasCompiler()? nodeClass->getCompiler()->newNode() :
87  nodeClass->newNode());
88  this->modelNode = new VuoRendererNode(baseNode, NULL);
89  modelNode->setAlwaysDisplayPortNames(true);
90  composition->addNode(modelNode->getBase());
91 
92  this->modelNodeView = new QGraphicsView(this);
93  modelNodeView->setBackgroundBrush(Qt::transparent);
94  modelNodeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
95  modelNodeView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
96  modelNodeView->setScene(composition);
97  modelNodeView->setRenderHints(QPainter::Antialiasing|QPainter::SmoothPixmapTransform);
98  modelNodeView->setFrameShape(QFrame::NoFrame);
99  modelNodeView->ensureVisible(modelNode->boundingRect());
100  modelNodeView->setEnabled(false);
101  }
102 
103  // Layout
104  layout = new QVBoxLayout(this);
105  layout->setContentsMargins(margin, margin, margin, margin);
106 
107  layout->addWidget(headerLabel, 0, Qt::AlignTop);
108  layout->setStretch(0, 0);
109 
110  if (displayModelNode)
111  {
112  layout->addWidget(modelNodeView, 0, Qt::AlignTop|Qt::AlignLeft);
113  layout->setStretch(1, 0);
114  }
115 
116  layout->addWidget(textLabel, 0, Qt::AlignTop);
117  layout->setStretch(2, 1);
118 
119  setLayout(layout);
120 
121  // Style
122  setStyle();
123 
124 
125  connect(editor, &VuoEditor::darkInterfaceToggled, this, &VuoNodePopover::updateColor);
126  updateColor(isDark);
127 }
128 
135 {
136  disconnect(static_cast<VuoEditor *>(qApp), &VuoEditor::darkInterfaceToggled, this, &VuoNodePopover::updateColor);
137 }
138 
143 {
144  // See `cleanup`.
145 }
146 
150 void VuoNodePopover::setStyle()
151 {
152  textLabel->setFont(VuoRendererFonts::getSharedFonts()->portPopoverFont());
153  textLabel->setMargin(0);
154  textLabel->setFixedWidth(defaultPopoverTextWidth);
155  textLabel->setWordWrap(true);
156 
157  headerLabel->setFont(VuoRendererFonts::getSharedFonts()->portPopoverFont());
158  headerLabel->setMargin(0);
159  headerLabel->setFixedWidth(defaultPopoverTextWidth);
160  headerLabel->setWordWrap(true);
161  headerLabel->setOpenExternalLinks(true);
162 
163  Qt::WindowFlags flags = windowFlags();
164  flags |= Qt::FramelessWindowHint;
165  flags |= Qt::WindowStaysOnTopHint;
166  flags |= Qt::ToolTip;
167  setWindowFlags(flags);
168 
169  // No border around embedded QGraphicsView
170  setBackgroundRole(QPalette::Base);
171  QPalette pal;
172  pal.setColor(QPalette::Base, QColor(Qt::transparent));
173  setPalette(pal);
174 
175  QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
176  textLabel->setSizePolicy(sizePolicy);
177  headerLabel->setSizePolicy(sizePolicy);
178 
179  if (displayModelNode)
180  {
181  modelNodeView->setSizePolicy(sizePolicy);
182 
183  //if (modelNodeView->scene()->width() > getTextWidth())
184  // setTextWidth(modelNodeView->scene()->width());
185  }
186 
187  setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
188 }
189 
193 QString VuoNodePopover::generateNodePopoverTextHeader()
194 {
195  QString nodeClassTitle = nodeClass->getDefaultTitle().c_str();
196 
197  QString nodeSetName = (nodeSet? nodeSet->getName().c_str() : "");
198  QString nodeClassBreadcrumbsBar = QString("<h3>")
199  .append((!nodeSet)?
200  nodeClassTitle :
201  QString("<a href=\"").
203  append("://").
204  append(nodeSetName).
205  append("\">").
207  append("</a>").
208  append(" › ").
209  append(nodeClassTitle))
210  .append("</h3>");
211 
212  return generateTextStyleString().append(nodeClassBreadcrumbsBar);
213 }
214 
218 QString VuoNodePopover::generateNodePopoverText(bool isDark)
219 {
220  QString nodeClassDescription = generateNodeClassDescription(isDark ? "#606060" : "#b0b0b0");
221 
222  // QLabel expects strings to be canonically composed,
223  // else it renders diacritics next to (instead of superimposed upon) their base glyphs.
224  nodeClassDescription = nodeClassDescription.normalized(QString::NormalizationForm_C);
225 
226  // Example compositions
227  vector<string> exampleCompositions = nodeClass->getExampleCompositionFileNames();
228  QString exampleCompositionLinks = "";
229 
230  foreach (string compositionFilePath, exampleCompositions)
231  {
232  string formattedCompositionName = "";
233  string compositionAsString = "";
234 
235  string genericNodeClassName;
236  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
237  genericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
238  else
239  genericNodeClassName = nodeClass->getClassName();
240 
241  exampleCompositionLinks.append("<li>")
242  .append("<a href=\"");
243 
244  // Case: compositionFilePath is of form "ExampleCompositionName.vuo"
245  // Example composition belongs to this node's own node set.
246  if (!QString(compositionFilePath.c_str()).startsWith(VuoEditor::vuoExampleCompositionScheme))
247  {
248  formattedCompositionName = VuoEditorComposition::formatCompositionFileNameForDisplay(compositionFilePath.c_str()).toUtf8().constData();
249 
250  if (nodeSet)
251  {
252  QString nodeSetName = nodeSet->getName().c_str();
253  compositionAsString = nodeSet->getExampleCompositionContents(compositionFilePath);
254 
255  exampleCompositionLinks
257  .append("://")
258  .append(nodeSetName)
259  .append("/");
260  }
261  }
262 
263  // Case: compositionFilePath is of form "vuo-example://nodeSet/ExampleCompositionName.vuo"
264  // Example composition belongs to some other node set.
265  else
266  {
267  QUrl exampleUrl(compositionFilePath.c_str());
268  string exampleNodeSetName = exampleUrl.host().toUtf8().constData();
269  string exampleFileName = VuoStringUtilities::substrAfter(exampleUrl.path().toUtf8().constData(), "/");
270  formattedCompositionName = VuoEditorComposition::formatCompositionFileNameForDisplay(exampleFileName.c_str()).toUtf8().constData();
271 
272  VuoNodeSet *exampleNodeSet = (compiler? compiler->getNodeSetForName(exampleNodeSetName) : NULL);
273  if (exampleNodeSet)
274  compositionAsString = exampleNodeSet->getExampleCompositionContents(exampleFileName);
275  }
276 
277  // Extract description and formatted name (if present) from composition header.
278  VuoCompositionMetadata metadata(compositionAsString);
279 
280  string customizedName = metadata.getCustomizedName();
281  if (! customizedName.empty())
282  formattedCompositionName = customizedName;
283 
284  string description = metadata.getDescription();
285  string filteredDescription = VuoEditor::removeVuoLinks(description);
286  QString compositionDescription = VuoStringUtilities::generateHtmlFromMarkdownLine(filteredDescription).c_str();
287 
288  exampleCompositionLinks
289  .append(compositionFilePath.c_str())
290  .append("?")
292  .append("=")
293  .append(genericNodeClassName.c_str())
294  .append("\">")
295  .append(formattedCompositionName.c_str())
296  .append("</a>");
297 
298  if (!compositionDescription.isEmpty())
299  exampleCompositionLinks.append(": ").append(compositionDescription);
300 
301  exampleCompositionLinks.append("</li>");
302  }
303 
304  if (exampleCompositions.size() > 0)
305  //: Appears in the node documentation panel at the bottom of the node library.
306  exampleCompositionLinks = "<BR><font size=+1><b>" + tr("Example composition(s)", "", exampleCompositions.size()) + ":</b></font><ul>" + exampleCompositionLinks + "</ul>";
307 
308  QString proNodeIndicator;
309 #if VUO_PRO
310  if (nodeClass->isPro())
311  proNodeIndicator = (nodeClass->hasCompiler() ? installedProNodeText : missingProNodeText);
312 #endif
313 
314  // Deprecated node indicator
315  //: Appears in the node documentation panel at the bottom of the node library.
316  QString deprecatedNodeIndicator = nodeClass->getDeprecated()
317  ? "<p><b>" + tr("This node is deprecated.") + "</b></p>"
318  : "";
319 
320  return generateTextStyleString()
321  .append(nodeClassDescription)
322  .append(exampleCompositionLinks)
323  .append(proNodeIndicator)
324  .append(deprecatedNodeIndicator);
325 }
326 
330 QString VuoNodePopover::generateNodeClassDescription(string smallTextColor)
331 {
332  string className = "";
333  string description = "";
334  string version = "";
335 
336  if (nodeClass->hasCompiler())
337  {
338  VuoCompilerSpecializedNodeClass *specialized = (nodeClass->hasCompiler() ?
339  dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()) :
340  NULL);
341  if (specialized)
342  {
343  className = specialized->getOriginalGenericNodeClassName();
344  description = specialized->getOriginalGenericNodeClassDescription();
345  }
346  else
347  {
348  className = nodeClass->getClassName();
349  description = nodeClass->getDescription();
350  }
351 
352  version = nodeClass->getVersion();
353  }
354  else
355  {
356  className = nodeClass->getClassName();
357  description = nodeClass->getDescription();
358  if (description.empty())
359  //: Appears in the node documentation panel at the bottom of the node library.
360  description = tr("This node is not installed. "
361  "You can either remove it from the composition or install it on your computer.").toUtf8().data();
362  }
363 
364  QString editLink = "";
365  QString enclosingFolderLink = "";
366  QString editLabel, sourcePath;
367  bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, editLabel, sourcePath);
368  bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
369 
370  if (nodeClassIsEditable)
371  editLink = QString("<a href=\"file://%1\">%2</a>").arg(sourcePath).arg(editLabel);
372 
373  if (nodeClassIsEditable || nodeClassIs3rdParty)
374  {
375  QString modulePath = (nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str());
376  if (!modulePath.isEmpty())
377  enclosingFolderLink = QString("<a href=\"file://%1\">%2</a>")
378  .arg(QFileInfo(modulePath).dir().absolutePath())
379  //: Appears in the node documentation panel at the bottom of the node library.
380  .arg(tr("Show in Finder"));
381  }
382 
383  QString formattedDescription = ((description.empty() || (description == VuoRendererComposition::deprecatedDefaultDescription))?
384  "" : VuoStringUtilities::generateHtmlFromMarkdown(description).c_str());
385 
386  QString formattedVersion = version.empty()
387  ? ""
388  //: Appears in the node documentation panel at the bottom of the node library.
389  : tr("Version %1").arg(QString::fromStdString(version));
390 
391  QString tooltipBody;
392  tooltipBody.append(QString("<font color=\"%2\">%1</font>")
393  .arg(formattedDescription)
394  .arg(smallTextColor.c_str()));
395 
396  tooltipBody.append("<p>");
397 
398  tooltipBody.append(QString("<font color=\"%2\">%1</font>")
399  .arg(className.c_str())
400  .arg(smallTextColor.c_str()));
401 
402  if (!formattedVersion.isEmpty())
403  tooltipBody.append(QString("<br><font color=\"%2\">%1</font>")
404  .arg(formattedVersion)
405  .arg(smallTextColor.c_str()));
406 
407  if (!editLink.isEmpty() || !enclosingFolderLink.isEmpty())
408  tooltipBody.append("<br>");
409 
410  if (!editLink.isEmpty())
411  tooltipBody.append(QString("<br><font color=\"%2\">%1</font>")
412  .arg(editLink)
413  .arg(smallTextColor.c_str()));
414 
415  if (!enclosingFolderLink.isEmpty())
416  tooltipBody.append(QString("<br><font color=\"%2\">%1</font>")
417  .arg(enclosingFolderLink)
418  .arg(smallTextColor.c_str()));
419 
420  tooltipBody.append("</p>");
421 
422  return tooltipBody;
423 }
424 
429 {
430  return textLabel->maximumWidth(); // maximumWidth() = minimumWidth() = fixedWidth
431 }
432 
438 {
439  if (textLabel->hasSelectedText())
440  return textLabel->selectedText();
441  else if (headerLabel->hasSelectedText())
442  return headerLabel->selectedText();
443  else
444  return QString("");
445 }
446 
452 {
453  int adjustedWidth = width-2*margin;
454  if (adjustedWidth != textLabel->width())
455  {
456  textLabel->setFixedWidth(adjustedWidth);
457  headerLabel->setFixedWidth(adjustedWidth);
458 
459  // The following setWordWrap() call is for some reason necessary
460  // to ensure that the popover does not end up with empty
461  // vertical space when resized. (Word wrap remains on regardless.)
462  textLabel->setWordWrap(true);
463  headerLabel->setWordWrap(true);
464  }
465 }
466 
471 {
472  dispatch_async(((VuoEditor *)qApp)->getDocumentationQueue(), ^{
473  string tmpSaveDir = "";
474 
475  // Extract documentation resources (e.g., images)
476  if (nodeSet)
477  {
478  try
479  {
480  string preexistingNodeSetResourceDir = ((VuoEditor *)qApp)->getResourceDirectoryForNodeSet(nodeSet->getName());
481  tmpSaveDir = (!preexistingNodeSetResourceDir.empty()? preexistingNodeSetResourceDir : VuoFileUtilities::makeTmpDir(nodeSet->getName()));
482 
483  if (tmpSaveDir != preexistingNodeSetResourceDir)
484  nodeSet->extractDocumentationResources(tmpSaveDir);
485  }
486  catch (VuoException &e)
487  {
488  VUserLog("%s", e.what());
489  }
490  }
491 
492  emit popoverDisplayRequested(static_cast<QWidget *>(this), tmpSaveDir.c_str());
493  });
494 }
495 
500 {
501  QRegExp embeddedImageHtml("<img\\s+", Qt::CaseInsensitive);
502  return textLabel->text().contains(embeddedImageHtml);
503 }
504 
509 {
510  return nodeClass;
511 }
512 
517 {
518  return modelNode;
519 }
520 
525 {
526  VuoEditor *editor = (VuoEditor *)qApp;
527  bool isDark = editor->isInterfaceDark();
528  return VuoEditor::generateHtmlDocumentationStyles(false, isDark) + VUO_QSTRINGIFY(
529  <style>
530  * {
531  font-size: 13px;
532  color: %1;
533  }
534  span {
535  font-size: 11px;
536  color: %1;
537  }
538  a {
539  color: %2;
540  }
541  p {
542  color: %3;
543  }
544 
545  // Reduce space between nested lists.
546  // https://b33p.net/kosada/node/11442#comment-54820
547  ul {
548  margin-bottom: 0px;
549  }
550  </style>)
551  .arg(isDark ? "#a0a0a0" : "#606060")
552  .arg(isDark ? "#6882be" : "#74acec")
553  .arg(isDark ? "#909090" : "#606060")
554  ;
555 }
556 
560 void VuoNodePopover::updateColor(bool isDark)
561 {
562  headerLabel->setText(generateNodePopoverTextHeader());
563  textLabel->setText(generateNodePopoverText(isDark));
564 
565  QPalette p;
566  QColor bulletColor = isDark ? "#a0a0a0" : "#606060";
567  p.setColor(QPalette::Normal, QPalette::Text, bulletColor);
568  p.setColor(QPalette::Inactive, QPalette::Text, bulletColor);
569  textLabel->setPalette(p);
570 }
571 
575 void VuoNodePopover::mouseMoveEvent(QMouseEvent *event)
576 {
577  // Detect whether we should initiate a drag.
578  if (event->buttons() & Qt::LeftButton)
579  {
580  // Display the model node during the drag.
581  QRectF r = modelNode->boundingRect().toRect();
582  qreal dpRatio = devicePixelRatio();
583  QPixmap pixmap(dpRatio*r.width(), dpRatio*r.height());
584  pixmap.setDevicePixelRatio(dpRatio);
585  pixmap.fill(Qt::transparent);
586 
587  QPainter painter(&pixmap);
588  painter.setRenderHint(QPainter::Antialiasing, true);
589  painter.setRenderHint(QPainter::HighQualityAntialiasing, true);
590  painter.setRenderHint(QPainter::TextAntialiasing, true);
591  painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
592 
593  painter.translate(-r.topLeft());
594  modelNode->paint(&painter, NULL, NULL);
595  painter.end();
596 
597  // Execute the drag.
598  QDrag *drag = new QDrag(this);
599  drag->setPixmap(pixmap);
600  drag->setHotSpot(event->pos()-(modelNodeView->pos()
601  +QPoint(38,0) // magic number that simulates dragging the node
602  )); // from the cursor position for most nodes
603 
604  // Include the node class name and the hotspot in the dragged data.
605  QMimeData *mimeData = new QMimeData();
606  const QString dropText = QString(nodeClass->getClassName().c_str())
607  .append(QLatin1Char('\n'))
608  .append(QString::number(drag->hotSpot().x()))
609  .append(QLatin1Char('\n'))
610  .append(QString::number(drag->hotSpot().y()));
611  mimeData->setData("text/plain", dropText.toUtf8().constData());
612  drag->setMimeData(mimeData);
613  drag->exec(Qt::CopyAction);
614  }
615 }
616 
621 {
623  if (!targetWindow || !targetWindow->getComposition())
624  return;
625 
626  QPointF startPos = targetWindow->getFittedScenePos(targetWindow->getCursorScenePos()-
628 
629  QList<QGraphicsItem *> newNodes;
630  VuoRendererNode *newNode = targetWindow->getComposition()->createNode(nodeClass->getClassName().c_str(), "", startPos.x(), startPos.y());
631  newNodes.append((QGraphicsItem *)newNode);
632 
633  targetWindow->componentsAdded(newNodes, targetWindow->getComposition());
634 }
635 
639 void VuoNodePopover::contextMenuEvent(QContextMenuEvent *event)
640 {
641  QMenu *contextMenu = new QMenu(this);
642  contextMenu->setSeparatorsCollapsible(false);
644  contextMenu->exec(event->globalPos());
645 }