Vuo 2.4.4
Loading...
Searching...
No Matches
VuoEditorComposition.cc
Go to the documentation of this file.
1
11
13#include "VuoCompiler.hh"
14#include "VuoCompilerIssue.hh"
15#include "VuoComposition.hh"
17#include "VuoEditorWindow.hh"
18#include "VuoErrorDialog.hh"
19#include "VuoException.hh"
20#include "VuoFileType.h"
21#include "VuoRendererComment.hh"
22#include "VuoRendererFonts.hh"
26#include "VuoCompilerCable.hh"
27#include "VuoCompilerComment.hh"
30#include "VuoCompilerDriver.hh"
32#include "VuoCompilerGraph.hh"
35#include "VuoCompilerNode.hh"
42#include "VuoCompilerType.hh"
43#include "VuoGenericType.hh"
45#include "VuoComment.hh"
46#include "VuoEditor.hh"
47#include "VuoErrorMark.hh"
48#include "VuoErrorPopover.hh"
49#include "VuoModuleManager.hh"
50#include "VuoNodeClass.hh"
51#include "VuoNodeSet.hh"
52#include "VuoPortPopover.hh"
53#include "VuoProtocol.hh"
54#include "VuoStringUtilities.hh"
55#include "VuoInputEditorIcon.hh"
58#include "VuoEditorUtilities.hh"
60
61#ifdef __APPLE__
62#include <ApplicationServices/ApplicationServices.h>
63#include <objc/objc-runtime.h>
64#endif
65
66const qreal VuoEditorComposition::nodeMoveRate = 15; // VuoRendererComposition::minorGridLineSpacing;
67const qreal VuoEditorComposition::nodeMoveRateMultiplier = 4; // VuoRendererComposition::majorGridLineSpacing / VuoRendererComposition::minorGridLineSpacing
69const qreal VuoEditorComposition::showEventsModeUpdateInterval = 1000/20.; // interval, in ms, after which to update component transparency levels and animations while in 'Show Events' mode
70const int VuoEditorComposition::initialChangeNodeSuggestionCount = 10; // The initial number of suggestions to list in the "Change (Node) To" context menu
71
76 VuoRendererComposition(baseComposition, false, true)
77{
78#if VUO_PRO
79 VuoEditorComposition_Pro();
80#endif
81
82 this->window = window;
83 compiler = NULL;
84 inputEditorManager = NULL;
85 activeProtocol = NULL;
86 runner = NULL;
87 runningComposition = NULL;
88 runningCompositionActiveDriver = NULL;
89 runningCompositionLibraries = NULL;
90 stopRequested = false;
91 duplicateOnNextMouseMove = false;
92 duplicationDragInProgress = false;
93 duplicationCancelled = false;
94 cursorPosBeforeDuplicationDragMove = QPointF(0,0);
95 cableInProgress = NULL;
96 cableInProgressWasNew = false;
97 cableInProgressShouldBeWireless = false;
98 portWithDragInitiated = NULL;
99 cableWithYankInitiated = NULL;
100 menuSelectionInProgress = false;
101 previousNearbyItem = NULL;
102 dragStickinessDisabled = false;
103 ignoreApplicationStateChangeEvents = false;
104 popoverEventsEnabled = true;
105 runCompositionQueue = dispatch_queue_create("org.vuo.editor.run", NULL);
106 activePortPopoversQueue = dispatch_queue_create("org.vuo.editor.popovers", NULL);
107 errorMark = NULL;
108 errorMarkingUpdatesEnabled = true;
109 triggerPortToRefire = "";
110
111 contextMenuDeleteSelected = new QAction(NULL);
112 contextMenuHideSelectedCables = new QAction(NULL);
113 contextMenuRenameSelected = new QAction(NULL);
114 contextMenuRefactorSelected = new QAction(NULL);
115 contextMenuPublishPort = new QAction(NULL);
116 contextMenuDeleteCables = new QAction(NULL);
117 contextMenuHideCables = new QAction(NULL);
118 contextMenuUnhideCables = new QAction(NULL);
119 contextMenuFireEvent = new QAction(NULL);
120 contextMenuAddInputPort = new QAction(NULL);
121 contextMenuRemoveInputPort = new QAction(NULL);
122 contextMenuSetPortConstant = new QAction(NULL);
123 contextMenuEditSelectedComments = new QAction(NULL);
124
125 contextMenuChangeNode = NULL;
126 contextMenuSpecializeGenericType = NULL;
127
128 contextMenuFireEvent->setText(tr("Fire Event"));
129 contextMenuHideSelectedCables->setText(tr("Hide"));
130 contextMenuRenameSelected->setText(tr("Rename…"));
131 contextMenuRefactorSelected->setText(tr("Package as Subcomposition"));
132 contextMenuAddInputPort->setText(tr("Add Input Port"));
133 contextMenuRemoveInputPort->setText(tr("Remove Input Port"));
134 contextMenuSetPortConstant->setText(tr("Edit Value…"));
135 contextMenuEditSelectedComments->setText(tr("Edit…"));
136
137 connect(contextMenuDeleteSelected, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
138 connect(contextMenuHideSelectedCables, &QAction::triggered, this, &VuoEditorComposition::selectedInternalCablesHidden);
139 connect(contextMenuRenameSelected, &QAction::triggered, this, &VuoEditorComposition::renameSelectedNodes);
140 connect(contextMenuRefactorSelected, &QAction::triggered, this, &VuoEditorComposition::refactorRequested);
141 connect(contextMenuPublishPort, &QAction::triggered, this, &VuoEditorComposition::togglePortPublicationStatus);
142 connect(contextMenuDeleteCables, &QAction::triggered, this, &VuoEditorComposition::deleteConnectedCables);
143 connect(contextMenuHideCables, &QAction::triggered, this, &VuoEditorComposition::hideConnectedCables);
144 connect(contextMenuUnhideCables, &QAction::triggered, this, &VuoEditorComposition::unhideConnectedCables);
145 connect(contextMenuFireEvent, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::fireTriggerPortEvent));
146 connect(contextMenuAddInputPort, &QAction::triggered, this, &VuoEditorComposition::addInputPort);
147 connect(contextMenuRemoveInputPort, &QAction::triggered, this, &VuoEditorComposition::removeInputPort);
148 connect(contextMenuEditSelectedComments, &QAction::triggered, this, &VuoEditorComposition::editSelectedComments);
149
150 // Use a queued connection to open input editors in order to avoid bug where invoking a
151 // QColorDialog by context menu prevents subsequent interaction with the editor window
152 // even after the color dialog has been closed.
153 connect(contextMenuSetPortConstant, &QAction::triggered, this, &VuoEditorComposition::setPortConstant, Qt::QueuedConnection);
154
155 {
156 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
157 // For now, this sets up the actions for a menu, rather than setting up the menu itself.
158
159 auto addThrottlingAction = [=](QString label, VuoPortClass::EventThrottling throttling) {
160 QAction *action = new QAction(label, this);
161 connect(action, &QAction::triggered, [=](){
162 emit triggerThrottlingUpdated(action->data().value<VuoRendererPort *>()->getBase(), throttling);
163 });
164 contextMenuThrottlingActions.append(action);
165 };
166 addThrottlingAction(tr("Enqueue Events"), VuoPortClass::EventThrottling_Enqueue);
167 addThrottlingAction(tr("Drop Events"), VuoPortClass::EventThrottling_Drop);
168 }
169
170 {
171 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
172 // For now, this sets up the actions for a menu, rather than setting up the menu itself.
173
174 auto addTintAction = [=](QString label, VuoNode::TintColor tint) {
175 QAction *action = new QAction(label, this);
176 connect(action, &QAction::triggered, [=](){
177 static_cast<VuoEditorWindow *>(window)->tintSelectedItems(tint);
178 });
179
180 // Add a color swatch to the menu item.
181 {
182 QColor fill(0,0,0,0);
183 // For TintNone, draw a transparent icon, so that menu item's text indent is consistent with the other items.
184 if (tint != VuoNode::TintNone)
185 {
186 VuoRendererColors colors(tint);
187 fill = colors.nodeFill();
188 }
189
190 QIcon *icon = VuoInputEditorIcon::renderIcon(^(QPainter &p){
191 p.setPen(Qt::NoPen);
192 p.setBrush(fill);
193 // Match distance between text baseline and ascender.
194 p.drawEllipse(3, 3, 10, 10);
195 });
196 action->setIcon(*icon);
197 delete icon;
198 }
199
200 contextMenuTintActions.append(action);
201 };
202 addTintAction(tr("Yellow"), VuoNode::TintYellow);
203 addTintAction(tr("Tangerine"), VuoNode::TintTangerine);
204 addTintAction(tr("Orange"), VuoNode::TintOrange);
205 addTintAction(tr("Magenta"), VuoNode::TintMagenta);
206 addTintAction(tr("Violet"), VuoNode::TintViolet);
207 addTintAction(tr("Blue"), VuoNode::TintBlue);
208 addTintAction(tr("Cyan"), VuoNode::TintCyan);
209 addTintAction(tr("Green"), VuoNode::TintGreen);
210 addTintAction(tr("Lime"), VuoNode::TintLime);
211 addTintAction(tr("None"), VuoNode::TintNone);
212 }
213
214 // 'Show Events' mode rendering setup
215 this->refreshComponentAlphaLevelTimer = new QTimer(this);
216 this->refreshComponentAlphaLevelTimer->setObjectName("VuoEditorComposition::refreshComponentAlphaLevelTimer");
217 refreshComponentAlphaLevelTimer->setInterval(showEventsModeUpdateInterval);
218 connect(refreshComponentAlphaLevelTimer, &QTimer::timeout, this, &VuoEditorComposition::updateGeometryForAllComponents);
219 setShowEventsMode(false);
220
221 connect(signaler, &VuoRendererSignaler::nodePopoverRequested, this, &VuoEditorComposition::enablePopoverForNode);
222 connect(signaler, &VuoRendererSignaler::nodesMoved, this, &VuoEditorComposition::moveNodesBy);
223 connect(signaler, &VuoRendererSignaler::commentsMoved, this, &VuoEditorComposition::moveCommentsBy);
224 connect(signaler, &VuoRendererSignaler::commentResized, this, &VuoEditorComposition::resizeCommentBy);
231 connect(signaler, &VuoRendererSignaler::dragStickinessDisableRequested, this, &VuoEditorComposition::setDisableDragStickiness);
232 connect(signaler, &VuoRendererSignaler::openUrl, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
233
234 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::activeApplicationStateChanged, this, &VuoEditorComposition::updatePopoversForApplicationStateChange, Qt::QueuedConnection);
235 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::focusChanged, this, &VuoEditorComposition::updatePopoversForActiveWindowChange, Qt::QueuedConnection);
236 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationWillHide, this, [=]{
237 setPopoversHideOnDeactivate(true);
238 });
239 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationDidUnhide, this, [=]{
240 setPopoversHideOnDeactivate(false);
241 });
242
243 identifierCache = new VuoNodeAndPortIdentifierCache;
244 identifierCache->addCompositionComponentsToCache(getBase());
245}
246
251{
252 this->compiler = compiler;
253}
254
259{
260 return compiler;
261}
262
267{
268 this->moduleManager = moduleManager;
269 moduleManager->setComposition(this);
270}
271
276{
277 return moduleManager;
278}
279
286{
287 this->inputEditorManager = inputEditorManager;
288}
289
296{
297 return this->inputEditorManager;
298}
299
303VuoRendererNode * VuoEditorComposition::createNode(QString nodeClassName, string title, double x, double y)
304{
305 if (compiler)
306 {
307 VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
308 if (nodeClass)
309 {
310 VuoNode *node = createBaseNode(nodeClass, nullptr, title, x, y);
311 if (node)
312 {
314 setCustomConstantsForNewNode(rn);
315 return rn;
316 }
317 }
318 }
319 return NULL;
320}
321
328VuoNode * VuoEditorComposition::createBaseNode(VuoCompilerNodeClass *nodeClass, VuoNode *modelNode, string title, double x, double y)
329{
330 // If adding the node would create recursion (subcomposition contains itself), create a node without a compiler detail.
331 __block bool isAllowed = true;
332 if (nodeClass->isSubcomposition())
333 {
334 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
335 string compositionModuleKey = VuoCompiler::getModuleKeyForPath(compositionPath);
336 bool nodeIsThisComposition = (compositionModuleKey == nodeClass->getBase()->getClassName());
337
338 set<string> dependencies = nodeClass->getDependencies();
339 auto iter = std::find_if(dependencies.begin(), dependencies.end(), [=](const string &d){ return d == compositionModuleKey; });
340 bool nodeContainsThisComposition = (iter != dependencies.end());
341
342 isAllowed = ! (nodeIsThisComposition || nodeContainsThisComposition);
343 });
344 }
345
346 VuoNode *node;
347 if (isAllowed)
348 {
349 node = (modelNode ?
350 compiler->createNode(nodeClass, modelNode) :
351 compiler->createNode(nodeClass, title, x, y));
352 }
353 else
354 {
355 node = createNodeWithMissingImplementation(nodeClass->getBase(), modelNode, title, x, y);
356 node->setForbidden(true);
357 }
358 return node;
359}
360
367VuoNode * VuoEditorComposition::createNodeWithMissingImplementation(VuoNodeClass *modelNodeClass, VuoNode *modelNode, string title, double x, double y)
368{
369 vector<string> inputPortClassNames;
370 vector<string> outputPortClassNames;
371 foreach (VuoPortClass *portClass, modelNodeClass->getInputPortClasses())
372 {
373 if (portClass == modelNodeClass->getRefreshPortClass())
374 continue;
375 inputPortClassNames.push_back(portClass->getName());
376 }
377 foreach (VuoPortClass *portClass, modelNodeClass->getOutputPortClasses())
378 outputPortClassNames.push_back(portClass->getName());
379
380 VuoNodeClass *dummyNodeClass = new VuoNodeClass(modelNodeClass->getClassName(), inputPortClassNames, outputPortClassNames);
381 return (modelNode ?
382 dummyNodeClass->newNode(modelNode) :
383 dummyNodeClass->newNode(! title.empty() ? title : modelNodeClass->getDefaultTitle(), x, y));
384}
385
391void VuoEditorComposition::setCustomConstantsForNewNode(VuoRendererNode *newNode)
392{
393 // vuo.time.make: Set the 'year' input to the current year.
394 if (newNode->getBase()->getNodeClass()->getClassName() == "vuo.time.make")
395 {
396 VuoPort *yearPort = newNode->getBase()->getInputPortWithName("year");
397 if (yearPort)
398 {
399 QString currentYear = QString::number(QDateTime::currentDateTime().date().year());
400 yearPort->getRenderer()->setConstant(VuoText_getString(currentYear.toUtf8().constData()));
401 }
402 }
403}
404
408void VuoEditorComposition::addNode(VuoNode *n, bool nodeShouldBeRendered, bool nodeShouldBeGivenUniqueIdentifier)
409{
410 VuoRendererComposition::addNode(n, nodeShouldBeRendered, nodeShouldBeGivenUniqueIdentifier);
411 identifierCache->addNodeToCache(n);
412}
413
420{
421 if (resetState)
422 {
423 disablePortPopovers(rn);
424
425 if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == rn))
427 }
428
430}
431
446{
447 // Inventory the port constants and connected input cables associated with the old node, to be re-associated with the new node.
448 map<VuoCable *, VuoPort *> cablesToTransferFromPort;
449 map<VuoCable *, VuoPort *> cablesToTransferToPort;
450 set<VuoCable *> cablesToRemove;
451 getBase()->getCompiler()->getChangesToReplaceNode(oldNode->getBase(), newNode, cablesToTransferFromPort, cablesToTransferToPort, cablesToRemove);
452
453 // Also inventory any typecasts collapsed onto the old node, to be attached to the new node instead.
454 vector<VuoRendererInputDrawer *> attachedDrawers;
455 vector<VuoRendererNode *> collapsedTypecasts;
456 vector<VuoPort *> oldInputPorts = oldNode->getBase()->getInputPorts();
457 for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
458 {
459 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
460 if (typecastPort)
461 {
462 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
463 collapsedTypecasts.push_back(typecastNode);
464 }
465
466 // If the original node is currently being rendered as collapsed typecast, uncollapse it.
467 if (oldNode->getProxyCollapsedTypecast())
468 uncollapseTypecastNode(oldNode);
469
470 // Uncollapse typecasts attached to the original node.
471 for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
473 }
474
475 // Inventory any attachments to the old node, to make sure none are stranded in the replacement.
476 for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
477 {
478 VuoRendererPort *inputPortRenderer = (*inputPort)->getRenderer();
479 VuoRendererInputDrawer *attachedDrawer = inputPortRenderer->getAttachedInputDrawer();
480 if (attachedDrawer)
481 attachedDrawers.push_back(attachedDrawer);
482 }
483
484 // Perform the node replacement.
485 replaceNode(oldNode, newNode);
486
487 // Restore connected cables.
488 for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferFromPort.begin(); i != cablesToTransferFromPort.end(); ++i)
489 i->first->getRenderer()->setFrom(newNode, i->second);
490 for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferToPort.begin(); i != cablesToTransferToPort.end(); ++i)
491 i->first->getRenderer()->setTo(newNode, i->second);
492 foreach (VuoCable *cable, cablesToRemove)
493 removeCable(cable->getRenderer());
494
495 // Restore constant values.
496 for (VuoPort *oldInputPort : oldNode->getBase()->getInputPorts())
497 {
498 VuoPort *newInputPort = newNode->getInputPortWithName(oldInputPort->getClass()->getName());
499 if (! newInputPort)
500 continue;
501
502 if (! oldInputPort->getRenderer()->carriesData() || ! newInputPort->getRenderer()->carriesData())
503 continue;
504
505 if (oldNode->getBase()->hasCompiler() && newNode->hasCompiler())
506 {
507 VuoType *oldDataType = static_cast<VuoCompilerPort *>(oldInputPort->getCompiler())->getDataVuoType();
508 VuoType *newDataType = static_cast<VuoCompilerPort *>(newInputPort->getCompiler())->getDataVuoType();
509 if (! (oldDataType == newDataType && oldDataType && ! dynamic_cast<VuoGenericType *>(oldDataType)) )
510 continue;
511 }
512
513 string oldConstantValue;
514 if (oldNode->getBase()->hasCompiler())
515 oldConstantValue = oldInputPort->getRenderer()->getConstantAsString();
516 else
517 oldConstantValue = oldInputPort->getRawInitialValue();
518
519 if (newNode->hasCompiler())
520 updatePortConstant(static_cast<VuoCompilerPort *>(newInputPort->getCompiler()), oldConstantValue, false);
521 else
522 newInputPort->setRawInitialValue(oldConstantValue);
523 }
524
525 // Remove any stranded drawers and their incoming cables.
526 // @todo https://b33p.net/kosada/node/16441 and https://b33p.net/kosada/node/16441 :
527 // Decide how to handle stranded attachment deletion and insertion properly, including
528 // updates to the running composition and all types of incoming connections to the attachments.
529 // For now just make sure not to leave behind a stranded drawer or any of its incoming cables.
530 foreach (VuoRendererInputDrawer *drawer, attachedDrawers)
531 {
532 if (!drawer->getRenderedHostPort())
533 {
534 foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
535 {
536 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(drawerPort->getBase()->getRenderer());
537 if (typecastPort)
538 uncollapseTypecastNode(typecastPort);
539
540 foreach (VuoCable *cable, drawerPort->getBase()->getConnectedCables())
541 removeCable(cable->getRenderer());
542 }
543
544 removeNode(drawer);
545 }
546 }
547
548 // Restore connected typecasts.
549 for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
551
552 // Re-collapse the updated node, if applicable.
554}
555
561{
562 disablePortPopovers(oldNode);
563
564 if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == oldNode))
566
567 string graphvizIdentifier;
568 string graphvizDeclaration;
569 if (oldNode->getBase()->hasCompiler())
570 {
571 graphvizIdentifier = oldNode->getBase()->getCompiler()->getGraphvizIdentifier();
572 graphvizDeclaration = oldNode->getBase()->getCompiler()->getGraphvizDeclaration();
573 }
574 else
575 {
576 graphvizIdentifier = oldNode->getBase()->getRawGraphvizIdentifier();
577 graphvizDeclaration = oldNode->getBase()->getRawGraphvizDeclaration();
578 }
579
580 newNode->setRawGraphvizDeclaration(graphvizDeclaration);
581 if (newNode->hasCompiler())
582 newNode->getCompiler()->setGraphvizIdentifier(graphvizIdentifier);
583
584 removeNode(oldNode);
585 addNode(newNode, true, false);
586
587 identifierCache->addNodeToCache(newNode);
588}
589
593void VuoEditorComposition::removeCable(VuoRendererCable *rc, bool emitHiddenCableNotification)
594{
595 bool cableHidden = rc->getBase()->getCompiler()->getHidden();
597
598 if (cableHidden && emitHiddenCableNotification)
600}
601
605void VuoEditorComposition::addCable(VuoCable *cable, bool emitHiddenCableNotification)
606{
607 bool cableHidden = cable->getCompiler()->getHidden();
609
610 if (cableHidden && emitHiddenCableNotification)
612}
613
623{
624 return VuoRendererComposition::createAndConnectMakeListNode(toNode, toPort, compiler, rendererCable);
625}
626
636 set<VuoRendererNode *> &createdNodes,
637 set<VuoRendererCable *> &createdCables)
638{
639 return VuoRendererComposition::createAndConnectDictionaryAttachmentsForNode(node, compiler, createdNodes, createdCables);
640}
641
648QList<QGraphicsItem *> VuoEditorComposition::createAndConnectInputAttachments(VuoRendererNode *node, bool createButDoNotAdd)
649{
650 QList<QGraphicsItem *> addedComponents = VuoRendererComposition::createAndConnectInputAttachments(node, compiler, createButDoNotAdd);
651 foreach (QGraphicsItem *component, addedComponents)
652 {
653 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(component);
654 if (rn && !createButDoNotAdd)
655 identifierCache->addNodeToCache(rn->getBase());
656 }
657
658 return addedComponents;
659}
660
668set<QGraphicsItem *> VuoEditorComposition::getDependentAttachmentsForNode(VuoRendererNode *rn, bool includeCoattachments)
669{
670 set<QGraphicsItem *> dependentAttachments;
671
672 // Get upstream attachments.
673 vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
674 for(unsigned int i = 0; i < inputPorts.size(); ++i)
675 {
676 set<VuoRendererInputAttachment *> portUpstreamAttachments = inputPorts[i]->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
677 dependentAttachments.insert(portUpstreamAttachments.begin(), portUpstreamAttachments.end());
678 }
679
680 // Get co-attachments.
681 VuoRendererInputAttachment *nodeAsAttachment = dynamic_cast<VuoRendererInputAttachment *>(rn);
682 if (nodeAsAttachment && includeCoattachments)
683 {
684 foreach (VuoNode *coattachment, nodeAsAttachment->getCoattachments())
685 dependentAttachments.insert(coattachment->getRenderer());
686 }
687
688 return dependentAttachments;
689}
690
691
697{
698 identifierCache->clearCache();
699
700 // Record the IDs of the currently selected components so that the selection status
701 // of the corresponding items may be restored after the composition is reset.
702 set<string> selectedNodeIDs;
703 foreach (VuoRendererNode *rn, getSelectedNodes())
704 {
705 if (rn->getBase()->hasCompiler())
706 selectedNodeIDs.insert(rn->getBase()->getCompiler()->getGraphvizIdentifier());
707 }
708
709 set<string> selectedCommentIDs;
711 {
712 if (rc->getBase()->hasCompiler())
713 selectedCommentIDs.insert(rc->getBase()->getCompiler()->getGraphvizIdentifier());
714 }
715
716 set<string> selectedCableIDs;
717 foreach (VuoRendererCable *rc, getSelectedCables(true))
718 {
719 if (rc->getBase()->hasCompiler())
720 selectedCableIDs.insert(rc->getBase()->getCompiler()->getGraphvizDeclaration());
721 }
722
723 modify();
724
725 // Restore the selection status of pre-existing components.
726 foreach (QGraphicsItem *item, items())
727 {
728 if (dynamic_cast<VuoRendererNode *>(item))
729 {
730 string currentNodeID = (dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler()?
731 dynamic_cast<VuoRendererNode *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
732 "");
733 if (!currentNodeID.empty() && (selectedNodeIDs.find(currentNodeID) != selectedNodeIDs.end()))
734 item->setSelected(true);
735 }
736
737 if (dynamic_cast<VuoRendererComment *>(item))
738 {
739 string currentCommentID = (dynamic_cast<VuoRendererComment *>(item)->getBase()->hasCompiler()?
740 dynamic_cast<VuoRendererComment *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
741 "");
742 if (!currentCommentID.empty() && (selectedCommentIDs.find(currentCommentID) != selectedCommentIDs.end()))
743 item->setSelected(true);
744 }
745
746 else if (dynamic_cast<VuoRendererCable *>(item))
747 {
748 string currentCableID = (dynamic_cast<VuoRendererCable *>(item)->getBase()->hasCompiler()?
749 dynamic_cast<VuoRendererCable *>(item)->getBase()->getCompiler()->getGraphvizDeclaration() :
750 "");
751 if (!currentCableID.empty() && (selectedCableIDs.find(currentCableID) != selectedCableIDs.end()))
752 item->setSelected(true);
753 }
754 }
755
756 // Re-establish mappings between the stored composition components and the running
757 // composition components, if applicable.
758 identifierCache->addCompositionComponentsToCache(getBase());
759
760 // Close popovers for ports no longer present in the composition.
762}
763
769{
770 string portName = port->getBase()->getClass()->getName();
771 VuoRendererNode *parentNode = port->getRenderedParentNode();
772
773 // A changed math expression input to a "Calculate" node will require changes to the node's
774 // upstream input lists of variable names and values.
775 if ((portName == "expression") &&
776 VuoStringUtilities::beginsWith(parentNode->getBase()->getNodeClass()->getClassName(), "vuo.math.calculate"))
777 return true;
778
779 return false;
780}
781
785void VuoEditorComposition::performStructuralChangesAfterValueChangeAtPort(VuoEditorWindow *editorWindow, QUndoStack *undoStack, VuoRendererPort *port, string originalEditingSessionValue, string finalEditingSessionValue)
786{
788 return;
789
790 VuoRendererNode *parentNode = port->getRenderedParentNode();
791
792 // Only current possibility: modifications to "Calculate" node's 'expression' input
793 string nodeClassName = parentNode->getBase()->getNodeClass()->getClassName();
794 vector<string> inputVariablesBeforeEditing = extractInputVariableListFromExpressionsConstant(originalEditingSessionValue, nodeClassName);
795 vector<string> inputVariablesAfterEditing = extractInputVariableListFromExpressionsConstant(finalEditingSessionValue, nodeClassName);
796
797 // Don't make any structural changes if the variables in the input expression remain
798 // the same, even if the expression itself has changed.
799 if (inputVariablesBeforeEditing != inputVariablesAfterEditing)
800 {
801 VuoPort *valuesPort = port->getRenderedParentNode()->getBase()->getInputPortWithName("values");
802
803 set<VuoRendererInputAttachment *> attachments = valuesPort->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
804
805 QList<QGraphicsItem *> attachmentsToRemove;
806
807 VuoRendererReadOnlyDictionary *oldDictionary = nullptr;
808 VuoRendererValueListForReadOnlyDictionary *oldValueList = nullptr;
809 VuoRendererKeyListForReadOnlyDictionary *oldKeyList = nullptr;
810 foreach (VuoRendererInputAttachment *attachment, attachments)
811 {
812 attachmentsToRemove.append(attachment);
813
814 if (dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment))
815 oldDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment);
816
817 else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment))
818 oldValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment);
819
820 else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment))
821 oldKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment);
822 }
823
824 if (oldValueList && oldDictionary && oldKeyList)
825 {
826 set<VuoRendererNode *> nodesToAdd;
827 set<VuoRendererCable *> cablesToAdd;
828 createAndConnectDictionaryAttachmentsForNode(parentNode->getBase(), nodesToAdd, cablesToAdd);
829
830 VuoRendererReadOnlyDictionary *newDictionary = nullptr;
831 VuoRendererValueListForReadOnlyDictionary *newValueList = nullptr;
832 VuoRendererKeyListForReadOnlyDictionary *newKeyList = nullptr;
833 foreach (VuoRendererNode *node, nodesToAdd)
834 {
835 if (dynamic_cast<VuoRendererReadOnlyDictionary *>(node))
836 newDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(node);
837
838 else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node))
839 newValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node);
840
841 else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node))
842 newKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node);
843 }
844
845 undoStack->push(new VuoCommandReplaceNode(oldValueList, newValueList, editorWindow, "Set Port Constant", false, false));
846 undoStack->push(new VuoCommandReplaceNode(oldKeyList, newKeyList, editorWindow, "Set Port Constant", false, true));
847 undoStack->push(new VuoCommandReplaceNode(oldDictionary, newDictionary, editorWindow, "Set Port Constant", false, true));
848
849 foreach (VuoRendererCable *cable, cablesToAdd)
850 {
851 cable->setFrom(nullptr, nullptr);
852 cable->setTo(nullptr, nullptr);
853 }
854 }
855 }
856}
857
865
870{
871 if (commandDescription.empty())
872 {
873 if (getContextMenuDeleteSelectedAction()->text().contains("Reset"))
874 commandDescription = "Reset";
875 else
876 commandDescription = "Delete";
877 }
878
879 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
880 emit componentsRemoved(selectedCompositionComponents, commandDescription);
881}
882
886void VuoEditorComposition::deleteSelectedNodes(string commandDescription)
887{
888 if (commandDescription.empty())
889 commandDescription = "Delete";
890
891 QList<QGraphicsItem *> selectedNodes;
892 foreach (VuoRendererNode *node, getSelectedNodes())
893 selectedNodes.append(node);
894
895 emit componentsRemoved(selectedNodes, commandDescription);
896}
897
902{
903 identifierCache->clearCache();
904
906
907 foreach (VuoCable *cable, getBase()->getCables())
908 removeCable(cable->getRenderer(), false);
909
910 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
912
913 foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedOutputPorts())
914 removePublishedPort(publishedPort, false);
915
916 foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
917 removePublishedPort(publishedPort, true);
918
919 foreach (VuoNode *node, getBase()->getNodes())
920 removeNode(node->getRenderer(), false);
921
923
924 foreach (VuoComment *comment, getBase()->getComments())
925 removeComment(comment->getRenderer());
926
928}
929
933void VuoEditorComposition::insertNode()
934{
935 QAction *sender = (QAction *)QObject::sender();
936 QPair<QPointF, QString> pair = sender->data().value<QPair<QPointF, QString> >();
937
938 QList<QGraphicsItem *> newNodes;
939 VuoRendererNode *newNode = createNode(pair.second, "",
940 pair.first.x(),
941 pair.first.y());
942
943 if (newNode)
944 {
945 newNodes.append(newNode);
946 emit componentsAdded(newNodes, this);
947 }
948}
949
953void VuoEditorComposition::insertComment()
954{
955 QAction *sender = (QAction *)QObject::sender();
956 QPointF scenePos = sender->data().value<QPointF>();
957
958 emit commentInsertionRequested(scenePos);
959}
960
964void VuoEditorComposition::insertSubcomposition()
965{
966 QAction *sender = (QAction *)QObject::sender();
967 QPointF scenePos = sender->data().value<QPointF>();
968
970}
971
977{
978 QAction *sender = (QAction *)QObject::sender();
979 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
980
981 if (isPortPublished(port))
983 else
984 emit portPublicationRequested(port->getBase(), false);
985}
986
993void VuoEditorComposition::deleteConnectedCables()
994{
995 QAction *sender = (QAction *)QObject::sender();
996 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
997 vector<VuoCable *> connectedCables = port->getBase()->getConnectedCables(true);
998 QList<QGraphicsItem *> cablesToRemove;
999
1000 // Delete visible directly connected cables.
1001 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
1002 {
1003 if (!cable->getRenderer()->paintingDisabled())
1004 cablesToRemove.append(cable->getRenderer());
1005 }
1006
1007 // Delete visible cables connected to the typecast's child port.
1008 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
1009 if (typecastPort)
1010 {
1011 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1012 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1013 foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
1014 {
1015 if (!cable->getRenderer()->paintingDisabled())
1016 cablesToRemove.append(cable->getRenderer());
1017 }
1018 }
1019
1020 emit componentsRemoved(QList<QGraphicsItem *>(cablesToRemove));
1021}
1022
1029void VuoEditorComposition::hideConnectedCables()
1030{
1031 QAction *sender = (QAction *)QObject::sender();
1032 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1033 set<VuoRendererCable *> cablesToHide;
1034
1035 // Hide visible directly connected cables.
1036 foreach (VuoCable *cable, port->getBase()->getConnectedCables(false))
1037 {
1038 if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
1039 cablesToHide.insert(cable->getRenderer());
1040 }
1041
1042 // Hide visible cables connected to the typecast's child port.
1043 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
1044 if (typecastPort)
1045 {
1046 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1047 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1048 foreach (VuoCable *cable, typecastInPort->getConnectedCables(false))
1049 {
1050 if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
1051 cablesToHide.insert(cable->getRenderer());
1052 }
1053 }
1054
1055 emit cablesHidden(cablesToHide);
1056}
1057
1064void VuoEditorComposition::unhideConnectedCables()
1065{
1066 QAction *sender = (QAction *)QObject::sender();
1067 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1068 set<VuoRendererCable *> cablesToUnhide;
1069
1070 // Unhide visible directly connected cables.
1071 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
1072 {
1073 if (cable->getRenderer()->getEffectivelyWireless())
1074 cablesToUnhide.insert(cable->getRenderer());
1075 }
1076
1077 // Unhide visible cables connected to the typecast's child port.
1078 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
1079 if (typecastPort)
1080 {
1081 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1082 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1083 foreach (VuoCable *cable, typecastInPort->getConnectedCables(true))
1084 {
1085 if (cable->getRenderer()->getEffectivelyWireless())
1086 cablesToUnhide.insert(cable->getRenderer());
1087 }
1088 }
1089
1090 emit cablesUnhidden(cablesToUnhide);
1091}
1092
1097void VuoEditorComposition::fireTriggerPortEvent()
1098{
1099 QAction *sender = (QAction *)QObject::sender();
1100 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1101 fireTriggerPortEvent(port->getBase());
1102}
1103
1108{
1109 fireTriggerPortEvent(getTriggerPortToRefire());
1110}
1111
1116{
1117 if (triggerPortToRefire.empty())
1118 return nullptr;
1119
1120 VuoPort *triggerPort = nullptr;
1121 identifierCache->doForPortWithIdentifier(triggerPortToRefire, [&triggerPort](VuoPort *port) {
1122 triggerPort = port;
1123 });
1124 return triggerPort;
1125}
1126
1131{
1132 string portID = getIdentifierForStaticPort(port);
1133 if (portID != this->triggerPortToRefire)
1134 {
1135 this->triggerPortToRefire = portID;
1136 emit refirePortChanged();
1137 }
1138}
1139
1144void VuoEditorComposition::setPortConstant()
1145{
1146 QAction *sender = (QAction *)QObject::sender();
1147 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1148
1149 if (port->isConstant())
1150 emit inputEditorRequested(port);
1151}
1152
1157void VuoEditorComposition::setPortConstantToValue(VuoRendererPort *port, string value)
1158{
1159 if (port->isConstant())
1160 emit portConstantChangeRequested(port, value);
1161}
1162
1167void VuoEditorComposition::specializeGenericPortType()
1168{
1169 QAction *sender = (QAction *)QObject::sender();
1170 QList<QVariant> portAndSpecializedType= sender->data().toList();
1171 VuoRendererPort *port = portAndSpecializedType[0].value<VuoRendererPort *>();
1172 QString specializedTypeName = portAndSpecializedType[1].toString();
1173
1174 // If the port is already specialized to the target type, do nothing.
1175 if (port && (port->getDataType()->getModuleKey() == specializedTypeName.toUtf8().constData()))
1176 return;
1177
1178 // If the port is already specialized to a different type, re-specialize it.
1179 if (port && !dynamic_cast<VuoGenericType *>(port->getDataType()))
1180 emit respecializePort(port, specializedTypeName.toUtf8().constData());
1181
1182 // Otherwise, specialize the port from generic.
1183 else
1184 emit specializePort(port, specializedTypeName.toUtf8().constData());
1185}
1186
1191void VuoEditorComposition::unspecializePortType()
1192{
1193 QAction *sender = (QAction *)QObject::sender();
1194 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1195
1196 // If the port is already generic, do nothing.
1197 if (port && dynamic_cast<VuoGenericType *>(port->getDataType()))
1198 return;
1199
1200 emit unspecializePort(port);
1201}
1202
1213void VuoEditorComposition::createReplacementsToUnspecializePort(VuoPort *portToUnspecialize, bool shouldOutputNodesToReplace, map<VuoNode *, string> &nodesToReplace, set<VuoCable *> &cablesToDelete)
1214{
1215 // Find the ports that will share the same generic type as portToUnspecialize, and organize them by node.
1216 set<pair<VuoNode *, VuoPort *>> connectedPotentiallyGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(portToUnspecialize->getRenderer()->getUnderlyingParentNode()->getBase(),
1217 portToUnspecialize, true);
1218 map<VuoNode *, set<VuoPort *> > portsToUnspecializeForNode;
1219 for (pair<VuoNode *, VuoPort *> i : connectedPotentiallyGenericPorts)
1220 {
1221 VuoNode *node = i.first;
1222 VuoPort *connectedPort = i.second;
1223
1224 // @todo: Don't just exclude ports that aren't currently revertible, also exclude ports that are only
1225 // within the current network by way of ports that aren't currently revertible.
1226 if (isPortCurrentlyRevertible(connectedPort->getRenderer()))
1227 portsToUnspecializeForNode[node].insert(connectedPort);
1228 }
1229
1230 for (map<VuoNode *, set<VuoPort *> >::iterator i = portsToUnspecializeForNode.begin(); i != portsToUnspecializeForNode.end(); ++i)
1231 {
1232 VuoNode *node = i->first;
1233 set<VuoPort *> ports = i->second;
1234
1235 if (shouldOutputNodesToReplace)
1236 {
1237 // Create the unspecialized node class name for each node to unspecialize.
1238 set<VuoPortClass *> portClasses;
1239 for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1240 portClasses.insert((*j)->getClass());
1242 string unspecializedNodeClassName = nodeClass->createUnspecializedNodeClassName(portClasses);
1243 nodesToReplace[node] = unspecializedNodeClassName;
1244 }
1245
1246 // Identify the cables that will become invalid (data-carrying cable with generic port at one end, non-generic port at the other end)
1247 // when the node is unspecialized.
1248 for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1249 {
1250 VuoPort *port = *j;
1251 for (VuoCable *cable : port->getConnectedCables(true))
1252 {
1253 bool areEndsCompatible = false;
1254
1255 if (!cable->getRenderer()->effectivelyCarriesData())
1256 areEndsCompatible = true;
1257
1258 else if (! cable->isPublished())
1259 {
1260 VuoPort *portOnOtherEnd = (cable->getFromPort() == port ? cable->getToPort() : cable->getFromPort());
1261 if (portOnOtherEnd && isPortCurrentlyRevertible(portOnOtherEnd->getRenderer()))
1262 {
1263 VuoNode *nodeOnOtherEnd = portOnOtherEnd->getRenderer()->getUnderlyingParentNode()->getBase();
1264 VuoCompilerNodeClass *nodeClassOnOtherEnd = nodeOnOtherEnd->getNodeClass()->getCompiler();
1265 VuoCompilerSpecializedNodeClass *specializedNodeClassOnOtherEnd = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClassOnOtherEnd);
1266 if (specializedNodeClassOnOtherEnd)
1267 {
1268 VuoType *typeOnOtherEnd = specializedNodeClassOnOtherEnd->getOriginalPortType( portOnOtherEnd->getClass() );
1269 if (! typeOnOtherEnd || dynamic_cast<VuoGenericType *>(typeOnOtherEnd))
1270 areEndsCompatible = true;
1271 }
1272 }
1273 }
1274
1275 if (! areEndsCompatible && (cable != cableInProgress))
1276 cablesToDelete.insert(cable);
1277 }
1278 }
1279 }
1280}
1281
1285void VuoEditorComposition::fireTriggerPortEvent(VuoPort *port)
1286{
1287 if (! (port && port->hasCompiler()) )
1288 return;
1289
1290 VuoPort *oldManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1291 string oldSnapshot = takeSnapshot();
1292
1293 string runningTriggerPortIdentifier = "";
1294 bool isTriggerPort = false;
1295 if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()))
1296 {
1297 // Trigger port — The event will be fired from the port.
1298
1299 getBase()->getCompiler()->setManuallyFirableInputPort(nullptr, nullptr);
1300
1301 runningTriggerPortIdentifier = identifierCache->getIdentifierForPort(port);
1302 isTriggerPort = true;
1303 }
1304 else if (port->hasRenderer() && port->getRenderer()->getInput())
1305 {
1306 // Input port — The event will be fired from the composition's manually firable trigger into the port.
1307
1309
1311 VuoCompilerTriggerPort *triggerPort = graph->getManuallyFirableTrigger();
1312 VuoCompilerNode *triggerNode = graph->getNodeForTriggerPort(triggerPort);
1313 runningTriggerPortIdentifier = getIdentifierForStaticPort(triggerPort->getBase(), triggerNode->getBase());
1314 }
1315 else
1316 return;
1317
1318 VUserLog("%s: Fire %s",
1319 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
1320 runningTriggerPortIdentifier.c_str());
1321
1322 VuoPort *newManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1323 bool manuallyFirableInputPortChanged = (oldManuallyFirableInputPort != newManuallyFirableInputPort);
1324 string newSnapshot = takeSnapshot();
1325
1326 auto fireIfRunning = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
1327 {
1328 dispatch_async(topLevelComposition->runCompositionQueue, ^{
1329 if (topLevelComposition->isRunningThreadUnsafe())
1330 {
1331 topLevelComposition->runner->fireTriggerPortEvent(thisCompositionIdentifier, runningTriggerPortIdentifier);
1332
1333 // Display the trigger port animation when the user manually fires an event
1334 // even if not in "Show Events" mode. (If in "Show Events" mode, this will
1335 // be handled for trigger ports by VuoEditorComposition::receivedTelemetryOutputPortUpdated(...).)
1336 if (! (this->showEventsMode && isTriggerPort) )
1337 this->animatePort(port->getRenderer());
1338 }
1339 });
1340 };
1341
1342 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1343 if (this == topLevelComposition || ! manuallyFirableInputPortChanged)
1344 {
1345 // Top-level composition or unmodified subcomposition — Fire the trigger immediately.
1346
1347 if (! newSnapshot.empty() && manuallyFirableInputPortChanged)
1348 updateRunningComposition(oldSnapshot, newSnapshot);
1349
1350 fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1351 }
1352 else
1353 {
1354 // Modified subcomposition — Fire the trigger after the subcomposition has been reloaded.
1355
1356 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
1357 string nodeClassName = VuoCompiler::getModuleKeyForPath(compositionPath);
1358 moduleManager->doNextTimeNodeClassIsLoaded(nodeClassName, ^{
1359 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1360 fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1361 });
1362 });
1363
1364 if (! newSnapshot.empty())
1365 updateRunningComposition(oldSnapshot, newSnapshot);
1366 });
1367 }
1368 });
1369
1370 setTriggerPortToRefire(port);
1371}
1372
1377void VuoEditorComposition::addInputPort()
1378{
1379 QAction *sender = (QAction *)QObject::sender();
1380 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
1381 emit inputPortCountAdjustmentRequested(node, 1, false);
1382}
1383
1388void VuoEditorComposition::removeInputPort()
1389{
1390 QAction *sender = (QAction *)QObject::sender();
1391 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
1392 emit inputPortCountAdjustmentRequested(node, -1, false);
1393}
1394
1399void VuoEditorComposition::swapNode()
1400{
1401 QAction *sender = (QAction *)QObject::sender();
1402 QList<QVariant> nodeAndReplacementType= sender->data().toList();
1403 VuoRendererNode *node = nodeAndReplacementType[0].value<VuoRendererNode *>();
1404 QString newNodeClassName = nodeAndReplacementType[1].toString();
1405 emit nodeSwapRequested(node, newNodeClassName.toUtf8().constData());
1406}
1407
1412{
1413 // Open a title editor for each selected non-attachment node.
1414 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1415 for (set<VuoRendererNode *>::iterator i = selectedNodes.begin(); i != selectedNodes.end(); ++i)
1416 {
1417 if (!dynamic_cast<VuoRendererInputAttachment *>(*i))
1418 emit nodeTitleEditorRequested(*i);
1419 }
1420}
1421
1425void VuoEditorComposition::editSelectedComments()
1426{
1427 // Open a text editor for each selected comment.
1428 set<VuoRendererComment *> selectedComments = getSelectedComments();
1429 for (set<VuoRendererComment *>::iterator i = selectedComments.begin(); i != selectedComments.end(); ++i)
1430 emit commentEditorRequested(*i);
1431}
1432
1436set<VuoRendererCable *> VuoEditorComposition::getCablesInternalToSubcomposition(QList<QGraphicsItem *> subcompositionComponents)
1437{
1438 set<VuoRendererCable *> internalCables;
1439
1440 for (QList<QGraphicsItem *>::iterator i = subcompositionComponents.begin(); i != subcompositionComponents.end(); ++i)
1441 {
1442 QGraphicsItem *compositionComponent = *i;
1443 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1444 if (rn)
1445 {
1446 set<VuoCable *> connectedCables = rn->getConnectedCables(false);
1447 for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1448 {
1449 VuoNode *fromNode = (*cable)->getFromNode();
1450 VuoNode *toNode = (*cable)->getToNode();
1451
1452 if (fromNode && toNode && subcompositionComponents.contains(fromNode->getRenderer()) && subcompositionComponents.contains(toNode->getRenderer()))
1453 internalCables.insert((*cable)->getRenderer());
1454 }
1455 }
1456 }
1457
1458 return internalCables;
1459}
1460
1465{
1466 return cableInProgress;
1467}
1468
1474{
1475 return cableInProgressWasNew;
1476}
1477
1482{
1483 return menuSelectionInProgress;
1484}
1485
1490{
1491 QList<QGraphicsItem *> compositionComponents = items();
1492 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1493 {
1494 QGraphicsItem *compositionComponent = *i;
1495
1496 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1497 if (!rc || !rc->paintingDisabled())
1498 compositionComponent->setSelected(true);
1499 }
1500}
1501
1506{
1507 QList<QGraphicsItem *> compositionComponents = items();
1508 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1509 {
1510 QGraphicsItem *compositionComponent = *i;
1511 VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
1512 if (rcomment)
1513 rcomment->setSelected(true);
1514 }
1515}
1516
1521{
1522 QList<QGraphicsItem *> compositionComponents = items();
1523 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1524 {
1525 QGraphicsItem *compositionComponent = *i;
1526 compositionComponent->setSelected(false);
1527 }
1528}
1529
1533void VuoEditorComposition::openSelectedEditableNodes()
1534{
1535 foreach (VuoRendererNode *node, getSelectedNodes())
1536 {
1537 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
1538 QString actionText, sourcePath;
1539 if (VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
1540 emit nodeSourceEditorRequested(node);
1541 }
1542}
1543
1548{
1549 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1550 set<VuoRendererComment *> selectedComments = getSelectedComments();
1551 moveItemsBy(selectedNodes, selectedComments, dx, dy, false);
1552}
1553
1557void VuoEditorComposition::moveNodesBy(set<VuoRendererNode *> nodes, qreal dx, qreal dy, bool movedByDragging)
1558{
1559 set<VuoRendererComment *> comments;
1560 moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1561}
1562
1566void VuoEditorComposition::moveCommentsBy(set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1567{
1568 set<VuoRendererNode *> nodes;
1569 moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1570}
1571
1575void VuoEditorComposition::moveItemsBy(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1576{
1577 emit itemsMoved(nodes, comments, dx, dy, movedByDragging);
1579 for (set<VuoRendererNode *>::iterator it = nodes.begin(); it != nodes.end(); ++it)
1580 (*it)->updateConnectedCableGeometry();
1581}
1582
1586void VuoEditorComposition::resizeCommentBy(VuoRendererComment *comment, qreal dx, qreal dy)
1587{
1588 emit commentResized(comment, dx, dy);
1589}
1590
1595{
1596 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1597 set<VuoRendererNode *> selectedNodes;
1598 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1599 {
1600 QGraphicsItem *compositionComponent = *i;
1601 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1602 if (rn)
1603 selectedNodes.insert(rn);
1604 }
1605
1606 return selectedNodes;
1607}
1608
1613{
1614 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1615 set<VuoRendererComment *> selectedComments;
1616 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1617 {
1618 QGraphicsItem *compositionComponent = *i;
1619 VuoRendererComment *rc = dynamic_cast<VuoRendererComment *>(compositionComponent);
1620 if (rc)
1621 selectedComments.insert(rc);
1622 }
1623
1624 return selectedComments;
1625}
1626
1630set<VuoRendererCable *> VuoEditorComposition::getSelectedCables(bool includePublishedCables)
1631{
1632 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1633 set<VuoRendererCable *> selectedCables;
1634 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1635 {
1636 QGraphicsItem *compositionComponent = *i;
1637 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1638 if (rc && (includePublishedCables || !rc->getBase()->isPublished()))
1639 selectedCables.insert(rc);
1640 }
1641
1642 return selectedCables;
1643}
1644
1648void VuoEditorComposition::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1649{
1650 const QMimeData *mimeData = event->mimeData();
1651 bool disablePortHoverHighlighting = true;
1652
1653 // Accept drags of files.
1654 if (mimeData->hasFormat("text/uri-list"))
1655 {
1656 QList<QUrl> urls = mimeData->urls();
1657
1658 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1659 VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1660 if (portAtDropLocation)
1661 {
1662 if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1663 (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1664 disablePortHoverHighlighting = false;
1665 else
1666 event->setDropAction(Qt::IgnoreAction);
1667 }
1668 else // if (!portAtDropLocation)
1669 {
1670 bool dragIncludesDroppableFile = false;
1671 foreach (QUrl url, urls)
1672 {
1673 char *urlZ = strdup(url.path().toUtf8().constData());
1674 bool isSupportedDragNDropFile =
1675 VuoFileType_isFileOfType(urlZ, VuoFileType_Image)
1676 || VuoFileType_isFileOfType(urlZ, VuoFileType_Movie)
1677 || VuoFileType_isFileOfType(urlZ, VuoFileType_Scene)
1678 || VuoFileType_isFileOfType(urlZ, VuoFileType_Audio)
1679 || VuoFileType_isFileOfType(urlZ, VuoFileType_Feed)
1680 || VuoFileType_isFileOfType(urlZ, VuoFileType_JSON)
1681 || VuoFileType_isFileOfType(urlZ, VuoFileType_XML)
1682 || VuoFileType_isFileOfType(urlZ, VuoFileType_Table)
1683 || VuoFileType_isFileOfType(urlZ, VuoFileType_Mesh)
1684 || VuoFileType_isFileOfType(urlZ, VuoFileType_Data)
1685 || VuoFileType_isFileOfType(urlZ, VuoFileType_App)
1686 || isDirectory(urlZ);
1687 free(urlZ);
1688 if (isSupportedDragNDropFile)
1689 {
1690 dragIncludesDroppableFile = true;
1691 break;
1692 }
1693 }
1694
1695 if (!dragIncludesDroppableFile)
1696 event->setDropAction(Qt::IgnoreAction);
1697 }
1698
1699 event->accept();
1700 }
1701
1702 // Accept drags of single or multiple nodes from the node library.
1703 else if (mimeData->hasFormat("text/plain") || mimeData->hasFormat("text/scsv"))
1704 event->acceptProposedAction();
1705
1706 else
1707 {
1708 event->setDropAction(Qt::IgnoreAction);
1709 event->accept();
1710 }
1711
1712 updateHoverHighlighting(event->scenePos(), disablePortHoverHighlighting);
1713}
1714
1718void VuoEditorComposition::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
1719{
1720 event->acceptProposedAction();
1721}
1722
1726void VuoEditorComposition::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
1727{
1728 dragEnterEvent(event);
1729}
1730
1734void VuoEditorComposition::dropEvent(QGraphicsSceneDragDropEvent *event)
1735{
1736 const QMimeData *mimeData = event->mimeData();
1737
1738 // Accept drops of certain types of files.
1739 if (mimeData->hasFormat("text/uri-list"))
1740 {
1741 // Retrieve the composition directory so that file paths may be specified relative to it.
1742 // Note: Providing the directory's canonical path as the argument to the
1743 // QDir constructor is necessary in order for QDir::relativeFilePath() to
1744 // work correctly when the non-canonical path contains symbolic links (e.g.,
1745 // '/tmp' -> '/private/tmp' for example compositions).
1746 string topCompositionPath = compiler->getCompositionLocalPath();
1747 if (topCompositionPath.empty())
1748 topCompositionPath = getBase()->getDirectory();
1749 QDir compositionDir(QDir(topCompositionPath.c_str()).canonicalPath());
1750
1751 // Use the absolute file path if the "Option" key was pressed.
1752 bool useAbsoluteFilePaths = VuoEditorUtilities::optionKeyPressedForEvent(event);
1753
1754 QList<QGraphicsItem *> newNodes;
1755 QList<QUrl> urls = mimeData->urls();
1756
1757 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1758 VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1759 if (portAtDropLocation)
1760 {
1761 if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1762 (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1763 {
1764 QString filePath = (useAbsoluteFilePaths? urls[0].path() : compositionDir.relativeFilePath(urls[0].path()));
1765 string constantValue = "\"" + string(filePath.toUtf8().constData()) + "\"";
1766 event->accept();
1767
1768 emit portConstantChangeRequested(portAtDropLocation, constantValue);
1769 }
1770 else
1771 event->ignore();
1772 }
1773
1774 else // if (!portAtDropLocation)
1775 {
1776 const int ySpacingForNewNodes = 11;
1777 int yOffsetForPreviousNewNode = -1 * ySpacingForNewNodes;
1778
1779 foreach (QUrl url, urls)
1780 {
1781 QStringList targetNodeClassNames;
1782 char *urlZ = strdup(url.path().toUtf8().constData());
1783
1784 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Image))
1785 targetNodeClassNames += "vuo.image.fetch";
1786 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Movie))
1787 {
1788 if (!getActiveProtocol())
1789 targetNodeClassNames += "vuo.video.play";
1790
1791 targetNodeClassNames += "vuo.video.decodeImage";
1792 }
1793 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Scene))
1794 targetNodeClassNames += "vuo.scene.fetch";
1795 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Audio))
1796 targetNodeClassNames += "vuo.audio.file.play";
1797 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Mesh))
1798 targetNodeClassNames += "vuo.image.project.dome";
1799 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Feed))
1800 targetNodeClassNames += "vuo.rss.fetch";
1801 if (VuoFileType_isFileOfType(urlZ, VuoFileType_JSON))
1802 targetNodeClassNames += "vuo.tree.fetch.json";
1803 if (VuoFileType_isFileOfType(urlZ, VuoFileType_XML))
1804 targetNodeClassNames += "vuo.tree.fetch.xml";
1805 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Table))
1806 targetNodeClassNames += "vuo.table.fetch";
1807 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Data))
1808 targetNodeClassNames += "vuo.data.fetch";
1809 if (VuoFileType_isFileOfType(urlZ, VuoFileType_App))
1810 targetNodeClassNames += "vuo.app.launch";
1811 if (isDirectory(urlZ))
1812 targetNodeClassNames += "vuo.file.list";
1813
1814 free(urlZ);
1815
1816 QString selectedNodeClassName = "";
1817 if (targetNodeClassNames.size() == 1)
1818 selectedNodeClassName = targetNodeClassNames[0];
1819 else if (targetNodeClassNames.size() > 1)
1820 {
1821 QMenu nodeMenu(views()[0]->viewport());
1822 nodeMenu.setSeparatorsCollapsible(false);
1823
1824 foreach (QString nodeClassName, targetNodeClassNames)
1825 {
1826 VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
1827 string nodeTitle = (nodeClass? nodeClass->getBase()->getDefaultTitle() : nodeClassName.toUtf8().constData());
1828 //: Appears in a popup menu when dragging files from Finder onto the composition canvas.
1829 QAction *nodeAction = nodeMenu.addAction(tr("Insert \"%1\" Node").arg(nodeTitle.c_str()));
1830 nodeAction->setData(nodeClassName);
1831 }
1832
1833 menuSelectionInProgress = true;
1834 QAction *selectedNode = nodeMenu.exec(QCursor::pos());
1835 menuSelectionInProgress = false;
1836
1837 selectedNodeClassName = (selectedNode? selectedNode->data().toString().toUtf8().constData() : "");
1838 }
1839
1840 if (!selectedNodeClassName.isEmpty())
1841 {
1842 VuoRendererNode *newNode = createNode(selectedNodeClassName, "",
1843 event->scenePos().x(),
1844 event->scenePos().y() + yOffsetForPreviousNewNode + ySpacingForNewNodes);
1845
1846 if (newNode)
1847 {
1848 VuoPort *urlPort = newNode->getBase()->getInputPortWithName(selectedNodeClassName == "vuo.file.list"?
1849 "folder" :
1850 "url");
1851 if (urlPort)
1852 {
1853 QString filePath = (useAbsoluteFilePaths? url.path() : compositionDir.relativeFilePath(url.path()));
1854 urlPort->getRenderer()->setConstant(VuoText_getString(filePath.toUtf8().constData()));
1855
1856 newNodes.append(newNode);
1857
1858 yOffsetForPreviousNewNode += newNode->boundingRect().height();
1859 yOffsetForPreviousNewNode += ySpacingForNewNodes;
1860 }
1861 }
1862 }
1863 }
1864
1865 if (newNodes.size() > 0)
1866 {
1867 event->accept();
1868
1869 emit componentsAdded(newNodes, this);
1870 }
1871 else
1872 event->ignore();
1873 }
1874 }
1875
1876 // Accept drops of one or more nodes from the node library class list.
1877 else if (mimeData->hasFormat("text/scsv"))
1878 {
1879 event->setDropAction(Qt::CopyAction);
1880 event->accept();
1881
1882 QByteArray scsvData = event->mimeData()->data("text/scsv");
1883 QString scsvText = QString::fromUtf8(scsvData);
1884 QStringList nodeClassNames = scsvText.split(';');
1885
1886 QPointF startPos = event->scenePos()-QPointF(0,VuoRendererNode::nodeHeaderYOffset);
1889 int snapDelta = nextYPos - startPos.y();
1890
1891 QList<QGraphicsItem *> newNodes;
1892 for (QStringList::iterator i = nodeClassNames.begin(); i != nodeClassNames.end(); ++i)
1893 {
1894 VuoRendererNode *newNode = createNode(*i, "",
1895 startPos.x(),
1896 nextYPos - (VuoRendererItem::getSnapToGrid()? 0 : snapDelta));
1897
1898 if (newNode)
1899 {
1900 int prevYPos = nextYPos;
1901 nextYPos = VuoRendererComposition::quantizeToNearestGridLine(QPointF(0,prevYPos+newNode->boundingRect().height()),
1903 if (nextYPos <= prevYPos+newNode->boundingRect().height())
1905
1906 newNodes.append((QGraphicsItem *)newNode);
1907 }
1908 }
1909
1910 emit componentsAdded(newNodes, this);
1911 }
1912
1913 // Accept drops of single nodes from the node library documentation panel.
1914 else if (mimeData->hasFormat("text/plain"))
1915 {
1916 event->setDropAction(Qt::CopyAction);
1917 event->accept();
1918
1919 QList<QGraphicsItem *> newNodes;
1920 QStringList dropItems = event->mimeData()->text().split('\n');
1921 QString nodeClassName = dropItems[0];
1922
1923 // Account for the offset between cursor and dragged item pixmaps
1924 // to drop node in-place.
1925 QPoint hotSpot = (dropItems.size() >= 3? QPoint(dropItems[1].toInt(), dropItems[2].toInt()) : QPoint(0,0));
1926 VuoRendererNode *newNode = createNode(nodeClassName, "",
1927 event->scenePos().x()-hotSpot.x()+1,
1928 event->scenePos().y()-VuoRendererNode::nodeHeaderYOffset-hotSpot.y()+1);
1929
1930 newNodes.append(newNode);
1931 emit componentsAdded(newNodes, this);
1932 }
1933 else
1934 {
1935 event->ignore();
1936 }
1937}
1938
1942void VuoEditorComposition::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
1943{
1944 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1945 if (dynamic_cast<VuoRendererPort *>(nearbyItem))
1946 {
1947 QGraphicsScene::sendEvent(nearbyItem, event);
1948 event->accept();
1949 }
1950
1951 else
1952 QGraphicsScene::mouseDoubleClickEvent(event);
1953}
1954
1958void VuoEditorComposition::mousePressEvent(QGraphicsSceneMouseEvent *event)
1959{
1960 QPointF scenePos = event->scenePos();
1961
1962 // Handle left-button presses.
1963 if (event->button() == Qt::LeftButton)
1964 {
1965 // Correct for erroneous tracking behavior that occurs following the first left-click
1966 // directly on a node, cable, or comment after the cancellation of a component duplication operation.
1967 // See https://b33p.net/kosada/node/3339
1968 if (duplicationCancelled)
1969 {
1970 QGraphicsItem *itemClickedDirectly = itemAt(scenePos, views()[0]->transform());
1971 if (dynamic_cast<VuoRendererNode *>(itemClickedDirectly) ||
1972 dynamic_cast<VuoRendererCable *>(itemClickedDirectly) ||
1973 dynamic_cast<VuoRendererComment *>(itemClickedDirectly)
1974 )
1975 {
1976 duplicationCancelled = false;
1977 correctForCancelledDuplication(event);
1978 }
1979 }
1980
1981 // Determine whether the cursor is in range of any operable composition components.
1982 QGraphicsItem *nearbyItem = findNearbyComponent(scenePos);
1983 leftMousePressEventAtNearbyItem(nearbyItem, event);
1984 }
1985
1986 // Handle non-left-button presses.
1987 else // if (event->button() != Qt::LeftButton)
1988 mousePressEventNonLeftButton(event);
1989}
1990
1995void VuoEditorComposition::leftMousePressEventAtNearbyItem(QGraphicsItem *nearbyItem, QGraphicsSceneMouseEvent *event)
1996{
1997 bool eventHandled = false;
1998
1999 // If click did not occur within range of a port, check whether
2000 // the click occured on a cable within its yank zone.
2001 VuoRendererCable *cableYankedDirectly = NULL;
2002 VuoRendererPort *currentPort = dynamic_cast<VuoRendererPort *>(nearbyItem);
2003 if (! currentPort)
2004 {
2005 VuoRendererCable *currentCable = dynamic_cast<VuoRendererCable *>(nearbyItem);
2006 if (currentCable &&
2007 currentCable->yankZoneIncludes(event->scenePos()) &&
2008 currentCable->getBase()->getToPort())
2009 {
2010 currentPort = currentCable->getBase()->getToPort()->getRenderer();
2011 cableYankedDirectly = currentCable;
2012 }
2013 }
2014
2015 if (currentPort)
2016 {
2017 // Case: Firing an event by Command+click
2018 bool isTriggerPort = (currentPort->getBase()->hasCompiler() && dynamic_cast<VuoCompilerTriggerPort *>(currentPort->getBase()->getCompiler()));
2019 if ((event->modifiers() & Qt::ControlModifier) && (currentPort->getInput() || isTriggerPort))
2020 fireTriggerPortEvent(currentPort->getBase());
2021
2022 else
2023 {
2024 portWithDragInitiated = currentPort;
2025 cableWithYankInitiated = cableYankedDirectly;
2026 event->accept();
2027 }
2028
2029 eventHandled = true;
2030 }
2031
2032 // Case: Initiating duplication of selected components with Option/Alt+drag
2033 else if (nearbyItem && VuoEditorUtilities::optionKeyPressedForEvent(event) && !duplicationDragInProgress)
2034 {
2035 // Duplicate the selected components.
2036 duplicateOnNextMouseMove = true;
2037 duplicationDragInProgress = true;
2038 cursorPosBeforeDuplicationDragMove = (VuoRendererItem::getSnapToGrid()?
2041 event->scenePos());
2042
2043 QGraphicsScene::mousePressEvent(event);
2044 eventHandled = true;
2045 }
2046
2047 // Case: Left mouse-click made near enough to a cable to trigger cable selection
2048 if (dynamic_cast<VuoRendererCable *>(nearbyItem))
2049 {
2050 if (event->modifiers() & Qt::ControlModifier)
2051 nearbyItem->setSelected(! nearbyItem->isSelected());
2052
2053 else
2054 {
2056 nearbyItem->setSelected(true);
2057 }
2058
2059 event->accept();
2060 eventHandled = true;
2061 }
2062
2063 // Case: Left mouse-click made for some other reason, not handled here
2064 if (!eventHandled)
2065 QGraphicsScene::mousePressEvent(event);
2066}
2067
2072void VuoEditorComposition::mousePressEventNonLeftButton(QGraphicsSceneMouseEvent *event)
2073{
2075
2076 // If a right-click occurred, generate the context menu event ourselves
2077 // rather than leaving it to QGraphicsScene. This prevents existing selected
2078 // components from being erroneously de-selected before
2079 // VuoEditorComposition::contextMenuEvent(...) is called if the right-click
2080 // occurred within range of, but not directly upon, a composition component.
2081 if (event->button() == Qt::RightButton)
2082 {
2083 QGraphicsSceneContextMenuEvent contextMenuEvent(QEvent::GraphicsSceneContextMenu);
2084 contextMenuEvent.setScreenPos(event->screenPos());
2085 contextMenuEvent.setScenePos(event->scenePos());
2086 contextMenuEvent.setReason(QGraphicsSceneContextMenuEvent::Mouse);
2087 QApplication::sendEvent(this, &contextMenuEvent);
2088 event->accept();
2089 }
2090
2091 else
2092 QGraphicsScene::mousePressEvent(event);
2093
2094 return;
2095}
2096
2103void VuoEditorComposition::correctForCancelledDuplication(QGraphicsSceneMouseEvent *event)
2104{
2105 // Simulate an extra mouse click for now to force the cursor
2106 // to track correctly with the next set of dragged components.
2107 QGraphicsSceneMouseEvent pressEvent(QEvent::GraphicsSceneMousePress);
2108 pressEvent.setScenePos(event->scenePos());
2109 pressEvent.setButton(Qt::LeftButton);
2110 QApplication::sendEvent(this, &pressEvent);
2111
2112 QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
2113 releaseEvent.setScenePos(event->scenePos());
2114 releaseEvent.setButton(Qt::LeftButton);
2115 QApplication::sendEvent(this, &releaseEvent);
2116}
2117
2125void VuoEditorComposition::initiateCableDrag(VuoRendererPort *currentPort, VuoRendererCable *cableYankedDirectly, QGraphicsSceneMouseEvent *event)
2126{
2127 Qt::KeyboardModifiers modifiers = event->modifiers();
2128 bool optionKeyPressed = (modifiers & Qt::AltModifier);
2129 bool shiftKeyPressed = (modifiers & Qt::ShiftModifier);
2130
2131 // For now, a left mouse press on an input port with a constant value, attached typecast, or attached "Make List" node does nothing.
2132 // Eventually, a mouse drag will detach the constant value, typecast, or "Make List" node.
2133 if (! (cableYankedDirectly || currentPort->getOutput() || currentPort->supportsDisconnectionByDragging()))
2134 {
2135 return;
2136 }
2137
2138 // Determine based on the keypress modifiers and the attributes of the port whether to
2139 // create a new cable, disconnect an existing cable, or duplicate an existing cable.
2140 bool creatingNewCable = false;
2141 bool disconnectingExistingCable = false;
2142 bool duplicatingExistingCable = false;
2143
2144 VuoPort *fixedPort = currentPort->getBase();
2145 VuoNode *fixedNode = getUnderlyingParentNodeForPort(fixedPort, this);
2146
2147 VuoNode *fromNode = NULL;
2148 VuoPort *fromPort = NULL;
2149 VuoNode *toNode = NULL;
2150 VuoPort *toPort = NULL;
2151
2152 // Case: Dragging from an output port
2153 if (currentPort->getOutput())
2154 {
2155 // Prepare for the "forward" creation of a new cable.
2156 fromPort = fixedPort;
2157 fromNode = fixedNode;
2158 creatingNewCable = true;
2159 }
2160
2161 // Case: Dragging from an input port
2162 else if (! currentPort->getFunctionPort())
2163 {
2164 // If the input port has no connected cables to disconnect, prepare for the
2165 // "backward" creation of a new cable.
2166 if (currentPort->getBase()->getConnectedCables(true).empty())
2167 {
2168 toPort = fixedPort;
2169 toNode = fixedNode;
2170 creatingNewCable = true;
2171 }
2172
2173 // If the input port does have connected cables, prepare for the
2174 // disconnection or duplication of one of these cables.
2175 else
2176 {
2177 if (optionKeyPressed)
2178 {
2179 duplicatingExistingCable = true;
2180 }
2181
2182 else
2183 disconnectingExistingCable = true;
2184 }
2185 }
2186
2187 // @todo: Case: Dragging from a function port
2188
2189
2190 // Perform the actual cable creation, if applicable.
2191 if (creatingNewCable)
2192 {
2193 // Create the cable first and set its endpoints later in case either endpoint is published,
2194 // since published nodes don't have compilers.
2195 cableInProgress = (new VuoCompilerCable(NULL,
2196 NULL,
2197 NULL,
2198 NULL))->getBase();
2199
2200 // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2201 // regardless of its connected ports.
2202 cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2203
2204 cableInProgressWasNew = true;
2205 cableInProgressShouldBeWireless = false;
2206
2207 addCable(cableInProgress);
2208 cableInProgress->getRenderer()->setFrom(fromNode, fromPort);
2209 cableInProgress->getRenderer()->setTo(toNode, toPort);
2210 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2211 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2212 highlightEligibleEndpointsForCable(cableInProgress);
2213 fixedPort->getRenderer()->updateGeometry();
2214 }
2215
2216 // Perform the actual cable disconnection, if applicable.
2217 else if (disconnectingExistingCable)
2218 {
2219 // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2220 if (cableYankedDirectly)
2221 cableInProgress = cableYankedDirectly->getBase();
2222
2223 // Otherwise, disconnect the cable that was connected to the port most recently.
2224 else
2225 cableInProgress = currentPort->getBase()->getConnectedCables(true).back();
2226
2227 cableInProgressWasNew = false;
2228 cableInProgressShouldBeWireless = cableInProgress->hasCompiler() && cableInProgress->getCompiler()->getHidden();
2229
2230 currentPort->updateGeometry();
2231 cableInProgress->getRenderer()->updateGeometry();
2232 cableInProgress->getRenderer()->setHovered(false);
2233 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2234 cableInProgress->getRenderer()->setFloatingEndpointPreviousToPort(cableInProgress->getToPort());
2235 cableInProgress->getRenderer()->setPreviouslyAlwaysEventOnly(cableInProgress->getCompiler()->getAlwaysEventOnly());
2236 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2237 cableInProgress->getRenderer()->setTo(NULL, NULL);
2238 highlightEligibleEndpointsForCable(cableInProgress);
2240 cableInProgress->getRenderer()->setSelected(true);
2241 }
2242 // Perform the actual cable duplication, if applicable.
2243 else if (duplicatingExistingCable)
2244 {
2245 VuoCable *cableToDuplicate = NULL;
2246 // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2247 if (cableYankedDirectly)
2248 cableToDuplicate = cableYankedDirectly->getBase();
2249
2250 // Otherwise, disconnect the cable that was connected to the port most recently.
2251 else
2252 cableToDuplicate = currentPort->getBase()->getConnectedCables(true).back();
2253
2254 // Create the cable first and set its endpoints later in case either endpoint is published,
2255 // since published nodes don't have compilers.
2256 cableInProgress = (new VuoCompilerCable(NULL,
2257 NULL,
2258 NULL,
2259 NULL))->getBase();
2260
2261 // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2262 // regardless of its connected ports.
2263 cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2264
2265 cableInProgressWasNew = true;
2266 cableInProgressShouldBeWireless = cableToDuplicate->hasCompiler() &&
2267 cableToDuplicate->getCompiler()->getHidden();
2268 addCable(cableInProgress);
2269 cableInProgress->getRenderer()->setFrom(getUnderlyingParentNodeForPort(cableToDuplicate->getFromPort(), this),
2270 cableToDuplicate->getFromPort());
2271 cableInProgress->getRenderer()->setTo(NULL, NULL);
2272 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2273 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2274 highlightEligibleEndpointsForCable(cableInProgress);
2276 cableInProgress->getRenderer()->setSelected(true);
2277 fixedPort->getRenderer()->updateGeometry();
2278 }
2279
2280 // The cable will need to be re-painted as its endpoint is dragged.
2281 cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2282
2283 emit cableDragInitiated();
2284
2285 event->accept();
2286}
2287
2291void VuoEditorComposition::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2292{
2293 // Handle left-button releases.
2294 if (event->button() == Qt::LeftButton)
2295 {
2296 portWithDragInitiated = NULL;
2297 cableWithYankInitiated = NULL;
2298 duplicateOnNextMouseMove = false;
2299 duplicationDragInProgress = false;
2300 dragStickinessDisabled = false;
2303
2304 // If there was a cable drag in progress at the time of the left-mouse-button
2305 // release, conclude the drag -- either by connecting the cable to the eligible port
2306 // at which it was dropped, or, if not dropped at an eligible port, by deleting the cable.
2307 bool cableDragEnding = cableInProgress;
2308 if (cableDragEnding)
2309 concludeCableDrag(event);
2310
2311 QGraphicsItem *item = findNearbyComponent(event->scenePos());
2312 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2313 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
2314 if (port)
2315 {
2316 // Display the port popover, as long as the mouse release did not have any keyboard modifiers
2317 // and did not mark the end of a drag.
2318 // Do so even if a cable drag was technically in progress, because all it takes to initiate
2319 // a cable drag is a mouse press. We could trigger cable drags upon mouse move events instead of
2320 // mouse press events to avoid this.
2321 if ((event->modifiers() == Qt::NoModifier) &&
2322 (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2323 {
2324 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
2325 if (typecastPort)
2326 {
2327 // Since the rendering of the typecast body includes its host port, display popovers for both.
2328 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
2329 enablePopoverForNode(typecastNode);
2330 enableInactivePopoverForPort(typecastPort->getReplacedPort());
2331 }
2332
2333 else
2334 enableInactivePopoverForPort(port);
2335 }
2336 }
2337
2338 else // if (!port)
2339 {
2340 if (!cableDragEnding && !node)
2342 }
2343
2344 if (!cableDragEnding)
2345 QGraphicsScene::mouseReleaseEvent(event);
2346 }
2347
2348 // Handle non-left-button releases.
2349 else // if (event->button() != Qt::LeftButton)
2350 {
2351 QGraphicsScene::mouseReleaseEvent(event);
2352 }
2353}
2354
2361void VuoEditorComposition::concludeCableDrag(QGraphicsSceneMouseEvent *event)
2362{
2363 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2364
2365 // Conclude drags of published cables as a special case.
2366 // @todo: Integrate this more naturally.
2367 if (cableInProgress->isPublished())
2368 {
2369 concludePublishedCableDrag(event);
2370 return;
2371 }
2372
2373 if (hasFeedbackErrors())
2374 {
2377 return;
2378 }
2379
2380 cableInProgress->getRenderer()->setWireless(cableInProgressShouldBeWireless);
2381
2382 // Input or output port that the cable is being dragged from (i.e., the fixed endpoint).
2383 VuoRendererPort *fixedPort = NULL;
2384 if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2385 fixedPort = cableInProgress->getFromPort()->getRenderer();
2386 else if (cableInProgress->getToNode() && cableInProgress->getToPort())
2387 fixedPort = cableInProgress->getToPort()->getRenderer();
2388
2389 // We will determine based on the presence or absence of an eligible port near the
2390 // location of the mouse release whether to connect or delete the dragged cable.
2391 bool completedCableConnection = false;
2392
2393 // Potential side effects of a newly completed cable connection:
2394 VuoCable *dataCableToDisplace = NULL; // A previously existing incoming data+event cable may need to be displaced.
2395 VuoCable *cableToReplace = NULL; // A previously existing cable connecting the same two ports may need to be replaced.
2396 VuoRendererNode *typecastNodeToDelete = NULL; // A previously attached typecast may need to be deleted.
2397 VuoRendererPort *portToUnpublish = NULL; // A previously published port may need to be unpublished.
2398 string typecastToInsert = ""; // A typecast may need to be automatically inserted.
2399
2400 // A generic port involved in the new connection may need to be specialized.
2401 VuoRendererPort *portToSpecialize = NULL;
2402 string specializedTypeName = "";
2403
2404 // Input or output port that the cable is being dropped onto, if any.
2405 VuoRendererPort *targetPort = (VuoRendererPort *)findNearbyPort(event->scenePos(), false);
2406
2407 // Node header area that the cable is being dropped onto, if any.
2408 // (If over both a port drop zone and a node header, the node header gets precedence.)
2409 {
2410 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2411 if (targetNode)
2412 {
2413 targetPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2414 if (targetPort)
2415 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2416 }
2417 }
2418
2419 bool draggingPreviouslyPublishedCable = (!cableInProgressWasNew &&
2420 (dynamic_cast<VuoRendererPublishedPort *>(fixedPort) ||
2421 ((cableInProgress->getToPort() == NULL) &&
2422 dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort()->getRenderer()))));
2423
2424 if (fixedPort && targetPort)
2425 {
2426 // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2427 // don't push the operation onto the Undo stack.
2428 bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2429 targetPort->getBase()) &&
2430 (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2431 cableInProgress->getCompiler()->getAlwaysEventOnly()));
2432 if (recreatingSameConnection)
2433 {
2435 return;
2436 }
2437
2438 bool cableInProgressExpectedToCarryData = cableInProgress->getRenderer()->effectivelyCarriesData() &&
2439 targetPort->getDataType();
2440
2441 VuoCable *preexistingCable = fixedPort->getCableConnectedTo(targetPort, false);
2442 bool preexistingCableWithMatchingDataCarryingStatus = (preexistingCable?
2443 (preexistingCable->getRenderer()->effectivelyCarriesData() ==
2444 cableInProgressExpectedToCarryData) :
2445 false);
2446
2447 // Case: Replacing a pre-existing cable that connected the same two ports
2448 // but with a different data-carrying status, and whose "To" port is
2449 // the child port of a collapsed typecast.
2450 if (preexistingCable && !preexistingCableWithMatchingDataCarryingStatus &&
2451 preexistingCable->getToPort()->hasRenderer() &&
2452 preexistingCable->getToPort()->getRenderer()->getTypecastParentPort())
2453 {
2454 // @todo Implement for https://b33p.net/kosada/node/14153
2455 // For now, don't attempt it.
2457 return;
2458 }
2459
2460 // Case: Completing a "forward" cable connection from an output port to an input port
2461 if (!preexistingCableWithMatchingDataCarryingStatus &&
2462 (fixedPort->canConnectDirectlyWithoutSpecializationTo(targetPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2463 selectBridgingSolution(fixedPort, targetPort, true, &portToSpecialize, specializedTypeName, typecastToInsert)))
2464 {
2465 // If input port had a connected collapsed typecast, uncollapse it.
2466 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2467 if (typecastPort)
2468 {
2469 VuoRendererPort *adjustedTargetPort = typecastPort->getReplacedPort();
2470 VuoRendererPort *childPort = typecastPort->getChildPort();
2471 VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2472 VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2473
2474 if (portToSpecialize == targetPort)
2475 portToSpecialize = adjustedTargetPort;
2476
2477 targetPort = adjustedTargetPort;
2478
2479 // If the typecast did not have multiple incoming cables, and the new cable will
2480 // be replacing it as a data source, delete the typecast.
2481 if (cableInProgressExpectedToCarryData &&
2482 childPort->getBase()->getConnectedCables(true).size() < 2 &&
2483 (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2484 typecastNodeToDelete = uncollapsedTypecast;
2485 }
2486
2487 // If connecting two ports that already had a cable (of different data-carrying status) connecting them,
2488 // replace the pre-existing cable.
2489 if (preexistingCable)
2490 cableToReplace = preexistingCable;
2491
2492 // If the cable carries data, determine what other sources of input data need to be
2493 // removed before completing this connection.
2494 if (cableInProgressExpectedToCarryData)
2495 {
2496 // If input port already had a connected data cable, delete that cable.
2497 vector<VuoCable *> previousConnectedCables = targetPort->getBase()->getConnectedCables(false);
2498 for (vector<VuoCable *>::iterator cable = previousConnectedCables.begin(); (! dataCableToDisplace) && (cable != previousConnectedCables.end()); ++cable)
2499 if ((((*cable)->getRenderer()->effectivelyCarriesData()) && (*cable) != cableToReplace))
2500 dataCableToDisplace = *cable;
2501
2502 // If the input port was published as a data+event port, unpublish it.
2503 // @todo: Let published cable disconnection handle port unpublication.
2504 if (isPortPublished(targetPort))
2505 {
2506 vector<VuoRendererPublishedPort *> publishedDataConnections = targetPort->getPublishedPortsConnectedByDataCarryingCables();
2507 if (publishedDataConnections.size() > 0)
2508 portToUnpublish = targetPort;
2509 }
2510 }
2511
2512 completedCableConnection = true;
2513 }
2514
2515 // Case: Completing a "backward" cable connection from an input port to an output port
2516 else if (!preexistingCableWithMatchingDataCarryingStatus &&
2517 (targetPort->canConnectDirectlyWithoutSpecializationTo(fixedPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2518 selectBridgingSolution(targetPort, fixedPort, false, &portToSpecialize, specializedTypeName, typecastToInsert)))
2519
2520 {
2521 completedCableConnection = true;
2522 }
2523 }
2524
2525 // Complete the actual cable connection, if applicable.
2526 if (completedCableConnection)
2527 {
2528 emit undoStackMacroBeginRequested("Cable Connection");
2529
2530 if (draggingPreviouslyPublishedCable)
2531 {
2532 // Record some information about the cable in progress, to create a replica.
2533 VuoNode *cableInProgressFromNode = cableInProgress->getFromNode();
2534 VuoNode *cableInProgressToNode = cableInProgress->getToNode();
2535 VuoPort *cableInProgressFromPort = cableInProgress->getFromPort();
2536 VuoPort *cableInProgressToPort = cableInProgress->getToPort();
2537 QPointF cableInProgressFloatingEndpointLoc = cableInProgress->getRenderer()->getFloatingEndpointLoc();
2538 bool cableInProgressAlwaysEventOnly = cableInProgress->getCompiler()->getAlwaysEventOnly();
2539
2540 // Remove the original cable, unpublishing the port in the process.
2542
2543 // Restore a copy of the cable to participate in the new connection.
2544 VuoCable *cableInProgressCopy = (new VuoCompilerCable(NULL,
2545 NULL,
2546 NULL,
2547 NULL))->getBase();
2548
2549 cableInProgressCopy->setFrom(cableInProgressFromNode, cableInProgressFromPort);
2550 cableInProgressCopy->setTo(cableInProgressToNode, cableInProgressToPort);
2551
2552 addCable(cableInProgressCopy);
2553
2554 cableInProgressCopy->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
2555 cableInProgressCopy->getRenderer()->setFloatingEndpointLoc(cableInProgressFloatingEndpointLoc);
2556
2557 cableInProgress = cableInProgressCopy;
2558 cableInProgressWasNew = true;
2559 }
2560
2561 // Workaround to avoid Qt's parameter count limit for signals/slots.
2562 pair<VuoRendererCable *, VuoRendererCable *> cableArgs = std::make_pair((dataCableToDisplace? dataCableToDisplace->getRenderer() : NULL),
2563 (cableToReplace? cableToReplace->getRenderer() : NULL));
2564 pair<string, string> typeArgs = std::make_pair(typecastToInsert, specializedTypeName);
2565 pair<VuoRendererPort *, VuoRendererPort *> portArgs = std::make_pair(portToUnpublish, portToSpecialize);
2566
2567 emit connectionCompletedByDragging(cableInProgress->getRenderer(),
2568 targetPort,
2569 cableArgs,
2570 typecastNodeToDelete,
2571 typeArgs,
2572 portArgs);
2573
2575
2576 // Resume caching for dragged cable.
2577 if (cableInProgress)
2578 {
2579 cableInProgress->getRenderer()->updateGeometry();
2580 cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2581 cableInProgress = NULL;
2582 }
2583 }
2584
2585 // Otherwise, delete the cable.
2586 else // if (! completedCableConnection)
2588
2589 emit cableDragEnded();
2590}
2591
2595void VuoEditorComposition::concludePublishedCableDrag(QGraphicsSceneMouseEvent *event)
2596{
2597 VuoRendererPort *fixedPort;
2598 if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2599 fixedPort = cableInProgress->getFromPort()->getRenderer();
2600 else // if (cableInProgress->getToNode() && cableInProgress->getToPort())
2601 fixedPort = cableInProgress->getToPort()->getRenderer();
2602
2603 // Potential side effects of a newly completed cable connection:
2604 // - A typecast may need to be automatically inserted.
2605 string typecastToInsert = "";
2606
2607 // - A generic port involved in the new connection may need to be specialized.
2608 VuoRendererPort *portToSpecialize = NULL;
2609 string specializedTypeName = "";
2610
2611 bool forceEventOnlyPublication = !cableInProgress->getRenderer()->effectivelyCarriesData();
2612
2613 // Case: Initiating a cable drag from a published input port.
2614 if (cableInProgress && cableInProgress->isPublishedInputCable())
2615 {
2616 VuoRendererPort *internalInputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2617 VuoRendererPublishedPort *publishedInputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getFromPort()->getRenderer());
2618
2619 // If the cable was dropped onto a node header area, connect an event-only cable to the node's first input port.
2620 // (If over both a port drop zone and a node header, the node header gets precedence.)
2621 {
2622 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2623 if (targetNode)
2624 {
2625 internalInputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2626 if (internalInputPort)
2627 {
2628 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2629 forceEventOnlyPublication = true;
2630 }
2631 }
2632 }
2633
2634 // Case: Cable was dropped onto an internal input port
2635 if (internalInputPort &&
2636 publishedInputPort)
2637 {
2638 // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2639 // don't push the operation onto the Undo stack.
2640 bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2641 internalInputPort->getBase()) &&
2642 (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2643 cableInProgress->getCompiler()->getAlwaysEventOnly()));
2644 if (recreatingSameConnection)
2645 {
2647 return;
2648 }
2649
2650 // Case: Ports are compatible
2651 if ((publishedInputPort->isCompatibleAliasWithSpecializationForInternalPort(internalInputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName))
2652 || selectBridgingSolution(publishedInputPort, internalInputPort, true, &portToSpecialize, specializedTypeName, typecastToInsert))
2653 {
2654 bool cableInProgressExpectedToCarryData = (cableInProgress->getRenderer()->effectivelyCarriesData() &&
2655 internalInputPort->getDataType());
2656 VuoCable *cableToReplace = publishedInputPort->getCableConnectedTo(internalInputPort, true);
2657 bool cableToReplaceHasMatchingDataCarryingStatus = (cableToReplace?
2658 (cableToReplace->getRenderer()->effectivelyCarriesData() ==
2659 cableInProgressExpectedToCarryData) :
2660 false);
2661
2662 // If replacing a preexisting cable with an identical cable, just cancel the operation
2663 // so that it doesn't go onto the Undo stack.
2664 if (cableToReplace && cableToReplaceHasMatchingDataCarryingStatus)
2666
2667 // Case: Replacing a pre-existing cable that connected the same two ports
2668 // but with a different data-carrying status, and whose "To" port is
2669 // the child port of a collapsed typecast.
2670 else if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus &&
2671 cableToReplace->getToPort()->hasRenderer() &&
2672 cableToReplace->getToPort()->getRenderer()->getTypecastParentPort())
2673 {
2674 // @todo Implement for https://b33p.net/kosada/node/14153
2675 // For now, don't attempt it.
2677 }
2678
2679 else
2680 {
2681 emit undoStackMacroBeginRequested("Cable Connection");
2683
2684 // If this source/target port combination already a cable connecting them, but of a different
2685 // data-carrying status, replace the old cable with the new one.
2686 if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus)
2687 {
2688 QList<QGraphicsItem *> removedComponents;
2689 removedComponents.append(cableToReplace->getRenderer());
2690 emit componentsRemoved(removedComponents, "Delete");
2691 }
2692
2693 // If the target port had a connected collapsed typecast, uncollapse and delete it.
2694 // But first check whether the collapsed typecast is still present on canvas -- it's possible it was
2695 // already removed during the cancelCableDrag() call, if the cable being dragged was the
2696 // typecast's incoming data+event cable.
2697 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(internalInputPort);
2698 if (typecastPort && typecastPort->scene())
2699 {
2700 VuoRendererPort *childPort = typecastPort->getChildPort();
2701 VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2702 VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2703
2704 // If the typecast did not have multiple incoming cables, and the new cable will
2705 // be replacing it as a data source, delete the typecast.
2706 if (cableInProgressExpectedToCarryData &&
2707 childPort->getBase()->getConnectedCables(true).size() < 2 &&
2708 (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2709 {
2710 QList<QGraphicsItem *> removedComponents;
2711 removedComponents.append(uncollapsedTypecast);
2712 emit componentsRemoved(removedComponents, "Delete");
2713 }
2714 }
2715
2716 emit portPublicationRequested(internalInputPort->getBase(),
2717 dynamic_cast<VuoPublishedPort *>(publishedInputPort->getBase()),
2718 forceEventOnlyPublication,
2719 (portToSpecialize? portToSpecialize->getBase() : NULL),
2720 specializedTypeName,
2721 typecastToInsert,
2722 false); // Avoid nested Undo stack macros.
2724 }
2725 }
2726 else // Source port and target port aren't compatible
2728 }
2729 else // Cable was dropped over empty canvas space
2731 }
2732
2733 // Case: Initiating a cable drag from a published output port.
2734 else if (cableInProgress && cableInProgress->isPublishedOutputCable())
2735 {
2736 VuoRendererPort *internalOutputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2737 VuoRendererPublishedPort *publishedOutputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getToPort()->getRenderer());
2738
2739 // If the cable was dropped onto a node header area, connect an event-only cable to the node's first output port.
2740 // (If over both a port drop zone and a node header, the node header gets precedence.)
2741 {
2742 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2743 if (targetNode)
2744 {
2745 internalOutputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2746 if (internalOutputPort)
2747 {
2748 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2749 forceEventOnlyPublication = true;
2750 }
2751 }
2752 }
2753
2754 // Case: Cable was dropped onto an internal output port
2755 if (internalOutputPort &&
2756 publishedOutputPort)
2757 {
2758
2759 // Case: Ports are compatible
2760 if ((publishedOutputPort->isCompatibleAliasWithSpecializationForInternalPort(internalOutputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName) &&
2761 publishedOutputPort->canAccommodateInternalPort(internalOutputPort, forceEventOnlyPublication))
2762 || selectBridgingSolution(internalOutputPort, publishedOutputPort, false, &portToSpecialize, specializedTypeName, typecastToInsert))
2763 {
2764 emit portPublicationRequested(internalOutputPort->getBase(),
2765 dynamic_cast<VuoPublishedPort *>(publishedOutputPort->getBase()),
2766 forceEventOnlyPublication,
2767 portToSpecialize? portToSpecialize->getBase() : NULL,
2768 specializedTypeName,
2769 typecastToInsert,
2770 true);
2771 }
2772 }
2773
2775 }
2776
2777 emit cableDragEnded();
2778}
2779
2784{
2786
2787 if (! cableInProgress)
2788 return;
2789
2790 if (cableInProgressWasNew)
2791 removeCable(cableInProgress->getRenderer());
2792 else
2793 {
2794 QList<QGraphicsItem *> removedComponents;
2795 removedComponents.append(cableInProgress->getRenderer());
2796 emit componentsRemoved(removedComponents, "Delete");
2797 }
2798
2799 cableInProgress = NULL;
2800}
2801
2809{
2810 if (cableInProgress && cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort())
2811 {
2812 cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2813 cableInProgress->getRenderer()->updateGeometry();
2814 cableInProgress->setTo(getUnderlyingParentNodeForPort(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort(), this),
2815 cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort());
2816 cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2817 cableInProgress = NULL;
2818 }
2819 else if (cableInProgress)
2820 {
2822 }
2823
2826 emit cableDragEnded();
2827}
2828
2832void VuoEditorComposition::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
2833{
2834 bool leftMouseButtonPressed = (event->buttons() & Qt::LeftButton);
2835
2836 // Disable Mission Control workaround, since it sometimes misdetects whether the left mouse
2837 // button is pressed and overzealously cancels cable drags (specifically, those originating from
2838 // sidebar published output ports). The workaround no longer appears to be effective as of Qt 5.2.1 anyway.
2839 /*
2840 // In the unlikely situation that we have a cable drag in progress but the left mouse
2841 // button is not pressed (e.g., if "Mission Control" was activated during a cable drag and
2842 // deactivated by releasing the mouse button), cancel the cable drag.
2843 // See https://b33p.net/kosada/node/3305
2844 if (cableInProgress && !menuSelectionInProgress && !leftMouseButtonPressed)
2845 cancelCableDrag();
2846 */
2847
2848 // If in the process of a cable drag or mouse-over operation,
2849 // locate the nearest port or cable eligible for hover highlighting.
2850 if (cableInProgress || (! leftMouseButtonPressed))
2851 updateHoverHighlighting(event->scenePos());
2852
2853
2854 // Case: Left mouse button pressed
2855 if (leftMouseButtonPressed)
2856 {
2857 // Eliminate mouse jitter noise.
2858 if ((! dragStickinessDisabled) && (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2859 return;
2860
2861 else
2862 dragStickinessDisabled = true;
2863
2864 // If a port or cable has been clicked for dragging and this is the first mouse move event
2865 // since the click, initiate the cable drag.
2866 if (portWithDragInitiated || cableWithYankInitiated)
2867 {
2868 initiateCableDrag(portWithDragInitiated, cableWithYankInitiated, event);
2869 portWithDragInitiated = NULL;
2870 cableWithYankInitiated = NULL;
2871 }
2872
2873 // If there is a cable drag in progress, update the location of the cable's
2874 // floating endpoint to match that of the cursor.
2875 if (cableInProgress)
2876 {
2877 VuoRendererCable *rc = cableInProgress->getRenderer();
2878
2879 rc->updateGeometry();
2880 rc->setFloatingEndpointLoc(event->scenePos());
2881
2883 }
2884
2885 else if (duplicationDragInProgress)
2886 {
2887 if (duplicateOnNextMouseMove)
2888 {
2890 duplicateOnNextMouseMove = false;
2891 }
2892
2893 QPointF newPos = (VuoRendererItem::getSnapToGrid()?
2896 event->scenePos());
2897 QPointF delta = newPos - cursorPosBeforeDuplicationDragMove;
2898 moveItemsBy(getSelectedNodes(), getSelectedComments(), delta.x(), delta.y(), false);
2899 cursorPosBeforeDuplicationDragMove = newPos;
2900 }
2901
2902 else
2903 QGraphicsScene::mouseMoveEvent(event);
2904 }
2905
2906 // Case: Left mouse button not pressed
2907 else
2908 QGraphicsScene::mouseMoveEvent(event);
2909}
2910
2916void VuoEditorComposition::updateHoverHighlighting(QPointF scenePos, bool disablePortHoverHighlighting)
2917{
2918 // Detect cable and port hover events ourselves, since we need to account
2919 // for their extended hover ranges.
2920 QGraphicsItem *item = cableInProgress? findNearbyPort(scenePos, false) : findNearbyComponent(scenePos);
2921 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
2922 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2923 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(item);
2924 VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(item);
2925
2926 // If hovering over a node header area while dragging a cable, treat it like hovering over the first input or output port.
2927 // (If over both a port drop zone and a node header, the node header gets precedence.)
2928 bool hoveringOverNodeHeader = false;
2929 if (cableInProgress)
2930 {
2931 VuoRendererNode *targetNode = findNearbyNodeHeader(scenePos);
2932 if (targetNode)
2933 {
2935 if (port)
2936 {
2937 item = targetNode;
2938 hoveringOverNodeHeader = true;
2939 }
2940 }
2941 }
2942
2943 VuoRendererNode *node = NULL;
2944 if (! hoveringOverNodeHeader)
2945 {
2946 // Do not handle hover events for (non-drawer) nodes here.
2947 if (dynamic_cast<VuoRendererNode *>(item) && !makeListDrawer)
2948 item = NULL;
2949
2950 node = dynamic_cast<VuoRendererNode *>(item);
2951 }
2952
2953 // Case: The item (if any) that requires hover-highlighting given the current position of the cursor
2954 // is not the same as the item (if any) that was hover-highlighted previously.
2955 if (item != previousNearbyItem)
2956 {
2957 VuoRendererPort *previousPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2958 VuoRendererNode *previousNode = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2959 if (previousNode)
2960 previousPort = findTargetPortForCableDroppedOnNodeHeader(previousNode);
2961
2962 // End hover-highlighting of the previous item, if any.
2963 if (previousNearbyItem)
2965
2966 // Begin hover-highlighting of the current item, if any.
2967 if (cable)
2968 cable->extendedHoverEnterEvent();
2969 else if (node)
2970 {
2971 if (! cableInProgress)
2972 {
2973 if (makeListDrawer)
2974 makeListDrawer->extendedHoverEnterEvent(scenePos);
2975 }
2976 }
2977 else if (typecastPort && !disablePortHoverHighlighting)
2978 port->extendedHoverEnterEvent((bool)cableInProgress);
2979 else if (port && !disablePortHoverHighlighting)
2980 {
2981 port->extendedHoverEnterEvent((bool)cableInProgress);
2982 if (!cableInProgress)
2983 {
2984 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2985 connectedAntennaPort->extendedHoverEnterEvent((bool)cableInProgress, true);
2986 }
2987 }
2988
2989 VuoRendererPort *previousTargetPort = (previousPort && previousPort->isEligibleForConnection() ? previousPort : NULL);
2990
2991 // If the current item or previous item is a port and it got this status because the mouse hovered
2992 // over a node header, update the eligibility-highlighting of the port.
2993 if (cableInProgress)
2994 {
2995 VuoRendererPort *fixedPort = (cableInProgress->getFromPort() ?
2996 cableInProgress->getFromPort()->getRenderer() :
2997 cableInProgress->getToPort()->getRenderer());
2998
2999 QList< QPair<VuoRendererPort *, bool> > updatedPorts;
3000 if (hoveringOverNodeHeader)
3001 updatedPorts.append( QPair<VuoRendererPort *, bool>(port, true) );
3002 if (previousNode && previousPort)
3003 updatedPorts.append( QPair<VuoRendererPort *, bool>(previousPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) );
3004
3005 QPair<VuoRendererPort *, bool> p;
3006 foreach (p, updatedPorts)
3007 {
3008 VuoRendererPort *updatedPort = p.first;
3009
3010 VuoRendererPort *typecastParentPort = updatedPort->getTypecastParentPort();
3011 if (typecastParentPort)
3012 updatedPort = typecastParentPort;
3013
3014 updateEligibilityHighlightingForPort(updatedPort, fixedPort, p.second);
3015
3016 VuoRendererNode *potentialDrawer = updatedPort->getUnderlyingParentNode();
3017 updateEligibilityHighlightingForNode(potentialDrawer);
3018 }
3019 }
3020
3021 VuoRendererPort *targetPort = (port && port->isEligibleForConnection() ? port : NULL);
3022
3023 // Update error dialogs and cable's data-carrying status.
3024 if (targetPort || previousTargetPort)
3025 {
3026 if (cableInProgress)
3027 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(targetPort && (!targetPort->getDataType() || hoveringOverNodeHeader));
3028
3029 updateFeedbackErrors(targetPort);
3030 }
3031
3032 previousNearbyItem = item;
3033 }
3034
3035 // Case: The previously hover-highlighted item should remain hover-highlighted.
3036 else if (item)
3037 {
3038 if (cable)
3039 cable->extendedHoverMoveEvent();
3040 else if (port && !disablePortHoverHighlighting)
3041 {
3042 port->extendedHoverMoveEvent((bool)cableInProgress);
3043 if (!cableInProgress)
3044 {
3045 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
3046 connectedAntennaPort->extendedHoverMoveEvent((bool)cableInProgress, true);
3047 }
3048 }
3049 else if (makeListDrawer)
3050 makeListDrawer->extendedHoverMoveEvent(scenePos);
3051 }
3052}
3053
3058{
3059 if (previousNearbyItem)
3060 {
3061 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(previousNearbyItem);
3062 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3063 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(previousNearbyItem);
3064 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
3065
3066 if (node)
3067 {
3068 VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(previousNearbyItem);
3069 if (drawer)
3070 drawer->extendedHoverLeaveEvent();
3071 else
3073 }
3074
3075 if (cable)
3076 cable->extendedHoverLeaveEvent();
3077 else if (typecastPort)
3079 else if (port)
3080 {
3082 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
3083 connectedAntennaPort->extendedHoverLeaveEvent();
3084 }
3085
3086 previousNearbyItem = NULL;
3087 }
3088}
3089
3094{
3095 if ((event->key() != Qt::Key_Alt) && (event->key() != Qt::Key_Shift) && (event->key() != Qt::Key_Escape))
3097
3098 Qt::KeyboardModifiers modifiers = event->modifiers();
3099 qreal adjustedNodeMoveRate = nodeMoveRate;
3100 if (modifiers & Qt::ShiftModifier)
3101 {
3102 adjustedNodeMoveRate *= nodeMoveRateMultiplier;
3103 }
3104
3105 switch (event->key()) {
3106 case Qt::Key_Backspace:
3107 {
3109 break;
3110 }
3111 case Qt::Key_Delete:
3112 {
3114 break;
3115 }
3116 case Qt::Key_Up:
3117 {
3118 moveSelectedItemsBy(0, -1*adjustedNodeMoveRate);
3119 break;
3120 }
3121 case Qt::Key_Down:
3122 {
3123 if (modifiers & Qt::ControlModifier)
3124 openSelectedEditableNodes();
3125 else
3126 moveSelectedItemsBy(0, adjustedNodeMoveRate);
3127 break;
3128 }
3129 case Qt::Key_Left:
3130 {
3131 moveSelectedItemsBy(-1*adjustedNodeMoveRate, 0);
3132 break;
3133 }
3134 case Qt::Key_Right:
3135 {
3136 moveSelectedItemsBy(adjustedNodeMoveRate, 0);
3137 break;
3138 }
3139 case Qt::Key_Shift:
3140 {
3141 if (cableInProgress)
3142 {
3143 cableInProgress->getRenderer()->updateGeometry();
3144 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
3145 highlightEligibleEndpointsForCable(cableInProgress);
3146
3147 VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3148 if (hoveredPort)
3149 updateFeedbackErrors(hoveredPort);
3150 }
3151
3152 break;
3153 }
3154 case Qt::Key_Enter:
3155 case Qt::Key_Return:
3156 {
3157 // Make sure the event is forwarded to any composition component within hover range.
3158 QGraphicsScene::keyPressEvent(event);
3159
3160 if (!event->isAccepted())
3161 {
3162 // Otherwise, if there are any (and only) nodes currently selected, open a title editor for each one;
3163 // if there are any (and only) comments currently selected, open a comment text editor for each one.
3164 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
3165 set<VuoRendererComment *> selectedComments = getSelectedComments();
3166
3167 if (selectedComments.empty() && !selectedNodes.empty())
3169 }
3170
3171 break;
3172 }
3173
3174 case Qt::Key_Escape:
3175 {
3176 if (duplicateOnNextMouseMove || duplicationDragInProgress)
3177 {
3179
3180 duplicateOnNextMouseMove = false;
3181 duplicationDragInProgress = false;
3182 duplicationCancelled = true;
3183 }
3184 else if (cableInProgress)
3185 {
3187 }
3188 break;
3189 }
3190
3191 default:
3192 {
3193 QGraphicsScene::keyPressEvent(event);
3194 break;
3195 }
3196 }
3197}
3198
3203{
3204 switch (event->key()) {
3205 case Qt::Key_Shift:
3206 {
3207 if (cableInProgress)
3208 {
3209 cableInProgress->getRenderer()->updateGeometry();
3210 cableInProgress->getCompiler()->setAlwaysEventOnly(false);
3211 highlightEligibleEndpointsForCable(cableInProgress);
3212
3213 VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3214 if (hoveredPort)
3215 updateFeedbackErrors(hoveredPort);
3216 }
3217
3218 break;
3219 }
3220
3221 default:
3222 {
3223 QGraphicsScene::keyReleaseEvent(event);
3224 break;
3225 }
3226 }
3227}
3228
3232void VuoEditorComposition::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
3233{
3234 // Determine whether the cursor is in range of any operable composition components.
3235 QGraphicsItem *item = findNearbyComponent(event->scenePos());
3236
3238 contextMenu.setSeparatorsCollapsible(false);
3239
3240 // Customize context menu for the canvas.
3241 if (! item)
3242 {
3243 QAction *insertNodeSection = new QAction(tr("Insert Node"), NULL);
3244 insertNodeSection->setEnabled(false);
3245 contextMenu.addAction(insertNodeSection);
3246
3247 QString spacer(" ");
3248
3249 {
3250 // "Share" nodes
3251 QMenu *shareMenu = new QMenu(&contextMenu);
3252 shareMenu->setSeparatorsCollapsible(false);
3253 shareMenu->setTitle(spacer + tr("Share"));
3254 contextMenu.addMenu(shareMenu);
3255
3256 {
3257 QAction *action = new QAction(tr("Share Value"), NULL);
3258 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share"))));
3259 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3260 shareMenu->addAction(action);
3261 }
3262
3263 {
3264 QAction *action = new QAction(tr("Share List"), NULL);
3265 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share.list"))));
3266 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3267 shareMenu->addAction(action);
3268 }
3269
3270 {
3271 QAction *action = new QAction(tr("Share Event"), NULL);
3272 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.share"))));
3273 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3274 shareMenu->addAction(action);
3275 }
3276 }
3277
3278 {
3279 // "Hold" nodes
3280 QMenu *holdMenu = new QMenu(&contextMenu);
3281 holdMenu->setSeparatorsCollapsible(false);
3282 holdMenu->setTitle(spacer + tr("Hold"));
3283 contextMenu.addMenu(holdMenu);
3284
3285 {
3286 QAction *action = new QAction(tr("Hold Value"), NULL);
3287 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold2"))));
3288 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3289 holdMenu->addAction(action);
3290 }
3291
3292 {
3293 QAction *action = new QAction(tr("Hold List"), NULL);
3294 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold.list2"))));
3295 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3296 holdMenu->addAction(action);
3297 }
3298 }
3299
3300 {
3301 // "Allow" nodes
3302 QMenu *allowMenu = new QMenu(&contextMenu);
3303 allowMenu->setSeparatorsCollapsible(false);
3304 allowMenu->setTitle(spacer + tr("Allow"));
3305 contextMenu.addMenu(allowMenu);
3306
3307 {
3308 QAction *action = new QAction(tr("Allow First Event"), NULL);
3309 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirst"))));
3310 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3311 allowMenu->addAction(action);
3312 }
3313
3314 {
3315 QAction *action = new QAction(tr("Allow First Value"), NULL);
3316 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirstValue"))));
3317 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3318 allowMenu->addAction(action);
3319 }
3320
3321 {
3322 QAction *action = new QAction(tr("Allow Periodic Events"), NULL);
3323 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.time.allowPeriodic"))));
3324 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3325 allowMenu->addAction(action);
3326 }
3327
3328 {
3329 QAction *action = new QAction(tr("Allow Changes"), NULL);
3330 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowChanges2"))));
3331 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3332 allowMenu->addAction(action);
3333 }
3334 }
3335
3336 contextMenu.addSeparator();
3337
3338 QAction *contextMenuInsertComment = new QAction(NULL);
3339 contextMenuInsertComment->setText(tr("Insert Comment"));
3340 contextMenuInsertComment->setData(event->scenePos());
3341 connect(contextMenuInsertComment, &QAction::triggered, this, &VuoEditorComposition::insertComment);
3342 contextMenu.addAction(contextMenuInsertComment);
3343
3344 QAction *contextMenuInsertSubcomposition = new QAction(NULL);
3345 contextMenuInsertSubcomposition->setText(tr("Insert Subcomposition"));
3346 contextMenuInsertSubcomposition->setData(event->scenePos());
3347 connect(contextMenuInsertSubcomposition, &QAction::triggered, this, &VuoEditorComposition::insertSubcomposition);
3348 contextMenu.addAction(contextMenuInsertSubcomposition);
3349 }
3350
3351 // Prepare to take a snapshot of the contextMenuDeleteSelection QAction's current values, so that
3352 // they do not change while the context menu is displayed.
3353 QAction *contextMenuDeleteSelectedSnapshot = new QAction(NULL);
3354
3355 // Customize context menu for ports.
3356 if (dynamic_cast<VuoRendererPort *>(item))
3357 {
3358 VuoRendererPort *port = (VuoRendererPort *)item;
3359
3360 if (port->isConstant() && inputEditorManager)
3361 {
3362 VuoType *dataType = static_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler())->getDataVuoType();
3363 VuoInputEditor *inputEditorLoadedForPortDataType = inputEditorManager->newInputEditor(dataType);
3364 if (inputEditorLoadedForPortDataType)
3365 {
3366 contextMenuSetPortConstant->setData(QVariant::fromValue(port));
3367 contextMenu.addAction(contextMenuSetPortConstant);
3368
3369 inputEditorLoadedForPortDataType->deleteLater();
3370 }
3371 }
3372
3373 if (!isPortPublished(port) && port->getPublishable())
3374 {
3375 contextMenuPublishPort->setText(tr("Publish Port"));
3376 contextMenuPublishPort->setData(QVariant::fromValue(port));
3377 contextMenu.addAction(contextMenuPublishPort);
3378 }
3379
3380 else if (isPortPublished(port))
3381 {
3382 vector<VuoRendererPublishedPort *> externalPublishedPorts = port->getPublishedPorts();
3383 bool hasExternalPublishedPortWithMultipleInternalPorts = false;
3384 bool hasExternalPublishedPortBelongingToActiveProtocol = false;
3385 foreach (VuoRendererPublishedPort *externalPort, externalPublishedPorts)
3386 {
3387 if (externalPort->getBase()->getConnectedCables(true).size() > 1)
3388 hasExternalPublishedPortWithMultipleInternalPorts = true;
3389
3390 if (dynamic_cast<VuoPublishedPort *>(externalPort->getBase())->isProtocolPort())
3391 hasExternalPublishedPortBelongingToActiveProtocol = true;
3392 }
3393
3394 // Omit the "Delete Published Port" context menu item if the internal port is connected to
3395 // an external published port that cannot be deleted, either because:
3396 // - It is part of an active protocol;
3397 // - It has more than one connected cable.
3398 if (!hasExternalPublishedPortWithMultipleInternalPorts &&!hasExternalPublishedPortBelongingToActiveProtocol)
3399 {
3400 contextMenuPublishPort->setText(tr("Delete Published Port"));
3401 contextMenuPublishPort->setData(QVariant::fromValue(port));
3402 contextMenu.addAction(contextMenuPublishPort);
3403 }
3404 }
3405
3406 bool isTriggerPort = dynamic_cast<VuoCompilerTriggerPort *>(port->getBase()->getCompiler());
3407 if (isTriggerPort || port->getInput())
3408 {
3409 __block bool isTopLevelCompositionRunning = false;
3410 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
3411 {
3412 isTopLevelCompositionRunning = topLevelComposition->isRunning();
3413 });
3414
3415 if (isTopLevelCompositionRunning)
3416 {
3417 contextMenuFireEvent->setData(QVariant::fromValue(port));
3418 contextMenu.addAction(contextMenuFireEvent);
3419 }
3420 }
3421
3422 if (isTriggerPort)
3423 {
3424 QMenu *contextMenuThrottling = new QMenu(&contextMenu);
3425 contextMenuThrottling->setSeparatorsCollapsible(false);
3426 contextMenuThrottling->setTitle(tr("Set Event Throttling"));
3428 foreach (QAction *action, contextMenuThrottlingActions)
3429 {
3430 contextMenuThrottling->addAction(action);
3431 action->setData(QVariant::fromValue(port));
3432 action->setCheckable(true);
3433 action->setChecked( i++ == port->getBase()->getEventThrottling() );
3434 }
3435 contextMenu.addMenu(contextMenuThrottling);
3436 }
3437
3438 // Allow the user to specialize, respecialize, or unspecialize generic data types.
3439 if (dynamic_cast<VuoGenericType *>(port->getDataType()) || isPortCurrentlyRevertible(port))
3440 {
3441 if (contextMenuSpecializeGenericType)
3442 contextMenuSpecializeGenericType->deleteLater();
3443
3444 contextMenuSpecializeGenericType = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3445 contextMenuSpecializeGenericType->setSeparatorsCollapsible(false);
3446 contextMenuSpecializeGenericType->setTitle(tr("Set Data Type"));
3447 contextMenuSpecializeGenericType->setToolTipsVisible(true);
3448
3449 populateSpecializePortMenu(contextMenuSpecializeGenericType, port, true);
3450 contextMenu.addMenu(contextMenuSpecializeGenericType);
3451 }
3452
3453 // Allow the user to hide, unhide, or delete cables connected directly to the port or, if the
3454 // port has a collapsed typecast, to the typecast's child port.
3455 int numVisibleDirectlyConnectedCables = 0;
3456 int numHidableDirectlyConnectedCables = 0;
3457 int numUnhidableDirectlyConnectedCables = 0;
3458
3459 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
3460 {
3461 if (!cable->getRenderer()->paintingDisabled())
3462 {
3463 numVisibleDirectlyConnectedCables++;
3464 if (!cable->isPublished())
3465 numHidableDirectlyConnectedCables++;
3466 }
3467
3468 else if (cable->getRenderer()->getEffectivelyWireless())
3469 numUnhidableDirectlyConnectedCables++;
3470 }
3471
3472 int numVisibleChildPortConnectedCables = 0;
3473 int numHidableChildPortConnectedCables = 0;
3474 int numUnhidableChildPortConnectedCables = 0;
3475 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
3476 if (typecastPort)
3477 {
3478 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
3479 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
3480 foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
3481 {
3482 if (!cable->getRenderer()->paintingDisabled())
3483 {
3484 numVisibleChildPortConnectedCables++;
3485
3486 if (!cable->isPublished())
3487 numHidableChildPortConnectedCables++;
3488 }
3489 else if (cable->getRenderer()->getEffectivelyWireless())
3490 numUnhidableChildPortConnectedCables++;
3491 }
3492 }
3493
3494 // Count the number of directly or indirectly connected cables that are currently visible.
3495 int numVisibleConnectedCables = numVisibleDirectlyConnectedCables + numVisibleChildPortConnectedCables;
3496
3497 // Count the number of directly or indirectly connected cables that are currently hidable.
3498 int numHidableConnectedCables = numHidableDirectlyConnectedCables + numHidableChildPortConnectedCables;
3499
3500 // Count the number of directly or indirectly connected cables that are currently hidden.
3501 int numUnhidableConnectedCables = numUnhidableDirectlyConnectedCables + numUnhidableChildPortConnectedCables;
3502
3503 if ((!renderHiddenCables && ((numHidableConnectedCables >= 1) || (numUnhidableConnectedCables >= 1)))
3504 || (numVisibleConnectedCables >= 1))
3505 {
3506 if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3507 contextMenu.addSeparator();
3508 }
3509
3510 if (!renderHiddenCables)
3511 {
3512 if (numHidableConnectedCables >= 1)
3513 {
3514 // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3515 // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3516 // impact visible published cables.
3517 int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3518 contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3519 contextMenuHideCables->setData(QVariant::fromValue(port));
3520 contextMenu.addAction(contextMenuHideCables);
3521 }
3522
3523 if (numUnhidableConnectedCables >= 1)
3524 {
3525 int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3526 contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3527 contextMenuUnhideCables->setData(QVariant::fromValue(port));
3528 contextMenu.addAction(contextMenuUnhideCables);
3529 }
3530 }
3531
3532 if (numVisibleConnectedCables >= 1)
3533 {
3534 contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3535 contextMenuDeleteCables->setData(QVariant::fromValue(port));
3536 contextMenu.addAction(contextMenuDeleteCables);
3537 }
3538 }
3539
3540 // Customize context menu for nodes, cables, and/or comments.
3541 else if (item)
3542 {
3543 if (! item->isSelected())
3544 {
3545 clearSelection();
3546 item->setSelected(true);
3547 }
3548
3549 QList<QGraphicsItem *> selectedComponents = selectedItems();
3550 bool onlyCommentsSelected = true;
3551 bool onlyCablesSelected = true;
3552 bool selectionContainsMissingNode = false;
3553 foreach (QGraphicsItem *item, selectedComponents)
3554 {
3555 if (!dynamic_cast<VuoRendererComment *>(item))
3556 onlyCommentsSelected = false;
3557
3558 if (!dynamic_cast<VuoRendererCable *>(item))
3559 onlyCablesSelected = false;
3560
3561 if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3562 selectionContainsMissingNode = true;
3563 }
3564
3565 contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3566 connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3567
3568 // Comments
3569 VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3570 if (comment)
3571 {
3572 // Option: Edit selected comments
3573 if (onlyCommentsSelected)
3574 contextMenu.addAction(contextMenuEditSelectedComments);
3575
3576 // Option: Tint selected components(s)
3577 contextMenu.addMenu(getContextMenuTints(&contextMenu));
3578
3579 contextMenu.addSeparator();
3580
3581 // Option: Refactor selected component(s)
3582 contextMenu.addAction(contextMenuRefactorSelected);
3583
3584 contextMenu.addSeparator();
3585
3586 // Option: Delete selected components(s)
3587 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3588 }
3589
3590 // Cables
3591 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3592 if (cable)
3593 {
3594 if (!renderHiddenCables)
3595 {
3596 if (onlyCablesSelected && !cable->getBase()->isPublished())
3597 contextMenu.addAction(contextMenuHideSelectedCables);
3598 }
3599
3600 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3601 }
3602
3603 // Nodes
3604 else if (dynamic_cast<VuoRendererNode *>(item))
3605 {
3606 VuoRendererNode *node = (VuoRendererNode *)item;
3607
3608 // Input drawers
3609 if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3610 node->getBase()->getNodeClass()->hasCompiler())
3611 {
3612 // Offer "Add/Remove Input Port" options for resizable list input drawers.
3613 if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3614 {
3615 contextMenuAddInputPort->setData(QVariant::fromValue(node));
3616 contextMenuRemoveInputPort->setData(QVariant::fromValue(node));
3617
3618 int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3619 contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3620
3621 contextMenu.addAction(contextMenuAddInputPort);
3622 contextMenu.addAction(contextMenuRemoveInputPort);
3623
3624 contextMenu.addSeparator();
3625 }
3626
3627 // Offer "Reset" option for all input drawers.
3628 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3629 }
3630
3631 // Non-input-drawer nodes
3632 else
3633 {
3634 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3635 if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3636 {
3637 string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3638 VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3639 if (originalGenericNodeClass)
3640 nodeClass = originalGenericNodeClass->getBase();
3641 }
3642
3643 QString actionText, sourcePath;
3644 bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3645 bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3646
3647 int numSelectedNonAttachmentNodes = 0;
3648 QList<QGraphicsItem *> selectedComponents = selectedItems();
3649 foreach (QGraphicsItem *item, selectedComponents)
3650 {
3651 if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3652 numSelectedNonAttachmentNodes++;
3653 }
3654
3655 if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3656 {
3657 // Option: Edit an installed subcomposition, shader, or text-code node.
3658 QAction *editAction = new QAction(NULL);
3659 editAction->setText(actionText);
3660 editAction->setData(QVariant::fromValue(node));
3661 connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3662
3663 contextMenu.addAction(editAction);
3664 contextMenu.addSeparator();
3665 }
3666
3667 // Option: Rename selected node(s)
3668 if (node->getBase()->hasCompiler())
3669 contextMenu.addAction(contextMenuRenameSelected);
3670
3671 // Option: Tint selected component(s)
3672 contextMenu.addMenu(getContextMenuTints(&contextMenu));
3673
3674 contextMenu.addSeparator();
3675
3676 // Option: Replace selected node with a similar node
3677 if (numSelectedNonAttachmentNodes == 1)
3678 {
3679 if (contextMenuChangeNode)
3680 contextMenuChangeNode->deleteLater();
3681
3682 contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3683 contextMenuChangeNode->setSeparatorsCollapsible(false);
3684 contextMenuChangeNode->setTitle(tr("Change To"));
3685
3686 populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3687 if (!contextMenuChangeNode->actions().isEmpty())
3688 contextMenu.addMenu(contextMenuChangeNode);
3689 } // End single selected node
3690
3691 // Option: Refactor selected component(s)
3692 if (!selectionContainsMissingNode)
3693 contextMenu.addAction(contextMenuRefactorSelected);
3694
3695 if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3696 {
3697 // Option: Open the enclosing folder for an editable or other 3rd party node class.
3698 QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3699 if (!modulePath.isEmpty())
3700 {
3701 QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3702 QAction *openEnclosingFolderAction = new QAction(NULL);
3703 openEnclosingFolderAction->setText("Show in Finder");
3704 openEnclosingFolderAction->setData(enclosingDirUrl);
3705 connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3706 contextMenu.addAction(openEnclosingFolderAction);
3707 }
3708 }
3709
3710 if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3711 contextMenu.addSeparator();
3712
3713 // Option: Delete selected components(s)
3714 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3715
3716 } // End non-input-drawer nodes
3717 } // End nodes
3718 } // End nodes or cables
3719
3720 if (!contextMenu.actions().isEmpty())
3721 {
3722 // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3724
3725 menuSelectionInProgress = true;
3726 contextMenu.exec(event->screenPos());
3727 menuSelectionInProgress = false;
3728 }
3729
3730 delete contextMenuDeleteSelectedSnapshot;
3731}
3732
3737{
3738 QAction *sender = (QAction *)QObject::sender();
3739 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3740 emit nodeSourceEditorRequested(node);
3741}
3742
3746void VuoEditorComposition::expandSpecializePortMenu()
3747{
3748 QAction *sender = (QAction *)QObject::sender();
3749 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
3750
3751 populateSpecializePortMenu(contextMenuSpecializeGenericType, port, false);
3752 contextMenuSpecializeGenericType->exec();
3753}
3754
3762void VuoEditorComposition::populateSpecializePortMenu(QMenu *menu, VuoRendererPort *port, bool limitInitialOptions)
3763{
3764 menu->clear();
3765
3766 if (!port)
3767 return;
3768
3769 VuoGenericType *genericDataType = dynamic_cast<VuoGenericType *>(port->getDataType());
3770 QAction *unspecializeAction = menu->addAction(tr("Generic"));
3771
3772 // It is safe to assume that there will be types listed after the "Generic" option
3773 // since any network of connected generic/specialized ports has at least one compatible
3774 // data type in common, so the separator can be added here without forward-checking.
3775 menu->addSeparator();
3776
3777 unspecializeAction->setData(QVariant::fromValue(port));
3778 unspecializeAction->setCheckable(true);
3779 unspecializeAction->setChecked(genericDataType);
3780
3781 VuoGenericType *genericTypeFromPortClass = NULL; // Original generic type of the port
3782 set<string> compatibleTypes; // Compatible types in the context of the connected generic port network
3783 set<string> compatibleTypesInIsolation; // Compatible types for the port in isolation
3784
3785 // Allow the user to specialize generic ports.
3786 if (genericDataType)
3787 {
3788 VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(port->getBase()->getClass()->getCompiler());
3789 genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3790
3791 // Determine compatible types in the context of the connected generic port network.
3792 VuoGenericType::Compatibility compatibility;
3793 vector<string> compatibleTypesVector = genericDataType->getCompatibleSpecializedTypes(compatibility);
3794 compatibleTypes = set<string>(compatibleTypesVector.begin(), compatibleTypesVector.end());
3795
3796 // If all types or all list types are compatible, add them to (currently empty) compatibleTypes.
3797 if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3798 {
3799 vector<string> compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3800 compatibleTypes.insert(compatibleTypeNames.begin(), compatibleTypeNames.end());
3801 }
3802 }
3803
3804 // Allow the user to re-specialize already specialized ports.
3805 else if (isPortCurrentlyRevertible(port))
3806 {
3807 map<VuoNode *, string> nodesToReplace;
3808 set<VuoCable *> cablesToDelete;
3809 createReplacementsToUnspecializePort(port->getBase(), false, nodesToReplace, cablesToDelete);
3810 if (cablesToDelete.size() >= 1)
3811 {
3812 // @todo https://b33p.net/kosada/node/8895 : Note that this specialization will break connections.
3813 }
3814
3815 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(
3817 genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(port->getBase()->getClass()) );
3818 compatibleTypes = getRespecializationOptionsForPortInNetwork(port);
3819 }
3820
3821 // Determine compatible types in isolation.
3822 if (genericTypeFromPortClass)
3823 {
3824 // "Make List" drawer child ports require special handling, since technically they are compatible with all types
3825 // but practically they are limited to types compatible with their host ports.
3827 if (drawer)
3828 {
3829 VuoPort *hostPort = drawer->getUnderlyingHostPort();
3830 if (hostPort && hostPort->hasRenderer())
3831 {
3832 VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(hostPort->getClass()->getCompiler());
3833 VuoGenericType *genericHostPortDataType = dynamic_cast<VuoGenericType *>(hostPort->getRenderer()->getDataType());
3834 if (genericHostPortDataType)
3835 genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3836 else if (isPortCurrentlyRevertible(hostPort->getRenderer()))
3837 {
3839 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3840 genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass->getBase()) );
3841 }
3842 }
3843 }
3844
3846 vector<string> compatibleTypesInIsolationVector;
3847 if (genericTypeFromPortClass)
3848 compatibleTypesInIsolationVector = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibilityInIsolation);
3849
3850 // If all types or all list types are compatible, add them to (currently empty) compatibleTypesInIsolationVector.
3851 if (compatibilityInIsolation == VuoGenericType::anyType || compatibilityInIsolation == VuoGenericType::anyListType)
3852 compatibleTypesInIsolationVector = getAllSpecializedTypeOptions(compatibilityInIsolation == VuoGenericType::anyListType);
3853
3854 foreach (string type, compatibleTypesInIsolationVector)
3855 compatibleTypesInIsolation.insert(drawer? VuoType::extractInnermostTypeName(type) : type);
3856 }
3857
3858 // List the compatible types in the menu.
3859 {
3860 // If there are only a handful of eligible types, display them all in a flat menu.
3861 const int maxTypeCountForFlatMenuDisplay = 10;
3862 if (compatibleTypesInIsolation.size() <= maxTypeCountForFlatMenuDisplay)
3863 {
3864 QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, false, "", menu);
3865 addTypeActionsToMenu(actions, menu);
3866 }
3867
3868 // Otherwise, organize them by node set.
3869 else
3870 {
3871 // First list compatible types that don't belong to any specific node set.
3872 QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, "", menu);
3873 addTypeActionsToMenu(actions, menu);
3874
3875 // Now add a submenu for each node set that contains compatible types.
3876 QList<QAction *> allNodeSetActionsToAdd;
3877 for (const string &nodeSet : moduleManager->getKnownNodeSets())
3878 allNodeSetActionsToAdd += getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, nodeSet, menu);
3879
3880 bool usingExpansionMenu = false;
3881 if ((menu->actions().size() > 0) && (allNodeSetActionsToAdd.size() > 0))
3882 {
3883 menu->addSeparator();
3884
3885 if (limitInitialOptions)
3886 {
3887 //: Appears at the bottom of the "Set Data Type" menu when there are additional options to display.
3888 QAction *showMoreAction = menu->addAction(tr("More…"));
3889 showMoreAction->setData(QVariant::fromValue(port));
3890 connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandSpecializePortMenu);
3891 usingExpansionMenu = true;
3892 }
3893 }
3894
3895 if (!usingExpansionMenu)
3896 addTypeActionsToMenu(allNodeSetActionsToAdd, menu);
3897 }
3898
3899 foreach (QAction *action, menu->actions())
3900 {
3901 QMenu *specializeSubmenu = action->menu();
3902 if (specializeSubmenu)
3903 {
3904 foreach (QAction *specializeSubaction, specializeSubmenu->actions())
3905 connect(specializeSubaction, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3906 }
3907 else if (action == unspecializeAction)
3908 connect(action, &QAction::triggered, this, &VuoEditorComposition::unspecializePortType);
3909 else
3910 connect(action, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3911 }
3912 }
3913}
3914
3921{
3922 if (lists)
3923 {
3924 set<string> types = moduleManager->getKnownListTypeNames(true);
3925 return vector<string>(types.begin(), types.end());
3926 }
3927 else
3928 {
3929 vector<string> typeOptions;
3930 for (auto i : moduleManager->getLoadedSingletonTypes(true))
3931 typeOptions.push_back(i.first);
3932
3933 return typeOptions;
3934 }
3935}
3936
3942set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3943{
3944 if (!port)
3945 return {};
3946
3947 VuoGenericType::Compatibility compatibility;
3948 vector<string> compatibleTypes = getBase()->getCompiler()->getCompatibleSpecializedTypesForPort(port->getUnderlyingParentNode()->getBase(),
3949 port->getBase(), compatibility);
3950
3951 if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3952 {
3953 vector<string> allTypes = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3954 return set<string>(allTypes.begin(), allTypes.end());
3955 }
3956 else
3957 return set<string>(compatibleTypes.begin(), compatibleTypes.end());
3958}
3959
3980 set<string> compatibleTypesInIsolation,
3981 set<string> compatibleTypesInContext,
3982 bool limitToNodeSet,
3983 string nodeSetName,
3984 QMenu *menu)
3985{
3986 QList<QAction *> actionsToAddToMenu;
3987
3988 vector<string> compatibleTypesForNodeSetDisplay;
3989 for (const string &typeName : compatibleTypesInIsolation)
3990 {
3991 string innermostTypeName = VuoType::extractInnermostTypeName(typeName);
3992 if (! VuoGenericType::isGenericTypeName(innermostTypeName) &&
3993 // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
3994 innermostTypeName != "VuoUrl" &&
3995 // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
3996 innermostTypeName != "VuoInteraction" &&
3997 innermostTypeName != "VuoInteractionType" &&
3998 innermostTypeName != "VuoUuid" &&
3999 // Hide deprecated types.
4000 innermostTypeName != "VuoIconPosition" &&
4001 innermostTypeName != "VuoMesh" &&
4002 innermostTypeName != "VuoWindowProperty" &&
4003 innermostTypeName != "VuoWindowReference" &&
4004 (! limitToNodeSet || moduleManager->getNodeSetForType(innermostTypeName) == nodeSetName))
4005 compatibleTypesForNodeSetDisplay.push_back(typeName);
4006 }
4007
4008 if (!compatibleTypesForNodeSetDisplay.empty())
4009 {
4010 QMenu *contextMenuNodeSetTypes = NULL;
4011 QList<QAction *> actionsToAddToNodeSetSubmenu;
4012 bool enabledContentAdded = false;
4013
4014 // Populate the "Specialize Type" submenu for the target node set.
4015 if (!nodeSetName.empty())
4016 {
4017 contextMenuNodeSetTypes = new QMenu(menu);
4018 contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
4019 contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
4020 contextMenuNodeSetTypes->setToolTipsVisible(true);
4021 actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
4022 }
4023
4024 for (const string &typeName : compatibleTypesForNodeSetDisplay)
4025 {
4026 QList<QVariant> portAndSpecializedType;
4027 portAndSpecializedType.append(QVariant::fromValue(genericPort));
4028 portAndSpecializedType.append(typeName.c_str());
4029
4030 QAction *specializeAction;
4031 QString typeTitle = formatTypeNameForDisplay(typeName);
4032
4033 // Case: Adding to the node set submenu
4034 if (!nodeSetName.empty())
4035 {
4036 specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
4037 actionsToAddToNodeSetSubmenu.append(specializeAction);
4038 }
4039
4040 // Case: Adding to the top-level action list
4041 else
4042 {
4043 specializeAction = new QAction(typeTitle, menu);
4044 actionsToAddToMenu.append(specializeAction);
4045 }
4046
4047 specializeAction->setData(QVariant(portAndSpecializedType));
4048 specializeAction->setCheckable(true);
4049 specializeAction->setChecked(genericPort && genericPort->getDataType() && (genericPort->getDataType()->getModuleKey() == typeName));
4050
4051 if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
4052 {
4053 specializeAction->setEnabled(false);
4054 //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
4055 specializeAction->setToolTip(tr("To change to this type, disconnect the cable from this port and from this node's other ports of the same type."));
4056 }
4057 else
4058 enabledContentAdded = true;
4059 }
4060
4061 if (contextMenuNodeSetTypes)
4062 {
4063 addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
4064
4065 if (!enabledContentAdded)
4066 contextMenuNodeSetTypes->setEnabled(false);
4067 }
4068 }
4069
4070 QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
4071 return actionListWithPromotions;
4072}
4073
4078QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
4079{
4080 QList<QAction *> modifiedActionList;
4081 foreach (QAction *action, actionList)
4082 {
4083 if (action->menu() && (action->menu()->actions().size() == 1))
4084 {
4085 QAction *singleSubaction = action->menu()->actions().first();
4086 action->menu()->removeAction(singleSubaction);
4087 modifiedActionList.append(singleSubaction);
4088 }
4089 else
4090 modifiedActionList.append(action);
4091 }
4092
4093 return modifiedActionList;
4094}
4095
4099void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
4100{
4101 std::sort(actionList.begin(), actionList.end(),
4102 [](QAction *action1, QAction *action2) { return action1->text().compare(action2->text(), Qt::CaseInsensitive) < 0; });
4103
4104 foreach (QAction *action, actionList)
4105 menu->addAction(action);
4106}
4107
4111void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4112{
4113 if (!now)
4114 return;
4115
4117 if (activeWindow)
4118 emit compositionOnTop(activeWindow->getComposition() == this);
4119}
4120
4124void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4125{
4126 if (ignoreApplicationStateChangeEvents)
4127 return;
4128
4130 if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4131 emit applicationActive(active);
4132}
4133
4142QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4143{
4144 return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4145}
4146
4151{
4152 QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4153 return dynamic_cast<VuoRendererNode *>(item);
4154}
4155
4168QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4169 targetComponentType targetType,
4170 bool limitPortCollisionRange)
4171{
4172 // Determine which types of components to filter out of search results.
4173 bool ignoreCables;
4174 bool ignoreNodes;
4175 bool ignorePorts;
4176 bool ignoreComments;
4177
4178 switch(targetType)
4179 {
4180 case VuoEditorComposition::targetTypePort:
4181 {
4182 ignoreCables = true;
4183 ignoreNodes = true;
4184 ignorePorts = false;
4185 ignoreComments = true;
4186 break;
4187 }
4188 case VuoEditorComposition::targetTypeNodeHeader:
4189 {
4190 ignoreCables = true;
4191 ignoreNodes = true;
4192 ignorePorts = true;
4193 ignoreComments = true;
4194 break;
4195 }
4196 default:
4197 {
4198 ignoreCables = false;
4199 ignoreNodes = false;
4200 ignorePorts = false;
4201 ignoreComments = false;
4202 break;
4203 }
4204 }
4205
4206 // The topmost item under the cursor is not necessarily the one we will return
4207 // (e.g., if the cursor is positioned directly over a node but also within range
4208 // of one of that node's ports), but it will factor in to the decision.
4209 QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4210 if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4211 topmostItemUnderCursor = NULL;
4212
4213 for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4214 {
4215 QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4216 QList<QGraphicsItem *> itemsInRange = items(searchRect);
4217 for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4218 {
4219 if (!(*i)->isEnabled())
4220 continue;
4221
4222 // Check whether we have located an unobscured "Make List" drawer drag handle.
4223 if (! ignoreNodes)
4224 {
4225 VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4226 bool makeListDragHandle =
4227 (makeListDrawer &&
4228 makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4229 if (makeListDragHandle &&
4230 ((! topmostItemUnderCursor) ||
4231 (topmostItemUnderCursor == makeListDrawer) ||
4232 (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4233 {
4234 return makeListDrawer;
4235 }
4236 }
4237
4238 // Check whether we have located an unobscured port.
4239 // Hovering within range of a port takes precedence over hovering
4240 // directly over that port's parent node.
4241 if (! ignorePorts)
4242 {
4243 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4244 if (port &&
4245 ((! topmostItemUnderCursor) ||
4246 (topmostItemUnderCursor == port) ||
4247 (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4248 (topmostItemUnderCursor->zValue() < port->zValue()))
4249 &&
4250 ((! limitPortCollisionRange) ||
4251 port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4252 &&
4253 ! port->getFunctionPort())
4254 {
4255 return port;
4256 }
4257 }
4258
4259 // Check whether we have located an unobscured cable.
4260 if (! ignoreCables)
4261 {
4262 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4263 if (cable &&
4264 ((! topmostItemUnderCursor) ||
4265 (topmostItemUnderCursor == cable) ||
4266 (topmostItemUnderCursor->zValue() < cable->zValue())))
4267 {
4268 return cable;
4269 }
4270 }
4271
4272 // Check whether we have located an unobscured comment.
4273 if (! ignoreComments)
4274 {
4275 VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4276 if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4277 comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4278
4279 if (comment &&
4280 ((! topmostItemUnderCursor) ||
4281 (topmostItemUnderCursor == (*i)) ||
4282 (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4283 {
4284 return comment;
4285 }
4286 }
4287
4288 // Check whether we have located an unobscured node header.
4289 if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4290 {
4291 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4292 if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4293 {
4294 QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4295 headerRect = node->mapToScene(headerRect).boundingRect();
4296 if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4297 return node;
4298 }
4299 }
4300 }
4301 }
4302
4303 // Having failed to locate any other relevant components within range, return the node
4304 // directly under the cursor, if applicable.
4305 if (! ignoreNodes)
4306 {
4307 if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4308 return topmostItemUnderCursor;
4309
4310 // It is possible that the item under the cursor is a port, but that it didn't meet
4311 // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4312 if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4313 return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4314 }
4315
4316 return NULL;
4317}
4318
4329{
4330 if (! cableInProgress)
4331 return NULL;
4332
4333 VuoPort *fromPort = cableInProgress->getFromPort();
4334 VuoPort *toPort = cableInProgress->getToPort();
4335 VuoPort *fixedPort = (fromPort? fromPort: toPort);
4336
4337 if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4338 return NULL;
4339
4340 return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4341}
4342
4348{
4349 // Start with the first input or output port (other than the refresh port).
4350 vector<VuoRendererPort *> portList;
4351 int firstPortIndex;
4352
4353 if (inputPort)
4354 {
4355 portList = node->getInputPorts();
4357 }
4358 else
4359 {
4360 portList = node->getOutputPorts();
4362 }
4363
4364 VuoRendererPort *targetPort = NULL;
4365 if (portList.size() > firstPortIndex)
4366 {
4367 targetPort = portList[firstPortIndex];
4368
4369 // If the first input port has a wall,
4370 // keep moving down until we find one without a wall.
4371 // (Unless they all have walls, in which case stick with the first.)
4372 VuoRendererPort *firstPortWithoutWall = nullptr;
4373 for (int i = firstPortIndex; i < portList.size(); ++i)
4374 {
4375 if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4376 {
4377 firstPortWithoutWall = portList[i];
4378 break;
4379 }
4380 }
4381 if (firstPortWithoutWall)
4382 targetPort = firstPortWithoutWall;
4383
4384 // If the first input port has a drawer attached to it,
4385 // instead select the first input port of the drawer.
4386 // (Unless the drawer has no input ports, in which case don't select any port.)
4387 VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4388 if (drawer)
4389 {
4390 portList = drawer->getInputPorts();
4391 if (portList.size() > firstPortIndex)
4392 targetPort = portList[firstPortIndex];
4393 else
4394 targetPort = NULL;
4395 }
4396
4397 // If the selected input port has a collapsed type-converter node attached to it,
4398 // instead select the type-converter node's input port.
4399 VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4400 if (typecast)
4401 targetPort = typecast->getChildPort();
4402 }
4403
4404 return targetPort;
4405}
4406
4412{
4413 QRectF boundingRect;
4414 foreach (QGraphicsItem *item, items())
4415 {
4416 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4417 if (! (rc && rc->getBase()->isPublished()))
4418 boundingRect |= item->sceneBoundingRect();
4419 }
4420
4421 return boundingRect;
4422}
4423
4429{
4430 QRectF boundingRect;
4431
4432 foreach (QGraphicsItem *item, selectedItems())
4433 {
4434 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4435 if (! (rc && rc->getBase()->isPublished()))
4436 boundingRect |= item->sceneBoundingRect();
4437 }
4438
4439 return boundingRect;
4440}
4441
4447{
4448 QRectF boundingRect;
4449
4450 foreach (QGraphicsItem *item, selectedItems())
4451 {
4452 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4453 if (! (rc && rc->getBase()->isPublished()))
4454 boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4455
4456 // Include attached drawers.
4457 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4458 if (rn)
4459 {
4460 foreach (VuoPort *port, rn->getBase()->getInputPorts())
4461 {
4462 VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4463 if (drawer)
4464 boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4465 }
4466 }
4467 }
4468
4469 return boundingRect;
4470}
4471
4476void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4477{
4479 if (!port)
4480 return;
4481
4482 updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4483}
4484
4489void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4490{
4491 // Internal ports
4492 if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4493 {
4494 port->getBase()->getRenderer()->setConstant(newValue);
4495
4496 if (updateInRunningComposition)
4498 }
4499
4500 // Published ports
4501 else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4502 {
4503 dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4504
4505 if (updateInRunningComposition)
4507 }
4508}
4509
4519
4528{
4529
4530 // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4531 if (!errorMarkingUpdatesEnabled)
4532 return;
4533
4534 errorMarkingUpdatesEnabled = false;
4535
4536 // Remove any error annotations from the previous call to this function.
4537 if (errorMark)
4538 {
4539 errorMark->removeFromScene();
4540 errorMark = NULL;
4541 }
4542
4544
4545 // Check for errors.
4546
4547 VuoCompilerIssues *issues = new VuoCompilerIssues();
4548 VuoCompilerCable *potentialCable = NULL;
4549 try
4550 {
4551 set<VuoCompilerCable *> potentialCables;
4552
4553 if (targetPort && cableInProgress)
4554 {
4555 VuoNode *fromNode;
4556 VuoPort *fromPort;
4557 VuoNode *toNode;
4558 VuoPort *toPort;
4559 if (cableInProgress->getFromNode())
4560 {
4561 fromNode = cableInProgress->getFromNode();
4562 fromPort = cableInProgress->getFromPort();
4563 toNode = targetPort->getUnderlyingParentNode()->getBase();
4564 toPort = targetPort->getBase();
4565 }
4566 else
4567 {
4568 fromNode = targetPort->getUnderlyingParentNode()->getBase();
4569 fromPort = targetPort->getBase();
4570 toNode = cableInProgress->getToNode();
4571 toPort = cableInProgress->getToPort();
4572 }
4573 potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4574 potentialCable->getBase()->setFrom(fromNode, fromPort);
4575 potentialCable->getBase()->setTo(toNode, toPort);
4576 potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4577 cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4578
4579 fromPort->removeConnectedCable(potentialCable->getBase());
4580 toPort->removeConnectedCable(potentialCable->getBase());
4581 potentialCables.insert(potentialCable);
4582 }
4583
4584 getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4585 }
4586 catch (const VuoCompilerException &e)
4587 {
4588 }
4589
4590 if (! issues->isEmpty())
4591 {
4592 VUserLog("%s: Showing error popover: %s",
4593 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4594 issues->getShortDescription(false).c_str());
4595
4596 this->errorMark = new VuoErrorMark();
4597
4598 foreach (VuoCompilerIssue issue, issues->getList())
4599 {
4600 set<VuoRendererNode *> nodesToMark;
4601 set<VuoRendererCable *> cablesToMark;
4602
4603 set<VuoNode *> problemNodes = issue.getNodes();
4604 foreach (VuoNode *node, problemNodes)
4605 if (node->hasRenderer())
4606 nodesToMark.insert(node->getRenderer());
4607
4608 set<VuoCable *> problemCables = issue.getCables();
4609 foreach (VuoCable *cable, problemCables)
4610 {
4611 VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4612 if (cableToMark->hasRenderer())
4613 cablesToMark.insert(cableToMark->getRenderer());
4614 }
4615
4617 errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4618
4619 VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4620 errorPopovers.insert(errorPopover);
4623
4624 QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4625
4626 // Place the popover near an appropriate nearby node involved in the feedback loop.
4627 VuoRendererNode *nearbyNode = NULL;
4628 if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4629 {
4630 nearbyNode = targetPort->getRenderedParentNode();
4631 }
4632 else if (! nodesToMark.empty())
4633 {
4634 VuoRendererNode *topmostVisibleNode = NULL;
4635 qreal topY = viewportRect.bottom();
4636 foreach (VuoRendererNode *node, nodesToMark)
4637 {
4638 if (node->getProxyNode())
4639 node = node->getProxyNode();
4640
4641 QPointF scenePos = node->scenePos();
4642 if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4643 {
4644 topmostVisibleNode = node;
4645 topY = scenePos.y();
4646 }
4647 }
4648
4649 if (topmostVisibleNode)
4650 nearbyNode = topmostVisibleNode;
4651 else
4652 {
4653 VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4654 nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4655 }
4656 }
4657 else
4658 {
4659 VUserLog("Warning: no nearby node (no marked nodes).");
4660 }
4661
4662 // If no nodes are known to be involved in the feedback loop, display the popover
4663 // in the center of the viewport.
4664 const QPoint offsetFromNode(0,10);
4665 QPoint popoverTopLeftInScene = (nearbyNode?
4666 (nearbyNode->scenePos().toPoint() +
4667 nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4668 offsetFromNode) :
4669 QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4670 viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4671
4672 // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4673 // of the viewport closest to the feedback loop.
4674 const int margin = 5;
4675 popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4676 fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4677
4678 popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4679 fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4680
4681 QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4682 QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4683
4684 errorPopover->move(popoverTopLeftGlobal);
4685 errorPopover->show();
4686 emit popoverDetached();
4687 }
4688
4689 // Add error annotations to the composition.
4690 addItem(errorMark);
4691 }
4692 delete issues;
4693 delete potentialCable;
4694
4695 errorMarkingUpdatesEnabled = true;
4696}
4697
4701bool VuoEditorComposition::hasFeedbackErrors(void)
4702{
4703 return this->errorMark;
4704}
4705
4710{
4711 if (hasFeedbackErrors())
4712 this->errorMark->updateErrorMarkPath();
4713}
4714
4720void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4721{
4722 try
4723 {
4724 emit buildStarted();
4725
4726 if (! dependenciesUninstalled.empty())
4727 {
4728 vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4729 string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4730 throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4731 }
4732
4733 delete runningComposition;
4734 runningComposition = NULL;
4735 runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4736
4737 runningCompositionActiveDriver = getDriverForActiveProtocol();
4738 if (runningCompositionActiveDriver)
4739 runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4740
4741 string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4742 string dir, file, ext;
4743 VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4744 linkedCompositionPath = dir + file + ".dylib";
4745
4746 compiler->setShouldPotentiallyShowSplashWindow(false);
4747
4748 VuoCompilerIssues *issues = new VuoCompilerIssues();
4749 compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4750 compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries.get());
4751 delete issues;
4752
4753 remove(compiledCompositionPath.c_str());
4754
4755 emit buildFinished("");
4756 }
4757 catch (VuoException &e)
4758 {
4759 delete runningComposition;
4760 runningComposition = NULL;
4761
4762 emit buildFinished(e.what());
4763 throw;
4764 }
4765}
4766
4772bool VuoEditorComposition::isRunningThreadUnsafe(void)
4773{
4774 return runner != NULL && ! stopRequested && ! runner->isStopped();
4775}
4776
4783{
4784 __block bool running;
4785 dispatch_sync(runCompositionQueue, ^{
4786 running = isRunningThreadUnsafe();
4787 });
4788 return running;
4789}
4790
4796void VuoEditorComposition::run(string compositionSnapshot)
4797{
4798 VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4799
4800 // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4801 subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4802
4803 // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4804 subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4805 {
4806 compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4807 });
4808
4809 // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4810 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4811 {
4812 if (matchingComposition->showEventsMode)
4813 matchingComposition->beginDisplayingActivity();
4814 });
4815
4816 stopRequested = false;
4817 dispatch_async(runCompositionQueue, ^{
4818 try
4819 {
4820 runningCompositionLibraries = std::make_shared<VuoRunningCompositionLibraries>();
4821
4822 buildComposition(compositionSnapshot);
4823
4824 string compositionLoaderPath = compiler->getCompositionLoaderPath();
4825 string compositionSourceDir = getBase()->getDirectory();
4826
4827 runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4828 runner->setDelegate(this);
4830 runner->startPaused();
4831 pid_t pid = runner->getCompositionPid();
4832
4833 // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4834 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4835 {
4836 if (matchingComposition->showEventsMode)
4837 this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4838
4839 dispatch_sync(activePortPopoversQueue, ^{
4840 for (auto i : matchingComposition->activePortPopovers)
4841 {
4842 string portID = i.first;
4843 updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4844 }
4845 });
4846 });
4847
4848 runner->unpause();
4849
4850 // Focus the composition's windows (if any).
4852 }
4853 catch (...) { }
4854 });
4855}
4856
4863{
4864 stopRequested = true;
4865 dispatch_async(runCompositionQueue, ^{
4866 if (runner && ! runner->isStopped())
4867 {
4868 runner->stop();
4869 runner->waitUntilStopped();
4870 }
4871 delete runner;
4872 runner = NULL;
4873
4874 linkedCompositionPath = "";
4875
4876 runningCompositionLibraries = nullptr; // release shared_ptr
4877
4878 delete runningComposition;
4879 runningComposition = NULL;
4880
4881 emit stopFinished();
4882 });
4883
4884 VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4885
4886 // Tell this composition and all subcompositions opened from it to stop display live debug info.
4887 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4888 {
4889 if (matchingComposition->showEventsMode)
4890 matchingComposition->stopDisplayingActivity();
4891
4892 dispatch_sync(activePortPopoversQueue, ^{
4893 for (auto i : matchingComposition->activePortPopovers)
4894 {
4895 VuoPortPopover *popover = i.second;
4896 popover->setCompositionRunning(false);
4897 }
4898 });
4899 });
4900}
4901
4913void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4914 VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4915{
4916 if (! diffInfo)
4917 diffInfo = new VuoCompilerCompositionDiff();
4918
4919 dispatch_async(runCompositionQueue, ^{
4920 if (isRunningThreadUnsafe())
4921 {
4922 try
4923 {
4924 foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4925 {
4926 runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4927 }
4928
4929 string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4930 VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4931 if (previouslyActiveDriver)
4932 {
4933 VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4934 previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4935 oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4936 }
4937
4938 buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4939
4940 string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4941 runner->replaceComposition(linkedCompositionPath, compositionDiff);
4942 }
4943 catch (exception &e)
4944 {
4945 VUserLog("Composition stopped itself: %s", e.what());
4947 }
4948 catch (...)
4949 {
4950 VUserLog("Composition stopped itself.");
4952 }
4953 }
4954 else
4955 {
4956 dispatch_async(dispatch_get_main_queue(), ^{
4958 });
4959 }
4960
4961 delete diffInfo;
4962 });
4963}
4964
4970{
4971 void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
4972 {
4973 compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
4974 };
4975 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
4976}
4977
4983{
4984 string constant;
4985 identifierCache->doForPortWithIdentifier(runningPortID, [&constant](VuoPort *port) {
4986 if (port->hasCompiler() && port->hasRenderer())
4987 constant = port->getRenderer()->getConstantAsString();
4988 });
4989
4990 // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4991 void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4992 {
4993 if (this == topLevelComposition)
4994 {
4995 dispatch_async(runCompositionQueue, ^{
4996 if (isRunningThreadUnsafe())
4997 {
4998 json_object *constantObject = json_tokener_parse(constant.c_str());
4999 runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
5000 }
5001 });
5002 }
5003 };
5004 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
5005
5006 // If this is a subcomposition, live-update all other top-level compositions that contain it.
5007 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
5008 {
5009 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
5010 {
5011 topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
5012 });
5013 });
5014}
5015
5021{
5023 if (!(port && port->hasCompiler()))
5024 return;
5025
5026 string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
5028}
5029
5034{
5035 string runningPortIdentifier = identifierCache->getIdentifierForPort(port->getBase());
5036 if (runningPortIdentifier.empty())
5037 return;
5038
5039 // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
5040 void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
5041 {
5042 if (this == topLevelComposition)
5043 {
5044 dispatch_async(runCompositionQueue, ^{
5045 if (isRunningThreadUnsafe())
5046 {
5047 json_object *constantObject = json_tokener_parse(constant.c_str());
5048 runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
5049 }
5050 });
5051 }
5052 };
5053 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
5054
5055 // If this is a subcomposition, live-update all other top-level compositions that contain it.
5056 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
5057 {
5058 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
5059 {
5060 topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
5061 });
5062 });
5063}
5064
5069void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
5070{
5071 dispatch_async(runCompositionQueue, ^{
5072 if (isRunningThreadUnsafe())
5073 {
5074 json_object *constantObject = json_tokener_parse(constant.c_str());
5075 set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
5076 foreach (string subcompositionIdentifier, subcompositionIdentifiers)
5077 {
5078 runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
5079 }
5080 }
5081 });
5082}
5083
5088{
5089 dispatch_async(runCompositionQueue, ^{
5090 if (isRunningThreadUnsafe())
5091 {
5092 VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
5093 if (publishedPort)
5094 {
5095 json_object *constantObject = json_tokener_parse(constant.c_str());
5096 map<VuoRunner::Port *, json_object *> m;
5097 m[publishedPort] = constantObject;
5098 runner->setPublishedInputPortValues(m);
5099 json_object_put(constantObject);
5100 }
5101 }
5102 });
5103}
5104
5105
5110{
5111 return contextMenuDeleteSelected;
5112}
5113
5118{
5119 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5120 // For now, this recreates the context menu rather than accessing a data member.
5121 QMenu *contextMenuTints = new QMenu(parent);
5122 contextMenuTints->setSeparatorsCollapsible(false);
5123 contextMenuTints->setTitle(tr("Tint"));
5124 foreach (QAction *tintAction, contextMenuTintActions)
5125 contextMenuTints->addAction(tintAction);
5126 contextMenuTints->insertSeparator(contextMenuTintActions.last());
5127
5128 return contextMenuTints;
5129}
5130
5134void VuoEditorComposition::expandChangeNodeMenu()
5135{
5136 QAction *sender = (QAction *)QObject::sender();
5137 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
5138
5139 // If the menu hasn't been expanded previously, expand it enough now to fill
5140 // the available vertical screenspace.
5141 int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5142 if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5143 {
5144 int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5145 int verticalSpacePerItem = 21; // menu row height in pixels
5146 // Estimate the number of matches that will fit within the screen without scrolling:
5147 // -1 to account for a possible extra "More…" row;
5148 // -1 wiggle room to match observations
5149 int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5150
5151 populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5152 }
5153
5154 // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5155 else
5156 populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5157
5158 contextMenuChangeNode->exec();
5159}
5160
5167{
5168 menu->clear();
5169
5170 if (!node)
5171 return;
5172
5173 map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5174 vector<VuoCompilerNodeClass *> loadedNodeClasses;
5175 for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5176 loadedNodeClasses.push_back(i->second);
5177 VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5178
5179 vector<string> bestMatches;
5180 map<string, double> matchScores;
5181 matchScores[""] = 0;
5182
5183 int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5184 for (int i = 0; i < targetMatchCount; ++i)
5185 bestMatches.push_back("");
5186
5187 // Maintain a priority queue with the @c targetMatchCount best matches.
5188 bool overflow = false;
5189
5190 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5191 string originalGenericNodeClassName;
5192 if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5193 originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5194 else
5195 originalGenericNodeClassName = nodeClass->getClassName();
5196
5197 foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5198 {
5199 string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5200 if (loadedNodeClassName == originalGenericNodeClassName)
5201 continue;
5202
5203 bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5204 double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5205 int highestIndexWithCompetitiveScore = -1;
5206 for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5207 if (matchScore <= matchScores[bestMatches[i] ])
5208 highestIndexWithCompetitiveScore = i;
5209
5210 if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5211 {
5212 if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5213 overflow = true;
5214
5215 for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5216 bestMatches[j+1] = bestMatches[j];
5217
5218 bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5219 matchScores[loadedNodeClassName] = matchScore;
5220 }
5221 }
5222
5223 for (int i = 0; i < targetMatchCount; ++i)
5224 {
5225 if (matchScores[bestMatches[i] ] > 0)
5226 {
5227 // Disambiguate between identical node titles using node class names.
5228 QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5229 if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5230 matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5231
5232 QAction *changeAction = menu->addAction(matchDisplayText);
5233
5234 QList<QVariant> currentNodeAndNewClass;
5235 currentNodeAndNewClass.append(QVariant::fromValue(node));
5236 currentNodeAndNewClass.append(bestMatches[i].c_str());
5237 changeAction->setData(QVariant(currentNodeAndNewClass));
5238 connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5239 }
5240 }
5241
5242 if (overflow)
5243 {
5244 //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5245 QAction *showMoreAction = menu->addAction(tr("More…"));
5246 showMoreAction->setData(QVariant::fromValue(node));
5247 connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5248 }
5249}
5250
5255bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5256{
5257 // Inventory required input port types (connected data inputs) in the node to be replaced.
5258 map<string, int> requiredInputs;
5259 bool inputEventSourceRequired = false;
5260 foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5261 {
5262 bool hasDrawerWithNoIncomingCables = false;
5263 bool hasDrawerWithNoIncomingDataCables = false;
5264
5265 if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5266 {
5267 VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5268 if (inputDrawer)
5269 {
5270 hasDrawerWithNoIncomingCables = true;
5271 hasDrawerWithNoIncomingDataCables = true;
5272 vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5273 foreach (VuoRendererPort *childPort, childPorts)
5274 {
5275 if (childPort->getBase()->getConnectedCables(true).size() > 0)
5276 hasDrawerWithNoIncomingCables = false;
5277 if (childPort->effectivelyHasConnectedDataCable(true))
5278 hasDrawerWithNoIncomingDataCables = false;
5279 }
5280 }
5281
5282 string typeKey = inputPort->getDataType()->getModuleKey();
5283 if (!hasDrawerWithNoIncomingDataCables)
5284 {
5285 // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5286 // Accommodate generic types.
5288 return false;
5289
5290 requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5291 }
5292 }
5293
5294 bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5295 if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5296 inputEventSourceRequired = true;
5297 }
5298
5299 // Inventory required output port types (connected data outputs) in the node to be replaced.
5300 map<string, int> requiredOutputs;
5301 bool outputEventSourceRequired = false;
5302 foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5303 {
5304 if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5305 {
5306 string typeKey = outputPort->getDataType()->getModuleKey();
5307
5308 // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5309 // Accommodate generic types.
5311 return false;
5312
5313 requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5314 }
5315
5316 if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5317 outputEventSourceRequired = true;
5318 }
5319
5320 // Inventory available input port types in the candidate replacement node.
5321 bool inputEventSourceAvailable = false;
5322 map<string, int> availableInputs;
5323 foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5324 {
5325 VuoType *dataType = (inputPortClass->hasCompiler()?
5326 static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5327 if (dataType)
5328 {
5329 string typeKey = dataType->getModuleKey();
5330 availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5331 }
5332 }
5333
5335 inputEventSourceAvailable = true;
5336
5337 // Inventory available output port types in the candidate replacement node.
5338 bool outputEventSourceAvailable = false;
5339 map<string, int> availableOutputs;
5340 foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5341 {
5342 VuoType *dataType = (outputPortClass->hasCompiler()?
5343 static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5344 if (dataType)
5345 {
5346 string typeKey = dataType->getModuleKey();
5347 availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5348 }
5349 }
5350
5352 outputEventSourceAvailable = true;
5353
5354 // Check whether the candidate replacement node meets input data requirements.
5355 for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5356 {
5357 string typeKey = it->first;
5358 int typeRequiredCount = it->second;
5359 if (availableInputs[typeKey] < typeRequiredCount)
5360 return false;
5361 }
5362
5363 // Check whether the candidate replacement node meets output data requirements.
5364 for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5365 {
5366 string typeKey = it->first;
5367 int typeRequiredCount = it->second;
5368 if (availableOutputs[typeKey] < typeRequiredCount)
5369 return false;
5370 }
5371
5372 if (inputEventSourceRequired && !inputEventSourceAvailable)
5373 return false;
5374
5375 if (outputEventSourceRequired && !outputEventSourceAvailable)
5376 return false;
5377
5378 return true;
5379}
5380
5385bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5386{
5387 // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5389 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5390
5391 if (!specializedNodeClass)
5392 return false;
5393
5394 VuoPortClass *portClass = port->getBase()->getClass();
5395 VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5396 if (!originalGenericType)
5397 return false;
5398
5399 // If this port belongs to an attachment connected to a port that is not revertible, then
5400 // this port cannot be reverted, either.
5402 if (attachment)
5403 {
5404 VuoPort *hostPort = attachment->getUnderlyingHostPort();
5405 if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5406 return false;
5407 }
5408
5409 return true;
5410}
5411
5430VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5431{
5432 string publishedPortName = ((! name.empty())?
5433 name :
5434 VuoRendererPort::sanitizePortName(port->getRenderer()->getPortNameToRenderWhenDisplayed().c_str()).toUtf8().constData());
5435 bool isPublishedInput = port->getRenderer()->getInput();
5436 VuoType *portType = port->getRenderer()->getDataType();
5437 VuoPublishedPort *publishedPort = NULL;
5438
5439 // If merging is enabled:
5440 // Check whether this composition has a pre-existing externally visible published port
5441 // that has the requested name and type and that can accommodate the newly published internal port.
5442 // If so, add this internal port as a connected port for the existing alias.
5443 bool performedMerge = false;
5444 if (attemptMerge)
5445 {
5446 publishedPort = (isPublishedInput ?
5447 getBase()->getPublishedInputPortWithName(publishedPortName) :
5448 getBase()->getPublishedOutputPortWithName(publishedPortName));
5449
5450 if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5451 {
5452 if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5453 {
5454 VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5456 publishedInputPort->getInitialValue(),
5457 false);
5458 }
5459
5460 performedMerge = true;
5461 }
5462 }
5463
5464
5465 // Otherwise, create a new externally visible published port with a unique name derived from
5466 // the specified name, containing the current port as its lone connected internal port.
5467 if (! performedMerge)
5468 {
5469 publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5470 if (isPublishedInput && type)
5471 {
5472 VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5473 publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5474 }
5475 }
5476
5477 addPublishedPort(publishedPort, isPublishedInput);
5478
5479 VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5480 dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5481 createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5482
5483 VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5484
5485 if (! existingPublishedCable)
5486 {
5487 VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5488 addCable(publishedCable);
5489 }
5490
5491 if (mergePerformed != NULL)
5492 *mergePerformed = performedMerge;
5493
5494 return rendererPublishedPort;
5495}
5496
5501VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5502{
5503 VuoCable *publishedCable = NULL;
5504 bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5505
5506 if (creatingPublishedInputCable)
5507 {
5508 // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5509 VuoPort *fromPort = externalPort;
5510 VuoNode *fromNode = this->publishedInputNode;
5511
5512 VuoPort *toPort = internalPort;
5513 VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5514
5515 publishedCable = (new VuoCompilerCable(NULL,
5516 NULL,
5517 toNode->getCompiler(),
5518 (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5519 publishedCable->setFrom(fromNode, fromPort);
5520 }
5521
5522 else
5523 {
5524 // If creating a published output cable, it will need to have an associated VuoCompilerCable
5525 // even though we don't currently construct a VuoCompilerNode for the published output node.
5526 VuoPort *fromPort = internalPort;
5527 VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5528
5529 VuoPort *toPort = externalPort;
5530 VuoNode *toNode = this->publishedOutputNode;
5531
5532 publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5533 (VuoCompilerPort *)(fromPort->getCompiler()),
5534 NULL,
5535 NULL))->getBase();
5536 publishedCable->setTo(toNode, toPort);
5537 }
5538
5539 if (forceEventOnlyPublication)
5540 publishedCable->getCompiler()->setAlwaysEventOnly(true);
5541
5542 return publishedCable;
5543}
5544
5557{
5558 vector<VuoPublishedPort *> publishedPortsToAdd;
5559 map<VuoPublishedPort *, string> publishedPortsToRename;
5560
5561 // Remove the previously active protocol.
5562 VuoProtocol *previousActiveProtocol = this->activeProtocol;
5563 bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5564
5565 bool portChangesMadeDuringProtocolRemoval = false;
5566 if (removingPreviousProtocol)
5567 portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5568
5569 if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5570 {
5571 VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5572 useUndoStack = true;
5573 }
5574
5575 // Add the newly active protocol.
5576 this->activeProtocol = protocol;
5577 if (!protocol)
5578 return;
5579
5580 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5581 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5582 {
5583 string portName = i->first;
5584 string portType = i->second;
5585
5586 bool compositionHadCompatiblePort = false;
5587 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5588 if (preexistingPublishedPort)
5589 {
5590 VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5591
5592 bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5593 (!preexistingType && (portType == "")));
5594 if (portTypesMatch)
5595 {
5596 compositionHadCompatiblePort = true;
5597 preexistingPublishedPort->setProtocolPort(true);
5598 }
5599 else
5600 {
5601 compositionHadCompatiblePort = false;
5602 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5603 }
5604 }
5605
5606 if (!compositionHadCompatiblePort)
5607 {
5608 VuoCompilerType *ctype = compiler->getType(portType);
5609 VuoType *type = (ctype? ctype->getBase() : NULL);
5611 publishedPort->setProtocolPort(true);
5612
5613 if (!useUndoStack)
5614 addPublishedPort(publishedPort, true);
5615 else
5616 publishedPortsToAdd.push_back(publishedPort);
5617
5619 }
5620 }
5621
5622 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5623 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5624 {
5625 string portName = i->first;
5626 string portType = i->second;
5627
5628 bool compositionHadCompatiblePort = false;
5629 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5630 if (preexistingPublishedPort)
5631 {
5632 VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5633 bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5634 (!preexistingType && (portType == "")));
5635 if (portTypesMatch)
5636 {
5637 compositionHadCompatiblePort = true;
5638 preexistingPublishedPort->setProtocolPort(true);
5639 }
5640 else
5641 {
5642 compositionHadCompatiblePort = false;
5643 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5644 }
5645 }
5646
5647 if (!compositionHadCompatiblePort)
5648 {
5649 VuoCompilerType *ctype = compiler->getType(portType);
5650 VuoType *type = (ctype? ctype->getBase() : NULL);
5652 publishedPort->setProtocolPort(true);
5653
5654 if (!useUndoStack)
5655 addPublishedPort(publishedPort, false);
5656 else
5657 publishedPortsToAdd.push_back(publishedPort);
5658
5659 createRendererForPublishedPortInComposition(publishedPort, false);
5660 }
5661 }
5662
5663 if (useUndoStack)
5664 {
5665 bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5666 if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5667 {
5668 set<VuoPublishedPort *> publishedPortsToRemove;
5669 bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5670 bool endUndoStackMacro = true;
5671 emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5672 }
5673 }
5674
5675 emit activeProtocolChanged();
5676}
5677
5685string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5686{
5687 string modifiedPortName = portName;
5688 if (modifiedPortName.length() > 0)
5689 modifiedPortName[0] = toupper(modifiedPortName[0]);
5690 modifiedPortName = "some" + modifiedPortName;
5691
5692 return modifiedPortName;
5693}
5694
5713{
5715
5716 set<VuoPublishedPort *> publishedPortsToRemove;
5717 map<VuoPublishedPort *, string> publishedPortsToRename;
5718
5719 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5720 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5721 {
5722 string portName = i->first;
5723 string portType = i->second;
5724
5725 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5726 if (preexistingPublishedPort)
5727 {
5728 bool portCompatibleAcrossProtocolTransition = false;
5729 if (replacementProtocol)
5730 {
5731 vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5732 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5733 {
5734 string replacementPortName = i->first;
5735 string replacementPortType = i->second;
5736
5737 if ((portName == replacementPortName) && (portType == replacementPortType))
5738 portCompatibleAcrossProtocolTransition = true;
5739 }
5740 }
5741
5742 if (preexistingPublishedPort->getConnectedCables(true).empty())
5743 publishedPortsToRemove.insert(preexistingPublishedPort);
5744 else if (!portCompatibleAcrossProtocolTransition)
5745 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5746 }
5747 }
5748
5749 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5750 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5751 {
5752 string portName = i->first;
5753 string portType = i->second;
5754
5755 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5756 if (preexistingPublishedPort)
5757 {
5758 bool portCompatibleAcrossProtocolTransition = false;
5759 if (replacementProtocol)
5760 {
5761 vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5762 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5763 {
5764 string replacementPortName = i->first;
5765 string replacementPortType = i->second;
5766
5767 if ((portName == replacementPortName) && (portType == replacementPortType))
5768 portCompatibleAcrossProtocolTransition = true;
5769 }
5770 }
5771
5772 if (preexistingPublishedPort->getConnectedCables(true).empty())
5773 publishedPortsToRemove.insert(preexistingPublishedPort);
5774 else if (!portCompatibleAcrossProtocolTransition)
5775 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5776 }
5777 }
5778
5779 // If we are removing any ports, the composition will no longer be deemed to adhere to the
5780 // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5781 if (!publishedPortsToRemove.empty())
5782 publishedPortsToRename.clear();
5783
5784 bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5785 if (portChangesRequired)
5786 {
5787 vector<VuoPublishedPort *> publishedPortsToAdd;
5788 bool beginUndoStackMacro = true;
5789 bool endUndoStackMacro = !replacementProtocol;
5790 emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5791 }
5792
5793 emit activeProtocolChanged();
5794
5795 return portChangesRequired;
5796}
5797
5805{
5806 if ((activeProtocol != protocol) || !activeProtocol)
5807 return;
5808
5809 activeProtocol = NULL;
5810
5811 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5812 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5813 {
5814 string portName = i->first;
5815 string portType = i->second;
5816
5817 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5818 if (preexistingPublishedPort)
5819 preexistingPublishedPort->setProtocolPort(false);
5820 }
5821
5822 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5823 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5824 {
5825 string portName = i->first;
5826 string portType = i->second;
5827
5828 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5829 if (preexistingPublishedPort)
5830 preexistingPublishedPort->setProtocolPort(false);
5831 }
5832
5833 emit activeProtocolChanged();
5834}
5835
5841{
5842 return activeProtocol;
5843}
5844
5850{
5851 if (!activeProtocol)
5852 return NULL;
5853
5854 return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5855}
5856
5860void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5861{
5862 VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5863
5864 identifierCache->addPublishedPortToCache(publishedPort);
5865
5866 if (shouldUpdateUi)
5867 emit publishedPortModified();
5868}
5869
5876int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5877{
5878 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5879 if (shouldUpdateUi && publishedPort->isProtocolPort())
5881
5882 int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5883
5884 if (shouldUpdateUi)
5885 emit publishedPortModified();
5886
5887 return removalResult;
5888}
5889
5895{
5896 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5897 if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5899
5900 VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5901
5902 identifierCache->addPublishedPortToCache( static_cast<VuoPublishedPort *>(publishedPort->getBase()) );
5903
5904 emit publishedPortModified();
5905}
5906
5914void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5915{
5916 bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5917 VuoRendererPort *fixedPort = NULL;
5918
5919 if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5920 fixedPort = cable->getFromPort()->getRenderer();
5921
5922 else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5923 fixedPort = cable->getToPort()->getRenderer();
5924
5925 if (fixedPort)
5926 {
5927 highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5928 emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5929 }
5930}
5931
5937void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5938{
5939 QList<QGraphicsItem *> compositionComponents = items();
5940
5941 // Cache the (fairly time-consuming) computation of eligibility highlighting for each port so we can reuse the result.
5942 map<VuoRendererPort *, VuoRendererColors::HighlightType> highlightForPort;
5943
5944 for (QGraphicsItem *compositionComponent : compositionComponents)
5945 {
5946 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5947 if (rn)
5948 {
5949 // Check for eligible internal input ports.
5950 vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5951 for (VuoPort *inputPort : inputPorts)
5952 highlightForPort[inputPort->getRenderer()] =
5953 updateEligibilityHighlightingForPort(inputPort->getRenderer(), port, !cable->effectivelyCarriesData());
5954
5955 // Check for eligible internal output ports.
5956 vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
5957 for (VuoPort *outputPort : outputPorts)
5958 highlightForPort[outputPort->getRenderer()] =
5959 updateEligibilityHighlightingForPort(outputPort->getRenderer(), port, !cable->effectivelyCarriesData());
5960 }
5961 }
5962
5963 for (QGraphicsItem *compositionComponent : compositionComponents)
5964 {
5965 // Fade out cables that aren't relevant to the current cable drag.
5966 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5967 if (rc && rc != cable)
5968 {
5969 QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
5970 rc->setCacheMode(QGraphicsItem::NoCache);
5971 rc->updateGeometry();
5972
5973 VuoPort *otherCablePort = port->getInput()
5974 ? rc->getBase()->getFromPort()
5975 : rc->getBase()->getToPort();
5976
5977 VuoRendererColors::HighlightType highlight = otherCablePort ?
5978 highlightForPort[otherCablePort->getRenderer()] :
5980\
5981 // Don't apply extra highlighting to compatible, already-connected cables.
5982 if (highlight == VuoRendererColors::standardHighlight)
5984
5985 rc->setEligibilityHighlight(highlight);
5986
5987 rc->setCacheMode(normalCacheMode);
5988 }
5989 }
5990
5991 // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
5992 for (QGraphicsItem *compositionComponent : compositionComponents)
5993 updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(compositionComponent));
5994}
5995
6000VuoRendererColors::HighlightType VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
6001 VuoRendererPort *fixedPort,
6002 bool eventOnlyConnection)
6003{
6004 QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
6005 portToHighlight->setCacheMode(QGraphicsItem::NoCache);
6006
6007 portToHighlight->updateGeometry();
6008
6009 VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection);
6010
6011 portToHighlight->setEligibilityHighlight(highlight);
6012 VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
6013 if (typecastPortToHighlight)
6014 typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
6015
6016 portToHighlight->setCacheMode(normalCacheMode);
6017
6018 if (typecastPortToHighlight)
6019 updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection);
6020
6021 return highlight;
6022}
6023
6034{
6035 // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
6036 VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
6037 VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
6038
6039 VuoRendererPort *fromPort;
6040 VuoRendererPort *toPort;
6041 bool forwardConnection;
6042 if (fixedPort->getOutput())
6043 {
6044 fromPort = fixedPort;
6045 toPort = portToHighlight;
6046 forwardConnection = true;
6047 }
6048 else
6049 {
6050 fromPort = portToHighlight;
6051 toPort = fixedPort;
6052 forwardConnection = false;
6053 }
6054
6055 bool directConnectionPossible;
6056
6057 // Temporarily disallow direct cable connections between published inputs and published outputs.
6058 // @todo: Allow for https://b33p.net/kosada/node/7756 .
6059 if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
6060 directConnectionPossible = false;
6061 else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
6062 directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
6063 else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
6064 directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
6065 else // both ports are internal canvas ports
6066 directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
6067
6069 if (directConnectionPossible)
6071 else if (!findBridgingSolutions(fromPort, toPort, forwardConnection).empty())
6073 else if (fixedPort == portToHighlight)
6075 else
6077
6078 return highlight;
6079}
6080
6096bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6097 VuoRendererPort *toPort,
6098 bool eventOnlyConnection,
6099 bool forwardConnection)
6100{
6101 VuoRendererPort *portToRespecialize = NULL;
6102 string respecializedTypeName = "";
6103
6104 return canConnectDirectlyWithRespecializationNondestructively(fromPort,
6105 toPort,
6106 eventOnlyConnection,
6107 forwardConnection,
6108 &portToRespecialize,
6109 respecializedTypeName);
6110}
6111
6122bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6123 VuoRendererPort *toPort,
6124 bool eventOnlyConnection,
6125 bool forwardConnection,
6126 VuoRendererPort **portToRespecialize,
6127 string &respecializedTypeName)
6128{
6129 *portToRespecialize = NULL;
6130 respecializedTypeName = "";
6131
6132 bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6133 toPort,
6134 eventOnlyConnection,
6135 forwardConnection,
6136 portToRespecialize,
6137 respecializedTypeName);
6138 if (!canConnectWithRespecialization)
6139 return false;
6140
6141 if (canConnectWithRespecialization && !portToRespecialize)
6142 return true;
6143
6144 bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6145 if (!nondestructive)
6146 {
6147 *portToRespecialize = NULL;
6148 respecializedTypeName = "";
6149 }
6150 return nondestructive;
6151}
6152
6158bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6159{
6160 map<VuoNode *, string> nodesToReplace;
6161 set<VuoCable *> cablesToDelete;
6162 createReplacementsToUnspecializePort(portToUnspecialize, false, nodesToReplace, cablesToDelete);
6163
6164 // Check whether unspecialization would disconnect any existing cables
6165 // (other than the cable that would normally be displaced by the new cable connection).
6166 if (cablesToDelete.empty())
6167 return true;
6168
6169 else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6170 return true;
6171
6172 return false;
6173}
6174
6194bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6195 VuoRendererPort *toPort,
6196 bool eventOnlyConnection,
6197 bool forwardConnection,
6198 VuoRendererPort **portToRespecialize,
6199 string &respecializedTypeName)
6200{
6201 // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6202
6203 *portToRespecialize = NULL;
6204 respecializedTypeName = "";
6205
6206 // // @todo https://b33p.net/kosada/node/10481 Necessary?
6207 if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6208 return true;
6209
6210 // // @todo https://b33p.net/kosada/node/10481 Necessary?
6211 if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6212 return true;
6213
6214 bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6215 bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6216
6217 if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6218 return false;
6219
6220 VuoType *currentFromDataType = fromPort->getDataType();
6221 VuoType *currentToDataType = toPort->getDataType();
6222
6223 if (!(currentFromDataType && currentToDataType))
6224 return false;
6225
6227 if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6228 return false;
6229
6230 VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6231 VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6232
6233 VuoGenericType *originalFromGenericType = NULL;
6234 if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6235 {
6237 VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6238 if (fromSpecializedNodeClass)
6239 {
6240 VuoPortClass *portClass = fromPort->getBase()->getClass();
6241 originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6242 }
6243 }
6244
6245 VuoGenericType *originalToGenericType = NULL;
6246 if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6247 {
6249 VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6250 if (toSpecializedNodeClass)
6251 {
6252 VuoPortClass *portClass = toPort->getBase()->getClass();
6253 originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6254 }
6255 }
6256
6257 // Determine whether the port at each endpoint is 1) generic, or
6258 // 2) specialized and currently revertible, or 3) effectively static.
6259 bool fromPortIsGeneric = currentFromGenericType;
6260 bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6261 bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6262
6263 bool toPortIsGeneric = currentToGenericType;
6264 bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6265 bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6266
6267 // Figure out which port to try to respecialize, and to what type.
6268 set<string> compatibleTypes;
6269 string specializedType = "";
6270 VuoRendererPort *portToTryToRespecialize = NULL;
6271
6272 // Case: One port static, one port specialized.
6273 if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6274 {
6275 VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6276 specializedType = staticPort->getDataType()->getModuleKey();
6277 portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6278 }
6279
6280 // Case: One port specialized, other port generic or specialized.
6281 else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6282 {
6283 VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6284 bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6285
6286 VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6287 bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6288
6289 // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6290 /*
6291 if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6292 {
6293 specializedType = dragDestination->getDataType()->getModuleKey();
6294 portToTryToRespecialize = dragSource;
6295 }
6296 else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6297 {
6298 specializedType = dragSource->getDataType()->getModuleKey();
6299 portToTryToRespecialize = dragDestination;
6300 }
6301 else
6302 */
6303
6304 if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6305 {
6306 specializedType = dragSource->getDataType()->getModuleKey();
6307 portToTryToRespecialize = dragDestination;
6308 }
6309 }
6310
6311 // @todo https://b33p.net/kosada/node/10481 Other cases.
6312 else
6313 return false;
6314
6315 if (portToTryToRespecialize)
6316 compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6317
6318 bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6319
6320 if (portsAreCompatible)
6321 {
6322 *portToRespecialize = portToTryToRespecialize;
6323 respecializedTypeName = specializedType;
6324 }
6325
6326 return portsAreCompatible;
6327}
6328
6335void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6336{
6337 VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6338 if (drawer)
6339 {
6341 foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6343 bestEligibility = VuoRendererColors::standardHighlight;
6345 && bestEligibility != VuoRendererColors::standardHighlight)
6346 bestEligibility = VuoRendererColors::subtleHighlight;
6347
6348 // If this drawer has no eligible ports, fade it out.
6349 {
6350 QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6351 drawer->setCacheMode(QGraphicsItem::NoCache);
6352 drawer->updateGeometry();
6353
6354 drawer->setEligibilityHighlight(bestEligibility);
6355
6356 drawer->setCacheMode(normalCacheMode);
6357 }
6358
6359 // Make sure the host port is repainted to take into account the eligibility of its drawer ports.
6360 if (drawer->getRenderedHostPort()
6361 && drawer->getRenderedHostPort()->getRenderer())
6362 {
6363 VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6364
6365 QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6366 hostPort->setCacheMode(QGraphicsItem::NoCache);
6367 hostPort->updateGeometry();
6368 hostPort->setCacheMode(normalCacheMode);
6369 }
6370 }
6371}
6372
6381
6401 VuoRendererPort *toPort,
6402 bool toPortIsDragDestination,
6403 VuoRendererPort **portToSpecialize,
6404 string &specializedTypeName,
6405 string &typecastToInsert)
6406{
6407 *portToSpecialize = NULL;
6408 specializedTypeName = "";
6409
6410 map<string, VuoRendererPort *> portToSpecializeForTypecast;
6411 map<string, string> specializedTypeNameForTypecast;
6412
6413 vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6414 bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6415
6416 if (!solutionSelected)
6417 return false;
6418
6419 if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6420 *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6421 if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6422 specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6423
6424 return true;
6425}
6426
6445bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6446 map<string, VuoRendererPort *> portToSpecializeForTypecast,
6447 map<string, string> specializedTypeNameForTypecast,
6448 string &selectedTypecast)
6449{
6450 if (suitableTypecasts.empty())
6451 {
6452 selectedTypecast = "";
6453 return false;
6454 }
6455
6456 else if (suitableTypecasts.size() == 1)
6457 {
6458 selectedTypecast = suitableTypecasts[0];
6459 return true;
6460 }
6461
6462 else
6463 return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6464}
6465
6471bool VuoEditorComposition::portsPassSanityCheckToBridge(VuoRendererPort *fromPort, VuoRendererPort *toPort)
6472{
6473 bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6474 bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6475
6476 return (fromPortIsEnabledOutput && toPortIsEnabledInput &&
6477 fromPort->getBase()->getClass()->hasCompiler() &&
6478 toPort->getBase()->getClass()->hasCompiler());
6479}
6480
6486bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort,
6487 const string &candidateFromTypeName, const string &candidateToTypeName)
6488{
6489 if (!portsPassSanityCheckToBridge(fromPort, toPort))
6490 return false;
6491
6492 VuoType *fromPortType = static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType();
6493 VuoType *toPortType = static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType();
6494
6495 string fromTypeName = ! candidateFromTypeName.empty() ?
6496 candidateFromTypeName :
6497 (fromPortType ? fromPortType->getModuleKey() : "");
6498 string toTypeName = ! candidateToTypeName.empty() ?
6499 candidateToTypeName :
6500 (toPortType ? toPortType->getModuleKey() : "");
6501
6502 // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6503 if (fromTypeName == "VuoBoolean" && toTypeName == "VuoInteger")
6504 {
6505 bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6510 );
6511
6512 if (toNodeUsesIndex)
6513 return false;
6514 }
6515
6516 return true;
6517}
6518
6537 VuoRendererPort *toPort,
6538 bool toPortIsDragDestination)
6539{
6540 map<string, VuoRendererPort *> portToSpecializeForTypecast;
6541 map<string, string> specializedTypeNameForTypecast;
6542 return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6543}
6544
6560 VuoRendererPort *toPort,
6561 bool toPortIsDragDestination,
6562 map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6563 map<string, string> &specializedTypeNameForTypecast)
6564{
6565 portToSpecializeForTypecast.clear();
6566 specializedTypeNameForTypecast.clear();
6567
6568 if (!portsPassSanityCheckToBridge(fromPort, toPort))
6569 return {};
6570
6571 // Temporarily disallow direct cable connections between published inputs and published outputs.
6572 // @todo: Allow for https://b33p.net/kosada/node/7756 .
6573 if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6574 return {};
6575
6576 // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6577 {
6578 VuoRendererPort *portToSpecialize = NULL;
6579 string specializedTypeName = "";
6580 if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6581 {
6582 portToSpecializeForTypecast[""] = portToSpecialize;
6583 specializedTypeNameForTypecast[""] = specializedTypeName;
6584 return {""};
6585 }
6586 }
6587
6588 VuoType *currentFromDataType = fromPort->getDataType();
6589 VuoType *currentToDataType = toPort->getDataType();
6590
6591 if (!(currentFromDataType && currentToDataType))
6592 return {};
6593
6594 VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6595 VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6596
6597 VuoGenericType *originalFromGenericType = NULL;
6598 if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6599 {
6601 VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6602 if (fromSpecializedNodeClass)
6603 {
6604 VuoPortClass *portClass = fromPort->getBase()->getClass();
6605 originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6606 }
6607 }
6608
6609 VuoGenericType *originalToGenericType = NULL;
6610 if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6611 {
6613 VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6614 if (toSpecializedNodeClass)
6615 {
6616 VuoPortClass *portClass = toPort->getBase()->getClass();
6617 originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6618 }
6619 }
6620
6621 // Determine whether the port at each endpoint is:
6622 // 1) generic (unspecialized), or
6623 // 2) specialized and currently revertible, or
6624 // 3) effectively static.
6625 bool fromPortIsGeneric = currentFromGenericType;
6626 bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6627 bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6628
6629 bool toPortIsGeneric = currentToGenericType;
6630 bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6631 bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6632
6633 // No typeconversion or specialization options between two unspecialized generic ports.
6634 if (fromPortIsGeneric && toPortIsGeneric)
6635 return {};
6636
6637 // Typeconversion options but no specialization options between two static ports.
6638 else if (fromPortIsStatic && toPortIsStatic)
6639 {
6640 if (! portsPassSanityCheckToTypeconvert(fromPort, toPort))
6641 return {};
6642
6643 return moduleManager->getCompatibleTypecastClasses(currentFromDataType->getModuleKey(), currentFromDataType,
6644 currentToDataType->getModuleKey(), currentToDataType);
6645 }
6646
6647 // Remaining combinations might require (re-)specializing one port or the other.
6648 // Figure out which port to consider (re-)specializing.
6649 bool specializeToPort = true;
6650 if (toPortIsGeneric)
6651 specializeToPort = true;
6652 else if (fromPortIsGeneric)
6653 specializeToPort = false;
6654 else if (fromPortIsSpecialized && toPortIsStatic)
6655 specializeToPort = false;
6656 else if (fromPortIsStatic && toPortIsSpecialized)
6657 specializeToPort = true;
6658 else if (fromPortIsSpecialized && toPortIsSpecialized)
6659 specializeToPort = toPortIsDragDestination;
6660
6661 // Now that ports have been categorized, figure out what combinations of (re-)specialization
6662 // and/or typeconversion we can use to bridge them.
6663 set<string> compatibleTypes;
6664 if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6665 compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6666 else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6667 compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6668
6669 // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6670 // non-destructively unspecializable, since it already has the appropriate specialization.
6671 compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6672
6673 // If there's at least one bridging solution that involves only type-conversion or only specialization, then return that.
6674 {
6675 vector<string> limitedSuitableTypecasts;
6676
6677 // Check for bridging solutions that involve typeconversion without specialization.
6678 if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6679 {
6680 limitedSuitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType->getModuleKey(), currentFromDataType,
6681 currentToDataType->getModuleKey(), currentToDataType);
6682 foreach (string typecastName, limitedSuitableTypecasts)
6683 {
6684 portToSpecializeForTypecast[typecastName] = specializeToPort? toPort : fromPort;
6685 specializedTypeNameForTypecast[typecastName] = specializeToPort? currentToDataType->getModuleKey() :
6686 currentFromDataType->getModuleKey();
6687 }
6688 }
6689
6690 // Check for bridging solutions that involve specialization without typeconversion.
6691 string fixedDataType = specializeToPort? currentFromDataType->getModuleKey() : currentToDataType->getModuleKey();
6692 if (compatibleTypes.find(fixedDataType) != compatibleTypes.end())
6693 {
6694 limitedSuitableTypecasts.push_back("");
6695 portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6696 specializedTypeNameForTypecast[""] = fixedDataType;
6697 }
6698
6699 if (limitedSuitableTypecasts.size() >= 1)
6700 return limitedSuitableTypecasts;
6701 }
6702
6703 // Search for bridging solutions that involve both type-conversion and specialization.
6704 vector<string> suitableTypecasts;
6705 for (const string &compatibleTypeName : compatibleTypes)
6706 {
6707 // Don't look up the VuoCompilerType for compatibleTypeName since we don't actually need it
6708 // and loading it (if not already loaded) would slow things down.
6709 string candidateFromTypeName;
6710 string candidateToTypeName;
6711 VuoType *candidateFromType;
6712 VuoType *candidateToType;
6713 if (specializeToPort)
6714 {
6715 candidateFromTypeName = currentFromDataType->getModuleKey();
6716 candidateFromType = currentFromDataType;
6717 candidateToTypeName = compatibleTypeName;
6718 candidateToType = nullptr;
6719 }
6720 else
6721 {
6722 candidateFromTypeName = compatibleTypeName;
6723 candidateFromType = nullptr;
6724 candidateToTypeName = currentToDataType->getModuleKey();
6725 candidateToType = currentToDataType;
6726 }
6727
6728 // Re-specialization without typeconversion may be possible.
6729 if (candidateFromTypeName == candidateToTypeName)
6730 {
6731 suitableTypecasts.push_back("");
6732 portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6733 specializedTypeNameForTypecast[""] = compatibleTypeName;
6734 }
6735
6736 if (portsPassSanityCheckToTypeconvert(fromPort, toPort, candidateFromTypeName, candidateToTypeName))
6737 {
6738 vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromTypeName, candidateFromType,
6739 candidateToTypeName, candidateToType);
6740 foreach (string typecast, suitableTypecastsForCurrentTypes)
6741 {
6742 suitableTypecasts.push_back(typecast);
6743 portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6744 specializedTypeNameForTypecast[typecast] = compatibleTypeName;
6745 }
6746 }
6747 }
6748
6749 return suitableTypecasts;
6750}
6751
6764bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6765 map<string, VuoRendererPort *> portToSpecializeForTypecast,
6766 map<string, string> specializedTypeNameForTypecast,
6767 string &selectedTypecast)
6768{
6769 QMenu typecastMenu(views()[0]->viewport());
6770 typecastMenu.setSeparatorsCollapsible(false);
6771 QString spacer(" ");
6772
6773 // Inventory specialization options
6774 set <pair<VuoRendererPort *, string> > specializationDetails;
6775 vector<string> typeconversionOptionsRequiringNoSpecialization;
6776 foreach (string typecastClassName, suitableTypecasts)
6777 {
6778 VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6779 string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6780 specializationDetails.insert(std::make_pair(portToSpecialize,
6781 specializedTypeName));
6782
6783 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6784 if (portAlreadyHasTargetType)
6785 typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6786 }
6787
6788 // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6789 // specialization heading under which multiple typeconversion options may be listed.
6790 // Selecting this item itself will invoke the specialization.
6791 if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6792 {
6793 QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6794 QAction *typecastAction = typecastMenu.addAction(menuText);
6795 typecastAction->setData(QVariant(""));
6796 }
6797
6798 bool foundSpecializationOptionsRequiringNoTypeconversion = typecastMenu.actions().size() >= 1;
6799 bool foundTypeconversionOptionsRequiringNoSpecialization = typeconversionOptionsRequiringNoSpecialization.size() >= 1;
6800
6801 // If there are bridging options that require no specialization, list them next.
6802 bool includingTypeconvertWithNoSpecializationHeader = foundSpecializationOptionsRequiringNoTypeconversion;
6803 if (foundTypeconversionOptionsRequiringNoSpecialization)
6804 {
6805 if (foundSpecializationOptionsRequiringNoTypeconversion)
6806 typecastMenu.addSeparator();
6807
6808 VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6809 string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6810
6811 if (portToSpecialize && !specializedTypeName.empty())
6812 {
6813 QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6814 QAction *typecastAction = typecastMenu.addAction(menuText);
6815 typecastAction->setEnabled(false);
6816 includingTypeconvertWithNoSpecializationHeader = true;
6817 }
6818 }
6819
6820 foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6821 {
6822 VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6823 if (typecastClass)
6824 {
6825 QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6826 typecastAction->setData(QVariant(typecastClassName.c_str()));
6827 }
6828 }
6829
6830 // Now list the remaining bridging options.
6831 for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6832 {
6833 VuoRendererPort *portToSpecialize = i->first;
6834 string specializedTypeName = i->second;
6835
6836 // We've already listed the no-typeconversion bridging option, so skip it here.
6837 if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6838 (portToSpecializeForTypecast[""] == portToSpecialize) &&
6839 (specializedTypeNameForTypecast[""] == specializedTypeName))
6840 {
6841 continue;
6842 }
6843
6844 // We've already listed the no-specialization bridging option, so skip it here.
6845 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6846 if (portAlreadyHasTargetType)
6847 {
6848 continue;
6849 }
6850
6851 if (typecastMenu.actions().size() >= 1)
6852 typecastMenu.addSeparator();
6853
6854 QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6855 QAction *typecastAction = typecastMenu.addAction(menuText);
6856 typecastAction->setEnabled(false);
6857
6858 // Inventory typeconversion options associated with this specialization option.
6859 foreach (string typecastClassName, suitableTypecasts)
6860 {
6861 if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6862 (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6863 {
6864 VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6865 if (typecastClass)
6866 {
6867 QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6868 typecastAction->setData(QVariant(typecastClassName.c_str()));
6869 }
6870 }
6871 }
6872 }
6873
6874 menuSelectionInProgress = true;
6875 QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6876 menuSelectionInProgress = false;
6877
6878 selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6879 return selectedTypecastAction;
6880}
6881
6885QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6886{
6887 if (!portToSpecialize || specializedTypeName.empty())
6888 return "";
6889
6890 bool isInput = portToSpecialize && portToSpecialize->getInput();
6891 QString typeDisplayName = formatTypeNameForDisplay(specializedTypeName);
6892
6893 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6894
6895 QString displayText;
6896 if (portAlreadyHasTargetType)
6897 {
6898 if (isInput)
6899 {
6900 //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6901 displayText = tr("Keep Input Port as %1");
6902 }
6903 else
6904 {
6905 //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6906 displayText = tr("Keep Output Port as %1");
6907 }
6908 }
6909 else
6910 {
6911 if (isInput)
6912 {
6913 //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6914 displayText = tr("Change Input Port to %1");
6915 }
6916 else
6917 {
6918 //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6919 displayText = tr("Change Output Port to %1");
6920 }
6921 }
6922
6923 return displayText.arg(typeDisplayName);
6924}
6925
6931{
6932 __block json_object *portValue = NULL;
6933
6934 if (! port->getRenderer()->getDataType())
6935 return portValue;
6936
6937 string runningPortIdentifier = identifierCache->getIdentifierForPort(port);
6938 bool isInput = port->getRenderer()->getInput();
6939
6940 void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6941 {
6942 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6943 if (topLevelComposition->isRunningThreadUnsafe())
6944 {
6945 portValue = isInput ?
6946 topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6947 topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6948 }
6949 });
6950 };
6951 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6952
6953 return portValue;
6954}
6955
6959string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6960{
6961 return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6962}
6963
6972{
6973 if (!staticPort)
6974 return "";
6975
6976 // Published ports
6977 if (dynamic_cast<VuoPublishedPort *>(staticPort))
6978 return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
6979
6980 // Internal ports
6981 // We might as well use the same naming scheme here as is used in the running composition,
6982 // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
6983 // node identifier has been explicitly set.
6984 string nodeIdentifier = "";
6985 if (parentNode && parentNode->hasCompiler())
6986 nodeIdentifier = parentNode->getCompiler()->getIdentifier();
6987 else if (staticPort->hasRenderer() &&
6988 staticPort->getRenderer()->getUnderlyingParentNode() &&
6990 {
6991 nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
6992 }
6993
6994 if (staticPort->hasCompiler() && !nodeIdentifier.empty())
6995 {
6996 dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
6997 return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
6998 }
6999 else
7000 return "";
7001}
7002
7007{
7008 VuoPort *port = nullptr;
7009 identifierCache->doForPortWithIdentifier(portID, [&port](VuoPort *p) {
7010 port = p;
7011 });
7012 return port;
7013}
7014
7021{
7022 if (port->hasRenderer())
7023 {
7024 if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
7025 {
7026 bool isPublishedInput = !port->getRenderer()->getInput();
7027 return (isPublishedInput? composition->getPublishedInputNode() :
7028 composition->getPublishedOutputNode());
7029 }
7030
7031 else
7032 return port->getRenderer()->getUnderlyingParentNode()->getBase();
7033 }
7034
7035 foreach (VuoNode *n, composition->getBase()->getNodes())
7036 {
7037 VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
7038 if (candidateInputPort == port)
7039 return n;
7040
7041 VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
7042 if (candidateOutputPort == port)
7043 return n;
7044 }
7045
7046 return NULL;
7047}
7048
7057{
7058 map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
7059 if (popover != activePortPopovers.end())
7060 return popover->second;
7061
7062 else
7063 return NULL;
7064}
7065
7073void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
7074{
7075 string portID = identifierCache->getIdentifierForPort(rp->getBase());
7076 bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
7077 if (!popoverJustClosedAtLastEvent)
7079}
7080
7085{
7086 if (!popoverEventsEnabled)
7087 return;
7088
7089 VuoPort *port = rp->getBase();
7090 string portID = identifierCache->getIdentifierForPort(port);
7091
7092 VUserLog("%s: Open popover for %s",
7093 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7094 portID.c_str());
7095
7096 dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
7097
7098 dispatch_sync(activePortPopoversQueue, ^{
7099
7100 if (activePortPopovers.find(portID) == activePortPopovers.end())
7101 {
7102 // Assigning the popover a parent widget allows us to give it rounded corners
7103 // and a background fill that respects its rounded boundaries.
7104 VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
7105
7106 connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
7107 connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
7108 VUserLog("%s: Detach popover for %s",
7109 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7110 portID.c_str());
7111 popoverDetached();
7112 });
7113 connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
7116
7117 // Line up the top left of the dialog with the port.
7118 QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
7119
7120 // Don't let popovers get cut off at the right or bottom edges of the canvas.
7121 const int cutoffMargin = 16;
7122 QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
7123 if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
7124 portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
7125 if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
7126 portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
7127
7128 QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
7129
7130 const QPoint offset = QPoint(12, 6);
7131
7132 QPoint popoverTopLeft = popoverLeftInView + offset;
7133 popover->move(popoverTopLeft);
7134 popover->show();
7135
7136 activePortPopovers[portID] = popover;
7137
7138 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
7140 });
7141 }
7142 });
7143 });
7144}
7145
7150void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
7151{
7152 if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
7154}
7155
7164{
7165 VUserLog("%s: Close popover for %s",
7166 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7167 portID.c_str());
7168
7169 VuoPortPopover *popover = NULL;
7170 map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
7171 if (i != activePortPopovers.end())
7172 {
7173 popover = i->second;
7174 activePortPopovers.erase(i);
7175 }
7176
7177 if (popover)
7178 {
7179 popover->hide();
7180 popover->deleteLater();
7181 }
7182
7183 bool isInput = false;
7184 bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7185 isInput = port->getRenderer()->getInput();
7186 });
7187
7188 if (! foundPort)
7189 return;
7190
7191 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7192 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7193 {
7194 dispatch_async(topLevelComposition->runCompositionQueue, ^{
7195 if (topLevelComposition->isRunningThreadUnsafe())
7196 {
7197 (isInput ?
7198 topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7199 topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7200 }
7201 });
7202 });
7203 });
7204}
7205
7209void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7210{
7211 dispatch_sync(activePortPopoversQueue, ^{
7212 disablePopoverForPort(portID);
7213 });
7214}
7215
7220{
7221 disablePortPopovers();
7223}
7224
7229{
7230 foreach (VuoErrorPopover *errorPopover, errorPopovers)
7231 {
7232 errorPopover->hide();
7233 errorPopover->deleteLater();
7234 }
7235
7236 errorPopovers.clear();
7237}
7238
7243void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7244{
7245 dispatch_sync(activePortPopoversQueue, ^{
7246 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7247 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7248 {
7249 string portID = i->first;
7250 bool shouldDisable = false;
7251
7252 if (! node)
7253 shouldDisable = true;
7254 else
7255 {
7256 identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7257 shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7258 });
7259 }
7260
7261 if (shouldDisable)
7262 disablePopoverForPort(portID);
7263 }
7264 });
7265}
7266
7271{
7272 dispatch_sync(activePortPopoversQueue, ^{
7273 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7274 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7275 {
7276 string portID = i->first;
7277
7278 bool foundPort = identifierCache->doForPortWithIdentifier(portID, [](VuoPort *port) {});
7279 if (! foundPort)
7280 disablePopoverForPort(portID);
7281 }
7282 });
7283}
7284
7290{
7291 if (recordWhichPopoversClosed)
7292 portsWithPopoversClosedAtLastEvent.clear();
7293
7294 dispatch_sync(activePortPopoversQueue, ^{
7295 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7296 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7297 {
7298 string portID = i->first;
7299 bool shouldDisable = false;
7300
7301 if (! node)
7302 shouldDisable = true;
7303 else
7304 {
7305 identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7306 shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7307 });
7308 }
7309
7310 if (shouldDisable)
7311 {
7312 VuoPortPopover *popover = getActivePopoverForPort(portID);
7313 if (! (popover && popover->getDetached()))
7314 {
7315 disablePopoverForPort(portID);
7316 portsWithPopoversClosedAtLastEvent.insert(portID);
7317 }
7318 }
7319 }
7320 });
7321}
7322
7327{
7328 moveDetachedPortPopoversBy(dx, dy);
7329 moveErrorPopoversBy(dx, dy);
7330}
7331
7335void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7336{
7337 foreach(VuoErrorPopover *errorPopover, errorPopovers)
7338 errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7339}
7340
7344void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7345{
7346 dispatch_sync(activePortPopoversQueue, ^{
7347 map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7348 for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7349 {
7350 VuoPortPopover *popover = i->second;
7351 if (popover && popover->getDetached())
7352 popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7353 }
7354 });
7355}
7356
7360void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7361{
7362 dispatch_sync(activePortPopoversQueue, ^{
7363 auto portPopovers = activePortPopovers;
7364 for (auto i : portPopovers)
7365 {
7366 VuoPortPopover *popover = i.second;
7367 if (popover && popover->getDetached())
7368 {
7369 id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7370 ((void (*)(id, SEL, BOOL))objc_msgSend)(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7371 }
7372 }
7373 });
7374}
7375
7381{
7382 dispatch_sync(activePortPopoversQueue, ^{
7383 for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7384 {
7385 string portID = i->first;
7386 VuoPortPopover *popover = i->second;
7387 bool shouldUpdate = false;
7388
7389 if (! node)
7390 shouldUpdate = true;
7391 else
7392 {
7393 identifierCache->doForPortWithIdentifier(portID, [&shouldUpdate, node](VuoPort *port) {
7394 shouldUpdate = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7395 });
7396 }
7397
7398 if (shouldUpdate)
7399 QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7400 }
7401 });
7402}
7403
7416 string popoverCompositionIdentifier,
7417 string portID)
7418{
7419 bool isInput;
7420 bool foundPort = popoverComposition->identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7421 isInput = port->getRenderer()->getInput();
7422 });
7423
7424 if (! foundPort)
7425 return;
7426
7427 string portSummary = (isInput ?
7428 runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7429 runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7430
7431 dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7432 VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7433 if (popover)
7434 {
7435 QMetaObject::invokeMethod(popover, "updateDataValueImmediately", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7436 QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7437 }
7438 });
7439}
7440
7449{
7450 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7451 {
7452 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7453 if (topLevelComposition->isRunningThreadUnsafe())
7454 topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7455 });
7456 });
7457}
7458
7463void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7464 bool receivedEvent, bool receivedData, string dataSummary)
7465{
7466 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7467 {
7468 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7469 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7470 if (popover)
7471 QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7472 Q_ARG(bool, receivedEvent),
7473 Q_ARG(bool, receivedData),
7474 Q_ARG(QString, dataSummary.c_str()));
7475 });
7476 };
7477 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7478}
7479
7484void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7485 bool sentEvent, bool sentData, string dataSummary)
7486{
7487 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7488 {
7489 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7490 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7491 if (popover)
7492 QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7493 Q_ARG(bool, sentEvent),
7494 Q_ARG(bool, sentData),
7495 Q_ARG(QString, dataSummary.c_str()));
7496 });
7497
7498 if (matchingComposition->showEventsMode && sentEvent)
7499 {
7500 matchingComposition->identifierCache->doForPortWithIdentifier(portIdentifier, [matchingComposition](VuoPort *port) {
7501 if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7502 {
7503 port->getRenderer()->setFiredEvent();
7504 matchingComposition->animatePort(port->getRenderer());
7505 }
7506 });
7507 }
7508 };
7509 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7510}
7511
7516void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7517{
7518 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7519 {
7520 dispatch_async(matchingComposition->runCompositionQueue, ^{
7521 if (matchingComposition->isRunningThreadUnsafe())
7522 {
7523 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7524 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7525 if (popover)
7526 QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7527 });
7528 }
7529 });
7530 };
7531 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7532}
7533
7538void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7539{
7540 void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7541 {
7542 if (matchingComposition->showEventsMode)
7543 {
7544 dispatch_async(this->runCompositionQueue, ^{
7545 if (this->isRunningThreadUnsafe())
7546 {
7547 matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7548 node->getRenderer()->setExecutionBegun();
7549 });
7550 }
7551 });
7552 }
7553 };
7554 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7555}
7556
7561void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7562{
7563 void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7564 {
7565 if (matchingComposition->showEventsMode)
7566 {
7567 dispatch_async(this->runCompositionQueue, ^{
7568 if (this->isRunningThreadUnsafe())
7569 {
7570 matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7571 node->getRenderer()->setExecutionEnded();
7572 });
7573 }
7574 });
7575 }
7576 };
7577 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7578}
7579
7590
7600
7605{
7606 return showEventsMode;
7607}
7608
7613{
7614 this->showEventsMode = showEventsMode;
7615
7616 if (showEventsMode)
7617 {
7619
7620 void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7621 {
7622 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7623 if (topLevelComposition->isRunningThreadUnsafe())
7624 topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7625 });
7626 };
7627 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7628 }
7629 else
7630 {
7632
7633 void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7634 {
7635 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7636 if (topLevelComposition->isRunningThreadUnsafe())
7637 topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7638 });
7639 };
7640 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7641 }
7642}
7643
7648{
7649 foreach (VuoCable *cable, getBase()->getCables())
7650 {
7651 if (cable->getCompiler()->getHidden() && !cable->isPublished())
7652 return true;
7653 }
7654
7655 return false;
7656}
7657
7662{
7663 foreach (VuoCable *cable, getBase()->getCables())
7664 {
7665 if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7666 return true;
7667 }
7668
7669 return false;
7670}
7671
7676QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7677{
7678 VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7679 NULL,
7680 port->getOutput(),
7681 port->getRefreshPort(),
7682 port->getFunctionPort());
7683 animatedPort->setAnimated(true);
7684 animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7685 animatedPort->setParentItem(port->getRenderedParentNode());
7686
7687 animation->setItem(animatedPort);
7688 animation->setScaleAt(0.0, 1, 1);
7689 animation->setScaleAt(0.999, 3, 3);
7690 animation->setScaleAt(1.0, 1, 1);
7691
7692 QTimeLine *animationTimeline = animation->timeLine();
7693 animationTimeline->setFrameRange(0, 100);
7694 animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7695 animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7696
7697 preparedAnimations.insert(animation);
7698 animationForTimeline[animation->timeLine()] = animation;
7699
7700 connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7701 connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7702
7703 return animation;
7704}
7705
7709void VuoEditorComposition::animatePort(VuoRendererPort *port)
7710{
7711 dispatch_async(dispatch_get_main_queue(), ^{
7712 QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7713 if (! animation)
7714 return;
7715
7716 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7717
7718 if (animation->timeLine()->state() == QTimeLine::Running)
7719 animation->timeLine()->setCurrentTime(0);
7720
7721 else
7722 {
7723 animatedPort->setPos(port->pos());
7724 animatedPort->setVisible(true);
7725 animation->timeLine()->start();
7726 }
7727 });
7728}
7729
7734QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7735{
7736 vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7737
7738 QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7739 qreal maxPercentAdvanced = -1;
7740
7741 for (int i = 0; i < animations.size(); ++i)
7742 {
7743 QGraphicsItemAnimation *animation = animations[i];
7744 bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7745 bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7746
7747 if (! animationPrepared)
7748 return setUpAnimationForPort(animation, port);
7749
7750 else if (! animationRunning)
7751 return animation;
7752
7753 // If all of the port's animations are already running, return the
7754 // one that has been running the longest.
7755 qreal percentAdvanced = animation->timeLine()->currentValue();
7756 if (percentAdvanced > maxPercentAdvanced)
7757 {
7758 mostAdvancedAnimation = animation;
7759 maxPercentAdvanced = percentAdvanced;
7760 }
7761 }
7762
7763 // If no animation is even halfway complete, return NULL to indicate
7764 // that no animation is currently available.
7765 return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7766}
7767
7773void VuoEditorComposition::updatePortAnimation(qreal value)
7774{
7775 QTimeLine *animationTimeline = (QTimeLine *)sender();
7776 QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7777 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7778 const qreal multiplier = 1000.;
7779 animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7780}
7781
7786void VuoEditorComposition::endPortAnimation(void)
7787{
7788 QTimeLine *animationTimeline = (QTimeLine *)sender();
7789 QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7790 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7791 animatedPort->setVisible(false);
7792}
7793
7797void VuoEditorComposition::setDisableDragStickiness(bool disable)
7798{
7799 this->dragStickinessDisabled = disable;
7800}
7801
7806{
7807 this->ignoreApplicationStateChangeEvents = ignore;
7808}
7809
7816{
7817 this->popoverEventsEnabled = enable;
7818}
7819
7824{
7825 setRenderActivity(true, includePorts);
7826 refreshComponentAlphaLevelTimer->start();
7827}
7828
7833{
7834 refreshComponentAlphaLevelTimer->stop();
7835 setRenderActivity(false);
7836}
7837
7855{
7856 // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7857 if (!activeProtocol)
7858 {
7859 VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7860 return false;
7861 }
7862
7863 // Can events from at least one trigger reach at least one published output port? If not, report an error.
7864 if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7865 {
7866 QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7867 QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7868 "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7869
7870 if (isExportingMovie)
7871 errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7872
7874 QMessageBox messageBox(window);
7875 messageBox.setWindowFlags(Qt::Sheet);
7876 messageBox.setWindowModality(Qt::WindowModal);
7877 messageBox.setFont(fonts->dialogHeadingFont());
7878 messageBox.setTextFormat(Qt::RichText);
7879
7880 messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7881 messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7882 messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7883 messageBox.setDefaultButton(QMessageBox::Ok);
7884
7885 messageBox.setText(errorHeadline);
7886 messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7887
7888 if (messageBox.exec() == QMessageBox::Help)
7889 {
7890 map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7891 map<QString, QString>::iterator i = examples.begin();
7892 if (i != examples.end())
7893 QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7894 }
7895 return false;
7896 }
7897
7898 return true;
7899}
7900
7905{
7906 return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7907}
7908
7916
7920string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7921{
7922 string dir, file, ext;
7923 VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7924 return file;
7925}
7926
7934{
7935 string customizedName = getBase()->getMetadata()->getCustomizedName();
7936 if (! customizedName.empty())
7937 return QString::fromStdString(customizedName);
7938
7939 string name = getBase()->getMetadata()->getName();
7940 return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7941}
7942
7950QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7951{
7952 // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7953 // If the extensionless filename contains dot-delimited segments, use only the final segment.
7954 vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7955 string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7956 fileNameParts[fileNameParts.size()-2] :
7957 (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7958
7959 // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7960 if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7961 {
7962 string formattedName = fileNameContentPart;
7963 if (formattedName.size() >= 1)
7964 formattedName[0] = toupper(formattedName[0]);
7965
7966 return QString(formattedName.c_str());
7967 }
7968
7969 // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
7970 return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
7971}
7972
7979{
7980 QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
7981 if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
7982 {
7983 // If not an official Vuo nodeset, return the name as-is.
7984 return nodeSetName;
7985 }
7986
7987 map<QString, QString> wordsToReformat;
7988 wordsToReformat["artnet"] = "Art-Net";
7989 wordsToReformat["bcf2000"] = "BCF2000";
7990 wordsToReformat["hid"] = "HID";
7991 wordsToReformat["midi"] = "MIDI";
7992 wordsToReformat["ndi"] = "NDI";
7993 wordsToReformat["osc"] = "OSC";
7994 wordsToReformat["rss"] = "RSS";
7995 wordsToReformat["ui"] = "UI";
7996 wordsToReformat["url"] = "URL";
7997
7998 QString nodeSetDisplayName = "";
7999 for (int i = 1; i < wordsInName.size(); ++i)
8000 {
8001 QString currentWord = wordsInName[i];
8002 if (currentWord.size() >= 1)
8003 {
8004 if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
8005 currentWord = wordsToReformat.at(currentWord.toLower());
8006 else
8007 currentWord[0] = currentWord[0].toUpper();
8008
8009 nodeSetDisplayName += currentWord;
8010
8011 if (i < wordsInName.size()-1)
8012 nodeSetDisplayName += " ";
8013 }
8014 }
8015 return nodeSetDisplayName;
8016}
8017
8022{
8023 set<string> listTypes = moduleManager->getKnownListTypeNames(false);
8024 auto foundListType = listTypes.find(typeName);
8025 if (foundListType != listTypes.end())
8026 {
8027 // Don't request the list type from the compiler, since that causes a delay if the type is not already generated.
8028 auto getVuoType = [this] (const string &typeName) -> VuoCompilerType *
8029 {
8030 map<string, VuoCompilerType *> singletonTypes = moduleManager->getLoadedSingletonTypes(false);
8031 VuoCompilerType *singletonType = singletonTypes[typeName];
8032 if (singletonType)
8033 return singletonType;
8034
8035 map<string, VuoCompilerType *> compoundTypes = moduleManager->getLoadedGenericCompoundTypes();
8036 VuoCompilerType *compoundType = compoundTypes[typeName];
8037 if (compoundType)
8038 return compoundType;
8039
8040 return nullptr;
8041 };
8042 string defaultTitle = VuoCompilerCompoundType::buildDefaultTitle(typeName, getVuoType);
8043 return QString::fromStdString(defaultTitle);
8044 }
8045
8046 map<string, VuoCompilerType *> singletonTypes = moduleManager->getLoadedSingletonTypes(false);
8047 VuoCompilerType *singletonType = singletonTypes[typeName];
8048 return formatTypeNameForDisplay(singletonType ? singletonType->getBase() : nullptr);
8049}
8050
8055{
8056 if (! type)
8057 return "(none)";
8058
8059 string defaultTitle = type->getDefaultTitle();
8060 return QString::fromStdString(defaultTitle);
8061}
8062
8067{
8068 if (!type)
8069 return "Event";
8070
8071 // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
8072 else if (type->getDefaultTitle() == "2D Point")
8073 return "Point2D";
8074 else if (type->getDefaultTitle() == "3D Point")
8075 return "Point3D";
8076 else if (type->getDefaultTitle() == "4D Point")
8077 return "Point4D";
8078 else if (type->getDefaultTitle() == "2D Transform")
8079 return "Transform2D";
8080 else if (type->getDefaultTitle() == "3D Transform")
8081 return "Transform3D";
8082
8083 return VuoRendererPort::sanitizePortName(formatTypeNameForDisplay(type)).toUtf8().constData();
8084}
8085
8090bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
8091{
8092 qreal item1Y = item1->scenePos().y();
8093 qreal item2Y = item2->scenePos().y();
8094
8095 qreal item1X = item1->scenePos().x();
8096 qreal item2X = item2->scenePos().x();
8097
8098 if (item1Y == item2Y)
8099 return (item1X < item2X);
8100
8101 return item1Y < item2Y;
8102}
8103
8110double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
8111{
8112 // Assign replacement (successor) node classes a perfect match score.
8113 {
8114 string originalGenericNode1ClassName, originalGenericNode2ClassName;
8115 if (node1->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler()))
8116 originalGenericNode1ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler())->getOriginalGenericNodeClassName();
8117 else
8118 originalGenericNode1ClassName = node1->getClassName();
8119
8120 if (node2->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler()))
8121 originalGenericNode2ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler())->getOriginalGenericNodeClassName();
8122 else
8123 originalGenericNode2ClassName = node2->getClassName();
8124
8125 if (VuoEditorUtilities::isNodeClassSuccessorTo(originalGenericNode1ClassName.c_str(), originalGenericNode2ClassName.c_str()))
8126 return 1;
8127 }
8128
8129 // Compare keywords.
8130 vector<string> node1Keywords = node1->getKeywords();
8131 vector<string> node2Keywords = node2->getKeywords();
8132
8133 // Compare node set names.
8134 if (node1->getNodeSet())
8135 node1Keywords.push_back(node1->getNodeSet()->getName());
8136
8137 if (node2->getNodeSet())
8138 node2Keywords.push_back(node2->getNodeSet()->getName());
8139
8140 // Compare tokens in node class display names.
8141 foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
8142 if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8143 node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8144
8145 foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
8146 if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8147 node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8148
8149 set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
8150 set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
8151
8152 set<string> nodeKeywordsIntersection;
8153 std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8154 node2KeywordSet.begin(), node2KeywordSet.end(),
8155 std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8156
8157 set<string> nodeKeywordsUnion = node1KeywordSet;
8158 nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8159
8160 // Avoid division by zero.
8161 if (nodeKeywordsUnion.size() == 0)
8162 return 0;
8163
8164 // Calculate Jaccard similarity.
8165 double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8166
8167 return nodeSimilarity;
8168}
8169
8174{
8175 emit compositionOnTop(top);
8176}
8177
8185
8186VuoEditorComposition::~VuoEditorComposition()
8187{
8188 dispatch_sync(runCompositionQueue, ^{});
8189 dispatch_release(runCompositionQueue);
8190
8191 preparedAnimations.clear();
8192 animationForTimeline.clear();
8193
8194 delete identifierCache;
8195
8196 delete moduleManager; // deletes compiler
8197}
8198
8203{
8204 // Update the canvas color.
8206
8207 // Force repainting the entire canvas.
8208 setComponentCaching(QGraphicsItem::NoCache);
8211}
8212
8218map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8219{
8220 vector<VuoRendererPort *> sortedPortsToPublish;
8221 foreach (string portID, portsToPublish)
8222 {
8223 VuoPort *port = getPortWithStaticIdentifier(portID);
8224 if (port && port->hasRenderer())
8225 sortedPortsToPublish.push_back(port->getRenderer());
8226 }
8227 std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8228
8229 map<string, string> publishedPortNames;
8230 foreach (VuoRendererPort *rp, sortedPortsToPublish)
8231 {
8232 rp->updateGeometry();
8233 VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8234
8235 string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8236 string publishedPortName = (!specializedPublishedPortName.empty()?
8237 specializedPublishedPortName :
8238 VuoRendererPort::sanitizePortName(rp->getPortNameToRenderWhenDisplayed().c_str()).toUtf8().constData());
8239
8240 bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8241 VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8242
8243 publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8244 }
8245
8246 return publishedPortNames;
8247}
8248
8255{
8256 if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8257 return "";
8258
8259 // If publishing a port on a "Share Value" node and the node's title has been
8260 // customized, request that title as the published port name.
8264 {
8265 return VuoRendererPort::sanitizePortName(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8266 }
8267
8268 return "";
8269}
8270
8274void VuoEditorComposition::repositionPopover()
8275{
8276 VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8277 if (popover && !popover->getDetached())
8278 {
8279 const int cutoffMargin = 16;
8280 if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8281 popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8282
8283 if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8284 popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8285 }
8286}