Vuo  2.0.3
VuoEditorComposition.cc
Go to the documentation of this file.
1 
10 #include "VuoEditorComposition.hh"
11 
12 #include "VuoCompilerIssue.hh"
13 #include "VuoComposition.hh"
15 #include "VuoEditorWindow.hh"
16 #include "VuoErrorDialog.hh"
17 #include "VuoException.hh"
18 #include "VuoRendererComment.hh"
19 #include "VuoRendererFonts.hh"
21 #include "VuoRendererSignaler.hh"
23 #include "VuoCompilerCable.hh"
24 #include "VuoCompilerComment.hh"
26 #include "VuoCompilerDriver.hh"
27 #include "VuoCompilerException.hh"
28 #include "VuoCompilerGraph.hh"
31 #include "VuoCompilerNode.hh"
35 #include "VuoCompilerType.hh"
36 #include "VuoGenericType.hh"
38 #include "VuoComment.hh"
39 #include "VuoEditor.hh"
40 #include "VuoErrorMark.hh"
41 #include "VuoErrorPopover.hh"
42 #include "VuoModuleManager.hh"
43 #include "VuoNodeClass.hh"
44 #include "VuoNodeSet.hh"
45 #include "VuoPortPopover.hh"
46 #include "VuoProtocol.hh"
47 #include "VuoStringUtilities.hh"
48 #include "VuoInputEditorIcon.hh"
49 #include "VuoInputEditorManager.hh"
51 #include "VuoEditorUtilities.hh"
53 
54 #ifdef __APPLE__
55 #include <ApplicationServices/ApplicationServices.h>
56 #include <objc/objc-runtime.h>
57 #endif
58 
59 const qreal VuoEditorComposition::nodeMoveRate = 15; // VuoRendererComposition::minorGridLineSpacing;
60 const qreal VuoEditorComposition::nodeMoveRateMultiplier = 4; // VuoRendererComposition::majorGridLineSpacing / VuoRendererComposition::minorGridLineSpacing
62 const qreal VuoEditorComposition::showEventsModeUpdateInterval = 1000/20.; // interval, in ms, after which to update component transparency levels and animations while in 'Show Events' mode
63 const int VuoEditorComposition::initialChangeNodeSuggestionCount = 10; // The initial number of suggestions to list in the "Change (Node) To" context menu
64 
65 Q_DECLARE_METATYPE(VuoRendererNode *)
66 
67 
71  VuoRendererComposition(baseComposition, false, true)
72 {
73 #if VUO_PRO
74  VuoEditorComposition_Pro();
75 #endif
76 
77  this->window = window;
78  compiler = NULL;
79  inputEditorManager = NULL;
80  activeProtocol = NULL;
81  runner = NULL;
82  runningComposition = NULL;
83  runningCompositionActiveDriver = NULL;
84  runningCompositionLibraries = NULL;
85  stopRequested = false;
86  duplicateOnNextMouseMove = false;
87  duplicationDragInProgress = false;
88  duplicationCancelled = false;
89  cursorPosBeforeDuplicationDragMove = QPointF(0,0);
90  cableInProgress = NULL;
91  cableInProgressWasNew = false;
92  cableInProgressShouldBeWireless = false;
93  portWithDragInitiated = NULL;
94  cableWithYankInitiated = NULL;
95  menuSelectionInProgress = false;
96  previousNearbyItem = NULL;
97  dragStickinessDisabled = false;
98  ignoreApplicationStateChangeEvents = false;
99  popoverEventsEnabled = true;
100  runCompositionQueue = dispatch_queue_create("org.vuo.editor.run", NULL);
101  activePortPopoversQueue = dispatch_queue_create("org.vuo.editor.popovers", NULL);
102  errorMark = NULL;
103  errorMarkingUpdatesEnabled = true;
104  triggerPortToRefire = "";
105 
106  contextMenuDeleteSelected = new QAction(NULL);
107  contextMenuHideSelectedCables = new QAction(NULL);
108  contextMenuRenameSelected = new QAction(NULL);
109  contextMenuRefactorSelected = new QAction(NULL);
110  contextMenuPublishPort = new QAction(NULL);
111  contextMenuDeleteCables = new QAction(NULL);
112  contextMenuHideCables = new QAction(NULL);
113  contextMenuUnhideCables = new QAction(NULL);
114  contextMenuFireEvent = new QAction(NULL);
115  contextMenuAddInputPort = new QAction(NULL);
116  contextMenuRemoveInputPort = new QAction(NULL);
117  contextMenuSetPortConstant = new QAction(NULL);
118  contextMenuEditSelectedComments = new QAction(NULL);
119 
120  contextMenuChangeNode = NULL;
121 
122  contextMenuFireEvent->setText(tr("Fire Event"));
123  contextMenuHideSelectedCables->setText(tr("Hide"));
124  contextMenuRenameSelected->setText(tr("Rename…"));
125  contextMenuRefactorSelected->setText(tr("Package as Subcomposition"));
126  contextMenuAddInputPort->setText(tr("Add Input Port"));
127  contextMenuRemoveInputPort->setText(tr("Remove Input Port"));
128  contextMenuSetPortConstant->setText(tr("Edit Value…"));
129  contextMenuEditSelectedComments->setText(tr("Edit…"));
130 
131  connect(contextMenuDeleteSelected, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
132  connect(contextMenuHideSelectedCables, &QAction::triggered, this, &VuoEditorComposition::selectedInternalCablesHidden);
133  connect(contextMenuRenameSelected, &QAction::triggered, this, &VuoEditorComposition::renameSelectedNodes);
134  connect(contextMenuRefactorSelected, &QAction::triggered, this, &VuoEditorComposition::refactorRequested);
135  connect(contextMenuPublishPort, &QAction::triggered, this, &VuoEditorComposition::togglePortPublicationStatus);
136  connect(contextMenuDeleteCables, &QAction::triggered, this, &VuoEditorComposition::deleteConnectedCables);
137  connect(contextMenuHideCables, &QAction::triggered, this, &VuoEditorComposition::hideConnectedCables);
138  connect(contextMenuUnhideCables, &QAction::triggered, this, &VuoEditorComposition::unhideConnectedCables);
139  connect(contextMenuFireEvent, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::fireTriggerPortEvent));
140  connect(contextMenuAddInputPort, &QAction::triggered, this, &VuoEditorComposition::addInputPort);
141  connect(contextMenuRemoveInputPort, &QAction::triggered, this, &VuoEditorComposition::removeInputPort);
142  connect(contextMenuEditSelectedComments, &QAction::triggered, this, &VuoEditorComposition::editSelectedComments);
143 
144  // Use a queued connection to open input editors in order to avoid bug where invoking a
145  // QColorDialog by context menu prevents subsequent interaction with the editor window
146  // even after the color dialog has been closed.
147  connect(contextMenuSetPortConstant, &QAction::triggered, this, &VuoEditorComposition::setPortConstant, Qt::QueuedConnection);
148 
149  {
150  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
151  // For now, this sets up the actions for a menu, rather than setting up the menu itself.
152  QSignalMapper *contextMenuThrottlingMapper = new QSignalMapper(this);
153  connect(contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::setTriggerThrottling);
154 
155  QList<QPair<QString, enum VuoPortClass::EventThrottling> > throttlingNamesAndIndices;
156  throttlingNamesAndIndices.append(QPair<QString, enum VuoPortClass::EventThrottling>(tr("Enqueue Events"), VuoPortClass::EventThrottling_Enqueue));
157  throttlingNamesAndIndices.append(QPair<QString, enum VuoPortClass::EventThrottling>(tr("Drop Events"), VuoPortClass::EventThrottling_Drop));
158 
159  for (QList<QPair<QString, enum VuoPortClass::EventThrottling> >::iterator i = throttlingNamesAndIndices.begin(); i != throttlingNamesAndIndices.end(); ++i)
160  {
161  QString name = i->first;
162  enum VuoPortClass::EventThrottling index = i->second;
163  QAction *action = new QAction(name, this);
164 
165  contextMenuThrottlingActions.append(action);
166 
167  contextMenuThrottlingMapper->setMapping(action, index);
168  connect(action, &QAction::triggered, contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
169  }
170 
171  /*
172  contextMenuThrottling = new VuoMenu(NULL);
173  contextMenuThrottling->setTitle(tr("Set Event Throttling"));
174  QSignalMapper *contextMenuThrottlingMapper = new QSignalMapper(this);
175  connect(contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::setEventThrottling);
176 
177  addActionToMenuAndMapper(contextMenuThrottling, contextMenuThrottlingMapper, "Enqueue Events", VuoPortClass::EventThrottling_Enqueue);
178  addActionToMenuAndMapper(contextMenuThrottling, contextMenuThrottlingMapper, "Drop Events", VuoPortClass::EventThrottling_Drop);
179  */
180  }
181 
182  {
183  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
184  // For now, this sets up the actions for a menu, rather than setting up the menu itself.
185  QSignalMapper *contextMenuTintsMapper = new QSignalMapper(this);
186  connect(contextMenuTintsMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::tintSelectedItems);
187 
188  QList<QPair<QString, enum VuoNode::TintColor> > tintNamesAndIndices;
189  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Yellow"), VuoNode::TintYellow));
190  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Tangerine"), VuoNode::TintTangerine));
191  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Orange"), VuoNode::TintOrange));
192  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Magenta"), VuoNode::TintMagenta));
193  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Violet"), VuoNode::TintViolet));
194  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Blue"), VuoNode::TintBlue));
195  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Cyan"), VuoNode::TintCyan));
196  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Green"), VuoNode::TintGreen));
197  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Lime"), VuoNode::TintLime));
198  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("None"), VuoNode::TintNone));
199 
200  for (QList<QPair<QString, enum VuoNode::TintColor> >::iterator i = tintNamesAndIndices.begin(); i != tintNamesAndIndices.end(); ++i)
201  {
202  QString name = i->first;
203  enum VuoNode::TintColor index = i->second;
204  QAction *action = new QAction(name, this);
205 
206  // Add a color swatch to the menu item.
207  {
208  QColor fill(0,0,0,0);
209  // For TintNone, draw a transparent icon, so that menu item's text indent is consistent with the other items.
210  if (index != VuoNode::TintNone)
211  {
212  VuoRendererColors colors(index);
213  fill = colors.nodeFill();
214  }
215 
216  QIcon *icon = VuoInputEditorIcon::renderIcon(^(QPainter &p){
217  p.setPen(Qt::NoPen);
218  p.setBrush(fill);
219  // Match distance between text baseline and ascender.
220  p.drawEllipse(3, 3, 10, 10);
221  });
222  action->setIcon(*icon);
223  delete icon;
224  }
225 
226  contextMenuTintActions.append(action);
227 
228  contextMenuTintsMapper->setMapping(action, index);
229  connect(action, &QAction::triggered, contextMenuTintsMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
230  }
231  }
232 
233  // 'Show Events' mode rendering setup
234  this->refreshComponentAlphaLevelTimer = new QTimer(this);
235  this->refreshComponentAlphaLevelTimer->setObjectName("VuoEditorComposition::refreshComponentAlphaLevelTimer");
236  refreshComponentAlphaLevelTimer->setInterval(showEventsModeUpdateInterval);
237  connect(refreshComponentAlphaLevelTimer, &QTimer::timeout, this, &VuoEditorComposition::updateGeometryForAllComponents);
238  setShowEventsMode(false);
239 
240  connect(signaler, &VuoRendererSignaler::nodePopoverRequested, this, &VuoEditorComposition::enablePopoverForNode);
241  connect(signaler, &VuoRendererSignaler::nodesMoved, this, &VuoEditorComposition::moveNodesBy);
242  connect(signaler, &VuoRendererSignaler::commentsMoved, this, &VuoEditorComposition::moveCommentsBy);
243  connect(signaler, &VuoRendererSignaler::commentResized, this, &VuoEditorComposition::resizeCommentBy);
250  connect(signaler, &VuoRendererSignaler::dragStickinessDisableRequested, this, &VuoEditorComposition::setDisableDragStickiness);
251  connect(signaler, &VuoRendererSignaler::openUrl, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
252 
253  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::activeApplicationStateChanged, this, &VuoEditorComposition::updatePopoversForApplicationStateChange, Qt::QueuedConnection);
254  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::focusChanged, this, &VuoEditorComposition::updatePopoversForActiveWindowChange, Qt::QueuedConnection);
255  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationWillHide, this, [=]{
256  setPopoversHideOnDeactivate(true);
257  });
258  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationDidUnhide, this, [=]{
259  setPopoversHideOnDeactivate(false);
260  });
261 
262  identifierCache = new VuoNodeAndPortIdentifierCache;
263  identifierCache->addCompositionComponentsToCache(getBase());
264 }
265 
270 {
271  this->compiler = compiler;
272 }
273 
278 {
279  return compiler;
280 }
281 
286 {
287  this->moduleManager = moduleManager;
288  moduleManager->setComposition(this);
289 }
290 
295 {
296  return moduleManager;
297 }
298 
305 {
306  this->inputEditorManager = inputEditorManager;
307 }
308 
315 {
316  return this->inputEditorManager;
317 }
318 
322 VuoRendererNode * VuoEditorComposition::createNode(QString nodeClassName, string title, double x, double y)
323 {
324  if (compiler)
325  {
326  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
327  if (nodeClass)
328  {
329  VuoNode *node = createBaseNode(nodeClass, nullptr, title, x, y);
330  if (node)
331  {
333  setCustomConstantsForNewNode(rn);
334  return rn;
335  }
336  }
337  }
338  return NULL;
339 }
340 
347 VuoNode * VuoEditorComposition::createBaseNode(VuoCompilerNodeClass *nodeClass, VuoNode *modelNode, string title, double x, double y)
348 {
349  // If adding the node would create recursion (subcomposition contains itself), create a node without a compiler detail.
350  __block bool isAllowed = true;
351  if (nodeClass->isSubcomposition())
352  {
353  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
354  string compositionModuleKey = VuoCompiler::getModuleKeyForPath(compositionPath);
355  bool nodeIsThisComposition = (compositionModuleKey == nodeClass->getBase()->getClassName());
356 
357  set<string> dependencies = nodeClass->getDependencies();
358  auto iter = std::find_if(dependencies.begin(), dependencies.end(), [=](const string &d){ return d == compositionModuleKey; });
359  bool nodeContainsThisComposition = (iter != dependencies.end());
360 
361  isAllowed = ! (nodeIsThisComposition || nodeContainsThisComposition);
362  });
363  }
364 
365  VuoNode *node;
366  if (isAllowed)
367  {
368  node = (modelNode ?
369  compiler->createNode(nodeClass, modelNode) :
370  compiler->createNode(nodeClass, title, x, y));
371  }
372  else
373  {
374  node = createNodeWithMissingImplementation(nodeClass->getBase(), modelNode, title, x, y);
375  node->setForbidden(true);
376  }
377  return node;
378 }
379 
386 VuoNode * VuoEditorComposition::createNodeWithMissingImplementation(VuoNodeClass *modelNodeClass, VuoNode *modelNode, string title, double x, double y)
387 {
388  vector<string> inputPortClassNames;
389  vector<string> outputPortClassNames;
390  foreach (VuoPortClass *portClass, modelNodeClass->getInputPortClasses())
391  {
392  if (portClass == modelNodeClass->getRefreshPortClass())
393  continue;
394  inputPortClassNames.push_back(portClass->getName());
395  }
396  foreach (VuoPortClass *portClass, modelNodeClass->getOutputPortClasses())
397  outputPortClassNames.push_back(portClass->getName());
398 
399  VuoNodeClass *dummyNodeClass = new VuoNodeClass(modelNodeClass->getClassName(), inputPortClassNames, outputPortClassNames);
400  return (modelNode ?
401  dummyNodeClass->newNode(modelNode) :
402  dummyNodeClass->newNode(! title.empty() ? title : modelNodeClass->getDefaultTitle(), x, y));
403 }
404 
410 void VuoEditorComposition::setCustomConstantsForNewNode(VuoRendererNode *newNode)
411 {
412  // vuo.time.make: Set the 'year' input to the current year.
413  if (newNode->getBase()->getNodeClass()->getClassName() == "vuo.time.make")
414  {
415  VuoPort *yearPort = newNode->getBase()->getInputPortWithName("year");
416  if (yearPort)
417  {
418  QString currentYear = QString::number(QDateTime::currentDateTime().date().year());
419  yearPort->getRenderer()->setConstant(VuoText_getString(currentYear.toUtf8().constData()));
420  }
421  }
422 }
423 
427 void VuoEditorComposition::addNode(VuoNode *n, bool nodeShouldBeRendered, bool nodeShouldBeGivenUniqueIdentifier)
428 {
429  VuoRendererComposition::addNode(n, nodeShouldBeRendered, nodeShouldBeGivenUniqueIdentifier);
430  identifierCache->addNodeToCache(n);
431 }
432 
439 {
440  if (resetState)
441  {
442  disablePortPopovers(rn);
443 
444  if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == rn))
446  }
447 
449 }
450 
465 {
466  // Inventory the port constants and connected input cables associated with the old node, to be re-associated with the new node.
467  map<VuoCable *, VuoPort *> cablesToTransferFromPort;
468  map<VuoCable *, VuoPort *> cablesToTransferToPort;
469  set<VuoCable *> cablesToRemove;
470  getBase()->getCompiler()->getChangesToReplaceNode(oldNode->getBase(), newNode, cablesToTransferFromPort, cablesToTransferToPort, cablesToRemove);
471 
472  // Also inventory any typecasts collapsed onto the old node, to be attached to the new node instead.
473  vector<VuoRendererInputDrawer *> attachedDrawers;
474  vector<VuoRendererNode *> collapsedTypecasts;
475  vector<VuoPort *> oldInputPorts = oldNode->getBase()->getInputPorts();
476  for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
477  {
478  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
479  if (typecastPort)
480  {
481  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
482  collapsedTypecasts.push_back(typecastNode);
483  }
484 
485  // If the original node is currently being rendered as collapsed typecast, uncollapse it.
486  if (oldNode->getProxyCollapsedTypecast())
487  uncollapseTypecastNode(oldNode);
488 
489  // Uncollapse typecasts attached to the original node.
490  for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
492  }
493 
494  // Inventory any attachments to the old node, to make sure none are stranded in the replacement.
495  for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
496  {
497  VuoRendererPort *inputPortRenderer = (*inputPort)->getRenderer();
498  VuoRendererInputDrawer *attachedDrawer = inputPortRenderer->getAttachedInputDrawer();
499  if (attachedDrawer)
500  attachedDrawers.push_back(attachedDrawer);
501  }
502 
503  // Perform the node replacement.
504  replaceNode(oldNode, newNode);
505 
506  // Restore connected cables.
507  for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferFromPort.begin(); i != cablesToTransferFromPort.end(); ++i)
508  i->first->getRenderer()->setFrom(newNode, i->second);
509  for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferToPort.begin(); i != cablesToTransferToPort.end(); ++i)
510  i->first->getRenderer()->setTo(newNode, i->second);
511  foreach (VuoCable *cable, cablesToRemove)
512  removeCable(cable->getRenderer());
513 
514  // Restore constant values.
515  for (VuoPort *oldInputPort : oldNode->getBase()->getInputPorts())
516  {
517  VuoPort *newInputPort = newNode->getInputPortWithName(oldInputPort->getClass()->getName());
518  if (! newInputPort)
519  continue;
520 
521  if (! oldInputPort->getRenderer()->carriesData() || ! newInputPort->getRenderer()->carriesData())
522  continue;
523 
524  if (oldNode->getBase()->hasCompiler() && newNode->hasCompiler())
525  {
526  VuoType *oldDataType = static_cast<VuoCompilerPort *>(oldInputPort->getCompiler())->getDataVuoType();
527  VuoType *newDataType = static_cast<VuoCompilerPort *>(newInputPort->getCompiler())->getDataVuoType();
528  if (! (oldDataType == newDataType && oldDataType && ! dynamic_cast<VuoGenericType *>(oldDataType)) )
529  continue;
530  }
531 
532  string oldConstantValue;
533  if (oldNode->getBase()->hasCompiler())
534  oldConstantValue = oldInputPort->getRenderer()->getConstantAsString();
535  else
536  oldConstantValue = oldInputPort->getRawInitialValue();
537 
538  if (newNode->hasCompiler())
539  updatePortConstant(static_cast<VuoCompilerPort *>(newInputPort->getCompiler()), oldConstantValue, false);
540  else
541  newInputPort->setRawInitialValue(oldConstantValue);
542  }
543 
544  // Remove any stranded drawers and their incoming cables.
545  // @todo https://b33p.net/kosada/node/16441 and https://b33p.net/kosada/node/16441 :
546  // Decide how to handle stranded attachment deletion and insertion properly, including
547  // updates to the running composition and all types of incoming connections to the attachments.
548  // For now just make sure not to leave behind a stranded drawer or any of its incoming cables.
549  foreach (VuoRendererInputDrawer *drawer, attachedDrawers)
550  {
551  if (!drawer->getRenderedHostPort())
552  {
553  foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
554  {
555  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(drawerPort->getBase()->getRenderer());
556  if (typecastPort)
557  uncollapseTypecastNode(typecastPort);
558 
559  foreach (VuoCable *cable, drawerPort->getBase()->getConnectedCables())
560  removeCable(cable->getRenderer());
561  }
562 
563  removeNode(drawer);
564  }
565  }
566 
567  // Restore connected typecasts.
568  for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
570 
571  // Re-collapse the updated node, if applicable.
572  collapseTypecastNode(newNode->getRenderer());
573 }
574 
580 {
581  disablePortPopovers(oldNode);
582 
583  if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == oldNode))
585 
587  if (newNode->hasCompiler())
588  {
589  string graphvizIdentifier = (oldNode->getBase()->hasCompiler() ?
590  oldNode->getBase()->getCompiler()->getGraphvizIdentifier() :
591  oldNode->getBase()->getRawGraphvizIdentifier());
592  newNode->getCompiler()->setGraphvizIdentifier(graphvizIdentifier);
593  }
594 
595  removeNode(oldNode);
596  addNode(newNode, true, false);
597 
598  identifierCache->addNodeToCache(newNode);
599 }
600 
604 void VuoEditorComposition::removeCable(VuoRendererCable *rc, bool emitHiddenCableNotification)
605 {
606  bool cableHidden = rc->getBase()->getCompiler()->getHidden();
608 
609  if (cableHidden && emitHiddenCableNotification)
610  emit changeInHiddenCables();
611 }
612 
616 void VuoEditorComposition::addCable(VuoCable *cable, bool emitHiddenCableNotification)
617 {
618  bool cableHidden = cable->getCompiler()->getHidden();
620 
621  if (cableHidden && emitHiddenCableNotification)
622  emit changeInHiddenCables();
623 }
624 
634 {
635  return VuoRendererComposition::createAndConnectMakeListNode(toNode, toPort, compiler, rendererCable);
636 }
637 
647  set<VuoRendererNode *> &createdNodes,
648  set<VuoRendererCable *> &createdCables)
649 {
650  return VuoRendererComposition::createAndConnectDictionaryAttachmentsForNode(node, compiler, createdNodes, createdCables);
651 }
652 
659 QList<QGraphicsItem *> VuoEditorComposition::createAndConnectInputAttachments(VuoRendererNode *node, bool createButDoNotAdd)
660 {
661  QList<QGraphicsItem *> addedComponents = VuoRendererComposition::createAndConnectInputAttachments(node, compiler, createButDoNotAdd);
662  foreach (QGraphicsItem *component, addedComponents)
663  {
664  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(component);
665  if (rn && !createButDoNotAdd)
666  identifierCache->addNodeToCache(rn->getBase());
667  }
668 
669  return addedComponents;
670 }
671 
676 void VuoEditorComposition::modifyComponents(void (^modify)(void))
677 {
678  identifierCache->clearCache();
679 
680  // Record the IDs of the currently selected components so that the selection status
681  // of the corresponding items may be restored after the composition is reset.
682  set<string> selectedNodeIDs;
683  foreach (VuoRendererNode *rn, getSelectedNodes())
684  {
685  if (rn->getBase()->hasCompiler())
686  selectedNodeIDs.insert(rn->getBase()->getCompiler()->getGraphvizIdentifier());
687  }
688 
689  set<string> selectedCommentIDs;
690  foreach (VuoRendererComment *rc, getSelectedComments())
691  {
692  if (rc->getBase()->hasCompiler())
693  selectedCommentIDs.insert(rc->getBase()->getCompiler()->getGraphvizIdentifier());
694  }
695 
696  set<string> selectedCableIDs;
697  foreach (VuoRendererCable *rc, getSelectedCables(true))
698  {
699  if (rc->getBase()->hasCompiler())
700  selectedCableIDs.insert(rc->getBase()->getCompiler()->getGraphvizDeclaration());
701  }
702 
703  modify();
704 
705  // Restore the selection status of pre-existing components.
706  foreach (QGraphicsItem *item, items())
707  {
708  if (dynamic_cast<VuoRendererNode *>(item))
709  {
710  string currentNodeID = (dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler()?
711  dynamic_cast<VuoRendererNode *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
712  "");
713  if (!currentNodeID.empty() && (selectedNodeIDs.find(currentNodeID) != selectedNodeIDs.end()))
714  item->setSelected(true);
715  }
716 
717  if (dynamic_cast<VuoRendererComment *>(item))
718  {
719  string currentCommentID = (dynamic_cast<VuoRendererComment *>(item)->getBase()->hasCompiler()?
720  dynamic_cast<VuoRendererComment *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
721  "");
722  if (!currentCommentID.empty() && (selectedCommentIDs.find(currentCommentID) != selectedCommentIDs.end()))
723  item->setSelected(true);
724  }
725 
726  else if (dynamic_cast<VuoRendererCable *>(item))
727  {
728  string currentCableID = (dynamic_cast<VuoRendererCable *>(item)->getBase()->hasCompiler()?
729  dynamic_cast<VuoRendererCable *>(item)->getBase()->getCompiler()->getGraphvizDeclaration() :
730  "");
731  if (!currentCableID.empty() && (selectedCableIDs.find(currentCableID) != selectedCableIDs.end()))
732  item->setSelected(true);
733  }
734  }
735 
736  // Re-establish mappings between the stored composition components and the running
737  // composition components, if applicable.
738  identifierCache->addCompositionComponentsToCache(getBase());
739 
740  // Close popovers for ports no longer present in the composition.
742 }
743 
749 {
750  string portName = port->getBase()->getClass()->getName();
751  VuoRendererNode *parentNode = port->getRenderedParentNode();
752 
753  // A changed math expression input to a "Calculate" node will require changes to the node's
754  // upstream input lists of variable names and values.
755  if ((portName == "expression") &&
756  VuoStringUtilities::beginsWith(parentNode->getBase()->getNodeClass()->getClassName(), "vuo.math.calculate"))
757  return true;
758 
759  return false;
760 }
761 
766 {
768 }
769 
774 {
775  if (commandDescription.empty())
776  {
777  if (getContextMenuDeleteSelectedAction()->text().contains("Reset"))
778  commandDescription = "Reset";
779  else
780  commandDescription = "Delete";
781  }
782 
783  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
784  emit componentsRemoved(selectedCompositionComponents, commandDescription);
785 }
786 
790 void VuoEditorComposition::deleteSelectedNodes(string commandDescription)
791 {
792  if (commandDescription.empty())
793  commandDescription = "Delete";
794 
795  QList<QGraphicsItem *> selectedNodes;
796  foreach (VuoRendererNode *node, getSelectedNodes())
797  selectedNodes.append(node);
798 
799  emit componentsRemoved(selectedNodes, commandDescription);
800 }
801 
806 {
807  identifierCache->clearCache();
808 
810 
811  foreach (VuoCable *cable, getBase()->getCables())
812  removeCable(cable->getRenderer(), false);
813 
814  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
816 
817  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedOutputPorts())
818  removePublishedPort(publishedPort, false);
819 
820  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
821  removePublishedPort(publishedPort, true);
822 
823  foreach (VuoNode *node, getBase()->getNodes())
824  removeNode(node->getRenderer(), false);
825 
827 
828  foreach (VuoComment *comment, getBase()->getComments())
829  removeComment(comment->getRenderer());
830 
832 }
833 
837 void VuoEditorComposition::insertNode()
838 {
839  QAction *sender = (QAction *)QObject::sender();
840  QPair<QPointF, QString> pair = sender->data().value<QPair<QPointF, QString> >();
841 
842  QList<QGraphicsItem *> newNodes;
843  VuoRendererNode *newNode = createNode(pair.second, "",
844  pair.first.x(),
845  pair.first.y());
846 
847  if (newNode)
848  {
849  newNodes.append(newNode);
850  emit componentsAdded(newNodes, this);
851  }
852 }
853 
857 void VuoEditorComposition::insertComment()
858 {
859  QAction *sender = (QAction *)QObject::sender();
860  QPointF scenePos = sender->data().value<QPointF>();
861 
862  emit commentInsertionRequested(scenePos);
863 }
864 
868 void VuoEditorComposition::insertSubcomposition()
869 {
870  QAction *sender = (QAction *)QObject::sender();
871  QPointF scenePos = sender->data().value<QPointF>();
872 
873  emit subcompositionInsertionRequested(scenePos);
874 }
875 
881 {
882  QAction *sender = (QAction *)QObject::sender();
883  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
884 
885  if (isPortPublished(port))
886  emit portUnpublicationRequested(port->getBase());
887  else
888  emit portPublicationRequested(port->getBase(), false);
889 }
890 
897 void VuoEditorComposition::deleteConnectedCables()
898 {
899  QAction *sender = (QAction *)QObject::sender();
900  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
901  vector<VuoCable *> connectedCables = port->getBase()->getConnectedCables(true);
902  QList<QGraphicsItem *> cablesToRemove;
903 
904  // Delete visible directly connected cables.
905  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
906  {
907  if (!cable->getRenderer()->paintingDisabled())
908  cablesToRemove.append(cable->getRenderer());
909  }
910 
911  // Delete visible cables connected to the typecast's child port.
912  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
913  if (typecastPort)
914  {
915  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
916  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
917  foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
918  {
919  if (!cable->getRenderer()->paintingDisabled())
920  cablesToRemove.append(cable->getRenderer());
921  }
922  }
923 
924  emit componentsRemoved(QList<QGraphicsItem *>(cablesToRemove));
925 }
926 
933 void VuoEditorComposition::hideConnectedCables()
934 {
935  QAction *sender = (QAction *)QObject::sender();
936  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
937  set<VuoRendererCable *> cablesToHide;
938 
939  // Hide visible directly connected cables.
940  foreach (VuoCable *cable, port->getBase()->getConnectedCables(false))
941  {
942  if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
943  cablesToHide.insert(cable->getRenderer());
944  }
945 
946  // Hide visible cables connected to the typecast's child port.
947  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
948  if (typecastPort)
949  {
950  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
951  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
952  foreach (VuoCable *cable, typecastInPort->getConnectedCables(false))
953  {
954  if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
955  cablesToHide.insert(cable->getRenderer());
956  }
957  }
958 
959  emit cablesHidden(cablesToHide);
960 }
961 
968 void VuoEditorComposition::unhideConnectedCables()
969 {
970  QAction *sender = (QAction *)QObject::sender();
971  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
972  set<VuoRendererCable *> cablesToUnhide;
973 
974  // Unhide visible directly connected cables.
975  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
976  {
977  if (cable->getRenderer()->getEffectivelyWireless())
978  cablesToUnhide.insert(cable->getRenderer());
979  }
980 
981  // Unhide visible cables connected to the typecast's child port.
982  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
983  if (typecastPort)
984  {
985  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
986  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
987  foreach (VuoCable *cable, typecastInPort->getConnectedCables(true))
988  {
989  if (cable->getRenderer()->getEffectivelyWireless())
990  cablesToUnhide.insert(cable->getRenderer());
991  }
992  }
993 
994  emit cablesUnhidden(cablesToUnhide);
995 }
996 
1001 void VuoEditorComposition::fireTriggerPortEvent()
1002 {
1003  QAction *sender = (QAction *)QObject::sender();
1004  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1005  fireTriggerPortEvent(port->getBase());
1006 }
1007 
1012 {
1013  fireTriggerPortEvent(getTriggerPortToRefire());
1014 }
1015 
1020 {
1021  if (triggerPortToRefire.empty())
1022  return nullptr;
1023 
1024  VuoPort *triggerPort = nullptr;
1025  identifierCache->doForPortWithIdentifier(triggerPortToRefire, [&triggerPort](VuoPort *port) {
1026  triggerPort = port;
1027  });
1028  return triggerPort;
1029 }
1030 
1035 {
1036  string portID = getIdentifierForStaticPort(port);
1037  if (portID != this->triggerPortToRefire)
1038  {
1039  this->triggerPortToRefire = portID;
1040  emit refirePortChanged();
1041  }
1042 }
1043 
1048 void VuoEditorComposition::setPortConstant()
1049 {
1050  QAction *sender = (QAction *)QObject::sender();
1051  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1052 
1053  if (port->isConstant())
1054  emit inputEditorRequested(port);
1055 }
1056 
1061 void VuoEditorComposition::setPortConstantToValue(VuoRendererPort *port, string value)
1062 {
1063  if (port->isConstant())
1064  emit portConstantChangeRequested(port, value);
1065 }
1066 
1071 void VuoEditorComposition::specializeGenericPortType()
1072 {
1073  QAction *sender = (QAction *)QObject::sender();
1074  QList<QVariant> portAndSpecializedType= sender->data().toList();
1075  VuoRendererPort *port = (VuoRendererPort *)portAndSpecializedType[0].value<void *>();
1076  QString specializedTypeName = portAndSpecializedType[1].toString();
1077 
1078  // If the port is already specialized to the target type, do nothing.
1079  if (port && (port->getDataType()->getModuleKey() == specializedTypeName.toUtf8().constData()))
1080  return;
1081 
1082  // If the port is already specialized to a different type, re-specialize it.
1083  if (port && !dynamic_cast<VuoGenericType *>(port->getDataType()))
1084  emit respecializePort(port, specializedTypeName.toUtf8().constData());
1085 
1086  // Otherwise, specialize the port from generic.
1087  else
1088  emit specializePort(port, specializedTypeName.toUtf8().constData());
1089 }
1090 
1095 void VuoEditorComposition::unspecializePortType()
1096 {
1097  QAction *sender = (QAction *)QObject::sender();
1098  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1099 
1100  // If the port is already generic, do nothing.
1101  if (port && dynamic_cast<VuoGenericType *>(port->getDataType()))
1102  return;
1103 
1104  emit unspecializePort(port);
1105 }
1106 
1117 void VuoEditorComposition::createReplacementsToUnspecializePort(VuoPort *portToUnspecialize, bool shouldOutputNodesToReplace, map<VuoNode *, string> &nodesToReplace, set<VuoCable *> &cablesToDelete)
1118 {
1119  // Find the ports that will share the same generic type as portToUnspecialize, and organize them by node.
1120  set<VuoPort *> connectedPotentiallyGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(portToUnspecialize->getRenderer()->getUnderlyingParentNode()->getBase(),
1121  portToUnspecialize, true);
1122  map<VuoNode *, set<VuoPort *> > portsToUnspecializeForNode;
1123  for (VuoPort *connectedPort : connectedPotentiallyGenericPorts)
1124  {
1125  VuoNode *node = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase();
1126 
1127  // @todo: Don't just exclude ports that aren't currently revertible, also exclude ports that are only
1128  // within the current network by way of ports that aren't currently revertible.
1129  if (isPortCurrentlyRevertible(connectedPort->getRenderer()))
1130  portsToUnspecializeForNode[node].insert(connectedPort);
1131  }
1132 
1133  for (map<VuoNode *, set<VuoPort *> >::iterator i = portsToUnspecializeForNode.begin(); i != portsToUnspecializeForNode.end(); ++i)
1134  {
1135  VuoNode *node = i->first;
1136  set<VuoPort *> ports = i->second;
1137 
1138  if (shouldOutputNodesToReplace)
1139  {
1140  // Create the unspecialized node class name for each node to unspecialize.
1141  set<VuoPortClass *> portClasses;
1142  for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1143  portClasses.insert((*j)->getClass());
1145  string unspecializedNodeClassName = nodeClass->createUnspecializedNodeClassName(portClasses);
1146  nodesToReplace[node] = unspecializedNodeClassName;
1147  }
1148 
1149  // Identify the cables that will become invalid (data-carrying cable with generic port at one end, non-generic port at the other end)
1150  // when the node is unspecialized.
1151  for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1152  {
1153  VuoPort *port = *j;
1154  for (VuoCable *cable : port->getConnectedCables(true))
1155  {
1156  bool areEndsCompatible = false;
1157 
1158  if (!cable->getRenderer()->effectivelyCarriesData())
1159  areEndsCompatible = true;
1160 
1161  else if (! cable->isPublished())
1162  {
1163  VuoPort *portOnOtherEnd = (cable->getFromPort() == port ? cable->getToPort() : cable->getFromPort());
1164  if (portOnOtherEnd && isPortCurrentlyRevertible(portOnOtherEnd->getRenderer()))
1165  {
1166  VuoNode *nodeOnOtherEnd = portOnOtherEnd->getRenderer()->getUnderlyingParentNode()->getBase();
1167  VuoCompilerNodeClass *nodeClassOnOtherEnd = nodeOnOtherEnd->getNodeClass()->getCompiler();
1168  VuoCompilerSpecializedNodeClass *specializedNodeClassOnOtherEnd = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClassOnOtherEnd);
1169  if (specializedNodeClassOnOtherEnd)
1170  {
1171  VuoType *typeOnOtherEnd = specializedNodeClassOnOtherEnd->getOriginalPortType( portOnOtherEnd->getClass() );
1172  if (! typeOnOtherEnd || dynamic_cast<VuoGenericType *>(typeOnOtherEnd))
1173  areEndsCompatible = true;
1174  }
1175  }
1176  }
1177 
1178  if (! areEndsCompatible && (cable != cableInProgress))
1179  cablesToDelete.insert(cable);
1180  }
1181  }
1182  }
1183 }
1184 
1188 void VuoEditorComposition::fireTriggerPortEvent(VuoPort *port)
1189 {
1190  if (! (port && port->hasCompiler()) )
1191  return;
1192 
1193  VuoPort *oldManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1194  string oldSnapshot = takeSnapshot();
1195 
1196  string runningTriggerPortIdentifier = "";
1197  bool isTriggerPort = false;
1198  if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()))
1199  {
1200  // Trigger port — The event will be fired from the port.
1201 
1202  getBase()->getCompiler()->setManuallyFirableInputPort(nullptr, nullptr);
1203 
1204  runningTriggerPortIdentifier = identifierCache->getIdentifierForPort(port);
1205  isTriggerPort = true;
1206  }
1207  else if (port->hasRenderer() && port->getRenderer()->getInput())
1208  {
1209  // Input port — The event will be fired from the composition's manually firable trigger into the port.
1210 
1212 
1214  VuoCompilerTriggerPort *triggerPort = graph->getManuallyFirableTrigger();
1215  VuoCompilerNode *triggerNode = graph->getNodeForTriggerPort(triggerPort);
1216  runningTriggerPortIdentifier = getIdentifierForStaticPort(triggerPort->getBase(), triggerNode->getBase());
1217  }
1218  else
1219  return;
1220 
1221  VUserLog("%s: Fire %s",
1222  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
1223  runningTriggerPortIdentifier.c_str());
1224 
1225  VuoPort *newManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1226  bool manuallyFirableInputPortChanged = (oldManuallyFirableInputPort != newManuallyFirableInputPort);
1227  string newSnapshot = takeSnapshot();
1228 
1229  auto fireIfRunning = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
1230  {
1231  dispatch_async(topLevelComposition->runCompositionQueue, ^{
1232  if (topLevelComposition->isRunningThreadUnsafe())
1233  {
1234  topLevelComposition->runner->fireTriggerPortEvent(thisCompositionIdentifier, runningTriggerPortIdentifier);
1235 
1236  // Display the trigger port animation when the user manually fires an event
1237  // even if not in "Show Events" mode. (If in "Show Events" mode, this will
1238  // be handled for trigger ports by VuoEditorComposition::receivedTelemetryOutputPortUpdated(...).)
1239  if (! (this->showEventsMode && isTriggerPort) )
1240  this->animatePort(port->getRenderer());
1241  }
1242  });
1243  };
1244 
1245  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1246  if (this == topLevelComposition || ! manuallyFirableInputPortChanged)
1247  {
1248  // Top-level composition or unmodified subcomposition — Fire the trigger immediately.
1249 
1250  if (! newSnapshot.empty() && manuallyFirableInputPortChanged)
1251  updateRunningComposition(oldSnapshot, newSnapshot);
1252 
1253  fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1254  }
1255  else
1256  {
1257  // Modified subcomposition — Fire the trigger after the subcomposition has been reloaded.
1258 
1259  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
1260  string nodeClassName = VuoCompiler::getModuleKeyForPath(compositionPath);
1261  moduleManager->doNextTimeNodeClassIsLoaded(nodeClassName, ^{
1262  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1263  fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1264  });
1265  });
1266 
1267  if (! newSnapshot.empty())
1268  updateRunningComposition(oldSnapshot, newSnapshot);
1269  });
1270  }
1271  });
1272 
1273  setTriggerPortToRefire(port);
1274 }
1275 
1279 void VuoEditorComposition::setTriggerThrottling(int eventThrottling)
1280 {
1281  QSignalMapper *signalMapper = (QSignalMapper *)QObject::sender();
1282  QAction *sender = (QAction *)signalMapper->mapping(eventThrottling);
1283  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1284  emit triggerThrottlingUpdated(port->getBase(), (enum VuoPortClass::EventThrottling)eventThrottling);
1285 }
1286 
1291 void VuoEditorComposition::addInputPort()
1292 {
1293  QAction *sender = (QAction *)QObject::sender();
1294  VuoRendererNode *node = (VuoRendererNode *)(sender->data().value<void *>());
1295  emit inputPortCountAdjustmentRequested(node, 1, false);
1296 }
1297 
1302 void VuoEditorComposition::removeInputPort()
1303 {
1304  QAction *sender = (QAction *)QObject::sender();
1305  VuoRendererNode *node = (VuoRendererNode *)(sender->data().value<void *>());
1306  emit inputPortCountAdjustmentRequested(node, -1, false);
1307 }
1308 
1313 void VuoEditorComposition::swapNode()
1314 {
1315  QAction *sender = (QAction *)QObject::sender();
1316  QList<QVariant> nodeAndReplacementType= sender->data().toList();
1317  VuoRendererNode *node = static_cast<VuoRendererNode *>(nodeAndReplacementType[0].value<void *>());
1318  QString newNodeClassName = nodeAndReplacementType[1].toString();
1319  emit nodeSwapRequested(node, newNodeClassName.toUtf8().constData());
1320 }
1321 
1322 
1327 {
1329 }
1330 
1335 {
1336  // Open a title editor for each selected non-attachment node.
1337  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1338  for (set<VuoRendererNode *>::iterator i = selectedNodes.begin(); i != selectedNodes.end(); ++i)
1339  {
1340  if (!dynamic_cast<VuoRendererInputAttachment *>(*i))
1341  emit nodeTitleEditorRequested(*i);
1342  }
1343 }
1344 
1348 void VuoEditorComposition::editSelectedComments()
1349 {
1350  // Open a text editor for each selected comment.
1351  set<VuoRendererComment *> selectedComments = getSelectedComments();
1352  for (set<VuoRendererComment *>::iterator i = selectedComments.begin(); i != selectedComments.end(); ++i)
1353  emit commentEditorRequested(*i);
1354 }
1355 
1359 set<VuoRendererCable *> VuoEditorComposition::getCablesInternalToSubcomposition(QList<QGraphicsItem *> subcompositionComponents)
1360 {
1361  set<VuoRendererCable *> internalCables;
1362 
1363  for (QList<QGraphicsItem *>::iterator i = subcompositionComponents.begin(); i != subcompositionComponents.end(); ++i)
1364  {
1365  QGraphicsItem *compositionComponent = *i;
1366  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1367  if (rn)
1368  {
1369  set<VuoCable *> connectedCables = rn->getConnectedCables(false);
1370  for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1371  {
1372  VuoNode *fromNode = (*cable)->getFromNode();
1373  VuoNode *toNode = (*cable)->getToNode();
1374 
1375  if (fromNode && toNode && subcompositionComponents.contains(fromNode->getRenderer()) && subcompositionComponents.contains(toNode->getRenderer()))
1376  internalCables.insert((*cable)->getRenderer());
1377  }
1378  }
1379  }
1380 
1381  return internalCables;
1382 }
1383 
1388 {
1389  return cableInProgress;
1390 }
1391 
1397 {
1398  return cableInProgressWasNew;
1399 }
1400 
1405 {
1406  return menuSelectionInProgress;
1407 }
1408 
1413 {
1414  QList<QGraphicsItem *> compositionComponents = items();
1415  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1416  {
1417  QGraphicsItem *compositionComponent = *i;
1418 
1419  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1420  if (!rc || !rc->paintingDisabled())
1421  compositionComponent->setSelected(true);
1422  }
1423 }
1424 
1429 {
1430  QList<QGraphicsItem *> compositionComponents = items();
1431  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1432  {
1433  QGraphicsItem *compositionComponent = *i;
1434  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
1435  if (rcomment)
1436  rcomment->setSelected(true);
1437  }
1438 }
1439 
1444 {
1445  QList<QGraphicsItem *> compositionComponents = items();
1446  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1447  {
1448  QGraphicsItem *compositionComponent = *i;
1449  compositionComponent->setSelected(false);
1450  }
1451 }
1452 
1456 void VuoEditorComposition::openSelectedEditableNodes()
1457 {
1458  foreach (VuoRendererNode *node, getSelectedNodes())
1459  {
1460  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
1461  QString actionText, sourcePath;
1462  if (VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
1463  emit nodeSourceEditorRequested(node);
1464  }
1465 }
1466 
1471 {
1472  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1473  set<VuoRendererComment *> selectedComments = getSelectedComments();
1474  moveItemsBy(selectedNodes, selectedComments, dx, dy, false);
1475 }
1476 
1480 void VuoEditorComposition::moveNodesBy(set<VuoRendererNode *> nodes, qreal dx, qreal dy, bool movedByDragging)
1481 {
1482  set<VuoRendererComment *> comments;
1483  moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1484 }
1485 
1489 void VuoEditorComposition::moveCommentsBy(set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1490 {
1491  set<VuoRendererNode *> nodes;
1492  moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1493 }
1494 
1498 void VuoEditorComposition::moveItemsBy(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1499 {
1500  emit itemsMoved(nodes, comments, dx, dy, movedByDragging);
1502  for (set<VuoRendererNode *>::iterator it = nodes.begin(); it != nodes.end(); ++it)
1503  (*it)->updateConnectedCableGeometry();
1504 }
1505 
1509 void VuoEditorComposition::resizeCommentBy(VuoRendererComment *comment, qreal dx, qreal dy)
1510 {
1511  emit commentResized(comment, dx, dy);
1512 }
1513 
1518 {
1519  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1520  set<VuoRendererNode *> selectedNodes;
1521  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1522  {
1523  QGraphicsItem *compositionComponent = *i;
1524  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1525  if (rn)
1526  selectedNodes.insert(rn);
1527  }
1528 
1529  return selectedNodes;
1530 }
1531 
1536 {
1537  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1538  set<VuoRendererComment *> selectedComments;
1539  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1540  {
1541  QGraphicsItem *compositionComponent = *i;
1542  VuoRendererComment *rc = dynamic_cast<VuoRendererComment *>(compositionComponent);
1543  if (rc)
1544  selectedComments.insert(rc);
1545  }
1546 
1547  return selectedComments;
1548 }
1549 
1553 set<VuoRendererCable *> VuoEditorComposition::getSelectedCables(bool includePublishedCables)
1554 {
1555  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1556  set<VuoRendererCable *> selectedCables;
1557  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1558  {
1559  QGraphicsItem *compositionComponent = *i;
1560  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1561  if (rc && (includePublishedCables || !rc->getBase()->isPublished()))
1562  selectedCables.insert(rc);
1563  }
1564 
1565  return selectedCables;
1566 }
1567 
1571 void VuoEditorComposition::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1572 {
1573  const QMimeData *mimeData = event->mimeData();
1574  bool disablePortHoverHighlighting = true;
1575 
1576  // Accept drags of files.
1577  if (mimeData->hasFormat("text/uri-list"))
1578  {
1579  QList<QUrl> urls = mimeData->urls();
1580 
1581  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1582  VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1583  if (portAtDropLocation)
1584  {
1585  if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1586  (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1587  disablePortHoverHighlighting = false;
1588  else
1589  event->setDropAction(Qt::IgnoreAction);
1590  }
1591  else // if (!portAtDropLocation)
1592  {
1593  bool dragIncludesDroppableFile = false;
1594  foreach (QUrl url, urls)
1595  {
1596  bool isSupportedDragNDropFile = isSupportedImageFile(url.path().toUtf8().constData()) ||
1597  isSupportedMovieFile(url.path().toUtf8().constData()) ||
1598  isSupportedSceneFile(url.path().toUtf8().constData()) ||
1599  isSupportedAudioFile(url.path().toUtf8().constData()) ||
1600  isSupportedFeedFile(url.path().toUtf8().constData()) ||
1601  isSupportedJsonFile(url.path().toUtf8().constData()) ||
1602  isSupportedXmlFile(url.path().toUtf8().constData()) ||
1603  isSupportedTableFile(url.path().toUtf8().constData()) ||
1604  isSupportedMeshFile(url.path().toUtf8().constData()) ||
1605  isSupportedDataFile(url.path().toUtf8().constData()) ||
1606  isSupportedAppFile(url.path().toUtf8().constData()) ||
1607  isDirectory(url.path().toUtf8().constData());
1608  if (isSupportedDragNDropFile)
1609  {
1610  dragIncludesDroppableFile = true;
1611  break;
1612  }
1613  }
1614 
1615  if (!dragIncludesDroppableFile)
1616  event->setDropAction(Qt::IgnoreAction);
1617  }
1618 
1619  event->accept();
1620  }
1621 
1622  // Accept drags of single or multiple nodes from the node library.
1623  else if (mimeData->hasFormat("text/plain") || mimeData->hasFormat("text/scsv"))
1624  event->acceptProposedAction();
1625 
1626  else
1627  {
1628  event->setDropAction(Qt::IgnoreAction);
1629  event->accept();
1630  }
1631 
1632  updateHoverHighlighting(event->scenePos(), disablePortHoverHighlighting);
1633 }
1634 
1638 void VuoEditorComposition::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
1639 {
1640  event->acceptProposedAction();
1641 }
1642 
1646 void VuoEditorComposition::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
1647 {
1648  dragEnterEvent(event);
1649 }
1650 
1654 void VuoEditorComposition::dropEvent(QGraphicsSceneDragDropEvent *event)
1655 {
1656  const QMimeData *mimeData = event->mimeData();
1657 
1658  // Accept drops of certain types of files.
1659  if (mimeData->hasFormat("text/uri-list"))
1660  {
1661  // Retrieve the composition directory so that file paths may be specified relative to it.
1662  // Note: Providing the directory's canonical path as the argument to the
1663  // QDir constructor is necessary in order for QDir::relativeFilePath() to
1664  // work correctly when the non-canonical path contains symbolic links (e.g.,
1665  // '/tmp' -> '/private/tmp' for example compositions).
1666  string topCompositionPath = compiler->getCompositionLocalPath();
1667  if (topCompositionPath.empty())
1668  topCompositionPath = getBase()->getDirectory();
1669  QDir compositionDir(QDir(topCompositionPath.c_str()).canonicalPath());
1670 
1671  // Use the absolute file path if the "Option" key was pressed.
1672  bool useAbsoluteFilePaths = VuoEditorUtilities::optionKeyPressedForEvent(event);
1673 
1674  QList<QGraphicsItem *> newNodes;
1675  QList<QUrl> urls = mimeData->urls();
1676 
1677  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1678  VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1679  if (portAtDropLocation)
1680  {
1681  if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1682  (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1683  {
1684  QString filePath = (useAbsoluteFilePaths? urls[0].path() : compositionDir.relativeFilePath(urls[0].path()));
1685  string constantValue = "\"" + string(filePath.toUtf8().constData()) + "\"";
1686  event->accept();
1687 
1688  emit portConstantChangeRequested(portAtDropLocation, constantValue);
1689  }
1690  else
1691  event->ignore();
1692  }
1693 
1694  else // if (!portAtDropLocation)
1695  {
1696  const int ySpacingForNewNodes = 11;
1697  int yOffsetForPreviousNewNode = -1 * ySpacingForNewNodes;
1698 
1699  foreach (QUrl url, urls)
1700  {
1701  QStringList targetNodeClassNames;
1702 
1703  if (isSupportedImageFile(url.path().toUtf8().constData()))
1704  targetNodeClassNames += "vuo.image.fetch";
1705  if (isSupportedMovieFile(url.path().toUtf8().constData()))
1706  {
1707  if (!getActiveProtocol())
1708  targetNodeClassNames += "vuo.video.play";
1709 
1710  targetNodeClassNames += "vuo.video.decodeImage";
1711  }
1712  if (isSupportedSceneFile(url.path().toUtf8().constData()))
1713  targetNodeClassNames += "vuo.scene.fetch";
1714  if (isSupportedAudioFile(url.path().toUtf8().constData()))
1715  targetNodeClassNames += "vuo.audio.file.play";
1716  if (isSupportedMeshFile(url.path().toUtf8().constData()))
1717  targetNodeClassNames += "vuo.image.project.dome";
1718  if (isSupportedFeedFile(url.path().toUtf8().constData()))
1719  targetNodeClassNames += "vuo.rss.fetch";
1720  if (isSupportedJsonFile(url.path().toUtf8().constData()))
1721  targetNodeClassNames += "vuo.tree.fetch.json";
1722  if (isSupportedXmlFile(url.path().toUtf8().constData()))
1723  targetNodeClassNames += "vuo.tree.fetch.xml";
1724  if (isSupportedTableFile(url.path().toUtf8().constData()))
1725  targetNodeClassNames += "vuo.table.fetch";
1726  if (isSupportedDataFile(url.path().toUtf8().constData()))
1727  targetNodeClassNames += "vuo.data.fetch";
1728  if (isSupportedAppFile(url.path().toUtf8().constData()))
1729  targetNodeClassNames += "vuo.app.launch";
1730  if (isDirectory(url.path().toUtf8().constData()))
1731  targetNodeClassNames += "vuo.file.list";
1732 
1733  QString selectedNodeClassName = "";
1734  if (targetNodeClassNames.size() == 1)
1735  selectedNodeClassName = targetNodeClassNames[0];
1736  else if (targetNodeClassNames.size() > 1)
1737  {
1738  QMenu nodeMenu(views()[0]->viewport());
1739  nodeMenu.setSeparatorsCollapsible(false);
1740 
1741  foreach (QString nodeClassName, targetNodeClassNames)
1742  {
1743  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
1744  string nodeTitle = (nodeClass? nodeClass->getBase()->getDefaultTitle() : nodeClassName.toUtf8().constData());
1745  //: Appears in a popup menu when dragging files from Finder onto the composition canvas.
1746  QAction *nodeAction = nodeMenu.addAction(tr("Insert \"%1\" Node").arg(nodeTitle.c_str()));
1747  nodeAction->setData(nodeClassName);
1748  }
1749 
1750  menuSelectionInProgress = true;
1751  QAction *selectedNode = nodeMenu.exec(QCursor::pos());
1752  menuSelectionInProgress = false;
1753 
1754  selectedNodeClassName = (selectedNode? selectedNode->data().toString().toUtf8().constData() : "");
1755  }
1756 
1757  if (!selectedNodeClassName.isEmpty())
1758  {
1759  VuoRendererNode *newNode = createNode(selectedNodeClassName, "",
1760  event->scenePos().x(),
1761  event->scenePos().y() + yOffsetForPreviousNewNode + ySpacingForNewNodes);
1762 
1763  if (newNode)
1764  {
1765  VuoPort *urlPort = newNode->getBase()->getInputPortWithName(selectedNodeClassName == "vuo.file.list"?
1766  "folder" :
1767  "url");
1768  if (urlPort)
1769  {
1770  QString filePath = (useAbsoluteFilePaths? url.path() : compositionDir.relativeFilePath(url.path()));
1771  urlPort->getRenderer()->setConstant(VuoText_getString(filePath.toUtf8().constData()));
1772 
1773  newNodes.append(newNode);
1774 
1775  yOffsetForPreviousNewNode += newNode->boundingRect().height();
1776  yOffsetForPreviousNewNode += ySpacingForNewNodes;
1777  }
1778  }
1779  }
1780  }
1781 
1782  if (newNodes.size() > 0)
1783  {
1784  event->accept();
1785 
1786  emit componentsAdded(newNodes, this);
1787  }
1788  else
1789  event->ignore();
1790  }
1791  }
1792 
1793  // Accept drops of one or more nodes from the node library class list.
1794  else if (mimeData->hasFormat("text/scsv"))
1795  {
1796  event->setDropAction(Qt::CopyAction);
1797  event->accept();
1798 
1799  QByteArray scsvData = event->mimeData()->data("text/scsv");
1800  QString scsvText = QString::fromUtf8(scsvData);
1801  QStringList nodeClassNames = scsvText.split(';');
1802 
1803  QPointF startPos = event->scenePos()-QPointF(0,VuoRendererNode::nodeHeaderYOffset);
1804  int nextYPos = VuoRendererComposition::quantizeToNearestGridLine(startPos,
1806  int snapDelta = nextYPos - startPos.y();
1807 
1808  QList<QGraphicsItem *> newNodes;
1809  for (QStringList::iterator i = nodeClassNames.begin(); i != nodeClassNames.end(); ++i)
1810  {
1811  VuoRendererNode *newNode = createNode(*i, "",
1812  startPos.x(),
1813  nextYPos - (VuoRendererItem::getSnapToGrid()? 0 : snapDelta));
1814 
1815  if (newNode)
1816  {
1817  int prevYPos = nextYPos;
1818  nextYPos = VuoRendererComposition::quantizeToNearestGridLine(QPointF(0,prevYPos+newNode->boundingRect().height()),
1820  if (nextYPos <= prevYPos+newNode->boundingRect().height())
1822 
1823  newNodes.append((QGraphicsItem *)newNode);
1824  }
1825  }
1826 
1827  emit componentsAdded(newNodes, this);
1828  }
1829 
1830  // Accept drops of single nodes from the node library documentation panel.
1831  else if (mimeData->hasFormat("text/plain"))
1832  {
1833  event->setDropAction(Qt::CopyAction);
1834  event->accept();
1835 
1836  QList<QGraphicsItem *> newNodes;
1837  QStringList dropItems = event->mimeData()->text().split('\n');
1838  QString nodeClassName = dropItems[0];
1839 
1840  // Account for the offset between cursor and dragged item pixmaps
1841  // to drop node in-place.
1842  QPoint hotSpot = (dropItems.size() >= 3? QPoint(dropItems[1].toInt(), dropItems[2].toInt()) : QPoint(0,0));
1843  VuoRendererNode *newNode = createNode(nodeClassName, "",
1844  event->scenePos().x()-hotSpot.x()+1,
1845  event->scenePos().y()-VuoRendererNode::nodeHeaderYOffset-hotSpot.y()+1);
1846 
1847  newNodes.append(newNode);
1848  emit componentsAdded(newNodes, this);
1849  }
1850  else
1851  {
1852  event->ignore();
1853  }
1854 }
1855 
1859 void VuoEditorComposition::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
1860 {
1861  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1862  if (dynamic_cast<VuoRendererPort *>(nearbyItem))
1863  {
1864  QGraphicsScene::sendEvent(nearbyItem, event);
1865  event->accept();
1866  }
1867 
1868  else
1869  QGraphicsScene::mouseDoubleClickEvent(event);
1870 }
1871 
1875 void VuoEditorComposition::mousePressEvent(QGraphicsSceneMouseEvent *event)
1876 {
1877  QPointF scenePos = event->scenePos();
1878 
1879  // Handle left-button presses.
1880  if (event->button() == Qt::LeftButton)
1881  {
1882  // Correct for erroneous tracking behavior that occurs following the first left-click
1883  // directly on a node, cable, or comment after the cancellation of a component duplication operation.
1884  // See https://b33p.net/kosada/node/3339
1885  if (duplicationCancelled)
1886  {
1887  QGraphicsItem *itemClickedDirectly = itemAt(scenePos, views()[0]->transform());
1888  if (dynamic_cast<VuoRendererNode *>(itemClickedDirectly) ||
1889  dynamic_cast<VuoRendererCable *>(itemClickedDirectly) ||
1890  dynamic_cast<VuoRendererComment *>(itemClickedDirectly)
1891  )
1892  {
1893  duplicationCancelled = false;
1894  correctForCancelledDuplication(event);
1895  }
1896  }
1897 
1898  // Determine whether the cursor is in range of any operable composition components.
1899  QGraphicsItem *nearbyItem = findNearbyComponent(scenePos);
1900  leftMousePressEventAtNearbyItem(nearbyItem, event);
1901  }
1902 
1903  // Handle non-left-button presses.
1904  else // if (event->button() != Qt::LeftButton)
1905  mousePressEventNonLeftButton(event);
1906 }
1907 
1912 void VuoEditorComposition::leftMousePressEventAtNearbyItem(QGraphicsItem *nearbyItem, QGraphicsSceneMouseEvent *event)
1913 {
1914  bool eventHandled = false;
1915 
1916  // If click did not occur within range of a port, check whether
1917  // the click occured on a cable within its yank zone.
1918  VuoRendererCable *cableYankedDirectly = NULL;
1919  VuoRendererPort *currentPort = dynamic_cast<VuoRendererPort *>(nearbyItem);
1920  if (! currentPort)
1921  {
1922  VuoRendererCable *currentCable = dynamic_cast<VuoRendererCable *>(nearbyItem);
1923  if (currentCable &&
1924  currentCable->yankZoneIncludes(event->scenePos()) &&
1925  currentCable->getBase()->getToPort())
1926  {
1927  currentPort = currentCable->getBase()->getToPort()->getRenderer();
1928  cableYankedDirectly = currentCable;
1929  }
1930  }
1931 
1932  if (currentPort)
1933  {
1934  // Case: Firing an event by Command+click
1935  bool isTriggerPort = (currentPort->getBase()->hasCompiler() && dynamic_cast<VuoCompilerTriggerPort *>(currentPort->getBase()->getCompiler()));
1936  if ((event->modifiers() & Qt::ControlModifier) && (currentPort->getInput() || isTriggerPort))
1937  fireTriggerPortEvent(currentPort->getBase());
1938 
1939  else
1940  {
1941  portWithDragInitiated = currentPort;
1942  cableWithYankInitiated = cableYankedDirectly;
1943  event->accept();
1944  }
1945 
1946  eventHandled = true;
1947  }
1948 
1949  // Case: Initiating duplication of selected components with Option/Alt+drag
1950  else if (nearbyItem && VuoEditorUtilities::optionKeyPressedForEvent(event) && !duplicationDragInProgress)
1951  {
1952  // Duplicate the selected components.
1953  duplicateOnNextMouseMove = true;
1954  duplicationDragInProgress = true;
1955  cursorPosBeforeDuplicationDragMove = (VuoRendererItem::getSnapToGrid()?
1958  event->scenePos());
1959 
1960  QGraphicsScene::mousePressEvent(event);
1961  eventHandled = true;
1962  }
1963 
1964  // Case: Left mouse-click made near enough to a cable to trigger cable selection
1965  if (dynamic_cast<VuoRendererCable *>(nearbyItem))
1966  {
1967  if (event->modifiers() & Qt::ControlModifier)
1968  nearbyItem->setSelected(! nearbyItem->isSelected());
1969 
1970  else
1971  {
1973  nearbyItem->setSelected(true);
1974  }
1975 
1976  event->accept();
1977  eventHandled = true;
1978  }
1979 
1980  // Case: Left mouse-click made for some other reason, not handled here
1981  if (!eventHandled)
1982  QGraphicsScene::mousePressEvent(event);
1983 }
1984 
1989 void VuoEditorComposition::mousePressEventNonLeftButton(QGraphicsSceneMouseEvent *event)
1990 {
1991  cancelCableDrag();
1992 
1993  // If a right-click occurred, generate the context menu event ourselves
1994  // rather than leaving it to QGraphicsScene. This prevents existing selected
1995  // components from being erroneously de-selected before
1996  // VuoEditorComposition::contextMenuEvent(...) is called if the right-click
1997  // occurred within range of, but not directly upon, a composition component.
1998  if (event->button() == Qt::RightButton)
1999  {
2000  QGraphicsSceneContextMenuEvent contextMenuEvent(QEvent::GraphicsSceneContextMenu);
2001  contextMenuEvent.setScreenPos(event->screenPos());
2002  contextMenuEvent.setScenePos(event->scenePos());
2003  contextMenuEvent.setReason(QGraphicsSceneContextMenuEvent::Mouse);
2004  QApplication::sendEvent(this, &contextMenuEvent);
2005  event->accept();
2006  }
2007 
2008  else
2009  QGraphicsScene::mousePressEvent(event);
2010 
2011  return;
2012 }
2013 
2020 void VuoEditorComposition::correctForCancelledDuplication(QGraphicsSceneMouseEvent *event)
2021 {
2022  // Simulate an extra mouse click for now to force the cursor
2023  // to track correctly with the next set of dragged components.
2024  QGraphicsSceneMouseEvent pressEvent(QEvent::GraphicsSceneMousePress);
2025  pressEvent.setScenePos(event->scenePos());
2026  pressEvent.setButton(Qt::LeftButton);
2027  QApplication::sendEvent(this, &pressEvent);
2028 
2029  QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
2030  releaseEvent.setScenePos(event->scenePos());
2031  releaseEvent.setButton(Qt::LeftButton);
2032  QApplication::sendEvent(this, &releaseEvent);
2033 }
2034 
2042 void VuoEditorComposition::initiateCableDrag(VuoRendererPort *currentPort, VuoRendererCable *cableYankedDirectly, QGraphicsSceneMouseEvent *event)
2043 {
2044  Qt::KeyboardModifiers modifiers = event->modifiers();
2045  bool optionKeyPressed = (modifiers & Qt::AltModifier);
2046  bool shiftKeyPressed = (modifiers & Qt::ShiftModifier);
2047 
2048  // For now, a left mouse press on an input port with a constant value, attached typecast, or attached "Make List" node does nothing.
2049  // Eventually, a mouse drag will detach the constant value, typecast, or "Make List" node.
2050  if (! (cableYankedDirectly || currentPort->getOutput() || currentPort->supportsDisconnectionByDragging()))
2051  {
2052  return;
2053  }
2054 
2055  // Determine based on the keypress modifiers and the attributes of the port whether to
2056  // create a new cable, disconnect an existing cable, or duplicate an existing cable.
2057  bool creatingNewCable = false;
2058  bool disconnectingExistingCable = false;
2059  bool duplicatingExistingCable = false;
2060 
2061  VuoPort *fixedPort = currentPort->getBase();
2062  VuoNode *fixedNode = getUnderlyingParentNodeForPort(fixedPort, this);
2063 
2064  VuoNode *fromNode = NULL;
2065  VuoPort *fromPort = NULL;
2066  VuoNode *toNode = NULL;
2067  VuoPort *toPort = NULL;
2068 
2069  // Case: Dragging from an output port
2070  if (currentPort->getOutput())
2071  {
2072  // Prepare for the "forward" creation of a new cable.
2073  fromPort = fixedPort;
2074  fromNode = fixedNode;
2075  creatingNewCable = true;
2076  }
2077 
2078  // Case: Dragging from an input port
2079  else if (! currentPort->getFunctionPort())
2080  {
2081  // If the input port has no connected cables to disconnect, prepare for the
2082  // "backward" creation of a new cable.
2083  if (currentPort->getBase()->getConnectedCables(true).empty())
2084  {
2085  toPort = fixedPort;
2086  toNode = fixedNode;
2087  creatingNewCable = true;
2088  }
2089 
2090  // If the input port does have connected cables, prepare for the
2091  // disconnection or duplication of one of these cables.
2092  else
2093  {
2094  if (optionKeyPressed)
2095  {
2096  duplicatingExistingCable = true;
2097  }
2098 
2099  else
2100  disconnectingExistingCable = true;
2101  }
2102  }
2103 
2104  // @todo: Case: Dragging from a function port
2105 
2106 
2107  // Perform the actual cable creation, if applicable.
2108  if (creatingNewCable)
2109  {
2110  // Create the cable first and set its endpoints later in case either endpoint is published,
2111  // since published nodes don't have compilers.
2112  cableInProgress = (new VuoCompilerCable(NULL,
2113  NULL,
2114  NULL,
2115  NULL))->getBase();
2116 
2117  // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2118  // regardless of its connected ports.
2119  cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2120 
2121  cableInProgressWasNew = true;
2122  cableInProgressShouldBeWireless = false;
2123 
2124  addCable(cableInProgress);
2125  cableInProgress->getRenderer()->setFrom(fromNode, fromPort);
2126  cableInProgress->getRenderer()->setTo(toNode, toPort);
2127  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2128  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2129  highlightEligibleEndpointsForCable(cableInProgress);
2130  fixedPort->getRenderer()->updateGeometry();
2131  }
2132 
2133  // Perform the actual cable disconnection, if applicable.
2134  else if (disconnectingExistingCable)
2135  {
2136  // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2137  if (cableYankedDirectly)
2138  cableInProgress = cableYankedDirectly->getBase();
2139 
2140  // Otherwise, disconnect the cable that was connected to the port most recently.
2141  else
2142  cableInProgress = currentPort->getBase()->getConnectedCables(true).back();
2143 
2144  cableInProgressWasNew = false;
2145  cableInProgressShouldBeWireless = cableInProgress->hasCompiler() && cableInProgress->getCompiler()->getHidden();
2146 
2147  currentPort->updateGeometry();
2148  cableInProgress->getRenderer()->updateGeometry();
2149  cableInProgress->getRenderer()->setHovered(false);
2150  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2151  cableInProgress->getRenderer()->setFloatingEndpointPreviousToPort(cableInProgress->getToPort());
2152  cableInProgress->getRenderer()->setPreviouslyAlwaysEventOnly(cableInProgress->getCompiler()->getAlwaysEventOnly());
2153  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2154  cableInProgress->getRenderer()->setTo(NULL, NULL);
2155  highlightEligibleEndpointsForCable(cableInProgress);
2157  cableInProgress->getRenderer()->setSelected(true);
2158  }
2159  // Perform the actual cable duplication, if applicable.
2160  else if (duplicatingExistingCable)
2161  {
2162  VuoCable *cableToDuplicate = NULL;
2163  // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2164  if (cableYankedDirectly)
2165  cableToDuplicate = cableYankedDirectly->getBase();
2166 
2167  // Otherwise, disconnect the cable that was connected to the port most recently.
2168  else
2169  cableToDuplicate = currentPort->getBase()->getConnectedCables(true).back();
2170 
2171  // Create the cable first and set its endpoints later in case either endpoint is published,
2172  // since published nodes don't have compilers.
2173  cableInProgress = (new VuoCompilerCable(NULL,
2174  NULL,
2175  NULL,
2176  NULL))->getBase();
2177 
2178  // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2179  // regardless of its connected ports.
2180  cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2181 
2182  cableInProgressWasNew = true;
2183  cableInProgressShouldBeWireless = cableToDuplicate->hasCompiler() &&
2184  cableToDuplicate->getCompiler()->getHidden();
2185  addCable(cableInProgress);
2186  cableInProgress->getRenderer()->setFrom(getUnderlyingParentNodeForPort(cableToDuplicate->getFromPort(), this),
2187  cableToDuplicate->getFromPort());
2188  cableInProgress->getRenderer()->setTo(NULL, NULL);
2189  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2190  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2191  highlightEligibleEndpointsForCable(cableInProgress);
2193  cableInProgress->getRenderer()->setSelected(true);
2194  fixedPort->getRenderer()->updateGeometry();
2195  }
2196 
2197  // The cable will need to be re-painted as its endpoint is dragged.
2198  cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2199 
2200  emit cableDragInitiated();
2201 
2202  event->accept();
2203 }
2204 
2208 void VuoEditorComposition::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2209 {
2210  // Handle left-button releases.
2211  if (event->button() == Qt::LeftButton)
2212  {
2213  portWithDragInitiated = NULL;
2214  cableWithYankInitiated = NULL;
2215  duplicateOnNextMouseMove = false;
2216  duplicationDragInProgress = false;
2217  dragStickinessDisabled = false;
2218  emit leftMouseButtonReleased();
2220 
2221  // If there was a cable drag in progress at the time of the left-mouse-button
2222  // release, conclude the drag -- either by connecting the cable to the eligible port
2223  // at which it was dropped, or, if not dropped at an eligible port, by deleting the cable.
2224  bool cableDragEnding = cableInProgress;
2225  if (cableDragEnding)
2226  concludeCableDrag(event);
2227 
2228  QGraphicsItem *item = findNearbyComponent(event->scenePos());
2229  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2230  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
2231  if (port)
2232  {
2233  // Display the port popover, as long as the mouse release did not have any keyboard modifiers
2234  // and did not mark the end of a drag.
2235  // Do so even if a cable drag was technically in progress, because all it takes to initiate
2236  // a cable drag is a mouse press. We could trigger cable drags upon mouse move events instead of
2237  // mouse press events to avoid this.
2238  if ((event->modifiers() == Qt::NoModifier) &&
2239  (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2240  {
2241  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
2242  if (typecastPort)
2243  {
2244  // Since the rendering of the typecast body includes its host port, display popovers for both.
2245  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
2246  enablePopoverForNode(typecastNode);
2247  enableInactivePopoverForPort(typecastPort->getReplacedPort());
2248  }
2249 
2250  else
2251  enableInactivePopoverForPort(port);
2252  }
2253  }
2254 
2255  else // if (!port)
2256  {
2257  if (!cableDragEnding && !node)
2259  }
2260 
2261  if (!cableDragEnding)
2262  QGraphicsScene::mouseReleaseEvent(event);
2263  }
2264 
2265  // Handle non-left-button releases.
2266  else // if (event->button() != Qt::LeftButton)
2267  {
2268  QGraphicsScene::mouseReleaseEvent(event);
2269  }
2270 }
2271 
2278 void VuoEditorComposition::concludeCableDrag(QGraphicsSceneMouseEvent *event)
2279 {
2280  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2281 
2282  // Conclude drags of published cables as a special case.
2283  // @todo: Integrate this more naturally.
2284  if (cableInProgress->isPublished())
2285  {
2286  concludePublishedCableDrag(event);
2287  return;
2288  }
2289 
2290  if (hasFeedbackErrors())
2291  {
2292  cancelCableDrag();
2294  return;
2295  }
2296 
2297  cableInProgress->getRenderer()->setWireless(cableInProgressShouldBeWireless);
2298 
2299  // Input or output port that the cable is being dragged from (i.e., the fixed endpoint).
2300  VuoRendererPort *fixedPort = NULL;
2301  if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2302  fixedPort = cableInProgress->getFromPort()->getRenderer();
2303  else if (cableInProgress->getToNode() && cableInProgress->getToPort())
2304  fixedPort = cableInProgress->getToPort()->getRenderer();
2305 
2306  // We will determine based on the presence or absence of an eligible port near the
2307  // location of the mouse release whether to connect or delete the dragged cable.
2308  bool completedCableConnection = false;
2309 
2310  // Potential side effects of a newly completed cable connection:
2311  VuoCable *dataCableToDisplace = NULL; // A previously existing incoming data+event cable may need to be displaced.
2312  VuoCable *cableToReplace = NULL; // A previously existing cable connecting the same two ports may need to be replaced.
2313  VuoRendererNode *typecastNodeToDelete = NULL; // A previously attached typecast may need to be deleted.
2314  VuoRendererPort *portToUnpublish = NULL; // A previously published port may need to be unpublished.
2315  string typecastToInsert = ""; // A typecast may need to be automatically inserted.
2316 
2317  // A generic port involved in the new connection may need to be specialized.
2318  VuoRendererPort *portToSpecialize = NULL;
2319  string specializedTypeName = "";
2320 
2321  // Input or output port that the cable is being dropped onto, if any.
2322  VuoRendererPort *targetPort = (VuoRendererPort *)findNearbyPort(event->scenePos(), false);
2323 
2324  // Node header area that the cable is being dropped onto, if any.
2325  // (If over both a port drop zone and a node header, the node header gets precedence.)
2326  {
2327  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2328  if (targetNode)
2329  {
2330  targetPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2331  if (targetPort)
2332  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2333  }
2334  }
2335 
2336  bool draggingPreviouslyPublishedCable = (!cableInProgressWasNew &&
2337  (dynamic_cast<VuoRendererPublishedPort *>(fixedPort) ||
2338  ((cableInProgress->getToPort() == NULL) &&
2339  dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort()->getRenderer()))));
2340 
2341  if (fixedPort && targetPort)
2342  {
2343  // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2344  // don't push the operation onto the Undo stack.
2345  bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2346  targetPort->getBase()) &&
2347  (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2348  cableInProgress->getCompiler()->getAlwaysEventOnly()));
2349  if (recreatingSameConnection)
2350  {
2351  revertCableDrag();
2352  return;
2353  }
2354 
2355  bool cableInProgressExpectedToCarryData = cableInProgress->getRenderer()->effectivelyCarriesData() &&
2356  targetPort->getDataType();
2357 
2358  VuoCable *preexistingCable = fixedPort->getCableConnectedTo(targetPort, false);
2359  bool preexistingCableWithMatchingDataCarryingStatus = (preexistingCable?
2360  (preexistingCable->getRenderer()->effectivelyCarriesData() ==
2361  cableInProgressExpectedToCarryData) :
2362  false);
2363 
2364  // Case: Replacing a pre-existing cable that connected the same two ports
2365  // but with a different data-carrying status, and whose "To" port is
2366  // the child port of a collapsed typecast.
2367  if (preexistingCable && !preexistingCableWithMatchingDataCarryingStatus &&
2368  preexistingCable->getToPort()->hasRenderer() &&
2369  preexistingCable->getToPort()->getRenderer()->getTypecastParentPort())
2370  {
2371  // @todo Implement for https://b33p.net/kosada/node/14153
2372  // For now, don't attempt it.
2373  revertCableDrag();
2374  return;
2375  }
2376 
2377  // Case: Completing a "forward" cable connection from an output port to an input port
2378  if (!preexistingCableWithMatchingDataCarryingStatus &&
2379  (fixedPort->canConnectDirectlyWithoutSpecializationTo(targetPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2380  selectBridgingSolution(fixedPort, targetPort, true, &portToSpecialize, specializedTypeName, typecastToInsert)))
2381  {
2382  // If input port had a connected collapsed typecast, uncollapse it.
2383  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2384  if (typecastPort)
2385  {
2386  VuoRendererPort *adjustedTargetPort = typecastPort->getReplacedPort();
2387  VuoRendererPort *childPort = typecastPort->getChildPort();
2388  VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2389  VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2390 
2391  targetPort = adjustedTargetPort;
2392 
2393  // If the typecast did not have multiple incoming cables, and the new cable will
2394  // be replacing it as a data source, delete the typecast.
2395  if (cableInProgressExpectedToCarryData &&
2396  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2397  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2398  typecastNodeToDelete = uncollapsedTypecast;
2399  }
2400 
2401  // If connecting two ports that already had a cable (of different data-carrying status) connecting them,
2402  // replace the pre-existing cable.
2403  if (preexistingCable)
2404  cableToReplace = preexistingCable;
2405 
2406  // If the cable carries data, determine what other sources of input data need to be
2407  // removed before completing this connection.
2408  if (cableInProgressExpectedToCarryData)
2409  {
2410  // If input port already had a connected data cable, delete that cable.
2411  vector<VuoCable *> previousConnectedCables = targetPort->getBase()->getConnectedCables(false);
2412  for (vector<VuoCable *>::iterator cable = previousConnectedCables.begin(); (! dataCableToDisplace) && (cable != previousConnectedCables.end()); ++cable)
2413  if ((((*cable)->getRenderer()->effectivelyCarriesData()) && (*cable) != cableToReplace))
2414  dataCableToDisplace = *cable;
2415 
2416  // If the input port was published as a data+event port, unpublish it.
2417  // @todo: Let published cable disconnection handle port unpublication.
2418  if (isPortPublished(targetPort))
2419  {
2420  vector<VuoRendererPublishedPort *> publishedDataConnections = targetPort->getPublishedPortsConnectedByDataCarryingCables();
2421  if (publishedDataConnections.size() > 0)
2422  portToUnpublish = targetPort;
2423  }
2424  }
2425 
2426  completedCableConnection = true;
2427  }
2428 
2429  // Case: Completing a "backward" cable connection from an input port to an output port
2430  else if (!preexistingCableWithMatchingDataCarryingStatus &&
2431  (targetPort->canConnectDirectlyWithoutSpecializationTo(fixedPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2432  selectBridgingSolution(targetPort, fixedPort, false, &portToSpecialize, specializedTypeName, typecastToInsert)))
2433 
2434  {
2435  completedCableConnection = true;
2436  }
2437  }
2438 
2439  // Complete the actual cable connection, if applicable.
2440  if (completedCableConnection)
2441  {
2442  emit undoStackMacroBeginRequested("Cable Connection");
2443 
2444  if (draggingPreviouslyPublishedCable)
2445  {
2446  // Record some information about the cable in progress, to create a replica.
2447  VuoNode *cableInProgressFromNode = cableInProgress->getFromNode();
2448  VuoNode *cableInProgressToNode = cableInProgress->getToNode();
2449  VuoPort *cableInProgressFromPort = cableInProgress->getFromPort();
2450  VuoPort *cableInProgressToPort = cableInProgress->getToPort();
2451  QPointF cableInProgressFloatingEndpointLoc = cableInProgress->getRenderer()->getFloatingEndpointLoc();
2452  bool cableInProgressAlwaysEventOnly = cableInProgress->getCompiler()->getAlwaysEventOnly();
2453 
2454  // Remove the original cable, unpublishing the port in the process.
2455  cancelCableDrag();
2456 
2457  // Restore a copy of the cable to participate in the new connection.
2458  VuoCable *cableInProgressCopy = (new VuoCompilerCable(NULL,
2459  NULL,
2460  NULL,
2461  NULL))->getBase();
2462 
2463  cableInProgressCopy->setFrom(cableInProgressFromNode, cableInProgressFromPort);
2464  cableInProgressCopy->setTo(cableInProgressToNode, cableInProgressToPort);
2465 
2466  addCable(cableInProgressCopy);
2467 
2468  cableInProgressCopy->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
2469  cableInProgressCopy->getRenderer()->setFloatingEndpointLoc(cableInProgressFloatingEndpointLoc);
2470 
2471  cableInProgress = cableInProgressCopy;
2472  cableInProgressWasNew = true;
2473  }
2474 
2475  // Workaround to avoid Qt's parameter count limit for signals/slots.
2476  pair<VuoRendererCable *, VuoRendererCable *> cableArgs = std::make_pair((dataCableToDisplace? dataCableToDisplace->getRenderer() : NULL),
2477  (cableToReplace? cableToReplace->getRenderer() : NULL));
2478  pair<string, string> typeArgs = std::make_pair(typecastToInsert, specializedTypeName);
2479  pair<VuoRendererPort *, VuoRendererPort *> portArgs = std::make_pair(portToUnpublish, portToSpecialize);
2480 
2481  emit connectionCompletedByDragging(cableInProgress->getRenderer(),
2482  targetPort,
2483  cableArgs,
2484  typecastNodeToDelete,
2485  typeArgs,
2486  portArgs);
2487 
2489 
2490  // Resume caching for dragged cable.
2491  if (cableInProgress)
2492  {
2493  cableInProgress->getRenderer()->updateGeometry();
2494  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2495  cableInProgress = NULL;
2496  }
2497  }
2498 
2499  // Otherwise, delete the cable.
2500  else // if (! completedCableConnection)
2501  cancelCableDrag();
2502 
2503  emit cableDragEnded();
2504 }
2505 
2509 void VuoEditorComposition::concludePublishedCableDrag(QGraphicsSceneMouseEvent *event)
2510 {
2511  VuoRendererPort *fixedPort;
2512  if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2513  fixedPort = cableInProgress->getFromPort()->getRenderer();
2514  else // if (cableInProgress->getToNode() && cableInProgress->getToPort())
2515  fixedPort = cableInProgress->getToPort()->getRenderer();
2516 
2517  // Potential side effects of a newly completed cable connection:
2518  // - A typecast may need to be automatically inserted.
2519  string typecastToInsert = "";
2520 
2521  // - A generic port involved in the new connection may need to be specialized.
2522  VuoRendererPort *portToSpecialize = NULL;
2523  string specializedTypeName = "";
2524 
2525  bool forceEventOnlyPublication = !cableInProgress->getRenderer()->effectivelyCarriesData();
2526 
2527  // Case: Initiating a cable drag from a published input port.
2528  if (cableInProgress && cableInProgress->isPublishedInputCable())
2529  {
2530  VuoRendererPort *internalInputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2531  VuoRendererPublishedPort *publishedInputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getFromPort()->getRenderer());
2532 
2533  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first input port.
2534  // (If over both a port drop zone and a node header, the node header gets precedence.)
2535  {
2536  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2537  if (targetNode)
2538  {
2539  internalInputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2540  if (internalInputPort)
2541  {
2542  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2543  forceEventOnlyPublication = true;
2544  }
2545  }
2546  }
2547 
2548  // Case: Cable was dropped onto an internal input port
2549  if (internalInputPort &&
2550  publishedInputPort)
2551  {
2552  // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2553  // don't push the operation onto the Undo stack.
2554  bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2555  internalInputPort->getBase()) &&
2556  (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2557  cableInProgress->getCompiler()->getAlwaysEventOnly()));
2558  if (recreatingSameConnection)
2559  {
2560  revertCableDrag();
2561  return;
2562  }
2563 
2564  // Case: Ports are compatible
2565  if ((publishedInputPort->isCompatibleAliasWithSpecializationForInternalPort(internalInputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName))
2566  || selectBridgingSolution(publishedInputPort, internalInputPort, true, &portToSpecialize, specializedTypeName, typecastToInsert))
2567  {
2568  bool cableInProgressExpectedToCarryData = (cableInProgress->getRenderer()->effectivelyCarriesData() &&
2569  internalInputPort->getDataType());
2570  VuoCable *cableToReplace = publishedInputPort->getCableConnectedTo(internalInputPort, true);
2571  bool cableToReplaceHasMatchingDataCarryingStatus = (cableToReplace?
2572  (cableToReplace->getRenderer()->effectivelyCarriesData() ==
2573  cableInProgressExpectedToCarryData) :
2574  false);
2575 
2576  // If replacing a preexisting cable with an identical cable, just cancel the operation
2577  // so that it doesn't go onto the Undo stack.
2578  if (cableToReplace && cableToReplaceHasMatchingDataCarryingStatus)
2579  cancelCableDrag();
2580 
2581  // Case: Replacing a pre-existing cable that connected the same two ports
2582  // but with a different data-carrying status, and whose "To" port is
2583  // the child port of a collapsed typecast.
2584  else if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus &&
2585  cableToReplace->getToPort()->hasRenderer() &&
2586  cableToReplace->getToPort()->getRenderer()->getTypecastParentPort())
2587  {
2588  // @todo Implement for https://b33p.net/kosada/node/14153
2589  // For now, don't attempt it.
2590  cancelCableDrag();
2591  }
2592 
2593  else
2594  {
2595  emit undoStackMacroBeginRequested("Cable Connection");
2596  cancelCableDrag();
2597 
2598  // If this source/target port combination already a cable connecting them, but of a different
2599  // data-carrying status, replace the old cable with the new one.
2600  if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus)
2601  {
2602  QList<QGraphicsItem *> removedComponents;
2603  removedComponents.append(cableToReplace->getRenderer());
2604  emit componentsRemoved(removedComponents, "Delete");
2605  }
2606 
2607  // If the target port had a connected collapsed typecast, uncollapse and delete it.
2608  // But first check whether the collapsed typecast is still present on canvas -- it's possible it was
2609  // already removed during the cancelCableDrag() call, if the cable being dragged was the
2610  // typecast's incoming data+event cable.
2611  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(internalInputPort);
2612  if (typecastPort && typecastPort->scene())
2613  {
2614  VuoRendererPort *childPort = typecastPort->getChildPort();
2615  VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2616  VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2617 
2618  // If the typecast did not have multiple incoming cables, and the new cable will
2619  // be replacing it as a data source, delete the typecast.
2620  if (cableInProgressExpectedToCarryData &&
2621  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2622  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2623  {
2624  QList<QGraphicsItem *> removedComponents;
2625  removedComponents.append(uncollapsedTypecast);
2626  emit componentsRemoved(removedComponents, "Delete");
2627  }
2628  }
2629 
2630  emit portPublicationRequested(internalInputPort->getBase(),
2631  dynamic_cast<VuoPublishedPort *>(publishedInputPort->getBase()),
2632  forceEventOnlyPublication,
2633  (portToSpecialize? portToSpecialize->getBase() : NULL),
2634  specializedTypeName,
2635  typecastToInsert,
2636  false); // Avoid nested Undo stack macros.
2638  }
2639  }
2640  else // Source port and target port aren't compatible
2641  cancelCableDrag();
2642  }
2643  else // Cable was dropped over empty canvas space
2644  cancelCableDrag();
2645  }
2646 
2647  // Case: Initiating a cable drag from a published output port.
2648  else if (cableInProgress && cableInProgress->isPublishedOutputCable())
2649  {
2650  VuoRendererPort *internalOutputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2651  VuoRendererPublishedPort *publishedOutputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getToPort()->getRenderer());
2652 
2653  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first output port.
2654  // (If over both a port drop zone and a node header, the node header gets precedence.)
2655  {
2656  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2657  if (targetNode)
2658  {
2659  internalOutputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2660  if (internalOutputPort)
2661  {
2662  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2663  forceEventOnlyPublication = true;
2664  }
2665  }
2666  }
2667 
2668  // Case: Cable was dropped onto an internal output port
2669  if (internalOutputPort &&
2670  publishedOutputPort)
2671  {
2672 
2673  // Case: Ports are compatible
2674  if ((publishedOutputPort->isCompatibleAliasWithSpecializationForInternalPort(internalOutputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName) &&
2675  publishedOutputPort->canAccommodateInternalPort(internalOutputPort, forceEventOnlyPublication))
2676  || selectBridgingSolution(internalOutputPort, publishedOutputPort, false, &portToSpecialize, specializedTypeName, typecastToInsert))
2677  {
2678  emit portPublicationRequested(internalOutputPort->getBase(),
2679  dynamic_cast<VuoPublishedPort *>(publishedOutputPort->getBase()),
2680  forceEventOnlyPublication,
2681  portToSpecialize? portToSpecialize->getBase() : NULL,
2682  specializedTypeName,
2683  typecastToInsert,
2684  true);
2685  }
2686  }
2687 
2688  cancelCableDrag();
2689  }
2690 
2691  emit cableDragEnded();
2692 }
2693 
2698 {
2700 
2701  if (! cableInProgress)
2702  return;
2703 
2704  if (cableInProgressWasNew)
2705  removeCable(cableInProgress->getRenderer());
2706  else
2707  {
2708  QList<QGraphicsItem *> removedComponents;
2709  removedComponents.append(cableInProgress->getRenderer());
2710  emit componentsRemoved(removedComponents, "Delete");
2711  }
2712 
2713  cableInProgress = NULL;
2714 }
2715 
2723 {
2724  if (cableInProgress && cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort())
2725  {
2726  cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2727  cableInProgress->getRenderer()->updateGeometry();
2728  cableInProgress->setTo(getUnderlyingParentNodeForPort(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort(), this),
2729  cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort());
2730  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2731  cableInProgress = NULL;
2732  }
2733  else if (cableInProgress)
2734  {
2735  cancelCableDrag();
2736  }
2737 
2740  emit cableDragEnded();
2741 }
2742 
2746 void VuoEditorComposition::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
2747 {
2748  bool leftMouseButtonPressed = (event->buttons() & Qt::LeftButton);
2749 
2750  // Disable Mission Control workaround, since it sometimes misdetects whether the left mouse
2751  // button is pressed and overzealously cancels cable drags (specifically, those originating from
2752  // sidebar published output ports). The workaround no longer appears to be effective as of Qt 5.2.1 anyway.
2753  /*
2754  // In the unlikely situation that we have a cable drag in progress but the left mouse
2755  // button is not pressed (e.g., if "Mission Control" was activated during a cable drag and
2756  // deactivated by releasing the mouse button), cancel the cable drag.
2757  // See https://b33p.net/kosada/node/3305
2758  if (cableInProgress && !menuSelectionInProgress && !leftMouseButtonPressed)
2759  cancelCableDrag();
2760  */
2761 
2762  // If in the process of a cable drag or mouse-over operation,
2763  // locate the nearest port or cable eligible for hover highlighting.
2764  if (cableInProgress || (! leftMouseButtonPressed))
2765  updateHoverHighlighting(event->scenePos());
2766 
2767 
2768  // Case: Left mouse button pressed
2769  if (leftMouseButtonPressed)
2770  {
2771  // Eliminate mouse jitter noise.
2772  if ((! dragStickinessDisabled) && (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2773  return;
2774 
2775  else
2776  dragStickinessDisabled = true;
2777 
2778  // If a port or cable has been clicked for dragging and this is the first mouse move event
2779  // since the click, initiate the cable drag.
2780  if (portWithDragInitiated || cableWithYankInitiated)
2781  {
2782  initiateCableDrag(portWithDragInitiated, cableWithYankInitiated, event);
2783  portWithDragInitiated = NULL;
2784  cableWithYankInitiated = NULL;
2785  }
2786 
2787  // If there is a cable drag in progress, update the location of the cable's
2788  // floating endpoint to match that of the cursor.
2789  if (cableInProgress)
2790  {
2791  VuoRendererCable *rc = cableInProgress->getRenderer();
2792 
2793  rc->updateGeometry();
2794  rc->setFloatingEndpointLoc(event->scenePos());
2795 
2797  }
2798 
2799  else if (duplicationDragInProgress)
2800  {
2801  if (duplicateOnNextMouseMove)
2802  {
2804  duplicateOnNextMouseMove = false;
2805  }
2806 
2807  QPointF newPos = (VuoRendererItem::getSnapToGrid()?
2810  event->scenePos());
2811  QPointF delta = newPos - cursorPosBeforeDuplicationDragMove;
2812  moveItemsBy(getSelectedNodes(), getSelectedComments(), delta.x(), delta.y(), false);
2813  cursorPosBeforeDuplicationDragMove = newPos;
2814  }
2815 
2816  else
2817  QGraphicsScene::mouseMoveEvent(event);
2818  }
2819 
2820  // Case: Left mouse button not pressed
2821  else
2822  QGraphicsScene::mouseMoveEvent(event);
2823 }
2824 
2830 void VuoEditorComposition::updateHoverHighlighting(QPointF scenePos, bool disablePortHoverHighlighting)
2831 {
2832  auto types = compiler->getTypes();
2833 
2834  // Detect cable and port hover events ourselves, since we need to account
2835  // for their extended hover ranges.
2836  QGraphicsItem *item = cableInProgress? findNearbyPort(scenePos, false) : findNearbyComponent(scenePos);
2837  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
2838  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2839  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(item);
2840  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(item);
2841 
2842  // If hovering over a node header area while dragging a cable, treat it like hovering over the first input or output port.
2843  // (If over both a port drop zone and a node header, the node header gets precedence.)
2844  bool hoveringOverNodeHeader = false;
2845  if (cableInProgress)
2846  {
2847  VuoRendererNode *targetNode = findNearbyNodeHeader(scenePos);
2848  if (targetNode)
2849  {
2850  port = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2851  if (port)
2852  {
2853  item = targetNode;
2854  hoveringOverNodeHeader = true;
2855  }
2856  }
2857  }
2858 
2859  VuoRendererNode *node = NULL;
2860  if (! hoveringOverNodeHeader)
2861  {
2862  // Do not handle hover events for (non-drawer) nodes here.
2863  if (dynamic_cast<VuoRendererNode *>(item) && !makeListDrawer)
2864  item = NULL;
2865 
2866  node = dynamic_cast<VuoRendererNode *>(item);
2867  }
2868 
2869  // Case: The item (if any) that requires hover-highlighting given the current position of the cursor
2870  // is not the same as the item (if any) that was hover-highlighted previously.
2871  if (item != previousNearbyItem)
2872  {
2873  VuoRendererPort *previousPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2874  VuoRendererNode *previousNode = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2875  if (previousNode)
2876  previousPort = findTargetPortForCableDroppedOnNodeHeader(previousNode);
2877 
2878  // End hover-highlighting of the previous item, if any.
2879  if (previousNearbyItem)
2881 
2882  // Begin hover-highlighting of the current item, if any.
2883  if (cable)
2884  cable->extendedHoverEnterEvent();
2885  else if (node)
2886  {
2887  if (! cableInProgress)
2888  {
2889  if (makeListDrawer)
2890  makeListDrawer->extendedHoverEnterEvent(scenePos);
2891  }
2892  }
2893  else if (typecastPort && !disablePortHoverHighlighting)
2894  port->extendedHoverEnterEvent((bool)cableInProgress);
2895  else if (port && !disablePortHoverHighlighting)
2896  {
2897  port->extendedHoverEnterEvent((bool)cableInProgress);
2898  if (!cableInProgress)
2899  {
2900  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2901  connectedAntennaPort->extendedHoverEnterEvent((bool)cableInProgress, true);
2902  }
2903  }
2904 
2905  VuoRendererPort *previousTargetPort = (previousPort && previousPort->isEligibleForConnection() ? previousPort : NULL);
2906 
2907  // If the current item or previous item is a port and it got this status because the mouse hovered
2908  // over a node header, update the eligibility-highlighting of the port.
2909  if (cableInProgress)
2910  {
2911  VuoRendererPort *fixedPort = (cableInProgress->getFromPort() ?
2912  cableInProgress->getFromPort()->getRenderer() :
2913  cableInProgress->getToPort()->getRenderer());
2914 
2915  QList< QPair<VuoRendererPort *, bool> > updatedPorts;
2916  if (hoveringOverNodeHeader)
2917  updatedPorts.append( QPair<VuoRendererPort *, bool>(port, true) );
2918  if (previousNode && previousPort)
2919  updatedPorts.append( QPair<VuoRendererPort *, bool>(previousPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) );
2920 
2921  QPair<VuoRendererPort *, bool> p;
2922  foreach (p, updatedPorts)
2923  {
2924  VuoRendererPort *updatedPort = p.first;
2925 
2926  VuoRendererPort *typecastParentPort = updatedPort->getTypecastParentPort();
2927  if (typecastParentPort)
2928  updatedPort = typecastParentPort;
2929 
2930  updateEligibilityHighlightingForPort(updatedPort, fixedPort, p.second, types);
2931 
2932  VuoRendererNode *potentialDrawer = updatedPort->getUnderlyingParentNode();
2933  updateEligibilityHighlightingForNode(potentialDrawer);
2934  }
2935  }
2936 
2937  VuoRendererPort *targetPort = (port && port->isEligibleForConnection() ? port : NULL);
2938 
2939  // Update error dialogs and cable's data-carrying status.
2940  if (targetPort || previousTargetPort)
2941  {
2942  if (cableInProgress)
2943  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(targetPort && (!targetPort->getDataType() || hoveringOverNodeHeader));
2944 
2945  updateFeedbackErrors(targetPort);
2946  }
2947 
2948  previousNearbyItem = item;
2949  }
2950 
2951  // Case: The previously hover-highlighted item should remain hover-highlighted.
2952  else if (item)
2953  {
2954  if (cable)
2955  cable->extendedHoverMoveEvent();
2956  else if (port && !disablePortHoverHighlighting)
2957  {
2958  port->extendedHoverMoveEvent((bool)cableInProgress);
2959  if (!cableInProgress)
2960  {
2961  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2962  connectedAntennaPort->extendedHoverMoveEvent((bool)cableInProgress, true);
2963  }
2964  }
2965  else if (makeListDrawer)
2966  makeListDrawer->extendedHoverMoveEvent(scenePos);
2967  }
2968 }
2969 
2974 {
2975  if (previousNearbyItem)
2976  {
2977  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(previousNearbyItem);
2978  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2979  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(previousNearbyItem);
2980  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2981 
2982  if (node)
2983  {
2984  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(previousNearbyItem);
2985  if (drawer)
2986  drawer->extendedHoverLeaveEvent();
2987  else
2989  }
2990 
2991  if (cable)
2992  cable->extendedHoverLeaveEvent();
2993  else if (typecastPort)
2994  port->extendedHoverLeaveEvent();
2995  else if (port)
2996  {
2997  port->extendedHoverLeaveEvent();
2998  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2999  connectedAntennaPort->extendedHoverLeaveEvent();
3000  }
3001 
3002  previousNearbyItem = NULL;
3003  }
3004 }
3005 
3010 {
3011  if ((event->key() != Qt::Key_Alt) && (event->key() != Qt::Key_Shift) && (event->key() != Qt::Key_Escape))
3012  cancelCableDrag();
3013 
3014  Qt::KeyboardModifiers modifiers = event->modifiers();
3015  qreal adjustedNodeMoveRate = nodeMoveRate;
3016  if (modifiers & Qt::ShiftModifier)
3017  {
3018  adjustedNodeMoveRate *= nodeMoveRateMultiplier;
3019  }
3020 
3021  switch (event->key()) {
3022  case Qt::Key_Backspace:
3023  {
3025  break;
3026  }
3027  case Qt::Key_Delete:
3028  {
3030  break;
3031  }
3032  case Qt::Key_Up:
3033  {
3034  moveSelectedItemsBy(0, -1*adjustedNodeMoveRate);
3035  break;
3036  }
3037  case Qt::Key_Down:
3038  {
3039  if (modifiers & Qt::ControlModifier)
3040  openSelectedEditableNodes();
3041  else
3042  moveSelectedItemsBy(0, adjustedNodeMoveRate);
3043  break;
3044  }
3045  case Qt::Key_Left:
3046  {
3047  moveSelectedItemsBy(-1*adjustedNodeMoveRate, 0);
3048  break;
3049  }
3050  case Qt::Key_Right:
3051  {
3052  moveSelectedItemsBy(adjustedNodeMoveRate, 0);
3053  break;
3054  }
3055  case Qt::Key_Shift:
3056  {
3057  if (cableInProgress)
3058  {
3059  cableInProgress->getRenderer()->updateGeometry();
3060  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
3061  highlightEligibleEndpointsForCable(cableInProgress);
3062 
3063  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3064  if (hoveredPort)
3065  updateFeedbackErrors(hoveredPort);
3066  }
3067 
3068  break;
3069  }
3070  case Qt::Key_Enter:
3071  case Qt::Key_Return:
3072  {
3073  // Make sure the event is forwarded to any composition component within hover range.
3074  QGraphicsScene::keyPressEvent(event);
3075 
3076  if (!event->isAccepted())
3077  {
3078  // Otherwise, if there are any (and only) nodes currently selected, open a title editor for each one;
3079  // if there are any (and only) comments currently selected, open a comment text editor for each one.
3080  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
3081  set<VuoRendererComment *> selectedComments = getSelectedComments();
3082 
3083  if (selectedComments.empty() && !selectedNodes.empty())
3085  }
3086 
3087  break;
3088  }
3089 
3090  case Qt::Key_Escape:
3091  {
3092  if (duplicateOnNextMouseMove || duplicationDragInProgress)
3093  {
3095 
3096  duplicateOnNextMouseMove = false;
3097  duplicationDragInProgress = false;
3098  duplicationCancelled = true;
3099  }
3100  else if (cableInProgress)
3101  {
3102  revertCableDrag();
3103  }
3104  break;
3105  }
3106 
3107  default:
3108  {
3109  QGraphicsScene::keyPressEvent(event);
3110  break;
3111  }
3112  }
3113 }
3114 
3119 {
3120  switch (event->key()) {
3121  case Qt::Key_Shift:
3122  {
3123  if (cableInProgress)
3124  {
3125  cableInProgress->getRenderer()->updateGeometry();
3126  cableInProgress->getCompiler()->setAlwaysEventOnly(false);
3127  highlightEligibleEndpointsForCable(cableInProgress);
3128 
3129  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3130  if (hoveredPort)
3131  updateFeedbackErrors(hoveredPort);
3132  }
3133 
3134  break;
3135  }
3136 
3137  default:
3138  {
3139  QGraphicsScene::keyReleaseEvent(event);
3140  break;
3141  }
3142  }
3143 }
3144 
3148 void VuoEditorComposition::addActionToMenuAndMapper(QMenu *menu, QSignalMapper *mapper, QString name, int index)
3149 {
3150  QAction *action = new QAction(name, this);
3151  menu->addAction(action);
3152  mapper->setMapping(action, index);
3153  connect(action, &QAction::triggered, mapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
3154 }
3155 
3159 void VuoEditorComposition::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
3160 {
3161  // Determine whether the cursor is in range of any operable composition components.
3162  QGraphicsItem *item = findNearbyComponent(event->scenePos());
3163 
3165  contextMenu.setSeparatorsCollapsible(false);
3166 
3167  // Customize context menu for the canvas.
3168  if (! item)
3169  {
3170  QAction *insertNodeSection = new QAction(tr("Insert Node"), NULL);
3171  insertNodeSection->setEnabled(false);
3172  contextMenu.addAction(insertNodeSection);
3173 
3174  QString spacer(" ");
3175 
3176  {
3177  // "Share" nodes
3178  QMenu *shareMenu = new QMenu(&contextMenu);
3179  shareMenu->setSeparatorsCollapsible(false);
3180  shareMenu->setTitle(spacer + tr("Share"));
3181  contextMenu.addMenu(shareMenu);
3182 
3183  {
3184  QAction *action = new QAction(tr("Share Value"), NULL);
3185  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share"))));
3186  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3187  shareMenu->addAction(action);
3188  }
3189 
3190  {
3191  QAction *action = new QAction(tr("Share List"), NULL);
3192  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share.list"))));
3193  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3194  shareMenu->addAction(action);
3195  }
3196 
3197  {
3198  QAction *action = new QAction(tr("Share Event"), NULL);
3199  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.share"))));
3200  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3201  shareMenu->addAction(action);
3202  }
3203  }
3204 
3205  {
3206  // "Hold" nodes
3207  QMenu *holdMenu = new QMenu(&contextMenu);
3208  holdMenu->setSeparatorsCollapsible(false);
3209  holdMenu->setTitle(spacer + tr("Hold"));
3210  contextMenu.addMenu(holdMenu);
3211 
3212  {
3213  QAction *action = new QAction(tr("Hold Value"), NULL);
3214  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold2"))));
3215  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3216  holdMenu->addAction(action);
3217  }
3218 
3219  {
3220  QAction *action = new QAction(tr("Hold List"), NULL);
3221  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold.list2"))));
3222  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3223  holdMenu->addAction(action);
3224  }
3225  }
3226 
3227  {
3228  // "Allow" nodes
3229  QMenu *allowMenu = new QMenu(&contextMenu);
3230  allowMenu->setSeparatorsCollapsible(false);
3231  allowMenu->setTitle(spacer + tr("Allow"));
3232  contextMenu.addMenu(allowMenu);
3233 
3234  {
3235  QAction *action = new QAction(tr("Allow First Event"), NULL);
3236  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirst"))));
3237  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3238  allowMenu->addAction(action);
3239  }
3240 
3241  {
3242  QAction *action = new QAction(tr("Allow First Value"), NULL);
3243  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirstValue"))));
3244  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3245  allowMenu->addAction(action);
3246  }
3247 
3248  {
3249  QAction *action = new QAction(tr("Allow Periodic Events"), NULL);
3250  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.time.allowPeriodic"))));
3251  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3252  allowMenu->addAction(action);
3253  }
3254 
3255  {
3256  QAction *action = new QAction(tr("Allow Changes"), NULL);
3257  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowChanges2"))));
3258  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3259  allowMenu->addAction(action);
3260  }
3261  }
3262 
3263  contextMenu.addSeparator();
3264 
3265  QAction *contextMenuInsertComment = new QAction(NULL);
3266  contextMenuInsertComment->setText(tr("Insert Comment"));
3267  contextMenuInsertComment->setData(qVariantFromValue(event->scenePos()));
3268  connect(contextMenuInsertComment, &QAction::triggered, this, &VuoEditorComposition::insertComment);
3269  contextMenu.addAction(contextMenuInsertComment);
3270 
3271  QAction *contextMenuInsertSubcomposition = new QAction(NULL);
3272  contextMenuInsertSubcomposition->setText(tr("Insert Subcomposition"));
3273  contextMenuInsertSubcomposition->setData(qVariantFromValue(event->scenePos()));
3274  connect(contextMenuInsertSubcomposition, &QAction::triggered, this, &VuoEditorComposition::insertSubcomposition);
3275  contextMenu.addAction(contextMenuInsertSubcomposition);
3276  }
3277 
3278  // Prepare to take a snapshot of the contextMenuDeleteSelection QAction's current values, so that
3279  // they do not change while the context menu is displayed.
3280  QAction *contextMenuDeleteSelectedSnapshot = new QAction(NULL);
3281 
3282  // Customize context menu for ports.
3283  if (dynamic_cast<VuoRendererPort *>(item))
3284  {
3285  VuoRendererPort *port = (VuoRendererPort *)item;
3286 
3287  if (port->isConstant() && inputEditorManager)
3288  {
3289  VuoType *dataType = static_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler())->getDataVuoType();
3290  VuoInputEditor *inputEditorLoadedForPortDataType = inputEditorManager->newInputEditor(dataType);
3291  if (inputEditorLoadedForPortDataType)
3292  {
3293  contextMenuSetPortConstant->setData(qVariantFromValue((void *)port));
3294  contextMenu.addAction(contextMenuSetPortConstant);
3295 
3296  inputEditorLoadedForPortDataType->deleteLater();
3297  }
3298  }
3299 
3300  if (!isPortPublished(port) && port->getPublishable())
3301  {
3302  contextMenuPublishPort->setText(tr("Publish Port"));
3303  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3304  contextMenu.addAction(contextMenuPublishPort);
3305  }
3306 
3307  else if (isPortPublished(port))
3308  {
3309  vector<VuoRendererPublishedPort *> externalPublishedPorts = port->getPublishedPorts();
3310  bool hasExternalPublishedPortWithMultipleInternalPorts = false;
3311  bool hasExternalPublishedPortBelongingToActiveProtocol = false;
3312  foreach (VuoRendererPublishedPort *externalPort, externalPublishedPorts)
3313  {
3314  if (externalPort->getBase()->getConnectedCables(true).size() > 1)
3315  hasExternalPublishedPortWithMultipleInternalPorts = true;
3316 
3317  if (dynamic_cast<VuoPublishedPort *>(externalPort->getBase())->isProtocolPort())
3318  hasExternalPublishedPortBelongingToActiveProtocol = true;
3319  }
3320 
3321  // Omit the "Delete Published Port" context menu item if the internal port is connected to
3322  // an external published port that cannot be deleted, either because:
3323  // - It is part of an active protocol;
3324  // - It has more than one connected cable.
3325  if (!hasExternalPublishedPortWithMultipleInternalPorts &&!hasExternalPublishedPortBelongingToActiveProtocol)
3326  {
3327  contextMenuPublishPort->setText(tr("Delete Published Port"));
3328  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3329  contextMenu.addAction(contextMenuPublishPort);
3330  }
3331  }
3332 
3333  bool isTriggerPort = dynamic_cast<VuoCompilerTriggerPort *>(port->getBase()->getCompiler());
3334  if (isTriggerPort || port->getInput())
3335  {
3336  __block bool isTopLevelCompositionRunning = false;
3337  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
3338  {
3339  isTopLevelCompositionRunning = topLevelComposition->isRunning();
3340  });
3341 
3342  if (isTopLevelCompositionRunning)
3343  {
3344  contextMenuFireEvent->setData(qVariantFromValue((void *)port));
3345  contextMenu.addAction(contextMenuFireEvent);
3346  }
3347  }
3348 
3349  if (isTriggerPort)
3350  {
3351  QMenu *contextMenuThrottling = new QMenu(&contextMenu);
3352  contextMenuThrottling->setSeparatorsCollapsible(false);
3353  contextMenuThrottling->setTitle(tr("Set Event Throttling"));
3355  foreach (QAction *action, contextMenuThrottlingActions)
3356  {
3357  contextMenuThrottling->addAction(action);
3358  action->setData(qVariantFromValue((void *)port));
3359  action->setCheckable(true);
3360  action->setChecked( i++ == port->getBase()->getEventThrottling() );
3361  }
3362  contextMenu.addMenu(contextMenuThrottling);
3363  }
3364 
3365  // Allow the user to specialize, respecialize, or unspecialize generic data types.
3366  VuoGenericType *genericDataType = dynamic_cast<VuoGenericType *>(port->getDataType());
3367  if (genericDataType || isPortCurrentlyRevertible(port))
3368  {
3369  QMenu *contextMenuSpecializeGenericType = new QMenu(&contextMenu);
3370  contextMenuSpecializeGenericType->setSeparatorsCollapsible(false);
3371  contextMenuSpecializeGenericType->setTitle(tr("Set Data Type"));
3372  contextMenuSpecializeGenericType->setToolTipsVisible(true);
3373 
3374  QAction *unspecializeAction = contextMenuSpecializeGenericType->addAction(tr("Generic"));
3375 
3376  // It is safe to assume that there will be types listed after the "Generic" option
3377  // since any network of connected generic/specialized ports has at least one compatible
3378  // data type in common, so the separator can be added here without forward-checking.
3379  contextMenuSpecializeGenericType->addSeparator();
3380 
3381  unspecializeAction->setData(qVariantFromValue((void *)port));
3382  unspecializeAction->setCheckable(true);
3383  unspecializeAction->setChecked(genericDataType);
3384 
3385  contextMenu.addMenu(contextMenuSpecializeGenericType);
3386 
3387  VuoGenericType *genericTypeFromPortClass = NULL; // Original generic type of the port
3388  set<string> compatibleTypes; // Compatible types in the context of the connected generic port network
3389  set<string> compatibleTypesInIsolation; // Compatible types for the port in isolation
3390 
3391  // Allow the user to specialize generic ports.
3392  if (genericDataType)
3393  {
3394  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(port->getBase()->getClass()->getCompiler());
3395  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3396 
3397  // Determine compatible types in the context of the connected generic port network.
3398  VuoGenericType::Compatibility compatibility;
3399  vector<string> compatibleTypesVector = genericDataType->getCompatibleSpecializedTypes(compatibility);
3400  compatibleTypes = set<string>(compatibleTypesVector.begin(), compatibleTypesVector.end());
3401 
3402  // If all types or all list types are compatible, add them to (currently empty) compatibleTypes.
3403  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3404  {
3405  vector<string> compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3406  compatibleTypes.insert(compatibleTypeNames.begin(), compatibleTypeNames.end());
3407  }
3408  }
3409 
3410  // Allow the user to re-specialize already specialized ports.
3411  else if (isPortCurrentlyRevertible(port))
3412  {
3413  map<VuoNode *, string> nodesToReplace;
3414  set<VuoCable *> cablesToDelete;
3415  createReplacementsToUnspecializePort(port->getBase(), false, nodesToReplace, cablesToDelete);
3416  if (cablesToDelete.size() >= 1)
3417  {
3418  // @todo https://b33p.net/kosada/node/8895 : Note that this specialization will break connections.
3419  }
3420 
3421  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(
3422  port->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler());
3423  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(port->getBase()->getClass()) );
3424  compatibleTypes = getRespecializationOptionsForPortInNetwork(port);
3425  }
3426 
3427  // Determine compatible types in isolation.
3428  if (genericTypeFromPortClass)
3429  {
3430  // "Make List" drawer child ports require special handling, since technically they are compatible with all types
3431  // but practically they are limited to types compatible with their host ports.
3432  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(port->getUnderlyingParentNode());
3433  if (drawer)
3434  {
3435  VuoPort *hostPort = drawer->getUnderlyingHostPort();
3436  if (hostPort && hostPort->hasRenderer())
3437  {
3438  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(hostPort->getClass()->getCompiler());
3439  VuoGenericType *genericHostPortDataType = dynamic_cast<VuoGenericType *>(hostPort->getRenderer()->getDataType());
3440  if (genericHostPortDataType)
3441  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3442  else if (isPortCurrentlyRevertible(hostPort->getRenderer()))
3443  {
3445  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3446  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass->getBase()) );
3447  }
3448  }
3449  }
3450 
3452  vector<string> compatibleTypesInIsolationVector;
3453  if (genericTypeFromPortClass)
3454  compatibleTypesInIsolationVector = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibilityInIsolation);
3455 
3456  // If all types or all list types are compatible, add them to (currently empty) compatibleTypesInIsolationVector.
3457  if (compatibilityInIsolation == VuoGenericType::anyType || compatibilityInIsolation == VuoGenericType::anyListType)
3458  compatibleTypesInIsolationVector = getAllSpecializedTypeOptions(compatibilityInIsolation == VuoGenericType::anyListType);
3459 
3460  foreach (string type, compatibleTypesInIsolationVector)
3461  compatibleTypesInIsolation.insert(drawer? VuoType::extractInnermostTypeName(type) : type);
3462  }
3463 
3464  // List the compatible types in the menu.
3465  {
3466  // If there are only a handful of eligible types, display them all in a flat menu.
3467  const int maxTypeCountForFlatMenuDisplay = 10;
3468  if (compatibleTypesInIsolation.size() <= maxTypeCountForFlatMenuDisplay)
3469  {
3470  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, false, "", contextMenuSpecializeGenericType);
3471  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3472  }
3473 
3474  // Otherwise, organize them by node set.
3475  else
3476  {
3477  // First list compatible types that don't belong to any specific node set.
3478  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, "", contextMenuSpecializeGenericType);
3479  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3480 
3481  // Now add a submenu for each node set that contains compatible types.
3482  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3483  QList<QAction *> allNodeSetActionsToAdd;
3484  for (map<string, set<VuoCompilerType *> >::iterator i = loadedTypesForNodeSet.begin(); i != loadedTypesForNodeSet.end(); ++i)
3485  {
3486  string nodeSetName = i->first;
3487  if (!nodeSetName.empty())
3488  allNodeSetActionsToAdd += getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, nodeSetName, contextMenuSpecializeGenericType);
3489  }
3490 
3491  if ((contextMenuSpecializeGenericType->actions().size() > 0) && (allNodeSetActionsToAdd.size() > 0))
3492  contextMenuSpecializeGenericType->addSeparator();
3493 
3494  addTypeActionsToMenu(allNodeSetActionsToAdd, contextMenuSpecializeGenericType);
3495  }
3496 
3497  foreach (QAction *action, contextMenuSpecializeGenericType->actions())
3498  {
3499  QMenu *specializeSubmenu = action->menu();
3500  if (specializeSubmenu)
3501  {
3502  foreach (QAction *specializeSubaction, specializeSubmenu->actions())
3503  connect(specializeSubaction, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3504  }
3505  else if (action == unspecializeAction)
3506  connect(action, &QAction::triggered, this, &VuoEditorComposition::unspecializePortType);
3507  else
3508  connect(action, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3509  }
3510  }
3511  }
3512 
3513  // Allow the user to hide, unhide, or delete cables connected directly to the port or, if the
3514  // port has a collapsed typecast, to the typecast's child port.
3515  int numVisibleDirectlyConnectedCables = 0;
3516  int numHidableDirectlyConnectedCables = 0;
3517  int numUnhidableDirectlyConnectedCables = 0;
3518 
3519  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
3520  {
3521  if (!cable->getRenderer()->paintingDisabled())
3522  {
3523  numVisibleDirectlyConnectedCables++;
3524  if (!cable->isPublished())
3525  numHidableDirectlyConnectedCables++;
3526  }
3527 
3528  else if (cable->getRenderer()->getEffectivelyWireless())
3529  numUnhidableDirectlyConnectedCables++;
3530  }
3531 
3532  int numVisibleChildPortConnectedCables = 0;
3533  int numHidableChildPortConnectedCables = 0;
3534  int numUnhidableChildPortConnectedCables = 0;
3535  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
3536  if (typecastPort)
3537  {
3538  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
3539  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
3540  foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
3541  {
3542  if (!cable->getRenderer()->paintingDisabled())
3543  {
3544  numVisibleChildPortConnectedCables++;
3545 
3546  if (!cable->isPublished())
3547  numHidableChildPortConnectedCables++;
3548  }
3549  else if (cable->getRenderer()->getEffectivelyWireless())
3550  numUnhidableChildPortConnectedCables++;
3551  }
3552  }
3553 
3554  // Count the number of directly or indirectly connected cables that are currently visible.
3555  int numVisibleConnectedCables = numVisibleDirectlyConnectedCables + numVisibleChildPortConnectedCables;
3556 
3557  // Count the number of directly or indirectly connected cables that are currently hidable.
3558  int numHidableConnectedCables = numHidableDirectlyConnectedCables + numHidableChildPortConnectedCables;
3559 
3560  // Count the number of directly or indirectly connected cables that are currently hidden.
3561  int numUnhidableConnectedCables = numUnhidableDirectlyConnectedCables + numUnhidableChildPortConnectedCables;
3562 
3563  if ((!renderHiddenCables && ((numHidableConnectedCables >= 1) || (numUnhidableConnectedCables >= 1)))
3564  || (numVisibleConnectedCables >= 1))
3565  {
3566  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3567  contextMenu.addSeparator();
3568  }
3569 
3570  if (!renderHiddenCables)
3571  {
3572  if (numHidableConnectedCables >= 1)
3573  {
3574  // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3575  // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3576  // impact visible published cables.
3577  int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3578  contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3579  contextMenuHideCables->setData(qVariantFromValue((void *)port));
3580  contextMenu.addAction(contextMenuHideCables);
3581  }
3582 
3583  if (numUnhidableConnectedCables >= 1)
3584  {
3585  int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3586  contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3587  contextMenuUnhideCables->setData(qVariantFromValue((void *)port));
3588  contextMenu.addAction(contextMenuUnhideCables);
3589  }
3590  }
3591 
3592  if (numVisibleConnectedCables >= 1)
3593  {
3594  contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3595  contextMenuDeleteCables->setData(qVariantFromValue((void *)port));
3596  contextMenu.addAction(contextMenuDeleteCables);
3597  }
3598  }
3599 
3600  // Customize context menu for nodes, cables, and/or comments.
3601  else if (item)
3602  {
3603  if (! item->isSelected())
3604  {
3605  clearSelection();
3606  item->setSelected(true);
3607  }
3608 
3609  QList<QGraphicsItem *> selectedComponents = selectedItems();
3610  bool onlyCommentsSelected = true;
3611  bool onlyCablesSelected = true;
3612  bool selectionContainsMissingNode = false;
3613  foreach (QGraphicsItem *item, selectedComponents)
3614  {
3615  if (!dynamic_cast<VuoRendererComment *>(item))
3616  onlyCommentsSelected = false;
3617 
3618  if (!dynamic_cast<VuoRendererCable *>(item))
3619  onlyCablesSelected = false;
3620 
3621  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3622  selectionContainsMissingNode = true;
3623  }
3624 
3625  contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3626  connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3627 
3628  // Comments
3629  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3630  if (comment)
3631  {
3632  // Option: Edit selected comments
3633  if (onlyCommentsSelected)
3634  contextMenu.addAction(contextMenuEditSelectedComments);
3635 
3636  // Option: Tint selected components(s)
3637  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3638 
3639  contextMenu.addSeparator();
3640 
3641  // Option: Refactor selected component(s)
3642  contextMenu.addAction(contextMenuRefactorSelected);
3643 
3644  contextMenu.addSeparator();
3645 
3646  // Option: Delete selected components(s)
3647  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3648  }
3649 
3650  // Cables
3651  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3652  if (cable)
3653  {
3654  if (!renderHiddenCables)
3655  {
3656  if (onlyCablesSelected && !cable->getBase()->isPublished())
3657  contextMenu.addAction(contextMenuHideSelectedCables);
3658  }
3659 
3660  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3661  }
3662 
3663  // Nodes
3664  else if (dynamic_cast<VuoRendererNode *>(item))
3665  {
3666  VuoRendererNode *node = (VuoRendererNode *)item;
3667 
3668  // Input drawers
3669  if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3670  node->getBase()->getNodeClass()->hasCompiler())
3671  {
3672  // Offer "Add/Remove Input Port" options for resizable list input drawers.
3673  if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3674  {
3675  contextMenuAddInputPort->setData(qVariantFromValue((void *)node));
3676  contextMenuRemoveInputPort->setData(qVariantFromValue((void *)node));
3677 
3678  int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3679  contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3680 
3681  contextMenu.addAction(contextMenuAddInputPort);
3682  contextMenu.addAction(contextMenuRemoveInputPort);
3683 
3684  contextMenu.addSeparator();
3685  }
3686 
3687  // Offer "Reset" option for all input drawers.
3688  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3689  }
3690 
3691  // Non-input-drawer nodes
3692  else
3693  {
3694  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3695  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3696  {
3697  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3698  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3699  if (originalGenericNodeClass)
3700  nodeClass = originalGenericNodeClass->getBase();
3701  }
3702 
3703  QString actionText, sourcePath;
3704  bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3705  bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3706 
3707  int numSelectedNonAttachmentNodes = 0;
3708  QList<QGraphicsItem *> selectedComponents = selectedItems();
3709  foreach (QGraphicsItem *item, selectedComponents)
3710  {
3711  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3712  numSelectedNonAttachmentNodes++;
3713  }
3714 
3715  if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3716  {
3717  // Option: Edit an installed subcomposition, shader, or text-code node.
3718  QAction *editAction = new QAction(NULL);
3719  editAction->setText(actionText);
3720  editAction->setData(qVariantFromValue(node));
3721  connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3722 
3723  contextMenu.addAction(editAction);
3724  contextMenu.addSeparator();
3725  }
3726 
3727  // Option: Rename selected node(s)
3728  if (node->getBase()->hasCompiler())
3729  contextMenu.addAction(contextMenuRenameSelected);
3730 
3731  // Option: Tint selected component(s)
3732  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3733 
3734  contextMenu.addSeparator();
3735 
3736  // Option: Replace selected node with a similar node
3737  if (numSelectedNonAttachmentNodes == 1)
3738  {
3739  if (contextMenuChangeNode)
3740  contextMenuChangeNode->deleteLater();
3741 
3742  contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3743  contextMenuChangeNode->setSeparatorsCollapsible(false);
3744  contextMenuChangeNode->setTitle(tr("Change To"));
3745 
3746  populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3747  if (!contextMenuChangeNode->actions().isEmpty())
3748  contextMenu.addMenu(contextMenuChangeNode);
3749  } // End single selected node
3750 
3751  // Option: Refactor selected component(s)
3752  if (!selectionContainsMissingNode)
3753  contextMenu.addAction(contextMenuRefactorSelected);
3754 
3755  if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3756  {
3757  // Option: Open the enclosing folder for an editable or other 3rd party node class.
3758  QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3759  if (!modulePath.isEmpty())
3760  {
3761  QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3762  QAction *openEnclosingFolderAction = new QAction(NULL);
3763  openEnclosingFolderAction->setText("Show in Finder");
3764  openEnclosingFolderAction->setData(qVariantFromValue(enclosingDirUrl));
3765  connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3766  contextMenu.addAction(openEnclosingFolderAction);
3767  }
3768  }
3769 
3770  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3771  contextMenu.addSeparator();
3772 
3773  // Option: Delete selected components(s)
3774  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3775 
3776  } // End non-input-drawer nodes
3777  } // End nodes
3778  } // End nodes or cables
3779 
3780  if (!contextMenu.actions().isEmpty())
3781  {
3782  // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3784 
3785  menuSelectionInProgress = true;
3786  contextMenu.exec(event->screenPos());
3787  menuSelectionInProgress = false;
3788  }
3789 
3790  delete contextMenuDeleteSelectedSnapshot;
3791 }
3792 
3797 {
3798  QAction *sender = (QAction *)QObject::sender();
3799  VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3800  emit nodeSourceEditorRequested(node);
3801 }
3802 
3809 {
3810  vector<string> typeOptions;
3811 
3812  map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
3813  for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
3814  {
3815  if (((!lists && !VuoType::isListTypeName(i->first)) ||
3816  (lists && VuoType::isListTypeName(i->first))) &&
3817  !VuoType::isDictionaryTypeName(i->first) &&
3818  (i->first != "VuoMathExpressionList"))
3819  {
3820  typeOptions.push_back(i->first);
3821  }
3822  }
3823 
3824  return typeOptions;
3825 }
3826 
3832 set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3833 {
3834  if (!port)
3835  return set<string>();
3836 
3837  // Find the set of connected generic ports that contains our target port.
3838  set<VuoPort *> connectedGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(port->getUnderlyingParentNode()->getBase(),
3839  port->getBase(), true);
3840 
3841  // Determine the set of types compatible with all generic ports in the connected network.
3842  vector<string> compatibleInnerTypeNames;
3843  vector<string> compatibleTypeNames;
3844  for (VuoPort *connectedPort : connectedGenericPorts)
3845  {
3846  VuoGenericType *genericTypeFromPortClass = NULL;
3847  VuoCompilerNodeClass *nodeClass = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler();
3848  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3849  if (specializedNodeClass)
3850  {
3851  VuoPortClass *portClass = connectedPort->getClass();
3852  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3853  }
3854 
3855  VuoGenericType::Compatibility compatibility;
3856  vector<string> compatibleTypeNamesForPort = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3857  vector<string> innermostCompatibleTypeNamesForPort;
3858  for (vector<string>::iterator k = compatibleTypeNamesForPort.begin(); k != compatibleTypeNamesForPort.end(); ++k)
3859  innermostCompatibleTypeNamesForPort.push_back( VuoType::extractInnermostTypeName(*k) );
3860 
3861  if (! innermostCompatibleTypeNamesForPort.empty())
3862  {
3863  if (compatibleInnerTypeNames.empty())
3864  compatibleInnerTypeNames = innermostCompatibleTypeNamesForPort;
3865  else
3866  {
3867  for (int k = compatibleInnerTypeNames.size() - 1; k >= 0; --k)
3868  if (find(innermostCompatibleTypeNamesForPort.begin(), innermostCompatibleTypeNamesForPort.end(), compatibleInnerTypeNames[k]) ==
3869  innermostCompatibleTypeNamesForPort.end())
3870  compatibleInnerTypeNames.erase(compatibleInnerTypeNames.begin() + k);
3871  }
3872  }
3873  }
3874 
3876  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3877  VuoPortClass *portClass = port->getBase()->getClass();
3878  VuoGenericType *genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3879  string typeNameForPort = genericTypeFromPortClass->getModuleKey();
3880 
3881  // Finish compiling set of compatible types in the context of the connected generic port network.
3882  string prefix = (VuoType::isListTypeName(typeNameForPort) ? VuoType::listTypeNamePrefix : "");
3883  for (vector<string>::iterator k = compatibleInnerTypeNames.begin(); k != compatibleInnerTypeNames.end(); ++k)
3884  compatibleTypeNames.push_back(prefix + *k);
3885 
3886  if (compatibleTypeNames.empty())
3887  {
3888  VuoGenericType::Compatibility compatibility;
3889  genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3890 
3891  // If all types or all list types are compatible, add them to (currently empty) compatibleTypeNames.
3892  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3893  compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3894  }
3895 
3896  return set<string>(compatibleTypeNames.begin(), compatibleTypeNames.end());
3897 }
3898 
3919  set<string> compatibleTypesInIsolation,
3920  set<string> compatibleTypesInContext,
3921  bool limitToNodeSet,
3922  string nodeSetName,
3923  QMenu *menu)
3924 {
3925  QList<QAction *> actionsToAddToMenu;
3926 
3927  map<string, VuoCompilerType *> allTypes = compiler->getTypes();
3928  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3929  QList<VuoCompilerType *> compatibleTypesForNodeSetDisplay;
3930  foreach (string typeName, compatibleTypesInIsolation)
3931  {
3932  VuoCompilerType *type = allTypes[typeName];
3933  if ((!limitToNodeSet || (loadedTypesForNodeSet[nodeSetName].find(type) != loadedTypesForNodeSet[nodeSetName].end())) &&
3934  (! VuoGenericType::isGenericTypeName(typeName)) &&
3935  // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
3936  (typeName != "VuoUrl" && typeName != "VuoList_VuoUrl"
3937  // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
3938  && typeName != "VuoInteraction" && typeName != "VuoList_VuoInteraction"
3939  && typeName != "VuoInteractionType" && typeName != "VuoList_VuoInteractionType"
3940  && typeName != "VuoUuid" && typeName != "VuoList_VuoUuid"
3941  // Hide deprecated types.
3942  && typeName != "VuoIconPosition" && typeName != "VuoList_VuoIconPosition"
3943  && typeName != "VuoMesh" && typeName != "VuoList_VuoMesh"
3944  && typeName != "VuoWindowProperty" && typeName != "VuoList_VuoWindowProperty"
3945  && typeName != "VuoWindowReference" && typeName != "VuoList_VuoWindowReference"))
3946  compatibleTypesForNodeSetDisplay.append(type);
3947  }
3948 
3949  if (!compatibleTypesForNodeSetDisplay.isEmpty())
3950  {
3951  QMenu *contextMenuNodeSetTypes = NULL;
3952  QList<QAction *> actionsToAddToNodeSetSubmenu;
3953  bool enabledContentAdded = false;
3954 
3955  // Populate the "Specialize Type" submenu for the target node set.
3956  if (!nodeSetName.empty())
3957  {
3958  contextMenuNodeSetTypes = new QMenu(menu);
3959  contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
3960  contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
3961  contextMenuNodeSetTypes->setToolTipsVisible(true);
3962  actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
3963  }
3964 
3965  foreach (VuoCompilerType *type, compatibleTypesForNodeSetDisplay)
3966  {
3967  string typeName = type->getBase()->getModuleKey();
3968  QList<QVariant> portAndSpecializedType;
3969  portAndSpecializedType.append(qVariantFromValue((void *)genericPort));
3970  portAndSpecializedType.append(typeName.c_str());
3971 
3972  QAction *specializeAction;
3973  QString typeTitle = formatTypeNameForDisplay(type->getBase());
3974 
3975  // Case: Adding to the node set submenu
3976  if (!nodeSetName.empty())
3977  {
3978  specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
3979  actionsToAddToNodeSetSubmenu.append(specializeAction);
3980  }
3981 
3982  // Case: Adding to the top-level action list
3983  else
3984  {
3985  specializeAction = new QAction(typeTitle, menu);
3986  actionsToAddToMenu.append(specializeAction);
3987  }
3988 
3989  specializeAction->setData(QVariant(portAndSpecializedType));
3990  specializeAction->setCheckable(true);
3991  specializeAction->setChecked(genericPort && (genericPort->getDataType()->getModuleKey() == type->getBase()->getModuleKey()));
3992 
3993  if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
3994  {
3995  specializeAction->setEnabled(false);
3996  //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
3997  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."));
3998  }
3999  else
4000  enabledContentAdded = true;
4001  }
4002 
4003  if (contextMenuNodeSetTypes)
4004  {
4005  addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
4006 
4007  if (!enabledContentAdded)
4008  contextMenuNodeSetTypes->setEnabled(false);
4009  }
4010  }
4011 
4012  QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
4013  return actionListWithPromotions;
4014 }
4015 
4020 QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
4021 {
4022  QList<QAction *> modifiedActionList;
4023  foreach (QAction *action, actionList)
4024  {
4025  if (action->menu() && (action->menu()->actions().size() == 1))
4026  {
4027  QAction *singleSubaction = action->menu()->actions().first();
4028  action->menu()->removeAction(singleSubaction);
4029  modifiedActionList.append(singleSubaction);
4030  }
4031  else
4032  modifiedActionList.append(action);
4033  }
4034 
4035  return modifiedActionList;
4036 }
4037 
4041 void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
4042 {
4043  std::sort(actionList.begin(), actionList.end(), nodeSetMenuActionLessThan);
4044  foreach (QAction *action, actionList)
4045  menu->addAction(action);
4046 }
4047 
4051 void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4052 {
4053  if (!now)
4054  return;
4055 
4057  if (activeWindow)
4058  emit compositionOnTop(activeWindow->getComposition() == this);
4059 }
4060 
4064 void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4065 {
4066  if (ignoreApplicationStateChangeEvents)
4067  return;
4068 
4070  if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4071  emit applicationActive(active);
4072 }
4073 
4082 QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4083 {
4084  return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4085 }
4086 
4091 {
4092  QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4093  return dynamic_cast<VuoRendererNode *>(item);
4094 }
4095 
4108 QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4109  targetComponentType targetType,
4110  bool limitPortCollisionRange)
4111 {
4112  // Determine which types of components to filter out of search results.
4113  bool ignoreCables;
4114  bool ignoreNodes;
4115  bool ignorePorts;
4116  bool ignoreComments;
4117 
4118  switch(targetType)
4119  {
4120  case VuoEditorComposition::targetTypePort:
4121  {
4122  ignoreCables = true;
4123  ignoreNodes = true;
4124  ignorePorts = false;
4125  ignoreComments = true;
4126  break;
4127  }
4128  case VuoEditorComposition::targetTypeNodeHeader:
4129  {
4130  ignoreCables = true;
4131  ignoreNodes = true;
4132  ignorePorts = true;
4133  ignoreComments = true;
4134  break;
4135  }
4136  default:
4137  {
4138  ignoreCables = false;
4139  ignoreNodes = false;
4140  ignorePorts = false;
4141  ignoreComments = false;
4142  break;
4143  }
4144  }
4145 
4146  // The topmost item under the cursor is not necessarily the one we will return
4147  // (e.g., if the cursor is positioned directly over a node but also within range
4148  // of one of that node's ports), but it will factor in to the decision.
4149  QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4150  if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4151  topmostItemUnderCursor = NULL;
4152 
4153  for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4154  {
4155  QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4156  QList<QGraphicsItem *> itemsInRange = items(searchRect);
4157  for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4158  {
4159  if (!(*i)->isEnabled())
4160  continue;
4161 
4162  // Check whether we have located an unobscured "Make List" drawer drag handle.
4163  if (! ignoreNodes)
4164  {
4165  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4166  bool makeListDragHandle =
4167  (makeListDrawer &&
4168  makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4169  if (makeListDragHandle &&
4170  ((! topmostItemUnderCursor) ||
4171  (topmostItemUnderCursor == makeListDrawer) ||
4172  (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4173  {
4174  return makeListDrawer;
4175  }
4176  }
4177 
4178  // Check whether we have located an unobscured port.
4179  // Hovering within range of a port takes precedence over hovering
4180  // directly over that port's parent node.
4181  if (! ignorePorts)
4182  {
4183  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4184  if (port &&
4185  ((! topmostItemUnderCursor) ||
4186  (topmostItemUnderCursor == port) ||
4187  (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4188  (topmostItemUnderCursor->zValue() < port->zValue()))
4189  &&
4190  ((! limitPortCollisionRange) ||
4191  port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4192  &&
4193  ! port->getFunctionPort())
4194  {
4195  return port;
4196  }
4197  }
4198 
4199  // Check whether we have located an unobscured cable.
4200  if (! ignoreCables)
4201  {
4202  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4203  if (cable &&
4204  ((! topmostItemUnderCursor) ||
4205  (topmostItemUnderCursor == cable) ||
4206  (topmostItemUnderCursor->zValue() < cable->zValue())))
4207  {
4208  return cable;
4209  }
4210  }
4211 
4212  // Check whether we have located an unobscured comment.
4213  if (! ignoreComments)
4214  {
4215  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4216  if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4217  comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4218 
4219  if (comment &&
4220  ((! topmostItemUnderCursor) ||
4221  (topmostItemUnderCursor == (*i)) ||
4222  (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4223  {
4224  return comment;
4225  }
4226  }
4227 
4228  // Check whether we have located an unobscured node header.
4229  if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4230  {
4231  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4232  if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4233  {
4234  QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4235  headerRect = node->mapToScene(headerRect).boundingRect();
4236  if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4237  return node;
4238  }
4239  }
4240  }
4241  }
4242 
4243  // Having failed to locate any other relevant components within range, return the node
4244  // directly under the cursor, if applicable.
4245  if (! ignoreNodes)
4246  {
4247  if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4248  return topmostItemUnderCursor;
4249 
4250  // It is possible that the item under the cursor is a port, but that it didn't meet
4251  // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4252  if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4253  return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4254  }
4255 
4256  return NULL;
4257 }
4258 
4269 {
4270  if (! cableInProgress)
4271  return NULL;
4272 
4273  VuoPort *fromPort = cableInProgress->getFromPort();
4274  VuoPort *toPort = cableInProgress->getToPort();
4275  VuoPort *fixedPort = (fromPort? fromPort: toPort);
4276 
4277  if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4278  return NULL;
4279 
4280  return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4281 }
4282 
4288 {
4289  // Start with the first input or output port (other than the refresh port).
4290  vector<VuoRendererPort *> portList;
4291  int firstPortIndex;
4292 
4293  if (inputPort)
4294  {
4295  portList = node->getInputPorts();
4297  }
4298  else
4299  {
4300  portList = node->getOutputPorts();
4302  }
4303 
4304  VuoRendererPort *targetPort = NULL;
4305  if (portList.size() > firstPortIndex)
4306  {
4307  targetPort = portList[firstPortIndex];
4308 
4309  // If the first input port has a wall,
4310  // keep moving down until we find one without a wall.
4311  // (Unless they all have walls, in which case stick with the first.)
4312  VuoRendererPort *firstPortWithoutWall = nullptr;
4313  for (int i = firstPortIndex; i < portList.size(); ++i)
4314  {
4315  if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4316  {
4317  firstPortWithoutWall = portList[i];
4318  break;
4319  }
4320  }
4321  if (firstPortWithoutWall)
4322  targetPort = firstPortWithoutWall;
4323 
4324  // If the first input port has a drawer attached to it,
4325  // instead select the first input port of the drawer.
4326  // (Unless the drawer has no input ports, in which case don't select any port.)
4327  VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4328  if (drawer)
4329  {
4330  portList = drawer->getInputPorts();
4331  if (portList.size() > firstPortIndex)
4332  targetPort = portList[firstPortIndex];
4333  else
4334  targetPort = NULL;
4335  }
4336 
4337  // If the selected input port has a collapsed type-converter node attached to it,
4338  // instead select the type-converter node's input port.
4339  VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4340  if (typecast)
4341  targetPort = typecast->getChildPort();
4342  }
4343 
4344  return targetPort;
4345 }
4346 
4352 {
4353  QRectF boundingRect;
4354  foreach (QGraphicsItem *item, items())
4355  {
4356  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4357  if (! (rc && rc->getBase()->isPublished()))
4358  boundingRect |= item->sceneBoundingRect();
4359  }
4360 
4361  return boundingRect;
4362 }
4363 
4369 {
4370  QRectF boundingRect;
4371 
4372  foreach (QGraphicsItem *item, selectedItems())
4373  {
4374  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4375  if (! (rc && rc->getBase()->isPublished()))
4376  boundingRect |= item->sceneBoundingRect();
4377  }
4378 
4379  return boundingRect;
4380 }
4381 
4387 {
4388  QRectF boundingRect;
4389 
4390  foreach (QGraphicsItem *item, selectedItems())
4391  {
4392  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4393  if (! (rc && rc->getBase()->isPublished()))
4394  boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4395 
4396  // Include attached drawers.
4397  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4398  if (rn)
4399  {
4400  foreach (VuoPort *port, rn->getBase()->getInputPorts())
4401  {
4402  VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4403  if (drawer)
4404  boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4405  }
4406  }
4407  }
4408 
4409  return boundingRect;
4410 }
4411 
4416 void VuoEditorComposition::updateInternalPortConstant(string portID, string newValue, bool updateInRunningComposition)
4417 {
4418  VuoPort *port = getPortWithStaticIdentifier(portID);
4419  if (!port)
4420  return;
4421 
4422  updatePortConstant(dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4423 }
4424 
4429 void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4430 {
4432  if (!port)
4433  return;
4434 
4435  updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4436 }
4437 
4442 void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4443 {
4444  // Internal ports
4445  if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4446  {
4447  port->getBase()->getRenderer()->setConstant(newValue);
4448 
4449  if (updateInRunningComposition)
4451  }
4452 
4453  // Published ports
4454  else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4455  {
4456  dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4457 
4458  if (updateInRunningComposition)
4460  }
4461 }
4462 
4468 {
4471 }
4472 
4481 {
4482 
4483  // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4484  if (!errorMarkingUpdatesEnabled)
4485  return;
4486 
4487  errorMarkingUpdatesEnabled = false;
4488 
4489  // Remove any error annotations from the previous call to this function.
4490  if (errorMark)
4491  {
4492  errorMark->removeFromScene();
4493  errorMark = NULL;
4494  }
4495 
4497 
4498  // Check for errors.
4499 
4500  VuoCompilerIssues *issues = new VuoCompilerIssues();
4501  VuoCompilerCable *potentialCable = NULL;
4502  try
4503  {
4504  set<VuoCompilerCable *> potentialCables;
4505 
4506  if (targetPort && cableInProgress)
4507  {
4508  VuoNode *fromNode;
4509  VuoPort *fromPort;
4510  VuoNode *toNode;
4511  VuoPort *toPort;
4512  if (cableInProgress->getFromNode())
4513  {
4514  fromNode = cableInProgress->getFromNode();
4515  fromPort = cableInProgress->getFromPort();
4516  toNode = targetPort->getUnderlyingParentNode()->getBase();
4517  toPort = targetPort->getBase();
4518  }
4519  else
4520  {
4521  fromNode = targetPort->getUnderlyingParentNode()->getBase();
4522  fromPort = targetPort->getBase();
4523  toNode = cableInProgress->getToNode();
4524  toPort = cableInProgress->getToPort();
4525  }
4526  potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4527  potentialCable->getBase()->setFrom(fromNode, fromPort);
4528  potentialCable->getBase()->setTo(toNode, toPort);
4529  potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4530  cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4531 
4532  fromPort->removeConnectedCable(potentialCable->getBase());
4533  toPort->removeConnectedCable(potentialCable->getBase());
4534  potentialCables.insert(potentialCable);
4535  }
4536 
4537  getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4538  }
4539  catch (const VuoCompilerException &e)
4540  {
4541  }
4542 
4543  if (! issues->isEmpty())
4544  {
4545  VUserLog("%s: Showing error popover: %s",
4546  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4547  issues->getShortDescription(false).c_str());
4548 
4549  this->errorMark = new VuoErrorMark();
4550 
4551  foreach (VuoCompilerIssue issue, issues->getList())
4552  {
4553  set<VuoRendererNode *> nodesToMark;
4554  set<VuoRendererCable *> cablesToMark;
4555 
4556  set<VuoNode *> problemNodes = issue.getNodes();
4557  foreach (VuoNode *node, problemNodes)
4558  if (node->hasRenderer())
4559  nodesToMark.insert(node->getRenderer());
4560 
4561  set<VuoCable *> problemCables = issue.getCables();
4562  foreach (VuoCable *cable, problemCables)
4563  {
4564  VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4565  if (cableToMark->hasRenderer())
4566  cablesToMark.insert(cableToMark->getRenderer());
4567  }
4568 
4570  errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4571 
4572  VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4573  errorPopovers.insert(errorPopover);
4576 
4577  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4578 
4579  // Place the popover near an appropriate nearby node involved in the feedback loop.
4580  VuoRendererNode *nearbyNode = NULL;
4581  if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4582  {
4583  nearbyNode = targetPort->getRenderedParentNode();
4584  }
4585  else if (! nodesToMark.empty())
4586  {
4587  VuoRendererNode *topmostVisibleNode = NULL;
4588  qreal topY = viewportRect.bottom();
4589  foreach (VuoRendererNode *node, nodesToMark)
4590  {
4591  if (node->getProxyNode())
4592  node = node->getProxyNode();
4593 
4594  QPointF scenePos = node->scenePos();
4595  if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4596  {
4597  topmostVisibleNode = node;
4598  topY = scenePos.y();
4599  }
4600  }
4601 
4602  if (topmostVisibleNode)
4603  nearbyNode = topmostVisibleNode;
4604  else
4605  {
4606  VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4607  nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4608  }
4609  }
4610  else
4611  {
4612  VUserLog("Warning: no nearby node (no marked nodes).");
4613  }
4614 
4615  // If no nodes are known to be involved in the feedback loop, display the popover
4616  // in the center of the viewport.
4617  const QPoint offsetFromNode(0,10);
4618  QPoint popoverTopLeftInScene = (nearbyNode?
4619  (nearbyNode->scenePos().toPoint() +
4620  nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4621  offsetFromNode) :
4622  QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4623  viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4624 
4625  // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4626  // of the viewport closest to the feedback loop.
4627  const int margin = 5;
4628  popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4629  fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4630 
4631  popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4632  fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4633 
4634  QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4635  QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4636 
4637  errorPopover->move(popoverTopLeftGlobal);
4638  errorPopover->show();
4639  emit popoverDetached();
4640  }
4641 
4642  // Add error annotations to the composition.
4643  addItem(errorMark);
4644  }
4645  delete issues;
4646  delete potentialCable;
4647 
4648  errorMarkingUpdatesEnabled = true;
4649 }
4650 
4654 bool VuoEditorComposition::hasFeedbackErrors(void)
4655 {
4656  return this->errorMark;
4657 }
4658 
4663 {
4664  if (hasFeedbackErrors())
4665  this->errorMark->updateErrorMarkPath();
4666 }
4667 
4673 void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4674 {
4675  try
4676  {
4677  emit buildStarted();
4678 
4679  if (! dependenciesUninstalled.empty())
4680  {
4681  vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4682  string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4683  throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4684  }
4685 
4686  delete runningComposition;
4687  runningComposition = NULL;
4688  runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4689 
4690  runningCompositionActiveDriver = getDriverForActiveProtocol();
4691  if (runningCompositionActiveDriver)
4692  runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4693 
4694  string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4695  string dir, file, ext;
4696  VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4697  linkedCompositionPath = dir + file + ".dylib";
4698 
4699  compiler->setShouldPotentiallyShowSplashWindow(false);
4700 
4701  VuoCompilerIssues *issues = new VuoCompilerIssues();
4702  compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4703  compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries);
4704  delete issues;
4705 
4706  remove(compiledCompositionPath.c_str());
4707 
4708  emit buildFinished("");
4709  }
4710  catch (VuoException &e)
4711  {
4712  delete runningComposition;
4713  runningComposition = NULL;
4714 
4715  emit buildFinished(e.what());
4716  throw;
4717  }
4718 }
4719 
4725 bool VuoEditorComposition::isRunningThreadUnsafe(void)
4726 {
4727  return runner != NULL && ! stopRequested && ! runner->isStopped();
4728 }
4729 
4736 {
4737  __block bool running;
4738  dispatch_sync(runCompositionQueue, ^{
4739  running = isRunningThreadUnsafe();
4740  });
4741  return running;
4742 }
4743 
4749 void VuoEditorComposition::run(string compositionSnapshot)
4750 {
4751  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4752 
4753  // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4754  subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4755 
4756  // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4757  subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4758  {
4759  compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4760  });
4761 
4762  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4763  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4764  {
4765  if (matchingComposition->showEventsMode)
4766  matchingComposition->beginDisplayingActivity();
4767  });
4768 
4769  stopRequested = false;
4770  dispatch_async(runCompositionQueue, ^{
4771  try
4772  {
4773  runningCompositionLibraries = new VuoRunningCompositionLibraries();
4774 
4775  buildComposition(compositionSnapshot);
4776 
4777  string compositionLoaderPath = compiler->getCompositionLoaderPath();
4778  string compositionSourceDir = getBase()->getDirectory();
4779 
4780  runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4781  runner->setDelegate(this);
4783  runner->startPaused();
4784  pid_t pid = runner->getCompositionPid();
4785 
4786  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4787  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4788  {
4789  if (matchingComposition->showEventsMode)
4790  this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4791 
4792  dispatch_sync(activePortPopoversQueue, ^{
4793  for (auto i : matchingComposition->activePortPopovers)
4794  {
4795  string portID = i.first;
4796  updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4797  }
4798  });
4799  });
4800 
4801  runner->unpause();
4802 
4803  // Focus the composition's windows (if any).
4804  VuoFileUtilities::focusProcess(pid, true);
4805  }
4806  catch (...) { }
4807  });
4808 }
4809 
4816 {
4817  stopRequested = true;
4818  dispatch_async(runCompositionQueue, ^{
4819  if (runner && ! runner->isStopped())
4820  {
4821  runner->stop();
4822  runner->waitUntilStopped();
4823  }
4824  delete runner;
4825  runner = NULL;
4826 
4827  linkedCompositionPath = "";
4828  runningCompositionLibraries = NULL;
4829 
4830  delete runningComposition;
4831  runningComposition = NULL;
4832 
4833  emit stopFinished();
4834  });
4835 
4836  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4837 
4838  // Tell this composition and all subcompositions opened from it to stop display live debug info.
4839  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4840  {
4841  if (matchingComposition->showEventsMode)
4842  matchingComposition->stopDisplayingActivity();
4843 
4844  dispatch_sync(activePortPopoversQueue, ^{
4845  for (auto i : matchingComposition->activePortPopovers)
4846  {
4847  VuoPortPopover *popover = i.second;
4848  popover->setCompositionRunning(false);
4849  }
4850  });
4851  });
4852 }
4853 
4865 void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4866  VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4867 {
4868  if (! diffInfo)
4869  diffInfo = new VuoCompilerCompositionDiff();
4870 
4871  dispatch_async(runCompositionQueue, ^{
4872  if (isRunningThreadUnsafe())
4873  {
4874  try
4875  {
4876  foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4877  {
4878  runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4879  }
4880 
4881  string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4882  VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4883  if (previouslyActiveDriver)
4884  {
4885  VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4886  previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4887  oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4888  }
4889 
4890  buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4891 
4892  string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4893  runner->replaceComposition(linkedCompositionPath, compositionDiff);
4894  }
4895  catch (exception &e)
4896  {
4897  VUserLog("Composition stopped itself: %s", e.what());
4898  emit compositionStoppedItself();
4899  }
4900  catch (...)
4901  {
4902  VUserLog("Composition stopped itself.");
4903  emit compositionStoppedItself();
4904  }
4905  }
4906  else
4907  {
4908  dispatch_async(dispatch_get_main_queue(), ^{
4909  updateCompositionsThatContainThisSubcomposition(newCompositionSnapshot);
4910  });
4911  }
4912 
4913  delete diffInfo;
4914  });
4915 }
4916 
4922 {
4923  void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
4924  {
4925  compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
4926  };
4927  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
4928 }
4929 
4935 {
4936  string constant;
4937  identifierCache->doForPortWithIdentifier(runningPortID, [&constant](VuoPort *port) {
4938  if (port->hasCompiler() && port->hasRenderer())
4939  constant = port->getRenderer()->getConstantAsString();
4940  });
4941 
4942  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4943  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4944  {
4945  if (this == topLevelComposition)
4946  {
4947  dispatch_async(runCompositionQueue, ^{
4948  if (isRunningThreadUnsafe())
4949  {
4950  json_object *constantObject = json_tokener_parse(constant.c_str());
4951  runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
4952  }
4953  });
4954  }
4955  };
4956  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4957 
4958  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4959  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4960  {
4961  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4962  {
4963  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
4964  });
4965  });
4966 }
4967 
4973 {
4975  if (!(port && port->hasCompiler()))
4976  return;
4977 
4978  string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
4980 }
4981 
4986 {
4987  string runningPortIdentifier = identifierCache->getIdentifierForPort(port->getBase());
4988  if (runningPortIdentifier.empty())
4989  return;
4990 
4991  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4992  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4993  {
4994  if (this == topLevelComposition)
4995  {
4996  dispatch_async(runCompositionQueue, ^{
4997  if (isRunningThreadUnsafe())
4998  {
4999  json_object *constantObject = json_tokener_parse(constant.c_str());
5000  runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
5001  }
5002  });
5003  }
5004  };
5005  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
5006 
5007  // If this is a subcomposition, live-update all other top-level compositions that contain it.
5008  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
5009  {
5010  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
5011  {
5012  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
5013  });
5014  });
5015 }
5016 
5021 void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
5022 {
5023  dispatch_async(runCompositionQueue, ^{
5024  if (isRunningThreadUnsafe())
5025  {
5026  json_object *constantObject = json_tokener_parse(constant.c_str());
5027  set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
5028  foreach (string subcompositionIdentifier, subcompositionIdentifiers)
5029  {
5030  runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
5031  }
5032  }
5033  });
5034 }
5035 
5040 {
5041  dispatch_async(runCompositionQueue, ^{
5042  if (isRunningThreadUnsafe())
5043  {
5044  VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
5045  if (publishedPort)
5046  {
5047  json_object *constantObject = json_tokener_parse(constant.c_str());
5048  map<VuoRunner::Port *, json_object *> m;
5049  m[publishedPort] = constantObject;
5050  runner->setPublishedInputPortValues(m);
5051  }
5052  }
5053  });
5054 }
5055 
5056 
5061 {
5062  return contextMenuDeleteSelected;
5063 }
5064 
5069 {
5070  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5071  // For now, this recreates the context menu rather than accessing a data member.
5072  QMenu *contextMenuTints = new QMenu(parent);
5073  contextMenuTints->setSeparatorsCollapsible(false);
5074  contextMenuTints->setTitle(tr("Tint"));
5075  foreach (QAction *tintAction, contextMenuTintActions)
5076  contextMenuTints->addAction(tintAction);
5077  contextMenuTints->insertSeparator(contextMenuTintActions.last());
5078 
5079  return contextMenuTints;
5080 }
5081 
5085 void VuoEditorComposition::expandChangeNodeMenu()
5086 {
5087  QAction *sender = (QAction *)QObject::sender();
5088  VuoRendererNode *node = static_cast<VuoRendererNode *>(sender->data().value<void *>());
5089 
5090  // If the menu hasn't been expanded previously, expand it enough now to fill
5091  // the available vertical screenspace.
5092  int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5093  if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5094  {
5095  int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5096  int verticalSpacePerItem = 21; // menu row height in pixels
5097  // Estimate the number of matches that will fit within the screen without scrolling:
5098  // -1 to account for a possible extra "More…" row;
5099  // -1 wiggle room to match observations
5100  int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5101 
5102  populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5103  }
5104 
5105  // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5106  else
5107  populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5108 
5109  contextMenuChangeNode->exec();
5110 }
5111 
5117 void VuoEditorComposition::populateChangeNodeMenu(QMenu *menu, VuoRendererNode *node, int matchLimit)
5118 {
5119  menu->clear();
5120 
5121  if (!node)
5122  return;
5123 
5124  map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5125  vector<VuoCompilerNodeClass *> loadedNodeClasses;
5126  for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5127  loadedNodeClasses.push_back(i->second);
5128  VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5129 
5130  vector<string> bestMatches;
5131  map<string, double> matchScores;
5132  matchScores[""] = 0;
5133 
5134  int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5135  for (int i = 0; i < targetMatchCount; ++i)
5136  bestMatches.push_back("");
5137 
5138  // Maintain a priority queue with the @c targetMatchCount best matches.
5139  bool overflow = false;
5140 
5141  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5142  string originalGenericNodeClassName;
5143  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5144  originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5145  else
5146  originalGenericNodeClassName = nodeClass->getClassName();
5147 
5148  foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5149  {
5150  string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5151  if (loadedNodeClassName == originalGenericNodeClassName)
5152  continue;
5153 
5154  bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5155  double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5156  int highestIndexWithCompetitiveScore = -1;
5157  for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5158  if (matchScore <= matchScores[bestMatches[i] ])
5159  highestIndexWithCompetitiveScore = i;
5160 
5161  if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5162  {
5163  if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5164  overflow = true;
5165 
5166  for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5167  bestMatches[j+1] = bestMatches[j];
5168 
5169  bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5170  matchScores[loadedNodeClassName] = matchScore;
5171  }
5172  }
5173 
5174  for (int i = 0; i < targetMatchCount; ++i)
5175  {
5176  if (matchScores[bestMatches[i] ] > 0)
5177  {
5178  // Disambiguate between identical node titles using node class names.
5179  QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5180  if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5181  matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5182 
5183  QAction *changeAction = menu->addAction(matchDisplayText);
5184 
5185  QList<QVariant> currentNodeAndNewClass;
5186  currentNodeAndNewClass.append(qVariantFromValue((void *)node));
5187  currentNodeAndNewClass.append(bestMatches[i].c_str());
5188  changeAction->setData(QVariant(currentNodeAndNewClass));
5189  connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5190  }
5191  }
5192 
5193  if (overflow)
5194  {
5195  //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5196  QAction *showMoreAction = menu->addAction(tr("More…"));
5197  showMoreAction->setData(qVariantFromValue(static_cast<void *>(node)));
5198  connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5199  }
5200 }
5201 
5206 bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5207 {
5208  // Inventory required input port types (connected data inputs) in the node to be replaced.
5209  map<string, int> requiredInputs;
5210  bool inputEventSourceRequired = false;
5211  foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5212  {
5213  bool hasDrawerWithNoIncomingCables = false;
5214  bool hasDrawerWithNoIncomingDataCables = false;
5215 
5216  if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5217  {
5218  VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5219  if (inputDrawer)
5220  {
5221  hasDrawerWithNoIncomingCables = true;
5222  hasDrawerWithNoIncomingDataCables = true;
5223  vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5224  foreach (VuoRendererPort *childPort, childPorts)
5225  {
5226  if (childPort->getBase()->getConnectedCables(true).size() > 0)
5227  hasDrawerWithNoIncomingCables = false;
5228  if (childPort->effectivelyHasConnectedDataCable(true))
5229  hasDrawerWithNoIncomingDataCables = false;
5230  }
5231  }
5232 
5233  string typeKey = inputPort->getDataType()->getModuleKey();
5234  if (!hasDrawerWithNoIncomingDataCables)
5235  {
5236  // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5237  // Accommodate generic types.
5238  if (VuoGenericType::isGenericTypeName(typeKey))
5239  return false;
5240 
5241  requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5242  }
5243  }
5244 
5245  bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5246  if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5247  inputEventSourceRequired = true;
5248  }
5249 
5250  // Inventory required output port types (connected data outputs) in the node to be replaced.
5251  map<string, int> requiredOutputs;
5252  bool outputEventSourceRequired = false;
5253  foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5254  {
5255  if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5256  {
5257  string typeKey = outputPort->getDataType()->getModuleKey();
5258 
5259  // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5260  // Accommodate generic types.
5261  if (VuoGenericType::isGenericTypeName(typeKey))
5262  return false;
5263 
5264  requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5265  }
5266 
5267  if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5268  outputEventSourceRequired = true;
5269  }
5270 
5271  // Inventory available input port types in the candidate replacement node.
5272  bool inputEventSourceAvailable = false;
5273  map<string, int> availableInputs;
5274  foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5275  {
5276  VuoType *dataType = (inputPortClass->hasCompiler()?
5277  static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5278  if (dataType)
5279  {
5280  string typeKey = dataType->getModuleKey();
5281  availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5282  }
5283  }
5284 
5286  inputEventSourceAvailable = true;
5287 
5288  // Inventory available output port types in the candidate replacement node.
5289  bool outputEventSourceAvailable = false;
5290  map<string, int> availableOutputs;
5291  foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5292  {
5293  VuoType *dataType = (outputPortClass->hasCompiler()?
5294  static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5295  if (dataType)
5296  {
5297  string typeKey = dataType->getModuleKey();
5298  availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5299  }
5300  }
5301 
5303  outputEventSourceAvailable = true;
5304 
5305  // Check whether the candidate replacement node meets input data requirements.
5306  for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5307  {
5308  string typeKey = it->first;
5309  int typeRequiredCount = it->second;
5310  if (availableInputs[typeKey] < typeRequiredCount)
5311  return false;
5312  }
5313 
5314  // Check whether the candidate replacement node meets output data requirements.
5315  for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5316  {
5317  string typeKey = it->first;
5318  int typeRequiredCount = it->second;
5319  if (availableOutputs[typeKey] < typeRequiredCount)
5320  return false;
5321  }
5322 
5323  if (inputEventSourceRequired && !inputEventSourceAvailable)
5324  return false;
5325 
5326  if (outputEventSourceRequired && !outputEventSourceAvailable)
5327  return false;
5328 
5329  return true;
5330 }
5331 
5336 bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5337 {
5338  // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5340  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5341 
5342  if (!specializedNodeClass)
5343  return false;
5344 
5345  VuoPortClass *portClass = port->getBase()->getClass();
5346  VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5347  if (!originalGenericType)
5348  return false;
5349 
5350  // If this port belongs to an attachment connected to a port that is not revertible, then
5351  // this port cannot be reverted, either.
5352  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(port->getUnderlyingParentNode());
5353  if (attachment)
5354  {
5355  VuoPort *hostPort = attachment->getUnderlyingHostPort();
5356  if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5357  return false;
5358  }
5359 
5360  return true;
5361 }
5362 
5381 VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5382 {
5383  string publishedPortName = ((! name.empty())?
5384  name :
5386  bool isPublishedInput = port->getRenderer()->getInput();
5387  VuoType *portType = port->getRenderer()->getDataType();
5388  VuoPublishedPort *publishedPort = NULL;
5389 
5390  // If merging is enabled:
5391  // Check whether this composition has a pre-existing externally visible published port
5392  // that has the requested name and type and that can accommodate the newly published internal port.
5393  // If so, add this internal port as a connected port for the existing alias.
5394  bool performedMerge = false;
5395  if (attemptMerge)
5396  {
5397  publishedPort = (isPublishedInput ?
5398  getBase()->getPublishedInputPortWithName(publishedPortName) :
5399  getBase()->getPublishedOutputPortWithName(publishedPortName));
5400 
5401  if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5402  {
5403  if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5404  {
5405  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5407  publishedInputPort->getInitialValue(),
5408  false);
5409  }
5410 
5411  performedMerge = true;
5412  }
5413  }
5414 
5415 
5416  // Otherwise, create a new externally visible published port with a unique name derived from
5417  // the specified name, containing the current port as its lone connected internal port.
5418  if (! performedMerge)
5419  {
5420  publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5421  if (isPublishedInput && type)
5422  {
5423  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5424  publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5425  }
5426  }
5427 
5428  addPublishedPort(publishedPort, isPublishedInput);
5429 
5430  VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5431  dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5432  createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5433 
5434  VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5435 
5436  if (! existingPublishedCable)
5437  {
5438  VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5439  addCable(publishedCable);
5440  }
5441 
5442  if (mergePerformed != NULL)
5443  *mergePerformed = performedMerge;
5444 
5445  return rendererPublishedPort;
5446 }
5447 
5452 VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5453 {
5454  VuoCable *publishedCable = NULL;
5455  bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5456 
5457  if (creatingPublishedInputCable)
5458  {
5459  // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5460  VuoPort *fromPort = externalPort;
5461  VuoNode *fromNode = this->publishedInputNode;
5462 
5463  VuoPort *toPort = internalPort;
5464  VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5465 
5466  publishedCable = (new VuoCompilerCable(NULL,
5467  NULL,
5468  toNode->getCompiler(),
5469  (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5470  publishedCable->setFrom(fromNode, fromPort);
5471  }
5472 
5473  else
5474  {
5475  // If creating a published output cable, it will need to have an associated VuoCompilerCable
5476  // even though we don't currently construct a VuoCompilerNode for the published output node.
5477  VuoPort *fromPort = internalPort;
5478  VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5479 
5480  VuoPort *toPort = externalPort;
5481  VuoNode *toNode = this->publishedOutputNode;
5482 
5483  publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5484  (VuoCompilerPort *)(fromPort->getCompiler()),
5485  NULL,
5486  NULL))->getBase();
5487  publishedCable->setTo(toNode, toPort);
5488  }
5489 
5490  if (forceEventOnlyPublication)
5491  publishedCable->getCompiler()->setAlwaysEventOnly(true);
5492 
5493  return publishedCable;
5494 }
5495 
5507 void VuoEditorComposition::addActiveProtocol(VuoProtocol *protocol, bool useUndoStack)
5508 {
5509  vector<VuoPublishedPort *> publishedPortsToAdd;
5510  map<VuoPublishedPort *, string> publishedPortsToRename;
5511 
5512  // Remove the previously active protocol.
5513  VuoProtocol *previousActiveProtocol = this->activeProtocol;
5514  bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5515 
5516  bool portChangesMadeDuringProtocolRemoval = false;
5517  if (removingPreviousProtocol)
5518  portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5519 
5520  if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5521  {
5522  VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5523  useUndoStack = true;
5524  }
5525 
5526  // Add the newly active protocol.
5527  this->activeProtocol = protocol;
5528  if (!protocol)
5529  return;
5530 
5531  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5532  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5533  {
5534  string portName = i->first;
5535  string portType = i->second;
5536 
5537  bool compositionHadCompatiblePort = false;
5538  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5539  if (preexistingPublishedPort)
5540  {
5541  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5542 
5543  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5544  (!preexistingType && (portType == "")));
5545  if (portTypesMatch)
5546  {
5547  compositionHadCompatiblePort = true;
5548  preexistingPublishedPort->setProtocolPort(true);
5549  }
5550  else
5551  {
5552  compositionHadCompatiblePort = false;
5553  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5554  }
5555  }
5556 
5557  if (!compositionHadCompatiblePort)
5558  {
5559  VuoType *type = compiler->getType(portType)->getBase();
5560  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5561  publishedPort->setProtocolPort(true);
5562 
5563  if (!useUndoStack)
5564  addPublishedPort(publishedPort, true);
5565  else
5566  publishedPortsToAdd.push_back(publishedPort);
5567 
5568  createRendererForPublishedPortInComposition(publishedPort, true);
5569  }
5570  }
5571 
5572  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5573  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5574  {
5575  string portName = i->first;
5576  string portType = i->second;
5577 
5578  bool compositionHadCompatiblePort = false;
5579  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5580  if (preexistingPublishedPort)
5581  {
5582  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5583  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5584  (!preexistingType && (portType == "")));
5585  if (portTypesMatch)
5586  {
5587  compositionHadCompatiblePort = true;
5588  preexistingPublishedPort->setProtocolPort(true);
5589  }
5590  else
5591  {
5592  compositionHadCompatiblePort = false;
5593  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5594  }
5595  }
5596 
5597  if (!compositionHadCompatiblePort)
5598  {
5599  VuoType *type = compiler->getType(portType)->getBase();
5600  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5601  publishedPort->setProtocolPort(true);
5602 
5603  if (!useUndoStack)
5604  addPublishedPort(publishedPort, false);
5605  else
5606  publishedPortsToAdd.push_back(publishedPort);
5607 
5608  createRendererForPublishedPortInComposition(publishedPort, false);
5609  }
5610  }
5611 
5612  if (useUndoStack)
5613  {
5614  bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5615  if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5616  {
5617  set<VuoPublishedPort *> publishedPortsToRemove;
5618  bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5619  bool endUndoStackMacro = true;
5620  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5621  }
5622  }
5623 
5624  emit activeProtocolChanged();
5625 }
5626 
5634 string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5635 {
5636  string modifiedPortName = portName;
5637  if (modifiedPortName.length() > 0)
5638  modifiedPortName[0] = toupper(modifiedPortName[0]);
5639  modifiedPortName = "some" + modifiedPortName;
5640 
5641  return modifiedPortName;
5642 }
5643 
5662 {
5664 
5665  set<VuoPublishedPort *> publishedPortsToRemove;
5666  map<VuoPublishedPort *, string> publishedPortsToRename;
5667 
5668  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5669  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5670  {
5671  string portName = i->first;
5672  string portType = i->second;
5673 
5674  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5675  if (preexistingPublishedPort)
5676  {
5677  bool portCompatibleAcrossProtocolTransition = false;
5678  if (replacementProtocol)
5679  {
5680  vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5681  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5682  {
5683  string replacementPortName = i->first;
5684  string replacementPortType = i->second;
5685 
5686  if ((portName == replacementPortName) && (portType == replacementPortType))
5687  portCompatibleAcrossProtocolTransition = true;
5688  }
5689  }
5690 
5691  if (preexistingPublishedPort->getConnectedCables(true).empty())
5692  publishedPortsToRemove.insert(preexistingPublishedPort);
5693  else if (!portCompatibleAcrossProtocolTransition)
5694  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5695  }
5696  }
5697 
5698  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5699  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5700  {
5701  string portName = i->first;
5702  string portType = i->second;
5703 
5704  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5705  if (preexistingPublishedPort)
5706  {
5707  bool portCompatibleAcrossProtocolTransition = false;
5708  if (replacementProtocol)
5709  {
5710  vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5711  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5712  {
5713  string replacementPortName = i->first;
5714  string replacementPortType = i->second;
5715 
5716  if ((portName == replacementPortName) && (portType == replacementPortType))
5717  portCompatibleAcrossProtocolTransition = true;
5718  }
5719  }
5720 
5721  if (preexistingPublishedPort->getConnectedCables(true).empty())
5722  publishedPortsToRemove.insert(preexistingPublishedPort);
5723  else if (!portCompatibleAcrossProtocolTransition)
5724  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5725  }
5726  }
5727 
5728  // If we are removing any ports, the composition will no longer be deemed to adhere to the
5729  // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5730  if (!publishedPortsToRemove.empty())
5731  publishedPortsToRename.clear();
5732 
5733  bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5734  if (portChangesRequired)
5735  {
5736  vector<VuoPublishedPort *> publishedPortsToAdd;
5737  bool beginUndoStackMacro = true;
5738  bool endUndoStackMacro = !replacementProtocol;
5739  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5740  }
5741 
5742  emit activeProtocolChanged();
5743 
5744  return portChangesRequired;
5745 }
5746 
5754 {
5755  if ((activeProtocol != protocol) || !activeProtocol)
5756  return;
5757 
5758  activeProtocol = NULL;
5759 
5760  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5761  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5762  {
5763  string portName = i->first;
5764  string portType = i->second;
5765 
5766  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5767  if (preexistingPublishedPort)
5768  preexistingPublishedPort->setProtocolPort(false);
5769  }
5770 
5771  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5772  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5773  {
5774  string portName = i->first;
5775  string portType = i->second;
5776 
5777  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5778  if (preexistingPublishedPort)
5779  preexistingPublishedPort->setProtocolPort(false);
5780  }
5781 
5782  emit activeProtocolChanged();
5783 }
5784 
5790 {
5791  return activeProtocol;
5792 }
5793 
5799 {
5800  if (!activeProtocol)
5801  return NULL;
5802 
5803  return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5804 }
5805 
5809 void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5810 {
5811  VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5812 
5813  identifierCache->addPublishedPortToCache(publishedPort);
5814 
5815  if (shouldUpdateUi)
5816  emit publishedPortModified();
5817 }
5818 
5825 int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5826 {
5827  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5828  if (shouldUpdateUi && publishedPort->isProtocolPort())
5830 
5831  int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5832 
5833  if (shouldUpdateUi)
5834  emit publishedPortModified();
5835 
5836  return removalResult;
5837 }
5838 
5844 {
5845  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5846  if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5848 
5849  VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5850 
5851  identifierCache->addPublishedPortToCache( static_cast<VuoPublishedPort *>(publishedPort->getBase()) );
5852 
5853  emit publishedPortModified();
5854 }
5855 
5863 void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5864 {
5865  bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5866  VuoRendererPort *fixedPort = NULL;
5867 
5868  if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5869  fixedPort = cable->getFromPort()->getRenderer();
5870 
5871  else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5872  fixedPort = cable->getToPort()->getRenderer();
5873 
5874  if (fixedPort)
5875  {
5876  highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5877  emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5878  }
5879 }
5880 
5886 void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5887 {
5888  auto types = compiler->getTypes();
5889 
5890  QList<QGraphicsItem *> compositionComponents = items();
5891  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5892  {
5893  QGraphicsItem *compositionComponent = *i;
5894  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5895  if (rn)
5896  {
5897  // Check for eligible internal input ports.
5898  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5899  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
5900  updateEligibilityHighlightingForPort((*inputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
5901 
5902  // Check for eligible internal output ports.
5903  vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
5904  for(vector<VuoPort *>::iterator outputPort = outputPorts.begin(); outputPort != outputPorts.end(); ++outputPort)
5905  updateEligibilityHighlightingForPort((*outputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
5906  }
5907 
5908  // Fade out cables that aren't relevant to the current cable drag.
5909  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5910  if (rc && rc != cable)
5911  {
5912  QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
5913  rc->setCacheMode(QGraphicsItem::NoCache);
5914  rc->updateGeometry();
5915 
5916  VuoPort *otherCablePort = port->getInput()
5917  ? rc->getBase()->getFromPort()
5918  : rc->getBase()->getToPort();
5919 
5920  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(otherCablePort? otherCablePort->getRenderer() : NULL,
5921  port,
5922  !cable->effectivelyCarriesData(),
5923  types);
5924 
5925  // Don't apply extra highlighting to compatible, already-connected cables.
5926  if (highlight == VuoRendererColors::standardHighlight)
5927  highlight = VuoRendererColors::noHighlight;
5928 
5929  rc->setEligibilityHighlight(highlight);
5930 
5931  rc->setCacheMode(normalCacheMode);
5932  }
5933  }
5934 
5935  // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
5936  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5937  updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(*i));
5938 }
5939 
5944 void VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
5945  VuoRendererPort *fixedPort,
5946  bool eventOnlyConnection,
5947  map<string, VuoCompilerType *> &types)
5948 {
5949  QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
5950  portToHighlight->setCacheMode(QGraphicsItem::NoCache);
5951 
5952  portToHighlight->updateGeometry();
5953 
5954  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection, types);
5955 
5956  portToHighlight->setEligibilityHighlight(highlight);
5957  VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
5958  if (typecastPortToHighlight)
5959  typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
5960 
5961  portToHighlight->setCacheMode(normalCacheMode);
5962 
5963  if (typecastPortToHighlight)
5964  updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection, types);
5965 }
5966 
5977 VuoRendererColors::HighlightType VuoEditorComposition::getEligibilityHighlightingForPort(VuoRendererPort *portToHighlight, VuoRendererPort *fixedPort, bool eventOnlyConnection, map<string, VuoCompilerType *> &types)
5978 {
5979  // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
5980  VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
5981  VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
5982 
5983  VuoRendererPort *fromPort;
5984  VuoRendererPort *toPort;
5985  bool forwardConnection;
5986  if (fixedPort->getOutput())
5987  {
5988  fromPort = fixedPort;
5989  toPort = portToHighlight;
5990  forwardConnection = true;
5991  }
5992  else
5993  {
5994  fromPort = portToHighlight;
5995  toPort = fixedPort;
5996  forwardConnection = false;
5997  }
5998 
5999  bool directConnectionPossible;
6000 
6001  // Temporarily disallow direct cable connections between published inputs and published outputs.
6002  // @todo: Allow for https://b33p.net/kosada/node/7756 .
6003  if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
6004  directConnectionPossible = false;
6005  else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
6006  directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
6007  else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
6008  directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
6009  else // both ports are internal canvas ports
6010  directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
6011 
6013  if (directConnectionPossible)
6015  else if (!findBridgingSolutions(fromPort, toPort, forwardConnection, types).empty())
6017  else if (fixedPort == portToHighlight)
6018  highlight = VuoRendererColors::noHighlight;
6019  else
6021 
6022  return highlight;
6023 }
6024 
6040 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6041  VuoRendererPort *toPort,
6042  bool eventOnlyConnection,
6043  bool forwardConnection)
6044 {
6045  VuoRendererPort *portToRespecialize = NULL;
6046  string respecializedTypeName = "";
6047 
6048  return canConnectDirectlyWithRespecializationNondestructively(fromPort,
6049  toPort,
6050  eventOnlyConnection,
6051  forwardConnection,
6052  &portToRespecialize,
6053  respecializedTypeName);
6054 }
6055 
6066 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6067  VuoRendererPort *toPort,
6068  bool eventOnlyConnection,
6069  bool forwardConnection,
6070  VuoRendererPort **portToRespecialize,
6071  string &respecializedTypeName)
6072 {
6073  *portToRespecialize = NULL;
6074  respecializedTypeName = "";
6075 
6076  bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6077  toPort,
6078  eventOnlyConnection,
6079  forwardConnection,
6080  portToRespecialize,
6081  respecializedTypeName);
6082  if (!canConnectWithRespecialization)
6083  return false;
6084 
6085  if (canConnectWithRespecialization && !portToRespecialize)
6086  return true;
6087 
6088  bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6089  if (!nondestructive)
6090  {
6091  *portToRespecialize = NULL;
6092  respecializedTypeName = "";
6093  }
6094  return nondestructive;
6095 }
6096 
6102 bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6103 {
6104  map<VuoNode *, string> nodesToReplace;
6105  set<VuoCable *> cablesToDelete;
6106  createReplacementsToUnspecializePort(portToUnspecialize, false, nodesToReplace, cablesToDelete);
6107 
6108  // Check whether unspecialization would disconnect any existing cables
6109  // (other than the cable that would normally be displaced by the new cable connection).
6110  if (cablesToDelete.empty())
6111  return true;
6112 
6113  else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6114  return true;
6115 
6116  return false;
6117 }
6118 
6138 bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6139  VuoRendererPort *toPort,
6140  bool eventOnlyConnection,
6141  bool forwardConnection,
6142  VuoRendererPort **portToRespecialize,
6143  string &respecializedTypeName)
6144 {
6145  // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6146 
6147  *portToRespecialize = NULL;
6148  respecializedTypeName = "";
6149 
6150  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6151  if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6152  return true;
6153 
6154  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6155  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6156  return true;
6157 
6158  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6159  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6160 
6161  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6162  return false;
6163 
6164  VuoType *currentFromDataType = fromPort->getDataType();
6165  VuoType *currentToDataType = toPort->getDataType();
6166 
6167  if (!(currentFromDataType && currentToDataType))
6168  return false;
6169 
6171  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6172  return false;
6173 
6174  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6175  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6176 
6177  VuoGenericType *originalFromGenericType = NULL;
6178  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6179  {
6181  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6182  if (fromSpecializedNodeClass)
6183  {
6184  VuoPortClass *portClass = fromPort->getBase()->getClass();
6185  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6186  }
6187  }
6188 
6189  VuoGenericType *originalToGenericType = NULL;
6190  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6191  {
6193  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6194  if (toSpecializedNodeClass)
6195  {
6196  VuoPortClass *portClass = toPort->getBase()->getClass();
6197  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6198  }
6199  }
6200 
6201  // Determine whether the port at each endpoint is 1) generic, or
6202  // 2) specialized and currently revertible, or 3) effectively static.
6203  bool fromPortIsGeneric = currentFromGenericType;
6204  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6205  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6206 
6207  bool toPortIsGeneric = currentToGenericType;
6208  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6209  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6210 
6211  // Figure out which port to try to respecialize, and to what type.
6212  set<string> compatibleTypes;
6213  string specializedType = "";
6214  VuoRendererPort *portToTryToRespecialize = NULL;
6215 
6216  // Case: One port static, one port specialized.
6217  if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6218  {
6219  VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6220  specializedType = staticPort->getDataType()->getModuleKey();
6221  portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6222  }
6223 
6224  // Case: One port specialized, other port generic or specialized.
6225  else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6226  {
6227  VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6228  bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6229 
6230  VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6231  bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6232 
6233  // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6234  /*
6235  if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6236  {
6237  specializedType = dragDestination->getDataType()->getModuleKey();
6238  portToTryToRespecialize = dragSource;
6239  }
6240  else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6241  {
6242  specializedType = dragSource->getDataType()->getModuleKey();
6243  portToTryToRespecialize = dragDestination;
6244  }
6245  else
6246  */
6247 
6248  if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6249  {
6250  specializedType = dragSource->getDataType()->getModuleKey();
6251  portToTryToRespecialize = dragDestination;
6252  }
6253  }
6254 
6255  // @todo https://b33p.net/kosada/node/10481 Other cases.
6256  else
6257  return false;
6258 
6259  if (portToTryToRespecialize)
6260  compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6261 
6262  bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6263 
6264  if (portsAreCompatible)
6265  {
6266  *portToRespecialize = portToTryToRespecialize;
6267  respecializedTypeName = specializedType;
6268  }
6269 
6270  return portsAreCompatible;
6271 }
6272 
6279 void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6280 {
6281  VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6282  if (drawer)
6283  {
6285  foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6287  bestEligibility = VuoRendererColors::standardHighlight;
6289  && bestEligibility != VuoRendererColors::standardHighlight)
6290  bestEligibility = VuoRendererColors::subtleHighlight;
6291 
6292  // If this drawer has no eligible ports, fade it out.
6293  {
6294  QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6295  drawer->setCacheMode(QGraphicsItem::NoCache);
6296  drawer->updateGeometry();
6297 
6298  drawer->setEligibilityHighlight(bestEligibility);
6299 
6300  drawer->setCacheMode(normalCacheMode);
6301  }
6302 
6303  // Make sure the host port is repainted to take into account the eligibility of its drawer ports.
6304  if (drawer->getRenderedHostPort()
6305  && drawer->getRenderedHostPort()->getRenderer())
6306  {
6307  VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6308 
6309  QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6310  hostPort->setCacheMode(QGraphicsItem::NoCache);
6311  hostPort->updateGeometry();
6312  hostPort->setCacheMode(normalCacheMode);
6313  }
6314  }
6315 }
6316 
6321 {
6324 }
6325 
6345  VuoRendererPort *toPort,
6346  bool toPortIsDragDestination,
6347  VuoRendererPort **portToSpecialize,
6348  string &specializedTypeName,
6349  string &typecastToInsert)
6350 {
6351  *portToSpecialize = NULL;
6352  specializedTypeName = "";
6353 
6354  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6355  map<string, string> specializedTypeNameForTypecast;
6356 
6357  auto types = compiler->getTypes();
6358  vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6359  bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6360 
6361  if (!solutionSelected)
6362  return false;
6363 
6364  if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6365  *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6366  if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6367  specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6368 
6369  return true;
6370 }
6371 
6390 bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6391  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6392  map<string, string> specializedTypeNameForTypecast,
6393  string &selectedTypecast)
6394 {
6395  if (suitableTypecasts.empty())
6396  {
6397  selectedTypecast = "";
6398  return false;
6399  }
6400 
6401  else if (suitableTypecasts.size() == 1)
6402  {
6403  selectedTypecast = suitableTypecasts[0];
6404  return true;
6405  }
6406 
6407  else
6408  return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6409 }
6410 
6416 bool VuoEditorComposition::portsPassSanityCheckToBridge(VuoRendererPort *fromPort, VuoRendererPort *toPort)
6417 {
6418  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6419  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6420 
6421  return (fromPortIsEnabledOutput && toPortIsEnabledInput &&
6422  fromPort->getBase()->getClass()->hasCompiler() &&
6423  toPort->getBase()->getClass()->hasCompiler());
6424 }
6425 
6431 bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort, VuoType *candidateFromType, VuoType *candidateToType)
6432 {
6433  if (!portsPassSanityCheckToBridge(fromPort, toPort))
6434  return false;
6435 
6436  VuoType *inType = (candidateFromType? candidateFromType : static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6437  VuoType *outType = (candidateToType? candidateToType : static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6438 
6439  // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6440  if (inType && (inType->getModuleKey() == "VuoBoolean") && outType && (outType->getModuleKey() == "VuoInteger"))
6441  {
6442  bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6447  );
6448 
6449  if (toNodeUsesIndex)
6450  return false;
6451  }
6452 
6453  return true;
6454 }
6455 
6475  VuoRendererPort *toPort,
6476  bool toPortIsDragDestination,
6477  map<string, VuoCompilerType *> &types)
6478 {
6479  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6480  map<string, string> specializedTypeNameForTypecast;
6481  return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6482 }
6483 
6494  VuoRendererPort *toPort,
6495  bool toPortIsDragDestination,
6496  map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6497  map<string, string> &specializedTypeNameForTypecast,
6498  map<string, VuoCompilerType *> &types)
6499 {
6500  // If `limitCombinations` is `true`, first considers solutions that involve typeconversion
6501  // or specialization, but not both; if no such solution exists, returns solutions that involve
6502  // typeconversion+specialization combinations.
6503  // If `limitCombinations` is `false`, returns all solutions, whether they involve typeconversion,
6504  // specialization, or both.
6505  const bool limitCombinations = true;
6506 
6507  portToSpecializeForTypecast.clear();
6508  specializedTypeNameForTypecast.clear();
6509  vector<string> suitableTypecasts;
6510 
6511  if (!portsPassSanityCheckToBridge(fromPort, toPort))
6512  return suitableTypecasts;
6513 
6514  // Temporarily disallow direct cable connections between published inputs and published outputs.
6515  // @todo: Allow for https://b33p.net/kosada/node/7756 .
6516  if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6517  return suitableTypecasts;
6518 
6519  // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6520  {
6521  VuoRendererPort *portToSpecialize = NULL;
6522  string specializedTypeName = "";
6523  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6524  {
6525  suitableTypecasts.push_back("");
6526  portToSpecializeForTypecast[""] = portToSpecialize;
6527  specializedTypeNameForTypecast[""] = specializedTypeName;
6528 
6529  return suitableTypecasts;
6530  }
6531  }
6532 
6533  VuoType *currentFromDataType = fromPort->getDataType();
6534  VuoType *currentToDataType = toPort->getDataType();
6535 
6536  if (!(currentFromDataType && currentToDataType))
6537  return suitableTypecasts;
6538 
6539  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6540  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6541 
6542  VuoGenericType *originalFromGenericType = NULL;
6543  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6544  {
6546  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6547  if (fromSpecializedNodeClass)
6548  {
6549  VuoPortClass *portClass = fromPort->getBase()->getClass();
6550  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6551  }
6552  }
6553 
6554  VuoGenericType *originalToGenericType = NULL;
6555  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6556  {
6558  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6559  if (toSpecializedNodeClass)
6560  {
6561  VuoPortClass *portClass = toPort->getBase()->getClass();
6562  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6563  }
6564  }
6565 
6566  // Determine whether the port at each endpoint is:
6567  // 1) generic (unspecialized), or
6568  // 2) specialized and currently revertible, or
6569  // 3) effectively static.
6570  bool fromPortIsGeneric = currentFromGenericType;
6571  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6572  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6573 
6574  bool toPortIsGeneric = currentToGenericType;
6575  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6576  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6577 
6578  // No typeconversion or specialization options between two unspecialized generic ports.
6579  if (fromPortIsGeneric && toPortIsGeneric)
6580  return suitableTypecasts;
6581 
6582  // Typeconversion options but no specialization options between two static ports.
6583  else if (fromPortIsStatic && toPortIsStatic)
6584  {
6585  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6586  suitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6587  return suitableTypecasts;
6588  }
6589 
6590  // Remaining combinations might require (re-)specializing one port or the other.
6591  // Figure out which port to consider (re-)specializing.
6592  bool specializeToPort = true;
6593  if (toPortIsGeneric)
6594  specializeToPort = true;
6595  else if (fromPortIsGeneric)
6596  specializeToPort = false;
6597  else if (fromPortIsSpecialized && toPortIsStatic)
6598  specializeToPort = false;
6599  else if (fromPortIsStatic && toPortIsSpecialized)
6600  specializeToPort = true;
6601  else if (fromPortIsSpecialized && toPortIsSpecialized)
6602  specializeToPort = toPortIsDragDestination;
6603 
6604  // Now that ports have been categorized, figure out what combinations of (re-)specialization
6605  // and/or typeconversion we can use to bridge them.
6606  set<string> compatibleTypes;
6607  if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6608  compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6609  else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6610  compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6611 
6612  // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6613  // non-destructively unspecializable, since it already has the appropriate specialization.
6614  compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6615 
6616  if (limitCombinations)
6617  {
6618  vector<string> limitedSuitableTypecasts;
6619 
6620  // Check for bridging solutions that involve typeconversion without specialization.
6621  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6622  {
6623  limitedSuitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6624  foreach (string typecastName, limitedSuitableTypecasts)
6625  {
6626  portToSpecializeForTypecast[typecastName] = specializeToPort? toPort : fromPort;
6627  specializedTypeNameForTypecast[typecastName] = specializeToPort? currentToDataType->getModuleKey() :
6628  currentFromDataType->getModuleKey();
6629  }
6630  }
6631 
6632  // Check for bridging solutions that involve specialization without typeconversion.
6633  string fixedDataType = specializeToPort? currentFromDataType->getModuleKey() : currentToDataType->getModuleKey();
6634  if (compatibleTypes.find(fixedDataType) != compatibleTypes.end())
6635  {
6636  limitedSuitableTypecasts.push_back("");
6637  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6638  specializedTypeNameForTypecast[""] = fixedDataType;
6639  }
6640 
6641  if (limitedSuitableTypecasts.size() >= 1)
6642  return limitedSuitableTypecasts;
6643  }
6644 
6645  foreach (string compatibleTypeName, compatibleTypes)
6646  {
6647  VuoCompilerType *compatibleSpecializedType = types[compatibleTypeName];
6648  if (!compatibleSpecializedType)
6649  compatibleSpecializedType = compiler->getType(compatibleTypeName);
6650  VuoType *candidateFromType = specializeToPort? currentFromDataType : compatibleSpecializedType->getBase();
6651  VuoType *candidateToType = specializeToPort? compatibleSpecializedType->getBase() : currentToDataType;
6652 
6653  if (compatibleSpecializedType)
6654  {
6655  // Re-specialization without typeconversion may be possible.
6656  if (candidateFromType == candidateToType)
6657  {
6658  suitableTypecasts.push_back("");
6659  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6660  specializedTypeNameForTypecast[""] = compatibleSpecializedType->getBase()->getModuleKey();
6661  }
6662 
6663  if (portsPassSanityCheckToTypeconvert(fromPort,
6664  toPort,
6665  candidateFromType,
6666  candidateToType))
6667  {
6668  vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromType, candidateToType);
6669  foreach (string typecast, suitableTypecastsForCurrentTypes)
6670  {
6671  suitableTypecasts.push_back(typecast);
6672  portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6673  specializedTypeNameForTypecast[typecast] = compatibleSpecializedType->getBase()->getModuleKey();
6674  }
6675  }
6676  }
6677  }
6678 
6679  return suitableTypecasts;
6680 }
6681 
6694 bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6695  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6696  map<string, string> specializedTypeNameForTypecast,
6697  string &selectedTypecast)
6698 {
6699  QMenu typecastMenu(views()[0]->viewport());
6700  typecastMenu.setSeparatorsCollapsible(false);
6701  QString spacer(" ");
6702 
6703  // Inventory specialization options
6704  set <pair<VuoRendererPort *, string> > specializationDetails;
6705  vector<string> typeconversionOptionsRequiringNoSpecialization;
6706  foreach (string typecastClassName, suitableTypecasts)
6707  {
6708  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6709  string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6710  specializationDetails.insert(std::make_pair(portToSpecialize,
6711  specializedTypeName));
6712 
6713  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6714  if (portAlreadyHasTargetType)
6715  typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6716  }
6717 
6718  // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6719  // specialization heading under which multiple typeconversion options may be listed.
6720  // Selecting this item itself will invoke the specialization.
6721  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6722  {
6723  QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6724  QAction *typecastAction = typecastMenu.addAction(menuText);
6725  typecastAction->setData(QVariant(""));
6726  }
6727 
6728  bool foundSpecializationOptionsRequiringNoTypeconversion = typecastMenu.actions().size() >= 1;
6729  bool foundTypeconversionOptionsRequiringNoSpecialization = typeconversionOptionsRequiringNoSpecialization.size() >= 1;
6730 
6731  // If there are bridging options that require no specialization, list them next.
6732  bool includingTypeconvertWithNoSpecializationHeader = foundSpecializationOptionsRequiringNoTypeconversion;
6733  if (foundTypeconversionOptionsRequiringNoSpecialization)
6734  {
6735  if (foundSpecializationOptionsRequiringNoTypeconversion)
6736  typecastMenu.addSeparator();
6737 
6738  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6739  string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6740 
6741  if (portToSpecialize && !specializedTypeName.empty())
6742  {
6743  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6744  QAction *typecastAction = typecastMenu.addAction(menuText);
6745  typecastAction->setEnabled(false);
6746  includingTypeconvertWithNoSpecializationHeader = true;
6747  }
6748  }
6749 
6750  foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6751  {
6752  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6753  if (typecastClass)
6754  {
6755  QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6756  typecastAction->setData(QVariant(typecastClassName.c_str()));
6757  }
6758  }
6759 
6760  // Now list the remaining bridging options.
6761  for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6762  {
6763  VuoRendererPort *portToSpecialize = i->first;
6764  string specializedTypeName = i->second;
6765 
6766  // We've already listed the no-typeconversion bridging option, so skip it here.
6767  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6768  (portToSpecializeForTypecast[""] == portToSpecialize) &&
6769  (specializedTypeNameForTypecast[""] == specializedTypeName))
6770  {
6771  continue;
6772  }
6773 
6774  // We've already listed the no-specialization bridging option, so skip it here.
6775  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6776  if (portAlreadyHasTargetType)
6777  {
6778  continue;
6779  }
6780 
6781  if (typecastMenu.actions().size() >= 1)
6782  typecastMenu.addSeparator();
6783 
6784  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6785  QAction *typecastAction = typecastMenu.addAction(menuText);
6786  typecastAction->setEnabled(false);
6787 
6788  // Inventory typeconversion options associated with this specialization option.
6789  foreach (string typecastClassName, suitableTypecasts)
6790  {
6791  if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6792  (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6793  {
6794  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6795  if (typecastClass)
6796  {
6797  QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6798  typecastAction->setData(QVariant(typecastClassName.c_str()));
6799  }
6800  }
6801  }
6802  }
6803 
6804  menuSelectionInProgress = true;
6805  QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6806  menuSelectionInProgress = false;
6807 
6808  selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6809  return selectedTypecastAction;
6810 }
6811 
6815 QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6816 {
6817  if (!portToSpecialize || specializedTypeName.empty())
6818  return "";
6819 
6820  bool isInput = portToSpecialize && portToSpecialize->getInput();
6821  QString typeDisplayName = compiler->getType(specializedTypeName)?
6822  formatTypeNameForDisplay(compiler->getType(specializedTypeName)->getBase()) :
6823  specializedTypeName.c_str();
6824 
6825  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6826 
6827  QString displayText;
6828  if (portAlreadyHasTargetType)
6829  {
6830  if (isInput)
6831  {
6832  //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6833  displayText = tr("Keep Input Port as %1");
6834  }
6835  else
6836  {
6837  //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6838  displayText = tr("Keep Output Port as %1");
6839  }
6840  }
6841  else
6842  {
6843  if (isInput)
6844  {
6845  //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6846  displayText = tr("Change Input Port to %1");
6847  }
6848  else
6849  {
6850  //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6851  displayText = tr("Change Output Port to %1");
6852  }
6853  }
6854 
6855  return displayText.arg(typeDisplayName);
6856 }
6857 
6863 {
6864  __block json_object *portValue = NULL;
6865 
6866  if (! port->getRenderer()->getDataType())
6867  return portValue;
6868 
6869  string runningPortIdentifier = identifierCache->getIdentifierForPort(port);
6870  bool isInput = port->getRenderer()->getInput();
6871 
6872  void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6873  {
6874  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6875  if (topLevelComposition->isRunningThreadUnsafe())
6876  {
6877  portValue = isInput ?
6878  topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6879  topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6880  }
6881  });
6882  };
6883  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6884 
6885  return portValue;
6886 }
6887 
6891 string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6892 {
6893  return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6894 }
6895 
6902 {
6903  if (!staticPort)
6904  return "";
6905 
6906  // Published ports
6907  if (dynamic_cast<VuoPublishedPort *>(staticPort))
6908  return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
6909 
6910  // Internal ports
6911  // We might as well use the same naming scheme here as is used in the running composition,
6912  // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
6913  // node identifier has been explicitly set.
6914  string nodeIdentifier = "";
6915  if (parentNode && parentNode->hasCompiler())
6916  nodeIdentifier = parentNode->getCompiler()->getIdentifier();
6917  else if (staticPort->hasRenderer() &&
6918  staticPort->getRenderer()->getUnderlyingParentNode() &&
6919  staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->hasCompiler())
6920  {
6921  nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
6922  }
6923 
6924  if (staticPort->hasCompiler() && !nodeIdentifier.empty())
6925  {
6926  dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
6927  return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
6928  }
6929  else
6930  return "";
6931 }
6932 
6937 {
6938  VuoPort *port = nullptr;
6939  identifierCache->doForPortWithIdentifier(portID, [&port](VuoPort *p) {
6940  port = p;
6941  });
6942  return port;
6943 }
6944 
6951 {
6952  if (port->hasRenderer())
6953  {
6954  if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
6955  {
6956  bool isPublishedInput = !port->getRenderer()->getInput();
6957  return (isPublishedInput? composition->getPublishedInputNode() :
6958  composition->getPublishedOutputNode());
6959  }
6960 
6961  else
6962  return port->getRenderer()->getUnderlyingParentNode()->getBase();
6963  }
6964 
6965  foreach (VuoNode *n, composition->getBase()->getNodes())
6966  {
6967  VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
6968  if (candidateInputPort == port)
6969  return n;
6970 
6971  VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
6972  if (candidateOutputPort == port)
6973  return n;
6974  }
6975 
6976  return NULL;
6977 }
6978 
6987 {
6988  map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
6989  if (popover != activePortPopovers.end())
6990  return popover->second;
6991 
6992  else
6993  return NULL;
6994 }
6995 
7003 void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
7004 {
7005  string portID = identifierCache->getIdentifierForPort(rp->getBase());
7006  bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
7007  if (!popoverJustClosedAtLastEvent)
7009 }
7010 
7015 {
7016  if (!popoverEventsEnabled)
7017  return;
7018 
7019  VuoPort *port = rp->getBase();
7020  string portID = identifierCache->getIdentifierForPort(port);
7021 
7022  VUserLog("%s: Open popover for %s",
7023  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7024  portID.c_str());
7025 
7026  dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
7027 
7028  dispatch_sync(activePortPopoversQueue, ^{
7029 
7030  if (activePortPopovers.find(portID) == activePortPopovers.end())
7031  {
7032  // Assigning the popover a parent widget allows us to give it rounded corners
7033  // and a background fill that respects its rounded boundaries.
7034  VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
7035 
7036  connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
7037  connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
7038  VUserLog("%s: Detach popover for %s",
7039  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7040  portID.c_str());
7041  popoverDetached();
7042  });
7043  connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
7046 
7047  // Line up the top left of the dialog with the port.
7048  QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
7049 
7050  // Don't let popovers get cut off at the right or bottom edges of the canvas.
7051  const int cutoffMargin = 16;
7052  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
7053  if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
7054  portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
7055  if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
7056  portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
7057 
7058  QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
7059 
7060  const QPoint offset = QPoint(12, 6);
7061 
7062  QPoint popoverTopLeft = popoverLeftInView + offset;
7063  popover->move(popoverTopLeft);
7064  popover->show();
7065 
7066  activePortPopovers[portID] = popover;
7067 
7068  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
7069  updateDataInPortPopover(portID);
7070  });
7071  }
7072  });
7073  });
7074 }
7075 
7080 void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
7081 {
7082  if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
7084 }
7085 
7094 {
7095  VUserLog("%s: Close popover for %s",
7096  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7097  portID.c_str());
7098 
7099  VuoPortPopover *popover = NULL;
7100  map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
7101  if (i != activePortPopovers.end())
7102  {
7103  popover = i->second;
7104  activePortPopovers.erase(i);
7105  }
7106 
7107  if (popover)
7108  {
7109  popover->hide();
7110  delete popover;
7111  }
7112 
7113  bool isInput = false;
7114  bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7115  isInput = port->getRenderer()->getInput();
7116  });
7117 
7118  if (! foundPort)
7119  return;
7120 
7121  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7122  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7123  {
7124  dispatch_async(topLevelComposition->runCompositionQueue, ^{
7125  if (topLevelComposition->isRunningThreadUnsafe())
7126  {
7127  (isInput ?
7128  topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7129  topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7130  }
7131  });
7132  });
7133  });
7134 }
7135 
7139 void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7140 {
7141  dispatch_sync(activePortPopoversQueue, ^{
7142  disablePopoverForPort(portID);
7143  });
7144 }
7145 
7150 {
7151  disablePortPopovers();
7153 }
7154 
7159 {
7160  foreach (VuoErrorPopover *errorPopover, errorPopovers)
7161  {
7162  errorPopover->hide();
7163  errorPopover->deleteLater();
7164  }
7165 
7166  errorPopovers.clear();
7167 }
7168 
7173 void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7174 {
7175  dispatch_sync(activePortPopoversQueue, ^{
7176  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7177  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7178  {
7179  string portID = i->first;
7180  bool shouldDisable = false;
7181 
7182  if (! node)
7183  shouldDisable = true;
7184  else
7185  {
7186  identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7187  shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7188  });
7189  }
7190 
7191  if (shouldDisable)
7192  disablePopoverForPort(portID);
7193  }
7194  });
7195 }
7196 
7201 {
7202  dispatch_sync(activePortPopoversQueue, ^{
7203  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7204  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7205  {
7206  string portID = i->first;
7207 
7208  bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&foundPort](VuoPort *port) {});
7209  if (! foundPort)
7210  disablePopoverForPort(portID);
7211  }
7212  });
7213 }
7214 
7220 {
7221  if (recordWhichPopoversClosed)
7222  portsWithPopoversClosedAtLastEvent.clear();
7223 
7224  dispatch_sync(activePortPopoversQueue, ^{
7225  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7226  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7227  {
7228  string portID = i->first;
7229  bool shouldDisable = false;
7230 
7231  if (! node)
7232  shouldDisable = true;
7233  else
7234  {
7235  identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7236  shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7237  });
7238  }
7239 
7240  if (shouldDisable)
7241  {
7242  VuoPortPopover *popover = getActivePopoverForPort(portID);
7243  if (! (popover && popover->getDetached()))
7244  {
7245  disablePopoverForPort(portID);
7246  portsWithPopoversClosedAtLastEvent.insert(portID);
7247  }
7248  }
7249  }
7250  });
7251 }
7252 
7257 {
7258  moveDetachedPortPopoversBy(dx, dy);
7259  moveErrorPopoversBy(dx, dy);
7260 }
7261 
7265 void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7266 {
7267  foreach(VuoErrorPopover *errorPopover, errorPopovers)
7268  errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7269 }
7270 
7274 void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7275 {
7276  dispatch_sync(activePortPopoversQueue, ^{
7277  map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7278  for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7279  {
7280  VuoPortPopover *popover = i->second;
7281  if (popover && popover->getDetached())
7282  popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7283  }
7284  });
7285 }
7286 
7290 void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7291 {
7292  dispatch_sync(activePortPopoversQueue, ^{
7293  auto portPopovers = activePortPopovers;
7294  for (auto i : portPopovers)
7295  {
7296  VuoPortPopover *popover = i.second;
7297  if (popover && popover->getDetached())
7298  {
7299  id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7300  objc_msgSend(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7301  }
7302  }
7303  });
7304 }
7305 
7311 {
7312  dispatch_sync(activePortPopoversQueue, ^{
7313  for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7314  {
7315  string portID = i->first;
7316  VuoPortPopover *popover = i->second;
7317  bool shouldUpdate = false;
7318 
7319  if (! node)
7320  shouldUpdate = true;
7321  else
7322  {
7323  identifierCache->doForPortWithIdentifier(portID, [&shouldUpdate, node](VuoPort *port) {
7324  shouldUpdate = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7325  });
7326  }
7327 
7328  if (shouldUpdate)
7329  QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7330  }
7331  });
7332 }
7333 
7346  string popoverCompositionIdentifier,
7347  string portID)
7348 {
7349  bool isInput;
7350  bool foundPort = popoverComposition->identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7351  isInput = port->getRenderer()->getInput();
7352  });
7353 
7354  if (! foundPort)
7355  return;
7356 
7357  string portSummary = (isInput ?
7358  runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7359  runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7360 
7361  dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7362  VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7363  if (popover)
7364  {
7365  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7366  QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7367  }
7368  });
7369 }
7370 
7379 {
7380  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7381  {
7382  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7383  if (topLevelComposition->isRunningThreadUnsafe())
7384  topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7385  });
7386  });
7387 }
7388 
7393 void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7394  bool receivedEvent, bool receivedData, string dataSummary)
7395 {
7396  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7397  {
7398  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7399  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7400 
7401  if (popover)
7402  {
7403  if (receivedEvent && receivedData)
7404  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7405  else if (receivedEvent)
7406  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7407  else if (receivedData)
7408  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7409  }
7410  });
7411  };
7412  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7413 }
7414 
7419 void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7420  bool sentEvent, bool sentData, string dataSummary)
7421 {
7422  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7423  {
7424  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7425  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7426 
7427  if (popover)
7428  {
7429  if (sentEvent && sentData)
7430  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7431  else if (sentEvent)
7432  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7433  else if (sentData)
7434  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7435  }
7436  });
7437 
7438  if (matchingComposition->showEventsMode && sentEvent)
7439  {
7440  matchingComposition->identifierCache->doForPortWithIdentifier(portIdentifier, [matchingComposition](VuoPort *port) {
7441  if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7442  {
7443  port->getRenderer()->setFiredEvent();
7444  matchingComposition->animatePort(port->getRenderer());
7445  }
7446  });
7447  }
7448  };
7449  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7450 }
7451 
7456 void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7457 {
7458  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7459  {
7460  dispatch_async(matchingComposition->runCompositionQueue, ^{
7461  if (matchingComposition->isRunningThreadUnsafe())
7462  {
7463  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7464  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7465  if (popover)
7466  QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7467  });
7468  }
7469  });
7470  };
7471  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7472 }
7473 
7478 void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7479 {
7480  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7481  {
7482  if (matchingComposition->showEventsMode)
7483  {
7484  dispatch_async(this->runCompositionQueue, ^{
7485  if (this->isRunningThreadUnsafe())
7486  {
7487  matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7488  node->getRenderer()->setExecutionBegun();
7489  });
7490  }
7491  });
7492  }
7493  };
7494  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7495 }
7496 
7501 void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7502 {
7503  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7504  {
7505  if (matchingComposition->showEventsMode)
7506  {
7507  dispatch_async(this->runCompositionQueue, ^{
7508  if (this->isRunningThreadUnsafe())
7509  {
7510  matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7511  node->getRenderer()->setExecutionEnded();
7512  });
7513  }
7514  });
7515  }
7516  };
7517  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7518 }
7519 
7527 {
7528  emit compositionStoppedItself();
7529 }
7530 
7537 {
7539 }
7540 
7545 {
7546  return showEventsMode;
7547 }
7548 
7553 {
7554  this->showEventsMode = showEventsMode;
7555 
7556  if (showEventsMode)
7557  {
7559 
7560  void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7561  {
7562  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7563  if (topLevelComposition->isRunningThreadUnsafe())
7564  topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7565  });
7566  };
7567  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7568  }
7569  else
7570  {
7572 
7573  void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7574  {
7575  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7576  if (topLevelComposition->isRunningThreadUnsafe())
7577  topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7578  });
7579  };
7580  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7581  }
7582 }
7583 
7588 {
7589  foreach (VuoCable *cable, getBase()->getCables())
7590  {
7591  if (cable->getCompiler()->getHidden() && !cable->isPublished())
7592  return true;
7593  }
7594 
7595  return false;
7596 }
7597 
7602 {
7603  foreach (VuoCable *cable, getBase()->getCables())
7604  {
7605  if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7606  return true;
7607  }
7608 
7609  return false;
7610 }
7611 
7616 QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7617 {
7618  VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7619  NULL,
7620  port->getOutput(),
7621  port->getRefreshPort(),
7622  port->getFunctionPort());
7623  animatedPort->setAnimated(true);
7624  animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7625  animatedPort->setParentItem(port->getRenderedParentNode());
7626 
7627  animation->setItem(animatedPort);
7628  animation->setScaleAt(0.0, 1, 1);
7629  animation->setScaleAt(0.999, 3, 3);
7630  animation->setScaleAt(1.0, 1, 1);
7631 
7632  QTimeLine *animationTimeline = animation->timeLine();
7633  animationTimeline->setFrameRange(0, 100);
7634  animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7635  animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7636 
7637  preparedAnimations.insert(animation);
7638  animationForTimeline[animation->timeLine()] = animation;
7639 
7640  connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7641  connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7642 
7643  return animation;
7644 }
7645 
7649 void VuoEditorComposition::animatePort(VuoRendererPort *port)
7650 {
7651  dispatch_async(dispatch_get_main_queue(), ^{
7652  QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7653  if (! animation)
7654  return;
7655 
7656  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7657 
7658  if (animation->timeLine()->state() == QTimeLine::Running)
7659  animation->timeLine()->setCurrentTime(0);
7660 
7661  else
7662  {
7663  animatedPort->setPos(port->pos());
7664  animatedPort->setVisible(true);
7665  animation->timeLine()->start();
7666  }
7667  });
7668 }
7669 
7674 QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7675 {
7676  vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7677 
7678  QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7679  qreal maxPercentAdvanced = -1;
7680 
7681  for (int i = 0; i < animations.size(); ++i)
7682  {
7683  QGraphicsItemAnimation *animation = animations[i];
7684  bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7685  bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7686 
7687  if (! animationPrepared)
7688  return setUpAnimationForPort(animation, port);
7689 
7690  else if (! animationRunning)
7691  return animation;
7692 
7693  // If all of the port's animations are already running, return the
7694  // one that has been running the longest.
7695  qreal percentAdvanced = animation->timeLine()->currentValue();
7696  if (percentAdvanced > maxPercentAdvanced)
7697  {
7698  mostAdvancedAnimation = animation;
7699  maxPercentAdvanced = percentAdvanced;
7700  }
7701  }
7702 
7703  // If no animation is even halfway complete, return NULL to indicate
7704  // that no animation is currently available.
7705  return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7706 }
7707 
7713 void VuoEditorComposition::updatePortAnimation(qreal value)
7714 {
7715  QTimeLine *animationTimeline = (QTimeLine *)sender();
7716  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7717  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7718  const qreal multiplier = 1000.;
7719  animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7720 }
7721 
7726 void VuoEditorComposition::endPortAnimation(void)
7727 {
7728  QTimeLine *animationTimeline = (QTimeLine *)sender();
7729  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7730  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7731  animatedPort->setVisible(false);
7732 }
7733 
7737 void VuoEditorComposition::setDisableDragStickiness(bool disable)
7738 {
7739  this->dragStickinessDisabled = disable;
7740 }
7741 
7746 {
7747  this->ignoreApplicationStateChangeEvents = ignore;
7748 }
7749 
7756 {
7757  this->popoverEventsEnabled = enable;
7758 }
7759 
7764 {
7765  setRenderActivity(true, includePorts);
7766  refreshComponentAlphaLevelTimer->start();
7767 }
7768 
7773 {
7774  refreshComponentAlphaLevelTimer->stop();
7775  setRenderActivity(false);
7776 }
7777 
7794 bool VuoEditorComposition::validateProtocol(VuoEditorWindow *window, bool isExportingMovie)
7795 {
7796  // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7797  if (!activeProtocol)
7798  {
7799  VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7800  return false;
7801  }
7802 
7803  // Can events from at least one trigger reach at least one published output port? If not, report an error.
7804  if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7805  {
7806  QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7807  QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7808  "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7809 
7810  if (isExportingMovie)
7811  errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7812 
7814  QMessageBox messageBox(window);
7815  messageBox.setWindowFlags(Qt::Sheet);
7816  messageBox.setWindowModality(Qt::WindowModal);
7817  messageBox.setFont(fonts->dialogHeadingFont());
7818  messageBox.setTextFormat(Qt::RichText);
7819 
7820  messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7821  messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7822  messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7823  messageBox.setDefaultButton(QMessageBox::Ok);
7824 
7825  messageBox.setText(errorHeadline);
7826  messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7827 
7828  if (messageBox.exec() == QMessageBox::Help)
7829  {
7830  map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7831  map<QString, QString>::iterator i = examples.begin();
7832  if (i != examples.end())
7833  QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7834  }
7835  return false;
7836  }
7837 
7838  return true;
7839 }
7840 
7845 {
7846  return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7847 }
7848 
7853 {
7855 }
7856 
7860 string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7861 {
7862  string dir, file, ext;
7863  VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7864  return file;
7865 }
7866 
7874 {
7875  string customizedName = getBase()->getMetadata()->getCustomizedName();
7876  if (! customizedName.empty())
7877  return QString::fromStdString(customizedName);
7878 
7879  string name = getBase()->getMetadata()->getName();
7880  return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7881 }
7882 
7890 QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7891 {
7892  // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7893  // If the extensionless filename contains dot-delimited segments, use only the final segment.
7894  vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7895  string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7896  fileNameParts[fileNameParts.size()-2] :
7897  (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7898 
7899  // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7900  if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7901  {
7902  string formattedName = fileNameContentPart;
7903  if (formattedName.size() >= 1)
7904  formattedName[0] = toupper(formattedName[0]);
7905 
7906  return QString(formattedName.c_str());
7907  }
7908 
7909  // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
7910  return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
7911 }
7912 
7919 {
7920  QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
7921  if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
7922  {
7923  // If not an official Vuo nodeset, return the name as-is.
7924  return nodeSetName;
7925  }
7926 
7927  map<QString, QString> wordsToReformat;
7928  wordsToReformat["artnet"] = "Art-Net";
7929  wordsToReformat["bcf2000"] = "BCF2000";
7930  wordsToReformat["hid"] = "HID";
7931  wordsToReformat["midi"] = "MIDI";
7932  wordsToReformat["osc"] = "OSC";
7933  wordsToReformat["rss"] = "RSS";
7934  wordsToReformat["ui"] = "UI";
7935  wordsToReformat["url"] = "URL";
7936 
7937  QString nodeSetDisplayName = "";
7938  for (int i = 1; i < wordsInName.size(); ++i)
7939  {
7940  QString currentWord = wordsInName[i];
7941  if (currentWord.size() >= 1)
7942  {
7943  if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
7944  currentWord = wordsToReformat.at(currentWord.toLower());
7945  else
7946  currentWord[0] = currentWord[0].toUpper();
7947 
7948  nodeSetDisplayName += currentWord;
7949 
7950  if (i < wordsInName.size()-1)
7951  nodeSetDisplayName += " ";
7952  }
7953  }
7954  return nodeSetDisplayName;
7955 }
7956 
7963 {
7964  if (!type)
7965  return "(none)";
7966 
7967  string formattedTypeName = "";
7968  if (type->hasCompiler() && VuoCompilerType::isListType(type->getCompiler()))
7969  {
7970  string innerTypeName = VuoType::extractInnermostTypeName(type->getModuleKey());
7971  VuoCompilerType *innerType = compiler->getType(innerTypeName);
7972  if (innerType)
7973  {
7974  string formattedInnerTypeName = innerType->getBase()->getDefaultTitle();
7975  formattedTypeName = "List of " + formattedInnerTypeName + " elements";
7976  }
7977  }
7978  else
7979  formattedTypeName = type->getDefaultTitle();
7980 
7981  return formattedTypeName.c_str();
7982 }
7983 
7988 {
7989  if (!type)
7990  return "Event";
7991 
7992  // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
7993  else if (type->getDefaultTitle() == "2D Point")
7994  return "Point2D";
7995  else if (type->getDefaultTitle() == "3D Point")
7996  return "Point3D";
7997  else if (type->getDefaultTitle() == "4D Point")
7998  return "Point4D";
7999  else if (type->getDefaultTitle() == "2D Transform")
8000  return "Transform2D";
8001  else if (type->getDefaultTitle() == "3D Transform")
8002  return "Transform3D";
8003 
8004  return VuoRendererPort::sanitizePortIdentifier(formatTypeNameForDisplay(type)).toUtf8().constData();
8005 }
8006 
8010 bool VuoEditorComposition::nodeSetMenuActionLessThan(QAction *action1, QAction *action2)
8011 {
8012  QString item1Text = action1->text();
8013  QString item2Text = action2->text();
8014 
8015  // Ignore list prefixes
8016  const QString listPrefix = "List of ";
8017  const QString builtInTypePrefix = "Vuo";
8018 
8019  if (item1Text.startsWith(listPrefix))
8020  {
8021  item1Text.remove(0, listPrefix.length());
8022  if (item1Text.startsWith(builtInTypePrefix))
8023  item1Text.remove(0, builtInTypePrefix.length());
8024  }
8025 
8026  if (item2Text.startsWith(listPrefix))
8027  {
8028  item2Text.remove(0, listPrefix.length());
8029  if (item2Text.startsWith(builtInTypePrefix))
8030  item2Text.remove(0, builtInTypePrefix.length());
8031  }
8032 
8033  // Sort alphabetically by title.
8034  return (item1Text.compare(item2Text, Qt::CaseInsensitive) < 0);
8035 }
8036 
8041 bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
8042 {
8043  qreal item1Y = item1->scenePos().y();
8044  qreal item2Y = item2->scenePos().y();
8045 
8046  qreal item1X = item1->scenePos().x();
8047  qreal item2X = item2->scenePos().x();
8048 
8049  if (item1Y == item2Y)
8050  return (item1X < item2X);
8051 
8052  return item1Y < item2Y;
8053 }
8054 
8061 double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
8062 {
8063  // Assign replacement (successor) node classes a perfect match score.
8064  {
8065  string originalGenericNode1ClassName, originalGenericNode2ClassName;
8066  if (node1->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler()))
8067  originalGenericNode1ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler())->getOriginalGenericNodeClassName();
8068  else
8069  originalGenericNode1ClassName = node1->getClassName();
8070 
8071  if (node2->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler()))
8072  originalGenericNode2ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler())->getOriginalGenericNodeClassName();
8073  else
8074  originalGenericNode2ClassName = node2->getClassName();
8075 
8076  if (VuoEditorUtilities::isNodeClassSuccessorTo(originalGenericNode1ClassName.c_str(), originalGenericNode2ClassName.c_str()))
8077  return 1;
8078  }
8079 
8080  // Compare keywords.
8081  vector<string> node1Keywords = node1->getKeywords();
8082  vector<string> node2Keywords = node2->getKeywords();
8083 
8084  // Compare node set names.
8085  if (node1->getNodeSet())
8086  node1Keywords.push_back(node1->getNodeSet()->getName());
8087 
8088  if (node2->getNodeSet())
8089  node2Keywords.push_back(node2->getNodeSet()->getName());
8090 
8091  // Compare tokens in node class display names.
8092  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
8093  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8094  node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8095 
8096  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
8097  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8098  node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8099 
8100  set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
8101  set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
8102 
8103  set<string> nodeKeywordsIntersection;
8104  std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8105  node2KeywordSet.begin(), node2KeywordSet.end(),
8106  std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8107 
8108  set<string> nodeKeywordsUnion = node1KeywordSet;
8109  nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8110 
8111  // Avoid division by zero.
8112  if (nodeKeywordsUnion.size() == 0)
8113  return 0;
8114 
8115  // Calculate Jaccard similarity.
8116  double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8117 
8118  return nodeSimilarity;
8119 }
8120 
8125 {
8126  emit compositionOnTop(top);
8127 }
8128 
8133 {
8134  emit publishedPortNameEditorRequested(port, false);
8135 }
8136 
8137 VuoEditorComposition::~VuoEditorComposition()
8138 {
8139  dispatch_sync(runCompositionQueue, ^{});
8140  dispatch_release(runCompositionQueue);
8141 
8142  preparedAnimations.clear();
8143  animationForTimeline.clear();
8144 
8145  delete identifierCache;
8146 
8147  moduleManager->deleteWhenReady(); // deletes compiler
8148 }
8149 
8154 {
8155  // Update the canvas color.
8156  setBackgroundTransparent(false);
8157 
8158  // Force repainting the entire canvas.
8159  setComponentCaching(QGraphicsItem::NoCache);
8162 }
8163 
8169 map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8170 {
8171  vector<VuoRendererPort *> sortedPortsToPublish;
8172  foreach (string portID, portsToPublish)
8173  {
8174  VuoPort *port = getPortWithStaticIdentifier(portID);
8175  if (port && port->hasRenderer())
8176  sortedPortsToPublish.push_back(port->getRenderer());
8177  }
8178  std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8179 
8180  map<string, string> publishedPortNames;
8181  foreach (VuoRendererPort *rp, sortedPortsToPublish)
8182  {
8183  rp->updateGeometry();
8184  VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8185 
8186  string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8187  string publishedPortName = (!specializedPublishedPortName.empty()?
8188  specializedPublishedPortName :
8190 
8191  bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8192  VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8193 
8194  publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8195  }
8196 
8197  return publishedPortNames;
8198 }
8199 
8206 {
8207  if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8208  return "";
8209 
8210  // If publishing a port on a "Share Value" node and the node's title has been
8211  // customized, request that title as the published port name.
8215  {
8216  return VuoRendererPort::sanitizePortIdentifier(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8217  }
8218 
8219  return "";
8220 }
8221 
8225 void VuoEditorComposition::repositionPopover()
8226 {
8227  VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8228  if (popover && !popover->getDetached())
8229  {
8230  const int cutoffMargin = 16;
8231  if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8232  popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8233 
8234  if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8235  popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8236  }
8237 }