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