Vuo  2.1.0
VuoEditorWindow.cc
Go to the documentation of this file.
1 
10 #include "VuoEditorWindow.hh"
11 #include "ui_VuoEditorWindow.h"
12 
13 #include "VuoCompilerCable.hh"
14 #include "VuoCompilerComment.hh"
16 #include "VuoCompilerException.hh"
20 #include "VuoCompilerIssue.hh"
24 #include "VuoCompilerType.hh"
25 #include "VuoComment.hh"
26 #include "VuoComposition.hh"
28 #include "VuoGenericType.hh"
29 #include "VuoCommandMove.hh"
30 #include "VuoCommandAdd.hh"
33 #include "VuoCommandRemove.hh"
36 #include "VuoCommandConnect.hh"
41 #include "VuoCommandSetItemTint.hh"
45 #include "VuoCommandChangeNode.hh"
46 #include "VuoCommandPublishPort.hh"
48 #include "VuoCommandReplaceNode.hh"
49 #include "VuoCommandSetMetadata.hh"
50 #include "VuoCommentEditor.hh"
53 #include "VuoEditorUtilities.hh"
54 #include "VuoEditorComposition.hh"
55 #include "VuoErrorDialog.hh"
56 #include "VuoGlPool.h"
57 #include "VuoMetadataEditor.hh"
58 #include "VuoModuleManager.hh"
59 #include "VuoRendererComment.hh"
60 #include "VuoRendererFonts.hh"
66 #include "VuoSearchBox.hh"
68 #include "VuoEditor.hh"
69 #include "VuoEditorCocoa.hh"
71 #include "VuoExampleMenu.hh"
72 #include "VuoInfoDialog.hh"
73 #include "VuoInputEditorManager.hh"
74 #include "VuoNodeClass.hh"
75 #include "VuoProtocol.hh"
77 #include "VuoRecentFileMenu.hh"
78 #include "VuoStringUtilities.hh"
80 #include "VuoCodeWindow.hh"
81 #include "VuoShaderFile.hh"
82 #include "VuoTitleEditor.hh"
83 #include "VuoInputEditorSession.hh"
84 
85 #ifdef __APPLE__
86 #include <objc/objc-runtime.h>
87 #undef check // Prevent macro defined in AssertMacros.h from overriding VuoCompilerComposition::check
88 #endif
89 
90 #include <fstream>
91 #include <sstream>
92 #include <sys/stat.h>
93 
94 
95 const qreal VuoEditorWindow::viewportStepRate = 1;
96 const qreal VuoEditorWindow::viewportStepRateMultiplier = 5;
97 const qreal VuoEditorWindow::zoomRate = 1.2;
98 const qreal VuoEditorWindow::pastedComponentOffset = 20;
99 const qreal VuoEditorWindow::compositionMargin = 20;
102 
112 VuoEditorWindow::VuoEditorWindow(QString documentIdentifier, QString compositionPath,
113  const string &compositionAsString,
114  VuoNodeLibrary::nodeLibraryDisplayMode nodeLibraryDisplayMode,
115  VuoNodeLibrary::nodeLibraryState nodeLibraryState,
116  VuoNodeLibrary *floater,
117  VuoProtocol *activeProtocol,
118  string nodeClassToHighlight) :
119  ui(new Ui::VuoEditorWindow)
120 {
121  doneInitializing = false;
122 
123  // Initialize the compiler.
124  this->compiler = new VuoCompiler(compositionPath.toStdString());
125 
126  this->compositionUpgradedSinceLastSave = false;
127  this->protocolComplianceReevaluationPending = false;
128  this->ignoreItemMoveSignals = false;
129  this->closing = false;
130  this->containedPrepopulatedContent = (!compositionAsString.empty() || activeProtocol);
131  this->publishedPortNearCursorPreviously = false;
132  this->zoomOutToFitOnNextShowEvent = nodeClassToHighlight.empty();
133  this->includeInRecentFileMenu = true;
134 
135  // Initialize the module manager — before the compiler loads any composition-local modules,
136  // so the module manager will catch any issues with them.
137  VuoModuleManager *moduleManager = new VuoModuleManager(compiler);
138 
139  ui->setupUi(this);
140 
141  // Set keyboard shortcuts.
142  // "On Mac OS X, references to "Ctrl", Qt::CTRL, Qt::Control and Qt::ControlModifier correspond
143  // to the Command keys on the Macintosh keyboard, and references to "Meta", Qt::META, Qt::Meta
144  // and Qt::MetaModifier correspond to the Control keys. Developers on Mac OS X can use the same
145  // shortcut descriptions across all platforms, and their applications will automatically work as
146  // expected on Mac OS X." -- http://qt-project.org/doc/qt-4.8/qkeysequence.html
147  ui->newComposition->setShortcut(QKeySequence("Ctrl+N"));
148  ui->openComposition->setShortcut(QKeySequence("Ctrl+O"));
149  ui->saveComposition->setShortcut(QKeySequence("Ctrl+S"));
150  ui->saveCompositionAs->setShortcut(QKeySequence("Ctrl+Shift+S"));
151  ui->closeComposition->setShortcut(QKeySequence("Ctrl+W"));
152  ui->selectAll->setShortcut(QKeySequence("Ctrl+A"));
153  ui->selectNone->setShortcut(QKeySequence("Ctrl+Shift+A"));
154  ui->cutCompositionComponents->setShortcut(QKeySequence("Ctrl+X"));
155  ui->copyCompositionComponents->setShortcut(QKeySequence("Ctrl+C"));
156  ui->duplicateCompositionComponents->setShortcut(QKeySequence("Ctrl+D"));
157  ui->paste->setShortcut(QKeySequence("Ctrl+V"));
158  ui->deleteCompositionComponents->setShortcut(QKeySequence("Backspace"));
159  ui->zoomIn->setShortcut(QKeySequence("Ctrl+="));
160  ui->zoomOut->setShortcut(QKeySequence("Ctrl+-"));
161  ui->zoom11->setShortcut(QKeySequence("Ctrl+0"));
162  isZoomedToFit = false;
163  addAction(ui->stopComposition);
164  ui->showNodeLibrary->setShortcut(QKeySequence("Ctrl+Return"));
165 
166  // Trigger app-wide menu with our local window menu.
167  connect(ui->newComposition, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::newComposition);
168  connect(ui->openComposition, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openFile);
169 
170  // Connect the "Quit" menu item action to our customized quit method. On Mac OS X, this menu
171  // item will automatically be moved from the "File" menu to the Application menu.
172  quitAction = ui->menuFile->addAction(tr("&Quit"), static_cast<VuoEditor *>(qApp), &VuoEditor::quitCleanly, QKeySequence("Ctrl+Q"));
173 
174  // "About" menu item
175  ui->menuFile->addAction(tr("About Vuo…"), static_cast<VuoEditor *>(qApp), &VuoEditor::about);
176 
177  VuoEditor *editor = (VuoEditor *)qApp;
178 
179  ui->menuView->addSeparator();
180 
181 #if VUO_PRO
182  // "Dark Interface" menu item
183  ui->menuView->addAction(editor->darkInterfaceAction);
184 
185  connect(editor, &VuoEditor::darkInterfaceToggled, this, &VuoEditorWindow::updateColor);
186  updateColor(editor->isInterfaceDark());
187 #endif
188 
189  // "Grid" menu
190  QMenu *menuGrid = new QMenu(ui->menuBar);
191  menuGrid->setSeparatorsCollapsible(false);
192  menuGrid->setTitle(tr("&Grid"));
193  menuGrid->addAction(editor->snapToGridAction);
194 
195  menuGrid->addSeparator();
196 
197  menuGrid->addAction(editor->showGridLinesAction);
198  menuGrid->addAction(editor->showGridPointsAction);
199  connect(editor, &VuoEditor::showGridToggled, this, &VuoEditorWindow::updateGrid);
200 
201  ui->menuView->addMenu(menuGrid);
202 
203  // "Canvas Transparency" menu
204  QMenu *menuCanvasTransparency = new QMenu(ui->menuBar);
205  menuCanvasTransparency->setSeparatorsCollapsible(false);
206  menuCanvasTransparency->setTitle(tr("&Canvas Transparency"));
207  ((VuoEditor *)qApp)->populateCanvasTransparencyMenu(menuCanvasTransparency);
208  connect(editor, &VuoEditor::canvasOpacityChanged, this, &VuoEditorWindow::updateCanvasOpacity);
209 
210  ui->menuView->addMenu(menuCanvasTransparency);
211 
212  // "New Composition from Template" menu
213  menuNewCompositionWithTemplate = new QMenu(tr("New Composition from Template"));
214  menuNewCompositionWithTemplate->setSeparatorsCollapsible(false);
215  ((VuoEditor *)qApp)->populateNewCompositionWithTemplateMenu(menuNewCompositionWithTemplate);
216 
217  // "New Shader" menu
218  QMenu *menuNewShader = new QMenu(tr("New Shader"));
219  menuNewShader->setSeparatorsCollapsible(false);
220  ((VuoEditor *)qApp)->populateNewShaderMenu(menuNewShader);
221 
222  // Insert the "New Composition from Template" and "New Shader" menus immediately after the "New Composition" menu item.
223  for (int menuFileIndex = 0; menuFileIndex < ui->menuFile->actions().count(); ++menuFileIndex)
224  {
225  if (ui->menuFile->actions().at(menuFileIndex) == ui->newComposition)
226  {
227  ui->menuFile->insertMenu(ui->menuFile->actions().at(menuFileIndex+1), menuNewCompositionWithTemplate);
228  ui->menuFile->insertMenu(ui->menuFile->actions().at(menuFileIndex+2), menuNewShader);
229  break;
230  }
231  }
232 
233  // "Open Example" menu
234  menuOpenExample = new VuoExampleMenu(ui->menuFile, compiler);
235  connect(menuOpenExample, &VuoExampleMenu::exampleSelected, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
236 
237  menuOpenRecent = new VuoRecentFileMenu();
238  connect(menuOpenRecent, &VuoRecentFileMenu::recentFileSelected, static_cast<VuoEditor *>(qApp), &VuoEditor::openUrl);
239 
240  // Insert the "Open Recent" and "Open Example" menus immediately after the "Open..." menu item.
241  for (int menuFileIndex = 0; menuFileIndex < ui->menuFile->actions().count(); ++menuFileIndex)
242  if (ui->menuFile->actions().at(menuFileIndex) == ui->openComposition)
243  {
244  ui->menuFile->insertMenu(ui->menuFile->actions().at(menuFileIndex+1), menuOpenRecent);
245  ui->menuFile->insertMenu(ui->menuFile->actions().at(menuFileIndex+2), menuOpenExample);
246  }
247 
248  // Ensure that the keyboard shortcuts for the "Open Recent" and "Open Example" submenus work
249  // even when no windows are open.
251 
252  // "Protocols" menu
253  menuProtocols = new QMenu(tr("Protocols"));
254  menuProtocols->setSeparatorsCollapsible(false);
255  populateProtocolsMenu(menuProtocols);
256 
257  // Insert the "Protocols" menu immediately before the "Composition Information" menu item.
258  ui->menuEdit->insertMenu(ui->compositionInformation, menuProtocols);
259 
260  // Populate the "Help" menu.
261  ((VuoEditor *)qApp)->populateHelpMenu(ui->menuHelp);
262 
263  // Prevent "Help > Search" from triggering lookup of all customized example composition titles at once.
264  connect(ui->menuHelp, &QMenu::aboutToShow, menuOpenExample, &VuoExampleMenu::disableExampleTitleLookup);
265  connect(ui->menuHelp, &QMenu::aboutToHide, menuOpenExample, &VuoExampleMenu::enableExampleTitleLookup);
266 
267  connect(ui->menuHelp, &QMenu::aboutToShow, [editor, this] { editor->populateHelpMenu(ui->menuHelp); });
268 
269  // Initialize the composition.
270  VuoCompilerComposition *compilerComposition = (compositionAsString.empty() ?
271  new VuoCompilerComposition(new VuoComposition(), NULL) :
273 
274  compilerComposition->getBase()->setDirectory(VuoEditor::getDefaultCompositionStorageDirectory().toUtf8().constData());
275 
276  composition = new VuoEditorComposition(this, compilerComposition->getBase());
277  composition->setCompiler(this->compiler);
278  composition->setModuleManager(moduleManager);
279 
280  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->addComposition(this);
281 
282  if (!compositionAsString.empty())
283  {
284  string dir, file, extension;
285  VuoFileUtilities::splitPath(compositionPath.toStdString(), dir, file, extension);
286 
287  if (!dir.empty())
288  compilerComposition->getBase()->setDirectory(dir);
289 
290  string defaultName = VuoEditorComposition::getDefaultNameForPath(documentIdentifier.toUtf8().data());
291  compilerComposition->getBase()->getMetadata()->setDefaultName(defaultName);
292  }
293 
294  ui->graphicsView->setScene(composition);
295  ui->graphicsView->setAlignment(Qt::AlignLeft | Qt::AlignTop);
296  ui->graphicsView->setMouseTracking(true);
297  composition->installEventFilter(this);
298  ui->graphicsView->viewport()->installEventFilter(this);
299 
300  connect(ui->cutCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::cutSelectedCompositionComponents);
301  connect(ui->copyCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::copySelectedCompositionComponents);
302  connect(ui->duplicateCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::duplicateSelectedCompositionComponentsByMenuItem);
303  connect(ui->paste, &QAction::triggered, this, &VuoEditorWindow::disambiguatePasteRequest);
304  connect(ui->deleteCompositionComponents, &QAction::triggered, composition, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
305  connect(ui->renameNodes, &QAction::triggered, composition, &VuoEditorComposition::renameSelectedNodes);
306  connect(ui->refactor, &QAction::triggered, this, &VuoEditorWindow::refactorSelectedItems);
307  connect(ui->selectAll, &QAction::triggered, composition, &VuoEditorComposition::selectAllCompositionComponents);
308  connect(ui->selectAllComments, &QAction::triggered, composition, &VuoEditorComposition::selectAllComments);
309 
310  // Initialize the action associated with raising this window to the application foreground.
311  raiseDocumentAction = new QAction(this);
312  raiseDocumentAction->setCheckable(true);
313  connect(raiseDocumentAction, &QAction::triggered, this, &VuoEditorWindow::setAsActiveWindow);
314  connect(ui->menuWindow, &QMenu::aboutToShow, this, &VuoEditorWindow::updateUI);
315 
316  // Prepare the input editors.
317  inputEditorManager = new VuoInputEditorManager();
318  composition->setInputEditorManager(inputEditorManager);
319  inputEditorSession = nullptr;
320  connect(composition, &VuoEditorComposition::portConstantChangeRequested, this, &VuoEditorWindow::setPortConstant);
321  connect(composition, &VuoEditorComposition::inputEditorRequested, this, static_cast<void (VuoEditorWindow::*)(VuoRendererPort *)>(&VuoEditorWindow::showInputEditor));
322  connect(composition, &VuoEditorComposition::nodeTitleEditorRequested, this, &VuoEditorWindow::showNodeTitleEditor);
323  connect(composition, &VuoEditorComposition::commentEditorRequested, this, &VuoEditorWindow::showCommentEditor);
324  connect(composition, &VuoEditorComposition::commentZoomRequested, this, &VuoEditorWindow::zoomToFitComment);
325 
326  connect(composition, &VuoEditorComposition::commentInsertionRequested, this, &VuoEditorWindow::insertCommentAtPos);
327  connect(composition, &VuoEditorComposition::subcompositionInsertionRequested, this, &VuoEditorWindow::insertSubcompositionAtPos);
328  connect(composition, &VuoEditorComposition::nodeSourceEditorRequested, this, &VuoEditorWindow::openEditableSourceForNode);
330 
331  connect(composition, &VuoEditorComposition::refactorRequested, this, &VuoEditorWindow::refactorSelectedItems);
332 
333  // Initialize the 'undo' stack.
334  undoStack = new QUndoStack(this);
335  connect(undoStack, &QUndoStack::cleanChanged, this, &VuoEditorWindow::undoStackCleanStateChanged);
336 
337  undoAction = undoStack->createUndoAction(this);
338  undoAction->setText(tr("Undo"));
339  undoAction->setShortcut(QKeySequence::Undo);
340 
341  redoAction = undoStack->createRedoAction(this);
342  redoAction->setText(tr("Redo"));
343  redoAction->setShortcut(QKeySequence::Redo);
344 
345  metadataEditor = new VuoMetadataEditor(composition, this, Qt::Sheet);
346  metadataEditor->setWindowModality(Qt::WindowModal);
347  connect(metadataEditor, &VuoMetadataEditor::finished, this, &VuoEditorWindow::editMetadata);
348 
349  searchBox = new VuoSearchBox(composition, this, Qt::Widget);
350  searchBox->setVisible(false);
351  connect(searchBox, &VuoSearchBox::searchPerformed, this, &VuoEditorWindow::updateUI);
352 
353  canvasDragEnabled = false;
354  canvasDragInProgress = false;
355  scrollInProgress = false;
356  timeOfLastScroll = 0;
357  consumeNextMouseReleaseToCanvas = false;
358  lastLeftMousePressHadOptionModifier = false;
359  rubberBandSelectionInProgress = false;
360  previousDragMoveWasOverSidebar = false;
361  duplicationMacroInProgress = false;
362  itemDragMacroInProgress = false;
363  itemDragDx = 0;
364  itemDragDy = 0;
365  latestDragTime = 0;
366  commentResizeMacroInProgress = false;
367  commentBeingResized = NULL;
368  commentResizeDx = 0;
369  commentResizeDy = 0;
370  forwardingEventsToCanvas = false;
371 
372  ui->menuEdit->insertAction(ui->menuEdit->actions()[0], redoAction);
373  ui->menuEdit->insertAction(redoAction, undoAction);
374 
375  contextMenuTints = composition->getContextMenuTints();
376  ui->menuEdit->insertMenu(ui->changeNodePlaceholder, contextMenuTints);
377 
378  menuChangeNode = new QMenu(ui->menuEdit);
379  menuChangeNode->setTitle(tr("Change To"));
380 
381  connect(ui->menuEdit, &QMenu::aboutToShow, this, &VuoEditorWindow::updateUI);
382 
383  foreach (QMenu *menu, ui->menuBar->findChildren<QMenu*>())
384  menu->setSeparatorsCollapsible(false);
385 
386  connect(composition, &VuoEditorComposition::itemsMoved, this, &VuoEditorWindow::itemsMoved);
388  connect(composition, &VuoEditorComposition::leftMouseButtonReleased, this, &VuoEditorWindow::resetUndoStackMacros);
394  connect(composition, SIGNAL(portPublicationRequested(VuoPort *, bool)), this, SLOT(internalPortPublished(VuoPort *, bool)));
395  connect(composition, &VuoEditorComposition::publishedPortNameEditorRequested, this, &VuoEditorWindow::showPublishedPortNameEditor);
398  connect(composition, &VuoEditorComposition::activeProtocolChanged, this, &VuoEditorWindow::updateActiveProtocolDisplay);
399  connect(composition, &VuoEditorComposition::publishedPortModified, this, &VuoEditorWindow::registerProtocolComplianceEvaluationRequest);
403  connect(composition, &VuoEditorComposition::specializePort, this, static_cast<VuoRendererNode *(VuoEditorWindow::*)(VuoRendererPort *port, string specializedTypeName)>(&VuoEditorWindow::specializePortNetwork));
405  connect(composition, &VuoEditorComposition::respecializePort, this, static_cast<VuoRendererNode *(VuoEditorWindow::*)(VuoRendererPort *port, string specializedTypeName)>(&VuoEditorWindow::respecializePortNetwork));
407  connect(composition, &VuoEditorComposition::selectedInternalCablesHidden, this, &VuoEditorWindow::hideSelectedInternalCables);
408  connect(composition, &VuoEditorComposition::cablesHidden, this, &VuoEditorWindow::hideCables);
409  connect(composition, &VuoEditorComposition::cablesUnhidden, this, &VuoEditorWindow::unhideCables);
411  connect(composition, &VuoEditorComposition::cableDragInitiated, this, &VuoEditorWindow::updateWindowForNewCableDrag);
415  connect(composition, &VuoEditorComposition::buildStarted, this, &VuoEditorWindow::showBuildActivityIndicator);
416  connect(composition, &VuoEditorComposition::buildFinished, this, &VuoEditorWindow::hideBuildActivityIndicator);
417  connect(composition, &VuoEditorComposition::stopFinished, this, &VuoEditorWindow::hideStopActivityIndicator);
418 
419  // Avoid bug where the first time the application is de-activated after a popover is detached,
420  // the application and its widgets (including the popover) fail to receive any notification of
421  // the de-activation unless this window has first been re-activated in place of the popover.
422  // See https://b33p.net/kosada/node/6281 .
424 
425  // Uncomment to display undo stack for debugging purposes:
426  //undoView = new QUndoView(undoStack);
427  //undoView->show();
428 
429  // Show the VuoSearchBox over just the composition area (not the node library or published port sidebar areas).
430  setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea);
431  setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea);
432 
433  connect(ui->toggleNodeLibraryDocking, &QAction::triggered, this, &VuoEditorWindow::toggleNodeLibraryDockedState);
434 
435  QActionGroup* toggleDisplay= new QActionGroup(this);
436  toggleDisplay->addAction(ui->actionShowNodeNames);
437  toggleDisplay->addAction(ui->actionShowNodeClassNames);
438 
439  // Initialize published port sidebars.
440  inputPortSidebar = new VuoPublishedPortSidebar(this, composition, true);
441  outputPortSidebar = new VuoPublishedPortSidebar(this, composition, false);
442  inputPortSidebar->setVisible(false);
443  outputPortSidebar->setVisible(false);
444  populateProtocolsMenu(inputPortSidebar->getProtocolsContextMenu());
445  populateProtocolsMenu(outputPortSidebar->getProtocolsContextMenu());
446 
447  connect(inputPortSidebar->getRemoveProtocolAction(), &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
448  connect(outputPortSidebar->getRemoveProtocolAction(), &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
449 
450  connect(inputPortSidebar, &VuoPublishedPortSidebar::closed, this, &VuoEditorWindow::closePublishedPortSidebars);
451  connect(outputPortSidebar, &VuoPublishedPortSidebar::closed, this, &VuoEditorWindow::closePublishedPortSidebars);
452 
453  connect(inputPortSidebar, &VuoPublishedPortSidebar::visibilityChanged, this, &VuoEditorWindow::conditionallyShowPublishedPortSidebars);
454  connect(outputPortSidebar, &VuoPublishedPortSidebar::visibilityChanged, this, &VuoEditorWindow::conditionallyShowPublishedPortSidebars);
455 
456  connect(inputPortSidebar, &VuoPublishedPortSidebar::publishedPortPositionsUpdated, this, &VuoEditorWindow::updatePublishedCableGeometry);
457  connect(outputPortSidebar, &VuoPublishedPortSidebar::publishedPortPositionsUpdated, this, &VuoEditorWindow::updatePublishedCableGeometry);
458  connect(inputPortSidebar, &VuoPublishedPortSidebar::publishedPortsReordered, this, &VuoEditorWindow::updatePublishedPortOrder);
459  connect(outputPortSidebar, &VuoPublishedPortSidebar::publishedPortsReordered, this, &VuoEditorWindow::updatePublishedPortOrder);
460 
461  connect(ui->graphicsView, &VuoEditorGraphicsView::viewResized, inputPortSidebar, &VuoPublishedPortSidebar::externalMoveEvent);
462  connect(ui->graphicsView, &VuoEditorGraphicsView::viewResized, outputPortSidebar, &VuoPublishedPortSidebar::externalMoveEvent);
463  connect(ui->graphicsView, &VuoEditorGraphicsView::viewResized, this, &VuoEditorWindow::viewportFitReset);
464  connect(ui->graphicsView->horizontalScrollBar(), &QScrollBar::valueChanged, this, &VuoEditorWindow::viewportFitReset);
465  connect(ui->graphicsView->horizontalScrollBar(), SIGNAL(valueChanged(int)), outputPortSidebar, SLOT(externalMoveEvent()));
466  connect(ui->graphicsView->horizontalScrollBar(), SIGNAL(rangeChanged(int, int)), outputPortSidebar, SLOT(externalMoveEvent()));
467  connect(ui->graphicsView->verticalScrollBar(), &QScrollBar::valueChanged, this, &VuoEditorWindow::viewportFitReset);
468  connect(ui->graphicsView->verticalScrollBar(), SIGNAL(valueChanged(int)), outputPortSidebar, SLOT(externalMoveEvent()));
469  connect(ui->graphicsView->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), outputPortSidebar, SLOT(externalMoveEvent()));
470  connect(ui->graphicsView, &VuoEditorGraphicsView::rubberBandChanged, this, &VuoEditorWindow::updateRubberBandSelectionMode);
471 
472  connect(composition, &VuoEditorComposition::sceneRectChanged, this, &VuoEditorWindow::viewportFitReset);
473  connect(composition, &VuoEditorComposition::selectionChanged, this, &VuoEditorWindow::viewportFitReset);
478  connect(composition, SIGNAL(portPublicationRequested(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)), this, SLOT(internalExternalPortPairPublished(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)));
479 
482 
483  connect(composition, SIGNAL(publishedPortModified()), inputPortSidebar, SLOT(updatePortList()), Qt::QueuedConnection);
484  connect(composition, SIGNAL(publishedPortModified()), outputPortSidebar, SLOT(updatePortList()), Qt::QueuedConnection);
485 
486  connect(inputPortSidebar, &VuoPublishedPortSidebar::newPublishedPortRequested, this, &VuoEditorWindow::createIsolatedExternalPublishedPort);
487  connect(outputPortSidebar, &VuoPublishedPortSidebar::newPublishedPortRequested, this, &VuoEditorWindow::createIsolatedExternalPublishedPort);
488 
491  connect(inputPortSidebar, SIGNAL(portPublicationRequestedViaSidebarPort(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)), this, SLOT(internalExternalPortPairPublished(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)));
492  connect(outputPortSidebar, SIGNAL(portPublicationRequestedViaSidebarPort(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)), this, SLOT(internalExternalPortPairPublished(VuoPort *, VuoPublishedPort *, bool, VuoPort *, string, string, bool)));
495 
498  connect(inputPortSidebar, &VuoPublishedPortSidebar::publishedPortNameEditorRequested, this, &VuoEditorWindow::showPublishedPortNameEditor);
499  connect(outputPortSidebar, &VuoPublishedPortSidebar::publishedPortNameEditorRequested, this, &VuoEditorWindow::showPublishedPortNameEditor);
500  connect(inputPortSidebar, SIGNAL(publishedPortDetailsChangeRequested(VuoRendererPublishedPort *, json_object *)), this, SLOT(changePublishedPortDetails(VuoRendererPublishedPort *, json_object *)));
501  connect(outputPortSidebar, SIGNAL(publishedPortDetailsChangeRequested(VuoRendererPublishedPort *, json_object *)), this, SLOT(changePublishedPortDetails(VuoRendererPublishedPort *, json_object *)));
502 
507 
508  connect(inputPortSidebar, &VuoPublishedPortSidebar::inputEditorRequested, this, static_cast<void (VuoEditorWindow::*)(VuoRendererPort *)>(&VuoEditorWindow::showInputEditor));
509 
510  connect(undoStack, &QUndoStack::indexChanged, searchBox, &VuoSearchBox::refreshResults);
511  connect(undoStack, &QUndoStack::indexChanged, this, &VuoEditorWindow::coalescedUpdateRunningComposition);
512  connect(undoStack, &QUndoStack::indexChanged, this, &VuoEditorWindow::handlePendingProtocolComplianceReevaluationRequests);
513  connect(undoStack, SIGNAL(indexChanged(int)), inputPortSidebar, SLOT(updatePortList()));
514  connect(undoStack, SIGNAL(indexChanged(int)), outputPortSidebar, SLOT(updatePortList()));
515  // Use a queued connection to avoid mutual recursion between QUndoStack::indexChanged() and VuoEditorComposition::updateFeedbackErrors().
516  connect(undoStack, SIGNAL(indexChanged(int)), composition, SLOT(updateFeedbackErrors()), Qt::QueuedConnection);
517 
518 #ifdef VUO_PRO
519  VuoEditorWindow_Pro();
520 #endif
521 
522  coalescedOldCompositionSnapshot = "";
523  coalescedNewCompositionSnapshot = "";
524  coalescedDiffInfo = nullptr;
525 
526  toolbar = NULL;
527 
528  metadataPanel = new VuoCompositionMetadataPanel(composition->getBase());
530  connect(metadataPanel, &VuoCompositionMetadataPanel::metadataEditRequested, this, &VuoEditorWindow::on_compositionInformation_triggered);
531 
532  initializeNodeLibrary(this->compiler, nodeLibraryDisplayMode, nodeLibraryState, floater);
533  moduleManager->setNodeLibrary(ownedNodeLibrary);
534  moduleManager->updateWithAlreadyLoadedModules();
536 
537  // Dynamically resize the sceneRect to accommodate current canvas items.
538  connect(composition, &VuoEditorComposition::changed, this, &VuoEditorWindow::ensureSceneRectContainsRegion);
539 
540  // Update relevant menu items when there is a change to the set of currently selected composition components.
541  connect(composition, &VuoEditorComposition::selectionChanged, this, &VuoEditorWindow::updateSelectedComponentMenuItems);
542 
543  // Update relevant menu items when the trigger port to manually re-fire changes.
544  connect(composition, &VuoEditorComposition::refirePortChanged, this, &VuoEditorWindow::updateRefireAction);
545 
546  // Update relevant menu items when clipboard data is modified.
547  connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &VuoEditorWindow::updateUI);
548 
549  // Performance optimizations
550  ui->graphicsView->setCacheMode(QGraphicsView::CacheBackground);
551 
552  ui->graphicsView->setAttribute(Qt::WA_NoBackground, true);
553  ui->graphicsView->setAttribute(Qt::WA_OpaquePaintEvent);
554  ui->graphicsView->setAttribute(Qt::WA_NoSystemBackground);
555 
556  if (nl)
557  {
558  nl->setAttribute(Qt::WA_NoBackground, true);
559  nl->setAttribute(Qt::WA_OpaquePaintEvent);
560  nl->setAttribute(Qt::WA_NoSystemBackground);
561  }
562 
563  if (activeProtocol)
564  {
565  composition->addActiveProtocol(activeProtocol, false);
566 
567  if (compositionAsString.empty())
568  {
569  // Automatically add an "Allow First Event" node to new protocol compositions
570  // and connect it to the published "time" input port.
571  VuoRendererNode *allowFirstEventNode = composition->createNode("vuo.event.allowFirst", "", 30, 3*VuoRendererComposition::majorGridLineSpacing);
572  composition->addNode(allowFirstEventNode->getBase());
573 
574  if (composition->getBase()->getPublishedInputPortWithName("time") &&
575  allowFirstEventNode->getBase()->getInputPortWithName("event"))
576  {
577  composition->publishInternalPort(allowFirstEventNode->getBase()->getInputPortWithName("event"), false, "time", NULL, true);
578  }
579  }
580  }
581  else
582  {
583  if (compositionAsString.empty())
584  {
585  // Automatically add a "Fire on Start" node to new compositions,
586  // except those initialized under an active protocol.
587  // Select x- and y- coordinates that don't cause the resulting sceneRect update
588  // to visibly jiggle the node in either node-library-floating or -docked mode.
589  VuoRendererNode *fireOnStartNode = composition->createNode("vuo.event.fireOnStart", "", 30, 50);
590  composition->addNode(fireOnStartNode->getBase());
591  composition->setTriggerPortToRefire(fireOnStartNode->getBase()->getOutputPortWithName("started"));
592  }
593  else
594  evaluateCompositionForProtocolPromotion();
595  }
596 
597  inputPortSidebar->updateActiveProtocol();
598  outputPortSidebar->updateActiveProtocol();
599 
600  // By default, display the published port sidebars if and only if the composition has any published ports.
601  bool displayPublishedPorts = ((! composition->getBase()->getPublishedInputPorts().empty()) ||
602  (! composition->getBase()->getPublishedOutputPorts().empty()));
603  setPublishedPortSidebarVisibility(displayPublishedPorts);
604 
605  // The toolbar must be initialized after the composition, since it triggers moveEvent(), which assumes the composition exists.
606  toolbar = new VuoEditorWindowToolbar(this);
607 
608  doneInitializing = true;
609  updateColor(editor->isInterfaceDark());
610  updateCanvasOpacity();
611 
612  string dir, file, ext;
613  VuoFileUtilities::splitPath(documentIdentifier.toUtf8().constData(), dir, file, ext);
614 
615  // Case: Creating a new composition (which may or may not have pre-populated content)
616  if (dir.empty())
617  {
618  setWindowTitle(documentIdentifier + "[*]");
619 #if VUO_PRO
620  toolbar->updateTitle();
621 #endif
622 
623  // Generate default description.
624  composition->getBase()->getMetadata()->setDescription("");
625 
626  // Generate version of Vuo used to create composition.
627  composition->getBase()->getMetadata()->setCreatedInVuoVersion(VUO_VERSION_STRING);
628 
629  // Generate author link and default copyright.
630  const string user = static_cast<VuoEditor *>(qApp)->getUserName();
631  const string userProfileURL = static_cast<VuoEditor *>(qApp)->getStoredUserProfileURL();
632  const string userProfileLink = (userProfileURL.empty()? user : "[" + user + "](" + userProfileURL + ")");
633  composition->getBase()->getMetadata()->setAuthor(userProfileLink);
634  composition->getBase()->getMetadata()->setCopyright(generateCurrentDefaultCopyright());
635  }
636 
637  // Case: Opening a pre-existing composition from the filesystem
638  else
639  {
640  setWindowFilePath(documentIdentifier);
641  setFocus();
642  }
643 
644  // Don't display the "Edit..." link in the metadata panel for example compositions.
645  QDir compositionDir(QDir(composition->getBase()->getDirectory().c_str()).canonicalPath());
646  bool tmpFile = (compositionDir.canonicalPath() == QDir(VuoFileUtilities::getTmpDir().c_str()).canonicalPath());
647  metadataPanel->setIsUserComposition(!tmpFile);
648 
650  updateSceneRect();
651 
652  if (!nodeClassToHighlight.empty())
653  highlightNodeClass(nodeClassToHighlight);
654 
655  updateUI();
656  updateRefireAction();
657 
658  // Schedule this after the constructor returns so there's not a brief glitchy grayness when showing the window.
659  QMetaObject::invokeMethod(this, "showUpdateHelpDialog", Qt::QueuedConnection);
660 }
661 
662 VuoEditorWindow::~VuoEditorWindow()
663 {
664  VUserLog("%s: Close", getWindowTitleWithoutPlaceholder().toUtf8().data());
665 
666  disconnect(inputPortSidebar->getRemoveProtocolAction(), &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
667  disconnect(outputPortSidebar->getRemoveProtocolAction(), &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
668  disconnect(inputPortSidebar, &VuoPublishedPortSidebar::publishedPortPositionsUpdated, this, &VuoEditorWindow::updatePublishedCableGeometry);
669  disconnect(outputPortSidebar, &VuoPublishedPortSidebar::publishedPortPositionsUpdated, this, &VuoEditorWindow::updatePublishedCableGeometry);
670  disconnect(inputPortSidebar, &VuoPublishedPortSidebar::publishedPortsReordered, this, &VuoEditorWindow::updatePublishedPortOrder);
671  disconnect(outputPortSidebar, &VuoPublishedPortSidebar::publishedPortsReordered, this, &VuoEditorWindow::updatePublishedPortOrder);
672 
677 
678  disconnect(inputPortSidebar, &VuoPublishedPortSidebar::inputEditorRequested, this, static_cast<void (VuoEditorWindow::*)(VuoRendererPort *)>(&VuoEditorWindow::showInputEditor));
679 
680  disconnect(ui->graphicsView, SIGNAL(viewResized()), outputPortSidebar, SLOT(externalMoveEvent()));
681  disconnect(ui->graphicsView, &VuoEditorGraphicsView::viewResized, this, &VuoEditorWindow::viewportFitReset);
682  disconnect(ui->graphicsView->horizontalScrollBar(), &QScrollBar::valueChanged, this, &VuoEditorWindow::viewportFitReset);
683  disconnect(ui->graphicsView->horizontalScrollBar(), SIGNAL(valueChanged(int)), outputPortSidebar, SLOT(externalMoveEvent()));
684  disconnect(ui->graphicsView->horizontalScrollBar(), SIGNAL(rangeChanged(int, int)), outputPortSidebar, SLOT(externalMoveEvent()));
685  disconnect(ui->graphicsView->verticalScrollBar(), &QScrollBar::valueChanged, this, &VuoEditorWindow::viewportFitReset);
686  disconnect(ui->graphicsView->verticalScrollBar(), SIGNAL(valueChanged(int)), outputPortSidebar, SLOT(externalMoveEvent()));
687  disconnect(ui->graphicsView->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), outputPortSidebar, SLOT(externalMoveEvent()));
688  disconnect(ui->graphicsView, &VuoEditorGraphicsView::rubberBandChanged, this, &VuoEditorWindow::updateRubberBandSelectionMode);
689 
690  disconnect(ui->deleteCompositionComponents, &QAction::triggered, composition, static_cast<void (VuoEditorComposition::*)()>(&VuoEditorComposition::deleteSelectedCompositionComponents));
691  disconnect(ui->renameNodes, &QAction::triggered, composition, &VuoEditorComposition::renameSelectedNodes);
692  disconnect(ui->selectAll, &QAction::triggered, composition, &VuoEditorComposition::selectAllCompositionComponents);
693  disconnect(ui->selectAllComments, &QAction::triggered, composition, &VuoEditorComposition::selectAllComments);
694 
695  disconnect(ui->newComposition, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::newComposition);
696  disconnect(ui->openComposition, &QAction::triggered, static_cast<VuoEditor *>(qApp), &VuoEditor::openFile);
697  disconnect(ui->cutCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::cutSelectedCompositionComponents);
698  disconnect(ui->copyCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::copySelectedCompositionComponents);
699  disconnect(ui->duplicateCompositionComponents, &QAction::triggered, this, &VuoEditorWindow::duplicateSelectedCompositionComponentsByMenuItem);
700  disconnect(ui->paste, &QAction::triggered, this, &VuoEditorWindow::disambiguatePasteRequest);
701  disconnect(ui->menuWindow, &QMenu::aboutToShow, this, &VuoEditorWindow::updateUI);
702  disconnect(ui->menuEdit, &QMenu::aboutToShow, this, &VuoEditorWindow::updateUI);
703  disconnect(ui->menuHelp, &QMenu::aboutToShow, menuOpenExample, &VuoExampleMenu::disableExampleTitleLookup);
704  disconnect(ui->menuHelp, &QMenu::aboutToHide, menuOpenExample, &VuoExampleMenu::enableExampleTitleLookup);
705 
706  disconnect(undoStack, &QUndoStack::cleanChanged, this, &VuoEditorWindow::undoStackCleanStateChanged);
707  disconnect(undoStack, &QUndoStack::indexChanged, searchBox, &VuoSearchBox::refreshResults);
708  disconnect(undoStack, &QUndoStack::indexChanged, this, &VuoEditorWindow::coalescedUpdateRunningComposition);
709  disconnect(undoStack, &QUndoStack::indexChanged, this, &VuoEditorWindow::handlePendingProtocolComplianceReevaluationRequests);
710  disconnect(undoStack, SIGNAL(indexChanged(int)), inputPortSidebar, SLOT(updatePortList()));
711  disconnect(undoStack, SIGNAL(indexChanged(int)), outputPortSidebar, SLOT(updatePortList()));
712  disconnect(undoStack, SIGNAL(indexChanged(int)), composition, SLOT(updateFeedbackErrors()));
713 
714  disconnect(composition, &VuoEditorComposition::sceneRectChanged, this, &VuoEditorWindow::viewportFitReset);
715  disconnect(composition, &VuoEditorComposition::selectionChanged, this, &VuoEditorWindow::viewportFitReset);
720 
721  transitionNodeLibraryConnections(nl, NULL);
723 
724  delete toolbar;
725  delete metadataPanel;
726 
727  // If this is a subcomposition, revert any unsaved changes.
728  VuoSubcompositionMessageRouter *subcompositionRouter = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter();
729  subcompositionRouter->applyIfInstalledAsSubcomposition(getComposition(), ^(VuoEditorComposition *subcomposition, string subcompositionPath) {
730 
731  map<string, string> constantPortIdentifiersAndValues;
732  string revertedSourceCode;
733  try
734  {
735  revertedSourceCode = VuoFileUtilities::readFileToString(subcompositionPath);
736  }
737  catch (std::exception const &e)
738  {
739  // The user may have deleted this subcomposition via Finder before closing its window.
740  // https://b33p.net/kosada/node/16404
741  return;
742  }
743 
744  VuoCompilerComposition *revertedComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(revertedSourceCode, compiler);
745  for (VuoNode *node : revertedComposition->getBase()->getNodes())
746  {
747  for (VuoPort *port : node->getInputPorts())
748  {
749  if (port->hasCompiler())
750  {
751  VuoCompilerInputEventPort *compilerPort = static_cast<VuoCompilerInputEventPort *>(port->getCompiler());
752  if (compilerPort->getDataVuoType() && ! compilerPort->hasConnectedDataCable())
753  {
754  compilerPort->setNodeIdentifier(node->getCompiler()->getIdentifier());
755  constantPortIdentifiersAndValues[compilerPort->getIdentifier()] = compilerPort->getData()->getInitialValue();
756  }
757  }
758  }
759  }
760  delete revertedComposition;
761 
762  subcompositionRouter->applyToAllOtherTopLevelCompositions(getComposition(), ^(VuoEditorComposition *topLevelComposition) {
763 
764  string nodeClassName = VuoCompiler::getModuleKeyForPath(subcompositionPath);
765  topLevelComposition->getModuleManager()->doNextTimeNodeClassIsLoaded(nodeClassName, ^{
766 
767  for (auto i : constantPortIdentifiersAndValues)
768  topLevelComposition->updateInternalPortConstantInSubcompositionInstances(subcompositionPath, i.first, i.second);
769  });
770  });
771 
772  compiler->revertOverriddenNodeClass(subcompositionPath);
773  });
774 
775  subcompositionRouter->removeComposition(this);
776 
777  composition->deleteLater();
778  delete ui;
779 
780 #ifdef __APPLE__
781  // Ensure a ghost of this window doesn't reappear after switching to another app and back.
782  // https://b33p.net/kosada/node/15322 comment #4
783  id nsView = (id)winId();
784  id nsWindow = objc_msgSend(nsView, sel_getUid("window"));
785  objc_msgSend(nsWindow, sel_getUid("close"));
786 #endif
787 
788  // Drain the documentation queue, to ensure any pending
789  // node library documentation requests have completed
790  // before QWidget deallocates the node library.
791  // https://b33p.net/kosada/node/15623
792  dispatch_sync(((VuoEditor *)qApp)->getDocumentationQueue(), ^{});
793 }
794 
799 void VuoEditorWindow::showUpdateHelpDialog()
800 {
801  if (composition->getBase()->getMetadata()->getLastSavedInVuoVersion().empty())
802  {
803  bool foundCommunityNode = false;
804  for (VuoNode *node : composition->getBase()->getNodes())
805  {
806  if (node->hasCompiler() && ! VuoStringUtilities::beginsWith(node->getNodeClass()->getClassName(), "vuo."))
807  {
808  foundCommunityNode = true;
809  break;
810  }
811  }
812 
813  if (foundCommunityNode)
814  {
815  QString summary = tr("This composition was created in an earlier version of Vuo. It might behave differently now.");
816  QString details = tr("<p><a href=\"%1\">How do I update my compositions from Vuo 1.x to Vuo 2.0?</a></p>")
817  .arg("https://vuo.org/node/2376");
818  QString checkboxLabel = tr("Show this window when opening compositions.");
819  QString settingsKey = "showUpdateHelp";
820  VuoInfoDialog *d = new VuoInfoDialog(this, summary, details, checkboxLabel, settingsKey);
821  d->show();
822  }
823  }
824 }
825 
830 {
831  updateSelectedComponentMenuItems();
832  updateToolbarElementUI();
833 
834  ui->paste->setEnabled(!VuoEditor::getClipboardText().isEmpty());
835  ui->compositionInformation->setEnabled(true);
836 
837  if (toolbar && toolbar->isStopInProgress())
838  {
839  ui->runComposition->setEnabled(!toolbar->isBuildPending());
840  ui->stopComposition->setEnabled(false);
841  ui->restartComposition->setEnabled(false);
842  }
843  else if (toolbar && toolbar->isBuildInProgress())
844  {
845  ui->runComposition->setEnabled(false);
846  ui->stopComposition->setEnabled(true);
847  ui->restartComposition->setEnabled(true);
848  }
849  else if (toolbar && toolbar->isRunning())
850  {
851  ui->runComposition->setEnabled(false);
852  ui->stopComposition->setEnabled(true);
853  ui->restartComposition->setEnabled(true);
854  }
855  else
856  {
857  ui->runComposition->setEnabled(true);
858  ui->stopComposition->setEnabled(false);
859  ui->restartComposition->setEnabled(false);
860  }
861 
862  ui->runComposition->setText(composition->getDriverForActiveProtocol()
863  ? tr("Run as") + " " + QString::fromStdString(composition->getActiveProtocol()->getName())
864  : tr("Run"));
865 
866  {
868  auto found = std::find_if(openWindows.begin(), openWindows.end(), [](VuoEditorWindow *w){ return w->metadataEditor->isVisible(); });
869  bool isCompositionInformationOpen = (found != openWindows.end());
870  quitAction->setEnabled(! isCompositionInformationOpen);
871  }
872 
873  QString savedPath = windowFilePath();
874  string savedDir, savedFile, savedExt;
875  VuoFileUtilities::splitPath(savedPath.toUtf8().constData(), savedDir, savedFile, savedExt);
876 
877  bool enableSaveMenuItem = (isWindowModified() || !VuoFileUtilities::fileExists(savedPath.toStdString()));
878  ui->saveComposition->setEnabled(enableSaveMenuItem);
879 
880  bool alreadyInstalledAsUserSubcomposition = (VuoFileUtilities::getUserModulesPath().c_str() == QFileInfo(savedDir.c_str()).canonicalFilePath());
881  bool hasErrors = true;
882  if (! alreadyInstalledAsUserSubcomposition)
883  {
884  try
885  {
886  VuoCompilerIssues issues;
887  composition->getBase()->getCompiler()->check(&issues);
888  hasErrors = false;
889  }
890  catch (VuoCompilerException &e) {}
891  }
892 
893  ui->saveComposition->setText(tr("Save"));
894  ui->saveCompositionAs->setText(tr("Save As…"));
895  ui->installSubcomposition->setEnabled(!alreadyInstalledAsUserSubcomposition && !hasErrors);
896  ui->installSubcomposition->setText(VuoFileUtilities::fileExists(windowFilePath().toStdString())?
897  tr("Move to User Library") :
898  tr("Save to User Library"));
899 
900  // Update the "Protocols" menus to reflect the currently active protocol(s).
901  updateProtocolsMenu(menuProtocols);
902  updateProtocolsMenu(inputPortSidebar->getProtocolsContextMenu());
903  updateProtocolsMenu(outputPortSidebar->getProtocolsContextMenu());
904 
905  if (nl)
906  {
907  if (nl->getHumanReadable())
908  ui->actionShowNodeNames->setChecked(true);
909  else
910  ui->actionShowNodeClassNames->setChecked(true);
911 
912  ui->toggleNodeLibraryDocking->setEnabled(!nl->isHidden());
913  ui->toggleNodeLibraryDocking->setText(nl->isFloating() ? tr("Attach to Editor Window") : tr("Detach from Editor Window"));
914  }
915 
916  if (inputPortSidebar && outputPortSidebar)
917  {
918  bool publishedPortSidebarsDisplayed = (! inputPortSidebar->isHidden());
919  ui->showPublishedPorts->setText(publishedPortSidebarsDisplayed ? tr("Hide Published Ports") : tr("Show Published Ports"));
920  ui->graphicsView->setVerticalScrollBarPolicy(publishedPortSidebarsDisplayed? Qt::ScrollBarAlwaysOff : Qt::ScrollBarAsNeeded);
921  }
922 
923  bool displaySearchTraversalOptions = (!searchBox->isHidden() && searchBox->traversalButtonsEnabled());
924  ui->findNext->setEnabled(displaySearchTraversalOptions);
925  ui->findPrevious->setEnabled(displaySearchTraversalOptions);
926 
927  bool cableDragInProgress = composition->getCableInProgress();
928  undoAction->setEnabled(!cableDragInProgress && undoStack->canUndo());
929  redoAction->setEnabled(!cableDragInProgress && undoStack->canRedo());
930 
931  bool showingHiddenCables = composition->getRenderHiddenCables();
932  if (!showingHiddenCables)
933  {
934  ui->showHiddenCables->setText(tr("Show Hidden Cables"));
935 
936  // In checking for hidden cables, include published cables, since selecting "Show Hidden Cables"
937  // will display the published port sidebars if there are any hidden published cables.
938  ui->showHiddenCables->setEnabled(composition->hasHiddenInternalCables() || composition->hasHiddenPublishedCables());
939  }
940  else
941  {
942  ui->showHiddenCables->setText(tr("Hide Hidden Cables"));
943 
944  // Always enable, to prevent the user from getting stuck in "Show Hidden Cables" mode and being
945  // unable to use context menu items to modify the hidden-status of cables in the future.
946  ui->showHiddenCables->setEnabled(true);
947  }
948 
949 #ifdef VUO_PRO
950  updateUI_Pro();
951 #else
952  ui->exportMovie->setEnabled(false);
953  ui->exportMacScreenSaver->setEnabled(false);
954  ui->exportMacFFGL->setEnabled(false);
955  ui->exportFxPlug->setEnabled(false);
956 #endif
957 
958  // Update this document's name in the "Window" menu.
959  raiseDocumentAction->setText(getWindowTitleWithoutPlaceholder());
960 
961  // Update the list of open documents in the "Windows" menu.
962  ui->menuWindow->clear();
963  static_cast<VuoEditor *>(qApp)->populateWindowMenu(ui->menuWindow, this);
964 
965  // In open editing windows for nodes contained in this composition,
966  // update UI actions that depend on whether this composition is running.
967  for (QMainWindow *window : VuoEditorUtilities::getOpenEditingWindows())
968  {
969  VuoCodeWindow *codeWindow = dynamic_cast<VuoCodeWindow *>(window);
970  if (codeWindow)
971  codeWindow->updateReloadAction();
972 
973  VuoEditorWindow *editorWindow = dynamic_cast<VuoEditorWindow *>(window);
974  if (editorWindow && editorWindow != this)
975  editorWindow->updateRefireAction();
976  }
977 
978  updateCursor();
979 }
980 
985 void VuoEditorWindow::updateToolbarElementUI()
986 {
987  if (toolbar)
988  toolbar->update(composition->getShowEventsMode(), ui->graphicsView->transform().isIdentity(), isZoomedToFit);
989 
990  ui->zoom11->setEnabled(! ui->graphicsView->transform().isIdentity());
991  ui->zoomToFit->setEnabled(! isZoomedToFit);
992  ui->showEvents->setText(composition->getShowEventsMode() ? tr("Hide Events") : tr("Show Events"));
993 }
994 
998 void VuoEditorWindow::updateRefireAction()
999 {
1000  __block bool isTopLevelCompositionRunning = false;
1001  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(composition, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
1002  {
1003  isTopLevelCompositionRunning = topLevelComposition->isRunning();
1004  });
1005 
1006  ui->refireEvent->setEnabled(isTopLevelCompositionRunning ? composition->getTriggerPortToRefire() : false);
1007 }
1008 
1012 void VuoEditorWindow::updateWindowForNewCableDrag()
1013 {
1014  if (composition->getCableInProgress() && composition->getCableInProgress()->isPublished())
1016 
1017  updateUI();
1018 }
1019 
1023 void VuoEditorWindow::updateCursor()
1024 {
1025  QCursor updatedCursor = (canvasDragInProgress? Qt::ClosedHandCursor :
1026  (canvasDragEnabled? Qt::OpenHandCursor :
1027  Qt::ArrowCursor));
1028 
1029  // Workaround to force a cursor update when the cursor believes it already has the correct updated
1030  // shape, but it actually does not. See https://b33p.net/kosada/node/7718#comment-27792 .
1031  setCursor(Qt::ArrowCursor);
1032 
1033  setCursor(updatedCursor);
1034 }
1035 
1044 {
1045  foreach (QWidget *widget, qApp->topLevelWidgets())
1046  {
1047  VuoEditorWindow *w = qobject_cast<VuoEditorWindow *>(widget);
1048  if (w && w->isVisible() && !w->containedPrepopulatedContent && (w->windowFilePath() == "") && (!w->isWindowModified()))
1049  return w;
1050  }
1051  return NULL;
1052 }
1053 
1058 void VuoEditorWindow::itemsMoved(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1059 {
1060  if ((!nodes.empty() || !comments.empty()) && !ignoreItemMoveSignals)
1061  {
1062  // Aggregate incremental moves of multiple items by mouse drag.
1063  if (movedByDragging && !duplicationMacroInProgress)
1064  {
1065  itemDragMacroInProgress = true;
1067 
1068  // Assumption: Notifications of item drags come item-by-item.
1069  QGraphicsItem *firstItem = (!nodes.empty()? static_cast<QGraphicsItem *>(*nodes.begin()) :
1070  static_cast<QGraphicsItem *>(*comments.begin()));
1071 
1072  if (std::find(itemsBeingDragged.begin(), itemsBeingDragged.end(), firstItem) == itemsBeingDragged.end())
1073  itemsBeingDragged.push_back(firstItem);
1074 
1075  if (firstItem == itemsBeingDragged[0])
1076  {
1077  itemDragDx += dx;
1078  itemDragDy += dy;
1079  }
1080  }
1081 
1082  if (!itemDragMacroInProgress)
1083  undoStack->push(new VuoCommandMove(nodes, comments, dx, dy, this, movedByDragging));
1084  }
1085 }
1086 
1091 void VuoEditorWindow::commentResized(VuoRendererComment *comment, qreal dx, qreal dy)
1092 {
1093  commentResizeMacroInProgress = true;
1094  commentBeingResized = comment;
1095  commentResizeDx += dx;
1096  commentResizeDy += dy;
1097 }
1098 
1103 void VuoEditorWindow::componentsAdded(QList<QGraphicsItem *> components, VuoEditorComposition *target)
1104 {
1105  if ((target == composition) && (! components.empty()))
1106  undoStack->push(new VuoCommandAdd(components, this, "Add"));
1107 }
1108 
1113 string VuoEditorWindow::getMaximumSubcompositionFromSelection(bool includePublishedPorts, bool includeHeader)
1114 {
1115  // Make sure that the drawers attached to any selected node are selected themselves.
1116  // Workaround for https://b33p.net/kosada/node/6064
1117  foreach (QGraphicsItem *item, composition->selectedItems())
1118  if (dynamic_cast<VuoRendererNode *>(item))
1119  dynamic_cast<VuoRendererNode *>(item)->layoutConnectedInputDrawers();
1120 
1121  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
1122  QList<QGraphicsItem *> selectedNonStrandedCompositionComponents;
1123 
1124  QStringList nodeDeclarations;
1125  vector<VuoRendererNode *> nodesToCopy;
1126  vector<QGraphicsItem *> potentialTypecastNodesToCopy;
1127  map<string, bool> nodeRepresented;
1128  vector<VuoRendererComment *> commentsToCopy;
1129 
1130  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1131  {
1132  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(*i);
1133  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(*i);
1134  bool strandedAttachment = (attachment && isStrandedAttachment(attachment, selectedCompositionComponents));
1135 
1136  if (rn && rn->getBase()->hasCompiler() && !strandedAttachment)
1137  {
1138  if (! nodeRepresented[rn->getBase()->getCompiler()->getGraphvizIdentifier()])
1139  {
1140  nodesToCopy.push_back(rn);
1141  nodeRepresented[rn->getBase()->getCompiler()->getGraphvizIdentifier()] = true;
1142  selectedNonStrandedCompositionComponents.append(rn);
1143  }
1144 
1145  // Check for collapsed typecasts within the set of selected components.
1146  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
1147  for (vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
1148  {
1149  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
1150  if (typecastPort)
1151  potentialTypecastNodesToCopy.push_back(typecastPort->getUncollapsedTypecastNode());
1152  }
1153  }
1154  else
1155  {
1156  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(*i);
1157  if (rcomment)
1158  commentsToCopy.push_back(rcomment);
1159  }
1160  }
1161 
1162  selectedNonStrandedCompositionComponents.append(QList<QGraphicsItem *>::fromVector(QVector<QGraphicsItem *>::fromStdVector(potentialTypecastNodesToCopy)));
1163  set<VuoRendererCable *> internalCableSet = composition->getCablesInternalToSubcomposition(selectedNonStrandedCompositionComponents);
1164 
1165  // Decide which of the original collapsed typecasts to copy.
1166  for (vector<QGraphicsItem *>::iterator node = potentialTypecastNodesToCopy.begin(); node != potentialTypecastNodesToCopy.end(); ++node)
1167  {
1168  bool inputCableCopied = false;
1169  bool childPortIsPublished = false;
1170  vector<VuoPort *> inputPorts = ((VuoRendererNode *)(*node))->getBase()->getInputPorts();
1171  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
1172  {
1173  vector<VuoCable *> inputCables = (*inputPort)->getConnectedCables(true);
1174  for (vector<VuoCable *>::iterator inputCable = inputCables.begin(); inputCable != inputCables.end(); ++inputCable)
1175  {
1176  if (std::find(internalCableSet.begin(), internalCableSet.end(), (*inputCable)->getRenderer()) != internalCableSet.end())
1177  inputCableCopied = true;
1178  if ((*inputCable)->isPublished())
1179  childPortIsPublished = true;
1180  }
1181  }
1182 
1183  // Include the collapsed typecast if the cable connected to its child input port was also included in the set of
1184  // selected components, or if the typecast child port was connected to a published input port.
1185  if (inputCableCopied || (childPortIsPublished && includePublishedPorts))
1186  {
1187  if (! nodeRepresented[((VuoRendererNode *)(*node))->getBase()->getCompiler()->getGraphvizIdentifier()])
1188  {
1189  nodesToCopy.push_back((VuoRendererNode *)(*node));
1190  nodeRepresented[((VuoRendererNode *)(*node))->getBase()->getCompiler()->getGraphvizIdentifier()] = true;
1191  }
1192  }
1193 
1194  // Otherwise, exclude the collapsed typecast and its connected cables.
1195  else
1196  {
1197  set<VuoCable *> connectedCables = ((VuoRendererNode *)(*node))->getConnectedCables(false);
1198  for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1199  internalCableSet.erase((*cable)->getRenderer());
1200  }
1201  }
1202 
1203  vector<VuoRendererCable *> cablesToCopy(internalCableSet.begin(), internalCableSet.end());
1204  QPointF viewportTopLeft = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect().topLeft();
1205 
1206  set<VuoNode *> baseNodesToCopy;
1207  for (vector<VuoRendererNode *>::iterator i = nodesToCopy.begin(); i != nodesToCopy.end(); ++i)
1208  baseNodesToCopy.insert( (*i)->getBase() );
1209  set<VuoCable *> baseCablesToCopy;
1210  for (vector<VuoRendererCable *>::iterator i = cablesToCopy.begin(); i != cablesToCopy.end(); ++i)
1211  baseCablesToCopy.insert( (*i)->getBase() );
1212  set<VuoComment *> baseCommentsToCopy;
1213  for (vector<VuoRendererComment *>::iterator i = commentsToCopy.begin(); i != commentsToCopy.end(); ++i)
1214  baseCommentsToCopy.insert( (*i)->getBase() );
1215 
1216  // Determine which of the composition's published ports are contained within the subcomposition.
1217  set<VuoPublishedPort *> subcompositionPublishedInputPortSet;
1218  set<VuoPublishedPort *> subcompositionPublishedOutputPortSet;
1219 
1220  if (includePublishedPorts)
1221  {
1222  foreach (VuoPublishedPort *publishedInputPort, composition->getBase()->getPublishedInputPorts())
1223  {
1224  foreach (VuoCable *cable, publishedInputPort->getConnectedCables())
1225  {
1226  if (baseNodesToCopy.find(cable->getToNode()) != baseNodesToCopy.end())
1227  {
1228  subcompositionPublishedInputPortSet.insert(publishedInputPort);
1229  baseCablesToCopy.insert(cable);
1230  }
1231  }
1232  }
1233 
1234  foreach (VuoPublishedPort *publishedOutputPort, composition->getBase()->getPublishedOutputPorts())
1235  {
1236  foreach (VuoCable *cable, publishedOutputPort->getConnectedCables())
1237  {
1238  if (baseNodesToCopy.find(cable->getFromNode()) != baseNodesToCopy.end())
1239  {
1240  subcompositionPublishedOutputPortSet.insert(publishedOutputPort);
1241  baseCablesToCopy.insert(cable);
1242  }
1243  }
1244  }
1245  }
1246 
1247  // Preserve the ordering of the published ports.
1248  vector<VuoPublishedPort *> subcompositionPublishedInputPorts;
1249  foreach (VuoPublishedPort *port, composition->getBase()->getProtocolAwarePublishedPortOrder(composition->getActiveProtocol(), true))
1250  if (subcompositionPublishedInputPortSet.find(port) != subcompositionPublishedInputPortSet.end())
1251  subcompositionPublishedInputPorts.push_back(port);
1252 
1253  vector<VuoPublishedPort *> subcompositionPublishedOutputPorts;
1254  foreach (VuoPublishedPort *port, composition->getBase()->getProtocolAwarePublishedPortOrder(composition->getActiveProtocol(), false))
1255  if (subcompositionPublishedOutputPortSet.find(port) != subcompositionPublishedOutputPortSet.end())
1256  subcompositionPublishedOutputPorts.push_back(port);
1257 
1258  string outputCompositionText = composition->getBase()->getCompiler()->getGraphvizDeclarationForComponents(baseNodesToCopy,
1259  baseCablesToCopy,
1260  baseCommentsToCopy,
1261  subcompositionPublishedInputPorts,
1262  subcompositionPublishedOutputPorts,
1263  (includeHeader? composition->generateCompositionHeader() : ""),
1264  "",
1265  -viewportTopLeft.x(),
1266  -viewportTopLeft.y());
1267 
1268  return outputCompositionText;
1269 }
1270 
1276 bool VuoEditorWindow::isStrandedAttachment(VuoRendererInputAttachment *attachment, QList<QGraphicsItem *> selectedItems)
1277 {
1278  VuoNode *renderedHostNode = attachment->getRenderedHostNode();
1279  if (!(renderedHostNode && renderedHostNode->hasRenderer() && selectedItems.contains(renderedHostNode->getRenderer())))
1280  return true;
1281 
1282  set<VuoNode *> coattachments = attachment->getCoattachments();
1283  foreach (VuoNode *coattachment, coattachments)
1284  {
1285  if (!(coattachment->hasRenderer() && selectedItems.contains(coattachment->getRenderer())))
1286  return true;
1287  }
1288 
1289  return false;
1290 }
1291 
1299 {
1300  QClipboard *clipboard = QApplication::clipboard();
1301  QMimeData *mimeData = new QMimeData();
1302 
1303  if (nl && !nl->getSelectedDocumentationText().isEmpty())
1304  mimeData->setText(nl->getSelectedDocumentationText());
1305 
1306  else
1307  {
1308  mimeData->setText(getMaximumSubcompositionFromSelection(false, true).c_str());
1309  cursorPosAtLastComponentCopy = getCursorScenePos();
1310  }
1311 
1312  clipboard->setMimeData(mimeData);
1313 }
1314 
1321 {
1322  QString clipboardText = VuoEditor::getClipboardText();
1323 
1324  if (containsLikelyVuoComposition(clipboardText))
1325  pasteCompositionComponents();
1326  else if (!clipboardText.isEmpty())
1327  {
1329  nl->searchForText(clipboardText);
1330  }
1331 }
1332 
1336 void VuoEditorWindow::pasteCompositionComponents()
1337 {
1338  QClipboard *clipboard = QApplication::clipboard();
1339  const QMimeData *mimeData = clipboard->mimeData();
1340 
1341  int publishedPortsBeforePaste = composition->getBase()->getPublishedInputPorts().size() +
1342  composition->getBase()->getPublishedOutputPorts().size();
1343 
1344  int publishedCablesBeforePaste = 0;
1345  foreach (VuoPublishedPort *publishedInput, composition->getBase()->getPublishedInputPorts())
1346  publishedCablesBeforePaste += publishedInput->getConnectedCables(true).size();
1347  foreach (VuoPublishedPort *publishedOutput, composition->getBase()->getPublishedOutputPorts())
1348  publishedCablesBeforePaste += publishedOutput->getConnectedCables(true).size();
1349 
1350  if (mimeData->hasFormat("text/plain"))
1351  {
1352  // Paste at the cursor location only if the cursor is currently within the viewport bounds
1353  // and has moved since the most recent copy operation.
1354  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
1355  QPointF cursorPos = getCursorScenePos();
1356  bool pasteAtCursorLoc = ((cursorPos != cursorPosAtLastComponentCopy) && viewportRect.contains(cursorPos));
1357 
1358  QString compositionText = mimeData->text();
1359  mergeCompositionComponentsFromString(compositionText.toUtf8().constData(), pasteAtCursorLoc, !pasteAtCursorLoc, "Paste");
1360  }
1361 
1362  int publishedPortsAfterPaste = composition->getBase()->getPublishedInputPorts().size() +
1363  composition->getBase()->getPublishedOutputPorts().size();
1364 
1365  int publishedCablesAfterPaste = 0;
1366  foreach (VuoPublishedPort *publishedInput, composition->getBase()->getPublishedInputPorts())
1367  publishedCablesAfterPaste += publishedInput->getConnectedCables(true).size();
1368  foreach (VuoPublishedPort *publishedOutput, composition->getBase()->getPublishedOutputPorts())
1369  publishedCablesAfterPaste += publishedOutput->getConnectedCables(true).size();
1370 
1371  // Display the published port sidebars if the composition has any new published ports or cables.
1372  if ((publishedPortsAfterPaste > publishedPortsBeforePaste) || (publishedCablesAfterPaste > publishedCablesBeforePaste))
1373  setPublishedPortSidebarVisibility(true);
1374 }
1375 
1383 {
1384  // Compose a macro to merge the duplication and subsequent mouse drag operation of duplicated components.
1385  if (! duplicationMacroInProgress)
1386  {
1387  undoStack->beginMacro(tr("Duplication"));
1388  duplicationMacroInProgress = true;
1389  }
1390 
1391  string subcompositionText = getMaximumSubcompositionFromSelection(false, true);
1392  QList<QGraphicsItem *> newComponents = mergeCompositionComponentsFromString(subcompositionText, false, false, "Duplication");
1393 
1394  // Clear the previous selection and select the duplicated components.
1395  composition->clearSelection();
1396  for (QList<QGraphicsItem *>::iterator i = newComponents.begin(); i != newComponents.end(); ++i)
1397  (*i)->setSelected(true);
1398 }
1399 
1405 {
1406  foreach (VuoNode *node, composition->getBase()->getNodes())
1407  {
1408  VuoNodeClass *currentNodeClass = node->getNodeClass();
1409  string genericNodeClassName;
1410  if (currentNodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(currentNodeClass->getCompiler()))
1411  genericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(currentNodeClass->getCompiler())->getOriginalGenericNodeClassName();
1412  else
1413  genericNodeClassName = currentNodeClass->getClassName();
1414 
1415  if ((genericNodeClassName == nodeClass) && node->hasRenderer())
1416  node->getRenderer()->setSelected(true);
1417  }
1418 
1419  // Try to keep selected components visible.
1420  QRectF selectedItemsRect;
1421  foreach (QGraphicsItem *selectedComponent, composition->selectedItems())
1422  selectedItemsRect |= selectedComponent->sceneBoundingRect();
1423 
1424  if (!selectedItemsRect.isNull())
1425  ui->graphicsView->centerOn(selectedItemsRect.center());
1426 }
1427 
1435 void VuoEditorWindow::duplicateSelectedCompositionComponentsByMenuItem()
1436 {
1437  string subcompositionText = getMaximumSubcompositionFromSelection(false, true);
1438  QList<QGraphicsItem *> newComponents = mergeCompositionComponentsFromString(subcompositionText, false, true, "Duplication");
1439 
1440  // Clear the previous selection and select the duplicated components.
1441  composition->clearSelection();
1442  for (QList<QGraphicsItem *>::iterator i = newComponents.begin(); i != newComponents.end(); ++i)
1443  (*i)->setSelected(true);
1444 }
1445 
1450 {
1451  // End composition of duplication macro.
1452  resetUndoStackMacros();
1453 
1454  // Undo the composite duplication operation already on the 'Undo' stack.
1455  undoAction->trigger();
1456 }
1457 
1462 {
1463  if (snapshot != composition->takeSnapshot())
1464  {
1465  composition->modifyComponents(^{
1466  composition->clear();
1467  instantiateNewCompositionComponentsFromString(snapshot);
1468  });
1469  updateUI();
1470  }
1471 }
1472 
1477 void VuoEditorWindow::instantiateNewCompositionComponentsFromString(string compositionText)
1478 {
1480 
1481  foreach (VuoNode *node, graphParser->getNodes())
1482  composition->addNode(node);
1483 
1484  foreach (VuoPublishedPort *publishedInputPort, graphParser->getPublishedInputPorts())
1485  {
1486  composition->addPublishedPort(publishedInputPort, true);
1487  composition->createRendererForPublishedPortInComposition(publishedInputPort, true);
1488  }
1489 
1490  foreach (VuoPublishedPort *publishedOutputPort, graphParser->getPublishedOutputPorts())
1491  {
1492  composition->addPublishedPort(publishedOutputPort, false);
1493  composition->createRendererForPublishedPortInComposition(publishedOutputPort, false);
1494  }
1495 
1496  foreach (VuoCable *cable, graphParser->getCables())
1497  {
1498  composition->addCable(cable, false);
1499 
1500  if (cable->isPublishedInputCable())
1501  cable->setFrom(composition->getPublishedInputNode(), cable->getFromPort());
1502  if (cable->isPublishedOutputCable())
1503  cable->setTo(composition->getPublishedOutputNode(), cable->getToPort());
1504  }
1505 
1506  foreach (VuoComment *comment, graphParser->getComments())
1507  composition->addComment(comment);
1508 
1509  // Collapse any typecasts possible.
1510  composition->collapseTypecastNodes();
1511 
1512  // Now that all renderer components have been created, calculate
1513  // the final positions of collapsed "Make List" drawers.
1514  foreach (VuoNode *node, graphParser->getNodes())
1516 
1517  // @todo https://b33p.net/kosada/node/10638
1518  //delete graphParser;
1519 
1520  updateUI();
1521 }
1522 
1529 QList<QGraphicsItem *> VuoEditorWindow::mergeCompositionComponentsFromString(string compositionText, bool pasteAtCursorLoc, bool pasteWithOffset, string commandDescription)
1530 {
1531  VuoCompilerComposition *pastedComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionText, compiler);
1532  QList<QGraphicsItem *> pastedComponents = QList<QGraphicsItem *>();
1533 
1534  QPointF startPos = (pasteWithOffset? QPointF(pastedComponentOffset, pastedComponentOffset) : QPointF(0,0));
1535 
1536  // Replace nodes that can't be legally added to this composition with implementation-less nodes.
1537  for (VuoNode *pastedNode : pastedComposition->getBase()->getNodes())
1538  {
1539  if (pastedNode->getNodeClass()->hasCompiler())
1540  {
1541  VuoNode *allowedNode = composition->createBaseNode(pastedNode->getNodeClass()->getCompiler(), pastedNode);
1542  if (! allowedNode->getNodeClass()->hasCompiler())
1543  {
1544  pastedComposition->replaceNode(pastedNode, allowedNode);
1545  allowedNode->setRawGraphvizDeclaration(pastedNode->getRawGraphvizDeclaration());
1546  delete pastedNode;
1547  }
1548  }
1549  }
1550 
1551  set<VuoNode *> pastedNodes = pastedComposition->getBase()->getNodes();
1552  for (VuoNode *node : pastedNodes)
1553  {
1554  VuoRendererNode *rn = ((VuoRendererComposition *)composition)->createRendererNode(node);
1555  pastedComponents.append(rn);
1556  }
1557 
1558  set<VuoComment *> pastedComments = pastedComposition->getBase()->getComments();
1559  for (VuoComment *comment : pastedComments)
1560  {
1561  VuoRendererComment *rc = ((VuoRendererComposition *)composition)->createRendererComment(comment);
1562  pastedComponents.append(rc);
1563  }
1564 
1565  qreal minX = std::numeric_limits<qreal>::max();
1566  qreal minY = std::numeric_limits<qreal>::max();
1567 
1568  if (pasteAtCursorLoc)
1569  {
1570  // Find the top-left of the pasted component cluster, to position at the cursor location.
1571  for (VuoNode *node : pastedNodes)
1572  {
1573  // Disregard the positions of nodes that will be rendered as attachments.
1574  if (dynamic_cast<VuoRendererInputAttachment *>(node->getRenderer()))
1575  continue;
1576 
1577  if (node->getX() < minX)
1578  minX = node->getX();
1579 
1580  if (node->getY() < minY)
1581  minY = node->getY();
1582  }
1583 
1584  for (VuoComment *comment : pastedComments)
1585  {
1586  if (comment->getX() < minX)
1587  minX = comment->getX();
1588 
1589  if (comment->getY() < minY)
1590  minY = comment->getY();
1591  }
1592 
1593  startPos += (getFittedScenePos(getCursorScenePos()) - QPointF(minX, minY));
1594  }
1595  else
1596  {
1597  QPointF viewportTopLeft = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect().topLeft();
1598  startPos += viewportTopLeft;
1599  }
1600 
1601  // Set new node and comment positions.
1602  this->ignoreItemMoveSignals = true;
1603  for (VuoNode *node : pastedNodes)
1604  {
1605  node->setX(startPos.x() + node->getX());
1606  node->setY(startPos.y() + node->getY());
1607 
1608  node->getRenderer()->setPos(node->getX(), node->getY());
1609  }
1610 
1611  for (VuoComment *comment : pastedComments)
1612  {
1613  comment->setX(startPos.x() + comment->getX());
1614  comment->setY(startPos.y() + comment->getY());
1615 
1616  comment->getRenderer()->setPos(comment->getX(), comment->getY());
1617  }
1618  this->ignoreItemMoveSignals = false;
1619 
1620  set<VuoCable *> pastedCables = pastedComposition->getBase()->getCables();
1621  for (VuoCable *cable : pastedCables)
1622  {
1623  if (! cable->isPublished())
1624  {
1625  VuoRendererCable *rc = new VuoRendererCable(cable);
1626  pastedComponents.append(rc);
1627  }
1628  }
1629 
1630  // Remove the pre-existing published cable connections from the pasted components.
1631  // We will make use of our own customized published port merging algorithm and will
1632  // create and connect our own published cables.
1633  // But first, document some information about the connections.
1634  map<VuoPublishedPort *, vector<pair<VuoPort *, bool> > > connectionsForPublishedPort;
1635  map<VuoPublishedPort *, bool> publishedPortHadConnectedDataCable;
1636 
1637  foreach (VuoPublishedPort *publishedInput, pastedComposition->getBase()->getPublishedInputPorts())
1638  {
1639  bool foundConnectedDataCable = false;
1640  vector<VuoCable *> publishedInputCables = publishedInput->getConnectedCables(true);
1641  foreach (VuoCable *cable, publishedInputCables)
1642  {
1643  VuoPort *toPort = cable->getToPort();
1644  VuoCompilerPort *compilerToPort = static_cast<VuoCompilerPort *>(toPort->getCompiler());
1645  bool alwaysEventOnly = cable->getCompiler()->getAlwaysEventOnly();
1646 
1647  bool cableCarriesData = (compilerToPort && compilerToPort->getDataVuoType() && !alwaysEventOnly);
1648  if (cableCarriesData)
1649  foundConnectedDataCable = true;
1650 
1651  connectionsForPublishedPort[publishedInput].push_back(make_pair(toPort, alwaysEventOnly));
1652 
1653  cable->setFrom(NULL, NULL);
1654  cable->setTo(NULL, NULL);
1655  }
1656 
1657  publishedPortHadConnectedDataCable[publishedInput] = foundConnectedDataCable;
1658  }
1659 
1660  foreach (VuoPublishedPort *publishedOutput, pastedComposition->getBase()->getPublishedOutputPorts())
1661  {
1662  bool foundConnectedDataCable = false;
1663  vector<VuoCable *> publishedOutputCables = publishedOutput->getConnectedCables(true);
1664  foreach (VuoCable *cable, publishedOutputCables)
1665  {
1666  VuoPort *fromPort = cable->getFromPort();
1667  VuoCompilerPort *compilerFromPort = static_cast<VuoCompilerPort *>(fromPort->getCompiler());
1668  bool alwaysEventOnly = cable->getCompiler()->getAlwaysEventOnly();
1669 
1670  bool cableCarriesData = (compilerFromPort && compilerFromPort->getDataVuoType() && !alwaysEventOnly);
1671  if (cableCarriesData)
1672  foundConnectedDataCable = true;
1673 
1674  connectionsForPublishedPort[publishedOutput].push_back(make_pair(fromPort, alwaysEventOnly));
1675 
1676  cable->setFrom(NULL, NULL);
1677  cable->setTo(NULL, NULL);
1678  }
1679 
1680  publishedPortHadConnectedDataCable[publishedOutput] = foundConnectedDataCable;
1681  }
1682 
1683  // Begin an 'Undo' macro to aggregate the instantiation of new composition components with the
1684  // publication of relevant ports.
1685  undoStack->beginMacro(tr(commandDescription.c_str()));
1686 
1687  // Add pasted components to composition.
1688  componentsPasted(pastedComponents, commandDescription);
1689 
1690  // Publish any input ports that were published in their source environment.
1691  vector<VuoPublishedPort *> unmergedPublishedInputPorts;
1692 
1693  // First pass: Check for any pasted published input ports that have identically named ports in the
1694  // target environment that can accommodate merging.
1695  foreach (VuoPublishedPort *publishedInputPort, pastedComposition->getBase()->getPublishedInputPorts())
1696  {
1697  string pastedPublishedPortName = publishedInputPort->getClass()->getName();
1698  VuoPublishedPort *existingPortWithSameName = composition->getBase()->getPublishedInputPortWithName(pastedPublishedPortName);
1699 
1700  if (existingPortWithSameName && dynamic_cast<VuoRendererPublishedPort *>(existingPortWithSameName->getRenderer())->canBeMergedWith(publishedInputPort,
1701  publishedPortHadConnectedDataCable[publishedInputPort]))
1702  {
1703  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[publishedInputPort];
1704  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1705  {
1706  VuoPort *connectedPort = i->first;
1707  bool forceEventOnlyPublication = i->second;
1708 
1709  internalPortPublished(connectedPort, forceEventOnlyPublication, pastedPublishedPortName, true);
1710  }
1711  }
1712  else
1713  unmergedPublishedInputPorts.push_back(publishedInputPort);
1714  }
1715 
1716  // Second pass: Re-name and publish the remaining pasted published input ports.
1717  foreach (VuoPublishedPort *unmergedPublishedInputPort, unmergedPublishedInputPorts)
1718  {
1719  string uniquePublishedPortName = composition->getUniquePublishedPortName(unmergedPublishedInputPort->getClass()->getName());
1720 
1721  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[unmergedPublishedInputPort];
1722  if (!internalConnections.empty())
1723  {
1724  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1725  {
1726  VuoPort *connectedPort = i->first;
1727  bool forceEventOnlyPublication = i->second;
1728 
1729  internalPortPublished(connectedPort, forceEventOnlyPublication, uniquePublishedPortName, true);
1730  }
1731  }
1732 
1733  else
1734  {
1735  unmergedPublishedInputPort->getClass()->setName(uniquePublishedPortName);
1736  composition->addPublishedPort(unmergedPublishedInputPort, true);
1737  composition->createRendererForPublishedPortInComposition(unmergedPublishedInputPort, true);
1738  }
1739  }
1740 
1741  // Publish any output ports that were published in their source environment.
1742  vector<VuoPublishedPort *> unmergedPublishedOutputPorts;
1743 
1744  // First pass: Check for any pasted published output ports that have identically named ports in the
1745  // target environment that can accommodate merging.
1746  foreach (VuoPublishedPort *publishedOutputPort, pastedComposition->getBase()->getPublishedOutputPorts())
1747  {
1748  string pastedPublishedPortName = publishedOutputPort->getClass()->getName();
1749  VuoPublishedPort *existingPortWithSameName = composition->getBase()->getPublishedOutputPortWithName(pastedPublishedPortName);
1750 
1751  if (existingPortWithSameName && dynamic_cast<VuoRendererPublishedPort *>(existingPortWithSameName->getRenderer())->canBeMergedWith(publishedOutputPort,
1752  publishedPortHadConnectedDataCable[publishedOutputPort]))
1753  {
1754  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[publishedOutputPort];
1755  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1756  {
1757  VuoPort *connectedPort = i->first;
1758  bool forceEventOnlyPublication = i->second;
1759 
1760  internalPortPublished(connectedPort, forceEventOnlyPublication, pastedPublishedPortName, true);
1761  }
1762  }
1763  else
1764  unmergedPublishedOutputPorts.push_back(publishedOutputPort);
1765  }
1766 
1767  // Second pass: Re-name and publish the remaining pasted published output ports.
1768  foreach (VuoPublishedPort *unmergedPublishedOutputPort, unmergedPublishedOutputPorts)
1769  {
1770  string uniquePublishedPortName = composition->getUniquePublishedPortName(unmergedPublishedOutputPort->getClass()->getName());
1771 
1772  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[unmergedPublishedOutputPort];
1773  if (!internalConnections.empty())
1774  {
1775  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1776  {
1777  VuoPort *connectedPort = i->first;
1778  bool forceEventOnlyPublication = i->second;
1779 
1780  internalPortPublished(connectedPort, forceEventOnlyPublication, uniquePublishedPortName, true);
1781  }
1782  }
1783 
1784  else
1785  {
1786  unmergedPublishedOutputPort->getClass()->setName(uniquePublishedPortName);
1787  composition->addPublishedPort(unmergedPublishedOutputPort, false);
1788  composition->createRendererForPublishedPortInComposition(unmergedPublishedOutputPort, false);
1789  }
1790  }
1791 
1792  delete pastedComposition;
1793 
1794  undoStack->endMacro();
1795 
1796  return pastedComponents;
1797 }
1798 
1803 void VuoEditorWindow::componentsPasted(QList<QGraphicsItem *> components, string commandDescription)
1804 {
1805  if (components.empty())
1806  return;
1807 
1808  // Add the components to the scene.
1809  undoStack->push(new VuoCommandAdd(components, this, commandDescription));
1810 
1811  // Clear the previous selection and select the pasted components.
1812  composition->clearSelection();
1813  for (QList<QGraphicsItem *>::iterator i = components.begin(); i != components.end(); ++i)
1814  (*i)->setSelected(true);
1815 }
1816 
1822 {
1824  composition->deleteSelectedCompositionComponents("Cut");
1825 }
1826 
1847  VuoRendererPort *targetPort,
1848  pair<VuoRendererCable *, VuoRendererCable *> cableArgs,
1849  VuoRendererNode *typecastNodeToDelete,
1850  pair<string, string> typeArgs,
1851  pair<VuoRendererPort *, VuoRendererPort *> portArgs)
1852 {
1854 
1855  // Unpack arguments that were packed into pairs to avoid Qt's parameter count limit for signals/slots.
1856  VuoRendererCable *dataCableToDisplace = cableArgs.first;
1857  VuoRendererCable *cableToReplace = cableArgs.second;
1858  string typecastToInsert = typeArgs.first;
1859  string specializedTypeName = typeArgs.second;
1860  VuoRendererPort *portToUnpublish = portArgs.first;
1861  VuoRendererPort *portToSpecialize = portArgs.second;
1862 
1863  // Reconstruct the state of the composition before the beginning of the cable drag
1864  // that concluded with this connection, for the composition's initial "Before" snapshot
1865  // in the following sequence of operations.
1866  if (!cableInProgress->getBase()->getToPort() &&
1867  !composition->getCableInProgressWasNew() &&
1868  cableInProgress->getFloatingEndpointPreviousToPort())
1869  {
1870  bool cableInProgressAlwaysEventOnly = cableInProgress->getBase()->getCompiler()->getAlwaysEventOnly();
1871  cableInProgress->setTo(composition->getUnderlyingParentNodeForPort(cableInProgress->getFloatingEndpointPreviousToPort(), composition),
1872  cableInProgress->getFloatingEndpointPreviousToPort());
1873  cableInProgress->getBase()->getCompiler()->setAlwaysEventOnly(cableInProgress->getPreviouslyAlwaysEventOnly());
1874 
1875  // Execute an identity Undo stack command simply to record the composition's screenshot
1876  // before the cable drag began.
1877  set<VuoRendererNode *> emptyNodeSet;
1878  set<VuoRendererComment *> emptyCommentSet;
1879  undoStack->push(new VuoCommandMove(emptyNodeSet, emptyCommentSet, 0, 0, this, false));
1880 
1881  // Now reconstruct the state of the composition mid-cable drag and carry on.
1882  cableInProgress->getBase()->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
1883  cableInProgress->setTo(NULL, NULL);
1884  }
1885 
1886  // Create the requested typecast node for eventual insertion.
1887  QList<QGraphicsItem *> typecastRelatedComponentsToAdd = QList<QGraphicsItem *>();
1888  VuoRendererNode *typecastNodeToAdd = NULL;
1889  VuoPort *typecastInPort = NULL;
1890  VuoPort *typecastOutPort = NULL;
1891  if (!typecastToInsert.empty())
1892  {
1893  typecastNodeToAdd = composition->createNode(typecastToInsert.c_str(), "",
1894  targetPort->scenePos().x()+100,
1895  targetPort->scenePos().y()+100);
1896 
1897  typecastRelatedComponentsToAdd.append(typecastNodeToAdd);
1898 
1899  typecastInPort = typecastNodeToAdd->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1900  typecastOutPort = typecastNodeToAdd->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
1901 
1902  // Apply NULL checks liberally to avoid currently undiagnosed getRenderer()-related crash. https://b33p.net/kosada/node/9053
1903  if (!(typecastInPort && typecastInPort->hasRenderer() && typecastOutPort && typecastOutPort->hasRenderer()))
1904  return;
1905  }
1906 
1907  VuoRendererPort *unadjustedFromPort = NULL;
1908  VuoRendererPort *unadjustedToPort = NULL;
1909 
1910  // Apply NULL checks liberally to avoid currently undiagnosed getRenderer()-related crash. https://b33p.net/kosada/node/9053
1911  if (targetPort->getInput() && !(cableInProgress->getBase()->getFromPort() && cableInProgress->getBase()->getFromPort()->hasRenderer()))
1912  return;
1913  if (!targetPort->getInput() && !(cableInProgress->getBase()->getToPort() && cableInProgress->getBase()->getToPort()->hasRenderer()))
1914  return;
1915 
1916  // Specialize the port and its network, if applicable.
1917  if (portToSpecialize)
1918  {
1919  bool currentlyGeneric = dynamic_cast<VuoGenericType *>(portToSpecialize->getDataType());
1920 
1921  VuoRendererNode *specializedNode = NULL;
1922  if (currentlyGeneric)
1923  specializedNode = specializePortNetwork(portToSpecialize, specializedTypeName, false);
1924  else
1925  specializedNode = respecializePortNetwork(portToSpecialize, specializedTypeName, false);
1926 
1927  if (!specializedNode)
1928  return;
1929 
1930  if (portToSpecialize == targetPort)
1931  targetPort = (targetPort->getInput()? specializedNode->getBase()->getInputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer() :
1932  specializedNode->getBase()->getOutputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer());
1933  }
1934 
1935  if (targetPort->getInput())
1936  {
1937  unadjustedFromPort = cableInProgress->getBase()->getFromPort()->getRenderer();
1938  unadjustedToPort = targetPort;
1939  }
1940  else
1941  {
1942  unadjustedFromPort = targetPort;
1943  unadjustedToPort = cableInProgress->getBase()->getToPort()->getRenderer();
1944  }
1945 
1946  // @todo: Possibly re-use logic from VuoCompilerCable::carriesData() even though we don't yet have a connected cable.
1947  // Should really only matter if we implement published cable connection by dragging before we implement external published port types
1948  // that are independent of their internal connected port types, since then we must take into account that the cable may carry data even
1949  // if the (published) 'From' port technically does not.
1950  bool cableWillCarryData = (cableInProgress->effectivelyCarriesData() &&
1951  (dynamic_cast<VuoRendererPublishedPort *>(unadjustedFromPort)?
1952  static_cast<VuoCompilerPortClass *>(unadjustedFromPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
1953  unadjustedFromPort->getDataType()) &&
1954  (dynamic_cast<VuoRendererPublishedPort *>(unadjustedToPort)?
1955  static_cast<VuoCompilerPortClass *>(unadjustedToPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
1956  unadjustedToPort->getDataType())
1957  );
1958 
1959  VuoPort *previousToPort = cableInProgress->getFloatingEndpointPreviousToPort();
1960 
1961  // If connecting a data+event cable, and the intended input port had a previously connected
1962  // collapsed "Make List" node, delete it.
1963  VuoRendererInputDrawer *attachedDrawer = unadjustedToPort->getAttachedInputDrawer();
1964  if (attachedDrawer && cableWillCarryData)
1965  {
1966  QList<QGraphicsItem *> drawerRelatedComponentsToRemove;
1967  drawerRelatedComponentsToRemove.append(attachedDrawer);
1968  bool disableAutomaticAttachmentInsertion = true;
1969  undoStack->push(new VuoCommandRemove(drawerRelatedComponentsToRemove, this, inputEditorManager, "", disableAutomaticAttachmentInsertion));
1970  }
1971 
1972  VuoRendererPort *adjustedTargetPort = targetPort;
1973 
1974  if (typecastNodeToDelete)
1975  {
1976  QList<QGraphicsItem *> typecastRelatedComponentsToRemove = QList<QGraphicsItem *>();
1977  typecastRelatedComponentsToRemove.append(typecastNodeToDelete);
1978  typecastRelatedComponentsToRemove.append(dataCableToDisplace);
1979 
1980  undoStack->push(new VuoCommandRemove(typecastRelatedComponentsToRemove, this, inputEditorManager, "", true));
1981 
1982  dataCableToDisplace = NULL;
1983  }
1984 
1985  // Replace a pre-existing cable that connected the same two ports (but that had a different data-carrying status).
1986  if (cableToReplace)
1987  {
1988  QList<QGraphicsItem *> componentsToReplace = QList<QGraphicsItem *>();
1989  componentsToReplace.append(cableToReplace);
1990  undoStack->push(new VuoCommandRemove(componentsToReplace, this, inputEditorManager, "", false));
1991  }
1992 
1993  if (! typecastToInsert.empty())
1994  {
1995  // Insert an extra cable leading to or from the typecast as appropriate.
1996  VuoRendererCable *newCableConnectingTypecast;
1997  VuoRendererCable *incomingTypecastCable;
1998 
1999  // Case: making a "forward" cable connection (from an output port to an input port)
2000  if (targetPort->getInput())
2001  {
2002  // Prepare to add a cable connecting the output port of the newly inserted
2003  // typecast to the originally intended input port.
2004  VuoCompilerNode *fromNode = typecastNodeToAdd->getBase()->getCompiler();
2005  VuoCompilerPort *fromPort = (VuoCompilerPort *)typecastOutPort->getCompiler();
2006  VuoCompilerNode *toNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2007  VuoCompilerPort *toPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2008 
2009  // Prepare to re-route the dragged cable's loose end to connect to the typecast's input port.
2010  adjustedTargetPort = typecastInPort->getRenderer();
2011 
2012  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2013  incomingTypecastCable = cableInProgress;
2014  }
2015 
2016  // Case: making a "backward" cable connection (from an input port to an output port)
2017  else
2018  {
2019  // Prepare to add a cable connecting the originally intended output port
2020  // to the input port of the newly inserted typecast.
2021  VuoCompilerNode *fromNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2022  VuoCompilerPort *fromPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2023  VuoCompilerNode *toNode = typecastNodeToAdd->getBase()->getCompiler();
2024  VuoCompilerPort *toPort = (VuoCompilerPort *)typecastInPort->getCompiler();
2025 
2026  // Prepare to re-route the dragged cable's loose end to connect to the typecast's output port.
2027  adjustedTargetPort = typecastOutPort->getRenderer();
2028 
2029  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2030  incomingTypecastCable = newCableConnectingTypecast;
2031  }
2032 
2033  typecastRelatedComponentsToAdd.append(newCableConnectingTypecast);
2034 
2035  bool disableAutomaticAttachmentInsertion = incomingTypecastCable->effectivelyCarriesData();
2036  undoStack->push(new VuoCommandAdd(typecastRelatedComponentsToAdd, this, "", disableAutomaticAttachmentInsertion));
2037  }
2038 
2039  undoStack->push(new VuoCommandConnect(cableInProgress, adjustedTargetPort, dataCableToDisplace, portToUnpublish, this, inputEditorManager));
2040 
2041  // If re-connecting a cable whose previous 'To' port had a collapsed typecast attached, decide what
2042  // to do with the typecast.
2043  if (targetPort->getInput() && previousToPort && (previousToPort != targetPort->getBase()))
2044  {
2045  VuoRendererTypecastPort *previousTypecastToPort = (VuoRendererTypecastPort *)(previousToPort->getRenderer()->getTypecastParentPort());
2046  if (previousTypecastToPort)
2047  {
2048  // If the typecast does not have any remaining incoming cables, delete it.
2049  VuoRendererPort *childPort = previousTypecastToPort->getChildPort();
2050  if (childPort->getBase()->getConnectedCables(true).size() < 1)
2051  {
2052  VuoRendererNode *uncollapsedTypecastToPort = composition->uncollapseTypecastNode(previousTypecastToPort);
2053  QList<QGraphicsItem *> typecastRelatedComponentsToRemove = QList<QGraphicsItem *>();
2054  typecastRelatedComponentsToRemove.append(uncollapsedTypecastToPort);
2055  undoStack->push(new VuoCommandRemove(typecastRelatedComponentsToRemove, this, inputEditorManager, ""));
2056  }
2057 
2058  // If the typecast does have remaining incoming cables but none of them carry data, uncollapse it.
2059  // @todo For consistency with cable deletion, we should really reroute the event-only cables
2060  // to the typecast host port.
2061  else if (!childPort->effectivelyHasConnectedDataCable(true))
2062  composition->uncollapseTypecastNode(previousTypecastToPort);
2063 
2065  }
2066  }
2067 
2068  // If the target port is generic and, as a result of this connection, has only a single
2069  // compatible specialized type, specialize its network.
2070  VuoGenericType *genericType = dynamic_cast<VuoGenericType *>((static_cast<VuoCompilerPort *>(targetPort->getBase()->getCompiler()))->getDataVuoType());
2071  if (genericType)
2072  {
2073  VuoGenericType::Compatibility compatibility;
2074  vector<string> compatibleSpecializedTypes = genericType->getCompatibleSpecializedTypes(compatibility);
2075  if (compatibleSpecializedTypes.size() == 1)
2076  {
2077  string loneCompatibleSpecializedType = *compatibleSpecializedTypes.begin();
2078  specializePortNetwork(targetPort, loneCompatibleSpecializedType, false);
2079  }
2080  }
2081 }
2082 
2087 void VuoEditorWindow::componentsRemoved(QList<QGraphicsItem *> components, string commandDescription)
2088 {
2089  if (! components.empty())
2090  undoStack->push(new VuoCommandRemove(components, this, inputEditorManager, commandDescription));
2091 }
2092 
2097 {
2098  if (triggerPort->getEventThrottling() != eventThrottling)
2099  undoStack->push(new VuoCommandSetTriggerThrottling(triggerPort, eventThrottling, this));
2100 }
2101 
2106 void VuoEditorWindow::adjustInputPortCountForNode(VuoRendererNode *node, int inputPortCountDelta, bool adjustmentedRequestedByDragging)
2107 {
2108  VuoCompilerNodeClass *origNodeClass = node->getBase()->getNodeClass()->getCompiler();
2109  VuoCompilerNodeClass *newNodeClass = NULL;
2110 
2111  // Case: "Make List" node
2113  {
2114  int origNumPorts = ((VuoCompilerMakeListNodeClass *)(origNodeClass))->getItemCount();
2115  VuoCompilerType *listType = ((VuoCompilerMakeListNodeClass *)(origNodeClass))->getListType();
2116  string newMakeListNodeClassName = VuoCompilerMakeListNodeClass::getNodeClassName(origNumPorts+inputPortCountDelta, listType);
2117  newNodeClass = compiler->getNodeClass(newMakeListNodeClassName);
2118  }
2119 
2120  if (newNodeClass)
2121  {
2122  VuoNode *newNode = newNodeClass->newNode(node->getBase());
2123  string commandText = (inputPortCountDelta == -1? "Remove Input Port" : (inputPortCountDelta == 1? "Add Input Port" : ""));
2124  undoStack->push(new VuoCommandReplaceNode(node, composition->createRendererNode(newNode), this, commandText));
2125 
2126  if (adjustmentedRequestedByDragging)
2127  {
2128  VuoRendererInputListDrawer *oldMakeListNode = dynamic_cast<VuoRendererInputListDrawer *>(node);
2129  VuoRendererInputListDrawer *newMakeListNode = dynamic_cast<VuoRendererInputListDrawer *>(newNode->getRenderer());
2130 
2131  if (oldMakeListNode)
2132  {
2133  if (composition->mouseGrabberItem() == oldMakeListNode)
2134  oldMakeListNode->ungrabMouse();
2135 
2136  oldMakeListNode->setDragInProgress(false);
2137  }
2138 
2139  if (newMakeListNode)
2140  {
2141  newMakeListNode->setDragInProgress(true);
2142  newMakeListNode->grabMouse();
2143  }
2144  }
2145  }
2146 }
2147 
2152 void VuoEditorWindow::swapNodes(VuoRendererNode *node, string newNodeClass)
2153 {
2154  VuoCompilerNodeClass *newNodeCompilerClass = compiler->getNodeClass(newNodeClass);
2155  if (!newNodeCompilerClass)
2156  return;
2157 
2158  VuoRendererNode *newNode = composition->createNode(newNodeClass.c_str(), "", node->getBase()->getX(), node->getBase()->getY());
2159  newNode->getBase()->setTintColor(node->getBase()->getTintColor());
2160  undoStack->push(new VuoCommandChangeNode(node, newNode, this));
2161 }
2162 
2171 {
2172  return specializePortNetwork(port, specializedTypeName, true);
2173 }
2174 
2183 VuoRendererNode * VuoEditorWindow::specializePortNetwork(VuoRendererPort *port, string specializedTypeName, bool encapsulateInMacro)
2184 {
2185  string commandText = "Port Specialization";
2186 
2187  if (encapsulateInMacro)
2188  undoStack->beginMacro(tr(commandText.c_str()));
2189 
2190  VuoRendererNode *originalNode = port->getUnderlyingParentNode();
2191  set<VuoPort *> networkedGenericPorts = composition->getBase()->getCompiler()->getCorrelatedGenericPorts(originalNode->getBase(), port->getBase(), false);
2192 
2193  VuoRendererNode *newNode = NULL;
2194  try
2195  {
2196  // Specialize the parent node of the target port.
2197  string innermostSpecializedTypeName = (VuoType::isListTypeName(port->getDataType()->getModuleKey())? VuoType::extractInnermostTypeName(specializedTypeName) : specializedTypeName);
2198  newNode = specializeSinglePort(port, innermostSpecializedTypeName);
2199  if (newNode)
2200  {
2201 
2202  map<VuoRendererNode *, VuoRendererNode *> nodesToSpecialize;
2203  nodesToSpecialize[originalNode] = newNode;
2204 
2205  // Specialize the parent node of each port in the target port's connected generic network.
2206  foreach (VuoPort *networkedPort, networkedGenericPorts)
2207  {
2208  // If we have specialized this port already, there is no need to do so again.
2209  VuoRendererNode *originalNetworkedNode = networkedPort->getRenderer()->getUnderlyingParentNode();
2210  VuoRendererNode *mostRecentVersionOfNetworkedNode = originalNetworkedNode;
2211  VuoPort *mostRecentVersionOfNetworkedPort = networkedPort;
2212  map<VuoRendererNode *, VuoRendererNode *>::iterator i = nodesToSpecialize.find(originalNetworkedNode);
2213  if (i != nodesToSpecialize.end())
2214  {
2215  VuoRendererNode *specializedNode = i->second;
2216  if (!specializedNode)
2217  {
2218  VuoCompilerIssue issue(VuoCompilerIssue::Error, "specializing node", "",
2219  tr("Couldn't specialize node '%1' to type '%2'.")
2220  .arg(QString::fromStdString(originalNetworkedNode->getBase()->getTitle()),
2221  QString::fromStdString(specializedTypeName)).toStdString(),
2222  tr("The node might have failed to compile. Check the console log for details.").toStdString());
2223  throw VuoCompilerException(issue);
2224  }
2225  VuoPort *networkedPortInSpecializedNode = (networkedPort->getRenderer()->getInput()? specializedNode->getBase()->getInputPortWithName(networkedPort->getClass()->getName()) :
2226  specializedNode->getBase()->getOutputPortWithName(networkedPort->getClass()->getName()));
2227 
2228  mostRecentVersionOfNetworkedNode = specializedNode;
2229  mostRecentVersionOfNetworkedPort = networkedPortInSpecializedNode;
2230 
2231  string originalTypeName = ((VuoCompilerPortClass *)(networkedPort->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2232  string specializedTypeName = ((VuoCompilerPortClass *)(networkedPortInSpecializedNode->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2233  if (specializedTypeName != originalTypeName)
2234  continue;
2235  }
2236 
2237  VuoRendererNode *newNetworkedNode = specializeSinglePort(mostRecentVersionOfNetworkedPort->getRenderer(), innermostSpecializedTypeName);
2238  nodesToSpecialize[originalNetworkedNode] = newNetworkedNode;
2239  }
2240 
2241  bool preserveDanglingCables = true;
2242  undoStack->push(new VuoCommandReplaceNode(nodesToSpecialize, this, commandText, preserveDanglingCables));
2243  }
2244  }
2245  catch (VuoCompilerException &e)
2246  {
2247  VuoErrorDialog::show(this, tr("Can't set data type"), e.what());
2248  }
2249 
2250  if (encapsulateInMacro)
2251  undoStack->endMacro();
2252 
2253  return newNode;
2254 }
2255 
2261 VuoRendererNode * VuoEditorWindow::specializeSinglePort(VuoRendererPort *port, string specializedTypeName)
2262 {
2263  VuoRendererNode *node = port->getUnderlyingParentNode();
2264  string originalTypeName = ((VuoCompilerPortClass *)(port->getBase()->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2265  string innermostOriginalTypeName = VuoType::extractInnermostTypeName(originalTypeName);
2266 
2267  VuoCompilerSpecializedNodeClass *specializableNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node->getBase()->getNodeClass()->getCompiler());
2268  if (!specializableNodeClass)
2269  return NULL;
2270 
2271  string newNodeClassName = specializableNodeClass->createSpecializedNodeClassNameWithReplacement(innermostOriginalTypeName, specializedTypeName);
2272  VuoCompilerNodeClass *newNodeClass = compiler->getNodeClass(newNodeClassName);
2273 
2274  if (!newNodeClass)
2275  return NULL;
2276 
2277  VuoNode *newNode = newNodeClass->newNode(node->getBase());
2278  return composition->createRendererNode(newNode);
2279 }
2280 
2289 {
2290  return unspecializePortNetwork(port, true);
2291 }
2292 
2302 {
2303  QString commandText = tr("Port Specialization");
2304  bool preserveDanglingCables = true;
2305  bool resetConstantValues = false;
2306 
2307  if (encapsulateInMacro)
2308  undoStack->beginMacro(commandText);
2309 
2310  VuoRendererNode *originalParentNode = port->getUnderlyingParentNode();
2311  VuoRendererNode *unspecializedParentNode = NULL;
2312 
2313  // Retrieve the set of networked nodes to be reverted and resulting cables to be disconnected.
2314  map<VuoNode *, string> nodesToReplace;
2315  set<VuoCable *> cablesToDelete;
2316  composition->createReplacementsToUnspecializePort(port->getBase(), false, nodesToReplace, cablesToDelete);
2317 
2318  // Disconnect the necessary cables.
2319  QList<QGraphicsItem *> rendererCablesToDelete;
2320  foreach (VuoCable *cableToDelete, cablesToDelete)
2321  {
2322  // Uncollapse any attached typecasts so that they are not automatically deleted with their incoming cables.
2323  if (cableToDelete->getToNode() && cableToDelete->getToNode()->hasRenderer())
2324  composition->uncollapseTypecastNode(cableToDelete->getToNode()->getRenderer());
2325 
2326  rendererCablesToDelete.append(cableToDelete->getRenderer());
2327  }
2328 
2329  undoStack->push(new VuoCommandRemove(rendererCablesToDelete, this, inputEditorManager, commandText.toStdString()));
2330 
2331  // Call createReplacementsToUnspecializePort(...) a second time to retrieve an up-to-date set of nodes
2332  // to replace, since nodes may have been added (in the case of collapsed drawers) or removed
2333  // (in the case of collapsed typecasts) as a result of the cable deletions just performed.
2334  nodesToReplace.clear();
2335  composition->createReplacementsToUnspecializePort(port->getBase(), true, nodesToReplace, cablesToDelete);
2336 
2337  map<VuoRendererNode *, VuoRendererNode *> nodesToUnspecialize;
2338 
2339  // Revert the parent node of each port in the target port's connected generic network.
2340  for (map<VuoNode *, string>::iterator i = nodesToReplace.begin(); i != nodesToReplace.end(); ++i)
2341  {
2342  VuoNode *nodeToRevert = i->first;
2343  string revertedNodeClass = i->second;
2344 
2345  VuoRendererNode *revertedNode = unspecializeSingleNode(nodeToRevert->getRenderer(), revertedNodeClass);
2346  nodesToUnspecialize[nodeToRevert->getRenderer()] = revertedNode;
2347 
2348  if (nodeToRevert->getRenderer() == originalParentNode)
2349  unspecializedParentNode = revertedNode;
2350  }
2351 
2352  undoStack->push(new VuoCommandReplaceNode(nodesToUnspecialize, this, commandText.toStdString(), preserveDanglingCables, resetConstantValues));
2353 
2354  if (encapsulateInMacro)
2355  undoStack->endMacro();
2356 
2357  return unspecializedParentNode;
2358 }
2359 
2369 {
2370  return respecializePortNetwork(port, specializedTypeName, true);
2371 }
2372 
2382 VuoRendererNode * VuoEditorWindow::respecializePortNetwork(VuoRendererPort *port, string specializedTypeName, bool encapsulateInMacro)
2383 {
2384  // If the port already has the requested type, no action is required.
2385  if (port->getDataType()->getModuleKey() == specializedTypeName)
2386  return port->getUnderlyingParentNode();
2387 
2388  string commandText = "Port Re-specialization";
2389  VuoRendererNode *specializedParentNode = NULL;
2390 
2391  if (encapsulateInMacro)
2392  undoStack->beginMacro(tr(commandText.c_str()));
2393 
2394  VuoRendererNode *revertedNode = unspecializePortNetwork(port, false);
2395  if (revertedNode)
2396  {
2397  VuoPort *revertedPort = (port->getInput()? revertedNode->getBase()->getInputPortWithName(port->getBase()->getClass()->getName()) :
2398  revertedNode->getBase()->getOutputPortWithName(port->getBase()->getClass()->getName()));
2399  if (revertedPort)
2400  specializedParentNode = specializePortNetwork(revertedPort->getRenderer(), specializedTypeName, false);
2401  }
2402 
2403  if (encapsulateInMacro)
2404  undoStack->endMacro();
2405 
2406  return specializedParentNode;
2407 }
2408 
2414 VuoRendererNode * VuoEditorWindow::unspecializeSingleNode(VuoRendererNode *node, string revertedNodeClassName)
2415 {
2416  VuoCompilerNodeClass *revertedNodeClass = compiler->getNodeClass(revertedNodeClassName);
2417 
2418  if (!revertedNodeClass)
2419  return NULL;
2420 
2421  VuoNode *revertedNode = revertedNodeClass->newNode(node->getBase());
2422  return composition->createRendererNode(revertedNode);
2423 }
2424 
2430 {
2431  undoStack->beginMacro(tr("Tint"));
2432 
2433  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
2434  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
2435  {
2436  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(*i);
2437  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(*i);
2438  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(*i);
2439  if ((rn || rcomment) && (!attachment || (attachment->getRenderedHostNode() &&
2440  attachment->getRenderedHostNode()->hasRenderer() &&
2441  selectedCompositionComponents.contains(attachment->getRenderedHostNode()->getRenderer()))))
2442  undoStack->push(new VuoCommandSetItemTint(*i, tintColor, this));
2443  }
2444 
2445  undoStack->endMacro();
2446 }
2447 
2451 void VuoEditorWindow::hideSelectedInternalCables()
2452 {
2453  hideCables(composition->getSelectedCables(false));
2454 }
2455 
2459 void VuoEditorWindow::hideCables(set<VuoRendererCable *> cables)
2460 {
2461  undoStack->beginMacro(tr("Hide"));
2462 
2463  foreach (VuoRendererCable *cable, cables)
2464  undoStack->push(new VuoCommandSetCableHidden(cable, true, this));
2465 
2466  undoStack->endMacro();
2467  updateUI();
2468 }
2469 
2473 void VuoEditorWindow::unhideCables(set<VuoRendererCable *> cables)
2474 {
2475  bool publishedCableToUnhide = false;
2476  undoStack->beginMacro(tr("Unhide"));
2477 
2478  foreach (VuoRendererCable *cable, cables)
2479  {
2480  if (cable->getBase()->isPublished())
2481  publishedCableToUnhide = true;
2482  else
2483  undoStack->push(new VuoCommandSetCableHidden(cable, false, this));
2484  }
2485 
2486  if ((inputPortSidebar->isHidden() || outputPortSidebar->isHidden()) && publishedCableToUnhide)
2487  on_showPublishedPorts_triggered();
2488 
2489  undoStack->endMacro();
2490  updateUI();
2491 }
2492 
2496 void VuoEditorWindow::createIsolatedExternalPublishedPort(string typeName, bool isInput)
2497 {
2498  VuoType *type = (typeName.empty()? NULL : (compiler->getType(typeName)? compiler->getType(typeName)->getBase() : NULL));
2499  if (!typeName.empty() && !type)
2500  return;
2501 
2502  undoStack->beginMacro(tr("Add Published Port"));
2503 
2504  string portName = composition->getDefaultPublishedPortNameForType(type);
2505  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(composition->getUniquePublishedPortName(portName), type)->getBase());
2506  VuoRendererPublishedPort *rpp = composition->createRendererForPublishedPortInComposition(publishedPort, isInput);
2507  undoStack->push(new VuoCommandAddPublishedPort(rpp, this));
2508  showPublishedPortNameEditor(rpp, true);
2509 
2510  undoStack->endMacro();
2511 }
2512 
2516 void VuoEditorWindow::showPublishedPortNameEditor(VuoRendererPublishedPort *port, bool useUndoStack)
2517 {
2518  string originalName = port->getBase()->getClass()->getName();
2519  bool isPublishedOutput = port->getInput();
2520  (isPublishedOutput? outputPortSidebar : inputPortSidebar)->updatePortList();
2521  string newName = (isPublishedOutput? outputPortSidebar : inputPortSidebar)->showPublishedPortNameEditor(port);
2522 
2523  if (originalName != newName)
2524  changePublishedPortName(port, newName, useUndoStack);
2525 }
2526 
2531 void VuoEditorWindow::internalExternalPortPairPublished(VuoPort *internalPort, VuoPublishedPort *externalPort, bool forceEventOnlyPublication, VuoPort *portToSpecialize, string specializedTypeName, string typecastToInsert, bool useUndoStackMacro)
2532 {
2533  internalPortPublished(internalPort, forceEventOnlyPublication, externalPort->getClass()->getName(), true, portToSpecialize, specializedTypeName, typecastToInsert, useUndoStackMacro);
2534 }
2535 
2539 void VuoEditorWindow::internalPortPublishedViaDropBox(VuoPort *port, bool forceEventOnlyPublication, bool useUndoStackMacro)
2540 {
2541  internalPortPublished(port, forceEventOnlyPublication, "", false, NULL, "", "", useUndoStackMacro);
2542 }
2543 
2547 void VuoEditorWindow::internalPortPublished(VuoPort *port, bool forceEventOnlyPublication, string name, bool merge, VuoPort *portToSpecialize, string specializedTypeName, string typecastToInsert, bool useUndoStackMacro)
2548 {
2549  // Display the published port sidebars whenever a port is published.
2550  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2551  on_showPublishedPorts_triggered();
2552 
2553  if (name.empty())
2554  name = composition->generateSpecialPublishedNameForPort(port);
2555 
2556  // If the provided port is already published under the requested name,
2557  // don't attempt to re-publish it.
2558  // @todo: Once re-connection of published cables is allowed, account for possibility that ports were already
2559  // connected, but with a cable of a different type. https://b33p.net/kosada/node/5142
2560  VuoRendererPort *targetPort = port->getRenderer();
2561  vector<VuoRendererPublishedPort *> preexistingPublishedPorts = targetPort->getPublishedPorts();
2562  foreach (VuoRendererPublishedPort * publishedPort, preexistingPublishedPorts)
2563  {
2564  if (publishedPort->getBase()->getClass()->getName() == name)
2565  return;
2566  }
2567 
2568  if (useUndoStackMacro)
2569  undoStack->beginMacro(tr("Publish Port"));
2570 
2571  // Specialize the port and its network, if applicable.
2572  if (portToSpecialize && portToSpecialize->hasRenderer())
2573  {
2574  bool currentlyGeneric = dynamic_cast<VuoGenericType *>(portToSpecialize->getRenderer()->getDataType());
2575 
2576  VuoRendererNode *specializedNode = NULL;
2577  if (currentlyGeneric)
2578  specializedNode = specializePortNetwork(portToSpecialize->getRenderer(), specializedTypeName, false);
2579  else
2580  specializedNode = respecializePortNetwork(portToSpecialize->getRenderer(), specializedTypeName, false);
2581 
2582  if (specializedNode && (portToSpecialize == targetPort->getBase()))
2583  targetPort = (targetPort->getInput()? specializedNode->getBase()->getInputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer() :
2584  specializedNode->getBase()->getOutputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer());
2585  }
2586 
2587  // Determine whether we need to uncollapse any attached typecasts, delete any attached
2588  // "Make List" nodes, or disconnect any cables in order to publish this port.
2589  VuoRendererCable *displacedCable = NULL;
2590 
2591  if (targetPort->getInput())
2592  {
2593  bool publishedCableExpectedToHaveData = false;
2594  bool publishedPortExpectedToBeMerged = false;
2595 
2596  VuoType *internalPortDataType = (dynamic_cast<VuoRendererPublishedPort *>(targetPort)?
2597  static_cast<VuoCompilerPortClass *>(targetPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
2598  targetPort->getDataType());
2599  if (merge)
2600  {
2601  VuoPublishedPort *publishedPortWithTargetName = (targetPort->getInput()?
2602  composition->getBase()->getPublishedInputPortWithName(name) :
2603  composition->getBase()->getPublishedOutputPortWithName(name));
2604 
2605  if (publishedPortWithTargetName &&
2606  dynamic_cast<VuoRendererPublishedPort *>(publishedPortWithTargetName->getRenderer())->canAccommodateInternalPort(targetPort, forceEventOnlyPublication))
2607  {
2608  publishedPortExpectedToBeMerged = true;
2609  VuoType *type = static_cast<VuoCompilerPortClass *>(publishedPortWithTargetName->getClass()->getCompiler())->getDataVuoType();
2610  publishedCableExpectedToHaveData = (!forceEventOnlyPublication && internalPortDataType && type);
2611  }
2612  }
2613 
2614  if (!publishedPortExpectedToBeMerged)
2615  {
2616  publishedCableExpectedToHaveData = (!forceEventOnlyPublication && internalPortDataType);
2617  }
2618 
2619  // If input port has a connected collapsed typecast, uncollapse it.
2620  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2621  if (typecastPort)
2622  {
2623  targetPort = typecastPort->getReplacedPort();
2624  composition->uncollapseTypecastNode(typecastPort);
2625  }
2626 
2627  // If the published input cable will have data...
2628  if (publishedCableExpectedToHaveData)
2629  {
2630  // If the internal input port has a connected drawer, delete it.
2631  VuoRendererInputDrawer *attachedDrawer = targetPort->getAttachedInputDrawer();
2632  if (attachedDrawer)
2633  {
2634  QList<QGraphicsItem *> componentsToRemove;
2635  componentsToRemove.append(attachedDrawer);
2636  bool disableAutomaticAttachmentInsertion = true;
2637  undoStack->push(new VuoCommandRemove(componentsToRemove, this, inputEditorManager, "", disableAutomaticAttachmentInsertion));
2638  }
2639 
2640  // If the internal input port has a connected data cable, displace it.
2641  else
2642  {
2643  vector<VuoCable *> connectedCables = targetPort->getBase()->getConnectedCables(true);
2644  for (vector<VuoCable *>::iterator cable = connectedCables.begin(); (! displacedCable) && (cable != connectedCables.end()); ++cable)
2645  if ((*cable)->getRenderer()->effectivelyCarriesData())
2646  displacedCable = (*cable)->getRenderer();
2647 
2648  }
2649  }
2650  }
2651 
2652  VuoRendererPort *adjustedTargetPort = targetPort;
2653  QList<QGraphicsItem *> typecastRelatedComponentsToAdd = QList<QGraphicsItem *>();
2654  if (! typecastToInsert.empty())
2655  {
2656  // Insert the necessary typecast node.
2657  VuoRendererNode *typecast = composition->createNode(typecastToInsert.c_str(), "",
2658  targetPort->scenePos().x()+100,
2659  targetPort->scenePos().y()+100);
2660 
2661  typecastRelatedComponentsToAdd.append(typecast);
2662 
2665 
2666  // Insert an extra cable leading to or from the typecast as appropriate.
2667  VuoRendererCable *newCableConnectingTypecast;
2668 
2669  // Case: Publishing an internal input port
2670  if (targetPort->getInput())
2671  {
2672  // Prepare to add a cable connecting the output port of the newly inserted
2673  // typecast to the originally intended input port.
2674  VuoCompilerNode *fromNode = typecast->getBase()->getCompiler();
2675  VuoCompilerPort *fromPort = (VuoCompilerPort *)typecastOutPort->getCompiler();
2676  VuoCompilerNode *toNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2677  VuoCompilerPort *toPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2678 
2679  // Prepare to re-route the dragged cable's loose end to connect to the typecast's input port.
2680  adjustedTargetPort = typecastInPort->getRenderer();
2681  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2682  }
2683 
2684  // Case: Publishing an internal output port
2685  else
2686  {
2687  // Prepare to add a cable connecting the originally intended output port
2688  // to the input port of the newly inserted typecast.
2689  VuoCompilerNode *fromNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2690  VuoCompilerPort *fromPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2691  VuoCompilerNode *toNode = typecast->getBase()->getCompiler();
2692  VuoCompilerPort *toPort = (VuoCompilerPort *)typecastInPort->getCompiler();
2693 
2694  // Prepare to re-route the dragged cable's loose end to connect to the typecast's output port.
2695  adjustedTargetPort = typecastOutPort->getRenderer();
2696 
2697  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2698  }
2699 
2700  typecastRelatedComponentsToAdd.append(newCableConnectingTypecast);
2701 
2702  bool disableAutomaticAttachmentInsertion = true;
2703  undoStack->push(new VuoCommandAdd(typecastRelatedComponentsToAdd, this, "", disableAutomaticAttachmentInsertion));
2704  }
2705 
2706  undoStack->push(new VuoCommandPublishPort(adjustedTargetPort->getBase(), displacedCable, this, forceEventOnlyPublication, name, merge));
2707 
2708  // @todo: If re-connecting a data+event cable whose previous 'To' port had a collapsed typecast attached, delete that typecast.
2709  // Re-connection of published cables is not currently possible (https://b33p.net/kosada/node/5142), so we can skip this for now.
2710 
2711  // If the target port is generic and, as a result of this connection, has only a single
2712  // compatible specialized type, specialize its network.
2713  VuoGenericType *genericType = dynamic_cast<VuoGenericType *>((static_cast<VuoCompilerPort *>(targetPort->getBase()->getCompiler()))->getDataVuoType());
2714  if (genericType)
2715  {
2716  VuoGenericType::Compatibility compatibility;
2717  vector<string> compatibleSpecializedTypes = genericType->getCompatibleSpecializedTypes(compatibility);
2718  if (compatibleSpecializedTypes.size() == 1)
2719  {
2720  string loneCompatibleSpecializedType = *compatibleSpecializedTypes.begin();
2721  specializePortNetwork(targetPort, loneCompatibleSpecializedType, false);
2722  }
2723  }
2724 
2725  if (useUndoStackMacro)
2726  undoStack->endMacro();
2727 }
2728 
2733 {
2734  // Display the published port sidebars whenever a port is unpublished.
2735  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2736  on_showPublishedPorts_triggered();
2737 
2738  undoStack->beginMacro(tr("Delete"));
2739 
2740  // Remove any connected data+event cables using a separate VuoCommandRemove so that typecast
2741  // deletion, list insertion, etc. are handled correctly.
2742  bool isPublishedInput = !port->getInput();
2743  if (isPublishedInput)
2744  {
2745  vector<VuoCable *> publishedInputCables = port->getBase()->getConnectedCables(true);
2746  QList<QGraphicsItem *> removedDataCables;
2747  foreach (VuoCable *cable, publishedInputCables)
2748  {
2749  if (cable->getRenderer()->effectivelyCarriesData())
2750  removedDataCables.append(cable->getRenderer());
2751  }
2752 
2753  if (!removedDataCables.empty())
2754  undoStack->push(new VuoCommandRemove(removedDataCables, this, inputEditorManager, "Delete"));
2755  }
2756 
2757  undoStack->push(new VuoCommandUnpublishPort(dynamic_cast<VuoPublishedPort *>(port->getBase()), this));
2758  undoStack->endMacro();
2759 }
2760 
2765 {
2766  // Display the published port sidebars whenever a port is unpublished.
2767  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2768  on_showPublishedPorts_triggered();
2769 
2770  undoStack->beginMacro(tr("Delete"));
2771 
2772  // If unpublishing a port with a list input, insert a new attached "Make List" node.
2773  // @todo: Eventually, account for the fact that the published cable might be
2774  // event-only even though the port is not.
2775  QList<QGraphicsItem *> makeListComponents = createAnyNecessaryMakeListComponents(port);
2776  if (! makeListComponents.empty())
2777  undoStack->push(new VuoCommandAdd(makeListComponents, this, ""));
2778 
2779  undoStack->push(new VuoCommandUnpublishPort(port, this));
2780  undoStack->endMacro();
2781 }
2782 
2787 void VuoEditorWindow::makeProtocolPortChanges(map<VuoPublishedPort *, string> publishedPortsToRename,
2788  set<VuoPublishedPort *> publishedPortsToRemove,
2789  vector<VuoPublishedPort *> publishedPortsToAdd,
2790  bool beginUndoStackMacro,
2791  bool endUndoStackMacro)
2792 {
2793  if (beginUndoStackMacro)
2794  undoStack->beginMacro(tr("Protocol Port Modification"));
2795 
2796  // Rename requested ports.
2797  for (map<VuoPublishedPort *, string>::iterator i = publishedPortsToRename.begin(); i != publishedPortsToRename.end(); ++i)
2798  {
2799  VuoPublishedPort *port = i->first;
2800  string name = i->second;
2801  undoStack->push(new VuoCommandSetPublishedPortName(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), name, this));
2802  }
2803 
2804  // Remove the requested protocol ports.
2805  foreach (VuoPublishedPort *port, publishedPortsToRemove)
2806  undoStack->push(new VuoCommandRemoveProtocolPort(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), this));
2807 
2808  // Add the requested protocol ports.
2809  foreach (VuoPublishedPort *port, publishedPortsToAdd)
2810  undoStack->push(new VuoCommandAddPublishedPort(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), this));
2811 
2812  if (endUndoStackMacro)
2813  undoStack->endMacro();
2814 }
2815 
2819 void VuoEditorWindow::resetUndoStackMacros()
2820 {
2821  if (itemDragMacroInProgress)
2822  {
2823  itemDragMacroInProgress = false;
2824 
2825  set<VuoRendererNode *> draggedNodeSet;
2826  set<VuoRendererComment *> draggedCommentSet;
2827 
2828  foreach (QGraphicsItem *item, itemsBeingDragged)
2829  {
2830  if (dynamic_cast<VuoRendererNode *>(item))
2831  draggedNodeSet.insert(dynamic_cast<VuoRendererNode *>(item));
2832  else if (dynamic_cast<VuoRendererComment *>(item))
2833  draggedCommentSet.insert(dynamic_cast<VuoRendererComment *>(item));
2834  }
2835 
2836  undoStack->push(new VuoCommandMove(draggedNodeSet, draggedCommentSet, itemDragDx, itemDragDy, this, true));
2837 
2838  itemsBeingDragged.clear();
2839 
2840  itemDragDx = 0;
2841  itemDragDy = 0;
2842  }
2843 
2844  if (commentResizeMacroInProgress)
2845  {
2846  commentResizeMacroInProgress = false;
2847 
2848  if (commentBeingResized && ((commentResizeDx != 0) || (commentResizeDy != 0)))
2849  undoStack->push(new VuoCommandResizeComment(commentBeingResized, commentResizeDx, commentResizeDy, this));
2850 
2851  commentBeingResized = NULL;
2852 
2853  commentResizeDx = 0;
2854  commentResizeDy = 0;
2855  }
2856 
2857  if (duplicationMacroInProgress)
2858  {
2859  undoStack->endMacro();
2860  duplicationMacroInProgress = false;
2861  }
2862 }
2863 
2867 bool VuoEditorWindow::event(QEvent *event)
2868 {
2869  if (event->type() == QEvent::Show)
2870  {
2871  if (zoomOutToFitOnNextShowEvent)
2872  {
2873  zoomOutToFit();
2874  zoomOutToFitOnNextShowEvent = false;
2875  }
2876 
2877  // Use a queued connection to avoid mutual recursion between VuoEditorWindow::event() and VuoEditorComposition::updateFeedbackErrors().
2878  QMetaObject::invokeMethod(composition, "updateFeedbackErrors", Qt::QueuedConnection);
2879  }
2880 
2881  // Workaround to force a cursor update when the cursor re-enters the window during a canvas drag.
2882  // See https://b33p.net/kosada/node/7718#comment-27792 .
2883  if (canvasDragInProgress && (event->type() == QEvent::MouseMove))
2884  updateCursor();
2885 
2886  // If the window is activated, update its UI elements (e.g., to correctly reflect
2887  // that it is the active document within the "Window" menu and OS X dock context menu).
2888  else if (event->type() == QEvent::WindowActivate)
2889  {
2891  updateUI();
2892  emit windowActivated();
2893  }
2894 
2895  else if (event->type() == QEvent::WindowDeactivate)
2896  emit windowDeactivated();
2897 
2898  if (event->type() == QEvent::WindowStateChange)
2899  {
2900  if (isMinimized())
2901  {
2902  composition->disableErrorPopovers();
2903  composition->emitCompositionOnTop(false);
2904  }
2905  else
2906  composition->updateFeedbackErrors();
2907  }
2908 
2909  else if (event->type() == QEvent::MouseButtonPress)
2910  {
2911  // Forward published cable drags initiated from the sidebar border onto the canvas.
2912  bool leftMouseButtonPressed = (((QMouseEvent *)(event))->button() == Qt::LeftButton);
2913  if (leftMouseButtonPressed)
2914  {
2915  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
2916  if (!inputPortSidebar->isHidden())
2917  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
2918 
2919  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
2920  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
2921 
2922  if (publishedPortNearCursor)
2923  {
2924  QGraphicsSceneMouseEvent mouseEvent;
2925  mouseEvent.setButtons(static_cast<QMouseEvent *>(event)->buttons());
2926  mouseEvent.setScenePos(ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos())));
2927  composition->leftMousePressEventAtNearbyItem(static_cast<QGraphicsItem *>(publishedPortNearCursor), &mouseEvent);
2928 
2929  forwardingEventsToCanvas = true;
2930  event->accept();
2931  return true;
2932  }
2933  }
2934 
2935  // If it's possible that the user is attempting to re-size a docked widget,
2936  // make sure that we have re-enabled re-sizing for the widgets that
2937  // share the left docking area, having possibly disabled this functionality
2938  // within setPublishedPortSidebarVisibility(...) and/or conformToGlobalNodeLibraryVisibility(...).
2939  restoreDefaultLeftDockedWidgetWidths();
2940  }
2941 
2942  else if (event->type() == QEvent::MouseMove)
2943  {
2944  // Forward published cable drags initiated from the sidebar border onto the canvas.
2945  if (forwardingEventsToCanvas)
2946  {
2947  QMouseEvent mouseEvent(QEvent::MouseMove,
2948  ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos()),
2949  static_cast<QMouseEvent *>(event)->screenPos(),
2950  static_cast<QMouseEvent *>(event)->button(),
2951  static_cast<QMouseEvent *>(event)->buttons(),
2952  static_cast<QMouseEvent *>(event)->modifiers());
2953 
2954  QApplication::sendEvent(ui->graphicsView->viewport(), &mouseEvent);
2955 
2956  event->accept();
2957  return true;
2958  }
2959  }
2960 
2961  else if (event->type() == QEvent::MouseButtonRelease)
2962  {
2963  // Forward published cable drags initiated from the sidebar border onto the canvas.
2964  if (forwardingEventsToCanvas)
2965  {
2966  QMouseEvent mouseEvent(QEvent::MouseButtonRelease,
2967  ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos()),
2968  static_cast<QMouseEvent *>(event)->screenPos(),
2969  static_cast<QMouseEvent *>(event)->button(),
2970  static_cast<QMouseEvent *>(event)->buttons(),
2971  static_cast<QMouseEvent *>(event)->modifiers());
2972 
2973  QApplication::sendEvent(ui->graphicsView->viewport(), &mouseEvent);
2974 
2975  forwardingEventsToCanvas = false;
2976  event->accept();
2977  return true;
2978  }
2979  }
2980 
2981  return QMainWindow::event(event);
2982 }
2983 
2987 bool VuoEditorWindow::eventFilter(QObject *object, QEvent *event)
2988 {
2989  // If it's been a while since we received a scroll event, turn interactivity back on.
2990  // Workaround for Qt sometimes not sending us a Qt::ScrollEnd event.
2991  const double scrollTimeoutSeconds = 0.5;
2992  if (scrollInProgress && VuoLogGetElapsedTime() - timeOfLastScroll > scrollTimeoutSeconds)
2993  {
2994  VDebugLog("Turning canvas interactivity back on even though we didn't receive a Qt::ScrollEnd event, since there hasn't been a scroll event in %g seconds.", scrollTimeoutSeconds);
2995  ui->graphicsView->setInteractive(true);
2996  scrollInProgress = false;
2997  }
2998 
2999  // Prevent the composition from responding to certain types of events during canvas grabs/drags.
3000  // The canvas drag itself must be handled in response to intercepted QEvent::MouseMove
3001  // events rather than here in response to intercepted QEvent::GraphicsSceneMouseMove
3002  // events; otherwise the canvas doesn't track correctly with the cursor.
3003  if ((canvasDragEnabled || canvasDragInProgress) && (object == composition) && (
3004  event->type() == QEvent::GraphicsSceneMouseMove ||
3005  event->type() == QEvent::GraphicsSceneMousePress ||
3006  event->type() == QEvent::GraphicsSceneMouseDoubleClick ||
3007  event->type() == QEvent::GraphicsSceneContextMenu ||
3008  event->type() == QEvent::KeyPress))
3009  return true;
3010 
3011  // If an input editor is displayed at the time that the canvas is clicked, the click should be consumed
3012  // with the closing of the input editor rather than having additional effects on the canvas.
3013  if ((object == composition) &&
3014  (event->type() == QEvent::GraphicsSceneMousePress) &&
3015  inputEditorSession)
3016  {
3017  consumeNextMouseReleaseToCanvas = true;
3018  return true;
3019  }
3020  if ((object == composition) &&
3021  (event->type() == QEvent::GraphicsSceneMouseRelease) &&
3022  consumeNextMouseReleaseToCanvas)
3023  {
3024  consumeNextMouseReleaseToCanvas = false;
3025  return true;
3026  }
3027 
3028  if (event->type() == QEvent::Wheel
3029  && ui->graphicsView->rubberBandRect().isNull()
3030  && !ui->graphicsView->pinchZoomInProgress())
3031  {
3032  // Disable interaction while scrolling, to improve the framerate.
3033  QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
3034  if (wheelEvent->phase() == Qt::ScrollBegin)
3035  {
3036  ui->graphicsView->setInteractive(false);
3037  scrollInProgress = true;
3038  }
3039  else if (wheelEvent->phase() == Qt::ScrollEnd)
3040  {
3041  ui->graphicsView->setInteractive(true);
3042  scrollInProgress = false;
3043  }
3044 
3045  // Skip rendering if we aren't keeping up.
3046  double lag = VuoEditorCocoa_systemUptime() - wheelEvent->timestamp()/1000.;
3047  const double lagLimit = .1;
3048  if (lag > lagLimit)
3049  return true;
3050 
3051  // Remove Shift modifier from mouse-wheel events to prevent it from triggering page-step mode.
3052  QInputEvent *filteredInputEvent = (QInputEvent *)(event);
3053  Qt::KeyboardModifiers modifiersOtherThanShift = (filteredInputEvent->modifiers() & ~Qt::ShiftModifier);
3054  filteredInputEvent->setModifiers(modifiersOtherThanShift);
3055  object->removeEventFilter(this);
3056  QApplication::sendEvent(object, filteredInputEvent);
3057  object->installEventFilter(this);
3058 
3059  timeOfLastScroll = VuoLogGetElapsedTime();
3060 
3061  return true;
3062  }
3063 
3064  // Disable any non-detached port or node popover upon mouse press.
3065  else if (event->type() == QEvent::MouseButtonPress)
3066  {
3067  bool leftMouseButtonPressed = (((QMouseEvent *)(event))->button() == Qt::LeftButton);
3068  if (leftMouseButtonPressed)
3069  this->lastLeftMousePressHadOptionModifier = VuoEditorUtilities::optionKeyPressedForEvent(event);
3070 
3071  composition->disableNondetachedPortPopovers(NULL, true);
3072 
3073  // Initiate canvas drag if previously enabled by press-and-hold of the spacebar.
3074  if (canvasDragEnabled && leftMouseButtonPressed && (object == ui->graphicsView->viewport()))
3075  {
3076  initiateCanvasDrag();
3077  return true;
3078  }
3079 
3080  // If a sidebar published port is near the cursor, send the mouse press directly to that published port
3081  // rather than handing hover detection over to the composition's findNearbyComponent(...) algorithm.
3082  else if (leftMouseButtonPressed)
3083  {
3084  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
3085  if (!inputPortSidebar->isHidden())
3086  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3087 
3088  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
3089  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3090 
3091  if (publishedPortNearCursor)
3092  {
3093  QGraphicsSceneMouseEvent mouseEvent;
3094  mouseEvent.setButtons(static_cast<QMouseEvent *>(event)->buttons());
3095  mouseEvent.setScenePos(ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos())));
3096  composition->leftMousePressEventAtNearbyItem(static_cast<QGraphicsItem *>(publishedPortNearCursor), &mouseEvent);
3097  return true;
3098  }
3099  }
3100  }
3101 
3102  // Continue any canvas drag currently in progress.
3103  else if ((event->type() == QEvent::MouseMove) &&
3104  canvasDragInProgress &&
3105  (object == ui->graphicsView->viewport()))
3106  {
3107  mouseMoveEvent((QMouseEvent *)event);
3108  return true;
3109  }
3110 
3111  // Conclude canvas drag.
3112  else if ((event->type() == QEvent::MouseButtonRelease) &&
3113  canvasDragInProgress &&
3114  (((QMouseEvent *)(event))->button() == Qt::LeftButton) &&
3115  (object == ui->graphicsView->viewport()))
3116  {
3117  concludeCanvasDrag();
3118  return true;
3119  }
3120 
3121  // If a cable drag is in progress and a published port sidebar is directly under the cursor,
3122  // let the sidebar, rather than the composition canvas, handle hover highlighting
3123  // and the conclusion of the cable drag.
3124  else if ((((event->type() == QEvent::MouseButtonRelease) &&
3125  (((QMouseEvent *)(event))->button() == Qt::LeftButton)) ||
3126  ((event->type() == QEvent::MouseMove) &&
3127  (((QMouseEvent *)(event))->buttons() & Qt::LeftButton)))
3128  &&
3129  composition->getCableInProgress())
3130  {
3131  QPoint cursorPosition = ((QMouseEvent *)(event))->globalPos();
3132 
3133  QRect inputPortSidebarRect = inputPortSidebar->geometry();
3134  inputPortSidebarRect.moveTopLeft(inputPortSidebar->parentWidget()->mapToGlobal(inputPortSidebarRect.topLeft()));
3135 
3136  QRect outputPortSidebarRect = outputPortSidebar->geometry();
3137  outputPortSidebarRect.moveTopLeft(outputPortSidebar->parentWidget()->mapToGlobal(outputPortSidebarRect.topLeft()));
3138 
3139  VuoRendererPublishedPort *publishedInputPortNearCursor = NULL;
3140  if (!inputPortSidebar->isHidden())
3141  publishedInputPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
3142 
3143  VuoRendererPublishedPort *publishedOutputPortNearCursor = NULL;
3144  if (!outputPortSidebar->isHidden())
3145  publishedOutputPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
3146 
3147  // Case: drag with left mouse button pressed
3148  if ((event->type() == QEvent::MouseMove) &&
3149  (((QMouseEvent *)(event))->buttons() & Qt::LeftButton))
3150  {
3151  bool dragOverInputPortSidebar = ((! inputPortSidebar->isHidden()) && (inputPortSidebarRect.contains(cursorPosition) || publishedInputPortNearCursor));
3152  bool dragOverOutputPortSidebar = ((! outputPortSidebar->isHidden()) && (outputPortSidebarRect.contains(cursorPosition) || publishedOutputPortNearCursor));
3153 
3154  if (dragOverInputPortSidebar || dragOverOutputPortSidebar)
3155  {
3156  if (!previousDragMoveWasOverSidebar)
3157  composition->clearHoverHighlighting();
3158 
3159  if (dragOverInputPortSidebar)
3161  else if (dragOverOutputPortSidebar)
3163 
3164  previousDragMoveWasOverSidebar = true;
3165  }
3166 
3167  else
3168  {
3169  if (previousDragMoveWasOverSidebar)
3170  {
3171  inputPortSidebar->clearHoverHighlighting();
3172  outputPortSidebar->clearHoverHighlighting();
3173  if (composition->getCableInProgress())
3175  }
3176 
3177  previousDragMoveWasOverSidebar = false;
3178  }
3179  }
3180 
3181  // Case: left mouse button release
3182  else if ((event->type() == QEvent::MouseButtonRelease) &&
3183  (((QMouseEvent *)(event))->button() == Qt::LeftButton))
3184  {
3185  VuoCable *cableInProgress = composition->getCableInProgress();
3186 
3187  // Case: Concluding a published cable drag at a sidebar published input port.
3188  if (cableInProgress && !inputPortSidebar->isHidden() && (inputPortSidebarRect.contains(cursorPosition) || publishedInputPortNearCursor))
3189  inputPortSidebar->concludePublishedCableDrag((QMouseEvent *)event, cableInProgress, composition->getCableInProgressWasNew());
3190 
3191  // Case: Concluding a published cable drag at a sidebar published output port.
3192  else if (cableInProgress && !outputPortSidebar->isHidden() && (outputPortSidebarRect.contains(cursorPosition) || publishedOutputPortNearCursor))
3193  outputPortSidebar->concludePublishedCableDrag((QMouseEvent *)event, cableInProgress, composition->getCableInProgressWasNew());
3194  }
3195 
3196  object->removeEventFilter(this);
3197  QApplication::sendEvent(object, event);
3198  object->installEventFilter(this);
3199  return true;
3200  }
3201 
3202  // Determine whether the cursor is near a published sidebar port, and hover-highlight that port,
3203  // before handing hover detection over to the composition's findNearbyComponent(...) algorithm.
3204  else if (event->type() == QEvent::MouseMove && !composition->getCableInProgress())
3205  {
3206  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
3207  if (!inputPortSidebar->isHidden())
3208  {
3209  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3211  }
3212 
3213  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
3214  {
3215  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3217  }
3218 
3219  if (publishedPortNearCursor)
3220  {
3221  if (!publishedPortNearCursorPreviously)
3222  composition->clearHoverHighlighting();
3223  }
3224 
3225  else
3226  {
3227  if (publishedPortNearCursorPreviously)
3228  {
3229  inputPortSidebar->clearHoverHighlighting();
3230  outputPortSidebar->clearHoverHighlighting();
3231  }
3232 
3233  object->removeEventFilter(this);
3234  QApplication::sendEvent(object, event);
3235  object->installEventFilter(this);
3236  }
3237 
3238  publishedPortNearCursorPreviously = publishedPortNearCursor;
3239 
3240  return true;
3241  }
3242 
3243  // Customize handling of keypress events.
3244  else if (event->type() == QEvent::KeyPress)
3245  {
3246  QKeyEvent *keyEvent = (QKeyEvent *)(event);
3247 
3248  // The Esc key closes non-detached popovers.
3249  if (keyEvent->key() == Qt::Key_Escape)
3250  composition->disableNondetachedPortPopovers();
3251 
3252  // Arrow keys may be used to move the viewport when no composition components are selected.
3253  if ((composition->selectedItems().isEmpty()) &&
3254  ((keyEvent->key() == Qt::Key_Up) ||
3255  (keyEvent->key() == Qt::Key_Down) ||
3256  (keyEvent->key() == Qt::Key_Left) ||
3257  (keyEvent->key() == Qt::Key_Right)))
3258  {
3259  keyPressEvent(keyEvent);
3260  }
3261 
3262  // The canvas may be dragged with spacebar+drag.
3263  else if ((object == composition) && (keyEvent->key() == Qt::Key_Space))
3264  {
3265  keyPressEvent(keyEvent);
3266  }
3267 
3268  else
3269  {
3270  object->removeEventFilter(this);
3271  QApplication::sendEvent(object, event);
3272  object->installEventFilter(this);
3273  }
3274  return true;
3275  }
3276 
3277  // Suppress ContextMenu events sent to the composition. We generate them ourselves
3278  // within VuoEditorComposition::mousePressEvent and VuoEditorGraphicsView::viewportEvent
3279  // to customize the selection behavior that accompanies the presentation of the context menu.
3280  else if (event->type() == QEvent::ContextMenu)
3281  {
3282  return true;
3283  }
3284 
3285  return QMainWindow::eventFilter(object, event);
3286 }
3287 
3292 {
3293  return scrollInProgress;
3294 }
3295 
3300 {
3301  return itemDragMacroInProgress;
3302 }
3303 
3308 {
3309  latestDragTime = VuoLogGetElapsedTime();
3310 }
3311 
3316 {
3317  return latestDragTime;
3318 }
3319 
3325 void VuoEditorWindow::enableCanvasDrag()
3326 {
3327  this->canvasDragEnabled = true;
3328  updateUI();
3329 
3330  composition->clearHoverHighlighting();
3331  composition->disableNondetachedPortPopovers();
3332 }
3333 
3337 void VuoEditorWindow::disableCanvasDrag()
3338 {
3339  this->canvasDragEnabled = false;
3340  updateUI();
3341 }
3342 
3346 void VuoEditorWindow::initiateCanvasDrag()
3347 {
3348  this->canvasDragInProgress = true;
3349  QPoint currentCursorPos = QCursor::pos();
3350  this->lastCursorLocationDuringCanvasDrag = currentCursorPos;
3351 
3352  int xNegativeScrollPotential = ui->graphicsView->horizontalScrollBar()->value() - ui->graphicsView->horizontalScrollBar()->minimum();
3353  int yNegativeScrollPotential = ui->graphicsView->verticalScrollBar()->value() - ui->graphicsView->verticalScrollBar()->minimum();
3354 
3355  int xPositiveScrollPotential = ui->graphicsView->horizontalScrollBar()->maximum() - ui->graphicsView->horizontalScrollBar()->value();
3356  int yPositiveScrollPotential = ui->graphicsView->verticalScrollBar()->maximum() - ui->graphicsView->verticalScrollBar()->value();
3357 
3358  // Positive changes in cursor position correspond to negative changes in scrollbar value.
3359  this->canvasDragMinCursorPos = QPoint(currentCursorPos.x() - xPositiveScrollPotential, currentCursorPos.y() - yPositiveScrollPotential);
3360  this->canvasDragMaxCursorPos = QPoint(currentCursorPos.x() + xNegativeScrollPotential, currentCursorPos.y() + yNegativeScrollPotential);
3361 
3362  updateUI();
3363 }
3364 
3368 void VuoEditorWindow::concludeCanvasDrag()
3369 {
3370  this->canvasDragInProgress = false;
3371  this->lastCursorLocationDuringCanvasDrag = QPoint();
3372  updateUI();
3373 }
3374 
3378 void VuoEditorWindow::keyPressEvent(QKeyEvent *event)
3379 {
3380  Qt::KeyboardModifiers modifiers = event->modifiers();
3381  qreal adjustedViewportStepRate = viewportStepRate;
3382  if (modifiers & Qt::ShiftModifier)
3383  {
3384  adjustedViewportStepRate *= viewportStepRateMultiplier;
3385  }
3386 
3387  if (event->key() == Qt::Key_Escape)
3388  {
3389  if (!getCurrentNodeLibrary()->isHidden())
3390  getCurrentNodeLibrary()->close();
3391  else if (arePublishedPortSidebarsVisible())
3392  on_showPublishedPorts_triggered();
3393  return;
3394  }
3395 
3396  if (composition->hasFocus())
3397  {
3398  switch (event->key())
3399  {
3400  case Qt::Key_Up:
3401  {
3402  const int y = ui->graphicsView->verticalScrollBar()->value() -
3403  adjustedViewportStepRate*(ui->graphicsView->verticalScrollBar()->singleStep());
3404  ui->graphicsView->verticalScrollBar()->setValue(y);
3405  break;
3406  }
3407  case Qt::Key_Down:
3408  {
3409  const int y = ui->graphicsView->verticalScrollBar()->value() +
3410  adjustedViewportStepRate*(ui->graphicsView->verticalScrollBar()->singleStep());
3411  ui->graphicsView->verticalScrollBar()->setValue(y);
3412  break;
3413  }
3414  case Qt::Key_Left:
3415  {
3416  const int x = ui->graphicsView->horizontalScrollBar()->value() -
3417  adjustedViewportStepRate*(ui->graphicsView->horizontalScrollBar()->singleStep());
3418  ui->graphicsView->horizontalScrollBar()->setValue(x);
3419  break;
3420  }
3421  case Qt::Key_Right:
3422  {
3423  const int x = ui->graphicsView->horizontalScrollBar()->value() +
3424  adjustedViewportStepRate*(ui->graphicsView->horizontalScrollBar()->singleStep());
3425  ui->graphicsView->horizontalScrollBar()->setValue(x);
3426  break;
3427  }
3428  case Qt::Key_Space:
3429  {
3430  enableCanvasDrag();
3431  break;
3432  }
3433  default:
3434  {
3435  QGraphicsItem *nearbyItem = composition->findNearbyComponent(getCursorScenePos());
3436  VuoRendererPort *nearbyPort = (dynamic_cast<VuoRendererPort *>(nearbyItem)? (VuoRendererPort *)nearbyItem : NULL);
3437  if (nearbyPort)
3438  {
3439  composition->sendEvent(nearbyPort, event);
3440  event->accept();
3441  }
3442  else
3443  QMainWindow::keyPressEvent(event);
3444 
3445  break;
3446  }
3447  }
3448  }
3449 }
3450 
3454 void VuoEditorWindow::keyReleaseEvent(QKeyEvent *event)
3455 {
3456  switch (event->key())
3457  {
3458  case Qt::Key_Space:
3459  {
3460  disableCanvasDrag();
3461  break;
3462  }
3463  default:
3464  {
3465  QMainWindow::keyReleaseEvent(event);
3466  break;
3467  }
3468  }
3469 }
3470 
3474 void VuoEditorWindow::mouseMoveEvent(QMouseEvent *event)
3475 {
3476  if (canvasDragInProgress)
3477  {
3478  QPoint oldPos = lastCursorLocationDuringCanvasDrag;
3479  QPoint currentCursorPos = QCursor::pos();
3480 
3481  // Ignore cursor movements beyond the range that would have affected the scollbar values at the beginning
3482  // of the drag, to ensure a single continuous contact point between cursor and canvas when the cursor is within range.
3483  QPoint effectiveNewPos = QPoint( fmax(canvasDragMinCursorPos.x(), fmin(canvasDragMaxCursorPos.x(), currentCursorPos.x())),
3484  fmax(canvasDragMinCursorPos.y(), fmin(canvasDragMaxCursorPos.y(), currentCursorPos.y())));
3485 
3486  // Positive changes in cursor position correspond to negative changes in scrollbar value.
3487  int dx = -1 * (effectiveNewPos - oldPos).x();
3488  int dy = -1 * (effectiveNewPos - oldPos).y();
3489 
3490  if (dx)
3491  {
3492  lastCursorLocationDuringCanvasDrag.setX(effectiveNewPos.x());
3493 
3494  int xScrollbarMin = ui->graphicsView->horizontalScrollBar()->minimum();
3495  int xScrollbarMax = ui->graphicsView->horizontalScrollBar()->maximum();
3496  int xScrollbarOldValue = ui->graphicsView->horizontalScrollBar()->value();
3497  int xScrollbarNewValue = fmax(xScrollbarMin, fmin(xScrollbarMax, xScrollbarOldValue + dx));
3498 
3499  if (xScrollbarNewValue != xScrollbarOldValue)
3500  ui->graphicsView->horizontalScrollBar()->setValue(xScrollbarNewValue);
3501  }
3502 
3503  if (dy)
3504  {
3505  lastCursorLocationDuringCanvasDrag.setY(effectiveNewPos.y());
3506 
3507  int yScrollbarMin = ui->graphicsView->verticalScrollBar()->minimum();
3508  int yScrollbarMax = ui->graphicsView->verticalScrollBar()->maximum();
3509  int yScrollbarOldValue = ui->graphicsView->verticalScrollBar()->value();
3510  int yScrollbarNewValue = fmax(yScrollbarMin, fmin(yScrollbarMax, yScrollbarOldValue + dy));
3511 
3512 
3513  if (yScrollbarNewValue != yScrollbarOldValue)
3514  ui->graphicsView->verticalScrollBar()->setValue(yScrollbarNewValue);
3515  }
3516  }
3517 
3518  QMainWindow::mouseMoveEvent(event);
3519 }
3520 
3524 void VuoEditorWindow::initializeNodeLibrary(VuoCompiler *nodeLibraryCompiler, VuoNodeLibrary::nodeLibraryDisplayMode nodeLibraryDisplayMode, VuoNodeLibrary::nodeLibraryState nodeLibraryState, VuoNodeLibrary *floater)
3525 {
3526  ownedNodeLibrary = new VuoNodeLibrary(nodeLibraryCompiler, this, nodeLibraryDisplayMode);
3527  ownedNodeLibrary->setObjectName(composition? composition->getBase()->getMetadata()->getName().c_str() : "");
3528 
3529  nl = ownedNodeLibrary;
3530  transitionNodeLibraryConnections(NULL, nl);
3531 
3532  // Dock the library initially before setting it to the docking state dictated by global settings.
3533  // This ensures that if it is ever undocked, double-clicking on its title bar will re-dock it.
3534  addDockWidget(Qt::LeftDockWidgetArea, nl);
3535  conformToGlobalNodeLibraryVisibility(nodeLibraryState, floater, false);
3536 
3537  nodeLibraryMinimumWidth = nl->minimumWidth();
3538  nodeLibraryMaximumWidth = nl->maximumWidth();
3539 }
3540 
3544 void VuoEditorWindow::on_compositionInformation_triggered()
3545 {
3546  metadataEditor->show();
3547 }
3548 
3553 {
3554  composition->deselectAllCompositionComponents();
3556 }
3557 
3561 void VuoEditorWindow::on_zoomIn_triggered()
3562 {
3563  ui->graphicsView->scale(zoomRate,zoomRate);
3564 
3565  // Try to keep selected components visible.
3566  QRectF selectedItemsRect;
3567  foreach (QGraphicsItem *selectedComponent, composition->selectedItems())
3568  selectedItemsRect |= selectedComponent->sceneBoundingRect();
3569 
3570  if (!selectedItemsRect.isNull())
3571  ui->graphicsView->centerOn(selectedItemsRect.center());
3572 
3573  isZoomedToFit = false;
3574  updateToolbarElementUI();
3575 }
3576 
3580 void VuoEditorWindow::on_zoomOut_triggered()
3581 {
3582  ui->graphicsView->scale(1/zoomRate,1/zoomRate);
3583  isZoomedToFit = false;
3584  updateToolbarElementUI();
3585 }
3586 
3590 void VuoEditorWindow::on_zoom11_triggered()
3591 {
3592  bool zoomingIn = (ui->graphicsView->transform().m11() <= 1.0);
3593  ui->graphicsView->setTransform(QTransform());
3594 
3595  if (zoomingIn)
3596  {
3597  // Try to keep selected components visible.
3598  QRectF selectedItemsRect;
3599  foreach (QGraphicsItem *selectedComponent, composition->selectedItems())
3600  selectedItemsRect |= selectedComponent->sceneBoundingRect();
3601 
3602  if (!selectedItemsRect.isNull())
3603  ui->graphicsView->centerOn(selectedItemsRect.center());
3604  }
3605 
3606  isZoomedToFit = false;
3607  updateToolbarElementUI();
3608 }
3609 
3614 {
3615  QRectF itemsTightBoundingRect = (!composition->selectedItems().isEmpty()? composition->internalSelectedItemsBoundingRect() :
3616  composition->internalItemsBoundingRect());
3617  QRectF itemsBoundingRect = itemsTightBoundingRect.adjusted(-VuoEditorWindow::compositionMargin,
3618  -VuoEditorWindow::compositionMargin,
3619  VuoEditorWindow::compositionMargin,
3620  VuoEditorWindow::compositionMargin);
3621  ui->graphicsView->fitInView(itemsBoundingRect, Qt::KeepAspectRatio);
3622  updateSceneRect();
3623  isZoomedToFit = true;
3624  updateToolbarElementUI();
3625 }
3626 
3631 {
3632  QRectF itemsTightBoundingRect = (!composition->selectedItems().isEmpty()? composition->internalSelectedItemsBoundingRect() :
3633  composition->internalItemsBoundingRect());
3634 
3635  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
3636 
3637  // If the itemsBoundingRect is larger than the viewport, zoom out to fit.
3638  if ((viewportRect.width() < itemsTightBoundingRect.width()) || (viewportRect.height() < itemsTightBoundingRect.height()))
3640 
3641  // If the viewport just needs to be shifted in order to display all composition components, do that.
3642  else if (!viewportRect.contains(itemsTightBoundingRect))
3643  ui->graphicsView->ensureVisible(itemsTightBoundingRect, VuoEditorWindow::compositionMargin, VuoEditorWindow::compositionMargin);
3644 }
3645 
3646 void VuoEditorWindow::viewportFitReset()
3647 {
3648  isZoomedToFit = false;
3649 
3650  updateToolbarElementUI();
3651 }
3652 
3653 void VuoEditorWindow::on_saveComposition_triggered()
3654 {
3655  QString savedPath = windowFilePath();
3656  if (! VuoFileUtilities::fileExists(savedPath.toStdString()))
3657  on_saveCompositionAs_triggered();
3658  else
3659  {
3660  VUserLog("%s: Save", getWindowTitleWithoutPlaceholder().toUtf8().data());
3661  saveFile(savedPath);
3662  }
3663 }
3664 
3665 void VuoEditorWindow::on_saveCompositionAs_triggered()
3666 {
3667  string savedPath = saveCompositionAs().toUtf8().constData();
3668  if (savedPath.empty())
3669  return;
3670 }
3671 
3678 QString VuoEditorWindow::saveCompositionAs()
3679 {
3680  // Don't use QFileDialog::getSaveFileName() here since it doesn't present the window as a Mac OS X sheet --- https://lists.qt-project.org/pipermail/qt4-feedback/2009-February/000518.html
3681  QFileDialog d(this, Qt::Sheet);
3682 // d.setWindowModality(Qt::WindowModal); // Causes dialog to jump.
3683 
3684  if (VuoFileUtilities::fileExists(windowFilePath().toStdString()))
3685  d.setDirectory(windowFilePath());
3686 
3687  else
3688  {
3689  d.selectFile(QString(composition->getBase()->getMetadata()->getName().c_str())
3690  .append(".")
3692  }
3693 
3694  d.setAcceptMode(QFileDialog::AcceptSave);
3695 
3696  // Temporary for https://b33p.net/kosada/node/6762 :
3697  // Since the QFileDialog warns the user about identically named files *before*
3698  // adding the default extension instead of after, for now, don't have it append
3699  // a default extension at all. This way we know exactly what filename the user has
3700  // entered, and if there is a conflict only after we have added the default
3701  // extension, we can abort the save rather than overwriting the existing file.
3702  //d.setDefaultSuffix(VuoEditor::vuoCompositionFileExtension);
3703 
3704  QString savedPath = "";
3705  if (d.exec() == QDialog::Accepted)
3706  {
3707  savedPath = d.selectedFiles()[0];
3708  string dir, file, ext;
3709  VuoFileUtilities::splitPath(savedPath.toUtf8().constData(), dir, file, ext);
3710  VUserLog("%s: Save as %s.%s", getWindowTitleWithoutPlaceholder().toUtf8().data(), file.c_str(), ext.c_str());
3711  saveFileAs(savedPath);
3712  }
3713 
3715  return savedPath;
3716 }
3717 
3725 bool VuoEditorWindow::saveFileAs(const QString & savePath)
3726 {
3727  string dir, file, ext;
3728  VuoFileUtilities::splitPath(savePath.toUtf8().constData(), dir, file, ext);
3729  bool installingAsSubcomposition = (VuoFileUtilities::getUserModulesPath().c_str() == QFileInfo(dir.c_str()).canonicalFilePath());
3730 
3731  QDir newCompositionDir(QFileInfo(savePath).absoluteDir().canonicalPath());
3732 
3733  map<VuoPort *, string> modifiedPortConstantRelativePaths = composition->getPortConstantResourcePathsRelativeToDir(newCompositionDir);
3734  string modifiedIconPath = composition->getAppIconResourcePathRelativeToDir(newCompositionDir);
3735  bool iconPathChanged = (modifiedIconPath != composition->getBase()->getMetadata()->getIconURL());
3736 
3737  if (!installingAsSubcomposition && (!modifiedPortConstantRelativePaths.empty() || iconPathChanged))
3738  {
3739  // If the previous storage directory was the /tmp directory (as in the case of example compositions),
3740  // offer to copy resource files to the new directory; otherwise, offer to update resource paths referenced
3741  // within the composition so that they are correct when resolved relative to the new directory.
3742  QDir compositionDir = QDir(composition->getBase()->getDirectory().c_str());
3743  bool copyTmpFiles = (compositionDir.canonicalPath() == QDir(VuoFileUtilities::getTmpDir().c_str()).canonicalPath());
3744 
3745  map<VuoPort *, string> pathsToUpdate;
3746  QString copiedFileDetails = "";
3747  QString updatedPathDetails = "";
3748 
3749  // Update relative paths referenced within port constants.
3750  for (map <VuoPort *, string>::iterator i = modifiedPortConstantRelativePaths.begin(); i != modifiedPortConstantRelativePaths.end(); ++i)
3751  {
3752  VuoPort *port = i->first;
3753  string modifiedPath = i->second;
3754 
3755  // Check whether this is a /tmp file (so that we copy it rather than updating the path).
3756  QString origRelativePath = VuoText_makeFromString(port->getRenderer()->getConstantAsString().c_str());
3757  string origRelativeDir, file, ext;
3758  VuoFileUtilities::splitPath(origRelativePath.toUtf8().constData(), origRelativeDir, file, ext);
3759  string resourceFileName = file;
3760  if (!ext.empty())
3761  {
3762  resourceFileName += ".";
3763  resourceFileName += ext;
3764  }
3765  QString origAbsolutePath = compositionDir.filePath(QDir(origRelativeDir.c_str()).filePath(resourceFileName.c_str()));
3766 
3767  // Mark the file for copying or the path for updating, as appropriate.
3768  if (copyTmpFiles && VuoRendererComposition::isTmpFile(origAbsolutePath.toUtf8().constData()))
3769  copiedFileDetails.append(QString("%1\n").arg(origRelativePath));
3770  else
3771  {
3772  updatedPathDetails.append(QString("%1 → %2\n").arg(origRelativePath, VuoText_makeFromString(modifiedPath.c_str())));
3773  pathsToUpdate[port] = modifiedPath;
3774  }
3775  }
3776 
3777  // Update relative path to custom app icon.
3778  if (iconPathChanged)
3779  {
3780  QString origRelativePath = composition->getBase()->getMetadata()->getIconURL().c_str();
3781 
3782  // Check whether this is a /tmp file (so that we copy it rather than updating the path).
3783  string origRelativeDir, file, ext;
3784  VuoFileUtilities::splitPath(origRelativePath.toUtf8().constData(), origRelativeDir, file, ext);
3785  string resourceFileName = file;
3786  if (!ext.empty())
3787  {
3788  resourceFileName += ".";
3789  resourceFileName += ext;
3790  }
3791  QString origAbsolutePath = compositionDir.filePath(QDir(origRelativeDir.c_str()).filePath(resourceFileName.c_str()));
3792 
3793  // Mark the file for copying or the path for updating, as appropriate.
3794  if (copyTmpFiles && VuoRendererComposition::isTmpFile(origAbsolutePath.toUtf8().constData()))
3795  copiedFileDetails.append(QString("%1\n").arg(origRelativePath));
3796  else
3797  updatedPathDetails.append(QString("%1 → %2\n").arg(origRelativePath, modifiedIconPath.c_str()));
3798  }
3799 
3800  bool copyFiles = (!copiedFileDetails.isEmpty());
3801  bool updatePaths = (!updatedPathDetails.isEmpty());
3802  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3803  const QString updateSummary = "<p>" + tr("You're saving your composition to a different folder.") + "</p>"
3804  + "<p>"
3805  + (copyFiles
3806  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3807  ? tr("Do you want to copy the example files used by your composition?")
3808  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3809  : tr("Do you want to update the paths to files in your composition?"))
3810  + "</p>";
3811 
3812  QString updateDetails = "";
3813  if (copyFiles)
3814  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3815  updateDetails += tr("The following file(s) will be copied", "", modifiedPortConstantRelativePaths.size()) + ":\n\n" + copiedFileDetails + "\n";
3816 
3817  if (updatePaths)
3818  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3819  updateDetails += tr("The following path(s) will be updated", "", pathsToUpdate.size()) + ":\n\n" + updatedPathDetails;
3820 
3821  QMessageBox messageBox(this);
3822  messageBox.setWindowFlags(Qt::Sheet);
3823  messageBox.setWindowModality(Qt::WindowModal);
3824  messageBox.setTextFormat(Qt::RichText);
3825  messageBox.setStandardButtons(QMessageBox::Discard | QMessageBox::Ok);
3826  messageBox.setButtonText(QMessageBox::Discard, (copyFiles? tr("Leave files in place") : tr("Leave paths unchanged")));
3827  messageBox.setButtonText(QMessageBox::Ok, (copyFiles? tr("Copy files") : tr("Update paths")));
3828  messageBox.setDefaultButton(QMessageBox::Ok);
3829  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
3830  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
3831  messageBox.setText(updateSummary);
3832  messageBox.setDetailedText(updateDetails);
3833 
3834  // Give the "Leave in place" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
3835  static_cast<QPushButton *>(messageBox.button(QMessageBox::Discard))->setAutoDefault(false);
3836  messageBox.button(QMessageBox::Discard)->setFocus();
3837 
3838  if (messageBox.exec() == QMessageBox::Ok)
3839  {
3840  // Copy example resource files to the new directory.
3841  if (copyFiles)
3842  {
3843  bool tmpFilesOnly = true;
3844  composition->bundleResourceFiles(newCompositionDir.canonicalPath().toUtf8().constData(), tmpFilesOnly);
3845  }
3846  // Update relative resource paths.
3847  if (updatePaths)
3848  {
3849  undoStack->beginMacro(tr("File Path Updates"));
3850 
3851  // Update relative URLs referenced within port constants.
3852  for (map <VuoPort *, string>::iterator i = pathsToUpdate.begin(); i != pathsToUpdate.end(); ++i)
3853  {
3854  VuoPort *port = i->first;
3855  string modifiedPath = i->second;
3856 
3857  QString origPath = VuoText_makeFromString(port->getRenderer()->getConstantAsString().c_str());
3858  setPortConstant(port->getRenderer(), modifiedPath);
3859  }
3860 
3861 
3862  // Update relative path to custom icon.
3863  if (iconPathChanged)
3864  {
3865  string customizedCompositionName = composition->getBase()->getMetadata()->getCustomizedName();
3866 
3867  VuoCompositionMetadata *metadataCopy = new VuoCompositionMetadata(*composition->getBase()->getMetadata());
3868  metadataCopy->setName(customizedCompositionName);
3869  metadataCopy->setIconURL(modifiedIconPath);
3870 
3871  undoStack->push(new VuoCommandSetMetadata(metadataCopy, this));
3872 
3873  }
3874 
3875  undoStack->endMacro();
3876  }
3877  }
3878  }
3879 
3880  return saveFile(savePath);
3881 }
3882 
3883 string VuoEditorWindow::on_installSubcomposition_triggered()
3884 {
3885  QString oldTitle = getWindowTitleWithoutPlaceholder();
3886 
3887  string nodeClassName = installSubcomposition("");
3888 
3889  VUserLog("%s: %s: %s",
3890  oldTitle.toUtf8().data(),
3891  ui->installSubcomposition->text().toUtf8().data(),
3892  nodeClassName.c_str());
3893 
3894  if (!nodeClassName.empty())
3895  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
3896 
3897  return nodeClassName;
3898 }
3899 
3904 string VuoEditorWindow::installSubcomposition(string parentCompositionPath)
3905 {
3906  string currentCompositionPath = windowFilePath().toUtf8().constData();
3907  bool currentCompositionExists = VuoFileUtilities::fileExists(currentCompositionPath);
3908 
3909  QString operationTitle;
3910  string installedSubcompositionDir;
3911  if (parentCompositionPath.empty()) // "File > Move/Save to User Library"
3912  {
3913  if (currentCompositionExists)
3914  operationTitle = tr("Move Subcomposition to User Library");
3915  else
3916  operationTitle = tr("Save Subcomposition to User Library");
3917  installedSubcompositionDir = VuoFileUtilities::getUserModulesPath();
3918  }
3919  else // "Edit > Insert Subcomposition" or "Edit > Package as Subcomposition"
3920  {
3922  {
3923  operationTitle = tr("Save Subcomposition to User Library");
3924  installedSubcompositionDir = VuoFileUtilities::getUserModulesPath();
3925  }
3927  {
3928  operationTitle = tr("Save Subcomposition to System Library");
3929  installedSubcompositionDir = VuoFileUtilities::getSystemModulesPath();
3930  }
3931  else
3932  {
3933  operationTitle = tr("Save Subcomposition to Composition-Local Library");
3934  installedSubcompositionDir = VuoFileUtilities::getCompositionLocalModulesPath(parentCompositionPath);
3935  }
3936  }
3937 
3938  VuoFileUtilities::makeDir(installedSubcompositionDir);
3939  string nodeClassName;
3940 
3941  // Case: The composition is already installed as a module. Preserve its existing node class name.
3942  if (VuoFileUtilities::fileExists(currentCompositionPath) && VuoFileUtilities::isInstalledAsModule(currentCompositionPath))
3943  nodeClassName = VuoCompiler::getModuleKeyForPath(currentCompositionPath);
3944 
3945  // Case: The composition is being installed as a module for the first time. It needs a name.
3946  else
3947  {
3948  QString defaultNodeDisplayName = composition->formatCompositionFileNameForDisplay(composition->getBase()->getMetadata()->getDefaultName().c_str());
3949  QString defaultNodeCategory = static_cast<VuoEditor *>(qApp)->getDefaultSubcompositionPrefix();
3950 
3951  QString currentNodeDisplayName = (!composition->getBase()->getMetadata()->getCustomizedName().empty()?
3952  composition->getBase()->getMetadata()->getCustomizedName().c_str() :
3953  defaultNodeDisplayName);
3954  QString currentNodeCategory = static_cast<VuoEditor *>(qApp)->getSubcompositionPrefix();
3955 
3956  VuoSubcompositionSaveAsDialog d(this, Qt::Sheet, operationTitle,
3957  defaultNodeDisplayName, defaultNodeCategory,
3958  currentNodeDisplayName, currentNodeCategory);
3959  bool ok = d.exec();
3960  if (!ok)
3961  return "";
3962 
3963  QString nodeDisplayName = d.nodeTitle();
3964  QString nodeCategory = d.nodeCategory();
3965 
3966  if (nodeCategory != currentNodeCategory)
3967  static_cast<VuoEditor *>(qApp)->updateSubcompositionPrefix(nodeCategory);
3968 
3969  if (composition->getBase()->getMetadata()->getCustomizedName() != nodeDisplayName.toUtf8().constData())
3970  {
3971  VuoCompositionMetadata *newMetadata = new VuoCompositionMetadata(*composition->getBase()->getMetadata());
3972  newMetadata->setName(nodeDisplayName.toUtf8().constData());
3973  undoStack->push(new VuoCommandSetMetadata(newMetadata, this));
3974  }
3975 
3976  nodeClassName = getNodeClassNameForDisplayNameAndCategory(nodeDisplayName, nodeCategory, defaultNodeDisplayName, defaultNodeCategory).toStdString();
3977  }
3978 
3979  string copiedCompositionPath = installedSubcompositionDir + "/" + nodeClassName + "." + VuoEditor::vuoCompositionFileExtension.toUtf8().constData();
3980 
3981  // Make sure the node class doesn't already exist as a .vuo or .vuonode file in the same directory.
3982  int documentIdentifierInstanceNum = 1;
3983  while (VuoFileUtilities::fileExists(copiedCompositionPath) ||
3984  VuoFileUtilities::fileExists(copiedCompositionPath + "node"))
3985  {
3986  std::ostringstream oss;
3987  oss << ++documentIdentifierInstanceNum;
3988  copiedCompositionPath = installedSubcompositionDir + "/" + nodeClassName + oss.str() + "." + VuoEditor::vuoCompositionFileExtension.toUtf8().constData();
3989  }
3990 
3991  bool saveSucceeded = saveFileAs(copiedCompositionPath.c_str());
3992  if (! saveSucceeded)
3993  return "";
3994 
3995  VuoFileUtilities::deleteFile(currentCompositionPath);
3996 
3997  return VuoCompiler::getModuleKeyForPath(windowFilePath().toStdString());
3998 }
3999 
4004 QString VuoEditorWindow::getNodeClassNameForDisplayNameAndCategory(QString compositionName, QString category, QString defaultCompositionName, QString defaultCategory)
4005 {
4006  compositionName = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(compositionName.toStdString()));
4007  if (compositionName.isEmpty())
4008  compositionName = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(defaultCompositionName.toStdString()));
4009 
4010  // Treat "vuo" as a reserved prefix.
4011  QString thirdPartyCategory = category.remove(QRegExp("^(vuo\\.?)+", Qt::CaseInsensitive));
4012 
4013  category = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(thirdPartyCategory.toStdString()));
4014  if (category.isEmpty())
4015  category = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(defaultCategory.toStdString()));
4016 
4017  return category + "." + compositionName;
4018 }
4019 
4029 {
4030  if (displayName.empty())
4031  return "";
4032 
4033  // Transliterate Unicode to Latin ASCII, to provide some support for non-English-language names.
4034  {
4035  CFStringRef cfs = CFStringCreateWithCString(NULL, displayName.c_str(), kCFStringEncodingUTF8);
4036  if (!cfs)
4037  return "";
4038  CFMutableStringRef cfsm = CFStringCreateMutableCopy(NULL, 0, cfs);
4039  CFStringTransform(cfsm, NULL, kCFStringTransformToLatin, false); // Converts 'ä' -> 'a', '張' -> 'zhang'.
4040  CFStringTransform(cfsm, NULL, kCFStringTransformStripCombiningMarks, false);
4041  CFStringTransform(cfsm, NULL, kCFStringTransformStripDiacritics, false);
4042  CFStringTransform(cfsm, NULL, CFSTR("ASCII"), false); // Converts 'ß' -> 'ss'.
4043  displayName = VuoStringUtilities::makeFromCFString(cfsm);
4044  CFRelease(cfsm);
4045  CFRelease(cfs);
4046  }
4047 
4048  return VuoStringUtilities::convertToCamelCase(displayName, false, true, false, true);
4049 }
4050 
4055 void VuoEditorWindow::editMetadata(int dialogResult)
4056 {
4057  if (dialogResult == QDialog::Accepted)
4058  undoStack->push(new VuoCommandSetMetadata(metadataEditor->toMetadata(), this));
4059 }
4060 
4064 string VuoEditorWindow::generateCurrentDefaultCopyright()
4065 {
4066  const string user = static_cast<VuoEditor *>(qApp)->getUserName();
4067  const string userProfileURL = static_cast<VuoEditor *>(qApp)->getStoredUserProfileURL();
4068  const string userProfileLink = (userProfileURL.empty()? user : "[" + user + "](" + userProfileURL + ")");
4069  const string currentYear = QDate::currentDate().toString("yyyy").toUtf8().constData();
4070 
4071  const string copyright = "Copyright © " + currentYear + " " + userProfileLink;
4072  return copyright;
4073 }
4074 
4078 bool VuoEditorWindow::confirmReplacingFile(string path)
4079 {
4081  QFileInfo fileInfo(QString::fromStdString(path));
4082  QMessageBox d(this);
4083  d.setWindowFlags(Qt::Sheet);
4084  d.setWindowModality(Qt::WindowModal);
4085  d.setFont(fonts->dialogHeadingFont());
4086  d.setTextFormat(Qt::RichText);
4087  d.setText(tr("<b>“%1” already exists. Do you want to replace it?</b>").arg(fileInfo.fileName()));
4088  d.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>"
4089  + tr("<p>A %1 with the same name already exists in the “%2” folder.</p><p>Replacing it will overwrite its current contents.<br></p>")
4090  .arg(VuoFileUtilities::dirExists(path) ? "folder" : "file")
4091  .arg(fileInfo.dir().dirName()));
4092  d.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
4093  d.setDefaultButton(QMessageBox::Cancel);
4094  d.setButtonText(QMessageBox::Yes, tr("Replace"));
4095  d.setIcon(QMessageBox::Warning);
4096 
4097  // Give the "Replace" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
4098  static_cast<QPushButton *>(d.button(QMessageBox::Yes))->setAutoDefault(false);
4099  d.button(QMessageBox::Yes)->setFocus();
4100 
4101  return d.exec() == QMessageBox::Yes;
4102 }
4103 
4109 {
4110  addDockWidget(Qt::TopDockWidgetArea, searchBox);
4111  searchBox->setFocus();
4112  searchBox->show();
4113  updateUI();
4114 }
4115 
4120 {
4121  if (!searchBox->isHidden())
4122  searchBox->goToNextResult();
4123 }
4124 
4129 {
4130  if (!searchBox->isHidden())
4131  searchBox->goToPreviousResult();
4132 }
4133 
4140 bool VuoEditorWindow::saveFile(const QString & savePath)
4141 {
4142  // Temporary for https://b33p.net/kosada/node/6762 :
4143  // The file dialogue does not currently append the default file suffix automatically;
4144  // do so here. If the addition of the suffix results in a file-name conflict,
4145  // abort the save, since the user has not given permission to overwrite the existing composition.
4146  bool saveAborted = false;
4147  QString failureDetails = "";
4148  QString modifiedSavePath = savePath;
4149  QString expectedFileExtension = QString(".").append(VuoEditor::vuoCompositionFileExtension);
4150  if (!savePath.endsWith(expectedFileExtension))
4151  {
4152  modifiedSavePath.append(expectedFileExtension);
4153  if (VuoFileUtilities::fileExists(modifiedSavePath.toStdString()))
4154  {
4155  saveAborted = true;
4156  failureDetails = "A file or folder with the same name already exists.";
4157  }
4158  }
4159 
4160  QString existingPath = windowFilePath();
4161  bool saveSucceeded = !saveAborted && saveFile(modifiedSavePath, existingPath);
4162  int error = errno;
4163 
4164  if (saveSucceeded)
4165  {
4166  // Clear the "Untitled Composition" window title, since windowTitle overrides windowFilePath.
4167  if (windowTitle().startsWith(untitledComposition))
4168  setWindowTitle("");
4169 
4170  setWindowFilePath(modifiedSavePath);
4171 #if VUO_PRO
4172  toolbar->updateTitle();
4173 #endif
4174  compositionUpgradedSinceLastSave = false;
4175  undoStack->setClean();
4176  setCompositionModified(false);
4177 
4178  string oldDir = composition->getBase()->getDirectory();
4179  string newDir, file, ext;
4180  VuoFileUtilities::splitPath(modifiedSavePath.toUtf8().constData(), newDir, file, ext);
4181  bool compositionDirChanged = ! VuoFileUtilities::arePathsEqual(oldDir, newDir);
4182  if (compositionDirChanged)
4183  {
4184  // Find any composition-local modules that the composition depends on.
4185  QStringList compositionLocalModules;
4186  for (const string &dependency : compiler->getDependenciesForComposition(getComposition()->getBase()->getCompiler()))
4187  if (compiler->isCompositionLocalModule(dependency))
4188  compositionLocalModules.append(QString::fromStdString(dependency));
4189 
4190  if (! compositionLocalModules.empty())
4191  {
4192  compositionLocalModules.sort();
4193 
4194  QString summary = QString("<p>You're saving your composition to a different folder.</p>")
4195  .append("<p>Where do you want to install the composition's local nodes?</p>");
4196 
4197  QString details = QString("The following nodes will be moved or copied:\n\n")
4198  .append(compositionLocalModules.join("\n"))
4199  .append("\n");
4200 
4201  QMessageBox messageBox(this);
4202  messageBox.setWindowFlags(Qt::Sheet);
4203  messageBox.setWindowModality(Qt::WindowModal);
4204  messageBox.setTextFormat(Qt::RichText);
4205  messageBox.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
4206  messageBox.setButtonText(QMessageBox::No, "Move to User Library");
4207  messageBox.setButtonText(QMessageBox::Yes, "Copy to new Composition Library");
4208  messageBox.setDefaultButton(QMessageBox::Yes);
4209  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
4210  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
4211  messageBox.setText(summary);
4212  messageBox.setDetailedText(details);
4213 
4214  // Give the non-default button keyboard focus so that it can be activated by spacebar.
4215  static_cast<QPushButton *>(messageBox.button(QMessageBox::No))->setAutoDefault(false);
4216  messageBox.button(QMessageBox::No)->setFocus();
4217 
4218  int ret = messageBox.exec();
4219 
4220  string oldModulesDirPath = VuoFileUtilities::getCompositionLocalModulesPath(existingPath.toStdString());
4221  QDir oldModulesDir = QDir(QString::fromStdString(oldModulesDirPath));
4222 
4223  string newModulesDirPath = (ret == QMessageBox::Yes ?
4224  VuoFileUtilities::getCompositionLocalModulesPath(modifiedSavePath.toStdString()) :
4225  VuoFileUtilities::getUserModulesPath());
4226  VuoFileUtilities::makeDir(newModulesDirPath);
4227  QDir newModulesDir(QString::fromStdString(newModulesDirPath));
4228 
4229  // Move/copy the required composition-local modules from the old to the new Modules dir.
4230  foreach (QString moduleFileName, oldModulesDir.entryList(QDir::Files))
4231  {
4232  string moduleKey = VuoCompiler::getModuleKeyForPath(moduleFileName.toStdString());
4233 
4234  if (compositionLocalModules.contains(QString::fromStdString(moduleKey)))
4235  {
4236  string oldModulePath = oldModulesDir.filePath(moduleFileName).toStdString();
4237  string newModulePath = newModulesDir.filePath(moduleFileName).toStdString();
4238 
4239  // Skip if the file already exists in the new Modules dir.
4240  if (! VuoFileUtilities::fileExists(newModulePath))
4241  {
4242  if (ret == QMessageBox::Yes)
4243  VuoFileUtilities::copyFile(oldModulePath, newModulePath);
4244  else
4245  VuoFileUtilities::moveFile(oldModulePath, newModulePath);
4246  }
4247  }
4248  }
4249  }
4250  }
4251 
4252  // Update the compiler's list of loaded modules.
4253  compiler->setCompositionPath(modifiedSavePath.toUtf8().constData());
4254 
4255  if (includeInRecentFileMenu)
4256  static_cast<VuoEditor *>(qApp)->addFileToAllOpenRecentFileMenus(modifiedSavePath);
4257  }
4258 
4259  else
4260  {
4261  if (failureDetails.isEmpty())
4262  failureDetails = strerror(error);
4263 
4264  QMessageBox fileSaveFailureDialog(this);
4265  fileSaveFailureDialog.setWindowFlags(Qt::Sheet);
4266  fileSaveFailureDialog.setWindowModality(Qt::WindowModal);
4267  //: Appears in a dialog after selecting File > Save or Save As.
4268  QString errorMessage = tr("The composition could not be saved at “%1”.").arg(modifiedSavePath);
4269  fileSaveFailureDialog.setText(tr(errorMessage.toUtf8().constData()));
4270  fileSaveFailureDialog.setStyleSheet("#qt_msgbox_informativelabel { font-weight: normal; font-size: 11pt; }");
4271  fileSaveFailureDialog.setInformativeText(failureDetails);
4272  fileSaveFailureDialog.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
4273  fileSaveFailureDialog.setButtonText(QMessageBox::Save, tr("Save As…"));
4274  fileSaveFailureDialog.setIcon(QMessageBox::Warning);
4275 
4276  switch(fileSaveFailureDialog.exec()) {
4277  case QMessageBox::Save:
4278  on_saveCompositionAs_triggered();
4279  break;
4280  case QMessageBox::Cancel:
4281  break;
4282  default:
4283  break;
4284  }
4285  }
4286 
4287  return saveSucceeded;
4288 }
4289 
4293 void VuoEditorWindow::populateProtocolsMenu(QMenu *m)
4294 {
4295  foreach (VuoProtocol *protocol, VuoProtocol::getProtocols())
4296  {
4297  QAction *protocolAction = new QAction(this);
4298  protocolAction->setText(VuoEditor::tr(protocol->getName().c_str()));
4299  protocolAction->setData(qVariantFromValue(static_cast<void *>(protocol)));
4300  protocolAction->setCheckable(true);
4301  connect(protocolAction, &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
4302  m->addAction(protocolAction);
4303  }
4304 }
4305 
4309 void VuoEditorWindow::updateProtocolsMenu(QMenu *m)
4310 {
4311  foreach (QAction *protocolAction, m->actions())
4312  {
4313  VuoProtocol *currentProtocol = static_cast<VuoProtocol *>(protocolAction->data().value<void *>());
4314  protocolAction->setChecked(composition->getActiveProtocol() == currentProtocol);
4315  }
4316 }
4317 
4322 void VuoEditorWindow::changeActiveProtocol(void)
4323 {
4324  QAction *sender = (QAction *)QObject::sender();
4325  VuoProtocol *selectedProtocol = static_cast<VuoProtocol *>(sender->data().value<void *>());
4326 
4327  VUserLog("%s: %s protocol %s {",
4328  getWindowTitleWithoutPlaceholder().toUtf8().data(),
4329  composition->getActiveProtocol() == selectedProtocol ? "Remove" : "Add",
4330  selectedProtocol->getName().c_str());
4331 
4332  // @todo: Account for multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
4333  toggleActiveStatusForProtocol(selectedProtocol);
4334 
4335  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
4336 }
4337 
4341 void VuoEditorWindow::toggleActiveStatusForProtocol(VuoProtocol *protocol)
4342 {
4343  if (composition->getActiveProtocol() == protocol)
4344  composition->removeActiveProtocol(protocol, NULL);
4345  else
4346  composition->addActiveProtocol(protocol, true);
4347 }
4348 
4352 void VuoEditorWindow::updateActiveProtocolDisplay(void)
4353 {
4354  inputPortSidebar->updateActiveProtocol();
4355  outputPortSidebar->updateActiveProtocol();
4356  setPublishedPortSidebarVisibility(true);
4357  updateUI();
4358 }
4359 
4364 void VuoEditorWindow::evaluateCompositionForProtocolPromotion()
4365 {
4366  // @todo: Account for multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
4367  if (composition->getActiveProtocol())
4368  return;
4369 
4370  string compositionAsString = composition->takeSnapshot();
4371  foreach (VuoProtocol *protocol, VuoProtocol::getProtocols())
4372  {
4373  if (protocol->isCompositionCompliant(compositionAsString))
4374  {
4375  composition->addActiveProtocol(protocol, false);
4376  return;
4377  }
4378  }
4379 
4380  return;
4381 }
4382 
4390 bool VuoEditorWindow::saveFile(const QString & savePath, QString & existingPath)
4391 {
4392  string existingCompositionFooter = "";
4393 
4394  // Modifying an existing composition
4395  if (! existingPath.isEmpty())
4396  {
4397  ifstream existingFile(existingPath.toUtf8().constData());
4398 
4399  // Preserve the original composition footer (everything after the final '}').
4400  bool graphvizStatementListEndFound = false;
4401  for (char c = existingFile.get(); existingFile.good(); c = existingFile.get())
4402  {
4403  if (existingFile.good())
4404  {
4405  if (c == '}')
4406  {
4407  existingCompositionFooter = "";
4408  graphvizStatementListEndFound = true;
4409  }
4410  else if (graphvizStatementListEndFound)
4411  existingCompositionFooter += c;
4412  }
4413  }
4414  existingFile.close();
4415  }
4416 
4417  // Update the version of Vuo last used to save the composition.
4418  string previousLastSavedInVuoVersion = composition->getBase()->getMetadata()->getLastSavedInVuoVersion();
4419  composition->getBase()->getMetadata()->setLastSavedInVuoVersion(VUO_VERSION_STRING);
4420 
4421  // Generate the modified composition.
4422  string compositionHeader = composition->generateCompositionHeader();
4423  string outputComposition = composition->getBase()->getCompiler()->getGraphvizDeclaration(composition->getActiveProtocol(),
4424  compositionHeader,
4425  existingCompositionFooter);
4426  // Save the modified composition to a temporary file.
4427  string dir, file, ext;
4428  string finalSavePath = savePath.toUtf8().constData();
4429  VuoFileUtilities::splitPath(finalSavePath, dir, file, ext);
4430  string tmpSavePath = VuoFileUtilities::makeTmpFile("." + file, ext, dir);
4431 
4432  // The mode of temp files defaults to 0600.
4433  // Change it to match the process's umask.
4434  {
4435  // http://man7.org/linux/man-pages/man2/umask.2.html says:
4436  // "It is impossible to use umask() to fetch a process's umask without at
4437  // the same time changing it. A second call to umask() would then be
4438  // needed to restore the umask."
4439  mode_t currentUMask = umask(0);
4440  umask(currentUMask);
4441 
4442  chmod(tmpSavePath.c_str(), 0666 & ~currentUMask);
4443  }
4444 
4445  ofstream savedFile(tmpSavePath.c_str(), ios::trunc);
4446  savedFile << outputComposition;
4447  savedFile.close();
4448 
4449  string defaultName = VuoEditorComposition::getDefaultNameForPath(finalSavePath);
4450  composition->getBase()->getMetadata()->setDefaultName(defaultName);
4451 
4452  // Move the generated temporary file to the desired save path.
4453  bool saveSucceeded = ((! savedFile.fail()) && (! rename(tmpSavePath.c_str(), finalSavePath.c_str())));
4454 
4455  if (saveSucceeded)
4456  {
4457  this->composition->getBase()->setDirectory(dir);
4458  this->metadataPanel->setIsUserComposition(true);
4459  this->metadataPanel->update();
4460  }
4461  else
4462  this->composition->getBase()->getMetadata()->setLastSavedInVuoVersion(previousLastSavedInVuoVersion);
4463 
4464  return saveSucceeded;
4465 }
4466 
4470 void VuoEditorWindow::undoStackCleanStateChanged(bool clean)
4471 {
4472  // When the Undo stack leaves its clean state, mark the composition as modified.
4473  if (!clean)
4474  setCompositionModified(true);
4475 
4476  // Mark the composition as unmodified when the Undo stack enters
4477  // its clean state as long as the composition has not been modified by the
4478  // upgrade manager since its last save.
4479  else if (clean && !compositionUpgradedSinceLastSave)
4480  setCompositionModified(false);
4481 }
4482 
4486 void VuoEditorWindow::setCompositionModified(bool modified)
4487 {
4488  setWindowModified(modified);
4489  updateUI();
4490 }
4491 
4496 {
4497  VUserLog("%s: Run", getWindowTitleWithoutPlaceholder().toUtf8().data());
4498 
4499  toolbar->changeStateToBuildPending();
4500 
4501  string snapshot = composition->takeSnapshot();
4502  composition->run(snapshot);
4503 
4504  updateUI();
4505 }
4506 
4511 {
4512  VUserLog("%s: Stop", getWindowTitleWithoutPlaceholder().toUtf8().data());
4513 
4514  toolbar->changeStateToStopInProgress();
4515  updateUI();
4516 
4517  composition->stop();
4518 }
4519 
4524 {
4527 }
4528 
4533 {
4534  composition->refireTriggerPortEvent();
4535 }
4536 
4540 void VuoEditorWindow::showBuildActivityIndicator()
4541 {
4542  toolbar->changeStateToBuildInProgress();
4543  updateUI();
4544 }
4545 
4549 void VuoEditorWindow::hideBuildActivityIndicator(QString buildError)
4550 {
4551  if (! buildError.isEmpty())
4552  {
4553  toolbar->changeStateToStopped();
4554  updateUI();
4555  updateRefireAction();
4556 
4557  QString details = "";
4558  if (buildError.contains("Nodes not installed", Qt::CaseInsensitive))
4559  details = "This composition contains nodes that aren't installed.";
4560 
4561  VuoErrorDialog::show(this, tr("There was a problem running the composition."), details, buildError);
4562  }
4563  else
4564  {
4565  toolbar->changeStateToRunning();
4566  updateUI();
4567  updateRefireAction();
4568  }
4569 }
4570 
4574 void VuoEditorWindow::hideStopActivityIndicator()
4575 {
4576  toolbar->changeStateToStopped();
4577  updateUI();
4578  updateRefireAction();
4579 }
4580 
4581 void VuoEditorWindow::on_openUserModulesFolder_triggered()
4582 {
4584 }
4585 
4586 void VuoEditorWindow::on_openSystemModulesFolder_triggered()
4587 {
4589 }
4590 
4595 {
4596  if (nl->isHidden())
4598 
4599  nl->focusTextFilter();
4600 }
4601 
4605 void VuoEditorWindow::on_showPublishedPorts_triggered(void)
4606 {
4607  setPublishedPortSidebarVisibility(!arePublishedPortSidebarsVisible());
4608  updateUI();
4609 }
4610 
4616 {
4617  if (!arePublishedPortSidebarsVisible())
4618  on_showPublishedPorts_triggered();
4619 }
4620 
4624 bool VuoEditorWindow::arePublishedPortSidebarsVisible()
4625 {
4626  return !inputPortSidebar->isHidden() && !outputPortSidebar->isHidden();
4627 }
4628 
4632 void VuoEditorWindow::on_showHiddenCables_triggered(void)
4633 {
4634  bool previouslyShowingHiddenCables = composition->getRenderHiddenCables();
4635  if (!previouslyShowingHiddenCables)
4636  {
4637  if (composition->hasHiddenPublishedCables() && (inputPortSidebar->isHidden() || outputPortSidebar->isHidden()))
4638  on_showPublishedPorts_triggered();
4639  }
4640 
4641  composition->setRenderHiddenCables(!previouslyShowingHiddenCables);
4642 
4643  updateUI();
4644 }
4645 
4649 void VuoEditorWindow::on_showEvents_triggered(void)
4650 {
4651  VUserLog("%s: %s Show Events mode",
4652  getWindowTitleWithoutPlaceholder().toUtf8().data(),
4653  composition->getShowEventsMode() ? "Disable" : "Enable");
4654 
4655  composition->setShowEventsMode(! composition->getShowEventsMode());
4656  updateUI();
4657 }
4658 
4662 void VuoEditorWindow::closePublishedPortSidebars()
4663 {
4664  setPublishedPortSidebarVisibility(false);
4665 }
4666 
4670 void VuoEditorWindow::conditionallyShowPublishedPortSidebars(bool visible)
4671 {
4672  if (visible)
4673  setPublishedPortSidebarVisibility(true);
4674 }
4675 
4679 void VuoEditorWindow::setPublishedPortSidebarVisibility(bool visible)
4680 {
4681  // Display the published input and output port sidebars as widgets docked in the left
4682  // and right docking areas, respectively.
4683  if (visible)
4684  {
4685  // If the node library is already docked in the left docking area, situate the
4686  // input port sidebar between the node library and the canvas.
4687  if (nl && (! nl->isHidden()) && (! nl->isFloating()))
4688  splitDockWidget(nl, inputPortSidebar, Qt::Horizontal);
4689 
4690  else
4691  addDockWidget(Qt::LeftDockWidgetArea, inputPortSidebar);
4692 
4693  addDockWidget(Qt::RightDockWidgetArea, outputPortSidebar);
4694 
4695  this->setFocus();
4696  }
4697 
4698  else
4699  {
4700  // Prevent the docked node library from expanding to fill all available space when
4701  // there are changes to the input port sidebar's visibility.
4702  if (! nl->isFloating())
4703  {
4704  nl->setMinimumWidth(nl->width());
4705  nl->setMaximumWidth(nl->minimumWidth());
4706  }
4707  }
4708 
4709  inputPortSidebar->setVisible(visible);
4710  outputPortSidebar->setVisible(visible);
4711 
4712  updatePublishedCableGeometry();
4713  inputPortSidebar->updatePortList();
4714  outputPortSidebar->updatePortList();
4715 
4716  if (!nl->isHidden() && !nl->isFloating())
4717  nl->fixWidth(visible);
4718 
4719  // Ensure port bounding boxes are large enough to include the antennae for hidden cables.
4720  composition->updateGeometryForAllComponents();
4721 
4722  updateUI();
4723 }
4724 
4729 void VuoEditorWindow::updatePublishedCableGeometry()
4730 {
4731  QGraphicsItem::CacheMode defaultCacheMode = composition->getCurrentDefaultCacheMode();
4732 
4733  set<VuoCable *> cables = composition->getBase()->getCables();
4734  foreach (VuoCable *cable, cables)
4735  {
4736  if (cable->isPublished())
4737  {
4738  cable->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
4739  cable->getRenderer()->updateGeometry();
4740  cable->getRenderer()->setCacheMode(defaultCacheMode);
4741  }
4742  }
4743 
4744  // Updating the entire viewport here prevents published cable artifacts from
4745  // remaining behind, e.g., after scrolling, but (@todo:) handle this with
4746  // more precision.
4747  if (!inputPortSidebar->isHidden() || !outputPortSidebar->isHidden())
4748  ui->graphicsView->viewport()->update();
4749 
4750  composition->repaintFeedbackErrorMarks();
4751 }
4752 
4756 void VuoEditorWindow::updatePublishedPortOrder(vector<VuoPublishedPort *> ports, bool isInput)
4757 {
4758  // If the requested ordering is identical to the current ordering, do nothing.
4759  if (isInput && (ports == composition->getBase()->getPublishedInputPorts()))
4760  return;
4761 
4762  if (!isInput && (ports == composition->getBase()->getPublishedOutputPorts()))
4763  return;
4764 
4765  // If the requested change would place non-protocol ports above protocol ports, ignore the request.
4766  bool foundNonProtocolPort = false;
4767  foreach (VuoPublishedPort *port, ports)
4768  {
4769  if (!port->isProtocolPort())
4770  foundNonProtocolPort = true;
4771  else
4772  {
4773  if (foundNonProtocolPort)
4774  return;
4775  }
4776  }
4777 
4778  undoStack->push(new VuoCommandReorderPublishedPorts(ports, isInput, this));
4779 }
4780 
4785 void VuoEditorWindow::displayDockedNodeLibrary()
4786 {
4787  addDockWidget(Qt::LeftDockWidgetArea, nl);
4788 
4789  bool publishedPortsDisplayed = (inputPortSidebar && (! inputPortSidebar->isHidden()));
4790  if (publishedPortsDisplayed)
4791  splitDockWidget(nl, inputPortSidebar, Qt::Horizontal);
4792 
4793  nl->setFloating(false);
4794  nl->fixWidth(publishedPortsDisplayed);
4795  nl->prepareAndMakeVisible();
4796  nl->setFocus();
4797 }
4798 
4803 {
4804  // Prevent the input port sidebar from expanding to fill all available space when there are changes
4805  // to the node library's docking status and/or visibility.
4806  inputPortSidebar->setMinimumWidth(inputPortSidebar->width());
4807  inputPortSidebar->setMaximumWidth(inputPortSidebar->minimumWidth());
4808 
4809  if (visibility == VuoNodeLibrary::nodeLibraryDocked)
4810  {
4811  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4812  displayDockedNodeLibrary();
4814  }
4815 
4816  else if (visibility == VuoNodeLibrary::nodeLibraryFloating)
4817  {
4818  nl->fixWidth(false);
4819 
4820  // If our own node library was the one that initiated global
4821  // floating-node-library mode by being undocked, let it float.
4822  // It is now the single application-wide floating library.
4823  if (nl == floater)
4824  {
4825  if (ownedNodeLibrary == floater)
4826  {
4827  if (! nl->isFloating())
4828  {
4829  nl->setFloating(true);
4830  }
4831 
4832  nl->prepareAndMakeVisible();
4833  nl->setFocus();
4835  }
4836  }
4837 
4838  // Otherwise, hide it and adopt the global floater as our own.
4839  else
4840  {
4841  ownedNodeLibrary->releaseDocumentationWidget();
4842  ownedNodeLibrary->setVisible(false);
4843  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4844  assignSurrogateNodeLibrary(floater);
4845  }
4846  }
4847 
4848  else if (visibility == VuoNodeLibrary::nodeLibraryHidden)
4849  {
4850  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4851  nl->setVisible(false);
4852  }
4853 
4854  updateUI();
4855 }
4856 
4862 {
4863  // Only display the composition information in the metadata pane if the relevant composition metadata has been changed from the default,
4864  // or if the composition has been saved.
4865  bool compositionHasCustomizedDescription = (!composition->getBase()->getMetadata()->getDescription().empty() && (composition->getBase()->getMetadata()->getDescription() != VuoRendererComposition::deprecatedDefaultDescription));
4866  bool compositionHasCustomizedCopyright = (!composition->getBase()->getMetadata()->getCopyright().empty() && (composition->getBase()->getMetadata()->getCopyright() != generateCurrentDefaultCopyright()));
4867  bool compositionHasOtherCustomizedMetadata = (
4868  !composition->getBase()->getMetadata()->getCustomizedName().empty() ||
4869  !composition->getBase()->getMetadata()->getHomepageURL().empty() ||
4870  !composition->getBase()->getMetadata()->getDocumentationURL().empty());
4871  bool compositionHasBeenSaved = !windowFilePath().isEmpty();
4872  if (compositionHasCustomizedDescription || compositionHasCustomizedCopyright || compositionHasOtherCustomizedMetadata || compositionHasBeenSaved)
4873  {
4874  dispatch_async(((VuoEditor *)qApp)->getDocumentationQueue(), ^{
4875  QMetaObject::invokeMethod(nl, "displayPopoverInPane", Qt::QueuedConnection, Q_ARG(QWidget *, metadataPanel));
4876  });
4877  }
4878  else
4880 }
4881 
4885 void VuoEditorWindow::closeEvent(QCloseEvent *event)
4886 {
4887  // If already in the process of closing, don't redo work.
4888  if (closing)
4889  {
4890  event->accept();
4891  return;
4892  }
4893 
4894  // If an input editor or popup menu is open, ignore the user's click on the composition window's red X, since it leads to a crash.
4895  // https://b33p.net/kosada/node/15831
4896  if (inputEditorSession
4897  || composition->getMenuSelectionInProgress()
4898  || inputPortSidebar->getMenuSelectionInProgress()
4899  || outputPortSidebar->getMenuSelectionInProgress())
4900  {
4901  event->ignore();
4902  return;
4903  }
4904 
4905  if (isWindowModified())
4906  {
4907  auto mb = new QMessageBox(this);
4908  mb->setWindowFlags(Qt::Sheet);
4909  mb->setWindowModality(Qt::WindowModal);
4910 
4911  //: Appears in the File > Close dialog.
4912  QString message = tr("Do you want to save the changes made to the document “%1”?").arg(getWindowTitleWithoutPlaceholder());
4913  //: Appears in the File > Close dialog.
4914  QString details = tr("Your changes will be lost if you don’t save them.");
4915  mb->setText(tr(message.toUtf8().constData()));
4916  mb->setStyleSheet("#qt_msgbox_informativelabel { font-weight: normal; font-size: 11pt; }");
4917  mb->setInformativeText(tr(details.toUtf8().constData()));
4918  mb->setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
4919 
4920  mb->setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
4921  mb->setDefaultButton(QMessageBox::Save);
4922 
4923  // Give the "Discard" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
4924  static_cast<QPushButton *>(mb->button(QMessageBox::Discard))->setAutoDefault(false);
4925  mb->button(QMessageBox::Discard)->setFocus();
4926 
4927  if (windowFilePath().isEmpty())
4928  mb->setButtonText(QMessageBox::Save, tr("Save As…"));
4929 
4930  connect(mb, &QMessageBox::buttonClicked, this, [=](QAbstractButton *button){
4931  auto editor = static_cast<VuoEditor *>(qApp);
4932  auto role = mb->buttonRole(button);
4933  if (role == QMessageBox::AcceptRole)
4934  {
4935  on_saveComposition_triggered();
4936  if (isWindowModified())
4937  editor->cancelQuit();
4938  else
4939  {
4940  acceptCloseEvent();
4941  editor->continueQuit(this);
4942  }
4943  }
4944  else if (role == QMessageBox::DestructiveRole)
4945  {
4946  acceptCloseEvent();
4947  editor->continueQuit(this);
4948  }
4949  else // if (role == QMessageBox::RejectRole)
4950  editor->cancelQuit();
4951  });
4952 
4953  mb->open();
4954 
4955  event->ignore();
4956  }
4957  else // if (! isWindowModified())
4958  {
4959  acceptCloseEvent();
4960  event->accept();
4961  static_cast<VuoEditor *>(qApp)->continueQuit(this);
4962  }
4963 }
4964 
4968 void VuoEditorWindow::acceptCloseEvent()
4969 {
4970  if (composition->isRunning())
4971  {
4972  connect(composition, &VuoEditorComposition::stopFinished, this, &VuoEditorWindow::deleteLater);
4974  }
4975  else
4976  VuoEditorWindow::deleteLater();
4977 
4978  composition->disablePopovers();
4979  searchBox->close();
4980 
4981  closing = true;
4982 
4983  if (VuoFileUtilities::fileExists(windowFilePath().toStdString()))
4984  static_cast<VuoEditor *>(qApp)->addFileToRecentlyClosedList(windowFilePath());
4985 }
4986 
4991 void VuoEditorWindow::updateSelectedComponentMenuItems()
4992 {
4993  string cutCommandText = tr("Cut").toStdString();
4994  string copyCommandText = tr("Copy").toStdString();
4995  string duplicateCommandText = tr("Duplicate").toStdString();
4996  string deleteCommandText = tr("Delete").toStdString();
4997  string resetCommandText = tr("Reset").toStdString();
4998 
4999  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
5000 
5001  int selectedNodesFound = 0;
5002  int selectedCablesFound = 0;
5003  int selectedListsFound = 0;
5004  int selectedCommentsFound = 0;
5005 
5006  VuoRendererNode *foundNode = NULL;
5007 
5008  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin();
5009  (! (selectedNodesFound && selectedCablesFound)) &&
5010  (i != selectedCompositionComponents.end());
5011  ++i)
5012  {
5013  QGraphicsItem *compositionComponent = *i;
5014 
5015  VuoRendererInputDrawer *rl = dynamic_cast<VuoRendererInputDrawer *>(compositionComponent);
5016  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
5017  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
5018  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
5019 
5020  if (rl && !rl->paintingDisabled())
5021  selectedListsFound++;
5022 
5023  else if (rn && !rn->paintingDisabled())
5024  {
5025  foundNode = rn;
5026  selectedNodesFound++;
5027  }
5028 
5029  else if (rc && !rc->paintingDisabled())
5030  selectedCablesFound++;
5031 
5032  else if (rcomment)
5033  selectedCommentsFound++;
5034  }
5035 
5036  int distinctComponentTypesFound = 0;
5037  if (selectedListsFound)
5038  distinctComponentTypesFound++;
5039  if (selectedNodesFound)
5040  distinctComponentTypesFound++;
5041  if (selectedCablesFound)
5042  distinctComponentTypesFound++;
5043  if (selectedCommentsFound)
5044  distinctComponentTypesFound++;
5045 
5046  if (selectedListsFound && (distinctComponentTypesFound == 1))
5047  {
5048  string selectedListText = "Selected List";
5049  string selectedListsText = "Selected Lists";
5050 
5051  ui->cutCompositionComponents->setText(tr(cutCommandText.c_str()));
5052  ui->copyCompositionComponents->setText(tr(copyCommandText.c_str()));
5053  ui->duplicateCompositionComponents->setText(tr(duplicateCommandText.c_str()));
5054 
5055  if (selectedListsFound > 1)
5056  {
5057  ui->deleteCompositionComponents->setText(tr((resetCommandText + " " + selectedListsText).c_str()));
5058  composition->getContextMenuDeleteSelectedAction()->setText(tr((resetCommandText + " " + selectedListsText).c_str()));
5059  }
5060  else
5061  {
5062  ui->deleteCompositionComponents->setText(tr((resetCommandText + " " + selectedListText).c_str()));
5063  composition->getContextMenuDeleteSelectedAction()->setText(tr((resetCommandText + " " + selectedListText).c_str()));
5064  }
5065  }
5066  else
5067  {
5068  ui->cutCompositionComponents->setText(tr(cutCommandText.c_str()));
5069  ui->copyCompositionComponents->setText(tr(copyCommandText.c_str()));
5070  ui->duplicateCompositionComponents->setText(tr(duplicateCommandText.c_str()));
5071  ui->deleteCompositionComponents->setText(tr(deleteCommandText.c_str()));
5072  composition->getContextMenuDeleteSelectedAction()->setText(tr(deleteCommandText.c_str()));
5073  }
5074 
5075  bool enableSelectedComponentDeleteMenuItem = (!selectedCompositionComponents.isEmpty());
5076  bool enableSelectedComponentCutMenuItem = (!selectedCompositionComponents.isEmpty() &&
5077  !(selectedListsFound && (distinctComponentTypesFound == 1)));
5078  bool enableSelectedComponentDuplicateMenuItem = ((selectedNodesFound || selectedCommentsFound) &&
5079  !(selectedListsFound && (distinctComponentTypesFound == 1)));
5080 
5081  bool copyAppliesToNodeLibraryDocumentation = (nl && !nl->getSelectedDocumentationText().isEmpty());
5082  bool enableCopyMenuItem = (copyAppliesToNodeLibraryDocumentation || enableSelectedComponentDuplicateMenuItem);
5083 
5084  ui->cutCompositionComponents->setEnabled(enableSelectedComponentCutMenuItem);
5085  ui->copyCompositionComponents->setEnabled(enableCopyMenuItem);
5086  ui->duplicateCompositionComponents->setEnabled(enableSelectedComponentDuplicateMenuItem);
5087  ui->deleteCompositionComponents->setEnabled(enableSelectedComponentDeleteMenuItem);
5088 
5089  ui->renameNodes->setEnabled(selectedNodesFound);
5090  ui->refactor->setEnabled(selectedNodesFound || selectedCommentsFound);
5091  contextMenuTints->setEnabled(selectedNodesFound || selectedCommentsFound);
5092 
5093  // Update "Change (Node) To" submenu
5094  ui->menuEdit->removeAction(menuChangeNode->menuAction());
5095 
5096  bool menuChangeNodeDisplayed = false;
5097  if ((selectedNodesFound == 1) && !selectedCommentsFound)
5098  {
5099  composition->populateChangeNodeMenu(menuChangeNode, foundNode);
5100  if (menuChangeNode->actions().size() > 0)
5101  {
5102  ui->menuEdit->insertMenu(ui->refactor, menuChangeNode);
5103  menuChangeNode->setEnabled(true);
5104  menuChangeNodeDisplayed = true;
5105  }
5106  }
5107  // Rather than including an empty submenu to indicate that this option is disabled,
5108  // display some disabled (gray) placeholder text in the parent menu.
5109  ui->changeNodePlaceholder->setVisible(!menuChangeNodeDisplayed);
5110 }
5111 
5116 bool VuoEditorWindow::containsLikelyVuoComposition(QString text)
5117 {
5118  const QString compositionIndicatorText = "digraph";
5119  return (text.toCaseFolded().contains(compositionIndicatorText.toCaseFolded()));
5120 }
5121 
5126 void VuoEditorWindow::ensureSceneRectContainsRegion(const QList<QRectF> &region)
5127 {
5128  // Autoscroll during cable drags.
5129  if (composition->getCableInProgress())
5130  {
5131  updateSceneRect();
5132  return;
5133  }
5134 
5135 
5136  bool regionContained = true;
5137  QRectF sceneRect = composition->sceneRect();
5138  foreach (QRectF rect, region)
5139  {
5140  if (!sceneRect.contains(rect))
5141  {
5142  regionContained = false;
5143  break;
5144  }
5145  }
5146 
5147  if (!regionContained)
5148  updateSceneRect();
5149 
5150  return;
5151 }
5152 
5157 void VuoEditorWindow::updateSceneRect()
5158 {
5159  // Disable sceneRect updates while menu option selection is in progress so that
5160  // the scene doesn't shift underneath the menu (e.g., after a
5161  // cable drag autoscrolled the canvas).
5162  if (composition->getMenuSelectionInProgress())
5163  return;
5164 
5165  // Enforce a margin around each edge of the composition when possible.
5166  const int horizontalMargin = 0.2 * ui->graphicsView->geometry().width();
5167  const int verticalMargin = 0.2 * ui->graphicsView->geometry().height();
5168 
5169  int horizontalScrollBarHeight = ui->graphicsView->horizontalScrollBar()->sizeHint().height();
5170  int verticalScrollBarWidth = ui->graphicsView->verticalScrollBar()->sizeHint().width();
5171 
5172  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
5173  QRectF itemsBoundingRect = composition->internalItemsBoundingRect().adjusted(-horizontalMargin, -verticalMargin, horizontalMargin, verticalMargin);
5174  QRectF viewportItemsUnionRect;
5175 
5176  // If the itemsBoundingRect is fully contained within the viewport (horizontally),
5177  // do not alter the sceneRect in this dimension.
5178  if (viewportRect.left() <= itemsBoundingRect.left() && viewportRect.right() >= itemsBoundingRect.right())
5179  {
5180  viewportItemsUnionRect.setLeft(viewportRect.left());
5181  viewportItemsUnionRect.setRight(viewportRect.right());
5182  }
5183 
5184  // Otherwise, if the discrepancy is greater at the left, align the sceneRect with the right edge
5185  // of the itemsBoundingRect to maximize stability.
5186  else if (abs(viewportRect.left() - itemsBoundingRect.left()) > abs(itemsBoundingRect.right() - viewportRect.right()))
5187  {
5188  viewportItemsUnionRect.setRight(itemsBoundingRect.right());
5189  viewportItemsUnionRect.setLeft(itemsBoundingRect.right() - max(itemsBoundingRect.width(), viewportRect.width()));
5190  }
5191 
5192  // If the discrepancy is greater at the right, align the sceneRect with the left edge
5193  // of the itemsBoundingRect to maximize stability.
5194  else
5195  {
5196  viewportItemsUnionRect.setLeft(itemsBoundingRect.left());
5197  viewportItemsUnionRect.setRight(itemsBoundingRect.left() + max(itemsBoundingRect.width(), viewportRect.width()));
5198  }
5199 
5200  // If the itemsBoundingRect is fully contained within the viewport (vertically),
5201  // do not alter the sceneRect in this dimension.
5202  if (viewportRect.top() <= itemsBoundingRect.top() && viewportRect.bottom() >= itemsBoundingRect.bottom())
5203  {
5204  viewportItemsUnionRect.setTop(viewportRect.top());
5205  viewportItemsUnionRect.setBottom(viewportRect.bottom());
5206  }
5207 
5208  // Otherwise, if the discrepancy is greater at the top, align the sceneRect with the bottom edge
5209  // of the itemsBoundingRect to maximize stability.
5210  else if (abs(viewportRect.top() - itemsBoundingRect.top()) > abs(itemsBoundingRect.bottom() - viewportRect.bottom()))
5211  {
5212  viewportItemsUnionRect.setBottom(itemsBoundingRect.bottom());
5213  viewportItemsUnionRect.setTop(itemsBoundingRect.bottom() - max(itemsBoundingRect.height(), viewportRect.height()));
5214  }
5215 
5216  // If the discrepancy is greater at the bottom, align the sceneRect with the top edge
5217  // of the itemsBoundingRect to maximize stability.
5218  else
5219  {
5220  viewportItemsUnionRect.setTop(itemsBoundingRect.top());
5221  viewportItemsUnionRect.setBottom(itemsBoundingRect.top() + max(itemsBoundingRect.height(), viewportRect.height()));
5222  }
5223 
5224  int viewportItemsUnionRectAdjustedWidth = viewportItemsUnionRect.width();
5225  int viewportItemsUnionRectAdjustedHeight = viewportItemsUnionRect.height();
5226 
5227  if ((viewportItemsUnionRect.width() > viewportRect.width()) &&
5228  (viewportItemsUnionRect.width() <= viewportRect.width() + verticalScrollBarWidth))
5229  {
5230  // Prevent the horizontal scrollbar from toggling into and out of existence as the scene is updated.
5231  viewportItemsUnionRectAdjustedWidth = viewportRect.width() + verticalScrollBarWidth + 1;
5232 
5233  // Prevent the introduction of the horizontal scrollbar from triggering the vertical scrollbar.
5234  viewportItemsUnionRectAdjustedHeight -= horizontalScrollBarHeight;
5235  }
5236 
5237  if ((viewportItemsUnionRect.height() > viewportRect.height()) &&
5238  (viewportItemsUnionRect.height() <= viewportRect.height() + horizontalScrollBarHeight))
5239  {
5240  // Prevent the vertical scrollbar from toggling into and out of existence as the scene is updated.
5241  viewportItemsUnionRectAdjustedHeight = viewportRect.height() + horizontalScrollBarHeight + 1;
5242 
5243  // Prevent the introduction of the vertical scrollbar from triggering the horizontal scrollbar.
5244  viewportItemsUnionRectAdjustedWidth -= verticalScrollBarWidth;
5245  }
5246 
5247  QRectF viewportItemsUnionRectAdjusted(viewportItemsUnionRect.topLeft().x(),
5248  viewportItemsUnionRect.topLeft().y(),
5249  viewportItemsUnionRectAdjustedWidth,
5250  viewportItemsUnionRectAdjustedHeight);
5251 
5252  // Do not allow the scene to shrink while the left mouse button is held down
5253  // (e.g., while a node is being dragged).
5254  if ((QApplication::mouseButtons() & Qt::LeftButton))
5255  viewportItemsUnionRectAdjusted = viewportItemsUnionRectAdjusted.united(composition->sceneRect());
5256  else
5257  viewportItemsUnionRectAdjusted = viewportItemsUnionRectAdjusted.united(viewportRect);
5258 
5259  VuoCable *cableBeingDragged = composition->getCableInProgress();
5260 
5261  // Cable drags should not cause the scene to grow.
5262  if (!cableBeingDragged)
5263  composition->setSceneRect(viewportItemsUnionRectAdjusted);
5264 
5265  // Cable drags should, however, auto-scroll the viewport under certain circumstances.
5266  if (cableBeingDragged)
5267  {
5268  QPointF cableFloatingEndpointSceneLoc = cableBeingDragged->getRenderer()->getFloatingEndpointLoc();
5269  VuoRendererPort *cableFixedPort = (cableBeingDragged->getFromPort()?
5270  cableBeingDragged->getFromPort()->getRenderer() :
5271  cableBeingDragged->getToPort()?
5272  cableBeingDragged->getToPort()->getRenderer() :
5273  NULL);
5274 
5275  // Autoscroll should normally be disabled if the cursor is positioned above the
5276  // canvas or one of the published port sidebars.
5277  QRect inputPortSidebarRect = inputPortSidebar->geometry();
5278  inputPortSidebarRect.moveTopLeft(inputPortSidebar->parentWidget()->mapToGlobal(inputPortSidebarRect.topLeft()+QPoint(1,0)));
5279 
5280  QRect outputPortSidebarRect = outputPortSidebar->geometry();
5281  outputPortSidebarRect.moveTopLeft(outputPortSidebar->parentWidget()->mapToGlobal(outputPortSidebarRect.topLeft()));
5282 
5283  QRect viewportRectGlobal = ui->graphicsView->viewport()->geometry();
5284  viewportRectGlobal.moveTopLeft(ui->graphicsView->viewport()->parentWidget()->mapToGlobal(viewportRectGlobal.topLeft()));
5285 
5286  QRect noAutoscrollZone = QRect();
5287  if (!inputPortSidebar->isHidden())
5288  noAutoscrollZone |= inputPortSidebarRect;
5289  if (!outputPortSidebar->isHidden())
5290  noAutoscrollZone |= outputPortSidebarRect;
5291  if (!inputPortSidebar->isHidden() || !outputPortSidebar->isHidden())
5292  noAutoscrollZone |= viewportRectGlobal;
5293 
5294  QPoint cursorPosition = QCursor::pos();
5295  bool cursorWithinNoAutoscrollZone = noAutoscrollZone.contains(cursorPosition);
5296  bool cursorWithinNoAutoscrollZoneLeft = (cursorWithinNoAutoscrollZone &&
5297  ((cursorPosition.x() - noAutoscrollZone.left()) < (noAutoscrollZone.right() - cursorPosition.x())));
5298 
5299  // Make an exception and allow the autoscroll if it would bring the cable's
5300  // connected node closer to being back within the viewport.
5301  bool autoscrollWouldBringSourceNodeCloser = false;
5302  if (cableFixedPort && !dynamic_cast<VuoRendererPublishedPort *>(cableFixedPort))
5303  {
5304  VuoRendererNode *fixedNode = cableFixedPort->getUnderlyingParentNode();
5305  QRectF fixedNodeBoundingRect = fixedNode->boundingRect().adjusted(-horizontalMargin, -verticalMargin, horizontalMargin, verticalMargin);
5306  bool fixedNodeVisible = ((fixedNodeBoundingRect.left()+fixedNode->scenePos().x() >= viewportRect.left()) &&
5307  (fixedNodeBoundingRect.right()+fixedNode->scenePos().x() <= viewportRect.right()));
5308 
5309  autoscrollWouldBringSourceNodeCloser = (!fixedNodeVisible && (cursorWithinNoAutoscrollZoneLeft ==
5310  (fixedNodeBoundingRect.left()+fixedNode->scenePos().x() < viewportRect.left())));
5311  }
5312 
5313  bool enableCableDragAutoscroll = (!cursorWithinNoAutoscrollZone || autoscrollWouldBringSourceNodeCloser);
5314  if (enableCableDragAutoscroll)
5315  {
5316  ui->graphicsView->ensureVisible(cableFloatingEndpointSceneLoc.x(),
5317  cableFloatingEndpointSceneLoc.y(),
5318  0,
5319  VuoEditorWindow::compositionMargin);
5320  }
5321  }
5322 }
5323 
5328 void VuoEditorWindow::updateRubberBandSelectionMode(QRect rubberBandRect, QPointF fromScenePoint, QPointF toScenePoint)
5329 {
5330  bool rubberBandSelectionPreviouslyInProgress = this->rubberBandSelectionInProgress;
5331  bool rubberBandSelectionNowInProgress = !(rubberBandRect.isNull() && fromScenePoint.isNull() && toScenePoint.isNull());
5332 
5333  bool beginningRubberBandSelection = (!rubberBandSelectionPreviouslyInProgress && rubberBandSelectionNowInProgress);
5334  bool endingRubberBandSelection = (rubberBandSelectionPreviouslyInProgress && !rubberBandSelectionNowInProgress);
5335 
5336  if (beginningRubberBandSelection)
5337  {
5338  // Disable selection of comment bodies (non-title-handle areas) during rubberband drags.
5339  foreach (VuoComment *comment, composition->getBase()->getComments())
5340  if (comment->hasRenderer())
5341  comment->getRenderer()->setBodySelectable(false);
5342 
5343  // Disable selection of cables during rubberband drags, unless the relevant keyboard modifier was pressed.
5344  if (!lastLeftMousePressHadOptionModifier)
5345  {
5346  foreach (VuoCable *cable, composition->getBase()->getCables())
5347  if (cable->hasRenderer())
5348  cable->getRenderer()->setSelectable(false);
5349  }
5350  }
5351  else if (endingRubberBandSelection)
5352  {
5353  foreach (VuoComment *comment, composition->getBase()->getComments())
5354  if (comment->hasRenderer())
5355  comment->getRenderer()->setBodySelectable(true);
5356 
5357  if (!lastLeftMousePressHadOptionModifier)
5358  {
5359  foreach (VuoCable *cable, composition->getBase()->getCables())
5360  if (cable->hasRenderer())
5361  cable->getRenderer()->setSelectable(true);
5362  }
5363  }
5364 
5365  this->rubberBandSelectionInProgress = rubberBandSelectionNowInProgress;
5366 }
5367 
5371 void VuoEditorWindow::setPortConstant(VuoRendererPort *port, string value)
5372 {
5373  if (value != port->getConstantAsString())
5374  {
5375  VuoCompilerInputEventPort *eventPort = dynamic_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler());
5376  if (eventPort)
5377  undoStack->push(new VuoCommandSetPortConstant(eventPort, value, this));
5378  }
5379 }
5380 
5384 void VuoEditorWindow::showInputEditor(VuoRendererPort *port)
5385 {
5386  showInputEditor(port, true);
5387 }
5388 
5392 void VuoEditorWindow::showInputEditor(VuoRendererPort *port, bool forwardTabTraversal)
5393 {
5394  VuoPublishedPortSidebar *sidebar = nullptr;
5395  VuoRendererPublishedPort *publishedPort = dynamic_cast<VuoRendererPublishedPort *>(port);
5396  if (publishedPort)
5397  sidebar = (publishedPort->getInput() ? outputPortSidebar : inputPortSidebar);
5398 
5399  inputEditorSession = new VuoInputEditorSession(inputEditorManager, composition, sidebar, this);
5400  map<VuoRendererPort *, pair<string, string> > originalAndFinalValueForPort = inputEditorSession->execute(port, forwardTabTraversal);
5401 
5402  delete inputEditorSession;
5403  inputEditorSession = nullptr;
5404 
5405  bool startedUndoStackMacro = false;
5406  for (auto i : originalAndFinalValueForPort)
5407  {
5408  VuoRendererPort *port = i.first;
5409  string originalEditingSessionValue = i.second.first;
5410  string finalEditingSessionValue = i.second.second;
5411 
5412  // Now that the editing session is over, check whether the port constant ended up with
5413  // a different value than it had initially. If so, push the change onto the Undo stack.
5414  if (finalEditingSessionValue != originalEditingSessionValue)
5415  {
5416  VuoCompilerPort *compilerPort = static_cast<VuoCompilerPort *>(port->getBase()->getCompiler());
5417 
5418  // Briefly set the constant back to its original value so that the Undo stack
5419  // has the information it needs to revert it properly. An alternative would be to
5420  // modify the VuoCommandSetPortConstant constructor to accept a revertedValue argument.
5421  composition->updatePortConstant(compilerPort, originalEditingSessionValue, false);
5422 
5423  if (!startedUndoStackMacro)
5424  {
5425  undoStack->beginMacro(tr("Set Port Constant"));
5426  startedUndoStackMacro = true;
5427  }
5428 
5429  undoStack->push(new VuoCommandSetPortConstant(compilerPort, finalEditingSessionValue, this));
5430 
5431  if (composition->requiresStructuralChangesAfterValueChangeAtPort(port))
5432  {
5433  VuoRendererNode *parentNode = port->getRenderedParentNode();
5434 
5435  // Only current possibility: modifications to "Calculate" node's 'expression' input
5436  string nodeClassName = parentNode->getBase()->getNodeClass()->getClassName();
5437  vector<string> inputVariablesBeforeEditing = composition->extractInputVariableListFromExpressionsConstant(originalEditingSessionValue, nodeClassName);
5438  vector<string> inputVariablesAfterEditing = composition->extractInputVariableListFromExpressionsConstant(finalEditingSessionValue, nodeClassName);
5439 
5440  // Don't make any structural changes if the variables in the input expression remain
5441  // the same, even if the expression itself has changed.
5442  if (inputVariablesBeforeEditing != inputVariablesAfterEditing)
5443  {
5444  VuoPort *valuesPort = port->getRenderedParentNode()->getBase()->getInputPortWithName("values");
5445 
5446  set<VuoRendererInputAttachment *> attachments = valuesPort->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
5447 
5448  QList<QGraphicsItem *> attachmentsToRemove;
5449 
5450  VuoRendererReadOnlyDictionary *oldDictionary = nullptr;
5451  VuoRendererValueListForReadOnlyDictionary *oldValueList = nullptr;
5452  VuoRendererKeyListForReadOnlyDictionary *oldKeyList = nullptr;
5453  foreach (VuoRendererInputAttachment *attachment, attachments)
5454  {
5455  attachmentsToRemove.append(attachment);
5456 
5457  if (dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment))
5458  oldDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment);
5459 
5460  else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment))
5461  oldValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment);
5462 
5463  else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment))
5464  oldKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment);
5465  }
5466 
5467  if (oldValueList && oldDictionary && oldKeyList)
5468  {
5469  set<VuoRendererNode *> nodesToAdd;
5470  set<VuoRendererCable *> cablesToAdd;
5471  composition->createAndConnectDictionaryAttachmentsForNode(parentNode->getBase(), nodesToAdd, cablesToAdd);
5472 
5473  VuoRendererReadOnlyDictionary *newDictionary = nullptr;
5474  VuoRendererValueListForReadOnlyDictionary *newValueList = nullptr;
5475  VuoRendererKeyListForReadOnlyDictionary *newKeyList = nullptr;
5476  foreach (VuoRendererNode *node, nodesToAdd)
5477  {
5478  if (dynamic_cast<VuoRendererReadOnlyDictionary *>(node))
5479  newDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(node);
5480 
5481  else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node))
5482  newValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node);
5483 
5484  else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node))
5485  newKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node);
5486  }
5487 
5488  undoStack->push(new VuoCommandReplaceNode(oldValueList, newValueList, this, "Set Port Constant", false, false));
5489  undoStack->push(new VuoCommandReplaceNode(oldKeyList, newKeyList, this, "Set Port Constant", false, true));
5490  undoStack->push(new VuoCommandReplaceNode(oldDictionary, newDictionary, this, "Set Port Constant", false, true));
5491 
5492  foreach (VuoRendererCable *cable, cablesToAdd)
5493  {
5494  cable->setFrom(nullptr, nullptr);
5495  cable->setTo(nullptr, nullptr);
5496  }
5497  }
5498  }
5499  }
5500  }
5501 
5502  // If the port constant ended up with the same value as it had initially
5503  // (either because the user dismissed the editor by pressing 'Esc' or
5504  // re-committed the initial value), do not push the change onto the Undo
5505  // stack, but do revert to the original value if there were intermediate
5506  // changes while the input editor was open.
5507  else if (port->getConstantAsString() != originalEditingSessionValue)
5508  {
5509  VuoCompilerPort *compilerPort = static_cast<VuoCompilerPort *>(port->getBase()->getCompiler());
5510  composition->updatePortConstant(compilerPort, originalEditingSessionValue);
5511  }
5512  }
5513 
5514  if (startedUndoStackMacro)
5515  undoStack->endMacro();
5516 }
5517 
5521 void VuoEditorWindow::showNodeTitleEditor(VuoRendererNode *node)
5522 {
5523  if (! node->getBase()->hasCompiler())
5524  return;
5525 
5526  composition->setPopoverEventsEnabled(false);
5527 
5528  VuoTitleEditor *titleEditor = new VuoTitleEditor();
5529  string originalValue = node->getBase()->getTitle();
5530 
5531  // Position the input editor overtop the node's title.
5532  QRectF nodeBoundingRect = node->boundingRect();
5533  QPointF nodeTitleRightCenterInScene = node->scenePos() + nodeBoundingRect.topRight() + QPointF(0, VuoRendererNode::nodeTitleHeight/2.);
5534  QPoint nodeTitleRightCenterInView = ui->graphicsView->mapFromScene(nodeTitleRightCenterInScene);
5535  QPoint nodeTitleRightCenterGlobal = ui->graphicsView->mapToGlobal(nodeTitleRightCenterInView);
5536 
5537  // Set the input editor's width to match the node's.
5538  int nodeWidth = node->boundingRect().width();
5539  int scaledWidth = ui->graphicsView->transform().m11() * nodeWidth;
5540  titleEditor->setWidth(scaledWidth);
5541 
5542  json_object *details = json_object_new_object();
5543  VuoEditor *editor = (VuoEditor *)qApp;
5544  json_object_object_add(details, "isDark", json_object_new_boolean(editor->isInterfaceDark()));
5545 
5546  json_object *originalValueAsJson = json_object_new_string(originalValue.c_str());
5547  json_object *newValueAsJson = titleEditor->show(nodeTitleRightCenterGlobal, originalValueAsJson, details);
5548  string newValue = json_object_get_string(newValueAsJson);
5549  json_object_put(originalValueAsJson);
5550  json_object_put(newValueAsJson);
5551 
5552  // If the user has deleted the title, revert to the node class default title.
5553  if (newValue == "")
5554  newValue = node->getBase()->getNodeClass()->getDefaultTitleWithoutSuffix();
5555 
5556  if (newValue != originalValue)
5557  undoStack->push(new VuoCommandSetNodeTitle(node->getBase()->getCompiler(), newValue, this));
5558 
5559  composition->setPopoverEventsEnabled(true);
5560 }
5561 
5565 void VuoEditorWindow::showCommentEditor(VuoRendererComment *comment)
5566 {
5567  string originalText = comment->getBase()->getContent();
5568 
5569  json_object *details = json_object_new_object();
5570  VuoEditor *editor = (VuoEditor *)qApp;
5571  json_object_object_add(details, "isDark", json_object_new_boolean(editor->isInterfaceDark()));
5572 
5573  json_object *originalTextAsJson = json_tokener_parse(originalText.c_str());
5574 
5575  VuoCommentEditor *commentEditor = new VuoCommentEditor();
5576 
5577  // Set the text editor's width to match the comment's.
5578  int commentWidth = comment->boundingRect().width();
5579  int scaledWidth = ui->graphicsView->transform().m11() * commentWidth;
5580  commentEditor->setWidth(scaledWidth);
5581 
5582  // Set the text editor's height to match the comment's.
5583  int commentHeight= comment->boundingRect().height();
5584  int scaledHeight = ui->graphicsView->transform().m11() * commentHeight;
5585  commentEditor->setHeight(scaledHeight);
5586 
5587  // Position the text editor overtop the comment.
5588  QRectF commentBoundingRect = comment->boundingRect();
5589  QPointF commentTextRightCenterInScene = comment->scenePos() + commentBoundingRect.topRight() + QPointF(0, commentBoundingRect.height()/2.0);
5590  QPoint commentTextRightCenterInView = ui->graphicsView->mapFromScene(commentTextRightCenterInScene);
5591  QPoint commentTextRightCenterGlobal = ui->graphicsView->mapToGlobal(commentTextRightCenterInView);
5592 
5593  json_object *newTextAsJson = commentEditor->show(commentTextRightCenterGlobal, originalTextAsJson, details);
5594 
5595  string newText = json_object_to_json_string_ext(newTextAsJson, JSON_C_TO_STRING_PLAIN);
5596  json_object_put(originalTextAsJson);
5597  json_object_put(newTextAsJson);
5598 
5599  if (newText != originalText)
5600  undoStack->push(new VuoCommandSetCommentText(comment, newText, this));
5601 }
5602 
5606 void VuoEditorWindow::zoomToFitComment(VuoRendererComment *comment)
5607 {
5608  comment->setSelected(true);
5610 }
5611 
5615 void VuoEditorWindow::changePublishedPortName(VuoRendererPublishedPort *port, string newName, bool useUndoStack)
5616 {
5617  if (useUndoStack)
5618  undoStack->push(new VuoCommandSetPublishedPortName(port, newName, this));
5619  else
5620  composition->setPublishedPortName(port, newName);
5621 }
5622 
5626 void VuoEditorWindow::changePublishedPortDetails(VuoRendererPublishedPort *port, json_object *newDetails)
5627 {
5628  undoStack->push(new VuoCommandSetPublishedPortDetails(dynamic_cast<VuoRendererPublishedPort *>(port), newDetails, this));
5629 }
5630 
5634 void VuoEditorWindow::openEditableSourceForNode(VuoRendererNode *node)
5635 {
5636  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5637  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5638  {
5639  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5640  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
5641  if (originalGenericNodeClass)
5642  nodeClass = originalGenericNodeClass->getBase();
5643  }
5644 
5645  QString actionText, sourcePath;
5646  if (!VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
5647  return;
5648 
5649  // If the node is an installed subcomposition or shader, open its source for editing within Vuo.
5650  if (nodeClass->getCompiler()->isSubcomposition() || nodeClass->getCompiler()->isIsf())
5651  {
5652  QMainWindow *window = static_cast<VuoEditor *>(qApp)->openFileWithName(sourcePath, false);
5653  if (nodeClass->getCompiler()->isSubcomposition())
5654  {
5655  VuoEditorWindow *compositionWindow = dynamic_cast<VuoEditorWindow *>(window);
5656  if (compositionWindow)
5657  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(compositionWindow->getComposition(), getComposition(), node);
5658  }
5659  }
5660  // If the node has other editable source code, open it in the appropriate application.
5661  else
5662  static_cast<VuoEditor *>(qApp)->openUrl("file://" + sourcePath);
5663 }
5664 
5670 {
5671  this->includeInRecentFileMenu = include;
5672 }
5673 
5678 void VuoEditorWindow::on_insertComment_triggered()
5679 {
5680  insertCommentAtPos(getFittedScenePos(getCursorScenePos()));
5681 }
5682 
5686 void VuoEditorWindow::insertCommentAtPos(QPointF targetScenePos)
5687 {
5688  // Create an empty comment.
5689  VuoRendererComment *comment = composition->createRendererComment(
5690  (new VuoCompilerComment(new VuoComment("",
5691  targetScenePos.x(),
5692  targetScenePos.y(),
5695  )))->getBase());
5696 
5697  // Add the comment to the composition.
5698  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
5699  componentsToAdd.append(comment);
5700  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "Insert Comment"));
5701 
5702  // Open the comment immediately for editing.
5703  showCommentEditor(comment);
5704 }
5705 
5709 void VuoEditorWindow::on_insertSubcomposition_triggered()
5710 {
5711  insertSubcompositionAtPos(getFittedScenePos(getCursorScenePos()-
5713 }
5714 
5722 bool VuoEditorWindow::ensureThisParentCompositionSaved()
5723 {
5724  if (!VuoFileUtilities::fileExists(windowFilePath().toUtf8().constData()))
5725  {
5726  QMessageBox messageBox(this);
5727  messageBox.setWindowFlags(Qt::Sheet);
5728  messageBox.setWindowModality(Qt::WindowModal);
5729  messageBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Save);
5730  messageBox.setDefaultButton(QMessageBox::Save);
5731  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
5732  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
5733  messageBox.setText("Save before packaging");
5734  messageBox.setInformativeText("Please save this composition so Vuo knows where to save your new subcomposition.");
5735  messageBox.setButtonText(QMessageBox::Save, tr("Save As…"));
5736 
5737  // Give the non-default button keyboard focus so that it can be activated by spacebar.
5738  static_cast<QPushButton *>(messageBox.button(QMessageBox::Cancel))->setAutoDefault(false);
5739  messageBox.button(QMessageBox::Cancel)->setFocus();
5740 
5741  // Give the user a chance to cancel subcomposition installation without saving.
5742  if (messageBox.exec() != QMessageBox::Save)
5743  return false;
5744 
5745  // Present the "Save As" dialogue for the user to save the parent composition.
5746  on_saveCompositionAs_triggered();
5747  }
5748 
5749  return VuoFileUtilities::fileExists(windowFilePath().toUtf8().constData());
5750 }
5751 
5755 void VuoEditorWindow::insertSubcompositionAtPos(QPointF targetScenePos)
5756 {
5757  VUserLog("%s: Insert subcomposition {", getWindowTitleWithoutPlaceholder().toUtf8().data());
5758 
5759  // If the parent composition has not been saved to the filesystem, prompt the user to do so now
5760  // so that the subcomposition can be installed in a composition-local Modules directory.
5761  if (!ensureThisParentCompositionSaved())
5762  {
5763  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5764  return;
5765  }
5766 
5767  string subcompositionContent = "digraph G {}";
5768  VuoEditorWindow *subcompositionWindow = static_cast<VuoEditor *>(qApp)->newCompositionWithContent(subcompositionContent);
5769  if (!subcompositionWindow)
5770  {
5771  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5772  return;
5773  }
5774  subcompositionWindow->setIncludeInRecentFileMenu(false);
5775  subcompositionWindow->showPublishedPortSidebars();
5776  subcompositionWindow->setAsActiveWindow();
5777 
5778  string nodeClassName = subcompositionWindow->installSubcomposition(windowFilePath().toUtf8().constData());
5779  if (!nodeClassName.empty())
5780  {
5781  VuoModuleManager::CallbackType subcompositionAdded = ^void (void) {
5782  composition->getModuleManager()->getNodeLibrary()->highlightNodeClass(nodeClassName);
5783 
5784  VuoRendererNode *subcompositionNode = composition->createNode(nodeClassName.c_str(), "",
5785  targetScenePos.x(),
5786  targetScenePos.y());
5787  if (!subcompositionNode)
5788  {
5789  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5790  return;
5791  }
5792 
5793  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(subcompositionWindow->getComposition(),
5794  getComposition(),
5795  subcompositionNode);
5796 
5797  // Add the new subcomposition node.
5798  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
5799  componentsToAdd.append(subcompositionNode);
5800  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "Insert Subcomposition"));
5801 
5802  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5803  };
5804 
5805  // The order of the following two lines matters -- the second callback registered
5806  // for a given module manager and node class replaces the first.
5807  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
5808  composition->getModuleManager()->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionAdded);
5809  }
5810  else
5811  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5812 }
5813 
5817 void VuoEditorWindow::refactorSelectedItems()
5818 {
5819  VUserLog("%s: Package as subcomposition {", getWindowTitleWithoutPlaceholder().toUtf8().data());
5820 
5821  // If the parent composition has not been saved to the filesystem, prompt the user to do so now
5822  // so that the subcomposition can be installed in a composition-local Modules directory.
5823  if (!ensureThisParentCompositionSaved())
5824  {
5825  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5826  return;
5827  }
5828 
5829  string subcompositionContent = getMaximumSubcompositionFromSelection(false, false);
5830 
5831  QPoint selectedItemsAvgPos = QPoint(0,0);
5832  int selectedItemCount = 0;
5833 
5834  set<string> portsToPublish;
5835  __block map<int, string> inputCablesToRestoreFromPort;
5836  __block map<int, string> inputCablesToRestoreToPort;
5837  __block map<int, bool> inputCablesToRestoreEventOnly;
5838  __block map<int, string> outputCablesToRestoreFromPort;
5839  __block map<int, string> outputCablesToRestoreToPort;
5840  __block map<int, bool> outputCablesToRestoreEventOnly;
5841 
5842  QList<QGraphicsItem *> selectedItems = composition->selectedItems();
5843  foreach (QGraphicsItem *item, selectedItems)
5844  {
5845  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
5846  if (node)
5847  {
5848  // Determine which input ports to publish.
5849  foreach (VuoPort *port, node->getBase()->getInputPorts())
5850  {
5851  if (!dynamic_cast<VuoRendererTypecastPort *>(port->getRenderer()))
5852  {
5853  vector<VuoCable *> incomingCables = port->getConnectedCables(true);
5854  foreach (VuoCable *cable, incomingCables)
5855  {
5856  bool fromNodeIsExternal = (cable->getFromNode()->hasRenderer() &&
5857  !cable->getFromNode()->getRenderer()->isSelected());
5858  if (fromNodeIsExternal || cable->isPublishedInputCable())
5859  {
5860  portsToPublish.insert(composition->getIdentifierForStaticPort(cable->getToPort()));
5861 
5862  int cableNum = inputCablesToRestoreEventOnly.size();
5863  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(cable->getFromPort());
5864  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(cable->getToPort());
5865  inputCablesToRestoreEventOnly[cableNum] = cable->getCompiler()->getAlwaysEventOnly();
5866  }
5867  }
5868  } // end if not an attached typecast
5869  else // if attached typecast
5870  {
5871  VuoRendererNode *typecastNode = dynamic_cast<VuoRendererTypecastPort *>(port->getRenderer())->getUncollapsedTypecastNode();
5872  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
5873  vector<VuoCable *> incomingCables = typecastInPort->getConnectedCables(true);
5874 
5875  // If the input port has an attached typecast, determine whether the typecast's incoming cables will be
5876  // internal or external to the refactored subcomposition, or some combination of internal and external.
5877  bool typecastHasInternalInput = false;
5878  bool typecastHasExternalInput = false;
5879  foreach (VuoCable *typecastInCable, incomingCables)
5880  {
5881  if ((typecastInCable->getFromNode() && typecastInCable->getFromNode()->hasRenderer() &&
5882  typecastInCable->getFromNode()->getRenderer()->isSelected()))
5883  typecastHasInternalInput = true;
5884  else
5885  typecastHasExternalInput = true;
5886  }
5887 
5888  // Case: All of the typecast’s incoming cables will be external.
5889  // The typecast’s host port needs to be published and reconnected to the soon-to-be-external typecast’s output port.
5890  // The soon-to-be-external typecast needs to be uncollapsed.
5891  if (!typecastHasInternalInput)
5892  {
5893  VuoPort *typecastOutPort = typecastNode->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
5894  VuoPort *typecastHostPort = port;
5895 
5896  portsToPublish.insert(composition->getIdentifierForStaticPort(typecastHostPort));
5897 
5898  int cableNum = inputCablesToRestoreEventOnly.size();
5899  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(typecastOutPort);
5900  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(typecastHostPort);
5901  inputCablesToRestoreEventOnly[cableNum] = false;
5902 
5903  composition->uncollapseTypecastNode(typecastNode);
5904  }
5905 
5906  // Case: Some of the typecast's incoming cables will be external, some internal.
5907  // The soon-to-be internal typecast's child port needs to be published and reconnected to its soon-to-be external input sources.
5908  else if (typecastHasExternalInput && typecastHasInternalInput)
5909  {
5910  portsToPublish.insert(composition->getIdentifierForStaticPort(typecastInPort));
5911 
5912  foreach (VuoCable *typecastInCable, incomingCables)
5913  {
5914  bool externalInput = !(typecastInCable->getFromNode() && typecastInCable->getFromNode()->hasRenderer() &&
5915  typecastInCable->getFromNode()->getRenderer()->isSelected());
5916  if (externalInput)
5917  {
5918  int cableNum = inputCablesToRestoreEventOnly.size();
5919  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(typecastInCable->getFromPort());
5920  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(typecastInPort);
5921  inputCablesToRestoreEventOnly[cableNum] = false;
5922  }
5923  }
5924  }
5925 
5926  // Case: All of the typecast's incoming cables will be internal.
5927  // The soon-to-be-internal typecast need not have either its child port or its host port published, nor any
5928  // external connections restored.
5929  //else if (!typecastHasExternalInput)
5930  //{}
5931  }
5932  }
5933 
5934  // Determine which output ports to publish.
5935  foreach (VuoPort *port, node->getBase()->getOutputPorts())
5936  {
5937  foreach (VuoCable *cable, port->getConnectedCables(true))
5938  {
5939  bool toNodeIsExternal = (cable->getToNode()->hasRenderer() &&
5940  !dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort()) &&
5941  !cable->getToNode()->getRenderer()->isSelected());
5942  bool connectedTypecastIsExternal = false;
5943  if (dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort()))
5944  {
5945  VuoRendererNode *typecastNode = dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort())->getUncollapsedTypecastNode();
5946  VuoPort *typecastOutPort = typecastNode->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
5947  vector<VuoCable *> outCables = typecastOutPort->getConnectedCables(true);
5948 
5949  connectedTypecastIsExternal = ((outCables.size() < 1) || !outCables[0]->getToNode() ||
5950  !outCables[0]->getToNode()->hasRenderer() ||
5951  !outCables[0]->getToNode()->getRenderer()->isSelected());
5952  if (connectedTypecastIsExternal)
5953  composition->uncollapseTypecastNode(typecastNode);
5954  }
5955 
5956  if (toNodeIsExternal || connectedTypecastIsExternal || cable->isPublishedOutputCable())
5957  {
5958  portsToPublish.insert(composition->getIdentifierForStaticPort(cable->getFromPort()));
5959 
5960  int cableNum = outputCablesToRestoreEventOnly.size();
5961  outputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(cable->getFromPort());
5962  outputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(cable->getToPort());
5963  outputCablesToRestoreEventOnly[cableNum] = cable->getCompiler()->getAlwaysEventOnly();
5964  }
5965  }
5966  }
5967 
5968  selectedItemsAvgPos += item->scenePos().toPoint();
5969  selectedItemCount++;
5970  }
5971 
5972  else if (dynamic_cast<VuoRendererComment *>(item))
5973  {
5974  selectedItemsAvgPos += item->scenePos().toPoint();
5975  selectedItemCount++;
5976  }
5977  }
5978 
5979  selectedItemsAvgPos /= selectedItemCount;
5980 
5981  string parentCompositionPath = windowFilePath().toStdString();
5982  string subcompositionDir = VuoFileUtilities::getCompositionLocalModulesPath(parentCompositionPath);
5983 
5984  VuoEditorWindow *subcompositionWindow = static_cast<VuoEditor *>(qApp)->newCompositionWithContent(subcompositionContent, subcompositionDir);
5985  subcompositionWindow->setIncludeInRecentFileMenu(false);
5986  __block map<string, string> publishedPortNames = subcompositionWindow->getComposition()->publishPorts(portsToPublish);
5987  subcompositionWindow->showPublishedPortSidebars();
5988  subcompositionWindow->setAsActiveWindow();
5989 
5990  string nodeClassName = subcompositionWindow->installSubcomposition(parentCompositionPath);
5991  if (!nodeClassName.empty())
5992  {
5993  VuoModuleManager::CallbackType subcompositionAdded = ^void (void) {
5994  composition->getModuleManager()->getNodeLibrary()->highlightNodeClass(nodeClassName);
5995 
5996  VuoRendererNode *subcompositionNode = composition->createNode(nodeClassName.c_str(), "",
5997  selectedItemsAvgPos.x(),
5998  selectedItemsAvgPos.y());
5999  if (!subcompositionNode)
6000  {
6001  composition->collapseTypecastNodes();
6002  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
6003  return;
6004  }
6005 
6006  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(subcompositionWindow->getComposition(),
6007  getComposition(),
6008  subcompositionNode);
6009 
6010  undoStack->beginMacro(tr("Package as Subcomposition"));
6011 
6012  // Remove the selected comments, nodes, and their internal connections.
6013  QList<QGraphicsItem *> itemsToRemove;
6014  foreach (VuoRendererNode *node, composition->getSelectedNodes())
6015  itemsToRemove.append(node);
6016  foreach (VuoRendererComment *comment, composition->getSelectedComments())
6017  itemsToRemove.append(comment);
6018 
6019  undoStack->push(new VuoCommandRemove(itemsToRemove, this, inputEditorManager, "Package as Subcomposition", true));
6020 
6021  // Add the refactored subcomposition node.
6022  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
6023  componentsToAdd.append(subcompositionNode);
6024  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "", true));
6025 
6026  // Restore external connections.
6027  QList<QGraphicsItem *> inputCablesToRestore;
6028  for (int i = 0; i < inputCablesToRestoreEventOnly.size(); ++i)
6029  {
6030  VuoPort *fromPort = composition->getPortWithStaticIdentifier(inputCablesToRestoreFromPort[i]);
6031  VuoNode *fromNode = (fromPort? composition->getUnderlyingParentNodeForPort(fromPort, composition) : NULL);
6032  VuoPort *toPort = subcompositionNode->getBase()->getInputPortWithName(publishedPortNames[inputCablesToRestoreToPort[i] ]);
6033  VuoNode *toNode = subcompositionNode->getBase();
6034 
6035  if (fromNode && fromPort && toNode && toPort &&
6036  toNode->hasRenderer() && (toNode->getRenderer()->scene() == composition))
6037  {
6038  // Restore internal input cables.
6039  if (fromNode->hasRenderer() && (fromNode->getRenderer()->scene() == composition) &&
6040  (fromNode != composition->getPublishedInputNode()))
6041  {
6042  VuoRendererCable *cable = new VuoRendererCable((new VuoCompilerCable(fromNode->getCompiler(),
6043  static_cast<VuoCompilerPort *>(fromPort->getCompiler()),
6044  toNode->getCompiler(),
6045  static_cast<VuoCompilerPort *>(toPort->getCompiler())))->getBase());
6046  cable->getBase()->getCompiler()->setAlwaysEventOnly(inputCablesToRestoreEventOnly[i]);
6047  inputCablesToRestore.append(cable);
6048  }
6049  // Restore published input cables.
6050  else if (fromNode == composition->getPublishedInputNode())
6051  {
6052  undoStack->push(new VuoCommandPublishPort(toPort,
6053  NULL,
6054  this,
6055  inputCablesToRestoreEventOnly[i],
6056  inputCablesToRestoreFromPort[i],
6057  true
6058  ));
6059  }
6060  }
6061  }
6062 
6063  QList<QGraphicsItem *> outputCablesToRestore;
6064  for (int i = 0; i < outputCablesToRestoreEventOnly.size(); ++i)
6065  {
6066  VuoPort *fromPort = subcompositionNode->getBase()->getOutputPortWithName(publishedPortNames[outputCablesToRestoreFromPort[i] ]);
6067  VuoNode *fromNode = subcompositionNode->getBase();
6068  VuoPort *toPort = composition->getPortWithStaticIdentifier(outputCablesToRestoreToPort[i]);
6069  VuoNode *toNode = (toPort? composition->getUnderlyingParentNodeForPort(toPort, composition) : NULL);
6070 
6071  if (fromNode && fromPort && toNode && toPort &&
6072  fromNode->hasRenderer() && (fromNode->getRenderer()->scene() == composition))
6073  {
6074  // Restore internal output cables.
6075  if (toNode->hasRenderer() && (toNode->getRenderer()->scene() == composition) &&
6076  (toNode != composition->getPublishedOutputNode()))
6077  {
6078  VuoRendererCable *cable = new VuoRendererCable((new VuoCompilerCable(fromNode->getCompiler(),
6079  static_cast<VuoCompilerPort *>(fromPort->getCompiler()),
6080  toNode->getCompiler(),
6081  static_cast<VuoCompilerPort *>(toPort->getCompiler())))->getBase());
6082  cable->getBase()->getCompiler()->setAlwaysEventOnly(outputCablesToRestoreEventOnly[i]);
6083  outputCablesToRestore.append(cable);
6084  }
6085  // Restore published output cables.
6086  else if (toNode == composition->getPublishedOutputNode())
6087  {
6088  undoStack->push(new VuoCommandPublishPort(fromPort,
6089  NULL,
6090  this,
6091  outputCablesToRestoreEventOnly[i],
6092  outputCablesToRestoreToPort[i],
6093  true
6094  ));
6095  }
6096  }
6097  }
6098 
6099  undoStack->push(new VuoCommandAdd(inputCablesToRestore, this));
6100  undoStack->push(new VuoCommandAdd(outputCablesToRestore, this));
6101 
6102  // Note the refactoring in the composition diff.
6103  {
6104  string compositionIdentifier = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->getCompositionIdentifier(composition);
6105 
6106  set<VuoCompilerNode *> nodesMoved;
6107  for (QGraphicsItem *item : itemsToRemove)
6108  {
6109  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
6110  if (node)
6111  nodesMoved.insert(node->getBase()->getCompiler());
6112  }
6113 
6114  VuoCompilerNode *subcompositionMovedTo = subcompositionNode->getBase()->getCompiler();
6115 
6116  string compositionSnapshot = composition->takeSnapshot();
6117 
6119  diffInfo->addRefactoring(compositionIdentifier, nodesMoved, subcompositionMovedTo);
6120  coalesceSnapshots(compositionSnapshot, compositionSnapshot, diffInfo);
6121  }
6122 
6123  undoStack->endMacro();
6124  composition->collapseTypecastNodes();
6125 
6126  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
6127  };
6128 
6129  // The order of the following two lines matters -- the second callback registered
6130  // for a given module manager and node class replaces the first.
6131  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
6132  composition->getModuleManager()->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionAdded);
6133  }
6134 
6135  // Case: Subcomposition installation failed or was cancelled.
6136  else // if (nodeClassName.empty())
6137  {
6138  // Re-collapse any typecasts that we uncollapsed during attempted refactoring.
6139  composition->collapseTypecastNodes();
6140 
6141  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
6142  }
6143 }
6144 
6149 {
6150  return raiseDocumentAction;
6151 }
6152 
6157 {
6158  return ui->showEvents;
6159 }
6160 
6165 {
6166  return ui->zoomOut;
6167 }
6168 
6173 {
6174  return ui->zoom11;
6175 }
6176 
6181 {
6182  return ui->zoomToFit;
6183 }
6184 
6189 {
6190  return ui->zoomIn;
6191 }
6192 
6197 {
6198  if (isMinimized())
6199  showNormal();
6200  else
6201  show();
6202 
6203  raise();
6204  activateWindow();
6205 }
6206 
6211 {
6213 }
6214 
6219 {
6220  return ownedNodeLibrary;
6221 }
6222 
6228 {
6229  return nl;
6230 }
6231 
6238 {
6239  transitionNodeLibraryConnections(this->nl, library);
6240  this->nl = library;
6241 }
6242 
6247 void VuoEditorWindow::releaseSurrogateNodeLibrary(bool previousFloaterDestroyed)
6248 {
6249  transitionNodeLibraryConnections((previousFloaterDestroyed? NULL : this->nl), this->ownedNodeLibrary);
6250  this->nl = this->ownedNodeLibrary;
6251 }
6252 
6257 {
6258  return ui->menuFile;
6259 }
6260 
6265 {
6266  return menuOpenRecent;
6267 }
6268 
6273 {
6274  return composition;
6275 }
6276 
6280 void VuoEditorWindow::transitionNodeLibraryConnections(VuoNodeLibrary *oldLibrary, VuoNodeLibrary *newLibrary)
6281 {
6282  if (oldLibrary)
6283  {
6285  disconnect(oldLibrary, &VuoNodeLibrary::componentsAdded, this, &VuoEditorWindow::componentsAdded);
6286  disconnect(ui->actionShowNodeNames, &QAction::triggered, oldLibrary, &VuoNodeLibrary::clickedNamesButton);
6287  disconnect(ui->actionShowNodeClassNames, &QAction::triggered, oldLibrary, &VuoNodeLibrary::clickedFlatClassButton);
6288  disconnect(oldLibrary, &VuoNodeLibrary::changedIsHumanReadable, this, &VuoEditorWindow::updateUI);
6290 
6291  disconnect(oldLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateSceneRect);
6292  disconnect(oldLibrary, &VuoNodeLibrary::dockLocationChanged, this, &VuoEditorWindow::updateSceneRect);
6294 
6295  disconnect(composition, SIGNAL(nodePopoverRequestedForClass(VuoNodeClass *)), oldLibrary, SLOT(prepareAndDisplayNodePopoverForClass(VuoNodeClass *)));
6296 
6297  // Update the "Show/Hide Node Library" menu item when the node library visibility is changed.
6298  disconnect(oldLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateUI);
6299  }
6300 
6301  if (newLibrary)
6302  {
6305  connect(ui->actionShowNodeNames, &QAction::triggered, newLibrary, &VuoNodeLibrary::clickedNamesButton);
6306  connect(ui->actionShowNodeClassNames, &QAction::triggered, newLibrary, &VuoNodeLibrary::clickedFlatClassButton);
6309 
6310  connect(newLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateSceneRect);
6311  connect(newLibrary, &VuoNodeLibrary::dockLocationChanged, this, &VuoEditorWindow::updateSceneRect);
6313 
6314  connect(composition, SIGNAL(nodePopoverRequestedForClass(VuoNodeClass *)), newLibrary, SLOT(prepareAndDisplayNodePopoverForClass(VuoNodeClass *)));
6315 
6316 
6317  // Update the "Show/Hide Node Library" menu item when the node library visibility is changed.
6318  connect(newLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateUI);
6319  }
6320 }
6321 
6330 void VuoEditorWindow::restoreDefaultLeftDockedWidgetWidths()
6331 {
6332  if (nl)
6333  {
6334  nl->setMinimumWidth(nodeLibraryMinimumWidth);
6335  nl->setMaximumWidth(nodeLibraryMaximumWidth);
6336  }
6337 
6338  if (inputPortSidebar && outputPortSidebar)
6339  {
6340  inputPortSidebar->setMinimumWidth(outputPortSidebar->minimumWidth());
6341  inputPortSidebar->setMaximumWidth(outputPortSidebar->maximumWidth());
6342  }
6343 }
6344 
6349 void VuoEditorWindow::moveEvent(QMoveEvent *event)
6350 {
6351  QPoint positionChange = event->pos() - event->oldPos();
6352  composition->movePopoversBy(positionChange.x(), positionChange.y());
6353 
6354  QMainWindow::moveEvent(event);
6355 }
6356 
6361 void VuoEditorWindow::resizeEvent(QResizeEvent *event)
6362 {
6363 #if VUO_PRO
6364  if (toolbar)
6365  toolbar->updateTitle();
6366 #endif
6367 
6368  QMainWindow::resizeEvent(event);
6369 }
6370 
6376 QList<QGraphicsItem *> VuoEditorWindow::createAnyNecessaryMakeListComponents(VuoPort *port)
6377 {
6378  QList<QGraphicsItem *> makeListComponents;
6379 
6380  VuoCompilerInputEventPort *inputEventPort = dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler());
6381  if (inputEventPort && VuoCompilerType::isListType(inputEventPort->getDataType()))
6382  {
6383  VuoNode *parentNode = port->getRenderer()->getUnderlyingParentNode()->getBase();
6384  VuoRendererCable *makeListCable = NULL;
6385  VuoRendererNode *makeListNode = composition->createAndConnectMakeListNode(parentNode, port, makeListCable);
6386 
6387  makeListComponents.append(makeListNode);
6388  makeListComponents.append(makeListCable);
6389  }
6390 
6391  return makeListComponents;
6392 }
6393 
6397 void VuoEditorWindow::updateGrid()
6398 {
6399  QGraphicsView::CacheMode defaultViewportCacheMode = ui->graphicsView->cacheMode();
6400 
6401  ui->graphicsView->setCacheMode(QGraphicsView::CacheNone);
6402  ui->graphicsView->viewport()->update();
6403 
6404  ui->graphicsView->setCacheMode(defaultViewportCacheMode);
6405 }
6406 
6410 void VuoEditorWindow::updateCanvasOpacity()
6411 {
6412  int opacity = static_cast<VuoEditor *>(qApp)->getCanvasOpacity();
6413  VuoEditorUtilities::setWindowOpacity(this, opacity);
6414 }
6415 
6419 void VuoEditorWindow::updateColor(bool isDark)
6420 {
6422  QString backgroundColor = colors->canvasFill().name();
6423  QString scrollBarColor = isDark ? "#505050" : "#dfdfdf";
6424  QString dockwidgetTitleBackgroundColor = isDark ? "#919191" : "#efefef";
6425 
6426  QString menuStyle = VUO_QSTRINGIFY(
6427  // Sync with VuoCodeWindow::updateColor.
6428  // Should parallel VuoDialogForInputEditor::getStyleSheet()'s QComboBox popup menu styles.
6429  QMenu {
6430  background-color: #404040;
6431  }
6432  QMenu::item {
6433  color: #cfcfcf;
6434  padding-left: 22px;
6435  padding-right: 36px;
6436  height: 21px;
6437  }
6438  QMenu::item:disabled {
6439  color: #707070;
6440  }
6441  QMenu::item:selected {
6442  background-color: #1060d0;
6443  color: #ffffff;
6444  }
6445  QMenu::right-arrow {
6446  left: -14px;
6447  }
6448  QMenu::indicator:checked {
6449  image: url(:/Icons/checkmark.svg);
6450  width: 11px;
6451  }
6452  QMenu::indicator:checked,
6453  QMenu::icon {
6454  margin-left: 6px;
6455  }
6456  QMenu::icon:checked,
6457  QMenu::icon:unchecked {
6458  margin-left: 0;
6459  }
6460  );
6461 
6462  if (doneInitializing)
6463  setStyleSheet(VUO_QSTRINGIFY(
6464  QMainWindow::separator {
6465  background-color: %1;
6466  width: 1px;
6467  height: 0px;
6468  margin: -1px;
6469  padding: 0px;
6470  }
6471  )
6472  .arg(dockwidgetTitleBackgroundColor)
6473  + (isDark ? menuStyle : ""));
6474 
6476  ui->graphicsView->setStyleSheet(isDark ? menuStyle : "");
6477  else
6478  ui->graphicsView->setStyleSheet(VUO_QSTRINGIFY(
6479  QScrollBar {
6480  background: %1;
6481  height: 14px;
6482  width: 14px;
6483  }
6484  QScrollBar::handle {
6485  background: %2;
6486  border-radius: 5px;
6487  min-width: 20px;
6488  min-height: 20px;
6489  margin: 2px;
6490  }
6491  QAbstractScrollArea::corner,
6492  QScrollBar::add-line,
6493  QScrollBar::sub-line,
6494  QScrollBar::add-page,
6495  QScrollBar::sub-page {
6496  background: %1;
6497  border: none;
6498  }
6499  )
6500  .arg(backgroundColor)
6501  .arg(scrollBarColor)
6502  + (isDark ? menuStyle : ""));
6503 
6504 
6505  if (doneInitializing)
6506  foreach (VuoComment *comment, composition->getBase()->getComments())
6507  {
6508  if (comment->hasRenderer())
6509  comment->getRenderer()->updateColor();
6510  }
6511 }
6512 
6517 void VuoEditorWindow::coalescedUpdateRunningComposition()
6518 {
6519  if (coalescedOldCompositionSnapshot.empty() != coalescedNewCompositionSnapshot.empty())
6520  return;
6521 
6522  foreach (string nodeID, coalescedNodesToUnlink)
6523  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->unlinkNodeInSupercompositionFromSubcomposition(composition, nodeID);
6524 
6525  if (!coalescedOldCompositionSnapshot.empty() && !coalescedNewCompositionSnapshot.empty())
6526  composition->updateRunningComposition(coalescedOldCompositionSnapshot, coalescedNewCompositionSnapshot, coalescedDiffInfo);
6527 
6528  foreach (string portID, coalescedInternalPortConstantsToSync)
6529  composition->syncInternalPortConstantInRunningComposition(portID);
6530 
6531  foreach (string portID, coalescedPublishedPortConstantsToSync)
6532  composition->syncPublishedPortConstantInRunningComposition(portID);
6533 
6534  foreach (string nodeID, coalescedNodesToRelink)
6535  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->relinkNodeInSupercompositionToSubcomposition(composition, nodeID);
6536 
6537  coalescedOldCompositionSnapshot = "";
6538  coalescedNewCompositionSnapshot = "";
6539  coalescedInternalPortConstantsToSync.clear();
6540  coalescedPublishedPortConstantsToSync.clear();
6541  coalescedNodesToUnlink.clear();
6542  coalescedNodesToRelink.clear();
6543  coalescedDiffInfo = nullptr;
6544 }
6545 
6549 void VuoEditorWindow::coalesceSnapshots(string oldCompositionSnapshot, string newCompositionSnapshot, VuoCompilerCompositionDiff *diffInfo)
6550 {
6551  // @todo For now, don't attempt to merge VuoCompilerCompositionDiffs.
6552  if (diffInfo && coalescedDiffInfo)
6553  coalescedUpdateRunningComposition();
6554 
6555  if (coalescedOldCompositionSnapshot.empty())
6556  coalescedOldCompositionSnapshot = oldCompositionSnapshot;
6557 
6558  coalescedNewCompositionSnapshot = newCompositionSnapshot;
6559  coalescedDiffInfo = diffInfo;
6560 }
6561 
6566 {
6567  coalescedInternalPortConstantsToSync.push_back(portID);
6568 }
6569 
6574 {
6575  coalescedPublishedPortConstantsToSync.push_back(portID);
6576 }
6577 
6582 {
6583  coalescedNodesToUnlink.push_back(nodeID);
6584 }
6585 
6590 {
6591  coalescedNodesToRelink.push_back(nodeID);
6592 }
6593 
6599 void VuoEditorWindow::handlePendingProtocolComplianceReevaluationRequests()
6600 {
6601  if (protocolComplianceReevaluationPending)
6602  {
6603  evaluateCompositionForProtocolPromotion();
6604  protocolComplianceReevaluationPending = false;
6605  }
6606 
6607  return;
6608 }
6609 
6616 void VuoEditorWindow::registerProtocolComplianceEvaluationRequest()
6617 {
6618  this->protocolComplianceReevaluationPending = true;
6619 }
6620 
6625 {
6626  return ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(QCursor::pos()));
6627 }
6628 
6633 {
6634  return metadataPanel;
6635 }
6636 
6646 QPointF VuoEditorWindow::getFittedScenePos(QPointF origPos, int leftMargin, int topMargin, int rightMargin, int bottomMargin)
6647 {
6648  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
6649  double targetX = min(max(origPos.x(), viewportRect.left()+leftMargin), viewportRect.right()-rightMargin);
6650  double targetY = min(max(origPos.y(), viewportRect.top()+topMargin), viewportRect.bottom()-bottomMargin);
6651 
6652  // Don't return the position of any pre-existing node.
6653  bool targetPositionClearOfCoincidentNodes = false;
6654  while (!targetPositionClearOfCoincidentNodes)
6655  {
6656  QGraphicsItem *preexistingItem = composition->itemAt(QPoint(targetX, targetY), composition->views()[0]->transform());
6657  if (dynamic_cast<VuoRendererNode *>(preexistingItem) && (preexistingItem->scenePos() == QPoint(targetX, targetY)))
6658  {
6659  targetX += pastedComponentOffset;
6660  targetY += pastedComponentOffset;
6661  }
6662  else
6663  targetPositionClearOfCoincidentNodes = true;
6664  }
6665 
6666  return QPointF(targetX, targetY);
6667 }
6668 
6672 void VuoEditorWindow::toggleNodeLibraryDockedState()
6673 {
6674  if (nl)
6675  {
6676  bool floatLibrary = !nl->isFloating();
6677  nl->setFloating(floatLibrary);
6678  nl->fixWidth(!floatLibrary && !inputPortSidebar->isHidden());
6679  updateUI();
6680  }
6681 }
6682 
6686 void VuoEditorWindow::beginUndoStackMacro(QString commandName)
6687 {
6688  undoStack->beginMacro(tr(commandName.toUtf8().constData()));
6689 }
6690 
6695 {
6696  undoStack->endMacro();
6697 }