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