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