Vuo  2.2.0
VuoEditorComposition.cc
Go to the documentation of this file.
1 
10 #include "VuoEditorComposition.hh"
11 
12 #include "VuoCompilerIssue.hh"
13 #include "VuoComposition.hh"
15 #include "VuoEditorWindow.hh"
16 #include "VuoErrorDialog.hh"
17 #include "VuoException.hh"
18 #include "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  targetPort = adjustedTargetPort;
2355 
2356  // If the typecast did not have multiple incoming cables, and the new cable will
2357  // be replacing it as a data source, delete the typecast.
2358  if (cableInProgressExpectedToCarryData &&
2359  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2360  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2361  typecastNodeToDelete = uncollapsedTypecast;
2362  }
2363 
2364  // If connecting two ports that already had a cable (of different data-carrying status) connecting them,
2365  // replace the pre-existing cable.
2366  if (preexistingCable)
2367  cableToReplace = preexistingCable;
2368 
2369  // If the cable carries data, determine what other sources of input data need to be
2370  // removed before completing this connection.
2371  if (cableInProgressExpectedToCarryData)
2372  {
2373  // If input port already had a connected data cable, delete that cable.
2374  vector<VuoCable *> previousConnectedCables = targetPort->getBase()->getConnectedCables(false);
2375  for (vector<VuoCable *>::iterator cable = previousConnectedCables.begin(); (! dataCableToDisplace) && (cable != previousConnectedCables.end()); ++cable)
2376  if ((((*cable)->getRenderer()->effectivelyCarriesData()) && (*cable) != cableToReplace))
2377  dataCableToDisplace = *cable;
2378 
2379  // If the input port was published as a data+event port, unpublish it.
2380  // @todo: Let published cable disconnection handle port unpublication.
2381  if (isPortPublished(targetPort))
2382  {
2383  vector<VuoRendererPublishedPort *> publishedDataConnections = targetPort->getPublishedPortsConnectedByDataCarryingCables();
2384  if (publishedDataConnections.size() > 0)
2385  portToUnpublish = targetPort;
2386  }
2387  }
2388 
2389  completedCableConnection = true;
2390  }
2391 
2392  // Case: Completing a "backward" cable connection from an input port to an output port
2393  else if (!preexistingCableWithMatchingDataCarryingStatus &&
2394  (targetPort->canConnectDirectlyWithoutSpecializationTo(fixedPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) ||
2395  selectBridgingSolution(targetPort, fixedPort, false, &portToSpecialize, specializedTypeName, typecastToInsert)))
2396 
2397  {
2398  completedCableConnection = true;
2399  }
2400  }
2401 
2402  // Complete the actual cable connection, if applicable.
2403  if (completedCableConnection)
2404  {
2405  emit undoStackMacroBeginRequested("Cable Connection");
2406 
2407  if (draggingPreviouslyPublishedCable)
2408  {
2409  // Record some information about the cable in progress, to create a replica.
2410  VuoNode *cableInProgressFromNode = cableInProgress->getFromNode();
2411  VuoNode *cableInProgressToNode = cableInProgress->getToNode();
2412  VuoPort *cableInProgressFromPort = cableInProgress->getFromPort();
2413  VuoPort *cableInProgressToPort = cableInProgress->getToPort();
2414  QPointF cableInProgressFloatingEndpointLoc = cableInProgress->getRenderer()->getFloatingEndpointLoc();
2415  bool cableInProgressAlwaysEventOnly = cableInProgress->getCompiler()->getAlwaysEventOnly();
2416 
2417  // Remove the original cable, unpublishing the port in the process.
2418  cancelCableDrag();
2419 
2420  // Restore a copy of the cable to participate in the new connection.
2421  VuoCable *cableInProgressCopy = (new VuoCompilerCable(NULL,
2422  NULL,
2423  NULL,
2424  NULL))->getBase();
2425 
2426  cableInProgressCopy->setFrom(cableInProgressFromNode, cableInProgressFromPort);
2427  cableInProgressCopy->setTo(cableInProgressToNode, cableInProgressToPort);
2428 
2429  addCable(cableInProgressCopy);
2430 
2431  cableInProgressCopy->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
2432  cableInProgressCopy->getRenderer()->setFloatingEndpointLoc(cableInProgressFloatingEndpointLoc);
2433 
2434  cableInProgress = cableInProgressCopy;
2435  cableInProgressWasNew = true;
2436  }
2437 
2438  // Workaround to avoid Qt's parameter count limit for signals/slots.
2439  pair<VuoRendererCable *, VuoRendererCable *> cableArgs = std::make_pair((dataCableToDisplace? dataCableToDisplace->getRenderer() : NULL),
2440  (cableToReplace? cableToReplace->getRenderer() : NULL));
2441  pair<string, string> typeArgs = std::make_pair(typecastToInsert, specializedTypeName);
2442  pair<VuoRendererPort *, VuoRendererPort *> portArgs = std::make_pair(portToUnpublish, portToSpecialize);
2443 
2444  emit connectionCompletedByDragging(cableInProgress->getRenderer(),
2445  targetPort,
2446  cableArgs,
2447  typecastNodeToDelete,
2448  typeArgs,
2449  portArgs);
2450 
2452 
2453  // Resume caching for dragged cable.
2454  if (cableInProgress)
2455  {
2456  cableInProgress->getRenderer()->updateGeometry();
2457  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2458  cableInProgress = NULL;
2459  }
2460  }
2461 
2462  // Otherwise, delete the cable.
2463  else // if (! completedCableConnection)
2464  cancelCableDrag();
2465 
2466  emit cableDragEnded();
2467 }
2468 
2472 void VuoEditorComposition::concludePublishedCableDrag(QGraphicsSceneMouseEvent *event)
2473 {
2474  VuoRendererPort *fixedPort;
2475  if (cableInProgress->getFromNode() && cableInProgress->getFromPort())
2476  fixedPort = cableInProgress->getFromPort()->getRenderer();
2477  else // if (cableInProgress->getToNode() && cableInProgress->getToPort())
2478  fixedPort = cableInProgress->getToPort()->getRenderer();
2479 
2480  // Potential side effects of a newly completed cable connection:
2481  // - A typecast may need to be automatically inserted.
2482  string typecastToInsert = "";
2483 
2484  // - A generic port involved in the new connection may need to be specialized.
2485  VuoRendererPort *portToSpecialize = NULL;
2486  string specializedTypeName = "";
2487 
2488  bool forceEventOnlyPublication = !cableInProgress->getRenderer()->effectivelyCarriesData();
2489 
2490  // Case: Initiating a cable drag from a published input port.
2491  if (cableInProgress && cableInProgress->isPublishedInputCable())
2492  {
2493  VuoRendererPort *internalInputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2494  VuoRendererPublishedPort *publishedInputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getFromPort()->getRenderer());
2495 
2496  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first input port.
2497  // (If over both a port drop zone and a node header, the node header gets precedence.)
2498  {
2499  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2500  if (targetNode)
2501  {
2502  internalInputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2503  if (internalInputPort)
2504  {
2505  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2506  forceEventOnlyPublication = true;
2507  }
2508  }
2509  }
2510 
2511  // Case: Cable was dropped onto an internal input port
2512  if (internalInputPort &&
2513  publishedInputPort)
2514  {
2515  // If the user has simply disconnected a cable and reconnected it within the same mouse drag,
2516  // don't push the operation onto the Undo stack.
2517  bool recreatingSameConnection = ((cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort() ==
2518  internalInputPort->getBase()) &&
2519  (cableInProgress->getRenderer()->getPreviouslyAlwaysEventOnly() ==
2520  cableInProgress->getCompiler()->getAlwaysEventOnly()));
2521  if (recreatingSameConnection)
2522  {
2523  revertCableDrag();
2524  return;
2525  }
2526 
2527  // Case: Ports are compatible
2528  if ((publishedInputPort->isCompatibleAliasWithSpecializationForInternalPort(internalInputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName))
2529  || selectBridgingSolution(publishedInputPort, internalInputPort, true, &portToSpecialize, specializedTypeName, typecastToInsert))
2530  {
2531  bool cableInProgressExpectedToCarryData = (cableInProgress->getRenderer()->effectivelyCarriesData() &&
2532  internalInputPort->getDataType());
2533  VuoCable *cableToReplace = publishedInputPort->getCableConnectedTo(internalInputPort, true);
2534  bool cableToReplaceHasMatchingDataCarryingStatus = (cableToReplace?
2535  (cableToReplace->getRenderer()->effectivelyCarriesData() ==
2536  cableInProgressExpectedToCarryData) :
2537  false);
2538 
2539  // If replacing a preexisting cable with an identical cable, just cancel the operation
2540  // so that it doesn't go onto the Undo stack.
2541  if (cableToReplace && cableToReplaceHasMatchingDataCarryingStatus)
2542  cancelCableDrag();
2543 
2544  // Case: Replacing a pre-existing cable that connected the same two ports
2545  // but with a different data-carrying status, and whose "To" port is
2546  // the child port of a collapsed typecast.
2547  else if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus &&
2548  cableToReplace->getToPort()->hasRenderer() &&
2549  cableToReplace->getToPort()->getRenderer()->getTypecastParentPort())
2550  {
2551  // @todo Implement for https://b33p.net/kosada/node/14153
2552  // For now, don't attempt it.
2553  cancelCableDrag();
2554  }
2555 
2556  else
2557  {
2558  emit undoStackMacroBeginRequested("Cable Connection");
2559  cancelCableDrag();
2560 
2561  // If this source/target port combination already a cable connecting them, but of a different
2562  // data-carrying status, replace the old cable with the new one.
2563  if (cableToReplace && !cableToReplaceHasMatchingDataCarryingStatus)
2564  {
2565  QList<QGraphicsItem *> removedComponents;
2566  removedComponents.append(cableToReplace->getRenderer());
2567  emit componentsRemoved(removedComponents, "Delete");
2568  }
2569 
2570  // If the target port had a connected collapsed typecast, uncollapse and delete it.
2571  // But first check whether the collapsed typecast is still present on canvas -- it's possible it was
2572  // already removed during the cancelCableDrag() call, if the cable being dragged was the
2573  // typecast's incoming data+event cable.
2574  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(internalInputPort);
2575  if (typecastPort && typecastPort->scene())
2576  {
2577  VuoRendererPort *childPort = typecastPort->getChildPort();
2578  VuoRendererNode *uncollapsedTypecast = uncollapseTypecastNode(typecastPort);
2579  VuoPort *typecastOutPort = uncollapsedTypecast->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
2580 
2581  // If the typecast did not have multiple incoming cables, and the new cable will
2582  // be replacing it as a data source, delete the typecast.
2583  if (cableInProgressExpectedToCarryData &&
2584  childPort->getBase()->getConnectedCables(true).size() < 2 &&
2585  (*typecastOutPort->getConnectedCables(true).begin())->getRenderer()->effectivelyCarriesData())
2586  {
2587  QList<QGraphicsItem *> removedComponents;
2588  removedComponents.append(uncollapsedTypecast);
2589  emit componentsRemoved(removedComponents, "Delete");
2590  }
2591  }
2592 
2593  emit portPublicationRequested(internalInputPort->getBase(),
2594  dynamic_cast<VuoPublishedPort *>(publishedInputPort->getBase()),
2595  forceEventOnlyPublication,
2596  (portToSpecialize? portToSpecialize->getBase() : NULL),
2597  specializedTypeName,
2598  typecastToInsert,
2599  false); // Avoid nested Undo stack macros.
2601  }
2602  }
2603  else // Source port and target port aren't compatible
2604  cancelCableDrag();
2605  }
2606  else // Cable was dropped over empty canvas space
2607  cancelCableDrag();
2608  }
2609 
2610  // Case: Initiating a cable drag from a published output port.
2611  else if (cableInProgress && cableInProgress->isPublishedOutputCable())
2612  {
2613  VuoRendererPort *internalOutputPort = static_cast<VuoRendererPort *>(findNearbyPort(event->scenePos(), false));
2614  VuoRendererPublishedPort *publishedOutputPort = dynamic_cast<VuoRendererPublishedPort *>(cableInProgress->getToPort()->getRenderer());
2615 
2616  // If the cable was dropped onto a node header area, connect an event-only cable to the node's first output port.
2617  // (If over both a port drop zone and a node header, the node header gets precedence.)
2618  {
2619  VuoRendererNode *targetNode = findNearbyNodeHeader(event->scenePos());
2620  if (targetNode)
2621  {
2622  internalOutputPort = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2623  if (internalOutputPort)
2624  {
2625  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
2626  forceEventOnlyPublication = true;
2627  }
2628  }
2629  }
2630 
2631  // Case: Cable was dropped onto an internal output port
2632  if (internalOutputPort &&
2633  publishedOutputPort)
2634  {
2635 
2636  // Case: Ports are compatible
2637  if ((publishedOutputPort->isCompatibleAliasWithSpecializationForInternalPort(internalOutputPort, forceEventOnlyPublication, &portToSpecialize, specializedTypeName) &&
2638  publishedOutputPort->canAccommodateInternalPort(internalOutputPort, forceEventOnlyPublication))
2639  || selectBridgingSolution(internalOutputPort, publishedOutputPort, false, &portToSpecialize, specializedTypeName, typecastToInsert))
2640  {
2641  emit portPublicationRequested(internalOutputPort->getBase(),
2642  dynamic_cast<VuoPublishedPort *>(publishedOutputPort->getBase()),
2643  forceEventOnlyPublication,
2644  portToSpecialize? portToSpecialize->getBase() : NULL,
2645  specializedTypeName,
2646  typecastToInsert,
2647  true);
2648  }
2649  }
2650 
2651  cancelCableDrag();
2652  }
2653 
2654  emit cableDragEnded();
2655 }
2656 
2661 {
2663 
2664  if (! cableInProgress)
2665  return;
2666 
2667  if (cableInProgressWasNew)
2668  removeCable(cableInProgress->getRenderer());
2669  else
2670  {
2671  QList<QGraphicsItem *> removedComponents;
2672  removedComponents.append(cableInProgress->getRenderer());
2673  emit componentsRemoved(removedComponents, "Delete");
2674  }
2675 
2676  cableInProgress = NULL;
2677 }
2678 
2686 {
2687  if (cableInProgress && cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort())
2688  {
2689  cableInProgress->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
2690  cableInProgress->getRenderer()->updateGeometry();
2691  cableInProgress->setTo(getUnderlyingParentNodeForPort(cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort(), this),
2692  cableInProgress->getRenderer()->getFloatingEndpointPreviousToPort());
2693  cableInProgress->getRenderer()->setCacheMode(getCurrentDefaultCacheMode());
2694  cableInProgress = NULL;
2695  }
2696  else if (cableInProgress)
2697  {
2698  cancelCableDrag();
2699  }
2700 
2703  emit cableDragEnded();
2704 }
2705 
2709 void VuoEditorComposition::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
2710 {
2711  bool leftMouseButtonPressed = (event->buttons() & Qt::LeftButton);
2712 
2713  // Disable Mission Control workaround, since it sometimes misdetects whether the left mouse
2714  // button is pressed and overzealously cancels cable drags (specifically, those originating from
2715  // sidebar published output ports). The workaround no longer appears to be effective as of Qt 5.2.1 anyway.
2716  /*
2717  // In the unlikely situation that we have a cable drag in progress but the left mouse
2718  // button is not pressed (e.g., if "Mission Control" was activated during a cable drag and
2719  // deactivated by releasing the mouse button), cancel the cable drag.
2720  // See https://b33p.net/kosada/node/3305
2721  if (cableInProgress && !menuSelectionInProgress && !leftMouseButtonPressed)
2722  cancelCableDrag();
2723  */
2724 
2725  // If in the process of a cable drag or mouse-over operation,
2726  // locate the nearest port or cable eligible for hover highlighting.
2727  if (cableInProgress || (! leftMouseButtonPressed))
2728  updateHoverHighlighting(event->scenePos());
2729 
2730 
2731  // Case: Left mouse button pressed
2732  if (leftMouseButtonPressed)
2733  {
2734  // Eliminate mouse jitter noise.
2735  if ((! dragStickinessDisabled) && (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)).length() < QApplication::startDragDistance()))
2736  return;
2737 
2738  else
2739  dragStickinessDisabled = true;
2740 
2741  // If a port or cable has been clicked for dragging and this is the first mouse move event
2742  // since the click, initiate the cable drag.
2743  if (portWithDragInitiated || cableWithYankInitiated)
2744  {
2745  initiateCableDrag(portWithDragInitiated, cableWithYankInitiated, event);
2746  portWithDragInitiated = NULL;
2747  cableWithYankInitiated = NULL;
2748  }
2749 
2750  // If there is a cable drag in progress, update the location of the cable's
2751  // floating endpoint to match that of the cursor.
2752  if (cableInProgress)
2753  {
2754  VuoRendererCable *rc = cableInProgress->getRenderer();
2755 
2756  rc->updateGeometry();
2757  rc->setFloatingEndpointLoc(event->scenePos());
2758 
2760  }
2761 
2762  else if (duplicationDragInProgress)
2763  {
2764  if (duplicateOnNextMouseMove)
2765  {
2767  duplicateOnNextMouseMove = false;
2768  }
2769 
2770  QPointF newPos = (VuoRendererItem::getSnapToGrid()?
2773  event->scenePos());
2774  QPointF delta = newPos - cursorPosBeforeDuplicationDragMove;
2775  moveItemsBy(getSelectedNodes(), getSelectedComments(), delta.x(), delta.y(), false);
2776  cursorPosBeforeDuplicationDragMove = newPos;
2777  }
2778 
2779  else
2780  QGraphicsScene::mouseMoveEvent(event);
2781  }
2782 
2783  // Case: Left mouse button not pressed
2784  else
2785  QGraphicsScene::mouseMoveEvent(event);
2786 }
2787 
2793 void VuoEditorComposition::updateHoverHighlighting(QPointF scenePos, bool disablePortHoverHighlighting)
2794 {
2795  auto types = compiler->getTypes();
2796 
2797  // Detect cable and port hover events ourselves, since we need to account
2798  // for their extended hover ranges.
2799  QGraphicsItem *item = cableInProgress? findNearbyPort(scenePos, false) : findNearbyComponent(scenePos);
2800  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
2801  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(item);
2802  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(item);
2803  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(item);
2804 
2805  // If hovering over a node header area while dragging a cable, treat it like hovering over the first input or output port.
2806  // (If over both a port drop zone and a node header, the node header gets precedence.)
2807  bool hoveringOverNodeHeader = false;
2808  if (cableInProgress)
2809  {
2810  VuoRendererNode *targetNode = findNearbyNodeHeader(scenePos);
2811  if (targetNode)
2812  {
2813  port = findTargetPortForCableDroppedOnNodeHeader(targetNode);
2814  if (port)
2815  {
2816  item = targetNode;
2817  hoveringOverNodeHeader = true;
2818  }
2819  }
2820  }
2821 
2822  VuoRendererNode *node = NULL;
2823  if (! hoveringOverNodeHeader)
2824  {
2825  // Do not handle hover events for (non-drawer) nodes here.
2826  if (dynamic_cast<VuoRendererNode *>(item) && !makeListDrawer)
2827  item = NULL;
2828 
2829  node = dynamic_cast<VuoRendererNode *>(item);
2830  }
2831 
2832  // Case: The item (if any) that requires hover-highlighting given the current position of the cursor
2833  // is not the same as the item (if any) that was hover-highlighted previously.
2834  if (item != previousNearbyItem)
2835  {
2836  VuoRendererPort *previousPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2837  VuoRendererNode *previousNode = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2838  if (previousNode)
2839  previousPort = findTargetPortForCableDroppedOnNodeHeader(previousNode);
2840 
2841  // End hover-highlighting of the previous item, if any.
2842  if (previousNearbyItem)
2844 
2845  // Begin hover-highlighting of the current item, if any.
2846  if (cable)
2847  cable->extendedHoverEnterEvent();
2848  else if (node)
2849  {
2850  if (! cableInProgress)
2851  {
2852  if (makeListDrawer)
2853  makeListDrawer->extendedHoverEnterEvent(scenePos);
2854  }
2855  }
2856  else if (typecastPort && !disablePortHoverHighlighting)
2857  port->extendedHoverEnterEvent((bool)cableInProgress);
2858  else if (port && !disablePortHoverHighlighting)
2859  {
2860  port->extendedHoverEnterEvent((bool)cableInProgress);
2861  if (!cableInProgress)
2862  {
2863  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2864  connectedAntennaPort->extendedHoverEnterEvent((bool)cableInProgress, true);
2865  }
2866  }
2867 
2868  VuoRendererPort *previousTargetPort = (previousPort && previousPort->isEligibleForConnection() ? previousPort : NULL);
2869 
2870  // If the current item or previous item is a port and it got this status because the mouse hovered
2871  // over a node header, update the eligibility-highlighting of the port.
2872  if (cableInProgress)
2873  {
2874  VuoRendererPort *fixedPort = (cableInProgress->getFromPort() ?
2875  cableInProgress->getFromPort()->getRenderer() :
2876  cableInProgress->getToPort()->getRenderer());
2877 
2878  QList< QPair<VuoRendererPort *, bool> > updatedPorts;
2879  if (hoveringOverNodeHeader)
2880  updatedPorts.append( QPair<VuoRendererPort *, bool>(port, true) );
2881  if (previousNode && previousPort)
2882  updatedPorts.append( QPair<VuoRendererPort *, bool>(previousPort, !cableInProgress->getRenderer()->effectivelyCarriesData()) );
2883 
2884  QPair<VuoRendererPort *, bool> p;
2885  foreach (p, updatedPorts)
2886  {
2887  VuoRendererPort *updatedPort = p.first;
2888 
2889  VuoRendererPort *typecastParentPort = updatedPort->getTypecastParentPort();
2890  if (typecastParentPort)
2891  updatedPort = typecastParentPort;
2892 
2893  updateEligibilityHighlightingForPort(updatedPort, fixedPort, p.second, types);
2894 
2895  VuoRendererNode *potentialDrawer = updatedPort->getUnderlyingParentNode();
2896  updateEligibilityHighlightingForNode(potentialDrawer);
2897  }
2898  }
2899 
2900  VuoRendererPort *targetPort = (port && port->isEligibleForConnection() ? port : NULL);
2901 
2902  // Update error dialogs and cable's data-carrying status.
2903  if (targetPort || previousTargetPort)
2904  {
2905  if (cableInProgress)
2906  cableInProgress->getRenderer()->setFloatingEndpointAboveEventPort(targetPort && (!targetPort->getDataType() || hoveringOverNodeHeader));
2907 
2908  updateFeedbackErrors(targetPort);
2909  }
2910 
2911  previousNearbyItem = item;
2912  }
2913 
2914  // Case: The previously hover-highlighted item should remain hover-highlighted.
2915  else if (item)
2916  {
2917  if (cable)
2918  cable->extendedHoverMoveEvent();
2919  else if (port && !disablePortHoverHighlighting)
2920  {
2921  port->extendedHoverMoveEvent((bool)cableInProgress);
2922  if (!cableInProgress)
2923  {
2924  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2925  connectedAntennaPort->extendedHoverMoveEvent((bool)cableInProgress, true);
2926  }
2927  }
2928  else if (makeListDrawer)
2929  makeListDrawer->extendedHoverMoveEvent(scenePos);
2930  }
2931 }
2932 
2937 {
2938  if (previousNearbyItem)
2939  {
2940  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(previousNearbyItem);
2941  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
2942  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(previousNearbyItem);
2943  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(previousNearbyItem);
2944 
2945  if (node)
2946  {
2947  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(previousNearbyItem);
2948  if (drawer)
2949  drawer->extendedHoverLeaveEvent();
2950  else
2952  }
2953 
2954  if (cable)
2955  cable->extendedHoverLeaveEvent();
2956  else if (typecastPort)
2957  port->extendedHoverLeaveEvent();
2958  else if (port)
2959  {
2960  port->extendedHoverLeaveEvent();
2961  foreach (VuoRendererPort *connectedAntennaPort, port->getPortsConnectedWirelessly(false))
2962  connectedAntennaPort->extendedHoverLeaveEvent();
2963  }
2964 
2965  previousNearbyItem = NULL;
2966  }
2967 }
2968 
2973 {
2974  if ((event->key() != Qt::Key_Alt) && (event->key() != Qt::Key_Shift) && (event->key() != Qt::Key_Escape))
2975  cancelCableDrag();
2976 
2977  Qt::KeyboardModifiers modifiers = event->modifiers();
2978  qreal adjustedNodeMoveRate = nodeMoveRate;
2979  if (modifiers & Qt::ShiftModifier)
2980  {
2981  adjustedNodeMoveRate *= nodeMoveRateMultiplier;
2982  }
2983 
2984  switch (event->key()) {
2985  case Qt::Key_Backspace:
2986  {
2988  break;
2989  }
2990  case Qt::Key_Delete:
2991  {
2993  break;
2994  }
2995  case Qt::Key_Up:
2996  {
2997  moveSelectedItemsBy(0, -1*adjustedNodeMoveRate);
2998  break;
2999  }
3000  case Qt::Key_Down:
3001  {
3002  if (modifiers & Qt::ControlModifier)
3003  openSelectedEditableNodes();
3004  else
3005  moveSelectedItemsBy(0, adjustedNodeMoveRate);
3006  break;
3007  }
3008  case Qt::Key_Left:
3009  {
3010  moveSelectedItemsBy(-1*adjustedNodeMoveRate, 0);
3011  break;
3012  }
3013  case Qt::Key_Right:
3014  {
3015  moveSelectedItemsBy(adjustedNodeMoveRate, 0);
3016  break;
3017  }
3018  case Qt::Key_Shift:
3019  {
3020  if (cableInProgress)
3021  {
3022  cableInProgress->getRenderer()->updateGeometry();
3023  cableInProgress->getCompiler()->setAlwaysEventOnly(true);
3024  highlightEligibleEndpointsForCable(cableInProgress);
3025 
3026  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3027  if (hoveredPort)
3028  updateFeedbackErrors(hoveredPort);
3029  }
3030 
3031  break;
3032  }
3033  case Qt::Key_Enter:
3034  case Qt::Key_Return:
3035  {
3036  // Make sure the event is forwarded to any composition component within hover range.
3037  QGraphicsScene::keyPressEvent(event);
3038 
3039  if (!event->isAccepted())
3040  {
3041  // Otherwise, if there are any (and only) nodes currently selected, open a title editor for each one;
3042  // if there are any (and only) comments currently selected, open a comment text editor for each one.
3043  set<VuoRendererNode *> selectedNodes = getSelectedNodes();
3044  set<VuoRendererComment *> selectedComments = getSelectedComments();
3045 
3046  if (selectedComments.empty() && !selectedNodes.empty())
3048  }
3049 
3050  break;
3051  }
3052 
3053  case Qt::Key_Escape:
3054  {
3055  if (duplicateOnNextMouseMove || duplicationDragInProgress)
3056  {
3058 
3059  duplicateOnNextMouseMove = false;
3060  duplicationDragInProgress = false;
3061  duplicationCancelled = true;
3062  }
3063  else if (cableInProgress)
3064  {
3065  revertCableDrag();
3066  }
3067  break;
3068  }
3069 
3070  default:
3071  {
3072  QGraphicsScene::keyPressEvent(event);
3073  break;
3074  }
3075  }
3076 }
3077 
3082 {
3083  switch (event->key()) {
3084  case Qt::Key_Shift:
3085  {
3086  if (cableInProgress)
3087  {
3088  cableInProgress->getRenderer()->updateGeometry();
3089  cableInProgress->getCompiler()->setAlwaysEventOnly(false);
3090  highlightEligibleEndpointsForCable(cableInProgress);
3091 
3092  VuoRendererPort *hoveredPort = dynamic_cast<VuoRendererPort *>(previousNearbyItem);
3093  if (hoveredPort)
3094  updateFeedbackErrors(hoveredPort);
3095  }
3096 
3097  break;
3098  }
3099 
3100  default:
3101  {
3102  QGraphicsScene::keyReleaseEvent(event);
3103  break;
3104  }
3105  }
3106 }
3107 
3111 void VuoEditorComposition::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
3112 {
3113  // Determine whether the cursor is in range of any operable composition components.
3114  QGraphicsItem *item = findNearbyComponent(event->scenePos());
3115 
3117  contextMenu.setSeparatorsCollapsible(false);
3118 
3119  // Customize context menu for the canvas.
3120  if (! item)
3121  {
3122  QAction *insertNodeSection = new QAction(tr("Insert Node"), NULL);
3123  insertNodeSection->setEnabled(false);
3124  contextMenu.addAction(insertNodeSection);
3125 
3126  QString spacer(" ");
3127 
3128  {
3129  // "Share" nodes
3130  QMenu *shareMenu = new QMenu(&contextMenu);
3131  shareMenu->setSeparatorsCollapsible(false);
3132  shareMenu->setTitle(spacer + tr("Share"));
3133  contextMenu.addMenu(shareMenu);
3134 
3135  {
3136  QAction *action = new QAction(tr("Share Value"), NULL);
3137  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share"))));
3138  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3139  shareMenu->addAction(action);
3140  }
3141 
3142  {
3143  QAction *action = new QAction(tr("Share List"), NULL);
3144  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.share.list"))));
3145  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3146  shareMenu->addAction(action);
3147  }
3148 
3149  {
3150  QAction *action = new QAction(tr("Share Event"), NULL);
3151  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.share"))));
3152  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3153  shareMenu->addAction(action);
3154  }
3155  }
3156 
3157  {
3158  // "Hold" nodes
3159  QMenu *holdMenu = new QMenu(&contextMenu);
3160  holdMenu->setSeparatorsCollapsible(false);
3161  holdMenu->setTitle(spacer + tr("Hold"));
3162  contextMenu.addMenu(holdMenu);
3163 
3164  {
3165  QAction *action = new QAction(tr("Hold Value"), NULL);
3166  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold2"))));
3167  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3168  holdMenu->addAction(action);
3169  }
3170 
3171  {
3172  QAction *action = new QAction(tr("Hold List"), NULL);
3173  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.data.hold.list2"))));
3174  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3175  holdMenu->addAction(action);
3176  }
3177  }
3178 
3179  {
3180  // "Allow" nodes
3181  QMenu *allowMenu = new QMenu(&contextMenu);
3182  allowMenu->setSeparatorsCollapsible(false);
3183  allowMenu->setTitle(spacer + tr("Allow"));
3184  contextMenu.addMenu(allowMenu);
3185 
3186  {
3187  QAction *action = new QAction(tr("Allow First Event"), NULL);
3188  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirst"))));
3189  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3190  allowMenu->addAction(action);
3191  }
3192 
3193  {
3194  QAction *action = new QAction(tr("Allow First Value"), NULL);
3195  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowFirstValue"))));
3196  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3197  allowMenu->addAction(action);
3198  }
3199 
3200  {
3201  QAction *action = new QAction(tr("Allow Periodic Events"), NULL);
3202  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.time.allowPeriodic"))));
3203  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3204  allowMenu->addAction(action);
3205  }
3206 
3207  {
3208  QAction *action = new QAction(tr("Allow Changes"), NULL);
3209  action->setData(qVariantFromValue(qMakePair(event->scenePos(), QStringLiteral("vuo.event.allowChanges2"))));
3210  connect(action, &QAction::triggered, this, &VuoEditorComposition::insertNode);
3211  allowMenu->addAction(action);
3212  }
3213  }
3214 
3215  contextMenu.addSeparator();
3216 
3217  QAction *contextMenuInsertComment = new QAction(NULL);
3218  contextMenuInsertComment->setText(tr("Insert Comment"));
3219  contextMenuInsertComment->setData(qVariantFromValue(event->scenePos()));
3220  connect(contextMenuInsertComment, &QAction::triggered, this, &VuoEditorComposition::insertComment);
3221  contextMenu.addAction(contextMenuInsertComment);
3222 
3223  QAction *contextMenuInsertSubcomposition = new QAction(NULL);
3224  contextMenuInsertSubcomposition->setText(tr("Insert Subcomposition"));
3225  contextMenuInsertSubcomposition->setData(qVariantFromValue(event->scenePos()));
3226  connect(contextMenuInsertSubcomposition, &QAction::triggered, this, &VuoEditorComposition::insertSubcomposition);
3227  contextMenu.addAction(contextMenuInsertSubcomposition);
3228  }
3229 
3230  // Prepare to take a snapshot of the contextMenuDeleteSelection QAction's current values, so that
3231  // they do not change while the context menu is displayed.
3232  QAction *contextMenuDeleteSelectedSnapshot = new QAction(NULL);
3233 
3234  // Customize context menu for ports.
3235  if (dynamic_cast<VuoRendererPort *>(item))
3236  {
3237  VuoRendererPort *port = (VuoRendererPort *)item;
3238 
3239  if (port->isConstant() && inputEditorManager)
3240  {
3241  VuoType *dataType = static_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler())->getDataVuoType();
3242  VuoInputEditor *inputEditorLoadedForPortDataType = inputEditorManager->newInputEditor(dataType);
3243  if (inputEditorLoadedForPortDataType)
3244  {
3245  contextMenuSetPortConstant->setData(qVariantFromValue((void *)port));
3246  contextMenu.addAction(contextMenuSetPortConstant);
3247 
3248  inputEditorLoadedForPortDataType->deleteLater();
3249  }
3250  }
3251 
3252  if (!isPortPublished(port) && port->getPublishable())
3253  {
3254  contextMenuPublishPort->setText(tr("Publish Port"));
3255  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3256  contextMenu.addAction(contextMenuPublishPort);
3257  }
3258 
3259  else if (isPortPublished(port))
3260  {
3261  vector<VuoRendererPublishedPort *> externalPublishedPorts = port->getPublishedPorts();
3262  bool hasExternalPublishedPortWithMultipleInternalPorts = false;
3263  bool hasExternalPublishedPortBelongingToActiveProtocol = false;
3264  foreach (VuoRendererPublishedPort *externalPort, externalPublishedPorts)
3265  {
3266  if (externalPort->getBase()->getConnectedCables(true).size() > 1)
3267  hasExternalPublishedPortWithMultipleInternalPorts = true;
3268 
3269  if (dynamic_cast<VuoPublishedPort *>(externalPort->getBase())->isProtocolPort())
3270  hasExternalPublishedPortBelongingToActiveProtocol = true;
3271  }
3272 
3273  // Omit the "Delete Published Port" context menu item if the internal port is connected to
3274  // an external published port that cannot be deleted, either because:
3275  // - It is part of an active protocol;
3276  // - It has more than one connected cable.
3277  if (!hasExternalPublishedPortWithMultipleInternalPorts &&!hasExternalPublishedPortBelongingToActiveProtocol)
3278  {
3279  contextMenuPublishPort->setText(tr("Delete Published Port"));
3280  contextMenuPublishPort->setData(qVariantFromValue((void *)port));
3281  contextMenu.addAction(contextMenuPublishPort);
3282  }
3283  }
3284 
3285  bool isTriggerPort = dynamic_cast<VuoCompilerTriggerPort *>(port->getBase()->getCompiler());
3286  if (isTriggerPort || port->getInput())
3287  {
3288  __block bool isTopLevelCompositionRunning = false;
3289  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
3290  {
3291  isTopLevelCompositionRunning = topLevelComposition->isRunning();
3292  });
3293 
3294  if (isTopLevelCompositionRunning)
3295  {
3296  contextMenuFireEvent->setData(qVariantFromValue((void *)port));
3297  contextMenu.addAction(contextMenuFireEvent);
3298  }
3299  }
3300 
3301  if (isTriggerPort)
3302  {
3303  QMenu *contextMenuThrottling = new QMenu(&contextMenu);
3304  contextMenuThrottling->setSeparatorsCollapsible(false);
3305  contextMenuThrottling->setTitle(tr("Set Event Throttling"));
3307  foreach (QAction *action, contextMenuThrottlingActions)
3308  {
3309  contextMenuThrottling->addAction(action);
3310  action->setData(qVariantFromValue(port));
3311  action->setCheckable(true);
3312  action->setChecked( i++ == port->getBase()->getEventThrottling() );
3313  }
3314  contextMenu.addMenu(contextMenuThrottling);
3315  }
3316 
3317  // Allow the user to specialize, respecialize, or unspecialize generic data types.
3318  VuoGenericType *genericDataType = dynamic_cast<VuoGenericType *>(port->getDataType());
3319  if (genericDataType || isPortCurrentlyRevertible(port))
3320  {
3321  QMenu *contextMenuSpecializeGenericType = new QMenu(&contextMenu);
3322  contextMenuSpecializeGenericType->setSeparatorsCollapsible(false);
3323  contextMenuSpecializeGenericType->setTitle(tr("Set Data Type"));
3324  contextMenuSpecializeGenericType->setToolTipsVisible(true);
3325 
3326  QAction *unspecializeAction = contextMenuSpecializeGenericType->addAction(tr("Generic"));
3327 
3328  // It is safe to assume that there will be types listed after the "Generic" option
3329  // since any network of connected generic/specialized ports has at least one compatible
3330  // data type in common, so the separator can be added here without forward-checking.
3331  contextMenuSpecializeGenericType->addSeparator();
3332 
3333  unspecializeAction->setData(qVariantFromValue((void *)port));
3334  unspecializeAction->setCheckable(true);
3335  unspecializeAction->setChecked(genericDataType);
3336 
3337  contextMenu.addMenu(contextMenuSpecializeGenericType);
3338 
3339  VuoGenericType *genericTypeFromPortClass = NULL; // Original generic type of the port
3340  set<string> compatibleTypes; // Compatible types in the context of the connected generic port network
3341  set<string> compatibleTypesInIsolation; // Compatible types for the port in isolation
3342 
3343  // Allow the user to specialize generic ports.
3344  if (genericDataType)
3345  {
3346  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(port->getBase()->getClass()->getCompiler());
3347  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3348 
3349  // Determine compatible types in the context of the connected generic port network.
3350  VuoGenericType::Compatibility compatibility;
3351  vector<string> compatibleTypesVector = genericDataType->getCompatibleSpecializedTypes(compatibility);
3352  compatibleTypes = set<string>(compatibleTypesVector.begin(), compatibleTypesVector.end());
3353 
3354  // If all types or all list types are compatible, add them to (currently empty) compatibleTypes.
3355  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3356  {
3357  vector<string> compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3358  compatibleTypes.insert(compatibleTypeNames.begin(), compatibleTypeNames.end());
3359  }
3360  }
3361 
3362  // Allow the user to re-specialize already specialized ports.
3363  else if (isPortCurrentlyRevertible(port))
3364  {
3365  map<VuoNode *, string> nodesToReplace;
3366  set<VuoCable *> cablesToDelete;
3367  createReplacementsToUnspecializePort(port->getBase(), false, nodesToReplace, cablesToDelete);
3368  if (cablesToDelete.size() >= 1)
3369  {
3370  // @todo https://b33p.net/kosada/node/8895 : Note that this specialization will break connections.
3371  }
3372 
3373  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(
3374  port->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler());
3375  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(port->getBase()->getClass()) );
3376  compatibleTypes = getRespecializationOptionsForPortInNetwork(port);
3377  }
3378 
3379  // Determine compatible types in isolation.
3380  if (genericTypeFromPortClass)
3381  {
3382  // "Make List" drawer child ports require special handling, since technically they are compatible with all types
3383  // but practically they are limited to types compatible with their host ports.
3384  VuoRendererInputListDrawer *drawer = dynamic_cast<VuoRendererInputListDrawer *>(port->getUnderlyingParentNode());
3385  if (drawer)
3386  {
3387  VuoPort *hostPort = drawer->getUnderlyingHostPort();
3388  if (hostPort && hostPort->hasRenderer())
3389  {
3390  VuoCompilerPortClass *portClass = static_cast<VuoCompilerPortClass *>(hostPort->getClass()->getCompiler());
3391  VuoGenericType *genericHostPortDataType = dynamic_cast<VuoGenericType *>(hostPort->getRenderer()->getDataType());
3392  if (genericHostPortDataType)
3393  genericTypeFromPortClass = static_cast<VuoGenericType *>(portClass->getDataVuoType());
3394  else if (isPortCurrentlyRevertible(hostPort->getRenderer()))
3395  {
3397  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3398  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass->getBase()) );
3399  }
3400  }
3401  }
3402 
3404  vector<string> compatibleTypesInIsolationVector;
3405  if (genericTypeFromPortClass)
3406  compatibleTypesInIsolationVector = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibilityInIsolation);
3407 
3408  // If all types or all list types are compatible, add them to (currently empty) compatibleTypesInIsolationVector.
3409  if (compatibilityInIsolation == VuoGenericType::anyType || compatibilityInIsolation == VuoGenericType::anyListType)
3410  compatibleTypesInIsolationVector = getAllSpecializedTypeOptions(compatibilityInIsolation == VuoGenericType::anyListType);
3411 
3412  foreach (string type, compatibleTypesInIsolationVector)
3413  compatibleTypesInIsolation.insert(drawer? VuoType::extractInnermostTypeName(type) : type);
3414  }
3415 
3416  // List the compatible types in the menu.
3417  {
3418  // If there are only a handful of eligible types, display them all in a flat menu.
3419  const int maxTypeCountForFlatMenuDisplay = 10;
3420  if (compatibleTypesInIsolation.size() <= maxTypeCountForFlatMenuDisplay)
3421  {
3422  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, false, "", contextMenuSpecializeGenericType);
3423  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3424  }
3425 
3426  // Otherwise, organize them by node set.
3427  else
3428  {
3429  // First list compatible types that don't belong to any specific node set.
3430  QList<QAction *> actions = getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, "", contextMenuSpecializeGenericType);
3431  addTypeActionsToMenu(actions, contextMenuSpecializeGenericType);
3432 
3433  // Now add a submenu for each node set that contains compatible types.
3434  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3435  QList<QAction *> allNodeSetActionsToAdd;
3436  for (map<string, set<VuoCompilerType *> >::iterator i = loadedTypesForNodeSet.begin(); i != loadedTypesForNodeSet.end(); ++i)
3437  {
3438  string nodeSetName = i->first;
3439  if (!nodeSetName.empty())
3440  allNodeSetActionsToAdd += getCompatibleTypesForMenu(port, compatibleTypesInIsolation, compatibleTypes, true, nodeSetName, contextMenuSpecializeGenericType);
3441  }
3442 
3443  if ((contextMenuSpecializeGenericType->actions().size() > 0) && (allNodeSetActionsToAdd.size() > 0))
3444  contextMenuSpecializeGenericType->addSeparator();
3445 
3446  addTypeActionsToMenu(allNodeSetActionsToAdd, contextMenuSpecializeGenericType);
3447  }
3448 
3449  foreach (QAction *action, contextMenuSpecializeGenericType->actions())
3450  {
3451  QMenu *specializeSubmenu = action->menu();
3452  if (specializeSubmenu)
3453  {
3454  foreach (QAction *specializeSubaction, specializeSubmenu->actions())
3455  connect(specializeSubaction, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3456  }
3457  else if (action == unspecializeAction)
3458  connect(action, &QAction::triggered, this, &VuoEditorComposition::unspecializePortType);
3459  else
3460  connect(action, &QAction::triggered, this, &VuoEditorComposition::specializeGenericPortType);
3461  }
3462  }
3463  }
3464 
3465  // Allow the user to hide, unhide, or delete cables connected directly to the port or, if the
3466  // port has a collapsed typecast, to the typecast's child port.
3467  int numVisibleDirectlyConnectedCables = 0;
3468  int numHidableDirectlyConnectedCables = 0;
3469  int numUnhidableDirectlyConnectedCables = 0;
3470 
3471  foreach (VuoCable *cable, port->getBase()->getConnectedCables(true))
3472  {
3473  if (!cable->getRenderer()->paintingDisabled())
3474  {
3475  numVisibleDirectlyConnectedCables++;
3476  if (!cable->isPublished())
3477  numHidableDirectlyConnectedCables++;
3478  }
3479 
3480  else if (cable->getRenderer()->getEffectivelyWireless())
3481  numUnhidableDirectlyConnectedCables++;
3482  }
3483 
3484  int numVisibleChildPortConnectedCables = 0;
3485  int numHidableChildPortConnectedCables = 0;
3486  int numUnhidableChildPortConnectedCables = 0;
3487  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(port);
3488  if (typecastPort)
3489  {
3490  VuoRendererNode *typecastNode = typecastPort->getUncollapsedTypecastNode();
3491  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
3492  foreach(VuoCable *cable, typecastInPort->getConnectedCables(true))
3493  {
3494  if (!cable->getRenderer()->paintingDisabled())
3495  {
3496  numVisibleChildPortConnectedCables++;
3497 
3498  if (!cable->isPublished())
3499  numHidableChildPortConnectedCables++;
3500  }
3501  else if (cable->getRenderer()->getEffectivelyWireless())
3502  numUnhidableChildPortConnectedCables++;
3503  }
3504  }
3505 
3506  // Count the number of directly or indirectly connected cables that are currently visible.
3507  int numVisibleConnectedCables = numVisibleDirectlyConnectedCables + numVisibleChildPortConnectedCables;
3508 
3509  // Count the number of directly or indirectly connected cables that are currently hidable.
3510  int numHidableConnectedCables = numHidableDirectlyConnectedCables + numHidableChildPortConnectedCables;
3511 
3512  // Count the number of directly or indirectly connected cables that are currently hidden.
3513  int numUnhidableConnectedCables = numUnhidableDirectlyConnectedCables + numUnhidableChildPortConnectedCables;
3514 
3515  if ((!renderHiddenCables && ((numHidableConnectedCables >= 1) || (numUnhidableConnectedCables >= 1)))
3516  || (numVisibleConnectedCables >= 1))
3517  {
3518  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3519  contextMenu.addSeparator();
3520  }
3521 
3522  if (!renderHiddenCables)
3523  {
3524  if (numHidableConnectedCables >= 1)
3525  {
3526  // Use numApparentlyHidableConnectedCables instead of numHidableConnectedCables to determine pluralization
3527  // of menu item text to avoid the appearance of a bug, even though the "Hide" operation will not
3528  // impact visible published cables.
3529  int numApparentlyHidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3530  contextMenuHideCables->setText(numApparentlyHidableConnectedCables > 1? "Hide Cables" : "Hide Cable");
3531  contextMenuHideCables->setData(qVariantFromValue((void *)port));
3532  contextMenu.addAction(contextMenuHideCables);
3533  }
3534 
3535  if (numUnhidableConnectedCables >= 1)
3536  {
3537  int numApparentlyUnhidableConnectedCables = numVisibleConnectedCables + numUnhidableConnectedCables;
3538  contextMenuUnhideCables->setText(numApparentlyUnhidableConnectedCables > 1? "Unhide Cables" : "Unhide Cable");
3539  contextMenuUnhideCables->setData(qVariantFromValue((void *)port));
3540  contextMenu.addAction(contextMenuUnhideCables);
3541  }
3542  }
3543 
3544  if (numVisibleConnectedCables >= 1)
3545  {
3546  contextMenuDeleteCables->setText(numVisibleConnectedCables > 1? "Delete Cables" : "Delete Cable");
3547  contextMenuDeleteCables->setData(qVariantFromValue((void *)port));
3548  contextMenu.addAction(contextMenuDeleteCables);
3549  }
3550  }
3551 
3552  // Customize context menu for nodes, cables, and/or comments.
3553  else if (item)
3554  {
3555  if (! item->isSelected())
3556  {
3557  clearSelection();
3558  item->setSelected(true);
3559  }
3560 
3561  QList<QGraphicsItem *> selectedComponents = selectedItems();
3562  bool onlyCommentsSelected = true;
3563  bool onlyCablesSelected = true;
3564  bool selectionContainsMissingNode = false;
3565  foreach (QGraphicsItem *item, selectedComponents)
3566  {
3567  if (!dynamic_cast<VuoRendererComment *>(item))
3568  onlyCommentsSelected = false;
3569 
3570  if (!dynamic_cast<VuoRendererCable *>(item))
3571  onlyCablesSelected = false;
3572 
3573  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererNode *>(item)->getBase()->hasCompiler())
3574  selectionContainsMissingNode = true;
3575  }
3576 
3577  contextMenuDeleteSelectedSnapshot->setText(contextMenuDeleteSelected->text());
3578  connect(contextMenuDeleteSelectedSnapshot, &QAction::triggered, this, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
3579 
3580  // Comments
3581  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(item);
3582  if (comment)
3583  {
3584  // Option: Edit selected comments
3585  if (onlyCommentsSelected)
3586  contextMenu.addAction(contextMenuEditSelectedComments);
3587 
3588  // Option: Tint selected components(s)
3589  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3590 
3591  contextMenu.addSeparator();
3592 
3593  // Option: Refactor selected component(s)
3594  contextMenu.addAction(contextMenuRefactorSelected);
3595 
3596  contextMenu.addSeparator();
3597 
3598  // Option: Delete selected components(s)
3599  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3600  }
3601 
3602  // Cables
3603  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(item);
3604  if (cable)
3605  {
3606  if (!renderHiddenCables)
3607  {
3608  if (onlyCablesSelected && !cable->getBase()->isPublished())
3609  contextMenu.addAction(contextMenuHideSelectedCables);
3610  }
3611 
3612  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3613  }
3614 
3615  // Nodes
3616  else if (dynamic_cast<VuoRendererNode *>(item))
3617  {
3618  VuoRendererNode *node = (VuoRendererNode *)item;
3619 
3620  // Input drawers
3621  if (dynamic_cast<VuoRendererInputDrawer *>(node) &&
3622  node->getBase()->getNodeClass()->hasCompiler())
3623  {
3624  // Offer "Add/Remove Input Port" options for resizable list input drawers.
3625  if (dynamic_cast<VuoRendererInputListDrawer *>(node))
3626  {
3627  contextMenuAddInputPort->setData(qVariantFromValue((void *)node));
3628  contextMenuRemoveInputPort->setData(qVariantFromValue((void *)node));
3629 
3630  int listItemCount = ((VuoCompilerMakeListNodeClass *)(node->getBase()->getNodeClass()->getCompiler()))->getItemCount();
3631  contextMenuRemoveInputPort->setEnabled(listItemCount >= 1);
3632 
3633  contextMenu.addAction(contextMenuAddInputPort);
3634  contextMenu.addAction(contextMenuRemoveInputPort);
3635 
3636  contextMenu.addSeparator();
3637  }
3638 
3639  // Offer "Reset" option for all input drawers.
3640  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3641  }
3642 
3643  // Non-input-drawer nodes
3644  else
3645  {
3646  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
3647  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
3648  {
3649  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
3650  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
3651  if (originalGenericNodeClass)
3652  nodeClass = originalGenericNodeClass->getBase();
3653  }
3654 
3655  QString actionText, sourcePath;
3656  bool nodeClassIsEditable = VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath);
3657  bool nodeClassIs3rdParty = nodeClass->hasCompiler() && !nodeClass->getCompiler()->isBuiltIn();
3658 
3659  int numSelectedNonAttachmentNodes = 0;
3660  QList<QGraphicsItem *> selectedComponents = selectedItems();
3661  foreach (QGraphicsItem *item, selectedComponents)
3662  {
3663  if (dynamic_cast<VuoRendererNode *>(item) && !dynamic_cast<VuoRendererInputAttachment *>(item))
3664  numSelectedNonAttachmentNodes++;
3665  }
3666 
3667  if ((numSelectedNonAttachmentNodes == 1) && nodeClassIsEditable)
3668  {
3669  // Option: Edit an installed subcomposition, shader, or text-code node.
3670  QAction *editAction = new QAction(NULL);
3671  editAction->setText(actionText);
3672  editAction->setData(qVariantFromValue(node));
3673  connect(editAction, &QAction::triggered, this, &VuoEditorComposition::emitNodeSourceEditorRequested);
3674 
3675  contextMenu.addAction(editAction);
3676  contextMenu.addSeparator();
3677  }
3678 
3679  // Option: Rename selected node(s)
3680  if (node->getBase()->hasCompiler())
3681  contextMenu.addAction(contextMenuRenameSelected);
3682 
3683  // Option: Tint selected component(s)
3684  contextMenu.addMenu(getContextMenuTints(&contextMenu));
3685 
3686  contextMenu.addSeparator();
3687 
3688  // Option: Replace selected node with a similar node
3689  if (numSelectedNonAttachmentNodes == 1)
3690  {
3691  if (contextMenuChangeNode)
3692  contextMenuChangeNode->deleteLater();
3693 
3694  contextMenuChangeNode = new QMenu(VuoEditorWindow::getMostRecentActiveEditorWindow());
3695  contextMenuChangeNode->setSeparatorsCollapsible(false);
3696  contextMenuChangeNode->setTitle(tr("Change To"));
3697 
3698  populateChangeNodeMenu(contextMenuChangeNode, node, initialChangeNodeSuggestionCount);
3699  if (!contextMenuChangeNode->actions().isEmpty())
3700  contextMenu.addMenu(contextMenuChangeNode);
3701  } // End single selected node
3702 
3703  // Option: Refactor selected component(s)
3704  if (!selectionContainsMissingNode)
3705  contextMenu.addAction(contextMenuRefactorSelected);
3706 
3707  if ((numSelectedNonAttachmentNodes == 1) && (nodeClassIsEditable || nodeClassIs3rdParty))
3708  {
3709  // Option: Open the enclosing folder for an editable or other 3rd party node class.
3710  QString modulePath = nodeClassIsEditable? sourcePath : nodeClass->getCompiler()->getModulePath().c_str();
3711  if (!modulePath.isEmpty())
3712  {
3713  QString enclosingDirUrl = "file://" + QFileInfo(modulePath).dir().absolutePath();
3714  QAction *openEnclosingFolderAction = new QAction(NULL);
3715  openEnclosingFolderAction->setText("Show in Finder");
3716  openEnclosingFolderAction->setData(qVariantFromValue(enclosingDirUrl));
3717  connect(openEnclosingFolderAction, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openExternalUrlFromSenderData);
3718  contextMenu.addAction(openEnclosingFolderAction);
3719  }
3720  }
3721 
3722  if (!contextMenu.actions().empty() && !contextMenu.actions().last()->isSeparator())
3723  contextMenu.addSeparator();
3724 
3725  // Option: Delete selected components(s)
3726  contextMenu.addAction(contextMenuDeleteSelectedSnapshot);
3727 
3728  } // End non-input-drawer nodes
3729  } // End nodes
3730  } // End nodes or cables
3731 
3732  if (!contextMenu.actions().isEmpty())
3733  {
3734  // Disable non-detached port popovers so that they don't obscure the view of the context menu.
3736 
3737  menuSelectionInProgress = true;
3738  contextMenu.exec(event->screenPos());
3739  menuSelectionInProgress = false;
3740  }
3741 
3742  delete contextMenuDeleteSelectedSnapshot;
3743 }
3744 
3749 {
3750  QAction *sender = (QAction *)QObject::sender();
3751  VuoRendererNode *node = sender->data().value<VuoRendererNode *>();
3752  emit nodeSourceEditorRequested(node);
3753 }
3754 
3761 {
3762  vector<string> typeOptions;
3763 
3764  map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
3765  for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
3766  {
3767  if (((!lists && !VuoType::isListTypeName(i->first)) ||
3768  (lists && VuoType::isListTypeName(i->first))) &&
3769  !VuoType::isDictionaryTypeName(i->first) &&
3770  (i->first != "VuoMathExpressionList"))
3771  {
3772  typeOptions.push_back(i->first);
3773  }
3774  }
3775 
3776  return typeOptions;
3777 }
3778 
3784 set<string> VuoEditorComposition::getRespecializationOptionsForPortInNetwork(VuoRendererPort *port)
3785 {
3786  if (!port)
3787  return set<string>();
3788 
3789  // Find the set of connected generic ports that contains our target port.
3790  set<VuoPort *> connectedGenericPorts = getBase()->getCompiler()->getCorrelatedGenericPorts(port->getUnderlyingParentNode()->getBase(),
3791  port->getBase(), true);
3792 
3793  // Determine the set of types compatible with all generic ports in the connected network.
3794  vector<string> compatibleInnerTypeNames;
3795  vector<string> compatibleTypeNames;
3796  for (VuoPort *connectedPort : connectedGenericPorts)
3797  {
3798  VuoGenericType *genericTypeFromPortClass = NULL;
3799  VuoCompilerNodeClass *nodeClass = connectedPort->getRenderer()->getUnderlyingParentNode()->getBase()->getNodeClass()->getCompiler();
3800  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3801  if (specializedNodeClass)
3802  {
3803  VuoPortClass *portClass = connectedPort->getClass();
3804  genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3805  }
3806 
3807  VuoGenericType::Compatibility compatibility;
3808  vector<string> compatibleTypeNamesForPort = genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3809  vector<string> innermostCompatibleTypeNamesForPort;
3810  for (vector<string>::iterator k = compatibleTypeNamesForPort.begin(); k != compatibleTypeNamesForPort.end(); ++k)
3811  innermostCompatibleTypeNamesForPort.push_back( VuoType::extractInnermostTypeName(*k) );
3812 
3813  if (! innermostCompatibleTypeNamesForPort.empty())
3814  {
3815  if (compatibleInnerTypeNames.empty())
3816  compatibleInnerTypeNames = innermostCompatibleTypeNamesForPort;
3817  else
3818  {
3819  for (int k = compatibleInnerTypeNames.size() - 1; k >= 0; --k)
3820  if (find(innermostCompatibleTypeNamesForPort.begin(), innermostCompatibleTypeNamesForPort.end(), compatibleInnerTypeNames[k]) ==
3821  innermostCompatibleTypeNamesForPort.end())
3822  compatibleInnerTypeNames.erase(compatibleInnerTypeNames.begin() + k);
3823  }
3824  }
3825  }
3826 
3828  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
3829  VuoPortClass *portClass = port->getBase()->getClass();
3830  VuoGenericType *genericTypeFromPortClass = dynamic_cast<VuoGenericType *>( specializedNodeClass->getOriginalPortType(portClass) );
3831  string typeNameForPort = genericTypeFromPortClass->getModuleKey();
3832 
3833  // Finish compiling set of compatible types in the context of the connected generic port network.
3834  string prefix = (VuoType::isListTypeName(typeNameForPort) ? VuoType::listTypeNamePrefix : "");
3835  for (vector<string>::iterator k = compatibleInnerTypeNames.begin(); k != compatibleInnerTypeNames.end(); ++k)
3836  compatibleTypeNames.push_back(prefix + *k);
3837 
3838  if (compatibleTypeNames.empty())
3839  {
3840  VuoGenericType::Compatibility compatibility;
3841  genericTypeFromPortClass->getCompatibleSpecializedTypes(compatibility);
3842 
3843  // If all types or all list types are compatible, add them to (currently empty) compatibleTypeNames.
3844  if (compatibility == VuoGenericType::anyType || compatibility == VuoGenericType::anyListType)
3845  compatibleTypeNames = getAllSpecializedTypeOptions(compatibility == VuoGenericType::anyListType);
3846  }
3847 
3848  return set<string>(compatibleTypeNames.begin(), compatibleTypeNames.end());
3849 }
3850 
3871  set<string> compatibleTypesInIsolation,
3872  set<string> compatibleTypesInContext,
3873  bool limitToNodeSet,
3874  string nodeSetName,
3875  QMenu *menu)
3876 {
3877  QList<QAction *> actionsToAddToMenu;
3878 
3879  map<string, VuoCompilerType *> allTypes = compiler->getTypes();
3880  map<string, set<VuoCompilerType *> > loadedTypesForNodeSet = moduleManager->getLoadedTypesForNodeSet();
3881  QList<VuoCompilerType *> compatibleTypesForNodeSetDisplay;
3882  foreach (string typeName, compatibleTypesInIsolation)
3883  {
3884  VuoCompilerType *type = allTypes[typeName];
3885  if ((!limitToNodeSet || (loadedTypesForNodeSet[nodeSetName].find(type) != loadedTypesForNodeSet[nodeSetName].end())) &&
3886  (! VuoGenericType::isGenericTypeName(typeName)) &&
3887  // @todo: Re-enable listing of VuoUrl type for https://b33p.net/kosada/node/9204
3888  (typeName != "VuoUrl" && typeName != "VuoList_VuoUrl"
3889  // @todo: Re-enable listing of interaction type for https://b33p.net/kosada/node/11631
3890  && typeName != "VuoInteraction" && typeName != "VuoList_VuoInteraction"
3891  && typeName != "VuoInteractionType" && typeName != "VuoList_VuoInteractionType"
3892  && typeName != "VuoUuid" && typeName != "VuoList_VuoUuid"
3893  // Hide deprecated types.
3894  && typeName != "VuoIconPosition" && typeName != "VuoList_VuoIconPosition"
3895  && typeName != "VuoMesh" && typeName != "VuoList_VuoMesh"
3896  && typeName != "VuoWindowProperty" && typeName != "VuoList_VuoWindowProperty"
3897  && typeName != "VuoWindowReference" && typeName != "VuoList_VuoWindowReference"))
3898  compatibleTypesForNodeSetDisplay.append(type);
3899  }
3900 
3901  if (!compatibleTypesForNodeSetDisplay.isEmpty())
3902  {
3903  QMenu *contextMenuNodeSetTypes = NULL;
3904  QList<QAction *> actionsToAddToNodeSetSubmenu;
3905  bool enabledContentAdded = false;
3906 
3907  // Populate the "Specialize Type" submenu for the target node set.
3908  if (!nodeSetName.empty())
3909  {
3910  contextMenuNodeSetTypes = new QMenu(menu);
3911  contextMenuNodeSetTypes->setSeparatorsCollapsible(false);
3912  contextMenuNodeSetTypes->setTitle(formatNodeSetNameForDisplay(nodeSetName.c_str()));
3913  contextMenuNodeSetTypes->setToolTipsVisible(true);
3914  actionsToAddToMenu.append(contextMenuNodeSetTypes->menuAction());
3915  }
3916 
3917  foreach (VuoCompilerType *type, compatibleTypesForNodeSetDisplay)
3918  {
3919  string typeName = type->getBase()->getModuleKey();
3920  QList<QVariant> portAndSpecializedType;
3921  portAndSpecializedType.append(qVariantFromValue((void *)genericPort));
3922  portAndSpecializedType.append(typeName.c_str());
3923 
3924  QAction *specializeAction;
3925  QString typeTitle = formatTypeNameForDisplay(type->getBase());
3926 
3927  // Case: Adding to the node set submenu
3928  if (!nodeSetName.empty())
3929  {
3930  specializeAction = new QAction(typeTitle, contextMenuNodeSetTypes);
3931  actionsToAddToNodeSetSubmenu.append(specializeAction);
3932  }
3933 
3934  // Case: Adding to the top-level action list
3935  else
3936  {
3937  specializeAction = new QAction(typeTitle, menu);
3938  actionsToAddToMenu.append(specializeAction);
3939  }
3940 
3941  specializeAction->setData(QVariant(portAndSpecializedType));
3942  specializeAction->setCheckable(true);
3943  specializeAction->setChecked(genericPort && (genericPort->getDataType()->getModuleKey() == type->getBase()->getModuleKey()));
3944 
3945  if (compatibleTypesInContext.find(typeName) == compatibleTypesInContext.end())
3946  {
3947  specializeAction->setEnabled(false);
3948  //: Appears in a tooltip when hovering over a menu item for a type specialization that's prevented by a cable connection.
3949  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."));
3950  }
3951  else
3952  enabledContentAdded = true;
3953  }
3954 
3955  if (contextMenuNodeSetTypes)
3956  {
3957  addTypeActionsToMenu(actionsToAddToNodeSetSubmenu, contextMenuNodeSetTypes);
3958 
3959  if (!enabledContentAdded)
3960  contextMenuNodeSetTypes->setEnabled(false);
3961  }
3962  }
3963 
3964  QList<QAction *> actionListWithPromotions = promoteSingletonsFromSubmenus(actionsToAddToMenu);
3965  return actionListWithPromotions;
3966 }
3967 
3972 QList<QAction *> VuoEditorComposition::promoteSingletonsFromSubmenus(QList<QAction *> actionList)
3973 {
3974  QList<QAction *> modifiedActionList;
3975  foreach (QAction *action, actionList)
3976  {
3977  if (action->menu() && (action->menu()->actions().size() == 1))
3978  {
3979  QAction *singleSubaction = action->menu()->actions().first();
3980  action->menu()->removeAction(singleSubaction);
3981  modifiedActionList.append(singleSubaction);
3982  }
3983  else
3984  modifiedActionList.append(action);
3985  }
3986 
3987  return modifiedActionList;
3988 }
3989 
3993 void VuoEditorComposition::addTypeActionsToMenu(QList<QAction *> actionList, QMenu *menu)
3994 {
3995  std::sort(actionList.begin(), actionList.end(), nodeSetMenuActionLessThan);
3996  foreach (QAction *action, actionList)
3997  menu->addAction(action);
3998 }
3999 
4003 void VuoEditorComposition::updatePopoversForActiveWindowChange(QWidget *old, QWidget *now)
4004 {
4005  if (!now)
4006  return;
4007 
4009  if (activeWindow)
4010  emit compositionOnTop(activeWindow->getComposition() == this);
4011 }
4012 
4016 void VuoEditorComposition::updatePopoversForApplicationStateChange(bool active)
4017 {
4018  if (ignoreApplicationStateChangeEvents)
4019  return;
4020 
4022  if (activeWindow && (activeWindow->getComposition() == this) && (!activeWindow->isMinimized()))
4023  emit applicationActive(active);
4024 }
4025 
4034 QGraphicsItem * VuoEditorComposition::findNearbyPort(QPointF scenePos, bool limitPortCollisionRange)
4035 {
4036  return findNearbyComponent(scenePos, VuoEditorComposition::targetTypePort, limitPortCollisionRange);
4037 }
4038 
4043 {
4044  QGraphicsItem *item = findNearbyComponent(scenePos, VuoEditorComposition::targetTypeNodeHeader);
4045  return dynamic_cast<VuoRendererNode *>(item);
4046 }
4047 
4060 QGraphicsItem * VuoEditorComposition::findNearbyComponent(QPointF scenePos,
4061  targetComponentType targetType,
4062  bool limitPortCollisionRange)
4063 {
4064  // Determine which types of components to filter out of search results.
4065  bool ignoreCables;
4066  bool ignoreNodes;
4067  bool ignorePorts;
4068  bool ignoreComments;
4069 
4070  switch(targetType)
4071  {
4072  case VuoEditorComposition::targetTypePort:
4073  {
4074  ignoreCables = true;
4075  ignoreNodes = true;
4076  ignorePorts = false;
4077  ignoreComments = true;
4078  break;
4079  }
4080  case VuoEditorComposition::targetTypeNodeHeader:
4081  {
4082  ignoreCables = true;
4083  ignoreNodes = true;
4084  ignorePorts = true;
4085  ignoreComments = true;
4086  break;
4087  }
4088  default:
4089  {
4090  ignoreCables = false;
4091  ignoreNodes = false;
4092  ignorePorts = false;
4093  ignoreComments = false;
4094  break;
4095  }
4096  }
4097 
4098  // The topmost item under the cursor is not necessarily the one we will return
4099  // (e.g., if the cursor is positioned directly over a node but also within range
4100  // of one of that node's ports), but it will factor in to the decision.
4101  QGraphicsItem *topmostItemUnderCursor = itemAt(scenePos, views()[0]->transform());
4102  if (topmostItemUnderCursor && (!topmostItemUnderCursor->isEnabled()))
4103  topmostItemUnderCursor = NULL;
4104 
4105  for (int rectLength = componentCollisionRange/2; rectLength <= componentCollisionRange; rectLength += componentCollisionRange/2)
4106  {
4107  QRectF searchRect(scenePos.x()-0.5*rectLength, scenePos.y()-0.5*rectLength, rectLength, rectLength);
4108  QList<QGraphicsItem *> itemsInRange = items(searchRect);
4109  for (QList<QGraphicsItem *>::iterator i = itemsInRange.begin(); i != itemsInRange.end(); ++i)
4110  {
4111  if (!(*i)->isEnabled())
4112  continue;
4113 
4114  // Check whether we have located an unobscured "Make List" drawer drag handle.
4115  if (! ignoreNodes)
4116  {
4117  VuoRendererInputListDrawer *makeListDrawer = dynamic_cast<VuoRendererInputListDrawer *>(*i);
4118  bool makeListDragHandle =
4119  (makeListDrawer &&
4120  makeListDrawer->getExtendedDragHandleRect().translated(makeListDrawer->scenePos()).contains(scenePos));
4121  if (makeListDragHandle &&
4122  ((! topmostItemUnderCursor) ||
4123  (topmostItemUnderCursor == makeListDrawer) ||
4124  (topmostItemUnderCursor->zValue() < makeListDrawer->zValue())))
4125  {
4126  return makeListDrawer;
4127  }
4128  }
4129 
4130  // Check whether we have located an unobscured port.
4131  // Hovering within range of a port takes precedence over hovering
4132  // directly over that port's parent node.
4133  if (! ignorePorts)
4134  {
4135  VuoRendererPort *port = dynamic_cast<VuoRendererPort *>(*i);
4136  if (port &&
4137  ((! topmostItemUnderCursor) ||
4138  (topmostItemUnderCursor == port) ||
4139  (topmostItemUnderCursor == port->getRenderedParentNode()) ||
4140  (topmostItemUnderCursor->zValue() < port->zValue()))
4141  &&
4142  ((! limitPortCollisionRange) ||
4143  port->getPortRect().united(port->getPortConstantTextRect()).translated(port->scenePos()).intersects(searchRect))
4144  &&
4145  ! port->getFunctionPort())
4146  {
4147  return port;
4148  }
4149  }
4150 
4151  // Check whether we have located an unobscured cable.
4152  if (! ignoreCables)
4153  {
4154  VuoRendererCable *cable = dynamic_cast<VuoRendererCable *>(*i);
4155  if (cable &&
4156  ((! topmostItemUnderCursor) ||
4157  (topmostItemUnderCursor == cable) ||
4158  (topmostItemUnderCursor->zValue() < cable->zValue())))
4159  {
4160  return cable;
4161  }
4162  }
4163 
4164  // Check whether we have located an unobscured comment.
4165  if (! ignoreComments)
4166  {
4167  VuoRendererComment *comment = dynamic_cast<VuoRendererComment *>(*i);
4168  if (!comment && dynamic_cast<VuoRendererComment *>((*i)->parentItem()))
4169  comment = dynamic_cast<VuoRendererComment *>((*i)->parentItem());
4170 
4171  if (comment &&
4172  ((! topmostItemUnderCursor) ||
4173  (topmostItemUnderCursor == (*i)) ||
4174  (topmostItemUnderCursor->zValue() < (*i)->zValue())))
4175  {
4176  return comment;
4177  }
4178  }
4179 
4180  // Check whether we have located an unobscured node header.
4181  if (targetType == VuoEditorComposition::targetTypeNodeHeader)
4182  {
4183  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(*i);
4184  if (node && ! dynamic_cast<VuoRendererInputDrawer *>(node))
4185  {
4186  QRectF headerRect = node->getOuterNodeFrameBoundingRect();
4187  headerRect = node->mapToScene(headerRect).boundingRect();
4188  if (headerRect.intersects(searchRect) && scenePos.y() <= headerRect.bottom())
4189  return node;
4190  }
4191  }
4192  }
4193  }
4194 
4195  // Having failed to locate any other relevant components within range, return the node
4196  // directly under the cursor, if applicable.
4197  if (! ignoreNodes)
4198  {
4199  if (dynamic_cast<VuoRendererNode *>(topmostItemUnderCursor))
4200  return topmostItemUnderCursor;
4201 
4202  // It is possible that the item under the cursor is a port, but that it didn't meet
4203  // the more stringent limitPortCollisionRange requirement. In this case, return its parent node.
4204  if (dynamic_cast<VuoRendererPort *>(topmostItemUnderCursor))
4205  return ((VuoRendererPort *)(topmostItemUnderCursor))->getRenderedParentNode();
4206  }
4207 
4208  return NULL;
4209 }
4210 
4221 {
4222  if (! cableInProgress)
4223  return NULL;
4224 
4225  VuoPort *fromPort = cableInProgress->getFromPort();
4226  VuoPort *toPort = cableInProgress->getToPort();
4227  VuoPort *fixedPort = (fromPort? fromPort: toPort);
4228 
4229  if (dynamic_cast<VuoRendererPort *>(fixedPort->getRenderer())->getUnderlyingParentNode() == node)
4230  return NULL;
4231 
4232  return findDefaultPortForEventOnlyConnection(node, (fixedPort == fromPort));
4233 }
4234 
4240 {
4241  // Start with the first input or output port (other than the refresh port).
4242  vector<VuoRendererPort *> portList;
4243  int firstPortIndex;
4244 
4245  if (inputPort)
4246  {
4247  portList = node->getInputPorts();
4249  }
4250  else
4251  {
4252  portList = node->getOutputPorts();
4254  }
4255 
4256  VuoRendererPort *targetPort = NULL;
4257  if (portList.size() > firstPortIndex)
4258  {
4259  targetPort = portList[firstPortIndex];
4260 
4261  // If the first input port has a wall,
4262  // keep moving down until we find one without a wall.
4263  // (Unless they all have walls, in which case stick with the first.)
4264  VuoRendererPort *firstPortWithoutWall = nullptr;
4265  for (int i = firstPortIndex; i < portList.size(); ++i)
4266  {
4267  if (portList[i]->getBase()->getClass()->getEventBlocking() != VuoPortClass::EventBlocking_Wall)
4268  {
4269  firstPortWithoutWall = portList[i];
4270  break;
4271  }
4272  }
4273  if (firstPortWithoutWall)
4274  targetPort = firstPortWithoutWall;
4275 
4276  // If the first input port has a drawer attached to it,
4277  // instead select the first input port of the drawer.
4278  // (Unless the drawer has no input ports, in which case don't select any port.)
4279  VuoRendererInputDrawer *drawer = targetPort->getAttachedInputDrawer();
4280  if (drawer)
4281  {
4282  portList = drawer->getInputPorts();
4283  if (portList.size() > firstPortIndex)
4284  targetPort = portList[firstPortIndex];
4285  else
4286  targetPort = NULL;
4287  }
4288 
4289  // If the selected input port has a collapsed type-converter node attached to it,
4290  // instead select the type-converter node's input port.
4291  VuoRendererTypecastPort *typecast = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
4292  if (typecast)
4293  targetPort = typecast->getChildPort();
4294  }
4295 
4296  return targetPort;
4297 }
4298 
4304 {
4305  QRectF boundingRect;
4306  foreach (QGraphicsItem *item, items())
4307  {
4308  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4309  if (! (rc && rc->getBase()->isPublished()))
4310  boundingRect |= item->sceneBoundingRect();
4311  }
4312 
4313  return boundingRect;
4314 }
4315 
4321 {
4322  QRectF boundingRect;
4323 
4324  foreach (QGraphicsItem *item, selectedItems())
4325  {
4326  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4327  if (! (rc && rc->getBase()->isPublished()))
4328  boundingRect |= item->sceneBoundingRect();
4329  }
4330 
4331  return boundingRect;
4332 }
4333 
4339 {
4340  QRectF boundingRect;
4341 
4342  foreach (QGraphicsItem *item, selectedItems())
4343  {
4344  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(item);
4345  if (! (rc && rc->getBase()->isPublished()))
4346  boundingRect |= item->mapToScene(item->childrenBoundingRect()).boundingRect();
4347 
4348  // Include attached drawers.
4349  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(item);
4350  if (rn)
4351  {
4352  foreach (VuoPort *port, rn->getBase()->getInputPorts())
4353  {
4354  VuoRendererInputDrawer *drawer = (port->hasRenderer()? port->getRenderer()->getAttachedInputDrawer() : NULL);
4355  if (drawer)
4356  boundingRect |= drawer->mapToScene(drawer->childrenBoundingRect()).boundingRect();
4357  }
4358  }
4359  }
4360 
4361  return boundingRect;
4362 }
4363 
4368 void VuoEditorComposition::updateInternalPortConstant(string portID, string newValue, bool updateInRunningComposition)
4369 {
4370  VuoPort *port = getPortWithStaticIdentifier(portID);
4371  if (!port)
4372  return;
4373 
4374  updatePortConstant(dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4375 }
4376 
4381 void VuoEditorComposition::updatePublishedPortConstant(string portName, string newValue, bool updateInRunningComposition)
4382 {
4384  if (!port)
4385  return;
4386 
4387  updatePortConstant(dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler()), newValue, updateInRunningComposition);
4388 }
4389 
4394 void VuoEditorComposition::updatePortConstant(VuoCompilerPort *port, string newValue, bool updateInRunningComposition)
4395 {
4396  // Internal ports
4397  if (dynamic_cast<VuoCompilerInputEventPort *>(port))
4398  {
4399  port->getBase()->getRenderer()->setConstant(newValue);
4400 
4401  if (updateInRunningComposition)
4403  }
4404 
4405  // Published ports
4406  else if (dynamic_cast<VuoCompilerPublishedPort *>(port))
4407  {
4408  dynamic_cast<VuoCompilerPublishedPort *>(port)->setInitialValue(newValue);
4409 
4410  if (updateInRunningComposition)
4412  }
4413 }
4414 
4420 {
4423 }
4424 
4433 {
4434 
4435  // Prevent recursive updates of feedback errors (resulting, e.g., from show()ing popovers).
4436  if (!errorMarkingUpdatesEnabled)
4437  return;
4438 
4439  errorMarkingUpdatesEnabled = false;
4440 
4441  // Remove any error annotations from the previous call to this function.
4442  if (errorMark)
4443  {
4444  errorMark->removeFromScene();
4445  errorMark = NULL;
4446  }
4447 
4449 
4450  // Check for errors.
4451 
4452  VuoCompilerIssues *issues = new VuoCompilerIssues();
4453  VuoCompilerCable *potentialCable = NULL;
4454  try
4455  {
4456  set<VuoCompilerCable *> potentialCables;
4457 
4458  if (targetPort && cableInProgress)
4459  {
4460  VuoNode *fromNode;
4461  VuoPort *fromPort;
4462  VuoNode *toNode;
4463  VuoPort *toPort;
4464  if (cableInProgress->getFromNode())
4465  {
4466  fromNode = cableInProgress->getFromNode();
4467  fromPort = cableInProgress->getFromPort();
4468  toNode = targetPort->getUnderlyingParentNode()->getBase();
4469  toPort = targetPort->getBase();
4470  }
4471  else
4472  {
4473  fromNode = targetPort->getUnderlyingParentNode()->getBase();
4474  fromPort = targetPort->getBase();
4475  toNode = cableInProgress->getToNode();
4476  toPort = cableInProgress->getToPort();
4477  }
4478  potentialCable = new VuoCompilerCable(NULL, NULL, NULL, NULL);
4479  potentialCable->getBase()->setFrom(fromNode, fromPort);
4480  potentialCable->getBase()->setTo(toNode, toPort);
4481  potentialCable->setAlwaysEventOnly(! cableInProgress->getRenderer()->effectivelyCarriesData() ||
4482  cableInProgress->getRenderer()->isFloatingEndpointAboveEventPort());
4483 
4484  fromPort->removeConnectedCable(potentialCable->getBase());
4485  toPort->removeConnectedCable(potentialCable->getBase());
4486  potentialCables.insert(potentialCable);
4487  }
4488 
4489  getBase()->getCompiler()->checkForEventFlowIssues(potentialCables, issues);
4490  }
4491  catch (const VuoCompilerException &e)
4492  {
4493  }
4494 
4495  if (! issues->isEmpty())
4496  {
4497  VUserLog("%s: Showing error popover: %s",
4498  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
4499  issues->getShortDescription(false).c_str());
4500 
4501  this->errorMark = new VuoErrorMark();
4502 
4503  foreach (VuoCompilerIssue issue, issues->getList())
4504  {
4505  set<VuoRendererNode *> nodesToMark;
4506  set<VuoRendererCable *> cablesToMark;
4507 
4508  set<VuoNode *> problemNodes = issue.getNodes();
4509  foreach (VuoNode *node, problemNodes)
4510  if (node->hasRenderer())
4511  nodesToMark.insert(node->getRenderer());
4512 
4513  set<VuoCable *> problemCables = issue.getCables();
4514  foreach (VuoCable *cable, problemCables)
4515  {
4516  VuoCable *cableToMark = (cable->getCompiler() == potentialCable ? cableInProgress : cable);
4517  if (cableToMark->hasRenderer())
4518  cablesToMark.insert(cableToMark->getRenderer());
4519  }
4520 
4522  errorMark->addMarkedComponents(nodesToMark, cablesToMark);
4523 
4524  VuoErrorPopover *errorPopover = new VuoErrorPopover(issue, NULL);
4525  errorPopovers.insert(errorPopover);
4528 
4529  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
4530 
4531  // Place the popover near an appropriate nearby node involved in the feedback loop.
4532  VuoRendererNode *nearbyNode = NULL;
4533  if (targetPort && cableInProgress && nodesToMark.find(targetPort->getRenderedParentNode()) != nodesToMark.end())
4534  {
4535  nearbyNode = targetPort->getRenderedParentNode();
4536  }
4537  else if (! nodesToMark.empty())
4538  {
4539  VuoRendererNode *topmostVisibleNode = NULL;
4540  qreal topY = viewportRect.bottom();
4541  foreach (VuoRendererNode *node, nodesToMark)
4542  {
4543  if (node->getProxyNode())
4544  node = node->getProxyNode();
4545 
4546  QPointF scenePos = node->scenePos();
4547  if (viewportRect.contains(scenePos) && (scenePos.y() < topY))
4548  {
4549  topmostVisibleNode = node;
4550  topY = scenePos.y();
4551  }
4552  }
4553 
4554  if (topmostVisibleNode)
4555  nearbyNode = topmostVisibleNode;
4556  else
4557  {
4558  VuoRendererNode *firstMarkedNode = *nodesToMark.begin();
4559  nearbyNode = (firstMarkedNode->getProxyNode()? firstMarkedNode->getProxyNode(): firstMarkedNode);
4560  }
4561  }
4562  else
4563  {
4564  VUserLog("Warning: no nearby node (no marked nodes).");
4565  }
4566 
4567  // If no nodes are known to be involved in the feedback loop, display the popover
4568  // in the center of the viewport.
4569  const QPoint offsetFromNode(0,10);
4570  QPoint popoverTopLeftInScene = (nearbyNode?
4571  (nearbyNode->scenePos().toPoint() +
4572  nearbyNode->getOuterNodeFrameBoundingRect().bottomLeft().toPoint() +
4573  offsetFromNode) :
4574  QPoint(viewportRect.center().x() - 0.5*errorPopover->sizeHint().width(),
4575  viewportRect.center().y() - 0.5*errorPopover->sizeHint().height()));
4576 
4577  // If all nodes involved in the feedback loop are offscreen, display the popover at the edge
4578  // of the viewport closest to the feedback loop.
4579  const int margin = 5;
4580  popoverTopLeftInScene = (QPoint(fmin(popoverTopLeftInScene.x(), viewportRect.bottomRight().x() - errorPopover->sizeHint().width() - margin),
4581  fmin(popoverTopLeftInScene.y(), viewportRect.bottomRight().y() - errorPopover->sizeHint().height() - margin)));
4582 
4583  popoverTopLeftInScene = (QPoint(fmax(popoverTopLeftInScene.x(), viewportRect.topLeft().x() + margin),
4584  fmax(popoverTopLeftInScene.y(), viewportRect.topLeft().y() + margin)));
4585 
4586  QPoint popoverTopLeftInView = views()[0]->mapFromScene(popoverTopLeftInScene);
4587  QPoint popoverTopLeftGlobal = views()[0]->mapToGlobal(popoverTopLeftInView);
4588 
4589  errorPopover->move(popoverTopLeftGlobal);
4590  errorPopover->show();
4591  emit popoverDetached();
4592  }
4593 
4594  // Add error annotations to the composition.
4595  addItem(errorMark);
4596  }
4597  delete issues;
4598  delete potentialCable;
4599 
4600  errorMarkingUpdatesEnabled = true;
4601 }
4602 
4606 bool VuoEditorComposition::hasFeedbackErrors(void)
4607 {
4608  return this->errorMark;
4609 }
4610 
4615 {
4616  if (hasFeedbackErrors())
4617  this->errorMark->updateErrorMarkPath();
4618 }
4619 
4625 void VuoEditorComposition::buildComposition(string compositionSnapshot, const set<string> &dependenciesUninstalled)
4626 {
4627  try
4628  {
4629  emit buildStarted();
4630 
4631  if (! dependenciesUninstalled.empty())
4632  {
4633  vector<string> dependenciesRemovedVec(dependenciesUninstalled.begin(), dependenciesUninstalled.end());
4634  string dependenciesStr = VuoStringUtilities::join(dependenciesRemovedVec, " ");
4635  throw VuoException("Some modules that the composition needs were uninstalled: " + dependenciesStr);
4636  }
4637 
4638  delete runningComposition;
4639  runningComposition = NULL;
4640  runningComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionSnapshot, compiler);
4641 
4642  runningCompositionActiveDriver = getDriverForActiveProtocol();
4643  if (runningCompositionActiveDriver)
4644  runningCompositionActiveDriver->applyToComposition(runningComposition, compiler);
4645 
4646  string compiledCompositionPath = VuoFileUtilities::makeTmpFile(this->getBase()->getMetadata()->getName(), "bc");
4647  string dir, file, ext;
4648  VuoFileUtilities::splitPath(compiledCompositionPath, dir, file, ext);
4649  linkedCompositionPath = dir + file + ".dylib";
4650 
4651  compiler->setShouldPotentiallyShowSplashWindow(false);
4652 
4653  VuoCompilerIssues *issues = new VuoCompilerIssues();
4654  compiler->compileComposition(runningComposition, compiledCompositionPath, true, issues);
4655  compiler->linkCompositionToCreateDynamicLibraries(compiledCompositionPath, linkedCompositionPath, runningCompositionLibraries.get());
4656  delete issues;
4657 
4658  remove(compiledCompositionPath.c_str());
4659 
4660  emit buildFinished("");
4661  }
4662  catch (VuoException &e)
4663  {
4664  delete runningComposition;
4665  runningComposition = NULL;
4666 
4667  emit buildFinished(e.what());
4668  throw;
4669  }
4670 }
4671 
4677 bool VuoEditorComposition::isRunningThreadUnsafe(void)
4678 {
4679  return runner != NULL && ! stopRequested && ! runner->isStopped();
4680 }
4681 
4688 {
4689  __block bool running;
4690  dispatch_sync(runCompositionQueue, ^{
4691  running = isRunningThreadUnsafe();
4692  });
4693  return running;
4694 }
4695 
4701 void VuoEditorComposition::run(string compositionSnapshot)
4702 {
4703  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4704 
4705  // If this is a subcomposition that was opened from a parent composition, now treat it as its own top-level composition.
4706  subcompositionRouter->unlinkSubcompositionFromNodeInSupercomposition(this);
4707 
4708  // If this is a subcomposition, tell the compiler to reload it as a node class and notify other compositions that depend on it.
4709  subcompositionRouter->applyToAllOtherCompositionsInstalledAsSubcompositions(this, ^void (VuoEditorComposition *subcomposition, string subcompositionPath)
4710  {
4711  compiler->overrideInstalledNodeClass(subcompositionPath, subcomposition->takeSnapshot());
4712  });
4713 
4714  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 1.
4715  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4716  {
4717  if (matchingComposition->showEventsMode)
4718  matchingComposition->beginDisplayingActivity();
4719  });
4720 
4721  stopRequested = false;
4722  dispatch_async(runCompositionQueue, ^{
4723  try
4724  {
4725  runningCompositionLibraries = std::make_shared<VuoRunningCompositionLibraries>();
4726 
4727  buildComposition(compositionSnapshot);
4728 
4729  string compositionLoaderPath = compiler->getCompositionLoaderPath();
4730  string compositionSourceDir = getBase()->getDirectory();
4731 
4732  runner = VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(compositionLoaderPath, linkedCompositionPath, runningCompositionLibraries, compositionSourceDir, true, true);
4733  runner->setDelegate(this);
4735  runner->startPaused();
4736  pid_t pid = runner->getCompositionPid();
4737 
4738  // Tell this composition and all subcompositions opened from it to start displaying live debug info — part 2.
4739  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4740  {
4741  if (matchingComposition->showEventsMode)
4742  this->runner->subscribeToEventTelemetry(matchingCompositionIdentifier);
4743 
4744  dispatch_sync(activePortPopoversQueue, ^{
4745  for (auto i : matchingComposition->activePortPopovers)
4746  {
4747  string portID = i.first;
4748  updateDataInPortPopoverFromRunningTopLevelComposition(matchingComposition, matchingCompositionIdentifier, portID);
4749  }
4750  });
4751  });
4752 
4753  runner->unpause();
4754 
4755  // Focus the composition's windows (if any).
4756  VuoFileUtilities::focusProcess(pid, true);
4757  }
4758  catch (...) { }
4759  });
4760 }
4761 
4768 {
4769  stopRequested = true;
4770  dispatch_async(runCompositionQueue, ^{
4771  if (runner && ! runner->isStopped())
4772  {
4773  runner->stop();
4774  runner->waitUntilStopped();
4775  }
4776  delete runner;
4777  runner = NULL;
4778 
4779  linkedCompositionPath = "";
4780 
4781  runningCompositionLibraries = nullptr; // release shared_ptr
4782 
4783  delete runningComposition;
4784  runningComposition = NULL;
4785 
4786  emit stopFinished();
4787  });
4788 
4789  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
4790 
4791  // Tell this composition and all subcompositions opened from it to stop display live debug info.
4792  subcompositionRouter->applyToAllLinkedCompositions(this, ^void (VuoEditorComposition *matchingComposition, string matchingCompositionIdentifier)
4793  {
4794  if (matchingComposition->showEventsMode)
4795  matchingComposition->stopDisplayingActivity();
4796 
4797  dispatch_sync(activePortPopoversQueue, ^{
4798  for (auto i : matchingComposition->activePortPopovers)
4799  {
4800  VuoPortPopover *popover = i.second;
4801  popover->setCompositionRunning(false);
4802  }
4803  });
4804  });
4805 }
4806 
4818 void VuoEditorComposition::updateRunningComposition(string oldCompositionSnapshot, string newCompositionSnapshot,
4819  VuoCompilerCompositionDiff *diffInfo, set<string> dependenciesUninstalled)
4820 {
4821  if (! diffInfo)
4822  diffInfo = new VuoCompilerCompositionDiff();
4823 
4824  dispatch_async(runCompositionQueue, ^{
4825  if (isRunningThreadUnsafe())
4826  {
4827  try
4828  {
4829  foreach (string moduleKey, diffInfo->getModuleKeysReplaced())
4830  {
4831  runningCompositionLibraries->enqueueLibraryContainingDependencyToUnload(moduleKey);
4832  }
4833 
4834  string oldBuiltCompositionSnapshot = oldCompositionSnapshot;
4835  VuoCompilerDriver *previouslyActiveDriver = runningCompositionActiveDriver;
4836  if (previouslyActiveDriver)
4837  {
4838  VuoCompilerComposition *oldBuiltComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(oldCompositionSnapshot, compiler);
4839  previouslyActiveDriver->applyToComposition(oldBuiltComposition, compiler);
4840  oldBuiltCompositionSnapshot = oldBuiltComposition->getGraphvizDeclaration(getActiveProtocol());
4841  }
4842 
4843  buildComposition(newCompositionSnapshot, dependenciesUninstalled);
4844 
4845  string compositionDiff = diffInfo->diff(oldBuiltCompositionSnapshot, runningComposition, compiler);
4846  runner->replaceComposition(linkedCompositionPath, compositionDiff);
4847  }
4848  catch (exception &e)
4849  {
4850  VUserLog("Composition stopped itself: %s", e.what());
4851  emit compositionStoppedItself();
4852  }
4853  catch (...)
4854  {
4855  VUserLog("Composition stopped itself.");
4856  emit compositionStoppedItself();
4857  }
4858  }
4859  else
4860  {
4861  dispatch_async(dispatch_get_main_queue(), ^{
4862  updateCompositionsThatContainThisSubcomposition(newCompositionSnapshot);
4863  });
4864  }
4865 
4866  delete diffInfo;
4867  });
4868 }
4869 
4875 {
4876  void (^reloadSubcompositionIfUnsaved)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *currComposition, string compositionPath)
4877  {
4878  compiler->overrideInstalledNodeClass(compositionPath, newCompositionSnapshot);
4879  };
4880  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, reloadSubcompositionIfUnsaved);
4881 }
4882 
4888 {
4889  string constant;
4890  identifierCache->doForPortWithIdentifier(runningPortID, [&constant](VuoPort *port) {
4891  if (port->hasCompiler() && port->hasRenderer())
4892  constant = port->getRenderer()->getConstantAsString();
4893  });
4894 
4895  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4896  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4897  {
4898  if (this == topLevelComposition)
4899  {
4900  dispatch_async(runCompositionQueue, ^{
4901  if (isRunningThreadUnsafe())
4902  {
4903  json_object *constantObject = json_tokener_parse(constant.c_str());
4904  runner->setInputPortValue(thisCompositionIdentifier, runningPortID, constantObject);
4905  }
4906  });
4907  }
4908  };
4909  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4910 
4911  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4912  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4913  {
4914  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4915  {
4916  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortID, constant);
4917  });
4918  });
4919 }
4920 
4926 {
4928  if (!(port && port->hasCompiler()))
4929  return;
4930 
4931  string constant = dynamic_cast<VuoCompilerPublishedPort *>(port->getCompiler())->getInitialValue();
4933 }
4934 
4939 {
4940  string runningPortIdentifier = identifierCache->getIdentifierForPort(port->getBase());
4941  if (runningPortIdentifier.empty())
4942  return;
4943 
4944  // Live-update the top-level composition, which may be either the composition itself or a supercomposition.
4945  void (^updateRunningComposition)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
4946  {
4947  if (this == topLevelComposition)
4948  {
4949  dispatch_async(runCompositionQueue, ^{
4950  if (isRunningThreadUnsafe())
4951  {
4952  json_object *constantObject = json_tokener_parse(constant.c_str());
4953  runner->setInputPortValue(thisCompositionIdentifier, runningPortIdentifier, constantObject);
4954  }
4955  });
4956  }
4957  };
4958  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, updateRunningComposition);
4959 
4960  // If this is a subcomposition, live-update all other top-level compositions that contain it.
4961  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyIfInstalledAsSubcomposition(this, ^(VuoEditorComposition *currComposition, string subcompositionPath)
4962  {
4963  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToAllOtherTopLevelCompositions(this, ^(VuoEditorComposition *topLevelComposition)
4964  {
4965  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, runningPortIdentifier, constant);
4966  });
4967  });
4968 }
4969 
4974 void VuoEditorComposition::updateInternalPortConstantInSubcompositionInstances(string subcompositionPath, string portIdentifier, string constant)
4975 {
4976  dispatch_async(runCompositionQueue, ^{
4977  if (isRunningThreadUnsafe())
4978  {
4979  json_object *constantObject = json_tokener_parse(constant.c_str());
4980  set<string> subcompositionIdentifiers = moduleManager->findInstancesOfNodeClass(subcompositionPath);
4981  foreach (string subcompositionIdentifier, subcompositionIdentifiers)
4982  {
4983  runner->setInputPortValue(subcompositionIdentifier, portIdentifier, constantObject);
4984  }
4985  }
4986  });
4987 }
4988 
4993 {
4994  dispatch_async(runCompositionQueue, ^{
4995  if (isRunningThreadUnsafe())
4996  {
4997  VuoRunner::Port *publishedPort = runner->getPublishedInputPortWithName(port->getClass()->getName());
4998  if (publishedPort)
4999  {
5000  json_object *constantObject = json_tokener_parse(constant.c_str());
5001  map<VuoRunner::Port *, json_object *> m;
5002  m[publishedPort] = constantObject;
5003  runner->setPublishedInputPortValues(m);
5004  }
5005  }
5006  });
5007 }
5008 
5009 
5014 {
5015  return contextMenuDeleteSelected;
5016 }
5017 
5022 {
5023  // Workaround for a bug in Qt 5.1.0-beta1 (https://b33p.net/kosada/node/5096).
5024  // For now, this recreates the context menu rather than accessing a data member.
5025  QMenu *contextMenuTints = new QMenu(parent);
5026  contextMenuTints->setSeparatorsCollapsible(false);
5027  contextMenuTints->setTitle(tr("Tint"));
5028  foreach (QAction *tintAction, contextMenuTintActions)
5029  contextMenuTints->addAction(tintAction);
5030  contextMenuTints->insertSeparator(contextMenuTintActions.last());
5031 
5032  return contextMenuTints;
5033 }
5034 
5038 void VuoEditorComposition::expandChangeNodeMenu()
5039 {
5040  QAction *sender = (QAction *)QObject::sender();
5041  VuoRendererNode *node = static_cast<VuoRendererNode *>(sender->data().value<void *>());
5042 
5043  // If the menu hasn't been expanded previously, expand it enough now to fill
5044  // the available vertical screenspace.
5045  int currentMatchesListed = contextMenuChangeNode->actions().size()-1; // -1 to account for the "More…" row
5046  if (currentMatchesListed <= initialChangeNodeSuggestionCount)
5047  {
5048  int availableVerticalSpace = QApplication::desktop()->availableGeometry(VuoEditorWindow::getMostRecentActiveEditorWindow()).height();
5049  int verticalSpacePerItem = 21; // menu row height in pixels
5050  // Estimate the number of matches that will fit within the screen without scrolling:
5051  // -1 to account for a possible extra "More…" row;
5052  // -1 wiggle room to match observations
5053  int targetMatches = availableVerticalSpace/verticalSpacePerItem - 2;
5054 
5055  populateChangeNodeMenu(contextMenuChangeNode, node, targetMatches);
5056  }
5057 
5058  // If the menu has already been expanded once, don't impose any cap on listed matches this time.
5059  else
5060  populateChangeNodeMenu(contextMenuChangeNode, node, 0);
5061 
5062  contextMenuChangeNode->exec();
5063 }
5064 
5070 void VuoEditorComposition::populateChangeNodeMenu(QMenu *menu, VuoRendererNode *node, int matchLimit)
5071 {
5072  menu->clear();
5073 
5074  if (!node)
5075  return;
5076 
5077  map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
5078  vector<VuoCompilerNodeClass *> loadedNodeClasses;
5079  for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
5080  loadedNodeClasses.push_back(i->second);
5081  VuoNodeLibrary::cullHiddenNodeClasses(loadedNodeClasses);
5082 
5083  vector<string> bestMatches;
5084  map<string, double> matchScores;
5085  matchScores[""] = 0;
5086 
5087  int targetMatchCount = (matchLimit > 0? matchLimit : loadedNodeClasses.size());
5088  for (int i = 0; i < targetMatchCount; ++i)
5089  bestMatches.push_back("");
5090 
5091  // Maintain a priority queue with the @c targetMatchCount best matches.
5092  bool overflow = false;
5093 
5094  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5095  string originalGenericNodeClassName;
5096  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5097  originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5098  else
5099  originalGenericNodeClassName = nodeClass->getClassName();
5100 
5101  foreach (VuoCompilerNodeClass *loadedNodeClass, loadedNodeClasses)
5102  {
5103  string loadedNodeClassName = loadedNodeClass->getBase()->getClassName();
5104  if (loadedNodeClassName == originalGenericNodeClassName)
5105  continue;
5106 
5107  bool canSwapNondestructively = canSwapWithoutBreakingCables(node, loadedNodeClass->getBase());
5108  double matchScore = (canSwapNondestructively? calculateNodeSimilarity(nodeClass, loadedNodeClass->getBase()) : 0);
5109  int highestIndexWithCompetitiveScore = -1;
5110  for (int i = targetMatchCount-1; (i >= 0) && (highestIndexWithCompetitiveScore == -1); --i)
5111  if (matchScore <= matchScores[bestMatches[i] ])
5112  highestIndexWithCompetitiveScore = i;
5113 
5114  if (highestIndexWithCompetitiveScore < targetMatchCount-1)
5115  {
5116  if (matchScores[bestMatches[targetMatchCount-1] ] > 0)
5117  overflow = true;
5118 
5119  for (int j = targetMatchCount-2; j > highestIndexWithCompetitiveScore; --j)
5120  bestMatches[j+1] = bestMatches[j];
5121 
5122  bestMatches[highestIndexWithCompetitiveScore+1] = loadedNodeClassName;
5123  matchScores[loadedNodeClassName] = matchScore;
5124  }
5125  }
5126 
5127  for (int i = 0; i < targetMatchCount; ++i)
5128  {
5129  if (matchScores[bestMatches[i] ] > 0)
5130  {
5131  // Disambiguate between identical node titles using node class names.
5132  QString matchDisplayText = compiler->getNodeClass(bestMatches[i])->getBase()->getDefaultTitle().c_str();
5133  if (matchDisplayText == nodeClass->getDefaultTitle().c_str())
5134  matchDisplayText += QString(" (%1)").arg(bestMatches[i].c_str());
5135 
5136  QAction *changeAction = menu->addAction(matchDisplayText);
5137 
5138  QList<QVariant> currentNodeAndNewClass;
5139  currentNodeAndNewClass.append(qVariantFromValue((void *)node));
5140  currentNodeAndNewClass.append(bestMatches[i].c_str());
5141  changeAction->setData(QVariant(currentNodeAndNewClass));
5142  connect(changeAction, &QAction::triggered, this, &VuoEditorComposition::swapNode);
5143  }
5144  }
5145 
5146  if (overflow)
5147  {
5148  //: Appears at the bottom of the "Change Node" menu when there are more options than can fit onscreen.
5149  QAction *showMoreAction = menu->addAction(tr("More…"));
5150  showMoreAction->setData(qVariantFromValue(static_cast<void *>(node)));
5151  connect(showMoreAction, &QAction::triggered, this, &VuoEditorComposition::expandChangeNodeMenu);
5152  }
5153 }
5154 
5159 bool VuoEditorComposition::canSwapWithoutBreakingCables(VuoRendererNode *origNode, VuoNodeClass *newNodeClass)
5160 {
5161  // Inventory required input port types (connected data inputs) in the node to be replaced.
5162  map<string, int> requiredInputs;
5163  bool inputEventSourceRequired = false;
5164  foreach (VuoRendererPort *inputPort, origNode->getInputPorts())
5165  {
5166  bool hasDrawerWithNoIncomingCables = false;
5167  bool hasDrawerWithNoIncomingDataCables = false;
5168 
5169  if (inputPort->getDataType() && inputPort->effectivelyHasConnectedDataCable(true))
5170  {
5171  VuoRendererInputDrawer *inputDrawer = inputPort->getAttachedInputDrawer();
5172  if (inputDrawer)
5173  {
5174  hasDrawerWithNoIncomingCables = true;
5175  hasDrawerWithNoIncomingDataCables = true;
5176  vector<VuoRendererPort *> childPorts = inputDrawer->getDrawerPorts();
5177  foreach (VuoRendererPort *childPort, childPorts)
5178  {
5179  if (childPort->getBase()->getConnectedCables(true).size() > 0)
5180  hasDrawerWithNoIncomingCables = false;
5181  if (childPort->effectivelyHasConnectedDataCable(true))
5182  hasDrawerWithNoIncomingDataCables = false;
5183  }
5184  }
5185 
5186  string typeKey = inputPort->getDataType()->getModuleKey();
5187  if (!hasDrawerWithNoIncomingDataCables)
5188  {
5189  // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5190  // Accommodate generic types.
5191  if (VuoGenericType::isGenericTypeName(typeKey))
5192  return false;
5193 
5194  requiredInputs[typeKey] = ((requiredInputs.find(typeKey) == requiredInputs.end())? 1 : requiredInputs[typeKey]+1);
5195  }
5196  }
5197 
5198  bool hasIncomingCables = (inputPort->getBase()->getConnectedCables(true).size() > 0);
5199  if (hasIncomingCables && !hasDrawerWithNoIncomingCables)
5200  inputEventSourceRequired = true;
5201  }
5202 
5203  // Inventory required output port types (connected data outputs) in the node to be replaced.
5204  map<string, int> requiredOutputs;
5205  bool outputEventSourceRequired = false;
5206  foreach (VuoRendererPort *outputPort, origNode->getOutputPorts())
5207  {
5208  if (outputPort->getDataType() && outputPort->effectivelyHasConnectedDataCable(true))
5209  {
5210  string typeKey = outputPort->getDataType()->getModuleKey();
5211 
5212  // @todo https://b33p.net/kosada/node/16966, https://b33p.net/kosada/node/16967 :
5213  // Accommodate generic types.
5214  if (VuoGenericType::isGenericTypeName(typeKey))
5215  return false;
5216 
5217  requiredOutputs[typeKey] = ((requiredOutputs.find(typeKey) == requiredOutputs.end())? 1 : requiredOutputs[typeKey]+1);
5218  }
5219 
5220  if (outputPort->getBase()->getConnectedCables(true).size() > 0)
5221  outputEventSourceRequired = true;
5222  }
5223 
5224  // Inventory available input port types in the candidate replacement node.
5225  bool inputEventSourceAvailable = false;
5226  map<string, int> availableInputs;
5227  foreach (VuoPortClass *inputPortClass, newNodeClass->getInputPortClasses())
5228  {
5229  VuoType *dataType = (inputPortClass->hasCompiler()?
5230  static_cast<VuoCompilerPortClass *>(inputPortClass->getCompiler())->getDataVuoType() : NULL);
5231  if (dataType)
5232  {
5233  string typeKey = dataType->getModuleKey();
5234  availableInputs[typeKey] = ((availableInputs.find(typeKey) == availableInputs.end())? 1 : availableInputs[typeKey]+1);
5235  }
5236  }
5237 
5239  inputEventSourceAvailable = true;
5240 
5241  // Inventory available output port types in the candidate replacement node.
5242  bool outputEventSourceAvailable = false;
5243  map<string, int> availableOutputs;
5244  foreach (VuoPortClass *outputPortClass, newNodeClass->getOutputPortClasses())
5245  {
5246  VuoType *dataType = (outputPortClass->hasCompiler()?
5247  static_cast<VuoCompilerPortClass *>(outputPortClass->getCompiler())->getDataVuoType() : NULL);
5248  if (dataType)
5249  {
5250  string typeKey = dataType->getModuleKey();
5251  availableOutputs[typeKey] = ((availableOutputs.find(typeKey) == availableOutputs.end())? 1 : availableOutputs[typeKey]+1);
5252  }
5253  }
5254 
5256  outputEventSourceAvailable = true;
5257 
5258  // Check whether the candidate replacement node meets input data requirements.
5259  for (std::map<string,int>::iterator it=requiredInputs.begin(); it!=requiredInputs.end(); ++it)
5260  {
5261  string typeKey = it->first;
5262  int typeRequiredCount = it->second;
5263  if (availableInputs[typeKey] < typeRequiredCount)
5264  return false;
5265  }
5266 
5267  // Check whether the candidate replacement node meets output data requirements.
5268  for (std::map<string,int>::iterator it=requiredOutputs.begin(); it!=requiredOutputs.end(); ++it)
5269  {
5270  string typeKey = it->first;
5271  int typeRequiredCount = it->second;
5272  if (availableOutputs[typeKey] < typeRequiredCount)
5273  return false;
5274  }
5275 
5276  if (inputEventSourceRequired && !inputEventSourceAvailable)
5277  return false;
5278 
5279  if (outputEventSourceRequired && !outputEventSourceAvailable)
5280  return false;
5281 
5282  return true;
5283 }
5284 
5289 bool VuoEditorComposition::isPortCurrentlyRevertible(VuoRendererPort *port)
5290 {
5291  // If this port is not a specialization of a formerly generic port, then it cannot be reverted.
5293  VuoCompilerSpecializedNodeClass *specializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass);
5294 
5295  if (!specializedNodeClass)
5296  return false;
5297 
5298  VuoPortClass *portClass = port->getBase()->getClass();
5299  VuoGenericType *originalGenericType = dynamic_cast<VuoGenericType *>(specializedNodeClass->getOriginalPortType(portClass));
5300  if (!originalGenericType)
5301  return false;
5302 
5303  // If this port belongs to an attachment connected to a port that is not revertible, then
5304  // this port cannot be reverted, either.
5305  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(port->getUnderlyingParentNode());
5306  if (attachment)
5307  {
5308  VuoPort *hostPort = attachment->getUnderlyingHostPort();
5309  if (hostPort && (!isPortCurrentlyRevertible(hostPort->getRenderer())))
5310  return false;
5311  }
5312 
5313  return true;
5314 }
5315 
5334 VuoRendererPublishedPort * VuoEditorComposition::publishInternalPort(VuoPort *port, bool forceEventOnlyPublication, string name, VuoType *type, bool attemptMerge, bool *mergePerformed)
5335 {
5336  string publishedPortName = ((! name.empty())?
5337  name :
5339  bool isPublishedInput = port->getRenderer()->getInput();
5340  VuoType *portType = port->getRenderer()->getDataType();
5341  VuoPublishedPort *publishedPort = NULL;
5342 
5343  // If merging is enabled:
5344  // Check whether this composition has a pre-existing externally visible published port
5345  // that has the requested name and type and that can accommodate the newly published internal port.
5346  // If so, add this internal port as a connected port for the existing alias.
5347  bool performedMerge = false;
5348  if (attemptMerge)
5349  {
5350  publishedPort = (isPublishedInput ?
5351  getBase()->getPublishedInputPortWithName(publishedPortName) :
5352  getBase()->getPublishedOutputPortWithName(publishedPortName));
5353 
5354  if (publishedPort && dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer())->canAccommodateInternalPort(port->getRenderer(), forceEventOnlyPublication))
5355  {
5356  if (isPublishedInput && portType && type && !forceEventOnlyPublication)
5357  {
5358  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5360  publishedInputPort->getInitialValue(),
5361  false);
5362  }
5363 
5364  performedMerge = true;
5365  }
5366  }
5367 
5368 
5369  // Otherwise, create a new externally visible published port with a unique name derived from
5370  // the specified name, containing the current port as its lone connected internal port.
5371  if (! performedMerge)
5372  {
5373  publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(publishedPortName), type)->getBase());
5374  if (isPublishedInput && type)
5375  {
5376  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getCompiler() );
5377  publishedInputPort->setInitialValue(port->getRenderer()->getConstantAsString());
5378  }
5379  }
5380 
5381  addPublishedPort(publishedPort, isPublishedInput);
5382 
5383  VuoRendererPublishedPort *rendererPublishedPort = (publishedPort->hasRenderer()?
5384  dynamic_cast<VuoRendererPublishedPort *>(publishedPort->getRenderer()) :
5385  createRendererForPublishedPortInComposition(publishedPort, isPublishedInput));
5386 
5387  VuoCable *existingPublishedCable = port->getCableConnecting(publishedPort);
5388 
5389  if (! existingPublishedCable)
5390  {
5391  VuoCable *publishedCable = createPublishedCable(publishedPort, port, forceEventOnlyPublication);
5392  addCable(publishedCable);
5393  }
5394 
5395  if (mergePerformed != NULL)
5396  *mergePerformed = performedMerge;
5397 
5398  return rendererPublishedPort;
5399 }
5400 
5405 VuoCable * VuoEditorComposition::createPublishedCable(VuoPort *externalPort, VuoPort *internalPort, bool forceEventOnlyPublication)
5406 {
5407  VuoCable *publishedCable = NULL;
5408  bool creatingPublishedInputCable = internalPort->getRenderer()->getInput();
5409 
5410  if (creatingPublishedInputCable)
5411  {
5412  // If creating a published input cable, it will need to have an associated VuoCompilerCable.
5413  VuoPort *fromPort = externalPort;
5414  VuoNode *fromNode = this->publishedInputNode;
5415 
5416  VuoPort *toPort = internalPort;
5417  VuoNode *toNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5418 
5419  publishedCable = (new VuoCompilerCable(NULL,
5420  NULL,
5421  toNode->getCompiler(),
5422  (VuoCompilerPort *)(toPort->getCompiler())))->getBase();
5423  publishedCable->setFrom(fromNode, fromPort);
5424  }
5425 
5426  else
5427  {
5428  // If creating a published output cable, it will need to have an associated VuoCompilerCable
5429  // even though we don't currently construct a VuoCompilerNode for the published output node.
5430  VuoPort *fromPort = internalPort;
5431  VuoNode *fromNode = internalPort->getRenderer()->getUnderlyingParentNode()->getBase();
5432 
5433  VuoPort *toPort = externalPort;
5434  VuoNode *toNode = this->publishedOutputNode;
5435 
5436  publishedCable = (new VuoCompilerCable(fromNode->getCompiler(),
5437  (VuoCompilerPort *)(fromPort->getCompiler()),
5438  NULL,
5439  NULL))->getBase();
5440  publishedCable->setTo(toNode, toPort);
5441  }
5442 
5443  if (forceEventOnlyPublication)
5444  publishedCable->getCompiler()->setAlwaysEventOnly(true);
5445 
5446  return publishedCable;
5447 }
5448 
5460 void VuoEditorComposition::addActiveProtocol(VuoProtocol *protocol, bool useUndoStack)
5461 {
5462  vector<VuoPublishedPort *> publishedPortsToAdd;
5463  map<VuoPublishedPort *, string> publishedPortsToRename;
5464 
5465  // Remove the previously active protocol.
5466  VuoProtocol *previousActiveProtocol = this->activeProtocol;
5467  bool removingPreviousProtocol = previousActiveProtocol && (previousActiveProtocol != protocol);
5468 
5469  bool portChangesMadeDuringProtocolRemoval = false;
5470  if (removingPreviousProtocol)
5471  portChangesMadeDuringProtocolRemoval = removeActiveProtocol(previousActiveProtocol, protocol);
5472 
5473  if (portChangesMadeDuringProtocolRemoval && !useUndoStack)
5474  {
5475  VUserLog("Warning: Unexpected combination: Removing protocol ports, but useUndoStack=false");
5476  useUndoStack = true;
5477  }
5478 
5479  // Add the newly active protocol.
5480  this->activeProtocol = protocol;
5481  if (!protocol)
5482  return;
5483 
5484  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5485  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5486  {
5487  string portName = i->first;
5488  string portType = i->second;
5489 
5490  bool compositionHadCompatiblePort = false;
5491  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5492  if (preexistingPublishedPort)
5493  {
5494  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5495 
5496  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5497  (!preexistingType && (portType == "")));
5498  if (portTypesMatch)
5499  {
5500  compositionHadCompatiblePort = true;
5501  preexistingPublishedPort->setProtocolPort(true);
5502  }
5503  else
5504  {
5505  compositionHadCompatiblePort = false;
5506  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5507  }
5508  }
5509 
5510  if (!compositionHadCompatiblePort)
5511  {
5512  VuoType *type = compiler->getType(portType)->getBase();
5513  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5514  publishedPort->setProtocolPort(true);
5515 
5516  if (!useUndoStack)
5517  addPublishedPort(publishedPort, true);
5518  else
5519  publishedPortsToAdd.push_back(publishedPort);
5520 
5521  createRendererForPublishedPortInComposition(publishedPort, true);
5522  }
5523  }
5524 
5525  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5526  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5527  {
5528  string portName = i->first;
5529  string portType = i->second;
5530 
5531  bool compositionHadCompatiblePort = false;
5532  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5533  if (preexistingPublishedPort)
5534  {
5535  VuoType *preexistingType = static_cast<VuoCompilerPortClass *>(preexistingPublishedPort->getClass()->getCompiler())->getDataVuoType();
5536  bool portTypesMatch = ((preexistingType && (preexistingType->getModuleKey() == portType)) ||
5537  (!preexistingType && (portType == "")));
5538  if (portTypesMatch)
5539  {
5540  compositionHadCompatiblePort = true;
5541  preexistingPublishedPort->setProtocolPort(true);
5542  }
5543  else
5544  {
5545  compositionHadCompatiblePort = false;
5546  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5547  }
5548  }
5549 
5550  if (!compositionHadCompatiblePort)
5551  {
5552  VuoType *type = compiler->getType(portType)->getBase();
5553  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(getUniquePublishedPortName(portName), type)->getBase());
5554  publishedPort->setProtocolPort(true);
5555 
5556  if (!useUndoStack)
5557  addPublishedPort(publishedPort, false);
5558  else
5559  publishedPortsToAdd.push_back(publishedPort);
5560 
5561  createRendererForPublishedPortInComposition(publishedPort, false);
5562  }
5563  }
5564 
5565  if (useUndoStack)
5566  {
5567  bool undoStackMacroBegunAlready = (removingPreviousProtocol && portChangesMadeDuringProtocolRemoval);
5568  if (!publishedPortsToRename.empty() || !publishedPortsToAdd.empty() || undoStackMacroBegunAlready)
5569  {
5570  set<VuoPublishedPort *> publishedPortsToRemove;
5571  bool beginUndoStackMacro = !undoStackMacroBegunAlready;
5572  bool endUndoStackMacro = true;
5573  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5574  }
5575  }
5576 
5577  emit activeProtocolChanged();
5578 }
5579 
5587 string VuoEditorComposition::getNonProtocolVariantForPortName(string portName)
5588 {
5589  string modifiedPortName = portName;
5590  if (modifiedPortName.length() > 0)
5591  modifiedPortName[0] = toupper(modifiedPortName[0]);
5592  modifiedPortName = "some" + modifiedPortName;
5593 
5594  return modifiedPortName;
5595 }
5596 
5615 {
5617 
5618  set<VuoPublishedPort *> publishedPortsToRemove;
5619  map<VuoPublishedPort *, string> publishedPortsToRename;
5620 
5621  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5622  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5623  {
5624  string portName = i->first;
5625  string portType = i->second;
5626 
5627  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5628  if (preexistingPublishedPort)
5629  {
5630  bool portCompatibleAcrossProtocolTransition = false;
5631  if (replacementProtocol)
5632  {
5633  vector<pair<string, string> > protocolInputs = replacementProtocol->getInputPortNamesAndTypes();
5634  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5635  {
5636  string replacementPortName = i->first;
5637  string replacementPortType = i->second;
5638 
5639  if ((portName == replacementPortName) && (portType == replacementPortType))
5640  portCompatibleAcrossProtocolTransition = true;
5641  }
5642  }
5643 
5644  if (preexistingPublishedPort->getConnectedCables(true).empty())
5645  publishedPortsToRemove.insert(preexistingPublishedPort);
5646  else if (!portCompatibleAcrossProtocolTransition)
5647  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5648  }
5649  }
5650 
5651  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5652  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5653  {
5654  string portName = i->first;
5655  string portType = i->second;
5656 
5657  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5658  if (preexistingPublishedPort)
5659  {
5660  bool portCompatibleAcrossProtocolTransition = false;
5661  if (replacementProtocol)
5662  {
5663  vector<pair<string, string> > protocolOutputs = replacementProtocol->getOutputPortNamesAndTypes();
5664  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end() && !portCompatibleAcrossProtocolTransition; ++i)
5665  {
5666  string replacementPortName = i->first;
5667  string replacementPortType = i->second;
5668 
5669  if ((portName == replacementPortName) && (portType == replacementPortType))
5670  portCompatibleAcrossProtocolTransition = true;
5671  }
5672  }
5673 
5674  if (preexistingPublishedPort->getConnectedCables(true).empty())
5675  publishedPortsToRemove.insert(preexistingPublishedPort);
5676  else if (!portCompatibleAcrossProtocolTransition)
5677  publishedPortsToRename[preexistingPublishedPort] = getUniquePublishedPortName(getNonProtocolVariantForPortName(portName));
5678  }
5679  }
5680 
5681  // If we are removing any ports, the composition will no longer be deemed to adhere to the
5682  // removed protocol when it is re-opened, so there is no need to re-name any other ports.
5683  if (!publishedPortsToRemove.empty())
5684  publishedPortsToRename.clear();
5685 
5686  bool portChangesRequired = (!publishedPortsToRename.empty() || !publishedPortsToRemove.empty());
5687  if (portChangesRequired)
5688  {
5689  vector<VuoPublishedPort *> publishedPortsToAdd;
5690  bool beginUndoStackMacro = true;
5691  bool endUndoStackMacro = !replacementProtocol;
5692  emit protocolPortChangesRequested(publishedPortsToRename, publishedPortsToRemove, publishedPortsToAdd, beginUndoStackMacro, endUndoStackMacro);
5693  }
5694 
5695  emit activeProtocolChanged();
5696 
5697  return portChangesRequired;
5698 }
5699 
5707 {
5708  if ((activeProtocol != protocol) || !activeProtocol)
5709  return;
5710 
5711  activeProtocol = NULL;
5712 
5713  vector<pair<string, string> > protocolInputs = protocol->getInputPortNamesAndTypes();
5714  for (vector<pair<string, string> >::iterator i = protocolInputs.begin(); i != protocolInputs.end(); ++i)
5715  {
5716  string portName = i->first;
5717  string portType = i->second;
5718 
5719  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedInputPortWithName(portName);
5720  if (preexistingPublishedPort)
5721  preexistingPublishedPort->setProtocolPort(false);
5722  }
5723 
5724  vector<pair<string, string> > protocolOutputs = protocol->getOutputPortNamesAndTypes();
5725  for (vector<pair<string, string> >::iterator i = protocolOutputs.begin(); i != protocolOutputs.end(); ++i)
5726  {
5727  string portName = i->first;
5728  string portType = i->second;
5729 
5730  VuoPublishedPort *preexistingPublishedPort = getBase()->getPublishedOutputPortWithName(portName);
5731  if (preexistingPublishedPort)
5732  preexistingPublishedPort->setProtocolPort(false);
5733  }
5734 
5735  emit activeProtocolChanged();
5736 }
5737 
5743 {
5744  return activeProtocol;
5745 }
5746 
5752 {
5753  if (!activeProtocol)
5754  return NULL;
5755 
5756  return static_cast<VuoEditor *>(qApp)->getBuiltInDriverForProtocol(activeProtocol);
5757 }
5758 
5762 void VuoEditorComposition::addPublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5763 {
5764  VuoRendererComposition::addPublishedPort(publishedPort, isPublishedInput, compiler);
5765 
5766  identifierCache->addPublishedPortToCache(publishedPort);
5767 
5768  if (shouldUpdateUi)
5769  emit publishedPortModified();
5770 }
5771 
5778 int VuoEditorComposition::removePublishedPort(VuoPublishedPort *publishedPort, bool isPublishedInput, bool shouldUpdateUi)
5779 {
5780  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5781  if (shouldUpdateUi && publishedPort->isProtocolPort())
5783 
5784  int removalResult = VuoRendererComposition::removePublishedPort(publishedPort, isPublishedInput, compiler);
5785 
5786  if (shouldUpdateUi)
5787  emit publishedPortModified();
5788 
5789  return removalResult;
5790 }
5791 
5797 {
5798  // @todo: Allow multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
5799  if (dynamic_cast<VuoPublishedPort *>(publishedPort->getBase())->isProtocolPort())
5801 
5802  VuoRendererComposition::setPublishedPortName(publishedPort, name, compiler);
5803 
5804  identifierCache->addPublishedPortToCache( static_cast<VuoPublishedPort *>(publishedPort->getBase()) );
5805 
5806  emit publishedPortModified();
5807 }
5808 
5816 void VuoEditorComposition::highlightEligibleEndpointsForCable(VuoCable *cable)
5817 {
5818  bool eventOnlyConnection = cable->hasRenderer() && !cable->getRenderer()->effectivelyCarriesData();
5819  VuoRendererPort *fixedPort = NULL;
5820 
5821  if ((cable->getFromNode()) && (cable->getFromPort()) && (! (cable->getToNode())) & (! (cable->getToPort())))
5822  fixedPort = cable->getFromPort()->getRenderer();
5823 
5824  else if ((! (cable->getFromNode())) && (! (cable->getFromPort())) && (cable->getToNode()) && (cable->getToPort()))
5825  fixedPort = cable->getToPort()->getRenderer();
5826 
5827  if (fixedPort)
5828  {
5829  highlightInternalPortsConnectableToPort(fixedPort, cable->getRenderer());
5830  emit highlightPublishedSidebarDropLocationsRequested(fixedPort, eventOnlyConnection);
5831  }
5832 }
5833 
5839 void VuoEditorComposition::highlightInternalPortsConnectableToPort(VuoRendererPort *port, VuoRendererCable *cable)
5840 {
5841  auto types = compiler->getTypes();
5842 
5843  QList<QGraphicsItem *> compositionComponents = items();
5844  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5845  {
5846  QGraphicsItem *compositionComponent = *i;
5847  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5848  if (rn)
5849  {
5850  // Check for eligible internal input ports.
5851  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
5852  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
5853  updateEligibilityHighlightingForPort((*inputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
5854 
5855  // Check for eligible internal output ports.
5856  vector<VuoPort *> outputPorts = rn->getBase()->getOutputPorts();
5857  for(vector<VuoPort *>::iterator outputPort = outputPorts.begin(); outputPort != outputPorts.end(); ++outputPort)
5858  updateEligibilityHighlightingForPort((*outputPort)->getRenderer(), port, !cable->effectivelyCarriesData(), types);
5859  }
5860 
5861  // Fade out cables that aren't relevant to the current cable drag.
5862  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5863  if (rc && rc != cable)
5864  {
5865  QGraphicsItem::CacheMode normalCacheMode = rc->cacheMode();
5866  rc->setCacheMode(QGraphicsItem::NoCache);
5867  rc->updateGeometry();
5868 
5869  VuoPort *otherCablePort = port->getInput()
5870  ? rc->getBase()->getFromPort()
5871  : rc->getBase()->getToPort();
5872 
5873  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(otherCablePort? otherCablePort->getRenderer() : NULL,
5874  port,
5875  !cable->effectivelyCarriesData(),
5876  types);
5877 
5878  // Don't apply extra highlighting to compatible, already-connected cables.
5879  if (highlight == VuoRendererColors::standardHighlight)
5880  highlight = VuoRendererColors::noHighlight;
5881 
5882  rc->setEligibilityHighlight(highlight);
5883 
5884  rc->setCacheMode(normalCacheMode);
5885  }
5886  }
5887 
5888  // Now that the ports and cables have been highlighted, also highlight the nodes based on those results.
5889  for (QList<QGraphicsItem *>::iterator i = compositionComponents.begin(); i != compositionComponents.end(); ++i)
5890  updateEligibilityHighlightingForNode(dynamic_cast<VuoRendererNode *>(*i));
5891 }
5892 
5897 void VuoEditorComposition::updateEligibilityHighlightingForPort(VuoRendererPort *portToHighlight,
5898  VuoRendererPort *fixedPort,
5899  bool eventOnlyConnection,
5900  map<string, VuoCompilerType *> &types)
5901 {
5902  QGraphicsItem::CacheMode normalCacheMode = portToHighlight->cacheMode();
5903  portToHighlight->setCacheMode(QGraphicsItem::NoCache);
5904 
5905  portToHighlight->updateGeometry();
5906 
5907  VuoRendererColors::HighlightType highlight = getEligibilityHighlightingForPort(portToHighlight, fixedPort, eventOnlyConnection, types);
5908 
5909  portToHighlight->setEligibilityHighlight(highlight);
5910  VuoRendererTypecastPort *typecastPortToHighlight = dynamic_cast<VuoRendererTypecastPort *>(portToHighlight);
5911  if (typecastPortToHighlight)
5912  typecastPortToHighlight->getReplacedPort()->setEligibilityHighlight(highlight);
5913 
5914  portToHighlight->setCacheMode(normalCacheMode);
5915 
5916  if (typecastPortToHighlight)
5917  updateEligibilityHighlightingForPort(typecastPortToHighlight->getChildPort(), fixedPort, eventOnlyConnection, types);
5918 }
5919 
5930 VuoRendererColors::HighlightType VuoEditorComposition::getEligibilityHighlightingForPort(VuoRendererPort *portToHighlight, VuoRendererPort *fixedPort, bool eventOnlyConnection, map<string, VuoCompilerType *> &types)
5931 {
5932  // Determine whether the port endpoints are internal canvas ports or external published sidebar ports.
5933  VuoRendererPublishedPort *fixedExternalPublishedPort = dynamic_cast<VuoRendererPublishedPort *>(fixedPort);
5934  VuoRendererPublishedPort *externalPublishedPortToHighlight = dynamic_cast<VuoRendererPublishedPort *>(portToHighlight);
5935 
5936  VuoRendererPort *fromPort;
5937  VuoRendererPort *toPort;
5938  bool forwardConnection;
5939  if (fixedPort->getOutput())
5940  {
5941  fromPort = fixedPort;
5942  toPort = portToHighlight;
5943  forwardConnection = true;
5944  }
5945  else
5946  {
5947  fromPort = portToHighlight;
5948  toPort = fixedPort;
5949  forwardConnection = false;
5950  }
5951 
5952  bool directConnectionPossible;
5953 
5954  // Temporarily disallow direct cable connections between published inputs and published outputs.
5955  // @todo: Allow for https://b33p.net/kosada/node/7756 .
5956  if (fixedExternalPublishedPort && externalPublishedPortToHighlight) // both ports are external published sidebar ports
5957  directConnectionPossible = false;
5958  else if (fixedExternalPublishedPort && !externalPublishedPortToHighlight) // only the fixed port is an external published sidebar port
5959  directConnectionPossible = fixedExternalPublishedPort->isCompatibleAliasWithSpecializationForInternalPort(portToHighlight, eventOnlyConnection);
5960  else if (!fixedExternalPublishedPort && externalPublishedPortToHighlight) // only the port to highlight is an external published sidebar port
5961  directConnectionPossible = externalPublishedPortToHighlight->isCompatibleAliasWithSpecializationForInternalPort(fixedPort, eventOnlyConnection);
5962  else // both ports are internal canvas ports
5963  directConnectionPossible = fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection);
5964 
5966  if (directConnectionPossible)
5968  else if (!findBridgingSolutions(fromPort, toPort, forwardConnection, types).empty())
5970  else if (fixedPort == portToHighlight)
5971  highlight = VuoRendererColors::noHighlight;
5972  else
5974 
5975  return highlight;
5976 }
5977 
5993 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
5994  VuoRendererPort *toPort,
5995  bool eventOnlyConnection,
5996  bool forwardConnection)
5997 {
5998  VuoRendererPort *portToRespecialize = NULL;
5999  string respecializedTypeName = "";
6000 
6001  return canConnectDirectlyWithRespecializationNondestructively(fromPort,
6002  toPort,
6003  eventOnlyConnection,
6004  forwardConnection,
6005  &portToRespecialize,
6006  respecializedTypeName);
6007 }
6008 
6019 bool VuoEditorComposition::canConnectDirectlyWithRespecializationNondestructively(VuoRendererPort *fromPort,
6020  VuoRendererPort *toPort,
6021  bool eventOnlyConnection,
6022  bool forwardConnection,
6023  VuoRendererPort **portToRespecialize,
6024  string &respecializedTypeName)
6025 {
6026  *portToRespecialize = NULL;
6027  respecializedTypeName = "";
6028 
6029  bool canConnectWithRespecialization = canConnectDirectlyWithRespecialization(fromPort,
6030  toPort,
6031  eventOnlyConnection,
6032  forwardConnection,
6033  portToRespecialize,
6034  respecializedTypeName);
6035  if (!canConnectWithRespecialization)
6036  return false;
6037 
6038  if (canConnectWithRespecialization && !portToRespecialize)
6039  return true;
6040 
6041  bool nondestructive = portCanBeUnspecializedNondestructively((*portToRespecialize)->getBase());
6042  if (!nondestructive)
6043  {
6044  *portToRespecialize = NULL;
6045  respecializedTypeName = "";
6046  }
6047  return nondestructive;
6048 }
6049 
6055 bool VuoEditorComposition::portCanBeUnspecializedNondestructively(VuoPort *portToUnspecialize)
6056 {
6057  map<VuoNode *, string> nodesToReplace;
6058  set<VuoCable *> cablesToDelete;
6059  createReplacementsToUnspecializePort(portToUnspecialize, false, nodesToReplace, cablesToDelete);
6060 
6061  // Check whether unspecialization would disconnect any existing cables
6062  // (other than the cable that would normally be displaced by the new cable connection).
6063  if (cablesToDelete.empty())
6064  return true;
6065 
6066  else if ((cablesToDelete.size() == 1) && ((*(cablesToDelete.begin()))->getToPort() == portToUnspecialize))
6067  return true;
6068 
6069  return false;
6070 }
6071 
6091 bool VuoEditorComposition::canConnectDirectlyWithRespecialization(VuoRendererPort *fromPort,
6092  VuoRendererPort *toPort,
6093  bool eventOnlyConnection,
6094  bool forwardConnection,
6095  VuoRendererPort **portToRespecialize,
6096  string &respecializedTypeName)
6097 {
6098  // @todo https://b33p.net/kosada/node/10481 Still need eventOnlyConnection?
6099 
6100  *portToRespecialize = NULL;
6101  respecializedTypeName = "";
6102 
6103  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6104  if (fromPort->canConnectDirectlyWithoutSpecializationTo(toPort, eventOnlyConnection))
6105  return true;
6106 
6107  // // @todo https://b33p.net/kosada/node/10481 Necessary?
6108  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, eventOnlyConnection, portToRespecialize, respecializedTypeName))
6109  return true;
6110 
6111  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6112  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6113 
6114  if (!(fromPortIsEnabledOutput && toPortIsEnabledInput))
6115  return false;
6116 
6117  VuoType *currentFromDataType = fromPort->getDataType();
6118  VuoType *currentToDataType = toPort->getDataType();
6119 
6120  if (!(currentFromDataType && currentToDataType))
6121  return false;
6122 
6124  if (VuoType::isListTypeName(currentFromDataType->getModuleKey()) != VuoType::isListTypeName(currentToDataType->getModuleKey()))
6125  return false;
6126 
6127  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6128  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6129 
6130  VuoGenericType *originalFromGenericType = NULL;
6131  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6132  {
6134  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6135  if (fromSpecializedNodeClass)
6136  {
6137  VuoPortClass *portClass = fromPort->getBase()->getClass();
6138  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6139  }
6140  }
6141 
6142  VuoGenericType *originalToGenericType = NULL;
6143  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6144  {
6146  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6147  if (toSpecializedNodeClass)
6148  {
6149  VuoPortClass *portClass = toPort->getBase()->getClass();
6150  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6151  }
6152  }
6153 
6154  // Determine whether the port at each endpoint is 1) generic, or
6155  // 2) specialized and currently revertible, or 3) effectively static.
6156  bool fromPortIsGeneric = currentFromGenericType;
6157  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6158  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6159 
6160  bool toPortIsGeneric = currentToGenericType;
6161  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6162  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6163 
6164  // Figure out which port to try to respecialize, and to what type.
6165  set<string> compatibleTypes;
6166  string specializedType = "";
6167  VuoRendererPort *portToTryToRespecialize = NULL;
6168 
6169  // Case: One port static, one port specialized.
6170  if ((fromPortIsStatic && toPortIsSpecialized) || (fromPortIsSpecialized && toPortIsStatic))
6171  {
6172  VuoRendererPort *staticPort = (fromPortIsStatic? fromPort : toPort);
6173  specializedType = staticPort->getDataType()->getModuleKey();
6174  portToTryToRespecialize = (fromPortIsSpecialized? fromPort : toPort);
6175  }
6176 
6177  // Case: One port specialized, other port generic or specialized.
6178  else if ((fromPortIsSpecialized || toPortIsSpecialized) && !fromPortIsStatic && !toPortIsStatic)
6179  {
6180  VuoRendererPort *dragSource = (forwardConnection? fromPort : toPort);
6181  bool dragSourceIsGeneric = (forwardConnection? fromPortIsGeneric : toPortIsGeneric);
6182 
6183  VuoRendererPort *dragDestination = (forwardConnection? toPort : fromPort);
6184  bool dragDestinationIsGeneric = (forwardConnection? toPortIsGeneric : fromPortIsGeneric);
6185 
6186  // @todo https://b33p.net/kosada/node/10481 : Currently handled in VuoEditorComposition::canConnectDirectlyWithSpecialization(); merge?
6187  /*
6188  if (dragSourceIsGeneric && !dragDestinationIsGeneric)
6189  {
6190  specializedType = dragDestination->getDataType()->getModuleKey();
6191  portToTryToRespecialize = dragSource;
6192  }
6193  else if (dragDestinationIsGeneric && !dragSourceIsGeneric)
6194  {
6195  specializedType = dragSource->getDataType()->getModuleKey();
6196  portToTryToRespecialize = dragDestination;
6197  }
6198  else
6199  */
6200 
6201  if (!dragSourceIsGeneric && !dragDestinationIsGeneric)
6202  {
6203  specializedType = dragSource->getDataType()->getModuleKey();
6204  portToTryToRespecialize = dragDestination;
6205  }
6206  }
6207 
6208  // @todo https://b33p.net/kosada/node/10481 Other cases.
6209  else
6210  return false;
6211 
6212  if (portToTryToRespecialize)
6213  compatibleTypes = getRespecializationOptionsForPortInNetwork(portToTryToRespecialize);
6214 
6215  bool portsAreCompatible = (compatibleTypes.find(specializedType) != compatibleTypes.end());
6216 
6217  if (portsAreCompatible)
6218  {
6219  *portToRespecialize = portToTryToRespecialize;
6220  respecializedTypeName = specializedType;
6221  }
6222 
6223  return portsAreCompatible;
6224 }
6225 
6232 void VuoEditorComposition::updateEligibilityHighlightingForNode(VuoRendererNode *node)
6233 {
6234  VuoRendererInputDrawer *drawer = dynamic_cast<VuoRendererInputDrawer *>(node);
6235  if (drawer)
6236  {
6238  foreach (VuoRendererPort *drawerPort, drawer->getDrawerPorts())
6240  bestEligibility = VuoRendererColors::standardHighlight;
6242  && bestEligibility != VuoRendererColors::standardHighlight)
6243  bestEligibility = VuoRendererColors::subtleHighlight;
6244 
6245  // If this drawer has no eligible ports, fade it out.
6246  {
6247  QGraphicsItem::CacheMode normalCacheMode = drawer->cacheMode();
6248  drawer->setCacheMode(QGraphicsItem::NoCache);
6249  drawer->updateGeometry();
6250 
6251  drawer->setEligibilityHighlight(bestEligibility);
6252 
6253  drawer->setCacheMode(normalCacheMode);
6254  }
6255 
6256  // Make sure the host port is repainted to take into account the eligibility of its drawer ports.
6257  if (drawer->getRenderedHostPort()
6258  && drawer->getRenderedHostPort()->getRenderer())
6259  {
6260  VuoRendererPort *hostPort = drawer->getRenderedHostPort()->getRenderer();
6261 
6262  QGraphicsItem::CacheMode normalCacheMode = hostPort->cacheMode();
6263  hostPort->setCacheMode(QGraphicsItem::NoCache);
6264  hostPort->updateGeometry();
6265  hostPort->setCacheMode(normalCacheMode);
6266  }
6267  }
6268 }
6269 
6274 {
6277 }
6278 
6298  VuoRendererPort *toPort,
6299  bool toPortIsDragDestination,
6300  VuoRendererPort **portToSpecialize,
6301  string &specializedTypeName,
6302  string &typecastToInsert)
6303 {
6304  *portToSpecialize = NULL;
6305  specializedTypeName = "";
6306 
6307  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6308  map<string, string> specializedTypeNameForTypecast;
6309 
6310  auto types = compiler->getTypes();
6311  vector<string> candidateTypecasts = findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6312  bool solutionSelected = selectBridgingSolutionFromOptions(candidateTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, typecastToInsert);
6313 
6314  if (!solutionSelected)
6315  return false;
6316 
6317  if (portToSpecializeForTypecast.find(typecastToInsert) != portToSpecializeForTypecast.end())
6318  *portToSpecialize = portToSpecializeForTypecast[typecastToInsert];
6319  if (specializedTypeNameForTypecast.find(typecastToInsert) != specializedTypeNameForTypecast.end())
6320  specializedTypeName = specializedTypeNameForTypecast[typecastToInsert];
6321 
6322  return true;
6323 }
6324 
6343 bool VuoEditorComposition::selectBridgingSolutionFromOptions(vector<string> suitableTypecasts,
6344  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6345  map<string, string> specializedTypeNameForTypecast,
6346  string &selectedTypecast)
6347 {
6348  if (suitableTypecasts.empty())
6349  {
6350  selectedTypecast = "";
6351  return false;
6352  }
6353 
6354  else if (suitableTypecasts.size() == 1)
6355  {
6356  selectedTypecast = suitableTypecasts[0];
6357  return true;
6358  }
6359 
6360  else
6361  return promptForBridgingSelectionFromOptions(suitableTypecasts, portToSpecializeForTypecast, specializedTypeNameForTypecast, selectedTypecast);
6362 }
6363 
6369 bool VuoEditorComposition::portsPassSanityCheckToBridge(VuoRendererPort *fromPort, VuoRendererPort *toPort)
6370 {
6371  bool fromPortIsEnabledOutput = (fromPort && fromPort->getOutput() && fromPort->isEnabled());
6372  bool toPortIsEnabledInput = (toPort && toPort->getInput() && toPort->isEnabled());
6373 
6374  return (fromPortIsEnabledOutput && toPortIsEnabledInput &&
6375  fromPort->getBase()->getClass()->hasCompiler() &&
6376  toPort->getBase()->getClass()->hasCompiler());
6377 }
6378 
6384 bool VuoEditorComposition::portsPassSanityCheckToTypeconvert(VuoRendererPort *fromPort, VuoRendererPort *toPort, VuoType *candidateFromType, VuoType *candidateToType)
6385 {
6386  if (!portsPassSanityCheckToBridge(fromPort, toPort))
6387  return false;
6388 
6389  VuoType *inType = (candidateFromType? candidateFromType : static_cast<VuoCompilerPortClass *>(fromPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6390  VuoType *outType = (candidateToType? candidateToType : static_cast<VuoCompilerPortClass *>(toPort->getBase()->getClass()->getCompiler())->getDataVuoType());
6391 
6392  // To reduce confusion, don't offer Boolean -> Integer as a type conversion option for nodes that use 1-based indices.
6393  if (inType && (inType->getModuleKey() == "VuoBoolean") && outType && (outType->getModuleKey() == "VuoInteger"))
6394  {
6395  bool toNodeUsesIndex = toPort->getUnderlyingParentNode() &&
6400  );
6401 
6402  if (toNodeUsesIndex)
6403  return false;
6404  }
6405 
6406  return true;
6407 }
6408 
6428  VuoRendererPort *toPort,
6429  bool toPortIsDragDestination,
6430  map<string, VuoCompilerType *> &types)
6431 {
6432  map<string, VuoRendererPort *> portToSpecializeForTypecast;
6433  map<string, string> specializedTypeNameForTypecast;
6434  return findBridgingSolutions(fromPort, toPort, toPortIsDragDestination, portToSpecializeForTypecast, specializedTypeNameForTypecast, types);
6435 }
6436 
6447  VuoRendererPort *toPort,
6448  bool toPortIsDragDestination,
6449  map<string, VuoRendererPort *> &portToSpecializeForTypecast,
6450  map<string, string> &specializedTypeNameForTypecast,
6451  map<string, VuoCompilerType *> &types)
6452 {
6453  // If `limitCombinations` is `true`, first considers solutions that involve typeconversion
6454  // or specialization, but not both; if no such solution exists, returns solutions that involve
6455  // typeconversion+specialization combinations.
6456  // If `limitCombinations` is `false`, returns all solutions, whether they involve typeconversion,
6457  // specialization, or both.
6458  const bool limitCombinations = true;
6459 
6460  portToSpecializeForTypecast.clear();
6461  specializedTypeNameForTypecast.clear();
6462  vector<string> suitableTypecasts;
6463 
6464  if (!portsPassSanityCheckToBridge(fromPort, toPort))
6465  return suitableTypecasts;
6466 
6467  // Temporarily disallow direct cable connections between published inputs and published outputs.
6468  // @todo: Allow for https://b33p.net/kosada/node/7756 .
6469  if (dynamic_cast<VuoRendererPublishedPort *>(fromPort) && dynamic_cast<VuoRendererPublishedPort *>(toPort))
6470  return suitableTypecasts;
6471 
6472  // Case: We have an unspecialized (generic) port. See whether we can specialize it to complete the connection without typeconversion.
6473  {
6474  VuoRendererPort *portToSpecialize = NULL;
6475  string specializedTypeName = "";
6476  if (fromPort->canConnectDirectlyWithSpecializationTo(toPort, !cableInProgress->getRenderer()->effectivelyCarriesData(), &portToSpecialize, specializedTypeName))
6477  {
6478  suitableTypecasts.push_back("");
6479  portToSpecializeForTypecast[""] = portToSpecialize;
6480  specializedTypeNameForTypecast[""] = specializedTypeName;
6481 
6482  return suitableTypecasts;
6483  }
6484  }
6485 
6486  VuoType *currentFromDataType = fromPort->getDataType();
6487  VuoType *currentToDataType = toPort->getDataType();
6488 
6489  if (!(currentFromDataType && currentToDataType))
6490  return suitableTypecasts;
6491 
6492  VuoGenericType *currentFromGenericType = dynamic_cast<VuoGenericType *>(currentFromDataType);
6493  VuoGenericType *currentToGenericType = dynamic_cast<VuoGenericType *>(currentToDataType);
6494 
6495  VuoGenericType *originalFromGenericType = NULL;
6496  if (fromPort->getBase()->getRenderer()->getUnderlyingParentNode())
6497  {
6499  VuoCompilerSpecializedNodeClass *fromSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(fromNodeClass);
6500  if (fromSpecializedNodeClass)
6501  {
6502  VuoPortClass *portClass = fromPort->getBase()->getClass();
6503  originalFromGenericType = dynamic_cast<VuoGenericType *>( fromSpecializedNodeClass->getOriginalPortType(portClass) );
6504  }
6505  }
6506 
6507  VuoGenericType *originalToGenericType = NULL;
6508  if (toPort->getBase()->getRenderer()->getUnderlyingParentNode())
6509  {
6511  VuoCompilerSpecializedNodeClass *toSpecializedNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(toNodeClass);
6512  if (toSpecializedNodeClass)
6513  {
6514  VuoPortClass *portClass = toPort->getBase()->getClass();
6515  originalToGenericType = dynamic_cast<VuoGenericType *>( toSpecializedNodeClass->getOriginalPortType(portClass) );
6516  }
6517  }
6518 
6519  // Determine whether the port at each endpoint is:
6520  // 1) generic (unspecialized), or
6521  // 2) specialized and currently revertible, or
6522  // 3) effectively static.
6523  bool fromPortIsGeneric = currentFromGenericType;
6524  bool fromPortIsSpecialized = originalFromGenericType && !currentFromGenericType && isPortCurrentlyRevertible(fromPort);
6525  bool fromPortIsStatic = (!fromPortIsGeneric && !fromPortIsSpecialized);
6526 
6527  bool toPortIsGeneric = currentToGenericType;
6528  bool toPortIsSpecialized = originalToGenericType && !currentToGenericType && isPortCurrentlyRevertible(toPort);
6529  bool toPortIsStatic = (!toPortIsGeneric && !toPortIsSpecialized);
6530 
6531  // No typeconversion or specialization options between two unspecialized generic ports.
6532  if (fromPortIsGeneric && toPortIsGeneric)
6533  return suitableTypecasts;
6534 
6535  // Typeconversion options but no specialization options between two static ports.
6536  else if (fromPortIsStatic && toPortIsStatic)
6537  {
6538  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6539  suitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6540  return suitableTypecasts;
6541  }
6542 
6543  // Remaining combinations might require (re-)specializing one port or the other.
6544  // Figure out which port to consider (re-)specializing.
6545  bool specializeToPort = true;
6546  if (toPortIsGeneric)
6547  specializeToPort = true;
6548  else if (fromPortIsGeneric)
6549  specializeToPort = false;
6550  else if (fromPortIsSpecialized && toPortIsStatic)
6551  specializeToPort = false;
6552  else if (fromPortIsStatic && toPortIsSpecialized)
6553  specializeToPort = true;
6554  else if (fromPortIsSpecialized && toPortIsSpecialized)
6555  specializeToPort = toPortIsDragDestination;
6556 
6557  // Now that ports have been categorized, figure out what combinations of (re-)specialization
6558  // and/or typeconversion we can use to bridge them.
6559  set<string> compatibleTypes;
6560  if (specializeToPort && (toPortIsGeneric || (toPortIsSpecialized && portCanBeUnspecializedNondestructively(toPort->getBase()))))
6561  compatibleTypes = getRespecializationOptionsForPortInNetwork(toPort);
6562  else if (!specializeToPort && (fromPortIsGeneric || (fromPortIsSpecialized && portCanBeUnspecializedNondestructively(fromPort->getBase()))))
6563  compatibleTypes = getRespecializationOptionsForPortInNetwork(fromPort);
6564 
6565  // Typeconversion without re-specialization may be possible. In this case, don't require that the port be
6566  // non-destructively unspecializable, since it already has the appropriate specialization.
6567  compatibleTypes.insert(specializeToPort? currentToDataType->getModuleKey() : currentFromDataType->getModuleKey());
6568 
6569  if (limitCombinations)
6570  {
6571  vector<string> limitedSuitableTypecasts;
6572 
6573  // Check for bridging solutions that involve typeconversion without specialization.
6574  if (portsPassSanityCheckToTypeconvert(fromPort, toPort))
6575  {
6576  limitedSuitableTypecasts = moduleManager->getCompatibleTypecastClasses(currentFromDataType, currentToDataType);
6577  foreach (string typecastName, limitedSuitableTypecasts)
6578  {
6579  portToSpecializeForTypecast[typecastName] = specializeToPort? toPort : fromPort;
6580  specializedTypeNameForTypecast[typecastName] = specializeToPort? currentToDataType->getModuleKey() :
6581  currentFromDataType->getModuleKey();
6582  }
6583  }
6584 
6585  // Check for bridging solutions that involve specialization without typeconversion.
6586  string fixedDataType = specializeToPort? currentFromDataType->getModuleKey() : currentToDataType->getModuleKey();
6587  if (compatibleTypes.find(fixedDataType) != compatibleTypes.end())
6588  {
6589  limitedSuitableTypecasts.push_back("");
6590  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6591  specializedTypeNameForTypecast[""] = fixedDataType;
6592  }
6593 
6594  if (limitedSuitableTypecasts.size() >= 1)
6595  return limitedSuitableTypecasts;
6596  }
6597 
6598  foreach (string compatibleTypeName, compatibleTypes)
6599  {
6600  VuoCompilerType *compatibleSpecializedType = types[compatibleTypeName];
6601  if (!compatibleSpecializedType)
6602  compatibleSpecializedType = compiler->getType(compatibleTypeName);
6603  VuoType *candidateFromType = specializeToPort? currentFromDataType : compatibleSpecializedType->getBase();
6604  VuoType *candidateToType = specializeToPort? compatibleSpecializedType->getBase() : currentToDataType;
6605 
6606  if (compatibleSpecializedType)
6607  {
6608  // Re-specialization without typeconversion may be possible.
6609  if (candidateFromType == candidateToType)
6610  {
6611  suitableTypecasts.push_back("");
6612  portToSpecializeForTypecast[""] = specializeToPort? toPort : fromPort;
6613  specializedTypeNameForTypecast[""] = compatibleSpecializedType->getBase()->getModuleKey();
6614  }
6615 
6616  if (portsPassSanityCheckToTypeconvert(fromPort,
6617  toPort,
6618  candidateFromType,
6619  candidateToType))
6620  {
6621  vector<string> suitableTypecastsForCurrentTypes = moduleManager->getCompatibleTypecastClasses(candidateFromType, candidateToType);
6622  foreach (string typecast, suitableTypecastsForCurrentTypes)
6623  {
6624  suitableTypecasts.push_back(typecast);
6625  portToSpecializeForTypecast[typecast] = specializeToPort? toPort : fromPort;
6626  specializedTypeNameForTypecast[typecast] = compatibleSpecializedType->getBase()->getModuleKey();
6627  }
6628  }
6629  }
6630  }
6631 
6632  return suitableTypecasts;
6633 }
6634 
6647 bool VuoEditorComposition::promptForBridgingSelectionFromOptions(vector<string> suitableTypecasts,
6648  map<string, VuoRendererPort *> portToSpecializeForTypecast,
6649  map<string, string> specializedTypeNameForTypecast,
6650  string &selectedTypecast)
6651 {
6652  QMenu typecastMenu(views()[0]->viewport());
6653  typecastMenu.setSeparatorsCollapsible(false);
6654  QString spacer(" ");
6655 
6656  // Inventory specialization options
6657  set <pair<VuoRendererPort *, string> > specializationDetails;
6658  vector<string> typeconversionOptionsRequiringNoSpecialization;
6659  foreach (string typecastClassName, suitableTypecasts)
6660  {
6661  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typecastClassName];
6662  string specializedTypeName = specializedTypeNameForTypecast[typecastClassName];
6663  specializationDetails.insert(std::make_pair(portToSpecialize,
6664  specializedTypeName));
6665 
6666  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6667  if (portAlreadyHasTargetType)
6668  typeconversionOptionsRequiringNoSpecialization.push_back(typecastClassName);
6669  }
6670 
6671  // If there is a bridging option that requires no typeconversion, it doesn't need the usual
6672  // specialization heading under which multiple typeconversion options may be listed.
6673  // Selecting this item itself will invoke the specialization.
6674  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()))
6675  {
6676  QString menuText = getDisplayTextForSpecializationOption(portToSpecializeForTypecast[""], specializedTypeNameForTypecast[""]);
6677  QAction *typecastAction = typecastMenu.addAction(menuText);
6678  typecastAction->setData(QVariant(""));
6679  }
6680 
6681  bool foundSpecializationOptionsRequiringNoTypeconversion = typecastMenu.actions().size() >= 1;
6682  bool foundTypeconversionOptionsRequiringNoSpecialization = typeconversionOptionsRequiringNoSpecialization.size() >= 1;
6683 
6684  // If there are bridging options that require no specialization, list them next.
6685  bool includingTypeconvertWithNoSpecializationHeader = foundSpecializationOptionsRequiringNoTypeconversion;
6686  if (foundTypeconversionOptionsRequiringNoSpecialization)
6687  {
6688  if (foundSpecializationOptionsRequiringNoTypeconversion)
6689  typecastMenu.addSeparator();
6690 
6691  VuoRendererPort *portToSpecialize = portToSpecializeForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6692  string specializedTypeName = specializedTypeNameForTypecast[typeconversionOptionsRequiringNoSpecialization[0] ];
6693 
6694  if (portToSpecialize && !specializedTypeName.empty())
6695  {
6696  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6697  QAction *typecastAction = typecastMenu.addAction(menuText);
6698  typecastAction->setEnabled(false);
6699  includingTypeconvertWithNoSpecializationHeader = true;
6700  }
6701  }
6702 
6703  foreach (string typecastClassName, typeconversionOptionsRequiringNoSpecialization)
6704  {
6705  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6706  if (typecastClass)
6707  {
6708  QAction *typecastAction = typecastMenu.addAction((includingTypeconvertWithNoSpecializationHeader? spacer : "") + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6709  typecastAction->setData(QVariant(typecastClassName.c_str()));
6710  }
6711  }
6712 
6713  // Now list the remaining bridging options.
6714  for (set<pair<VuoRendererPort *, string> >::iterator i = specializationDetails.begin(); i != specializationDetails.end(); ++i)
6715  {
6716  VuoRendererPort *portToSpecialize = i->first;
6717  string specializedTypeName = i->second;
6718 
6719  // We've already listed the no-typeconversion bridging option, so skip it here.
6720  if ((std::find(suitableTypecasts.begin(), suitableTypecasts.end(), "") != suitableTypecasts.end()) &&
6721  (portToSpecializeForTypecast[""] == portToSpecialize) &&
6722  (specializedTypeNameForTypecast[""] == specializedTypeName))
6723  {
6724  continue;
6725  }
6726 
6727  // We've already listed the no-specialization bridging option, so skip it here.
6728  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6729  if (portAlreadyHasTargetType)
6730  {
6731  continue;
6732  }
6733 
6734  if (typecastMenu.actions().size() >= 1)
6735  typecastMenu.addSeparator();
6736 
6737  QString menuText = getDisplayTextForSpecializationOption(portToSpecialize, specializedTypeName);
6738  QAction *typecastAction = typecastMenu.addAction(menuText);
6739  typecastAction->setEnabled(false);
6740 
6741  // Inventory typeconversion options associated with this specialization option.
6742  foreach (string typecastClassName, suitableTypecasts)
6743  {
6744  if ((portToSpecializeForTypecast[typecastClassName] == portToSpecialize) &&
6745  (specializedTypeNameForTypecast[typecastClassName] == specializedTypeName))
6746  {
6747  VuoCompilerNodeClass *typecastClass = compiler->getNodeClass(typecastClassName);
6748  if (typecastClass)
6749  {
6750  QAction *typecastAction = typecastMenu.addAction(spacer + VuoRendererTypecastPort::getTypecastTitleForNodeClass(typecastClass->getBase(), true));
6751  typecastAction->setData(QVariant(typecastClassName.c_str()));
6752  }
6753  }
6754  }
6755  }
6756 
6757  menuSelectionInProgress = true;
6758  QAction *selectedTypecastAction = typecastMenu.exec(QCursor::pos());
6759  menuSelectionInProgress = false;
6760 
6761  selectedTypecast = (selectedTypecastAction? selectedTypecastAction->data().toString().toUtf8().constData() : "");
6762  return selectedTypecastAction;
6763 }
6764 
6768 QString VuoEditorComposition::getDisplayTextForSpecializationOption(VuoRendererPort *portToSpecialize, string specializedTypeName)
6769 {
6770  if (!portToSpecialize || specializedTypeName.empty())
6771  return "";
6772 
6773  bool isInput = portToSpecialize && portToSpecialize->getInput();
6774  QString typeDisplayName = compiler->getType(specializedTypeName)?
6775  formatTypeNameForDisplay(compiler->getType(specializedTypeName)->getBase()) :
6776  specializedTypeName.c_str();
6777 
6778  bool portAlreadyHasTargetType = (!portToSpecialize || (portToSpecialize->getDataType() && (portToSpecialize->getDataType()->getModuleKey() == specializedTypeName)));
6779 
6780  QString displayText;
6781  if (portAlreadyHasTargetType)
6782  {
6783  if (isInput)
6784  {
6785  //: Appears as a section label in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6786  displayText = tr("Keep Input Port as %1");
6787  }
6788  else
6789  {
6790  //: Appears as a section label in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6791  displayText = tr("Keep Output Port as %1");
6792  }
6793  }
6794  else
6795  {
6796  if (isInput)
6797  {
6798  //: Appears as an item in the popup menu when connecting a cable to an input port that has multiple specialization/type-conversion options.
6799  displayText = tr("Change Input Port to %1");
6800  }
6801  else
6802  {
6803  //: Appears as an item in the popup menu when connecting a cable to an output port that has multiple specialization/type-conversion options.
6804  displayText = tr("Change Output Port to %1");
6805  }
6806  }
6807 
6808  return displayText.arg(typeDisplayName);
6809 }
6810 
6816 {
6817  __block json_object *portValue = NULL;
6818 
6819  if (! port->getRenderer()->getDataType())
6820  return portValue;
6821 
6822  string runningPortIdentifier = identifierCache->getIdentifierForPort(port);
6823  bool isInput = port->getRenderer()->getInput();
6824 
6825  void (^getPortValue)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
6826  {
6827  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
6828  if (topLevelComposition->isRunningThreadUnsafe())
6829  {
6830  portValue = isInput ?
6831  topLevelComposition->runner->getInputPortValue(thisCompositionIdentifier, runningPortIdentifier) :
6832  topLevelComposition->runner->getOutputPortValue(thisCompositionIdentifier, runningPortIdentifier);
6833  }
6834  });
6835  };
6836  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, getPortValue);
6837 
6838  return portValue;
6839 }
6840 
6844 string VuoEditorComposition::getIdentifierForRunningPort(VuoPort *runningPort)
6845 {
6846  return static_cast<VuoCompilerPort *>(runningPort->getCompiler())->getIdentifier();
6847 }
6848 
6855 {
6856  if (!staticPort)
6857  return "";
6858 
6859  // Published ports
6860  if (dynamic_cast<VuoPublishedPort *>(staticPort))
6861  return dynamic_cast<VuoPublishedPort *>(staticPort)->getClass()->getName();
6862 
6863  // Internal ports
6864  // We might as well use the same naming scheme here as is used in the running composition,
6865  // but the VuoCompilerPort::getIdentifier() call will fail unless its parent
6866  // node identifier has been explicitly set.
6867  string nodeIdentifier = "";
6868  if (parentNode && parentNode->hasCompiler())
6869  nodeIdentifier = parentNode->getCompiler()->getIdentifier();
6870  else if (staticPort->hasRenderer() &&
6871  staticPort->getRenderer()->getUnderlyingParentNode() &&
6872  staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->hasCompiler())
6873  {
6874  nodeIdentifier = staticPort->getRenderer()->getUnderlyingParentNode()->getBase()->getCompiler()->getIdentifier();
6875  }
6876 
6877  if (staticPort->hasCompiler() && !nodeIdentifier.empty())
6878  {
6879  dynamic_cast<VuoCompilerPort *>(staticPort->getCompiler())->setNodeIdentifier(nodeIdentifier);
6880  return static_cast<VuoCompilerPort *>(staticPort->getCompiler())->getIdentifier();
6881  }
6882  else
6883  return "";
6884 }
6885 
6890 {
6891  VuoPort *port = nullptr;
6892  identifierCache->doForPortWithIdentifier(portID, [&port](VuoPort *p) {
6893  port = p;
6894  });
6895  return port;
6896 }
6897 
6904 {
6905  if (port->hasRenderer())
6906  {
6907  if (dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()))
6908  {
6909  bool isPublishedInput = !port->getRenderer()->getInput();
6910  return (isPublishedInput? composition->getPublishedInputNode() :
6911  composition->getPublishedOutputNode());
6912  }
6913 
6914  else
6915  return port->getRenderer()->getUnderlyingParentNode()->getBase();
6916  }
6917 
6918  foreach (VuoNode *n, composition->getBase()->getNodes())
6919  {
6920  VuoPort *candidateInputPort = n->getInputPortWithName(port->getClass()->getName());
6921  if (candidateInputPort == port)
6922  return n;
6923 
6924  VuoPort *candidateOutputPort = n->getOutputPortWithName(port->getClass()->getName());
6925  if (candidateOutputPort == port)
6926  return n;
6927  }
6928 
6929  return NULL;
6930 }
6931 
6940 {
6941  map<string, VuoPortPopover *>::iterator popover = activePortPopovers.find(portID);
6942  if (popover != activePortPopovers.end())
6943  return popover->second;
6944 
6945  else
6946  return NULL;
6947 }
6948 
6956 void VuoEditorComposition::enableInactivePopoverForPort(VuoRendererPort *rp)
6957 {
6958  string portID = identifierCache->getIdentifierForPort(rp->getBase());
6959  bool popoverJustClosedAtLastEvent = portsWithPopoversClosedAtLastEvent.find(portID) != portsWithPopoversClosedAtLastEvent.end();
6960  if (!popoverJustClosedAtLastEvent)
6962 }
6963 
6968 {
6969  if (!popoverEventsEnabled)
6970  return;
6971 
6972  VuoPort *port = rp->getBase();
6973  string portID = identifierCache->getIdentifierForPort(port);
6974 
6975  VUserLog("%s: Open popover for %s",
6976  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6977  portID.c_str());
6978 
6979  dispatch_sync(runCompositionQueue, ^{ // Don't add any new popovers while the composition is starting. https://b33p.net/kosada/node/15572
6980 
6981  dispatch_sync(activePortPopoversQueue, ^{
6982 
6983  if (activePortPopovers.find(portID) == activePortPopovers.end())
6984  {
6985  // Assigning the popover a parent widget allows us to give it rounded corners
6986  // and a background fill that respects its rounded boundaries.
6987  VuoPortPopover *popover = new VuoPortPopover(port, this, views()[0]->viewport());
6988 
6989  connect(popover, &VuoPortPopover::popoverClosedForPort, this, &VuoEditorComposition::disablePopoverForPortThreadSafe);
6990  connect(popover, &VuoPortPopover::popoverDetachedFromPort, [=]{
6991  VUserLog("%s: Detach popover for %s",
6992  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
6993  portID.c_str());
6994  popoverDetached();
6995  });
6996  connect(popover, &VuoPortPopover::popoverResized, this, &VuoEditorComposition::repositionPopover);
6999 
7000  // Line up the top left of the dialog with the port.
7001  QPoint portLeftInScene = port->getRenderer()->scenePos().toPoint() - QPoint(port->getRenderer()->getPortRect().width()/2., 0);
7002 
7003  // Don't let popovers get cut off at the right or bottom edges of the canvas.
7004  const int cutoffMargin = 16;
7005  QRectF viewportRect = views()[0]->mapToScene(views()[0]->viewport()->rect()).boundingRect();
7006  if (portLeftInScene.x() + popover->size().width() + cutoffMargin > viewportRect.right())
7007  portLeftInScene = QPoint(viewportRect.right() - popover->size().width() - cutoffMargin, portLeftInScene.y());
7008  if (portLeftInScene.y() + popover->size().height() + cutoffMargin > viewportRect.bottom())
7009  portLeftInScene = QPoint(portLeftInScene.x(), viewportRect.bottom() - popover->size().height() - cutoffMargin);
7010 
7011  QPoint popoverLeftInView = views()[0]->mapFromScene(portLeftInScene);
7012 
7013  const QPoint offset = QPoint(12, 6);
7014 
7015  QPoint popoverTopLeft = popoverLeftInView + offset;
7016  popover->move(popoverTopLeft);
7017  popover->show();
7018 
7019  activePortPopovers[portID] = popover;
7020 
7021  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of runCompositionQueue. https://b33p.net/kosada/node/14612
7022  updateDataInPortPopover(portID);
7023  });
7024  }
7025  });
7026  });
7027 }
7028 
7033 void VuoEditorComposition::enablePopoverForNode(VuoRendererNode *rn)
7034 {
7035  if (popoverEventsEnabled && !dynamic_cast<VuoRendererInputDrawer *>(rn))
7037 }
7038 
7047 {
7048  VUserLog("%s: Close popover for %s",
7049  window->getWindowTitleWithoutPlaceholder().toUtf8().data(),
7050  portID.c_str());
7051 
7052  VuoPortPopover *popover = NULL;
7053  map<string, VuoPortPopover *>::iterator i = activePortPopovers.find(portID);
7054  if (i != activePortPopovers.end())
7055  {
7056  popover = i->second;
7057  activePortPopovers.erase(i);
7058  }
7059 
7060  if (popover)
7061  {
7062  popover->hide();
7063  popover->deleteLater();
7064  }
7065 
7066  bool isInput = false;
7067  bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7068  isInput = port->getRenderer()->getInput();
7069  });
7070 
7071  if (! foundPort)
7072  return;
7073 
7074  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Get off of activePortPopoversQueue.
7075  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7076  {
7077  dispatch_async(topLevelComposition->runCompositionQueue, ^{
7078  if (topLevelComposition->isRunningThreadUnsafe())
7079  {
7080  (isInput ?
7081  topLevelComposition->runner->unsubscribeFromInputPortTelemetry(thisCompositionIdentifier, portID) :
7082  topLevelComposition->runner->unsubscribeFromOutputPortTelemetry(thisCompositionIdentifier, portID));
7083  }
7084  });
7085  });
7086  });
7087 }
7088 
7092 void VuoEditorComposition::disablePopoverForPortThreadSafe(string portID)
7093 {
7094  dispatch_sync(activePortPopoversQueue, ^{
7095  disablePopoverForPort(portID);
7096  });
7097 }
7098 
7103 {
7104  disablePortPopovers();
7106 }
7107 
7112 {
7113  foreach (VuoErrorPopover *errorPopover, errorPopovers)
7114  {
7115  errorPopover->hide();
7116  errorPopover->deleteLater();
7117  }
7118 
7119  errorPopovers.clear();
7120 }
7121 
7126 void VuoEditorComposition::disablePortPopovers(VuoRendererNode *node)
7127 {
7128  dispatch_sync(activePortPopoversQueue, ^{
7129  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7130  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7131  {
7132  string portID = i->first;
7133  bool shouldDisable = false;
7134 
7135  if (! node)
7136  shouldDisable = true;
7137  else
7138  {
7139  identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7140  shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7141  });
7142  }
7143 
7144  if (shouldDisable)
7145  disablePopoverForPort(portID);
7146  }
7147  });
7148 }
7149 
7154 {
7155  dispatch_sync(activePortPopoversQueue, ^{
7156  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7157  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7158  {
7159  string portID = i->first;
7160 
7161  bool foundPort = identifierCache->doForPortWithIdentifier(portID, [&foundPort](VuoPort *port) {});
7162  if (! foundPort)
7163  disablePopoverForPort(portID);
7164  }
7165  });
7166 }
7167 
7173 {
7174  if (recordWhichPopoversClosed)
7175  portsWithPopoversClosedAtLastEvent.clear();
7176 
7177  dispatch_sync(activePortPopoversQueue, ^{
7178  map<string, VuoPortPopover *> popoversToDisable = activePortPopovers;
7179  for (map<string, VuoPortPopover *>::iterator i = popoversToDisable.begin(); i != popoversToDisable.end(); ++i)
7180  {
7181  string portID = i->first;
7182  bool shouldDisable = false;
7183 
7184  if (! node)
7185  shouldDisable = true;
7186  else
7187  {
7188  identifierCache->doForPortWithIdentifier(portID, [&shouldDisable, node](VuoPort *port) {
7189  shouldDisable = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7190  });
7191  }
7192 
7193  if (shouldDisable)
7194  {
7195  VuoPortPopover *popover = getActivePopoverForPort(portID);
7196  if (! (popover && popover->getDetached()))
7197  {
7198  disablePopoverForPort(portID);
7199  portsWithPopoversClosedAtLastEvent.insert(portID);
7200  }
7201  }
7202  }
7203  });
7204 }
7205 
7210 {
7211  moveDetachedPortPopoversBy(dx, dy);
7212  moveErrorPopoversBy(dx, dy);
7213 }
7214 
7218 void VuoEditorComposition::moveErrorPopoversBy(int dx, int dy)
7219 {
7220  foreach(VuoErrorPopover *errorPopover, errorPopovers)
7221  errorPopover->move(errorPopover->pos().x()+dx, errorPopover->pos().y()+dy);
7222 }
7223 
7227 void VuoEditorComposition::moveDetachedPortPopoversBy(int dx, int dy)
7228 {
7229  dispatch_sync(activePortPopoversQueue, ^{
7230  map<string, VuoPortPopover *> portPopovers = activePortPopovers;
7231  for (map<string, VuoPortPopover *>::iterator i = portPopovers.begin(); i != portPopovers.end(); ++i)
7232  {
7233  VuoPortPopover *popover = i->second;
7234  if (popover && popover->getDetached())
7235  popover->move(popover->pos().x()+dx, popover->pos().y()+dy);
7236  }
7237  });
7238 }
7239 
7243 void VuoEditorComposition::setPopoversHideOnDeactivate(bool shouldHide)
7244 {
7245  dispatch_sync(activePortPopoversQueue, ^{
7246  auto portPopovers = activePortPopovers;
7247  for (auto i : portPopovers)
7248  {
7249  VuoPortPopover *popover = i.second;
7250  if (popover && popover->getDetached())
7251  {
7252  id nsWindow = (id)VuoPopover::getWindowForPopover(popover);
7253  objc_msgSend(nsWindow, sel_getUid("setHidesOnDeactivate:"), shouldHide);
7254  }
7255  }
7256  });
7257 }
7258 
7264 {
7265  dispatch_sync(activePortPopoversQueue, ^{
7266  for (map<string, VuoPortPopover *>::iterator i = activePortPopovers.begin(); i != activePortPopovers.end(); ++i)
7267  {
7268  string portID = i->first;
7269  VuoPortPopover *popover = i->second;
7270  bool shouldUpdate = false;
7271 
7272  if (! node)
7273  shouldUpdate = true;
7274  else
7275  {
7276  identifierCache->doForPortWithIdentifier(portID, [&shouldUpdate, node](VuoPort *port) {
7277  shouldUpdate = port->hasRenderer() && (port->getRenderer()->getUnderlyingParentNode() == node);
7278  });
7279  }
7280 
7281  if (shouldUpdate)
7282  QMetaObject::invokeMethod(popover, "updateTextAndResize", Qt::QueuedConnection);
7283  }
7284  });
7285 }
7286 
7299  string popoverCompositionIdentifier,
7300  string portID)
7301 {
7302  bool isInput;
7303  bool foundPort = popoverComposition->identifierCache->doForPortWithIdentifier(portID, [&isInput](VuoPort *port) {
7304  isInput = port->getRenderer()->getInput();
7305  });
7306 
7307  if (! foundPort)
7308  return;
7309 
7310  string portSummary = (isInput ?
7311  runner->subscribeToInputPortTelemetry(popoverCompositionIdentifier, portID) :
7312  runner->subscribeToOutputPortTelemetry(popoverCompositionIdentifier, portID));
7313 
7314  dispatch_async(popoverComposition->activePortPopoversQueue, ^{
7315  VuoPortPopover *popover = popoverComposition->getActivePopoverForPort(portID);
7316  if (popover)
7317  {
7318  QMetaObject::invokeMethod(popover, "updateDataValueImmediately", Qt::QueuedConnection, Q_ARG(QString, portSummary.c_str()));
7319  QMetaObject::invokeMethod(popover, "setCompositionRunning", Qt::QueuedConnection, Q_ARG(bool, true), Q_ARG(bool, false));
7320  }
7321  });
7322 }
7323 
7332 {
7333  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7334  {
7335  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7336  if (topLevelComposition->isRunningThreadUnsafe())
7337  topLevelComposition->updateDataInPortPopoverFromRunningTopLevelComposition(this, thisCompositionIdentifier, portID);
7338  });
7339  });
7340 }
7341 
7346 void VuoEditorComposition::receivedTelemetryInputPortUpdated(string compositionIdentifier, string portIdentifier,
7347  bool receivedEvent, bool receivedData, string dataSummary)
7348 {
7349  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7350  {
7351  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7352  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7353  if (popover)
7354  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7355  Q_ARG(bool, receivedEvent),
7356  Q_ARG(bool, receivedData),
7357  Q_ARG(QString, dataSummary.c_str()));
7358  });
7359  };
7360  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7361 }
7362 
7367 void VuoEditorComposition::receivedTelemetryOutputPortUpdated(string compositionIdentifier, string portIdentifier,
7368  bool sentEvent, bool sentData, string dataSummary)
7369 {
7370  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7371  {
7372  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7373  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7374  if (popover)
7375  QMetaObject::invokeMethod(popover, "updateLastEventTimeAndDataValue", Qt::QueuedConnection,
7376  Q_ARG(bool, sentEvent),
7377  Q_ARG(bool, sentData),
7378  Q_ARG(QString, dataSummary.c_str()));
7379  });
7380 
7381  if (matchingComposition->showEventsMode && sentEvent)
7382  {
7383  matchingComposition->identifierCache->doForPortWithIdentifier(portIdentifier, [matchingComposition](VuoPort *port) {
7384  if (dynamic_cast<VuoCompilerTriggerPort *>(port->getCompiler()) && port->hasRenderer())
7385  {
7386  port->getRenderer()->setFiredEvent();
7387  matchingComposition->animatePort(port->getRenderer());
7388  }
7389  });
7390  }
7391  };
7392  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7393 }
7394 
7399 void VuoEditorComposition::receivedTelemetryEventDropped(string compositionIdentifier, string portIdentifier)
7400 {
7401  void (^updatePortDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7402  {
7403  dispatch_async(matchingComposition->runCompositionQueue, ^{
7404  if (matchingComposition->isRunningThreadUnsafe())
7405  {
7406  dispatch_sync(matchingComposition->activePortPopoversQueue, ^{
7407  VuoPortPopover *popover = matchingComposition->getActivePopoverForPort(portIdentifier);
7408  if (popover)
7409  QMetaObject::invokeMethod(popover, "incrementDroppedEventCount", Qt::QueuedConnection);
7410  });
7411  }
7412  });
7413  };
7414  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updatePortDisplay);
7415 }
7416 
7421 void VuoEditorComposition::receivedTelemetryNodeExecutionStarted(string compositionIdentifier, string nodeIdentifier)
7422 {
7423  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7424  {
7425  if (matchingComposition->showEventsMode)
7426  {
7427  dispatch_async(this->runCompositionQueue, ^{
7428  if (this->isRunningThreadUnsafe())
7429  {
7430  matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7431  node->getRenderer()->setExecutionBegun();
7432  });
7433  }
7434  });
7435  }
7436  };
7437  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7438 }
7439 
7444 void VuoEditorComposition::receivedTelemetryNodeExecutionFinished(string compositionIdentifier, string nodeIdentifier)
7445 {
7446  void (^updateNodeDisplay)(VuoEditorComposition *) = ^void (VuoEditorComposition *matchingComposition)
7447  {
7448  if (matchingComposition->showEventsMode)
7449  {
7450  dispatch_async(this->runCompositionQueue, ^{
7451  if (this->isRunningThreadUnsafe())
7452  {
7453  matchingComposition->identifierCache->doForNodeWithIdentifier(nodeIdentifier, [](VuoNode *node) {
7454  node->getRenderer()->setExecutionEnded();
7455  });
7456  }
7457  });
7458  }
7459  };
7460  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedCompositionWithIdentifier(this, compositionIdentifier, updateNodeDisplay);
7461 }
7462 
7470 {
7471  emit compositionStoppedItself();
7472 }
7473 
7480 {
7482 }
7483 
7488 {
7489  return showEventsMode;
7490 }
7491 
7496 {
7497  this->showEventsMode = showEventsMode;
7498 
7499  if (showEventsMode)
7500  {
7502 
7503  void (^subscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7504  {
7505  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7506  if (topLevelComposition->isRunningThreadUnsafe())
7507  topLevelComposition->runner->subscribeToEventTelemetry(thisCompositionIdentifier);
7508  });
7509  };
7510  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, subscribe);
7511  }
7512  else
7513  {
7515 
7516  void (^unsubscribe)(VuoEditorComposition *, string) = ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
7517  {
7518  dispatch_sync(topLevelComposition->runCompositionQueue, ^{
7519  if (topLevelComposition->isRunningThreadUnsafe())
7520  topLevelComposition->runner->unsubscribeFromEventTelemetry(thisCompositionIdentifier);
7521  });
7522  };
7523  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(this, unsubscribe);
7524  }
7525 }
7526 
7531 {
7532  foreach (VuoCable *cable, getBase()->getCables())
7533  {
7534  if (cable->getCompiler()->getHidden() && !cable->isPublished())
7535  return true;
7536  }
7537 
7538  return false;
7539 }
7540 
7545 {
7546  foreach (VuoCable *cable, getBase()->getCables())
7547  {
7548  if (cable->hasRenderer() && cable->getRenderer()->getEffectivelyWireless() && cable->isPublished())
7549  return true;
7550  }
7551 
7552  return false;
7553 }
7554 
7559 QGraphicsItemAnimation * VuoEditorComposition::setUpAnimationForPort(QGraphicsItemAnimation *animation, VuoRendererPort *port)
7560 {
7561  VuoRendererPort *animatedPort = new VuoRendererPort(new VuoPort(port->getBase()->getClass()),
7562  NULL,
7563  port->getOutput(),
7564  port->getRefreshPort(),
7565  port->getFunctionPort());
7566  animatedPort->setAnimated(true);
7567  animatedPort->setZValue(VuoRendererItem::triggerAnimationZValue);
7568  animatedPort->setParentItem(port->getRenderedParentNode());
7569 
7570  animation->setItem(animatedPort);
7571  animation->setScaleAt(0.0, 1, 1);
7572  animation->setScaleAt(0.999, 3, 3);
7573  animation->setScaleAt(1.0, 1, 1);
7574 
7575  QTimeLine *animationTimeline = animation->timeLine();
7576  animationTimeline->setFrameRange(0, 100);
7577  animationTimeline->setUpdateInterval(showEventsModeUpdateInterval);
7578  animationTimeline->setCurveShape(QTimeLine::LinearCurve);
7579 
7580  preparedAnimations.insert(animation);
7581  animationForTimeline[animation->timeLine()] = animation;
7582 
7583  connect(animationTimeline, &QTimeLine::valueChanged, this, &VuoEditorComposition::updatePortAnimation);
7584  connect(animationTimeline, &QTimeLine::finished, this, &VuoEditorComposition::endPortAnimation);
7585 
7586  return animation;
7587 }
7588 
7592 void VuoEditorComposition::animatePort(VuoRendererPort *port)
7593 {
7594  dispatch_async(dispatch_get_main_queue(), ^{
7595  QGraphicsItemAnimation *animation = getAvailableAnimationForPort(port);
7596  if (! animation)
7597  return;
7598 
7599  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7600 
7601  if (animation->timeLine()->state() == QTimeLine::Running)
7602  animation->timeLine()->setCurrentTime(0);
7603 
7604  else
7605  {
7606  animatedPort->setPos(port->pos());
7607  animatedPort->setVisible(true);
7608  animation->timeLine()->start();
7609  }
7610  });
7611 }
7612 
7617 QGraphicsItemAnimation * VuoEditorComposition::getAvailableAnimationForPort(VuoRendererPort *port)
7618 {
7619  vector<QGraphicsItemAnimation *> animations = port->getAnimations();
7620 
7621  QGraphicsItemAnimation *mostAdvancedAnimation = NULL;
7622  qreal maxPercentAdvanced = -1;
7623 
7624  for (int i = 0; i < animations.size(); ++i)
7625  {
7626  QGraphicsItemAnimation *animation = animations[i];
7627  bool animationPrepared = (preparedAnimations.find(animation) != preparedAnimations.end());
7628  bool animationRunning = (animation->timeLine()->state() == QTimeLine::Running);
7629 
7630  if (! animationPrepared)
7631  return setUpAnimationForPort(animation, port);
7632 
7633  else if (! animationRunning)
7634  return animation;
7635 
7636  // If all of the port's animations are already running, return the
7637  // one that has been running the longest.
7638  qreal percentAdvanced = animation->timeLine()->currentValue();
7639  if (percentAdvanced > maxPercentAdvanced)
7640  {
7641  mostAdvancedAnimation = animation;
7642  maxPercentAdvanced = percentAdvanced;
7643  }
7644  }
7645 
7646  // If no animation is even halfway complete, return NULL to indicate
7647  // that no animation is currently available.
7648  return (maxPercentAdvanced >= 0.5? mostAdvancedAnimation : NULL);
7649 }
7650 
7656 void VuoEditorComposition::updatePortAnimation(qreal value)
7657 {
7658  QTimeLine *animationTimeline = (QTimeLine *)sender();
7659  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7660  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7661  const qreal multiplier = 1000.;
7662  animatedPort->setFadePercentageSinceEventFired(pow((multiplier*value),2)/pow(multiplier,2));
7663 }
7664 
7669 void VuoEditorComposition::endPortAnimation(void)
7670 {
7671  QTimeLine *animationTimeline = (QTimeLine *)sender();
7672  QGraphicsItemAnimation *animation = animationForTimeline[animationTimeline];
7673  VuoRendererPort *animatedPort = (VuoRendererPort *)(animation->item());
7674  animatedPort->setVisible(false);
7675 }
7676 
7680 void VuoEditorComposition::setDisableDragStickiness(bool disable)
7681 {
7682  this->dragStickinessDisabled = disable;
7683 }
7684 
7689 {
7690  this->ignoreApplicationStateChangeEvents = ignore;
7691 }
7692 
7699 {
7700  this->popoverEventsEnabled = enable;
7701 }
7702 
7707 {
7708  setRenderActivity(true, includePorts);
7709  refreshComponentAlphaLevelTimer->start();
7710 }
7711 
7716 {
7717  refreshComponentAlphaLevelTimer->stop();
7718  setRenderActivity(false);
7719 }
7720 
7737 bool VuoEditorComposition::validateProtocol(VuoEditorWindow *window, bool isExportingMovie)
7738 {
7739  // This should never happen if we've enabled the "Export" menu options in the appropriate contexts.
7740  if (!activeProtocol)
7741  {
7742  VuoErrorDialog::show(window, "To export, activate a protocol.", "");
7743  return false;
7744  }
7745 
7746  // Can events from at least one trigger reach at least one published output port? If not, report an error.
7747  if (! getBase()->getCompiler()->getCachedGraph()->mayEventsReachPublishedOutputPorts())
7748  {
7749  QString errorHeadline = tr("<b>This composition doesn't send any images to <code>outputImage</code>.</b>");
7750  QString errorDetails = tr("<p>To export, your composition should use the data and events from the published input ports "
7751  "to output a stream of images through the <code>outputImage</code> published output port.</p>");
7752 
7753  if (isExportingMovie)
7754  errorDetails.append("<p>Alternatively, you can record a realtime movie by running the composition and selecting File > Start Recording.</p>");
7755 
7757  QMessageBox messageBox(window);
7758  messageBox.setWindowFlags(Qt::Sheet);
7759  messageBox.setWindowModality(Qt::WindowModal);
7760  messageBox.setFont(fonts->dialogHeadingFont());
7761  messageBox.setTextFormat(Qt::RichText);
7762 
7763  messageBox.setStandardButtons(QMessageBox::Help | QMessageBox::Ok);
7764  messageBox.setButtonText(QMessageBox::Help, tr("Open an Example"));
7765  messageBox.setButtonText(QMessageBox::Ok, tr("OK"));
7766  messageBox.setDefaultButton(QMessageBox::Ok);
7767 
7768  messageBox.setText(errorHeadline);
7769  messageBox.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>" + errorDetails);
7770 
7771  if (messageBox.exec() == QMessageBox::Help)
7772  {
7773  map<QString, QString> examples = static_cast<VuoEditor *>(qApp)->getExampleCompositionsForProtocol(activeProtocol);
7774  map<QString, QString>::iterator i = examples.begin();
7775  if (i != examples.end())
7776  QDesktopServices::openUrl(QUrl(VuoEditor::getURLForExampleComposition(i->first, i->second)));
7777  }
7778  return false;
7779  }
7780 
7781  return true;
7782 }
7783 
7788 {
7789  return (getBase()->hasCompiler()? getBase()->getCompiler()->getGraphvizDeclaration(getActiveProtocol(), generateCompositionHeader()) : "");
7790 }
7791 
7796 {
7798 }
7799 
7803 string VuoEditorComposition::getDefaultNameForPath(const string &compositionPath)
7804 {
7805  string dir, file, ext;
7806  VuoFileUtilities::splitPath(compositionPath, dir, file, ext);
7807  return file;
7808 }
7809 
7817 {
7818  string customizedName = getBase()->getMetadata()->getCustomizedName();
7819  if (! customizedName.empty())
7820  return QString::fromStdString(customizedName);
7821 
7822  string name = getBase()->getMetadata()->getName();
7823  return formatCompositionFileNameForDisplay(QString::fromStdString(name));
7824 }
7825 
7833 QString VuoEditorComposition::formatCompositionFileNameForDisplay(QString unformattedCompositionFileName)
7834 {
7835  // Remove the file extension. Do this correctly even for subcompositions whose filenames contain dot-delimited segments.
7836  // If the extensionless filename contains dot-delimited segments, use only the final segment.
7837  vector<string> fileNameParts = VuoStringUtilities::split(unformattedCompositionFileName.toUtf8().constData(), '.');
7838  string fileNameContentPart = (fileNameParts.size() >= 2 && fileNameParts[fileNameParts.size()-1] == "vuo"?
7839  fileNameParts[fileNameParts.size()-2] :
7840  (fileNameParts.size() >= 1? fileNameParts[fileNameParts.size()-1] : ""));
7841 
7842  // If the filename already contains spaces, init-cap the first word but otherwise leave the formatting alone.
7843  if (QRegExp("\\s").indexIn(fileNameContentPart.c_str()) != -1)
7844  {
7845  string formattedName = fileNameContentPart;
7846  if (formattedName.size() >= 1)
7847  formattedName[0] = toupper(formattedName[0]);
7848 
7849  return QString(formattedName.c_str());
7850  }
7851 
7852  // Otherwise, init-cap the first word and insert spaces among CamelCase transitions.
7853  return QString(VuoStringUtilities::expandCamelCase(fileNameContentPart).c_str());
7854 }
7855 
7862 {
7863  QStringList wordsInName = nodeSetName.split(QRegularExpression("\\."));
7864  if (wordsInName.size() < 2 || wordsInName[0] != "vuo")
7865  {
7866  // If not an official Vuo nodeset, return the name as-is.
7867  return nodeSetName;
7868  }
7869 
7870  map<QString, QString> wordsToReformat;
7871  wordsToReformat["artnet"] = "Art-Net";
7872  wordsToReformat["bcf2000"] = "BCF2000";
7873  wordsToReformat["hid"] = "HID";
7874  wordsToReformat["midi"] = "MIDI";
7875  wordsToReformat["ndi"] = "NDI";
7876  wordsToReformat["osc"] = "OSC";
7877  wordsToReformat["rss"] = "RSS";
7878  wordsToReformat["ui"] = "UI";
7879  wordsToReformat["url"] = "URL";
7880 
7881  QString nodeSetDisplayName = "";
7882  for (int i = 1; i < wordsInName.size(); ++i)
7883  {
7884  QString currentWord = wordsInName[i];
7885  if (currentWord.size() >= 1)
7886  {
7887  if (wordsToReformat.find(currentWord.toLower()) != wordsToReformat.end())
7888  currentWord = wordsToReformat.at(currentWord.toLower());
7889  else
7890  currentWord[0] = currentWord[0].toUpper();
7891 
7892  nodeSetDisplayName += currentWord;
7893 
7894  if (i < wordsInName.size()-1)
7895  nodeSetDisplayName += " ";
7896  }
7897  }
7898  return nodeSetDisplayName;
7899 }
7900 
7907 {
7908  if (!type)
7909  return "(none)";
7910 
7911  string formattedTypeName = "";
7912  if (type->hasCompiler() && VuoCompilerType::isListType(type->getCompiler()))
7913  {
7914  string innerTypeName = VuoType::extractInnermostTypeName(type->getModuleKey());
7915  VuoCompilerType *innerType = compiler->getType(innerTypeName);
7916  if (innerType)
7917  {
7918  string formattedInnerTypeName = innerType->getBase()->getDefaultTitle();
7919  formattedTypeName = "List of " + formattedInnerTypeName + " elements";
7920  }
7921  }
7922  else
7923  formattedTypeName = type->getDefaultTitle();
7924 
7925  return formattedTypeName.c_str();
7926 }
7927 
7932 {
7933  if (!type)
7934  return "Event";
7935 
7936  // Special handling for points and transforms so that the initial numeral in their display name doesn't get sanitized away.
7937  else if (type->getDefaultTitle() == "2D Point")
7938  return "Point2D";
7939  else if (type->getDefaultTitle() == "3D Point")
7940  return "Point3D";
7941  else if (type->getDefaultTitle() == "4D Point")
7942  return "Point4D";
7943  else if (type->getDefaultTitle() == "2D Transform")
7944  return "Transform2D";
7945  else if (type->getDefaultTitle() == "3D Transform")
7946  return "Transform3D";
7947 
7948  return VuoRendererPort::sanitizePortIdentifier(formatTypeNameForDisplay(type)).toUtf8().constData();
7949 }
7950 
7954 bool VuoEditorComposition::nodeSetMenuActionLessThan(QAction *action1, QAction *action2)
7955 {
7956  QString item1Text = action1->text();
7957  QString item2Text = action2->text();
7958 
7959  // Ignore list prefixes
7960  const QString listPrefix = "List of ";
7961  const QString builtInTypePrefix = "Vuo";
7962 
7963  if (item1Text.startsWith(listPrefix))
7964  {
7965  item1Text.remove(0, listPrefix.length());
7966  if (item1Text.startsWith(builtInTypePrefix))
7967  item1Text.remove(0, builtInTypePrefix.length());
7968  }
7969 
7970  if (item2Text.startsWith(listPrefix))
7971  {
7972  item2Text.remove(0, listPrefix.length());
7973  if (item2Text.startsWith(builtInTypePrefix))
7974  item2Text.remove(0, builtInTypePrefix.length());
7975  }
7976 
7977  // Sort alphabetically by title.
7978  return (item1Text.compare(item2Text, Qt::CaseInsensitive) < 0);
7979 }
7980 
7985 bool VuoEditorComposition::itemHigherOnCanvas(QGraphicsItem *item1, QGraphicsItem *item2)
7986 {
7987  qreal item1Y = item1->scenePos().y();
7988  qreal item2Y = item2->scenePos().y();
7989 
7990  qreal item1X = item1->scenePos().x();
7991  qreal item2X = item2->scenePos().x();
7992 
7993  if (item1Y == item2Y)
7994  return (item1X < item2X);
7995 
7996  return item1Y < item2Y;
7997 }
7998 
8005 double VuoEditorComposition::calculateNodeSimilarity(VuoNodeClass *node1, VuoNodeClass *node2)
8006 {
8007  // Assign replacement (successor) node classes a perfect match score.
8008  {
8009  string originalGenericNode1ClassName, originalGenericNode2ClassName;
8010  if (node1->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler()))
8011  originalGenericNode1ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node1->getCompiler())->getOriginalGenericNodeClassName();
8012  else
8013  originalGenericNode1ClassName = node1->getClassName();
8014 
8015  if (node2->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler()))
8016  originalGenericNode2ClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node2->getCompiler())->getOriginalGenericNodeClassName();
8017  else
8018  originalGenericNode2ClassName = node2->getClassName();
8019 
8020  if (VuoEditorUtilities::isNodeClassSuccessorTo(originalGenericNode1ClassName.c_str(), originalGenericNode2ClassName.c_str()))
8021  return 1;
8022  }
8023 
8024  // Compare keywords.
8025  vector<string> node1Keywords = node1->getKeywords();
8026  vector<string> node2Keywords = node2->getKeywords();
8027 
8028  // Compare node set names.
8029  if (node1->getNodeSet())
8030  node1Keywords.push_back(node1->getNodeSet()->getName());
8031 
8032  if (node2->getNodeSet())
8033  node2Keywords.push_back(node2->getNodeSet()->getName());
8034 
8035  // Compare tokens in node class display names.
8036  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node1->getDefaultTitle().c_str(), ""))
8037  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8038  node1Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8039 
8040  foreach (QString nodeTitleToken, VuoNodeLibrary::tokenizeNodeName(node2->getDefaultTitle().c_str(), ""))
8041  if (!VuoNodeLibrary::isStopWord(nodeTitleToken.toLower()))
8042  node2Keywords.push_back(nodeTitleToken.toLower().toUtf8().constData());
8043 
8044  set<string> node1KeywordSet(node1Keywords.begin(), node1Keywords.end());
8045  set<string> node2KeywordSet(node2Keywords.begin(), node2Keywords.end());
8046 
8047  set<string> nodeKeywordsIntersection;
8048  std::set_intersection(node1KeywordSet.begin(), node1KeywordSet.end(),
8049  node2KeywordSet.begin(), node2KeywordSet.end(),
8050  std::inserter(nodeKeywordsIntersection, nodeKeywordsIntersection.end()));
8051 
8052  set<string> nodeKeywordsUnion = node1KeywordSet;
8053  nodeKeywordsUnion.insert(node2KeywordSet.begin(), node2KeywordSet.end());
8054 
8055  // Avoid division by zero.
8056  if (nodeKeywordsUnion.size() == 0)
8057  return 0;
8058 
8059  // Calculate Jaccard similarity.
8060  double nodeSimilarity = nodeKeywordsIntersection.size()/(1.0*nodeKeywordsUnion.size());
8061 
8062  return nodeSimilarity;
8063 }
8064 
8069 {
8070  emit compositionOnTop(top);
8071 }
8072 
8077 {
8078  emit publishedPortNameEditorRequested(port, false);
8079 }
8080 
8081 VuoEditorComposition::~VuoEditorComposition()
8082 {
8083  dispatch_sync(runCompositionQueue, ^{});
8084  dispatch_release(runCompositionQueue);
8085 
8086  preparedAnimations.clear();
8087  animationForTimeline.clear();
8088 
8089  delete identifierCache;
8090 
8091  moduleManager->deleteWhenReady(); // deletes compiler
8092 }
8093 
8098 {
8099  // Update the canvas color.
8100  setBackgroundTransparent(false);
8101 
8102  // Force repainting the entire canvas.
8103  setComponentCaching(QGraphicsItem::NoCache);
8106 }
8107 
8113 map<string, string> VuoEditorComposition::publishPorts(set<string> portsToPublish)
8114 {
8115  vector<VuoRendererPort *> sortedPortsToPublish;
8116  foreach (string portID, portsToPublish)
8117  {
8118  VuoPort *port = getPortWithStaticIdentifier(portID);
8119  if (port && port->hasRenderer())
8120  sortedPortsToPublish.push_back(port->getRenderer());
8121  }
8122  std::sort(sortedPortsToPublish.begin(), sortedPortsToPublish.end(), itemHigherOnCanvas);
8123 
8124  map<string, string> publishedPortNames;
8125  foreach (VuoRendererPort *rp, sortedPortsToPublish)
8126  {
8127  rp->updateGeometry();
8128  VuoType *publishedPortType = ((VuoCompilerPortClass *)(rp->getBase()->getClass()->getCompiler()))->getDataVuoType();
8129 
8130  string specializedPublishedPortName = generateSpecialPublishedNameForPort(rp->getBase());
8131  string publishedPortName = (!specializedPublishedPortName.empty()?
8132  specializedPublishedPortName :
8134 
8135  bool forceEventOnlyPublication = rp->effectivelyHasConnectedDataCable(false);
8136  VuoRendererPort *publishedPort = publishInternalPort(rp->getBase(), forceEventOnlyPublication, publishedPortName, publishedPortType, false);
8137 
8138  publishedPortNames[getIdentifierForStaticPort(rp->getBase())] = publishedPort->getBase()->getClass()->getName();
8139  }
8140 
8141  return publishedPortNames;
8142 }
8143 
8150 {
8151  if (!port || !port->hasRenderer() || !port->getRenderer()->getUnderlyingParentNode())
8152  return "";
8153 
8154  // If publishing a port on a "Share Value" node and the node's title has been
8155  // customized, request that title as the published port name.
8159  {
8160  return VuoRendererPort::sanitizePortIdentifier(port->getRenderer()->getUnderlyingParentNode()->getBase()->getTitle().c_str()).toUtf8().constData();
8161  }
8162 
8163  return "";
8164 }
8165 
8169 void VuoEditorComposition::repositionPopover()
8170 {
8171  VuoPortPopover *popover = static_cast<VuoPortPopover *>(QObject::sender());
8172  if (popover && !popover->getDetached())
8173  {
8174  const int cutoffMargin = 16;
8175  if (popover->pos().x()+popover->size().width()+cutoffMargin > views()[0]->viewport()->rect().right())
8176  popover->move(QPoint(views()[0]->viewport()->rect().right()-popover->size().width()-cutoffMargin, popover->pos().y()));
8177 
8178  if (popover->pos().y()+popover->size().height()+cutoffMargin > views()[0]->viewport()->rect().bottom())
8179  popover->move(QPoint(popover->pos().x(), views()[0]->viewport()->rect().bottom()-popover->size().height()-cutoffMargin));
8180  }
8181 }