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>("Enqueue Events", VuoPortClass::EventThrottling_Enqueue));
156  throttlingNamesAndIndices.append(QPair<QString, enum VuoPortClass::EventThrottling>("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)
3532  {
3533  if (numHidableConnectedCables >= 1)
3534  {
3535  // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3536  // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3537  // impact visible published cables.
3538  int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3539  contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3540  contextMenuHideCables->setData(qVariantFromValue((void *)port));
3541  contextMenu.addAction(contextMenuHideCables);
3542  }
3543 
3544  if (numUnhidableConnectedCables >= 1)
3545  {
3546  int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3547  contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3548  contextMenuUnhideCables->setData(qVariantFromValue((void *)port));
3549  contextMenu.addAction(contextMenuUnhideCables);
3550  }
3551  }
3552 
3553  if (numVisibleConnectedCables >= 1)
3554  {
3555  contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3556  contextMenuDeleteCables->setData(qVariantFromValue((void *)port));
3557  contextMenu.addAction(contextMenuDeleteCables);
3558  }
3559  }
3560 
3561  // Customize context menu for nodes, cables, and/or comments.
3562  else if (item)
3563  {
3564  if (! item->isSelected())
3565  {
3566  clearSelection();
3567  item->setSelected(true);
3568  }
3569 
3570  QList<QGraphicsItem *> selectedComponents = selectedItems();
3571  bool onlyCommentsSelected = true;
3572  bool onlyCablesSelected = true;
3573  bool selectionContainsMissingNode = false;
3574  foreach (QGraphicsItem *item, selectedComponents)
3575  {
3576  if (!dynamic_cast<VuoRendererComment *>(item))
3577  onlyCommentsSelected = false;
3578 
3579  if (!dynamic_cast<VuoRendererCable *>(item))
3580  onlyCablesSelected = false;
3581 
3582  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3583  selectionContainsMissingNode = true;
3584  }
3585 
3586  contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3587  connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3588 
3589  // Comments
3590  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3591  if (comment)
3592  {
3593  // Option: Edit selected comments
3594  if (onlyCommentsSelected)
3595  contextMenu.addAction(contextMenuEditSelectedComments);
3596 
3597  // Option: Tint selected components(s)
3598  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3599 
3600  contextMenu.addSeparator();
3601 
3602  // Option: Refactor selected component(s)
3603  contextMenu.addAction(contextMenuRefactorSelected);
3604 
3605  contextMenu.addSeparator();
3606 
3607  // Option: Delete selected components(s)
3608  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3609  }
3610 
3611  // Cables
3612  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3613  if (cable)
3614  {
3615  if (!renderHiddenCables)
3616  {
3617  if (onlyCablesSelected && !cable->getBase()->isPublished())
3618  contextMenu.addAction(contextMenuHideSelectedCables);
3619  }
3620 
3621  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3622  }
3623 
3624  // Nodes
3625  else if (dynamic_cast<VuoRendererNode *>(item))
3626  {
3627  VuoRendererNode *node = (VuoRendererNode *)item;
3628 
3629  // Input drawers
3630  if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3631  node->getBase()->getNodeClass()->hasCompiler())
3632  {
3633  // Offer "Add/Remove Input Port" options for resizable list input drawers.
3634  if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3635  {
3636  contextMenuAddInputPort->setData(qVariantFromValue((void *)node));
3637  contextMenuRemoveInputPort->setData(qVariantFromValue((void *)node));
3638 
3639  int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3640  contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3641 
3642  contextMenu.addAction(contextMenuAddInputPort);
3643  contextMenu.addAction(contextMenuRemoveInputPort);
3644 
3645  contextMenu.addSeparator();
3646  }
3647 
3648  // Offer "Reset" option for all input drawers.
3649  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3650  }
3651 
3652  // Non-input-drawer nodes
3653  else
3654  {
3655  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3656  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3657  {
3658  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3659  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3660  if (originalGenericNodeClass)
3661  nodeClass = originalGenericNodeClass->getBase();
3662  }
3663 
3664  QString actionText, sourcePath;
3665  bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3666  bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3667 
3668  int numSelectedNonAttachmentNodes = 0;
3669  QList<QGraphicsItem *> selectedComponents = selectedItems();
3670  foreach (QGraphicsItem *item, selectedComponents)
3671  {
3672  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3673  numSelectedNonAttachmentNodes++;
3674  }
3675 
3676  if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3677  {
3678  // Option: Edit an installed subcomposition, shader, or text-code node.
3679  QAction *editAction = new QAction(NULL);
3680  editAction->setText(actionText);
3681  editAction->setData(qVariantFromValue(node));
3682  connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3683 
3684  contextMenu.addAction(editAction);
3685  contextMenu.addSeparator();
3686  }
3687 
3688  // Option: Rename selected node(s)
3689  if (node->getBase()->hasCompiler())
3690  contextMenu.addAction(contextMenuRenameSelected);
3691 
3692  // Option: Tint selected component(s)
3693  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3694 
3695  contextMenu.addSeparator();
3696 
3697  // Option: Replace selected node with a similar node
3698  if (numSelectedNonAttachmentNodes == 1)
3699  {
3700  if (contextMenuChangeNode)
3701  contextMenuChangeNode->deleteLater();
3702 
3703  contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3704  contextMenuChangeNode->setSeparatorsCollapsible(false);
3705  contextMenuChangeNode->setTitle(tr("Change To"));
3706 
3707  populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3708  if (!contextMenuChangeNode->actions().isEmpty())
3709  contextMenu.addMenu(contextMenuChangeNode);
3710  } // End single selected node
3711 
3712  // Option: Refactor selected component(s)
3713  if (!selectionContainsMissingNode)
3714  contextMenu.addAction(contextMenuRefactorSelected);
3715 
3716  if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3717  {
3718  // Option: Open the enclosing folder for an editable or other 3rd party node class.
3719  QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3720  if (!modulePath.isEmpty())
3721  {
3722  QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3723  QAction *openEnclosingFolderAction = new QAction(NULL);
3724  openEnclosingFolderAction->setText("Show in Finder");
3725  openEnclosingFolderAction->setData(qVariantFromValue(enclosingDirUrl));
3726  connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3727  contextMenu.addAction(openEnclosingFolderAction);
3728  }
3729  }
3730 
3731  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3732  contextMenu.addSeparator();
3733 
3734  // Option: Delete selected components(s)
3735  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3736 
3737  } // End non-input-drawer nodes
3738  } // End nodes
3739  } // End nodes or cables
3740 
3741  if (!contextMenu.actions().isEmpty())
3742  {
3743  // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3745 
3746  menuSelectionInProgress = true;
3747  contextMenu.exec(event->screenPos());
3748  menuSelectionInProgress = false;
3749  }
3750 
3751  delete contextMenuDeleteSelectedSnapshot;
3752 }
3753 
3758 {
3759  QAction *sender = (QAction *)QObject::sender();
3760  VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3761  emit nodeSourceEditorRequested(node);
3762 }
3763 
3770 {
3771  vector<string> typeOptions;
3772 
3773  map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
3774  for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
3775  {
3776  if (((!lists && !VuoType::isListTypeName(i->first)) ||
3777  (lists && VuoType::isListTypeName(i->first))) &&
3778  !VuoType::isDictionaryTypeName(i->first) &&
3779  (i->first != "VuoMathExpressionList"))
3780  {
3781  typeOptions.push_back(i->first);
3782  }
3783  }
3784 
3785  return typeOptions;
3786 }
3787 
3793 set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3794 {
3795  if (!port)
3796  return set<string>();
3797 
3798  // Find the set of connected generic ports that contains our target port.
3799  set<VuoPort *> connectedGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(port->getUnderlyingParentNode()->getBase(),
3800  port->getBase(), true);
3801 
3802  // Determine the set of types compatible with all generic ports in the connected network.
3803  vector<string> compatibleInnerTypeNames;
3804  vector<string> compatibleTypeNames;
3805  for (VuoPort *connectedPort : connectedGenericPorts)
3806  {
3807  VuoGenericType *genericTypeFromPortClass = NULL;
3808  VuoCompilerNodeClass *nodeClass = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler();
3809  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3810  if (specializedNodeClass)
3811  {
3812  VuoPortClass *portClass = connectedPort->getClass();
3813  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3814  }
3815 
3816  VuoGenericType::Compatibility compatibility;
3817  vector<string> compatibleTypeNamesForPort = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3818  vector<string> innermostCompatibleTypeNamesForPort;
3819  for (vector<string>::iterator k = compatibleTypeNamesForPort.begin(); k != compatibleTypeNamesForPort.end(); ++k)
3820  innermostCompatibleTypeNamesForPort.push_back( VuoType::extractInnermostTypeName(*k) );
3821 
3822  if (! innermostCompatibleTypeNamesForPort.empty())
3823  {
3824  if (compatibleInnerTypeNames.empty())
3825  compatibleInnerTypeNames = innermostCompatibleTypeNamesForPort;
3826  else
3827  {
3828  for (int k = compatibleInnerTypeNames.size() - 1; k >= 0; --k)
3829  if (find(innermostCompatibleTypeNamesForPort.begin(), innermostCompatibleTypeNamesForPort.end(), compatibleInnerTypeNames[k]) ==
3830  innermostCompatibleTypeNamesForPort.end())
3831  compatibleInnerTypeNames.erase(compatibleInnerTypeNames.begin() + k);
3832  }
3833  }
3834  }
3835 
3837  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3838  VuoPortClass *portClass = port->getBase()->getClass();
3839  VuoGenericType *genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3840  string typeNameForPort = genericTypeFromPortClass->getModuleKey();
3841 
3842  // Finish compiling set of compatible types in the context of the connected generic port network.
3843  string prefix = (VuoType::isListTypeName(typeNameForPort) ? VuoType::listTypeNamePrefix : "");
3844  for (vector<string>::iterator k = compatibleInnerTypeNames.begin(); k != compatibleInnerTypeNames.end(); ++k)
3845  compatibleTypeNames.push_back(prefix + *k);
3846 
3847  if (compatibleTypeNames.empty())
3848  {
3849  VuoGenericType::Compatibility compatibility;
3850  genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3851 
3852  // If all types or all list types are compatible, add them to (currently empty) compatibleTypeNames.
3853  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3854  compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3855  }
3856 
3857  return set<string>(compatibleTypeNames.begin(), compatibleTypeNames.end());
3858 }
3859 
3880  set<string> compatibleTypesInIsolation,
3881  set<string> compatibleTypesInContext,
3882  bool limitToNodeSet,
3883  string nodeSetName,
3884  QMenu *menu)
3885 {
3886  QList<QAction *> actionsToAddToMenu;
3887 
3888  map<string, VuoCompilerType *> allTypes = compiler->getTypes();
3889  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3890  QList<VuoCompilerType *> compatibleTypesForNodeSetDisplay;
3891  foreach (string typeName, compatibleTypesInIsolation)
3892  {
3893  VuoCompilerType *type = allTypes[typeName];
3894  if ((!limitToNodeSet || (loadedTypesForNodeSet[nodeSetName].find(type) != loadedTypesForNodeSet[nodeSetName].end())) &&
3895  (! VuoGenericType::isGenericTypeName(typeName)) &&
3896  // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
3897  (typeName != "VuoUrl" && typeName != "VuoList_VuoUrl"
3898  // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
3899  && typeName != "VuoInteraction" && typeName != "VuoList_VuoInteraction"
3900  && typeName != "VuoInteractionType" && typeName != "VuoList_VuoInteractionType"
3901  && typeName != "VuoUuid" && typeName != "VuoList_VuoUuid"
3902  // Hide deprecated types.
3903  && typeName != "VuoIconPosition" && typeName != "VuoList_VuoIconPosition"
3904  && typeName != "VuoWindowProperty" && typeName != "VuoList_VuoWindowProperty"
3905  && typeName != "VuoWindowReference" && typeName != "VuoList_VuoWindowReference"))
3906  compatibleTypesForNodeSetDisplay.append(type);
3907  }
3908 
3909  if (!compatibleTypesForNodeSetDisplay.isEmpty())
3910  {
3911  QMenu *contextMenuNodeSetTypes = NULL;
3912  QList<QAction *> actionsToAddToNodeSetSubmenu;
3913  bool enabledContentAdded = false;
3914 
3915  // Populate the "Specialize Type" submenu for the target node set.
3916  if (!nodeSetName.empty())
3917  {
3918  contextMenuNodeSetTypes = new QMenu(menu);
3919  contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
3920  contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
3921  contextMenuNodeSetTypes->setToolTipsVisible(true);
3922  actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
3923  }
3924 
3925  foreach (VuoCompilerType *type, compatibleTypesForNodeSetDisplay)
3926  {
3927  string typeName = type->getBase()->getModuleKey();
3928  QList<QVariant> portAndSpecializedType;
3929  portAndSpecializedType.append(qVariantFromValue((void *)genericPort));
3930  portAndSpecializedType.append(typeName.c_str());
3931 
3932  QAction *specializeAction;
3933  QString typeTitle = formatTypeNameForDisplay(type->getBase());
3934 
3935  // Case: Adding to the node set submenu
3936  if (!nodeSetName.empty())
3937  {
3938  specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
3939  actionsToAddToNodeSetSubmenu.append(specializeAction);
3940  }
3941 
3942  // Case: Adding to the top-level action list
3943  else
3944  {
3945  specializeAction = new QAction(typeTitle, menu);
3946  actionsToAddToMenu.append(specializeAction);
3947  }
3948 
3949  specializeAction->setData(QVariant(portAndSpecializedType));
3950  specializeAction->setCheckable(true);
3951  specializeAction->setChecked(genericPort && (genericPort->getDataType()->getModuleKey() == type->getBase()->getModuleKey()));
3952 
3953  if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
3954  {
3955  specializeAction->setEnabled(false);
3956  //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
3957  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."));
3958  }
3959  else
3960  enabledContentAdded = true;
3961  }
3962 
3963  if (contextMenuNodeSetTypes)
3964  {
3965  addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
3966 
3967  if (!enabledContentAdded)
3968  contextMenuNodeSetTypes->setEnabled(false);
3969  }
3970  }
3971 
3972  QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
3973  return actionListWithPromotions;
3974 }
3975 
3980 QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
3981 {
3982  QList<QAction *> modifiedActionList;
3983  foreach (QAction *action, actionList)
3984  {
3985  if (action->menu() && (action->menu()->actions().size() == 1))
3986  {
3987  QAction *singleSubaction = action->menu()->actions().first();
3988  action->menu()->removeAction(singleSubaction);
3989  modifiedActionList.append(singleSubaction);
3990  }
3991  else
3992  modifiedActionList.append(action);
3993  }
3994 
3995  return modifiedActionList;
3996 }
3997 
4001 void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
4002 {
4003  std::sort(actionList.begin(), actionList.end(), nodeSetMenuActionLessThan);
4004  foreach (QAction *action, actionList)
4005  menu->addAction(action);
4006 }
4007 
4011 void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4012 {
4013  if (!now)
4014  return;
4015 
4017  if (activeWindow)
4018  emit compositionOnTop(activeWindow->getComposition() == this);
4019 }
4020 
4024 void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4025 {
4026  if (ignoreApplicationStateChangeEvents)
4027  return;
4028 
4030  if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4031  emit applicationActive(active);
4032 }
4033 
4042 QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4043 {
4044  return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4045 }
4046 
4051 {
4052  QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4053  return dynamic_cast<VuoRendererNode *>(item);
4054 }
4055 
4068 QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4069  targetComponentType targetType,
4070  bool limitPortCollisionRange)
4071 {
4072  // Determine which types of components to filter out of search results.
4073  bool ignoreCables;
4074  bool ignoreNodes;
4075  bool ignorePorts;
4076  bool ignoreComments;
4077 
4078  switch(targetType)
4079  {
4080  case VuoEditorComposition::targetTypePort:
4081  {
4082  ignoreCables = true;
4083  ignoreNodes = true;
4084  ignorePorts = false;
4085  ignoreComments = true;
4086  break;
4087  }
4088  case VuoEditorComposition::targetTypeNodeHeader:
4089  {
4090  ignoreCables = true;
4091  ignoreNodes = true;
4092  ignorePorts = true;
4093  ignoreComments = true;
4094  break;
4095  }
4096  default:
4097  {
4098  ignoreCables = false;
4099  ignoreNodes = false;
4100  ignorePorts = false;
4101  ignoreComments = false;
4102  break;
4103  }
4104  }
4105 
4106  // The topmost item under the cursor is not necessarily the one we will return
4107  // (e.g., if the cursor is positioned directly over a node but also within range
4108  // of one of that node's ports), but it will factor in to the decision.
4109  QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4110  if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4111  topmostItemUnderCursor = NULL;
4112 
4113  for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4114  {
4115  QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4116  QList<QGraphicsItem *> itemsInRange = items(searchRect);
4117  for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4118  {
4119  if (!(*i)->isEnabled())
4120  continue;
4121 
4122  // Check whether we have located an unobscured "Make List" drawer drag handle.
4123  if (! ignoreNodes)
4124  {
4125  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4126  bool makeListDragHandle =
4127  (makeListDrawer &&
4128  makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4129  if (makeListDragHandle &&
4130  ((! topmostItemUnderCursor) ||
4131  (topmostItemUnderCursor == makeListDrawer) ||
4132  (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4133  {
4134  return makeListDrawer;
4135  }
4136  }
4137 
4138  // Check whether we have located an unobscured port.
4139  // Hovering within range of a port takes precedence over hovering
4140  // directly over that port's parent node.
4141  if (! ignorePorts)
4142  {
4143  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4144  if (port &&
4145  ((! topmostItemUnderCursor) ||
4146  (topmostItemUnderCursor == port) ||
4147  (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4148  (topmostItemUnderCursor->zValue() < port->zValue()))
4149  &&
4150  ((! limitPortCollisionRange) ||
4151  port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4152  &&
4153  ! port->getFunctionPort())
4154  {
4155  return port;
4156  }
4157  }
4158 
4159  // Check whether we have located an unobscured cable.
4160  if (! ignoreCables)
4161  {
4162  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4163  if (cable &&
4164  ((! topmostItemUnderCursor) ||
4165  (topmostItemUnderCursor == cable) ||
4166  (topmostItemUnderCursor->zValue() < cable->zValue())))
4167  {
4168  return cable;
4169  }
4170  }
4171 
4172  // Check whether we have located an unobscured comment.
4173  if (! ignoreComments)
4174  {
4175  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4176  if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4177  comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4178 
4179  if (comment &&
4180  ((! topmostItemUnderCursor) ||
4181  (topmostItemUnderCursor == (*i)) ||
4182  (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4183  {
4184  return comment;
4185  }
4186  }
4187 
4188  // Check whether we have located an unobscured node header.
4189  if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4190  {
4191  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4192  if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4193  {
4194  QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4195  headerRect = node->mapToScene(headerRect).boundingRect();
4196  if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4197  return node;
4198  }
4199  }
4200  }
4201  }
4202 
4203  // Having failed to locate any other relevant components within range, return the node
4204  // directly under the cursor, if applicable.
4205  if (! ignoreNodes)
4206  {
4207  if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4208  return topmostItemUnderCursor;
4209 
4210  // It is possible that the item under the cursor is a port, but that it didn't meet
4211  // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4212  if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4213  return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4214  }
4215 
4216  return NULL;
4217 }
4218 
4229 {
4230  if (! cableInProgress)
4231  return NULL;
4232 
4233  VuoPort *fromPort = cableInProgress->getFromPort();
4234  VuoPort *toPort = cableInProgress->getToPort();
4235  VuoPort *fixedPort = (fromPort? fromPort: toPort);
4236 
4237  if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4238  return NULL;
4239 
4240  return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4241 }
4242 
4248 {
4249  // Start with the first input or output port (other than the refresh port).
4250  vector<VuoRendererPort *> portList;
4251  int firstPortIndex;
4252 
4253  if (inputPort)
4254  {
4255  portList = node->getInputPorts();
4257  }
4258  else
4259  {
4260  portList = node->getOutputPorts();
4262  }
4263 
4264  VuoRendererPort *targetPort = NULL;
4265  if (portList.size() > firstPortIndex)
4266  {
4267  targetPort = portList[firstPortIndex];
4268 
4269  // If the first input port has a wall,
4270  // keep moving down until we find one without a wall.
4271  // (Unless they all have walls, in which case stick with the first.)
4272  VuoRendererPort *firstPortWithoutWall = nullptr;
4273  for (int i = firstPortIndex; i < portList.size(); ++i)
4274  {
4275  if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4276  {
4277  firstPortWithoutWall = portList[i];
4278  break;
4279  }
4280  }
4281  if (firstPortWithoutWall)
4282  targetPort = firstPortWithoutWall;
4283 
4284  // If the first input port has a drawer attached to it,
4285  // instead select the first input port of the drawer.
4286  // (Unless the drawer has no input ports, in which case don't select any port.)
4287  VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4288  if (drawer)
4289  {
4290  portList = drawer->getInputPorts();
4291  if (portList.size() > firstPortIndex)
4292  targetPort = portList[firstPortIndex];
4293  else
4294  targetPort = NULL;
4295  }
4296 
4297  // If the selected input port has a collapsed type-converter node attached to it,
4298  // instead select the type-converter node's input port.
4299  VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4300  if (typecast)
4301  targetPort = typecast->getChildPort();
4302  }
4303 
4304  return targetPort;
4305 }
4306 
4312 {
4313  QRectF boundingRect;
4314  foreach (QGraphicsItem *item, items())
4315  {
4316  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4317  if (! (rc && rc->getBase()->isPublished()))
4318  boundingRect |= item->sceneBoundingRect();
4319  }
4320 
4321  return boundingRect;
4322 }
4323 
4329 {
4330  QRectF boundingRect;
4331 
4332  foreach (QGraphicsItem *item, selectedItems())
4333  {
4334  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4335  if (! (rc && rc->getBase()->isPublished()))
4336  boundingRect |= item->sceneBoundingRect();
4337  }
4338 
4339  return boundingRect;
4340 }
4341 
4347 {
4348  QRectF boundingRect;
4349 
4350  foreach (QGraphicsItem *item, selectedItems())
4351  {
4352  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4353  if (! (rc && rc->getBase()->isPublished()))
4354  boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4355 
4356  // Include attached drawers.
4357  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4358  if (rn)
4359  {
4360  foreach (VuoPort *port, rn->getBase()->getInputPorts())
4361  {
4362  VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4363  if (drawer)
4364  boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4365  }
4366  }
4367  }
4368 
4369  return boundingRect;
4370 }
4371 
4376 void VuoEditorComposition::updateInternalPortConstant(string portID, string newValue, bool updateInRunningComposition)
4377 {
4378  VuoPort *port = portWithStaticIdentifier[portID];
4379  if (!port)
4380  return;
4381 
4382  updatePortConstant(dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4383 }
4384 
4389 void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4390 {
4392  if (!port)
4393  return;
4394 
4395  updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4396 }
4397 
4402 void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4403 {
4404  // Internal ports
4405  if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4406  {
4407  port->getBase()->getRenderer()->setConstant(newValue);
4408 
4409  if (updateInRunningComposition)
4410  updateInternalPortConstantInRunningComposition(dynamic_cast<VuoCompilerInputEventPort *>(port), newValue);
4411  }
4412 
4413  // Published ports
4414  else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4415  {
4416  dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4417 
4418  if (updateInRunningComposition)
4419  updatePublishedInputPortConstantInRunningComposition(dynamic_cast<VuoPublishedPort *>(port->getBase()), newValue);
4420  }
4421 }
4422 
4428 {
4431 }
4432 
4441 {
4442 
4443  // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4444  if (!errorMarkingUpdatesEnabled)
4445  return;
4446 
4447  errorMarkingUpdatesEnabled = false;
4448 
4449  // Remove any error annotations from the previous call to this function.
4450  if (errorMark)
4451  {
4452  errorMark->removeFromScene();
4453  errorMark = NULL;
4454  }
4455 
4457 
4458  // Check for errors.
4459 
4460  VuoCompilerIssues *issues = new VuoCompilerIssues();
4461  VuoCompilerCable *potentialCable = NULL;
4462  try
4463  {
4464  set<VuoCompilerCable *> potentialCables;
4465 
4466  if (targetPort && cableInProgress)
4467  {
4468  VuoNode *fromNode;
4469  VuoPort *fromPort;
4470  VuoNode *toNode;
4471  VuoPort *toPort;
4472  if (cableInProgress->getFromNode())
4473  {
4474  fromNode = cableInProgress->getFromNode();
4475  fromPort = cableInProgress->getFromPort();
4476  toNode = targetPort->getUnderlyingParentNode()->getBase();
4477  toPort = targetPort->getBase();
4478  }
4479  else
4480  {
4481  fromNode = targetPort->getUnderlyingParentNode()->getBase();
4482  fromPort = targetPort->getBase();
4483  toNode = cableInProgress->getToNode();
4484  toPort = cableInProgress->getToPort();
4485  }
4486  potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4487  potentialCable->getBase()->setFrom(fromNode, fromPort);
4488  potentialCable->getBase()->setTo(toNode, toPort);
4489  potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4490  cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4491 
4492  fromPort->removeConnectedCable(potentialCable->getBase());
4493  toPort->removeConnectedCable(potentialCable->getBase());
4494  potentialCables.insert(potentialCable);
4495  }
4496 
4497  getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4498  }
4499  catch (const VuoCompilerException &e)
4500  {
4501  }
4502 
4503  if (! issues->isEmpty())
4504  {
4505  VUserLog("%s: Showing error popover: %s",
4506  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4507  issues->getShortDescription(false).c_str());
4508 
4509  this->errorMark = new VuoErrorMark();
4510 
4511  foreach (VuoCompilerIssue issue, issues->getList())
4512  {
4513  set<VuoRendererNode *> nodesToMark;
4514  set<VuoRendererCable *> cablesToMark;
4515 
4516  set<VuoNode *> problemNodes = issue.getNodes();
4517  foreach (VuoNode *node, problemNodes)
4518  if (node->hasRenderer())
4519  nodesToMark.insert(node->getRenderer());
4520 
4521  set<VuoCable *> problemCables = issue.getCables();
4522  foreach (VuoCable *cable, problemCables)
4523  {
4524  VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4525  if (cableToMark->hasRenderer())
4526  cablesToMark.insert(cableToMark->getRenderer());
4527  }
4528 
4530  errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4531 
4532  VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4533  errorPopovers.insert(errorPopover);
4536 
4537  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4538 
4539  // Place the popover near an appropriate nearby node involved in the feedback loop.
4540  VuoRendererNode *nearbyNode = NULL;
4541  if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4542  {
4543  nearbyNode = targetPort->getRenderedParentNode();
4544  }
4545  else if (! nodesToMark.empty())
4546  {
4547  VuoRendererNode *topmostVisibleNode = NULL;
4548  qreal topY = viewportRect.bottom();
4549  foreach (VuoRendererNode *node, nodesToMark)
4550  {
4551  if (node->getProxyNode())
4552  node = node->getProxyNode();
4553 
4554  QPointF scenePos = node->scenePos();
4555  if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4556  {
4557  topmostVisibleNode = node;
4558  topY = scenePos.y();
4559  }
4560  }
4561 
4562  if (topmostVisibleNode)
4563  nearbyNode = topmostVisibleNode;
4564  else
4565  {
4566  VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4567  nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4568  }
4569  }
4570  else
4571  {
4572  VUserLog("Warning: no nearby node (no marked nodes).");
4573  }
4574 
4575  // If no nodes are known to be involved in the feedback loop, display the popover
4576  // in the center of the viewport.
4577  const QPoint offsetFromNode(0,10);
4578  QPoint popoverTopLeftInScene = (nearbyNode?
4579  (nearbyNode->scenePos().toPoint() +
4580  nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4581  offsetFromNode) :
4582  QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4583  viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4584 
4585  // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4586  // of the viewport closest to the feedback loop.
4587  const int margin = 5;
4588  popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4589  fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4590 
4591  popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4592  fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4593 
4594  QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4595  QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4596 
4597  errorPopover->move(popoverTopLeftGlobal);
4598  errorPopover->show();
4599  emit popoverDetached();
4600  }
4601 
4602  // Add error annotations to the composition.
4603  addItem(errorMark);
4604  }
4605  delete issues;
4606  delete potentialCable;
4607 
4608  errorMarkingUpdatesEnabled = true;
4609 }
4610 
4614 bool VuoEditorComposition::hasFeedbackErrors(void)
4615 {
4616  return this->errorMark;
4617 }
4618 
4623 {
4624  if (hasFeedbackErrors())
4625  this->errorMark->updateErrorMarkPath();
4626 }
4627 
4633 void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4634 {
4635  try
4636  {
4637  emit buildStarted();
4638 
4639  if (! dependenciesUninstalled.empty())
4640  {
4641  vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4642  string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4643  throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4644  }
4645 
4646  delete runningComposition;
4647  runningComposition = NULL;
4648  runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4649 
4650  runningCompositionActiveDriver = getDriverForActiveProtocol();
4651  if (runningCompositionActiveDriver)
4652  runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4653 
4654  string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4655  string dir, file, ext;
4656  VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4657  linkedCompositionPath = dir + file + ".dylib";
4658 
4659  compiler->setShouldPotentiallyShowSplashWindow(false);
4660 
4661  VuoCompilerIssues *issues = new VuoCompilerIssues();
4662  compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4663  compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries);
4664  delete issues;
4665 
4666  remove(compiledCompositionPath.c_str());
4667 
4668  emit buildFinished("");
4669  }
4670  catch (VuoException &e)
4671  {
4672  delete runningComposition;
4673  runningComposition = NULL;
4674 
4675  emit buildFinished(e.what());
4676  throw;
4677  }
4678 }
4679 
4685 bool VuoEditorComposition::isRunningThreadUnsafe(void)
4686 {
4687  return runner != NULL && ! stopRequested && ! runner->isStopped();
4688 }
4689 
4696 {
4697  __block bool running;
4698  dispatch_sync(runCompositionQueue, ^{
4699  running = isRunningThreadUnsafe();
4700  });
4701  return running;
4702 }
4703 
4709 void VuoEditorComposition::run(string compositionSnapshot)
4710 {
4711  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4712 
4713  // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4714  subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4715 
4716  // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4717  subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4718  {
4719  compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4720  });
4721 
4722  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4723  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4724  {
4725  if (matchingComposition->showEventsMode)
4726  matchingComposition->beginDisplayingActivity();
4727  });
4728 
4729  stopRequested = false;
4730  dispatch_async(runCompositionQueue, ^{
4731  try
4732  {
4733  runningCompositionLibraries = new VuoRunningCompositionLibraries();
4734 
4735  buildComposition(compositionSnapshot);
4736 
4737  string compositionLoaderPath = compiler->getCompositionLoaderPath();
4738  string compositionSourceDir = getBase()->getDirectory();
4739 
4740  runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4741  runner->setDelegate(this);
4743  runner->startPaused();
4744  pid_t pid = runner->getCompositionPid();
4745 
4746  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4747  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4748  {
4749  if (matchingComposition->showEventsMode)
4750  this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4751 
4752  dispatch_sync(activePortPopoversQueue, ^{
4753  for (auto i : matchingComposition->activePortPopovers)
4754  {
4755  string portID = i.first;
4756  updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4757  }
4758  });
4759  });
4760 
4761  runner->unpause();
4762 
4763  // Focus the composition's windows (if any).
4765  }
4766  catch (...) { }
4767  });
4768 }
4769 
4776 {
4777  stopRequested = true;
4778  dispatch_async(runCompositionQueue, ^{
4779  if (runner && ! runner->isStopped())
4780  {
4781  runner->stop();
4782  runner->waitUntilStopped();
4783  }
4784  delete runner;
4785  runner = NULL;
4786 
4787  linkedCompositionPath = "";
4788  runningCompositionLibraries = NULL;
4789 
4790  delete runningComposition;
4791  runningComposition = NULL;
4792 
4793  emit stopFinished();
4794  });
4795 
4796  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4797 
4798  // Tell this composition and all subcompositions opened from it to stop display live debug info.
4799  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4800  {
4801  if (matchingComposition->showEventsMode)
4802  matchingComposition->stopDisplayingActivity();
4803 
4804  dispatch_sync(activePortPopoversQueue, ^{
4805  for (auto i : matchingComposition->activePortPopovers)
4806  {
4807  VuoPortPopover *popover = i.second;
4808  popover->setCompositionRunning(false);
4809  }
4810  });
4811  });
4812 }
4813 
4825 void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4826  VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4827 {
4828  if (! diffInfo)
4829  diffInfo = new VuoCompilerCompositionDiff();
4830 
4831  dispatch_async(runCompositionQueue, ^{
4832  if (isRunningThreadUnsafe())
4833  {
4834  try
4835  {
4836  foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4837  {
4838  runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4839  }
4840 
4841  string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4842  VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4843  if (previouslyActiveDriver)
4844  {
4845  VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4846  previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4847  oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4848  }
4849 
4850  buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4851 
4852  string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4853  runner->replaceComposition(linkedCompositionPath, compositionDiff);
4854  }
4855  catch (exception &e)
4856  {
4857  VUserLog("Composition stopped itself: %s", e.what());
4858  emit compositionStoppedItself();
4859  }
4860  catch (...)
4861  {
4862  VUserLog("Composition stopped itself.");
4863  emit compositionStoppedItself();
4864  }
4865  }
4866  else
4867  {
4868  dispatch_async(dispatch_get_main_queue(), ^{
4869  updateCompositionsThatContainThisSubcomposition(newCompositionSnapshot);
4870  });
4871  }
4872 
4873  delete diffInfo;
4874  });
4875 }
4876 
4882 {
4883  void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
4884  {
4885  compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
4886  };
4887  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
4888 }
4889 
4895 {
4896  VuoPort *port = portWithStaticIdentifier[runningPortID];
4897  if (!(port && port->hasCompiler() && port->hasRenderer()))
4898  return;
4899 
4900  string constant = port->getRenderer()->getConstantAsString();
4901 
4902  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4903  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4904  {
4905  if (this == topLevelComposition)
4906  {
4907  dispatch_async(runCompositionQueue, ^{
4908  if (isRunningThreadUnsafe())
4909  {
4910  json_object *constantObject = json_tokener_parse(constant.c_str());
4911  runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
4912  }
4913  });
4914  }
4915  };
4916  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4917 
4918  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4919  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4920  {
4921  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4922  {
4923  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
4924  });
4925  });
4926 }
4927 
4933 {
4935  if (!(port && port->hasCompiler()))
4936  return;
4937 
4938  string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
4940 }
4941 
4946 {
4947  map<VuoPort *, string>::iterator i = staticIdentifierForPort.find(port->getBase());
4948  if (i == staticIdentifierForPort.end())
4949  return;
4950  string runningPortIdentifier = i->second;
4951 
4952  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4953  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4954  {
4955  if (this == topLevelComposition)
4956  {
4957  dispatch_async(runCompositionQueue, ^{
4958  if (isRunningThreadUnsafe())
4959  {
4960  json_object *constantObject = json_tokener_parse(constant.c_str());
4961  runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
4962  }
4963  });
4964  }
4965  };
4966  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4967 
4968  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4969  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4970  {
4971  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4972  {
4973  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
4974  });
4975  });
4976 }
4977 
4982 void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
4983 {
4984  dispatch_async(runCompositionQueue, ^{
4985  if (isRunningThreadUnsafe())
4986  {
4987  json_object *constantObject = json_tokener_parse(constant.c_str());
4988  set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
4989  foreach (string subcompositionIdentifier, subcompositionIdentifiers)
4990  {
4991  runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
4992  }
4993  }
4994  });
4995 }
4996 
5001 {
5002  dispatch_async(runCompositionQueue, ^{
5003  if (isRunningThreadUnsafe())
5004  {
5005  VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
5006  if (publishedPort)
5007  {
5008  json_object *constantObject = json_tokener_parse(constant.c_str());
5009  map<VuoRunner::Port *, json_object *> m;
5010  m[publishedPort] = constantObject;
5011  runner->setPublishedInputPortValues(m);
5012  }
5013  }
5014  });
5015 }
5016 
5017 
5022 {
5023  return contextMenuDeleteSelected;
5024 }
5025 
5030 {
5031  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5032  // For now, this recreates the context menu rather than accessing a data member.
5033  QMenu *contextMenuTints = new QMenu(parent);
5034  contextMenuTints->setSeparatorsCollapsible(false);
5035  contextMenuTints->setTitle(tr("Tint"));
5036  foreach (QAction *tintAction, contextMenuTintActions)
5037  contextMenuTints->addAction(tintAction);
5038  contextMenuTints->insertSeparator(contextMenuTintActions.last());
5039 
5040  return contextMenuTints;
5041 }
5042 
5046 void VuoEditorComposition::expandChangeNodeMenu()
5047 {
5048  QAction *sender = (QAction *)QObject::sender();
5049  VuoRendererNode *node = static_cast<VuoRendererNode *>(sender->data().value<void *>());
5050 
5051  // If the menu hasn't been expanded previously, expand it enough now to fill
5052  // the available vertical screenspace.
5053  int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5054  if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5055  {
5056  int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5057  int verticalSpacePerItem = 21; // menu row height in pixels
5058  // Estimate the number of matches that will fit within the screen without scrolling:
5059  // -1 to account for a possible extra "More…" row;
5060  // -1 wiggle room to match observations
5061  int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5062 
5063  populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5064  }
5065 
5066  // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5067  else
5068  populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5069 
5070  contextMenuChangeNode->exec();
5071 }
5072 
5078 void VuoEditorComposition::populateChangeNodeMenu(QMenu *menu, VuoRendererNode *node, int matchLimit)
5079 {
5080  menu->clear();
5081 
5082  if (!node)
5083  return;
5084 
5085  map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5086  vector<VuoCompilerNodeClass *> loadedNodeClasses;
5087  for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5088  loadedNodeClasses.push_back(i->second);
5089  VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5090 
5091  vector<string> bestMatches;
5092  map<string, double> matchScores;
5093  matchScores[""] = 0;
5094 
5095  int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5096  for (int i = 0; i < targetMatchCount; ++i)
5097  bestMatches.push_back("");
5098 
5099  // Maintain a priority queue with the @c targetMatchCount best matches.
5100  bool overflow = false;
5101 
5102  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5103  string originalGenericNodeClassName;
5104  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5105  originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5106  else
5107  originalGenericNodeClassName = nodeClass->getClassName();
5108 
5109  foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5110  {
5111  string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5112  if (loadedNodeClassName == originalGenericNodeClassName)
5113  continue;
5114 
5115  bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5116  double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5117  int highestIndexWithCompetitiveScore = -1;
5118  for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5119  if (matchScore <= matchScores[bestMatches[i] ])
5120  highestIndexWithCompetitiveScore = i;
5121 
5122  if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5123  {
5124  if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5125  overflow = true;
5126 
5127  for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5128  bestMatches[j+1] = bestMatches[j];
5129 
5130  bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5131  matchScores[loadedNodeClassName] = matchScore;
5132  }
5133  }
5134 
5135  for (int i = 0; i < targetMatchCount; ++i)
5136  {
5137  if (matchScores[bestMatches[i] ] > 0)
5138  {
5139  // Disambiguate between identical node titles using node class names.
5140  QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5141  if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5142  matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5143 
5144  QAction *changeAction = menu->addAction(matchDisplayText);
5145 
5146  QList<QVariant> currentNodeAndNewClass;
5147  currentNodeAndNewClass.append(qVariantFromValue((void *)node));
5148  currentNodeAndNewClass.append(bestMatches[i].c_str());
5149  changeAction->setData(QVariant(currentNodeAndNewClass));
5150  connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5151  }
5152  }
5153 
5154  if (overflow)
5155  {
5156  //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5157  QAction *showMoreAction = menu->addAction(tr("More…"));
5158  showMoreAction->setData(qVariantFromValue(static_cast<void *>(node)));
5159  connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5160  }
5161 }
5162 
5167 bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5168 {
5169  // Inventory required input port types (connected data inputs) in the node to be replaced.
5170  map<string, int> requiredInputs;
5171  bool inputEventSourceRequired = false;
5172  foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5173  {
5174  bool hasDrawerWithNoIncomingCables = false;
5175  bool hasDrawerWithNoIncomingDataCables = false;
5176 
5177  if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5178  {
5179  VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5180  if (inputDrawer)
5181  {
5182  hasDrawerWithNoIncomingCables = true;
5183  hasDrawerWithNoIncomingDataCables = true;
5184  vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5185  foreach (VuoRendererPort *childPort, childPorts)
5186  {
5187  if (childPort->getBase()->getConnectedCables(true).size() > 0)
5188  hasDrawerWithNoIncomingCables = false;
5189  if (childPort->effectivelyHasConnectedDataCable(true))
5190  hasDrawerWithNoIncomingDataCables = false;
5191  }
5192  }
5193 
5194  string typeKey = inputPort->getDataType()->getModuleKey();
5195  if (!hasDrawerWithNoIncomingDataCables)
5196  requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5197  }
5198 
5199  bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5200  if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5201  inputEventSourceRequired = true;
5202  }
5203 
5204  // Inventory required output port types (connected data outputs) in the node to be replaced.
5205  map<string, int> requiredOutputs;
5206  bool outputEventSourceRequired = false;
5207  foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5208  {
5209  if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5210  {
5211  string typeKey = outputPort->getDataType()->getModuleKey();
5212  requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5213  }
5214 
5215  if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5216  outputEventSourceRequired = true;
5217  }
5218 
5219  // Inventory available input port types in the candidate replacement node.
5220  bool inputEventSourceAvailable = false;
5221  map<string, int> availableInputs;
5222  foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5223  {
5224  VuoType *dataType = (inputPortClass->hasCompiler()?
5225  static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5226  if (dataType)
5227  {
5228  string typeKey = dataType->getModuleKey();
5229  availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5230  }
5231  }
5232 
5234  inputEventSourceAvailable = true;
5235 
5236  // Inventory available output port types in the candidate replacement node.
5237  bool outputEventSourceAvailable = false;
5238  map<string, int> availableOutputs;
5239  foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5240  {
5241  VuoType *dataType = (outputPortClass->hasCompiler()?
5242  static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5243  if (dataType)
5244  {
5245  string typeKey = dataType->getModuleKey();
5246  availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5247  }
5248  }
5249 
5251  outputEventSourceAvailable = true;
5252 
5253  // Check whether the candidate replacement node meets input data requirements.
5254  for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5255  {
5256  string typeKey = it->first;
5257  int typeRequiredCount = it->second;
5258  if (availableInputs[typeKey] < typeRequiredCount)
5259  return false;
5260  }
5261 
5262  // Check whether the candidate replacement node meets output data requirements.
5263  for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5264  {
5265  string typeKey = it->first;
5266  int typeRequiredCount = it->second;
5267  if (availableOutputs[typeKey] < typeRequiredCount)
5268  return false;
5269  }
5270 
5271  if (inputEventSourceRequired && !inputEventSourceAvailable)
5272  return false;
5273 
5274  if (outputEventSourceRequired && !outputEventSourceAvailable)
5275  return false;
5276 
5277  return true;
5278 }
5279 
5284 bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5285 {
5286  // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5288  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5289 
5290  if (!specializedNodeClass)
5291  return false;
5292 
5293  VuoPortClass *portClass = port->getBase()->getClass();
5294  VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5295  if (!originalGenericType)
5296  return false;
5297 
5298  // If this port belongs to an attachment connected to a port that is not revertible, then
5299  // this port cannot be reverted, either.
5300  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(port->getUnderlyingParentNode());
5301  if (attachment)
5302  {
5303  VuoPort *hostPort = attachment->getUnderlyingHostPort();
5304  if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5305  return false;
5306  }
5307 
5308  return true;
5309 }
5310 
5329 VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5330 {
5331  string publishedPortName = ((! name.empty())?
5332  name :
5334  bool isPublishedInput = port->getRenderer()->getInput();
5335  VuoType *portType = port->getRenderer()->getDataType();
5336  VuoPublishedPort *publishedPort = NULL;
5337 
5338  // If merging is enabled:
5339  // Check whether this composition has a pre-existing externally visible published port
5340  // that has the requested name and type and that can accommodate the newly published internal port.
5341  // If so, add this internal port as a connected port for the existing alias.
5342  bool performedMerge = false;
5343  if (attemptMerge)
5344  {
5345  publishedPort = (isPublishedInput ?
5346  getBase()->getPublishedInputPortWithName(publishedPortName) :
5347  getBase()->getPublishedOutputPortWithName(publishedPortName));
5348 
5349  if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5350  {
5351  if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5352  {
5353  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5354  updatePortConstant(static_cast<VuoCompilerInputEventPort *>(port->getCompiler()),
5355  publishedInputPort->getInitialValue(),
5356  false);
5357  }
5358 
5359  performedMerge = true;
5360  }
5361  }
5362 
5363 
5364  // Otherwise, create a new externally visible published port with a unique name derived from
5365  // the specified name, containing the current port as its lone connected internal port.
5366  if (! performedMerge)
5367  {
5368  publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5369  if (isPublishedInput && type)
5370  {
5371  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5372  publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5373  }
5374  }
5375 
5376  addPublishedPort(publishedPort, isPublishedInput);
5377 
5378  VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5379  dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5380  createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5381 
5382  VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5383 
5384  if (! existingPublishedCable)
5385  {
5386  VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5387  addCable(publishedCable);
5388  }
5389 
5390  if (mergePerformed != NULL)
5391  *mergePerformed = performedMerge;
5392 
5393  return rendererPublishedPort;
5394 }
5395 
5400 VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5401 {
5402  VuoCable *publishedCable = NULL;
5403  bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5404 
5405  if (creatingPublishedInputCable)
5406  {
5407  // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5408  VuoPort *fromPort = externalPort;
5409  VuoNode *fromNode = this->publishedInputNode;
5410 
5411  VuoPort *toPort = internalPort;
5412  VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5413 
5414  publishedCable = (new VuoCompilerCable(NULL,
5415  NULL,
5416  toNode->getCompiler(),
5417  (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5418  publishedCable->setFrom(fromNode, fromPort);
5419  }
5420 
5421  else
5422  {
5423  // If creating a published output cable, it will need to have an associated VuoCompilerCable
5424  // even though we don't currently construct a VuoCompilerNode for the published output node.
5425  VuoPort *fromPort = internalPort;
5426  VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5427 
5428  VuoPort *toPort = externalPort;
5429  VuoNode *toNode = this->publishedOutputNode;
5430 
5431  publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5432  (VuoCompilerPort *)(fromPort->getCompiler()),
5433  NULL,
5434  NULL))->getBase();
5435  publishedCable->setTo(toNode, toPort);
5436  }
5437 
5438  if (forceEventOnlyPublication)
5439  publishedCable->getCompiler()->setAlwaysEventOnly(true);
5440 
5441  return publishedCable;
5442 }
5443 
5455 void VuoEditorComposition::addActiveProtocol(VuoProtocol *protocol, bool useUndoStack)
5456 {
5457  vector<VuoPublishedPort *> publishedPortsToAdd;
5458  map<VuoPublishedPort *, string> publishedPortsToRename;
5459 
5460  // Remove the previously active protocol.
5461  VuoProtocol *previousActiveProtocol = this->activeProtocol;
5462  bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5463 
5464  bool portChangesMadeDuringProtocolRemoval = false;
5465  if (removingPreviousProtocol)
5466  portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5467 
5468  if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5469  {
5470  VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5471  useUndoStack = true;
5472  }
5473 
5474  // Add the newly active protocol.
5475  this->activeProtocol = protocol;
5476  if (!protocol)
5477  return;
5478 
5479  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5480  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5481  {
5482  string portName = i->first;
5483  string portType = i->second;
5484 
5485  bool compositionHadCompatiblePort = false;
5486  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5487  if (preexistingPublishedPort)
5488  {
5489  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5490 
5491  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5492  (!preexistingType && (portType == "")));
5493  if (portTypesMatch)
5494  {
5495  compositionHadCompatiblePort = true;
5496  preexistingPublishedPort->setProtocolPort(true);
5497  }
5498  else
5499  {
5500  compositionHadCompatiblePort = false;
5501  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5502  }
5503  }
5504 
5505  if (!compositionHadCompatiblePort)
5506  {
5507  VuoType *type = compiler->getType(portType)->getBase();
5508  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5509  publishedPort->setProtocolPort(true);
5510 
5511  if (!useUndoStack)
5512  addPublishedPort(publishedPort, true);
5513  else
5514  publishedPortsToAdd.push_back(publishedPort);
5515 
5516  createRendererForPublishedPortInComposition(publishedPort, true);
5517  }
5518  }
5519 
5520  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5521  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5522  {
5523  string portName = i->first;
5524  string portType = i->second;
5525 
5526  bool compositionHadCompatiblePort = false;
5527  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5528  if (preexistingPublishedPort)
5529  {
5530  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5531  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5532  (!preexistingType && (portType == "")));
5533  if (portTypesMatch)
5534  {
5535  compositionHadCompatiblePort = true;
5536  preexistingPublishedPort->setProtocolPort(true);
5537  }
5538  else
5539  {
5540  compositionHadCompatiblePort = false;
5541  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5542  }
5543  }
5544 
5545  if (!compositionHadCompatiblePort)
5546  {
5547  VuoType *type = compiler->getType(portType)->getBase();
5548  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5549  publishedPort->setProtocolPort(true);
5550 
5551  if (!useUndoStack)
5552  addPublishedPort(publishedPort, false);
5553  else
5554  publishedPortsToAdd.push_back(publishedPort);
5555 
5556  createRendererForPublishedPortInComposition(publishedPort, false);
5557  }
5558  }
5559 
5560  if (useUndoStack)
5561  {
5562  bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5563  if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5564  {
5565  set<VuoPublishedPort *> publishedPortsToRemove;
5566  bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5567  bool endUndoStackMacro = true;
5568  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5569  }
5570  }
5571 
5572  emit activeProtocolChanged();
5573 }
5574 
5582 string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5583 {
5584  string modifiedPortName = portName;
5585  if (modifiedPortName.length() > 0)
5586  modifiedPortName[0] = toupper(modifiedPortName[0]);
5587  modifiedPortName = "some" + modifiedPortName;
5588 
5589  return modifiedPortName;
5590 }
5591 
5610 {
5612 
5613  set<VuoPublishedPort *> publishedPortsToRemove;
5614  map<VuoPublishedPort *, string> publishedPortsToRename;
5615 
5616  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5617  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5618  {
5619  string portName = i->first;
5620  string portType = i->second;
5621 
5622  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5623  if (preexistingPublishedPort)
5624  {
5625  bool portCompatibleAcrossProtocolTransition = false;
5626  if (replacementProtocol)
5627  {
5628  vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5629  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5630  {
5631  string replacementPortName = i->first;
5632  string replacementPortType = i->second;
5633 
5634  if ((portName == replacementPortName) && (portType == replacementPortType))
5635  portCompatibleAcrossProtocolTransition = true;
5636  }
5637  }
5638 
5639  if (preexistingPublishedPort->getConnectedCables(true).empty())
5640  publishedPortsToRemove.insert(preexistingPublishedPort);
5641  else if (!portCompatibleAcrossProtocolTransition)
5642  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5643  }
5644  }
5645 
5646  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5647  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5648  {
5649  string portName = i->first;
5650  string portType = i->second;
5651 
5652  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5653  if (preexistingPublishedPort)
5654  {
5655  bool portCompatibleAcrossProtocolTransition = false;
5656  if (replacementProtocol)
5657  {
5658  vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5659  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5660  {
5661  string replacementPortName = i->first;
5662  string replacementPortType = i->second;
5663 
5664  if ((portName == replacementPortName) && (portType == replacementPortType))
5665  portCompatibleAcrossProtocolTransition = true;
5666  }
5667  }
5668 
5669  if (preexistingPublishedPort->getConnectedCables(true).empty())
5670  publishedPortsToRemove.insert(preexistingPublishedPort);
5671  else if (!portCompatibleAcrossProtocolTransition)
5672  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5673  }
5674  }
5675 
5676  // If we are removing any ports, the composition will no longer be deemed to adhere to the
5677  // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5678  if (!publishedPortsToRemove.empty())
5679  publishedPortsToRename.clear();
5680 
5681  bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5682  if (portChangesRequired)
5683  {
5684  vector<VuoPublishedPort *> publishedPortsToAdd;
5685  bool beginUndoStackMacro = true;
5686  bool endUndoStackMacro = !replacementProtocol;
5687  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5688  }
5689 
5690  emit activeProtocolChanged();
5691 
5692  return portChangesRequired;
5693 }
5694 
5702 {
5703  if ((activeProtocol != protocol) || !activeProtocol)
5704  return;
5705 
5706  activeProtocol = NULL;
5707 
5708  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5709  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5710  {
5711  string portName = i->first;
5712  string portType = i->second;
5713 
5714  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5715  if (preexistingPublishedPort)
5716  preexistingPublishedPort->setProtocolPort(false);
5717  }
5718 
5719  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5720  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5721  {
5722  string portName = i->first;
5723  string portType = i->second;
5724 
5725  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5726  if (preexistingPublishedPort)
5727  preexistingPublishedPort->setProtocolPort(false);
5728  }
5729 
5730  emit activeProtocolChanged();
5731 }
5732 
5738 {
5739  return activeProtocol;
5740 }
5741 
5747 {
5748  if (!activeProtocol)
5749  return NULL;
5750 
5751  return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5752 }
5753 
5757 void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5758 {
5759  VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5760 
5761  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
5762  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
5763  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
5764 
5765  if (shouldUpdateUi)
5766  emit publishedPortModified();
5767 }
5768 
5775 int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5776 {
5777  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5778  if (shouldUpdateUi && publishedPort->isProtocolPort())
5780 
5781  int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5782 
5783  if (shouldUpdateUi)
5784  emit publishedPortModified();
5785 
5786  return removalResult;
5787 }
5788 
5794 {
5795  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5796  if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5798 
5799  VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5800 
5801  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort->getBase());
5802  portWithStaticIdentifier[staticPortIdentifier] = publishedPort->getBase();
5803  staticIdentifierForPort[publishedPort->getBase()] = staticPortIdentifier;
5804 
5805  emit publishedPortModified();
5806 }
5807 
5815 void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5816 {
5817  bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5818  VuoRendererPort *fixedPort = NULL;
5819 
5820  if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5821  fixedPort = cable->getFromPort()->getRenderer();
5822 
5823  else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5824  fixedPort = cable->getToPort()->getRenderer();
5825 
5826  if (fixedPort)
5827  {
5828  highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5829  emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5830  }
5831 }
5832 
5838 void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5839 {
5840  QList<QGraphicsItem *> compositionComponents = items();
5841  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5842  {
5843  QGraphicsItem *compositionComponent = *i;
5844  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5845  if (rn)
5846  {
5847  // Check for eligible internal input ports.
5848  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5849  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
5850  updateEligibilityHighlightingForPort((*inputPort)->getRenderer(), port, !cable->effectivelyCarriesData());
5851 
5852  // Check for eligible internal output ports.
5853  vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
5854  for(vector<VuoPort *>::iterator outputPort = outputPorts.begin(); outputPort != outputPorts.end(); ++outputPort)
5855  updateEligibilityHighlightingForPort((*outputPort)->getRenderer(), port, !cable->effectivelyCarriesData());
5856  }
5857 
5858  // Fade out cables that aren't relevant to the current cable drag.
5859  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5860  if (rc && rc != cable)
5861  {
5862  QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
5863  rc->setCacheMode(QGraphicsItem::NoCache);
5864  rc->updateGeometry();
5865 
5866  VuoPort *otherCablePort = port->getInput()
5867  ? rc->getBase()->getFromPort()
5868  : rc->getBase()->getToPort();
5869 
5870  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(otherCablePort? otherCablePort->getRenderer() : NULL,
5871  port,
5872  !cable->effectivelyCarriesData());
5873 
5874  // Don't apply extra highlighting to compatible, already-connected cables.
5875  if (highlight == VuoRendererColors::standardHighlight)
5876  highlight = VuoRendererColors::noHighlight;
5877 
5878  rc->setEligibilityHighlight(highlight);
5879 
5880  rc->setCacheMode(normalCacheMode);
5881  }
5882  }
5883 
5884  // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
5885  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5886  updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(*i));
5887 }
5888 
5893 void VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
5894  VuoRendererPort *fixedPort,
5895  bool eventOnlyConnection)
5896 {
5897  QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
5898  portToHighlight->setCacheMode(QGraphicsItem::NoCache);
5899 
5900  portToHighlight->updateGeometry();
5901 
5902  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection);
5903 
5904  portToHighlight->setEligibilityHighlight(highlight);
5905  VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
5906  if (typecastPortToHighlight)
5907  typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
5908 
5909  portToHighlight->setCacheMode(normalCacheMode);
5910 
5911  if (typecastPortToHighlight)
5912  updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection);
5913 }
5914 
5925 {
5926  // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
5927  VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
5928  VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
5929 
5930  VuoRendererPort *fromPort;
5931  VuoRendererPort *toPort;
5932  bool forwardConnection;
5933  if (fixedPort->getOutput())
5934  {
5935  fromPort = fixedPort;
5936  toPort = portToHighlight;
5937  forwardConnection = true;
5938  }
5939  else
5940  {
5941  fromPort = portToHighlight;
5942  toPort = fixedPort;
5943  forwardConnection = false;
5944  }
5945 
5946  bool directConnectionPossible;
5947 
5948  // Temporarily disallow direct cable connections between published inputs and published outputs.
5949  // @todo: Allow for https://b33p.net/kosada/node/7756 .
5950  if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
5951  directConnectionPossible = false;
5952  else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
5953  directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
5954  else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
5955  directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
5956  else // both ports are internal canvas ports
5957  directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
5958 
5960  if (directConnectionPossible)
5962  else if (!findBridgingSolutions(fromPort, toPort, forwardConnection).empty())
5964  else if (fixedPort == portToHighlight)
5965  highlight = VuoRendererColors::noHighlight;
5966  else
5968 
5969  return highlight;
5970 }
5971 
5987 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
5988  VuoRendererPort *toPort,
5989  bool eventOnlyConnection,
5990  bool forwardConnection)
5991 {
5992  VuoRendererPort *portToRespecialize = NULL;
5993  string respecializedTypeName = "";
5994 
5995  return canConnectDirectlyWithRespecializationNondestructively(fromPort,
5996  toPort,
5997  eventOnlyConnection,
5998  forwardConnection,
5999  &portToRespecialize,
6000  respecializedTypeName);
6001 }
6002 
6013 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6014  VuoRendererPort *toPort,
6015  bool eventOnlyConnection,
6016  bool forwardConnection,
6017  VuoRendererPort **portToRespecialize,
6018  string &respecializedTypeName)
6019 {
6020  *portToRespecialize = NULL;
6021  respecializedTypeName = "";
6022 
6023  bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6024  toPort,
6025  eventOnlyConnection,
6026  forwardConnection,
6027  portToRespecialize,
6028  respecializedTypeName);
6029  if (!canConnectWithRespecialization)
6030  return false;
6031 
6032  if (canConnectWithRespecialization && !portToRespecialize)
6033  return true;
6034 
6035  bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6036  if (!nondestructive)
6037  {
6038  *portToRespecialize = NULL;
6039  respecializedTypeName = "";
6040  }
6041  return nondestructive;
6042 }
6043 
6049 bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6050 {
6051  map<VuoNode *, string> nodesToReplace;
6052  set<VuoCable *> cablesToDelete;
6053  createReplacementsToUnspecializePort(portToUnspecialize, nodesToReplace, cablesToDelete);
6054 
6055  // Check whether unspecialization would disconnect any existing cables
6056  // (other than the cable that would normally be displaced by the new cable connection).
6057  if (cablesToDelete.empty())
6058  return true;
6059 
6060  else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6061  return true;
6062 
6063  return false;
6064 }
6065 
6085 bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6086  VuoRendererPort *toPort,
6087  bool eventOnlyConnection,
6088  bool forwardConnection,
6089  VuoRendererPort **portToRespecialize,
6090  string &respecializedTypeName)
6091 {
6092  // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6093 
6094  *portToRespecialize = NULL;
6095  respecializedTypeName = "";
6096 
6097  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6098  if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6099  return true;
6100 
6101  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6102  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6103  return true;
6104 
6105  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6106  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6107 
6108  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6109  return false;
6110 
6111  VuoType *currentFromDataType = fromPort->getDataType();
6112  VuoType *currentToDataType = toPort->getDataType();
6113 
6114  if (!(currentFromDataType && currentToDataType))
6115  return false;
6116 
6118  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6119  return false;
6120 
6121  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6122  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6123 
6124  VuoGenericType *originalFromGenericType = NULL;
6125  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6126  {
6128  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6129  if (fromSpecializedNodeClass)
6130  {
6131  VuoPortClass *portClass = fromPort->getBase()->getClass();
6132  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6133  }
6134  }
6135 
6136  VuoGenericType *originalToGenericType = NULL;
6137  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6138  {
6140  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6141  if (toSpecializedNodeClass)
6142  {
6143  VuoPortClass *portClass = toPort->getBase()->getClass();
6144  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6145  }
6146  }
6147 
6148  // Determine whether the port at each endpoint is 1) generic, or
6149  // 2) specialized and currently revertible, or 3) effectively static.
6150  bool fromPortIsGeneric = currentFromGenericType;
6151  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6152  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6153 
6154  bool toPortIsGeneric = currentToGenericType;
6155  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6156  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6157 
6158  // Figure out which port to try to respecialize, and to what type.
6159  set<string> compatibleTypes;
6160  string specializedType = "";
6161  VuoRendererPort *portToTryToRespecialize = NULL;
6162 
6163  // Case: One port static, one port specialized.
6164  if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6165  {
6166  VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6167  specializedType = staticPort->getDataType()->getModuleKey();
6168  portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6169  }
6170 
6171  // Case: One port specialized, other port generic or specialized.
6172  else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6173  {
6174  VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6175  bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6176 
6177  VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6178  bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6179 
6180  // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6181  /*
6182  if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6183  {
6184  specializedType = dragDestination->getDataType()->getModuleKey();
6185  portToTryToRespecialize = dragSource;
6186  }
6187  else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6188  {
6189  specializedType = dragSource->getDataType()->getModuleKey();
6190  portToTryToRespecialize = dragDestination;
6191  }
6192  else
6193  */
6194 
6195  if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6196  {
6197  specializedType = dragSource->getDataType()->getModuleKey();
6198  portToTryToRespecialize = dragDestination;
6199  }
6200  }
6201 
6202  // @todo https://b33p.net/kosada/node/10481 Other cases.
6203  else
6204  return false;
6205 
6206  if (portToTryToRespecialize)
6207  compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6208 
6209  bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6210 
6211  if (portsAreCompatible)
6212  {
6213  *portToRespecialize = portToTryToRespecialize;
6214  respecializedTypeName = specializedType;
6215  }
6216 
6217  return portsAreCompatible;
6218 }
6219 
6226 void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6227 {
6228  VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6229  if (drawer)
6230  {
6232  foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6234  bestEligibility = VuoRendererColors::standardHighlight;
6236  && bestEligibility != VuoRendererColors::standardHighlight)
6237  bestEligibility = VuoRendererColors::subtleHighlight;
6238 
6239  // If this drawer has no eligible ports, fade it out.
6240  {
6241  QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6242  drawer->setCacheMode(QGraphicsItem::NoCache);
6243  drawer->updateGeometry();
6244 
6245  drawer->setEligibilityHighlight(bestEligibility);
6246 
6247  drawer->setCacheMode(normalCacheMode);
6248  }
6249 
6250  // If this drawer does have eligible ports, ensure its host port isn't faded out, so the port name is legible.
6251  if (drawer->getRenderedHostPort()
6252  && drawer->getRenderedHostPort()->getRenderer())
6253  {
6254  VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6255 
6256  QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6257  hostPort->setCacheMode(QGraphicsItem::NoCache);
6258  hostPort->updateGeometry();
6259 
6260  hostPort->setEligibilityHighlight(MIN(bestEligibility, hostPort->eligibilityHighlight()));
6261 
6262  hostPort->setCacheMode(normalCacheMode);
6263  }
6264  }
6265 }
6266 
6271 {
6274 }
6275 
6295  VuoRendererPort *toPort,
6296  bool toPortIsDragDestination,
6297  VuoRendererPort **portToSpecialize,
6298  string &specializedTypeName,
6299  string &typecastToInsert)
6300 {
6301  *portToSpecialize = NULL;
6302  specializedTypeName = "";
6303 
6304  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6305  map<string, string> specializedTypeNameForTypecast;
6306 
6307  vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6308  bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6309 
6310  if (!solutionSelected)
6311  return false;
6312 
6313  if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6314  *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6315  if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6316  specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6317 
6318  return true;
6319 }
6320 
6339 bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6340  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6341  map<string, string> specializedTypeNameForTypecast,
6342  string &selectedTypecast)
6343 {
6344  if (suitableTypecasts.empty())
6345  {
6346  selectedTypecast = "";
6347  return false;
6348  }
6349 
6350  else if (suitableTypecasts.size() == 1)
6351  {
6352  selectedTypecast = suitableTypecasts[0];
6353  return true;
6354  }
6355 
6356  else
6357  return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6358 }
6359 
6365 bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort, VuoType *candidateFromType, VuoType *candidateToType)
6366 {
6367  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6368  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6369 
6370  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput &&
6371  fromPort->getBase()->getClass()->hasCompiler() &&
6372  toPort->getBase()->getClass()->hasCompiler()))
6373  {
6374  return false;
6375  }
6376 
6377  VuoType *inType = (candidateFromType? candidateFromType : static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6378  VuoType *outType = (candidateToType? candidateToType : static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6379 
6380  // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6381  if (inType && (inType->getModuleKey() == "VuoBoolean") && outType && (outType->getModuleKey() == "VuoInteger"))
6382  {
6383  bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6388  );
6389 
6390  if (toNodeUsesIndex)
6391  return false;
6392  }
6393 
6394  return true;
6395 }
6396 
6415  VuoRendererPort *toPort,
6416  bool toPortIsDragDestination)
6417 {
6418  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6419  map<string, string> specializedTypeNameForTypecast;
6420  return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast);
6421 }
6422 
6433  VuoRendererPort *toPort,
6434  bool toPortIsDragDestination,
6435  map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6436  map<string, string> &specializedTypeNameForTypecast)
6437 {
6438  portToSpecializeForTypecast.clear();
6439  specializedTypeNameForTypecast.clear();
6440  vector<string> suitableTypecasts;
6441 
6442  // Temporarily disallow direct cable connections between published inputs and published outputs.
6443  // @todo: Allow for https://b33p.net/kosada/node/7756 .
6444  if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6445  return suitableTypecasts;
6446 
6447  // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6448  {
6449  VuoRendererPort *portToSpecialize = NULL;
6450  string specializedTypeName = "";
6451  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6452  {
6453  suitableTypecasts.push_back("");
6454  portToSpecializeForTypecast[""] = portToSpecialize;
6455  specializedTypeNameForTypecast[""] = specializedTypeName;
6456 
6457  return suitableTypecasts;
6458  }
6459  }
6460 
6461  VuoType *currentFromDataType = fromPort->getDataType();
6462  VuoType *currentToDataType = toPort->getDataType();
6463 
6464  if (!(currentFromDataType && currentToDataType))
6465  return suitableTypecasts;
6466 
6468  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6469  return suitableTypecasts;
6470 
6471  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6472  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6473 
6474  VuoGenericType *originalFromGenericType = NULL;
6475  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6476  {
6478  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6479  if (fromSpecializedNodeClass)
6480  {
6481  VuoPortClass *portClass = fromPort->getBase()->getClass();
6482  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6483  }
6484  }
6485 
6486  VuoGenericType *originalToGenericType = NULL;
6487  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6488  {
6490  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6491  if (toSpecializedNodeClass)
6492  {
6493  VuoPortClass *portClass = toPort->getBase()->getClass();
6494  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6495  }
6496  }
6497 
6498  // Determine whether the port at each endpoint is:
6499  // 1) generic (unspecialized), or
6500  // 2) specialized and currently revertible, or
6501  // 3) effectively static.
6502  bool fromPortIsGeneric = currentFromGenericType;
6503  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6504  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6505 
6506  bool toPortIsGeneric = currentToGenericType;
6507  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6508  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6509 
6510  // No typeconversion or specialization options between two unspecialized generic ports.
6511  if (fromPortIsGeneric && toPortIsGeneric)
6512  return suitableTypecasts;
6513 
6514  // Typeconversion options but no specialization options between two static ports.
6515  else if (fromPortIsStatic && toPortIsStatic)
6516  {
6517  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6518  suitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6519  return suitableTypecasts;
6520  }
6521 
6522  // Remaining combinations might require (re-)specializing one port or the other.
6523  // Figure out which port to consider (re-)specializing.
6524  bool specializeToPort = true;
6525  if (toPortIsGeneric)
6526  specializeToPort = true;
6527  else if (fromPortIsGeneric)
6528  specializeToPort = false;
6529  else if (fromPortIsSpecialized && toPortIsStatic)
6530  specializeToPort = false;
6531  else if (fromPortIsStatic && toPortIsSpecialized)
6532  specializeToPort = true;
6533  else if (fromPortIsSpecialized && toPortIsSpecialized)
6534  specializeToPort = toPortIsDragDestination;
6535 
6536  // Now that ports have been categorized, figure out what combinations of (re-)specialization
6537  // and/or typeconversion we can use to bridge them.
6538  set<string> compatibleTypes;
6539  if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6540  compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6541  else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6542  compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6543 
6544  // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6545  // non-destructively unspecializable, since it already has the appropriate specialization.
6546  compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6547 
6548  foreach (string compatibleTypeName, compatibleTypes)
6549  {
6550  VuoCompilerType *compatibleSpecializedType = compiler->getType(compatibleTypeName);
6551  VuoType *candidateFromType = specializeToPort? currentFromDataType : compatibleSpecializedType->getBase();
6552  VuoType *candidateToType = specializeToPort? compatibleSpecializedType->getBase() : currentToDataType;
6553 
6554  if (compatibleSpecializedType && portsPassSanityCheckToTypeconvert(fromPort,
6555  toPort,
6556  candidateFromType,
6557  candidateToType))
6558  {
6559  // Re-specialization without typeconversion may be possible.
6560  if (candidateFromType == candidateToType)
6561  {
6562  suitableTypecasts.push_back("");
6563  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6564  specializedTypeNameForTypecast[""] = compatibleSpecializedType->getBase()->getModuleKey();
6565  }
6566 
6567  vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromType, candidateToType);
6568  foreach (string typecast, suitableTypecastsForCurrentTypes)
6569  {
6570  suitableTypecasts.push_back(typecast);
6571  portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6572  specializedTypeNameForTypecast[typecast] = compatibleSpecializedType->getBase()->getModuleKey();
6573  }
6574  }
6575  }
6576 
6577  return suitableTypecasts;
6578 }
6579 
6592 bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6593  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6594  map<string, string> specializedTypeNameForTypecast,
6595  string &selectedTypecast)
6596 {
6597  QMenu typecastMenu(views()[0]->viewport());
6598  typecastMenu.setSeparatorsCollapsible(false);
6599  QString spacer(" ");
6600 
6601  // Inventory specialization options
6602  set <pair<VuoRendererPort *, string> > specializationDetails;
6603  vector<string> typeconversionOptionsRequiringNoSpecialization;
6604  foreach (string typecastClassName, suitableTypecasts)
6605  {
6606  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6607  string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6608  specializationDetails.insert(std::make_pair(portToSpecialize,
6609  specializedTypeName));
6610 
6611  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6612  if (portAlreadyHasTargetType)
6613  typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6614  }
6615 
6616  // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6617  // specialization heading under which multiple typeconversion options may be listed.
6618  // Selecting this item itself will invoke the specialization.
6619  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6620  {
6621  QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6622  QAction *typecastAction = typecastMenu.addAction(menuText);
6623  typecastAction->setData(QVariant(""));
6624  }
6625 
6626  // If there are bridging options that require no specialization, list them next.
6627  bool includingTypeconvertWithNoSpecializationHeader = false;
6628  if (typeconversionOptionsRequiringNoSpecialization.size() >= 1)
6629  {
6630  if (typecastMenu.actions().size() >= 1)
6631  typecastMenu.addSeparator();
6632 
6633  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6634  string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6635 
6636  if (portToSpecialize && !specializedTypeName.empty())
6637  {
6638  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6639  QAction *typecastAction = typecastMenu.addAction(menuText);
6640  typecastAction->setEnabled(false);
6641  includingTypeconvertWithNoSpecializationHeader = true;
6642  }
6643  }
6644 
6645  foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6646  {
6647  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6648  if (typecastClass)
6649  {
6650  QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6651  typecastAction->setData(QVariant(typecastClassName.c_str()));
6652  }
6653  }
6654 
6655  // Now list the remaining bridging options.
6656  for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6657  {
6658  VuoRendererPort *portToSpecialize = i->first;
6659  string specializedTypeName = i->second;
6660 
6661  // We've already listed the no-typeconversion bridging option, so skip it here.
6662  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6663  (portToSpecializeForTypecast[""] == portToSpecialize) &&
6664  (specializedTypeNameForTypecast[""] == specializedTypeName))
6665  {
6666  continue;
6667  }
6668 
6669  // We've already listed the no-specialization bridging option, so skip it here.
6670  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6671  if (portAlreadyHasTargetType)
6672  {
6673  continue;
6674  }
6675 
6676  if (typecastMenu.actions().size() >= 1)
6677  typecastMenu.addSeparator();
6678 
6679  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6680  QAction *typecastAction = typecastMenu.addAction(menuText);
6681  typecastAction->setEnabled(false);
6682 
6683  // Inventory typeconversion options associated with this specialization option.
6684  foreach (string typecastClassName, suitableTypecasts)
6685  {
6686  if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6687  (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6688  {
6689  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6690  if (typecastClass)
6691  {
6692  QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6693  typecastAction->setData(QVariant(typecastClassName.c_str()));
6694  }
6695  }
6696  }
6697  }
6698 
6699  menuSelectionInProgress = true;
6700  QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6701  menuSelectionInProgress = false;
6702 
6703  selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6704  return selectedTypecastAction;
6705 }
6706 
6710 QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6711 {
6712  if (!portToSpecialize || specializedTypeName.empty())
6713  return "";
6714 
6715  bool isInput = portToSpecialize && portToSpecialize->getInput();
6716  QString typeDisplayName = compiler->getType(specializedTypeName)?
6717  formatTypeNameForDisplay(compiler->getType(specializedTypeName)->getBase()) :
6718  specializedTypeName.c_str();
6719 
6720  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6721 
6722  QString displayText;
6723  if (portAlreadyHasTargetType)
6724  {
6725  if (isInput)
6726  {
6727  //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6728  displayText = tr("Keep Input Port as %1");
6729  }
6730  else
6731  {
6732  //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6733  displayText = tr("Keep Output Port as %1");
6734  }
6735  }
6736  else
6737  {
6738  if (isInput)
6739  {
6740  //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6741  displayText = tr("Change Input Port to %1");
6742  }
6743  else
6744  {
6745  //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6746  displayText = tr("Change Output Port to %1");
6747  }
6748  }
6749 
6750  return displayText.arg(typeDisplayName);
6751 }
6752 
6758 {
6759  __block json_object *portValue = NULL;
6760 
6761  if (! port->getRenderer()->getDataType())
6762  return portValue;
6763 
6764  string runningPortIdentifier = staticIdentifierForPort[port];
6765  bool isInput = port->getRenderer()->getInput();
6766 
6767  void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6768  {
6769  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6770  if (topLevelComposition->isRunningThreadUnsafe())
6771  {
6772  portValue = isInput ?
6773  topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6774  topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6775  }
6776  });
6777  };
6778  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6779 
6780  return portValue;
6781 }
6782 
6786 string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6787 {
6788  return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6789 }
6790 
6797 {
6798  if (!staticPort)
6799  return "";
6800 
6801  // Published ports
6802  if (dynamic_cast<VuoPublishedPort *>(staticPort))
6803  return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
6804 
6805  // Internal ports
6806  // We might as well use the same naming scheme here as is used in the running composition,
6807  // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
6808  // node identifier has been explicitly set.
6809  string nodeIdentifier = "";
6810  if (parentNode && parentNode->hasCompiler())
6811  nodeIdentifier = parentNode->getCompiler()->getIdentifier();
6812  else if (staticPort->hasRenderer() &&
6813  staticPort->getRenderer()->getUnderlyingParentNode() &&
6814  staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->hasCompiler())
6815  {
6816  nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
6817  }
6818 
6819  if (staticPort->hasCompiler() && !nodeIdentifier.empty())
6820  {
6821  dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
6822  return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
6823  }
6824  else
6825  return "";
6826 }
6827 
6832 {
6833  return portWithStaticIdentifier[portID];
6834 }
6835 
6842 {
6843  if (port->hasRenderer())
6844  {
6845  if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
6846  {
6847  bool isPublishedInput = !port->getRenderer()->getInput();
6848  return (isPublishedInput? composition->getPublishedInputNode() :
6849  composition->getPublishedOutputNode());
6850  }
6851 
6852  else
6853  return port->getRenderer()->getUnderlyingParentNode()->getBase();
6854  }
6855 
6856  foreach (VuoNode *n, composition->getBase()->getNodes())
6857  {
6858  VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
6859  if (candidateInputPort == port)
6860  return n;
6861 
6862  VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
6863  if (candidateOutputPort == port)
6864  return n;
6865  }
6866 
6867  return NULL;
6868 }
6869 
6878 {
6879  map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
6880  if (popover != activePortPopovers.end())
6881  return popover->second;
6882 
6883  else
6884  return NULL;
6885 }
6886 
6894 void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
6895 {
6896  string portID = staticIdentifierForPort[rp->getBase()];
6897  bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
6898  if (!popoverJustClosedAtLastEvent)
6900 }
6901 
6906 {
6907  if (!popoverEventsEnabled)
6908  return;
6909 
6910  VuoPort *port = rp->getBase();
6911  string portID = staticIdentifierForPort[port];
6912 
6913  VUserLog("%s: Open popover for %s",
6914  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6915  portID.c_str());
6916 
6917  dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
6918 
6919  dispatch_sync(activePortPopoversQueue, ^{
6920 
6921  if (activePortPopovers.find(portID) == activePortPopovers.end())
6922  {
6923  // Assigning the popover a parent widget allows us to give it rounded corners
6924  // and a background fill that respects its rounded boundaries.
6925  VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
6926 
6927  connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
6928  connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
6929  VUserLog("%s: Detach popover for %s",
6930  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6931  portID.c_str());
6932  popoverDetached();
6933  });
6934  connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
6937 
6938  // Line up the top left of the dialog with the port.
6939  QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
6940 
6941  // Don't let popovers get cut off at the right or bottom edges of the canvas.
6942  const int cutoffMargin = 16;
6943  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
6944  if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
6945  portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
6946  if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
6947  portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
6948 
6949  QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
6950 
6951  const QPoint offset = QPoint(12, 6);
6952 
6953  QPoint popoverTopLeft = popoverLeftInView + offset;
6954  popover->move(popoverTopLeft);
6955  popover->show();
6956 
6957  activePortPopovers[portID] = popover;
6958 
6959  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
6960  updateDataInPortPopover(portID);
6961  });
6962  }
6963  });
6964  });
6965 }
6966 
6971 void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
6972 {
6973  if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
6975 }
6976 
6985 {
6986  VUserLog("%s: Close popover for %s",
6987  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6988  portID.c_str());
6989 
6990  VuoPortPopover *popover = NULL;
6991  map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
6992  if (i != activePortPopovers.end())
6993  {
6994  popover = i->second;
6995  activePortPopovers.erase(i);
6996  }
6997 
6998  if (popover)
6999  {
7000  popover->hide();
7001  delete popover;
7002  }
7003 
7004  VuoPort *port = portWithStaticIdentifier[portID];
7005  if (!port)
7006  return;
7007 
7008  bool isInput = port->getRenderer()->getInput();
7009 
7010  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7011  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7012  {
7013  dispatch_async(topLevelComposition->runCompositionQueue, ^{
7014  if (topLevelComposition->isRunningThreadUnsafe())
7015  {
7016  (isInput ?
7017  topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7018  topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7019  }
7020  });
7021  });
7022  });
7023 }
7024 
7028 void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7029 {
7030  dispatch_sync(activePortPopoversQueue, ^{
7031  disablePopoverForPort(portID);
7032  });
7033 }
7034 
7039 {
7040  disablePortPopovers();
7042 }
7043 
7048 {
7049  foreach (VuoErrorPopover *errorPopover, errorPopovers)
7050  {
7051  errorPopover->hide();
7052  errorPopover->deleteLater();
7053  }
7054 
7055  errorPopovers.clear();
7056 }
7057 
7062 void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7063 {
7064  dispatch_sync(activePortPopoversQueue, ^{
7065  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7066  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7067  {
7068  string portID = i->first;
7069  VuoPort *port = portWithStaticIdentifier[portID];
7070  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7071  disablePopoverForPort(portID);
7072  }
7073  });
7074 }
7075 
7080 {
7081  dispatch_sync(activePortPopoversQueue, ^{
7082  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7083  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7084  {
7085  string portID = i->first;
7086  VuoPort *port = portWithStaticIdentifier[portID];
7087  if (!port)
7088  disablePopoverForPort(portID);
7089  }
7090  });
7091 }
7092 
7098 {
7099  if (recordWhichPopoversClosed)
7100  portsWithPopoversClosedAtLastEvent.clear();
7101 
7102  dispatch_sync(activePortPopoversQueue, ^{
7103  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7104  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7105  {
7106  string portID = i->first;
7107  VuoPort *port = portWithStaticIdentifier[portID];
7108  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7109  {
7110  VuoPortPopover *popover = getActivePopoverForPort(portID);
7111  if (! (popover && popover->getDetached()))
7112  {
7113  disablePopoverForPort(portID);
7114  portsWithPopoversClosedAtLastEvent.insert(portID);
7115  }
7116  }
7117  }
7118  });
7119 }
7120 
7125 {
7126  moveDetachedPortPopoversBy(dx, dy);
7127  moveErrorPopoversBy(dx, dy);
7128 }
7129 
7133 void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7134 {
7135  foreach(VuoErrorPopover *errorPopover, errorPopovers)
7136  errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7137 }
7138 
7142 void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7143 {
7144  dispatch_sync(activePortPopoversQueue, ^{
7145  map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7146  for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7147  {
7148  VuoPortPopover *popover = i->second;
7149  if (popover && popover->getDetached())
7150  popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7151  }
7152  });
7153 }
7154 
7158 void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7159 {
7160  dispatch_sync(activePortPopoversQueue, ^{
7161  auto portPopovers = activePortPopovers;
7162  for (auto i : portPopovers)
7163  {
7164  VuoPortPopover *popover = i.second;
7165  if (popover && popover->getDetached())
7166  {
7167  id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7168  objc_msgSend(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7169  }
7170  }
7171  });
7172 }
7173 
7179 {
7180  dispatch_sync(activePortPopoversQueue, ^{
7181  for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7182  {
7183  string portID = i->first;
7184  VuoPort *port = portWithStaticIdentifier[portID];
7185  VuoPortPopover *popover = i->second;
7186 
7187  if ((! node) || (port && port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node)))
7188  QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7189  }
7190  });
7191 }
7192 
7205  string popoverCompositionIdentifier,
7206  string portID)
7207 {
7208  VuoPort *port = popoverComposition->portWithStaticIdentifier[portID];
7209  if (! port)
7210  return;
7211 
7212  string portSummary = (port->getRenderer()->getInput() ?
7213  runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7214  runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7215 
7216  dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7217  VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7218  if (popover)
7219  {
7220  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7221  QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7222  }
7223  });
7224 }
7225 
7234 {
7235  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7236  {
7237  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7238  if (topLevelComposition->isRunningThreadUnsafe())
7239  topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7240  });
7241  });
7242 }
7243 
7248 void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7249  bool receivedEvent, bool receivedData, string dataSummary)
7250 {
7251  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7252  {
7253  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7254  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7255 
7256  if (popover)
7257  {
7258  if (receivedEvent && receivedData)
7259  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7260  else if (receivedEvent)
7261  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7262  else if (receivedData)
7263  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7264  }
7265  });
7266  };
7267  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7268 }
7269 
7274 void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7275  bool sentEvent, bool sentData, string dataSummary)
7276 {
7277  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7278  {
7279  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7280  VuoPort *port = matchingComposition->portWithStaticIdentifier[portIdentifier];
7281  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7282 
7283  if (popover)
7284  {
7285  if (sentEvent && sentData)
7286  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7287  else if (sentEvent)
7288  QMetaObject::invokeMethod(popover, "updateLastEventTime", Qt::QueuedConnection);
7289  else if (sentData)
7290  QMetaObject::invokeMethod(popover, "updateCachedDataValue", Qt::QueuedConnection, Q_ARG(QString, dataSummary.c_str()));
7291  }
7292 
7293  if (matchingComposition->showEventsMode && sentEvent)
7294  {
7295  if (port && dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7296  {
7297  port->getRenderer()->setFiredEvent();
7298  matchingComposition->animatePort(port->getRenderer());
7299  }
7300  }
7301  });
7302  };
7303  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7304 }
7305 
7310 void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7311 {
7312  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7313  {
7314  dispatch_async(matchingComposition->runCompositionQueue, ^{
7315  if (matchingComposition->isRunningThreadUnsafe())
7316  {
7317  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7318  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7319  if (popover)
7320  QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7321  });
7322  }
7323  });
7324  };
7325  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7326 }
7327 
7332 void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7333 {
7334  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7335  {
7336  if (matchingComposition->showEventsMode)
7337  {
7338  dispatch_async(this->runCompositionQueue, ^{
7339  if (this->isRunningThreadUnsafe())
7340  {
7341  map<string, VuoNode *>::iterator i = matchingComposition->nodeWithGraphvizIdentifier.find(nodeIdentifier);
7342  if (i != nodeWithGraphvizIdentifier.end())
7343  {
7344  VuoNode *nodeInBaseComposition = i->second;
7345  VuoRendererNode *rn = nodeInBaseComposition->getRenderer();
7346  rn->setExecutionBegun();
7347  }
7348  }
7349  });
7350  }
7351  };
7352  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7353 }
7354 
7359 void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7360 {
7361  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7362  {
7363  if (matchingComposition->showEventsMode)
7364  {
7365  dispatch_async(this->runCompositionQueue, ^{
7366  if (this->isRunningThreadUnsafe())
7367  {
7368  map<string, VuoNode *>::iterator i = matchingComposition->nodeWithGraphvizIdentifier.find(nodeIdentifier);
7369  if (i != nodeWithGraphvizIdentifier.end())
7370  {
7371  VuoNode *nodeInBaseComposition = i->second;
7372  VuoRendererNode *rn = nodeInBaseComposition->getRenderer();
7373  rn->setExecutionEnded();
7374  }
7375  }
7376  });
7377  }
7378  };
7379  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7380 }
7381 
7389 {
7390  emit compositionStoppedItself();
7391 }
7392 
7399 {
7401 }
7402 
7407 {
7408  return showEventsMode;
7409 }
7410 
7415 {
7416  this->showEventsMode = showEventsMode;
7417 
7418  if (showEventsMode)
7419  {
7421 
7422  void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7423  {
7424  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7425  if (topLevelComposition->isRunningThreadUnsafe())
7426  topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7427  });
7428  };
7429  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7430  }
7431  else
7432  {
7434 
7435  void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7436  {
7437  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7438  if (topLevelComposition->isRunningThreadUnsafe())
7439  topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7440  });
7441  };
7442  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7443  }
7444 }
7445 
7450 {
7451  foreach (VuoCable *cable, getBase()->getCables())
7452  {
7453  if (cable->getCompiler()->getHidden() && !cable->isPublished())
7454  return true;
7455  }
7456 
7457  return false;
7458 }
7459 
7464 {
7465  foreach (VuoCable *cable, getBase()->getCables())
7466  {
7467  if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7468  return true;
7469  }
7470 
7471  return false;
7472 }
7473 
7478 QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7479 {
7480  VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7481  NULL,
7482  port->getOutput(),
7483  port->getRefreshPort(),
7484  port->getFunctionPort());
7485  animatedPort->setAnimated(true);
7486  animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7487  animatedPort->setParentItem(port->getRenderedParentNode());
7488 
7489  animation->setItem(animatedPort);
7490  animation->setScaleAt(0.0, 1, 1);
7491  animation->setScaleAt(0.999, 3, 3);
7492  animation->setScaleAt(1.0, 1, 1);
7493 
7494  QTimeLine *animationTimeline = animation->timeLine();
7495  animationTimeline->setFrameRange(0, 100);
7496  animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7497  animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7498 
7499  preparedAnimations.insert(animation);
7500  animationForTimeline[animation->timeLine()] = animation;
7501 
7502  connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7503  connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7504 
7505  return animation;
7506 }
7507 
7511 void VuoEditorComposition::animatePort(VuoRendererPort *port)
7512 {
7513  dispatch_async(dispatch_get_main_queue(), ^{
7514  QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7515  if (! animation)
7516  return;
7517 
7518  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7519 
7520  if (animation->timeLine()->state() == QTimeLine::Running)
7521  animation->timeLine()->setCurrentTime(0);
7522 
7523  else
7524  {
7525  animatedPort->setPos(port->pos());
7526  animatedPort->setVisible(true);
7527  animation->timeLine()->start();
7528  }
7529  });
7530 }
7531 
7536 QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7537 {
7538  vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7539 
7540  QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7541  qreal maxPercentAdvanced = -1;
7542 
7543  for (int i = 0; i < animations.size(); ++i)
7544  {
7545  QGraphicsItemAnimation *animation = animations[i];
7546  bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7547  bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7548 
7549  if (! animationPrepared)
7550  return setUpAnimationForPort(animation, port);
7551 
7552  else if (! animationRunning)
7553  return animation;
7554 
7555  // If all of the port's animations are already running, return the
7556  // one that has been running the longest.
7557  qreal percentAdvanced = animation->timeLine()->currentValue();
7558  if (percentAdvanced > maxPercentAdvanced)
7559  {
7560  mostAdvancedAnimation = animation;
7561  maxPercentAdvanced = percentAdvanced;
7562  }
7563  }
7564 
7565  // If no animation is even halfway complete, return NULL to indicate
7566  // that no animation is currently available.
7567  return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7568 }
7569 
7575 void VuoEditorComposition::updatePortAnimation(qreal value)
7576 {
7577  QTimeLine *animationTimeline = (QTimeLine *)sender();
7578  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7579  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7580  const qreal multiplier = 1000.;
7581  animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7582 }
7583 
7588 void VuoEditorComposition::endPortAnimation(void)
7589 {
7590  QTimeLine *animationTimeline = (QTimeLine *)sender();
7591  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7592  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7593  animatedPort->setVisible(false);
7594 }
7595 
7599 void VuoEditorComposition::setDisableDragStickiness(bool disable)
7600 {
7601  this->dragStickinessDisabled = disable;
7602 }
7603 
7608 {
7609  this->ignoreApplicationStateChangeEvents = ignore;
7610 }
7611 
7618 {
7619  this->popoverEventsEnabled = enable;
7620 }
7621 
7627 {
7628  nodeWithGraphvizIdentifier.clear();
7629  portWithStaticIdentifier.clear();
7630  staticIdentifierForPort.clear();
7631 
7632  foreach (VuoNode *node, getBase()->getNodes())
7633  registerNodeID(node);
7634 
7635  foreach (VuoPublishedPort *publishedPort, getBase()->getPublishedInputPorts())
7636  {
7637  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
7638  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
7639  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
7640  }
7641 
7642  foreach (VuoPort *publishedPort, getBase()->getPublishedOutputPorts())
7643  {
7644  string staticPortIdentifier = getIdentifierForStaticPort(publishedPort);
7645  portWithStaticIdentifier[staticPortIdentifier] = publishedPort;
7646  staticIdentifierForPort[publishedPort] = staticPortIdentifier;
7647  }
7648 }
7649 
7653 void VuoEditorComposition::registerNodeID(VuoNode *node)
7654 {
7655  if (node && node->hasCompiler())
7656  nodeWithGraphvizIdentifier[node->getCompiler()->getGraphvizIdentifier()] = node;
7657 
7658  foreach (VuoPort *port, node->getInputPorts())
7659  {
7660  string staticPortIdentifier = getIdentifierForStaticPort(port, node);
7661  portWithStaticIdentifier[staticPortIdentifier] = port;
7662  staticIdentifierForPort[port] = staticPortIdentifier;
7663  }
7664 
7665  foreach (VuoPort *port, node->getOutputPorts())
7666  {
7667  string staticPortIdentifier = getIdentifierForStaticPort(port, node);
7668  portWithStaticIdentifier[staticPortIdentifier] = port;
7669  staticIdentifierForPort[port] = staticPortIdentifier;
7670  }
7671 }
7672 
7677 {
7678  setRenderActivity(true, includePorts);
7679  refreshComponentAlphaLevelTimer->start();
7680 }
7681 
7686 {
7687  refreshComponentAlphaLevelTimer->stop();
7688  setRenderActivity(false);
7689 }
7690 
7707 bool VuoEditorComposition::validateProtocol(VuoEditorWindow *window, bool isExportingMovie)
7708 {
7709  // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7710  if (!activeProtocol)
7711  {
7712  VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7713  return false;
7714  }
7715 
7716  // Can events from at least one trigger reach at least one published output port? If not, report an error.
7717  if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7718  {
7719  QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7720  QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7721  "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7722 
7723  if (isExportingMovie)
7724  errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7725 
7727  QMessageBox messageBox(window);
7728  messageBox.setWindowFlags(Qt::Sheet);
7729  messageBox.setWindowModality(Qt::WindowModal);
7730  messageBox.setFont(fonts->dialogHeadingFont());
7731  messageBox.setTextFormat(Qt::RichText);
7732 
7733  messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7734  messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7735  messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7736  messageBox.setDefaultButton(QMessageBox::Ok);
7737 
7738  messageBox.setText(errorHeadline);
7739  messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7740 
7741  if (messageBox.exec() == QMessageBox::Help)
7742  {
7743  map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7744  map<QString, QString>::iterator i = examples.begin();
7745  if (i != examples.end())
7746  QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7747  }
7748  return false;
7749  }
7750 
7751  return true;
7752 }
7753 
7758 {
7759  return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7760 }
7761 
7766 {
7768 }
7769 
7773 string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7774 {
7775  string dir, file, ext;
7776  VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7777  return file;
7778 }
7779 
7787 {
7788  string customizedName = getBase()->getMetadata()->getCustomizedName();
7789  if (! customizedName.empty())
7790  return QString::fromStdString(customizedName);
7791 
7792  string name = getBase()->getMetadata()->getName();
7793  return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7794 }
7795 
7803 QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7804 {
7805  // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7806  // If the extensionless filename contains dot-delimited segments, use only the final segment.
7807  vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7808  string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7809  fileNameParts[fileNameParts.size()-2] :
7810  (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7811 
7812  // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7813  if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7814  {
7815  string formattedName = fileNameContentPart;
7816  if (formattedName.size() >= 1)
7817  formattedName[0] = toupper(formattedName[0]);
7818 
7819  return QString(formattedName.c_str());
7820  }
7821 
7822  // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
7823  return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
7824 }
7825 
7832 {
7833  QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
7834  if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
7835  {
7836  // If not an official Vuo nodeset, return the name as-is.
7837  return nodeSetName;
7838  }
7839 
7840  map<QString, QString> wordsToReformat;
7841  wordsToReformat["artnet"] = "Art-Net";
7842  wordsToReformat["bcf2000"] = "BCF2000";
7843  wordsToReformat["hid"] = "HID";
7844  wordsToReformat["midi"] = "MIDI";
7845  wordsToReformat["osc"] = "OSC";
7846  wordsToReformat["rss"] = "RSS";
7847  wordsToReformat["ui"] = "UI";
7848  wordsToReformat["url"] = "URL";
7849 
7850  QString nodeSetDisplayName = "";
7851  for (int i = 1; i < wordsInName.size(); ++i)
7852  {
7853  QString currentWord = wordsInName[i];
7854  if (currentWord.size() >= 1)
7855  {
7856  if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
7857  currentWord = wordsToReformat.at(currentWord.toLower());
7858  else
7859  currentWord[0] = currentWord[0].toUpper();
7860 
7861  nodeSetDisplayName += currentWord;
7862 
7863  if (i < wordsInName.size()-1)
7864  nodeSetDisplayName += " ";
7865  }
7866  }
7867  return nodeSetDisplayName;
7868 }
7869 
7876 {
7877  if (!type)
7878  return "(none)";
7879 
7880  string formattedTypeName = "";
7881  if (type->hasCompiler() && VuoCompilerType::isListType(type->getCompiler()))
7882  {
7883  string innerTypeName = VuoType::extractInnermostTypeName(type->getModuleKey());
7884  VuoCompilerType *innerType = compiler->getType(innerTypeName);
7885  if (innerType)
7886  {
7887  string formattedInnerTypeName = innerType->getBase()->getDefaultTitle();
7888  formattedTypeName = "List of " + formattedInnerTypeName + " elements";
7889  }
7890  }
7891  else
7892  formattedTypeName = type->getDefaultTitle();
7893 
7894  return formattedTypeName.c_str();
7895 }
7896 
7901 {
7902  if (!type)
7903  return "Event";
7904 
7905  // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
7906  else if (type->getDefaultTitle() == "2D Point")
7907  return "Point2D";
7908  else if (type->getDefaultTitle() == "3D Point")
7909  return "Point3D";
7910  else if (type->getDefaultTitle() == "4D Point")
7911  return "Point4D";
7912  else if (type->getDefaultTitle() == "2D Transform")
7913  return "Transform2D";
7914  else if (type->getDefaultTitle() == "3D Transform")
7915  return "Transform3D";
7916 
7917  return VuoRendererPort::sanitizePortIdentifier(formatTypeNameForDisplay(type)).toUtf8().constData();
7918 }
7919 
7923 bool VuoEditorComposition::nodeSetMenuActionLessThan(QAction *action1, QAction *action2)
7924 {
7925  QString item1Text = action1->text();
7926  QString item2Text = action2->text();
7927 
7928  // Ignore list prefixes
7929  const QString listPrefix = "List of ";
7930  const QString builtInTypePrefix = "Vuo";
7931 
7932  if (item1Text.startsWith(listPrefix))
7933  {
7934  item1Text.remove(0, listPrefix.length());
7935  if (item1Text.startsWith(builtInTypePrefix))
7936  item1Text.remove(0, builtInTypePrefix.length());
7937  }
7938 
7939  if (item2Text.startsWith(listPrefix))
7940  {
7941  item2Text.remove(0, listPrefix.length());
7942  if (item2Text.startsWith(builtInTypePrefix))
7943  item2Text.remove(0, builtInTypePrefix.length());
7944  }
7945 
7946  // Sort alphabetically by title.
7947  return (item1Text.compare(item2Text, Qt::CaseInsensitive) < 0);
7948 }
7949 
7954 bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
7955 {
7956  qreal item1Y = item1->scenePos().y();
7957  qreal item2Y = item2->scenePos().y();
7958 
7959  qreal item1X = item1->scenePos().x();
7960  qreal item2X = item2->scenePos().x();
7961 
7962  if (item1Y == item2Y)
7963  return (item1X < item2X);
7964 
7965  return item1Y < item2Y;
7966 }
7967 
7974 double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
7975 {
7976  // Compare keywords.
7977  vector<string> node1Keywords = node1->getKeywords();
7978  vector<string> node2Keywords = node2->getKeywords();
7979 
7980  // Compare node set names.
7981  if (node1->getNodeSet())
7982  node1Keywords.push_back(node1->getNodeSet()->getName());
7983 
7984  if (node2->getNodeSet())
7985  node2Keywords.push_back(node2->getNodeSet()->getName());
7986 
7987  // Compare tokens in node class display names.
7988  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
7989  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
7990  node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
7991 
7992  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
7993  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
7994  node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
7995 
7996  set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
7997  set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
7998 
7999  set<string> nodeKeywordsIntersection;
8000  std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8001  node2KeywordSet.begin(), node2KeywordSet.end(),
8002  std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8003 
8004  set<string> nodeKeywordsUnion = node1KeywordSet;
8005  nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8006 
8007  // Avoid division by zero.
8008  if (nodeKeywordsUnion.size() == 0)
8009  return 0;
8010 
8011  // Calculate Jaccard similarity.
8012  double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8013 
8014  return nodeSimilarity;
8015 }
8016 
8021 {
8022  emit compositionOnTop(top);
8023 }
8024 
8029 {
8030  emit publishedPortNameEditorRequested(port, false);
8031 }
8032 
8033 VuoEditorComposition::~VuoEditorComposition()
8034 {
8035  dispatch_sync(runCompositionQueue, ^{});
8036  dispatch_release(runCompositionQueue);
8037 
8038  preparedAnimations.clear();
8039  animationForTimeline.clear();
8040 
8041  moduleManager->deleteWhenReady(); // deletes compiler
8042 }
8043 
8048 {
8049  // Update the canvas color.
8050  setBackgroundTransparent(false);
8051 
8052  // Force repainting the entire canvas.
8053  setComponentCaching(QGraphicsItem::NoCache);
8056 }
8057 
8063 map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8064 {
8065  vector<VuoRendererPort *> sortedPortsToPublish;
8066  foreach (string portID, portsToPublish)
8067  {
8068  VuoPort *port = getPortWithStaticIdentifier(portID);
8069  if (port && port->hasRenderer())
8070  sortedPortsToPublish.push_back(port->getRenderer());
8071  }
8072  std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8073 
8074  map<string, string> publishedPortNames;
8075  foreach (VuoRendererPort *rp, sortedPortsToPublish)
8076  {
8077  rp->updateGeometry();
8078  VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8079 
8080  string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8081  string publishedPortName = (!specializedPublishedPortName.empty()?
8082  specializedPublishedPortName :
8084 
8085  bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8086  VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8087 
8088  publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8089  }
8090 
8091  return publishedPortNames;
8092 }
8093 
8100 {
8101  if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8102  return "";
8103 
8104  // If publishing a port on a "Share Value" node and the node's title has been
8105  // customized, request that title as the published port name.
8109  {
8110  return VuoRendererPort::sanitizePortIdentifier(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8111  }
8112 
8113  return "";
8114 }
8115 
8119 void VuoEditorComposition::repositionPopover()
8120 {
8121  VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8122  if (popover && !popover->getDetached())
8123  {
8124  const int cutoffMargin = 16;
8125  if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8126  popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8127 
8128  if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8129  popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8130  }
8131 }