Vuo  2.4.1
VuoEditorComposition.cc
Go to the documentation of this file.
1
11
13#include "VuoCompilerIssue.hh"
14#include "VuoComposition.hh"
16#include "VuoEditorWindow.hh"
17#include "VuoErrorDialog.hh"
18#include "VuoException.hh"
19#include "VuoFileType.h"
20#include "VuoRendererComment.hh"
21#include "VuoRendererFonts.hh"
25#include "VuoCompilerCable.hh"
26#include "VuoCompilerComment.hh"
28#include "VuoCompilerDriver.hh"
30#include "VuoCompilerGraph.hh"
33#include "VuoCompilerNode.hh"
40#include "VuoCompilerType.hh"
41#include "VuoGenericType.hh"
43#include "VuoComment.hh"
44#include "VuoEditor.hh"
45#include "VuoErrorMark.hh"
46#include "VuoErrorPopover.hh"
47#include "VuoModuleManager.hh"
48#include "VuoNodeClass.hh"
49#include "VuoNodeSet.hh"
50#include "VuoPortPopover.hh"
51#include "VuoProtocol.hh"
52#include "VuoStringUtilities.hh"
53#include "VuoInputEditorIcon.hh"
56#include "VuoEditorUtilities.hh"
58
59#ifdef __APPLE__
60#include <ApplicationServices/ApplicationServices.h>
61#include <objc/objc-runtime.h>
62#endif
63
64const qreal VuoEditorComposition::nodeMoveRate = 15; // VuoRendererComposition::minorGridLineSpacing;
65const qreal VuoEditorComposition::nodeMoveRateMultiplier = 4; // VuoRendererComposition::majorGridLineSpacing / VuoRendererComposition::minorGridLineSpacing
67const qreal VuoEditorComposition::showEventsModeUpdateInterval = 1000/20.; // interval, in ms, after which to update component transparency levels and animations while in 'Show Events' mode
68const int VuoEditorComposition::initialChangeNodeSuggestionCount = 10; // The initial number of suggestions to list in the "Change (Node) To" context menu
69
74 VuoRendererComposition(baseComposition, false, true)
75{
76#if VUO_PRO
77 VuoEditorComposition_Pro();
78#endif
79
80 this->window = window;
81 compiler = NULL;
82 inputEditorManager = NULL;
83 activeProtocol = NULL;
84 runner = NULL;
85 runningComposition = NULL;
86 runningCompositionActiveDriver = NULL;
87 runningCompositionLibraries = NULL;
88 stopRequested = false;
89 duplicateOnNextMouseMove = false;
90 duplicationDragInProgress = false;
91 duplicationCancelled = false;
92 cursorPosBeforeDuplicationDragMove = QPointF(0,0);
93 cableInProgress = NULL;
94 cableInProgressWasNew = false;
95 cableInProgressShouldBeWireless = false;
96 portWithDragInitiated = NULL;
97 cableWithYankInitiated = NULL;
98 menuSelectionInProgress = false;
99 previousNearbyItem = NULL;
100 dragStickinessDisabled = false;
101 ignoreApplicationStateChangeEvents = false;
102 popoverEventsEnabled = true;
103 runCompositionQueue = dispatch_queue_create("org.vuo.editor.run", NULL);
104 activePortPopoversQueue = dispatch_queue_create("org.vuo.editor.popovers", NULL);
105 errorMark = NULL;
106 errorMarkingUpdatesEnabled = true;
107 triggerPortToRefire = "";
108
109 contextMenuDeleteSelected = new QAction(NULL);
110 contextMenuHideSelectedCables = new QAction(NULL);
111 contextMenuRenameSelected = new QAction(NULL);
112 contextMenuRefactorSelected = new QAction(NULL);
113 contextMenuPublishPort = new QAction(NULL);
114 contextMenuDeleteCables = new QAction(NULL);
115 contextMenuHideCables = new QAction(NULL);
116 contextMenuUnhideCables = new QAction(NULL);
117 contextMenuFireEvent = new QAction(NULL);
118 contextMenuAddInputPort = new QAction(NULL);
119 contextMenuRemoveInputPort = new QAction(NULL);
120 contextMenuSetPortConstant = new QAction(NULL);
121 contextMenuEditSelectedComments = new QAction(NULL);
122
123 contextMenuChangeNode = NULL;
124 contextMenuSpecializeGenericType = NULL;
125
126 contextMenuFireEvent->setText(tr("Fire Event"));
127 contextMenuHideSelectedCables->setText(tr("Hide"));
128 contextMenuRenameSelected->setText(tr("Rename…"));
129 contextMenuRefactorSelected->setText(tr("Package as Subcomposition"));
130 contextMenuAddInputPort->setText(tr("Add Input Port"));
131 contextMenuRemoveInputPort->setText(tr("Remove Input Port"));
132 contextMenuSetPortConstant->setText(tr("Edit Value…"));
133 contextMenuEditSelectedComments->setText(tr("Edit…"));
134
135 connect(contextMenuDeleteSelected, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
136 connect(contextMenuHideSelectedCables, &QAction::triggered, this, &VuoEditorComposition::selectedInternalCablesHidden);
137 connect(contextMenuRenameSelected, &QAction::triggered, this, &VuoEditorComposition::renameSelectedNodes);
138 connect(contextMenuRefactorSelected, &QAction::triggered, this, &VuoEditorComposition::refactorRequested);
139 connect(contextMenuPublishPort, &QAction::triggered, this, &VuoEditorComposition::togglePortPublicationStatus);
140 connect(contextMenuDeleteCables, &QAction::triggered, this, &VuoEditorComposition::deleteConnectedCables);
141 connect(contextMenuHideCables, &QAction::triggered, this, &VuoEditorComposition::hideConnectedCables);
142 connect(contextMenuUnhideCables, &QAction::triggered, this, &VuoEditorComposition::unhideConnectedCables);
143 connect(contextMenuFireEvent, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::fireTriggerPortEvent));
144 connect(contextMenuAddInputPort, &QAction::triggered, this, &VuoEditorComposition::addInputPort);
145 connect(contextMenuRemoveInputPort, &QAction::triggered, this, &VuoEditorComposition::removeInputPort);
146 connect(contextMenuEditSelectedComments, &QAction::triggered, this, &VuoEditorComposition::editSelectedComments);
147
148 // Use a queued connection to open input editors in order to avoid bug where invoking a
149 // QColorDialog by context menu prevents subsequent interaction with the editor window
150 // even after the color dialog has been closed.
151 connect(contextMenuSetPortConstant, &QAction::triggered, this, &VuoEditorComposition::setPortConstant, Qt::QueuedConnection);
152
153 {
154 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
155 // For now, this sets up the actions for a menu, rather than setting up the menu itself.
156
157 auto addThrottlingAction = [=](QString label, VuoPortClass::EventThrottling throttling) {
158 QAction *action = new QAction(label, this);
159 connect(action, &QAction::triggered, [=](){
160 emit triggerThrottlingUpdated(action->data().value<VuoRendererPort *>()->getBase(), throttling);
161 });
162 contextMenuThrottlingActions.append(action);
163 };
164 addThrottlingAction(tr("Enqueue Events"), VuoPortClass::EventThrottling_Enqueue);
165 addThrottlingAction(tr("Drop Events"), VuoPortClass::EventThrottling_Drop);
166 }
167
168 {
169 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
170 // For now, this sets up the actions for a menu, rather than setting up the menu itself.
171
172 auto addTintAction = [=](QString label, VuoNode::TintColor tint) {
173 QAction *action = new QAction(label, this);
174 connect(action, &QAction::triggered, [=](){
175 static_cast<VuoEditorWindow *>(window)->tintSelectedItems(tint);
176 });
177
178 // Add a color swatch to the menu item.
179 {
180 QColor fill(0,0,0,0);
181 // For TintNone, draw a transparent icon, so that menu item's text indent is consistent with the other items.
182 if (tint != VuoNode::TintNone)
183 {
184 VuoRendererColors colors(tint);
185 fill = colors.nodeFill();
186 }
187
188 QIcon *icon = VuoInputEditorIcon::renderIcon(^(QPainter &p){
189 p.setPen(Qt::NoPen);
190 p.setBrush(fill);
191 // Match distance between text baseline and ascender.
192 p.drawEllipse(3, 3, 10, 10);
193 });
194 action->setIcon(*icon);
195 delete icon;
196 }
197
198 contextMenuTintActions.append(action);
199 };
200 addTintAction(tr("Yellow"), VuoNode::TintYellow);
201 addTintAction(tr("Tangerine"), VuoNode::TintTangerine);
202 addTintAction(tr("Orange"), VuoNode::TintOrange);
203 addTintAction(tr("Magenta"), VuoNode::TintMagenta);
204 addTintAction(tr("Violet"), VuoNode::TintViolet);
205 addTintAction(tr("Blue"), VuoNode::TintBlue);
206 addTintAction(tr("Cyan"), VuoNode::TintCyan);
207 addTintAction(tr("Green"), VuoNode::TintGreen);
208 addTintAction(tr("Lime"), VuoNode::TintLime);
209 addTintAction(tr("None"), VuoNode::TintNone);
210 }
211
212 // 'Show Events' mode rendering setup
213 this->refreshComponentAlphaLevelTimer = new QTimer(this);
214 this->refreshComponentAlphaLevelTimer->setObjectName("VuoEditorComposition::refreshComponentAlphaLevelTimer");
215 refreshComponentAlphaLevelTimer->setInterval(showEventsModeUpdateInterval);
216 connect(refreshComponentAlphaLevelTimer, &QTimer::timeout, this, &VuoEditorComposition::updateGeometryForAllComponents);
217 setShowEventsMode(false);
218
219 connect(signaler, &VuoRendererSignaler::nodePopoverRequested, this, &VuoEditorComposition::enablePopoverForNode);
220 connect(signaler, &VuoRendererSignaler::nodesMoved, this, &VuoEditorComposition::moveNodesBy);
221 connect(signaler, &VuoRendererSignaler::commentsMoved, this, &VuoEditorComposition::moveCommentsBy);
222 connect(signaler, &VuoRendererSignaler::commentResized, this, &VuoEditorComposition::resizeCommentBy);
229 connect(signaler, &VuoRendererSignaler::dragStickinessDisableRequested, this, &VuoEditorComposition::setDisableDragStickiness);
230 connect(signaler, &VuoRendererSignaler::openUrl, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
231
232 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::activeApplicationStateChanged, this, &VuoEditorComposition::updatePopoversForApplicationStateChange, Qt::QueuedConnection);
233 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::focusChanged, this, &VuoEditorComposition::updatePopoversForActiveWindowChange, Qt::QueuedConnection);
234 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationWillHide, this, [=]{
235 setPopoversHideOnDeactivate(true);
236 });
237 connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationDidUnhide, this, [=]{
238 setPopoversHideOnDeactivate(false);
239 });
240
241 identifierCache = new VuoNodeAndPortIdentifierCache;
242 identifierCache->addCompositionComponentsToCache(getBase());
243}
244
249{
250 this->compiler = compiler;
251}
252
257{
258 return compiler;
259}
260
265{
266 this->moduleManager = moduleManager;
267 moduleManager->setComposition(this);
268}
269
274{
275 return moduleManager;
276}
277
284{
285 this->inputEditorManager = inputEditorManager;
286}
287
294{
295 return this->inputEditorManager;
296}
297
301VuoRendererNode * VuoEditorComposition::createNode(QString nodeClassName, string title, double x, double y)
302{
303 if (compiler)
304 {
305 VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
306 if (nodeClass)
307 {
308 VuoNode *node = createBaseNode(nodeClass, nullptr, title, x, y);
309 if (node)
310 {
312 setCustomConstantsForNewNode(rn);
313 return rn;
314 }
315 }
316 }
317 return NULL;
318}
319
326VuoNode * VuoEditorComposition::createBaseNode(VuoCompilerNodeClass *nodeClass, VuoNode *modelNode, string title, double x, double y)
327{
328 // If adding the node would create recursion (subcomposition contains itself), create a node without a compiler detail.
329 __block bool isAllowed = true;
330 if (nodeClass->isSubcomposition())
331 {
332 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
333 string compositionModuleKey = VuoCompiler::getModuleKeyForPath(compositionPath);
334 bool nodeIsThisComposition = (compositionModuleKey == nodeClass->getBase()->getClassName());
335
336 set<string> dependencies = nodeClass->getDependencies();
337 auto iter = std::find_if(dependencies.begin(), dependencies.end(), [=](const string &d){ return d == compositionModuleKey; });
338 bool nodeContainsThisComposition = (iter != dependencies.end());
339
340 isAllowed = ! (nodeIsThisComposition || nodeContainsThisComposition);
341 });
342 }
343
344 VuoNode *node;
345 if (isAllowed)
346 {
347 node = (modelNode ?
348 compiler->createNode(nodeClass, modelNode) :
349 compiler->createNode(nodeClass, title, x, y));
350 }
351 else
352 {
353 node = createNodeWithMissingImplementation(nodeClass->getBase(), modelNode, title, x, y);
354 node->setForbidden(true);
355 }
356 return node;
357}
358
365VuoNode * VuoEditorComposition::createNodeWithMissingImplementation(VuoNodeClass *modelNodeClass, VuoNode *modelNode, string title, double x, double y)
366{
367 vector<string> inputPortClassNames;
368 vector<string> outputPortClassNames;
369 foreach (VuoPortClass *portClass, modelNodeClass->getInputPortClasses())
370 {
371 if (portClass == modelNodeClass->getRefreshPortClass())
372 continue;
373 inputPortClassNames.push_back(portClass->getName());
374 }
375 foreach (VuoPortClass *portClass, modelNodeClass->getOutputPortClasses())
376 outputPortClassNames.push_back(portClass->getName());
377
378 VuoNodeClass *dummyNodeClass = new VuoNodeClass(modelNodeClass->getClassName(), inputPortClassNames, outputPortClassNames);
379 return (modelNode ?
380 dummyNodeClass->newNode(modelNode) :
381 dummyNodeClass->newNode(! title.empty() ? title : modelNodeClass->getDefaultTitle(), x, y));
382}
383
389void VuoEditorComposition::setCustomConstantsForNewNode(VuoRendererNode *newNode)
390{
391 // vuo.time.make: Set the 'year' input to the current year.
392 if (newNode->getBase()->getNodeClass()->getClassName() == "vuo.time.make")
393 {
394 VuoPort *yearPort = newNode->getBase()->getInputPortWithName("year");
395 if (yearPort)
396 {
397 QString currentYear = QString::number(QDateTime::currentDateTime().date().year());
398 yearPort->getRenderer()->setConstant(VuoText_getString(currentYear.toUtf8().constData()));
399 }
400 }
401}
402
406void VuoEditorComposition::addNode(VuoNode *n, bool nodeShouldBeRendered, bool nodeShouldBeGivenUniqueIdentifier)
407{
408 VuoRendererComposition::addNode(n, nodeShouldBeRendered, nodeShouldBeGivenUniqueIdentifier);
409 identifierCache->addNodeToCache(n);
410}
411
418{
419 if (resetState)
420 {
421 disablePortPopovers(rn);
422
423 if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == rn))
425 }
426
428}
429
444{
445 // Inventory the port constants and connected input cables associated with the old node, to be re-associated with the new node.
446 map<VuoCable *, VuoPort *> cablesToTransferFromPort;
447 map<VuoCable *, VuoPort *> cablesToTransferToPort;
448 set<VuoCable *> cablesToRemove;
449 getBase()->getCompiler()->getChangesToReplaceNode(oldNode->getBase(), newNode, cablesToTransferFromPort, cablesToTransferToPort, cablesToRemove);
450
451 // Also inventory any typecasts collapsed onto the old node, to be attached to the new node instead.
452 vector<VuoRendererInputDrawer *> attachedDrawers;
453 vector<VuoRendererNode *> collapsedTypecasts;
454 vector<VuoPort *> oldInputPorts = oldNode->getBase()->getInputPorts();
455 for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
456 {
457 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
458 if (typecastPort)
459 {
460 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
461 collapsedTypecasts.push_back(typecastNode);
462 }
463
464 // If the original node is currently being rendered as collapsed typecast, uncollapse it.
465 if (oldNode->getProxyCollapsedTypecast())
466 uncollapseTypecastNode(oldNode);
467
468 // Uncollapse typecasts attached to the original node.
469 for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
471 }
472
473 // Inventory any attachments to the old node, to make sure none are stranded in the replacement.
474 for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
475 {
476 VuoRendererPort *inputPortRenderer = (*inputPort)->getRenderer();
477 VuoRendererInputDrawer *attachedDrawer = inputPortRenderer->getAttachedInputDrawer();
478 if (attachedDrawer)
479 attachedDrawers.push_back(attachedDrawer);
480 }
481
482 // Perform the node replacement.
483 replaceNode(oldNode, newNode);
484
485 // Restore connected cables.
486 for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferFromPort.begin(); i != cablesToTransferFromPort.end(); ++i)
487 i->first->getRenderer()->setFrom(newNode, i->second);
488 for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferToPort.begin(); i != cablesToTransferToPort.end(); ++i)
489 i->first->getRenderer()->setTo(newNode, i->second);
490 foreach (VuoCable *cable, cablesToRemove)
491 removeCable(cable->getRenderer());
492
493 // Restore constant values.
494 for (VuoPort *oldInputPort : oldNode->getBase()->getInputPorts())
495 {
496 VuoPort *newInputPort = newNode->getInputPortWithName(oldInputPort->getClass()->getName());
497 if (! newInputPort)
498 continue;
499
500 if (! oldInputPort->getRenderer()->carriesData() || ! newInputPort->getRenderer()->carriesData())
501 continue;
502
503 if (oldNode->getBase()->hasCompiler() && newNode->hasCompiler())
504 {
505 VuoType *oldDataType = static_cast<VuoCompilerPort *>(oldInputPort->getCompiler())->getDataVuoType();
506 VuoType *newDataType = static_cast<VuoCompilerPort *>(newInputPort->getCompiler())->getDataVuoType();
507 if (! (oldDataType == newDataType && oldDataType && ! dynamic_cast<VuoGenericType *>(oldDataType)) )
508 continue;
509 }
510
511 string oldConstantValue;
512 if (oldNode->getBase()->hasCompiler())
513 oldConstantValue = oldInputPort->getRenderer()->getConstantAsString();
514 else
515 oldConstantValue = oldInputPort->getRawInitialValue();
516
517 if (newNode->hasCompiler())
518 updatePortConstant(static_cast<VuoCompilerPort *>(newInputPort->getCompiler()), oldConstantValue, false);
519 else
520 newInputPort->setRawInitialValue(oldConstantValue);
521 }
522
523 // Remove any stranded drawers and their incoming cables.
524 // @todo https://b33p.net/kosada/node/16441 and https://b33p.net/kosada/node/16441 :
525 // Decide how to handle stranded attachment deletion and insertion properly, including
526 // updates to the running composition and all types of incoming connections to the attachments.
527 // For now just make sure not to leave behind a stranded drawer or any of its incoming cables.
528 foreach (VuoRendererInputDrawer *drawer, attachedDrawers)
529 {
530 if (!drawer->getRenderedHostPort())
531 {
532 foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
533 {
534 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(drawerPort->getBase()->getRenderer());
535 if (typecastPort)
536 uncollapseTypecastNode(typecastPort);
537
538 foreach (VuoCable *cable, drawerPort->getBase()->getConnectedCables())
539 removeCable(cable->getRenderer());
540 }
541
542 removeNode(drawer);
543 }
544 }
545
546 // Restore connected typecasts.
547 for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
549
550 // Re-collapse the updated node, if applicable.
552}
553
559{
560 disablePortPopovers(oldNode);
561
562 if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == oldNode))
564
566 if (newNode->hasCompiler())
567 {
568 string graphvizIdentifier = (oldNode->getBase()->hasCompiler() ?
569 oldNode->getBase()->getCompiler()->getGraphvizIdentifier() :
570 oldNode->getBase()->getRawGraphvizIdentifier());
571 newNode->getCompiler()->setGraphvizIdentifier(graphvizIdentifier);
572 }
573
574 removeNode(oldNode);
575 addNode(newNode, true, false);
576
577 identifierCache->addNodeToCache(newNode);
578}
579
583void VuoEditorComposition::removeCable(VuoRendererCable *rc, bool emitHiddenCableNotification)
584{
585 bool cableHidden = rc->getBase()->getCompiler()->getHidden();
587
588 if (cableHidden && emitHiddenCableNotification)
590}
591
595void VuoEditorComposition::addCable(VuoCable *cable, bool emitHiddenCableNotification)
596{
597 bool cableHidden = cable->getCompiler()->getHidden();
599
600 if (cableHidden && emitHiddenCableNotification)
602}
603
613{
614 return VuoRendererComposition::createAndConnectMakeListNode(toNode, toPort, compiler, rendererCable);
615}
616
626 set<VuoRendererNode *> &createdNodes,
627 set<VuoRendererCable *> &createdCables)
628{
629 return VuoRendererComposition::createAndConnectDictionaryAttachmentsForNode(node, compiler, createdNodes, createdCables);
630}
631
638QList<QGraphicsItem *> VuoEditorComposition::createAndConnectInputAttachments(VuoRendererNode *node, bool createButDoNotAdd)
639{
640 QList<QGraphicsItem *> addedComponents = VuoRendererComposition::createAndConnectInputAttachments(node, compiler, createButDoNotAdd);
641 foreach (QGraphicsItem *component, addedComponents)
642 {
643 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(component);
644 if (rn && !createButDoNotAdd)
645 identifierCache->addNodeToCache(rn->getBase());
646 }
647
648 return addedComponents;
649}
650
658set<QGraphicsItem *> VuoEditorComposition::getDependentAttachmentsForNode(VuoRendererNode *rn, bool includeCoattachments)
659{
660 set<QGraphicsItem *> dependentAttachments;
661
662 // Get upstream attachments.
663 vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
664 for(unsigned int i = 0; i < inputPorts.size(); ++i)
665 {
666 set<VuoRendererInputAttachment *> portUpstreamAttachments = inputPorts[i]->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
667 dependentAttachments.insert(portUpstreamAttachments.begin(), portUpstreamAttachments.end());
668 }
669
670 // Get co-attachments.
671 VuoRendererInputAttachment *nodeAsAttachment = dynamic_cast<VuoRendererInputAttachment *>(rn);
672 if (nodeAsAttachment && includeCoattachments)
673 {
674 foreach (VuoNode *coattachment, nodeAsAttachment->getCoattachments())
675 dependentAttachments.insert(coattachment->getRenderer());
676 }
677
678 return dependentAttachments;
679}
680
681
687{
688 identifierCache->clearCache();
689
690 // Record the IDs of the currently selected components so that the selection status
691 // of the corresponding items may be restored after the composition is reset.
692 set<string> selectedNodeIDs;
693 foreach (VuoRendererNode *rn, getSelectedNodes())
694 {
695 if (rn->getBase()->hasCompiler())
696 selectedNodeIDs.insert(rn->getBase()->getCompiler()->getGraphvizIdentifier());
697 }
698
699 set<string> selectedCommentIDs;
701 {
702 if (rc->getBase()->hasCompiler())
703 selectedCommentIDs.insert(rc->getBase()->getCompiler()->getGraphvizIdentifier());
704 }
705
706 set<string> selectedCableIDs;
707 foreach (VuoRendererCable *rc, getSelectedCables(true))
708 {
709 if (rc->getBase()->hasCompiler())
710 selectedCableIDs.insert(rc->getBase()->getCompiler()->getGraphvizDeclaration());
711 }
712
713 modify();
714
715 // Restore the selection status of pre-existing components.
716 foreach (QGraphicsItem *item, items())
717 {
718 if (dynamic_cast<VuoRendererNode *>(item))
719 {
720 string currentNodeID = (dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler()?
721 dynamic_cast<VuoRendererNode *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
722 "");
723 if (!currentNodeID.empty() && (selectedNodeIDs.find(currentNodeID) != selectedNodeIDs.end()))
724 item->setSelected(true);
725 }
726
727 if (dynamic_cast<VuoRendererComment *>(item))
728 {
729 string currentCommentID = (dynamic_cast<VuoRendererComment *>(item)->getBase()->hasCompiler()?
730 dynamic_cast<VuoRendererComment *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
731 "");
732 if (!currentCommentID.empty() && (selectedCommentIDs.find(currentCommentID) != selectedCommentIDs.end()))
733 item->setSelected(true);
734 }
735
736 else if (dynamic_cast<VuoRendererCable *>(item))
737 {
738 string currentCableID = (dynamic_cast<VuoRendererCable *>(item)->getBase()->hasCompiler()?
739 dynamic_cast<VuoRendererCable *>(item)->getBase()->getCompiler()->getGraphvizDeclaration() :
740 "");
741 if (!currentCableID.empty() && (selectedCableIDs.find(currentCableID) != selectedCableIDs.end()))
742 item->setSelected(true);
743 }
744 }
745
746 // Re-establish mappings between the stored composition components and the running
747 // composition components, if applicable.
748 identifierCache->addCompositionComponentsToCache(getBase());
749
750 // Close popovers for ports no longer present in the composition.
752}
753
759{
760 string portName = port->getBase()->getClass()->getName();
761 VuoRendererNode *parentNode = port->getRenderedParentNode();
762
763 // A changed math expression input to a "Calculate" node will require changes to the node's
764 // upstream input lists of variable names and values.
765 if ((portName == "expression") &&
766 VuoStringUtilities::beginsWith(parentNode->getBase()->getNodeClass()->getClassName(), "vuo.math.calculate"))
767 return true;
768
769 return false;
770}
771
775void VuoEditorComposition::performStructuralChangesAfterValueChangeAtPort(VuoEditorWindow *editorWindow, QUndoStack *undoStack, VuoRendererPort *port, string originalEditingSessionValue, string finalEditingSessionValue)
776{
778 return;
779
780 VuoRendererNode *parentNode = port->getRenderedParentNode();
781
782 // Only current possibility: modifications to "Calculate" node's 'expression' input
783 string nodeClassName = parentNode->getBase()->getNodeClass()->getClassName();
784 vector<string> inputVariablesBeforeEditing = extractInputVariableListFromExpressionsConstant(originalEditingSessionValue, nodeClassName);
785 vector<string> inputVariablesAfterEditing = extractInputVariableListFromExpressionsConstant(finalEditingSessionValue, nodeClassName);
786
787 // Don't make any structural changes if the variables in the input expression remain
788 // the same, even if the expression itself has changed.
789 if (inputVariablesBeforeEditing != inputVariablesAfterEditing)
790 {
791 VuoPort *valuesPort = port->getRenderedParentNode()->getBase()->getInputPortWithName("values");
792
793 set<VuoRendererInputAttachment *> attachments = valuesPort->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
794
795 QList<QGraphicsItem *> attachmentsToRemove;
796
797 VuoRendererReadOnlyDictionary *oldDictionary = nullptr;
798 VuoRendererValueListForReadOnlyDictionary *oldValueList = nullptr;
799 VuoRendererKeyListForReadOnlyDictionary *oldKeyList = nullptr;
800 foreach (VuoRendererInputAttachment *attachment, attachments)
801 {
802 attachmentsToRemove.append(attachment);
803
804 if (dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment))
805 oldDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment);
806
807 else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment))
808 oldValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment);
809
810 else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment))
811 oldKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment);
812 }
813
814 if (oldValueList && oldDictionary && oldKeyList)
815 {
816 set<VuoRendererNode *> nodesToAdd;
817 set<VuoRendererCable *> cablesToAdd;
818 createAndConnectDictionaryAttachmentsForNode(parentNode->getBase(), nodesToAdd, cablesToAdd);
819
820 VuoRendererReadOnlyDictionary *newDictionary = nullptr;
821 VuoRendererValueListForReadOnlyDictionary *newValueList = nullptr;
822 VuoRendererKeyListForReadOnlyDictionary *newKeyList = nullptr;
823 foreach (VuoRendererNode *node, nodesToAdd)
824 {
825 if (dynamic_cast<VuoRendererReadOnlyDictionary *>(node))
826 newDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(node);
827
828 else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node))
829 newValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node);
830
831 else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node))
832 newKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node);
833 }
834
835 undoStack->push(new VuoCommandReplaceNode(oldValueList, newValueList, editorWindow, "Set Port Constant", false, false));
836 undoStack->push(new VuoCommandReplaceNode(oldKeyList, newKeyList, editorWindow, "Set Port Constant", false, true));
837 undoStack->push(new VuoCommandReplaceNode(oldDictionary, newDictionary, editorWindow, "Set Port Constant", false, true));
838
839 foreach (VuoRendererCable *cable, cablesToAdd)
840 {
841 cable->setFrom(nullptr, nullptr);
842 cable->setTo(nullptr, nullptr);
843 }
844 }
845 }
846}
847
852{
854}
855
860{
861 if (commandDescription.empty())
862 {
863 if (getContextMenuDeleteSelectedAction()->text().contains("Reset"))
864 commandDescription = "Reset";
865 else
866 commandDescription = "Delete";
867 }
868
869 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
870 emit componentsRemoved(selectedCompositionComponents, commandDescription);
871}
872
876void VuoEditorComposition::deleteSelectedNodes(string commandDescription)
877{
878 if (commandDescription.empty())
879 commandDescription = "Delete";
880
881 QList<QGraphicsItem *> selectedNodes;
882 foreach (VuoRendererNode *node, getSelectedNodes())
883 selectedNodes.append(node);
884
885 emit componentsRemoved(selectedNodes, commandDescription);
886}
887
892{
893 identifierCache->clearCache();
894
896
897 foreach (VuoCable *cable, getBase()->getCables())
898 removeCable(cable->getRenderer(), false);
899
900 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
902
903 foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedOutputPorts())
904 removePublishedPort(publishedPort, false);
905
906 foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
907 removePublishedPort(publishedPort, true);
908
909 foreach (VuoNode *node, getBase()->getNodes())
910 removeNode(node->getRenderer(), false);
911
913
914 foreach (VuoComment *comment, getBase()->getComments())
915 removeComment(comment->getRenderer());
916
918}
919
923void VuoEditorComposition::insertNode()
924{
925 QAction *sender = (QAction *)QObject::sender();
926 QPair<QPointF, QString> pair = sender->data().value<QPair<QPointF, QString> >();
927
928 QList<QGraphicsItem *> newNodes;
929 VuoRendererNode *newNode = createNode(pair.second, "",
930 pair.first.x(),
931 pair.first.y());
932
933 if (newNode)
934 {
935 newNodes.append(newNode);
936 emit componentsAdded(newNodes, this);
937 }
938}
939
943void VuoEditorComposition::insertComment()
944{
945 QAction *sender = (QAction *)QObject::sender();
946 QPointF scenePos = sender->data().value<QPointF>();
947
948 emit commentInsertionRequested(scenePos);
949}
950
954void VuoEditorComposition::insertSubcomposition()
955{
956 QAction *sender = (QAction *)QObject::sender();
957 QPointF scenePos = sender->data().value<QPointF>();
958
960}
961
967{
968 QAction *sender = (QAction *)QObject::sender();
969 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
970
971 if (isPortPublished(port))
973 else
974 emit portPublicationRequested(port->getBase(), false);
975}
976
983void VuoEditorComposition::deleteConnectedCables()
984{
985 QAction *sender = (QAction *)QObject::sender();
986 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
987 vector<VuoCable *> connectedCables = port->getBase()->getConnectedCables(true);
988 QList<QGraphicsItem *> cablesToRemove;
989
990 // Delete visible directly connected cables.
991 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
992 {
993 if (!cable->getRenderer()->paintingDisabled())
994 cablesToRemove.append(cable->getRenderer());
995 }
996
997 // Delete visible cables connected to the typecast's child port.
998 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
999 if (typecastPort)
1000 {
1001 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1002 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1003 foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
1004 {
1005 if (!cable->getRenderer()->paintingDisabled())
1006 cablesToRemove.append(cable->getRenderer());
1007 }
1008 }
1009
1010 emit componentsRemoved(QList<QGraphicsItem *>(cablesToRemove));
1011}
1012
1019void VuoEditorComposition::hideConnectedCables()
1020{
1021 QAction *sender = (QAction *)QObject::sender();
1022 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1023 set<VuoRendererCable *> cablesToHide;
1024
1025 // Hide visible directly connected cables.
1026 foreach (VuoCable *cable, port->getBase()->getConnectedCables(false))
1027 {
1028 if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
1029 cablesToHide.insert(cable->getRenderer());
1030 }
1031
1032 // Hide visible cables connected to the typecast's child port.
1033 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
1034 if (typecastPort)
1035 {
1036 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1037 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1038 foreach (VuoCable *cable, typecastInPort->getConnectedCables(false))
1039 {
1040 if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
1041 cablesToHide.insert(cable->getRenderer());
1042 }
1043 }
1044
1045 emit cablesHidden(cablesToHide);
1046}
1047
1054void VuoEditorComposition::unhideConnectedCables()
1055{
1056 QAction *sender = (QAction *)QObject::sender();
1057 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1058 set<VuoRendererCable *> cablesToUnhide;
1059
1060 // Unhide visible directly connected cables.
1061 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
1062 {
1063 if (cable->getRenderer()->getEffectivelyWireless())
1064 cablesToUnhide.insert(cable->getRenderer());
1065 }
1066
1067 // Unhide visible cables connected to the typecast's child port.
1068 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
1069 if (typecastPort)
1070 {
1071 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
1072 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1073 foreach (VuoCable *cable, typecastInPort->getConnectedCables(true))
1074 {
1075 if (cable->getRenderer()->getEffectivelyWireless())
1076 cablesToUnhide.insert(cable->getRenderer());
1077 }
1078 }
1079
1080 emit cablesUnhidden(cablesToUnhide);
1081}
1082
1087void VuoEditorComposition::fireTriggerPortEvent()
1088{
1089 QAction *sender = (QAction *)QObject::sender();
1090 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1091 fireTriggerPortEvent(port->getBase());
1092}
1093
1098{
1099 fireTriggerPortEvent(getTriggerPortToRefire());
1100}
1101
1106{
1107 if (triggerPortToRefire.empty())
1108 return nullptr;
1109
1110 VuoPort *triggerPort = nullptr;
1111 identifierCache->doForPortWithIdentifier(triggerPortToRefire, [&triggerPort](VuoPort *port) {
1112 triggerPort = port;
1113 });
1114 return triggerPort;
1115}
1116
1121{
1122 string portID = getIdentifierForStaticPort(port);
1123 if (portID != this->triggerPortToRefire)
1124 {
1125 this->triggerPortToRefire = portID;
1126 emit refirePortChanged();
1127 }
1128}
1129
1134void VuoEditorComposition::setPortConstant()
1135{
1136 QAction *sender = (QAction *)QObject::sender();
1137 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1138
1139 if (port->isConstant())
1140 emit inputEditorRequested(port);
1141}
1142
1147void VuoEditorComposition::setPortConstantToValue(VuoRendererPort *port, string value)
1148{
1149 if (port->isConstant())
1150 emit portConstantChangeRequested(port, value);
1151}
1152
1157void VuoEditorComposition::specializeGenericPortType()
1158{
1159 QAction *sender = (QAction *)QObject::sender();
1160 QList<QVariant> portAndSpecializedType= sender->data().toList();
1161 VuoRendererPort *port = portAndSpecializedType[0].value<VuoRendererPort *>();
1162 QString specializedTypeName = portAndSpecializedType[1].toString();
1163
1164 // If the port is already specialized to the target type, do nothing.
1165 if (port && (port->getDataType()->getModuleKey() == specializedTypeName.toUtf8().constData()))
1166 return;
1167
1168 // If the port is already specialized to a different type, re-specialize it.
1169 if (port && !dynamic_cast<VuoGenericType *>(port->getDataType()))
1170 emit respecializePort(port, specializedTypeName.toUtf8().constData());
1171
1172 // Otherwise, specialize the port from generic.
1173 else
1174 emit specializePort(port, specializedTypeName.toUtf8().constData());
1175}
1176
1181void VuoEditorComposition::unspecializePortType()
1182{
1183 QAction *sender = (QAction *)QObject::sender();
1184 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
1185
1186 // If the port is already generic, do nothing.
1187 if (port && dynamic_cast<VuoGenericType *>(port->getDataType()))
1188 return;
1189
1190 emit unspecializePort(port);
1191}
1192
1203void VuoEditorComposition::createReplacementsToUnspecializePort(VuoPort *portToUnspecialize, bool shouldOutputNodesToReplace, map<VuoNode *, string> &nodesToReplace, set<VuoCable *> &cablesToDelete)
1204{
1205 // Find the ports that will share the same generic type as portToUnspecialize, and organize them by node.
1206 set<VuoPort *> connectedPotentiallyGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(portToUnspecialize->getRenderer()->getUnderlyingParentNode()->getBase(),
1207 portToUnspecialize, true);
1208 map<VuoNode *, set<VuoPort *> > portsToUnspecializeForNode;
1209 for (VuoPort *connectedPort : connectedPotentiallyGenericPorts)
1210 {
1211 VuoNode *node = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase();
1212
1213 // @todo: Don't just exclude ports that aren't currently revertible, also exclude ports that are only
1214 // within the current network by way of ports that aren't currently revertible.
1215 if (isPortCurrentlyRevertible(connectedPort->getRenderer()))
1216 portsToUnspecializeForNode[node].insert(connectedPort);
1217 }
1218
1219 for (map<VuoNode *, set<VuoPort *> >::iterator i = portsToUnspecializeForNode.begin(); i != portsToUnspecializeForNode.end(); ++i)
1220 {
1221 VuoNode *node = i->first;
1222 set<VuoPort *> ports = i->second;
1223
1224 if (shouldOutputNodesToReplace)
1225 {
1226 // Create the unspecialized node class name for each node to unspecialize.
1227 set<VuoPortClass *> portClasses;
1228 for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1229 portClasses.insert((*j)->getClass());
1231 string unspecializedNodeClassName = nodeClass->createUnspecializedNodeClassName(portClasses);
1232 nodesToReplace[node] = unspecializedNodeClassName;
1233 }
1234
1235 // Identify the cables that will become invalid (data-carrying cable with generic port at one end, non-generic port at the other end)
1236 // when the node is unspecialized.
1237 for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1238 {
1239 VuoPort *port = *j;
1240 for (VuoCable *cable : port->getConnectedCables(true))
1241 {
1242 bool areEndsCompatible = false;
1243
1244 if (!cable->getRenderer()->effectivelyCarriesData())
1245 areEndsCompatible = true;
1246
1247 else if (! cable->isPublished())
1248 {
1249 VuoPort *portOnOtherEnd = (cable->getFromPort() == port ? cable->getToPort() : cable->getFromPort());
1250 if (portOnOtherEnd && isPortCurrentlyRevertible(portOnOtherEnd->getRenderer()))
1251 {
1252 VuoNode *nodeOnOtherEnd = portOnOtherEnd->getRenderer()->getUnderlyingParentNode()->getBase();
1253 VuoCompilerNodeClass *nodeClassOnOtherEnd = nodeOnOtherEnd->getNodeClass()->getCompiler();
1254 VuoCompilerSpecializedNodeClass *specializedNodeClassOnOtherEnd = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClassOnOtherEnd);
1255 if (specializedNodeClassOnOtherEnd)
1256 {
1257 VuoType *typeOnOtherEnd = specializedNodeClassOnOtherEnd->getOriginalPortType( portOnOtherEnd->getClass() );
1258 if (! typeOnOtherEnd || dynamic_cast<VuoGenericType *>(typeOnOtherEnd))
1259 areEndsCompatible = true;
1260 }
1261 }
1262 }
1263
1264 if (! areEndsCompatible && (cable != cableInProgress))
1265 cablesToDelete.insert(cable);
1266 }
1267 }
1268 }
1269}
1270
1274void VuoEditorComposition::fireTriggerPortEvent(VuoPort *port)
1275{
1276 if (! (port && port->hasCompiler()) )
1277 return;
1278
1279 VuoPort *oldManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1280 string oldSnapshot = takeSnapshot();
1281
1282 string runningTriggerPortIdentifier = "";
1283 bool isTriggerPort = false;
1284 if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()))
1285 {
1286 // Trigger port — The event will be fired from the port.
1287
1288 getBase()->getCompiler()->setManuallyFirableInputPort(nullptr, nullptr);
1289
1290 runningTriggerPortIdentifier = identifierCache->getIdentifierForPort(port);
1291 isTriggerPort = true;
1292 }
1293 else if (port->hasRenderer() && port->getRenderer()->getInput())
1294 {
1295 // Input port — The event will be fired from the composition's manually firable trigger into the port.
1296
1298
1300 VuoCompilerTriggerPort *triggerPort = graph->getManuallyFirableTrigger();
1301 VuoCompilerNode *triggerNode = graph->getNodeForTriggerPort(triggerPort);
1302 runningTriggerPortIdentifier = getIdentifierForStaticPort(triggerPort->getBase(), triggerNode->getBase());
1303 }
1304 else
1305 return;
1306
1307 VUserLog("%s: Fire %s",
1308 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
1309 runningTriggerPortIdentifier.c_str());
1310
1311 VuoPort *newManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1312 bool manuallyFirableInputPortChanged = (oldManuallyFirableInputPort != newManuallyFirableInputPort);
1313 string newSnapshot = takeSnapshot();
1314
1315 auto fireIfRunning = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
1316 {
1317 dispatch_async(topLevelComposition->runCompositionQueue, ^{
1318 if (topLevelComposition->isRunningThreadUnsafe())
1319 {
1320 topLevelComposition->runner->fireTriggerPortEvent(thisCompositionIdentifier, runningTriggerPortIdentifier);
1321
1322 // Display the trigger port animation when the user manually fires an event
1323 // even if not in "Show Events" mode. (If in "Show Events" mode, this will
1324 // be handled for trigger ports by VuoEditorComposition::receivedTelemetryOutputPortUpdated(...).)
1325 if (! (this->showEventsMode && isTriggerPort) )
1326 this->animatePort(port->getRenderer());
1327 }
1328 });
1329 };
1330
1331 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1332 if (this == topLevelComposition || ! manuallyFirableInputPortChanged)
1333 {
1334 // Top-level composition or unmodified subcomposition — Fire the trigger immediately.
1335
1336 if (! newSnapshot.empty() && manuallyFirableInputPortChanged)
1337 updateRunningComposition(oldSnapshot, newSnapshot);
1338
1339 fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1340 }
1341 else
1342 {
1343 // Modified subcomposition — Fire the trigger after the subcomposition has been reloaded.
1344
1345 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
1346 string nodeClassName = VuoCompiler::getModuleKeyForPath(compositionPath);
1347 moduleManager->doNextTimeNodeClassIsLoaded(nodeClassName, ^{
1348 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1349 fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1350 });
1351 });
1352
1353 if (! newSnapshot.empty())
1354 updateRunningComposition(oldSnapshot, newSnapshot);
1355 });
1356 }
1357 });
1358
1359 setTriggerPortToRefire(port);
1360}
1361
1366void VuoEditorComposition::addInputPort()
1367{
1368 QAction *sender = (QAction *)QObject::sender();
1369 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
1370 emit inputPortCountAdjustmentRequested(node, 1, false);
1371}
1372
1377void VuoEditorComposition::removeInputPort()
1378{
1379 QAction *sender = (QAction *)QObject::sender();
1380 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
1381 emit inputPortCountAdjustmentRequested(node, -1, false);
1382}
1383
1388void VuoEditorComposition::swapNode()
1389{
1390 QAction *sender = (QAction *)QObject::sender();
1391 QList<QVariant> nodeAndReplacementType= sender->data().toList();
1392 VuoRendererNode *node = nodeAndReplacementType[0].value<VuoRendererNode *>();
1393 QString newNodeClassName = nodeAndReplacementType[1].toString();
1394 emit nodeSwapRequested(node, newNodeClassName.toUtf8().constData());
1395}
1396
1401{
1402 // Open a title editor for each selected non-attachment node.
1403 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1404 for (set<VuoRendererNode *>::iterator i = selectedNodes.begin(); i != selectedNodes.end(); ++i)
1405 {
1406 if (!dynamic_cast<VuoRendererInputAttachment *>(*i))
1407 emit nodeTitleEditorRequested(*i);
1408 }
1409}
1410
1414void VuoEditorComposition::editSelectedComments()
1415{
1416 // Open a text editor for each selected comment.
1417 set<VuoRendererComment *> selectedComments = getSelectedComments();
1418 for (set<VuoRendererComment *>::iterator i = selectedComments.begin(); i != selectedComments.end(); ++i)
1419 emit commentEditorRequested(*i);
1420}
1421
1425set<VuoRendererCable *> VuoEditorComposition::getCablesInternalToSubcomposition(QList<QGraphicsItem *> subcompositionComponents)
1426{
1427 set<VuoRendererCable *> internalCables;
1428
1429 for (QList<QGraphicsItem *>::iterator i = subcompositionComponents.begin(); i != subcompositionComponents.end(); ++i)
1430 {
1431 QGraphicsItem *compositionComponent = *i;
1432 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1433 if (rn)
1434 {
1435 set<VuoCable *> connectedCables = rn->getConnectedCables(false);
1436 for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1437 {
1438 VuoNode *fromNode = (*cable)->getFromNode();
1439 VuoNode *toNode = (*cable)->getToNode();
1440
1441 if (fromNode && toNode && subcompositionComponents.contains(fromNode->getRenderer()) && subcompositionComponents.contains(toNode->getRenderer()))
1442 internalCables.insert((*cable)->getRenderer());
1443 }
1444 }
1445 }
1446
1447 return internalCables;
1448}
1449
1454{
1455 return cableInProgress;
1456}
1457
1463{
1464 return cableInProgressWasNew;
1465}
1466
1471{
1472 return menuSelectionInProgress;
1473}
1474
1479{
1480 QList<QGraphicsItem *> compositionComponents = items();
1481 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1482 {
1483 QGraphicsItem *compositionComponent = *i;
1484
1485 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1486 if (!rc || !rc->paintingDisabled())
1487 compositionComponent->setSelected(true);
1488 }
1489}
1490
1495{
1496 QList<QGraphicsItem *> compositionComponents = items();
1497 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1498 {
1499 QGraphicsItem *compositionComponent = *i;
1500 VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
1501 if (rcomment)
1502 rcomment->setSelected(true);
1503 }
1504}
1505
1510{
1511 QList<QGraphicsItem *> compositionComponents = items();
1512 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1513 {
1514 QGraphicsItem *compositionComponent = *i;
1515 compositionComponent->setSelected(false);
1516 }
1517}
1518
1522void VuoEditorComposition::openSelectedEditableNodes()
1523{
1524 foreach (VuoRendererNode *node, getSelectedNodes())
1525 {
1526 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
1527 QString actionText, sourcePath;
1528 if (VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
1529 emit nodeSourceEditorRequested(node);
1530 }
1531}
1532
1537{
1538 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1539 set<VuoRendererComment *> selectedComments = getSelectedComments();
1540 moveItemsBy(selectedNodes, selectedComments, dx, dy, false);
1541}
1542
1546void VuoEditorComposition::moveNodesBy(set<VuoRendererNode *> nodes, qreal dx, qreal dy, bool movedByDragging)
1547{
1548 set<VuoRendererComment *> comments;
1549 moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1550}
1551
1555void VuoEditorComposition::moveCommentsBy(set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1556{
1557 set<VuoRendererNode *> nodes;
1558 moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1559}
1560
1564void VuoEditorComposition::moveItemsBy(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1565{
1566 emit itemsMoved(nodes, comments, dx, dy, movedByDragging);
1568 for (set<VuoRendererNode *>::iterator it = nodes.begin(); it != nodes.end(); ++it)
1569 (*it)->updateConnectedCableGeometry();
1570}
1571
1575void VuoEditorComposition::resizeCommentBy(VuoRendererComment *comment, qreal dx, qreal dy)
1576{
1577 emit commentResized(comment, dx, dy);
1578}
1579
1584{
1585 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1586 set<VuoRendererNode *> selectedNodes;
1587 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1588 {
1589 QGraphicsItem *compositionComponent = *i;
1590 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1591 if (rn)
1592 selectedNodes.insert(rn);
1593 }
1594
1595 return selectedNodes;
1596}
1597
1602{
1603 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1604 set<VuoRendererComment *> selectedComments;
1605 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1606 {
1607 QGraphicsItem *compositionComponent = *i;
1608 VuoRendererComment *rc = dynamic_cast<VuoRendererComment *>(compositionComponent);
1609 if (rc)
1610 selectedComments.insert(rc);
1611 }
1612
1613 return selectedComments;
1614}
1615
1619set<VuoRendererCable *> VuoEditorComposition::getSelectedCables(bool includePublishedCables)
1620{
1621 QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1622 set<VuoRendererCable *> selectedCables;
1623 for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1624 {
1625 QGraphicsItem *compositionComponent = *i;
1626 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1627 if (rc && (includePublishedCables || !rc->getBase()->isPublished()))
1628 selectedCables.insert(rc);
1629 }
1630
1631 return selectedCables;
1632}
1633
1637void VuoEditorComposition::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1638{
1639 const QMimeData *mimeData = event->mimeData();
1640 bool disablePortHoverHighlighting = true;
1641
1642 // Accept drags of files.
1643 if (mimeData->hasFormat("text/uri-list"))
1644 {
1645 QList<QUrl> urls = mimeData->urls();
1646
1647 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1648 VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1649 if (portAtDropLocation)
1650 {
1651 if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1652 (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1653 disablePortHoverHighlighting = false;
1654 else
1655 event->setDropAction(Qt::IgnoreAction);
1656 }
1657 else // if (!portAtDropLocation)
1658 {
1659 bool dragIncludesDroppableFile = false;
1660 foreach (QUrl url, urls)
1661 {
1662 char *urlZ = strdup(url.path().toUtf8().constData());
1663 bool isSupportedDragNDropFile =
1664 VuoFileType_isFileOfType(urlZ, VuoFileType_Image)
1665 || VuoFileType_isFileOfType(urlZ, VuoFileType_Movie)
1666 || VuoFileType_isFileOfType(urlZ, VuoFileType_Scene)
1667 || VuoFileType_isFileOfType(urlZ, VuoFileType_Audio)
1668 || VuoFileType_isFileOfType(urlZ, VuoFileType_Feed)
1669 || VuoFileType_isFileOfType(urlZ, VuoFileType_JSON)
1670 || VuoFileType_isFileOfType(urlZ, VuoFileType_XML)
1671 || VuoFileType_isFileOfType(urlZ, VuoFileType_Table)
1672 || VuoFileType_isFileOfType(urlZ, VuoFileType_Mesh)
1673 || VuoFileType_isFileOfType(urlZ, VuoFileType_Data)
1674 || VuoFileType_isFileOfType(urlZ, VuoFileType_App)
1675 || isDirectory(urlZ);
1676 free(urlZ);
1677 if (isSupportedDragNDropFile)
1678 {
1679 dragIncludesDroppableFile = true;
1680 break;
1681 }
1682 }
1683
1684 if (!dragIncludesDroppableFile)
1685 event->setDropAction(Qt::IgnoreAction);
1686 }
1687
1688 event->accept();
1689 }
1690
1691 // Accept drags of single or multiple nodes from the node library.
1692 else if (mimeData->hasFormat("text/plain") || mimeData->hasFormat("text/scsv"))
1693 event->acceptProposedAction();
1694
1695 else
1696 {
1697 event->setDropAction(Qt::IgnoreAction);
1698 event->accept();
1699 }
1700
1701 updateHoverHighlighting(event->scenePos(), disablePortHoverHighlighting);
1702}
1703
1707void VuoEditorComposition::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
1708{
1709 event->acceptProposedAction();
1710}
1711
1715void VuoEditorComposition::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
1716{
1717 dragEnterEvent(event);
1718}
1719
1723void VuoEditorComposition::dropEvent(QGraphicsSceneDragDropEvent *event)
1724{
1725 const QMimeData *mimeData = event->mimeData();
1726
1727 // Accept drops of certain types of files.
1728 if (mimeData->hasFormat("text/uri-list"))
1729 {
1730 // Retrieve the composition directory so that file paths may be specified relative to it.
1731 // Note: Providing the directory's canonical path as the argument to the
1732 // QDir constructor is necessary in order for QDir::relativeFilePath() to
1733 // work correctly when the non-canonical path contains symbolic links (e.g.,
1734 // '/tmp' -> '/private/tmp' for example compositions).
1735 string topCompositionPath = compiler->getCompositionLocalPath();
1736 if (topCompositionPath.empty())
1737 topCompositionPath = getBase()->getDirectory();
1738 QDir compositionDir(QDir(topCompositionPath.c_str()).canonicalPath());
1739
1740 // Use the absolute file path if the "Option" key was pressed.
1741 bool useAbsoluteFilePaths = VuoEditorUtilities::optionKeyPressedForEvent(event);
1742
1743 QList<QGraphicsItem *> newNodes;
1744 QList<QUrl> urls = mimeData->urls();
1745
1746 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1747 VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1748 if (portAtDropLocation)
1749 {
1750 if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1751 (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1752 {
1753 QString filePath = (useAbsoluteFilePaths? urls[0].path() : compositionDir.relativeFilePath(urls[0].path()));
1754 string constantValue = "\"" + string(filePath.toUtf8().constData()) + "\"";
1755 event->accept();
1756
1757 emit portConstantChangeRequested(portAtDropLocation, constantValue);
1758 }
1759 else
1760 event->ignore();
1761 }
1762
1763 else // if (!portAtDropLocation)
1764 {
1765 const int ySpacingForNewNodes = 11;
1766 int yOffsetForPreviousNewNode = -1 * ySpacingForNewNodes;
1767
1768 foreach (QUrl url, urls)
1769 {
1770 QStringList targetNodeClassNames;
1771 char *urlZ = strdup(url.path().toUtf8().constData());
1772
1773 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Image))
1774 targetNodeClassNames += "vuo.image.fetch";
1775 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Movie))
1776 {
1777 if (!getActiveProtocol())
1778 targetNodeClassNames += "vuo.video.play";
1779
1780 targetNodeClassNames += "vuo.video.decodeImage";
1781 }
1782 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Scene))
1783 targetNodeClassNames += "vuo.scene.fetch";
1784 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Audio))
1785 targetNodeClassNames += "vuo.audio.file.play";
1786 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Mesh))
1787 targetNodeClassNames += "vuo.image.project.dome";
1788 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Feed))
1789 targetNodeClassNames += "vuo.rss.fetch";
1790 if (VuoFileType_isFileOfType(urlZ, VuoFileType_JSON))
1791 targetNodeClassNames += "vuo.tree.fetch.json";
1792 if (VuoFileType_isFileOfType(urlZ, VuoFileType_XML))
1793 targetNodeClassNames += "vuo.tree.fetch.xml";
1794 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Table))
1795 targetNodeClassNames += "vuo.table.fetch";
1796 if (VuoFileType_isFileOfType(urlZ, VuoFileType_Data))
1797 targetNodeClassNames += "vuo.data.fetch";
1798 if (VuoFileType_isFileOfType(urlZ, VuoFileType_App))
1799 targetNodeClassNames += "vuo.app.launch";
1800 if (isDirectory(urlZ))
1801 targetNodeClassNames += "vuo.file.list";
1802
1803 free(urlZ);
1804
1805 QString selectedNodeClassName = "";
1806 if (targetNodeClassNames.size() == 1)
1807 selectedNodeClassName = targetNodeClassNames[0];
1808 else if (targetNodeClassNames.size() > 1)
1809 {
1810 QMenu nodeMenu(views()[0]->viewport());
1811 nodeMenu.setSeparatorsCollapsible(false);
1812
1813 foreach (QString nodeClassName, targetNodeClassNames)
1814 {
1815 VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
1816 string nodeTitle = (nodeClass? nodeClass->getBase()->getDefaultTitle() : nodeClassName.toUtf8().constData());
1817 //: Appears in a popup menu when dragging files from Finder onto the composition canvas.
1818 QAction *nodeAction = nodeMenu.addAction(tr("Insert \"%1\" Node").arg(nodeTitle.c_str()));
1819 nodeAction->setData(nodeClassName);
1820 }
1821
1822 menuSelectionInProgress = true;
1823 QAction *selectedNode = nodeMenu.exec(QCursor::pos());
1824 menuSelectionInProgress = false;
1825
1826 selectedNodeClassName = (selectedNode? selectedNode->data().toString().toUtf8().constData() : "");
1827 }
1828
1829 if (!selectedNodeClassName.isEmpty())
1830 {
1831 VuoRendererNode *newNode = createNode(selectedNodeClassName, "",
1832 event->scenePos().x(),
1833 event->scenePos().y() + yOffsetForPreviousNewNode + ySpacingForNewNodes);
1834
1835 if (newNode)
1836 {
1837 VuoPort *urlPort = newNode->getBase()->getInputPortWithName(selectedNodeClassName == "vuo.file.list"?
1838 "folder" :
1839 "url");
1840 if (urlPort)
1841 {
1842 QString filePath = (useAbsoluteFilePaths? url.path() : compositionDir.relativeFilePath(url.path()));
1843 urlPort->getRenderer()->setConstant(VuoText_getString(filePath.toUtf8().constData()));
1844
1845 newNodes.append(newNode);
1846
1847 yOffsetForPreviousNewNode += newNode->boundingRect().height();
1848 yOffsetForPreviousNewNode += ySpacingForNewNodes;
1849 }
1850 }
1851 }
1852 }
1853
1854 if (newNodes.size() > 0)
1855 {
1856 event->accept();
1857
1858 emit componentsAdded(newNodes, this);
1859 }
1860 else
1861 event->ignore();
1862 }
1863 }
1864
1865 // Accept drops of one or more nodes from the node library class list.
1866 else if (mimeData->hasFormat("text/scsv"))
1867 {
1868 event->setDropAction(Qt::CopyAction);
1869 event->accept();
1870
1871 QByteArray scsvData = event->mimeData()->data("text/scsv");
1872 QString scsvText = QString::fromUtf8(scsvData);
1873 QStringList nodeClassNames = scsvText.split(';');
1874
1875 QPointF startPos = event->scenePos()-QPointF(0,VuoRendererNode::nodeHeaderYOffset);
1878 int snapDelta = nextYPos - startPos.y();
1879
1880 QList<QGraphicsItem *> newNodes;
1881 for (QStringList::iterator i = nodeClassNames.begin(); i != nodeClassNames.end(); ++i)
1882 {
1883 VuoRendererNode *newNode = createNode(*i, "",
1884 startPos.x(),
1885 nextYPos - (VuoRendererItem::getSnapToGrid()? 0 : snapDelta));
1886
1887 if (newNode)
1888 {
1889 int prevYPos = nextYPos;
1890 nextYPos = VuoRendererComposition::quantizeToNearestGridLine(QPointF(0,prevYPos+newNode->boundingRect().height()),
1892 if (nextYPos <= prevYPos+newNode->boundingRect().height())
1894
1895 newNodes.append((QGraphicsItem *)newNode);
1896 }
1897 }
1898
1899 emit componentsAdded(newNodes, this);
1900 }
1901
1902 // Accept drops of single nodes from the node library documentation panel.
1903 else if (mimeData->hasFormat("text/plain"))
1904 {
1905 event->setDropAction(Qt::CopyAction);
1906 event->accept();
1907
1908 QList<QGraphicsItem *> newNodes;
1909 QStringList dropItems = event->mimeData()->text().split('\n');
1910 QString nodeClassName = dropItems[0];
1911
1912 // Account for the offset between cursor and dragged item pixmaps
1913 // to drop node in-place.
1914 QPoint hotSpot = (dropItems.size() >= 3? QPoint(dropItems[1].toInt(), dropItems[2].toInt()) : QPoint(0,0));
1915 VuoRendererNode *newNode = createNode(nodeClassName, "",
1916 event->scenePos().x()-hotSpot.x()+1,
1917 event->scenePos().y()-VuoRendererNode::nodeHeaderYOffset-hotSpot.y()+1);
1918
1919 newNodes.append(newNode);
1920 emit componentsAdded(newNodes, this);
1921 }
1922 else
1923 {
1924 event->ignore();
1925 }
1926}
1927
1931void VuoEditorComposition::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
1932{
1933 QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1934 if (dynamic_cast<VuoRendererPort *>(nearbyItem))
1935 {
1936 QGraphicsScene::sendEvent(nearbyItem, event);
1937 event->accept();
1938 }
1939
1940 else
1941 QGraphicsScene::mouseDoubleClickEvent(event);
1942}
1943
1947void VuoEditorComposition::mousePressEvent(QGraphicsSceneMouseEvent *event)
1948{
1949 QPointF scenePos = event->scenePos();
1950
1951 // Handle left-button presses.
1952 if (event->button() == Qt::LeftButton)
1953 {
1954 // Correct for erroneous tracking behavior that occurs following the first left-click
1955 // directly on a node, cable, or comment after the cancellation of a component duplication operation.
1956 // See https://b33p.net/kosada/node/3339
1957 if (duplicationCancelled)
1958 {
1959 QGraphicsItem *itemClickedDirectly = itemAt(scenePos, views()[0]->transform());
1960 if (dynamic_cast<VuoRendererNode *>(itemClickedDirectly) ||
1961 dynamic_cast<VuoRendererCable *>(itemClickedDirectly) ||
1962 dynamic_cast<VuoRendererComment *>(itemClickedDirectly)
1963 )
1964 {
1965 duplicationCancelled = false;
1966 correctForCancelledDuplication(event);
1967 }
1968 }
1969
1970 // Determine whether the cursor is in range of any operable composition components.
1971 QGraphicsItem *nearbyItem = findNearbyComponent(scenePos);
1972 leftMousePressEventAtNearbyItem(nearbyItem, event);
1973 }
1974
1975 // Handle non-left-button presses.
1976 else // if (event->button() != Qt::LeftButton)
1977 mousePressEventNonLeftButton(event);
1978}
1979
1984void VuoEditorComposition::leftMousePressEventAtNearbyItem(QGraphicsItem *nearbyItem, QGraphicsSceneMouseEvent *event)
1985{
1986 bool eventHandled = false;
1987
1988 // If click did not occur within range of a port, check whether
1989 // the click occured on a cable within its yank zone.
1990 VuoRendererCable *cableYankedDirectly = NULL;
1991 VuoRendererPort *currentPort = dynamic_cast<VuoRendererPort *>(nearbyItem);
1992 if (! currentPort)
1993 {
1994 VuoRendererCable *currentCable = dynamic_cast<VuoRendererCable *>(nearbyItem);
1995 if (currentCable &&
1996 currentCable->yankZoneIncludes(event->scenePos()) &&
1997 currentCable->getBase()->getToPort())
1998 {
1999 currentPort = currentCable->getBase()->getToPort()->getRenderer();
2000 cableYankedDirectly = currentCable;
2001 }
2002 }
2003
2004 if (currentPort)
2005 {
2006 // Case: Firing an event by Command+click
2007 bool isTriggerPort = (currentPort->getBase()->hasCompiler() && dynamic_cast<VuoCompilerTriggerPort *>(currentPort->getBase()->getCompiler()));
2008 if ((event->modifiers() & Qt::ControlModifier) && (currentPort->getInput() || isTriggerPort))
2009 fireTriggerPortEvent(currentPort->getBase());
2010
2011 else
2012 {
2013 portWithDragInitiated = currentPort;
2014 cableWithYankInitiated = cableYankedDirectly;
2015 event->accept();
2016 }
2017
2018 eventHandled = true;
2019 }
2020
2021 // Case: Initiating duplication of selected components with Option/Alt+drag
2022 else if (nearbyItem && VuoEditorUtilities::optionKeyPressedForEvent(event) && !duplicationDragInProgress)
2023 {
2024 // Duplicate the selected components.
2025 duplicateOnNextMouseMove = true;
2026 duplicationDragInProgress = true;
2027 cursorPosBeforeDuplicationDragMove = (VuoRendererItem::getSnapToGrid()?
2030 event->scenePos());
2031
2032 QGraphicsScene::mousePressEvent(event);
2033 eventHandled = true;
2034 }
2035
2036 // Case: Left mouse-click made near enough to a cable to trigger cable selection
2037 if (dynamic_cast<VuoRendererCable *>(nearbyItem))
2038 {
2039 if (event->modifiers() & Qt::ControlModifier)
2040 nearbyItem->setSelected(! nearbyItem->isSelected());
2041
2042 else
2043 {
2045 nearbyItem->setSelected(true);
2046 }
2047
2048 event->accept();
2049 eventHandled = true;
2050 }
2051
2052 // Case: Left mouse-click made for some other reason, not handled here
2053 if (!eventHandled)
2054 QGraphicsScene::mousePressEvent(event);
2055}
2056
2061void VuoEditorComposition::mousePressEventNonLeftButton(QGraphicsSceneMouseEvent *event)
2062{
2064
2065 // If a right-click occurred, generate the context menu event ourselves
2066 // rather than leaving it to QGraphicsScene. This prevents existing selected
2067 // components from being erroneously de-selected before
2068 // VuoEditorComposition::contextMenuEvent(...) is called if the right-click
2069 // occurred within range of, but not directly upon, a composition component.
2070 if (event->button() == Qt::RightButton)
2071 {
2072 QGraphicsSceneContextMenuEvent contextMenuEvent(QEvent::GraphicsSceneContextMenu);
2073 contextMenuEvent.setScreenPos(event->screenPos());
2074 contextMenuEvent.setScenePos(event->scenePos());
2075 contextMenuEvent.setReason(QGraphicsSceneContextMenuEvent::Mouse);
2076 QApplication::sendEvent(this, &contextMenuEvent);
2077 event->accept();
2078 }
2079
2080 else
2081 QGraphicsScene::mousePressEvent(event);
2082
2083 return;
2084}
2085
2092void VuoEditorComposition::correctForCancelledDuplication(QGraphicsSceneMouseEvent *event)
2093{
2094 // Simulate an extra mouse click for now to force the cursor
2095 // to track correctly with the next set of dragged components.
2096 QGraphicsSceneMouseEvent pressEvent(QEvent::GraphicsSceneMousePress);
2097 pressEvent.setScenePos(event->scenePos());
2098 pressEvent.setButton(Qt::LeftButton);
2099 QApplication::sendEvent(this, &pressEvent);
2100
2101 QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
2102 releaseEvent.setScenePos(event->scenePos());
2103 releaseEvent.setButton(Qt::LeftButton);
2104 QApplication::sendEvent(this, &releaseEvent);
2105}
2106
2114void VuoEditorComposition::initiateCableDrag(VuoRendererPort *currentPort, VuoRendererCable *cableYankedDirectly, QGraphicsSceneMouseEvent *event)
2115{
2116 Qt::KeyboardModifiers modifiers = event->modifiers();
2117 bool optionKeyPressed = (modifiers & Qt::AltModifier);
2118 bool shiftKeyPressed = (modifiers & Qt::ShiftModifier);
2119
2120 // For now, a left mouse press on an input port with a constant value, attached typecast, or attached "Make List" node does nothing.
2121 // Eventually, a mouse drag will detach the constant value, typecast, or "Make List" node.
2122 if (! (cableYankedDirectly || currentPort->getOutput() || currentPort->supportsDisconnectionByDragging()))
2123 {
2124 return;
2125 }
2126
2127 // Determine based on the keypress modifiers and the attributes of the port whether to
2128 // create a new cable, disconnect an existing cable, or duplicate an existing cable.
2129 bool creatingNewCable = false;
2130 bool disconnectingExistingCable = false;
2131 bool duplicatingExistingCable = false;
2132
2133 VuoPort *fixedPort = currentPort->getBase();
2134 VuoNode *fixedNode = getUnderlyingParentNodeForPort(fixedPort, this);
2135
2136 VuoNode *fromNode = NULL;
2137 VuoPort *fromPort = NULL;
2138 VuoNode *toNode = NULL;
2139 VuoPort *toPort = NULL;
2140
2141 // Case: Dragging from an output port
2142 if (currentPort->getOutput())
2143 {
2144 // Prepare for the "forward" creation of a new cable.
2145 fromPort = fixedPort;
2146 fromNode = fixedNode;
2147 creatingNewCable = true;
2148 }
2149
2150 // Case: Dragging from an input port
2151 else if (! currentPort->getFunctionPort())
2152 {
2153 // If the input port has no connected cables to disconnect, prepare for the
2154 // "backward" creation of a new cable.
2155 if (currentPort->getBase()->getConnectedCables(true).empty())
2156 {
2157 toPort = fixedPort;
2158 toNode = fixedNode;
2159 creatingNewCable = true;
2160 }
2161
2162 // If the input port does have connected cables, prepare for the
2163 // disconnection or duplication of one of these cables.
2164 else
2165 {
2166 if (optionKeyPressed)
2167 {
2168 duplicatingExistingCable = true;
2169 }
2170
2171 else
2172 disconnectingExistingCable = true;
2173 }
2174 }
2175
2176 // @todo: Case: Dragging from a function port
2177
2178
2179 // Perform the actual cable creation, if applicable.
2180 if (creatingNewCable)
2181 {
2182 // Create the cable first and set its endpoints later in case either endpoint is published,
2183 // since published nodes don't have compilers.
2184 cableInProgress = (new VuoCompilerCable(NULL,
2185 NULL,
2186 NULL,
2187 NULL))->getBase();
2188
2189 // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2190 // regardless of its connected ports.
2191 cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2192
2193 cableInProgressWasNew = true;
2194 cableInProgressShouldBeWireless = false;
2195
2196 addCable(cableInProgress);
2197 cableInProgress->getRenderer()->setFrom(fromNode, fromPort);
2198 cableInProgress->getRenderer()->setTo(toNode, toPort);
2199 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2200 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2201 highlightEligibleEndpointsForCable(cableInProgress);
2202 fixedPort->getRenderer()->updateGeometry();
2203 }
2204
2205 // Perform the actual cable disconnection, if applicable.
2206 else if (disconnectingExistingCable)
2207 {
2208 // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2209 if (cableYankedDirectly)
2210 cableInProgress = cableYankedDirectly->getBase();
2211
2212 // Otherwise, disconnect the cable that was connected to the port most recently.
2213 else
2214 cableInProgress = currentPort->getBase()->getConnectedCables(true).back();
2215
2216 cableInProgressWasNew = false;
2217 cableInProgressShouldBeWireless = cableInProgress->hasCompiler() && cableInProgress->getCompiler()->getHidden();
2218
2219 currentPort->updateGeometry();
2220 cableInProgress->getRenderer()->updateGeometry();
2221 cableInProgress->getRenderer()->setHovered(false);
2222 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2223 cableInProgress->getRenderer()->setFloatingEndpointPreviousToPort(cableInProgress->getToPort());
2224 cableInProgress->getRenderer()->setPreviouslyAlwaysEventOnly(cableInProgress->getCompiler()->getAlwaysEventOnly());
2225 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2226 cableInProgress->getRenderer()->setTo(NULL, NULL);
2227 highlightEligibleEndpointsForCable(cableInProgress);
2229 cableInProgress->getRenderer()->setSelected(true);
2230 }
2231 // Perform the actual cable duplication, if applicable.
2232 else if (duplicatingExistingCable)
2233 {
2234 VuoCable *cableToDuplicate = NULL;
2235 // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2236 if (cableYankedDirectly)
2237 cableToDuplicate = cableYankedDirectly->getBase();
2238
2239 // Otherwise, disconnect the cable that was connected to the port most recently.
2240 else
2241 cableToDuplicate = currentPort->getBase()->getConnectedCables(true).back();
2242
2243 // Create the cable first and set its endpoints later in case either endpoint is published,
2244 // since published nodes don't have compilers.
2245 cableInProgress = (new VuoCompilerCable(NULL,
2246 NULL,
2247 NULL,
2248 NULL))->getBase();
2249
2250 // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2251 // regardless of its connected ports.
2252 cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2253
2254 cableInProgressWasNew = true;
2255 cableInProgressShouldBeWireless = cableToDuplicate->hasCompiler() &&
2256 cableToDuplicate->getCompiler()->getHidden();
2257 addCable(cableInProgress);
2258 cableInProgress->getRenderer()->setFrom(getUnderlyingParentNodeForPort(cableToDuplicate->getFromPort(), this),
2259 cableToDuplicate->getFromPort());
2260 cableInProgress->getRenderer()->setTo(NULL, NULL);
2261 cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2262 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2263 highlightEligibleEndpointsForCable(cableInProgress);
2265 cableInProgress->getRenderer()->setSelected(true);
2266 fixedPort->getRenderer()->updateGeometry();
2267 }
2268
2269 // The cable will need to be re-painted as its endpoint is dragged.
2270 cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2271
2272 emit cableDragInitiated();
2273
2274 event->accept();
2275}
2276
2280void VuoEditorComposition::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2281{
2282 // Handle left-button releases.
2283 if (event->button() == Qt::LeftButton)
2284 {
2285 portWithDragInitiated = NULL;
2286 cableWithYankInitiated = NULL;
2287 duplicateOnNextMouseMove = false;
2288 duplicationDragInProgress = false;
2289 dragStickinessDisabled = false;
2292
2293 // If there was a cable drag in progress at the time of the left-mouse-button
2294 // release, conclude the drag -- either by connecting the cable to the eligible port
2295 // at which it was dropped, or, if not dropped at an eligible port, by deleting the cable.
2296 bool cableDragEnding = cableInProgress;
2297 if (cableDragEnding)
2298 concludeCableDrag(event);
2299
2300 QGraphicsItem *item = findNearbyComponent(event->scenePos());
2301 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2302 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
2303 if (port)
2304 {
2305 // Display the port popover, as long as the mouse release did not have any keyboard modifiers
2306 // and did not mark the end of a drag.
2307 // Do so even if a cable drag was technically in progress, because all it takes to initiate
2308 // a cable drag is a mouse press. We could trigger cable drags upon mouse move events instead of
2309 // mouse press events to avoid this.
2310 if ((event->modifiers() == Qt::NoModifier) &&
2311 (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2312 {
2313 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
2314 if (typecastPort)
2315 {
2316 // Since the rendering of the typecast body includes its host port, display popovers for both.
2317 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
2318 enablePopoverForNode(typecastNode);
2319 enableInactivePopoverForPort(typecastPort->getReplacedPort());
2320 }
2321
2322 else
2323 enableInactivePopoverForPort(port);
2324 }
2325 }
2326
2327 else // if (!port)
2328 {
2329 if (!cableDragEnding && !node)
2331 }
2332
2333 if (!cableDragEnding)
2334 QGraphicsScene::mouseReleaseEvent(event);
2335 }
2336
2337 // Handle non-left-button releases.
2338 else // if (event->button() != Qt::LeftButton)
2339 {
2340 QGraphicsScene::mouseReleaseEvent(event);
2341 }
2342}
2343
2350void VuoEditorComposition::concludeCableDrag(QGraphicsSceneMouseEvent *event)
2351{
2352 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2353
2354 // Conclude drags of published cables as a special case.
2355 // @todo: Integrate this more naturally.
2356 if (cableInProgress->isPublished())
2357 {
2358 concludePublishedCableDrag(event);
2359 return;
2360 }
2361
2362 if (hasFeedbackErrors())
2363 {
2366 return;
2367 }
2368
2369 cableInProgress->getRenderer()->setWireless(cableInProgressShouldBeWireless);
2370
2371 // Input or output port that the cable is being dragged from (i.e., the fixed endpoint).
2372 VuoRendererPort *fixedPort = NULL;
2373 if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2374 fixedPort = cableInProgress->getFromPort()->getRenderer();
2375 else if (cableInProgress->getToNode() && cableInProgress->getToPort())
2376 fixedPort = cableInProgress->getToPort()->getRenderer();
2377
2378 // We will determine based on the presence or absence of an eligible port near the
2379 // location of the mouse release whether to connect or delete the dragged cable.
2380 bool completedCableConnection = false;
2381
2382 // Potential side effects of a newly completed cable connection:
2383 VuoCable *dataCableToDisplace = NULL; // A previously existing incoming data+event cable may need to be displaced.
2384 VuoCable *cableToReplace = NULL; // A previously existing cable connecting the same two ports may need to be replaced.
2385 VuoRendererNode *typecastNodeToDelete = NULL; // A previously attached typecast may need to be deleted.
2386 VuoRendererPort *portToUnpublish = NULL; // A previously published port may need to be unpublished.
2387 string typecastToInsert = ""; // A typecast may need to be automatically inserted.
2388
2389 // A generic port involved in the new connection may need to be specialized.
2390 VuoRendererPort *portToSpecialize = NULL;
2391 string specializedTypeName = "";
2392
2393 // Input or output port that the cable is being dropped onto, if any.
2394 VuoRendererPort *targetPort = (VuoRendererPort *)findNearbyPort(event->scenePos(), false);
2395
2396 // Node header area that the cable is being dropped onto, if any.
2397 // (If over both a port drop zone and a node header, the node header gets precedence.)
2398 {
2399 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2400 if (targetNode)
2401 {
2402 targetPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2403 if (targetPort)
2404 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2405 }
2406 }
2407
2408 bool draggingPreviouslyPublishedCable = (!cableInProgressWasNew &&
2409 (dynamic_cast<VuoRendererPublishedPort *>(fixedPort) ||
2410 ((cableInProgress->getToPort() == NULL) &&
2411 dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort()->getRenderer()))));
2412
2413 if (fixedPort && targetPort)
2414 {
2415 // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2416 // don't push the operation onto the Undo stack.
2417 bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2418 targetPort->getBase()) &&
2419 (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2420 cableInProgress->getCompiler()->getAlwaysEventOnly()));
2421 if (recreatingSameConnection)
2422 {
2424 return;
2425 }
2426
2427 bool cableInProgressExpectedToCarryData = cableInProgress->getRenderer()->effectivelyCarriesData() &&
2428 targetPort->getDataType();
2429
2430 VuoCable *preexistingCable = fixedPort->getCableConnectedTo(targetPort, false);
2431 bool preexistingCableWithMatchingDataCarryingStatus = (preexistingCable?
2432 (preexistingCable->getRenderer()->effectivelyCarriesData() ==
2433 cableInProgressExpectedToCarryData) :
2434 false);
2435
2436 // Case: Replacing a pre-existing cable that connected the same two ports
2437 // but with a different data-carrying status, and whose "To" port is
2438 // the child port of a collapsed typecast.
2439 if (preexistingCable && !preexistingCableWithMatchingDataCarryingStatus &&
2440 preexistingCable->getToPort()->hasRenderer() &&
2441 preexistingCable->getToPort()->getRenderer()->getTypecastParentPort())
2442 {
2443 // @todo Implement for https://b33p.net/kosada/node/14153
2444 // For now, don't attempt it.
2446 return;
2447 }
2448
2449 // Case: Completing a "forward" cable connection from an output port to an input port
2450 if (!preexistingCableWithMatchingDataCarryingStatus &&
2451 (fixedPort->canConnectDirectlyWithoutSpecializationTo(targetPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2452 selectBridgingSolution(fixedPort, targetPort, true, &portToSpecialize, specializedTypeName, typecastToInsert)))
2453 {
2454 // If input port had a connected collapsed typecast, uncollapse it.
2455 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2456 if (typecastPort)
2457 {
2458 VuoRendererPort *adjustedTargetPort = typecastPort->getReplacedPort();
2459 VuoRendererPort *childPort = typecastPort->getChildPort();
2460 VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2461 VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2462
2463 if (portToSpecialize == targetPort)
2464 portToSpecialize = adjustedTargetPort;
2465
2466 targetPort = adjustedTargetPort;
2467
2468 // If the typecast did not have multiple incoming cables, and the new cable will
2469 // be replacing it as a data source, delete the typecast.
2470 if (cableInProgressExpectedToCarryData &&
2471 childPort->getBase()->getConnectedCables(true).size() < 2 &&
2472 (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2473 typecastNodeToDelete = uncollapsedTypecast;
2474 }
2475
2476 // If connecting two ports that already had a cable (of different data-carrying status) connecting them,
2477 // replace the pre-existing cable.
2478 if (preexistingCable)
2479 cableToReplace = preexistingCable;
2480
2481 // If the cable carries data, determine what other sources of input data need to be
2482 // removed before completing this connection.
2483 if (cableInProgressExpectedToCarryData)
2484 {
2485 // If input port already had a connected data cable, delete that cable.
2486 vector<VuoCable *> previousConnectedCables = targetPort->getBase()->getConnectedCables(false);
2487 for (vector<VuoCable *>::iterator cable = previousConnectedCables.begin(); (! dataCableToDisplace) && (cable != previousConnectedCables.end()); ++cable)
2488 if ((((*cable)->getRenderer()->effectivelyCarriesData()) && (*cable) != cableToReplace))
2489 dataCableToDisplace = *cable;
2490
2491 // If the input port was published as a data+event port, unpublish it.
2492 // @todo: Let published cable disconnection handle port unpublication.
2493 if (isPortPublished(targetPort))
2494 {
2495 vector<VuoRendererPublishedPort *> publishedDataConnections = targetPort->getPublishedPortsConnectedByDataCarryingCables();
2496 if (publishedDataConnections.size() > 0)
2497 portToUnpublish = targetPort;
2498 }
2499 }
2500
2501 completedCableConnection = true;
2502 }
2503
2504 // Case: Completing a "backward" cable connection from an input port to an output port
2505 else if (!preexistingCableWithMatchingDataCarryingStatus &&
2506 (targetPort->canConnectDirectlyWithoutSpecializationTo(fixedPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2507 selectBridgingSolution(targetPort, fixedPort, false, &portToSpecialize, specializedTypeName, typecastToInsert)))
2508
2509 {
2510 completedCableConnection = true;
2511 }
2512 }
2513
2514 // Complete the actual cable connection, if applicable.
2515 if (completedCableConnection)
2516 {
2517 emit undoStackMacroBeginRequested("Cable Connection");
2518
2519 if (draggingPreviouslyPublishedCable)
2520 {
2521 // Record some information about the cable in progress, to create a replica.
2522 VuoNode *cableInProgressFromNode = cableInProgress->getFromNode();
2523 VuoNode *cableInProgressToNode = cableInProgress->getToNode();
2524 VuoPort *cableInProgressFromPort = cableInProgress->getFromPort();
2525 VuoPort *cableInProgressToPort = cableInProgress->getToPort();
2526 QPointF cableInProgressFloatingEndpointLoc = cableInProgress->getRenderer()->getFloatingEndpointLoc();
2527 bool cableInProgressAlwaysEventOnly = cableInProgress->getCompiler()->getAlwaysEventOnly();
2528
2529 // Remove the original cable, unpublishing the port in the process.
2531
2532 // Restore a copy of the cable to participate in the new connection.
2533 VuoCable *cableInProgressCopy = (new VuoCompilerCable(NULL,
2534 NULL,
2535 NULL,
2536 NULL))->getBase();
2537
2538 cableInProgressCopy->setFrom(cableInProgressFromNode, cableInProgressFromPort);
2539 cableInProgressCopy->setTo(cableInProgressToNode, cableInProgressToPort);
2540
2541 addCable(cableInProgressCopy);
2542
2543 cableInProgressCopy->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
2544 cableInProgressCopy->getRenderer()->setFloatingEndpointLoc(cableInProgressFloatingEndpointLoc);
2545
2546 cableInProgress = cableInProgressCopy;
2547 cableInProgressWasNew = true;
2548 }
2549
2550 // Workaround to avoid Qt's parameter count limit for signals/slots.
2551 pair<VuoRendererCable *, VuoRendererCable *> cableArgs = std::make_pair((dataCableToDisplace? dataCableToDisplace->getRenderer() : NULL),
2552 (cableToReplace? cableToReplace->getRenderer() : NULL));
2553 pair<string, string> typeArgs = std::make_pair(typecastToInsert, specializedTypeName);
2554 pair<VuoRendererPort *, VuoRendererPort *> portArgs = std::make_pair(portToUnpublish, portToSpecialize);
2555
2556 emit connectionCompletedByDragging(cableInProgress->getRenderer(),
2557 targetPort,
2558 cableArgs,
2559 typecastNodeToDelete,
2560 typeArgs,
2561 portArgs);
2562
2564
2565 // Resume caching for dragged cable.
2566 if (cableInProgress)
2567 {
2568 cableInProgress->getRenderer()->updateGeometry();
2569 cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2570 cableInProgress = NULL;
2571 }
2572 }
2573
2574 // Otherwise, delete the cable.
2575 else // if (! completedCableConnection)
2577
2578 emit cableDragEnded();
2579}
2580
2584void VuoEditorComposition::concludePublishedCableDrag(QGraphicsSceneMouseEvent *event)
2585{
2586 VuoRendererPort *fixedPort;
2587 if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2588 fixedPort = cableInProgress->getFromPort()->getRenderer();
2589 else // if (cableInProgress->getToNode() && cableInProgress->getToPort())
2590 fixedPort = cableInProgress->getToPort()->getRenderer();
2591
2592 // Potential side effects of a newly completed cable connection:
2593 // - A typecast may need to be automatically inserted.
2594 string typecastToInsert = "";
2595
2596 // - A generic port involved in the new connection may need to be specialized.
2597 VuoRendererPort *portToSpecialize = NULL;
2598 string specializedTypeName = "";
2599
2600 bool forceEventOnlyPublication = !cableInProgress->getRenderer()->effectivelyCarriesData();
2601
2602 // Case: Initiating a cable drag from a published input port.
2603 if (cableInProgress && cableInProgress->isPublishedInputCable())
2604 {
2605 VuoRendererPort *internalInputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2606 VuoRendererPublishedPort *publishedInputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getFromPort()->getRenderer());
2607
2608 // If the cable was dropped onto a node header area, connect an event-only cable to the node's first input port.
2609 // (If over both a port drop zone and a node header, the node header gets precedence.)
2610 {
2611 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2612 if (targetNode)
2613 {
2614 internalInputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2615 if (internalInputPort)
2616 {
2617 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2618 forceEventOnlyPublication = true;
2619 }
2620 }
2621 }
2622
2623 // Case: Cable was dropped onto an internal input port
2624 if (internalInputPort &&
2625 publishedInputPort)
2626 {
2627 // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2628 // don't push the operation onto the Undo stack.
2629 bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2630 internalInputPort->getBase()) &&
2631 (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2632 cableInProgress->getCompiler()->getAlwaysEventOnly()));
2633 if (recreatingSameConnection)
2634 {
2636 return;
2637 }
2638
2639 // Case: Ports are compatible
2640 if ((publishedInputPort->isCompatibleAliasWithSpecializationForInternalPort(internalInputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName))
2641 || selectBridgingSolution(publishedInputPort, internalInputPort, true, &portToSpecialize, specializedTypeName, typecastToInsert))
2642 {
2643 bool cableInProgressExpectedToCarryData = (cableInProgress->getRenderer()->effectivelyCarriesData() &&
2644 internalInputPort->getDataType());
2645 VuoCable *cableToReplace = publishedInputPort->getCableConnectedTo(internalInputPort, true);
2646 bool cableToReplaceHasMatchingDataCarryingStatus = (cableToReplace?
2647 (cableToReplace->getRenderer()->effectivelyCarriesData() ==
2648 cableInProgressExpectedToCarryData) :
2649 false);
2650
2651 // If replacing a preexisting cable with an identical cable, just cancel the operation
2652 // so that it doesn't go onto the Undo stack.
2653 if (cableToReplace && cableToReplaceHasMatchingDataCarryingStatus)
2655
2656 // Case: Replacing a pre-existing cable that connected the same two ports
2657 // but with a different data-carrying status, and whose "To" port is
2658 // the child port of a collapsed typecast.
2659 else if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus &&
2660 cableToReplace->getToPort()->hasRenderer() &&
2661 cableToReplace->getToPort()->getRenderer()->getTypecastParentPort())
2662 {
2663 // @todo Implement for https://b33p.net/kosada/node/14153
2664 // For now, don't attempt it.
2666 }
2667
2668 else
2669 {
2670 emit undoStackMacroBeginRequested("Cable Connection");
2672
2673 // If this source/target port combination already a cable connecting them, but of a different
2674 // data-carrying status, replace the old cable with the new one.
2675 if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus)
2676 {
2677 QList<QGraphicsItem *> removedComponents;
2678 removedComponents.append(cableToReplace->getRenderer());
2679 emit componentsRemoved(removedComponents, "Delete");
2680 }
2681
2682 // If the target port had a connected collapsed typecast, uncollapse and delete it.
2683 // But first check whether the collapsed typecast is still present on canvas -- it's possible it was
2684 // already removed during the cancelCableDrag() call, if the cable being dragged was the
2685 // typecast's incoming data+event cable.
2686 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(internalInputPort);
2687 if (typecastPort && typecastPort->scene())
2688 {
2689 VuoRendererPort *childPort = typecastPort->getChildPort();
2690 VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2691 VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2692
2693 // If the typecast did not have multiple incoming cables, and the new cable will
2694 // be replacing it as a data source, delete the typecast.
2695 if (cableInProgressExpectedToCarryData &&
2696 childPort->getBase()->getConnectedCables(true).size() < 2 &&
2697 (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2698 {
2699 QList<QGraphicsItem *> removedComponents;
2700 removedComponents.append(uncollapsedTypecast);
2701 emit componentsRemoved(removedComponents, "Delete");
2702 }
2703 }
2704
2705 emit portPublicationRequested(internalInputPort->getBase(),
2706 dynamic_cast<VuoPublishedPort *>(publishedInputPort->getBase()),
2707 forceEventOnlyPublication,
2708 (portToSpecialize? portToSpecialize->getBase() : NULL),
2709 specializedTypeName,
2710 typecastToInsert,
2711 false); // Avoid nested Undo stack macros.
2713 }
2714 }
2715 else // Source port and target port aren't compatible
2717 }
2718 else // Cable was dropped over empty canvas space
2720 }
2721
2722 // Case: Initiating a cable drag from a published output port.
2723 else if (cableInProgress && cableInProgress->isPublishedOutputCable())
2724 {
2725 VuoRendererPort *internalOutputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2726 VuoRendererPublishedPort *publishedOutputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getToPort()->getRenderer());
2727
2728 // If the cable was dropped onto a node header area, connect an event-only cable to the node's first output port.
2729 // (If over both a port drop zone and a node header, the node header gets precedence.)
2730 {
2731 VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2732 if (targetNode)
2733 {
2734 internalOutputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2735 if (internalOutputPort)
2736 {
2737 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2738 forceEventOnlyPublication = true;
2739 }
2740 }
2741 }
2742
2743 // Case: Cable was dropped onto an internal output port
2744 if (internalOutputPort &&
2745 publishedOutputPort)
2746 {
2747
2748 // Case: Ports are compatible
2749 if ((publishedOutputPort->isCompatibleAliasWithSpecializationForInternalPort(internalOutputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName) &&
2750 publishedOutputPort->canAccommodateInternalPort(internalOutputPort, forceEventOnlyPublication))
2751 || selectBridgingSolution(internalOutputPort, publishedOutputPort, false, &portToSpecialize, specializedTypeName, typecastToInsert))
2752 {
2753 emit portPublicationRequested(internalOutputPort->getBase(),
2754 dynamic_cast<VuoPublishedPort *>(publishedOutputPort->getBase()),
2755 forceEventOnlyPublication,
2756 portToSpecialize? portToSpecialize->getBase() : NULL,
2757 specializedTypeName,
2758 typecastToInsert,
2759 true);
2760 }
2761 }
2762
2764 }
2765
2766 emit cableDragEnded();
2767}
2768
2773{
2775
2776 if (! cableInProgress)
2777 return;
2778
2779 if (cableInProgressWasNew)
2780 removeCable(cableInProgress->getRenderer());
2781 else
2782 {
2783 QList<QGraphicsItem *> removedComponents;
2784 removedComponents.append(cableInProgress->getRenderer());
2785 emit componentsRemoved(removedComponents, "Delete");
2786 }
2787
2788 cableInProgress = NULL;
2789}
2790
2798{
2799 if (cableInProgress && cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort())
2800 {
2801 cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2802 cableInProgress->getRenderer()->updateGeometry();
2803 cableInProgress->setTo(getUnderlyingParentNodeForPort(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort(), this),
2804 cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort());
2805 cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2806 cableInProgress = NULL;
2807 }
2808 else if (cableInProgress)
2809 {
2811 }
2812
2815 emit cableDragEnded();
2816}
2817
2821void VuoEditorComposition::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
2822{
2823 bool leftMouseButtonPressed = (event->buttons() & Qt::LeftButton);
2824
2825 // Disable Mission Control workaround, since it sometimes misdetects whether the left mouse
2826 // button is pressed and overzealously cancels cable drags (specifically, those originating from
2827 // sidebar published output ports). The workaround no longer appears to be effective as of Qt 5.2.1 anyway.
2828 /*
2829 // In the unlikely situation that we have a cable drag in progress but the left mouse
2830 // button is not pressed (e.g., if "Mission Control" was activated during a cable drag and
2831 // deactivated by releasing the mouse button), cancel the cable drag.
2832 // See https://b33p.net/kosada/node/3305
2833 if (cableInProgress && !menuSelectionInProgress && !leftMouseButtonPressed)
2834 cancelCableDrag();
2835 */
2836
2837 // If in the process of a cable drag or mouse-over operation,
2838 // locate the nearest port or cable eligible for hover highlighting.
2839 if (cableInProgress || (! leftMouseButtonPressed))
2840 updateHoverHighlighting(event->scenePos());
2841
2842
2843 // Case: Left mouse button pressed
2844 if (leftMouseButtonPressed)
2845 {
2846 // Eliminate mouse jitter noise.
2847 if ((! dragStickinessDisabled) && (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2848 return;
2849
2850 else
2851 dragStickinessDisabled = true;
2852
2853 // If a port or cable has been clicked for dragging and this is the first mouse move event
2854 // since the click, initiate the cable drag.
2855 if (portWithDragInitiated || cableWithYankInitiated)
2856 {
2857 initiateCableDrag(portWithDragInitiated, cableWithYankInitiated, event);
2858 portWithDragInitiated = NULL;
2859 cableWithYankInitiated = NULL;
2860 }
2861
2862 // If there is a cable drag in progress, update the location of the cable's
2863 // floating endpoint to match that of the cursor.
2864 if (cableInProgress)
2865 {
2866 VuoRendererCable *rc = cableInProgress->getRenderer();
2867
2868 rc->updateGeometry();
2869 rc->setFloatingEndpointLoc(event->scenePos());
2870
2872 }
2873
2874 else if (duplicationDragInProgress)
2875 {
2876 if (duplicateOnNextMouseMove)
2877 {
2879 duplicateOnNextMouseMove = false;
2880 }
2881
2882 QPointF newPos = (VuoRendererItem::getSnapToGrid()?
2885 event->scenePos());
2886 QPointF delta = newPos - cursorPosBeforeDuplicationDragMove;
2887 moveItemsBy(getSelectedNodes(), getSelectedComments(), delta.x(), delta.y(), false);
2888 cursorPosBeforeDuplicationDragMove = newPos;
2889 }
2890
2891 else
2892 QGraphicsScene::mouseMoveEvent(event);
2893 }
2894
2895 // Case: Left mouse button not pressed
2896 else
2897 QGraphicsScene::mouseMoveEvent(event);
2898}
2899
2905void VuoEditorComposition::updateHoverHighlighting(QPointF scenePos, bool disablePortHoverHighlighting)
2906{
2907 auto types = compiler->getTypes();
2908
2909 // Detect cable and port hover events ourselves, since we need to account
2910 // for their extended hover ranges.
2911 QGraphicsItem *item = cableInProgress? findNearbyPort(scenePos, false) : findNearbyComponent(scenePos);
2912 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
2913 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2914 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(item);
2915 VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(item);
2916
2917 // If hovering over a node header area while dragging a cable, treat it like hovering over the first input or output port.
2918 // (If over both a port drop zone and a node header, the node header gets precedence.)
2919 bool hoveringOverNodeHeader = false;
2920 if (cableInProgress)
2921 {
2922 VuoRendererNode *targetNode = findNearbyNodeHeader(scenePos);
2923 if (targetNode)
2924 {
2926 if (port)
2927 {
2928 item = targetNode;
2929 hoveringOverNodeHeader = true;
2930 }
2931 }
2932 }
2933
2934 VuoRendererNode *node = NULL;
2935 if (! hoveringOverNodeHeader)
2936 {
2937 // Do not handle hover events for (non-drawer) nodes here.
2938 if (dynamic_cast<VuoRendererNode *>(item) && !makeListDrawer)
2939 item = NULL;
2940
2941 node = dynamic_cast<VuoRendererNode *>(item);
2942 }
2943
2944 // Case: The item (if any) that requires hover-highlighting given the current position of the cursor
2945 // is not the same as the item (if any) that was hover-highlighted previously.
2946 if (item != previousNearbyItem)
2947 {
2948 VuoRendererPort *previousPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2949 VuoRendererNode *previousNode = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2950 if (previousNode)
2951 previousPort = findTargetPortForCableDroppedOnNodeHeader(previousNode);
2952
2953 // End hover-highlighting of the previous item, if any.
2954 if (previousNearbyItem)
2956
2957 // Begin hover-highlighting of the current item, if any.
2958 if (cable)
2959 cable->extendedHoverEnterEvent();
2960 else if (node)
2961 {
2962 if (! cableInProgress)
2963 {
2964 if (makeListDrawer)
2965 makeListDrawer->extendedHoverEnterEvent(scenePos);
2966 }
2967 }
2968 else if (typecastPort && !disablePortHoverHighlighting)
2969 port->extendedHoverEnterEvent((bool)cableInProgress);
2970 else if (port && !disablePortHoverHighlighting)
2971 {
2972 port->extendedHoverEnterEvent((bool)cableInProgress);
2973 if (!cableInProgress)
2974 {
2975 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2976 connectedAntennaPort->extendedHoverEnterEvent((bool)cableInProgress, true);
2977 }
2978 }
2979
2980 VuoRendererPort *previousTargetPort = (previousPort && previousPort->isEligibleForConnection() ? previousPort : NULL);
2981
2982 // If the current item or previous item is a port and it got this status because the mouse hovered
2983 // over a node header, update the eligibility-highlighting of the port.
2984 if (cableInProgress)
2985 {
2986 VuoRendererPort *fixedPort = (cableInProgress->getFromPort() ?
2987 cableInProgress->getFromPort()->getRenderer() :
2988 cableInProgress->getToPort()->getRenderer());
2989
2990 QList< QPair<VuoRendererPort *, bool> > updatedPorts;
2991 if (hoveringOverNodeHeader)
2992 updatedPorts.append( QPair<VuoRendererPort *, bool>(port, true) );
2993 if (previousNode && previousPort)
2994 updatedPorts.append( QPair<VuoRendererPort *, bool>(previousPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) );
2995
2996 QPair<VuoRendererPort *, bool> p;
2997 foreach (p, updatedPorts)
2998 {
2999 VuoRendererPort *updatedPort = p.first;
3000
3001 VuoRendererPort *typecastParentPort = updatedPort->getTypecastParentPort();
3002 if (typecastParentPort)
3003 updatedPort = typecastParentPort;
3004
3005 updateEligibilityHighlightingForPort(updatedPort, fixedPort, p.second, types);
3006
3007 VuoRendererNode *potentialDrawer = updatedPort->getUnderlyingParentNode();
3008 updateEligibilityHighlightingForNode(potentialDrawer);
3009 }
3010 }
3011
3012 VuoRendererPort *targetPort = (port && port->isEligibleForConnection() ? port : NULL);
3013
3014 // Update error dialogs and cable's data-carrying status.
3015 if (targetPort || previousTargetPort)
3016 {
3017 if (cableInProgress)
3018 cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(targetPort && (!targetPort->getDataType() || hoveringOverNodeHeader));
3019
3020 updateFeedbackErrors(targetPort);
3021 }
3022
3023 previousNearbyItem = item;
3024 }
3025
3026 // Case: The previously hover-highlighted item should remain hover-highlighted.
3027 else if (item)
3028 {
3029 if (cable)
3030 cable->extendedHoverMoveEvent();
3031 else if (port && !disablePortHoverHighlighting)
3032 {
3033 port->extendedHoverMoveEvent((bool)cableInProgress);
3034 if (!cableInProgress)
3035 {
3036 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
3037 connectedAntennaPort->extendedHoverMoveEvent((bool)cableInProgress, true);
3038 }
3039 }
3040 else if (makeListDrawer)
3041 makeListDrawer->extendedHoverMoveEvent(scenePos);
3042 }
3043}
3044
3049{
3050 if (previousNearbyItem)
3051 {
3052 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(previousNearbyItem);
3053 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3054 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(previousNearbyItem);
3055 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
3056
3057 if (node)
3058 {
3059 VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(previousNearbyItem);
3060 if (drawer)
3061 drawer->extendedHoverLeaveEvent();
3062 else
3064 }
3065
3066 if (cable)
3067 cable->extendedHoverLeaveEvent();
3068 else if (typecastPort)
3070 else if (port)
3071 {
3073 foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
3074 connectedAntennaPort->extendedHoverLeaveEvent();
3075 }
3076
3077 previousNearbyItem = NULL;
3078 }
3079}
3080
3085{
3086 if ((event->key() != Qt::Key_Alt) && (event->key() != Qt::Key_Shift) && (event->key() != Qt::Key_Escape))
3088
3089 Qt::KeyboardModifiers modifiers = event->modifiers();
3090 qreal adjustedNodeMoveRate = nodeMoveRate;
3091 if (modifiers & Qt::ShiftModifier)
3092 {
3093 adjustedNodeMoveRate *= nodeMoveRateMultiplier;
3094 }
3095
3096 switch (event->key()) {
3097 case Qt::Key_Backspace:
3098 {
3100 break;
3101 }
3102 case Qt::Key_Delete:
3103 {
3105 break;
3106 }
3107 case Qt::Key_Up:
3108 {
3109 moveSelectedItemsBy(0, -1*adjustedNodeMoveRate);
3110 break;
3111 }
3112 case Qt::Key_Down:
3113 {
3114 if (modifiers & Qt::ControlModifier)
3115 openSelectedEditableNodes();
3116 else
3117 moveSelectedItemsBy(0, adjustedNodeMoveRate);
3118 break;
3119 }
3120 case Qt::Key_Left:
3121 {
3122 moveSelectedItemsBy(-1*adjustedNodeMoveRate, 0);
3123 break;
3124 }
3125 case Qt::Key_Right:
3126 {
3127 moveSelectedItemsBy(adjustedNodeMoveRate, 0);
3128 break;
3129 }
3130 case Qt::Key_Shift:
3131 {
3132 if (cableInProgress)
3133 {
3134 cableInProgress->getRenderer()->updateGeometry();
3135 cableInProgress->getCompiler()->setAlwaysEventOnly(true);
3136 highlightEligibleEndpointsForCable(cableInProgress);
3137
3138 VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3139 if (hoveredPort)
3140 updateFeedbackErrors(hoveredPort);
3141 }
3142
3143 break;
3144 }
3145 case Qt::Key_Enter:
3146 case Qt::Key_Return:
3147 {
3148 // Make sure the event is forwarded to any composition component within hover range.
3149 QGraphicsScene::keyPressEvent(event);
3150
3151 if (!event->isAccepted())
3152 {
3153 // Otherwise, if there are any (and only) nodes currently selected, open a title editor for each one;
3154 // if there are any (and only) comments currently selected, open a comment text editor for each one.
3155 set<VuoRendererNode *> selectedNodes = getSelectedNodes();
3156 set<VuoRendererComment *> selectedComments = getSelectedComments();
3157
3158 if (selectedComments.empty() && !selectedNodes.empty())
3160 }
3161
3162 break;
3163 }
3164
3165 case Qt::Key_Escape:
3166 {
3167 if (duplicateOnNextMouseMove || duplicationDragInProgress)
3168 {
3170
3171 duplicateOnNextMouseMove = false;
3172 duplicationDragInProgress = false;
3173 duplicationCancelled = true;
3174 }
3175 else if (cableInProgress)
3176 {
3178 }
3179 break;
3180 }
3181
3182 default:
3183 {
3184 QGraphicsScene::keyPressEvent(event);
3185 break;
3186 }
3187 }
3188}
3189
3194{
3195 switch (event->key()) {
3196 case Qt::Key_Shift:
3197 {
3198 if (cableInProgress)
3199 {
3200 cableInProgress->getRenderer()->updateGeometry();
3201 cableInProgress->getCompiler()->setAlwaysEventOnly(false);
3202 highlightEligibleEndpointsForCable(cableInProgress);
3203
3204 VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3205 if (hoveredPort)
3206 updateFeedbackErrors(hoveredPort);
3207 }
3208
3209 break;
3210 }
3211
3212 default:
3213 {
3214 QGraphicsScene::keyReleaseEvent(event);
3215 break;
3216 }
3217 }
3218}
3219
3223void VuoEditorComposition::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
3224{
3225 // Determine whether the cursor is in range of any operable composition components.
3226 QGraphicsItem *item = findNearbyComponent(event->scenePos());
3227
3229 contextMenu.setSeparatorsCollapsible(false);
3230
3231 // Customize context menu for the canvas.
3232 if (! item)
3233 {
3234 QAction *insertNodeSection = new QAction(tr("Insert Node"), NULL);
3235 insertNodeSection->setEnabled(false);
3236 contextMenu.addAction(insertNodeSection);
3237
3238 QString spacer(" ");
3239
3240 {
3241 // "Share" nodes
3242 QMenu *shareMenu = new QMenu(&contextMenu);
3243 shareMenu->setSeparatorsCollapsible(false);
3244 shareMenu->setTitle(spacer + tr("Share"));
3245 contextMenu.addMenu(shareMenu);
3246
3247 {
3248 QAction *action = new QAction(tr("Share Value"), NULL);
3249 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share"))));
3250 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3251 shareMenu->addAction(action);
3252 }
3253
3254 {
3255 QAction *action = new QAction(tr("Share List"), NULL);
3256 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share.list"))));
3257 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3258 shareMenu->addAction(action);
3259 }
3260
3261 {
3262 QAction *action = new QAction(tr("Share Event"), NULL);
3263 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.share"))));
3264 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3265 shareMenu->addAction(action);
3266 }
3267 }
3268
3269 {
3270 // "Hold" nodes
3271 QMenu *holdMenu = new QMenu(&contextMenu);
3272 holdMenu->setSeparatorsCollapsible(false);
3273 holdMenu->setTitle(spacer + tr("Hold"));
3274 contextMenu.addMenu(holdMenu);
3275
3276 {
3277 QAction *action = new QAction(tr("Hold Value"), NULL);
3278 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold2"))));
3279 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3280 holdMenu->addAction(action);
3281 }
3282
3283 {
3284 QAction *action = new QAction(tr("Hold List"), NULL);
3285 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold.list2"))));
3286 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3287 holdMenu->addAction(action);
3288 }
3289 }
3290
3291 {
3292 // "Allow" nodes
3293 QMenu *allowMenu = new QMenu(&contextMenu);
3294 allowMenu->setSeparatorsCollapsible(false);
3295 allowMenu->setTitle(spacer + tr("Allow"));
3296 contextMenu.addMenu(allowMenu);
3297
3298 {
3299 QAction *action = new QAction(tr("Allow First Event"), NULL);
3300 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirst"))));
3301 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3302 allowMenu->addAction(action);
3303 }
3304
3305 {
3306 QAction *action = new QAction(tr("Allow First Value"), NULL);
3307 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirstValue"))));
3308 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3309 allowMenu->addAction(action);
3310 }
3311
3312 {
3313 QAction *action = new QAction(tr("Allow Periodic Events"), NULL);
3314 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.time.allowPeriodic"))));
3315 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3316 allowMenu->addAction(action);
3317 }
3318
3319 {
3320 QAction *action = new QAction(tr("Allow Changes"), NULL);
3321 action->setData(QVariant::fromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowChanges2"))));
3322 connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3323 allowMenu->addAction(action);
3324 }
3325 }
3326
3327 contextMenu.addSeparator();
3328
3329 QAction *contextMenuInsertComment = new QAction(NULL);
3330 contextMenuInsertComment->setText(tr("Insert Comment"));
3331 contextMenuInsertComment->setData(event->scenePos());
3332 connect(contextMenuInsertComment, &QAction::triggered, this, &VuoEditorComposition::insertComment);
3333 contextMenu.addAction(contextMenuInsertComment);
3334
3335 QAction *contextMenuInsertSubcomposition = new QAction(NULL);
3336 contextMenuInsertSubcomposition->setText(tr("Insert Subcomposition"));
3337 contextMenuInsertSubcomposition->setData(event->scenePos());
3338 connect(contextMenuInsertSubcomposition, &QAction::triggered, this, &VuoEditorComposition::insertSubcomposition);
3339 contextMenu.addAction(contextMenuInsertSubcomposition);
3340 }
3341
3342 // Prepare to take a snapshot of the contextMenuDeleteSelection QAction's current values, so that
3343 // they do not change while the context menu is displayed.
3344 QAction *contextMenuDeleteSelectedSnapshot = new QAction(NULL);
3345
3346 // Customize context menu for ports.
3347 if (dynamic_cast<VuoRendererPort *>(item))
3348 {
3349 VuoRendererPort *port = (VuoRendererPort *)item;
3350
3351 if (port->isConstant() && inputEditorManager)
3352 {
3353 VuoType *dataType = static_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler())->getDataVuoType();
3354 VuoInputEditor *inputEditorLoadedForPortDataType = inputEditorManager->newInputEditor(dataType);
3355 if (inputEditorLoadedForPortDataType)
3356 {
3357 contextMenuSetPortConstant->setData(QVariant::fromValue(port));
3358 contextMenu.addAction(contextMenuSetPortConstant);
3359
3360 inputEditorLoadedForPortDataType->deleteLater();
3361 }
3362 }
3363
3364 if (!isPortPublished(port) && port->getPublishable())
3365 {
3366 contextMenuPublishPort->setText(tr("Publish Port"));
3367 contextMenuPublishPort->setData(QVariant::fromValue(port));
3368 contextMenu.addAction(contextMenuPublishPort);
3369 }
3370
3371 else if (isPortPublished(port))
3372 {
3373 vector<VuoRendererPublishedPort *> externalPublishedPorts = port->getPublishedPorts();
3374 bool hasExternalPublishedPortWithMultipleInternalPorts = false;
3375 bool hasExternalPublishedPortBelongingToActiveProtocol = false;
3376 foreach (VuoRendererPublishedPort *externalPort, externalPublishedPorts)
3377 {
3378 if (externalPort->getBase()->getConnectedCables(true).size() > 1)
3379 hasExternalPublishedPortWithMultipleInternalPorts = true;
3380
3381 if (dynamic_cast<VuoPublishedPort *>(externalPort->getBase())->isProtocolPort())
3382 hasExternalPublishedPortBelongingToActiveProtocol = true;
3383 }
3384
3385 // Omit the "Delete Published Port" context menu item if the internal port is connected to
3386 // an external published port that cannot be deleted, either because:
3387 // - It is part of an active protocol;
3388 // - It has more than one connected cable.
3389 if (!hasExternalPublishedPortWithMultipleInternalPorts &&!hasExternalPublishedPortBelongingToActiveProtocol)
3390 {
3391 contextMenuPublishPort->setText(tr("Delete Published Port"));
3392 contextMenuPublishPort->setData(QVariant::fromValue(port));
3393 contextMenu.addAction(contextMenuPublishPort);
3394 }
3395 }
3396
3397 bool isTriggerPort = dynamic_cast<VuoCompilerTriggerPort *>(port->getBase()->getCompiler());
3398 if (isTriggerPort || port->getInput())
3399 {
3400 __block bool isTopLevelCompositionRunning = false;
3401 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
3402 {
3403 isTopLevelCompositionRunning = topLevelComposition->isRunning();
3404 });
3405
3406 if (isTopLevelCompositionRunning)
3407 {
3408 contextMenuFireEvent->setData(QVariant::fromValue(port));
3409 contextMenu.addAction(contextMenuFireEvent);
3410 }
3411 }
3412
3413 if (isTriggerPort)
3414 {
3415 QMenu *contextMenuThrottling = new QMenu(&contextMenu);
3416 contextMenuThrottling->setSeparatorsCollapsible(false);
3417 contextMenuThrottling->setTitle(tr("Set Event Throttling"));
3419 foreach (QAction *action, contextMenuThrottlingActions)
3420 {
3421 contextMenuThrottling->addAction(action);
3422 action->setData(QVariant::fromValue(port));
3423 action->setCheckable(true);
3424 action->setChecked( i++ == port->getBase()->getEventThrottling() );
3425 }
3426 contextMenu.addMenu(contextMenuThrottling);
3427 }
3428
3429 // Allow the user to specialize, respecialize, or unspecialize generic data types.
3430 if (dynamic_cast<VuoGenericType *>(port->getDataType()) || isPortCurrentlyRevertible(port))
3431 {
3432 if (contextMenuSpecializeGenericType)
3433 contextMenuSpecializeGenericType->deleteLater();
3434
3435 contextMenuSpecializeGenericType = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3436 contextMenuSpecializeGenericType->setSeparatorsCollapsible(false);
3437 contextMenuSpecializeGenericType->setTitle(tr("Set Data Type"));
3438 contextMenuSpecializeGenericType->setToolTipsVisible(true);
3439
3440 populateSpecializePortMenu(contextMenuSpecializeGenericType, port, true);
3441 contextMenu.addMenu(contextMenuSpecializeGenericType);
3442 }
3443
3444 // Allow the user to hide, unhide, or delete cables connected directly to the port or, if the
3445 // port has a collapsed typecast, to the typecast's child port.
3446 int numVisibleDirectlyConnectedCables = 0;
3447 int numHidableDirectlyConnectedCables = 0;
3448 int numUnhidableDirectlyConnectedCables = 0;
3449
3450 foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
3451 {
3452 if (!cable->getRenderer()->paintingDisabled())
3453 {
3454 numVisibleDirectlyConnectedCables++;
3455 if (!cable->isPublished())
3456 numHidableDirectlyConnectedCables++;
3457 }
3458
3459 else if (cable->getRenderer()->getEffectivelyWireless())
3460 numUnhidableDirectlyConnectedCables++;
3461 }
3462
3463 int numVisibleChildPortConnectedCables = 0;
3464 int numHidableChildPortConnectedCables = 0;
3465 int numUnhidableChildPortConnectedCables = 0;
3466 VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
3467 if (typecastPort)
3468 {
3469 VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
3470 VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
3471 foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
3472 {
3473 if (!cable->getRenderer()->paintingDisabled())
3474 {
3475 numVisibleChildPortConnectedCables++;
3476
3477 if (!cable->isPublished())
3478 numHidableChildPortConnectedCables++;
3479 }
3480 else if (cable->getRenderer()->getEffectivelyWireless())
3481 numUnhidableChildPortConnectedCables++;
3482 }
3483 }
3484
3485 // Count the number of directly or indirectly connected cables that are currently visible.
3486 int numVisibleConnectedCables = numVisibleDirectlyConnectedCables + numVisibleChildPortConnectedCables;
3487
3488 // Count the number of directly or indirectly connected cables that are currently hidable.
3489 int numHidableConnectedCables = numHidableDirectlyConnectedCables + numHidableChildPortConnectedCables;
3490
3491 // Count the number of directly or indirectly connected cables that are currently hidden.
3492 int numUnhidableConnectedCables = numUnhidableDirectlyConnectedCables + numUnhidableChildPortConnectedCables;
3493
3494 if ((!renderHiddenCables && ((numHidableConnectedCables >= 1) || (numUnhidableConnectedCables >= 1)))
3495 || (numVisibleConnectedCables >= 1))
3496 {
3497 if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3498 contextMenu.addSeparator();
3499 }
3500
3501 if (!renderHiddenCables)
3502 {
3503 if (numHidableConnectedCables >= 1)
3504 {
3505 // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3506 // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3507 // impact visible published cables.
3508 int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3509 contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3510 contextMenuHideCables->setData(QVariant::fromValue(port));
3511 contextMenu.addAction(contextMenuHideCables);
3512 }
3513
3514 if (numUnhidableConnectedCables >= 1)
3515 {
3516 int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3517 contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3518 contextMenuUnhideCables->setData(QVariant::fromValue(port));
3519 contextMenu.addAction(contextMenuUnhideCables);
3520 }
3521 }
3522
3523 if (numVisibleConnectedCables >= 1)
3524 {
3525 contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3526 contextMenuDeleteCables->setData(QVariant::fromValue(port));
3527 contextMenu.addAction(contextMenuDeleteCables);
3528 }
3529 }
3530
3531 // Customize context menu for nodes, cables, and/or comments.
3532 else if (item)
3533 {
3534 if (! item->isSelected())
3535 {
3536 clearSelection();
3537 item->setSelected(true);
3538 }
3539
3540 QList<QGraphicsItem *> selectedComponents = selectedItems();
3541 bool onlyCommentsSelected = true;
3542 bool onlyCablesSelected = true;
3543 bool selectionContainsMissingNode = false;
3544 foreach (QGraphicsItem *item, selectedComponents)
3545 {
3546 if (!dynamic_cast<VuoRendererComment *>(item))
3547 onlyCommentsSelected = false;
3548
3549 if (!dynamic_cast<VuoRendererCable *>(item))
3550 onlyCablesSelected = false;
3551
3552 if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3553 selectionContainsMissingNode = true;
3554 }
3555
3556 contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3557 connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3558
3559 // Comments
3560 VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3561 if (comment)
3562 {
3563 // Option: Edit selected comments
3564 if (onlyCommentsSelected)
3565 contextMenu.addAction(contextMenuEditSelectedComments);
3566
3567 // Option: Tint selected components(s)
3568 contextMenu.addMenu(getContextMenuTints(&contextMenu));
3569
3570 contextMenu.addSeparator();
3571
3572 // Option: Refactor selected component(s)
3573 contextMenu.addAction(contextMenuRefactorSelected);
3574
3575 contextMenu.addSeparator();
3576
3577 // Option: Delete selected components(s)
3578 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3579 }
3580
3581 // Cables
3582 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3583 if (cable)
3584 {
3585 if (!renderHiddenCables)
3586 {
3587 if (onlyCablesSelected && !cable->getBase()->isPublished())
3588 contextMenu.addAction(contextMenuHideSelectedCables);
3589 }
3590
3591 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3592 }
3593
3594 // Nodes
3595 else if (dynamic_cast<VuoRendererNode *>(item))
3596 {
3597 VuoRendererNode *node = (VuoRendererNode *)item;
3598
3599 // Input drawers
3600 if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3601 node->getBase()->getNodeClass()->hasCompiler())
3602 {
3603 // Offer "Add/Remove Input Port" options for resizable list input drawers.
3604 if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3605 {
3606 contextMenuAddInputPort->setData(QVariant::fromValue(node));
3607 contextMenuRemoveInputPort->setData(QVariant::fromValue(node));
3608
3609 int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3610 contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3611
3612 contextMenu.addAction(contextMenuAddInputPort);
3613 contextMenu.addAction(contextMenuRemoveInputPort);
3614
3615 contextMenu.addSeparator();
3616 }
3617
3618 // Offer "Reset" option for all input drawers.
3619 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3620 }
3621
3622 // Non-input-drawer nodes
3623 else
3624 {
3625 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3626 if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3627 {
3628 string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3629 VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3630 if (originalGenericNodeClass)
3631 nodeClass = originalGenericNodeClass->getBase();
3632 }
3633
3634 QString actionText, sourcePath;
3635 bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3636 bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3637
3638 int numSelectedNonAttachmentNodes = 0;
3639 QList<QGraphicsItem *> selectedComponents = selectedItems();
3640 foreach (QGraphicsItem *item, selectedComponents)
3641 {
3642 if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3643 numSelectedNonAttachmentNodes++;
3644 }
3645
3646 if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3647 {
3648 // Option: Edit an installed subcomposition, shader, or text-code node.
3649 QAction *editAction = new QAction(NULL);
3650 editAction->setText(actionText);
3651 editAction->setData(QVariant::fromValue(node));
3652 connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3653
3654 contextMenu.addAction(editAction);
3655 contextMenu.addSeparator();
3656 }
3657
3658 // Option: Rename selected node(s)
3659 if (node->getBase()->hasCompiler())
3660 contextMenu.addAction(contextMenuRenameSelected);
3661
3662 // Option: Tint selected component(s)
3663 contextMenu.addMenu(getContextMenuTints(&contextMenu));
3664
3665 contextMenu.addSeparator();
3666
3667 // Option: Replace selected node with a similar node
3668 if (numSelectedNonAttachmentNodes == 1)
3669 {
3670 if (contextMenuChangeNode)
3671 contextMenuChangeNode->deleteLater();
3672
3673 contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3674 contextMenuChangeNode->setSeparatorsCollapsible(false);
3675 contextMenuChangeNode->setTitle(tr("Change To"));
3676
3677 populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3678 if (!contextMenuChangeNode->actions().isEmpty())
3679 contextMenu.addMenu(contextMenuChangeNode);
3680 } // End single selected node
3681
3682 // Option: Refactor selected component(s)
3683 if (!selectionContainsMissingNode)
3684 contextMenu.addAction(contextMenuRefactorSelected);
3685
3686 if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3687 {
3688 // Option: Open the enclosing folder for an editable or other 3rd party node class.
3689 QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3690 if (!modulePath.isEmpty())
3691 {
3692 QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3693 QAction *openEnclosingFolderAction = new QAction(NULL);
3694 openEnclosingFolderAction->setText("Show in Finder");
3695 openEnclosingFolderAction->setData(enclosingDirUrl);
3696 connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3697 contextMenu.addAction(openEnclosingFolderAction);
3698 }
3699 }
3700
3701 if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3702 contextMenu.addSeparator();
3703
3704 // Option: Delete selected components(s)
3705 contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3706
3707 } // End non-input-drawer nodes
3708 } // End nodes
3709 } // End nodes or cables
3710
3711 if (!contextMenu.actions().isEmpty())
3712 {
3713 // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3715
3716 menuSelectionInProgress = true;
3717 contextMenu.exec(event->screenPos());
3718 menuSelectionInProgress = false;
3719 }
3720
3721 delete contextMenuDeleteSelectedSnapshot;
3722}
3723
3728{
3729 QAction *sender = (QAction *)QObject::sender();
3730 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3731 emit nodeSourceEditorRequested(node);
3732}
3733
3737void VuoEditorComposition::expandSpecializePortMenu()
3738{
3739 QAction *sender = (QAction *)QObject::sender();
3740 VuoRendererPort *port = sender->data().value<VuoRendererPort *>();
3741
3742 populateSpecializePortMenu(contextMenuSpecializeGenericType, port, false);
3743 contextMenuSpecializeGenericType->exec();
3744}
3745
3753void VuoEditorComposition::populateSpecializePortMenu(QMenu *menu, VuoRendererPort *port, bool limitInitialOptions)
3754{
3755 menu->clear();
3756
3757 if (!port)
3758 return;
3759
3760 VuoGenericType *genericDataType = dynamic_cast<VuoGenericType *>(port->getDataType());
3761 QAction *unspecializeAction = menu->addAction(tr("Generic"));
3762
3763 // It is safe to assume that there will be types listed after the "Generic" option
3764 // since any network of connected generic/specialized ports has at least one compatible
3765 // data type in common, so the separator can be added here without forward-checking.
3766 menu->addSeparator();
3767
3768 unspecializeAction->setData(QVariant::fromValue(port));
3769 unspecializeAction->setCheckable(true);
3770 unspecializeAction->setChecked(genericDataType);
3771
3772 VuoGenericType *genericTypeFromPortClass = NULL; // Original generic type of the port
3773 set<string> compatibleTypes; // Compatible types in the context of the connected generic port network
3774 set<string> compatibleTypesInIsolation; // Compatible types for the port in isolation
3775
3776 // Allow the user to specialize generic ports.
3777 if (genericDataType)
3778 {
3779 VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(port->getBase()->getClass()->getCompiler());
3780 genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3781
3782 // Determine compatible types in the context of the connected generic port network.
3783 VuoGenericType::Compatibility compatibility;
3784 vector<string> compatibleTypesVector = genericDataType->getCompatibleSpecializedTypes(compatibility);
3785 compatibleTypes = set<string>(compatibleTypesVector.begin(), compatibleTypesVector.end());
3786
3787 // If all types or all list types are compatible, add them to (currently empty) compatibleTypes.
3788 if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3789 {
3790 vector<string> compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3791 compatibleTypes.insert(compatibleTypeNames.begin(), compatibleTypeNames.end());
3792 }
3793 }
3794
3795 // Allow the user to re-specialize already specialized ports.
3796 else if (isPortCurrentlyRevertible(port))
3797 {
3798 map<VuoNode *, string> nodesToReplace;
3799 set<VuoCable *> cablesToDelete;
3800 createReplacementsToUnspecializePort(port->getBase(), false, nodesToReplace, cablesToDelete);
3801 if (cablesToDelete.size() >= 1)
3802 {
3803 // @todo https://b33p.net/kosada/node/8895 : Note that this specialization will break connections.
3804 }
3805
3806 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(
3808 genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(port->getBase()->getClass()) );
3809 compatibleTypes = getRespecializationOptionsForPortInNetwork(port);
3810 }
3811
3812 // Determine compatible types in isolation.
3813 if (genericTypeFromPortClass)
3814 {
3815 // "Make List" drawer child ports require special handling, since technically they are compatible with all types
3816 // but practically they are limited to types compatible with their host ports.
3818 if (drawer)
3819 {
3820 VuoPort *hostPort = drawer->getUnderlyingHostPort();
3821 if (hostPort && hostPort->hasRenderer())
3822 {
3823 VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(hostPort->getClass()->getCompiler());
3824 VuoGenericType *genericHostPortDataType = dynamic_cast<VuoGenericType *>(hostPort->getRenderer()->getDataType());
3825 if (genericHostPortDataType)
3826 genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3827 else if (isPortCurrentlyRevertible(hostPort->getRenderer()))
3828 {
3830 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3831 genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass->getBase()) );
3832 }
3833 }
3834 }
3835
3837 vector<string> compatibleTypesInIsolationVector;
3838 if (genericTypeFromPortClass)
3839 compatibleTypesInIsolationVector = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibilityInIsolation);
3840
3841 // If all types or all list types are compatible, add them to (currently empty) compatibleTypesInIsolationVector.
3842 if (compatibilityInIsolation == VuoGenericType::anyType || compatibilityInIsolation == VuoGenericType::anyListType)
3843 compatibleTypesInIsolationVector = getAllSpecializedTypeOptions(compatibilityInIsolation == VuoGenericType::anyListType);
3844
3845 foreach (string type, compatibleTypesInIsolationVector)
3846 compatibleTypesInIsolation.insert(drawer? VuoType::extractInnermostTypeName(type) : type);
3847 }
3848
3849 // List the compatible types in the menu.
3850 {
3851 // If there are only a handful of eligible types, display them all in a flat menu.
3852 const int maxTypeCountForFlatMenuDisplay = 10;
3853 if (compatibleTypesInIsolation.size() <= maxTypeCountForFlatMenuDisplay)
3854 {
3855 QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, false, "", menu);
3856 addTypeActionsToMenu(actions, menu);
3857 }
3858
3859 // Otherwise, organize them by node set.
3860 else
3861 {
3862 // First list compatible types that don't belong to any specific node set.
3863 QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, "", menu);
3864 addTypeActionsToMenu(actions, menu);
3865
3866 // Now add a submenu for each node set that contains compatible types.
3867 map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3868 QList<QAction *> allNodeSetActionsToAdd;
3869 for (map<string, set<VuoCompilerType *> >::iterator i = loadedTypesForNodeSet.begin(); i != loadedTypesForNodeSet.end(); ++i)
3870 {
3871 string nodeSetName = i->first;
3872 if (!nodeSetName.empty())
3873 allNodeSetActionsToAdd += getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, nodeSetName, menu);
3874 }
3875
3876 bool usingExpansionMenu = false;
3877 if ((menu->actions().size() > 0) && (allNodeSetActionsToAdd.size() > 0))
3878 {
3879 menu->addSeparator();
3880
3881 if (limitInitialOptions)
3882 {
3883 //: Appears at the bottom of the "Set Data Type" menu when there are additional options to display.
3884 QAction *showMoreAction = menu->addAction(tr("More…"));
3885 showMoreAction->setData(QVariant::fromValue(port));
3886 connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandSpecializePortMenu);
3887 usingExpansionMenu = true;
3888 }
3889 }
3890
3891 if (!usingExpansionMenu)
3892 addTypeActionsToMenu(allNodeSetActionsToAdd, menu);
3893 }
3894
3895 foreach (QAction *action, menu->actions())
3896 {
3897 QMenu *specializeSubmenu = action->menu();
3898 if (specializeSubmenu)
3899 {
3900 foreach (QAction *specializeSubaction, specializeSubmenu->actions())
3901 connect(specializeSubaction, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3902 }
3903 else if (action == unspecializeAction)
3904 connect(action, &QAction::triggered, this, &VuoEditorComposition::unspecializePortType);
3905 else
3906 connect(action, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3907 }
3908 }
3909}
3910
3917{
3918 vector<string> typeOptions;
3919
3920 map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
3921 for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
3922 {
3923 if (((!lists && !VuoType::isListTypeName(i->first)) ||
3924 (lists && VuoType::isListTypeName(i->first))) &&
3925 !VuoType::isDictionaryTypeName(i->first) &&
3926 (i->first != "VuoMathExpressionList"))
3927 {
3928 typeOptions.push_back(i->first);
3929 }
3930 }
3931
3932 return typeOptions;
3933}
3934
3940set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3941{
3942 if (!port)
3943 return set<string>();
3944
3945 // Find the set of connected generic ports that contains our target port.
3946 set<VuoPort *> connectedGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(port->getUnderlyingParentNode()->getBase(),
3947 port->getBase(), true);
3948
3949 // Determine the set of types compatible with all generic ports in the connected network.
3950 vector<string> compatibleInnerTypeNames;
3951 vector<string> compatibleTypeNames;
3952 for (VuoPort *connectedPort : connectedGenericPorts)
3953 {
3954 VuoGenericType *genericTypeFromPortClass = NULL;
3955 VuoCompilerNodeClass *nodeClass = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler();
3956 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3957 if (specializedNodeClass)
3958 {
3959 VuoPortClass *portClass = connectedPort->getClass();
3960 genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3961 }
3962
3963 VuoGenericType::Compatibility compatibility;
3964 vector<string> compatibleTypeNamesForPort = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3965 vector<string> innermostCompatibleTypeNamesForPort;
3966 for (vector<string>::iterator k = compatibleTypeNamesForPort.begin(); k != compatibleTypeNamesForPort.end(); ++k)
3967 innermostCompatibleTypeNamesForPort.push_back( VuoType::extractInnermostTypeName(*k) );
3968
3969 if (! innermostCompatibleTypeNamesForPort.empty())
3970 {
3971 if (compatibleInnerTypeNames.empty())
3972 compatibleInnerTypeNames = innermostCompatibleTypeNamesForPort;
3973 else
3974 {
3975 for (int k = compatibleInnerTypeNames.size() - 1; k >= 0; --k)
3976 if (find(innermostCompatibleTypeNamesForPort.begin(), innermostCompatibleTypeNamesForPort.end(), compatibleInnerTypeNames[k]) ==
3977 innermostCompatibleTypeNamesForPort.end())
3978 compatibleInnerTypeNames.erase(compatibleInnerTypeNames.begin() + k);
3979 }
3980 }
3981 }
3982
3984 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3985 VuoPortClass *portClass = port->getBase()->getClass();
3986 VuoGenericType *genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3987 string typeNameForPort = genericTypeFromPortClass->getModuleKey();
3988
3989 // Finish compiling set of compatible types in the context of the connected generic port network.
3990 string prefix = (VuoType::isListTypeName(typeNameForPort) ? VuoType::listTypeNamePrefix : "");
3991 for (vector<string>::iterator k = compatibleInnerTypeNames.begin(); k != compatibleInnerTypeNames.end(); ++k)
3992 compatibleTypeNames.push_back(prefix + *k);
3993
3994 if (compatibleTypeNames.empty())
3995 {
3996 VuoGenericType::Compatibility compatibility;
3997 genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3998
3999 // If all types or all list types are compatible, add them to (currently empty) compatibleTypeNames.
4000 if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
4001 compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
4002 }
4003
4004 return set<string>(compatibleTypeNames.begin(), compatibleTypeNames.end());
4005}
4006
4027 set<string> compatibleTypesInIsolation,
4028 set<string> compatibleTypesInContext,
4029 bool limitToNodeSet,
4030 string nodeSetName,
4031 QMenu *menu)
4032{
4033 QList<QAction *> actionsToAddToMenu;
4034
4035 map<string, VuoCompilerType *> allTypes = compiler->getTypes();
4036 map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
4037 QList<VuoCompilerType *> compatibleTypesForNodeSetDisplay;
4038 foreach (string typeName, compatibleTypesInIsolation)
4039 {
4040 VuoCompilerType *type = allTypes[typeName];
4041 if (type && (!limitToNodeSet || (loadedTypesForNodeSet[nodeSetName].find(type) != loadedTypesForNodeSet[nodeSetName].end())) &&
4042 (! VuoGenericType::isGenericTypeName(typeName)) &&
4043 // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
4044 (typeName != "VuoUrl" && typeName != "VuoList_VuoUrl"
4045 // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
4046 && typeName != "VuoInteraction" && typeName != "VuoList_VuoInteraction"
4047 && typeName != "VuoInteractionType" && typeName != "VuoList_VuoInteractionType"
4048 && typeName != "VuoUuid" && typeName != "VuoList_VuoUuid"
4049 // Hide deprecated types.
4050 && typeName != "VuoIconPosition" && typeName != "VuoList_VuoIconPosition"
4051 && typeName != "VuoMesh" && typeName != "VuoList_VuoMesh"
4052 && typeName != "VuoWindowProperty" && typeName != "VuoList_VuoWindowProperty"
4053 && typeName != "VuoWindowReference" && typeName != "VuoList_VuoWindowReference"))
4054 compatibleTypesForNodeSetDisplay.append(type);
4055 }
4056
4057 if (!compatibleTypesForNodeSetDisplay.isEmpty())
4058 {
4059 QMenu *contextMenuNodeSetTypes = NULL;
4060 QList<QAction *> actionsToAddToNodeSetSubmenu;
4061 bool enabledContentAdded = false;
4062
4063 // Populate the "Specialize Type" submenu for the target node set.
4064 if (!nodeSetName.empty())
4065 {
4066 contextMenuNodeSetTypes = new QMenu(menu);
4067 contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
4068 contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
4069 contextMenuNodeSetTypes->setToolTipsVisible(true);
4070 actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
4071 }
4072
4073 foreach (VuoCompilerType *type, compatibleTypesForNodeSetDisplay)
4074 {
4075 string typeName = type->getBase()->getModuleKey();
4076 QList<QVariant> portAndSpecializedType;
4077 portAndSpecializedType.append(QVariant::fromValue(genericPort));
4078 portAndSpecializedType.append(typeName.c_str());
4079
4080 QAction *specializeAction;
4081 QString typeTitle = formatTypeNameForDisplay(type->getBase());
4082
4083 // Case: Adding to the node set submenu
4084 if (!nodeSetName.empty())
4085 {
4086 specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
4087 actionsToAddToNodeSetSubmenu.append(specializeAction);
4088 }
4089
4090 // Case: Adding to the top-level action list
4091 else
4092 {
4093 specializeAction = new QAction(typeTitle, menu);
4094 actionsToAddToMenu.append(specializeAction);
4095 }
4096
4097 specializeAction->setData(QVariant(portAndSpecializedType));
4098 specializeAction->setCheckable(true);
4099 specializeAction->setChecked(genericPort && genericPort->getDataType() && (genericPort->getDataType()->getModuleKey() == type->getBase()->getModuleKey()));
4100
4101 if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
4102 {
4103 specializeAction->setEnabled(false);
4104 //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
4105 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."));
4106 }
4107 else
4108 enabledContentAdded = true;
4109 }
4110
4111 if (contextMenuNodeSetTypes)
4112 {
4113 addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
4114
4115 if (!enabledContentAdded)
4116 contextMenuNodeSetTypes->setEnabled(false);
4117 }
4118 }
4119
4120 QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
4121 return actionListWithPromotions;
4122}
4123
4128QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
4129{
4130 QList<QAction *> modifiedActionList;
4131 foreach (QAction *action, actionList)
4132 {
4133 if (action->menu() && (action->menu()->actions().size() == 1))
4134 {
4135 QAction *singleSubaction = action->menu()->actions().first();
4136 action->menu()->removeAction(singleSubaction);
4137 modifiedActionList.append(singleSubaction);
4138 }
4139 else
4140 modifiedActionList.append(action);
4141 }
4142
4143 return modifiedActionList;
4144}
4145
4149void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
4150{
4151 std::sort(actionList.begin(), actionList.end(), nodeSetMenuActionLessThan);
4152 foreach (QAction *action, actionList)
4153 menu->addAction(action);
4154}
4155
4159void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4160{
4161 if (!now)
4162 return;
4163
4165 if (activeWindow)
4166 emit compositionOnTop(activeWindow->getComposition() == this);
4167}
4168
4172void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4173{
4174 if (ignoreApplicationStateChangeEvents)
4175 return;
4176
4178 if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4179 emit applicationActive(active);
4180}
4181
4190QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4191{
4192 return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4193}
4194
4199{
4200 QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4201 return dynamic_cast<VuoRendererNode *>(item);
4202}
4203
4216QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4217 targetComponentType targetType,
4218 bool limitPortCollisionRange)
4219{
4220 // Determine which types of components to filter out of search results.
4221 bool ignoreCables;
4222 bool ignoreNodes;
4223 bool ignorePorts;
4224 bool ignoreComments;
4225
4226 switch(targetType)
4227 {
4228 case VuoEditorComposition::targetTypePort:
4229 {
4230 ignoreCables = true;
4231 ignoreNodes = true;
4232 ignorePorts = false;
4233 ignoreComments = true;
4234 break;
4235 }
4236 case VuoEditorComposition::targetTypeNodeHeader:
4237 {
4238 ignoreCables = true;
4239 ignoreNodes = true;
4240 ignorePorts = true;
4241 ignoreComments = true;
4242 break;
4243 }
4244 default:
4245 {
4246 ignoreCables = false;
4247 ignoreNodes = false;
4248 ignorePorts = false;
4249 ignoreComments = false;
4250 break;
4251 }
4252 }
4253
4254 // The topmost item under the cursor is not necessarily the one we will return
4255 // (e.g., if the cursor is positioned directly over a node but also within range
4256 // of one of that node's ports), but it will factor in to the decision.
4257 QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4258 if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4259 topmostItemUnderCursor = NULL;
4260
4261 for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4262 {
4263 QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4264 QList<QGraphicsItem *> itemsInRange = items(searchRect);
4265 for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4266 {
4267 if (!(*i)->isEnabled())
4268 continue;
4269
4270 // Check whether we have located an unobscured "Make List" drawer drag handle.
4271 if (! ignoreNodes)
4272 {
4273 VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4274 bool makeListDragHandle =
4275 (makeListDrawer &&
4276 makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4277 if (makeListDragHandle &&
4278 ((! topmostItemUnderCursor) ||
4279 (topmostItemUnderCursor == makeListDrawer) ||
4280 (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4281 {
4282 return makeListDrawer;
4283 }
4284 }
4285
4286 // Check whether we have located an unobscured port.
4287 // Hovering within range of a port takes precedence over hovering
4288 // directly over that port's parent node.
4289 if (! ignorePorts)
4290 {
4291 VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4292 if (port &&
4293 ((! topmostItemUnderCursor) ||
4294 (topmostItemUnderCursor == port) ||
4295 (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4296 (topmostItemUnderCursor->zValue() < port->zValue()))
4297 &&
4298 ((! limitPortCollisionRange) ||
4299 port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4300 &&
4301 ! port->getFunctionPort())
4302 {
4303 return port;
4304 }
4305 }
4306
4307 // Check whether we have located an unobscured cable.
4308 if (! ignoreCables)
4309 {
4310 VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4311 if (cable &&
4312 ((! topmostItemUnderCursor) ||
4313 (topmostItemUnderCursor == cable) ||
4314 (topmostItemUnderCursor->zValue() < cable->zValue())))
4315 {
4316 return cable;
4317 }
4318 }
4319
4320 // Check whether we have located an unobscured comment.
4321 if (! ignoreComments)
4322 {
4323 VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4324 if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4325 comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4326
4327 if (comment &&
4328 ((! topmostItemUnderCursor) ||
4329 (topmostItemUnderCursor == (*i)) ||
4330 (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4331 {
4332 return comment;
4333 }
4334 }
4335
4336 // Check whether we have located an unobscured node header.
4337 if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4338 {
4339 VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4340 if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4341 {
4342 QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4343 headerRect = node->mapToScene(headerRect).boundingRect();
4344 if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4345 return node;
4346 }
4347 }
4348 }
4349 }
4350
4351 // Having failed to locate any other relevant components within range, return the node
4352 // directly under the cursor, if applicable.
4353 if (! ignoreNodes)
4354 {
4355 if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4356 return topmostItemUnderCursor;
4357
4358 // It is possible that the item under the cursor is a port, but that it didn't meet
4359 // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4360 if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4361 return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4362 }
4363
4364 return NULL;
4365}
4366
4377{
4378 if (! cableInProgress)
4379 return NULL;
4380
4381 VuoPort *fromPort = cableInProgress->getFromPort();
4382 VuoPort *toPort = cableInProgress->getToPort();
4383 VuoPort *fixedPort = (fromPort? fromPort: toPort);
4384
4385 if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4386 return NULL;
4387
4388 return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4389}
4390
4396{
4397 // Start with the first input or output port (other than the refresh port).
4398 vector<VuoRendererPort *> portList;
4399 int firstPortIndex;
4400
4401 if (inputPort)
4402 {
4403 portList = node->getInputPorts();
4405 }
4406 else
4407 {
4408 portList = node->getOutputPorts();
4410 }
4411
4412 VuoRendererPort *targetPort = NULL;
4413 if (portList.size() > firstPortIndex)
4414 {
4415 targetPort = portList[firstPortIndex];
4416
4417 // If the first input port has a wall,
4418 // keep moving down until we find one without a wall.
4419 // (Unless they all have walls, in which case stick with the first.)
4420 VuoRendererPort *firstPortWithoutWall = nullptr;
4421 for (int i = firstPortIndex; i < portList.size(); ++i)
4422 {
4423 if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4424 {
4425 firstPortWithoutWall = portList[i];
4426 break;
4427 }
4428 }
4429 if (firstPortWithoutWall)
4430 targetPort = firstPortWithoutWall;
4431
4432 // If the first input port has a drawer attached to it,
4433 // instead select the first input port of the drawer.
4434 // (Unless the drawer has no input ports, in which case don't select any port.)
4435 VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4436 if (drawer)
4437 {
4438 portList = drawer->getInputPorts();
4439 if (portList.size() > firstPortIndex)
4440 targetPort = portList[firstPortIndex];
4441 else
4442 targetPort = NULL;
4443 }
4444
4445 // If the selected input port has a collapsed type-converter node attached to it,
4446 // instead select the type-converter node's input port.
4447 VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4448 if (typecast)
4449 targetPort = typecast->getChildPort();
4450 }
4451
4452 return targetPort;
4453}
4454
4460{
4461 QRectF boundingRect;
4462 foreach (QGraphicsItem *item, items())
4463 {
4464 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4465 if (! (rc && rc->getBase()->isPublished()))
4466 boundingRect |= item->sceneBoundingRect();
4467 }
4468
4469 return boundingRect;
4470}
4471
4477{
4478 QRectF boundingRect;
4479
4480 foreach (QGraphicsItem *item, selectedItems())
4481 {
4482 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4483 if (! (rc && rc->getBase()->isPublished()))
4484 boundingRect |= item->sceneBoundingRect();
4485 }
4486
4487 return boundingRect;
4488}
4489
4495{
4496 QRectF boundingRect;
4497
4498 foreach (QGraphicsItem *item, selectedItems())
4499 {
4500 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4501 if (! (rc && rc->getBase()->isPublished()))
4502 boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4503
4504 // Include attached drawers.
4505 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4506 if (rn)
4507 {
4508 foreach (VuoPort *port, rn->getBase()->getInputPorts())
4509 {
4510 VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4511 if (drawer)
4512 boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4513 }
4514 }
4515 }
4516
4517 return boundingRect;
4518}
4519
4524void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4525{
4527 if (!port)
4528 return;
4529
4530 updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4531}
4532
4537void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4538{
4539 // Internal ports
4540 if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4541 {
4542 port->getBase()->getRenderer()->setConstant(newValue);
4543
4544 if (updateInRunningComposition)
4546 }
4547
4548 // Published ports
4549 else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4550 {
4551 dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4552
4553 if (updateInRunningComposition)
4555 }
4556}
4557
4563{
4566}
4567
4576{
4577
4578 // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4579 if (!errorMarkingUpdatesEnabled)
4580 return;
4581
4582 errorMarkingUpdatesEnabled = false;
4583
4584 // Remove any error annotations from the previous call to this function.
4585 if (errorMark)
4586 {
4587 errorMark->removeFromScene();
4588 errorMark = NULL;
4589 }
4590
4592
4593 // Check for errors.
4594
4595 VuoCompilerIssues *issues = new VuoCompilerIssues();
4596 VuoCompilerCable *potentialCable = NULL;
4597 try
4598 {
4599 set<VuoCompilerCable *> potentialCables;
4600
4601 if (targetPort && cableInProgress)
4602 {
4603 VuoNode *fromNode;
4604 VuoPort *fromPort;
4605 VuoNode *toNode;
4606 VuoPort *toPort;
4607 if (cableInProgress->getFromNode())
4608 {
4609 fromNode = cableInProgress->getFromNode();
4610 fromPort = cableInProgress->getFromPort();
4611 toNode = targetPort->getUnderlyingParentNode()->getBase();
4612 toPort = targetPort->getBase();
4613 }
4614 else
4615 {
4616 fromNode = targetPort->getUnderlyingParentNode()->getBase();
4617 fromPort = targetPort->getBase();
4618 toNode = cableInProgress->getToNode();
4619 toPort = cableInProgress->getToPort();
4620 }
4621 potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4622 potentialCable->getBase()->setFrom(fromNode, fromPort);
4623 potentialCable->getBase()->setTo(toNode, toPort);
4624 potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4625 cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4626
4627 fromPort->removeConnectedCable(potentialCable->getBase());
4628 toPort->removeConnectedCable(potentialCable->getBase());
4629 potentialCables.insert(potentialCable);
4630 }
4631
4632 getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4633 }
4634 catch (const VuoCompilerException &e)
4635 {
4636 }
4637
4638 if (! issues->isEmpty())
4639 {
4640 VUserLog("%s: Showing error popover: %s",
4641 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4642 issues->getShortDescription(false).c_str());
4643
4644 this->errorMark = new VuoErrorMark();
4645
4646 foreach (VuoCompilerIssue issue, issues->getList())
4647 {
4648 set<VuoRendererNode *> nodesToMark;
4649 set<VuoRendererCable *> cablesToMark;
4650
4651 set<VuoNode *> problemNodes = issue.getNodes();
4652 foreach (VuoNode *node, problemNodes)
4653 if (node->hasRenderer())
4654 nodesToMark.insert(node->getRenderer());
4655
4656 set<VuoCable *> problemCables = issue.getCables();
4657 foreach (VuoCable *cable, problemCables)
4658 {
4659 VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4660 if (cableToMark->hasRenderer())
4661 cablesToMark.insert(cableToMark->getRenderer());
4662 }
4663
4665 errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4666
4667 VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4668 errorPopovers.insert(errorPopover);
4671
4672 QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4673
4674 // Place the popover near an appropriate nearby node involved in the feedback loop.
4675 VuoRendererNode *nearbyNode = NULL;
4676 if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4677 {
4678 nearbyNode = targetPort->getRenderedParentNode();
4679 }
4680 else if (! nodesToMark.empty())
4681 {
4682 VuoRendererNode *topmostVisibleNode = NULL;
4683 qreal topY = viewportRect.bottom();
4684 foreach (VuoRendererNode *node, nodesToMark)
4685 {
4686 if (node->getProxyNode())
4687 node = node->getProxyNode();
4688
4689 QPointF scenePos = node->scenePos();
4690 if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4691 {
4692 topmostVisibleNode = node;
4693 topY = scenePos.y();
4694 }
4695 }
4696
4697 if (topmostVisibleNode)
4698 nearbyNode = topmostVisibleNode;
4699 else
4700 {
4701 VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4702 nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4703 }
4704 }
4705 else
4706 {
4707 VUserLog("Warning: no nearby node (no marked nodes).");
4708 }
4709
4710 // If no nodes are known to be involved in the feedback loop, display the popover
4711 // in the center of the viewport.
4712 const QPoint offsetFromNode(0,10);
4713 QPoint popoverTopLeftInScene = (nearbyNode?
4714 (nearbyNode->scenePos().toPoint() +
4715 nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4716 offsetFromNode) :
4717 QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4718 viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4719
4720 // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4721 // of the viewport closest to the feedback loop.
4722 const int margin = 5;
4723 popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4724 fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4725
4726 popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4727 fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4728
4729 QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4730 QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4731
4732 errorPopover->move(popoverTopLeftGlobal);
4733 errorPopover->show();
4734 emit popoverDetached();
4735 }
4736
4737 // Add error annotations to the composition.
4738 addItem(errorMark);
4739 }
4740 delete issues;
4741 delete potentialCable;
4742
4743 errorMarkingUpdatesEnabled = true;
4744}
4745
4749bool VuoEditorComposition::hasFeedbackErrors(void)
4750{
4751 return this->errorMark;
4752}
4753
4758{
4759 if (hasFeedbackErrors())
4760 this->errorMark->updateErrorMarkPath();
4761}
4762
4768void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4769{
4770 try
4771 {
4772 emit buildStarted();
4773
4774 if (! dependenciesUninstalled.empty())
4775 {
4776 vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4777 string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4778 throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4779 }
4780
4781 delete runningComposition;
4782 runningComposition = NULL;
4783 runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4784
4785 runningCompositionActiveDriver = getDriverForActiveProtocol();
4786 if (runningCompositionActiveDriver)
4787 runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4788
4789 string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4790 string dir, file, ext;
4791 VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4792 linkedCompositionPath = dir + file + ".dylib";
4793
4794 compiler->setShouldPotentiallyShowSplashWindow(false);
4795
4796 VuoCompilerIssues *issues = new VuoCompilerIssues();
4797 compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4798 compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries.get());
4799 delete issues;
4800
4801 remove(compiledCompositionPath.c_str());
4802
4803 emit buildFinished("");
4804 }
4805 catch (VuoException &e)
4806 {
4807 delete runningComposition;
4808 runningComposition = NULL;
4809
4810 emit buildFinished(e.what());
4811 throw;
4812 }
4813}
4814
4820bool VuoEditorComposition::isRunningThreadUnsafe(void)
4821{
4822 return runner != NULL && ! stopRequested && ! runner->isStopped();
4823}
4824
4831{
4832 __block bool running;
4833 dispatch_sync(runCompositionQueue, ^{
4834 running = isRunningThreadUnsafe();
4835 });
4836 return running;
4837}
4838
4844void VuoEditorComposition::run(string compositionSnapshot)
4845{
4846 VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4847
4848 // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4849 subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4850
4851 // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4852 subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4853 {
4854 compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4855 });
4856
4857 // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4858 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4859 {
4860 if (matchingComposition->showEventsMode)
4861 matchingComposition->beginDisplayingActivity();
4862 });
4863
4864 stopRequested = false;
4865 dispatch_async(runCompositionQueue, ^{
4866 try
4867 {
4868 runningCompositionLibraries = std::make_shared<VuoRunningCompositionLibraries>();
4869
4870 buildComposition(compositionSnapshot);
4871
4872 string compositionLoaderPath = compiler->getCompositionLoaderPath();
4873 string compositionSourceDir = getBase()->getDirectory();
4874
4875 runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4876 runner->setDelegate(this);
4878 runner->startPaused();
4879 pid_t pid = runner->getCompositionPid();
4880
4881 // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4882 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4883 {
4884 if (matchingComposition->showEventsMode)
4885 this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4886
4887 dispatch_sync(activePortPopoversQueue, ^{
4888 for (auto i : matchingComposition->activePortPopovers)
4889 {
4890 string portID = i.first;
4891 updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4892 }
4893 });
4894 });
4895
4896 runner->unpause();
4897
4898 // Focus the composition's windows (if any).
4900 }
4901 catch (...) { }
4902 });
4903}
4904
4911{
4912 stopRequested = true;
4913 dispatch_async(runCompositionQueue, ^{
4914 if (runner && ! runner->isStopped())
4915 {
4916 runner->stop();
4917 runner->waitUntilStopped();
4918 }
4919 delete runner;
4920 runner = NULL;
4921
4922 linkedCompositionPath = "";
4923
4924 runningCompositionLibraries = nullptr; // release shared_ptr
4925
4926 delete runningComposition;
4927 runningComposition = NULL;
4928
4929 emit stopFinished();
4930 });
4931
4932 VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4933
4934 // Tell this composition and all subcompositions opened from it to stop display live debug info.
4935 subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4936 {
4937 if (matchingComposition->showEventsMode)
4938 matchingComposition->stopDisplayingActivity();
4939
4940 dispatch_sync(activePortPopoversQueue, ^{
4941 for (auto i : matchingComposition->activePortPopovers)
4942 {
4943 VuoPortPopover *popover = i.second;
4944 popover->setCompositionRunning(false);
4945 }
4946 });
4947 });
4948}
4949
4961void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4962 VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4963{
4964 if (! diffInfo)
4965 diffInfo = new VuoCompilerCompositionDiff();
4966
4967 dispatch_async(runCompositionQueue, ^{
4968 if (isRunningThreadUnsafe())
4969 {
4970 try
4971 {
4972 foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4973 {
4974 runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4975 }
4976
4977 string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4978 VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4979 if (previouslyActiveDriver)
4980 {
4981 VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4982 previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4983 oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4984 }
4985
4986 buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4987
4988 string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4989 runner->replaceComposition(linkedCompositionPath, compositionDiff);
4990 }
4991 catch (exception &e)
4992 {
4993 VUserLog("Composition stopped itself: %s", e.what());
4995 }
4996 catch (...)
4997 {
4998 VUserLog("Composition stopped itself.");
5000 }
5001 }
5002 else
5003 {
5004 dispatch_async(dispatch_get_main_queue(), ^{
5006 });
5007 }
5008
5009 delete diffInfo;
5010 });
5011}
5012
5018{
5019 void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
5020 {
5021 compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
5022 };
5023 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
5024}
5025
5031{
5032 string constant;
5033 identifierCache->doForPortWithIdentifier(runningPortID, [&constant](VuoPort *port) {
5034 if (port->hasCompiler() && port->hasRenderer())
5035 constant = port->getRenderer()->getConstantAsString();
5036 });
5037
5038 // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
5039 void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
5040 {
5041 if (this == topLevelComposition)
5042 {
5043 dispatch_async(runCompositionQueue, ^{
5044 if (isRunningThreadUnsafe())
5045 {
5046 json_object *constantObject = json_tokener_parse(constant.c_str());
5047 runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
5048 }
5049 });
5050 }
5051 };
5052 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
5053
5054 // If this is a subcomposition, live-update all other top-level compositions that contain it.
5055 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
5056 {
5057 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
5058 {
5059 topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
5060 });
5061 });
5062}
5063
5069{
5071 if (!(port && port->hasCompiler()))
5072 return;
5073
5074 string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
5076}
5077
5082{
5083 string runningPortIdentifier = identifierCache->getIdentifierForPort(port->getBase());
5084 if (runningPortIdentifier.empty())
5085 return;
5086
5087 // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
5088 void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
5089 {
5090 if (this == topLevelComposition)
5091 {
5092 dispatch_async(runCompositionQueue, ^{
5093 if (isRunningThreadUnsafe())
5094 {
5095 json_object *constantObject = json_tokener_parse(constant.c_str());
5096 runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
5097 }
5098 });
5099 }
5100 };
5101 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
5102
5103 // If this is a subcomposition, live-update all other top-level compositions that contain it.
5104 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
5105 {
5106 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
5107 {
5108 topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
5109 });
5110 });
5111}
5112
5117void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
5118{
5119 dispatch_async(runCompositionQueue, ^{
5120 if (isRunningThreadUnsafe())
5121 {
5122 json_object *constantObject = json_tokener_parse(constant.c_str());
5123 set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
5124 foreach (string subcompositionIdentifier, subcompositionIdentifiers)
5125 {
5126 runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
5127 }
5128 }
5129 });
5130}
5131
5136{
5137 dispatch_async(runCompositionQueue, ^{
5138 if (isRunningThreadUnsafe())
5139 {
5140 VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
5141 if (publishedPort)
5142 {
5143 json_object *constantObject = json_tokener_parse(constant.c_str());
5144 map<VuoRunner::Port *, json_object *> m;
5145 m[publishedPort] = constantObject;
5146 runner->setPublishedInputPortValues(m);
5147 json_object_put(constantObject);
5148 }
5149 }
5150 });
5151}
5152
5153
5158{
5159 return contextMenuDeleteSelected;
5160}
5161
5166{
5167 // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5168 // For now, this recreates the context menu rather than accessing a data member.
5169 QMenu *contextMenuTints = new QMenu(parent);
5170 contextMenuTints->setSeparatorsCollapsible(false);
5171 contextMenuTints->setTitle(tr("Tint"));
5172 foreach (QAction *tintAction, contextMenuTintActions)
5173 contextMenuTints->addAction(tintAction);
5174 contextMenuTints->insertSeparator(contextMenuTintActions.last());
5175
5176 return contextMenuTints;
5177}
5178
5182void VuoEditorComposition::expandChangeNodeMenu()
5183{
5184 QAction *sender = (QAction *)QObject::sender();
5185 VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
5186
5187 // If the menu hasn't been expanded previously, expand it enough now to fill
5188 // the available vertical screenspace.
5189 int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5190 if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5191 {
5192 int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5193 int verticalSpacePerItem = 21; // menu row height in pixels
5194 // Estimate the number of matches that will fit within the screen without scrolling:
5195 // -1 to account for a possible extra "More…" row;
5196 // -1 wiggle room to match observations
5197 int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5198
5199 populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5200 }
5201
5202 // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5203 else
5204 populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5205
5206 contextMenuChangeNode->exec();
5207}
5208
5215{
5216 menu->clear();
5217
5218 if (!node)
5219 return;
5220
5221 map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5222 vector<VuoCompilerNodeClass *> loadedNodeClasses;
5223 for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5224 loadedNodeClasses.push_back(i->second);
5225 VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5226
5227 vector<string> bestMatches;
5228 map<string, double> matchScores;
5229 matchScores[""] = 0;
5230
5231 int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5232 for (int i = 0; i < targetMatchCount; ++i)
5233 bestMatches.push_back("");
5234
5235 // Maintain a priority queue with the @c targetMatchCount best matches.
5236 bool overflow = false;
5237
5238 VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5239 string originalGenericNodeClassName;
5240 if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5241 originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5242 else
5243 originalGenericNodeClassName = nodeClass->getClassName();
5244
5245 foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5246 {
5247 string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5248 if (loadedNodeClassName == originalGenericNodeClassName)
5249 continue;
5250
5251 bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5252 double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5253 int highestIndexWithCompetitiveScore = -1;
5254 for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5255 if (matchScore <= matchScores[bestMatches[i] ])
5256 highestIndexWithCompetitiveScore = i;
5257
5258 if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5259 {
5260 if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5261 overflow = true;
5262
5263 for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5264 bestMatches[j+1] = bestMatches[j];
5265
5266 bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5267 matchScores[loadedNodeClassName] = matchScore;
5268 }
5269 }
5270
5271 for (int i = 0; i < targetMatchCount; ++i)
5272 {
5273 if (matchScores[bestMatches[i] ] > 0)
5274 {
5275 // Disambiguate between identical node titles using node class names.
5276 QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5277 if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5278 matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5279
5280 QAction *changeAction = menu->addAction(matchDisplayText);
5281
5282 QList<QVariant> currentNodeAndNewClass;
5283 currentNodeAndNewClass.append(QVariant::fromValue(node));
5284 currentNodeAndNewClass.append(bestMatches[i].c_str());
5285 changeAction->setData(QVariant(currentNodeAndNewClass));
5286 connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5287 }
5288 }
5289
5290 if (overflow)
5291 {
5292 //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5293 QAction *showMoreAction = menu->addAction(tr("More…"));
5294 showMoreAction->setData(QVariant::fromValue(node));
5295 connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5296 }
5297}
5298
5303bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5304{
5305 // Inventory required input port types (connected data inputs) in the node to be replaced.
5306 map<string, int> requiredInputs;
5307 bool inputEventSourceRequired = false;
5308 foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5309 {
5310 bool hasDrawerWithNoIncomingCables = false;
5311 bool hasDrawerWithNoIncomingDataCables = false;
5312
5313 if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5314 {
5315 VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5316 if (inputDrawer)
5317 {
5318 hasDrawerWithNoIncomingCables = true;
5319 hasDrawerWithNoIncomingDataCables = true;
5320 vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5321 foreach (VuoRendererPort *childPort, childPorts)
5322 {
5323 if (childPort->getBase()->getConnectedCables(true).size() > 0)
5324 hasDrawerWithNoIncomingCables = false;
5325 if (childPort->effectivelyHasConnectedDataCable(true))
5326 hasDrawerWithNoIncomingDataCables = false;
5327 }
5328 }
5329
5330 string typeKey = inputPort->getDataType()->getModuleKey();
5331 if (!hasDrawerWithNoIncomingDataCables)
5332 {
5333 // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5334 // Accommodate generic types.
5336 return false;
5337
5338 requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5339 }
5340 }
5341
5342 bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5343 if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5344 inputEventSourceRequired = true;
5345 }
5346
5347 // Inventory required output port types (connected data outputs) in the node to be replaced.
5348 map<string, int> requiredOutputs;
5349 bool outputEventSourceRequired = false;
5350 foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5351 {
5352 if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5353 {
5354 string typeKey = outputPort->getDataType()->getModuleKey();
5355
5356 // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5357 // Accommodate generic types.
5359 return false;
5360
5361 requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5362 }
5363
5364 if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5365 outputEventSourceRequired = true;
5366 }
5367
5368 // Inventory available input port types in the candidate replacement node.
5369 bool inputEventSourceAvailable = false;
5370 map<string, int> availableInputs;
5371 foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5372 {
5373 VuoType *dataType = (inputPortClass->hasCompiler()?
5374 static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5375 if (dataType)
5376 {
5377 string typeKey = dataType->getModuleKey();
5378 availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5379 }
5380 }
5381
5383 inputEventSourceAvailable = true;
5384
5385 // Inventory available output port types in the candidate replacement node.
5386 bool outputEventSourceAvailable = false;
5387 map<string, int> availableOutputs;
5388 foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5389 {
5390 VuoType *dataType = (outputPortClass->hasCompiler()?
5391 static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5392 if (dataType)
5393 {
5394 string typeKey = dataType->getModuleKey();
5395 availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5396 }
5397 }
5398
5400 outputEventSourceAvailable = true;
5401
5402 // Check whether the candidate replacement node meets input data requirements.
5403 for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5404 {
5405 string typeKey = it->first;
5406 int typeRequiredCount = it->second;
5407 if (availableInputs[typeKey] < typeRequiredCount)
5408 return false;
5409 }
5410
5411 // Check whether the candidate replacement node meets output data requirements.
5412 for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5413 {
5414 string typeKey = it->first;
5415 int typeRequiredCount = it->second;
5416 if (availableOutputs[typeKey] < typeRequiredCount)
5417 return false;
5418 }
5419
5420 if (inputEventSourceRequired && !inputEventSourceAvailable)
5421 return false;
5422
5423 if (outputEventSourceRequired && !outputEventSourceAvailable)
5424 return false;
5425
5426 return true;
5427}
5428
5433bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5434{
5435 // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5437 VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5438
5439 if (!specializedNodeClass)
5440 return false;
5441
5442 VuoPortClass *portClass = port->getBase()->getClass();
5443 VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5444 if (!originalGenericType)
5445 return false;
5446
5447 // If this port belongs to an attachment connected to a port that is not revertible, then
5448 // this port cannot be reverted, either.
5450 if (attachment)
5451 {
5452 VuoPort *hostPort = attachment->getUnderlyingHostPort();
5453 if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5454 return false;
5455 }
5456
5457 return true;
5458}
5459
5478VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5479{
5480 string publishedPortName = ((! name.empty())?
5481 name :
5482 VuoRendererPort::sanitizePortName(port->getRenderer()->getPortNameToRenderWhenDisplayed().c_str()).toUtf8().constData());
5483 bool isPublishedInput = port->getRenderer()->getInput();
5484 VuoType *portType = port->getRenderer()->getDataType();
5485 VuoPublishedPort *publishedPort = NULL;
5486
5487 // If merging is enabled:
5488 // Check whether this composition has a pre-existing externally visible published port
5489 // that has the requested name and type and that can accommodate the newly published internal port.
5490 // If so, add this internal port as a connected port for the existing alias.
5491 bool performedMerge = false;
5492 if (attemptMerge)
5493 {
5494 publishedPort = (isPublishedInput ?
5495 getBase()->getPublishedInputPortWithName(publishedPortName) :
5496 getBase()->getPublishedOutputPortWithName(publishedPortName));
5497
5498 if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5499 {
5500 if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5501 {
5502 VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5504 publishedInputPort->getInitialValue(),
5505 false);
5506 }
5507
5508 performedMerge = true;
5509 }
5510 }
5511
5512
5513 // Otherwise, create a new externally visible published port with a unique name derived from
5514 // the specified name, containing the current port as its lone connected internal port.
5515 if (! performedMerge)
5516 {
5517 publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5518 if (isPublishedInput && type)
5519 {
5520 VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5521 publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5522 }
5523 }
5524
5525 addPublishedPort(publishedPort, isPublishedInput);
5526
5527 VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5528 dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5529 createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5530
5531 VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5532
5533 if (! existingPublishedCable)
5534 {
5535 VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5536 addCable(publishedCable);
5537 }
5538
5539 if (mergePerformed != NULL)
5540 *mergePerformed = performedMerge;
5541
5542 return rendererPublishedPort;
5543}
5544
5549VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5550{
5551 VuoCable *publishedCable = NULL;
5552 bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5553
5554 if (creatingPublishedInputCable)
5555 {
5556 // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5557 VuoPort *fromPort = externalPort;
5558 VuoNode *fromNode = this->publishedInputNode;
5559
5560 VuoPort *toPort = internalPort;
5561 VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5562
5563 publishedCable = (new VuoCompilerCable(NULL,
5564 NULL,
5565 toNode->getCompiler(),
5566 (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5567 publishedCable->setFrom(fromNode, fromPort);
5568 }
5569
5570 else
5571 {
5572 // If creating a published output cable, it will need to have an associated VuoCompilerCable
5573 // even though we don't currently construct a VuoCompilerNode for the published output node.
5574 VuoPort *fromPort = internalPort;
5575 VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5576
5577 VuoPort *toPort = externalPort;
5578 VuoNode *toNode = this->publishedOutputNode;
5579
5580 publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5581 (VuoCompilerPort *)(fromPort->getCompiler()),
5582 NULL,
5583 NULL))->getBase();
5584 publishedCable->setTo(toNode, toPort);
5585 }
5586
5587 if (forceEventOnlyPublication)
5588 publishedCable->getCompiler()->setAlwaysEventOnly(true);
5589
5590 return publishedCable;
5591}
5592
5605{
5606 vector<VuoPublishedPort *> publishedPortsToAdd;
5607 map<VuoPublishedPort *, string> publishedPortsToRename;
5608
5609 // Remove the previously active protocol.
5610 VuoProtocol *previousActiveProtocol = this->activeProtocol;
5611 bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5612
5613 bool portChangesMadeDuringProtocolRemoval = false;
5614 if (removingPreviousProtocol)
5615 portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5616
5617 if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5618 {
5619 VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5620 useUndoStack = true;
5621 }
5622
5623 // Add the newly active protocol.
5624 this->activeProtocol = protocol;
5625 if (!protocol)
5626 return;
5627
5628 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5629 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5630 {
5631 string portName = i->first;
5632 string portType = i->second;
5633
5634 bool compositionHadCompatiblePort = false;
5635 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5636 if (preexistingPublishedPort)
5637 {
5638 VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5639
5640 bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5641 (!preexistingType && (portType == "")));
5642 if (portTypesMatch)
5643 {
5644 compositionHadCompatiblePort = true;
5645 preexistingPublishedPort->setProtocolPort(true);
5646 }
5647 else
5648 {
5649 compositionHadCompatiblePort = false;
5650 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5651 }
5652 }
5653
5654 if (!compositionHadCompatiblePort)
5655 {
5656 VuoCompilerType *ctype = compiler->getType(portType);
5657 VuoType *type = (ctype? ctype->getBase() : NULL);
5659 publishedPort->setProtocolPort(true);
5660
5661 if (!useUndoStack)
5662 addPublishedPort(publishedPort, true);
5663 else
5664 publishedPortsToAdd.push_back(publishedPort);
5665
5667 }
5668 }
5669
5670 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5671 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5672 {
5673 string portName = i->first;
5674 string portType = i->second;
5675
5676 bool compositionHadCompatiblePort = false;
5677 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5678 if (preexistingPublishedPort)
5679 {
5680 VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5681 bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5682 (!preexistingType && (portType == "")));
5683 if (portTypesMatch)
5684 {
5685 compositionHadCompatiblePort = true;
5686 preexistingPublishedPort->setProtocolPort(true);
5687 }
5688 else
5689 {
5690 compositionHadCompatiblePort = false;
5691 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5692 }
5693 }
5694
5695 if (!compositionHadCompatiblePort)
5696 {
5697 VuoCompilerType *ctype = compiler->getType(portType);
5698 VuoType *type = (ctype? ctype->getBase() : NULL);
5700 publishedPort->setProtocolPort(true);
5701
5702 if (!useUndoStack)
5703 addPublishedPort(publishedPort, false);
5704 else
5705 publishedPortsToAdd.push_back(publishedPort);
5706
5707 createRendererForPublishedPortInComposition(publishedPort, false);
5708 }
5709 }
5710
5711 if (useUndoStack)
5712 {
5713 bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5714 if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5715 {
5716 set<VuoPublishedPort *> publishedPortsToRemove;
5717 bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5718 bool endUndoStackMacro = true;
5719 emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5720 }
5721 }
5722
5723 emit activeProtocolChanged();
5724}
5725
5733string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5734{
5735 string modifiedPortName = portName;
5736 if (modifiedPortName.length() > 0)
5737 modifiedPortName[0] = toupper(modifiedPortName[0]);
5738 modifiedPortName = "some" + modifiedPortName;
5739
5740 return modifiedPortName;
5741}
5742
5761{
5763
5764 set<VuoPublishedPort *> publishedPortsToRemove;
5765 map<VuoPublishedPort *, string> publishedPortsToRename;
5766
5767 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5768 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5769 {
5770 string portName = i->first;
5771 string portType = i->second;
5772
5773 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5774 if (preexistingPublishedPort)
5775 {
5776 bool portCompatibleAcrossProtocolTransition = false;
5777 if (replacementProtocol)
5778 {
5779 vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5780 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5781 {
5782 string replacementPortName = i->first;
5783 string replacementPortType = i->second;
5784
5785 if ((portName == replacementPortName) && (portType == replacementPortType))
5786 portCompatibleAcrossProtocolTransition = true;
5787 }
5788 }
5789
5790 if (preexistingPublishedPort->getConnectedCables(true).empty())
5791 publishedPortsToRemove.insert(preexistingPublishedPort);
5792 else if (!portCompatibleAcrossProtocolTransition)
5793 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5794 }
5795 }
5796
5797 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5798 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5799 {
5800 string portName = i->first;
5801 string portType = i->second;
5802
5803 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5804 if (preexistingPublishedPort)
5805 {
5806 bool portCompatibleAcrossProtocolTransition = false;
5807 if (replacementProtocol)
5808 {
5809 vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5810 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5811 {
5812 string replacementPortName = i->first;
5813 string replacementPortType = i->second;
5814
5815 if ((portName == replacementPortName) && (portType == replacementPortType))
5816 portCompatibleAcrossProtocolTransition = true;
5817 }
5818 }
5819
5820 if (preexistingPublishedPort->getConnectedCables(true).empty())
5821 publishedPortsToRemove.insert(preexistingPublishedPort);
5822 else if (!portCompatibleAcrossProtocolTransition)
5823 publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5824 }
5825 }
5826
5827 // If we are removing any ports, the composition will no longer be deemed to adhere to the
5828 // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5829 if (!publishedPortsToRemove.empty())
5830 publishedPortsToRename.clear();
5831
5832 bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5833 if (portChangesRequired)
5834 {
5835 vector<VuoPublishedPort *> publishedPortsToAdd;
5836 bool beginUndoStackMacro = true;
5837 bool endUndoStackMacro = !replacementProtocol;
5838 emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5839 }
5840
5841 emit activeProtocolChanged();
5842
5843 return portChangesRequired;
5844}
5845
5853{
5854 if ((activeProtocol != protocol) || !activeProtocol)
5855 return;
5856
5857 activeProtocol = NULL;
5858
5859 vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5860 for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5861 {
5862 string portName = i->first;
5863 string portType = i->second;
5864
5865 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5866 if (preexistingPublishedPort)
5867 preexistingPublishedPort->setProtocolPort(false);
5868 }
5869
5870 vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5871 for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5872 {
5873 string portName = i->first;
5874 string portType = i->second;
5875
5876 VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5877 if (preexistingPublishedPort)
5878 preexistingPublishedPort->setProtocolPort(false);
5879 }
5880
5881 emit activeProtocolChanged();
5882}
5883
5889{
5890 return activeProtocol;
5891}
5892
5898{
5899 if (!activeProtocol)
5900 return NULL;
5901
5902 return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5903}
5904
5908void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5909{
5910 VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5911
5912 identifierCache->addPublishedPortToCache(publishedPort);
5913
5914 if (shouldUpdateUi)
5915 emit publishedPortModified();
5916}
5917
5924int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5925{
5926 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5927 if (shouldUpdateUi && publishedPort->isProtocolPort())
5929
5930 int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5931
5932 if (shouldUpdateUi)
5933 emit publishedPortModified();
5934
5935 return removalResult;
5936}
5937
5943{
5944 // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5945 if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5947
5948 VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5949
5950 identifierCache->addPublishedPortToCache( static_cast<VuoPublishedPort *>(publishedPort->getBase()) );
5951
5952 emit publishedPortModified();
5953}
5954
5962void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5963{
5964 bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5965 VuoRendererPort *fixedPort = NULL;
5966
5967 if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5968 fixedPort = cable->getFromPort()->getRenderer();
5969
5970 else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5971 fixedPort = cable->getToPort()->getRenderer();
5972
5973 if (fixedPort)
5974 {
5975 highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5976 emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5977 }
5978}
5979
5985void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5986{
5987 auto types = compiler->getTypes();
5988
5989 QList<QGraphicsItem *> compositionComponents = items();
5990 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5991 {
5992 QGraphicsItem *compositionComponent = *i;
5993 VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5994 if (rn)
5995 {
5996 // Check for eligible internal input ports.
5997 vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5998 for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
5999 updateEligibilityHighlightingForPort((*inputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
6000
6001 // Check for eligible internal output ports.
6002 vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
6003 for(vector<VuoPort *>::iterator outputPort = outputPorts.begin(); outputPort != outputPorts.end(); ++outputPort)
6004 updateEligibilityHighlightingForPort((*outputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
6005 }
6006
6007 // Fade out cables that aren't relevant to the current cable drag.
6008 VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
6009 if (rc && rc != cable)
6010 {
6011 QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
6012 rc->setCacheMode(QGraphicsItem::NoCache);
6013 rc->updateGeometry();
6014
6015 VuoPort *otherCablePort = port->getInput()
6016 ? rc->getBase()->getFromPort()
6017 : rc->getBase()->getToPort();
6018
6019 VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(otherCablePort? otherCablePort->getRenderer() : NULL,
6020 port,
6021 !cable->effectivelyCarriesData(),
6022 types);
6023
6024 // Don't apply extra highlighting to compatible, already-connected cables.
6025 if (highlight == VuoRendererColors::standardHighlight)
6027
6028 rc->setEligibilityHighlight(highlight);
6029
6030 rc->setCacheMode(normalCacheMode);
6031 }
6032 }
6033
6034 // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
6035 for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
6036 updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(*i));
6037}
6038
6043void VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
6044 VuoRendererPort *fixedPort,
6045 bool eventOnlyConnection,
6046 map<string, VuoCompilerType *> &types)
6047{
6048 QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
6049 portToHighlight->setCacheMode(QGraphicsItem::NoCache);
6050
6051 portToHighlight->updateGeometry();
6052
6053 VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection, types);
6054
6055 portToHighlight->setEligibilityHighlight(highlight);
6056 VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
6057 if (typecastPortToHighlight)
6058 typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
6059
6060 portToHighlight->setCacheMode(normalCacheMode);
6061
6062 if (typecastPortToHighlight)
6063 updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection, types);
6064}
6065
6076VuoRendererColors::HighlightType VuoEditorComposition::getEligibilityHighlightingForPort(VuoRendererPort *portToHighlight, VuoRendererPort *fixedPort, bool eventOnlyConnection, map<string, VuoCompilerType *> &types)
6077{
6078 // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
6079 VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
6080 VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
6081
6082 VuoRendererPort *fromPort;
6083 VuoRendererPort *toPort;
6084 bool forwardConnection;
6085 if (fixedPort->getOutput())
6086 {
6087 fromPort = fixedPort;
6088 toPort = portToHighlight;
6089 forwardConnection = true;
6090 }
6091 else
6092 {
6093 fromPort = portToHighlight;
6094 toPort = fixedPort;
6095 forwardConnection = false;
6096 }
6097
6098 bool directConnectionPossible;
6099
6100 // Temporarily disallow direct cable connections between published inputs and published outputs.
6101 // @todo: Allow for https://b33p.net/kosada/node/7756 .
6102 if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
6103 directConnectionPossible = false;
6104 else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
6105 directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
6106 else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
6107 directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
6108 else // both ports are internal canvas ports
6109 directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
6110
6112 if (directConnectionPossible)
6114 else if (!findBridgingSolutions(fromPort, toPort, forwardConnection, types).empty())
6116 else if (fixedPort == portToHighlight)
6118 else
6120
6121 return highlight;
6122}
6123
6139bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6140 VuoRendererPort *toPort,
6141 bool eventOnlyConnection,
6142 bool forwardConnection)
6143{
6144 VuoRendererPort *portToRespecialize = NULL;
6145 string respecializedTypeName = "";
6146
6147 return canConnectDirectlyWithRespecializationNondestructively(fromPort,
6148 toPort,
6149 eventOnlyConnection,
6150 forwardConnection,
6151 &portToRespecialize,
6152 respecializedTypeName);
6153}
6154
6165bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6166 VuoRendererPort *toPort,
6167 bool eventOnlyConnection,
6168 bool forwardConnection,
6169 VuoRendererPort **portToRespecialize,
6170 string &respecializedTypeName)
6171{
6172 *portToRespecialize = NULL;
6173 respecializedTypeName = "";
6174
6175 bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6176 toPort,
6177 eventOnlyConnection,
6178 forwardConnection,
6179 portToRespecialize,
6180 respecializedTypeName);
6181 if (!canConnectWithRespecialization)
6182 return false;
6183
6184 if (canConnectWithRespecialization && !portToRespecialize)
6185 return true;
6186
6187 bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6188 if (!nondestructive)
6189 {
6190 *portToRespecialize = NULL;
6191 respecializedTypeName = "";
6192 }
6193 return nondestructive;
6194}
6195
6201bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6202{
6203 map<VuoNode *, string> nodesToReplace;
6204 set<VuoCable *> cablesToDelete;
6205 createReplacementsToUnspecializePort(portToUnspecialize, false, nodesToReplace, cablesToDelete);
6206
6207 // Check whether unspecialization would disconnect any existing cables
6208 // (other than the cable that would normally be displaced by the new cable connection).
6209 if (cablesToDelete.empty())
6210 return true;
6211
6212 else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6213 return true;
6214
6215 return false;
6216}
6217
6237bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6238 VuoRendererPort *toPort,
6239 bool eventOnlyConnection,
6240 bool forwardConnection,
6241 VuoRendererPort **portToRespecialize,
6242 string &respecializedTypeName)
6243{
6244 // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6245
6246 *portToRespecialize = NULL;
6247 respecializedTypeName = "";
6248
6249 // // @todo https://b33p.net/kosada/node/10481 Necessary?
6250 if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6251 return true;
6252
6253 // // @todo https://b33p.net/kosada/node/10481 Necessary?
6254 if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6255 return true;
6256
6257 bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6258 bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6259
6260 if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6261 return false;
6262
6263 VuoType *currentFromDataType = fromPort->getDataType();
6264 VuoType *currentToDataType = toPort->getDataType();
6265
6266 if (!(currentFromDataType && currentToDataType))
6267 return false;
6268
6270 if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6271 return false;
6272
6273 VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6274 VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6275
6276 VuoGenericType *originalFromGenericType = NULL;
6277 if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6278 {
6280 VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6281 if (fromSpecializedNodeClass)
6282 {
6283 VuoPortClass *portClass = fromPort->getBase()->getClass();
6284 originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6285 }
6286 }
6287
6288 VuoGenericType *originalToGenericType = NULL;
6289 if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6290 {
6292 VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6293 if (toSpecializedNodeClass)
6294 {
6295 VuoPortClass *portClass = toPort->getBase()->getClass();
6296 originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6297 }
6298 }
6299
6300 // Determine whether the port at each endpoint is 1) generic, or
6301 // 2) specialized and currently revertible, or 3) effectively static.
6302 bool fromPortIsGeneric = currentFromGenericType;
6303 bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6304 bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6305
6306 bool toPortIsGeneric = currentToGenericType;
6307 bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6308 bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6309
6310 // Figure out which port to try to respecialize, and to what type.
6311 set<string> compatibleTypes;
6312 string specializedType = "";
6313 VuoRendererPort *portToTryToRespecialize = NULL;
6314
6315 // Case: One port static, one port specialized.
6316 if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6317 {
6318 VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6319 specializedType = staticPort->getDataType()->getModuleKey();
6320 portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6321 }
6322
6323 // Case: One port specialized, other port generic or specialized.
6324 else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6325 {
6326 VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6327 bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6328
6329 VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6330 bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6331
6332 // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6333 /*
6334 if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6335 {
6336 specializedType = dragDestination->getDataType()->getModuleKey();
6337 portToTryToRespecialize = dragSource;
6338 }
6339 else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6340 {
6341 specializedType = dragSource->getDataType()->getModuleKey();
6342 portToTryToRespecialize = dragDestination;
6343 }
6344 else
6345 */
6346
6347 if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6348 {
6349 specializedType = dragSource->getDataType()->getModuleKey();
6350 portToTryToRespecialize = dragDestination;
6351 }
6352 }
6353
6354 // @todo https://b33p.net/kosada/node/10481 Other cases.
6355 else
6356 return false;
6357
6358 if (portToTryToRespecialize)
6359 compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6360
6361 bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6362
6363 if (portsAreCompatible)
6364 {
6365 *portToRespecialize = portToTryToRespecialize;
6366 respecializedTypeName = specializedType;
6367 }
6368
6369 return portsAreCompatible;
6370}
6371
6378void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6379{
6380 VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6381 if (drawer)
6382 {
6384 foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6386 bestEligibility = VuoRendererColors::standardHighlight;
6388 && bestEligibility != VuoRendererColors::standardHighlight)
6389 bestEligibility = VuoRendererColors::subtleHighlight;
6390
6391 // If this drawer has no eligible ports, fade it out.
6392 {
6393 QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6394 drawer->setCacheMode(QGraphicsItem::NoCache);
6395 drawer->updateGeometry();
6396
6397 drawer->setEligibilityHighlight(bestEligibility);
6398
6399 drawer->setCacheMode(normalCacheMode);
6400 }
6401
6402 // Make sure the host port is repainted to take into account the eligibility of its drawer ports.
6403 if (drawer->getRenderedHostPort()
6404 && drawer->getRenderedHostPort()->getRenderer())
6405 {
6406 VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6407
6408 QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6409 hostPort->setCacheMode(QGraphicsItem::NoCache);
6410 hostPort->updateGeometry();
6411 hostPort->setCacheMode(normalCacheMode);
6412 }
6413 }
6414}
6415
6420{
6423}
6424
6444 VuoRendererPort *toPort,
6445 bool toPortIsDragDestination,
6446 VuoRendererPort **portToSpecialize,
6447 string &specializedTypeName,
6448 string &typecastToInsert)
6449{
6450 *portToSpecialize = NULL;
6451 specializedTypeName = "";
6452
6453 map<string, VuoRendererPort *> portToSpecializeForTypecast;
6454 map<string, string> specializedTypeNameForTypecast;
6455
6456 auto types = compiler->getTypes();
6457 vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6458 bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6459
6460 if (!solutionSelected)
6461 return false;
6462
6463 if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6464 *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6465 if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6466 specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6467
6468 return true;
6469}
6470
6489bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6490 map<string, VuoRendererPort *> portToSpecializeForTypecast,
6491 map<string, string> specializedTypeNameForTypecast,
6492 string &selectedTypecast)
6493{
6494 if (suitableTypecasts.empty())
6495 {
6496 selectedTypecast = "";
6497 return false;
6498 }
6499
6500 else if (suitableTypecasts.size() == 1)
6501 {
6502 selectedTypecast = suitableTypecasts[0];
6503 return true;
6504 }
6505
6506 else
6507 return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6508}
6509
6515bool VuoEditorComposition::portsPassSanityCheckToBridge(VuoRendererPort *fromPort, VuoRendererPort *toPort)
6516{
6517 bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6518 bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6519
6520 return (fromPortIsEnabledOutput && toPortIsEnabledInput &&
6521 fromPort->getBase()->getClass()->hasCompiler() &&
6522 toPort->getBase()->getClass()->hasCompiler());
6523}
6524
6530bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort, VuoType *candidateFromType, VuoType *candidateToType)
6531{
6532 if (!portsPassSanityCheckToBridge(fromPort, toPort))
6533 return false;
6534
6535 VuoType *inType = (candidateFromType? candidateFromType : static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6536 VuoType *outType = (candidateToType? candidateToType : static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6537
6538 // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6539 if (inType && (inType->getModuleKey() == "VuoBoolean") && outType && (outType->getModuleKey() == "VuoInteger"))
6540 {
6541 bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6546 );
6547
6548 if (toNodeUsesIndex)
6549 return false;
6550 }
6551
6552 return true;
6553}
6554
6574 VuoRendererPort *toPort,
6575 bool toPortIsDragDestination,
6576 map<string, VuoCompilerType *> &types)
6577{
6578 map<string, VuoRendererPort *> portToSpecializeForTypecast;
6579 map<string, string> specializedTypeNameForTypecast;
6580 return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6581}
6582
6593 VuoRendererPort *toPort,
6594 bool toPortIsDragDestination,
6595 map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6596 map<string, string> &specializedTypeNameForTypecast,
6597 map<string, VuoCompilerType *> &types)
6598{
6599 // If `limitCombinations` is `true`, first considers solutions that involve typeconversion
6600 // or specialization, but not both; if no such solution exists, returns solutions that involve
6601 // typeconversion+specialization combinations.
6602 // If `limitCombinations` is `false`, returns all solutions, whether they involve typeconversion,
6603 // specialization, or both.
6604 const bool limitCombinations = true;
6605
6606 portToSpecializeForTypecast.clear();
6607 specializedTypeNameForTypecast.clear();
6608 vector<string> suitableTypecasts;
6609
6610 if (!portsPassSanityCheckToBridge(fromPort, toPort))
6611 return suitableTypecasts;
6612
6613 // Temporarily disallow direct cable connections between published inputs and published outputs.
6614 // @todo: Allow for https://b33p.net/kosada/node/7756 .
6615 if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6616 return suitableTypecasts;
6617
6618 // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6619 {
6620 VuoRendererPort *portToSpecialize = NULL;
6621 string specializedTypeName = "";
6622 if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6623 {
6624 suitableTypecasts.push_back("");
6625 portToSpecializeForTypecast[""] = portToSpecialize;
6626 specializedTypeNameForTypecast[""] = specializedTypeName;
6627
6628 return suitableTypecasts;
6629 }
6630 }
6631
6632 VuoType *currentFromDataType = fromPort->getDataType();
6633 VuoType *currentToDataType = toPort->getDataType();
6634
6635 if (!(currentFromDataType && currentToDataType))
6636 return suitableTypecasts;
6637
6638 VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6639 VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6640
6641 VuoGenericType *originalFromGenericType = NULL;
6642 if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6643 {
6645 VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6646 if (fromSpecializedNodeClass)
6647 {
6648 VuoPortClass *portClass = fromPort->getBase()->getClass();
6649 originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6650 }
6651 }
6652
6653 VuoGenericType *originalToGenericType = NULL;
6654 if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6655 {
6657 VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6658 if (toSpecializedNodeClass)
6659 {
6660 VuoPortClass *portClass = toPort->getBase()->getClass();
6661 originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6662 }
6663 }
6664
6665 // Determine whether the port at each endpoint is:
6666 // 1) generic (unspecialized), or
6667 // 2) specialized and currently revertible, or
6668 // 3) effectively static.
6669 bool fromPortIsGeneric = currentFromGenericType;
6670 bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6671 bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6672
6673 bool toPortIsGeneric = currentToGenericType;
6674 bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6675 bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6676
6677 // No typeconversion or specialization options between two unspecialized generic ports.
6678 if (fromPortIsGeneric && toPortIsGeneric)
6679 return suitableTypecasts;
6680
6681 // Typeconversion options but no specialization options between two static ports.
6682 else if (fromPortIsStatic && toPortIsStatic)
6683 {
6684 if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6685 suitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6686 return suitableTypecasts;
6687 }
6688
6689 // Remaining combinations might require (re-)specializing one port or the other.
6690 // Figure out which port to consider (re-)specializing.
6691 bool specializeToPort = true;
6692 if (toPortIsGeneric)
6693 specializeToPort = true;
6694 else if (fromPortIsGeneric)
6695 specializeToPort = false;
6696 else if (fromPortIsSpecialized && toPortIsStatic)
6697 specializeToPort = false;
6698 else if (fromPortIsStatic && toPortIsSpecialized)
6699 specializeToPort = true;
6700 else if (fromPortIsSpecialized && toPortIsSpecialized)
6701 specializeToPort = toPortIsDragDestination;
6702
6703 // Now that ports have been categorized, figure out what combinations of (re-)specialization
6704 // and/or typeconversion we can use to bridge them.
6705 set<string> compatibleTypes;
6706 if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6707 compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6708 else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6709 compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6710
6711 // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6712 // non-destructively unspecializable, since it already has the appropriate specialization.
6713 compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6714
6715 if (limitCombinations)
6716 {
6717 vector<string> limitedSuitableTypecasts;
6718
6719 // Check for bridging solutions that involve typeconversion without specialization.
6720 if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6721 {
6722 limitedSuitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6723 foreach (string typecastName, limitedSuitableTypecasts)
6724 {
6725 portToSpecializeForTypecast[typecastName] = specializeToPort? toPort : fromPort;
6726 specializedTypeNameForTypecast[typecastName] = specializeToPort? currentToDataType->getModuleKey() :
6727 currentFromDataType->getModuleKey();
6728 }
6729 }
6730
6731 // Check for bridging solutions that involve specialization without typeconversion.
6732 string fixedDataType = specializeToPort? currentFromDataType->getModuleKey() : currentToDataType->getModuleKey();
6733 if (compatibleTypes.find(fixedDataType) != compatibleTypes.end())
6734 {
6735 limitedSuitableTypecasts.push_back("");
6736 portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6737 specializedTypeNameForTypecast[""] = fixedDataType;
6738 }
6739
6740 if (limitedSuitableTypecasts.size() >= 1)
6741 return limitedSuitableTypecasts;
6742 }
6743
6744 foreach (string compatibleTypeName, compatibleTypes)
6745 {
6746 VuoCompilerType *compatibleSpecializedType = types[compatibleTypeName];
6747 if (!compatibleSpecializedType)
6748 compatibleSpecializedType = compiler->getType(compatibleTypeName);
6749
6750 if (compatibleSpecializedType)
6751 {
6752 VuoType *candidateFromType = specializeToPort? currentFromDataType : compatibleSpecializedType->getBase();
6753 VuoType *candidateToType = specializeToPort? compatibleSpecializedType->getBase() : currentToDataType;
6754
6755 // Re-specialization without typeconversion may be possible.
6756 if (candidateFromType == candidateToType)
6757 {
6758 suitableTypecasts.push_back("");
6759 portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6760 specializedTypeNameForTypecast[""] = compatibleSpecializedType->getBase()->getModuleKey();
6761 }
6762
6763 if (portsPassSanityCheckToTypeconvert(fromPort,
6764 toPort,
6765 candidateFromType,
6766 candidateToType))
6767 {
6768 vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromType, candidateToType);
6769 foreach (string typecast, suitableTypecastsForCurrentTypes)
6770 {
6771 suitableTypecasts.push_back(typecast);
6772 portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6773 specializedTypeNameForTypecast[typecast] = compatibleSpecializedType->getBase()->getModuleKey();
6774 }
6775 }
6776 }
6777 }
6778
6779 return suitableTypecasts;
6780}
6781
6794bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6795 map<string, VuoRendererPort *> portToSpecializeForTypecast,
6796 map<string, string> specializedTypeNameForTypecast,
6797 string &selectedTypecast)
6798{
6799 QMenu typecastMenu(views()[0]->viewport());
6800 typecastMenu.setSeparatorsCollapsible(false);
6801 QString spacer(" ");
6802
6803 // Inventory specialization options
6804 set <pair<VuoRendererPort *, string> > specializationDetails;
6805 vector<string> typeconversionOptionsRequiringNoSpecialization;
6806 foreach (string typecastClassName, suitableTypecasts)
6807 {
6808 VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6809 string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6810 specializationDetails.insert(std::make_pair(portToSpecialize,
6811 specializedTypeName));
6812
6813 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6814 if (portAlreadyHasTargetType)
6815 typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6816 }
6817
6818 // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6819 // specialization heading under which multiple typeconversion options may be listed.
6820 // Selecting this item itself will invoke the specialization.
6821 if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6822 {
6823 QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6824 QAction *typecastAction = typecastMenu.addAction(menuText);
6825 typecastAction->setData(QVariant(""));
6826 }
6827
6828 bool foundSpecializationOptionsRequiringNoTypeconversion = typecastMenu.actions().size() >= 1;
6829 bool foundTypeconversionOptionsRequiringNoSpecialization = typeconversionOptionsRequiringNoSpecialization.size() >= 1;
6830
6831 // If there are bridging options that require no specialization, list them next.
6832 bool includingTypeconvertWithNoSpecializationHeader = foundSpecializationOptionsRequiringNoTypeconversion;
6833 if (foundTypeconversionOptionsRequiringNoSpecialization)
6834 {
6835 if (foundSpecializationOptionsRequiringNoTypeconversion)
6836 typecastMenu.addSeparator();
6837
6838 VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6839 string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6840
6841 if (portToSpecialize && !specializedTypeName.empty())
6842 {
6843 QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6844 QAction *typecastAction = typecastMenu.addAction(menuText);
6845 typecastAction->setEnabled(false);
6846 includingTypeconvertWithNoSpecializationHeader = true;
6847 }
6848 }
6849
6850 foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6851 {
6852 VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6853 if (typecastClass)
6854 {
6855 QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6856 typecastAction->setData(QVariant(typecastClassName.c_str()));
6857 }
6858 }
6859
6860 // Now list the remaining bridging options.
6861 for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6862 {
6863 VuoRendererPort *portToSpecialize = i->first;
6864 string specializedTypeName = i->second;
6865
6866 // We've already listed the no-typeconversion bridging option, so skip it here.
6867 if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6868 (portToSpecializeForTypecast[""] == portToSpecialize) &&
6869 (specializedTypeNameForTypecast[""] == specializedTypeName))
6870 {
6871 continue;
6872 }
6873
6874 // We've already listed the no-specialization bridging option, so skip it here.
6875 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6876 if (portAlreadyHasTargetType)
6877 {
6878 continue;
6879 }
6880
6881 if (typecastMenu.actions().size() >= 1)
6882 typecastMenu.addSeparator();
6883
6884 QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6885 QAction *typecastAction = typecastMenu.addAction(menuText);
6886 typecastAction->setEnabled(false);
6887
6888 // Inventory typeconversion options associated with this specialization option.
6889 foreach (string typecastClassName, suitableTypecasts)
6890 {
6891 if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6892 (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6893 {
6894 VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6895 if (typecastClass)
6896 {
6897 QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6898 typecastAction->setData(QVariant(typecastClassName.c_str()));
6899 }
6900 }
6901 }
6902 }
6903
6904 menuSelectionInProgress = true;
6905 QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6906 menuSelectionInProgress = false;
6907
6908 selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6909 return selectedTypecastAction;
6910}
6911
6915QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6916{
6917 if (!portToSpecialize || specializedTypeName.empty())
6918 return "";
6919
6920 bool isInput = portToSpecialize && portToSpecialize->getInput();
6921 QString typeDisplayName = compiler->getType(specializedTypeName)?
6922 formatTypeNameForDisplay(compiler->getType(specializedTypeName)->getBase()) :
6923 specializedTypeName.c_str();
6924
6925 bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6926
6927 QString displayText;
6928 if (portAlreadyHasTargetType)
6929 {
6930 if (isInput)
6931 {
6932 //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6933 displayText = tr("Keep Input Port as %1");
6934 }
6935 else
6936 {
6937 //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6938 displayText = tr("Keep Output Port as %1");
6939 }
6940 }
6941 else
6942 {
6943 if (isInput)
6944 {
6945 //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6946 displayText = tr("Change Input Port to %1");
6947 }
6948 else
6949 {
6950 //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6951 displayText = tr("Change Output Port to %1");
6952 }
6953 }
6954
6955 return displayText.arg(typeDisplayName);
6956}
6957
6963{
6964 __block json_object *portValue = NULL;
6965
6966 if (! port->getRenderer()->getDataType())
6967 return portValue;
6968
6969 string runningPortIdentifier = identifierCache->getIdentifierForPort(port);
6970 bool isInput = port->getRenderer()->getInput();
6971
6972 void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6973 {
6974 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6975 if (topLevelComposition->isRunningThreadUnsafe())
6976 {
6977 portValue = isInput ?
6978 topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6979 topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6980 }
6981 });
6982 };
6983 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6984
6985 return portValue;
6986}
6987
6991string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6992{
6993 return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6994}
6995
7004{
7005 if (!staticPort)
7006 return "";
7007
7008 // Published ports
7009 if (dynamic_cast<VuoPublishedPort *>(staticPort))
7010 return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
7011
7012 // Internal ports
7013 // We might as well use the same naming scheme here as is used in the running composition,
7014 // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
7015 // node identifier has been explicitly set.
7016 string nodeIdentifier = "";
7017 if (parentNode && parentNode->hasCompiler())
7018 nodeIdentifier = parentNode->getCompiler()->getIdentifier();
7019 else if (staticPort->hasRenderer() &&
7020 staticPort->getRenderer()->getUnderlyingParentNode() &&
7022 {
7023 nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
7024 }
7025
7026 if (staticPort->hasCompiler() && !nodeIdentifier.empty())
7027 {
7028 dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
7029 return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
7030 }
7031 else
7032 return "";
7033}
7034
7039{
7040 VuoPort *port = nullptr;
7041 identifierCache->doForPortWithIdentifier(portID, [&port](VuoPort *p) {
7042 port = p;
7043 });
7044 return port;
7045}
7046
7053{
7054 if (port->hasRenderer())
7055 {
7056 if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
7057 {
7058 bool isPublishedInput = !port->getRenderer()->getInput();
7059 return (isPublishedInput? composition->getPublishedInputNode() :
7060 composition->getPublishedOutputNode());
7061 }
7062
7063 else
7064 return port->getRenderer()->getUnderlyingParentNode()->getBase();
7065 }
7066
7067 foreach (VuoNode *n, composition->getBase()->getNodes())
7068 {
7069 VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
7070 if (candidateInputPort == port)
7071 return n;
7072
7073 VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
7074 if (candidateOutputPort == port)
7075 return n;
7076 }
7077
7078 return NULL;
7079}
7080
7089{
7090 map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
7091 if (popover != activePortPopovers.end())
7092 return popover->second;
7093
7094 else
7095 return NULL;
7096}
7097
7105void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
7106{
7107 string portID = identifierCache->getIdentifierForPort(rp->getBase());
7108 bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
7109 if (!popoverJustClosedAtLastEvent)
7111}
7112
7117{
7118 if (!popoverEventsEnabled)
7119 return;
7120
7121 VuoPort *port = rp->getBase();
7122 string portID = identifierCache->getIdentifierForPort(port);
7123
7124 VUserLog("%s: Open popover for %s",
7125 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7126 portID.c_str());
7127
7128 dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
7129
7130 dispatch_sync(activePortPopoversQueue, ^{
7131
7132 if (activePortPopovers.find(portID) == activePortPopovers.end())
7133 {
7134 // Assigning the popover a parent widget allows us to give it rounded corners
7135 // and a background fill that respects its rounded boundaries.
7136 VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
7137
7138 connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
7139 connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
7140 VUserLog("%s: Detach popover for %s",
7141 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7142 portID.c_str());
7143 popoverDetached();
7144 });
7145 connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
7148
7149 // Line up the top left of the dialog with the port.
7150 QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
7151
7152 // Don't let popovers get cut off at the right or bottom edges of the canvas.
7153 const int cutoffMargin = 16;
7154 QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
7155 if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
7156 portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
7157 if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
7158 portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
7159
7160 QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
7161
7162 const QPoint offset = QPoint(12, 6);
7163
7164 QPoint popoverTopLeft = popoverLeftInView + offset;
7165 popover->move(popoverTopLeft);
7166 popover->show();
7167
7168 activePortPopovers[portID] = popover;
7169
7170 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
7172 });
7173 }
7174 });
7175 });
7176}
7177
7182void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
7183{
7184 if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
7186}
7187
7196{
7197 VUserLog("%s: Close popover for %s",
7198 window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7199 portID.c_str());
7200
7201 VuoPortPopover *popover = NULL;
7202 map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
7203 if (i != activePortPopovers.end())
7204 {
7205 popover = i->second;
7206 activePortPopovers.erase(i);
7207 }
7208
7209 if (popover)
7210 {
7211 popover->hide();
7212 popover->deleteLater();
7213 }
7214
7215 bool isInput = false;
7216 bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7217 isInput = port->getRenderer()->getInput();
7218 });
7219
7220 if (! foundPort)
7221 return;
7222
7223 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7224 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7225 {
7226 dispatch_async(topLevelComposition->runCompositionQueue, ^{
7227 if (topLevelComposition->isRunningThreadUnsafe())
7228 {
7229 (isInput ?
7230 topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7231 topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7232 }
7233 });
7234 });
7235 });
7236}
7237
7241void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7242{
7243 dispatch_sync(activePortPopoversQueue, ^{
7244 disablePopoverForPort(portID);
7245 });
7246}
7247
7252{
7253 disablePortPopovers();
7255}
7256
7261{
7262 foreach (VuoErrorPopover *errorPopover, errorPopovers)
7263 {
7264 errorPopover->hide();
7265 errorPopover->deleteLater();
7266 }
7267
7268 errorPopovers.clear();
7269}
7270
7275void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7276{
7277 dispatch_sync(activePortPopoversQueue, ^{
7278 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7279 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7280 {
7281 string portID = i->first;
7282 bool shouldDisable = false;
7283
7284 if (! node)
7285 shouldDisable = true;
7286 else
7287 {
7288 identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7289 shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7290 });
7291 }
7292
7293 if (shouldDisable)
7294 disablePopoverForPort(portID);
7295 }
7296 });
7297}
7298
7303{
7304 dispatch_sync(activePortPopoversQueue, ^{
7305 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7306 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7307 {
7308 string portID = i->first;
7309
7310 bool foundPort = identifierCache->doForPortWithIdentifier(portID, [](VuoPort *port) {});
7311 if (! foundPort)
7312 disablePopoverForPort(portID);
7313 }
7314 });
7315}
7316
7322{
7323 if (recordWhichPopoversClosed)
7324 portsWithPopoversClosedAtLastEvent.clear();
7325
7326 dispatch_sync(activePortPopoversQueue, ^{
7327 map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7328 for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7329 {
7330 string portID = i->first;
7331 bool shouldDisable = false;
7332
7333 if (! node)
7334 shouldDisable = true;
7335 else
7336 {
7337 identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7338 shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7339 });
7340 }
7341
7342 if (shouldDisable)
7343 {
7344 VuoPortPopover *popover = getActivePopoverForPort(portID);
7345 if (! (popover && popover->getDetached()))
7346 {
7347 disablePopoverForPort(portID);
7348 portsWithPopoversClosedAtLastEvent.insert(portID);
7349 }
7350 }
7351 }
7352 });
7353}
7354
7359{
7360 moveDetachedPortPopoversBy(dx, dy);
7361 moveErrorPopoversBy(dx, dy);
7362}
7363
7367void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7368{
7369 foreach(VuoErrorPopover *errorPopover, errorPopovers)
7370 errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7371}
7372
7376void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7377{
7378 dispatch_sync(activePortPopoversQueue, ^{
7379 map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7380 for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7381 {
7382 VuoPortPopover *popover = i->second;
7383 if (popover && popover->getDetached())
7384 popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7385 }
7386 });
7387}
7388
7392void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7393{
7394 dispatch_sync(activePortPopoversQueue, ^{
7395 auto portPopovers = activePortPopovers;
7396 for (auto i : portPopovers)
7397 {
7398 VuoPortPopover *popover = i.second;
7399 if (popover && popover->getDetached())
7400 {
7401 id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7402 ((void (*)(id, SEL, BOOL))objc_msgSend)(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7403 }
7404 }
7405 });
7406}
7407
7413{
7414 dispatch_sync(activePortPopoversQueue, ^{
7415 for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7416 {
7417 string portID = i->first;
7418 VuoPortPopover *popover = i->second;
7419 bool shouldUpdate = false;
7420
7421 if (! node)
7422 shouldUpdate = true;
7423 else
7424 {
7425 identifierCache->doForPortWithIdentifier(portID, [&shouldUpdate, node](VuoPort *port) {
7426 shouldUpdate = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7427 });
7428 }
7429
7430 if (shouldUpdate)
7431 QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7432 }
7433 });
7434}
7435
7448 string popoverCompositionIdentifier,
7449 string portID)
7450{
7451 bool isInput;
7452 bool foundPort = popoverComposition->identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7453 isInput = port->getRenderer()->getInput();
7454 });
7455
7456 if (! foundPort)
7457 return;
7458
7459 string portSummary = (isInput ?
7460 runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7461 runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7462
7463 dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7464 VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7465 if (popover)
7466 {
7467 QMetaObject::invokeMethod(popover, "updateDataValueImmediately", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7468 QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7469 }
7470 });
7471}
7472
7481{
7482 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7483 {
7484 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7485 if (topLevelComposition->isRunningThreadUnsafe())
7486 topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7487 });
7488 });
7489}
7490
7495void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7496 bool receivedEvent, bool receivedData, string dataSummary)
7497{
7498 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7499 {
7500 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7501 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7502 if (popover)
7503 QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7504 Q_ARG(bool, receivedEvent),
7505 Q_ARG(bool, receivedData),
7506 Q_ARG(QString, dataSummary.c_str()));
7507 });
7508 };
7509 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7510}
7511
7516void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7517 bool sentEvent, bool sentData, string dataSummary)
7518{
7519 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7520 {
7521 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7522 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7523 if (popover)
7524 QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7525 Q_ARG(bool, sentEvent),
7526 Q_ARG(bool, sentData),
7527 Q_ARG(QString, dataSummary.c_str()));
7528 });
7529
7530 if (matchingComposition->showEventsMode && sentEvent)
7531 {
7532 matchingComposition->identifierCache->doForPortWithIdentifier(portIdentifier, [matchingComposition](VuoPort *port) {
7533 if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7534 {
7535 port->getRenderer()->setFiredEvent();
7536 matchingComposition->animatePort(port->getRenderer());
7537 }
7538 });
7539 }
7540 };
7541 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7542}
7543
7548void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7549{
7550 void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7551 {
7552 dispatch_async(matchingComposition->runCompositionQueue, ^{
7553 if (matchingComposition->isRunningThreadUnsafe())
7554 {
7555 dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7556 VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7557 if (popover)
7558 QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7559 });
7560 }
7561 });
7562 };
7563 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7564}
7565
7570void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7571{
7572 void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7573 {
7574 if (matchingComposition->showEventsMode)
7575 {
7576 dispatch_async(this->runCompositionQueue, ^{
7577 if (this->isRunningThreadUnsafe())
7578 {
7579 matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7580 node->getRenderer()->setExecutionBegun();
7581 });
7582 }
7583 });
7584 }
7585 };
7586 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7587}
7588
7593void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7594{
7595 void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7596 {
7597 if (matchingComposition->showEventsMode)
7598 {
7599 dispatch_async(this->runCompositionQueue, ^{
7600 if (this->isRunningThreadUnsafe())
7601 {
7602 matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7603 node->getRenderer()->setExecutionEnded();
7604 });
7605 }
7606 });
7607 }
7608 };
7609 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7610}
7611
7619{
7621}
7622
7629{
7631}
7632
7637{
7638 return showEventsMode;
7639}
7640
7645{
7646 this->showEventsMode = showEventsMode;
7647
7648 if (showEventsMode)
7649 {
7651
7652 void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7653 {
7654 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7655 if (topLevelComposition->isRunningThreadUnsafe())
7656 topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7657 });
7658 };
7659 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7660 }
7661 else
7662 {
7664
7665 void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7666 {
7667 dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7668 if (topLevelComposition->isRunningThreadUnsafe())
7669 topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7670 });
7671 };
7672 static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7673 }
7674}
7675
7680{
7681 foreach (VuoCable *cable, getBase()->getCables())
7682 {
7683 if (cable->getCompiler()->getHidden() && !cable->isPublished())
7684 return true;
7685 }
7686
7687 return false;
7688}
7689
7694{
7695 foreach (VuoCable *cable, getBase()->getCables())
7696 {
7697 if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7698 return true;
7699 }
7700
7701 return false;
7702}
7703
7708QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7709{
7710 VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7711 NULL,
7712 port->getOutput(),
7713 port->getRefreshPort(),
7714 port->getFunctionPort());
7715 animatedPort->setAnimated(true);
7716 animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7717 animatedPort->setParentItem(port->getRenderedParentNode());
7718
7719 animation->setItem(animatedPort);
7720 animation->setScaleAt(0.0, 1, 1);
7721 animation->setScaleAt(0.999, 3, 3);
7722 animation->setScaleAt(1.0, 1, 1);
7723
7724 QTimeLine *animationTimeline = animation->timeLine();
7725 animationTimeline->setFrameRange(0, 100);
7726 animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7727 animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7728
7729 preparedAnimations.insert(animation);
7730 animationForTimeline[animation->timeLine()] = animation;
7731
7732 connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7733 connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7734
7735 return animation;
7736}
7737
7741void VuoEditorComposition::animatePort(VuoRendererPort *port)
7742{
7743 dispatch_async(dispatch_get_main_queue(), ^{
7744 QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7745 if (! animation)
7746 return;
7747
7748 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7749
7750 if (animation->timeLine()->state() == QTimeLine::Running)
7751 animation->timeLine()->setCurrentTime(0);
7752
7753 else
7754 {
7755 animatedPort->setPos(port->pos());
7756 animatedPort->setVisible(true);
7757 animation->timeLine()->start();
7758 }
7759 });
7760}
7761
7766QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7767{
7768 vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7769
7770 QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7771 qreal maxPercentAdvanced = -1;
7772
7773 for (int i = 0; i < animations.size(); ++i)
7774 {
7775 QGraphicsItemAnimation *animation = animations[i];
7776 bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7777 bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7778
7779 if (! animationPrepared)
7780 return setUpAnimationForPort(animation, port);
7781
7782 else if (! animationRunning)
7783 return animation;
7784
7785 // If all of the port's animations are already running, return the
7786 // one that has been running the longest.
7787 qreal percentAdvanced = animation->timeLine()->currentValue();
7788 if (percentAdvanced > maxPercentAdvanced)
7789 {
7790 mostAdvancedAnimation = animation;
7791 maxPercentAdvanced = percentAdvanced;
7792 }
7793 }
7794
7795 // If no animation is even halfway complete, return NULL to indicate
7796 // that no animation is currently available.
7797 return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7798}
7799
7805void VuoEditorComposition::updatePortAnimation(qreal value)
7806{
7807 QTimeLine *animationTimeline = (QTimeLine *)sender();
7808 QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7809 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7810 const qreal multiplier = 1000.;
7811 animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7812}
7813
7818void VuoEditorComposition::endPortAnimation(void)
7819{
7820 QTimeLine *animationTimeline = (QTimeLine *)sender();
7821 QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7822 VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7823 animatedPort->setVisible(false);
7824}
7825
7829void VuoEditorComposition::setDisableDragStickiness(bool disable)
7830{
7831 this->dragStickinessDisabled = disable;
7832}
7833
7838{
7839 this->ignoreApplicationStateChangeEvents = ignore;
7840}
7841
7848{
7849 this->popoverEventsEnabled = enable;
7850}
7851
7856{
7857 setRenderActivity(true, includePorts);
7858 refreshComponentAlphaLevelTimer->start();
7859}
7860
7865{
7866 refreshComponentAlphaLevelTimer->stop();
7867 setRenderActivity(false);
7868}
7869
7887{
7888 // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7889 if (!activeProtocol)
7890 {
7891 VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7892 return false;
7893 }
7894
7895 // Can events from at least one trigger reach at least one published output port? If not, report an error.
7896 if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7897 {
7898 QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7899 QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7900 "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7901
7902 if (isExportingMovie)
7903 errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7904
7906 QMessageBox messageBox(window);
7907 messageBox.setWindowFlags(Qt::Sheet);
7908 messageBox.setWindowModality(Qt::WindowModal);
7909 messageBox.setFont(fonts->dialogHeadingFont());
7910 messageBox.setTextFormat(Qt::RichText);
7911
7912 messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7913 messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7914 messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7915 messageBox.setDefaultButton(QMessageBox::Ok);
7916
7917 messageBox.setText(errorHeadline);
7918 messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7919
7920 if (messageBox.exec() == QMessageBox::Help)
7921 {
7922 map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7923 map<QString, QString>::iterator i = examples.begin();
7924 if (i != examples.end())
7925 QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7926 }
7927 return false;
7928 }
7929
7930 return true;
7931}
7932
7937{
7938 return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7939}
7940
7945{
7947}
7948
7952string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7953{
7954 string dir, file, ext;
7955 VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7956 return file;
7957}
7958
7966{
7967 string customizedName = getBase()->getMetadata()->getCustomizedName();
7968 if (! customizedName.empty())
7969 return QString::fromStdString(customizedName);
7970
7971 string name = getBase()->getMetadata()->getName();
7972 return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7973}
7974
7982QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7983{
7984 // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7985 // If the extensionless filename contains dot-delimited segments, use only the final segment.
7986 vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7987 string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7988 fileNameParts[fileNameParts.size()-2] :
7989 (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7990
7991 // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7992 if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7993 {
7994 string formattedName = fileNameContentPart;
7995 if (formattedName.size() >= 1)
7996 formattedName[0] = toupper(formattedName[0]);
7997
7998 return QString(formattedName.c_str());
7999 }
8000
8001 // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
8002 return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
8003}
8004
8011{
8012 QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
8013 if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
8014 {
8015 // If not an official Vuo nodeset, return the name as-is.
8016 return nodeSetName;
8017 }
8018
8019 map<QString, QString> wordsToReformat;
8020 wordsToReformat["artnet"] = "Art-Net";
8021 wordsToReformat["bcf2000"] = "BCF2000";
8022 wordsToReformat["hid"] = "HID";
8023 wordsToReformat["midi"] = "MIDI";
8024 wordsToReformat["ndi"] = "NDI";
8025 wordsToReformat["osc"] = "OSC";
8026 wordsToReformat["rss"] = "RSS";
8027 wordsToReformat["ui"] = "UI";
8028 wordsToReformat["url"] = "URL";
8029
8030 QString nodeSetDisplayName = "";
8031 for (int i = 1; i < wordsInName.size(); ++i)
8032 {
8033 QString currentWord = wordsInName[i];
8034 if (currentWord.size() >= 1)
8035 {
8036 if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
8037 currentWord = wordsToReformat.at(currentWord.toLower());
8038 else
8039 currentWord[0] = currentWord[0].toUpper();
8040
8041 nodeSetDisplayName += currentWord;
8042
8043 if (i < wordsInName.size()-1)
8044 nodeSetDisplayName += " ";
8045 }
8046 }
8047 return nodeSetDisplayName;
8048}
8049
8056{
8057 if (!type)
8058 return "(none)";
8059
8060 string formattedTypeName = "";
8061 if (type->hasCompiler() && VuoCompilerType::isListType(type->getCompiler()))
8062 {
8063 string innerTypeName = VuoType::extractInnermostTypeName(type->getModuleKey());
8064 VuoCompilerType *innerType = compiler->getType(innerTypeName);
8065 if (innerType)
8066 {
8067 string formattedInnerTypeName = innerType->getBase()->getDefaultTitle();
8068 formattedTypeName = "List of " + formattedInnerTypeName + " elements";
8069 }
8070 }
8071 else
8072 formattedTypeName = type->getDefaultTitle();
8073
8074 return formattedTypeName.c_str();
8075}
8076
8081{
8082 if (!type)
8083 return "Event";
8084
8085 // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
8086 else if (type->getDefaultTitle() == "2D Point")
8087 return "Point2D";
8088 else if (type->getDefaultTitle() == "3D Point")
8089 return "Point3D";
8090 else if (type->getDefaultTitle() == "4D Point")
8091 return "Point4D";
8092 else if (type->getDefaultTitle() == "2D Transform")
8093 return "Transform2D";
8094 else if (type->getDefaultTitle() == "3D Transform")
8095 return "Transform3D";
8096
8097 return VuoRendererPort::sanitizePortName(formatTypeNameForDisplay(type)).toUtf8().constData();
8098}
8099
8103bool VuoEditorComposition::nodeSetMenuActionLessThan(QAction *action1, QAction *action2)
8104{
8105 QString item1Text = action1->text();
8106 QString item2Text = action2->text();
8107
8108 // Ignore list prefixes
8109 const QString listPrefix = "List of ";
8110 const QString builtInTypePrefix = "Vuo";
8111
8112 if (item1Text.startsWith(listPrefix))
8113 {
8114 item1Text.remove(0, listPrefix.length());
8115 if (item1Text.startsWith(builtInTypePrefix))
8116 item1Text.remove(0, builtInTypePrefix.length());
8117 }
8118
8119 if (item2Text.startsWith(listPrefix))
8120 {
8121 item2Text.remove(0, listPrefix.length());
8122 if (item2Text.startsWith(builtInTypePrefix))
8123 item2Text.remove(0, builtInTypePrefix.length());
8124 }
8125
8126 // Sort alphabetically by title.
8127 return (item1Text.compare(item2Text, Qt::CaseInsensitive) < 0);
8128}
8129
8134bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
8135{
8136 qreal item1Y = item1->scenePos().y();
8137 qreal item2Y = item2->scenePos().y();
8138
8139 qreal item1X = item1->scenePos().x();
8140 qreal item2X = item2->scenePos().x();
8141
8142 if (item1Y == item2Y)
8143 return (item1X < item2X);
8144
8145 return item1Y < item2Y;
8146}
8147
8154double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
8155{
8156 // Assign replacement (successor) node classes a perfect match score.
8157 {
8158 string originalGenericNode1ClassName, originalGenericNode2ClassName;
8159 if (node1->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler()))
8160 originalGenericNode1ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler())->getOriginalGenericNodeClassName();
8161 else
8162 originalGenericNode1ClassName = node1->getClassName();
8163
8164 if (node2->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler()))
8165 originalGenericNode2ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler())->getOriginalGenericNodeClassName();
8166 else
8167 originalGenericNode2ClassName = node2->getClassName();
8168
8169 if (VuoEditorUtilities::isNodeClassSuccessorTo(originalGenericNode1ClassName.c_str(), originalGenericNode2ClassName.c_str()))
8170 return 1;
8171 }
8172
8173 // Compare keywords.
8174 vector<string> node1Keywords = node1->getKeywords();
8175 vector<string> node2Keywords = node2->getKeywords();
8176
8177 // Compare node set names.
8178 if (node1->getNodeSet())
8179 node1Keywords.push_back(node1->getNodeSet()->getName());
8180
8181 if (node2->getNodeSet())
8182 node2Keywords.push_back(node2->getNodeSet()->getName());
8183
8184 // Compare tokens in node class display names.
8185 foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
8186 if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8187 node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8188
8189 foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
8190 if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8191 node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8192
8193 set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
8194 set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
8195
8196 set<string> nodeKeywordsIntersection;
8197 std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8198 node2KeywordSet.begin(), node2KeywordSet.end(),
8199 std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8200
8201 set<string> nodeKeywordsUnion = node1KeywordSet;
8202 nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8203
8204 // Avoid division by zero.
8205 if (nodeKeywordsUnion.size() == 0)
8206 return 0;
8207
8208 // Calculate Jaccard similarity.
8209 double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8210
8211 return nodeSimilarity;
8212}
8213
8218{
8219 emit compositionOnTop(top);
8220}
8221
8226{
8227 emit publishedPortNameEditorRequested(port, false);
8228}
8229
8230VuoEditorComposition::~VuoEditorComposition()
8231{
8232 dispatch_sync(runCompositionQueue, ^{});
8233 dispatch_release(runCompositionQueue);
8234
8235 preparedAnimations.clear();
8236 animationForTimeline.clear();
8237
8238 delete identifierCache;
8239
8240 moduleManager->deleteWhenReady(); // deletes compiler
8241}
8242
8247{
8248 // Update the canvas color.
8250
8251 // Force repainting the entire canvas.
8252 setComponentCaching(QGraphicsItem::NoCache);
8255}
8256
8262map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8263{
8264 vector<VuoRendererPort *> sortedPortsToPublish;
8265 foreach (string portID, portsToPublish)
8266 {
8267 VuoPort *port = getPortWithStaticIdentifier(portID);
8268 if (port && port->hasRenderer())
8269 sortedPortsToPublish.push_back(port->getRenderer());
8270 }
8271 std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8272
8273 map<string, string> publishedPortNames;
8274 foreach (VuoRendererPort *rp, sortedPortsToPublish)
8275 {
8276 rp->updateGeometry();
8277 VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8278
8279 string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8280 string publishedPortName = (!specializedPublishedPortName.empty()?
8281 specializedPublishedPortName :
8282 VuoRendererPort::sanitizePortName(rp->getPortNameToRenderWhenDisplayed().c_str()).toUtf8().constData());
8283
8284 bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8285 VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8286
8287 publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8288 }
8289
8290 return publishedPortNames;
8291}
8292
8299{
8300 if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8301 return "";
8302
8303 // If publishing a port on a "Share Value" node and the node's title has been
8304 // customized, request that title as the published port name.
8308 {
8309 return VuoRendererPort::sanitizePortName(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8310 }
8311
8312 return "";
8313}
8314
8318void VuoEditorComposition::repositionPopover()
8319{
8320 VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8321 if (popover && !popover->getDetached())
8322 {
8323 const int cutoffMargin = 16;
8324 if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8325 popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8326
8327 if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8328 popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8329 }
8330}