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