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