Vuo  2.0.0
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"
52 
53 #ifdef __APPLE__
54 #include <ApplicationServices/ApplicationServices.h>
55 #include <objc/objc-runtime.h>
56 #endif
57 
58 const qreal VuoEditorComposition::nodeMoveRate = 15; // VuoRendererComposition::minorGridLineSpacing;
59 const qreal VuoEditorComposition::nodeMoveRateMultiplier = 4; // VuoRendererComposition::majorGridLineSpacing / VuoRendererComposition::minorGridLineSpacing
61 const qreal VuoEditorComposition::showEventsModeUpdateInterval = 1000/20.; // interval, in ms, after which to update component transparency levels and animations while in 'Show Events' mode
62 const int VuoEditorComposition::initialChangeNodeSuggestionCount = 10; // The initial number of suggestions to list in the "Change (Node) To" context menu
63 
64 Q_DECLARE_METATYPE(VuoRendererNode *)
65 
66 
70  VuoRendererComposition(baseComposition, false, true)
71 {
72 #if VUO_PRO
73  VuoEditorComposition_Pro();
74 #endif
75 
76  this->window = window;
77  compiler = NULL;
78  inputEditorManager = NULL;
79  activeProtocol = NULL;
80  runner = NULL;
81  runningComposition = NULL;
82  runningCompositionActiveDriver = NULL;
83  runningCompositionLibraries = NULL;
84  stopRequested = false;
85  duplicateOnNextMouseMove = false;
86  duplicationDragInProgress = false;
87  duplicationCancelled = false;
88  cursorPosBeforeDuplicationDragMove = QPointF(0,0);
89  cableInProgress = NULL;
90  cableInProgressWasNew = false;
91  cableInProgressShouldBeWireless = false;
92  portWithDragInitiated = NULL;
93  cableWithYankInitiated = NULL;
94  menuSelectionInProgress = false;
95  previousNearbyItem = NULL;
96  dragStickinessDisabled = false;
97  ignoreApplicationStateChangeEvents = false;
98  popoverEventsEnabled = true;
99  runCompositionQueue = dispatch_queue_create("org.vuo.editor.run", NULL);
100  activePortPopoversQueue = dispatch_queue_create("org.vuo.editor.popovers", NULL);
101  errorMark = NULL;
102  errorMarkingUpdatesEnabled = true;
103  triggerPortToRefire = "";
104 
105  contextMenuDeleteSelected = new QAction(NULL);
106  contextMenuHideSelectedCables = new QAction(NULL);
107  contextMenuRenameSelected = new QAction(NULL);
108  contextMenuRefactorSelected = new QAction(NULL);
109  contextMenuPublishPort = new QAction(NULL);
110  contextMenuDeleteCables = new QAction(NULL);
111  contextMenuHideCables = new QAction(NULL);
112  contextMenuUnhideCables = new QAction(NULL);
113  contextMenuFireEvent = new QAction(NULL);
114  contextMenuAddInputPort = new QAction(NULL);
115  contextMenuRemoveInputPort = new QAction(NULL);
116  contextMenuSetPortConstant = new QAction(NULL);
117  contextMenuEditSelectedComments = new QAction(NULL);
118 
119  contextMenuChangeNode = NULL;
120 
121  contextMenuFireEvent->setText(tr("Fire Event"));
122  contextMenuHideSelectedCables->setText(tr("Hide"));
123  contextMenuRenameSelected->setText(tr("Rename…"));
124  contextMenuRefactorSelected->setText(tr("Package as Subcomposition"));
125  contextMenuAddInputPort->setText(tr("Add Input Port"));
126  contextMenuRemoveInputPort->setText(tr("Remove Input Port"));
127  contextMenuSetPortConstant->setText(tr("Edit Value…"));
128  contextMenuEditSelectedComments->setText(tr("Edit…"));
129 
130  connect(contextMenuDeleteSelected, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
131  connect(contextMenuHideSelectedCables, &QAction::triggered, this, &VuoEditorComposition::selectedInternalCablesHidden);
132  connect(contextMenuRenameSelected, &QAction::triggered, this, &VuoEditorComposition::renameSelectedNodes);
133  connect(contextMenuRefactorSelected, &QAction::triggered, this, &VuoEditorComposition::refactorRequested);
134  connect(contextMenuPublishPort, &QAction::triggered, this, &VuoEditorComposition::togglePortPublicationStatus);
135  connect(contextMenuDeleteCables, &QAction::triggered, this, &VuoEditorComposition::deleteConnectedCables);
136  connect(contextMenuHideCables, &QAction::triggered, this, &VuoEditorComposition::hideConnectedCables);
137  connect(contextMenuUnhideCables, &QAction::triggered, this, &VuoEditorComposition::unhideConnectedCables);
138  connect(contextMenuFireEvent, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::fireTriggerPortEvent));
139  connect(contextMenuAddInputPort, &QAction::triggered, this, &VuoEditorComposition::addInputPort);
140  connect(contextMenuRemoveInputPort, &QAction::triggered, this, &VuoEditorComposition::removeInputPort);
141  connect(contextMenuEditSelectedComments, &QAction::triggered, this, &VuoEditorComposition::editSelectedComments);
142 
143  // Use a queued connection to open input editors in order to avoid bug where invoking a
144  // QColorDialog by context menu prevents subsequent interaction with the editor window
145  // even after the color dialog has been closed.
146  connect(contextMenuSetPortConstant, &QAction::triggered, this, &VuoEditorComposition::setPortConstant, Qt::QueuedConnection);
147 
148  {
149  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
150  // For now, this sets up the actions for a menu, rather than setting up the menu itself.
151  QSignalMapper *contextMenuThrottlingMapper = new QSignalMapper(this);
152  connect(contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::setTriggerThrottling);
153 
154  QList<QPair<QString, enum VuoPortClass::EventThrottling> > throttlingNamesAndIndices;
155  throttlingNamesAndIndices.append(QPair<QString, enum VuoPortClass::EventThrottling>(tr("Enqueue Events"), VuoPortClass::EventThrottling_Enqueue));
156  throttlingNamesAndIndices.append(QPair<QString, enum VuoPortClass::EventThrottling>(tr("Drop Events"), VuoPortClass::EventThrottling_Drop));
157 
158  for (QList<QPair<QString, enum VuoPortClass::EventThrottling> >::iterator i = throttlingNamesAndIndices.begin(); i != throttlingNamesAndIndices.end(); ++i)
159  {
160  QString name = i->first;
161  enum VuoPortClass::EventThrottling index = i->second;
162  QAction *action = new QAction(name, this);
163 
164  contextMenuThrottlingActions.append(action);
165 
166  contextMenuThrottlingMapper->setMapping(action, index);
167  connect(action, &QAction::triggered, contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
168  }
169 
170  /*
171  contextMenuThrottling = new VuoMenu(NULL);
172  contextMenuThrottling->setTitle(tr("Set Event Throttling"));
173  QSignalMapper *contextMenuThrottlingMapper = new QSignalMapper(this);
174  connect(contextMenuThrottlingMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::setEventThrottling);
175 
176  addActionToMenuAndMapper(contextMenuThrottling, contextMenuThrottlingMapper, "Enqueue Events", VuoPortClass::EventThrottling_Enqueue);
177  addActionToMenuAndMapper(contextMenuThrottling, contextMenuThrottlingMapper, "Drop Events", VuoPortClass::EventThrottling_Drop);
178  */
179  }
180 
181  {
182  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
183  // For now, this sets up the actions for a menu, rather than setting up the menu itself.
184  QSignalMapper *contextMenuTintsMapper = new QSignalMapper(this);
185  connect(contextMenuTintsMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &VuoEditorComposition::tintSelectedItems);
186 
187  QList<QPair<QString, enum VuoNode::TintColor> > tintNamesAndIndices;
188  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Yellow"), VuoNode::TintYellow));
189  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Tangerine"), VuoNode::TintTangerine));
190  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Orange"), VuoNode::TintOrange));
191  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Magenta"), VuoNode::TintMagenta));
192  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Violet"), VuoNode::TintViolet));
193  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Blue"), VuoNode::TintBlue));
194  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Cyan"), VuoNode::TintCyan));
195  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Green"), VuoNode::TintGreen));
196  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("Lime"), VuoNode::TintLime));
197  tintNamesAndIndices.append(QPair<QString, enum VuoNode::TintColor>(tr("None"), VuoNode::TintNone));
198 
199  for (QList<QPair<QString, enum VuoNode::TintColor> >::iterator i = tintNamesAndIndices.begin(); i != tintNamesAndIndices.end(); ++i)
200  {
201  QString name = i->first;
202  enum VuoNode::TintColor index = i->second;
203  QAction *action = new QAction(name, this);
204 
205  // Add a color swatch to the menu item.
206  {
207  QColor fill(0,0,0,0);
208  // For TintNone, draw a transparent icon, so that menu item's text indent is consistent with the other items.
209  if (index != VuoNode::TintNone)
210  {
211  VuoRendererColors colors(index);
212  fill = colors.nodeFill();
213  }
214 
215  QIcon *icon = VuoInputEditorIcon::renderIcon(^(QPainter &p){
216  p.setPen(Qt::NoPen);
217  p.setBrush(fill);
218  // Match distance between text baseline and ascender.
219  p.drawEllipse(3, 3, 10, 10);
220  });
221  action->setIcon(*icon);
222  delete icon;
223  }
224 
225  contextMenuTintActions.append(action);
226 
227  contextMenuTintsMapper->setMapping(action, index);
228  connect(action, &QAction::triggered, contextMenuTintsMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
229  }
230  }
231 
232  // 'Show Events' mode rendering setup
233  this->refreshComponentAlphaLevelTimer = new QTimer(this);
234  this->refreshComponentAlphaLevelTimer->setObjectName("VuoEditorComposition::refreshComponentAlphaLevelTimer");
235  refreshComponentAlphaLevelTimer->setInterval(showEventsModeUpdateInterval);
236  connect(refreshComponentAlphaLevelTimer, &QTimer::timeout, this, &VuoEditorComposition::updateGeometryForAllComponents);
237  setShowEventsMode(false);
238 
239  connect(signaler, &VuoRendererSignaler::nodePopoverRequested, this, &VuoEditorComposition::enablePopoverForNode);
240  connect(signaler, &VuoRendererSignaler::nodesMoved, this, &VuoEditorComposition::moveNodesBy);
241  connect(signaler, &VuoRendererSignaler::commentsMoved, this, &VuoEditorComposition::moveCommentsBy);
242  connect(signaler, &VuoRendererSignaler::commentResized, this, &VuoEditorComposition::resizeCommentBy);
249  connect(signaler, &VuoRendererSignaler::dragStickinessDisableRequested, this, &VuoEditorComposition::setDisableDragStickiness);
250  connect(signaler, &VuoRendererSignaler::openUrl, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
251 
252  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::activeApplicationStateChanged, this, &VuoEditorComposition::updatePopoversForApplicationStateChange, Qt::QueuedConnection);
253  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::focusChanged, this, &VuoEditorComposition::updatePopoversForActiveWindowChange, Qt::QueuedConnection);
254  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationWillHide, this, [=]{
255  setPopoversHideOnDeactivate(true);
256  });
257  connect(static_cast<VuoEditor *>(qApp), &VuoEditor::applicationDidUnhide, this, [=]{
258  setPopoversHideOnDeactivate(false);
259  });
260 
261  populateNodeAndPortIdentifierMappings();
262 }
263 
268 {
269  this->compiler = compiler;
270 }
271 
276 {
277  this->moduleManager = moduleManager;
278  moduleManager->setComposition(this);
279 }
280 
285 {
286  return moduleManager;
287 }
288 
295 {
296  this->inputEditorManager = inputEditorManager;
297 }
298 
305 {
306  return this->inputEditorManager;
307 }
308 
312 VuoRendererNode * VuoEditorComposition::createNode(QString nodeClassName, string title, double x, double y)
313 {
314  if (compiler)
315  {
316  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
317  if (nodeClass)
318  {
319  VuoNode *node = createBaseNode(nodeClass, nullptr, title, x, y);
321  setCustomConstantsForNewNode(rn);
322  return rn;
323  }
324  }
325  return NULL;
326 }
327 
334 VuoNode * VuoEditorComposition::createBaseNode(VuoCompilerNodeClass *nodeClass, VuoNode *modelNode, string title, double x, double y)
335 {
336  // If adding the node would create recursion (subcomposition contains itself), create a node without a compiler detail.
337  __block bool isAllowed = true;
338  if (nodeClass->isSubcomposition())
339  {
340  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
341  string compositionModuleKey = VuoCompiler::getModuleKeyForPath(compositionPath);
342  bool nodeIsThisComposition = (compositionModuleKey == nodeClass->getBase()->getClassName());
343 
344  set<string> dependencies = nodeClass->getDependencies();
345  auto iter = std::find_if(dependencies.begin(), dependencies.end(), [=](const string &d){ return d == compositionModuleKey; });
346  bool nodeContainsThisComposition = (iter != dependencies.end());
347 
348  isAllowed = ! (nodeIsThisComposition || nodeContainsThisComposition);
349  });
350  }
351 
352  VuoNode *node;
353  if (isAllowed)
354  {
355  node = (modelNode ?
356  nodeClass->newNode(modelNode) :
357  compiler->createNode(nodeClass, title, x, y));
358  }
359  else
360  {
361  node = createNodeWithMissingImplementation(nodeClass->getBase(), modelNode, title, x, y);
362  node->setForbidden(true);
363  }
364  return node;
365 }
366 
373 VuoNode * VuoEditorComposition::createNodeWithMissingImplementation(VuoNodeClass *modelNodeClass, VuoNode *modelNode, string title, double x, double y)
374 {
375  vector<string> inputPortClassNames;
376  vector<string> outputPortClassNames;
377  foreach (VuoPortClass *portClass, modelNodeClass->getInputPortClasses())
378  {
379  if (portClass == modelNodeClass->getRefreshPortClass())
380  continue;
381  inputPortClassNames.push_back(portClass->getName());
382  }
383  foreach (VuoPortClass *portClass, modelNodeClass->getOutputPortClasses())
384  outputPortClassNames.push_back(portClass->getName());
385 
386  VuoNodeClass *dummyNodeClass = new VuoNodeClass(modelNodeClass->getClassName(), inputPortClassNames, outputPortClassNames);
387  return (modelNode ?
388  dummyNodeClass->newNode(modelNode) :
389  dummyNodeClass->newNode(! title.empty() ? title : modelNodeClass->getDefaultTitle(), x, y));
390 }
391 
397 void VuoEditorComposition::setCustomConstantsForNewNode(VuoRendererNode *newNode)
398 {
399  // vuo.time.make: Set the 'year' input to the current year.
400  if (newNode->getBase()->getNodeClass()->getClassName() == "vuo.time.make")
401  {
402  VuoPort *yearPort = newNode->getBase()->getInputPortWithName("year");
403  if (yearPort)
404  {
405  QString currentYear = QString::number(QDateTime::currentDateTime().date().year());
406  yearPort->getRenderer()->setConstant(VuoText_getString(currentYear.toUtf8().constData()));
407  }
408  }
409 }
410 
414 void VuoEditorComposition::addNode(VuoNode *n, bool nodeShouldBeRendered, bool nodeShouldBeGivenUniqueIdentifier)
415 {
416  VuoRendererComposition::addNode(n, nodeShouldBeRendered, nodeShouldBeGivenUniqueIdentifier);
417  registerNodeID(n);
418 }
419 
426 {
427  if (resetState)
428  {
429  disablePortPopovers(rn);
430 
431  if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == rn))
433  }
434 
436 }
437 
449 {
450  // Inventory the port constants and connected input cables associated with the old node, to be re-associated with the new node.
451  map<VuoCable *, VuoPort *> cablesToTransferFromPort;
452  map<VuoCable *, VuoPort *> cablesToTransferToPort;
453  set<VuoCable *> cablesToRemove;
454  getBase()->getCompiler()->getChangesToReplaceNode(oldNode->getBase(), newNode, cablesToTransferFromPort, cablesToTransferToPort, cablesToRemove);
455 
456  // Also inventory any typecasts attached to the old node, to be attached to the new node instead.
457  vector<VuoRendererNode *> collapsedTypecasts;
458  vector<VuoPort *> oldInputPorts = oldNode->getBase()->getInputPorts();
459  for (vector<VuoPort *>::iterator inputPort = oldInputPorts.begin(); inputPort != oldInputPorts.end(); ++inputPort)
460  {
461  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
462  if (typecastPort)
463  {
464  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
465  collapsedTypecasts.push_back(typecastNode);
466  }
467 
468  // If the original node is currently being rendered as collapsed typecast, uncollapse it.
469  if (oldNode->getProxyCollapsedTypecast())
470  uncollapseTypecastNode(oldNode);
471 
472  // Uncollapse typecasts attached to the original node.
473  for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
475  }
476 
477  // Perform the node replacement.
478  replaceNode(oldNode, newNode);
479 
480  // Restore connected cables.
481  for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferFromPort.begin(); i != cablesToTransferFromPort.end(); ++i)
482  i->first->getRenderer()->setFrom(newNode, i->second);
483  for (map<VuoCable *, VuoPort *>::iterator i = cablesToTransferToPort.begin(); i != cablesToTransferToPort.end(); ++i)
484  i->first->getRenderer()->setTo(newNode, i->second);
485  foreach (VuoCable *cable, cablesToRemove)
486  removeCable(cable->getRenderer());
487 
488  // Restore constant values.
489  for (VuoPort *oldInputPort : oldNode->getBase()->getInputPorts())
490  {
491  VuoPort *newInputPort = newNode->getInputPortWithName(oldInputPort->getClass()->getName());
492  if (! newInputPort)
493  continue;
494 
495  if (! oldInputPort->getRenderer()->carriesData() || ! newInputPort->getRenderer()->carriesData())
496  continue;
497 
498  if (oldNode->getBase()->hasCompiler() && newNode->hasCompiler())
499  {
500  VuoType *oldDataType = static_cast<VuoCompilerPort *>(oldInputPort->getCompiler())->getDataVuoType();
501  VuoType *newDataType = static_cast<VuoCompilerPort *>(newInputPort->getCompiler())->getDataVuoType();
502  if (! (oldDataType == newDataType && oldDataType && ! dynamic_cast<VuoGenericType *>(oldDataType)) )
503  continue;
504  }
505 
506  string oldConstantValue;
507  if (oldNode->getBase()->hasCompiler())
508  oldConstantValue = oldInputPort->getRenderer()->getConstantAsString();
509  else
510  oldConstantValue = oldInputPort->getRawInitialValue();
511 
512  if (newNode->hasCompiler())
513  updatePortConstant(static_cast<VuoCompilerPort *>(newInputPort->getCompiler()), oldConstantValue, false);
514  else
515  newInputPort->setRawInitialValue(oldConstantValue);
516  }
517 
518  // Restore connected typecasts.
519  for (vector<VuoRendererNode *>::iterator i = collapsedTypecasts.begin(); i != collapsedTypecasts.end(); ++i)
521 
522  // Re-collapse the updated node, if applicable.
523  collapseTypecastNode(newNode->getRenderer());
524 }
525 
531 {
532  disablePortPopovers(oldNode);
533 
534  if (getTriggerPortToRefire() && (getTriggerPortToRefire()->getRenderer()->getUnderlyingParentNode() == oldNode))
536 
538  if (newNode->hasCompiler())
539  {
540  string graphvizIdentifier = (oldNode->getBase()->hasCompiler() ?
541  oldNode->getBase()->getCompiler()->getGraphvizIdentifier() :
542  oldNode->getBase()->getRawGraphvizIdentifier());
543  newNode->getCompiler()->setGraphvizIdentifier(graphvizIdentifier);
544  }
545 
546  removeNode(oldNode);
547  addNode(newNode, true, false);
548 
549  registerNodeID(newNode);
550 }
551 
556 {
557  bool cableHidden = rc->getBase()->getCompiler()->getHidden();
559 
560  if (cableHidden)
561  emit changeInHiddenCables();
562 }
563 
568 {
569  bool cableHidden = cable->getCompiler()->getHidden();
571 
572  if (cableHidden)
573  emit changeInHiddenCables();
574 }
575 
585 {
586  return VuoRendererComposition::createAndConnectMakeListNode(toNode, toPort, compiler, rendererCable);
587 }
588 
598  set<VuoRendererNode *> &createdNodes,
599  set<VuoRendererCable *> &createdCables)
600 {
601  return VuoRendererComposition::createAndConnectDictionaryAttachmentsForNode(node, compiler, createdNodes, createdCables);
602 }
603 
610 QList<QGraphicsItem *> VuoEditorComposition::createAndConnectInputAttachments(VuoRendererNode *node, bool createButDoNotAdd)
611 {
612  QList<QGraphicsItem *> addedComponents = VuoRendererComposition::createAndConnectInputAttachments(node, compiler, createButDoNotAdd);
613  foreach (QGraphicsItem *component, addedComponents)
614  {
615  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(component);
616  if (rn && !createButDoNotAdd)
617  registerNodeID(rn->getBase());
618  }
619 
620  return addedComponents;
621 }
622 
627 void VuoEditorComposition::modifyComponents(void (^modify)(void))
628 {
629  // Record the IDs of the currently selected components so that the selection status
630  // of the corresponding items may be restored after the composition is reset.
631  set<string> selectedNodeIDs;
632  foreach (VuoRendererNode *rn, getSelectedNodes())
633  {
634  if (rn->getBase()->hasCompiler())
635  selectedNodeIDs.insert(rn->getBase()->getCompiler()->getGraphvizIdentifier());
636  }
637 
638  set<string> selectedCommentIDs;
639  foreach (VuoRendererComment *rc, getSelectedComments())
640  {
641  if (rc->getBase()->hasCompiler())
642  selectedCommentIDs.insert(rc->getBase()->getCompiler()->getGraphvizIdentifier());
643  }
644 
645  set<string> selectedCableIDs;
646  foreach (VuoRendererCable *rc, getSelectedCables(true))
647  {
648  if (rc->getBase()->hasCompiler())
649  selectedCableIDs.insert(rc->getBase()->getCompiler()->getGraphvizDeclaration());
650  }
651 
652  modify();
653 
654  // Restore the selection status of pre-existing components.
655  foreach (QGraphicsItem *item, items())
656  {
657  if (dynamic_cast<VuoRendererNode *>(item))
658  {
659  string currentNodeID = (dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler()?
660  dynamic_cast<VuoRendererNode *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
661  "");
662  if (!currentNodeID.empty() && (selectedNodeIDs.find(currentNodeID) != selectedNodeIDs.end()))
663  item->setSelected(true);
664  }
665 
666  if (dynamic_cast<VuoRendererComment *>(item))
667  {
668  string currentCommentID = (dynamic_cast<VuoRendererComment *>(item)->getBase()->hasCompiler()?
669  dynamic_cast<VuoRendererComment *>(item)->getBase()->getCompiler()->getGraphvizIdentifier() :
670  "");
671  if (!currentCommentID.empty() && (selectedCommentIDs.find(currentCommentID) != selectedCommentIDs.end()))
672  item->setSelected(true);
673  }
674 
675  else if (dynamic_cast<VuoRendererCable *>(item))
676  {
677  string currentCableID = (dynamic_cast<VuoRendererCable *>(item)->getBase()->hasCompiler()?
678  dynamic_cast<VuoRendererCable *>(item)->getBase()->getCompiler()->getGraphvizDeclaration() :
679  "");
680  if (!currentCableID.empty() && (selectedCableIDs.find(currentCableID) != selectedCableIDs.end()))
681  item->setSelected(true);
682  }
683  }
684 
685  // Re-establish mappings between the stored composition components and the running
686  // composition components, if applicable.
688 
689  // Close popovers for ports no longer present in the composition.
691 }
692 
698 {
699  string portName = port->getBase()->getClass()->getName();
700  VuoRendererNode *parentNode = port->getRenderedParentNode();
701 
702  // A changed math expression input to a "Calculate" node will require changes to the node's
703  // upstream input lists of variable names and values.
704  if ((portName == "expression") &&
705  VuoStringUtilities::beginsWith(parentNode->getBase()->getNodeClass()->getClassName(), "vuo.math.calculate"))
706  return true;
707 
708  return false;
709 }
710 
715 {
717 }
718 
723 {
724  if (commandDescription.empty())
725  {
726  if (getContextMenuDeleteSelectedAction()->text().contains("Reset"))
727  commandDescription = "Reset";
728  else
729  commandDescription = "Delete";
730  }
731 
732  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
733  emit componentsRemoved(selectedCompositionComponents, commandDescription);
734 }
735 
739 void VuoEditorComposition::deleteSelectedNodes(string commandDescription)
740 {
741  if (commandDescription.empty())
742  commandDescription = "Delete";
743 
744  QList<QGraphicsItem *> selectedNodes;
745  foreach (VuoRendererNode *node, getSelectedNodes())
746  selectedNodes.append(node);
747 
748  emit componentsRemoved(selectedNodes, commandDescription);
749 }
750 
755 {
757 
758  foreach (VuoCable *cable, getBase()->getCables())
759  removeCable(cable->getRenderer());
760 
761  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
763 
764  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedOutputPorts())
765  removePublishedPort(publishedPort, false);
766 
767  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
768  removePublishedPort(publishedPort, true);
769 
770  foreach (VuoNode *node, getBase()->getNodes())
771  removeNode(node->getRenderer(), false);
772 
774 
775  foreach (VuoComment *comment, getBase()->getComments())
776  removeComment(comment->getRenderer());
777 
779 
780  nodeWithGraphvizIdentifier.clear();
781  portWithStaticIdentifier.clear();
782  staticIdentifierForPort.clear();
783 }
784 
788 void VuoEditorComposition::insertNode()
789 {
790  QAction *sender = (QAction *)QObject::sender();
791  QPair<QPointF, QString> pair = sender->data().value<QPair<QPointF, QString> >();
792 
793  QList<QGraphicsItem *> newNodes;
794  VuoRendererNode *newNode = createNode(pair.second, "",
795  pair.first.x(),
796  pair.first.y());
797 
798  if (newNode)
799  {
800  newNodes.append(newNode);
801  emit componentsAdded(newNodes, this);
802  }
803 }
804 
808 void VuoEditorComposition::insertComment()
809 {
810  QAction *sender = (QAction *)QObject::sender();
811  QPointF scenePos = sender->data().value<QPointF>();
812 
813  emit commentInsertionRequested(scenePos);
814 }
815 
819 void VuoEditorComposition::insertSubcomposition()
820 {
821  QAction *sender = (QAction *)QObject::sender();
822  QPointF scenePos = sender->data().value<QPointF>();
823 
824  emit subcompositionInsertionRequested(scenePos);
825 }
826 
832 {
833  QAction *sender = (QAction *)QObject::sender();
834  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
835 
836  if (isPortPublished(port))
837  emit portUnpublicationRequested(port->getBase());
838  else
839  emit portPublicationRequested(port->getBase(), false);
840 }
841 
848 void VuoEditorComposition::deleteConnectedCables()
849 {
850  QAction *sender = (QAction *)QObject::sender();
851  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
852  vector<VuoCable *> connectedCables = port->getBase()->getConnectedCables(true);
853  QList<QGraphicsItem *> cablesToRemove;
854 
855  // Delete visible directly connected cables.
856  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
857  {
858  if (!cable->getRenderer()->paintingDisabled())
859  cablesToRemove.append(cable->getRenderer());
860  }
861 
862  // Delete visible cables connected to the typecast's child port.
863  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
864  if (typecastPort)
865  {
866  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
867  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
868  foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
869  {
870  if (!cable->getRenderer()->paintingDisabled())
871  cablesToRemove.append(cable->getRenderer());
872  }
873  }
874 
875  emit componentsRemoved(QList<QGraphicsItem *>(cablesToRemove));
876 }
877 
884 void VuoEditorComposition::hideConnectedCables()
885 {
886  QAction *sender = (QAction *)QObject::sender();
887  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
888  set<VuoRendererCable *> cablesToHide;
889 
890  // Hide visible directly connected cables.
891  foreach (VuoCable *cable, port->getBase()->getConnectedCables(false))
892  {
893  if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
894  cablesToHide.insert(cable->getRenderer());
895  }
896 
897  // Hide visible cables connected to the typecast's child port.
898  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
899  if (typecastPort)
900  {
901  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
902  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
903  foreach (VuoCable *cable, typecastInPort->getConnectedCables(false))
904  {
905  if (!cable->getRenderer()->paintingDisabled() && !cable->getCompiler()->getHidden())
906  cablesToHide.insert(cable->getRenderer());
907  }
908  }
909 
910  emit cablesHidden(cablesToHide);
911 }
912 
919 void VuoEditorComposition::unhideConnectedCables()
920 {
921  QAction *sender = (QAction *)QObject::sender();
922  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
923  set<VuoRendererCable *> cablesToUnhide;
924 
925  // Unhide visible directly connected cables.
926  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
927  {
928  if (cable->getRenderer()->getEffectivelyWireless())
929  cablesToUnhide.insert(cable->getRenderer());
930  }
931 
932  // Unhide visible cables connected to the typecast's child port.
933  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
934  if (typecastPort)
935  {
936  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
937  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
938  foreach (VuoCable *cable, typecastInPort->getConnectedCables(true))
939  {
940  if (cable->getRenderer()->getEffectivelyWireless())
941  cablesToUnhide.insert(cable->getRenderer());
942  }
943  }
944 
945  emit cablesUnhidden(cablesToUnhide);
946 }
947 
952 void VuoEditorComposition::fireTriggerPortEvent()
953 {
954  QAction *sender = (QAction *)QObject::sender();
955  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
956  fireTriggerPortEvent(port->getBase());
957 }
958 
963 {
964  fireTriggerPortEvent(getTriggerPortToRefire());
965 }
966 
971 {
972  if (triggerPortToRefire.empty() ||
973  (portWithStaticIdentifier.find(triggerPortToRefire) == portWithStaticIdentifier.end()))
974  return NULL;
975 
976  return portWithStaticIdentifier[triggerPortToRefire];
977 }
978 
983 {
984  string portID = getIdentifierForStaticPort(port);
985  if (portID != this->triggerPortToRefire)
986  {
987  this->triggerPortToRefire = portID;
988  emit refirePortChanged();
989  }
990 }
991 
996 void VuoEditorComposition::setPortConstant()
997 {
998  QAction *sender = (QAction *)QObject::sender();
999  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1000 
1001  if (port->isConstant())
1002  emit inputEditorRequested(port);
1003 }
1004 
1009 void VuoEditorComposition::setPortConstantToValue(VuoRendererPort *port, string value)
1010 {
1011  if (port->isConstant())
1012  emit portConstantChangeRequested(port, value);
1013 }
1014 
1019 void VuoEditorComposition::specializeGenericPortType()
1020 {
1021  QAction *sender = (QAction *)QObject::sender();
1022  QList<QVariant> portAndSpecializedType= sender->data().toList();
1023  VuoRendererPort *port = (VuoRendererPort *)portAndSpecializedType[0].value<void *>();
1024  QString specializedTypeName = portAndSpecializedType[1].toString();
1025 
1026  // If the port is already specialized to the target type, do nothing.
1027  if (port && (port->getDataType()->getModuleKey() == specializedTypeName.toUtf8().constData()))
1028  return;
1029 
1030  // If the port is already specialized to a different type, re-specialize it.
1031  if (port && !dynamic_cast<VuoGenericType *>(port->getDataType()))
1032  emit respecializePort(port, specializedTypeName.toUtf8().constData());
1033 
1034  // Otherwise, specialize the port from generic.
1035  else
1036  emit specializePort(port, specializedTypeName.toUtf8().constData());
1037 }
1038 
1043 void VuoEditorComposition::unspecializePortType()
1044 {
1045  QAction *sender = (QAction *)QObject::sender();
1046  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1047 
1048  // If the port is already generic, do nothing.
1049  if (port && dynamic_cast<VuoGenericType *>(port->getDataType()))
1050  return;
1051 
1052  emit unspecializePort(port);
1053 }
1054 
1064 void VuoEditorComposition::createReplacementsToUnspecializePort(VuoPort *portToUnspecialize, map<VuoNode *, string> &nodesToReplace, set<VuoCable *> &cablesToDelete)
1065 {
1066  // Be able to look up the node and cables for a port.
1067  map<VuoPort *, VuoNode *> nodeForPort;
1068  map<VuoPort *, set<VuoCable *> > cablesForPort;
1069  set<VuoNode *> nodes = getBase()->getNodes();
1070  for (set<VuoNode *>::iterator i = nodes.begin(); i != nodes.end(); ++i)
1071  {
1072  VuoNode *node = *i;
1073  vector<VuoPort *> inputPorts = node->getInputPorts();
1074  vector<VuoPort *> outputPorts = node->getOutputPorts();
1075  vector<VuoPort *> ports;
1076  ports.insert(ports.end(), inputPorts.begin(), inputPorts.end());
1077  ports.insert(ports.end(), outputPorts.begin(), outputPorts.end());
1078 
1079  for (vector<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1080  {
1081  VuoPort *port = *j;
1082 
1083  nodeForPort[port] = node;
1084  vector<VuoCable *> cables = port->getConnectedCables(true);
1085  cablesForPort[port].insert(cables.begin(), cables.end());
1086  }
1087  }
1088 
1089  // Find the ports that will share the same generic type as portToUnspecialize, and organize them by node.
1090  set<VuoPort *> connectedPotentiallyGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(portToUnspecialize->getRenderer()->getUnderlyingParentNode()->getBase(),
1091  portToUnspecialize, true);
1092  map<VuoNode *, set<VuoPort *> > portsToUnspecializeForNode;
1093  for (VuoPort *connectedPort : connectedPotentiallyGenericPorts)
1094  {
1095  VuoNode *node = nodeForPort[connectedPort];
1096 
1097  // @todo: Don't just exclude ports that aren't currently revertible, also exclude ports that are only
1098  // within the current network by way of ports that aren't currently revertible.
1099  if (isPortCurrentlyRevertible(connectedPort->getRenderer()))
1100  portsToUnspecializeForNode[node].insert(connectedPort);
1101  }
1102 
1103  for (map<VuoNode *, set<VuoPort *> >::iterator i = portsToUnspecializeForNode.begin(); i != portsToUnspecializeForNode.end(); ++i)
1104  {
1105  VuoNode *node = i->first;
1106  set<VuoPort *> ports = i->second;
1107 
1108  // Create the unspecialized node class name for each node to unspecialize.
1109  set<VuoPortClass *> portClasses;
1110  for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1111  portClasses.insert((*j)->getClass());
1112  VuoCompilerSpecializedNodeClass *nodeClass = static_cast<VuoCompilerSpecializedNodeClass *>(node->getNodeClass()->getCompiler());
1113  string unspecializedNodeClassName = nodeClass->createUnspecializedNodeClassName(portClasses);
1114  nodesToReplace[node] = unspecializedNodeClassName;
1115 
1116  // Identify the cables that will become invalid (data-carrying cable with generic port at one end, non-generic port at the other end)
1117  // when the node is unspecialized.
1118  for (set<VuoPort *>::iterator j = ports.begin(); j != ports.end(); ++j)
1119  {
1120  VuoPort *port = *j;
1121  set<VuoCable *> connectedCables = cablesForPort[port];
1122  for (set<VuoCable *>::iterator k = connectedCables.begin(); k != connectedCables.end(); ++k)
1123  {
1124  VuoCable *cable = *k;
1125 
1126  bool areEndsCompatible = false;
1127 
1128  if (!cable->getRenderer()->effectivelyCarriesData())
1129  areEndsCompatible = true;
1130 
1131  else if (! cable->isPublished())
1132  {
1133  VuoPort *portOnOtherEnd = (cable->getFromPort() == port ? cable->getToPort() : cable->getFromPort());
1134  if (portOnOtherEnd && isPortCurrentlyRevertible(portOnOtherEnd->getRenderer()))
1135  {
1136  VuoNode *nodeOnOtherEnd = nodeForPort[portOnOtherEnd];
1137  VuoCompilerNodeClass *nodeClassOnOtherEnd = nodeOnOtherEnd->getNodeClass()->getCompiler();
1138  VuoCompilerSpecializedNodeClass *specializedNodeClassOnOtherEnd = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClassOnOtherEnd);
1139  if (specializedNodeClassOnOtherEnd)
1140  {
1141  VuoType *typeOnOtherEnd = specializedNodeClassOnOtherEnd->getOriginalPortType( portOnOtherEnd->getClass() );
1142  if (! typeOnOtherEnd || dynamic_cast<VuoGenericType *>(typeOnOtherEnd))
1143  areEndsCompatible = true;
1144  }
1145  }
1146  }
1147 
1148  if (! areEndsCompatible && (cable != cableInProgress))
1149  cablesToDelete.insert(cable);
1150  }
1151  }
1152  }
1153 }
1154 
1158 void VuoEditorComposition::fireTriggerPortEvent(VuoPort *port)
1159 {
1160  if (! (port && port->hasCompiler()) )
1161  return;
1162 
1163  VuoPort *oldManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1164  string oldSnapshot = takeSnapshot();
1165 
1166  string runningTriggerPortIdentifier = "";
1167  bool isTriggerPort = false;
1168  if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()))
1169  {
1170  // Trigger port — The event will be fired from the port.
1171 
1172  getBase()->getCompiler()->setManuallyFirableInputPort(nullptr, nullptr);
1173 
1174  runningTriggerPortIdentifier = staticIdentifierForPort[port];
1175  isTriggerPort = true;
1176  }
1177  else if (port->hasRenderer() && port->getRenderer()->getInput())
1178  {
1179  // Input port — The event will be fired from the composition's manually firable trigger into the port.
1180 
1182 
1184  VuoCompilerTriggerPort *triggerPort = graph->getManuallyFirableTrigger();
1185  VuoCompilerNode *triggerNode = graph->getNodeForTriggerPort(triggerPort);
1186  runningTriggerPortIdentifier = getIdentifierForStaticPort(triggerPort->getBase(), triggerNode->getBase());
1187  }
1188  else
1189  return;
1190 
1191  VUserLog("%s: Fire %s",
1192  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
1193  runningTriggerPortIdentifier.c_str());
1194 
1195  VuoPort *newManuallyFirableInputPort = getBase()->getCompiler()->getManuallyFirableInputPort();
1196  bool manuallyFirableInputPortChanged = (oldManuallyFirableInputPort != newManuallyFirableInputPort);
1197  string newSnapshot = takeSnapshot();
1198 
1199  auto fireIfRunning = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
1200  {
1201  dispatch_async(topLevelComposition->runCompositionQueue, ^{
1202  if (topLevelComposition->isRunningThreadUnsafe())
1203  {
1204  topLevelComposition->runner->fireTriggerPortEvent(thisCompositionIdentifier, runningTriggerPortIdentifier);
1205 
1206  // Display the trigger port animation when the user manually fires an event
1207  // even if not in "Show Events" mode. (If in "Show Events" mode, this will
1208  // be handled for trigger ports by VuoEditorComposition::receivedTelemetryOutputPortUpdated(...).)
1209  if (! (this->showEventsMode && isTriggerPort) )
1210  this->animatePort(port->getRenderer());
1211  }
1212  });
1213  };
1214 
1215  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1216  if (this == topLevelComposition || ! manuallyFirableInputPortChanged)
1217  {
1218  // Top-level composition or unmodified subcomposition — Fire the trigger immediately.
1219 
1220  if (! newSnapshot.empty() && manuallyFirableInputPortChanged)
1221  updateRunningComposition(oldSnapshot, newSnapshot);
1222 
1223  fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1224  }
1225  else
1226  {
1227  // Modified subcomposition — Fire the trigger after the subcomposition has been reloaded.
1228 
1229  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^void (VuoEditorComposition *currComposition, string compositionPath) {
1230  string nodeClassName = VuoCompiler::getModuleKeyForPath(compositionPath);
1231  moduleManager->doNextTimeNodeClassIsLoaded(nodeClassName, ^{
1232  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier) {
1233  fireIfRunning(topLevelComposition, thisCompositionIdentifier);
1234  });
1235  });
1236 
1237  if (! newSnapshot.empty())
1238  updateRunningComposition(oldSnapshot, newSnapshot);
1239  });
1240  }
1241  });
1242 
1243  setTriggerPortToRefire(port);
1244 }
1245 
1249 void VuoEditorComposition::setTriggerThrottling(int eventThrottling)
1250 {
1251  QSignalMapper *signalMapper = (QSignalMapper *)QObject::sender();
1252  QAction *sender = (QAction *)signalMapper->mapping(eventThrottling);
1253  VuoRendererPort *port = (VuoRendererPort *)(sender->data().value<void *>());
1254  emit triggerThrottlingUpdated(port->getBase(), (enum VuoPortClass::EventThrottling)eventThrottling);
1255 }
1256 
1261 void VuoEditorComposition::addInputPort()
1262 {
1263  QAction *sender = (QAction *)QObject::sender();
1264  VuoRendererNode *node = (VuoRendererNode *)(sender->data().value<void *>());
1265  emit inputPortCountAdjustmentRequested(node, 1, false);
1266 }
1267 
1272 void VuoEditorComposition::removeInputPort()
1273 {
1274  QAction *sender = (QAction *)QObject::sender();
1275  VuoRendererNode *node = (VuoRendererNode *)(sender->data().value<void *>());
1276  emit inputPortCountAdjustmentRequested(node, -1, false);
1277 }
1278 
1283 void VuoEditorComposition::swapNode()
1284 {
1285  QAction *sender = (QAction *)QObject::sender();
1286  QList<QVariant> nodeAndReplacementType= sender->data().toList();
1287  VuoRendererNode *node = static_cast<VuoRendererNode *>(nodeAndReplacementType[0].value<void *>());
1288  QString newNodeClassName = nodeAndReplacementType[1].toString();
1289  emit nodeSwapRequested(node, newNodeClassName.toUtf8().constData());
1290 }
1291 
1292 
1297 {
1299 }
1300 
1305 {
1306  // Open a title editor for each selected non-attachment node.
1307  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1308  for (set<VuoRendererNode *>::iterator i = selectedNodes.begin(); i != selectedNodes.end(); ++i)
1309  {
1310  if (!dynamic_cast<VuoRendererInputAttachment *>(*i))
1311  emit nodeTitleEditorRequested(*i);
1312  }
1313 }
1314 
1318 void VuoEditorComposition::editSelectedComments()
1319 {
1320  // Open a text editor for each selected comment.
1321  set<VuoRendererComment *> selectedComments = getSelectedComments();
1322  for (set<VuoRendererComment *>::iterator i = selectedComments.begin(); i != selectedComments.end(); ++i)
1323  emit commentEditorRequested(*i);
1324 }
1325 
1329 set<VuoRendererCable *> VuoEditorComposition::getCablesInternalToSubcomposition(QList<QGraphicsItem *> subcompositionComponents)
1330 {
1331  set<VuoRendererCable *> internalCables;
1332 
1333  for (QList<QGraphicsItem *>::iterator i = subcompositionComponents.begin(); i != subcompositionComponents.end(); ++i)
1334  {
1335  QGraphicsItem *compositionComponent = *i;
1336  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1337  if (rn)
1338  {
1339  set<VuoCable *> connectedCables = rn->getConnectedCables(false);
1340  for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1341  {
1342  VuoNode *fromNode = (*cable)->getFromNode();
1343  VuoNode *toNode = (*cable)->getToNode();
1344 
1345  if (fromNode && toNode && subcompositionComponents.contains(fromNode->getRenderer()) && subcompositionComponents.contains(toNode->getRenderer()))
1346  internalCables.insert((*cable)->getRenderer());
1347  }
1348  }
1349  }
1350 
1351  return internalCables;
1352 }
1353 
1358 {
1359  return cableInProgress;
1360 }
1361 
1367 {
1368  return cableInProgressWasNew;
1369 }
1370 
1375 {
1376  return menuSelectionInProgress;
1377 }
1378 
1383 {
1384  QList<QGraphicsItem *> compositionComponents = items();
1385  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1386  {
1387  QGraphicsItem *compositionComponent = *i;
1388 
1389  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1390  if (!rc || !rc->paintingDisabled())
1391  compositionComponent->setSelected(true);
1392  }
1393 }
1394 
1399 {
1400  QList<QGraphicsItem *> compositionComponents = items();
1401  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1402  {
1403  QGraphicsItem *compositionComponent = *i;
1404  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
1405  if (rcomment)
1406  rcomment->setSelected(true);
1407  }
1408 }
1409 
1414 {
1415  QList<QGraphicsItem *> compositionComponents = items();
1416  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
1417  {
1418  QGraphicsItem *compositionComponent = *i;
1419  compositionComponent->setSelected(false);
1420  }
1421 }
1422 
1426 void VuoEditorComposition::openSelectedEditableNodes()
1427 {
1428  foreach (VuoRendererNode *node, getSelectedNodes())
1429  {
1430  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
1431  QString actionText, sourcePath;
1432  if (VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
1433  emit nodeSourceEditorRequested(node);
1434  }
1435 }
1436 
1441 {
1442  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
1443  set<VuoRendererComment *> selectedComments = getSelectedComments();
1444  moveItemsBy(selectedNodes, selectedComments, dx, dy, false);
1445 }
1446 
1450 void VuoEditorComposition::moveNodesBy(set<VuoRendererNode *> nodes, qreal dx, qreal dy, bool movedByDragging)
1451 {
1452  set<VuoRendererComment *> comments;
1453  moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1454 }
1455 
1459 void VuoEditorComposition::moveCommentsBy(set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1460 {
1461  set<VuoRendererNode *> nodes;
1462  moveItemsBy(nodes, comments, dx, dy, movedByDragging);
1463 }
1464 
1468 void VuoEditorComposition::moveItemsBy(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1469 {
1470  emit itemsMoved(nodes, comments, dx, dy, movedByDragging);
1472  for (set<VuoRendererNode *>::iterator it = nodes.begin(); it != nodes.end(); ++it)
1473  (*it)->updateConnectedCableGeometry();
1474 }
1475 
1479 void VuoEditorComposition::resizeCommentBy(VuoRendererComment *comment, qreal dx, qreal dy)
1480 {
1481  emit commentResized(comment, dx, dy);
1482 }
1483 
1488 {
1489  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1490  set<VuoRendererNode *> selectedNodes;
1491  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1492  {
1493  QGraphicsItem *compositionComponent = *i;
1494  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
1495  if (rn)
1496  selectedNodes.insert(rn);
1497  }
1498 
1499  return selectedNodes;
1500 }
1501 
1506 {
1507  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1508  set<VuoRendererComment *> selectedComments;
1509  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1510  {
1511  QGraphicsItem *compositionComponent = *i;
1512  VuoRendererComment *rc = dynamic_cast<VuoRendererComment *>(compositionComponent);
1513  if (rc)
1514  selectedComments.insert(rc);
1515  }
1516 
1517  return selectedComments;
1518 }
1519 
1523 set<VuoRendererCable *> VuoEditorComposition::getSelectedCables(bool includePublishedCables)
1524 {
1525  QList<QGraphicsItem *> selectedCompositionComponents = selectedItems();
1526  set<VuoRendererCable *> selectedCables;
1527  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1528  {
1529  QGraphicsItem *compositionComponent = *i;
1530  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
1531  if (rc && (includePublishedCables || !rc->getBase()->isPublished()))
1532  selectedCables.insert(rc);
1533  }
1534 
1535  return selectedCables;
1536 }
1537 
1541 void VuoEditorComposition::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1542 {
1543  const QMimeData *mimeData = event->mimeData();
1544  bool disablePortHoverHighlighting = true;
1545 
1546  // Accept drags of files.
1547  if (mimeData->hasFormat("text/uri-list"))
1548  {
1549  QList<QUrl> urls = mimeData->urls();
1550 
1551  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1552  VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1553  if (portAtDropLocation)
1554  {
1555  if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1556  (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1557  disablePortHoverHighlighting = false;
1558  else
1559  event->setDropAction(Qt::IgnoreAction);
1560  }
1561  else // if (!portAtDropLocation)
1562  {
1563  bool dragIncludesDroppableFile = false;
1564  foreach (QUrl url, urls)
1565  {
1566  bool isSupportedDragNDropFile = isSupportedImageFile(url.path().toUtf8().constData()) ||
1567  isSupportedMovieFile(url.path().toUtf8().constData()) ||
1568  isSupportedSceneFile(url.path().toUtf8().constData()) ||
1569  isSupportedAudioFile(url.path().toUtf8().constData()) ||
1570  isSupportedFeedFile(url.path().toUtf8().constData()) ||
1571  isSupportedJsonFile(url.path().toUtf8().constData()) ||
1572  isSupportedXmlFile(url.path().toUtf8().constData()) ||
1573  isSupportedTableFile(url.path().toUtf8().constData()) ||
1574  isSupportedMeshFile(url.path().toUtf8().constData()) ||
1575  isSupportedDataFile(url.path().toUtf8().constData()) ||
1576  isSupportedAppFile(url.path().toUtf8().constData()) ||
1577  isDirectory(url.path().toUtf8().constData());
1578  if (isSupportedDragNDropFile)
1579  {
1580  dragIncludesDroppableFile = true;
1581  break;
1582  }
1583  }
1584 
1585  if (!dragIncludesDroppableFile)
1586  event->setDropAction(Qt::IgnoreAction);
1587  }
1588 
1589  event->accept();
1590  }
1591 
1592  // Accept drags of single or multiple nodes from the node library.
1593  else if (mimeData->hasFormat("text/plain") || mimeData->hasFormat("text/scsv"))
1594  event->acceptProposedAction();
1595 
1596  else
1597  {
1598  event->setDropAction(Qt::IgnoreAction);
1599  event->accept();
1600  }
1601 
1602  updateHoverHighlighting(event->scenePos(), disablePortHoverHighlighting);
1603 }
1604 
1608 void VuoEditorComposition::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
1609 {
1610  event->acceptProposedAction();
1611 }
1612 
1616 void VuoEditorComposition::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
1617 {
1618  dragEnterEvent(event);
1619 }
1620 
1624 void VuoEditorComposition::dropEvent(QGraphicsSceneDragDropEvent *event)
1625 {
1626  const QMimeData *mimeData = event->mimeData();
1627 
1628  // Accept drops of certain types of files.
1629  if (mimeData->hasFormat("text/uri-list"))
1630  {
1631  // Retrieve the composition directory so that file paths may be specified relative to it.
1632  // Note: Providing the directory's canonical path as the argument to the
1633  // QDir constructor is necessary in order for QDir::relativeFilePath() to
1634  // work correctly when the non-canonical path contains symbolic links (e.g.,
1635  // '/tmp' -> '/private/tmp' for example compositions).
1636  string topCompositionPath = compiler->getCompositionLocalPath();
1637  if (topCompositionPath.empty())
1638  topCompositionPath = getBase()->getDirectory();
1639  QDir compositionDir(QDir(topCompositionPath.c_str()).canonicalPath());
1640 
1641  // Use the absolute file path if the "Option" key was pressed.
1642  bool useAbsoluteFilePaths = VuoEditorUtilities::optionKeyPressedForEvent(event);
1643 
1644  QList<QGraphicsItem *> newNodes;
1645  QList<QUrl> urls = mimeData->urls();
1646 
1647  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1648  VuoRendererPort *portAtDropLocation = dynamic_cast<VuoRendererPort *>(nearbyItem);
1649  if (portAtDropLocation)
1650  {
1651  if ((urls.size() == 1) && portAtDropLocation->isConstant() &&
1652  (portAtDropLocation->getDataType()->getModuleKey() == "VuoText"))
1653  {
1654  QString filePath = (useAbsoluteFilePaths? urls[0].path() : compositionDir.relativeFilePath(urls[0].path()));
1655  string constantValue = "\"" + string(filePath.toUtf8().constData()) + "\"";
1656  event->accept();
1657 
1658  emit portConstantChangeRequested(portAtDropLocation, constantValue);
1659  }
1660  else
1661  event->ignore();
1662  }
1663 
1664  else // if (!portAtDropLocation)
1665  {
1666  const int ySpacingForNewNodes = 11;
1667  int yOffsetForPreviousNewNode = -1 * ySpacingForNewNodes;
1668 
1669  foreach (QUrl url, urls)
1670  {
1671  QStringList targetNodeClassNames;
1672 
1673  if (isSupportedImageFile(url.path().toUtf8().constData()))
1674  targetNodeClassNames += "vuo.image.fetch";
1675  if (isSupportedMovieFile(url.path().toUtf8().constData()))
1676  {
1677  if (!getActiveProtocol())
1678  targetNodeClassNames += "vuo.video.play";
1679 
1680  targetNodeClassNames += "vuo.video.decodeImage";
1681  }
1682  if (isSupportedSceneFile(url.path().toUtf8().constData()))
1683  targetNodeClassNames += "vuo.scene.fetch";
1684  if (isSupportedAudioFile(url.path().toUtf8().constData()))
1685  targetNodeClassNames += "vuo.audio.file.play";
1686  if (isSupportedMeshFile(url.path().toUtf8().constData()))
1687  targetNodeClassNames += "vuo.image.project.dome";
1688  if (isSupportedFeedFile(url.path().toUtf8().constData()))
1689  targetNodeClassNames += "vuo.rss.fetch";
1690  if (isSupportedJsonFile(url.path().toUtf8().constData()))
1691  targetNodeClassNames += "vuo.tree.fetch.json";
1692  if (isSupportedXmlFile(url.path().toUtf8().constData()))
1693  targetNodeClassNames += "vuo.tree.fetch.xml";
1694  if (isSupportedTableFile(url.path().toUtf8().constData()))
1695  targetNodeClassNames += "vuo.table.fetch";
1696  if (isSupportedDataFile(url.path().toUtf8().constData()))
1697  targetNodeClassNames += "vuo.data.fetch";
1698  if (isSupportedAppFile(url.path().toUtf8().constData()))
1699  targetNodeClassNames += "vuo.app.launch";
1700  if (isDirectory(url.path().toUtf8().constData()))
1701  targetNodeClassNames += "vuo.file.list";
1702 
1703  QString selectedNodeClassName = "";
1704  if (targetNodeClassNames.size() == 1)
1705  selectedNodeClassName = targetNodeClassNames[0];
1706  else if (targetNodeClassNames.size() > 1)
1707  {
1708  QMenu nodeMenu(views()[0]->viewport());
1709  nodeMenu.setSeparatorsCollapsible(false);
1710 
1711  foreach (QString nodeClassName, targetNodeClassNames)
1712  {
1713  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName.toUtf8().constData());
1714  string nodeTitle = (nodeClass? nodeClass->getBase()->getDefaultTitle() : nodeClassName.toUtf8().constData());
1715  //: Appears in a popup menu when dragging files from Finder onto the composition canvas.
1716  QAction *nodeAction = nodeMenu.addAction(tr("Insert \"%1\" Node").arg(nodeTitle.c_str()));
1717  nodeAction->setData(nodeClassName);
1718  }
1719 
1720  menuSelectionInProgress = true;
1721  QAction *selectedNode = nodeMenu.exec(QCursor::pos());
1722  menuSelectionInProgress = false;
1723 
1724  selectedNodeClassName = (selectedNode? selectedNode->data().toString().toUtf8().constData() : "");
1725  }
1726 
1727  if (!selectedNodeClassName.isEmpty())
1728  {
1729  VuoRendererNode *newNode = createNode(selectedNodeClassName, "",
1730  event->scenePos().x(),
1731  event->scenePos().y() + yOffsetForPreviousNewNode + ySpacingForNewNodes);
1732 
1733  if (newNode)
1734  {
1735  VuoPort *urlPort = newNode->getBase()->getInputPortWithName(selectedNodeClassName == "vuo.file.list"?
1736  "folder" :
1737  "url");
1738  if (urlPort)
1739  {
1740  QString filePath = (useAbsoluteFilePaths? url.path() : compositionDir.relativeFilePath(url.path()));
1741  urlPort->getRenderer()->setConstant(VuoText_getString(filePath.toUtf8().constData()));
1742 
1743  newNodes.append(newNode);
1744 
1745  yOffsetForPreviousNewNode += newNode->boundingRect().height();
1746  yOffsetForPreviousNewNode += ySpacingForNewNodes;
1747  }
1748  }
1749  }
1750  }
1751 
1752  if (newNodes.size() > 0)
1753  {
1754  event->accept();
1755 
1756  emit componentsAdded(newNodes, this);
1757  }
1758  else
1759  event->ignore();
1760  }
1761  }
1762 
1763  // Accept drops of one or more nodes from the node library class list.
1764  else if (mimeData->hasFormat("text/scsv"))
1765  {
1766  event->setDropAction(Qt::CopyAction);
1767  event->accept();
1768 
1769  QByteArray scsvData = event->mimeData()->data("text/scsv");
1770  QString scsvText = QString::fromUtf8(scsvData);
1771  QStringList nodeClassNames = scsvText.split(';');
1772 
1773  QPointF startPos = event->scenePos()-QPointF(0,VuoRendererNode::nodeHeaderYOffset);
1774  int nextYPos = VuoRendererComposition::quantizeToNearestGridLine(startPos,
1776  int snapDelta = nextYPos - startPos.y();
1777 
1778  QList<QGraphicsItem *> newNodes;
1779  for (QStringList::iterator i = nodeClassNames.begin(); i != nodeClassNames.end(); ++i)
1780  {
1781  VuoRendererNode *newNode = createNode(*i, "",
1782  startPos.x(),
1783  nextYPos - (VuoRendererItem::getSnapToGrid()? 0 : snapDelta));
1784 
1785  if (newNode)
1786  {
1787  int prevYPos = nextYPos;
1788  nextYPos = VuoRendererComposition::quantizeToNearestGridLine(QPointF(0,prevYPos+newNode->boundingRect().height()),
1790  if (nextYPos <= prevYPos+newNode->boundingRect().height())
1792 
1793  newNodes.append((QGraphicsItem *)newNode);
1794  }
1795  }
1796 
1797  emit componentsAdded(newNodes, this);
1798  }
1799 
1800  // Accept drops of single nodes from the node library documentation panel.
1801  else if (mimeData->hasFormat("text/plain"))
1802  {
1803  event->setDropAction(Qt::CopyAction);
1804  event->accept();
1805 
1806  QList<QGraphicsItem *> newNodes;
1807  QStringList dropItems = event->mimeData()->text().split('\n');
1808  QString nodeClassName = dropItems[0];
1809 
1810  // Account for the offset between cursor and dragged item pixmaps
1811  // to drop node in-place.
1812  QPoint hotSpot = (dropItems.size() >= 3? QPoint(dropItems[1].toInt(), dropItems[2].toInt()) : QPoint(0,0));
1813  VuoRendererNode *newNode = createNode(nodeClassName, "",
1814  event->scenePos().x()-hotSpot.x()+1,
1815  event->scenePos().y()-VuoRendererNode::nodeHeaderYOffset-hotSpot.y()+1);
1816 
1817  newNodes.append(newNode);
1818  emit componentsAdded(newNodes, this);
1819  }
1820  else
1821  {
1822  event->ignore();
1823  }
1824 }
1825 
1829 void VuoEditorComposition::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
1830 {
1831  QGraphicsItem *nearbyItem = findNearbyComponent(event->scenePos());
1832  if (dynamic_cast<VuoRendererPort *>(nearbyItem))
1833  {
1834  QGraphicsScene::sendEvent(nearbyItem, event);
1835  event->accept();
1836  }
1837 
1838  else
1839  QGraphicsScene::mouseDoubleClickEvent(event);
1840 }
1841 
1845 void VuoEditorComposition::mousePressEvent(QGraphicsSceneMouseEvent *event)
1846 {
1847  QPointF scenePos = event->scenePos();
1848 
1849  // Handle left-button presses.
1850  if (event->button() == Qt::LeftButton)
1851  {
1852  // Correct for erroneous tracking behavior that occurs following the first left-click
1853  // directly on a node, cable, or comment after the cancellation of a component duplication operation.
1854  // See https://b33p.net/kosada/node/3339
1855  if (duplicationCancelled)
1856  {
1857  QGraphicsItem *itemClickedDirectly = itemAt(scenePos, views()[0]->transform());
1858  if (dynamic_cast<VuoRendererNode *>(itemClickedDirectly) ||
1859  dynamic_cast<VuoRendererCable *>(itemClickedDirectly) ||
1860  dynamic_cast<VuoRendererComment *>(itemClickedDirectly)
1861  )
1862  {
1863  duplicationCancelled = false;
1864  correctForCancelledDuplication(event);
1865  }
1866  }
1867 
1868  // Determine whether the cursor is in range of any operable composition components.
1869  QGraphicsItem *nearbyItem = findNearbyComponent(scenePos);
1870  leftMousePressEventAtNearbyItem(nearbyItem, event);
1871  }
1872 
1873  // Handle non-left-button presses.
1874  else // if (event->button() != Qt::LeftButton)
1875  mousePressEventNonLeftButton(event);
1876 }
1877 
1882 void VuoEditorComposition::leftMousePressEventAtNearbyItem(QGraphicsItem *nearbyItem, QGraphicsSceneMouseEvent *event)
1883 {
1884  bool eventHandled = false;
1885 
1886  // If click did not occur within range of a port, check whether
1887  // the click occured on a cable within its yank zone.
1888  VuoRendererCable *cableYankedDirectly = NULL;
1889  VuoRendererPort *currentPort = dynamic_cast<VuoRendererPort *>(nearbyItem);
1890  if (! currentPort)
1891  {
1892  VuoRendererCable *currentCable = dynamic_cast<VuoRendererCable *>(nearbyItem);
1893  if (currentCable &&
1894  currentCable->yankZoneIncludes(event->scenePos()) &&
1895  currentCable->getBase()->getToPort())
1896  {
1897  currentPort = currentCable->getBase()->getToPort()->getRenderer();
1898  cableYankedDirectly = currentCable;
1899  }
1900  }
1901 
1902  if (currentPort)
1903  {
1904  // Case: Firing an event by Command+click
1905  bool isTriggerPort = (currentPort->getBase()->hasCompiler() && dynamic_cast<VuoCompilerTriggerPort *>(currentPort->getBase()->getCompiler()));
1906  if ((event->modifiers() & Qt::ControlModifier) && (currentPort->getInput() || isTriggerPort))
1907  fireTriggerPortEvent(currentPort->getBase());
1908 
1909  else
1910  {
1911  portWithDragInitiated = currentPort;
1912  cableWithYankInitiated = cableYankedDirectly;
1913  event->accept();
1914  }
1915 
1916  eventHandled = true;
1917  }
1918 
1919  // Case: Initiating duplication of selected components with Option/Alt+drag
1920  else if (nearbyItem && VuoEditorUtilities::optionKeyPressedForEvent(event) && !duplicationDragInProgress)
1921  {
1922  // Duplicate the selected components.
1923  duplicateOnNextMouseMove = true;
1924  duplicationDragInProgress = true;
1925  cursorPosBeforeDuplicationDragMove = (VuoRendererItem::getSnapToGrid()?
1928  event->scenePos());
1929 
1930  QGraphicsScene::mousePressEvent(event);
1931  eventHandled = true;
1932  }
1933 
1934  // Case: Left mouse-click made near enough to a cable to trigger cable selection
1935  if (dynamic_cast<VuoRendererCable *>(nearbyItem))
1936  {
1937  if (event->modifiers() & Qt::ControlModifier)
1938  nearbyItem->setSelected(! nearbyItem->isSelected());
1939 
1940  else
1941  {
1943  nearbyItem->setSelected(true);
1944  }
1945 
1946  event->accept();
1947  eventHandled = true;
1948  }
1949 
1950  // Case: Left mouse-click made for some other reason, not handled here
1951  if (!eventHandled)
1952  QGraphicsScene::mousePressEvent(event);
1953 }
1954 
1959 void VuoEditorComposition::mousePressEventNonLeftButton(QGraphicsSceneMouseEvent *event)
1960 {
1961  cancelCableDrag();
1962 
1963  // If a right-click occurred, generate the context menu event ourselves
1964  // rather than leaving it to QGraphicsScene. This prevents existing selected
1965  // components from being erroneously de-selected before
1966  // VuoEditorComposition::contextMenuEvent(...) is called if the right-click
1967  // occurred within range of, but not directly upon, a composition component.
1968  if (event->button() == Qt::RightButton)
1969  {
1970  QGraphicsSceneContextMenuEvent contextMenuEvent(QEvent::GraphicsSceneContextMenu);
1971  contextMenuEvent.setScreenPos(event->screenPos());
1972  contextMenuEvent.setScenePos(event->scenePos());
1973  contextMenuEvent.setReason(QGraphicsSceneContextMenuEvent::Mouse);
1974  QApplication::sendEvent(this, &contextMenuEvent);
1975  event->accept();
1976  }
1977 
1978  else
1979  QGraphicsScene::mousePressEvent(event);
1980 
1981  return;
1982 }
1983 
1990 void VuoEditorComposition::correctForCancelledDuplication(QGraphicsSceneMouseEvent *event)
1991 {
1992  // Simulate an extra mouse click for now to force the cursor
1993  // to track correctly with the next set of dragged components.
1994  QGraphicsSceneMouseEvent pressEvent(QEvent::GraphicsSceneMousePress);
1995  pressEvent.setScenePos(event->scenePos());
1996  pressEvent.setButton(Qt::LeftButton);
1997  QApplication::sendEvent(this, &pressEvent);
1998 
1999  QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
2000  releaseEvent.setScenePos(event->scenePos());
2001  releaseEvent.setButton(Qt::LeftButton);
2002  QApplication::sendEvent(this, &releaseEvent);
2003 }
2004 
2012 void VuoEditorComposition::initiateCableDrag(VuoRendererPort *currentPort, VuoRendererCable *cableYankedDirectly, QGraphicsSceneMouseEvent *event)
2013 {
2014  Qt::KeyboardModifiers modifiers = event->modifiers();
2015  bool optionKeyPressed = (modifiers & Qt::AltModifier);
2016  bool shiftKeyPressed = (modifiers & Qt::ShiftModifier);
2017 
2018  // For now, a left mouse press on an input port with a constant value, attached typecast, or attached "Make List" node does nothing.
2019  // Eventually, a mouse drag will detach the constant value, typecast, or "Make List" node.
2020  if (! (cableYankedDirectly || currentPort->getOutput() || currentPort->supportsDisconnectionByDragging()))
2021  {
2022  return;
2023  }
2024 
2025  // Determine based on the keypress modifiers and the attributes of the port whether to
2026  // create a new cable, disconnect an existing cable, or duplicate an existing cable.
2027  bool creatingNewCable = false;
2028  bool disconnectingExistingCable = false;
2029  bool duplicatingExistingCable = false;
2030 
2031  VuoPort *fixedPort = currentPort->getBase();
2032  VuoNode *fixedNode = getUnderlyingParentNodeForPort(fixedPort, this);
2033 
2034  VuoNode *fromNode = NULL;
2035  VuoPort *fromPort = NULL;
2036  VuoNode *toNode = NULL;
2037  VuoPort *toPort = NULL;
2038 
2039  // Case: Dragging from an output port
2040  if (currentPort->getOutput())
2041  {
2042  // Prepare for the "forward" creation of a new cable.
2043  fromPort = fixedPort;
2044  fromNode = fixedNode;
2045  creatingNewCable = true;
2046  }
2047 
2048  // Case: Dragging from an input port
2049  else if (! currentPort->getFunctionPort())
2050  {
2051  // If the input port has no connected cables to disconnect, prepare for the
2052  // "backward" creation of a new cable.
2053  if (currentPort->getBase()->getConnectedCables(true).empty())
2054  {
2055  toPort = fixedPort;
2056  toNode = fixedNode;
2057  creatingNewCable = true;
2058  }
2059 
2060  // If the input port does have connected cables, prepare for the
2061  // disconnection or duplication of one of these cables.
2062  else
2063  {
2064  if (optionKeyPressed)
2065  {
2066  duplicatingExistingCable = true;
2067  }
2068 
2069  else
2070  disconnectingExistingCable = true;
2071  }
2072  }
2073 
2074  // @todo: Case: Dragging from a function port
2075 
2076 
2077  // Perform the actual cable creation, if applicable.
2078  if (creatingNewCable)
2079  {
2080  // Create the cable first and set its endpoints later in case either endpoint is published,
2081  // since published nodes don't have compilers.
2082  cableInProgress = (new VuoCompilerCable(NULL,
2083  NULL,
2084  NULL,
2085  NULL))->getBase();
2086 
2087  // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2088  // regardless of its connected ports.
2089  cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2090 
2091  cableInProgressWasNew = true;
2092  cableInProgressShouldBeWireless = false;
2093 
2094  addCable(cableInProgress);
2095  cableInProgress->getRenderer()->setFrom(fromNode, fromPort);
2096  cableInProgress->getRenderer()->setTo(toNode, toPort);
2097  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2098  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2099  highlightEligibleEndpointsForCable(cableInProgress);
2100  fixedPort->getRenderer()->updateGeometry();
2101  }
2102 
2103  // Perform the actual cable disconnection, if applicable.
2104  else if (disconnectingExistingCable)
2105  {
2106  // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2107  if (cableYankedDirectly)
2108  cableInProgress = cableYankedDirectly->getBase();
2109 
2110  // Otherwise, disconnect the cable that was connected to the port most recently.
2111  else
2112  cableInProgress = currentPort->getBase()->getConnectedCables(true).back();
2113 
2114  cableInProgressWasNew = false;
2115  cableInProgressShouldBeWireless = cableInProgress->hasCompiler() && cableInProgress->getCompiler()->getHidden();
2116 
2117  currentPort->updateGeometry();
2118  cableInProgress->getRenderer()->updateGeometry();
2119  cableInProgress->getRenderer()->setHovered(false);
2120  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2121  cableInProgress->getRenderer()->setFloatingEndpointPreviousToPort(cableInProgress->getToPort());
2122  cableInProgress->getRenderer()->setPreviouslyAlwaysEventOnly(cableInProgress->getCompiler()->getAlwaysEventOnly());
2123  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2124  cableInProgress->getRenderer()->setTo(NULL, NULL);
2125  highlightEligibleEndpointsForCable(cableInProgress);
2127  cableInProgress->getRenderer()->setSelected(true);
2128  }
2129  // Perform the actual cable duplication, if applicable.
2130  else if (duplicatingExistingCable)
2131  {
2132  VuoCable *cableToDuplicate = NULL;
2133  // If a cable was yanked by clicking on it directly (rather than on the port), disconnect that cable.
2134  if (cableYankedDirectly)
2135  cableToDuplicate = cableYankedDirectly->getBase();
2136 
2137  // Otherwise, disconnect the cable that was connected to the port most recently.
2138  else
2139  cableToDuplicate = currentPort->getBase()->getConnectedCables(true).back();
2140 
2141  // Create the cable first and set its endpoints later in case either endpoint is published,
2142  // since published nodes don't have compilers.
2143  cableInProgress = (new VuoCompilerCable(NULL,
2144  NULL,
2145  NULL,
2146  NULL))->getBase();
2147 
2148  // If the 'Option' key was pressed at the time of the mouse click, make the cable event-only
2149  // regardless of its connected ports.
2150  cableInProgress->getCompiler()->setAlwaysEventOnly(shiftKeyPressed);
2151 
2152  cableInProgressWasNew = true;
2153  cableInProgressShouldBeWireless = cableToDuplicate->hasCompiler() &&
2154  cableToDuplicate->getCompiler()->getHidden();
2155  addCable(cableInProgress);
2156  cableInProgress->getRenderer()->setFrom(getUnderlyingParentNodeForPort(cableToDuplicate->getFromPort(), this),
2157  cableToDuplicate->getFromPort());
2158  cableInProgress->getRenderer()->setTo(NULL, NULL);
2159  cableInProgress->getRenderer()->setFloatingEndpointLoc(event->scenePos());
2160  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2161  highlightEligibleEndpointsForCable(cableInProgress);
2163  cableInProgress->getRenderer()->setSelected(true);
2164  fixedPort->getRenderer()->updateGeometry();
2165  }
2166 
2167  // The cable will need to be re-painted as its endpoint is dragged.
2168  cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2169 
2170  emit cableDragInitiated();
2171 
2172  event->accept();
2173 }
2174 
2178 void VuoEditorComposition::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2179 {
2180  // Handle left-button releases.
2181  if (event->button() == Qt::LeftButton)
2182  {
2183  portWithDragInitiated = NULL;
2184  cableWithYankInitiated = NULL;
2185  duplicateOnNextMouseMove = false;
2186  duplicationDragInProgress = false;
2187  dragStickinessDisabled = false;
2188  emit leftMouseButtonReleased();
2190 
2191  // If there was a cable drag in progress at the time of the left-mouse-button
2192  // release, conclude the drag -- either by connecting the cable to the eligible port
2193  // at which it was dropped, or, if not dropped at an eligible port, by deleting the cable.
2194  bool cableDragEnding = cableInProgress;
2195  if (cableDragEnding)
2196  concludeCableDrag(event);
2197 
2198  QGraphicsItem *item = findNearbyComponent(event->scenePos());
2199  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2200  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
2201  if (port)
2202  {
2203  // Display the port popover, as long as the mouse release did not have any keyboard modifiers
2204  // and did not mark the end of a drag.
2205  // Do so even if a cable drag was technically in progress, because all it takes to initiate
2206  // a cable drag is a mouse press. We could trigger cable drags upon mouse move events instead of
2207  // mouse press events to avoid this.
2208  if ((event->modifiers() == Qt::NoModifier) &&
2209  (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2210  {
2211  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
2212  if (typecastPort)
2213  {
2214  // Since the rendering of the typecast body includes its host port, display popovers for both.
2215  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
2216  enablePopoverForNode(typecastNode);
2217  enableInactivePopoverForPort(typecastPort->getReplacedPort());
2218  }
2219 
2220  else
2221  enableInactivePopoverForPort(port);
2222  }
2223  }
2224 
2225  else // if (!port)
2226  {
2227  if (!cableDragEnding && !node)
2229  }
2230 
2231  if (!cableDragEnding)
2232  QGraphicsScene::mouseReleaseEvent(event);
2233  }
2234 
2235  // Handle non-left-button releases.
2236  else // if (event->button() != Qt::LeftButton)
2237  {
2238  QGraphicsScene::mouseReleaseEvent(event);
2239  }
2240 }
2241 
2248 void VuoEditorComposition::concludeCableDrag(QGraphicsSceneMouseEvent *event)
2249 {
2250  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(false);
2251 
2252  // Conclude drags of published cables as a special case.
2253  // @todo: Integrate this more naturally.
2254  if (cableInProgress->isPublished())
2255  {
2256  concludePublishedCableDrag(event);
2257  return;
2258  }
2259 
2260  if (hasFeedbackErrors())
2261  {
2262  cancelCableDrag();
2264  return;
2265  }
2266 
2267  cableInProgress->getRenderer()->setWireless(cableInProgressShouldBeWireless);
2268 
2269  // Input or output port that the cable is being dragged from (i.e., the fixed endpoint).
2270  VuoRendererPort *fixedPort = NULL;
2271  if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2272  fixedPort = cableInProgress->getFromPort()->getRenderer();
2273  else if (cableInProgress->getToNode() && cableInProgress->getToPort())
2274  fixedPort = cableInProgress->getToPort()->getRenderer();
2275 
2276  // We will determine based on the presence or absence of an eligible port near the
2277  // location of the mouse release whether to connect or delete the dragged cable.
2278  bool completedCableConnection = false;
2279 
2280  // Potential side effects of a newly completed cable connection:
2281  VuoCable *dataCableToDisplace = NULL; // A previously existing incoming data+event cable may need to be displaced.
2282  VuoCable *cableToReplace = NULL; // A previously existing cable connecting the same two ports may need to be replaced.
2283  VuoRendererNode *typecastNodeToDelete = NULL; // A previously attached typecast may need to be deleted.
2284  VuoRendererPort *portToUnpublish = NULL; // A previously published port may need to be unpublished.
2285  string typecastToInsert = ""; // A typecast may need to be automatically inserted.
2286 
2287  // A generic port involved in the new connection may need to be specialized.
2288  VuoRendererPort *portToSpecialize = NULL;
2289  string specializedTypeName = "";
2290 
2291  // Input or output port that the cable is being dropped onto, if any.
2292  VuoRendererPort *targetPort = (VuoRendererPort *)findNearbyPort(event->scenePos(), false);
2293 
2294  // Node header area that the cable is being dropped onto, if any.
2295  // (If over both a port drop zone and a node header, the node header gets precedence.)
2296  {
2297  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2298  if (targetNode)
2299  {
2300  targetPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2301  if (targetPort)
2302  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2303  }
2304  }
2305 
2306  bool draggingPreviouslyPublishedCable = (!cableInProgressWasNew &&
2307  (dynamic_cast<VuoRendererPublishedPort *>(fixedPort) ||
2308  ((cableInProgress->getToPort() == NULL) &&
2309  dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort()->getRenderer()))));
2310 
2311  if (fixedPort && targetPort)
2312  {
2313  // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2314  // don't push the operation onto the Undo stack.
2315  bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2316  targetPort->getBase()) &&
2317  (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2318  cableInProgress->getCompiler()->getAlwaysEventOnly()));
2319  if (recreatingSameConnection)
2320  {
2321  revertCableDrag();
2322  return;
2323  }
2324 
2325  bool cableInProgressExpectedToCarryData = cableInProgress->getRenderer()->effectivelyCarriesData() &&
2326  targetPort->getDataType();
2327 
2328  VuoCable *preexistingCable = fixedPort->getCableConnectedTo(targetPort, false);
2329  bool preexistingCableWithMatchingDataCarryingStatus = (preexistingCable?
2330  (preexistingCable->getRenderer()->effectivelyCarriesData() ==
2331  cableInProgressExpectedToCarryData) :
2332  false);
2333 
2334  // Case: Replacing a pre-existing cable that connected the same two ports
2335  // but with a different data-carrying status, and whose "To" port is
2336  // the child port of a collapsed typecast.
2337  if (preexistingCable && !preexistingCableWithMatchingDataCarryingStatus &&
2338  preexistingCable->getToPort()->hasRenderer() &&
2339  preexistingCable->getToPort()->getRenderer()->getTypecastParentPort())
2340  {
2341  // @todo Implement for https://b33p.net/kosada/node/14153
2342  // For now, don't attempt it.
2343  revertCableDrag();
2344  return;
2345  }
2346 
2347  // Case: Completing a "forward" cable connection from an output port to an input port
2348  if (!preexistingCableWithMatchingDataCarryingStatus &&
2349  (fixedPort->canConnectDirectlyWithoutSpecializationTo(targetPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2350  selectBridgingSolution(fixedPort, targetPort, true, &portToSpecialize, specializedTypeName, typecastToInsert)))
2351  {
2352  // If input port had a connected collapsed typecast, uncollapse it.
2353  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2354  if (typecastPort)
2355  {
2356  VuoRendererPort *adjustedTargetPort = typecastPort->getReplacedPort();
2357  VuoRendererPort *childPort = typecastPort->getChildPort();
2358  VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2359  VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2360 
2361  targetPort = adjustedTargetPort;
2362 
2363  // If the typecast did not have multiple incoming cables, and the new cable will
2364  // be replacing it as a data source, delete the typecast.
2365  if (cableInProgressExpectedToCarryData &&
2366  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2367  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2368  typecastNodeToDelete = uncollapsedTypecast;
2369  }
2370 
2371  // If connecting two ports that already had a cable (of different data-carrying status) connecting them,
2372  // replace the pre-existing cable.
2373  if (preexistingCable)
2374  cableToReplace = preexistingCable;
2375 
2376  // If the cable carries data, determine what other sources of input data need to be
2377  // removed before completing this connection.
2378  if (cableInProgressExpectedToCarryData)
2379  {
2380  // If input port already had a connected data cable, delete that cable.
2381  vector<VuoCable *> previousConnectedCables = targetPort->getBase()->getConnectedCables(false);
2382  for (vector<VuoCable *>::iterator cable = previousConnectedCables.begin(); (! dataCableToDisplace) && (cable != previousConnectedCables.end()); ++cable)
2383  if ((((*cable)->getRenderer()->effectivelyCarriesData()) && (*cable) != cableToReplace))
2384  dataCableToDisplace = *cable;
2385 
2386  // If the input port was published as a data+event port, unpublish it.
2387  // @todo: Let published cable disconnection handle port unpublication.
2388  if (isPortPublished(targetPort))
2389  {
2390  vector<VuoRendererPublishedPort *> publishedDataConnections = targetPort->getPublishedPortsConnectedByDataCarryingCables();
2391  if (publishedDataConnections.size() > 0)
2392  portToUnpublish = targetPort;
2393  }
2394  }
2395 
2396  completedCableConnection = true;
2397  }
2398 
2399  // Case: Completing a "backward" cable connection from an input port to an output port
2400  else if (!preexistingCableWithMatchingDataCarryingStatus &&
2401  (targetPort->canConnectDirectlyWithoutSpecializationTo(fixedPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2402  selectBridgingSolution(targetPort, fixedPort, false, &portToSpecialize, specializedTypeName, typecastToInsert)))
2403 
2404  {
2405  completedCableConnection = true;
2406  }
2407  }
2408 
2409  // Complete the actual cable connection, if applicable.
2410  if (completedCableConnection)
2411  {
2412  emit undoStackMacroBeginRequested("Cable Connection");
2413 
2414  if (draggingPreviouslyPublishedCable)
2415  {
2416  // Record some information about the cable in progress, to create a replica.
2417  VuoNode *cableInProgressFromNode = cableInProgress->getFromNode();
2418  VuoNode *cableInProgressToNode = cableInProgress->getToNode();
2419  VuoPort *cableInProgressFromPort = cableInProgress->getFromPort();
2420  VuoPort *cableInProgressToPort = cableInProgress->getToPort();
2421  QPointF cableInProgressFloatingEndpointLoc = cableInProgress->getRenderer()->getFloatingEndpointLoc();
2422  bool cableInProgressAlwaysEventOnly = cableInProgress->getCompiler()->getAlwaysEventOnly();
2423 
2424  // Remove the original cable, unpublishing the port in the process.
2425  cancelCableDrag();
2426 
2427  // Restore a copy of the cable to participate in the new connection.
2428  VuoCable *cableInProgressCopy = (new VuoCompilerCable(NULL,
2429  NULL,
2430  NULL,
2431  NULL))->getBase();
2432 
2433  cableInProgressCopy->setFrom(cableInProgressFromNode, cableInProgressFromPort);
2434  cableInProgressCopy->setTo(cableInProgressToNode, cableInProgressToPort);
2435 
2436  addCable(cableInProgressCopy);
2437 
2438  cableInProgressCopy->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
2439  cableInProgressCopy->getRenderer()->setFloatingEndpointLoc(cableInProgressFloatingEndpointLoc);
2440 
2441  cableInProgress = cableInProgressCopy;
2442  cableInProgressWasNew = true;
2443  }
2444 
2445  // Workaround to avoid Qt's parameter count limit for signals/slots.
2446  pair<VuoRendererCable *, VuoRendererCable *> cableArgs = std::make_pair((dataCableToDisplace? dataCableToDisplace->getRenderer() : NULL),
2447  (cableToReplace? cableToReplace->getRenderer() : NULL));
2448  pair<string, string> typeArgs = std::make_pair(typecastToInsert, specializedTypeName);
2449  pair<VuoRendererPort *, VuoRendererPort *> portArgs = std::make_pair(portToUnpublish, portToSpecialize);
2450 
2451  emit connectionCompletedByDragging(cableInProgress->getRenderer(),
2452  targetPort,
2453  cableArgs,
2454  typecastNodeToDelete,
2455  typeArgs,
2456  portArgs);
2457 
2459 
2460  // Resume caching for dragged cable.
2461  if (cableInProgress)
2462  {
2463  cableInProgress->getRenderer()->updateGeometry();
2464  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2465  cableInProgress = NULL;
2466  }
2467  }
2468 
2469  // Otherwise, delete the cable.
2470  else // if (! completedCableConnection)
2471  cancelCableDrag();
2472 
2473  emit cableDragEnded();
2474 }
2475 
2479 void VuoEditorComposition::concludePublishedCableDrag(QGraphicsSceneMouseEvent *event)
2480 {
2481  VuoRendererPort *fixedPort;
2482  if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2483  fixedPort = cableInProgress->getFromPort()->getRenderer();
2484  else // if (cableInProgress->getToNode() && cableInProgress->getToPort())
2485  fixedPort = cableInProgress->getToPort()->getRenderer();
2486 
2487  // Potential side effects of a newly completed cable connection:
2488  // - A typecast may need to be automatically inserted.
2489  string typecastToInsert = "";
2490 
2491  // - A generic port involved in the new connection may need to be specialized.
2492  VuoRendererPort *portToSpecialize = NULL;
2493  string specializedTypeName = "";
2494 
2495  bool forceEventOnlyPublication = !cableInProgress->getRenderer()->effectivelyCarriesData();
2496 
2497  // Case: Initiating a cable drag from a published input port.
2498  if (cableInProgress && cableInProgress->isPublishedInputCable())
2499  {
2500  VuoRendererPort *internalInputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2501  VuoRendererPublishedPort *publishedInputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getFromPort()->getRenderer());
2502 
2503  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first input port.
2504  // (If over both a port drop zone and a node header, the node header gets precedence.)
2505  {
2506  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2507  if (targetNode)
2508  {
2509  internalInputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2510  if (internalInputPort)
2511  {
2512  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2513  forceEventOnlyPublication = true;
2514  }
2515  }
2516  }
2517 
2518  // Case: Cable was dropped onto an internal input port
2519  if (internalInputPort &&
2520  publishedInputPort)
2521  {
2522  // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2523  // don't push the operation onto the Undo stack.
2524  bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2525  internalInputPort->getBase()) &&
2526  (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2527  cableInProgress->getCompiler()->getAlwaysEventOnly()));
2528  if (recreatingSameConnection)
2529  {
2530  revertCableDrag();
2531  return;
2532  }
2533 
2534  // Case: Ports are compatible
2535  if ((publishedInputPort->isCompatibleAliasWithSpecializationForInternalPort(internalInputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName))
2536  || selectBridgingSolution(publishedInputPort, internalInputPort, true, &portToSpecialize, specializedTypeName, typecastToInsert))
2537  {
2538  bool cableInProgressExpectedToCarryData = (cableInProgress->getRenderer()->effectivelyCarriesData() &&
2539  internalInputPort->getDataType());
2540  VuoCable *cableToReplace = publishedInputPort->getCableConnectedTo(internalInputPort, true);
2541  bool cableToReplaceHasMatchingDataCarryingStatus = (cableToReplace?
2542  (cableToReplace->getRenderer()->effectivelyCarriesData() ==
2543  cableInProgressExpectedToCarryData) :
2544  false);
2545 
2546  // If replacing a preexisting cable with an identical cable, just cancel the operation
2547  // so that it doesn't go onto the Undo stack.
2548  if (cableToReplace && cableToReplaceHasMatchingDataCarryingStatus)
2549  cancelCableDrag();
2550 
2551  // Case: Replacing a pre-existing cable that connected the same two ports
2552  // but with a different data-carrying status, and whose "To" port is
2553  // the child port of a collapsed typecast.
2554  else if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus &&
2555  cableToReplace->getToPort()->hasRenderer() &&
2556  cableToReplace->getToPort()->getRenderer()->getTypecastParentPort())
2557  {
2558  // @todo Implement for https://b33p.net/kosada/node/14153
2559  // For now, don't attempt it.
2560  cancelCableDrag();
2561  }
2562 
2563  else
2564  {
2565  emit undoStackMacroBeginRequested("Cable Connection");
2566  cancelCableDrag();
2567 
2568  // If this source/target port combination already a cable connecting them, but of a different
2569  // data-carrying status, replace the old cable with the new one.
2570  if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus)
2571  {
2572  QList<QGraphicsItem *> removedComponents;
2573  removedComponents.append(cableToReplace->getRenderer());
2574  emit componentsRemoved(removedComponents, "Delete");
2575  }
2576 
2577  // If the target port had a connected collapsed typecast, uncollapse and delete it.
2578  // But first check whether the collapsed typecast is still present on canvas -- it's possible it was
2579  // already removed during the cancelCableDrag() call, if the cable being dragged was the
2580  // typecast's incoming data+event cable.
2581  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(internalInputPort);
2582  if (typecastPort && typecastPort->scene())
2583  {
2584  VuoRendererPort *childPort = typecastPort->getChildPort();
2585  VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2586  VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2587 
2588  // If the typecast did not have multiple incoming cables, and the new cable will
2589  // be replacing it as a data source, delete the typecast.
2590  if (cableInProgressExpectedToCarryData &&
2591  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2592  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2593  {
2594  QList<QGraphicsItem *> removedComponents;
2595  removedComponents.append(uncollapsedTypecast);
2596  emit componentsRemoved(removedComponents, "Delete");
2597  }
2598  }
2599 
2600  emit portPublicationRequested(internalInputPort->getBase(),
2601  dynamic_cast<VuoPublishedPort *>(publishedInputPort->getBase()),
2602  forceEventOnlyPublication,
2603  (portToSpecialize? portToSpecialize->getBase() : NULL),
2604  specializedTypeName,
2605  typecastToInsert,
2606  false); // Avoid nested Undo stack macros.
2608  }
2609  }
2610  else // Source port and target port aren't compatible
2611  cancelCableDrag();
2612  }
2613  else // Cable was dropped over empty canvas space
2614  cancelCableDrag();
2615  }
2616 
2617  // Case: Initiating a cable drag from a published output port.
2618  else if (cableInProgress && cableInProgress->isPublishedOutputCable())
2619  {
2620  VuoRendererPort *internalOutputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2621  VuoRendererPublishedPort *publishedOutputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getToPort()->getRenderer());
2622 
2623  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first output port.
2624  // (If over both a port drop zone and a node header, the node header gets precedence.)
2625  {
2626  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2627  if (targetNode)
2628  {
2629  internalOutputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2630  if (internalOutputPort)
2631  {
2632  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2633  forceEventOnlyPublication = true;
2634  }
2635  }
2636  }
2637 
2638  // Case: Cable was dropped onto an internal output port
2639  if (internalOutputPort &&
2640  publishedOutputPort)
2641  {
2642 
2643  // Case: Ports are compatible
2644  if ((publishedOutputPort->isCompatibleAliasWithSpecializationForInternalPort(internalOutputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName) &&
2645  publishedOutputPort->canAccommodateInternalPort(internalOutputPort, forceEventOnlyPublication))
2646  || selectBridgingSolution(internalOutputPort, publishedOutputPort, false, &portToSpecialize, specializedTypeName, typecastToInsert))
2647  {
2648  emit portPublicationRequested(internalOutputPort->getBase(),
2649  dynamic_cast<VuoPublishedPort *>(publishedOutputPort->getBase()),
2650  forceEventOnlyPublication,
2651  portToSpecialize? portToSpecialize->getBase() : NULL,
2652  specializedTypeName,
2653  typecastToInsert,
2654  true);
2655  }
2656  }
2657 
2658  cancelCableDrag();
2659  }
2660 
2661  emit cableDragEnded();
2662 }
2663 
2668 {
2670 
2671  if (! cableInProgress)
2672  return;
2673 
2674  if (cableInProgressWasNew)
2675  removeCable(cableInProgress->getRenderer());
2676  else
2677  {
2678  QList<QGraphicsItem *> removedComponents;
2679  removedComponents.append(cableInProgress->getRenderer());
2680  emit componentsRemoved(removedComponents, "Delete");
2681  }
2682 
2683  cableInProgress = NULL;
2684 }
2685 
2693 {
2694  if (cableInProgress && cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort())
2695  {
2696  cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2697  cableInProgress->getRenderer()->updateGeometry();
2698  cableInProgress->setTo(getUnderlyingParentNodeForPort(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort(), this),
2699  cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort());
2700  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2701  cableInProgress = NULL;
2702  }
2703  else if (cableInProgress)
2704  {
2705  cancelCableDrag();
2706  }
2707 
2710  emit cableDragEnded();
2711 }
2712 
2716 void VuoEditorComposition::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
2717 {
2718  bool leftMouseButtonPressed = (event->buttons() & Qt::LeftButton);
2719 
2720  // Disable Mission Control workaround, since it sometimes misdetects whether the left mouse
2721  // button is pressed and overzealously cancels cable drags (specifically, those originating from
2722  // sidebar published output ports). The workaround no longer appears to be effective as of Qt 5.2.1 anyway.
2723  /*
2724  // In the unlikely situation that we have a cable drag in progress but the left mouse
2725  // button is not pressed (e.g., if "Mission Control" was activated during a cable drag and
2726  // deactivated by releasing the mouse button), cancel the cable drag.
2727  // See https://b33p.net/kosada/node/3305
2728  if (cableInProgress && !menuSelectionInProgress && !leftMouseButtonPressed)
2729  cancelCableDrag();
2730  */
2731 
2732  // If in the process of a cable drag or mouse-over operation,
2733  // locate the nearest port or cable eligible for hover highlighting.
2734  if (cableInProgress || (! leftMouseButtonPressed))
2735  updateHoverHighlighting(event->scenePos());
2736 
2737 
2738  // Case: Left mouse button pressed
2739  if (leftMouseButtonPressed)
2740  {
2741  // Eliminate mouse jitter noise.
2742  if ((! dragStickinessDisabled) && (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2743  return;
2744 
2745  else
2746  dragStickinessDisabled = true;
2747 
2748  // If a port or cable has been clicked for dragging and this is the first mouse move event
2749  // since the click, initiate the cable drag.
2750  if (portWithDragInitiated || cableWithYankInitiated)
2751  {
2752  initiateCableDrag(portWithDragInitiated, cableWithYankInitiated, event);
2753  portWithDragInitiated = NULL;
2754  cableWithYankInitiated = NULL;
2755  }
2756 
2757  // If there is a cable drag in progress, update the location of the cable's
2758  // floating endpoint to match that of the cursor.
2759  if (cableInProgress)
2760  {
2761  VuoRendererCable *rc = cableInProgress->getRenderer();
2762 
2763  rc->updateGeometry();
2764  rc->setFloatingEndpointLoc(event->scenePos());
2765 
2767  }
2768 
2769  else if (duplicationDragInProgress)
2770  {
2771  if (duplicateOnNextMouseMove)
2772  {
2774  duplicateOnNextMouseMove = false;
2775  }
2776 
2777  QPointF newPos = (VuoRendererItem::getSnapToGrid()?
2780  event->scenePos());
2781  QPointF delta = newPos - cursorPosBeforeDuplicationDragMove;
2782  moveItemsBy(getSelectedNodes(), getSelectedComments(), delta.x(), delta.y(), false);
2783  cursorPosBeforeDuplicationDragMove = newPos;
2784  }
2785 
2786  else
2787  QGraphicsScene::mouseMoveEvent(event);
2788  }
2789 
2790  // Case: Left mouse button not pressed
2791  else
2792  QGraphicsScene::mouseMoveEvent(event);
2793 }
2794 
2800 void VuoEditorComposition::updateHoverHighlighting(QPointF scenePos, bool disablePortHoverHighlighting)
2801 {
2802  // Detect cable and port hover events ourselves, since we need to account
2803  // for their extended hover ranges.
2804  QGraphicsItem *item = cableInProgress? findNearbyPort(scenePos, false) : findNearbyComponent(scenePos);
2805  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
2806  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2807  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(item);
2808  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(item);
2809 
2810  // If hovering over a node header area while dragging a cable, treat it like hovering over the first input or output port.
2811  // (If over both a port drop zone and a node header, the node header gets precedence.)
2812  bool hoveringOverNodeHeader = false;
2813  if (cableInProgress)
2814  {
2815  VuoRendererNode *targetNode = findNearbyNodeHeader(scenePos);
2816  if (targetNode)
2817  {
2818  port = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2819  if (port)
2820  {
2821  item = targetNode;
2822  hoveringOverNodeHeader = true;
2823  }
2824  }
2825  }
2826 
2827  VuoRendererNode *node = NULL;
2828  if (! hoveringOverNodeHeader)
2829  {
2830  // Do not handle hover events for (non-drawer) nodes here.
2831  if (dynamic_cast<VuoRendererNode *>(item) && !makeListDrawer)
2832  item = NULL;
2833 
2834  node = dynamic_cast<VuoRendererNode *>(item);
2835  }
2836 
2837  // Case: The item (if any) that requires hover-highlighting given the current position of the cursor
2838  // is not the same as the item (if any) that was hover-highlighted previously.
2839  if (item != previousNearbyItem)
2840  {
2841  VuoRendererPort *previousPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2842  VuoRendererNode *previousNode = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2843  if (previousNode)
2844  previousPort = findTargetPortForCableDroppedOnNodeHeader(previousNode);
2845 
2846  // End hover-highlighting of the previous item, if any.
2847  if (previousNearbyItem)
2849 
2850  // Begin hover-highlighting of the current item, if any.
2851  if (cable)
2852  cable->extendedHoverEnterEvent();
2853  else if (node)
2854  {
2855  if (! cableInProgress)
2856  {
2857  if (makeListDrawer)
2858  makeListDrawer->extendedHoverEnterEvent(scenePos);
2859  }
2860  }
2861  else if (typecastPort && !disablePortHoverHighlighting)
2862  port->extendedHoverEnterEvent((bool)cableInProgress);
2863  else if (port && !disablePortHoverHighlighting)
2864  {
2865  port->extendedHoverEnterEvent((bool)cableInProgress);
2866  if (!cableInProgress)
2867  {
2868  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2869  connectedAntennaPort->extendedHoverEnterEvent((bool)cableInProgress, true);
2870  }
2871  }
2872 
2873  VuoRendererPort *previousTargetPort = (previousPort && previousPort->isEligibleForConnection() ? previousPort : NULL);
2874 
2875  // If the current item or previous item is a port and it got this status because the mouse hovered
2876  // over a node header, update the eligibility-highlighting of the port.
2877  if (cableInProgress)
2878  {
2879  VuoRendererPort *fixedPort = (cableInProgress->getFromPort() ?
2880  cableInProgress->getFromPort()->getRenderer() :
2881  cableInProgress->getToPort()->getRenderer());
2882 
2883  QList< QPair<VuoRendererPort *, bool> > updatedPorts;
2884  if (hoveringOverNodeHeader)
2885  updatedPorts.append( QPair<VuoRendererPort *, bool>(port, true) );
2886  if (previousNode && previousPort)
2887  updatedPorts.append( QPair<VuoRendererPort *, bool>(previousPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) );
2888 
2889  QPair<VuoRendererPort *, bool> p;
2890  foreach (p, updatedPorts)
2891  {
2892  VuoRendererPort *updatedPort = p.first;
2893 
2894  VuoRendererPort *typecastParentPort = updatedPort->getTypecastParentPort();
2895  if (typecastParentPort)
2896  updatedPort = typecastParentPort;
2897 
2898  updateEligibilityHighlightingForPort(updatedPort, fixedPort, p.second);
2899 
2900  VuoRendererNode *potentialDrawer = updatedPort->getUnderlyingParentNode();
2901  updateEligibilityHighlightingForNode(potentialDrawer);
2902  }
2903  }
2904 
2905  VuoRendererPort *targetPort = (port && port->isEligibleForConnection() ? port : NULL);
2906 
2907  // Update error dialogs and cable's data-carrying status.
2908  if (targetPort || previousTargetPort)
2909  {
2910  if (cableInProgress)
2911  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(targetPort && (!targetPort->getDataType() || hoveringOverNodeHeader));
2912 
2913  updateFeedbackErrors(targetPort);
2914  }
2915 
2916  previousNearbyItem = item;
2917  }
2918 
2919  // Case: The previously hover-highlighted item should remain hover-highlighted.
2920  else if (item)
2921  {
2922  if (cable)
2923  cable->extendedHoverMoveEvent();
2924  else if (port && !disablePortHoverHighlighting)
2925  {
2926  port->extendedHoverMoveEvent((bool)cableInProgress);
2927  if (!cableInProgress)
2928  {
2929  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2930  connectedAntennaPort->extendedHoverMoveEvent((bool)cableInProgress, true);
2931  }
2932  }
2933  else if (makeListDrawer)
2934  makeListDrawer->extendedHoverMoveEvent(scenePos);
2935  }
2936 }
2937 
2942 {
2943  if (previousNearbyItem)
2944  {
2945  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(previousNearbyItem);
2946  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2947  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(previousNearbyItem);
2948  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2949 
2950  if (node)
2951  {
2952  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(previousNearbyItem);
2953  if (drawer)
2954  drawer->extendedHoverLeaveEvent();
2955  else
2957  }
2958 
2959  if (cable)
2960  cable->extendedHoverLeaveEvent();
2961  else if (typecastPort)
2962  port->extendedHoverLeaveEvent();
2963  else if (port)
2964  {
2965  port->extendedHoverLeaveEvent();
2966  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2967  connectedAntennaPort->extendedHoverLeaveEvent();
2968  }
2969 
2970  previousNearbyItem = NULL;
2971  }
2972 }
2973 
2978 {
2979  if ((event->key() != Qt::Key_Alt) && (event->key() != Qt::Key_Shift) && (event->key() != Qt::Key_Escape))
2980  cancelCableDrag();
2981 
2982  Qt::KeyboardModifiers modifiers = event->modifiers();
2983  qreal adjustedNodeMoveRate = nodeMoveRate;
2984  if (modifiers & Qt::ShiftModifier)
2985  {
2986  adjustedNodeMoveRate *= nodeMoveRateMultiplier;
2987  }
2988 
2989  switch (event->key()) {
2990  case Qt::Key_Backspace:
2991  {
2993  break;
2994  }
2995  case Qt::Key_Delete:
2996  {
2998  break;
2999  }
3000  case Qt::Key_Up:
3001  {
3002  moveSelectedItemsBy(0, -1*adjustedNodeMoveRate);
3003  break;
3004  }
3005  case Qt::Key_Down:
3006  {
3007  if (modifiers & Qt::ControlModifier)
3008  openSelectedEditableNodes();
3009  else
3010  moveSelectedItemsBy(0, adjustedNodeMoveRate);
3011  break;
3012  }
3013  case Qt::Key_Left:
3014  {
3015  moveSelectedItemsBy(-1*adjustedNodeMoveRate, 0);
3016  break;
3017  }
3018  case Qt::Key_Right:
3019  {
3020  moveSelectedItemsBy(adjustedNodeMoveRate, 0);
3021  break;
3022  }
3023  case Qt::Key_Shift:
3024  {
3025  if (cableInProgress)
3026  {
3027  cableInProgress->getRenderer()->updateGeometry();
3028  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
3029  highlightEligibleEndpointsForCable(cableInProgress);
3030 
3031  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3032  if (hoveredPort)
3033  updateFeedbackErrors(hoveredPort);
3034  }
3035 
3036  break;
3037  }
3038  case Qt::Key_Enter:
3039  case Qt::Key_Return:
3040  {
3041  // Make sure the event is forwarded to any composition component within hover range.
3042  QGraphicsScene::keyPressEvent(event);
3043 
3044  if (!event->isAccepted())
3045  {
3046  // Otherwise, if there are any (and only) nodes currently selected, open a title editor for each one;
3047  // if there are any (and only) comments currently selected, open a comment text editor for each one.
3048  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
3049  set<VuoRendererComment *> selectedComments = getSelectedComments();
3050 
3051  if (selectedComments.empty() && !selectedNodes.empty())
3053  }
3054 
3055  break;
3056  }
3057 
3058  case Qt::Key_Escape:
3059  {
3060  if (duplicateOnNextMouseMove || duplicationDragInProgress)
3061  {
3063 
3064  duplicateOnNextMouseMove = false;
3065  duplicationDragInProgress = false;
3066  duplicationCancelled = true;
3067  }
3068  else if (cableInProgress)
3069  {
3070  revertCableDrag();
3071  }
3072  break;
3073  }
3074 
3075  default:
3076  {
3077  QGraphicsScene::keyPressEvent(event);
3078  break;
3079  }
3080  }
3081 }
3082 
3087 {
3088  switch (event->key()) {
3089  case Qt::Key_Shift:
3090  {
3091  if (cableInProgress)
3092  {
3093  cableInProgress->getRenderer()->updateGeometry();
3094  cableInProgress->getCompiler()->setAlwaysEventOnly(false);
3095  highlightEligibleEndpointsForCable(cableInProgress);
3096 
3097  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3098  if (hoveredPort)
3099  updateFeedbackErrors(hoveredPort);
3100  }
3101 
3102  break;
3103  }
3104 
3105  default:
3106  {
3107  QGraphicsScene::keyReleaseEvent(event);
3108  break;
3109  }
3110  }
3111 }
3112 
3116 void VuoEditorComposition::addActionToMenuAndMapper(QMenu *menu, QSignalMapper *mapper, QString name, int index)
3117 {
3118  QAction *action = new QAction(name, this);
3119  menu->addAction(action);
3120  mapper->setMapping(action, index);
3121  connect(action, &QAction::triggered, mapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
3122 }
3123 
3127 void VuoEditorComposition::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
3128 {
3129  // Determine whether the cursor is in range of any operable composition components.
3130  QGraphicsItem *item = findNearbyComponent(event->scenePos());
3131 
3133  contextMenu.setSeparatorsCollapsible(false);
3134 
3135  // Customize context menu for the canvas.
3136  if (! item)
3137  {
3138  QAction *insertNodeSection = new QAction(tr("Insert Node"), NULL);
3139  insertNodeSection->setEnabled(false);
3140  contextMenu.addAction(insertNodeSection);
3141 
3142  QString spacer(" ");
3143 
3144  {
3145  // "Share" nodes
3146  QMenu *shareMenu = new QMenu(&contextMenu);
3147  shareMenu->setSeparatorsCollapsible(false);
3148  shareMenu->setTitle(spacer + tr("Share"));
3149  contextMenu.addMenu(shareMenu);
3150 
3151  {
3152  QAction *action = new QAction(tr("Share Value"), NULL);
3153  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share"))));
3154  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3155  shareMenu->addAction(action);
3156  }
3157 
3158  {
3159  QAction *action = new QAction(tr("Share List"), NULL);
3160  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share.list"))));
3161  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3162  shareMenu->addAction(action);
3163  }
3164 
3165  {
3166  QAction *action = new QAction(tr("Share Event"), NULL);
3167  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.share"))));
3168  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3169  shareMenu->addAction(action);
3170  }
3171  }
3172 
3173  {
3174  // "Hold" nodes
3175  QMenu *holdMenu = new QMenu(&contextMenu);
3176  holdMenu->setSeparatorsCollapsible(false);
3177  holdMenu->setTitle(spacer + tr("Hold"));
3178  contextMenu.addMenu(holdMenu);
3179 
3180  {
3181  QAction *action = new QAction(tr("Hold Value"), NULL);
3182  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold2"))));
3183  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3184  holdMenu->addAction(action);
3185  }
3186 
3187  {
3188  QAction *action = new QAction(tr("Hold List"), NULL);
3189  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold.list2"))));
3190  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3191  holdMenu->addAction(action);
3192  }
3193  }
3194 
3195  {
3196  // "Allow" nodes
3197  QMenu *allowMenu = new QMenu(&contextMenu);
3198  allowMenu->setSeparatorsCollapsible(false);
3199  allowMenu->setTitle(spacer + tr("Allow"));
3200  contextMenu.addMenu(allowMenu);
3201 
3202  {
3203  QAction *action = new QAction(tr("Allow First Event"), NULL);
3204  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirst"))));
3205  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3206  allowMenu->addAction(action);
3207  }
3208 
3209  {
3210  QAction *action = new QAction(tr("Allow First Value"), NULL);
3211  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirstValue"))));
3212  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3213  allowMenu->addAction(action);
3214  }
3215 
3216  {
3217  QAction *action = new QAction(tr("Allow Periodic Events"), NULL);
3218  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.time.allowPeriodic"))));
3219  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3220  allowMenu->addAction(action);
3221  }
3222 
3223  {
3224  QAction *action = new QAction(tr("Allow Changes"), NULL);
3225  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowChanges2"))));
3226  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3227  allowMenu->addAction(action);
3228  }
3229  }
3230 
3231  contextMenu.addSeparator();
3232 
3233  QAction *contextMenuInsertComment = new QAction(NULL);
3234  contextMenuInsertComment->setText(tr("Insert Comment"));
3235  contextMenuInsertComment->setData(qVariantFromValue(event->scenePos()));
3236  connect(contextMenuInsertComment, &QAction::triggered, this, &VuoEditorComposition::insertComment);
3237  contextMenu.addAction(contextMenuInsertComment);
3238 
3239  QAction *contextMenuInsertSubcomposition = new QAction(NULL);
3240  contextMenuInsertSubcomposition->setText(tr("Insert Subcomposition"));
3241  contextMenuInsertSubcomposition->setData(qVariantFromValue(event->scenePos()));
3242  connect(contextMenuInsertSubcomposition, &QAction::triggered, this, &VuoEditorComposition::insertSubcomposition);
3243  contextMenu.addAction(contextMenuInsertSubcomposition);
3244  }
3245 
3246  // Prepare to take a snapshot of the contextMenuDeleteSelection QAction's current values, so that
3247  // they do not change while the context menu is displayed.
3248  QAction *contextMenuDeleteSelectedSnapshot = new QAction(NULL);
3249 
3250  // Customize context menu for ports.
3251  if (dynamic_cast<VuoRendererPort *>(item))
3252  {
3253  VuoRendererPort *port = (VuoRendererPort *)item;
3254 
3255  if (port->isConstant() && inputEditorManager)
3256  {
3257  VuoType *dataType = static_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler())->getDataVuoType();
3258  VuoInputEditor *inputEditorLoadedForPortDataType = inputEditorManager->newInputEditor(dataType);
3259  if (inputEditorLoadedForPortDataType)
3260  {
3261  contextMenuSetPortConstant->setData(qVariantFromValue((void *)port));
3262  contextMenu.addAction(contextMenuSetPortConstant);
3263 
3264  inputEditorLoadedForPortDataType->deleteLater();
3265  }
3266  }
3267 
3268  if (!isPortPublished(port) && port->getPublishable())
3269  {
3270  contextMenuPublishPort->setText(tr("Publish Port"));
3271  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3272  contextMenu.addAction(contextMenuPublishPort);
3273  }
3274 
3275  else if (isPortPublished(port))
3276  {
3277  vector<VuoRendererPublishedPort *> externalPublishedPorts = port->getPublishedPorts();
3278  bool hasExternalPublishedPortWithMultipleInternalPorts = false;
3279  bool hasExternalPublishedPortBelongingToActiveProtocol = false;
3280  foreach (VuoRendererPublishedPort *externalPort, externalPublishedPorts)
3281  {
3282  if (externalPort->getBase()->getConnectedCables(true).size() > 1)
3283  hasExternalPublishedPortWithMultipleInternalPorts = true;
3284 
3285  if (dynamic_cast<VuoPublishedPort *>(externalPort->getBase())->isProtocolPort())
3286  hasExternalPublishedPortBelongingToActiveProtocol = true;
3287  }
3288 
3289  // Omit the "Delete Published Port" context menu item if the internal port is connected to
3290  // an external published port that cannot be deleted, either because:
3291  // - It is part of an active protocol;
3292  // - It has more than one connected cable.
3293  if (!hasExternalPublishedPortWithMultipleInternalPorts &&!hasExternalPublishedPortBelongingToActiveProtocol)
3294  {
3295  contextMenuPublishPort->setText(tr("Delete Published Port"));
3296  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3297  contextMenu.addAction(contextMenuPublishPort);
3298  }
3299  }
3300 
3301  bool isTriggerPort = dynamic_cast<VuoCompilerTriggerPort *>(port->getBase()->getCompiler());
3302  if (isTriggerPort || port->getInput())
3303  {
3304  __block bool isTopLevelCompositionRunning = false;
3305  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
3306  {
3307  isTopLevelCompositionRunning = topLevelComposition->isRunning();
3308  });
3309 
3310  if (isTopLevelCompositionRunning)
3311  {
3312  contextMenuFireEvent->setData(qVariantFromValue((void *)port));
3313  contextMenu.addAction(contextMenuFireEvent);
3314  }
3315  }
3316 
3317  if (isTriggerPort)
3318  {
3319  QMenu *contextMenuThrottling = new QMenu(&contextMenu);
3320  contextMenuThrottling->setSeparatorsCollapsible(false);
3321  contextMenuThrottling->setTitle(tr("Set Event Throttling"));
3323  foreach (QAction *action, contextMenuThrottlingActions)
3324  {
3325  contextMenuThrottling->addAction(action);
3326  action->setData(qVariantFromValue((void *)port));
3327  action->setCheckable(true);
3328  action->setChecked( i++ == port->getBase()->getEventThrottling() );
3329  }
3330  contextMenu.addMenu(contextMenuThrottling);
3331  }
3332 
3333  // Allow the user to specialize, respecialize, or unspecialize generic data types.
3334  VuoGenericType *genericDataType = dynamic_cast<VuoGenericType *>(port->getDataType());
3335  if (genericDataType || isPortCurrentlyRevertible(port))
3336  {
3337  QMenu *contextMenuSpecializeGenericType = new QMenu(&contextMenu);
3338  contextMenuSpecializeGenericType->setSeparatorsCollapsible(false);
3339  contextMenuSpecializeGenericType->setTitle(tr("Set Data Type"));
3340  contextMenuSpecializeGenericType->setToolTipsVisible(true);
3341 
3342  QAction *unspecializeAction = contextMenuSpecializeGenericType->addAction(tr("Generic"));
3343 
3344  // It is safe to assume that there will be types listed after the "Generic" option
3345  // since any network of connected generic/specialized ports has at least one compatible
3346  // data type in common, so the separator can be added here without forward-checking.
3347  contextMenuSpecializeGenericType->addSeparator();
3348 
3349  unspecializeAction->setData(qVariantFromValue((void *)port));
3350  unspecializeAction->setCheckable(true);
3351  unspecializeAction->setChecked(genericDataType);
3352 
3353  contextMenu.addMenu(contextMenuSpecializeGenericType);
3354 
3355  VuoGenericType *genericTypeFromPortClass = NULL; // Original generic type of the port
3356  set<string> compatibleTypes; // Compatible types in the context of the connected generic port network
3357  set<string> compatibleTypesInIsolation; // Compatible types for the port in isolation
3358 
3359  // Allow the user to specialize generic ports.
3360  if (genericDataType)
3361  {
3362  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(port->getBase()->getClass()->getCompiler());
3363  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3364 
3365  // Determine compatible types in the context of the connected generic port network.
3366  VuoGenericType::Compatibility compatibility;
3367  vector<string> compatibleTypesVector = genericDataType->getCompatibleSpecializedTypes(compatibility);
3368  compatibleTypes = set<string>(compatibleTypesVector.begin(), compatibleTypesVector.end());
3369 
3370  // If all types or all list types are compatible, add them to (currently empty) compatibleTypes.
3371  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3372  {
3373  vector<string> compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3374  compatibleTypes.insert(compatibleTypeNames.begin(), compatibleTypeNames.end());
3375  }
3376  }
3377 
3378  // Allow the user to re-specialize already specialized ports.
3379  else if (isPortCurrentlyRevertible(port))
3380  {
3381  map<VuoNode *, string> nodesToReplace;
3382  set<VuoCable *> cablesToDelete;
3383  createReplacementsToUnspecializePort(port->getBase(), nodesToReplace, cablesToDelete);
3384  if (cablesToDelete.size() >= 1)
3385  {
3386  // @todo https://b33p.net/kosada/node/8895 : Note that this specialization will break connections.
3387  }
3388 
3389  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(
3390  port->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler());
3391  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(port->getBase()->getClass()) );
3392  compatibleTypes = getRespecializationOptionsForPortInNetwork(port);
3393  }
3394 
3395  // Determine compatible types in isolation.
3396  if (genericTypeFromPortClass)
3397  {
3398  // "Make List" drawer child ports require special handling, since technically they are compatible with all types
3399  // but practically they are limited to types compatible with their host ports.
3400  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(port->getUnderlyingParentNode());
3401  if (drawer)
3402  {
3403  VuoPort *hostPort = drawer->getUnderlyingHostPort();
3404  if (hostPort && hostPort->hasRenderer())
3405  {
3406  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(hostPort->getClass()->getCompiler());
3407  VuoGenericType *genericHostPortDataType = dynamic_cast<VuoGenericType *>(hostPort->getRenderer()->getDataType());
3408  if (genericHostPortDataType)
3409  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3410  else if (isPortCurrentlyRevertible(hostPort->getRenderer()))
3411  {
3413  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3414  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass->getBase()) );
3415  }
3416  }
3417  }
3418 
3420  vector<string> compatibleTypesInIsolationVector;
3421  if (genericTypeFromPortClass)
3422  compatibleTypesInIsolationVector = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibilityInIsolation);
3423 
3424  // If all types or all list types are compatible, add them to (currently empty) compatibleTypesInIsolationVector.
3425  if (compatibilityInIsolation == VuoGenericType::anyType || compatibilityInIsolation == VuoGenericType::anyListType)
3426  compatibleTypesInIsolationVector = getAllSpecializedTypeOptions(compatibilityInIsolation == VuoGenericType::anyListType);
3427 
3428  foreach (string type, compatibleTypesInIsolationVector)
3429  compatibleTypesInIsolation.insert(drawer? VuoType::extractInnermostTypeName(type) : type);
3430  }
3431 
3432  // List the compatible types in the menu.
3433  {
3434  // If there are only a handful of eligible types, display them all in a flat menu.
3435  const int maxTypeCountForFlatMenuDisplay = 10;
3436  if (compatibleTypesInIsolation.size() <= maxTypeCountForFlatMenuDisplay)
3437  {
3438  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, false, "", contextMenuSpecializeGenericType);
3439  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3440  }
3441 
3442  // Otherwise, organize them by node set.
3443  else
3444  {
3445  // First list compatible types that don't belong to any specific node set.
3446  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, "", contextMenuSpecializeGenericType);
3447  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3448 
3449  // Now add a submenu for each node set that contains compatible types.
3450  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3451  QList<QAction *> allNodeSetActionsToAdd;
3452  for (map<string, set<VuoCompilerType *> >::iterator i = loadedTypesForNodeSet.begin(); i != loadedTypesForNodeSet.end(); ++i)
3453  {
3454  string nodeSetName = i->first;
3455  if (!nodeSetName.empty())
3456  allNodeSetActionsToAdd += getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, nodeSetName, contextMenuSpecializeGenericType);
3457  }
3458 
3459  if ((contextMenuSpecializeGenericType->actions().size() > 0) && (allNodeSetActionsToAdd.size() > 0))
3460  contextMenuSpecializeGenericType->addSeparator();
3461 
3462  addTypeActionsToMenu(allNodeSetActionsToAdd, contextMenuSpecializeGenericType);
3463  }
3464 
3465  foreach (QAction *action, contextMenuSpecializeGenericType->actions())
3466  {
3467  QMenu *specializeSubmenu = action->menu();
3468  if (specializeSubmenu)
3469  {
3470  foreach (QAction *specializeSubaction, specializeSubmenu->actions())
3471  connect(specializeSubaction, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3472  }
3473  else if (action == unspecializeAction)
3474  connect(action, &QAction::triggered, this, &VuoEditorComposition::unspecializePortType);
3475  else
3476  connect(action, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3477  }
3478  }
3479  }
3480 
3481  // Allow the user to hide, unhide, or delete cables connected directly to the port or, if the
3482  // port has a collapsed typecast, to the typecast's child port.
3483  int numVisibleDirectlyConnectedCables = 0;
3484  int numHidableDirectlyConnectedCables = 0;
3485  int numUnhidableDirectlyConnectedCables = 0;
3486 
3487  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
3488  {
3489  if (!cable->getRenderer()->paintingDisabled())
3490  {
3491  numVisibleDirectlyConnectedCables++;
3492  if (!cable->isPublished())
3493  numHidableDirectlyConnectedCables++;
3494  }
3495 
3496  else if (cable->getRenderer()->getEffectivelyWireless())
3497  numUnhidableDirectlyConnectedCables++;
3498  }
3499 
3500  int numVisibleChildPortConnectedCables = 0;
3501  int numHidableChildPortConnectedCables = 0;
3502  int numUnhidableChildPortConnectedCables = 0;
3503  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
3504  if (typecastPort)
3505  {
3506  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
3507  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
3508  foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
3509  {
3510  if (!cable->getRenderer()->paintingDisabled())
3511  {
3512  numVisibleChildPortConnectedCables++;
3513 
3514  if (!cable->isPublished())
3515  numHidableChildPortConnectedCables++;
3516  }
3517  else if (cable->getRenderer()->getEffectivelyWireless())
3518  numUnhidableChildPortConnectedCables++;
3519  }
3520  }
3521 
3522  // Count the number of directly or indirectly connected cables that are currently visible.
3523  int numVisibleConnectedCables = numVisibleDirectlyConnectedCables + numVisibleChildPortConnectedCables;
3524 
3525  // Count the number of directly or indirectly connected cables that are currently hidable.
3526  int numHidableConnectedCables = numHidableDirectlyConnectedCables + numHidableChildPortConnectedCables;
3527 
3528  // Count the number of directly or indirectly connected cables that are currently hidden.
3529  int numUnhidableConnectedCables = numUnhidableDirectlyConnectedCables + numUnhidableChildPortConnectedCables;
3530 
3531  if ((!renderHiddenCables && ((numHidableConnectedCables >= 1) || (numUnhidableConnectedCables >= 1)))
3532  || (numVisibleConnectedCables >= 1))
3533  {
3534  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3535  contextMenu.addSeparator();
3536  }
3537 
3538  if (!renderHiddenCables)
3539  {
3540  if (numHidableConnectedCables >= 1)
3541  {
3542  // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3543  // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3544  // impact visible published cables.
3545  int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3546  contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3547  contextMenuHideCables->setData(qVariantFromValue((void *)port));
3548  contextMenu.addAction(contextMenuHideCables);
3549  }
3550 
3551  if (numUnhidableConnectedCables >= 1)
3552  {
3553  int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3554  contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3555  contextMenuUnhideCables->setData(qVariantFromValue((void *)port));
3556  contextMenu.addAction(contextMenuUnhideCables);
3557  }
3558  }
3559 
3560  if (numVisibleConnectedCables >= 1)
3561  {
3562  contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3563  contextMenuDeleteCables->setData(qVariantFromValue((void *)port));
3564  contextMenu.addAction(contextMenuDeleteCables);
3565  }
3566  }
3567 
3568  // Customize context menu for nodes, cables, and/or comments.
3569  else if (item)
3570  {
3571  if (! item->isSelected())
3572  {
3573  clearSelection();
3574  item->setSelected(true);
3575  }
3576 
3577  QList<QGraphicsItem *> selectedComponents = selectedItems();
3578  bool onlyCommentsSelected = true;
3579  bool onlyCablesSelected = true;
3580  bool selectionContainsMissingNode = false;
3581  foreach (QGraphicsItem *item, selectedComponents)
3582  {
3583  if (!dynamic_cast<VuoRendererComment *>(item))
3584  onlyCommentsSelected = false;
3585 
3586  if (!dynamic_cast<VuoRendererCable *>(item))
3587  onlyCablesSelected = false;
3588 
3589  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3590  selectionContainsMissingNode = true;
3591  }
3592 
3593  contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3594  connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3595 
3596  // Comments
3597  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3598  if (comment)
3599  {
3600  // Option: Edit selected comments
3601  if (onlyCommentsSelected)
3602  contextMenu.addAction(contextMenuEditSelectedComments);
3603 
3604  // Option: Tint selected components(s)
3605  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3606 
3607  contextMenu.addSeparator();
3608 
3609  // Option: Refactor selected component(s)
3610  contextMenu.addAction(contextMenuRefactorSelected);
3611 
3612  contextMenu.addSeparator();
3613 
3614  // Option: Delete selected components(s)
3615  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3616  }
3617 
3618  // Cables
3619  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3620  if (cable)
3621  {
3622  if (!renderHiddenCables)
3623  {
3624  if (onlyCablesSelected && !cable->getBase()->isPublished())
3625  contextMenu.addAction(contextMenuHideSelectedCables);
3626  }
3627 
3628  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3629  }
3630 
3631  // Nodes
3632  else if (dynamic_cast<VuoRendererNode *>(item))
3633  {
3634  VuoRendererNode *node = (VuoRendererNode *)item;
3635 
3636  // Input drawers
3637  if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3638  node->getBase()->getNodeClass()->hasCompiler())
3639  {
3640  // Offer "Add/Remove Input Port" options for resizable list input drawers.
3641  if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3642  {
3643  contextMenuAddInputPort->setData(qVariantFromValue((void *)node));
3644  contextMenuRemoveInputPort->setData(qVariantFromValue((void *)node));
3645 
3646  int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3647  contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3648 
3649  contextMenu.addAction(contextMenuAddInputPort);
3650  contextMenu.addAction(contextMenuRemoveInputPort);
3651 
3652  contextMenu.addSeparator();
3653  }
3654 
3655  // Offer "Reset" option for all input drawers.
3656  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3657  }
3658 
3659  // Non-input-drawer nodes
3660  else
3661  {
3662  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3663  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3664  {
3665  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3666  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3667  if (originalGenericNodeClass)
3668  nodeClass = originalGenericNodeClass->getBase();
3669  }
3670 
3671  QString actionText, sourcePath;
3672  bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3673  bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3674 
3675  int numSelectedNonAttachmentNodes = 0;
3676  QList<QGraphicsItem *> selectedComponents = selectedItems();
3677  foreach (QGraphicsItem *item, selectedComponents)
3678  {
3679  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3680  numSelectedNonAttachmentNodes++;
3681  }
3682 
3683  if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3684  {
3685  // Option: Edit an installed subcomposition, shader, or text-code node.
3686  QAction *editAction = new QAction(NULL);
3687  editAction->setText(actionText);
3688  editAction->setData(qVariantFromValue(node));
3689  connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3690 
3691  contextMenu.addAction(editAction);
3692  contextMenu.addSeparator();
3693  }
3694 
3695  // Option: Rename selected node(s)
3696  if (node->getBase()->hasCompiler())
3697  contextMenu.addAction(contextMenuRenameSelected);
3698 
3699  // Option: Tint selected component(s)
3700  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3701 
3702  contextMenu.addSeparator();
3703 
3704  // Option: Replace selected node with a similar node
3705  if (numSelectedNonAttachmentNodes == 1)
3706  {
3707  if (contextMenuChangeNode)
3708  contextMenuChangeNode->deleteLater();
3709 
3710  contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3711  contextMenuChangeNode->setSeparatorsCollapsible(false);
3712  contextMenuChangeNode->setTitle(tr("Change To"));
3713 
3714  populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3715  if (!contextMenuChangeNode->actions().isEmpty())
3716  contextMenu.addMenu(contextMenuChangeNode);
3717  } // End single selected node
3718 
3719  // Option: Refactor selected component(s)
3720  if (!selectionContainsMissingNode)
3721  contextMenu.addAction(contextMenuRefactorSelected);
3722 
3723  if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3724  {
3725  // Option: Open the enclosing folder for an editable or other 3rd party node class.
3726  QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3727  if (!modulePath.isEmpty())
3728  {
3729  QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3730  QAction *openEnclosingFolderAction = new QAction(NULL);
3731  openEnclosingFolderAction->setText("Show in Finder");
3732  openEnclosingFolderAction->setData(qVariantFromValue(enclosingDirUrl));
3733  connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3734  contextMenu.addAction(openEnclosingFolderAction);
3735  }
3736  }
3737 
3738  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3739  contextMenu.addSeparator();
3740 
3741  // Option: Delete selected components(s)
3742  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3743 
3744  } // End non-input-drawer nodes
3745  } // End nodes
3746  } // End nodes or cables
3747 
3748  if (!contextMenu.actions().isEmpty())
3749  {
3750  // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3752 
3753  menuSelectionInProgress = true;
3754  contextMenu.exec(event->screenPos());
3755  menuSelectionInProgress = false;
3756  }
3757 
3758  delete contextMenuDeleteSelectedSnapshot;
3759 }
3760 
3765 {
3766  QAction *sender = (QAction *)QObject::sender();
3767  VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3768  emit nodeSourceEditorRequested(node);
3769 }
3770 
3777 {
3778  vector<string> typeOptions;
3779 
3780  map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
3781  for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
3782  {
3783  if (((!lists && !VuoType::isListTypeName(i->first)) ||
3784  (lists && VuoType::isListTypeName(i->first))) &&
3785  !VuoType::isDictionaryTypeName(i->first) &&
3786  (i->first != "VuoMathExpressionList"))
3787  {
3788  typeOptions.push_back(i->first);
3789  }
3790  }
3791 
3792  return typeOptions;
3793 }
3794 
3800 set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3801 {
3802  if (!port)
3803  return set<string>();
3804 
3805  // Find the set of connected generic ports that contains our target port.
3806  set<VuoPort *> connectedGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(port->getUnderlyingParentNode()->getBase(),
3807  port->getBase(), true);
3808 
3809  // Determine the set of types compatible with all generic ports in the connected network.
3810  vector<string> compatibleInnerTypeNames;
3811  vector<string> compatibleTypeNames;
3812  for (VuoPort *connectedPort : connectedGenericPorts)
3813  {
3814  VuoGenericType *genericTypeFromPortClass = NULL;
3815  VuoCompilerNodeClass *nodeClass = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler();
3816  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3817  if (specializedNodeClass)
3818  {
3819  VuoPortClass *portClass = connectedPort->getClass();
3820  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3821  }
3822 
3823  VuoGenericType::Compatibility compatibility;
3824  vector<string> compatibleTypeNamesForPort = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3825  vector<string> innermostCompatibleTypeNamesForPort;
3826  for (vector<string>::iterator k = compatibleTypeNamesForPort.begin(); k != compatibleTypeNamesForPort.end(); ++k)
3827  innermostCompatibleTypeNamesForPort.push_back( VuoType::extractInnermostTypeName(*k) );
3828 
3829  if (! innermostCompatibleTypeNamesForPort.empty())
3830  {
3831  if (compatibleInnerTypeNames.empty())
3832  compatibleInnerTypeNames = innermostCompatibleTypeNamesForPort;
3833  else
3834  {
3835  for (int k = compatibleInnerTypeNames.size() - 1; k >= 0; --k)
3836  if (find(innermostCompatibleTypeNamesForPort.begin(), innermostCompatibleTypeNamesForPort.end(), compatibleInnerTypeNames[k]) ==
3837  innermostCompatibleTypeNamesForPort.end())
3838  compatibleInnerTypeNames.erase(compatibleInnerTypeNames.begin() + k);
3839  }
3840  }
3841  }
3842 
3844  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3845  VuoPortClass *portClass = port->getBase()->getClass();
3846  VuoGenericType *genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3847  string typeNameForPort = genericTypeFromPortClass->getModuleKey();
3848 
3849  // Finish compiling set of compatible types in the context of the connected generic port network.
3850  string prefix = (VuoType::isListTypeName(typeNameForPort) ? VuoType::listTypeNamePrefix : "");
3851  for (vector<string>::iterator k = compatibleInnerTypeNames.begin(); k != compatibleInnerTypeNames.end(); ++k)
3852  compatibleTypeNames.push_back(prefix + *k);
3853 
3854  if (compatibleTypeNames.empty())
3855  {
3856  VuoGenericType::Compatibility compatibility;
3857  genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3858 
3859  // If all types or all list types are compatible, add them to (currently empty) compatibleTypeNames.
3860  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3861  compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3862  }
3863 
3864  return set<string>(compatibleTypeNames.begin(), compatibleTypeNames.end());
3865 }
3866 
3887  set<string> compatibleTypesInIsolation,
3888  set<string> compatibleTypesInContext,
3889  bool limitToNodeSet,
3890  string nodeSetName,
3891  QMenu *menu)
3892 {
3893  QList<QAction *> actionsToAddToMenu;
3894 
3895  map<string, VuoCompilerType *> allTypes = compiler->getTypes();
3896  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3897  QList<VuoCompilerType *> compatibleTypesForNodeSetDisplay;
3898  foreach (string typeName, compatibleTypesInIsolation)
3899  {
3900  VuoCompilerType *type = allTypes[typeName];
3901  if ((!limitToNodeSet || (loadedTypesForNodeSet[nodeSetName].find(type) != loadedTypesForNodeSet[nodeSetName].end())) &&
3902  (! VuoGenericType::isGenericTypeName(typeName)) &&
3903  // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
3904  (typeName != "VuoUrl" && typeName != "VuoList_VuoUrl"
3905  // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
3906  && typeName != "VuoInteraction" && typeName != "VuoList_VuoInteraction"
3907  && typeName != "VuoInteractionType" && typeName != "VuoList_VuoInteractionType"
3908  && typeName != "VuoUuid" && typeName != "VuoList_VuoUuid"
3909  // Hide deprecated types.
3910  && typeName != "VuoIconPosition" && typeName != "VuoList_VuoIconPosition"
3911  && typeName != "VuoMesh" && typeName != "VuoList_VuoMesh"
3912  && typeName != "VuoWindowProperty" && typeName != "VuoList_VuoWindowProperty"
3913  && typeName != "VuoWindowReference" && typeName != "VuoList_VuoWindowReference"))
3914  compatibleTypesForNodeSetDisplay.append(type);
3915  }
3916 
3917  if (!compatibleTypesForNodeSetDisplay.isEmpty())
3918  {
3919  QMenu *contextMenuNodeSetTypes = NULL;
3920  QList<QAction *> actionsToAddToNodeSetSubmenu;
3921  bool enabledContentAdded = false;
3922 
3923  // Populate the "Specialize Type" submenu for the target node set.
3924  if (!nodeSetName.empty())
3925  {
3926  contextMenuNodeSetTypes = new QMenu(menu);
3927  contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
3928  contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
3929  contextMenuNodeSetTypes->setToolTipsVisible(true);
3930  actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
3931  }
3932 
3933  foreach (VuoCompilerType *type, compatibleTypesForNodeSetDisplay)
3934  {
3935  string typeName = type->getBase()->getModuleKey();
3936  QList<QVariant> portAndSpecializedType;
3937  portAndSpecializedType.append(qVariantFromValue((void *)genericPort));
3938  portAndSpecializedType.append(typeName.c_str());
3939 
3940  QAction *specializeAction;
3941  QString typeTitle = formatTypeNameForDisplay(type->getBase());
3942 
3943  // Case: Adding to the node set submenu
3944  if (!nodeSetName.empty())
3945  {
3946  specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
3947  actionsToAddToNodeSetSubmenu.append(specializeAction);
3948  }
3949 
3950  // Case: Adding to the top-level action list
3951  else
3952  {
3953  specializeAction = new QAction(typeTitle, menu);
3954  actionsToAddToMenu.append(specializeAction);
3955  }
3956 
3957  specializeAction->setData(QVariant(portAndSpecializedType));
3958  specializeAction->setCheckable(true);
3959  specializeAction->setChecked(genericPort && (genericPort->getDataType()->getModuleKey() == type->getBase()->getModuleKey()));
3960 
3961  if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
3962  {
3963  specializeAction->setEnabled(false);
3964  //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
3965  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."));
3966  }
3967  else
3968  enabledContentAdded = true;
3969  }
3970 
3971  if (contextMenuNodeSetTypes)
3972  {
3973  addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
3974 
3975  if (!enabledContentAdded)
3976  contextMenuNodeSetTypes->setEnabled(false);
3977  }
3978  }
3979 
3980  QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
3981  return actionListWithPromotions;
3982 }
3983 
3988 QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
3989 {
3990  QList<QAction *> modifiedActionList;
3991  foreach (QAction *action, actionList)
3992  {
3993  if (action->menu() && (action->menu()->actions().size() == 1))
3994  {
3995  QAction *singleSubaction = action->menu()->actions().first();
3996  action->menu()->removeAction(singleSubaction);
3997  modifiedActionList.append(singleSubaction);
3998  }
3999  else
4000  modifiedActionList.append(action);
4001  }
4002 
4003  return modifiedActionList;
4004 }
4005 
4009 void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
4010 {
4011  std::sort(actionList.begin(), actionList.end(), nodeSetMenuActionLessThan);
4012  foreach (QAction *action, actionList)
4013  menu->addAction(action);
4014 }
4015 
4019 void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4020 {
4021  if (!now)
4022  return;
4023 
4025  if (activeWindow)
4026  emit compositionOnTop(activeWindow->getComposition() == this);
4027 }
4028 
4032 void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4033 {
4034  if (ignoreApplicationStateChangeEvents)
4035  return;
4036 
4038  if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4039  emit applicationActive(active);
4040 }
4041 
4050 QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4051 {
4052  return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4053 }
4054 
4059 {
4060  QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4061  return dynamic_cast<VuoRendererNode *>(item);
4062 }
4063 
4076 QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4077  targetComponentType targetType,
4078  bool limitPortCollisionRange)
4079 {
4080  // Determine which types of components to filter out of search results.
4081  bool ignoreCables;
4082  bool ignoreNodes;
4083  bool ignorePorts;
4084  bool ignoreComments;
4085 
4086  switch(targetType)
4087  {
4088  case VuoEditorComposition::targetTypePort:
4089  {
4090  ignoreCables = true;
4091  ignoreNodes = true;
4092  ignorePorts = false;
4093  ignoreComments = true;
4094  break;
4095  }
4096  case VuoEditorComposition::targetTypeNodeHeader:
4097  {
4098  ignoreCables = true;
4099  ignoreNodes = true;
4100  ignorePorts = true;
4101  ignoreComments = true;
4102  break;
4103  }
4104  default:
4105  {
4106  ignoreCables = false;
4107  ignoreNodes = false;
4108  ignorePorts = false;
4109  ignoreComments = false;
4110  break;
4111  }
4112  }
4113 
4114  // The topmost item under the cursor is not necessarily the one we will return
4115  // (e.g., if the cursor is positioned directly over a node but also within range
4116  // of one of that node's ports), but it will factor in to the decision.
4117  QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4118  if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4119  topmostItemUnderCursor = NULL;
4120 
4121  for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4122  {
4123  QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4124  QList<QGraphicsItem *> itemsInRange = items(searchRect);
4125  for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4126  {
4127  if (!(*i)->isEnabled())
4128  continue;
4129 
4130  // Check whether we have located an unobscured "Make List" drawer drag handle.
4131  if (! ignoreNodes)
4132  {
4133  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4134  bool makeListDragHandle =
4135  (makeListDrawer &&
4136  makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4137  if (makeListDragHandle &&
4138  ((! topmostItemUnderCursor) ||
4139  (topmostItemUnderCursor == makeListDrawer) ||
4140  (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4141  {
4142  return makeListDrawer;
4143  }
4144  }
4145 
4146  // Check whether we have located an unobscured port.
4147  // Hovering within range of a port takes precedence over hovering
4148  // directly over that port's parent node.
4149  if (! ignorePorts)
4150  {
4151  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4152  if (port &&
4153  ((! topmostItemUnderCursor) ||
4154  (topmostItemUnderCursor == port) ||
4155  (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4156  (topmostItemUnderCursor->zValue() < port->zValue()))
4157  &&
4158  ((! limitPortCollisionRange) ||
4159  port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4160  &&
4161  ! port->getFunctionPort())
4162  {
4163  return port;
4164  }
4165  }
4166 
4167  // Check whether we have located an unobscured cable.
4168  if (! ignoreCables)
4169  {
4170  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4171  if (cable &&
4172  ((! topmostItemUnderCursor) ||
4173  (topmostItemUnderCursor == cable) ||
4174  (topmostItemUnderCursor->zValue() < cable->zValue())))
4175  {
4176  return cable;
4177  }
4178  }
4179 
4180  // Check whether we have located an unobscured comment.
4181  if (! ignoreComments)
4182  {
4183  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4184  if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4185  comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4186 
4187  if (comment &&
4188  ((! topmostItemUnderCursor) ||
4189  (topmostItemUnderCursor == (*i)) ||
4190  (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4191  {
4192  return comment;
4193  }
4194  }
4195 
4196  // Check whether we have located an unobscured node header.
4197  if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4198  {
4199  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4200  if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4201  {
4202  QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4203  headerRect = node->mapToScene(headerRect).boundingRect();
4204  if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4205  return node;
4206  }
4207  }
4208  }
4209  }
4210 
4211  // Having failed to locate any other relevant components within range, return the node
4212  // directly under the cursor, if applicable.
4213  if (! ignoreNodes)
4214  {
4215  if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4216  return topmostItemUnderCursor;
4217 
4218  // It is possible that the item under the cursor is a port, but that it didn't meet
4219  // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4220  if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4221  return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4222  }
4223 
4224  return NULL;
4225 }
4226 
4237 {
4238  if (! cableInProgress)
4239  return NULL;
4240 
4241  VuoPort *fromPort = cableInProgress->getFromPort();
4242  VuoPort *toPort = cableInProgress->getToPort();
4243  VuoPort *fixedPort = (fromPort? fromPort: toPort);
4244 
4245  if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4246  return NULL;
4247 
4248  return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4249 }
4250 
4256 {
4257  // Start with the first input or output port (other than the refresh port).
4258  vector<VuoRendererPort *> portList;
4259  int firstPortIndex;
4260 
4261  if (inputPort)
4262  {
4263  portList = node->getInputPorts();
4265  }
4266  else
4267  {
4268  portList = node->getOutputPorts();
4270  }
4271 
4272  VuoRendererPort *targetPort = NULL;
4273  if (portList.size() > firstPortIndex)
4274  {
4275  targetPort = portList[firstPortIndex];
4276 
4277  // If the first input port has a wall,
4278  // keep moving down until we find one without a wall.
4279  // (Unless they all have walls, in which case stick with the first.)
4280  VuoRendererPort *firstPortWithoutWall = nullptr;
4281  for (int i = firstPortIndex; i < portList.size(); ++i)
4282  {
4283  if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4284  {
4285  firstPortWithoutWall = portList[i];
4286  break;
4287  }
4288  }
4289  if (firstPortWithoutWall)
4290  targetPort = firstPortWithoutWall;
4291 
4292  // If the first input port has a drawer attached to it,
4293  // instead select the first input port of the drawer.
4294  // (Unless the drawer has no input ports, in which case don't select any port.)
4295  VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4296  if (drawer)
4297  {
4298  portList = drawer->getInputPorts();
4299  if (portList.size() > firstPortIndex)
4300  targetPort = portList[firstPortIndex];
4301  else
4302  targetPort = NULL;
4303  }
4304 
4305  // If the selected input port has a collapsed type-converter node attached to it,
4306  // instead select the type-converter node's input port.
4307  VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4308  if (typecast)
4309  targetPort = typecast->getChildPort();
4310  }
4311 
4312  return targetPort;
4313 }
4314 
4320 {
4321  QRectF boundingRect;
4322  foreach (QGraphicsItem *item, items())
4323  {
4324  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4325  if (! (rc && rc->getBase()->isPublished()))
4326  boundingRect |= item->sceneBoundingRect();
4327  }
4328 
4329  return boundingRect;
4330 }
4331 
4337 {
4338  QRectF boundingRect;
4339 
4340  foreach (QGraphicsItem *item, selectedItems())
4341  {
4342  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4343  if (! (rc && rc->getBase()->isPublished()))
4344  boundingRect |= item->sceneBoundingRect();
4345  }
4346 
4347  return boundingRect;
4348 }
4349 
4355 {
4356  QRectF boundingRect;
4357 
4358  foreach (QGraphicsItem *item, selectedItems())
4359  {
4360  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4361  if (! (rc && rc->getBase()->isPublished()))
4362  boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4363 
4364  // Include attached drawers.
4365  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4366  if (rn)
4367  {
4368  foreach (VuoPort *port, rn->getBase()->getInputPorts())
4369  {
4370  VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4371  if (drawer)
4372  boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4373  }
4374  }
4375  }
4376 
4377  return boundingRect;
4378 }
4379 
4384 void VuoEditorComposition::updateInternalPortConstant(string portID, string newValue, bool updateInRunningComposition)
4385 {
4386  VuoPort *port = portWithStaticIdentifier[portID];
4387  if (!port)
4388  return;
4389 
4390  updatePortConstant(dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4391 }
4392 
4397 void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4398 {
4400  if (!port)
4401  return;
4402 
4403  updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4404 }
4405 
4410 void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4411 {
4412  // Internal ports
4413  if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4414  {
4415  port->getBase()->getRenderer()->setConstant(newValue);
4416 
4417  if (updateInRunningComposition)
4418  updateInternalPortConstantInRunningComposition(dynamic_cast<VuoCompilerInputEventPort *>(port), newValue);
4419  }
4420 
4421  // Published ports
4422  else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4423  {
4424  dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4425 
4426  if (updateInRunningComposition)
4427  updatePublishedInputPortConstantInRunningComposition(dynamic_cast<VuoPublishedPort *>(port->getBase()), newValue);
4428  }
4429 }
4430 
4436 {
4439 }
4440 
4449 {
4450 
4451  // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4452  if (!errorMarkingUpdatesEnabled)
4453  return;
4454 
4455  errorMarkingUpdatesEnabled = false;
4456 
4457  // Remove any error annotations from the previous call to this function.
4458  if (errorMark)
4459  {
4460  errorMark->removeFromScene();
4461  errorMark = NULL;
4462  }
4463 
4465 
4466  // Check for errors.
4467 
4468  VuoCompilerIssues *issues = new VuoCompilerIssues();
4469  VuoCompilerCable *potentialCable = NULL;
4470  try
4471  {
4472  set<VuoCompilerCable *> potentialCables;
4473 
4474  if (targetPort && cableInProgress)
4475  {
4476  VuoNode *fromNode;
4477  VuoPort *fromPort;
4478  VuoNode *toNode;
4479  VuoPort *toPort;
4480  if (cableInProgress->getFromNode())
4481  {
4482  fromNode = cableInProgress->getFromNode();
4483  fromPort = cableInProgress->getFromPort();
4484  toNode = targetPort->getUnderlyingParentNode()->getBase();
4485  toPort = targetPort->getBase();
4486  }
4487  else
4488  {
4489  fromNode = targetPort->getUnderlyingParentNode()->getBase();
4490  fromPort = targetPort->getBase();
4491  toNode = cableInProgress->getToNode();
4492  toPort = cableInProgress->getToPort();
4493  }
4494  potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4495  potentialCable->getBase()->setFrom(fromNode, fromPort);
4496  potentialCable->getBase()->setTo(toNode, toPort);
4497  potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4498  cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4499 
4500  fromPort->removeConnectedCable(potentialCable->getBase());
4501  toPort->removeConnectedCable(potentialCable->getBase());
4502  potentialCables.insert(potentialCable);
4503  }
4504 
4505  getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4506  }
4507  catch (const VuoCompilerException &e)
4508  {
4509  }
4510 
4511  if (! issues->isEmpty())
4512  {
4513  VUserLog("%s: Showing error popover: %s",
4514  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4515  issues->getShortDescription(false).c_str());
4516 
4517  this->errorMark = new VuoErrorMark();
4518 
4519  foreach (VuoCompilerIssue issue, issues->getList())
4520  {
4521  set<VuoRendererNode *> nodesToMark;
4522  set<VuoRendererCable *> cablesToMark;
4523 
4524  set<VuoNode *> problemNodes = issue.getNodes();
4525  foreach (VuoNode *node, problemNodes)
4526  if (node->hasRenderer())
4527  nodesToMark.insert(node->getRenderer());
4528 
4529  set<VuoCable *> problemCables = issue.getCables();
4530  foreach (VuoCable *cable, problemCables)
4531  {
4532  VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4533  if (cableToMark->hasRenderer())
4534  cablesToMark.insert(cableToMark->getRenderer());
4535  }
4536 
4538  errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4539 
4540  VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4541  errorPopovers.insert(errorPopover);
4544 
4545  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4546 
4547  // Place the popover near an appropriate nearby node involved in the feedback loop.
4548  VuoRendererNode *nearbyNode = NULL;
4549  if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4550  {
4551  nearbyNode = targetPort->getRenderedParentNode();
4552  }
4553  else if (! nodesToMark.empty())
4554  {
4555  VuoRendererNode *topmostVisibleNode = NULL;
4556  qreal topY = viewportRect.bottom();
4557  foreach (VuoRendererNode *node, nodesToMark)
4558  {
4559  if (node->getProxyNode())
4560  node = node->getProxyNode();
4561 
4562  QPointF scenePos = node->scenePos();
4563  if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4564  {
4565  topmostVisibleNode = node;
4566  topY = scenePos.y();
4567  }
4568  }
4569 
4570  if (topmostVisibleNode)
4571  nearbyNode = topmostVisibleNode;
4572  else
4573  {
4574  VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4575  nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4576  }
4577  }
4578  else
4579  {
4580  VUserLog("Warning: no nearby node (no marked nodes).");
4581  }
4582 
4583  // If no nodes are known to be involved in the feedback loop, display the popover
4584  // in the center of the viewport.
4585  const QPoint offsetFromNode(0,10);
4586  QPoint popoverTopLeftInScene = (nearbyNode?
4587  (nearbyNode->scenePos().toPoint() +
4588  nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4589  offsetFromNode) :
4590  QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4591  viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4592 
4593  // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4594  // of the viewport closest to the feedback loop.
4595  const int margin = 5;
4596  popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4597  fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4598 
4599  popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4600  fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4601 
4602  QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4603  QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4604 
4605  errorPopover->move(popoverTopLeftGlobal);
4606  errorPopover->show();
4607  emit popoverDetached();
4608  }
4609 
4610  // Add error annotations to the composition.
4611  addItem(errorMark);
4612  }
4613  delete issues;
4614  delete potentialCable;
4615 
4616  errorMarkingUpdatesEnabled = true;
4617 }
4618 
4622 bool VuoEditorComposition::hasFeedbackErrors(void)
4623 {
4624  return this->errorMark;
4625 }
4626 
4631 {
4632  if (hasFeedbackErrors())
4633  this->errorMark->updateErrorMarkPath();
4634 }
4635 
4641 void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4642 {
4643  try
4644  {
4645  emit buildStarted();
4646 
4647  if (! dependenciesUninstalled.empty())
4648  {
4649  vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4650  string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4651  throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4652  }
4653 
4654  delete runningComposition;
4655  runningComposition = NULL;
4656  runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4657 
4658  runningCompositionActiveDriver = getDriverForActiveProtocol();
4659  if (runningCompositionActiveDriver)
4660  runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4661 
4662  string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4663  string dir, file, ext;
4664  VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4665  linkedCompositionPath = dir + file + ".dylib";
4666 
4667  compiler->setShouldPotentiallyShowSplashWindow(false);
4668 
4669  VuoCompilerIssues *issues = new VuoCompilerIssues();
4670  compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4671  compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries);
4672  delete issues;
4673 
4674  remove(compiledCompositionPath.c_str());
4675 
4676  emit buildFinished("");
4677  }
4678  catch (VuoException &e)
4679  {
4680  delete runningComposition;
4681  runningComposition = NULL;
4682 
4683  emit buildFinished(e.what());
4684  throw;
4685  }
4686 }
4687 
4693 bool VuoEditorComposition::isRunningThreadUnsafe(void)
4694 {
4695  return runner != NULL && ! stopRequested && ! runner->isStopped();
4696 }
4697 
4704 {
4705  __block bool running;
4706  dispatch_sync(runCompositionQueue, ^{
4707  running = isRunningThreadUnsafe();
4708  });
4709  return running;
4710 }
4711 
4717 void VuoEditorComposition::run(string compositionSnapshot)
4718 {
4719  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4720 
4721  // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4722  subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4723 
4724  // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4725  subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4726  {
4727  compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4728  });
4729 
4730  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4731  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4732  {
4733  if (matchingComposition->showEventsMode)
4734  matchingComposition->beginDisplayingActivity();
4735  });
4736 
4737  stopRequested = false;
4738  dispatch_async(runCompositionQueue, ^{
4739  try
4740  {
4741  runningCompositionLibraries = new VuoRunningCompositionLibraries();
4742 
4743  buildComposition(compositionSnapshot);
4744 
4745  string compositionLoaderPath = compiler->getCompositionLoaderPath();
4746  string compositionSourceDir = getBase()->getDirectory();
4747 
4748  runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4749  runner->setDelegate(this);
4751  runner->startPaused();
4752  pid_t pid = runner->getCompositionPid();
4753 
4754  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4755  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4756  {
4757  if (matchingComposition->showEventsMode)
4758  this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4759 
4760  dispatch_sync(activePortPopoversQueue, ^{
4761  for (auto i : matchingComposition->activePortPopovers)
4762  {
4763  string portID = i.first;
4764  updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4765  }
4766  });
4767  });
4768 
4769  runner->unpause();
4770 
4771  // Focus the composition's windows (if any).
4773  }
4774  catch (...) { }
4775  });
4776 }
4777 
4784 {
4785  stopRequested = true;
4786  dispatch_async(runCompositionQueue, ^{
4787  if (runner && ! runner->isStopped())
4788  {
4789  runner->stop();
4790  runner->waitUntilStopped();
4791  }
4792  delete runner;
4793  runner = NULL;
4794 
4795  linkedCompositionPath = "";
4796  runningCompositionLibraries = NULL;
4797 
4798  delete runningComposition;
4799  runningComposition = NULL;
4800 
4801  emit stopFinished();
4802  });
4803 
4804  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4805 
4806  // Tell this composition and all subcompositions opened from it to stop display live debug info.
4807  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4808  {
4809  if (matchingComposition->showEventsMode)
4810  matchingComposition->stopDisplayingActivity();
4811 
4812  dispatch_sync(activePortPopoversQueue, ^{
4813  for (auto i : matchingComposition->activePortPopovers)
4814  {
4815  VuoPortPopover *popover = i.second;
4816  popover->setCompositionRunning(false);
4817  }
4818  });
4819  });
4820 }
4821 
4833 void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4834  VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4835 {
4836  if (! diffInfo)
4837  diffInfo = new VuoCompilerCompositionDiff();
4838 
4839  dispatch_async(runCompositionQueue, ^{
4840  if (isRunningThreadUnsafe())
4841  {
4842  try
4843  {
4844  foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4845  {
4846  runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4847  }
4848 
4849  string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4850  VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4851  if (previouslyActiveDriver)
4852  {
4853  VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4854  previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4855  oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4856  }
4857 
4858  buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4859 
4860  string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4861  runner->replaceComposition(linkedCompositionPath, compositionDiff);
4862  }
4863  catch (exception &e)
4864  {
4865  VUserLog("Composition stopped itself: %s", e.what());
4866  emit compositionStoppedItself();
4867  }
4868  catch (...)
4869  {
4870  VUserLog("Composition stopped itself.");
4871  emit compositionStoppedItself();
4872  }
4873  }
4874  else
4875  {
4876  dispatch_async(dispatch_get_main_queue(), ^{
4877  updateCompositionsThatContainThisSubcomposition(newCompositionSnapshot);
4878  });
4879  }
4880 
4881  delete diffInfo;
4882  });
4883 }
4884 
4890 {
4891  void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
4892  {
4893  compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
4894  };
4895  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
4896 }
4897 
4903 {
4904  VuoPort *port = portWithStaticIdentifier[runningPortID];
4905  if (!(port && port->hasCompiler() && port->hasRenderer()))
4906  return;
4907 
4908  string constant = port->getRenderer()->getConstantAsString();
4909 
4910  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4911  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4912  {
4913  if (this == topLevelComposition)
4914  {
4915  dispatch_async(runCompositionQueue, ^{
4916  if (isRunningThreadUnsafe())
4917  {
4918  json_object *constantObject = json_tokener_parse(constant.c_str());
4919  runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
4920  }
4921  });
4922  }
4923  };
4924  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4925 
4926  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4927  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4928  {
4929  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4930  {
4931  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
4932  });
4933  });
4934 }
4935 
4941 {
4943  if (!(port && port->hasCompiler()))
4944  return;
4945 
4946  string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
4948 }
4949 
4954 {
4955  map<VuoPort *, string>::iterator i = staticIdentifierForPort.find(port->getBase());
4956  if (i == staticIdentifierForPort.end())
4957  return;
4958  string runningPortIdentifier = i->second;
4959 
4960  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4961  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4962  {
4963  if (this == topLevelComposition)
4964  {
4965  dispatch_async(runCompositionQueue, ^{
4966  if (isRunningThreadUnsafe())
4967  {
4968  json_object *constantObject = json_tokener_parse(constant.c_str());
4969  runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
4970  }
4971  });
4972  }
4973  };
4974  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4975 
4976  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4977  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4978  {
4979  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4980  {
4981  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
4982  });
4983  });
4984 }
4985 
4990 void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
4991 {
4992  dispatch_async(runCompositionQueue, ^{
4993  if (isRunningThreadUnsafe())
4994  {
4995  json_object *constantObject = json_tokener_parse(constant.c_str());
4996  set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
4997  foreach (string subcompositionIdentifier, subcompositionIdentifiers)
4998  {
4999  runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
5000  }
5001  }
5002  });
5003 }
5004 
5009 {
5010  dispatch_async(runCompositionQueue, ^{
5011  if (isRunningThreadUnsafe())
5012  {
5013  VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
5014  if (publishedPort)
5015  {
5016  json_object *constantObject = json_tokener_parse(constant.c_str());
5017  map<VuoRunner::Port *, json_object *> m;
5018  m[publishedPort] = constantObject;
5019  runner->setPublishedInputPortValues(m);
5020  }
5021  }
5022  });
5023 }
5024 
5025 
5030 {
5031  return contextMenuDeleteSelected;
5032 }
5033 
5038 {
5039  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5040  // For now, this recreates the context menu rather than accessing a data member.
5041  QMenu *contextMenuTints = new QMenu(parent);
5042  contextMenuTints->setSeparatorsCollapsible(false);
5043  contextMenuTints->setTitle(tr("Tint"));
5044  foreach (QAction *tintAction, contextMenuTintActions)
5045  contextMenuTints->addAction(tintAction);
5046  contextMenuTints->insertSeparator(contextMenuTintActions.last());
5047 
5048  return contextMenuTints;
5049 }
5050 
5054 void VuoEditorComposition::expandChangeNodeMenu()
5055 {
5056  QAction *sender = (QAction *)QObject::sender();
5057  VuoRendererNode *node = static_cast<VuoRendererNode *>(sender->data().value<void *>());
5058 
5059  // If the menu hasn't been expanded previously, expand it enough now to fill
5060  // the available vertical screenspace.
5061  int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5062  if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5063  {
5064  int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5065  int verticalSpacePerItem = 21; // menu row height in pixels
5066  // Estimate the number of matches that will fit within the screen without scrolling:
5067  // -1 to account for a possible extra "More…" row;
5068  // -1 wiggle room to match observations
5069  int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5070 
5071  populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5072  }
5073 
5074  // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5075  else
5076  populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5077 
5078  contextMenuChangeNode->exec();
5079 }
5080 
5086 void VuoEditorComposition::populateChangeNodeMenu(QMenu *menu, VuoRendererNode *node, int matchLimit)
5087 {
5088  menu->clear();
5089 
5090  if (!node)
5091  return;
5092 
5093  map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5094  vector<VuoCompilerNodeClass *> loadedNodeClasses;
5095  for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5096  loadedNodeClasses.push_back(i->second);
5097  VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5098 
5099  vector<string> bestMatches;
5100  map<string, double> matchScores;
5101  matchScores[""] = 0;
5102 
5103  int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5104  for (int i = 0; i < targetMatchCount; ++i)
5105  bestMatches.push_back("");
5106 
5107  // Maintain a priority queue with the @c targetMatchCount best matches.
5108  bool overflow = false;
5109 
5110  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5111  string originalGenericNodeClassName;
5112  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5113  originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5114  else
5115  originalGenericNodeClassName = nodeClass->getClassName();
5116 
5117  foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5118  {
5119  string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5120  if (loadedNodeClassName == originalGenericNodeClassName)
5121  continue;
5122 
5123  bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5124  double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5125  int highestIndexWithCompetitiveScore = -1;
5126  for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5127  if (matchScore <= matchScores[bestMatches[i] ])
5128  highestIndexWithCompetitiveScore = i;
5129 
5130  if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5131  {
5132  if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5133  overflow = true;
5134 
5135  for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5136  bestMatches[j+1] = bestMatches[j];
5137 
5138  bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5139  matchScores[loadedNodeClassName] = matchScore;
5140  }
5141  }
5142 
5143  for (int i = 0; i < targetMatchCount; ++i)
5144  {
5145  if (matchScores[bestMatches[i] ] > 0)
5146  {
5147  // Disambiguate between identical node titles using node class names.
5148  QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5149  if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5150  matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5151 
5152  QAction *changeAction = menu->addAction(matchDisplayText);
5153 
5154  QList<QVariant> currentNodeAndNewClass;
5155  currentNodeAndNewClass.append(qVariantFromValue((void *)node));
5156  currentNodeAndNewClass.append(bestMatches[i].c_str());
5157  changeAction->setData(QVariant(currentNodeAndNewClass));
5158  connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5159  }
5160  }
5161 
5162  if (overflow)
5163  {
5164  //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5165  QAction *showMoreAction = menu->addAction(tr("More…"));
5166  showMoreAction->setData(qVariantFromValue(static_cast<void *>(node)));
5167  connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5168  }
5169 }
5170 
5175 bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5176 {
5177  // Inventory required input port types (connected data inputs) in the node to be replaced.
5178  map<string, int> requiredInputs;
5179  bool inputEventSourceRequired = false;
5180  foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5181  {
5182  bool hasDrawerWithNoIncomingCables = false;
5183  bool hasDrawerWithNoIncomingDataCables = false;
5184 
5185  if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5186  {
5187  VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5188  if (inputDrawer)
5189  {
5190  hasDrawerWithNoIncomingCables = true;
5191  hasDrawerWithNoIncomingDataCables = true;
5192  vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5193  foreach (VuoRendererPort *childPort, childPorts)
5194  {
5195  if (childPort->getBase()->getConnectedCables(true).size() > 0)
5196  hasDrawerWithNoIncomingCables = false;
5197  if (childPort->effectivelyHasConnectedDataCable(true))
5198  hasDrawerWithNoIncomingDataCables = false;
5199  }
5200  }
5201 
5202  string typeKey = inputPort->getDataType()->getModuleKey();
5203  if (!hasDrawerWithNoIncomingDataCables)
5204  requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5205  }
5206 
5207  bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5208  if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5209  inputEventSourceRequired = true;
5210  }
5211 
5212  // Inventory required output port types (connected data outputs) in the node to be replaced.
5213  map<string, int> requiredOutputs;
5214  bool outputEventSourceRequired = false;
5215  foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5216  {
5217  if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5218  {
5219  string typeKey = outputPort->getDataType()->getModuleKey();
5220  requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5221  }
5222 
5223  if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5224  outputEventSourceRequired = true;
5225  }
5226 
5227  // Inventory available input port types in the candidate replacement node.
5228  bool inputEventSourceAvailable = false;
5229  map<string, int> availableInputs;
5230  foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5231  {
5232  VuoType *dataType = (inputPortClass->hasCompiler()?
5233  static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5234  if (dataType)
5235  {
5236  string typeKey = dataType->getModuleKey();
5237  availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5238  }
5239  }
5240 
5242  inputEventSourceAvailable = true;
5243 
5244  // Inventory available output port types in the candidate replacement node.
5245  bool outputEventSourceAvailable = false;
5246  map<string, int> availableOutputs;
5247  foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5248  {
5249  VuoType *dataType = (outputPortClass->hasCompiler()?
5250  static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5251  if (dataType)
5252  {
5253  string typeKey = dataType->getModuleKey();
5254  availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5255  }
5256  }
5257 
5259  outputEventSourceAvailable = true;
5260 
5261  // Check whether the candidate replacement node meets input data requirements.
5262  for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5263  {
5264  string typeKey = it->first;
5265  int typeRequiredCount = it->second;
5266  if (availableInputs[typeKey] < typeRequiredCount)
5267  return false;
5268  }
5269 
5270  // Check whether the candidate replacement node meets output data requirements.
5271  for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5272  {
5273  string typeKey = it->first;
5274  int typeRequiredCount = it->second;
5275  if (availableOutputs[typeKey] < typeRequiredCount)
5276  return false;
5277  }
5278 
5279  if (inputEventSourceRequired && !inputEventSourceAvailable)
5280  return false;
5281 
5282  if (outputEventSourceRequired && !outputEventSourceAvailable)
5283  return false;
5284 
5285  return true;
5286 }
5287 
5292 bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5293 {
5294  // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5296  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5297 
5298  if (!specializedNodeClass)
5299  return false;
5300 
5301  VuoPortClass *portClass = port->getBase()->getClass();
5302  VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5303  if (!originalGenericType)
5304  return false;
5305 
5306  // If this port belongs to an attachment connected to a port that is not revertible, then
5307  // this port cannot be reverted, either.
5308  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(port->getUnderlyingParentNode());
5309  if (attachment)
5310  {
5311  VuoPort *hostPort = attachment->getUnderlyingHostPort();
5312  if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5313  return false;
5314  }
5315 
5316  return true;
5317 }
5318 
5337 VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5338 {
5339  string publishedPortName = ((! name.empty())?
5340  name :
5342  bool isPublishedInput = port->getRenderer()->getInput();
5343  VuoType *portType = port->getRenderer()->getDataType();
5344  VuoPublishedPort *publishedPort = NULL;
5345 
5346  // If merging is enabled:
5347  // Check whether this composition has a pre-existing externally visible published port
5348  // that has the requested name and type and that can accommodate the newly published internal port.
5349  // If so, add this internal port as a connected port for the existing alias.
5350  bool performedMerge = false;
5351  if (attemptMerge)
5352  {
5353  publishedPort = (isPublishedInput ?
5354  getBase()->getPublishedInputPortWithName(publishedPortName) :
5355  getBase()->getPublishedOutputPortWithName(publishedPortName));
5356 
5357  if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5358  {
5359  if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5360  {
5361  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5362  updatePortConstant(static_cast<VuoCompilerInputEventPort *>(port->getCompiler()),
5363  publishedInputPort->getInitialValue(),
5364  false);
5365  }
5366 
5367  performedMerge = true;
5368  }
5369  }
5370 
5371 
5372  // Otherwise, create a new externally visible published port with a unique name derived from
5373  // the specified name, containing the current port as its lone connected internal port.
5374  if (! performedMerge)
5375  {
5376  publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5377  if (isPublishedInput && type)
5378  {
5379  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5380  publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5381  }
5382  }
5383 
5384  addPublishedPort(publishedPort, isPublishedInput);
5385 
5386  VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5387  dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5388  createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5389 
5390  VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5391 
5392  if (! existingPublishedCable)
5393  {
5394  VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5395  addCable(publishedCable);
5396  }
5397 
5398  if (mergePerformed != NULL)
5399  *mergePerformed = performedMerge;
5400 
5401  return rendererPublishedPort;
5402 }
5403 
5408 VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5409 {
5410  VuoCable *publishedCable = NULL;
5411  bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5412 
5413  if (creatingPublishedInputCable)
5414  {
5415  // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5416  VuoPort *fromPort = externalPort;
5417  VuoNode *fromNode = this->publishedInputNode;
5418 
5419  VuoPort *toPort = internalPort;
5420  VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5421 
5422  publishedCable = (new VuoCompilerCable(NULL,
5423  NULL,
5424  toNode->getCompiler(),
5425  (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5426  publishedCable->setFrom(fromNode, fromPort);
5427  }
5428 
5429  else
5430  {
5431  // If creating a published output cable, it will need to have an associated VuoCompilerCable
5432  // even though we don't currently construct a VuoCompilerNode for the published output node.
5433  VuoPort *fromPort = internalPort;
5434  VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5435 
5436  VuoPort *toPort = externalPort;
5437  VuoNode *toNode = this->publishedOutputNode;
5438 
5439  publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5440  (VuoCompilerPort *)(fromPort->getCompiler()),
5441  NULL,
5442  NULL))->getBase();
5443  publishedCable->setTo(toNode, toPort);
5444  }
5445 
5446  if (forceEventOnlyPublication)
5447  publishedCable->getCompiler()->setAlwaysEventOnly(true);
5448 
5449  return publishedCable;
5450 }
5451 
5463 void VuoEditorComposition::addActiveProtocol(VuoProtocol *protocol, bool useUndoStack)
5464 {
5465  vector<VuoPublishedPort *> publishedPortsToAdd;
5466  map<VuoPublishedPort *, string> publishedPortsToRename;
5467 
5468  // Remove the previously active protocol.
5469  VuoProtocol *previousActiveProtocol = this->activeProtocol;
5470  bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5471 
5472  bool portChangesMadeDuringProtocolRemoval = false;
5473  if (removingPreviousProtocol)
5474  portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5475 
5476  if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5477  {
5478  VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5479  useUndoStack = true;
5480  }
5481 
5482  // Add the newly active protocol.
5483  this->activeProtocol = protocol;
5484  if (!protocol)
5485  return;
5486 
5487  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5488  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5489  {
5490  string portName = i->first;
5491  string portType = i->second;
5492 
5493  bool compositionHadCompatiblePort = false;
5494  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5495  if (preexistingPublishedPort)
5496  {
5497  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5498 
5499  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5500  (!preexistingType && (portType == "")));
5501  if (portTypesMatch)
5502  {
5503  compositionHadCompatiblePort = true;
5504  preexistingPublishedPort->setProtocolPort(true);
5505  }
5506  else
5507  {
5508  compositionHadCompatiblePort = false;
5509  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5510  }
5511  }
5512 
5513  if (!compositionHadCompatiblePort)
5514  {
5515  VuoType *type = compiler->getType(portType)->getBase();
5516  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5517  publishedPort->setProtocolPort(true);
5518 
5519  if (!useUndoStack)
5520  addPublishedPort(publishedPort, true);
5521  else
5522  publishedPortsToAdd.push_back(publishedPort);
5523 
5524  createRendererForPublishedPortInComposition(publishedPort, true);
5525  }
5526  }
5527 
5528  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5529  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5530  {
5531  string portName = i->first;
5532  string portType = i->second;
5533 
5534  bool compositionHadCompatiblePort = false;
5535  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5536  if (preexistingPublishedPort)
5537  {
5538  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5539  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5540  (!preexistingType && (portType == "")));
5541  if (portTypesMatch)
5542  {
5543  compositionHadCompatiblePort = true;
5544  preexistingPublishedPort->setProtocolPort(true);
5545  }
5546  else
5547  {
5548  compositionHadCompatiblePort = false;
5549  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5550  }
5551  }
5552 
5553  if (!compositionHadCompatiblePort)
5554  {
5555  VuoType *type = compiler->getType(portType)->getBase();
5556  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5557  publishedPort->setProtocolPort(true);
5558 
5559  if (!useUndoStack)
5560  addPublishedPort(publishedPort, false);
5561  else
5562  publishedPortsToAdd.push_back(publishedPort);
5563 
5564  createRendererForPublishedPortInComposition(publishedPort, false);
5565  }
5566  }
5567 
5568  if (useUndoStack)
5569  {
5570  bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5571  if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5572  {
5573  set<VuoPublishedPort *> publishedPortsToRemove;
5574  bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5575  bool endUndoStackMacro = true;
5576  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5577  }
5578  }
5579 
5580  emit activeProtocolChanged();
5581 }
5582 
5590 string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5591 {
5592  string modifiedPortName = portName;
5593  if (modifiedPortName.length() > 0)
5594  modifiedPortName[0] = toupper(modifiedPortName[0]);
5595  modifiedPortName = "some" + modifiedPortName;
5596 
5597  return modifiedPortName;
5598 }
5599 
5618 {
5620 
5621  set<VuoPublishedPort *> publishedPortsToRemove;
5622  map<VuoPublishedPort *, string> publishedPortsToRename;
5623 
5624  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5625  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5626  {
5627  string portName = i->first;
5628  string portType = i->second;
5629 
5630  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5631  if (preexistingPublishedPort)
5632  {
5633  bool portCompatibleAcrossProtocolTransition = false;
5634  if (replacementProtocol)
5635  {
5636  vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5637  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5638  {
5639  string replacementPortName = i->first;
5640  string replacementPortType = i->second;
5641 
5642  if ((portName == replacementPortName) && (portType == replacementPortType))
5643  portCompatibleAcrossProtocolTransition = true;
5644  }
5645  }
5646 
5647  if (preexistingPublishedPort->getConnectedCables(true).empty())
5648  publishedPortsToRemove.insert(preexistingPublishedPort);
5649  else if (!portCompatibleAcrossProtocolTransition)
5650  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5651  }
5652  }
5653 
5654  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5655  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5656  {
5657  string portName = i->first;
5658  string portType = i->second;
5659 
5660  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5661  if (preexistingPublishedPort)
5662  {
5663  bool portCompatibleAcrossProtocolTransition = false;
5664  if (replacementProtocol)
5665  {
5666  vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5667  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5668  {
5669  string replacementPortName = i->first;
5670  string replacementPortType = i->second;
5671 
5672  if ((portName == replacementPortName) && (portType == replacementPortType))
5673  portCompatibleAcrossProtocolTransition = true;
5674  }
5675  }
5676 
5677  if (preexistingPublishedPort->getConnectedCables(true).empty())
5678  publishedPortsToRemove.insert(preexistingPublishedPort);
5679  else if (!portCompatibleAcrossProtocolTransition)
5680  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5681  }
5682  }
5683 
5684  // If we are removing any ports, the composition will no longer be deemed to adhere to the
5685  // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5686  if (!publishedPortsToRemove.empty())
5687  publishedPortsToRename.clear();
5688 
5689  bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5690  if (portChangesRequired)
5691  {
5692  vector<VuoPublishedPort *> publishedPortsToAdd;
5693  bool beginUndoStackMacro = true;
5694  bool endUndoStackMacro = !replacementProtocol;
5695  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5696  }
5697 
5698  emit activeProtocolChanged();
5699 
5700  return portChangesRequired;
5701 }
5702 
5710 {
5711  if ((activeProtocol != protocol) || !activeProtocol)
5712  return;
5713 
5714  activeProtocol = NULL;
5715 
5716  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5717  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5718  {
5719  string portName = i->first;
5720  string portType = i->second;
5721 
5722  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5723  if (preexistingPublishedPort)
5724  preexistingPublishedPort->setProtocolPort(false);
5725  }
5726 
5727  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5728  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5729  {
5730  string portName = i->first;
5731  string portType = i->second;
5732 
5733  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5734  if (preexistingPublishedPort)
5735  preexistingPublishedPort->setProtocolPort(false);
5736  }
5737 
5738  emit activeProtocolChanged();
5739 }
5740 
5746 {
5747  return activeProtocol;
5748 }
5749 
5755 {
5756  if (!activeProtocol)
5757  return NULL;
5758 
5759  return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5760 }
5761 
5765 void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5766 {
5767  VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5768 
5769  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
5770  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
5771  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
5772 
5773  if (shouldUpdateUi)
5774  emit publishedPortModified();
5775 }
5776 
5783 int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5784 {
5785  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5786  if (shouldUpdateUi && publishedPort->isProtocolPort())
5788 
5789  int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5790 
5791  if (shouldUpdateUi)
5792  emit publishedPortModified();
5793 
5794  return removalResult;
5795 }
5796 
5802 {
5803  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5804  if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5806 
5807  VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5808 
5809  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort->getBase());
5810  portWithStaticIdentifier[staticPortIdentifier] = publishedPort->getBase();
5811  staticIdentifierForPort[publishedPort->getBase()] = staticPortIdentifier;
5812 
5813  emit publishedPortModified();
5814 }
5815 
5823 void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5824 {
5825  bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5826  VuoRendererPort *fixedPort = NULL;
5827 
5828  if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5829  fixedPort = cable->getFromPort()->getRenderer();
5830 
5831  else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5832  fixedPort = cable->getToPort()->getRenderer();
5833 
5834  if (fixedPort)
5835  {
5836  highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5837  emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5838  }
5839 }
5840 
5846 void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5847 {
5848  QList<QGraphicsItem *> compositionComponents = items();
5849  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5850  {
5851  QGraphicsItem *compositionComponent = *i;
5852  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5853  if (rn)
5854  {
5855  // Check for eligible internal input ports.
5856  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5857  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
5858  updateEligibilityHighlightingForPort((*inputPort)->getRenderer(), port, !cable->effectivelyCarriesData());
5859 
5860  // Check for eligible internal output ports.
5861  vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
5862  for(vector<VuoPort *>::iterator outputPort = outputPorts.begin(); outputPort != outputPorts.end(); ++outputPort)
5863  updateEligibilityHighlightingForPort((*outputPort)->getRenderer(), port, !cable->effectivelyCarriesData());
5864  }
5865 
5866  // Fade out cables that aren't relevant to the current cable drag.
5867  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5868  if (rc && rc != cable)
5869  {
5870  QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
5871  rc->setCacheMode(QGraphicsItem::NoCache);
5872  rc->updateGeometry();
5873 
5874  VuoPort *otherCablePort = port->getInput()
5875  ? rc->getBase()->getFromPort()
5876  : rc->getBase()->getToPort();
5877 
5878  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(otherCablePort? otherCablePort->getRenderer() : NULL,
5879  port,
5880  !cable->effectivelyCarriesData());
5881 
5882  // Don't apply extra highlighting to compatible, already-connected cables.
5883  if (highlight == VuoRendererColors::standardHighlight)
5884  highlight = VuoRendererColors::noHighlight;
5885 
5886  rc->setEligibilityHighlight(highlight);
5887 
5888  rc->setCacheMode(normalCacheMode);
5889  }
5890  }
5891 
5892  // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
5893  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5894  updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(*i));
5895 }
5896 
5901 void VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
5902  VuoRendererPort *fixedPort,
5903  bool eventOnlyConnection)
5904 {
5905  QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
5906  portToHighlight->setCacheMode(QGraphicsItem::NoCache);
5907 
5908  portToHighlight->updateGeometry();
5909 
5910  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection);
5911 
5912  portToHighlight->setEligibilityHighlight(highlight);
5913  VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
5914  if (typecastPortToHighlight)
5915  typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
5916 
5917  portToHighlight->setCacheMode(normalCacheMode);
5918 
5919  if (typecastPortToHighlight)
5920  updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection);
5921 }
5922 
5933 {
5934  // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
5935  VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
5936  VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
5937 
5938  VuoRendererPort *fromPort;
5939  VuoRendererPort *toPort;
5940  bool forwardConnection;
5941  if (fixedPort->getOutput())
5942  {
5943  fromPort = fixedPort;
5944  toPort = portToHighlight;
5945  forwardConnection = true;
5946  }
5947  else
5948  {
5949  fromPort = portToHighlight;
5950  toPort = fixedPort;
5951  forwardConnection = false;
5952  }
5953 
5954  bool directConnectionPossible;
5955 
5956  // Temporarily disallow direct cable connections between published inputs and published outputs.
5957  // @todo: Allow for https://b33p.net/kosada/node/7756 .
5958  if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
5959  directConnectionPossible = false;
5960  else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
5961  directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
5962  else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
5963  directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
5964  else // both ports are internal canvas ports
5965  directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
5966 
5968  if (directConnectionPossible)
5970  else if (!findBridgingSolutions(fromPort, toPort, forwardConnection).empty())
5972  else if (fixedPort == portToHighlight)
5973  highlight = VuoRendererColors::noHighlight;
5974  else
5976 
5977  return highlight;
5978 }
5979 
5995 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
5996  VuoRendererPort *toPort,
5997  bool eventOnlyConnection,
5998  bool forwardConnection)
5999 {
6000  VuoRendererPort *portToRespecialize = NULL;
6001  string respecializedTypeName = "";
6002 
6003  return canConnectDirectlyWithRespecializationNondestructively(fromPort,
6004  toPort,
6005  eventOnlyConnection,
6006  forwardConnection,
6007  &portToRespecialize,
6008  respecializedTypeName);
6009 }
6010 
6021 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6022  VuoRendererPort *toPort,
6023  bool eventOnlyConnection,
6024  bool forwardConnection,
6025  VuoRendererPort **portToRespecialize,
6026  string &respecializedTypeName)
6027 {
6028  *portToRespecialize = NULL;
6029  respecializedTypeName = "";
6030 
6031  bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6032  toPort,
6033  eventOnlyConnection,
6034  forwardConnection,
6035  portToRespecialize,
6036  respecializedTypeName);
6037  if (!canConnectWithRespecialization)
6038  return false;
6039 
6040  if (canConnectWithRespecialization && !portToRespecialize)
6041  return true;
6042 
6043  bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6044  if (!nondestructive)
6045  {
6046  *portToRespecialize = NULL;
6047  respecializedTypeName = "";
6048  }
6049  return nondestructive;
6050 }
6051 
6057 bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6058 {
6059  map<VuoNode *, string> nodesToReplace;
6060  set<VuoCable *> cablesToDelete;
6061  createReplacementsToUnspecializePort(portToUnspecialize, nodesToReplace, cablesToDelete);
6062 
6063  // Check whether unspecialization would disconnect any existing cables
6064  // (other than the cable that would normally be displaced by the new cable connection).
6065  if (cablesToDelete.empty())
6066  return true;
6067 
6068  else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6069  return true;
6070 
6071  return false;
6072 }
6073 
6093 bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6094  VuoRendererPort *toPort,
6095  bool eventOnlyConnection,
6096  bool forwardConnection,
6097  VuoRendererPort **portToRespecialize,
6098  string &respecializedTypeName)
6099 {
6100  // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6101 
6102  *portToRespecialize = NULL;
6103  respecializedTypeName = "";
6104 
6105  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6106  if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6107  return true;
6108 
6109  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6110  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6111  return true;
6112 
6113  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6114  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6115 
6116  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6117  return false;
6118 
6119  VuoType *currentFromDataType = fromPort->getDataType();
6120  VuoType *currentToDataType = toPort->getDataType();
6121 
6122  if (!(currentFromDataType && currentToDataType))
6123  return false;
6124 
6126  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6127  return false;
6128 
6129  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6130  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6131 
6132  VuoGenericType *originalFromGenericType = NULL;
6133  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6134  {
6136  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6137  if (fromSpecializedNodeClass)
6138  {
6139  VuoPortClass *portClass = fromPort->getBase()->getClass();
6140  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6141  }
6142  }
6143 
6144  VuoGenericType *originalToGenericType = NULL;
6145  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6146  {
6148  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6149  if (toSpecializedNodeClass)
6150  {
6151  VuoPortClass *portClass = toPort->getBase()->getClass();
6152  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6153  }
6154  }
6155 
6156  // Determine whether the port at each endpoint is 1) generic, or
6157  // 2) specialized and currently revertible, or 3) effectively static.
6158  bool fromPortIsGeneric = currentFromGenericType;
6159  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6160  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6161 
6162  bool toPortIsGeneric = currentToGenericType;
6163  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6164  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6165 
6166  // Figure out which port to try to respecialize, and to what type.
6167  set<string> compatibleTypes;
6168  string specializedType = "";
6169  VuoRendererPort *portToTryToRespecialize = NULL;
6170 
6171  // Case: One port static, one port specialized.
6172  if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6173  {
6174  VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6175  specializedType = staticPort->getDataType()->getModuleKey();
6176  portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6177  }
6178 
6179  // Case: One port specialized, other port generic or specialized.
6180  else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6181  {
6182  VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6183  bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6184 
6185  VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6186  bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6187 
6188  // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6189  /*
6190  if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6191  {
6192  specializedType = dragDestination->getDataType()->getModuleKey();
6193  portToTryToRespecialize = dragSource;
6194  }
6195  else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6196  {
6197  specializedType = dragSource->getDataType()->getModuleKey();
6198  portToTryToRespecialize = dragDestination;
6199  }
6200  else
6201  */
6202 
6203  if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6204  {
6205  specializedType = dragSource->getDataType()->getModuleKey();
6206  portToTryToRespecialize = dragDestination;
6207  }
6208  }
6209 
6210  // @todo https://b33p.net/kosada/node/10481 Other cases.
6211  else
6212  return false;
6213 
6214  if (portToTryToRespecialize)
6215  compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6216 
6217  bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6218 
6219  if (portsAreCompatible)
6220  {
6221  *portToRespecialize = portToTryToRespecialize;
6222  respecializedTypeName = specializedType;
6223  }
6224 
6225  return portsAreCompatible;
6226 }
6227 
6234 void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6235 {
6236  VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6237  if (drawer)
6238  {
6240  foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6242  bestEligibility = VuoRendererColors::standardHighlight;
6244  && bestEligibility != VuoRendererColors::standardHighlight)
6245  bestEligibility = VuoRendererColors::subtleHighlight;
6246 
6247  // If this drawer has no eligible ports, fade it out.
6248  {
6249  QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6250  drawer->setCacheMode(QGraphicsItem::NoCache);
6251  drawer->updateGeometry();
6252 
6253  drawer->setEligibilityHighlight(bestEligibility);
6254 
6255  drawer->setCacheMode(normalCacheMode);
6256  }
6257 
6258  // If this drawer does have eligible ports, ensure its host port isn't faded out, so the port name is legible.
6259  if (drawer->getRenderedHostPort()
6260  && drawer->getRenderedHostPort()->getRenderer())
6261  {
6262  VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6263 
6264  QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6265  hostPort->setCacheMode(QGraphicsItem::NoCache);
6266  hostPort->updateGeometry();
6267 
6268  hostPort->setEligibilityHighlight(MIN(bestEligibility, hostPort->eligibilityHighlight()));
6269 
6270  hostPort->setCacheMode(normalCacheMode);
6271  }
6272  }
6273 }
6274 
6279 {
6282 }
6283 
6303  VuoRendererPort *toPort,
6304  bool toPortIsDragDestination,
6305  VuoRendererPort **portToSpecialize,
6306  string &specializedTypeName,
6307  string &typecastToInsert)
6308 {
6309  *portToSpecialize = NULL;
6310  specializedTypeName = "";
6311 
6312  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6313  map<string, string> specializedTypeNameForTypecast;
6314 
6315  vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6316  bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6317 
6318  if (!solutionSelected)
6319  return false;
6320 
6321  if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6322  *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6323  if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6324  specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6325 
6326  return true;
6327 }
6328 
6347 bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6348  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6349  map<string, string> specializedTypeNameForTypecast,
6350  string &selectedTypecast)
6351 {
6352  if (suitableTypecasts.empty())
6353  {
6354  selectedTypecast = "";
6355  return false;
6356  }
6357 
6358  else if (suitableTypecasts.size() == 1)
6359  {
6360  selectedTypecast = suitableTypecasts[0];
6361  return true;
6362  }
6363 
6364  else
6365  return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6366 }
6367 
6373 bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort, VuoType *candidateFromType, VuoType *candidateToType)
6374 {
6375  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6376  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6377 
6378  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput &&
6379  fromPort->getBase()->getClass()->hasCompiler() &&
6380  toPort->getBase()->getClass()->hasCompiler()))
6381  {
6382  return false;
6383  }
6384 
6385  VuoType *inType = (candidateFromType? candidateFromType : static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6386  VuoType *outType = (candidateToType? candidateToType : static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6387 
6388  // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6389  if (inType && (inType->getModuleKey() == "VuoBoolean") && outType && (outType->getModuleKey() == "VuoInteger"))
6390  {
6391  bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6396  );
6397 
6398  if (toNodeUsesIndex)
6399  return false;
6400  }
6401 
6402  return true;
6403 }
6404 
6423  VuoRendererPort *toPort,
6424  bool toPortIsDragDestination)
6425 {
6426  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6427  map<string, string> specializedTypeNameForTypecast;
6428  return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6429 }
6430 
6441  VuoRendererPort *toPort,
6442  bool toPortIsDragDestination,
6443  map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6444  map<string, string> &specializedTypeNameForTypecast)
6445 {
6446  portToSpecializeForTypecast.clear();
6447  specializedTypeNameForTypecast.clear();
6448  vector<string> suitableTypecasts;
6449 
6450  // Temporarily disallow direct cable connections between published inputs and published outputs.
6451  // @todo: Allow for https://b33p.net/kosada/node/7756 .
6452  if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6453  return suitableTypecasts;
6454 
6455  // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6456  {
6457  VuoRendererPort *portToSpecialize = NULL;
6458  string specializedTypeName = "";
6459  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6460  {
6461  suitableTypecasts.push_back("");
6462  portToSpecializeForTypecast[""] = portToSpecialize;
6463  specializedTypeNameForTypecast[""] = specializedTypeName;
6464 
6465  return suitableTypecasts;
6466  }
6467  }
6468 
6469  VuoType *currentFromDataType = fromPort->getDataType();
6470  VuoType *currentToDataType = toPort->getDataType();
6471 
6472  if (!(currentFromDataType && currentToDataType))
6473  return suitableTypecasts;
6474 
6476  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6477  return suitableTypecasts;
6478 
6479  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6480  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6481 
6482  VuoGenericType *originalFromGenericType = NULL;
6483  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6484  {
6486  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6487  if (fromSpecializedNodeClass)
6488  {
6489  VuoPortClass *portClass = fromPort->getBase()->getClass();
6490  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6491  }
6492  }
6493 
6494  VuoGenericType *originalToGenericType = NULL;
6495  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6496  {
6498  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6499  if (toSpecializedNodeClass)
6500  {
6501  VuoPortClass *portClass = toPort->getBase()->getClass();
6502  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6503  }
6504  }
6505 
6506  // Determine whether the port at each endpoint is:
6507  // 1) generic (unspecialized), or
6508  // 2) specialized and currently revertible, or
6509  // 3) effectively static.
6510  bool fromPortIsGeneric = currentFromGenericType;
6511  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6512  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6513 
6514  bool toPortIsGeneric = currentToGenericType;
6515  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6516  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6517 
6518  // No typeconversion or specialization options between two unspecialized generic ports.
6519  if (fromPortIsGeneric && toPortIsGeneric)
6520  return suitableTypecasts;
6521 
6522  // Typeconversion options but no specialization options between two static ports.
6523  else if (fromPortIsStatic && toPortIsStatic)
6524  {
6525  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6526  suitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6527  return suitableTypecasts;
6528  }
6529 
6530  // Remaining combinations might require (re-)specializing one port or the other.
6531  // Figure out which port to consider (re-)specializing.
6532  bool specializeToPort = true;
6533  if (toPortIsGeneric)
6534  specializeToPort = true;
6535  else if (fromPortIsGeneric)
6536  specializeToPort = false;
6537  else if (fromPortIsSpecialized && toPortIsStatic)
6538  specializeToPort = false;
6539  else if (fromPortIsStatic && toPortIsSpecialized)
6540  specializeToPort = true;
6541  else if (fromPortIsSpecialized && toPortIsSpecialized)
6542  specializeToPort = toPortIsDragDestination;
6543 
6544  // Now that ports have been categorized, figure out what combinations of (re-)specialization
6545  // and/or typeconversion we can use to bridge them.
6546  set<string> compatibleTypes;
6547  if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6548  compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6549  else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6550  compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6551 
6552  // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6553  // non-destructively unspecializable, since it already has the appropriate specialization.
6554  compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6555 
6556  foreach (string compatibleTypeName, compatibleTypes)
6557  {
6558  VuoCompilerType *compatibleSpecializedType = compiler->getType(compatibleTypeName);
6559  VuoType *candidateFromType = specializeToPort? currentFromDataType : compatibleSpecializedType->getBase();
6560  VuoType *candidateToType = specializeToPort? compatibleSpecializedType->getBase() : currentToDataType;
6561 
6562  if (compatibleSpecializedType && portsPassSanityCheckToTypeconvert(fromPort,
6563  toPort,
6564  candidateFromType,
6565  candidateToType))
6566  {
6567  // Re-specialization without typeconversion may be possible.
6568  if (candidateFromType == candidateToType)
6569  {
6570  suitableTypecasts.push_back("");
6571  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6572  specializedTypeNameForTypecast[""] = compatibleSpecializedType->getBase()->getModuleKey();
6573  }
6574 
6575  vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromType, candidateToType);
6576  foreach (string typecast, suitableTypecastsForCurrentTypes)
6577  {
6578  suitableTypecasts.push_back(typecast);
6579  portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6580  specializedTypeNameForTypecast[typecast] = compatibleSpecializedType->getBase()->getModuleKey();
6581  }
6582  }
6583  }
6584 
6585  return suitableTypecasts;
6586 }
6587 
6600 bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6601  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6602  map<string, string> specializedTypeNameForTypecast,
6603  string &selectedTypecast)
6604 {
6605  QMenu typecastMenu(views()[0]->viewport());
6606  typecastMenu.setSeparatorsCollapsible(false);
6607  QString spacer(" ");
6608 
6609  // Inventory specialization options
6610  set <pair<VuoRendererPort *, string> > specializationDetails;
6611  vector<string> typeconversionOptionsRequiringNoSpecialization;
6612  foreach (string typecastClassName, suitableTypecasts)
6613  {
6614  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6615  string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6616  specializationDetails.insert(std::make_pair(portToSpecialize,
6617  specializedTypeName));
6618 
6619  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6620  if (portAlreadyHasTargetType)
6621  typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6622  }
6623 
6624  // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6625  // specialization heading under which multiple typeconversion options may be listed.
6626  // Selecting this item itself will invoke the specialization.
6627  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6628  {
6629  QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6630  QAction *typecastAction = typecastMenu.addAction(menuText);
6631  typecastAction->setData(QVariant(""));
6632  }
6633 
6634  // If there are bridging options that require no specialization, list them next.
6635  bool includingTypeconvertWithNoSpecializationHeader = false;
6636  if (typeconversionOptionsRequiringNoSpecialization.size() >= 1)
6637  {
6638  if (typecastMenu.actions().size() >= 1)
6639  typecastMenu.addSeparator();
6640 
6641  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6642  string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6643 
6644  if (portToSpecialize && !specializedTypeName.empty())
6645  {
6646  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6647  QAction *typecastAction = typecastMenu.addAction(menuText);
6648  typecastAction->setEnabled(false);
6649  includingTypeconvertWithNoSpecializationHeader = true;
6650  }
6651  }
6652 
6653  foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6654  {
6655  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6656  if (typecastClass)
6657  {
6658  QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6659  typecastAction->setData(QVariant(typecastClassName.c_str()));
6660  }
6661  }
6662 
6663  // Now list the remaining bridging options.
6664  for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6665  {
6666  VuoRendererPort *portToSpecialize = i->first;
6667  string specializedTypeName = i->second;
6668 
6669  // We've already listed the no-typeconversion bridging option, so skip it here.
6670  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6671  (portToSpecializeForTypecast[""] == portToSpecialize) &&
6672  (specializedTypeNameForTypecast[""] == specializedTypeName))
6673  {
6674  continue;
6675  }
6676 
6677  // We've already listed the no-specialization bridging option, so skip it here.
6678  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6679  if (portAlreadyHasTargetType)
6680  {
6681  continue;
6682  }
6683 
6684  if (typecastMenu.actions().size() >= 1)
6685  typecastMenu.addSeparator();
6686 
6687  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6688  QAction *typecastAction = typecastMenu.addAction(menuText);
6689  typecastAction->setEnabled(false);
6690 
6691  // Inventory typeconversion options associated with this specialization option.
6692  foreach (string typecastClassName, suitableTypecasts)
6693  {
6694  if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6695  (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6696  {
6697  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6698  if (typecastClass)
6699  {
6700  QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6701  typecastAction->setData(QVariant(typecastClassName.c_str()));
6702  }
6703  }
6704  }
6705  }
6706 
6707  menuSelectionInProgress = true;
6708  QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6709  menuSelectionInProgress = false;
6710 
6711  selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6712  return selectedTypecastAction;
6713 }
6714 
6718 QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6719 {
6720  if (!portToSpecialize || specializedTypeName.empty())
6721  return "";
6722 
6723  bool isInput = portToSpecialize && portToSpecialize->getInput();
6724  QString typeDisplayName = compiler->getType(specializedTypeName)?
6725  formatTypeNameForDisplay(compiler->getType(specializedTypeName)->getBase()) :
6726  specializedTypeName.c_str();
6727 
6728  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6729 
6730  QString displayText;
6731  if (portAlreadyHasTargetType)
6732  {
6733  if (isInput)
6734  {
6735  //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6736  displayText = tr("Keep Input Port as %1");
6737  }
6738  else
6739  {
6740  //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6741  displayText = tr("Keep Output Port as %1");
6742  }
6743  }
6744  else
6745  {
6746  if (isInput)
6747  {
6748  //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6749  displayText = tr("Change Input Port to %1");
6750  }
6751  else
6752  {
6753  //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6754  displayText = tr("Change Output Port to %1");
6755  }
6756  }
6757 
6758  return displayText.arg(typeDisplayName);
6759 }
6760 
6766 {
6767  __block json_object *portValue = NULL;
6768 
6769  if (! port->getRenderer()->getDataType())
6770  return portValue;
6771 
6772  string runningPortIdentifier = staticIdentifierForPort[port];
6773  bool isInput = port->getRenderer()->getInput();
6774 
6775  void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6776  {
6777  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6778  if (topLevelComposition->isRunningThreadUnsafe())
6779  {
6780  portValue = isInput ?
6781  topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6782  topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6783  }
6784  });
6785  };
6786  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6787 
6788  return portValue;
6789 }
6790 
6794 string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6795 {
6796  return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6797 }
6798 
6805 {
6806  if (!staticPort)
6807  return "";
6808 
6809  // Published ports
6810  if (dynamic_cast<VuoPublishedPort *>(staticPort))
6811  return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
6812 
6813  // Internal ports
6814  // We might as well use the same naming scheme here as is used in the running composition,
6815  // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
6816  // node identifier has been explicitly set.
6817  string nodeIdentifier = "";
6818  if (parentNode && parentNode->hasCompiler())
6819  nodeIdentifier = parentNode->getCompiler()->getIdentifier();
6820  else if (staticPort->hasRenderer() &&
6821  staticPort->getRenderer()->getUnderlyingParentNode() &&
6822  staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->hasCompiler())
6823  {
6824  nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
6825  }
6826 
6827  if (staticPort->hasCompiler() && !nodeIdentifier.empty())
6828  {
6829  dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
6830  return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
6831  }
6832  else
6833  return "";
6834 }
6835 
6840 {
6841  return portWithStaticIdentifier[portID];
6842 }
6843 
6850 {
6851  if (port->hasRenderer())
6852  {
6853  if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
6854  {
6855  bool isPublishedInput = !port->getRenderer()->getInput();
6856  return (isPublishedInput? composition->getPublishedInputNode() :
6857  composition->getPublishedOutputNode());
6858  }
6859 
6860  else
6861  return port->getRenderer()->getUnderlyingParentNode()->getBase();
6862  }
6863 
6864  foreach (VuoNode *n, composition->getBase()->getNodes())
6865  {
6866  VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
6867  if (candidateInputPort == port)
6868  return n;
6869 
6870  VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
6871  if (candidateOutputPort == port)
6872  return n;
6873  }
6874 
6875  return NULL;
6876 }
6877 
6886 {
6887  map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
6888  if (popover != activePortPopovers.end())
6889  return popover->second;
6890 
6891  else
6892  return NULL;
6893 }
6894 
6902 void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
6903 {
6904  string portID = staticIdentifierForPort[rp->getBase()];
6905  bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
6906  if (!popoverJustClosedAtLastEvent)
6908 }
6909 
6914 {
6915  if (!popoverEventsEnabled)
6916  return;
6917 
6918  VuoPort *port = rp->getBase();
6919  string portID = staticIdentifierForPort[port];
6920 
6921  VUserLog("%s: Open popover for %s",
6922  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6923  portID.c_str());
6924 
6925  dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
6926 
6927  dispatch_sync(activePortPopoversQueue, ^{
6928 
6929  if (activePortPopovers.find(portID) == activePortPopovers.end())
6930  {
6931  // Assigning the popover a parent widget allows us to give it rounded corners
6932  // and a background fill that respects its rounded boundaries.
6933  VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
6934 
6935  connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
6936  connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
6937  VUserLog("%s: Detach popover for %s",
6938  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6939  portID.c_str());
6940  popoverDetached();
6941  });
6942  connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
6945 
6946  // Line up the top left of the dialog with the port.
6947  QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
6948 
6949  // Don't let popovers get cut off at the right or bottom edges of the canvas.
6950  const int cutoffMargin = 16;
6951  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
6952  if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
6953  portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
6954  if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
6955  portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
6956 
6957  QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
6958 
6959  const QPoint offset = QPoint(12, 6);
6960 
6961  QPoint popoverTopLeft = popoverLeftInView + offset;
6962  popover->move(popoverTopLeft);
6963  popover->show();
6964 
6965  activePortPopovers[portID] = popover;
6966 
6967  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
6968  updateDataInPortPopover(portID);
6969  });
6970  }
6971  });
6972  });
6973 }
6974 
6979 void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
6980 {
6981  if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
6983 }
6984 
6993 {
6994  VUserLog("%s: Close popover for %s",
6995  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6996  portID.c_str());
6997 
6998  VuoPortPopover *popover = NULL;
6999  map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
7000  if (i != activePortPopovers.end())
7001  {
7002  popover = i->second;
7003  activePortPopovers.erase(i);
7004  }
7005 
7006  if (popover)
7007  {
7008  popover->hide();
7009  delete popover;
7010  }
7011 
7012  VuoPort *port = portWithStaticIdentifier[portID];
7013  if (!port)
7014  return;
7015 
7016  bool isInput = port->getRenderer()->getInput();
7017 
7018  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7019  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7020  {
7021  dispatch_async(topLevelComposition->runCompositionQueue, ^{
7022  if (topLevelComposition->isRunningThreadUnsafe())
7023  {
7024  (isInput ?
7025  topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7026  topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7027  }
7028  });
7029  });
7030  });
7031 }
7032 
7036 void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7037 {
7038  dispatch_sync(activePortPopoversQueue, ^{
7039  disablePopoverForPort(portID);
7040  });
7041 }
7042 
7047 {
7048  disablePortPopovers();
7050 }
7051 
7056 {
7057  foreach (VuoErrorPopover *errorPopover, errorPopovers)
7058  {
7059  errorPopover->hide();
7060  errorPopover->deleteLater();
7061  }
7062 
7063  errorPopovers.clear();
7064 }
7065 
7070 void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7071 {
7072  dispatch_sync(activePortPopoversQueue, ^{
7073  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7074  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7075  {
7076  string portID = i->first;
7077  VuoPort *port = portWithStaticIdentifier[portID];
7078  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7079  disablePopoverForPort(portID);
7080  }
7081  });
7082 }
7083 
7088 {
7089  dispatch_sync(activePortPopoversQueue, ^{
7090  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7091  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7092  {
7093  string portID = i->first;
7094  VuoPort *port = portWithStaticIdentifier[portID];
7095  if (!port)
7096  disablePopoverForPort(portID);
7097  }
7098  });
7099 }
7100 
7106 {
7107  if (recordWhichPopoversClosed)
7108  portsWithPopoversClosedAtLastEvent.clear();
7109 
7110  dispatch_sync(activePortPopoversQueue, ^{
7111  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7112  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7113  {
7114  string portID = i->first;
7115  VuoPort *port = portWithStaticIdentifier[portID];
7116  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7117  {
7118  VuoPortPopover *popover = getActivePopoverForPort(portID);
7119  if (! (popover && popover->getDetached()))
7120  {
7121  disablePopoverForPort(portID);
7122  portsWithPopoversClosedAtLastEvent.insert(portID);
7123  }
7124  }
7125  }
7126  });
7127 }
7128 
7133 {
7134  moveDetachedPortPopoversBy(dx, dy);
7135  moveErrorPopoversBy(dx, dy);
7136 }
7137 
7141 void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7142 {
7143  foreach(VuoErrorPopover *errorPopover, errorPopovers)
7144  errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7145 }
7146 
7150 void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7151 {
7152  dispatch_sync(activePortPopoversQueue, ^{
7153  map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7154  for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7155  {
7156  VuoPortPopover *popover = i->second;
7157  if (popover && popover->getDetached())
7158  popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7159  }
7160  });
7161 }
7162 
7166 void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7167 {
7168  dispatch_sync(activePortPopoversQueue, ^{
7169  auto portPopovers = activePortPopovers;
7170  for (auto i : portPopovers)
7171  {
7172  VuoPortPopover *popover = i.second;
7173  if (popover && popover->getDetached())
7174  {
7175  id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7176  objc_msgSend(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7177  }
7178  }
7179  });
7180 }
7181 
7187 {
7188  dispatch_sync(activePortPopoversQueue, ^{
7189  for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7190  {
7191  string portID = i->first;
7192  VuoPort *port = portWithStaticIdentifier[portID];
7193  VuoPortPopover *popover = i->second;
7194 
7195  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7196  QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7197  }
7198  });
7199 }
7200 
7213  string popoverCompositionIdentifier,
7214  string portID)
7215 {
7216  VuoPort *port = popoverComposition->portWithStaticIdentifier[portID];
7217  if (! port)
7218  return;
7219 
7220  string portSummary = (port->getRenderer()->getInput() ?
7221  runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7222  runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7223 
7224  dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7225  VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7226  if (popover)
7227  {
7228  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7229  QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7230  }
7231  });
7232 }
7233 
7242 {
7243  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7244  {
7245  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7246  if (topLevelComposition->isRunningThreadUnsafe())
7247  topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7248  });
7249  });
7250 }
7251 
7256 void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7257  bool receivedEvent, bool receivedData, string dataSummary)
7258 {
7259  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7260  {
7261  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7262  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7263 
7264  if (popover)
7265  {
7266  if (receivedEvent && receivedData)
7267  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7268  else if (receivedEvent)
7269  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7270  else if (receivedData)
7271  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7272  }
7273  });
7274  };
7275  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7276 }
7277 
7282 void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7283  bool sentEvent, bool sentData, string dataSummary)
7284 {
7285  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7286  {
7287  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7288  VuoPort *port = matchingComposition->portWithStaticIdentifier[portIdentifier];
7289  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7290 
7291  if (popover)
7292  {
7293  if (sentEvent && sentData)
7294  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7295  else if (sentEvent)
7296  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7297  else if (sentData)
7298  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7299  }
7300 
7301  if (matchingComposition->showEventsMode && sentEvent)
7302  {
7303  if (port && dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7304  {
7305  port->getRenderer()->setFiredEvent();
7306  matchingComposition->animatePort(port->getRenderer());
7307  }
7308  }
7309  });
7310  };
7311  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7312 }
7313 
7318 void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7319 {
7320  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7321  {
7322  dispatch_async(matchingComposition->runCompositionQueue, ^{
7323  if (matchingComposition->isRunningThreadUnsafe())
7324  {
7325  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7326  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7327  if (popover)
7328  QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7329  });
7330  }
7331  });
7332  };
7333  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7334 }
7335 
7340 void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7341 {
7342  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7343  {
7344  if (matchingComposition->showEventsMode)
7345  {
7346  dispatch_async(this->runCompositionQueue, ^{
7347  if (this->isRunningThreadUnsafe())
7348  {
7349  map<string, VuoNode *>::iterator i = matchingComposition->nodeWithGraphvizIdentifier.find(nodeIdentifier);
7350  if (i != nodeWithGraphvizIdentifier.end())
7351  {
7352  VuoNode *nodeInBaseComposition = i->second;
7353  VuoRendererNode *rn = nodeInBaseComposition->getRenderer();
7354  rn->setExecutionBegun();
7355  }
7356  }
7357  });
7358  }
7359  };
7360  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7361 }
7362 
7367 void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7368 {
7369  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7370  {
7371  if (matchingComposition->showEventsMode)
7372  {
7373  dispatch_async(this->runCompositionQueue, ^{
7374  if (this->isRunningThreadUnsafe())
7375  {
7376  map<string, VuoNode *>::iterator i = matchingComposition->nodeWithGraphvizIdentifier.find(nodeIdentifier);
7377  if (i != nodeWithGraphvizIdentifier.end())
7378  {
7379  VuoNode *nodeInBaseComposition = i->second;
7380  VuoRendererNode *rn = nodeInBaseComposition->getRenderer();
7381  rn->setExecutionEnded();
7382  }
7383  }
7384  });
7385  }
7386  };
7387  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7388 }
7389 
7397 {
7398  emit compositionStoppedItself();
7399 }
7400 
7407 {
7409 }
7410 
7415 {
7416  return showEventsMode;
7417 }
7418 
7423 {
7424  this->showEventsMode = showEventsMode;
7425 
7426  if (showEventsMode)
7427  {
7429 
7430  void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7431  {
7432  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7433  if (topLevelComposition->isRunningThreadUnsafe())
7434  topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7435  });
7436  };
7437  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7438  }
7439  else
7440  {
7442 
7443  void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7444  {
7445  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7446  if (topLevelComposition->isRunningThreadUnsafe())
7447  topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7448  });
7449  };
7450  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7451  }
7452 }
7453 
7458 {
7459  foreach (VuoCable *cable, getBase()->getCables())
7460  {
7461  if (cable->getCompiler()->getHidden() && !cable->isPublished())
7462  return true;
7463  }
7464 
7465  return false;
7466 }
7467 
7472 {
7473  foreach (VuoCable *cable, getBase()->getCables())
7474  {
7475  if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7476  return true;
7477  }
7478 
7479  return false;
7480 }
7481 
7486 QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7487 {
7488  VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7489  NULL,
7490  port->getOutput(),
7491  port->getRefreshPort(),
7492  port->getFunctionPort());
7493  animatedPort->setAnimated(true);
7494  animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7495  animatedPort->setParentItem(port->getRenderedParentNode());
7496 
7497  animation->setItem(animatedPort);
7498  animation->setScaleAt(0.0, 1, 1);
7499  animation->setScaleAt(0.999, 3, 3);
7500  animation->setScaleAt(1.0, 1, 1);
7501 
7502  QTimeLine *animationTimeline = animation->timeLine();
7503  animationTimeline->setFrameRange(0, 100);
7504  animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7505  animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7506 
7507  preparedAnimations.insert(animation);
7508  animationForTimeline[animation->timeLine()] = animation;
7509 
7510  connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7511  connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7512 
7513  return animation;
7514 }
7515 
7519 void VuoEditorComposition::animatePort(VuoRendererPort *port)
7520 {
7521  dispatch_async(dispatch_get_main_queue(), ^{
7522  QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7523  if (! animation)
7524  return;
7525 
7526  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7527 
7528  if (animation->timeLine()->state() == QTimeLine::Running)
7529  animation->timeLine()->setCurrentTime(0);
7530 
7531  else
7532  {
7533  animatedPort->setPos(port->pos());
7534  animatedPort->setVisible(true);
7535  animation->timeLine()->start();
7536  }
7537  });
7538 }
7539 
7544 QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7545 {
7546  vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7547 
7548  QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7549  qreal maxPercentAdvanced = -1;
7550 
7551  for (int i = 0; i < animations.size(); ++i)
7552  {
7553  QGraphicsItemAnimation *animation = animations[i];
7554  bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7555  bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7556 
7557  if (! animationPrepared)
7558  return setUpAnimationForPort(animation, port);
7559 
7560  else if (! animationRunning)
7561  return animation;
7562 
7563  // If all of the port's animations are already running, return the
7564  // one that has been running the longest.
7565  qreal percentAdvanced = animation->timeLine()->currentValue();
7566  if (percentAdvanced > maxPercentAdvanced)
7567  {
7568  mostAdvancedAnimation = animation;
7569  maxPercentAdvanced = percentAdvanced;
7570  }
7571  }
7572 
7573  // If no animation is even halfway complete, return NULL to indicate
7574  // that no animation is currently available.
7575  return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7576 }
7577 
7583 void VuoEditorComposition::updatePortAnimation(qreal value)
7584 {
7585  QTimeLine *animationTimeline = (QTimeLine *)sender();
7586  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7587  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7588  const qreal multiplier = 1000.;
7589  animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7590 }
7591 
7596 void VuoEditorComposition::endPortAnimation(void)
7597 {
7598  QTimeLine *animationTimeline = (QTimeLine *)sender();
7599  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7600  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7601  animatedPort->setVisible(false);
7602 }
7603 
7607 void VuoEditorComposition::setDisableDragStickiness(bool disable)
7608 {
7609  this->dragStickinessDisabled = disable;
7610 }
7611 
7616 {
7617  this->ignoreApplicationStateChangeEvents = ignore;
7618 }
7619 
7626 {
7627  this->popoverEventsEnabled = enable;
7628 }
7629 
7635 {
7636  nodeWithGraphvizIdentifier.clear();
7637  portWithStaticIdentifier.clear();
7638  staticIdentifierForPort.clear();
7639 
7640  foreach (VuoNode *node, getBase()->getNodes())
7641  registerNodeID(node);
7642 
7643  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
7644  {
7645  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
7646  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
7647  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
7648  }
7649 
7650  foreach (VuoPort *publishedPort, getBase()->getPublishedOutputPorts())
7651  {
7652  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
7653  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
7654  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
7655  }
7656 }
7657 
7661 void VuoEditorComposition::registerNodeID(VuoNode *node)
7662 {
7663  if (node && node->hasCompiler())
7664  nodeWithGraphvizIdentifier[node->getCompiler()->getGraphvizIdentifier()] = node;
7665 
7666  foreach (VuoPort *port, node->getInputPorts())
7667  {
7668  string staticPortIdentifier = getIdentifierForStaticPort(port, node);
7669  portWithStaticIdentifier[staticPortIdentifier] = port;
7670  staticIdentifierForPort[port] = staticPortIdentifier;
7671  }
7672 
7673  foreach (VuoPort *port, node->getOutputPorts())
7674  {
7675  string staticPortIdentifier = getIdentifierForStaticPort(port, node);
7676  portWithStaticIdentifier[staticPortIdentifier] = port;
7677  staticIdentifierForPort[port] = staticPortIdentifier;
7678  }
7679 }
7680 
7685 {
7686  setRenderActivity(true, includePorts);
7687  refreshComponentAlphaLevelTimer->start();
7688 }
7689 
7694 {
7695  refreshComponentAlphaLevelTimer->stop();
7696  setRenderActivity(false);
7697 }
7698 
7715 bool VuoEditorComposition::validateProtocol(VuoEditorWindow *window, bool isExportingMovie)
7716 {
7717  // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7718  if (!activeProtocol)
7719  {
7720  VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7721  return false;
7722  }
7723 
7724  // Can events from at least one trigger reach at least one published output port? If not, report an error.
7725  if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7726  {
7727  QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7728  QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7729  "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7730 
7731  if (isExportingMovie)
7732  errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7733 
7735  QMessageBox messageBox(window);
7736  messageBox.setWindowFlags(Qt::Sheet);
7737  messageBox.setWindowModality(Qt::WindowModal);
7738  messageBox.setFont(fonts->dialogHeadingFont());
7739  messageBox.setTextFormat(Qt::RichText);
7740 
7741  messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7742  messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7743  messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7744  messageBox.setDefaultButton(QMessageBox::Ok);
7745 
7746  messageBox.setText(errorHeadline);
7747  messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7748 
7749  if (messageBox.exec() == QMessageBox::Help)
7750  {
7751  map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7752  map<QString, QString>::iterator i = examples.begin();
7753  if (i != examples.end())
7754  QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7755  }
7756  return false;
7757  }
7758 
7759  return true;
7760 }
7761 
7766 {
7767  return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7768 }
7769 
7774 {
7776 }
7777 
7781 string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7782 {
7783  string dir, file, ext;
7784  VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7785  return file;
7786 }
7787 
7795 {
7796  string customizedName = getBase()->getMetadata()->getCustomizedName();
7797  if (! customizedName.empty())
7798  return QString::fromStdString(customizedName);
7799 
7800  string name = getBase()->getMetadata()->getName();
7801  return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7802 }
7803 
7811 QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7812 {
7813  // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7814  // If the extensionless filename contains dot-delimited segments, use only the final segment.
7815  vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7816  string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7817  fileNameParts[fileNameParts.size()-2] :
7818  (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7819 
7820  // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7821  if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7822  {
7823  string formattedName = fileNameContentPart;
7824  if (formattedName.size() >= 1)
7825  formattedName[0] = toupper(formattedName[0]);
7826 
7827  return QString(formattedName.c_str());
7828  }
7829 
7830  // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
7831  return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
7832 }
7833 
7840 {
7841  QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
7842  if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
7843  {
7844  // If not an official Vuo nodeset, return the name as-is.
7845  return nodeSetName;
7846  }
7847 
7848  map<QString, QString> wordsToReformat;
7849  wordsToReformat["artnet"] = "Art-Net";
7850  wordsToReformat["bcf2000"] = "BCF2000";
7851  wordsToReformat["hid"] = "HID";
7852  wordsToReformat["midi"] = "MIDI";
7853  wordsToReformat["osc"] = "OSC";
7854  wordsToReformat["rss"] = "RSS";
7855  wordsToReformat["ui"] = "UI";
7856  wordsToReformat["url"] = "URL";
7857 
7858  QString nodeSetDisplayName = "";
7859  for (int i = 1; i < wordsInName.size(); ++i)
7860  {
7861  QString currentWord = wordsInName[i];
7862  if (currentWord.size() >= 1)
7863  {
7864  if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
7865  currentWord = wordsToReformat.at(currentWord.toLower());
7866  else
7867  currentWord[0] = currentWord[0].toUpper();
7868 
7869  nodeSetDisplayName += currentWord;
7870 
7871  if (i < wordsInName.size()-1)
7872  nodeSetDisplayName += " ";
7873  }
7874  }
7875  return nodeSetDisplayName;
7876 }
7877 
7884 {
7885  if (!type)
7886  return "(none)";
7887 
7888  string formattedTypeName = "";
7889  if (type->hasCompiler() && VuoCompilerType::isListType(type->getCompiler()))
7890  {
7891  string innerTypeName = VuoType::extractInnermostTypeName(type->getModuleKey());
7892  VuoCompilerType *innerType = compiler->getType(innerTypeName);
7893  if (innerType)
7894  {
7895  string formattedInnerTypeName = innerType->getBase()->getDefaultTitle();
7896  formattedTypeName = "List of " + formattedInnerTypeName + " elements";
7897  }
7898  }
7899  else
7900  formattedTypeName = type->getDefaultTitle();
7901 
7902  return formattedTypeName.c_str();
7903 }
7904 
7909 {
7910  if (!type)
7911  return "Event";
7912 
7913  // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
7914  else if (type->getDefaultTitle() == "2D Point")
7915  return "Point2D";
7916  else if (type->getDefaultTitle() == "3D Point")
7917  return "Point3D";
7918  else if (type->getDefaultTitle() == "4D Point")
7919  return "Point4D";
7920  else if (type->getDefaultTitle() == "2D Transform")
7921  return "Transform2D";
7922  else if (type->getDefaultTitle() == "3D Transform")
7923  return "Transform3D";
7924 
7925  return VuoRendererPort::sanitizePortIdentifier(formatTypeNameForDisplay(type)).toUtf8().constData();
7926 }
7927 
7931 bool VuoEditorComposition::nodeSetMenuActionLessThan(QAction *action1, QAction *action2)
7932 {
7933  QString item1Text = action1->text();
7934  QString item2Text = action2->text();
7935 
7936  // Ignore list prefixes
7937  const QString listPrefix = "List of ";
7938  const QString builtInTypePrefix = "Vuo";
7939 
7940  if (item1Text.startsWith(listPrefix))
7941  {
7942  item1Text.remove(0, listPrefix.length());
7943  if (item1Text.startsWith(builtInTypePrefix))
7944  item1Text.remove(0, builtInTypePrefix.length());
7945  }
7946 
7947  if (item2Text.startsWith(listPrefix))
7948  {
7949  item2Text.remove(0, listPrefix.length());
7950  if (item2Text.startsWith(builtInTypePrefix))
7951  item2Text.remove(0, builtInTypePrefix.length());
7952  }
7953 
7954  // Sort alphabetically by title.
7955  return (item1Text.compare(item2Text, Qt::CaseInsensitive) < 0);
7956 }
7957 
7962 bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
7963 {
7964  qreal item1Y = item1->scenePos().y();
7965  qreal item2Y = item2->scenePos().y();
7966 
7967  qreal item1X = item1->scenePos().x();
7968  qreal item2X = item2->scenePos().x();
7969 
7970  if (item1Y == item2Y)
7971  return (item1X < item2X);
7972 
7973  return item1Y < item2Y;
7974 }
7975 
7982 double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
7983 {
7984  // Compare keywords.
7985  vector<string> node1Keywords = node1->getKeywords();
7986  vector<string> node2Keywords = node2->getKeywords();
7987 
7988  // Compare node set names.
7989  if (node1->getNodeSet())
7990  node1Keywords.push_back(node1->getNodeSet()->getName());
7991 
7992  if (node2->getNodeSet())
7993  node2Keywords.push_back(node2->getNodeSet()->getName());
7994 
7995  // Compare tokens in node class display names.
7996  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
7997  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
7998  node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
7999 
8000  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
8001  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8002  node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8003 
8004  set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
8005  set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
8006 
8007  set<string> nodeKeywordsIntersection;
8008  std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8009  node2KeywordSet.begin(), node2KeywordSet.end(),
8010  std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8011 
8012  set<string> nodeKeywordsUnion = node1KeywordSet;
8013  nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8014 
8015  // Avoid division by zero.
8016  if (nodeKeywordsUnion.size() == 0)
8017  return 0;
8018 
8019  // Calculate Jaccard similarity.
8020  double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8021 
8022  return nodeSimilarity;
8023 }
8024 
8029 {
8030  emit compositionOnTop(top);
8031 }
8032 
8037 {
8038  emit publishedPortNameEditorRequested(port, false);
8039 }
8040 
8041 VuoEditorComposition::~VuoEditorComposition()
8042 {
8043  dispatch_sync(runCompositionQueue, ^{});
8044  dispatch_release(runCompositionQueue);
8045 
8046  preparedAnimations.clear();
8047  animationForTimeline.clear();
8048 
8049  moduleManager->deleteWhenReady(); // deletes compiler
8050 }
8051 
8056 {
8057  // Update the canvas color.
8058  setBackgroundTransparent(false);
8059 
8060  // Force repainting the entire canvas.
8061  setComponentCaching(QGraphicsItem::NoCache);
8064 }
8065 
8071 map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8072 {
8073  vector<VuoRendererPort *> sortedPortsToPublish;
8074  foreach (string portID, portsToPublish)
8075  {
8076  VuoPort *port = getPortWithStaticIdentifier(portID);
8077  if (port && port->hasRenderer())
8078  sortedPortsToPublish.push_back(port->getRenderer());
8079  }
8080  std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8081 
8082  map<string, string> publishedPortNames;
8083  foreach (VuoRendererPort *rp, sortedPortsToPublish)
8084  {
8085  rp->updateGeometry();
8086  VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8087 
8088  string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8089  string publishedPortName = (!specializedPublishedPortName.empty()?
8090  specializedPublishedPortName :
8092 
8093  bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8094  VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8095 
8096  publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8097  }
8098 
8099  return publishedPortNames;
8100 }
8101 
8108 {
8109  if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8110  return "";
8111 
8112  // If publishing a port on a "Share Value" node and the node's title has been
8113  // customized, request that title as the published port name.
8117  {
8118  return VuoRendererPort::sanitizePortIdentifier(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8119  }
8120 
8121  return "";
8122 }
8123 
8127 void VuoEditorComposition::repositionPopover()
8128 {
8129  VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8130  if (popover && !popover->getDetached())
8131  {
8132  const int cutoffMargin = 16;
8133  if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8134  popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8135 
8136  if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8137  popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8138  }
8139 }