Vuo  2.0.2
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::updateSceneRect);
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 
833  if (toolbar)
834  toolbar->update(composition->getShowEventsMode(), ui->graphicsView->transform().isIdentity(), isZoomedToFit);
835 
836  ui->paste->setEnabled(!VuoEditor::getClipboardText().isEmpty());
837  ui->zoom11->setEnabled(! ui->graphicsView->transform().isIdentity());
838  ui->zoomToFit->setEnabled(! isZoomedToFit);
839  ui->compositionInformation->setEnabled(true);
840 
841  if (toolbar && toolbar->isStopInProgress())
842  {
843  ui->runComposition->setEnabled(!toolbar->isBuildPending());
844  ui->stopComposition->setEnabled(false);
845  ui->restartComposition->setEnabled(false);
846  }
847  else if (toolbar && toolbar->isBuildInProgress())
848  {
849  ui->runComposition->setEnabled(false);
850  ui->stopComposition->setEnabled(true);
851  ui->restartComposition->setEnabled(true);
852  }
853  else if (toolbar && toolbar->isRunning())
854  {
855  ui->runComposition->setEnabled(false);
856  ui->stopComposition->setEnabled(true);
857  ui->restartComposition->setEnabled(true);
858  }
859  else
860  {
861  ui->runComposition->setEnabled(true);
862  ui->stopComposition->setEnabled(false);
863  ui->restartComposition->setEnabled(false);
864  }
865 
866  ui->runComposition->setText(composition->getDriverForActiveProtocol()
867  ? tr("Run as") + " " + QString::fromStdString(composition->getActiveProtocol()->getName())
868  : tr("Run"));
869 
870  {
872  auto found = std::find_if(openWindows.begin(), openWindows.end(), [](VuoEditorWindow *w){ return w->metadataEditor->isVisible(); });
873  bool isCompositionInformationOpen = (found != openWindows.end());
874  quitAction->setEnabled(! isCompositionInformationOpen);
875  }
876 
877  QString savedPath = windowFilePath();
878  string savedDir, savedFile, savedExt;
879  VuoFileUtilities::splitPath(savedPath.toUtf8().constData(), savedDir, savedFile, savedExt);
880 
881  bool enableSaveMenuItem = (isWindowModified() || !VuoFileUtilities::fileExists(savedPath.toStdString()));
882  ui->saveComposition->setEnabled(enableSaveMenuItem);
883 
884  bool alreadyInstalledAsUserSubcomposition = (VuoFileUtilities::getUserModulesPath().c_str() == QFileInfo(savedDir.c_str()).canonicalFilePath());
885  bool hasErrors = true;
886  if (! alreadyInstalledAsUserSubcomposition)
887  {
888  try
889  {
890  VuoCompilerIssues issues;
891  composition->getBase()->getCompiler()->check(&issues);
892  hasErrors = false;
893  }
894  catch (VuoCompilerException &e) {}
895  }
896 
897  ui->saveComposition->setText(tr("Save"));
898  ui->saveCompositionAs->setText(tr("Save As…"));
899  ui->installSubcomposition->setEnabled(!alreadyInstalledAsUserSubcomposition && !hasErrors);
900  ui->installSubcomposition->setText(VuoFileUtilities::fileExists(windowFilePath().toStdString())?
901  tr("Move to User Library") :
902  tr("Save to User Library"));
903 
904  // Update the "Protocols" menus to reflect the currently active protocol(s).
905  updateProtocolsMenu(menuProtocols);
906  updateProtocolsMenu(inputPortSidebar->getProtocolsContextMenu());
907  updateProtocolsMenu(outputPortSidebar->getProtocolsContextMenu());
908 
909  ui->showEvents->setText(composition->getShowEventsMode() ? tr("Hide Events") : tr("Show Events"));
910 
911  if (nl)
912  {
913  if (nl->getHumanReadable())
914  ui->actionShowNodeNames->setChecked(true);
915  else
916  ui->actionShowNodeClassNames->setChecked(true);
917 
918  ui->toggleNodeLibraryDocking->setEnabled(!nl->isHidden());
919  ui->toggleNodeLibraryDocking->setText(nl->isFloating() ? tr("Attach to Editor Window") : tr("Detach from Editor Window"));
920  }
921 
922  if (inputPortSidebar && outputPortSidebar)
923  {
924  bool publishedPortSidebarsDisplayed = (! inputPortSidebar->isHidden());
925  ui->showPublishedPorts->setText(publishedPortSidebarsDisplayed ? tr("Hide Published Ports") : tr("Show Published Ports"));
926  ui->graphicsView->setVerticalScrollBarPolicy(publishedPortSidebarsDisplayed? Qt::ScrollBarAlwaysOff : Qt::ScrollBarAsNeeded);
927  }
928 
929  bool displaySearchTraversalOptions = (!searchBox->isHidden() && searchBox->traversalButtonsEnabled());
930  ui->findNext->setEnabled(displaySearchTraversalOptions);
931  ui->findPrevious->setEnabled(displaySearchTraversalOptions);
932 
933  bool cableDragInProgress = composition->getCableInProgress();
934  undoAction->setEnabled(!cableDragInProgress && undoStack->canUndo());
935  redoAction->setEnabled(!cableDragInProgress && undoStack->canRedo());
936 
937  bool showingHiddenCables = composition->getRenderHiddenCables();
938  if (!showingHiddenCables)
939  {
940  ui->showHiddenCables->setText(tr("Show Hidden Cables"));
941 
942  // In checking for hidden cables, include published cables, since selecting "Show Hidden Cables"
943  // will display the published port sidebars if there are any hidden published cables.
944  ui->showHiddenCables->setEnabled(composition->hasHiddenInternalCables() || composition->hasHiddenPublishedCables());
945  }
946  else
947  {
948  ui->showHiddenCables->setText(tr("Hide Hidden Cables"));
949 
950  // Always enable, to prevent the user from getting stuck in "Show Hidden Cables" mode and being
951  // unable to use context menu items to modify the hidden-status of cables in the future.
952  ui->showHiddenCables->setEnabled(true);
953  }
954 
955 #ifdef VUO_PRO
956  updateUI_Pro();
957 #else
958  ui->exportMovie->setEnabled(false);
959  ui->exportMacScreenSaver->setEnabled(false);
960  ui->exportMacFFGL->setEnabled(false);
961  ui->exportFxPlug->setEnabled(false);
962 #endif
963 
964  // Update this document's name in the "Window" menu.
965  raiseDocumentAction->setText(getWindowTitleWithoutPlaceholder());
966 
967  // Update the list of open documents in the "Windows" menu.
968  ui->menuWindow->clear();
969  static_cast<VuoEditor *>(qApp)->populateWindowMenu(ui->menuWindow, this);
970 
971  // In open editing windows for nodes contained in this composition,
972  // update UI actions that depend on whether this composition is running.
973  for (QMainWindow *window : VuoEditorUtilities::getOpenEditingWindows())
974  {
975  VuoCodeWindow *codeWindow = dynamic_cast<VuoCodeWindow *>(window);
976  if (codeWindow)
977  codeWindow->updateReloadAction();
978 
979  VuoEditorWindow *editorWindow = dynamic_cast<VuoEditorWindow *>(window);
980  if (editorWindow && editorWindow != this)
981  editorWindow->updateRefireAction();
982  }
983 
984  updateCursor();
985 }
986 
990 void VuoEditorWindow::updateRefireAction()
991 {
992  __block bool isTopLevelCompositionRunning = false;
993  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->applyToLinkedTopLevelComposition(composition, ^void (VuoEditorComposition *topLevelComposition, string thisCompositionIdentifier)
994  {
995  isTopLevelCompositionRunning = topLevelComposition->isRunning();
996  });
997 
998  ui->refireEvent->setEnabled(isTopLevelCompositionRunning ? composition->getTriggerPortToRefire() : false);
999 }
1000 
1004 void VuoEditorWindow::updateWindowForNewCableDrag()
1005 {
1006  if (composition->getCableInProgress() && composition->getCableInProgress()->isPublished())
1008 
1009  updateUI();
1010 }
1011 
1015 void VuoEditorWindow::updateCursor()
1016 {
1017  QCursor updatedCursor = (canvasDragInProgress? Qt::ClosedHandCursor :
1018  (canvasDragEnabled? Qt::OpenHandCursor :
1019  Qt::ArrowCursor));
1020 
1021  // Workaround to force a cursor update when the cursor believes it already has the correct updated
1022  // shape, but it actually does not. See https://b33p.net/kosada/node/7718#comment-27792 .
1023  setCursor(Qt::ArrowCursor);
1024 
1025  setCursor(updatedCursor);
1026 }
1027 
1036 {
1037  foreach (QWidget *widget, qApp->topLevelWidgets())
1038  {
1039  VuoEditorWindow *w = qobject_cast<VuoEditorWindow *>(widget);
1040  if (w && w->isVisible() && !w->containedPrepopulatedContent && (w->windowFilePath() == "") && (!w->isWindowModified()))
1041  return w;
1042  }
1043  return NULL;
1044 }
1045 
1050 void VuoEditorWindow::itemsMoved(set<VuoRendererNode *> nodes, set<VuoRendererComment *> comments, qreal dx, qreal dy, bool movedByDragging)
1051 {
1052  if ((!nodes.empty() || !comments.empty()) && !ignoreItemMoveSignals)
1053  {
1054  // Aggregate incremental moves of multiple items by mouse drag.
1055  if (movedByDragging && !duplicationMacroInProgress)
1056  {
1057  itemDragMacroInProgress = true;
1059 
1060  // Assumption: Notifications of item drags come item-by-item.
1061  QGraphicsItem *firstItem = (!nodes.empty()? static_cast<QGraphicsItem *>(*nodes.begin()) :
1062  static_cast<QGraphicsItem *>(*comments.begin()));
1063 
1064  if (std::find(itemsBeingDragged.begin(), itemsBeingDragged.end(), firstItem) == itemsBeingDragged.end())
1065  itemsBeingDragged.push_back(firstItem);
1066 
1067  if (firstItem == itemsBeingDragged[0])
1068  {
1069  itemDragDx += dx;
1070  itemDragDy += dy;
1071  }
1072  }
1073 
1074  if (!itemDragMacroInProgress)
1075  undoStack->push(new VuoCommandMove(nodes, comments, dx, dy, this, movedByDragging));
1076  }
1077 }
1078 
1083 void VuoEditorWindow::commentResized(VuoRendererComment *comment, qreal dx, qreal dy)
1084 {
1085  commentResizeMacroInProgress = true;
1086  commentBeingResized = comment;
1087  commentResizeDx += dx;
1088  commentResizeDy += dy;
1089 }
1090 
1095 void VuoEditorWindow::componentsAdded(QList<QGraphicsItem *> components, VuoEditorComposition *target)
1096 {
1097  if ((target == composition) && (! components.empty()))
1098  undoStack->push(new VuoCommandAdd(components, this, "Add"));
1099 }
1100 
1105 string VuoEditorWindow::getMaximumSubcompositionFromSelection(bool includePublishedPorts, bool includeHeader)
1106 {
1107  // Make sure that the drawers attached to any selected node are selected themselves.
1108  // Workaround for https://b33p.net/kosada/node/6064
1109  foreach (QGraphicsItem *item, composition->selectedItems())
1110  if (dynamic_cast<VuoRendererNode *>(item))
1111  dynamic_cast<VuoRendererNode *>(item)->layoutConnectedInputDrawers();
1112 
1113  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
1114  QList<QGraphicsItem *> selectedNonStrandedCompositionComponents;
1115 
1116  QStringList nodeDeclarations;
1117  vector<VuoRendererNode *> nodesToCopy;
1118  vector<QGraphicsItem *> potentialTypecastNodesToCopy;
1119  map<string, bool> nodeRepresented;
1120  vector<VuoRendererComment *> commentsToCopy;
1121 
1122  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
1123  {
1124  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(*i);
1125  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(*i);
1126  bool strandedAttachment = (attachment && isStrandedAttachment(attachment, selectedCompositionComponents));
1127 
1128  if (rn && rn->getBase()->hasCompiler() && !strandedAttachment)
1129  {
1130  if (! nodeRepresented[rn->getBase()->getCompiler()->getGraphvizIdentifier()])
1131  {
1132  nodesToCopy.push_back(rn);
1133  nodeRepresented[rn->getBase()->getCompiler()->getGraphvizIdentifier()] = true;
1134  selectedNonStrandedCompositionComponents.append(rn);
1135  }
1136 
1137  // Check for collapsed typecasts within the set of selected components.
1138  vector<VuoPort *> inputPorts = rn->getBase()->getInputPorts();
1139  for (vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
1140  {
1141  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>((*inputPort)->getRenderer());
1142  if (typecastPort)
1143  potentialTypecastNodesToCopy.push_back(typecastPort->getUncollapsedTypecastNode());
1144  }
1145  }
1146  else
1147  {
1148  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(*i);
1149  if (rcomment)
1150  commentsToCopy.push_back(rcomment);
1151  }
1152  }
1153 
1154  selectedNonStrandedCompositionComponents.append(QList<QGraphicsItem *>::fromVector(QVector<QGraphicsItem *>::fromStdVector(potentialTypecastNodesToCopy)));
1155  set<VuoRendererCable *> internalCableSet = composition->getCablesInternalToSubcomposition(selectedNonStrandedCompositionComponents);
1156 
1157  // Decide which of the original collapsed typecasts to copy.
1158  for (vector<QGraphicsItem *>::iterator node = potentialTypecastNodesToCopy.begin(); node != potentialTypecastNodesToCopy.end(); ++node)
1159  {
1160  bool inputCableCopied = false;
1161  bool childPortIsPublished = false;
1162  vector<VuoPort *> inputPorts = ((VuoRendererNode *)(*node))->getBase()->getInputPorts();
1163  for(vector<VuoPort *>::iterator inputPort = inputPorts.begin(); inputPort != inputPorts.end(); ++inputPort)
1164  {
1165  vector<VuoCable *> inputCables = (*inputPort)->getConnectedCables(true);
1166  for (vector<VuoCable *>::iterator inputCable = inputCables.begin(); inputCable != inputCables.end(); ++inputCable)
1167  {
1168  if (std::find(internalCableSet.begin(), internalCableSet.end(), (*inputCable)->getRenderer()) != internalCableSet.end())
1169  inputCableCopied = true;
1170  if ((*inputCable)->isPublished())
1171  childPortIsPublished = true;
1172  }
1173  }
1174 
1175  // Include the collapsed typecast if the cable connected to its child input port was also included in the set of
1176  // selected components, or if the typecast child port was connected to a published input port.
1177  if (inputCableCopied || (childPortIsPublished && includePublishedPorts))
1178  {
1179  if (! nodeRepresented[((VuoRendererNode *)(*node))->getBase()->getCompiler()->getGraphvizIdentifier()])
1180  {
1181  nodesToCopy.push_back((VuoRendererNode *)(*node));
1182  nodeRepresented[((VuoRendererNode *)(*node))->getBase()->getCompiler()->getGraphvizIdentifier()] = true;
1183  }
1184  }
1185 
1186  // Otherwise, exclude the collapsed typecast and its connected cables.
1187  else
1188  {
1189  set<VuoCable *> connectedCables = ((VuoRendererNode *)(*node))->getConnectedCables(false);
1190  for (set<VuoCable *>::iterator cable = connectedCables.begin(); cable != connectedCables.end(); ++cable)
1191  internalCableSet.erase((*cable)->getRenderer());
1192  }
1193  }
1194 
1195  vector<VuoRendererCable *> cablesToCopy(internalCableSet.begin(), internalCableSet.end());
1196  QPointF viewportTopLeft = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect().topLeft();
1197 
1198  set<VuoNode *> baseNodesToCopy;
1199  for (vector<VuoRendererNode *>::iterator i = nodesToCopy.begin(); i != nodesToCopy.end(); ++i)
1200  baseNodesToCopy.insert( (*i)->getBase() );
1201  set<VuoCable *> baseCablesToCopy;
1202  for (vector<VuoRendererCable *>::iterator i = cablesToCopy.begin(); i != cablesToCopy.end(); ++i)
1203  baseCablesToCopy.insert( (*i)->getBase() );
1204  set<VuoComment *> baseCommentsToCopy;
1205  for (vector<VuoRendererComment *>::iterator i = commentsToCopy.begin(); i != commentsToCopy.end(); ++i)
1206  baseCommentsToCopy.insert( (*i)->getBase() );
1207 
1208  // Determine which of the composition's published ports are contained within the subcomposition.
1209  set<VuoPublishedPort *> subcompositionPublishedInputPortSet;
1210  set<VuoPublishedPort *> subcompositionPublishedOutputPortSet;
1211 
1212  if (includePublishedPorts)
1213  {
1214  foreach (VuoPublishedPort *publishedInputPort, composition->getBase()->getPublishedInputPorts())
1215  {
1216  foreach (VuoCable *cable, publishedInputPort->getConnectedCables())
1217  {
1218  if (baseNodesToCopy.find(cable->getToNode()) != baseNodesToCopy.end())
1219  {
1220  subcompositionPublishedInputPortSet.insert(publishedInputPort);
1221  baseCablesToCopy.insert(cable);
1222  }
1223  }
1224  }
1225 
1226  foreach (VuoPublishedPort *publishedOutputPort, composition->getBase()->getPublishedOutputPorts())
1227  {
1228  foreach (VuoCable *cable, publishedOutputPort->getConnectedCables())
1229  {
1230  if (baseNodesToCopy.find(cable->getFromNode()) != baseNodesToCopy.end())
1231  {
1232  subcompositionPublishedOutputPortSet.insert(publishedOutputPort);
1233  baseCablesToCopy.insert(cable);
1234  }
1235  }
1236  }
1237  }
1238 
1239  // Preserve the ordering of the published ports.
1240  vector<VuoPublishedPort *> subcompositionPublishedInputPorts;
1241  foreach (VuoPublishedPort *port, composition->getBase()->getProtocolAwarePublishedPortOrder(composition->getActiveProtocol(), true))
1242  if (subcompositionPublishedInputPortSet.find(port) != subcompositionPublishedInputPortSet.end())
1243  subcompositionPublishedInputPorts.push_back(port);
1244 
1245  vector<VuoPublishedPort *> subcompositionPublishedOutputPorts;
1246  foreach (VuoPublishedPort *port, composition->getBase()->getProtocolAwarePublishedPortOrder(composition->getActiveProtocol(), false))
1247  if (subcompositionPublishedOutputPortSet.find(port) != subcompositionPublishedOutputPortSet.end())
1248  subcompositionPublishedOutputPorts.push_back(port);
1249 
1250  string outputCompositionText = composition->getBase()->getCompiler()->getGraphvizDeclarationForComponents(baseNodesToCopy,
1251  baseCablesToCopy,
1252  baseCommentsToCopy,
1253  subcompositionPublishedInputPorts,
1254  subcompositionPublishedOutputPorts,
1255  (includeHeader? composition->generateCompositionHeader() : ""),
1256  "",
1257  -viewportTopLeft.x(),
1258  -viewportTopLeft.y());
1259 
1260  return outputCompositionText;
1261 }
1262 
1268 bool VuoEditorWindow::isStrandedAttachment(VuoRendererInputAttachment *attachment, QList<QGraphicsItem *> selectedItems)
1269 {
1270  VuoNode *renderedHostNode = attachment->getRenderedHostNode();
1271  if (!(renderedHostNode && renderedHostNode->hasRenderer() && selectedItems.contains(renderedHostNode->getRenderer())))
1272  return true;
1273 
1274  set<VuoNode *> coattachments = attachment->getCoattachments();
1275  foreach (VuoNode *coattachment, coattachments)
1276  {
1277  if (!(coattachment->hasRenderer() && selectedItems.contains(coattachment->getRenderer())))
1278  return true;
1279  }
1280 
1281  return false;
1282 }
1283 
1291 {
1292  QClipboard *clipboard = QApplication::clipboard();
1293  QMimeData *mimeData = new QMimeData();
1294 
1295  if (nl && !nl->getSelectedDocumentationText().isEmpty())
1296  mimeData->setText(nl->getSelectedDocumentationText());
1297 
1298  else
1299  {
1300  mimeData->setText(getMaximumSubcompositionFromSelection(false, true).c_str());
1301  cursorPosAtLastComponentCopy = getCursorScenePos();
1302  }
1303 
1304  clipboard->setMimeData(mimeData);
1305 }
1306 
1313 {
1314  QString clipboardText = VuoEditor::getClipboardText();
1315 
1316  if (containsLikelyVuoComposition(clipboardText))
1317  pasteCompositionComponents();
1318  else if (!clipboardText.isEmpty())
1319  {
1321  nl->searchForText(clipboardText);
1322  }
1323 }
1324 
1328 void VuoEditorWindow::pasteCompositionComponents()
1329 {
1330  QClipboard *clipboard = QApplication::clipboard();
1331  const QMimeData *mimeData = clipboard->mimeData();
1332 
1333  int publishedPortsBeforePaste = composition->getBase()->getPublishedInputPorts().size() +
1334  composition->getBase()->getPublishedOutputPorts().size();
1335 
1336  int publishedCablesBeforePaste = 0;
1337  foreach (VuoPublishedPort *publishedInput, composition->getBase()->getPublishedInputPorts())
1338  publishedCablesBeforePaste += publishedInput->getConnectedCables(true).size();
1339  foreach (VuoPublishedPort *publishedOutput, composition->getBase()->getPublishedOutputPorts())
1340  publishedCablesBeforePaste += publishedOutput->getConnectedCables(true).size();
1341 
1342  if (mimeData->hasFormat("text/plain"))
1343  {
1344  // Paste at the cursor location only if the cursor is currently within the viewport bounds
1345  // and has moved since the most recent copy operation.
1346  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
1347  QPointF cursorPos = getCursorScenePos();
1348  bool pasteAtCursorLoc = ((cursorPos != cursorPosAtLastComponentCopy) && viewportRect.contains(cursorPos));
1349 
1350  QString compositionText = mimeData->text();
1351  mergeCompositionComponentsFromString(compositionText.toUtf8().constData(), pasteAtCursorLoc, !pasteAtCursorLoc, "Paste");
1352  }
1353 
1354  int publishedPortsAfterPaste = composition->getBase()->getPublishedInputPorts().size() +
1355  composition->getBase()->getPublishedOutputPorts().size();
1356 
1357  int publishedCablesAfterPaste = 0;
1358  foreach (VuoPublishedPort *publishedInput, composition->getBase()->getPublishedInputPorts())
1359  publishedCablesAfterPaste += publishedInput->getConnectedCables(true).size();
1360  foreach (VuoPublishedPort *publishedOutput, composition->getBase()->getPublishedOutputPorts())
1361  publishedCablesAfterPaste += publishedOutput->getConnectedCables(true).size();
1362 
1363  // Display the published port sidebars if the composition has any new published ports or cables.
1364  if ((publishedPortsAfterPaste > publishedPortsBeforePaste) || (publishedCablesAfterPaste > publishedCablesBeforePaste))
1365  setPublishedPortSidebarVisibility(true);
1366 }
1367 
1375 {
1376  // Compose a macro to merge the duplication and subsequent mouse drag operation of duplicated components.
1377  if (! duplicationMacroInProgress)
1378  {
1379  undoStack->beginMacro(tr("Duplication"));
1380  duplicationMacroInProgress = true;
1381  }
1382 
1383  string subcompositionText = getMaximumSubcompositionFromSelection(false, true);
1384  QList<QGraphicsItem *> newComponents = mergeCompositionComponentsFromString(subcompositionText, false, false, "Duplication");
1385 
1386  // Clear the previous selection and select the duplicated components.
1387  composition->clearSelection();
1388  for (QList<QGraphicsItem *>::iterator i = newComponents.begin(); i != newComponents.end(); ++i)
1389  (*i)->setSelected(true);
1390 }
1391 
1397 {
1398  foreach (VuoNode *node, composition->getBase()->getNodes())
1399  {
1400  VuoNodeClass *currentNodeClass = node->getNodeClass();
1401  string genericNodeClassName;
1402  if (currentNodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(currentNodeClass->getCompiler()))
1403  genericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(currentNodeClass->getCompiler())->getOriginalGenericNodeClassName();
1404  else
1405  genericNodeClassName = currentNodeClass->getClassName();
1406 
1407  if ((genericNodeClassName == nodeClass) && node->hasRenderer())
1408  node->getRenderer()->setSelected(true);
1409  }
1410 
1411  // Try to keep selected components visible.
1412  QRectF selectedItemsRect;
1413  foreach (QGraphicsItem *selectedComponent, composition->selectedItems())
1414  selectedItemsRect |= selectedComponent->sceneBoundingRect();
1415 
1416  if (!selectedItemsRect.isNull())
1417  ui->graphicsView->centerOn(selectedItemsRect.center());
1418 }
1419 
1427 void VuoEditorWindow::duplicateSelectedCompositionComponentsByMenuItem()
1428 {
1429  string subcompositionText = getMaximumSubcompositionFromSelection(false, true);
1430  QList<QGraphicsItem *> newComponents = mergeCompositionComponentsFromString(subcompositionText, false, true, "Duplication");
1431 
1432  // Clear the previous selection and select the duplicated components.
1433  composition->clearSelection();
1434  for (QList<QGraphicsItem *>::iterator i = newComponents.begin(); i != newComponents.end(); ++i)
1435  (*i)->setSelected(true);
1436 }
1437 
1442 {
1443  // End composition of duplication macro.
1444  resetUndoStackMacros();
1445 
1446  // Undo the composite duplication operation already on the 'Undo' stack.
1447  undoAction->trigger();
1448 }
1449 
1454 {
1455  if (snapshot != composition->takeSnapshot())
1456  {
1457  composition->modifyComponents(^{
1458  composition->clear();
1459  instantiateNewCompositionComponentsFromString(snapshot);
1460  });
1461  updateUI();
1462  }
1463 }
1464 
1469 void VuoEditorWindow::instantiateNewCompositionComponentsFromString(string compositionText)
1470 {
1472 
1473  foreach (VuoNode *node, graphParser->getNodes())
1474  composition->addNode(node);
1475 
1476  foreach (VuoPublishedPort *publishedInputPort, graphParser->getPublishedInputPorts())
1477  {
1478  composition->addPublishedPort(publishedInputPort, true);
1479  composition->createRendererForPublishedPortInComposition(publishedInputPort, true);
1480  }
1481 
1482  foreach (VuoPublishedPort *publishedOutputPort, graphParser->getPublishedOutputPorts())
1483  {
1484  composition->addPublishedPort(publishedOutputPort, false);
1485  composition->createRendererForPublishedPortInComposition(publishedOutputPort, false);
1486  }
1487 
1488  foreach (VuoCable *cable, graphParser->getCables())
1489  {
1490  composition->addCable(cable, false);
1491 
1492  if (cable->isPublishedInputCable())
1493  cable->setFrom(composition->getPublishedInputNode(), cable->getFromPort());
1494  if (cable->isPublishedOutputCable())
1495  cable->setTo(composition->getPublishedOutputNode(), cable->getToPort());
1496  }
1497 
1498  foreach (VuoComment *comment, graphParser->getComments())
1499  composition->addComment(comment);
1500 
1501  // Collapse any typecasts possible.
1502  composition->collapseTypecastNodes();
1503 
1504  // Now that all renderer components have been created, calculate
1505  // the final positions of collapsed "Make List" drawers.
1506  foreach (VuoNode *node, graphParser->getNodes())
1508 
1509  // @todo https://b33p.net/kosada/node/10638
1510  //delete graphParser;
1511 
1512  updateUI();
1513 }
1514 
1521 QList<QGraphicsItem *> VuoEditorWindow::mergeCompositionComponentsFromString(string compositionText, bool pasteAtCursorLoc, bool pasteWithOffset, string commandDescription)
1522 {
1523  VuoCompilerComposition *pastedComposition = VuoCompilerComposition::newCompositionFromGraphvizDeclaration(compositionText, compiler);
1524  QList<QGraphicsItem *> pastedComponents = QList<QGraphicsItem *>();
1525 
1526  QPointF startPos = (pasteWithOffset? QPointF(pastedComponentOffset, pastedComponentOffset) : QPointF(0,0));
1527 
1528  // Replace nodes that can't be legally added to this composition with implementation-less nodes.
1529  for (VuoNode *pastedNode : pastedComposition->getBase()->getNodes())
1530  {
1531  if (pastedNode->getNodeClass()->hasCompiler())
1532  {
1533  VuoNode *allowedNode = composition->createBaseNode(pastedNode->getNodeClass()->getCompiler(), pastedNode);
1534  if (! allowedNode->getNodeClass()->hasCompiler())
1535  {
1536  pastedComposition->replaceNode(pastedNode, allowedNode);
1537  allowedNode->setRawGraphvizDeclaration(pastedNode->getRawGraphvizDeclaration());
1538  delete pastedNode;
1539  }
1540  }
1541  }
1542 
1543  set<VuoNode *> pastedNodes = pastedComposition->getBase()->getNodes();
1544  for (VuoNode *node : pastedNodes)
1545  {
1546  VuoRendererNode *rn = ((VuoRendererComposition *)composition)->createRendererNode(node);
1547  pastedComponents.append(rn);
1548  }
1549 
1550  set<VuoComment *> pastedComments = pastedComposition->getBase()->getComments();
1551  for (VuoComment *comment : pastedComments)
1552  {
1553  VuoRendererComment *rc = ((VuoRendererComposition *)composition)->createRendererComment(comment);
1554  pastedComponents.append(rc);
1555  }
1556 
1557  qreal minX = std::numeric_limits<qreal>::max();
1558  qreal minY = std::numeric_limits<qreal>::max();
1559 
1560  if (pasteAtCursorLoc)
1561  {
1562  // Find the top-left of the pasted component cluster, to position at the cursor location.
1563  for (VuoNode *node : pastedNodes)
1564  {
1565  // Disregard the positions of nodes that will be rendered as attachments.
1566  if (dynamic_cast<VuoRendererInputAttachment *>(node->getRenderer()))
1567  continue;
1568 
1569  if (node->getX() < minX)
1570  minX = node->getX();
1571 
1572  if (node->getY() < minY)
1573  minY = node->getY();
1574  }
1575 
1576  for (VuoComment *comment : pastedComments)
1577  {
1578  if (comment->getX() < minX)
1579  minX = comment->getX();
1580 
1581  if (comment->getY() < minY)
1582  minY = comment->getY();
1583  }
1584 
1585  startPos += (getFittedScenePos(getCursorScenePos()) - QPointF(minX, minY));
1586  }
1587  else
1588  {
1589  QPointF viewportTopLeft = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect().topLeft();
1590  startPos += viewportTopLeft;
1591  }
1592 
1593  // Set new node and comment positions.
1594  this->ignoreItemMoveSignals = true;
1595  for (VuoNode *node : pastedNodes)
1596  {
1597  node->setX(startPos.x() + node->getX());
1598  node->setY(startPos.y() + node->getY());
1599 
1600  node->getRenderer()->setPos(node->getX(), node->getY());
1601  }
1602 
1603  for (VuoComment *comment : pastedComments)
1604  {
1605  comment->setX(startPos.x() + comment->getX());
1606  comment->setY(startPos.y() + comment->getY());
1607 
1608  comment->getRenderer()->setPos(comment->getX(), comment->getY());
1609  }
1610  this->ignoreItemMoveSignals = false;
1611 
1612  set<VuoCable *> pastedCables = pastedComposition->getBase()->getCables();
1613  for (VuoCable *cable : pastedCables)
1614  {
1615  if (! cable->isPublished())
1616  {
1617  VuoRendererCable *rc = new VuoRendererCable(cable);
1618  pastedComponents.append(rc);
1619  }
1620  }
1621 
1622  // Remove the pre-existing published cable connections from the pasted components.
1623  // We will make use of our own customized published port merging algorithm and will
1624  // create and connect our own published cables.
1625  // But first, document some information about the connections.
1626  map<VuoPublishedPort *, vector<pair<VuoPort *, bool> > > connectionsForPublishedPort;
1627  map<VuoPublishedPort *, bool> publishedPortHadConnectedDataCable;
1628 
1629  foreach (VuoPublishedPort *publishedInput, pastedComposition->getBase()->getPublishedInputPorts())
1630  {
1631  bool foundConnectedDataCable = false;
1632  vector<VuoCable *> publishedInputCables = publishedInput->getConnectedCables(true);
1633  foreach (VuoCable *cable, publishedInputCables)
1634  {
1635  VuoPort *toPort = cable->getToPort();
1636  VuoCompilerPort *compilerToPort = static_cast<VuoCompilerPort *>(toPort->getCompiler());
1637  bool alwaysEventOnly = cable->getCompiler()->getAlwaysEventOnly();
1638 
1639  bool cableCarriesData = (compilerToPort && compilerToPort->getDataVuoType() && !alwaysEventOnly);
1640  if (cableCarriesData)
1641  foundConnectedDataCable = true;
1642 
1643  connectionsForPublishedPort[publishedInput].push_back(make_pair(toPort, alwaysEventOnly));
1644 
1645  cable->setFrom(NULL, NULL);
1646  cable->setTo(NULL, NULL);
1647  }
1648 
1649  publishedPortHadConnectedDataCable[publishedInput] = foundConnectedDataCable;
1650  }
1651 
1652  foreach (VuoPublishedPort *publishedOutput, pastedComposition->getBase()->getPublishedOutputPorts())
1653  {
1654  bool foundConnectedDataCable = false;
1655  vector<VuoCable *> publishedOutputCables = publishedOutput->getConnectedCables(true);
1656  foreach (VuoCable *cable, publishedOutputCables)
1657  {
1658  VuoPort *fromPort = cable->getFromPort();
1659  VuoCompilerPort *compilerFromPort = static_cast<VuoCompilerPort *>(fromPort->getCompiler());
1660  bool alwaysEventOnly = cable->getCompiler()->getAlwaysEventOnly();
1661 
1662  bool cableCarriesData = (compilerFromPort && compilerFromPort->getDataVuoType() && !alwaysEventOnly);
1663  if (cableCarriesData)
1664  foundConnectedDataCable = true;
1665 
1666  connectionsForPublishedPort[publishedOutput].push_back(make_pair(fromPort, alwaysEventOnly));
1667 
1668  cable->setFrom(NULL, NULL);
1669  cable->setTo(NULL, NULL);
1670  }
1671 
1672  publishedPortHadConnectedDataCable[publishedOutput] = foundConnectedDataCable;
1673  }
1674 
1675  // Begin an 'Undo' macro to aggregate the instantiation of new composition components with the
1676  // publication of relevant ports.
1677  undoStack->beginMacro(tr(commandDescription.c_str()));
1678 
1679  // Add pasted components to composition.
1680  componentsPasted(pastedComponents, commandDescription);
1681 
1682  // Publish any input ports that were published in their source environment.
1683  vector<VuoPublishedPort *> unmergedPublishedInputPorts;
1684 
1685  // First pass: Check for any pasted published input ports that have identically named ports in the
1686  // target environment that can accommodate merging.
1687  foreach (VuoPublishedPort *publishedInputPort, pastedComposition->getBase()->getPublishedInputPorts())
1688  {
1689  string pastedPublishedPortName = publishedInputPort->getClass()->getName();
1690  VuoPublishedPort *existingPortWithSameName = composition->getBase()->getPublishedInputPortWithName(pastedPublishedPortName);
1691 
1692  if (existingPortWithSameName && dynamic_cast<VuoRendererPublishedPort *>(existingPortWithSameName->getRenderer())->canBeMergedWith(publishedInputPort,
1693  publishedPortHadConnectedDataCable[publishedInputPort]))
1694  {
1695  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[publishedInputPort];
1696  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1697  {
1698  VuoPort *connectedPort = i->first;
1699  bool forceEventOnlyPublication = i->second;
1700 
1701  internalPortPublished(connectedPort, forceEventOnlyPublication, pastedPublishedPortName, true);
1702  }
1703  }
1704  else
1705  unmergedPublishedInputPorts.push_back(publishedInputPort);
1706  }
1707 
1708  // Second pass: Re-name and publish the remaining pasted published input ports.
1709  foreach (VuoPublishedPort *unmergedPublishedInputPort, unmergedPublishedInputPorts)
1710  {
1711  string uniquePublishedPortName = composition->getUniquePublishedPortName(unmergedPublishedInputPort->getClass()->getName());
1712 
1713  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[unmergedPublishedInputPort];
1714  if (!internalConnections.empty())
1715  {
1716  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1717  {
1718  VuoPort *connectedPort = i->first;
1719  bool forceEventOnlyPublication = i->second;
1720 
1721  internalPortPublished(connectedPort, forceEventOnlyPublication, uniquePublishedPortName, true);
1722  }
1723  }
1724 
1725  else
1726  {
1727  unmergedPublishedInputPort->getClass()->setName(uniquePublishedPortName);
1728  composition->addPublishedPort(unmergedPublishedInputPort, true);
1729  composition->createRendererForPublishedPortInComposition(unmergedPublishedInputPort, true);
1730  }
1731  }
1732 
1733  // Publish any output ports that were published in their source environment.
1734  vector<VuoPublishedPort *> unmergedPublishedOutputPorts;
1735 
1736  // First pass: Check for any pasted published output ports that have identically named ports in the
1737  // target environment that can accommodate merging.
1738  foreach (VuoPublishedPort *publishedOutputPort, pastedComposition->getBase()->getPublishedOutputPorts())
1739  {
1740  string pastedPublishedPortName = publishedOutputPort->getClass()->getName();
1741  VuoPublishedPort *existingPortWithSameName = composition->getBase()->getPublishedOutputPortWithName(pastedPublishedPortName);
1742 
1743  if (existingPortWithSameName && dynamic_cast<VuoRendererPublishedPort *>(existingPortWithSameName->getRenderer())->canBeMergedWith(publishedOutputPort,
1744  publishedPortHadConnectedDataCable[publishedOutputPort]))
1745  {
1746  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[publishedOutputPort];
1747  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1748  {
1749  VuoPort *connectedPort = i->first;
1750  bool forceEventOnlyPublication = i->second;
1751 
1752  internalPortPublished(connectedPort, forceEventOnlyPublication, pastedPublishedPortName, true);
1753  }
1754  }
1755  else
1756  unmergedPublishedOutputPorts.push_back(publishedOutputPort);
1757  }
1758 
1759  // Second pass: Re-name and publish the remaining pasted published output ports.
1760  foreach (VuoPublishedPort *unmergedPublishedOutputPort, unmergedPublishedOutputPorts)
1761  {
1762  string uniquePublishedPortName = composition->getUniquePublishedPortName(unmergedPublishedOutputPort->getClass()->getName());
1763 
1764  vector<pair<VuoPort *, bool> > internalConnections = connectionsForPublishedPort[unmergedPublishedOutputPort];
1765  if (!internalConnections.empty())
1766  {
1767  for (vector<pair<VuoPort *, bool> >::iterator i = internalConnections.begin(); i != internalConnections.end(); ++i)
1768  {
1769  VuoPort *connectedPort = i->first;
1770  bool forceEventOnlyPublication = i->second;
1771 
1772  internalPortPublished(connectedPort, forceEventOnlyPublication, uniquePublishedPortName, true);
1773  }
1774  }
1775 
1776  else
1777  {
1778  unmergedPublishedOutputPort->getClass()->setName(uniquePublishedPortName);
1779  composition->addPublishedPort(unmergedPublishedOutputPort, false);
1780  composition->createRendererForPublishedPortInComposition(unmergedPublishedOutputPort, false);
1781  }
1782  }
1783 
1784  delete pastedComposition;
1785 
1786  undoStack->endMacro();
1787 
1788  return pastedComponents;
1789 }
1790 
1795 void VuoEditorWindow::componentsPasted(QList<QGraphicsItem *> components, string commandDescription)
1796 {
1797  if (components.empty())
1798  return;
1799 
1800  // Add the components to the scene.
1801  undoStack->push(new VuoCommandAdd(components, this, commandDescription));
1802 
1803  // Clear the previous selection and select the pasted components.
1804  composition->clearSelection();
1805  for (QList<QGraphicsItem *>::iterator i = components.begin(); i != components.end(); ++i)
1806  (*i)->setSelected(true);
1807 }
1808 
1814 {
1816  composition->deleteSelectedCompositionComponents("Cut");
1817 }
1818 
1839  VuoRendererPort *targetPort,
1840  pair<VuoRendererCable *, VuoRendererCable *> cableArgs,
1841  VuoRendererNode *typecastNodeToDelete,
1842  pair<string, string> typeArgs,
1843  pair<VuoRendererPort *, VuoRendererPort *> portArgs)
1844 {
1846 
1847  // Unpack arguments that were packed into pairs to avoid Qt's parameter count limit for signals/slots.
1848  VuoRendererCable *dataCableToDisplace = cableArgs.first;
1849  VuoRendererCable *cableToReplace = cableArgs.second;
1850  string typecastToInsert = typeArgs.first;
1851  string specializedTypeName = typeArgs.second;
1852  VuoRendererPort *portToUnpublish = portArgs.first;
1853  VuoRendererPort *portToSpecialize = portArgs.second;
1854 
1855  // Reconstruct the state of the composition before the beginning of the cable drag
1856  // that concluded with this connection, for the composition's initial "Before" snapshot
1857  // in the following sequence of operations.
1858  if (!cableInProgress->getBase()->getToPort() &&
1859  !composition->getCableInProgressWasNew() &&
1860  cableInProgress->getFloatingEndpointPreviousToPort())
1861  {
1862  bool cableInProgressAlwaysEventOnly = cableInProgress->getBase()->getCompiler()->getAlwaysEventOnly();
1863  cableInProgress->setTo(composition->getUnderlyingParentNodeForPort(cableInProgress->getFloatingEndpointPreviousToPort(), composition),
1864  cableInProgress->getFloatingEndpointPreviousToPort());
1865  cableInProgress->getBase()->getCompiler()->setAlwaysEventOnly(cableInProgress->getPreviouslyAlwaysEventOnly());
1866 
1867  // Execute an identity Undo stack command simply to record the composition's screenshot
1868  // before the cable drag began.
1869  set<VuoRendererNode *> emptyNodeSet;
1870  set<VuoRendererComment *> emptyCommentSet;
1871  undoStack->push(new VuoCommandMove(emptyNodeSet, emptyCommentSet, 0, 0, this, false));
1872 
1873  // Now reconstruct the state of the composition mid-cable drag and carry on.
1874  cableInProgress->getBase()->getCompiler()->setAlwaysEventOnly(cableInProgressAlwaysEventOnly);
1875  cableInProgress->setTo(NULL, NULL);
1876  }
1877 
1878  // Create the requested typecast node for eventual insertion.
1879  QList<QGraphicsItem *> typecastRelatedComponentsToAdd = QList<QGraphicsItem *>();
1880  VuoRendererNode *typecastNodeToAdd = NULL;
1881  VuoPort *typecastInPort = NULL;
1882  VuoPort *typecastOutPort = NULL;
1883  if (!typecastToInsert.empty())
1884  {
1885  typecastNodeToAdd = composition->createNode(typecastToInsert.c_str(), "",
1886  targetPort->scenePos().x()+100,
1887  targetPort->scenePos().y()+100);
1888 
1889  typecastRelatedComponentsToAdd.append(typecastNodeToAdd);
1890 
1891  typecastInPort = typecastNodeToAdd->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
1892  typecastOutPort = typecastNodeToAdd->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
1893 
1894  // Apply NULL checks liberally to avoid currently undiagnosed getRenderer()-related crash. https://b33p.net/kosada/node/9053
1895  if (!(typecastInPort && typecastInPort->hasRenderer() && typecastOutPort && typecastOutPort->hasRenderer()))
1896  return;
1897  }
1898 
1899  VuoRendererPort *unadjustedFromPort = NULL;
1900  VuoRendererPort *unadjustedToPort = NULL;
1901 
1902  // Apply NULL checks liberally to avoid currently undiagnosed getRenderer()-related crash. https://b33p.net/kosada/node/9053
1903  if (targetPort->getInput() && !(cableInProgress->getBase()->getFromPort() && cableInProgress->getBase()->getFromPort()->hasRenderer()))
1904  return;
1905  if (!targetPort->getInput() && !(cableInProgress->getBase()->getToPort() && cableInProgress->getBase()->getToPort()->hasRenderer()))
1906  return;
1907 
1908  // Specialize the port and its network, if applicable.
1909  if (portToSpecialize)
1910  {
1911  bool currentlyGeneric = dynamic_cast<VuoGenericType *>(portToSpecialize->getDataType());
1912 
1913  VuoRendererNode *specializedNode = NULL;
1914  if (currentlyGeneric)
1915  specializedNode = specializePortNetwork(portToSpecialize, specializedTypeName, false);
1916  else
1917  specializedNode = respecializePortNetwork(portToSpecialize, specializedTypeName, false);
1918 
1919  if (!specializedNode)
1920  return;
1921 
1922  if (portToSpecialize == targetPort)
1923  targetPort = (targetPort->getInput()? specializedNode->getBase()->getInputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer() :
1924  specializedNode->getBase()->getOutputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer());
1925  }
1926 
1927  if (targetPort->getInput())
1928  {
1929  unadjustedFromPort = cableInProgress->getBase()->getFromPort()->getRenderer();
1930  unadjustedToPort = targetPort;
1931  }
1932  else
1933  {
1934  unadjustedFromPort = targetPort;
1935  unadjustedToPort = cableInProgress->getBase()->getToPort()->getRenderer();
1936  }
1937 
1938  // @todo: Possibly re-use logic from VuoCompilerCable::carriesData() even though we don't yet have a connected cable.
1939  // Should really only matter if we implement published cable connection by dragging before we implement external published port types
1940  // that are independent of their internal connected port types, since then we must take into account that the cable may carry data even
1941  // if the (published) 'From' port technically does not.
1942  bool cableWillCarryData = (cableInProgress->effectivelyCarriesData() &&
1943  (dynamic_cast<VuoRendererPublishedPort *>(unadjustedFromPort)?
1944  static_cast<VuoCompilerPortClass *>(unadjustedFromPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
1945  unadjustedFromPort->getDataType()) &&
1946  (dynamic_cast<VuoRendererPublishedPort *>(unadjustedToPort)?
1947  static_cast<VuoCompilerPortClass *>(unadjustedToPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
1948  unadjustedToPort->getDataType())
1949  );
1950 
1951  VuoPort *previousToPort = cableInProgress->getFloatingEndpointPreviousToPort();
1952 
1953  // If connecting a data+event cable, and the intended input port had a previously connected
1954  // collapsed "Make List" node, delete it.
1955  VuoRendererInputDrawer *attachedDrawer = unadjustedToPort->getAttachedInputDrawer();
1956  if (attachedDrawer && cableWillCarryData)
1957  {
1958  QList<QGraphicsItem *> drawerRelatedComponentsToRemove;
1959  drawerRelatedComponentsToRemove.append(attachedDrawer);
1960  bool disableAutomaticAttachmentInsertion = true;
1961  undoStack->push(new VuoCommandRemove(drawerRelatedComponentsToRemove, this, inputEditorManager, "", disableAutomaticAttachmentInsertion));
1962  }
1963 
1964  VuoRendererPort *adjustedTargetPort = targetPort;
1965 
1966  if (typecastNodeToDelete)
1967  {
1968  QList<QGraphicsItem *> typecastRelatedComponentsToRemove = QList<QGraphicsItem *>();
1969  typecastRelatedComponentsToRemove.append(typecastNodeToDelete);
1970  typecastRelatedComponentsToRemove.append(dataCableToDisplace);
1971 
1972  undoStack->push(new VuoCommandRemove(typecastRelatedComponentsToRemove, this, inputEditorManager, "", true));
1973 
1974  dataCableToDisplace = NULL;
1975  }
1976 
1977  // Replace a pre-existing cable that connected the same two ports (but that had a different data-carrying status).
1978  if (cableToReplace)
1979  {
1980  QList<QGraphicsItem *> componentsToReplace = QList<QGraphicsItem *>();
1981  componentsToReplace.append(cableToReplace);
1982  undoStack->push(new VuoCommandRemove(componentsToReplace, this, inputEditorManager, "", false));
1983  }
1984 
1985  if (! typecastToInsert.empty())
1986  {
1987  // Insert an extra cable leading to or from the typecast as appropriate.
1988  VuoRendererCable *newCableConnectingTypecast;
1989  VuoRendererCable *incomingTypecastCable;
1990 
1991  // Case: making a "forward" cable connection (from an output port to an input port)
1992  if (targetPort->getInput())
1993  {
1994  // Prepare to add a cable connecting the output port of the newly inserted
1995  // typecast to the originally intended input port.
1996  VuoCompilerNode *fromNode = typecastNodeToAdd->getBase()->getCompiler();
1997  VuoCompilerPort *fromPort = (VuoCompilerPort *)typecastOutPort->getCompiler();
1998  VuoCompilerNode *toNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
1999  VuoCompilerPort *toPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2000 
2001  // Prepare to re-route the dragged cable's loose end to connect to the typecast's input port.
2002  adjustedTargetPort = typecastInPort->getRenderer();
2003 
2004  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2005  incomingTypecastCable = cableInProgress;
2006  }
2007 
2008  // Case: making a "backward" cable connection (from an input port to an output port)
2009  else
2010  {
2011  // Prepare to add a cable connecting the originally intended output port
2012  // to the input port of the newly inserted typecast.
2013  VuoCompilerNode *fromNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2014  VuoCompilerPort *fromPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2015  VuoCompilerNode *toNode = typecastNodeToAdd->getBase()->getCompiler();
2016  VuoCompilerPort *toPort = (VuoCompilerPort *)typecastInPort->getCompiler();
2017 
2018  // Prepare to re-route the dragged cable's loose end to connect to the typecast's output port.
2019  adjustedTargetPort = typecastOutPort->getRenderer();
2020 
2021  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2022  incomingTypecastCable = newCableConnectingTypecast;
2023  }
2024 
2025  typecastRelatedComponentsToAdd.append(newCableConnectingTypecast);
2026 
2027  bool disableAutomaticAttachmentInsertion = incomingTypecastCable->effectivelyCarriesData();
2028  undoStack->push(new VuoCommandAdd(typecastRelatedComponentsToAdd, this, "", disableAutomaticAttachmentInsertion));
2029  }
2030 
2031  undoStack->push(new VuoCommandConnect(cableInProgress, adjustedTargetPort, dataCableToDisplace, portToUnpublish, this, inputEditorManager));
2032 
2033  // If re-connecting a cable whose previous 'To' port had a collapsed typecast attached, decide what
2034  // to do with the typecast.
2035  if (targetPort->getInput() && previousToPort && (previousToPort != targetPort->getBase()))
2036  {
2037  VuoRendererTypecastPort *previousTypecastToPort = (VuoRendererTypecastPort *)(previousToPort->getRenderer()->getTypecastParentPort());
2038  if (previousTypecastToPort)
2039  {
2040  // If the typecast does not have any remaining incoming cables, delete it.
2041  VuoRendererPort *childPort = previousTypecastToPort->getChildPort();
2042  if (childPort->getBase()->getConnectedCables(true).size() < 1)
2043  {
2044  VuoRendererNode *uncollapsedTypecastToPort = composition->uncollapseTypecastNode(previousTypecastToPort);
2045  QList<QGraphicsItem *> typecastRelatedComponentsToRemove = QList<QGraphicsItem *>();
2046  typecastRelatedComponentsToRemove.append(uncollapsedTypecastToPort);
2047  undoStack->push(new VuoCommandRemove(typecastRelatedComponentsToRemove, this, inputEditorManager, ""));
2048  }
2049 
2050  // If the typecast does have remaining incoming cables but none of them carry data, uncollapse it.
2051  // @todo For consistency with cable deletion, we should really reroute the event-only cables
2052  // to the typecast host port.
2053  else if (!childPort->effectivelyHasConnectedDataCable(true))
2054  composition->uncollapseTypecastNode(previousTypecastToPort);
2055 
2057  }
2058  }
2059 
2060  // If the target port is generic and, as a result of this connection, has only a single
2061  // compatible specialized type, specialize its network.
2062  VuoGenericType *genericType = dynamic_cast<VuoGenericType *>((static_cast<VuoCompilerPort *>(targetPort->getBase()->getCompiler()))->getDataVuoType());
2063  if (genericType)
2064  {
2065  VuoGenericType::Compatibility compatibility;
2066  vector<string> compatibleSpecializedTypes = genericType->getCompatibleSpecializedTypes(compatibility);
2067  if (compatibleSpecializedTypes.size() == 1)
2068  {
2069  string loneCompatibleSpecializedType = *compatibleSpecializedTypes.begin();
2070  specializePortNetwork(targetPort, loneCompatibleSpecializedType, false);
2071  }
2072  }
2073 }
2074 
2079 void VuoEditorWindow::componentsRemoved(QList<QGraphicsItem *> components, string commandDescription)
2080 {
2081  if (! components.empty())
2082  undoStack->push(new VuoCommandRemove(components, this, inputEditorManager, commandDescription));
2083 }
2084 
2089 {
2090  if (triggerPort->getEventThrottling() != eventThrottling)
2091  undoStack->push(new VuoCommandSetTriggerThrottling(triggerPort, eventThrottling, this));
2092 }
2093 
2098 void VuoEditorWindow::adjustInputPortCountForNode(VuoRendererNode *node, int inputPortCountDelta, bool adjustmentedRequestedByDragging)
2099 {
2100  VuoCompilerNodeClass *origNodeClass = node->getBase()->getNodeClass()->getCompiler();
2101  VuoCompilerNodeClass *newNodeClass = NULL;
2102 
2103  // Case: "Make List" node
2105  {
2106  int origNumPorts = ((VuoCompilerMakeListNodeClass *)(origNodeClass))->getItemCount();
2107  VuoCompilerType *listType = ((VuoCompilerMakeListNodeClass *)(origNodeClass))->getListType();
2108  string newMakeListNodeClassName = VuoCompilerMakeListNodeClass::getNodeClassName(origNumPorts+inputPortCountDelta, listType);
2109  newNodeClass = compiler->getNodeClass(newMakeListNodeClassName);
2110  }
2111 
2112  if (newNodeClass)
2113  {
2114  VuoNode *newNode = newNodeClass->newNode(node->getBase());
2115  string commandText = (inputPortCountDelta == -1? "Remove Input Port" : (inputPortCountDelta == 1? "Add Input Port" : ""));
2116  undoStack->push(new VuoCommandReplaceNode(node, composition->createRendererNode(newNode), this, commandText));
2117 
2118  if (adjustmentedRequestedByDragging)
2119  {
2120  VuoRendererInputListDrawer *oldMakeListNode = dynamic_cast<VuoRendererInputListDrawer *>(node);
2121  VuoRendererInputListDrawer *newMakeListNode = dynamic_cast<VuoRendererInputListDrawer *>(newNode->getRenderer());
2122 
2123  if (oldMakeListNode)
2124  {
2125  if (composition->mouseGrabberItem() == oldMakeListNode)
2126  oldMakeListNode->ungrabMouse();
2127 
2128  oldMakeListNode->setDragInProgress(false);
2129  }
2130 
2131  if (newMakeListNode)
2132  {
2133  newMakeListNode->setDragInProgress(true);
2134  newMakeListNode->grabMouse();
2135  }
2136  }
2137  }
2138 }
2139 
2144 void VuoEditorWindow::swapNodes(VuoRendererNode *node, string newNodeClass)
2145 {
2146  VuoCompilerNodeClass *newNodeCompilerClass = compiler->getNodeClass(newNodeClass);
2147  if (!newNodeCompilerClass)
2148  return;
2149 
2150  VuoRendererNode *newNode = composition->createNode(newNodeClass.c_str(), "", node->getBase()->getX(), node->getBase()->getY());
2151  newNode->getBase()->setTintColor(node->getBase()->getTintColor());
2152  undoStack->push(new VuoCommandChangeNode(node, newNode, this));
2153 }
2154 
2163 {
2164  return specializePortNetwork(port, specializedTypeName, true);
2165 }
2166 
2175 VuoRendererNode * VuoEditorWindow::specializePortNetwork(VuoRendererPort *port, string specializedTypeName, bool encapsulateInMacro)
2176 {
2177  string commandText = "Port Specialization";
2178 
2179  if (encapsulateInMacro)
2180  undoStack->beginMacro(tr(commandText.c_str()));
2181 
2182  VuoRendererNode *originalNode = port->getUnderlyingParentNode();
2183  set<VuoPort *> networkedGenericPorts = composition->getBase()->getCompiler()->getCorrelatedGenericPorts(originalNode->getBase(), port->getBase(), false);
2184 
2185  VuoRendererNode *newNode = NULL;
2186  try
2187  {
2188  // Specialize the parent node of the target port.
2189  string innermostSpecializedTypeName = (VuoType::isListTypeName(port->getDataType()->getModuleKey())? VuoType::extractInnermostTypeName(specializedTypeName) : specializedTypeName);
2190  newNode = specializeSinglePort(port, innermostSpecializedTypeName);
2191  if (newNode)
2192  {
2193 
2194  map<VuoRendererNode *, VuoRendererNode *> nodesToSpecialize;
2195  nodesToSpecialize[originalNode] = newNode;
2196 
2197  // Specialize the parent node of each port in the target port's connected generic network.
2198  foreach (VuoPort *networkedPort, networkedGenericPorts)
2199  {
2200  // If we have specialized this port already, there is no need to do so again.
2201  VuoRendererNode *originalNetworkedNode = networkedPort->getRenderer()->getUnderlyingParentNode();
2202  VuoRendererNode *mostRecentVersionOfNetworkedNode = originalNetworkedNode;
2203  VuoPort *mostRecentVersionOfNetworkedPort = networkedPort;
2204  map<VuoRendererNode *, VuoRendererNode *>::iterator i = nodesToSpecialize.find(originalNetworkedNode);
2205  if (i != nodesToSpecialize.end())
2206  {
2207  VuoRendererNode *specializedNode = i->second;
2208  VuoPort *networkedPortInSpecializedNode = (networkedPort->getRenderer()->getInput()? specializedNode->getBase()->getInputPortWithName(networkedPort->getClass()->getName()) :
2209  specializedNode->getBase()->getOutputPortWithName(networkedPort->getClass()->getName()));
2210 
2211  mostRecentVersionOfNetworkedNode = specializedNode;
2212  mostRecentVersionOfNetworkedPort = networkedPortInSpecializedNode;
2213 
2214  string originalTypeName = ((VuoCompilerPortClass *)(networkedPort->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2215  string specializedTypeName = ((VuoCompilerPortClass *)(networkedPortInSpecializedNode->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2216  if (specializedTypeName != originalTypeName)
2217  continue;
2218  }
2219 
2220  VuoRendererNode *newNetworkedNode = specializeSinglePort(mostRecentVersionOfNetworkedPort->getRenderer(), innermostSpecializedTypeName);
2221  nodesToSpecialize[originalNetworkedNode] = newNetworkedNode;
2222  }
2223 
2224  bool preserveDanglingCables = true;
2225  undoStack->push(new VuoCommandReplaceNode(nodesToSpecialize, this, commandText, preserveDanglingCables));
2226  }
2227  }
2228  catch (VuoCompilerException &e)
2229  {
2230  VuoErrorDialog::show(NULL, tr("Can't set data type"), e.what());
2231  }
2232 
2233  if (encapsulateInMacro)
2234  undoStack->endMacro();
2235 
2236  return newNode;
2237 }
2238 
2244 VuoRendererNode * VuoEditorWindow::specializeSinglePort(VuoRendererPort *port, string specializedTypeName)
2245 {
2246  VuoRendererNode *node = port->getUnderlyingParentNode();
2247  string originalTypeName = ((VuoCompilerPortClass *)(port->getBase()->getClass()->getCompiler()))->getDataVuoType()->getModuleKey();
2248  string innermostOriginalTypeName = VuoType::extractInnermostTypeName(originalTypeName);
2249 
2250  VuoCompilerSpecializedNodeClass *specializableNodeClass = dynamic_cast<VuoCompilerSpecializedNodeClass *>(node->getBase()->getNodeClass()->getCompiler());
2251  if (!specializableNodeClass)
2252  return NULL;
2253 
2254  string newNodeClassName = specializableNodeClass->createSpecializedNodeClassNameWithReplacement(innermostOriginalTypeName, specializedTypeName);
2255  VuoCompilerNodeClass *newNodeClass = compiler->getNodeClass(newNodeClassName);
2256 
2257  if (!newNodeClass)
2258  return NULL;
2259 
2260  VuoNode *newNode = newNodeClass->newNode(node->getBase());
2261  return composition->createRendererNode(newNode);
2262 }
2263 
2272 {
2273  return unspecializePortNetwork(port, true);
2274 }
2275 
2285 {
2286  QString commandText = tr("Port Specialization");
2287  bool preserveDanglingCables = true;
2288  bool resetConstantValues = false;
2289 
2290  if (encapsulateInMacro)
2291  undoStack->beginMacro(commandText);
2292 
2293  VuoRendererNode *originalParentNode = port->getUnderlyingParentNode();
2294  VuoRendererNode *unspecializedParentNode = NULL;
2295 
2296  // Retrieve the set of networked nodes to be reverted and resulting cables to be disconnected.
2297  map<VuoNode *, string> nodesToReplace;
2298  set<VuoCable *> cablesToDelete;
2299  composition->createReplacementsToUnspecializePort(port->getBase(), nodesToReplace, cablesToDelete);
2300 
2301  // Disconnect the necessary cables.
2302  QList<QGraphicsItem *> rendererCablesToDelete;
2303  foreach (VuoCable *cableToDelete, cablesToDelete)
2304  {
2305  // Uncollapse any attached typecasts so that they are not automatically deleted with their incoming cables.
2306  if (cableToDelete->getToNode() && cableToDelete->getToNode()->hasRenderer())
2307  composition->uncollapseTypecastNode(cableToDelete->getToNode()->getRenderer());
2308 
2309  rendererCablesToDelete.append(cableToDelete->getRenderer());
2310  }
2311 
2312  undoStack->push(new VuoCommandRemove(rendererCablesToDelete, this, inputEditorManager, commandText.toStdString()));
2313 
2314  // Call createReplacementsToUnspecializePort(...) a second time to retrieve an up-to-date set of nodes
2315  // to replace, since nodes may have been added (in the case of collapsed drawers) or removed
2316  // (in the case of collapsed typecasts) as a result of the cable deletions just performed.
2317  nodesToReplace.clear();
2318  composition->createReplacementsToUnspecializePort(port->getBase(), nodesToReplace, cablesToDelete);
2319 
2320  map<VuoRendererNode *, VuoRendererNode *> nodesToUnspecialize;
2321 
2322  // Revert the parent node of each port in the target port's connected generic network.
2323  for (map<VuoNode *, string>::iterator i = nodesToReplace.begin(); i != nodesToReplace.end(); ++i)
2324  {
2325  VuoNode *nodeToRevert = i->first;
2326  string revertedNodeClass = i->second;
2327 
2328  VuoRendererNode *revertedNode = unspecializeSingleNode(nodeToRevert->getRenderer(), revertedNodeClass);
2329  nodesToUnspecialize[nodeToRevert->getRenderer()] = revertedNode;
2330 
2331  if (nodeToRevert->getRenderer() == originalParentNode)
2332  unspecializedParentNode = revertedNode;
2333  }
2334 
2335  undoStack->push(new VuoCommandReplaceNode(nodesToUnspecialize, this, commandText.toStdString(), preserveDanglingCables, resetConstantValues));
2336 
2337  if (encapsulateInMacro)
2338  undoStack->endMacro();
2339 
2340  return unspecializedParentNode;
2341 }
2342 
2352 {
2353  return respecializePortNetwork(port, specializedTypeName, true);
2354 }
2355 
2365 VuoRendererNode * VuoEditorWindow::respecializePortNetwork(VuoRendererPort *port, string specializedTypeName, bool encapsulateInMacro)
2366 {
2367  // If the port already has the requested type, no action is required.
2368  if (port->getDataType()->getModuleKey() == specializedTypeName)
2369  return port->getUnderlyingParentNode();
2370 
2371  string commandText = "Port Re-specialization";
2372  VuoRendererNode *specializedParentNode = NULL;
2373 
2374  if (encapsulateInMacro)
2375  undoStack->beginMacro(tr(commandText.c_str()));
2376 
2377  VuoRendererNode *revertedNode = unspecializePortNetwork(port, false);
2378  if (revertedNode)
2379  {
2380  VuoPort *revertedPort = (port->getInput()? revertedNode->getBase()->getInputPortWithName(port->getBase()->getClass()->getName()) :
2381  revertedNode->getBase()->getOutputPortWithName(port->getBase()->getClass()->getName()));
2382  if (revertedPort)
2383  specializedParentNode = specializePortNetwork(revertedPort->getRenderer(), specializedTypeName, false);
2384  }
2385 
2386  if (encapsulateInMacro)
2387  undoStack->endMacro();
2388 
2389  return specializedParentNode;
2390 }
2391 
2397 VuoRendererNode * VuoEditorWindow::unspecializeSingleNode(VuoRendererNode *node, string revertedNodeClassName)
2398 {
2399  VuoCompilerNodeClass *revertedNodeClass = compiler->getNodeClass(revertedNodeClassName);
2400 
2401  if (!revertedNodeClass)
2402  return NULL;
2403 
2404  VuoNode *revertedNode = revertedNodeClass->newNode(node->getBase());
2405  return composition->createRendererNode(revertedNode);
2406 }
2407 
2413 {
2414  undoStack->beginMacro(tr("Tint"));
2415 
2416  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
2417  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin(); i != selectedCompositionComponents.end(); ++i)
2418  {
2419  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(*i);
2420  VuoRendererInputAttachment *attachment = dynamic_cast<VuoRendererInputAttachment *>(*i);
2421  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(*i);
2422  if ((rn || rcomment) && (!attachment || (attachment->getRenderedHostNode() &&
2423  attachment->getRenderedHostNode()->hasRenderer() &&
2424  selectedCompositionComponents.contains(attachment->getRenderedHostNode()->getRenderer()))))
2425  undoStack->push(new VuoCommandSetItemTint(*i, tintColor, this));
2426  }
2427 
2428  undoStack->endMacro();
2429 }
2430 
2434 void VuoEditorWindow::hideSelectedInternalCables()
2435 {
2436  hideCables(composition->getSelectedCables(false));
2437 }
2438 
2442 void VuoEditorWindow::hideCables(set<VuoRendererCable *> cables)
2443 {
2444  undoStack->beginMacro(tr("Hide"));
2445 
2446  foreach (VuoRendererCable *cable, cables)
2447  undoStack->push(new VuoCommandSetCableHidden(cable, true, this));
2448 
2449  undoStack->endMacro();
2450  updateUI();
2451 }
2452 
2456 void VuoEditorWindow::unhideCables(set<VuoRendererCable *> cables)
2457 {
2458  bool publishedCableToUnhide = false;
2459  undoStack->beginMacro(tr("Unhide"));
2460 
2461  foreach (VuoRendererCable *cable, cables)
2462  {
2463  if (cable->getBase()->isPublished())
2464  publishedCableToUnhide = true;
2465  else
2466  undoStack->push(new VuoCommandSetCableHidden(cable, false, this));
2467  }
2468 
2469  if ((inputPortSidebar->isHidden() || outputPortSidebar->isHidden()) && publishedCableToUnhide)
2470  on_showPublishedPorts_triggered();
2471 
2472  undoStack->endMacro();
2473  updateUI();
2474 }
2475 
2479 void VuoEditorWindow::createIsolatedExternalPublishedPort(string typeName, bool isInput)
2480 {
2481  VuoType *type = (typeName.empty()? NULL : (compiler->getType(typeName)? compiler->getType(typeName)->getBase() : NULL));
2482  if (!typeName.empty() && !type)
2483  return;
2484 
2485  undoStack->beginMacro(tr("Add Published Port"));
2486 
2487  string portName = composition->getDefaultPublishedPortNameForType(type);
2488  VuoPublishedPort *publishedPort = static_cast<VuoPublishedPort *>(VuoCompilerPublishedPort::newPort(composition->getUniquePublishedPortName(portName), type)->getBase());
2489  VuoRendererPublishedPort *rpp = composition->createRendererForPublishedPortInComposition(publishedPort, isInput);
2490  undoStack->push(new VuoCommandAddPublishedPort(rpp, this));
2491  showPublishedPortNameEditor(rpp, true);
2492 
2493  undoStack->endMacro();
2494 }
2495 
2499 void VuoEditorWindow::showPublishedPortNameEditor(VuoRendererPublishedPort *port, bool useUndoStack)
2500 {
2501  string originalName = port->getBase()->getClass()->getName();
2502  bool isPublishedOutput = port->getInput();
2503  (isPublishedOutput? outputPortSidebar : inputPortSidebar)->updatePortList();
2504  string newName = (isPublishedOutput? outputPortSidebar : inputPortSidebar)->showPublishedPortNameEditor(port);
2505 
2506  if (originalName != newName)
2507  changePublishedPortName(port, newName, useUndoStack);
2508 }
2509 
2514 void VuoEditorWindow::internalExternalPortPairPublished(VuoPort *internalPort, VuoPublishedPort *externalPort, bool forceEventOnlyPublication, VuoPort *portToSpecialize, string specializedTypeName, string typecastToInsert, bool useUndoStackMacro)
2515 {
2516  internalPortPublished(internalPort, forceEventOnlyPublication, externalPort->getClass()->getName(), true, portToSpecialize, specializedTypeName, typecastToInsert, useUndoStackMacro);
2517 }
2518 
2522 void VuoEditorWindow::internalPortPublishedViaDropBox(VuoPort *port, bool forceEventOnlyPublication, bool useUndoStackMacro)
2523 {
2524  internalPortPublished(port, forceEventOnlyPublication, "", false, NULL, "", "", useUndoStackMacro);
2525 }
2526 
2530 void VuoEditorWindow::internalPortPublished(VuoPort *port, bool forceEventOnlyPublication, string name, bool merge, VuoPort *portToSpecialize, string specializedTypeName, string typecastToInsert, bool useUndoStackMacro)
2531 {
2532  // Display the published port sidebars whenever a port is published.
2533  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2534  on_showPublishedPorts_triggered();
2535 
2536  if (name.empty())
2537  name = composition->generateSpecialPublishedNameForPort(port);
2538 
2539  // If the provided port is already published under the requested name,
2540  // don't attempt to re-publish it.
2541  // @todo: Once re-connection of published cables is allowed, account for possibility that ports were already
2542  // connected, but with a cable of a different type. https://b33p.net/kosada/node/5142
2543  VuoRendererPort *targetPort = port->getRenderer();
2544  vector<VuoRendererPublishedPort *> preexistingPublishedPorts = targetPort->getPublishedPorts();
2545  foreach (VuoRendererPublishedPort * publishedPort, preexistingPublishedPorts)
2546  {
2547  if (publishedPort->getBase()->getClass()->getName() == name)
2548  return;
2549  }
2550 
2551  if (useUndoStackMacro)
2552  undoStack->beginMacro(tr("Publish Port"));
2553 
2554  // Specialize the port and its network, if applicable.
2555  if (portToSpecialize && portToSpecialize->hasRenderer())
2556  {
2557  bool currentlyGeneric = dynamic_cast<VuoGenericType *>(portToSpecialize->getRenderer()->getDataType());
2558 
2559  VuoRendererNode *specializedNode = NULL;
2560  if (currentlyGeneric)
2561  specializedNode = specializePortNetwork(portToSpecialize->getRenderer(), specializedTypeName, false);
2562  else
2563  specializedNode = respecializePortNetwork(portToSpecialize->getRenderer(), specializedTypeName, false);
2564 
2565  if (specializedNode && (portToSpecialize == targetPort->getBase()))
2566  targetPort = (targetPort->getInput()? specializedNode->getBase()->getInputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer() :
2567  specializedNode->getBase()->getOutputPortWithName(targetPort->getBase()->getClass()->getName())->getRenderer());
2568  }
2569 
2570  // Determine whether we need to uncollapse any attached typecasts, delete any attached
2571  // "Make List" nodes, or disconnect any cables in order to publish this port.
2572  VuoRendererCable *displacedCable = NULL;
2573 
2574  if (targetPort->getInput())
2575  {
2576  bool publishedCableExpectedToHaveData = false;
2577  bool publishedPortExpectedToBeMerged = false;
2578 
2579  VuoType *internalPortDataType = (dynamic_cast<VuoRendererPublishedPort *>(targetPort)?
2580  static_cast<VuoCompilerPortClass *>(targetPort->getBase()->getClass()->getCompiler())->getDataVuoType() :
2581  targetPort->getDataType());
2582  if (merge)
2583  {
2584  VuoPublishedPort *publishedPortWithTargetName = (targetPort->getInput()?
2585  composition->getBase()->getPublishedInputPortWithName(name) :
2586  composition->getBase()->getPublishedOutputPortWithName(name));
2587 
2588  if (publishedPortWithTargetName &&
2589  dynamic_cast<VuoRendererPublishedPort *>(publishedPortWithTargetName->getRenderer())->canAccommodateInternalPort(targetPort, forceEventOnlyPublication))
2590  {
2591  publishedPortExpectedToBeMerged = true;
2592  VuoType *type = static_cast<VuoCompilerPortClass *>(publishedPortWithTargetName->getClass()->getCompiler())->getDataVuoType();
2593  publishedCableExpectedToHaveData = (!forceEventOnlyPublication && internalPortDataType && type);
2594  }
2595  }
2596 
2597  if (!publishedPortExpectedToBeMerged)
2598  {
2599  publishedCableExpectedToHaveData = (!forceEventOnlyPublication && internalPortDataType);
2600  }
2601 
2602  // If input port has a connected collapsed typecast, uncollapse it.
2603  VuoRendererTypecastPort *typecastPort = dynamic_cast<VuoRendererTypecastPort *>(targetPort);
2604  if (typecastPort)
2605  {
2606  targetPort = typecastPort->getReplacedPort();
2607  composition->uncollapseTypecastNode(typecastPort);
2608  }
2609 
2610  // If the published input cable will have data...
2611  if (publishedCableExpectedToHaveData)
2612  {
2613  // If the internal input port has a connected drawer, delete it.
2614  VuoRendererInputDrawer *attachedDrawer = targetPort->getAttachedInputDrawer();
2615  if (attachedDrawer)
2616  {
2617  QList<QGraphicsItem *> componentsToRemove;
2618  componentsToRemove.append(attachedDrawer);
2619  bool disableAutomaticAttachmentInsertion = true;
2620  undoStack->push(new VuoCommandRemove(componentsToRemove, this, inputEditorManager, "", disableAutomaticAttachmentInsertion));
2621  }
2622 
2623  // If the internal input port has a connected data cable, displace it.
2624  else
2625  {
2626  vector<VuoCable *> connectedCables = targetPort->getBase()->getConnectedCables(true);
2627  for (vector<VuoCable *>::iterator cable = connectedCables.begin(); (! displacedCable) && (cable != connectedCables.end()); ++cable)
2628  if ((*cable)->getRenderer()->effectivelyCarriesData())
2629  displacedCable = (*cable)->getRenderer();
2630 
2631  }
2632  }
2633  }
2634 
2635  VuoRendererPort *adjustedTargetPort = targetPort;
2636  QList<QGraphicsItem *> typecastRelatedComponentsToAdd = QList<QGraphicsItem *>();
2637  if (! typecastToInsert.empty())
2638  {
2639  // Insert the necessary typecast node.
2640  VuoRendererNode *typecast = composition->createNode(typecastToInsert.c_str(), "",
2641  targetPort->scenePos().x()+100,
2642  targetPort->scenePos().y()+100);
2643 
2644  typecastRelatedComponentsToAdd.append(typecast);
2645 
2648 
2649  // Insert an extra cable leading to or from the typecast as appropriate.
2650  VuoRendererCable *newCableConnectingTypecast;
2651 
2652  // Case: Publishing an internal input port
2653  if (targetPort->getInput())
2654  {
2655  // Prepare to add a cable connecting the output port of the newly inserted
2656  // typecast to the originally intended input port.
2657  VuoCompilerNode *fromNode = typecast->getBase()->getCompiler();
2658  VuoCompilerPort *fromPort = (VuoCompilerPort *)typecastOutPort->getCompiler();
2659  VuoCompilerNode *toNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2660  VuoCompilerPort *toPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2661 
2662  // Prepare to re-route the dragged cable's loose end to connect to the typecast's input port.
2663  adjustedTargetPort = typecastInPort->getRenderer();
2664  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2665  }
2666 
2667  // Case: Publishing an internal output port
2668  else
2669  {
2670  // Prepare to add a cable connecting the originally intended output port
2671  // to the input port of the newly inserted typecast.
2672  VuoCompilerNode *fromNode = targetPort->getUnderlyingParentNode()->getBase()->getCompiler();
2673  VuoCompilerPort *fromPort = (VuoCompilerPort *)targetPort->getBase()->getCompiler();
2674  VuoCompilerNode *toNode = typecast->getBase()->getCompiler();
2675  VuoCompilerPort *toPort = (VuoCompilerPort *)typecastInPort->getCompiler();
2676 
2677  // Prepare to re-route the dragged cable's loose end to connect to the typecast's output port.
2678  adjustedTargetPort = typecastOutPort->getRenderer();
2679 
2680  newCableConnectingTypecast = new VuoRendererCable((new VuoCompilerCable(fromNode, fromPort, toNode, toPort))->getBase());
2681  }
2682 
2683  typecastRelatedComponentsToAdd.append(newCableConnectingTypecast);
2684 
2685  bool disableAutomaticAttachmentInsertion = true;
2686  undoStack->push(new VuoCommandAdd(typecastRelatedComponentsToAdd, this, "", disableAutomaticAttachmentInsertion));
2687  }
2688 
2689  undoStack->push(new VuoCommandPublishPort(adjustedTargetPort->getBase(), displacedCable, this, forceEventOnlyPublication, name, merge));
2690 
2691  // @todo: If re-connecting a data+event cable whose previous 'To' port had a collapsed typecast attached, delete that typecast.
2692  // Re-connection of published cables is not currently possible (https://b33p.net/kosada/node/5142), so we can skip this for now.
2693 
2694  // If the target port is generic and, as a result of this connection, has only a single
2695  // compatible specialized type, specialize its network.
2696  VuoGenericType *genericType = dynamic_cast<VuoGenericType *>((static_cast<VuoCompilerPort *>(targetPort->getBase()->getCompiler()))->getDataVuoType());
2697  if (genericType)
2698  {
2699  VuoGenericType::Compatibility compatibility;
2700  vector<string> compatibleSpecializedTypes = genericType->getCompatibleSpecializedTypes(compatibility);
2701  if (compatibleSpecializedTypes.size() == 1)
2702  {
2703  string loneCompatibleSpecializedType = *compatibleSpecializedTypes.begin();
2704  specializePortNetwork(targetPort, loneCompatibleSpecializedType, false);
2705  }
2706  }
2707 
2708  if (useUndoStackMacro)
2709  undoStack->endMacro();
2710 }
2711 
2716 {
2717  // Display the published port sidebars whenever a port is unpublished.
2718  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2719  on_showPublishedPorts_triggered();
2720 
2721  undoStack->beginMacro(tr("Delete"));
2722 
2723  // Remove any connected data+event cables using a separate VuoCommandRemove so that typecast
2724  // deletion, list insertion, etc. are handled correctly.
2725  bool isPublishedInput = !port->getInput();
2726  if (isPublishedInput)
2727  {
2728  vector<VuoCable *> publishedInputCables = port->getBase()->getConnectedCables(true);
2729  QList<QGraphicsItem *> removedDataCables;
2730  foreach (VuoCable *cable, publishedInputCables)
2731  {
2732  if (cable->getRenderer()->effectivelyCarriesData())
2733  removedDataCables.append(cable->getRenderer());
2734  }
2735 
2736  if (!removedDataCables.empty())
2737  undoStack->push(new VuoCommandRemove(removedDataCables, this, inputEditorManager, "Delete"));
2738  }
2739 
2740  undoStack->push(new VuoCommandUnpublishPort(dynamic_cast<VuoPublishedPort *>(port->getBase()), this));
2741  undoStack->endMacro();
2742 }
2743 
2748 {
2749  // Display the published port sidebars whenever a port is unpublished.
2750  if (inputPortSidebar->isHidden() || outputPortSidebar->isHidden())
2751  on_showPublishedPorts_triggered();
2752 
2753  undoStack->beginMacro(tr("Delete"));
2754 
2755  // If unpublishing a port with a list input, insert a new attached "Make List" node.
2756  // @todo: Eventually, account for the fact that the published cable might be
2757  // event-only even though the port is not.
2758  QList<QGraphicsItem *> makeListComponents = createAnyNecessaryMakeListComponents(port);
2759  if (! makeListComponents.empty())
2760  undoStack->push(new VuoCommandAdd(makeListComponents, this, ""));
2761 
2762  undoStack->push(new VuoCommandUnpublishPort(port, this));
2763  undoStack->endMacro();
2764 }
2765 
2770 void VuoEditorWindow::makeProtocolPortChanges(map<VuoPublishedPort *, string> publishedPortsToRename,
2771  set<VuoPublishedPort *> publishedPortsToRemove,
2772  vector<VuoPublishedPort *> publishedPortsToAdd,
2773  bool beginUndoStackMacro,
2774  bool endUndoStackMacro)
2775 {
2776  if (beginUndoStackMacro)
2777  undoStack->beginMacro(tr("Protocol Port Modification"));
2778 
2779  // Rename requested ports.
2780  for (map<VuoPublishedPort *, string>::iterator i = publishedPortsToRename.begin(); i != publishedPortsToRename.end(); ++i)
2781  {
2782  VuoPublishedPort *port = i->first;
2783  string name = i->second;
2784  undoStack->push(new VuoCommandSetPublishedPortName(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), name, this));
2785  }
2786 
2787  // Remove the requested protocol ports.
2788  foreach (VuoPublishedPort *port, publishedPortsToRemove)
2789  undoStack->push(new VuoCommandRemoveProtocolPort(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), this));
2790 
2791  // Add the requested protocol ports.
2792  foreach (VuoPublishedPort *port, publishedPortsToAdd)
2793  undoStack->push(new VuoCommandAddPublishedPort(dynamic_cast<VuoRendererPublishedPort *>(port->getRenderer()), this));
2794 
2795  if (endUndoStackMacro)
2796  undoStack->endMacro();
2797 }
2798 
2802 void VuoEditorWindow::resetUndoStackMacros()
2803 {
2804  if (itemDragMacroInProgress)
2805  {
2806  itemDragMacroInProgress = false;
2807 
2808  set<VuoRendererNode *> draggedNodeSet;
2809  set<VuoRendererComment *> draggedCommentSet;
2810 
2811  foreach (QGraphicsItem *item, itemsBeingDragged)
2812  {
2813  if (dynamic_cast<VuoRendererNode *>(item))
2814  draggedNodeSet.insert(dynamic_cast<VuoRendererNode *>(item));
2815  else if (dynamic_cast<VuoRendererComment *>(item))
2816  draggedCommentSet.insert(dynamic_cast<VuoRendererComment *>(item));
2817  }
2818 
2819  undoStack->push(new VuoCommandMove(draggedNodeSet, draggedCommentSet, itemDragDx, itemDragDy, this, true));
2820 
2821  itemsBeingDragged.clear();
2822 
2823  itemDragDx = 0;
2824  itemDragDy = 0;
2825  }
2826 
2827  if (commentResizeMacroInProgress)
2828  {
2829  commentResizeMacroInProgress = false;
2830 
2831  if (commentBeingResized && ((commentResizeDx != 0) || (commentResizeDy != 0)))
2832  undoStack->push(new VuoCommandResizeComment(commentBeingResized, commentResizeDx, commentResizeDy, this));
2833 
2834  commentBeingResized = NULL;
2835 
2836  commentResizeDx = 0;
2837  commentResizeDy = 0;
2838  }
2839 
2840  if (duplicationMacroInProgress)
2841  {
2842  undoStack->endMacro();
2843  duplicationMacroInProgress = false;
2844  }
2845 }
2846 
2850 bool VuoEditorWindow::event(QEvent *event)
2851 {
2852  if (event->type() == QEvent::Show)
2853  {
2854  if (zoomOutToFitOnNextShowEvent)
2855  {
2856  zoomOutToFit();
2857  zoomOutToFitOnNextShowEvent = false;
2858  }
2859 
2860  // Use a queued connection to avoid mutual recursion between VuoEditorWindow::event() and VuoEditorComposition::updateFeedbackErrors().
2861  QMetaObject::invokeMethod(composition, "updateFeedbackErrors", Qt::QueuedConnection);
2862  }
2863 
2864  // Workaround to force a cursor update when the cursor re-enters the window during a canvas drag.
2865  // See https://b33p.net/kosada/node/7718#comment-27792 .
2866  if (canvasDragInProgress && (event->type() == QEvent::MouseMove))
2867  updateCursor();
2868 
2869  // If the window is activated, update its UI elements (e.g., to correctly reflect
2870  // that it is the active document within the "Window" menu and OS X dock context menu).
2871  else if (event->type() == QEvent::WindowActivate)
2872  {
2874  updateUI();
2875  emit windowActivated();
2876  }
2877 
2878  else if (event->type() == QEvent::WindowDeactivate)
2879  emit windowDeactivated();
2880 
2881  if (event->type() == QEvent::WindowStateChange)
2882  {
2883  if (isMinimized())
2884  {
2885  composition->disableErrorPopovers();
2886  composition->emitCompositionOnTop(false);
2887  }
2888  else
2889  composition->updateFeedbackErrors();
2890  }
2891 
2892  else if (event->type() == QEvent::MouseButtonPress)
2893  {
2894  // Forward published cable drags initiated from the sidebar border onto the canvas.
2895  bool leftMouseButtonPressed = (((QMouseEvent *)(event))->button() == Qt::LeftButton);
2896  if (leftMouseButtonPressed)
2897  {
2898  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
2899  if (!inputPortSidebar->isHidden())
2900  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
2901 
2902  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
2903  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
2904 
2905  if (publishedPortNearCursor)
2906  {
2907  QGraphicsSceneMouseEvent mouseEvent;
2908  mouseEvent.setButtons(static_cast<QMouseEvent *>(event)->buttons());
2909  mouseEvent.setScenePos(ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos())));
2910  composition->leftMousePressEventAtNearbyItem(static_cast<QGraphicsItem *>(publishedPortNearCursor), &mouseEvent);
2911 
2912  forwardingEventsToCanvas = true;
2913  event->accept();
2914  return true;
2915  }
2916  }
2917 
2918  // If it's possible that the user is attempting to re-size a docked widget,
2919  // make sure that we have re-enabled re-sizing for the widgets that
2920  // share the left docking area, having possibly disabled this functionality
2921  // within setPublishedPortSidebarVisibility(...) and/or conformToGlobalNodeLibraryVisibility(...).
2922  restoreDefaultLeftDockedWidgetWidths();
2923  }
2924 
2925  else if (event->type() == QEvent::MouseMove)
2926  {
2927  // Forward published cable drags initiated from the sidebar border onto the canvas.
2928  if (forwardingEventsToCanvas)
2929  {
2930  QMouseEvent mouseEvent(QEvent::MouseMove,
2931  ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos()),
2932  static_cast<QMouseEvent *>(event)->screenPos(),
2933  static_cast<QMouseEvent *>(event)->button(),
2934  static_cast<QMouseEvent *>(event)->buttons(),
2935  static_cast<QMouseEvent *>(event)->modifiers());
2936 
2937  QApplication::sendEvent(ui->graphicsView->viewport(), &mouseEvent);
2938 
2939  event->accept();
2940  return true;
2941  }
2942  }
2943 
2944  else if (event->type() == QEvent::MouseButtonRelease)
2945  {
2946  // Forward published cable drags initiated from the sidebar border onto the canvas.
2947  if (forwardingEventsToCanvas)
2948  {
2949  QMouseEvent mouseEvent(QEvent::MouseButtonRelease,
2950  ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos()),
2951  static_cast<QMouseEvent *>(event)->screenPos(),
2952  static_cast<QMouseEvent *>(event)->button(),
2953  static_cast<QMouseEvent *>(event)->buttons(),
2954  static_cast<QMouseEvent *>(event)->modifiers());
2955 
2956  QApplication::sendEvent(ui->graphicsView->viewport(), &mouseEvent);
2957 
2958  forwardingEventsToCanvas = false;
2959  event->accept();
2960  return true;
2961  }
2962  }
2963 
2964  return QMainWindow::event(event);
2965 }
2966 
2970 bool VuoEditorWindow::eventFilter(QObject *object, QEvent *event)
2971 {
2972  // If it's been a while since we received a scroll event, turn interactivity back on.
2973  // Workaround for Qt sometimes not sending us a Qt::ScrollEnd event.
2974  const double scrollTimeoutSeconds = 0.5;
2975  if (scrollInProgress && VuoLogGetElapsedTime() - timeOfLastScroll > scrollTimeoutSeconds)
2976  {
2977  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);
2978  ui->graphicsView->setInteractive(true);
2979  scrollInProgress = false;
2980  }
2981 
2982  // Prevent the composition from responding to certain types of events during canvas grabs/drags.
2983  // The canvas drag itself must be handled in response to intercepted QEvent::MouseMove
2984  // events rather than here in response to intercepted QEvent::GraphicsSceneMouseMove
2985  // events; otherwise the canvas doesn't track correctly with the cursor.
2986  if ((canvasDragEnabled || canvasDragInProgress) && (object == composition) && (
2987  event->type() == QEvent::GraphicsSceneMouseMove ||
2988  event->type() == QEvent::GraphicsSceneMousePress ||
2989  event->type() == QEvent::GraphicsSceneMouseDoubleClick ||
2990  event->type() == QEvent::GraphicsSceneContextMenu ||
2991  event->type() == QEvent::KeyPress))
2992  return true;
2993 
2994  // If an input editor is displayed at the time that the canvas is clicked, the click should be consumed
2995  // with the closing of the input editor rather than having additional effects on the canvas.
2996  if ((object == composition) &&
2997  (event->type() == QEvent::GraphicsSceneMousePress) &&
2998  inputEditorSession)
2999  {
3000  consumeNextMouseReleaseToCanvas = true;
3001  return true;
3002  }
3003  if ((object == composition) &&
3004  (event->type() == QEvent::GraphicsSceneMouseRelease) &&
3005  consumeNextMouseReleaseToCanvas)
3006  {
3007  consumeNextMouseReleaseToCanvas = false;
3008  return true;
3009  }
3010 
3011  if (event->type() == QEvent::Wheel
3012  && ui->graphicsView->rubberBandRect().isNull()
3013  && !ui->graphicsView->pinchZoomInProgress())
3014  {
3015  // Disable interaction while scrolling, to improve the framerate.
3016  QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
3017  if (wheelEvent->phase() == Qt::ScrollBegin)
3018  {
3019  ui->graphicsView->setInteractive(false);
3020  scrollInProgress = true;
3021  }
3022  else if (wheelEvent->phase() == Qt::ScrollEnd)
3023  {
3024  ui->graphicsView->setInteractive(true);
3025  scrollInProgress = false;
3026  }
3027 
3028  // Skip rendering if we aren't keeping up.
3029  double lag = VuoEditorCocoa_systemUptime() - wheelEvent->timestamp()/1000.;
3030  const double lagLimit = .1;
3031  if (lag > lagLimit)
3032  return true;
3033 
3034  // Remove Shift modifier from mouse-wheel events to prevent it from triggering page-step mode.
3035  QInputEvent *filteredInputEvent = (QInputEvent *)(event);
3036  Qt::KeyboardModifiers modifiersOtherThanShift = (filteredInputEvent->modifiers() & ~Qt::ShiftModifier);
3037  filteredInputEvent->setModifiers(modifiersOtherThanShift);
3038  object->removeEventFilter(this);
3039  QApplication::sendEvent(object, filteredInputEvent);
3040  object->installEventFilter(this);
3041 
3042  timeOfLastScroll = VuoLogGetElapsedTime();
3043 
3044  return true;
3045  }
3046 
3047  // Disable any non-detached port or node popover upon mouse press.
3048  else if (event->type() == QEvent::MouseButtonPress)
3049  {
3050  bool leftMouseButtonPressed = (((QMouseEvent *)(event))->button() == Qt::LeftButton);
3051  if (leftMouseButtonPressed)
3052  this->lastLeftMousePressHadOptionModifier = VuoEditorUtilities::optionKeyPressedForEvent(event);
3053 
3054  composition->disableNondetachedPortPopovers(NULL, true);
3055 
3056  // Initiate canvas drag if previously enabled by press-and-hold of the spacebar.
3057  if (canvasDragEnabled && leftMouseButtonPressed && (object == ui->graphicsView->viewport()))
3058  {
3059  initiateCanvasDrag();
3060  return true;
3061  }
3062 
3063  // If a sidebar published port is near the cursor, send the mouse press directly to that published port
3064  // rather than handing hover detection over to the composition's findNearbyComponent(...) algorithm.
3065  else if (leftMouseButtonPressed)
3066  {
3067  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
3068  if (!inputPortSidebar->isHidden())
3069  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3070 
3071  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
3072  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3073 
3074  if (publishedPortNearCursor)
3075  {
3076  QGraphicsSceneMouseEvent mouseEvent;
3077  mouseEvent.setButtons(static_cast<QMouseEvent *>(event)->buttons());
3078  mouseEvent.setScenePos(ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(static_cast<QMouseEvent *>(event)->globalPos())));
3079  composition->leftMousePressEventAtNearbyItem(static_cast<QGraphicsItem *>(publishedPortNearCursor), &mouseEvent);
3080  return true;
3081  }
3082  }
3083  }
3084 
3085  // Continue any canvas drag currently in progress.
3086  else if ((event->type() == QEvent::MouseMove) &&
3087  canvasDragInProgress &&
3088  (object == ui->graphicsView->viewport()))
3089  {
3090  mouseMoveEvent((QMouseEvent *)event);
3091  return true;
3092  }
3093 
3094  // Conclude canvas drag.
3095  else if ((event->type() == QEvent::MouseButtonRelease) &&
3096  canvasDragInProgress &&
3097  (((QMouseEvent *)(event))->button() == Qt::LeftButton) &&
3098  (object == ui->graphicsView->viewport()))
3099  {
3100  concludeCanvasDrag();
3101  return true;
3102  }
3103 
3104  // If a cable drag is in progress and a published port sidebar is directly under the cursor,
3105  // let the sidebar, rather than the composition canvas, handle hover highlighting
3106  // and the conclusion of the cable drag.
3107  else if ((((event->type() == QEvent::MouseButtonRelease) &&
3108  (((QMouseEvent *)(event))->button() == Qt::LeftButton)) ||
3109  ((event->type() == QEvent::MouseMove) &&
3110  (((QMouseEvent *)(event))->buttons() & Qt::LeftButton)))
3111  &&
3112  composition->getCableInProgress())
3113  {
3114  QPoint cursorPosition = ((QMouseEvent *)(event))->globalPos();
3115 
3116  QRect inputPortSidebarRect = inputPortSidebar->geometry();
3117  inputPortSidebarRect.moveTopLeft(inputPortSidebar->parentWidget()->mapToGlobal(inputPortSidebarRect.topLeft()));
3118 
3119  QRect outputPortSidebarRect = outputPortSidebar->geometry();
3120  outputPortSidebarRect.moveTopLeft(outputPortSidebar->parentWidget()->mapToGlobal(outputPortSidebarRect.topLeft()));
3121 
3122  VuoRendererPublishedPort *publishedInputPortNearCursor = NULL;
3123  if (!inputPortSidebar->isHidden())
3124  publishedInputPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
3125 
3126  VuoRendererPublishedPort *publishedOutputPortNearCursor = NULL;
3127  if (!outputPortSidebar->isHidden())
3128  publishedOutputPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange, true);
3129 
3130  // Case: drag with left mouse button pressed
3131  if ((event->type() == QEvent::MouseMove) &&
3132  (((QMouseEvent *)(event))->buttons() & Qt::LeftButton))
3133  {
3134  bool dragOverInputPortSidebar = ((! inputPortSidebar->isHidden()) && (inputPortSidebarRect.contains(cursorPosition) || publishedInputPortNearCursor));
3135  bool dragOverOutputPortSidebar = ((! outputPortSidebar->isHidden()) && (outputPortSidebarRect.contains(cursorPosition) || publishedOutputPortNearCursor));
3136 
3137  if (dragOverInputPortSidebar || dragOverOutputPortSidebar)
3138  {
3139  if (!previousDragMoveWasOverSidebar)
3140  composition->clearHoverHighlighting();
3141 
3142  if (dragOverInputPortSidebar)
3144  else if (dragOverOutputPortSidebar)
3146 
3147  previousDragMoveWasOverSidebar = true;
3148  }
3149 
3150  else
3151  {
3152  if (previousDragMoveWasOverSidebar)
3153  {
3154  inputPortSidebar->clearHoverHighlighting();
3155  outputPortSidebar->clearHoverHighlighting();
3156  if (composition->getCableInProgress())
3158  }
3159 
3160  previousDragMoveWasOverSidebar = false;
3161  }
3162  }
3163 
3164  // Case: left mouse button release
3165  else if ((event->type() == QEvent::MouseButtonRelease) &&
3166  (((QMouseEvent *)(event))->button() == Qt::LeftButton))
3167  {
3168  VuoCable *cableInProgress = composition->getCableInProgress();
3169 
3170  // Case: Concluding a published cable drag at a sidebar published input port.
3171  if (cableInProgress && !inputPortSidebar->isHidden() && (inputPortSidebarRect.contains(cursorPosition) || publishedInputPortNearCursor))
3172  inputPortSidebar->concludePublishedCableDrag((QMouseEvent *)event, cableInProgress, composition->getCableInProgressWasNew());
3173 
3174  // Case: Concluding a published cable drag at a sidebar published output port.
3175  else if (cableInProgress && !outputPortSidebar->isHidden() && (outputPortSidebarRect.contains(cursorPosition) || publishedOutputPortNearCursor))
3176  outputPortSidebar->concludePublishedCableDrag((QMouseEvent *)event, cableInProgress, composition->getCableInProgressWasNew());
3177  }
3178 
3179  object->removeEventFilter(this);
3180  QApplication::sendEvent(object, event);
3181  object->installEventFilter(this);
3182  return true;
3183  }
3184 
3185  // Determine whether the cursor is near a published sidebar port, and hover-highlight that port,
3186  // before handing hover detection over to the composition's findNearbyComponent(...) algorithm.
3187  else if (event->type() == QEvent::MouseMove && !composition->getCableInProgress())
3188  {
3189  VuoRendererPublishedPort *publishedPortNearCursor = NULL;
3190  if (!inputPortSidebar->isHidden())
3191  {
3192  publishedPortNearCursor = inputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3194  }
3195 
3196  if (!outputPortSidebar->isHidden() && !publishedPortNearCursor)
3197  {
3198  publishedPortNearCursor = outputPortSidebar->getPublishedPortUnderCursorForEvent(static_cast<QMouseEvent *>(event), VuoEditorComposition::componentCollisionRange);
3200  }
3201 
3202  if (publishedPortNearCursor)
3203  {
3204  if (!publishedPortNearCursorPreviously)
3205  composition->clearHoverHighlighting();
3206  }
3207 
3208  else
3209  {
3210  if (publishedPortNearCursorPreviously)
3211  {
3212  inputPortSidebar->clearHoverHighlighting();
3213  outputPortSidebar->clearHoverHighlighting();
3214  }
3215 
3216  object->removeEventFilter(this);
3217  QApplication::sendEvent(object, event);
3218  object->installEventFilter(this);
3219  }
3220 
3221  publishedPortNearCursorPreviously = publishedPortNearCursor;
3222 
3223  return true;
3224  }
3225 
3226  // Customize handling of keypress events.
3227  else if (event->type() == QEvent::KeyPress)
3228  {
3229  QKeyEvent *keyEvent = (QKeyEvent *)(event);
3230 
3231  // The Esc key closes non-detached popovers.
3232  if (keyEvent->key() == Qt::Key_Escape)
3233  composition->disableNondetachedPortPopovers();
3234 
3235  // Arrow keys may be used to move the viewport when no composition components are selected.
3236  if ((composition->selectedItems().isEmpty()) &&
3237  ((keyEvent->key() == Qt::Key_Up) ||
3238  (keyEvent->key() == Qt::Key_Down) ||
3239  (keyEvent->key() == Qt::Key_Left) ||
3240  (keyEvent->key() == Qt::Key_Right)))
3241  {
3242  keyPressEvent(keyEvent);
3243  }
3244 
3245  // The canvas may be dragged with spacebar+drag.
3246  else if ((object == composition) && (keyEvent->key() == Qt::Key_Space))
3247  {
3248  keyPressEvent(keyEvent);
3249  }
3250 
3251  else
3252  {
3253  object->removeEventFilter(this);
3254  QApplication::sendEvent(object, event);
3255  object->installEventFilter(this);
3256  }
3257  return true;
3258  }
3259 
3260  // Suppress ContextMenu events sent to the composition. We generate them ourselves
3261  // within VuoEditorComposition::mousePressEvent and VuoEditorGraphicsView::viewportEvent
3262  // to customize the selection behavior that accompanies the presentation of the context menu.
3263  else if (event->type() == QEvent::ContextMenu)
3264  {
3265  return true;
3266  }
3267 
3268  return QMainWindow::eventFilter(object, event);
3269 }
3270 
3275 {
3276  return scrollInProgress;
3277 }
3278 
3283 {
3284  return itemDragMacroInProgress;
3285 }
3286 
3291 {
3292  latestDragTime = VuoLogGetElapsedTime();
3293 }
3294 
3299 {
3300  return latestDragTime;
3301 }
3302 
3308 void VuoEditorWindow::enableCanvasDrag()
3309 {
3310  this->canvasDragEnabled = true;
3311  updateUI();
3312 
3313  composition->clearHoverHighlighting();
3314  composition->disableNondetachedPortPopovers();
3315 }
3316 
3320 void VuoEditorWindow::disableCanvasDrag()
3321 {
3322  this->canvasDragEnabled = false;
3323  updateUI();
3324 }
3325 
3329 void VuoEditorWindow::initiateCanvasDrag()
3330 {
3331  this->canvasDragInProgress = true;
3332  QPoint currentCursorPos = QCursor::pos();
3333  this->lastCursorLocationDuringCanvasDrag = currentCursorPos;
3334 
3335  int xNegativeScrollPotential = ui->graphicsView->horizontalScrollBar()->value() - ui->graphicsView->horizontalScrollBar()->minimum();
3336  int yNegativeScrollPotential = ui->graphicsView->verticalScrollBar()->value() - ui->graphicsView->verticalScrollBar()->minimum();
3337 
3338  int xPositiveScrollPotential = ui->graphicsView->horizontalScrollBar()->maximum() - ui->graphicsView->horizontalScrollBar()->value();
3339  int yPositiveScrollPotential = ui->graphicsView->verticalScrollBar()->maximum() - ui->graphicsView->verticalScrollBar()->value();
3340 
3341  // Positive changes in cursor position correspond to negative changes in scrollbar value.
3342  this->canvasDragMinCursorPos = QPoint(currentCursorPos.x() - xPositiveScrollPotential, currentCursorPos.y() - yPositiveScrollPotential);
3343  this->canvasDragMaxCursorPos = QPoint(currentCursorPos.x() + xNegativeScrollPotential, currentCursorPos.y() + yNegativeScrollPotential);
3344 
3345  updateUI();
3346 }
3347 
3351 void VuoEditorWindow::concludeCanvasDrag()
3352 {
3353  this->canvasDragInProgress = false;
3354  this->lastCursorLocationDuringCanvasDrag = QPoint();
3355  updateUI();
3356 }
3357 
3361 void VuoEditorWindow::keyPressEvent(QKeyEvent *event)
3362 {
3363  Qt::KeyboardModifiers modifiers = event->modifiers();
3364  qreal adjustedViewportStepRate = viewportStepRate;
3365  if (modifiers & Qt::ShiftModifier)
3366  {
3367  adjustedViewportStepRate *= viewportStepRateMultiplier;
3368  }
3369 
3370  if (event->key() == Qt::Key_Escape)
3371  {
3372  if (!getCurrentNodeLibrary()->isHidden())
3373  getCurrentNodeLibrary()->close();
3374  else if (arePublishedPortSidebarsVisible())
3375  on_showPublishedPorts_triggered();
3376  return;
3377  }
3378 
3379  if (composition->hasFocus())
3380  {
3381  switch (event->key())
3382  {
3383  case Qt::Key_Up:
3384  {
3385  const int y = ui->graphicsView->verticalScrollBar()->value() -
3386  adjustedViewportStepRate*(ui->graphicsView->verticalScrollBar()->singleStep());
3387  ui->graphicsView->verticalScrollBar()->setValue(y);
3388  break;
3389  }
3390  case Qt::Key_Down:
3391  {
3392  const int y = ui->graphicsView->verticalScrollBar()->value() +
3393  adjustedViewportStepRate*(ui->graphicsView->verticalScrollBar()->singleStep());
3394  ui->graphicsView->verticalScrollBar()->setValue(y);
3395  break;
3396  }
3397  case Qt::Key_Left:
3398  {
3399  const int x = ui->graphicsView->horizontalScrollBar()->value() -
3400  adjustedViewportStepRate*(ui->graphicsView->horizontalScrollBar()->singleStep());
3401  ui->graphicsView->horizontalScrollBar()->setValue(x);
3402  break;
3403  }
3404  case Qt::Key_Right:
3405  {
3406  const int x = ui->graphicsView->horizontalScrollBar()->value() +
3407  adjustedViewportStepRate*(ui->graphicsView->horizontalScrollBar()->singleStep());
3408  ui->graphicsView->horizontalScrollBar()->setValue(x);
3409  break;
3410  }
3411  case Qt::Key_Space:
3412  {
3413  enableCanvasDrag();
3414  break;
3415  }
3416  default:
3417  {
3418  QGraphicsItem *nearbyItem = composition->findNearbyComponent(getCursorScenePos());
3419  VuoRendererPort *nearbyPort = (dynamic_cast<VuoRendererPort *>(nearbyItem)? (VuoRendererPort *)nearbyItem : NULL);
3420  if (nearbyPort)
3421  {
3422  composition->sendEvent(nearbyPort, event);
3423  event->accept();
3424  }
3425  else
3426  QMainWindow::keyPressEvent(event);
3427 
3428  break;
3429  }
3430  }
3431  }
3432 }
3433 
3437 void VuoEditorWindow::keyReleaseEvent(QKeyEvent *event)
3438 {
3439  switch (event->key())
3440  {
3441  case Qt::Key_Space:
3442  {
3443  disableCanvasDrag();
3444  break;
3445  }
3446  default:
3447  {
3448  QMainWindow::keyReleaseEvent(event);
3449  break;
3450  }
3451  }
3452 }
3453 
3457 void VuoEditorWindow::mouseMoveEvent(QMouseEvent *event)
3458 {
3459  if (canvasDragInProgress)
3460  {
3461  QPoint oldPos = lastCursorLocationDuringCanvasDrag;
3462  QPoint currentCursorPos = QCursor::pos();
3463 
3464  // Ignore cursor movements beyond the range that would have affected the scollbar values at the beginning
3465  // of the drag, to ensure a single continuous contact point between cursor and canvas when the cursor is within range.
3466  QPoint effectiveNewPos = QPoint( fmax(canvasDragMinCursorPos.x(), fmin(canvasDragMaxCursorPos.x(), currentCursorPos.x())),
3467  fmax(canvasDragMinCursorPos.y(), fmin(canvasDragMaxCursorPos.y(), currentCursorPos.y())));
3468 
3469  // Positive changes in cursor position correspond to negative changes in scrollbar value.
3470  int dx = -1 * (effectiveNewPos - oldPos).x();
3471  int dy = -1 * (effectiveNewPos - oldPos).y();
3472 
3473  if (dx)
3474  {
3475  lastCursorLocationDuringCanvasDrag.setX(effectiveNewPos.x());
3476 
3477  int xScrollbarMin = ui->graphicsView->horizontalScrollBar()->minimum();
3478  int xScrollbarMax = ui->graphicsView->horizontalScrollBar()->maximum();
3479  int xScrollbarOldValue = ui->graphicsView->horizontalScrollBar()->value();
3480  int xScrollbarNewValue = fmax(xScrollbarMin, fmin(xScrollbarMax, xScrollbarOldValue + dx));
3481 
3482  if (xScrollbarNewValue != xScrollbarOldValue)
3483  ui->graphicsView->horizontalScrollBar()->setValue(xScrollbarNewValue);
3484  }
3485 
3486  if (dy)
3487  {
3488  lastCursorLocationDuringCanvasDrag.setY(effectiveNewPos.y());
3489 
3490  int yScrollbarMin = ui->graphicsView->verticalScrollBar()->minimum();
3491  int yScrollbarMax = ui->graphicsView->verticalScrollBar()->maximum();
3492  int yScrollbarOldValue = ui->graphicsView->verticalScrollBar()->value();
3493  int yScrollbarNewValue = fmax(yScrollbarMin, fmin(yScrollbarMax, yScrollbarOldValue + dy));
3494 
3495 
3496  if (yScrollbarNewValue != yScrollbarOldValue)
3497  ui->graphicsView->verticalScrollBar()->setValue(yScrollbarNewValue);
3498  }
3499  }
3500 
3501  QMainWindow::mouseMoveEvent(event);
3502 }
3503 
3507 void VuoEditorWindow::initializeNodeLibrary(VuoCompiler *nodeLibraryCompiler, VuoNodeLibrary::nodeLibraryDisplayMode nodeLibraryDisplayMode, VuoNodeLibrary::nodeLibraryState nodeLibraryState, VuoNodeLibrary *floater)
3508 {
3509  ownedNodeLibrary = new VuoNodeLibrary(nodeLibraryCompiler, this, nodeLibraryDisplayMode);
3510  ownedNodeLibrary->setObjectName(composition? composition->getBase()->getMetadata()->getName().c_str() : "");
3511 
3512  nl = ownedNodeLibrary;
3513  transitionNodeLibraryConnections(NULL, nl);
3514 
3515  // Dock the library initially before setting it to the docking state dictated by global settings.
3516  // This ensures that if it is ever undocked, double-clicking on its title bar will re-dock it.
3517  addDockWidget(Qt::LeftDockWidgetArea, nl);
3518  conformToGlobalNodeLibraryVisibility(nodeLibraryState, floater, false);
3519 
3520  nodeLibraryMinimumWidth = nl->minimumWidth();
3521  nodeLibraryMaximumWidth = nl->maximumWidth();
3522 }
3523 
3527 void VuoEditorWindow::on_compositionInformation_triggered()
3528 {
3529  metadataEditor->show();
3530 }
3531 
3536 {
3537  composition->deselectAllCompositionComponents();
3539 }
3540 
3544 void VuoEditorWindow::on_zoomIn_triggered()
3545 {
3546  ui->graphicsView->scale(zoomRate,zoomRate);
3547 
3548  // Try to keep selected components visible.
3549  QRectF selectedItemsRect;
3550  foreach (QGraphicsItem *selectedComponent, composition->selectedItems())
3551  selectedItemsRect |= selectedComponent->sceneBoundingRect();
3552 
3553  if (!selectedItemsRect.isNull())
3554  ui->graphicsView->centerOn(selectedItemsRect.center());
3555 
3556  isZoomedToFit = false;
3557  updateUI();
3558 }
3559 
3563 void VuoEditorWindow::on_zoomOut_triggered()
3564 {
3565  ui->graphicsView->scale(1/zoomRate,1/zoomRate);
3566  isZoomedToFit = false;
3567  updateUI();
3568 }
3569 
3573 void VuoEditorWindow::on_zoom11_triggered()
3574 {
3575  ui->graphicsView->setTransform(QTransform());
3576  isZoomedToFit = false;
3577  updateUI();
3578 }
3579 
3584 {
3585  QRectF itemsTightBoundingRect = (!composition->selectedItems().isEmpty()? composition->internalSelectedItemsBoundingRect() :
3586  composition->internalItemsBoundingRect());
3587  QRectF itemsBoundingRect = itemsTightBoundingRect.adjusted(-VuoEditorWindow::compositionMargin,
3588  -VuoEditorWindow::compositionMargin,
3589  VuoEditorWindow::compositionMargin,
3590  VuoEditorWindow::compositionMargin);
3591  ui->graphicsView->fitInView(itemsBoundingRect, Qt::KeepAspectRatio);
3592  updateSceneRect();
3593  isZoomedToFit = true;
3594  updateUI();
3595 }
3596 
3601 {
3602  QRectF itemsTightBoundingRect = (!composition->selectedItems().isEmpty()? composition->internalSelectedItemsBoundingRect() :
3603  composition->internalItemsBoundingRect());
3604 
3605  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
3606 
3607  // If the itemsBoundingRect is larger than the viewport, zoom out to fit.
3608  if ((viewportRect.width() < itemsTightBoundingRect.width()) || (viewportRect.height() < itemsTightBoundingRect.height()))
3610 
3611  // If the viewport just needs to be shifted in order to display all composition components, do that.
3612  else if (!viewportRect.contains(itemsTightBoundingRect))
3613  ui->graphicsView->ensureVisible(itemsTightBoundingRect, VuoEditorWindow::compositionMargin, VuoEditorWindow::compositionMargin);
3614 }
3615 
3616 void VuoEditorWindow::viewportFitReset()
3617 {
3618  isZoomedToFit = false;
3619 
3620  // Instead of calling updateUI(), which is fairly expensive, just update the single affected widget.
3621 // updateUI();
3622  ui->zoomToFit->setEnabled(! isZoomedToFit);
3623 }
3624 
3625 
3626 void VuoEditorWindow::on_saveComposition_triggered()
3627 {
3628  QString savedPath = windowFilePath();
3629  if (! VuoFileUtilities::fileExists(savedPath.toStdString()))
3630  on_saveCompositionAs_triggered();
3631  else
3632  {
3633  VUserLog("%s: Save", getWindowTitleWithoutPlaceholder().toUtf8().data());
3634  saveFile(savedPath);
3635  }
3636 }
3637 
3638 void VuoEditorWindow::on_saveCompositionAs_triggered()
3639 {
3640  string savedPath = saveCompositionAs().toUtf8().constData();
3641  if (savedPath.empty())
3642  return;
3643 }
3644 
3651 QString VuoEditorWindow::saveCompositionAs()
3652 {
3653  // 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
3654  QFileDialog d(this, Qt::Sheet);
3655 // d.setWindowModality(Qt::WindowModal); // Causes dialog to jump.
3656 
3657  if (VuoFileUtilities::fileExists(windowFilePath().toStdString()))
3658  d.setDirectory(windowFilePath());
3659 
3660  else
3661  {
3662  d.selectFile(QString(composition->getBase()->getMetadata()->getName().c_str())
3663  .append(".")
3665  }
3666 
3667  d.setAcceptMode(QFileDialog::AcceptSave);
3668 
3669  // Temporary for https://b33p.net/kosada/node/6762 :
3670  // Since the QFileDialog warns the user about identically named files *before*
3671  // adding the default extension instead of after, for now, don't have it append
3672  // a default extension at all. This way we know exactly what filename the user has
3673  // entered, and if there is a conflict only after we have added the default
3674  // extension, we can abort the save rather than overwriting the existing file.
3675  //d.setDefaultSuffix(VuoEditor::vuoCompositionFileExtension);
3676 
3677  QString savedPath = "";
3678  if (d.exec() == QDialog::Accepted)
3679  {
3680  savedPath = d.selectedFiles()[0];
3681  string dir, file, ext;
3682  VuoFileUtilities::splitPath(savedPath.toUtf8().constData(), dir, file, ext);
3683  VUserLog("%s: Save as %s.%s", getWindowTitleWithoutPlaceholder().toUtf8().data(), file.c_str(), ext.c_str());
3684  saveFileAs(savedPath);
3685  }
3686 
3688  return savedPath;
3689 }
3690 
3698 bool VuoEditorWindow::saveFileAs(const QString & savePath)
3699 {
3700  string dir, file, ext;
3701  VuoFileUtilities::splitPath(savePath.toUtf8().constData(), dir, file, ext);
3702  bool installingAsSubcomposition = (VuoFileUtilities::getUserModulesPath().c_str() == QFileInfo(dir.c_str()).canonicalFilePath());
3703 
3704  QDir newCompositionDir(QFileInfo(savePath).absoluteDir().canonicalPath());
3705 
3706  map<VuoPort *, string> modifiedPortConstantRelativePaths = composition->getPortConstantResourcePathsRelativeToDir(newCompositionDir);
3707  string modifiedIconPath = composition->getAppIconResourcePathRelativeToDir(newCompositionDir);
3708  bool iconPathChanged = (modifiedIconPath != composition->getBase()->getMetadata()->getIconURL());
3709 
3710  if (!installingAsSubcomposition && (!modifiedPortConstantRelativePaths.empty() || iconPathChanged))
3711  {
3712  // If the previous storage directory was the /tmp directory (as in the case of example compositions),
3713  // offer to copy resource files to the new directory; otherwise, offer to update resource paths referenced
3714  // within the composition so that they are correct when resolved relative to the new directory.
3715  QDir compositionDir = QDir(composition->getBase()->getDirectory().c_str());
3716  bool copyTmpFiles = (compositionDir.canonicalPath() == QDir(VuoFileUtilities::getTmpDir().c_str()).canonicalPath());
3717 
3718  map<VuoPort *, string> pathsToUpdate;
3719  QString copiedFileDetails = "";
3720  QString updatedPathDetails = "";
3721 
3722  // Update relative paths referenced within port constants.
3723  for (map <VuoPort *, string>::iterator i = modifiedPortConstantRelativePaths.begin(); i != modifiedPortConstantRelativePaths.end(); ++i)
3724  {
3725  VuoPort *port = i->first;
3726  string modifiedPath = i->second;
3727 
3728  // Check whether this is a /tmp file (so that we copy it rather than updating the path).
3729  QString origRelativePath = VuoText_makeFromString(port->getRenderer()->getConstantAsString().c_str());
3730  string origRelativeDir, file, ext;
3731  VuoFileUtilities::splitPath(origRelativePath.toUtf8().constData(), origRelativeDir, file, ext);
3732  string resourceFileName = file;
3733  if (!ext.empty())
3734  {
3735  resourceFileName += ".";
3736  resourceFileName += ext;
3737  }
3738  QString origAbsolutePath = compositionDir.filePath(QDir(origRelativeDir.c_str()).filePath(resourceFileName.c_str()));
3739 
3740  // Mark the file for copying or the path for updating, as appropriate.
3741  if (copyTmpFiles && VuoRendererComposition::isTmpFile(origAbsolutePath.toUtf8().constData()))
3742  copiedFileDetails.append(QString("%1\n").arg(origRelativePath));
3743  else
3744  {
3745  updatedPathDetails.append(QString("%1 → %2\n").arg(origRelativePath, VuoText_makeFromString(modifiedPath.c_str())));
3746  pathsToUpdate[port] = modifiedPath;
3747  }
3748  }
3749 
3750  // Update relative path to custom app icon.
3751  if (iconPathChanged)
3752  {
3753  QString origRelativePath = composition->getBase()->getMetadata()->getIconURL().c_str();
3754 
3755  // Check whether this is a /tmp file (so that we copy it rather than updating the path).
3756  string origRelativeDir, file, ext;
3757  VuoFileUtilities::splitPath(origRelativePath.toUtf8().constData(), origRelativeDir, file, ext);
3758  string resourceFileName = file;
3759  if (!ext.empty())
3760  {
3761  resourceFileName += ".";
3762  resourceFileName += ext;
3763  }
3764  QString origAbsolutePath = compositionDir.filePath(QDir(origRelativeDir.c_str()).filePath(resourceFileName.c_str()));
3765 
3766  // Mark the file for copying or the path for updating, as appropriate.
3767  if (copyTmpFiles && VuoRendererComposition::isTmpFile(origAbsolutePath.toUtf8().constData()))
3768  copiedFileDetails.append(QString("%1\n").arg(origRelativePath));
3769  else
3770  updatedPathDetails.append(QString("%1 → %2\n").arg(origRelativePath, modifiedIconPath.c_str()));
3771  }
3772 
3773  bool copyFiles = (!copiedFileDetails.isEmpty());
3774  bool updatePaths = (!updatedPathDetails.isEmpty());
3775  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3776  const QString updateSummary = "<p>" + tr("You're saving your composition to a different folder.") + "</p>"
3777  + "<p>"
3778  + (copyFiles
3779  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3780  ? tr("Do you want to copy the example files used by your composition?")
3781  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3782  : tr("Do you want to update the paths to files in your composition?"))
3783  + "</p>";
3784 
3785  QString updateDetails = "";
3786  if (copyFiles)
3787  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3788  updateDetails += tr("The following file(s) will be copied", "", modifiedPortConstantRelativePaths.size()) + ":\n\n" + copiedFileDetails + "\n";
3789 
3790  if (updatePaths)
3791  //: Appears in a dialog after selecting File > Save As on a composition that refers to resources with relative paths.
3792  updateDetails += tr("The following path(s) will be updated", "", pathsToUpdate.size()) + ":\n\n" + updatedPathDetails;
3793 
3794  QMessageBox messageBox(this);
3795  messageBox.setWindowFlags(Qt::Sheet);
3796  messageBox.setWindowModality(Qt::WindowModal);
3797  messageBox.setTextFormat(Qt::RichText);
3798  messageBox.setStandardButtons(QMessageBox::Discard | QMessageBox::Ok);
3799  messageBox.setButtonText(QMessageBox::Discard, (copyFiles? tr("Leave files in place") : tr("Leave paths unchanged")));
3800  messageBox.setButtonText(QMessageBox::Ok, (copyFiles? tr("Copy files") : tr("Update paths")));
3801  messageBox.setDefaultButton(QMessageBox::Ok);
3802  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
3803  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
3804  messageBox.setText(updateSummary);
3805  messageBox.setDetailedText(updateDetails);
3806 
3807  // Give the "Leave in place" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
3808  static_cast<QPushButton *>(messageBox.button(QMessageBox::Discard))->setAutoDefault(false);
3809  messageBox.button(QMessageBox::Discard)->setFocus();
3810 
3811  if (messageBox.exec() == QMessageBox::Ok)
3812  {
3813  // Copy example resource files to the new directory.
3814  if (copyFiles)
3815  {
3816  bool tmpFilesOnly = true;
3817  composition->bundleResourceFiles(newCompositionDir.canonicalPath().toUtf8().constData(), tmpFilesOnly);
3818  }
3819  // Update relative resource paths.
3820  if (updatePaths)
3821  {
3822  undoStack->beginMacro(tr("File Path Updates"));
3823 
3824  // Update relative URLs referenced within port constants.
3825  for (map <VuoPort *, string>::iterator i = pathsToUpdate.begin(); i != pathsToUpdate.end(); ++i)
3826  {
3827  VuoPort *port = i->first;
3828  string modifiedPath = i->second;
3829 
3830  QString origPath = VuoText_makeFromString(port->getRenderer()->getConstantAsString().c_str());
3831  setPortConstant(port->getRenderer(), modifiedPath);
3832  }
3833 
3834 
3835  // Update relative path to custom icon.
3836  if (iconPathChanged)
3837  {
3838  string customizedCompositionName = composition->getBase()->getMetadata()->getCustomizedName();
3839 
3840  VuoCompositionMetadata *metadataCopy = new VuoCompositionMetadata(*composition->getBase()->getMetadata());
3841  metadataCopy->setName(customizedCompositionName);
3842  metadataCopy->setIconURL(modifiedIconPath);
3843 
3844  undoStack->push(new VuoCommandSetMetadata(metadataCopy, this));
3845 
3846  }
3847 
3848  undoStack->endMacro();
3849  }
3850  }
3851  }
3852 
3853  return saveFile(savePath);
3854 }
3855 
3856 string VuoEditorWindow::on_installSubcomposition_triggered()
3857 {
3858  QString oldTitle = getWindowTitleWithoutPlaceholder();
3859 
3860  string nodeClassName = installSubcomposition("");
3861 
3862  VUserLog("%s: %s: %s",
3863  oldTitle.toUtf8().data(),
3864  ui->installSubcomposition->text().toUtf8().data(),
3865  nodeClassName.c_str());
3866 
3867  if (!nodeClassName.empty())
3868  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
3869 
3870  return nodeClassName;
3871 }
3872 
3877 string VuoEditorWindow::installSubcomposition(string parentCompositionPath)
3878 {
3879  string currentCompositionPath = windowFilePath().toUtf8().constData();
3880  bool currentCompositionExists = VuoFileUtilities::fileExists(currentCompositionPath);
3881 
3882  QString operationTitle;
3883  string installedSubcompositionDir;
3884  if (parentCompositionPath.empty()) // "File > Move/Save to User Library"
3885  {
3886  if (currentCompositionExists)
3887  operationTitle = tr("Move Subcomposition to User Library");
3888  else
3889  operationTitle = tr("Save Subcomposition to User Library");
3890  installedSubcompositionDir = VuoFileUtilities::getUserModulesPath();
3891  }
3892  else // "Edit > Insert Subcomposition" or "Edit > Package as Subcomposition"
3893  {
3895  {
3896  operationTitle = tr("Save Subcomposition to User Library");
3897  installedSubcompositionDir = VuoFileUtilities::getUserModulesPath();
3898  }
3900  {
3901  operationTitle = tr("Save Subcomposition to System Library");
3902  installedSubcompositionDir = VuoFileUtilities::getSystemModulesPath();
3903  }
3904  else
3905  {
3906  operationTitle = tr("Save Subcomposition to Composition-Local Library");
3907  installedSubcompositionDir = VuoFileUtilities::getCompositionLocalModulesPath(parentCompositionPath);
3908  }
3909  }
3910 
3911  VuoFileUtilities::makeDir(installedSubcompositionDir);
3912  string nodeClassName;
3913 
3914  // Case: The composition is already installed as a module. Preserve its existing node class name.
3915  if (VuoFileUtilities::fileExists(currentCompositionPath) && VuoFileUtilities::isInstalledAsModule(currentCompositionPath))
3916  nodeClassName = VuoCompiler::getModuleKeyForPath(currentCompositionPath);
3917 
3918  // Case: The composition is being installed as a module for the first time. It needs a name.
3919  else
3920  {
3921  QString defaultNodeDisplayName = composition->formatCompositionFileNameForDisplay(composition->getBase()->getMetadata()->getDefaultName().c_str());
3922  QString defaultNodeCategory = static_cast<VuoEditor *>(qApp)->getDefaultSubcompositionPrefix();
3923 
3924  QString currentNodeDisplayName = (!composition->getBase()->getMetadata()->getCustomizedName().empty()?
3925  composition->getBase()->getMetadata()->getCustomizedName().c_str() :
3926  defaultNodeDisplayName);
3927  QString currentNodeCategory = static_cast<VuoEditor *>(qApp)->getSubcompositionPrefix();
3928 
3929  VuoSubcompositionSaveAsDialog d(this, Qt::Sheet, operationTitle,
3930  defaultNodeDisplayName, defaultNodeCategory,
3931  currentNodeDisplayName, currentNodeCategory);
3932  bool ok = d.exec();
3933  if (!ok)
3934  return "";
3935 
3936  QString nodeDisplayName = d.nodeTitle();
3937  QString nodeCategory = d.nodeCategory();
3938 
3939  if (nodeCategory != currentNodeCategory)
3940  static_cast<VuoEditor *>(qApp)->updateSubcompositionPrefix(nodeCategory);
3941 
3942  if (composition->getBase()->getMetadata()->getCustomizedName() != nodeDisplayName.toUtf8().constData())
3943  {
3944  VuoCompositionMetadata *newMetadata = new VuoCompositionMetadata(*composition->getBase()->getMetadata());
3945  newMetadata->setName(nodeDisplayName.toUtf8().constData());
3946  undoStack->push(new VuoCommandSetMetadata(newMetadata, this));
3947  }
3948 
3949  nodeClassName = getNodeClassNameForDisplayNameAndCategory(nodeDisplayName, nodeCategory, defaultNodeDisplayName, defaultNodeCategory).toStdString();
3950  }
3951 
3952  string copiedCompositionPath = installedSubcompositionDir + "/" + nodeClassName + "." + VuoEditor::vuoCompositionFileExtension.toUtf8().constData();
3953 
3954  // Make sure the node class doesn't already exist as a .vuo or .vuonode file in the same directory.
3955  int documentIdentifierInstanceNum = 1;
3956  while (VuoFileUtilities::fileExists(copiedCompositionPath) ||
3957  VuoFileUtilities::fileExists(copiedCompositionPath + "node"))
3958  {
3959  std::ostringstream oss;
3960  oss << ++documentIdentifierInstanceNum;
3961  copiedCompositionPath = installedSubcompositionDir + "/" + nodeClassName + oss.str() + "." + VuoEditor::vuoCompositionFileExtension.toUtf8().constData();
3962  }
3963 
3964  bool saveSucceeded = saveFileAs(copiedCompositionPath.c_str());
3965  if (! saveSucceeded)
3966  return "";
3967 
3968  VuoFileUtilities::deleteFile(currentCompositionPath);
3969 
3970  return VuoCompiler::getModuleKeyForPath(windowFilePath().toStdString());
3971 }
3972 
3977 QString VuoEditorWindow::getNodeClassNameForDisplayNameAndCategory(QString compositionName, QString category, QString defaultCompositionName, QString defaultCategory)
3978 {
3979  compositionName = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(compositionName.toStdString()));
3980  if (compositionName.isEmpty())
3981  compositionName = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(defaultCompositionName.toStdString()));
3982 
3983  // Treat "vuo" as a reserved prefix.
3984  QString thirdPartyCategory = category.remove(QRegExp("^(vuo\\.?)+", Qt::CaseInsensitive));
3985 
3986  category = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(thirdPartyCategory.toStdString()));
3987  if (category.isEmpty())
3988  category = QString::fromStdString(deriveBaseNodeClassNameFromDisplayName(defaultCategory.toStdString()));
3989 
3990  return category + "." + compositionName;
3991 }
3992 
4002 {
4003  if (displayName.empty())
4004  return "";
4005 
4006  // Transliterate Unicode to Latin ASCII, to provide some support for non-English-language names.
4007  {
4008  CFStringRef cfs = CFStringCreateWithCString(NULL, displayName.c_str(), kCFStringEncodingUTF8);
4009  if (!cfs)
4010  return "";
4011  CFMutableStringRef cfsm = CFStringCreateMutableCopy(NULL, 0, cfs);
4012  CFStringTransform(cfsm, NULL, kCFStringTransformToLatin, false); // Converts 'ä' -> 'a', '張' -> 'zhang'.
4013  CFStringTransform(cfsm, NULL, kCFStringTransformStripCombiningMarks, false);
4014  CFStringTransform(cfsm, NULL, kCFStringTransformStripDiacritics, false);
4015  CFStringTransform(cfsm, NULL, CFSTR("ASCII"), false); // Converts 'ß' -> 'ss'.
4016  displayName = VuoStringUtilities::makeFromCFString(cfsm);
4017  CFRelease(cfsm);
4018  CFRelease(cfs);
4019  }
4020 
4021  return VuoStringUtilities::convertToCamelCase(displayName, false, true, false, true);
4022 }
4023 
4028 void VuoEditorWindow::editMetadata(int dialogResult)
4029 {
4030  if (dialogResult == QDialog::Accepted)
4031  undoStack->push(new VuoCommandSetMetadata(metadataEditor->toMetadata(), this));
4032 }
4033 
4037 string VuoEditorWindow::generateCurrentDefaultCopyright()
4038 {
4039  const string user = static_cast<VuoEditor *>(qApp)->getUserName();
4040  const string userProfileURL = static_cast<VuoEditor *>(qApp)->getStoredUserProfileURL();
4041  const string userProfileLink = (userProfileURL.empty()? user : "[" + user + "](" + userProfileURL + ")");
4042  const string currentYear = QDate::currentDate().toString("yyyy").toUtf8().constData();
4043 
4044  const string copyright = "Copyright © " + currentYear + " " + userProfileLink;
4045  return copyright;
4046 }
4047 
4051 bool VuoEditorWindow::confirmReplacingFile(string path)
4052 {
4054  QFileInfo fileInfo(QString::fromStdString(path));
4055  QMessageBox d(this);
4056  d.setWindowFlags(Qt::Sheet);
4057  d.setWindowModality(Qt::WindowModal);
4058  d.setFont(fonts->dialogHeadingFont());
4059  d.setTextFormat(Qt::RichText);
4060  d.setText(tr("<b>“%1” already exists. Do you want to replace it?</b>").arg(fileInfo.fileName()));
4061  d.setInformativeText("<style>p{" + fonts->getCSS(fonts->dialogBodyFont()) + "}</style>"
4062  + 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>")
4063  .arg(VuoFileUtilities::dirExists(path) ? "folder" : "file")
4064  .arg(fileInfo.dir().dirName()));
4065  d.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
4066  d.setDefaultButton(QMessageBox::Cancel);
4067  d.setButtonText(QMessageBox::Yes, tr("Replace"));
4068  d.setIcon(QMessageBox::Warning);
4069 
4070  // Give the "Replace" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
4071  static_cast<QPushButton *>(d.button(QMessageBox::Yes))->setAutoDefault(false);
4072  d.button(QMessageBox::Yes)->setFocus();
4073 
4074  return d.exec() == QMessageBox::Yes;
4075 }
4076 
4082 {
4083  addDockWidget(Qt::TopDockWidgetArea, searchBox);
4084  searchBox->setFocus();
4085  searchBox->show();
4086  updateUI();
4087 }
4088 
4093 {
4094  if (!searchBox->isHidden())
4095  searchBox->goToNextResult();
4096 }
4097 
4102 {
4103  if (!searchBox->isHidden())
4104  searchBox->goToPreviousResult();
4105 }
4106 
4113 bool VuoEditorWindow::saveFile(const QString & savePath)
4114 {
4115  // Temporary for https://b33p.net/kosada/node/6762 :
4116  // The file dialogue does not currently append the default file suffix automatically;
4117  // do so here. If the addition of the suffix results in a file-name conflict,
4118  // abort the save, since the user has not given permission to overwrite the existing composition.
4119  bool saveAborted = false;
4120  QString failureDetails = "";
4121  QString modifiedSavePath = savePath;
4122  QString expectedFileExtension = QString(".").append(VuoEditor::vuoCompositionFileExtension);
4123  if (!savePath.endsWith(expectedFileExtension))
4124  {
4125  modifiedSavePath.append(expectedFileExtension);
4126  if (VuoFileUtilities::fileExists(modifiedSavePath.toStdString()))
4127  {
4128  saveAborted = true;
4129  failureDetails = "A file or folder with the same name already exists.";
4130  }
4131  }
4132 
4133  QString existingPath = windowFilePath();
4134  bool saveSucceeded = !saveAborted && saveFile(modifiedSavePath, existingPath);
4135  int error = errno;
4136 
4137  if (saveSucceeded)
4138  {
4139  // Clear the "Untitled Composition" window title, since windowTitle overrides windowFilePath.
4140  if (windowTitle().startsWith(untitledComposition))
4141  setWindowTitle("");
4142 
4143  setWindowFilePath(modifiedSavePath);
4144 #if VUO_PRO
4145  toolbar->updateTitle();
4146 #endif
4147  compositionUpgradedSinceLastSave = false;
4148  undoStack->setClean();
4149  setCompositionModified(false);
4150 
4151  string oldDir = composition->getBase()->getDirectory();
4152  string newDir, file, ext;
4153  VuoFileUtilities::splitPath(modifiedSavePath.toUtf8().constData(), newDir, file, ext);
4154  bool compositionDirChanged = ! VuoFileUtilities::arePathsEqual(oldDir, newDir);
4155  if (compositionDirChanged)
4156  {
4157  // Find any composition-local modules that the composition depends on.
4158  QStringList compositionLocalModules;
4159  for (const string &dependency : compiler->getDependenciesForComposition(getComposition()->getBase()->getCompiler()))
4160  if (compiler->isCompositionLocalModule(dependency))
4161  compositionLocalModules.append(QString::fromStdString(dependency));
4162 
4163  if (! compositionLocalModules.empty())
4164  {
4165  compositionLocalModules.sort();
4166 
4167  QString summary = QString("<p>You're saving your composition to a different folder.</p>")
4168  .append("<p>Where do you want to install the composition's local nodes?</p>");
4169 
4170  QString details = QString("The following nodes will be moved or copied:\n\n")
4171  .append(compositionLocalModules.join("\n"))
4172  .append("\n");
4173 
4174  QMessageBox messageBox(this);
4175  messageBox.setWindowFlags(Qt::Sheet);
4176  messageBox.setWindowModality(Qt::WindowModal);
4177  messageBox.setTextFormat(Qt::RichText);
4178  messageBox.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
4179  messageBox.setButtonText(QMessageBox::No, "Move to User Library");
4180  messageBox.setButtonText(QMessageBox::Yes, "Copy to new Composition Library");
4181  messageBox.setDefaultButton(QMessageBox::Yes);
4182  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
4183  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
4184  messageBox.setText(summary);
4185  messageBox.setDetailedText(details);
4186 
4187  // Give the non-default button keyboard focus so that it can be activated by spacebar.
4188  static_cast<QPushButton *>(messageBox.button(QMessageBox::No))->setAutoDefault(false);
4189  messageBox.button(QMessageBox::No)->setFocus();
4190 
4191  int ret = messageBox.exec();
4192 
4193  string oldModulesDirPath = VuoFileUtilities::getCompositionLocalModulesPath(existingPath.toStdString());
4194  QDir oldModulesDir = QDir(QString::fromStdString(oldModulesDirPath));
4195 
4196  string newModulesDirPath = (ret == QMessageBox::Yes ?
4197  VuoFileUtilities::getCompositionLocalModulesPath(modifiedSavePath.toStdString()) :
4198  VuoFileUtilities::getUserModulesPath());
4199  VuoFileUtilities::makeDir(newModulesDirPath);
4200  QDir newModulesDir(QString::fromStdString(newModulesDirPath));
4201 
4202  // Move/copy the required composition-local modules from the old to the new Modules dir.
4203  foreach (QString moduleFileName, oldModulesDir.entryList(QDir::Files))
4204  {
4205  string moduleKey = VuoCompiler::getModuleKeyForPath(moduleFileName.toStdString());
4206 
4207  if (compositionLocalModules.contains(QString::fromStdString(moduleKey)))
4208  {
4209  string oldModulePath = oldModulesDir.filePath(moduleFileName).toStdString();
4210  string newModulePath = newModulesDir.filePath(moduleFileName).toStdString();
4211 
4212  // Skip if the file already exists in the new Modules dir.
4213  if (! VuoFileUtilities::fileExists(newModulePath))
4214  {
4215  if (ret == QMessageBox::Yes)
4216  VuoFileUtilities::copyFile(oldModulePath, newModulePath);
4217  else
4218  VuoFileUtilities::moveFile(oldModulePath, newModulePath);
4219  }
4220  }
4221  }
4222  }
4223  }
4224 
4225  // Update the compiler's list of loaded modules.
4226  compiler->setCompositionPath(modifiedSavePath.toUtf8().constData());
4227 
4228  if (includeInRecentFileMenu)
4229  static_cast<VuoEditor *>(qApp)->addFileToAllOpenRecentFileMenus(modifiedSavePath);
4230  }
4231 
4232  else
4233  {
4234  if (failureDetails.isEmpty())
4235  failureDetails = strerror(error);
4236 
4237  QMessageBox fileSaveFailureDialog(this);
4238  fileSaveFailureDialog.setWindowFlags(Qt::Sheet);
4239  fileSaveFailureDialog.setWindowModality(Qt::WindowModal);
4240  //: Appears in a dialog after selecting File > Save or Save As.
4241  QString errorMessage = tr("The composition could not be saved at “%1”.").arg(modifiedSavePath);
4242  fileSaveFailureDialog.setText(tr(errorMessage.toUtf8().constData()));
4243  fileSaveFailureDialog.setStyleSheet("#qt_msgbox_informativelabel { font-weight: normal; font-size: 11pt; }");
4244  fileSaveFailureDialog.setInformativeText(failureDetails);
4245  fileSaveFailureDialog.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
4246  fileSaveFailureDialog.setButtonText(QMessageBox::Save, tr("Save As…"));
4247  fileSaveFailureDialog.setIcon(QMessageBox::Warning);
4248 
4249  switch(fileSaveFailureDialog.exec()) {
4250  case QMessageBox::Save:
4251  on_saveCompositionAs_triggered();
4252  break;
4253  case QMessageBox::Cancel:
4254  break;
4255  default:
4256  break;
4257  }
4258  }
4259 
4260  return saveSucceeded;
4261 }
4262 
4266 void VuoEditorWindow::populateProtocolsMenu(QMenu *m)
4267 {
4268  foreach (VuoProtocol *protocol, VuoProtocol::getProtocols())
4269  {
4270  QAction *protocolAction = new QAction(this);
4271  protocolAction->setText(VuoEditor::tr(protocol->getName().c_str()));
4272  protocolAction->setData(qVariantFromValue(static_cast<void *>(protocol)));
4273  protocolAction->setCheckable(true);
4274  connect(protocolAction, &QAction::triggered, this, &VuoEditorWindow::changeActiveProtocol);
4275  m->addAction(protocolAction);
4276  }
4277 }
4278 
4282 void VuoEditorWindow::updateProtocolsMenu(QMenu *m)
4283 {
4284  foreach (QAction *protocolAction, m->actions())
4285  {
4286  VuoProtocol *currentProtocol = static_cast<VuoProtocol *>(protocolAction->data().value<void *>());
4287  protocolAction->setChecked(composition->getActiveProtocol() == currentProtocol);
4288  }
4289 }
4290 
4295 void VuoEditorWindow::changeActiveProtocol(void)
4296 {
4297  QAction *sender = (QAction *)QObject::sender();
4298  VuoProtocol *selectedProtocol = static_cast<VuoProtocol *>(sender->data().value<void *>());
4299 
4300  VUserLog("%s: %s protocol %s {",
4301  getWindowTitleWithoutPlaceholder().toUtf8().data(),
4302  composition->getActiveProtocol() == selectedProtocol ? "Remove" : "Add",
4303  selectedProtocol->getName().c_str());
4304 
4305  // @todo: Account for multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
4306  toggleActiveStatusForProtocol(selectedProtocol);
4307 
4308  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
4309 }
4310 
4314 void VuoEditorWindow::toggleActiveStatusForProtocol(VuoProtocol *protocol)
4315 {
4316  if (composition->getActiveProtocol() == protocol)
4317  composition->removeActiveProtocol(protocol, NULL);
4318  else
4319  composition->addActiveProtocol(protocol, true);
4320 }
4321 
4325 void VuoEditorWindow::updateActiveProtocolDisplay(void)
4326 {
4327  inputPortSidebar->updateActiveProtocol();
4328  outputPortSidebar->updateActiveProtocol();
4329  setPublishedPortSidebarVisibility(true);
4330  updateUI();
4331 }
4332 
4337 void VuoEditorWindow::evaluateCompositionForProtocolPromotion()
4338 {
4339  // @todo: Account for multiple simultaneous active protocols. https://b33p.net/kosada/node/9585
4340  if (composition->getActiveProtocol())
4341  return;
4342 
4343  string compositionAsString = composition->takeSnapshot();
4344  foreach (VuoProtocol *protocol, VuoProtocol::getProtocols())
4345  {
4346  if (protocol->isCompositionCompliant(compositionAsString))
4347  {
4348  composition->addActiveProtocol(protocol, false);
4349  return;
4350  }
4351  }
4352 
4353  return;
4354 }
4355 
4363 bool VuoEditorWindow::saveFile(const QString & savePath, QString & existingPath)
4364 {
4365  string existingCompositionFooter = "";
4366 
4367  // Modifying an existing composition
4368  if (! existingPath.isEmpty())
4369  {
4370  ifstream existingFile(existingPath.toUtf8().constData());
4371 
4372  // Preserve the original composition footer (everything after the final '}').
4373  bool graphvizStatementListEndFound = false;
4374  for (char c = existingFile.get(); existingFile.good(); c = existingFile.get())
4375  {
4376  if (existingFile.good())
4377  {
4378  if (c == '}')
4379  {
4380  existingCompositionFooter = "";
4381  graphvizStatementListEndFound = true;
4382  }
4383  else if (graphvizStatementListEndFound)
4384  existingCompositionFooter += c;
4385  }
4386  }
4387  existingFile.close();
4388  }
4389 
4390  // Update the version of Vuo last used to save the composition.
4391  string previousLastSavedInVuoVersion = composition->getBase()->getMetadata()->getLastSavedInVuoVersion();
4392  composition->getBase()->getMetadata()->setLastSavedInVuoVersion(VUO_VERSION_STRING);
4393 
4394  // Generate the modified composition.
4395  string compositionHeader = composition->generateCompositionHeader();
4396  string outputComposition = composition->getBase()->getCompiler()->getGraphvizDeclaration(composition->getActiveProtocol(),
4397  compositionHeader,
4398  existingCompositionFooter);
4399  // Save the modified composition to a temporary file.
4400  string dir, file, ext;
4401  string finalSavePath = savePath.toUtf8().constData();
4402  VuoFileUtilities::splitPath(finalSavePath, dir, file, ext);
4403  string tmpSavePath = VuoFileUtilities::makeTmpFile("." + file, ext, dir);
4404 
4405  // The mode of temp files defaults to 0600.
4406  // Change it to match the process's umask.
4407  {
4408  // http://man7.org/linux/man-pages/man2/umask.2.html says:
4409  // "It is impossible to use umask() to fetch a process's umask without at
4410  // the same time changing it. A second call to umask() would then be
4411  // needed to restore the umask."
4412  mode_t currentUMask = umask(0);
4413  umask(currentUMask);
4414 
4415  chmod(tmpSavePath.c_str(), 0666 & ~currentUMask);
4416  }
4417 
4418  ofstream savedFile(tmpSavePath.c_str(), ios::trunc);
4419  savedFile << outputComposition;
4420  savedFile.close();
4421 
4422  string defaultName = VuoEditorComposition::getDefaultNameForPath(finalSavePath);
4423  composition->getBase()->getMetadata()->setDefaultName(defaultName);
4424 
4425  // Move the generated temporary file to the desired save path.
4426  bool saveSucceeded = ((! savedFile.fail()) && (! rename(tmpSavePath.c_str(), finalSavePath.c_str())));
4427 
4428  if (saveSucceeded)
4429  {
4430  this->composition->getBase()->setDirectory(dir);
4431  this->metadataPanel->setIsUserComposition(true);
4432  this->metadataPanel->update();
4433  }
4434  else
4435  this->composition->getBase()->getMetadata()->setLastSavedInVuoVersion(previousLastSavedInVuoVersion);
4436 
4437  return saveSucceeded;
4438 }
4439 
4443 void VuoEditorWindow::undoStackCleanStateChanged(bool clean)
4444 {
4445  // When the Undo stack leaves its clean state, mark the composition as modified.
4446  if (!clean)
4447  setCompositionModified(true);
4448 
4449  // Mark the composition as unmodified when the Undo stack enters
4450  // its clean state as long as the composition has not been modified by the
4451  // upgrade manager since its last save.
4452  else if (clean && !compositionUpgradedSinceLastSave)
4453  setCompositionModified(false);
4454 }
4455 
4459 void VuoEditorWindow::setCompositionModified(bool modified)
4460 {
4461  setWindowModified(modified);
4462  updateUI();
4463 }
4464 
4469 {
4470  VUserLog("%s: Run", getWindowTitleWithoutPlaceholder().toUtf8().data());
4471 
4472  toolbar->changeStateToBuildPending();
4473 
4474  string snapshot = composition->takeSnapshot();
4475  composition->run(snapshot);
4476 
4477  updateUI();
4478 }
4479 
4484 {
4485  VUserLog("%s: Stop", getWindowTitleWithoutPlaceholder().toUtf8().data());
4486 
4487  toolbar->changeStateToStopInProgress();
4488  updateUI();
4489 
4490  composition->stop();
4491 }
4492 
4497 {
4500 }
4501 
4506 {
4507  composition->refireTriggerPortEvent();
4508 }
4509 
4513 void VuoEditorWindow::showBuildActivityIndicator()
4514 {
4515  toolbar->changeStateToBuildInProgress();
4516  updateUI();
4517 }
4518 
4522 void VuoEditorWindow::hideBuildActivityIndicator(QString buildError)
4523 {
4524  if (! buildError.isEmpty())
4525  {
4526  toolbar->changeStateToStopped();
4527  updateUI();
4528  updateRefireAction();
4529 
4530  QString details = "";
4531  if (buildError.contains("Nodes not installed", Qt::CaseInsensitive))
4532  details = "This composition contains nodes that aren't installed.";
4533 
4534  VuoErrorDialog::show(this, tr("There was a problem running the composition."), details, buildError);
4535  }
4536  else
4537  {
4538  toolbar->changeStateToRunning();
4539  updateUI();
4540  updateRefireAction();
4541  }
4542 }
4543 
4547 void VuoEditorWindow::hideStopActivityIndicator()
4548 {
4549  toolbar->changeStateToStopped();
4550  updateUI();
4551  updateRefireAction();
4552 }
4553 
4554 void VuoEditorWindow::on_openUserModulesFolder_triggered()
4555 {
4557 }
4558 
4559 void VuoEditorWindow::on_openSystemModulesFolder_triggered()
4560 {
4562 }
4563 
4568 {
4569  if (nl->isHidden())
4571 
4572  nl->focusTextFilter();
4573 }
4574 
4578 void VuoEditorWindow::on_showPublishedPorts_triggered(void)
4579 {
4580  setPublishedPortSidebarVisibility(!arePublishedPortSidebarsVisible());
4581  updateUI();
4582 }
4583 
4589 {
4590  if (!arePublishedPortSidebarsVisible())
4591  on_showPublishedPorts_triggered();
4592 }
4593 
4597 bool VuoEditorWindow::arePublishedPortSidebarsVisible()
4598 {
4599  return !inputPortSidebar->isHidden() && !outputPortSidebar->isHidden();
4600 }
4601 
4605 void VuoEditorWindow::on_showHiddenCables_triggered(void)
4606 {
4607  bool previouslyShowingHiddenCables = composition->getRenderHiddenCables();
4608  if (!previouslyShowingHiddenCables)
4609  {
4610  if (composition->hasHiddenPublishedCables() && (inputPortSidebar->isHidden() || outputPortSidebar->isHidden()))
4611  on_showPublishedPorts_triggered();
4612  }
4613 
4614  composition->setRenderHiddenCables(!previouslyShowingHiddenCables);
4615 
4616  updateUI();
4617 }
4618 
4622 void VuoEditorWindow::on_showEvents_triggered(void)
4623 {
4624  VUserLog("%s: %s Show Events mode",
4625  getWindowTitleWithoutPlaceholder().toUtf8().data(),
4626  composition->getShowEventsMode() ? "Disable" : "Enable");
4627 
4628  composition->setShowEventsMode(! composition->getShowEventsMode());
4629  updateUI();
4630 }
4631 
4635 void VuoEditorWindow::closePublishedPortSidebars()
4636 {
4637  setPublishedPortSidebarVisibility(false);
4638 }
4639 
4643 void VuoEditorWindow::conditionallyShowPublishedPortSidebars(bool visible)
4644 {
4645  if (visible)
4646  setPublishedPortSidebarVisibility(true);
4647 }
4648 
4652 void VuoEditorWindow::setPublishedPortSidebarVisibility(bool visible)
4653 {
4654  // Display the published input and output port sidebars as widgets docked in the left
4655  // and right docking areas, respectively.
4656  if (visible)
4657  {
4658  // If the node library is already docked in the left docking area, situate the
4659  // input port sidebar between the node library and the canvas.
4660  if (nl && (! nl->isHidden()) && (! nl->isFloating()))
4661  splitDockWidget(nl, inputPortSidebar, Qt::Horizontal);
4662 
4663  else
4664  addDockWidget(Qt::LeftDockWidgetArea, inputPortSidebar);
4665 
4666  addDockWidget(Qt::RightDockWidgetArea, outputPortSidebar);
4667 
4668  this->setFocus();
4669  }
4670 
4671  else
4672  {
4673  // Prevent the docked node library from expanding to fill all available space when
4674  // there are changes to the input port sidebar's visibility.
4675  if (! nl->isFloating())
4676  {
4677  nl->setMinimumWidth(nl->width());
4678  nl->setMaximumWidth(nl->minimumWidth());
4679  }
4680  }
4681 
4682  inputPortSidebar->setVisible(visible);
4683  outputPortSidebar->setVisible(visible);
4684 
4685  updatePublishedCableGeometry();
4686  inputPortSidebar->updatePortList();
4687  outputPortSidebar->updatePortList();
4688 
4689  if (!nl->isHidden() && !nl->isFloating())
4690  nl->fixWidth(visible);
4691 
4692  // Ensure port bounding boxes are large enough to include the antennae for hidden cables.
4693  composition->updateGeometryForAllComponents();
4694 
4695  updateUI();
4696 }
4697 
4702 void VuoEditorWindow::updatePublishedCableGeometry()
4703 {
4704  QGraphicsItem::CacheMode defaultCacheMode = composition->getCurrentDefaultCacheMode();
4705 
4706  set<VuoCable *> cables = composition->getBase()->getCables();
4707  foreach (VuoCable *cable, cables)
4708  {
4709  if (cable->isPublished())
4710  {
4711  cable->getRenderer()->setCacheMode(QGraphicsItem::NoCache);
4712  cable->getRenderer()->updateGeometry();
4713  cable->getRenderer()->setCacheMode(defaultCacheMode);
4714  }
4715  }
4716 
4717  // Updating the entire viewport here prevents published cable artifacts from
4718  // remaining behind, e.g., after scrolling, but (@todo:) handle this with
4719  // more precision.
4720  if (!inputPortSidebar->isHidden() || !outputPortSidebar->isHidden())
4721  ui->graphicsView->viewport()->update();
4722 
4723  composition->repaintFeedbackErrorMarks();
4724 }
4725 
4729 void VuoEditorWindow::updatePublishedPortOrder(vector<VuoPublishedPort *> ports, bool isInput)
4730 {
4731  // If the requested ordering is identical to the current ordering, do nothing.
4732  if (isInput && (ports == composition->getBase()->getPublishedInputPorts()))
4733  return;
4734 
4735  if (!isInput && (ports == composition->getBase()->getPublishedOutputPorts()))
4736  return;
4737 
4738  // If the requested change would place non-protocol ports above protocol ports, ignore the request.
4739  bool foundNonProtocolPort = false;
4740  foreach (VuoPublishedPort *port, ports)
4741  {
4742  if (!port->isProtocolPort())
4743  foundNonProtocolPort = true;
4744  else
4745  {
4746  if (foundNonProtocolPort)
4747  return;
4748  }
4749  }
4750 
4751  undoStack->push(new VuoCommandReorderPublishedPorts(ports, isInput, this));
4752 }
4753 
4758 void VuoEditorWindow::displayDockedNodeLibrary()
4759 {
4760  addDockWidget(Qt::LeftDockWidgetArea, nl);
4761 
4762  bool publishedPortsDisplayed = (inputPortSidebar && (! inputPortSidebar->isHidden()));
4763  if (publishedPortsDisplayed)
4764  splitDockWidget(nl, inputPortSidebar, Qt::Horizontal);
4765 
4766  nl->setFloating(false);
4767  nl->fixWidth(publishedPortsDisplayed);
4768  nl->prepareAndMakeVisible();
4769  nl->setFocus();
4770 }
4771 
4776 {
4777  // Prevent the input port sidebar from expanding to fill all available space when there are changes
4778  // to the node library's docking status and/or visibility.
4779  inputPortSidebar->setMinimumWidth(inputPortSidebar->width());
4780  inputPortSidebar->setMaximumWidth(inputPortSidebar->minimumWidth());
4781 
4782  if (visibility == VuoNodeLibrary::nodeLibraryDocked)
4783  {
4784  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4785  displayDockedNodeLibrary();
4787  }
4788 
4789  else if (visibility == VuoNodeLibrary::nodeLibraryFloating)
4790  {
4791  nl->fixWidth(false);
4792 
4793  // If our own node library was the one that initiated global
4794  // floating-node-library mode by being undocked, let it float.
4795  // It is now the single application-wide floating library.
4796  if (nl == floater)
4797  {
4798  if (ownedNodeLibrary == floater)
4799  {
4800  if (! nl->isFloating())
4801  {
4802  nl->setFloating(true);
4803  }
4804 
4805  nl->prepareAndMakeVisible();
4806  nl->setFocus();
4808  }
4809  }
4810 
4811  // Otherwise, hide it and adopt the global floater as our own.
4812  else
4813  {
4814  ownedNodeLibrary->releaseDocumentationWidget();
4815  ownedNodeLibrary->setVisible(false);
4816  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4817  assignSurrogateNodeLibrary(floater);
4818  }
4819  }
4820 
4821  else if (visibility == VuoNodeLibrary::nodeLibraryHidden)
4822  {
4823  releaseSurrogateNodeLibrary(previousFloaterDestroyed);
4824  nl->setVisible(false);
4825  }
4826 
4827  updateUI();
4828 }
4829 
4835 {
4836  // Only display the composition information in the metadata pane if the relevant composition metadata has been changed from the default,
4837  // or if the composition has been saved.
4838  bool compositionHasCustomizedDescription = (!composition->getBase()->getMetadata()->getDescription().empty() && (composition->getBase()->getMetadata()->getDescription() != VuoRendererComposition::deprecatedDefaultDescription));
4839  bool compositionHasCustomizedCopyright = (!composition->getBase()->getMetadata()->getCopyright().empty() && (composition->getBase()->getMetadata()->getCopyright() != generateCurrentDefaultCopyright()));
4840  bool compositionHasOtherCustomizedMetadata = (
4841  !composition->getBase()->getMetadata()->getCustomizedName().empty() ||
4842  !composition->getBase()->getMetadata()->getHomepageURL().empty() ||
4843  !composition->getBase()->getMetadata()->getDocumentationURL().empty());
4844  bool compositionHasBeenSaved = !windowFilePath().isEmpty();
4845  if (compositionHasCustomizedDescription || compositionHasCustomizedCopyright || compositionHasOtherCustomizedMetadata || compositionHasBeenSaved)
4846  {
4847  dispatch_async(((VuoEditor *)qApp)->getDocumentationQueue(), ^{
4848  QMetaObject::invokeMethod(nl, "displayPopoverInPane", Qt::QueuedConnection, Q_ARG(QWidget *, metadataPanel));
4849  });
4850  }
4851  else
4853 }
4854 
4858 void VuoEditorWindow::closeEvent(QCloseEvent *event)
4859 {
4860  // If already in the process of closing, don't redo work.
4861  if (closing)
4862  {
4863  event->accept();
4864  return;
4865  }
4866 
4867  // 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.
4868  // https://b33p.net/kosada/node/15831
4869  if (inputEditorSession
4870  || composition->getMenuSelectionInProgress()
4871  || inputPortSidebar->getMenuSelectionInProgress()
4872  || outputPortSidebar->getMenuSelectionInProgress())
4873  {
4874  event->ignore();
4875  return;
4876  }
4877 
4878  if (isWindowModified())
4879  {
4880  auto mb = new QMessageBox(this);
4881  mb->setWindowFlags(Qt::Sheet);
4882  mb->setWindowModality(Qt::WindowModal);
4883 
4884  //: Appears in the File > Close dialog.
4885  QString message = tr("Do you want to save the changes made to the document “%1”?").arg(getWindowTitleWithoutPlaceholder());
4886  //: Appears in the File > Close dialog.
4887  QString details = tr("Your changes will be lost if you don’t save them.");
4888  mb->setText(tr(message.toUtf8().constData()));
4889  mb->setStyleSheet("#qt_msgbox_informativelabel { font-weight: normal; font-size: 11pt; }");
4890  mb->setInformativeText(tr(details.toUtf8().constData()));
4891  mb->setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
4892 
4893  mb->setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
4894  mb->setDefaultButton(QMessageBox::Save);
4895 
4896  // Give the "Discard" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
4897  static_cast<QPushButton *>(mb->button(QMessageBox::Discard))->setAutoDefault(false);
4898  mb->button(QMessageBox::Discard)->setFocus();
4899 
4900  if (windowFilePath().isEmpty())
4901  mb->setButtonText(QMessageBox::Save, tr("Save As…"));
4902 
4903  connect(mb, &QMessageBox::buttonClicked, this, [=](QAbstractButton *button){
4904  auto editor = static_cast<VuoEditor *>(qApp);
4905  auto role = mb->buttonRole(button);
4906  if (role == QMessageBox::AcceptRole)
4907  {
4908  on_saveComposition_triggered();
4909  if (isWindowModified())
4910  editor->cancelQuit();
4911  else
4912  {
4913  acceptCloseEvent();
4914  editor->continueQuit(this);
4915  }
4916  }
4917  else if (role == QMessageBox::DestructiveRole)
4918  {
4919  acceptCloseEvent();
4920  editor->continueQuit(this);
4921  }
4922  else // if (role == QMessageBox::RejectRole)
4923  editor->cancelQuit();
4924  });
4925 
4926  mb->open();
4927 
4928  event->ignore();
4929  }
4930  else // if (! isWindowModified())
4931  {
4932  acceptCloseEvent();
4933  event->accept();
4934  static_cast<VuoEditor *>(qApp)->continueQuit(this);
4935  }
4936 }
4937 
4941 void VuoEditorWindow::acceptCloseEvent()
4942 {
4943  if (composition->isRunning())
4944  {
4945  connect(composition, &VuoEditorComposition::stopFinished, this, &VuoEditorWindow::deleteLater);
4947  }
4948  else
4949  VuoEditorWindow::deleteLater();
4950 
4951  composition->disablePopovers();
4952  searchBox->close();
4953 
4954  closing = true;
4955 
4956  if (VuoFileUtilities::fileExists(windowFilePath().toStdString()))
4957  static_cast<VuoEditor *>(qApp)->addFileToRecentlyClosedList(windowFilePath());
4958 }
4959 
4964 void VuoEditorWindow::updateSelectedComponentMenuItems()
4965 {
4966  string cutCommandText = tr("Cut").toStdString();
4967  string copyCommandText = tr("Copy").toStdString();
4968  string duplicateCommandText = tr("Duplicate").toStdString();
4969  string deleteCommandText = tr("Delete").toStdString();
4970  string resetCommandText = tr("Reset").toStdString();
4971 
4972  QList<QGraphicsItem *> selectedCompositionComponents = composition->selectedItems();
4973 
4974  int selectedNodesFound = 0;
4975  int selectedCablesFound = 0;
4976  int selectedListsFound = 0;
4977  int selectedCommentsFound = 0;
4978 
4979  VuoRendererNode *foundNode = NULL;
4980 
4981  for (QList<QGraphicsItem *>::iterator i = selectedCompositionComponents.begin();
4982  (! (selectedNodesFound && selectedCablesFound)) &&
4983  (i != selectedCompositionComponents.end());
4984  ++i)
4985  {
4986  QGraphicsItem *compositionComponent = *i;
4987 
4988  VuoRendererInputDrawer *rl = dynamic_cast<VuoRendererInputDrawer *>(compositionComponent);
4989  VuoRendererNode *rn = dynamic_cast<VuoRendererNode *>(compositionComponent);
4990  VuoRendererCable *rc = dynamic_cast<VuoRendererCable *>(compositionComponent);
4991  VuoRendererComment *rcomment = dynamic_cast<VuoRendererComment *>(compositionComponent);
4992 
4993  if (rl && !rl->paintingDisabled())
4994  selectedListsFound++;
4995 
4996  else if (rn && !rn->paintingDisabled())
4997  {
4998  foundNode = rn;
4999  selectedNodesFound++;
5000  }
5001 
5002  else if (rc && !rc->paintingDisabled())
5003  selectedCablesFound++;
5004 
5005  else if (rcomment)
5006  selectedCommentsFound++;
5007  }
5008 
5009  int distinctComponentTypesFound = 0;
5010  if (selectedListsFound)
5011  distinctComponentTypesFound++;
5012  if (selectedNodesFound)
5013  distinctComponentTypesFound++;
5014  if (selectedCablesFound)
5015  distinctComponentTypesFound++;
5016  if (selectedCommentsFound)
5017  distinctComponentTypesFound++;
5018 
5019  if (selectedListsFound && (distinctComponentTypesFound == 1))
5020  {
5021  string selectedListText = "Selected List";
5022  string selectedListsText = "Selected Lists";
5023 
5024  ui->cutCompositionComponents->setText(tr(cutCommandText.c_str()));
5025  ui->copyCompositionComponents->setText(tr(copyCommandText.c_str()));
5026  ui->duplicateCompositionComponents->setText(tr(duplicateCommandText.c_str()));
5027 
5028  if (selectedListsFound > 1)
5029  {
5030  ui->deleteCompositionComponents->setText(tr((resetCommandText + " " + selectedListsText).c_str()));
5031  composition->getContextMenuDeleteSelectedAction()->setText(tr((resetCommandText + " " + selectedListsText).c_str()));
5032  }
5033  else
5034  {
5035  ui->deleteCompositionComponents->setText(tr((resetCommandText + " " + selectedListText).c_str()));
5036  composition->getContextMenuDeleteSelectedAction()->setText(tr((resetCommandText + " " + selectedListText).c_str()));
5037  }
5038  }
5039  else
5040  {
5041  ui->cutCompositionComponents->setText(tr(cutCommandText.c_str()));
5042  ui->copyCompositionComponents->setText(tr(copyCommandText.c_str()));
5043  ui->duplicateCompositionComponents->setText(tr(duplicateCommandText.c_str()));
5044  ui->deleteCompositionComponents->setText(tr(deleteCommandText.c_str()));
5045  composition->getContextMenuDeleteSelectedAction()->setText(tr(deleteCommandText.c_str()));
5046  }
5047 
5048  bool enableSelectedComponentDeleteMenuItem = (!selectedCompositionComponents.isEmpty());
5049  bool enableSelectedComponentCutMenuItem = (!selectedCompositionComponents.isEmpty() &&
5050  !(selectedListsFound && (distinctComponentTypesFound == 1)));
5051  bool enableSelectedComponentDuplicateMenuItem = ((selectedNodesFound || selectedCommentsFound) &&
5052  !(selectedListsFound && (distinctComponentTypesFound == 1)));
5053 
5054  bool copyAppliesToNodeLibraryDocumentation = (nl && !nl->getSelectedDocumentationText().isEmpty());
5055  bool enableCopyMenuItem = (copyAppliesToNodeLibraryDocumentation || enableSelectedComponentDuplicateMenuItem);
5056 
5057  ui->cutCompositionComponents->setEnabled(enableSelectedComponentCutMenuItem);
5058  ui->copyCompositionComponents->setEnabled(enableCopyMenuItem);
5059  ui->duplicateCompositionComponents->setEnabled(enableSelectedComponentDuplicateMenuItem);
5060  ui->deleteCompositionComponents->setEnabled(enableSelectedComponentDeleteMenuItem);
5061 
5062  ui->renameNodes->setEnabled(selectedNodesFound);
5063  ui->refactor->setEnabled(selectedNodesFound || selectedCommentsFound);
5064  contextMenuTints->setEnabled(selectedNodesFound || selectedCommentsFound);
5065 
5066  // Update "Change (Node) To" submenu
5067  ui->menuEdit->removeAction(menuChangeNode->menuAction());
5068 
5069  bool menuChangeNodeDisplayed = false;
5070  if ((selectedNodesFound == 1) && !selectedCommentsFound)
5071  {
5072  composition->populateChangeNodeMenu(menuChangeNode, foundNode);
5073  if (menuChangeNode->actions().size() > 0)
5074  {
5075  ui->menuEdit->insertMenu(ui->refactor, menuChangeNode);
5076  menuChangeNode->setEnabled(true);
5077  menuChangeNodeDisplayed = true;
5078  }
5079  }
5080  // Rather than including an empty submenu to indicate that this option is disabled,
5081  // display some disabled (gray) placeholder text in the parent menu.
5082  ui->changeNodePlaceholder->setVisible(!menuChangeNodeDisplayed);
5083 }
5084 
5089 bool VuoEditorWindow::containsLikelyVuoComposition(QString text)
5090 {
5091  const QString compositionIndicatorText = "digraph";
5092  return (text.toCaseFolded().contains(compositionIndicatorText.toCaseFolded()));
5093 }
5094 
5099 void VuoEditorWindow::updateSceneRect()
5100 {
5101  // Disable sceneRect updates while menu option selection is in progress so that
5102  // the scene doesn't shift underneath the menu (e.g., after a
5103  // cable drag autoscrolled the canvas).
5104  if (composition->getMenuSelectionInProgress())
5105  return;
5106 
5107  // Enforce a margin around each edge of the composition when possible.
5108  const int horizontalMargin = 0.2 * ui->graphicsView->geometry().width();
5109  const int verticalMargin = 0.2 * ui->graphicsView->geometry().height();
5110 
5111  int horizontalScrollBarHeight = ui->graphicsView->horizontalScrollBar()->sizeHint().height();
5112  int verticalScrollBarWidth = ui->graphicsView->verticalScrollBar()->sizeHint().width();
5113 
5114  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
5115  QRectF itemsBoundingRect = composition->internalItemsBoundingRect().adjusted(-horizontalMargin, -verticalMargin, horizontalMargin, verticalMargin);
5116  QRectF viewportItemsUnionRect;
5117 
5118  // If the itemsBoundingRect is fully contained within the viewport (horizontally),
5119  // do not alter the sceneRect in this dimension.
5120  if (viewportRect.left() <= itemsBoundingRect.left() && viewportRect.right() >= itemsBoundingRect.right())
5121  {
5122  viewportItemsUnionRect.setLeft(viewportRect.left());
5123  viewportItemsUnionRect.setRight(viewportRect.right());
5124  }
5125 
5126  // Otherwise, if the discrepancy is greater at the left, align the sceneRect with the right edge
5127  // of the itemsBoundingRect to maximize stability.
5128  else if (abs(viewportRect.left() - itemsBoundingRect.left()) > abs(itemsBoundingRect.right() - viewportRect.right()))
5129  {
5130  viewportItemsUnionRect.setRight(itemsBoundingRect.right());
5131  viewportItemsUnionRect.setLeft(itemsBoundingRect.right() - max(itemsBoundingRect.width(), viewportRect.width()));
5132  }
5133 
5134  // If the discrepancy is greater at the right, align the sceneRect with the left edge
5135  // of the itemsBoundingRect to maximize stability.
5136  else
5137  {
5138  viewportItemsUnionRect.setLeft(itemsBoundingRect.left());
5139  viewportItemsUnionRect.setRight(itemsBoundingRect.left() + max(itemsBoundingRect.width(), viewportRect.width()));
5140  }
5141 
5142  // If the itemsBoundingRect is fully contained within the viewport (vertically),
5143  // do not alter the sceneRect in this dimension.
5144  if (viewportRect.top() <= itemsBoundingRect.top() && viewportRect.bottom() >= itemsBoundingRect.bottom())
5145  {
5146  viewportItemsUnionRect.setTop(viewportRect.top());
5147  viewportItemsUnionRect.setBottom(viewportRect.bottom());
5148  }
5149 
5150  // Otherwise, if the discrepancy is greater at the top, align the sceneRect with the bottom edge
5151  // of the itemsBoundingRect to maximize stability.
5152  else if (abs(viewportRect.top() - itemsBoundingRect.top()) > abs(itemsBoundingRect.bottom() - viewportRect.bottom()))
5153  {
5154  viewportItemsUnionRect.setBottom(itemsBoundingRect.bottom());
5155  viewportItemsUnionRect.setTop(itemsBoundingRect.bottom() - max(itemsBoundingRect.height(), viewportRect.height()));
5156  }
5157 
5158  // If the discrepancy is greater at the bottom, align the sceneRect with the top edge
5159  // of the itemsBoundingRect to maximize stability.
5160  else
5161  {
5162  viewportItemsUnionRect.setTop(itemsBoundingRect.top());
5163  viewportItemsUnionRect.setBottom(itemsBoundingRect.top() + max(itemsBoundingRect.height(), viewportRect.height()));
5164  }
5165 
5166  int viewportItemsUnionRectAdjustedWidth = viewportItemsUnionRect.width();
5167  int viewportItemsUnionRectAdjustedHeight = viewportItemsUnionRect.height();
5168 
5169  if ((viewportItemsUnionRect.width() > viewportRect.width()) &&
5170  (viewportItemsUnionRect.width() <= viewportRect.width() + verticalScrollBarWidth))
5171  {
5172  // Prevent the horizontal scrollbar from toggling into and out of existence as the scene is updated.
5173  viewportItemsUnionRectAdjustedWidth = viewportRect.width() + verticalScrollBarWidth + 1;
5174 
5175  // Prevent the introduction of the horizontal scrollbar from triggering the vertical scrollbar.
5176  viewportItemsUnionRectAdjustedHeight -= horizontalScrollBarHeight;
5177  }
5178 
5179  if ((viewportItemsUnionRect.height() > viewportRect.height()) &&
5180  (viewportItemsUnionRect.height() <= viewportRect.height() + horizontalScrollBarHeight))
5181  {
5182  // Prevent the vertical scrollbar from toggling into and out of existence as the scene is updated.
5183  viewportItemsUnionRectAdjustedHeight = viewportRect.height() + horizontalScrollBarHeight + 1;
5184 
5185  // Prevent the introduction of the vertical scrollbar from triggering the horizontal scrollbar.
5186  viewportItemsUnionRectAdjustedWidth -= verticalScrollBarWidth;
5187  }
5188 
5189  QRectF viewportItemsUnionRectAdjusted(viewportItemsUnionRect.topLeft().x(),
5190  viewportItemsUnionRect.topLeft().y(),
5191  viewportItemsUnionRectAdjustedWidth,
5192  viewportItemsUnionRectAdjustedHeight);
5193 
5194  // Do not allow the scene to shrink while the left mouse button is held down
5195  // (e.g., while a node is being dragged).
5196  if ((QApplication::mouseButtons() & Qt::LeftButton))
5197  viewportItemsUnionRectAdjusted = viewportItemsUnionRectAdjusted.united(composition->sceneRect());
5198  else
5199  viewportItemsUnionRectAdjusted = viewportItemsUnionRectAdjusted.united(viewportRect);
5200 
5201  VuoCable *cableBeingDragged = composition->getCableInProgress();
5202 
5203  // Cable drags should not cause the scene to grow.
5204  if (!cableBeingDragged)
5205  composition->setSceneRect(viewportItemsUnionRectAdjusted);
5206 
5207  // Cable drags should, however, auto-scroll the viewport under certain circumstances.
5208  if (cableBeingDragged)
5209  {
5210  QPointF cableFloatingEndpointSceneLoc = cableBeingDragged->getRenderer()->getFloatingEndpointLoc();
5211  VuoRendererPort *cableFixedPort = (cableBeingDragged->getFromPort()?
5212  cableBeingDragged->getFromPort()->getRenderer() :
5213  cableBeingDragged->getToPort()?
5214  cableBeingDragged->getToPort()->getRenderer() :
5215  NULL);
5216 
5217  // Autoscroll should normally be disabled if the cursor is positioned above the
5218  // canvas or one of the published port sidebars.
5219  QRect inputPortSidebarRect = inputPortSidebar->geometry();
5220  inputPortSidebarRect.moveTopLeft(inputPortSidebar->parentWidget()->mapToGlobal(inputPortSidebarRect.topLeft()+QPoint(1,0)));
5221 
5222  QRect outputPortSidebarRect = outputPortSidebar->geometry();
5223  outputPortSidebarRect.moveTopLeft(outputPortSidebar->parentWidget()->mapToGlobal(outputPortSidebarRect.topLeft()));
5224 
5225  QRect viewportRectGlobal = ui->graphicsView->viewport()->geometry();
5226  viewportRectGlobal.moveTopLeft(ui->graphicsView->viewport()->parentWidget()->mapToGlobal(viewportRectGlobal.topLeft()));
5227 
5228  QRect noAutoscrollZone = QRect();
5229  if (!inputPortSidebar->isHidden())
5230  noAutoscrollZone |= inputPortSidebarRect;
5231  if (!outputPortSidebar->isHidden())
5232  noAutoscrollZone |= outputPortSidebarRect;
5233  if (!inputPortSidebar->isHidden() || !outputPortSidebar->isHidden())
5234  noAutoscrollZone |= viewportRectGlobal;
5235 
5236  QPoint cursorPosition = QCursor::pos();
5237  bool cursorWithinNoAutoscrollZone = noAutoscrollZone.contains(cursorPosition);
5238  bool cursorWithinNoAutoscrollZoneLeft = (cursorWithinNoAutoscrollZone &&
5239  ((cursorPosition.x() - noAutoscrollZone.left()) < (noAutoscrollZone.right() - cursorPosition.x())));
5240 
5241  // Make an exception and allow the autoscroll if it would bring the cable's
5242  // connected node closer to being back within the viewport.
5243  bool autoscrollWouldBringSourceNodeCloser = false;
5244  if (cableFixedPort && !dynamic_cast<VuoRendererPublishedPort *>(cableFixedPort))
5245  {
5246  VuoRendererNode *fixedNode = cableFixedPort->getUnderlyingParentNode();
5247  QRectF fixedNodeBoundingRect = fixedNode->boundingRect().adjusted(-horizontalMargin, -verticalMargin, horizontalMargin, verticalMargin);
5248  bool fixedNodeVisible = ((fixedNodeBoundingRect.left()+fixedNode->scenePos().x() >= viewportRect.left()) &&
5249  (fixedNodeBoundingRect.right()+fixedNode->scenePos().x() <= viewportRect.right()));
5250 
5251  autoscrollWouldBringSourceNodeCloser = (!fixedNodeVisible && (cursorWithinNoAutoscrollZoneLeft ==
5252  (fixedNodeBoundingRect.left()+fixedNode->scenePos().x() < viewportRect.left())));
5253  }
5254 
5255  bool enableCableDragAutoscroll = (!cursorWithinNoAutoscrollZone || autoscrollWouldBringSourceNodeCloser);
5256  if (enableCableDragAutoscroll)
5257  {
5258  ui->graphicsView->ensureVisible(cableFloatingEndpointSceneLoc.x(),
5259  cableFloatingEndpointSceneLoc.y(),
5260  0,
5261  VuoEditorWindow::compositionMargin);
5262  }
5263  }
5264 }
5265 
5270 void VuoEditorWindow::updateRubberBandSelectionMode(QRect rubberBandRect, QPointF fromScenePoint, QPointF toScenePoint)
5271 {
5272  bool rubberBandSelectionPreviouslyInProgress = this->rubberBandSelectionInProgress;
5273  bool rubberBandSelectionNowInProgress = !(rubberBandRect.isNull() && fromScenePoint.isNull() && toScenePoint.isNull());
5274 
5275  bool beginningRubberBandSelection = (!rubberBandSelectionPreviouslyInProgress && rubberBandSelectionNowInProgress);
5276  bool endingRubberBandSelection = (rubberBandSelectionPreviouslyInProgress && !rubberBandSelectionNowInProgress);
5277 
5278  if (beginningRubberBandSelection)
5279  {
5280  // Disable selection of comment bodies (non-title-handle areas) during rubberband drags.
5281  foreach (VuoComment *comment, composition->getBase()->getComments())
5282  if (comment->hasRenderer())
5283  comment->getRenderer()->setBodySelectable(false);
5284 
5285  // Disable selection of cables during rubberband drags, unless the relevant keyboard modifier was pressed.
5286  if (!lastLeftMousePressHadOptionModifier)
5287  {
5288  foreach (VuoCable *cable, composition->getBase()->getCables())
5289  if (cable->hasRenderer())
5290  cable->getRenderer()->setSelectable(false);
5291  }
5292  }
5293  else if (endingRubberBandSelection)
5294  {
5295  foreach (VuoComment *comment, composition->getBase()->getComments())
5296  if (comment->hasRenderer())
5297  comment->getRenderer()->setBodySelectable(true);
5298 
5299  if (!lastLeftMousePressHadOptionModifier)
5300  {
5301  foreach (VuoCable *cable, composition->getBase()->getCables())
5302  if (cable->hasRenderer())
5303  cable->getRenderer()->setSelectable(true);
5304  }
5305  }
5306 
5307  this->rubberBandSelectionInProgress = rubberBandSelectionNowInProgress;
5308 }
5309 
5313 void VuoEditorWindow::setPortConstant(VuoRendererPort *port, string value)
5314 {
5315  if (value != port->getConstantAsString())
5316  {
5317  VuoCompilerInputEventPort *eventPort = dynamic_cast<VuoCompilerInputEventPort *>(port->getBase()->getCompiler());
5318  if (eventPort)
5319  undoStack->push(new VuoCommandSetPortConstant(eventPort, value, this));
5320  }
5321 }
5322 
5326 void VuoEditorWindow::showInputEditor(VuoRendererPort *port)
5327 {
5328  showInputEditor(port, true);
5329 }
5330 
5334 void VuoEditorWindow::showInputEditor(VuoRendererPort *port, bool forwardTabTraversal)
5335 {
5336  VuoPublishedPortSidebar *sidebar = nullptr;
5337  VuoRendererPublishedPort *publishedPort = dynamic_cast<VuoRendererPublishedPort *>(port);
5338  if (publishedPort)
5339  sidebar = (publishedPort->getInput() ? outputPortSidebar : inputPortSidebar);
5340 
5341  inputEditorSession = new VuoInputEditorSession(inputEditorManager, composition, sidebar, this);
5342  map<VuoRendererPort *, pair<string, string> > originalAndFinalValueForPort = inputEditorSession->execute(port, forwardTabTraversal);
5343 
5344  delete inputEditorSession;
5345  inputEditorSession = nullptr;
5346 
5347  bool startedUndoStackMacro = false;
5348  for (auto i : originalAndFinalValueForPort)
5349  {
5350  VuoRendererPort *port = i.first;
5351  string originalEditingSessionValue = i.second.first;
5352  string finalEditingSessionValue = i.second.second;
5353 
5354  // Now that the editing session is over, check whether the port constant ended up with
5355  // a different value than it had initially. If so, push the change onto the Undo stack.
5356  if (finalEditingSessionValue != originalEditingSessionValue)
5357  {
5358  VuoCompilerPort *compilerPort = static_cast<VuoCompilerPort *>(port->getBase()->getCompiler());
5359 
5360  // Briefly set the constant back to its original value so that the Undo stack
5361  // has the information it needs to revert it properly. An alternative would be to
5362  // modify the VuoCommandSetPortConstant constructor to accept a revertedValue argument.
5363  composition->updatePortConstant(compilerPort, originalEditingSessionValue, false);
5364 
5365  if (!startedUndoStackMacro)
5366  {
5367  undoStack->beginMacro(tr("Set Port Constant"));
5368  startedUndoStackMacro = true;
5369  }
5370 
5371  undoStack->push(new VuoCommandSetPortConstant(compilerPort, finalEditingSessionValue, this));
5372 
5373  if (composition->requiresStructuralChangesAfterValueChangeAtPort(port))
5374  {
5375  VuoRendererNode *parentNode = port->getRenderedParentNode();
5376 
5377  // Only current possibility: modifications to "Calculate" node's 'expression' input
5378  string nodeClassName = parentNode->getBase()->getNodeClass()->getClassName();
5379  vector<string> inputVariablesBeforeEditing = composition->extractInputVariableListFromExpressionsConstant(originalEditingSessionValue, nodeClassName);
5380  vector<string> inputVariablesAfterEditing = composition->extractInputVariableListFromExpressionsConstant(finalEditingSessionValue, nodeClassName);
5381 
5382  // Don't make any structural changes if the variables in the input expression remain
5383  // the same, even if the expression itself has changed.
5384  if (inputVariablesBeforeEditing != inputVariablesAfterEditing)
5385  {
5386  VuoPort *valuesPort = port->getRenderedParentNode()->getBase()->getInputPortWithName("values");
5387 
5388  set<VuoRendererInputAttachment *> attachments = valuesPort->getRenderer()->getAllUnderlyingUpstreamInputAttachments();
5389 
5390  QList<QGraphicsItem *> attachmentsToRemove;
5391 
5392  VuoRendererReadOnlyDictionary *oldDictionary = nullptr;
5393  VuoRendererValueListForReadOnlyDictionary *oldValueList = nullptr;
5394  VuoRendererKeyListForReadOnlyDictionary *oldKeyList = nullptr;
5395  foreach (VuoRendererInputAttachment *attachment, attachments)
5396  {
5397  attachmentsToRemove.append(attachment);
5398 
5399  if (dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment))
5400  oldDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(attachment);
5401 
5402  else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment))
5403  oldValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(attachment);
5404 
5405  else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment))
5406  oldKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(attachment);
5407  }
5408 
5409  if (oldValueList && oldDictionary && oldKeyList)
5410  {
5411  set<VuoRendererNode *> nodesToAdd;
5412  set<VuoRendererCable *> cablesToAdd;
5413  composition->createAndConnectDictionaryAttachmentsForNode(parentNode->getBase(), nodesToAdd, cablesToAdd);
5414 
5415  VuoRendererReadOnlyDictionary *newDictionary = nullptr;
5416  VuoRendererValueListForReadOnlyDictionary *newValueList = nullptr;
5417  VuoRendererKeyListForReadOnlyDictionary *newKeyList = nullptr;
5418  foreach (VuoRendererNode *node, nodesToAdd)
5419  {
5420  if (dynamic_cast<VuoRendererReadOnlyDictionary *>(node))
5421  newDictionary = dynamic_cast<VuoRendererReadOnlyDictionary *>(node);
5422 
5423  else if (dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node))
5424  newValueList = dynamic_cast<VuoRendererValueListForReadOnlyDictionary *>(node);
5425 
5426  else if (dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node))
5427  newKeyList = dynamic_cast<VuoRendererKeyListForReadOnlyDictionary *>(node);
5428  }
5429 
5430  undoStack->push(new VuoCommandReplaceNode(oldValueList, newValueList, this, "Set Port Constant", false, false));
5431  undoStack->push(new VuoCommandReplaceNode(oldKeyList, newKeyList, this, "Set Port Constant", false, true));
5432  undoStack->push(new VuoCommandReplaceNode(oldDictionary, newDictionary, this, "Set Port Constant", false, true));
5433 
5434  foreach (VuoRendererCable *cable, cablesToAdd)
5435  {
5436  cable->setFrom(nullptr, nullptr);
5437  cable->setTo(nullptr, nullptr);
5438  }
5439  }
5440  }
5441  }
5442  }
5443 
5444  // If the port constant ended up with the same value as it had initially
5445  // (either because the user dismissed the editor by pressing 'Esc' or
5446  // re-committed the initial value), do not push the change onto the Undo
5447  // stack, but do revert to the original value if there were intermediate
5448  // changes while the input editor was open.
5449  else if (port->getConstantAsString() != originalEditingSessionValue)
5450  {
5451  VuoCompilerPort *compilerPort = static_cast<VuoCompilerPort *>(port->getBase()->getCompiler());
5452  composition->updatePortConstant(compilerPort, originalEditingSessionValue);
5453  }
5454  }
5455 
5456  if (startedUndoStackMacro)
5457  undoStack->endMacro();
5458 }
5459 
5463 void VuoEditorWindow::showNodeTitleEditor(VuoRendererNode *node)
5464 {
5465  if (! node->getBase()->hasCompiler())
5466  return;
5467 
5468  composition->setPopoverEventsEnabled(false);
5469 
5470  VuoTitleEditor *titleEditor = new VuoTitleEditor();
5471  string originalValue = node->getBase()->getTitle();
5472 
5473  // Position the input editor overtop the node's title.
5474  QRectF nodeBoundingRect = node->boundingRect();
5475  QPointF nodeTitleRightCenterInScene = node->scenePos() + nodeBoundingRect.topRight() + QPointF(0, VuoRendererNode::nodeTitleHeight/2.);
5476  QPoint nodeTitleRightCenterInView = ui->graphicsView->mapFromScene(nodeTitleRightCenterInScene);
5477  QPoint nodeTitleRightCenterGlobal = ui->graphicsView->mapToGlobal(nodeTitleRightCenterInView);
5478 
5479  // Set the input editor's width to match the node's.
5480  int nodeWidth = node->boundingRect().width();
5481  int scaledWidth = ui->graphicsView->transform().m11() * nodeWidth;
5482  titleEditor->setWidth(scaledWidth);
5483 
5484  json_object *details = json_object_new_object();
5485  VuoEditor *editor = (VuoEditor *)qApp;
5486  json_object_object_add(details, "isDark", json_object_new_boolean(editor->isInterfaceDark()));
5487 
5488  json_object *originalValueAsJson = json_object_new_string(originalValue.c_str());
5489  json_object *newValueAsJson = titleEditor->show(nodeTitleRightCenterGlobal, originalValueAsJson, details);
5490  string newValue = json_object_get_string(newValueAsJson);
5491  json_object_put(originalValueAsJson);
5492  json_object_put(newValueAsJson);
5493 
5494  // If the user has deleted the title, revert to the node class default title.
5495  if (newValue == "")
5496  newValue = node->getBase()->getNodeClass()->getDefaultTitleWithoutSuffix();
5497 
5498  if (newValue != originalValue)
5499  undoStack->push(new VuoCommandSetNodeTitle(node->getBase()->getCompiler(), newValue, this));
5500 
5501  composition->setPopoverEventsEnabled(true);
5502 }
5503 
5507 void VuoEditorWindow::showCommentEditor(VuoRendererComment *comment)
5508 {
5509  string originalText = comment->getBase()->getContent();
5510 
5511  json_object *details = json_object_new_object();
5512  VuoEditor *editor = (VuoEditor *)qApp;
5513  json_object_object_add(details, "isDark", json_object_new_boolean(editor->isInterfaceDark()));
5514 
5515  json_object *originalTextAsJson = json_tokener_parse(originalText.c_str());
5516 
5517  VuoCommentEditor *commentEditor = new VuoCommentEditor();
5518 
5519  // Set the text editor's width to match the comment's.
5520  int commentWidth = comment->boundingRect().width();
5521  int scaledWidth = ui->graphicsView->transform().m11() * commentWidth;
5522  commentEditor->setWidth(scaledWidth);
5523 
5524  // Set the text editor's height to match the comment's.
5525  int commentHeight= comment->boundingRect().height();
5526  int scaledHeight = ui->graphicsView->transform().m11() * commentHeight;
5527  commentEditor->setHeight(scaledHeight);
5528 
5529  // Position the text editor overtop the comment.
5530  QRectF commentBoundingRect = comment->boundingRect();
5531  QPointF commentTextRightCenterInScene = comment->scenePos() + commentBoundingRect.topRight() + QPointF(0, commentBoundingRect.height()/2.0);
5532  QPoint commentTextRightCenterInView = ui->graphicsView->mapFromScene(commentTextRightCenterInScene);
5533  QPoint commentTextRightCenterGlobal = ui->graphicsView->mapToGlobal(commentTextRightCenterInView);
5534 
5535  json_object *newTextAsJson = commentEditor->show(commentTextRightCenterGlobal, originalTextAsJson, details);
5536 
5537  string newText = json_object_to_json_string_ext(newTextAsJson, JSON_C_TO_STRING_PLAIN);
5538  json_object_put(originalTextAsJson);
5539  json_object_put(newTextAsJson);
5540 
5541  if (newText != originalText)
5542  undoStack->push(new VuoCommandSetCommentText(comment, newText, this));
5543 }
5544 
5548 void VuoEditorWindow::zoomToFitComment(VuoRendererComment *comment)
5549 {
5550  comment->setSelected(true);
5552 }
5553 
5557 void VuoEditorWindow::changePublishedPortName(VuoRendererPublishedPort *port, string newName, bool useUndoStack)
5558 {
5559  if (useUndoStack)
5560  undoStack->push(new VuoCommandSetPublishedPortName(port, newName, this));
5561  else
5562  composition->setPublishedPortName(port, newName);
5563 }
5564 
5568 void VuoEditorWindow::changePublishedPortDetails(VuoRendererPublishedPort *port, json_object *newDetails)
5569 {
5570  undoStack->push(new VuoCommandSetPublishedPortDetails(dynamic_cast<VuoRendererPublishedPort *>(port), newDetails, this));
5571 }
5572 
5576 void VuoEditorWindow::openEditableSourceForNode(VuoRendererNode *node)
5577 {
5578  VuoNodeClass *nodeClass = node->getBase()->getNodeClass();
5579  if (nodeClass->hasCompiler() && dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler()))
5580  {
5581  string originalGenericNodeClassName = dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass->getCompiler())->getOriginalGenericNodeClassName();
5582  VuoCompilerNodeClass *originalGenericNodeClass = compiler->getNodeClass(originalGenericNodeClassName);
5583  if (originalGenericNodeClass)
5584  nodeClass = originalGenericNodeClass->getBase();
5585  }
5586 
5587  QString actionText, sourcePath;
5588  if (!VuoEditorUtilities::isNodeClassEditable(nodeClass, actionText, sourcePath))
5589  return;
5590 
5591  // If the node is an installed subcomposition or shader, open its source for editing within Vuo.
5592  if (nodeClass->getCompiler()->isSubcomposition() || nodeClass->getCompiler()->isIsf())
5593  {
5594  QMainWindow *window = static_cast<VuoEditor *>(qApp)->openFileWithName(sourcePath, false);
5595  if (nodeClass->getCompiler()->isSubcomposition())
5596  {
5597  VuoEditorWindow *compositionWindow = dynamic_cast<VuoEditorWindow *>(window);
5598  if (compositionWindow)
5599  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(compositionWindow->getComposition(), getComposition(), node);
5600  }
5601  }
5602  // If the node has other editable source code, open it in the appropriate application.
5603  else
5604  static_cast<VuoEditor *>(qApp)->openUrl("file://" + sourcePath);
5605 }
5606 
5612 {
5613  this->includeInRecentFileMenu = include;
5614 }
5615 
5620 void VuoEditorWindow::on_insertComment_triggered()
5621 {
5622  insertCommentAtPos(getFittedScenePos(getCursorScenePos()));
5623 }
5624 
5628 void VuoEditorWindow::insertCommentAtPos(QPointF targetScenePos)
5629 {
5630  // Create an empty comment.
5631  VuoRendererComment *comment = composition->createRendererComment(
5632  (new VuoCompilerComment(new VuoComment("",
5633  targetScenePos.x(),
5634  targetScenePos.y(),
5637  )))->getBase());
5638 
5639  // Add the comment to the composition.
5640  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
5641  componentsToAdd.append(comment);
5642  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "Insert Comment"));
5643 
5644  // Open the comment immediately for editing.
5645  showCommentEditor(comment);
5646 }
5647 
5651 void VuoEditorWindow::on_insertSubcomposition_triggered()
5652 {
5653  insertSubcompositionAtPos(getFittedScenePos(getCursorScenePos()-
5655 }
5656 
5664 bool VuoEditorWindow::ensureThisParentCompositionSaved()
5665 {
5666  if (!VuoFileUtilities::fileExists(windowFilePath().toUtf8().constData()))
5667  {
5668  QMessageBox messageBox(this);
5669  messageBox.setWindowFlags(Qt::Sheet);
5670  messageBox.setWindowModality(Qt::WindowModal);
5671  messageBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Save);
5672  messageBox.setDefaultButton(QMessageBox::Save);
5673  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
5674  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
5675  messageBox.setText("Save before packaging");
5676  messageBox.setInformativeText("Please save this composition so Vuo knows where to save your new subcomposition.");
5677  messageBox.setButtonText(QMessageBox::Save, tr("Save As…"));
5678 
5679  // Give the non-default button keyboard focus so that it can be activated by spacebar.
5680  static_cast<QPushButton *>(messageBox.button(QMessageBox::Cancel))->setAutoDefault(false);
5681  messageBox.button(QMessageBox::Cancel)->setFocus();
5682 
5683  // Give the user a chance to cancel subcomposition installation without saving.
5684  if (messageBox.exec() != QMessageBox::Save)
5685  return false;
5686 
5687  // Present the "Save As" dialogue for the user to save the parent composition.
5688  on_saveCompositionAs_triggered();
5689  }
5690 
5691  return VuoFileUtilities::fileExists(windowFilePath().toUtf8().constData());
5692 }
5693 
5697 void VuoEditorWindow::insertSubcompositionAtPos(QPointF targetScenePos)
5698 {
5699  VUserLog("%s: Insert subcomposition {", getWindowTitleWithoutPlaceholder().toUtf8().data());
5700 
5701  // If the parent composition has not been saved to the filesystem, prompt the user to do so now
5702  // so that the subcomposition can be installed in a composition-local Modules directory.
5703  if (!ensureThisParentCompositionSaved())
5704  {
5705  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5706  return;
5707  }
5708 
5709  string subcompositionContent = "digraph G {}";
5710  VuoEditorWindow *subcompositionWindow = static_cast<VuoEditor *>(qApp)->newCompositionWithContent(subcompositionContent);
5711  subcompositionWindow->setIncludeInRecentFileMenu(false);
5712  subcompositionWindow->showPublishedPortSidebars();
5713  subcompositionWindow->setAsActiveWindow();
5714 
5715  string nodeClassName = subcompositionWindow->installSubcomposition(windowFilePath().toUtf8().constData());
5716  if (!nodeClassName.empty())
5717  {
5718  VuoModuleManager::CallbackType subcompositionAdded = ^void (void) {
5719  composition->getModuleManager()->getNodeLibrary()->highlightNodeClass(nodeClassName);
5720 
5721  VuoRendererNode *subcompositionNode = composition->createNode(nodeClassName.c_str(), "",
5722  targetScenePos.x(),
5723  targetScenePos.y());
5724  if (!subcompositionNode)
5725  {
5726  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5727  return;
5728  }
5729 
5730  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(subcompositionWindow->getComposition(),
5731  getComposition(),
5732  subcompositionNode);
5733 
5734  // Add the new subcomposition node.
5735  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
5736  componentsToAdd.append(subcompositionNode);
5737  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "Insert Subcomposition"));
5738 
5739  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5740  };
5741 
5742  // The order of the following two lines matters -- the second callback registered
5743  // for a given module manager and node class replaces the first.
5744  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
5745  composition->getModuleManager()->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionAdded);
5746  }
5747  else
5748  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5749 }
5750 
5754 void VuoEditorWindow::refactorSelectedItems()
5755 {
5756  VUserLog("%s: Package as subcomposition {", getWindowTitleWithoutPlaceholder().toUtf8().data());
5757 
5758  // If the parent composition has not been saved to the filesystem, prompt the user to do so now
5759  // so that the subcomposition can be installed in a composition-local Modules directory.
5760  if (!ensureThisParentCompositionSaved())
5761  {
5762  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5763  return;
5764  }
5765 
5766  string subcompositionContent = getMaximumSubcompositionFromSelection(false, false);
5767 
5768  QPoint selectedItemsAvgPos = QPoint(0,0);
5769  int selectedItemCount = 0;
5770 
5771  set<string> portsToPublish;
5772  __block map<int, string> inputCablesToRestoreFromPort;
5773  __block map<int, string> inputCablesToRestoreToPort;
5774  __block map<int, bool> inputCablesToRestoreEventOnly;
5775  __block map<int, string> outputCablesToRestoreFromPort;
5776  __block map<int, string> outputCablesToRestoreToPort;
5777  __block map<int, bool> outputCablesToRestoreEventOnly;
5778 
5779  QList<QGraphicsItem *> selectedItems = composition->selectedItems();
5780  foreach (QGraphicsItem *item, selectedItems)
5781  {
5782  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
5783  if (node)
5784  {
5785  // Determine which input ports to publish.
5786  foreach (VuoPort *port, node->getBase()->getInputPorts())
5787  {
5788  if (!dynamic_cast<VuoRendererTypecastPort *>(port->getRenderer()))
5789  {
5790  vector<VuoCable *> incomingCables = port->getConnectedCables(true);
5791  foreach (VuoCable *cable, incomingCables)
5792  {
5793  bool fromNodeIsExternal = (cable->getFromNode()->hasRenderer() &&
5794  !cable->getFromNode()->getRenderer()->isSelected());
5795  if (fromNodeIsExternal || cable->isPublishedInputCable())
5796  {
5797  portsToPublish.insert(composition->getIdentifierForStaticPort(cable->getToPort()));
5798 
5799  int cableNum = inputCablesToRestoreEventOnly.size();
5800  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(cable->getFromPort());
5801  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(cable->getToPort());
5802  inputCablesToRestoreEventOnly[cableNum] = cable->getCompiler()->getAlwaysEventOnly();
5803  }
5804  }
5805  } // end if not an attached typecast
5806  else // if attached typecast
5807  {
5808  VuoRendererNode *typecastNode = dynamic_cast<VuoRendererTypecastPort *>(port->getRenderer())->getUncollapsedTypecastNode();
5809  VuoPort *typecastInPort = typecastNode->getBase()->getInputPorts()[VuoNodeClass::unreservedInputPortStartIndex];
5810  vector<VuoCable *> incomingCables = typecastInPort->getConnectedCables(true);
5811 
5812  // If the input port has an attached typecast, determine whether the typecast's incoming cables will be
5813  // internal or external to the refactored subcomposition, or some combination of internal and external.
5814  bool typecastHasInternalInput = false;
5815  bool typecastHasExternalInput = false;
5816  foreach (VuoCable *typecastInCable, incomingCables)
5817  {
5818  if ((typecastInCable->getFromNode() && typecastInCable->getFromNode()->hasRenderer() &&
5819  typecastInCable->getFromNode()->getRenderer()->isSelected()))
5820  typecastHasInternalInput = true;
5821  else
5822  typecastHasExternalInput = true;
5823  }
5824 
5825  // Case: All of the typecast’s incoming cables will be external.
5826  // The typecast’s host port needs to be published and reconnected to the soon-to-be-external typecast’s output port.
5827  // The soon-to-be-external typecast needs to be uncollapsed.
5828  if (!typecastHasInternalInput)
5829  {
5830  VuoPort *typecastOutPort = typecastNode->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
5831  VuoPort *typecastHostPort = port;
5832 
5833  portsToPublish.insert(composition->getIdentifierForStaticPort(typecastHostPort));
5834 
5835  int cableNum = inputCablesToRestoreEventOnly.size();
5836  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(typecastOutPort);
5837  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(typecastHostPort);
5838  inputCablesToRestoreEventOnly[cableNum] = false;
5839 
5840  composition->uncollapseTypecastNode(typecastNode);
5841  }
5842 
5843  // Case: Some of the typecast's incoming cables will be external, some internal.
5844  // The soon-to-be internal typecast's child port needs to be published and reconnected to its soon-to-be external input sources.
5845  else if (typecastHasExternalInput && typecastHasInternalInput)
5846  {
5847  portsToPublish.insert(composition->getIdentifierForStaticPort(typecastInPort));
5848 
5849  foreach (VuoCable *typecastInCable, incomingCables)
5850  {
5851  bool externalInput = !(typecastInCable->getFromNode() && typecastInCable->getFromNode()->hasRenderer() &&
5852  typecastInCable->getFromNode()->getRenderer()->isSelected());
5853  if (externalInput)
5854  {
5855  int cableNum = inputCablesToRestoreEventOnly.size();
5856  inputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(typecastInCable->getFromPort());
5857  inputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(typecastInPort);
5858  inputCablesToRestoreEventOnly[cableNum] = false;
5859  }
5860  }
5861  }
5862 
5863  // Case: All of the typecast's incoming cables will be internal.
5864  // The soon-to-be-internal typecast need not have either its child port or its host port published, nor any
5865  // external connections restored.
5866  //else if (!typecastHasExternalInput)
5867  //{}
5868  }
5869  }
5870 
5871  // Determine which output ports to publish.
5872  foreach (VuoPort *port, node->getBase()->getOutputPorts())
5873  {
5874  foreach (VuoCable *cable, port->getConnectedCables(true))
5875  {
5876  bool toNodeIsExternal = (cable->getToNode()->hasRenderer() &&
5877  !dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort()) &&
5878  !cable->getToNode()->getRenderer()->isSelected());
5879  bool connectedTypecastIsExternal = false;
5880  if (dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort()))
5881  {
5882  VuoRendererNode *typecastNode = dynamic_cast<VuoRendererTypecastPort *>(cable->getToPort()->getRenderer()->getTypecastParentPort())->getUncollapsedTypecastNode();
5883  VuoPort *typecastOutPort = typecastNode->getBase()->getOutputPorts()[VuoNodeClass::unreservedOutputPortStartIndex];
5884  vector<VuoCable *> outCables = typecastOutPort->getConnectedCables(true);
5885 
5886  connectedTypecastIsExternal = ((outCables.size() < 1) || !outCables[0]->getToNode() ||
5887  !outCables[0]->getToNode()->hasRenderer() ||
5888  !outCables[0]->getToNode()->getRenderer()->isSelected());
5889  if (connectedTypecastIsExternal)
5890  composition->uncollapseTypecastNode(typecastNode);
5891  }
5892 
5893  if (toNodeIsExternal || connectedTypecastIsExternal || cable->isPublishedOutputCable())
5894  {
5895  portsToPublish.insert(composition->getIdentifierForStaticPort(cable->getFromPort()));
5896 
5897  int cableNum = outputCablesToRestoreEventOnly.size();
5898  outputCablesToRestoreFromPort[cableNum] = composition->getIdentifierForStaticPort(cable->getFromPort());
5899  outputCablesToRestoreToPort[cableNum] = composition->getIdentifierForStaticPort(cable->getToPort());
5900  outputCablesToRestoreEventOnly[cableNum] = cable->getCompiler()->getAlwaysEventOnly();
5901  }
5902  }
5903  }
5904 
5905  selectedItemsAvgPos += item->scenePos().toPoint();
5906  selectedItemCount++;
5907  }
5908 
5909  else if (dynamic_cast<VuoRendererComment *>(item))
5910  {
5911  selectedItemsAvgPos += item->scenePos().toPoint();
5912  selectedItemCount++;
5913  }
5914  }
5915 
5916  selectedItemsAvgPos /= selectedItemCount;
5917 
5918  string parentCompositionPath = windowFilePath().toStdString();
5919  string subcompositionDir = VuoFileUtilities::getCompositionLocalModulesPath(parentCompositionPath);
5920 
5921  VuoEditorWindow *subcompositionWindow = static_cast<VuoEditor *>(qApp)->newCompositionWithContent(subcompositionContent, subcompositionDir);
5922  subcompositionWindow->setIncludeInRecentFileMenu(false);
5923  __block map<string, string> publishedPortNames = subcompositionWindow->getComposition()->publishPorts(portsToPublish);
5924  subcompositionWindow->showPublishedPortSidebars();
5925  subcompositionWindow->setAsActiveWindow();
5926 
5927  string nodeClassName = subcompositionWindow->installSubcomposition(parentCompositionPath);
5928  if (!nodeClassName.empty())
5929  {
5930  VuoModuleManager::CallbackType subcompositionAdded = ^void (void) {
5931  composition->getModuleManager()->getNodeLibrary()->highlightNodeClass(nodeClassName);
5932 
5933  VuoRendererNode *subcompositionNode = composition->createNode(nodeClassName.c_str(), "",
5934  selectedItemsAvgPos.x(),
5935  selectedItemsAvgPos.y());
5936  if (!subcompositionNode)
5937  {
5938  composition->collapseTypecastNodes();
5939  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
5940  return;
5941  }
5942 
5943  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->linkSubcompositionToNodeInSupercomposition(subcompositionWindow->getComposition(),
5944  getComposition(),
5945  subcompositionNode);
5946 
5947  undoStack->beginMacro(tr("Package as Subcomposition"));
5948 
5949  // Remove the selected comments, nodes, and their internal connections.
5950  QList<QGraphicsItem *> itemsToRemove;
5951  foreach (VuoRendererNode *node, composition->getSelectedNodes())
5952  itemsToRemove.append(node);
5953  foreach (VuoRendererComment *comment, composition->getSelectedComments())
5954  itemsToRemove.append(comment);
5955 
5956  undoStack->push(new VuoCommandRemove(itemsToRemove, this, inputEditorManager, "Package as Subcomposition", true));
5957 
5958  // Add the refactored subcomposition node.
5959  QList<QGraphicsItem *> componentsToAdd = QList<QGraphicsItem *>();
5960  componentsToAdd.append(subcompositionNode);
5961  undoStack->push(new VuoCommandAdd(componentsToAdd, this, "", true));
5962 
5963  // Restore external connections.
5964  QList<QGraphicsItem *> inputCablesToRestore;
5965  for (int i = 0; i < inputCablesToRestoreEventOnly.size(); ++i)
5966  {
5967  VuoPort *fromPort = composition->getPortWithStaticIdentifier(inputCablesToRestoreFromPort[i]);
5968  VuoNode *fromNode = (fromPort? composition->getUnderlyingParentNodeForPort(fromPort, composition) : NULL);
5969  VuoPort *toPort = subcompositionNode->getBase()->getInputPortWithName(publishedPortNames[inputCablesToRestoreToPort[i] ]);
5970  VuoNode *toNode = subcompositionNode->getBase();
5971 
5972  if (fromNode && fromPort && toNode && toPort &&
5973  toNode->hasRenderer() && (toNode->getRenderer()->scene() == composition))
5974  {
5975  // Restore internal input cables.
5976  if (fromNode->hasRenderer() && (fromNode->getRenderer()->scene() == composition) &&
5977  (fromNode != composition->getPublishedInputNode()))
5978  {
5979  VuoRendererCable *cable = new VuoRendererCable((new VuoCompilerCable(fromNode->getCompiler(),
5980  static_cast<VuoCompilerPort *>(fromPort->getCompiler()),
5981  toNode->getCompiler(),
5982  static_cast<VuoCompilerPort *>(toPort->getCompiler())))->getBase());
5983  cable->getBase()->getCompiler()->setAlwaysEventOnly(inputCablesToRestoreEventOnly[i]);
5984  inputCablesToRestore.append(cable);
5985  }
5986  // Restore published input cables.
5987  else if (fromNode == composition->getPublishedInputNode())
5988  {
5989  undoStack->push(new VuoCommandPublishPort(toPort,
5990  NULL,
5991  this,
5992  inputCablesToRestoreEventOnly[i],
5993  inputCablesToRestoreFromPort[i],
5994  true
5995  ));
5996  }
5997  }
5998  }
5999 
6000  QList<QGraphicsItem *> outputCablesToRestore;
6001  for (int i = 0; i < outputCablesToRestoreEventOnly.size(); ++i)
6002  {
6003  VuoPort *fromPort = subcompositionNode->getBase()->getOutputPortWithName(publishedPortNames[outputCablesToRestoreFromPort[i] ]);
6004  VuoNode *fromNode = subcompositionNode->getBase();
6005  VuoPort *toPort = composition->getPortWithStaticIdentifier(outputCablesToRestoreToPort[i]);
6006  VuoNode *toNode = (toPort? composition->getUnderlyingParentNodeForPort(toPort, composition) : NULL);
6007 
6008  if (fromNode && fromPort && toNode && toPort &&
6009  fromNode->hasRenderer() && (fromNode->getRenderer()->scene() == composition))
6010  {
6011  // Restore internal output cables.
6012  if (toNode->hasRenderer() && (toNode->getRenderer()->scene() == composition) &&
6013  (toNode != composition->getPublishedOutputNode()))
6014  {
6015  VuoRendererCable *cable = new VuoRendererCable((new VuoCompilerCable(fromNode->getCompiler(),
6016  static_cast<VuoCompilerPort *>(fromPort->getCompiler()),
6017  toNode->getCompiler(),
6018  static_cast<VuoCompilerPort *>(toPort->getCompiler())))->getBase());
6019  cable->getBase()->getCompiler()->setAlwaysEventOnly(outputCablesToRestoreEventOnly[i]);
6020  outputCablesToRestore.append(cable);
6021  }
6022  // Restore published output cables.
6023  else if (toNode == composition->getPublishedOutputNode())
6024  {
6025  undoStack->push(new VuoCommandPublishPort(fromPort,
6026  NULL,
6027  this,
6028  outputCablesToRestoreEventOnly[i],
6029  outputCablesToRestoreToPort[i],
6030  true
6031  ));
6032  }
6033  }
6034  }
6035 
6036  undoStack->push(new VuoCommandAdd(inputCablesToRestore, this));
6037  undoStack->push(new VuoCommandAdd(outputCablesToRestore, this));
6038 
6039  // Note the refactoring in the composition diff.
6040  {
6041  string compositionIdentifier = static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->getCompositionIdentifier(composition);
6042 
6043  set<VuoCompilerNode *> nodesMoved;
6044  for (QGraphicsItem *item : itemsToRemove)
6045  {
6046  VuoRendererNode *node = dynamic_cast<VuoRendererNode *>(item);
6047  if (node)
6048  nodesMoved.insert(node->getBase()->getCompiler());
6049  }
6050 
6051  VuoCompilerNode *subcompositionMovedTo = subcompositionNode->getBase()->getCompiler();
6052 
6053  string compositionSnapshot = composition->takeSnapshot();
6054 
6056  diffInfo->addRefactoring(compositionIdentifier, nodesMoved, subcompositionMovedTo);
6057  coalesceSnapshots(compositionSnapshot, compositionSnapshot, diffInfo);
6058  }
6059 
6060  undoStack->endMacro();
6061  composition->collapseTypecastNodes();
6062 
6063  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
6064  };
6065 
6066  // The order of the following two lines matters -- the second callback registered
6067  // for a given module manager and node class replaces the first.
6068  static_cast<VuoEditor *>(qApp)->highlightNewNodeClassInAllLibraries(nodeClassName);
6069  composition->getModuleManager()->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionAdded);
6070  }
6071 
6072  // Case: Subcomposition installation failed or was cancelled.
6073  else // if (nodeClassName.empty())
6074  {
6075  // Re-collapse any typecasts that we uncollapsed during attempted refactoring.
6076  composition->collapseTypecastNodes();
6077 
6078  VUserLog("%s: }", getWindowTitleWithoutPlaceholder().toUtf8().data());
6079  }
6080 }
6081 
6086 {
6087  return raiseDocumentAction;
6088 }
6089 
6094 {
6095  return ui->showEvents;
6096 }
6097 
6102 {
6103  return ui->zoomOut;
6104 }
6105 
6110 {
6111  return ui->zoom11;
6112 }
6113 
6118 {
6119  return ui->zoomToFit;
6120 }
6121 
6126 {
6127  return ui->zoomIn;
6128 }
6129 
6134 {
6135  if (isMinimized())
6136  showNormal();
6137  else
6138  show();
6139 
6140  raise();
6141  activateWindow();
6142 }
6143 
6148 {
6150 }
6151 
6156 {
6157  return ownedNodeLibrary;
6158 }
6159 
6165 {
6166  return nl;
6167 }
6168 
6175 {
6176  transitionNodeLibraryConnections(this->nl, library);
6177  this->nl = library;
6178 }
6179 
6184 void VuoEditorWindow::releaseSurrogateNodeLibrary(bool previousFloaterDestroyed)
6185 {
6186  transitionNodeLibraryConnections((previousFloaterDestroyed? NULL : this->nl), this->ownedNodeLibrary);
6187  this->nl = this->ownedNodeLibrary;
6188 }
6189 
6194 {
6195  return ui->menuFile;
6196 }
6197 
6202 {
6203  return menuOpenRecent;
6204 }
6205 
6210 {
6211  return composition;
6212 }
6213 
6217 void VuoEditorWindow::transitionNodeLibraryConnections(VuoNodeLibrary *oldLibrary, VuoNodeLibrary *newLibrary)
6218 {
6219  if (oldLibrary)
6220  {
6222  disconnect(oldLibrary, &VuoNodeLibrary::componentsAdded, this, &VuoEditorWindow::componentsAdded);
6223  disconnect(ui->actionShowNodeNames, &QAction::triggered, oldLibrary, &VuoNodeLibrary::clickedNamesButton);
6224  disconnect(ui->actionShowNodeClassNames, &QAction::triggered, oldLibrary, &VuoNodeLibrary::clickedFlatClassButton);
6225  disconnect(oldLibrary, &VuoNodeLibrary::changedIsHumanReadable, this, &VuoEditorWindow::updateUI);
6227 
6228  disconnect(oldLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateSceneRect);
6229  disconnect(oldLibrary, &VuoNodeLibrary::dockLocationChanged, this, &VuoEditorWindow::updateSceneRect);
6231 
6232  disconnect(composition, SIGNAL(nodePopoverRequestedForClass(VuoNodeClass *)), oldLibrary, SLOT(prepareAndDisplayNodePopoverForClass(VuoNodeClass *)));
6233 
6234  // Update the "Show/Hide Node Library" menu item when the node library visibility is changed.
6235  disconnect(oldLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateUI);
6236  }
6237 
6238  if (newLibrary)
6239  {
6242  connect(ui->actionShowNodeNames, &QAction::triggered, newLibrary, &VuoNodeLibrary::clickedNamesButton);
6243  connect(ui->actionShowNodeClassNames, &QAction::triggered, newLibrary, &VuoNodeLibrary::clickedFlatClassButton);
6246 
6247  connect(newLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateSceneRect);
6248  connect(newLibrary, &VuoNodeLibrary::dockLocationChanged, this, &VuoEditorWindow::updateSceneRect);
6250 
6251  connect(composition, SIGNAL(nodePopoverRequestedForClass(VuoNodeClass *)), newLibrary, SLOT(prepareAndDisplayNodePopoverForClass(VuoNodeClass *)));
6252 
6253 
6254  // Update the "Show/Hide Node Library" menu item when the node library visibility is changed.
6255  connect(newLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditorWindow::updateUI);
6256  }
6257 }
6258 
6267 void VuoEditorWindow::restoreDefaultLeftDockedWidgetWidths()
6268 {
6269  if (nl)
6270  {
6271  nl->setMinimumWidth(nodeLibraryMinimumWidth);
6272  nl->setMaximumWidth(nodeLibraryMaximumWidth);
6273  }
6274 
6275  if (inputPortSidebar && outputPortSidebar)
6276  {
6277  inputPortSidebar->setMinimumWidth(outputPortSidebar->minimumWidth());
6278  inputPortSidebar->setMaximumWidth(outputPortSidebar->maximumWidth());
6279  }
6280 }
6281 
6286 void VuoEditorWindow::moveEvent(QMoveEvent *event)
6287 {
6288  QPoint positionChange = event->pos() - event->oldPos();
6289  composition->movePopoversBy(positionChange.x(), positionChange.y());
6290 
6291  QMainWindow::moveEvent(event);
6292 }
6293 
6298 void VuoEditorWindow::resizeEvent(QResizeEvent *event)
6299 {
6300 #if VUO_PRO
6301  if (toolbar)
6302  toolbar->updateTitle();
6303 #endif
6304 
6305  QMainWindow::resizeEvent(event);
6306 }
6307 
6313 QList<QGraphicsItem *> VuoEditorWindow::createAnyNecessaryMakeListComponents(VuoPort *port)
6314 {
6315  QList<QGraphicsItem *> makeListComponents;
6316 
6317  VuoCompilerInputEventPort *inputEventPort = dynamic_cast<VuoCompilerInputEventPort *>(port->getCompiler());
6318  if (inputEventPort && VuoCompilerType::isListType(inputEventPort->getDataType()))
6319  {
6320  VuoNode *parentNode = port->getRenderer()->getUnderlyingParentNode()->getBase();
6321  VuoRendererCable *makeListCable = NULL;
6322  VuoRendererNode *makeListNode = composition->createAndConnectMakeListNode(parentNode, port, makeListCable);
6323 
6324  makeListComponents.append(makeListNode);
6325  makeListComponents.append(makeListCable);
6326  }
6327 
6328  return makeListComponents;
6329 }
6330 
6334 void VuoEditorWindow::updateGrid()
6335 {
6336  QGraphicsView::CacheMode defaultViewportCacheMode = ui->graphicsView->cacheMode();
6337 
6338  ui->graphicsView->setCacheMode(QGraphicsView::CacheNone);
6339  ui->graphicsView->viewport()->update();
6340 
6341  ui->graphicsView->setCacheMode(defaultViewportCacheMode);
6342 }
6343 
6347 void VuoEditorWindow::updateCanvasOpacity()
6348 {
6349  int opacity = static_cast<VuoEditor *>(qApp)->getCanvasOpacity();
6350  VuoEditorUtilities::setWindowOpacity(this, opacity);
6351 }
6352 
6356 void VuoEditorWindow::updateColor(bool isDark)
6357 {
6359  QString backgroundColor = colors->canvasFill().name();
6360  QString scrollBarColor = isDark ? "#505050" : "#dfdfdf";
6361  QString dockwidgetTitleBackgroundColor = isDark ? "#919191" : "#efefef";
6362 
6363  QString menuStyle = VUO_QSTRINGIFY(
6364  // Sync with VuoCodeWindow::updateColor.
6365  // Should parallel VuoDialogForInputEditor::getStyleSheet()'s QComboBox popup menu styles.
6366  QMenu {
6367  background-color: #404040;
6368  }
6369  QMenu::item {
6370  color: #cfcfcf;
6371  padding-left: 22px;
6372  padding-right: 36px;
6373  height: 21px;
6374  }
6375  QMenu::item:disabled {
6376  color: #707070;
6377  }
6378  QMenu::item:selected {
6379  background-color: #1060d0;
6380  color: #ffffff;
6381  }
6382  QMenu::right-arrow {
6383  left: -14px;
6384  }
6385  QMenu::indicator:checked {
6386  image: url(:/Icons/checkmark.svg);
6387  width: 11px;
6388  }
6389  QMenu::indicator:checked,
6390  QMenu::icon {
6391  margin-left: 6px;
6392  }
6393  QMenu::icon:checked,
6394  QMenu::icon:unchecked {
6395  margin-left: 0;
6396  }
6397  );
6398 
6399  if (doneInitializing)
6400  setStyleSheet(VUO_QSTRINGIFY(
6401  QMainWindow::separator {
6402  background-color: %1;
6403  width: 1px;
6404  height: 0px;
6405  margin: -1px;
6406  padding: 0px;
6407  }
6408  )
6409  .arg(dockwidgetTitleBackgroundColor)
6410  + (isDark ? menuStyle : ""));
6411 
6413  ui->graphicsView->setStyleSheet(isDark ? menuStyle : "");
6414  else
6415  ui->graphicsView->setStyleSheet(VUO_QSTRINGIFY(
6416  QScrollBar {
6417  background: %1;
6418  height: 14px;
6419  width: 14px;
6420  }
6421  QScrollBar::handle {
6422  background: %2;
6423  border-radius: 5px;
6424  min-width: 20px;
6425  min-height: 20px;
6426  margin: 2px;
6427  }
6428  QAbstractScrollArea::corner,
6429  QScrollBar::add-line,
6430  QScrollBar::sub-line,
6431  QScrollBar::add-page,
6432  QScrollBar::sub-page {
6433  background: %1;
6434  border: none;
6435  }
6436  )
6437  .arg(backgroundColor)
6438  .arg(scrollBarColor)
6439  + (isDark ? menuStyle : ""));
6440 
6441 
6442  if (doneInitializing)
6443  foreach (VuoComment *comment, composition->getBase()->getComments())
6444  {
6445  if (comment->hasRenderer())
6446  comment->getRenderer()->updateColor();
6447  }
6448 }
6449 
6454 void VuoEditorWindow::coalescedUpdateRunningComposition()
6455 {
6456  if (coalescedOldCompositionSnapshot.empty() != coalescedNewCompositionSnapshot.empty())
6457  return;
6458 
6459  foreach (string nodeID, coalescedNodesToUnlink)
6460  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->unlinkNodeInSupercompositionFromSubcomposition(composition, nodeID);
6461 
6462  if (!coalescedOldCompositionSnapshot.empty() && !coalescedNewCompositionSnapshot.empty())
6463  composition->updateRunningComposition(coalescedOldCompositionSnapshot, coalescedNewCompositionSnapshot, coalescedDiffInfo);
6464 
6465  foreach (string portID, coalescedInternalPortConstantsToSync)
6466  composition->syncInternalPortConstantInRunningComposition(portID);
6467 
6468  foreach (string portID, coalescedPublishedPortConstantsToSync)
6469  composition->syncPublishedPortConstantInRunningComposition(portID);
6470 
6471  foreach (string nodeID, coalescedNodesToRelink)
6472  static_cast<VuoEditor *>(qApp)->getSubcompositionRouter()->relinkNodeInSupercompositionToSubcomposition(composition, nodeID);
6473 
6474  coalescedOldCompositionSnapshot = "";
6475  coalescedNewCompositionSnapshot = "";
6476  coalescedInternalPortConstantsToSync.clear();
6477  coalescedPublishedPortConstantsToSync.clear();
6478  coalescedNodesToUnlink.clear();
6479  coalescedNodesToRelink.clear();
6480  coalescedDiffInfo = nullptr;
6481 }
6482 
6486 void VuoEditorWindow::coalesceSnapshots(string oldCompositionSnapshot, string newCompositionSnapshot, VuoCompilerCompositionDiff *diffInfo)
6487 {
6488  // @todo For now, don't attempt to merge VuoCompilerCompositionDiffs.
6489  if (diffInfo && coalescedDiffInfo)
6490  coalescedUpdateRunningComposition();
6491 
6492  if (coalescedOldCompositionSnapshot.empty())
6493  coalescedOldCompositionSnapshot = oldCompositionSnapshot;
6494 
6495  coalescedNewCompositionSnapshot = newCompositionSnapshot;
6496  coalescedDiffInfo = diffInfo;
6497 }
6498 
6503 {
6504  coalescedInternalPortConstantsToSync.push_back(portID);
6505 }
6506 
6511 {
6512  coalescedPublishedPortConstantsToSync.push_back(portID);
6513 }
6514 
6519 {
6520  coalescedNodesToUnlink.push_back(nodeID);
6521 }
6522 
6527 {
6528  coalescedNodesToRelink.push_back(nodeID);
6529 }
6530 
6536 void VuoEditorWindow::handlePendingProtocolComplianceReevaluationRequests()
6537 {
6538  if (protocolComplianceReevaluationPending)
6539  {
6540  evaluateCompositionForProtocolPromotion();
6541  protocolComplianceReevaluationPending = false;
6542  }
6543 
6544  return;
6545 }
6546 
6553 void VuoEditorWindow::registerProtocolComplianceEvaluationRequest()
6554 {
6555  this->protocolComplianceReevaluationPending = true;
6556 }
6557 
6562 {
6563  return ui->graphicsView->mapToScene(ui->graphicsView->mapFromGlobal(QCursor::pos()));
6564 }
6565 
6570 {
6571  return metadataPanel;
6572 }
6573 
6583 QPointF VuoEditorWindow::getFittedScenePos(QPointF origPos, int leftMargin, int topMargin, int rightMargin, int bottomMargin)
6584 {
6585  QRectF viewportRect = ui->graphicsView->mapToScene(ui->graphicsView->viewport()->rect()).boundingRect();
6586  double targetX = min(max(origPos.x(), viewportRect.left()+leftMargin), viewportRect.right()-rightMargin);
6587  double targetY = min(max(origPos.y(), viewportRect.top()+topMargin), viewportRect.bottom()-bottomMargin);
6588 
6589  // Don't return the position of any pre-existing node.
6590  bool targetPositionClearOfCoincidentNodes = false;
6591  while (!targetPositionClearOfCoincidentNodes)
6592  {
6593  QGraphicsItem *preexistingItem = composition->itemAt(QPoint(targetX, targetY), composition->views()[0]->transform());
6594  if (dynamic_cast<VuoRendererNode *>(preexistingItem) && (preexistingItem->scenePos() == QPoint(targetX, targetY)))
6595  {
6596  targetX += pastedComponentOffset;
6597  targetY += pastedComponentOffset;
6598  }
6599  else
6600  targetPositionClearOfCoincidentNodes = true;
6601  }
6602 
6603  return QPointF(targetX, targetY);
6604 }
6605 
6609 void VuoEditorWindow::toggleNodeLibraryDockedState()
6610 {
6611  if (nl)
6612  {
6613  bool floatLibrary = !nl->isFloating();
6614  nl->setFloating(floatLibrary);
6615  nl->fixWidth(!floatLibrary && !inputPortSidebar->isHidden());
6616  updateUI();
6617  }
6618 }
6619 
6623 void VuoEditorWindow::beginUndoStackMacro(QString commandName)
6624 {
6625  undoStack->beginMacro(tr(commandName.toUtf8().constData()));
6626 }
6627 
6632 {
6633  undoStack->endMacro();
6634 }