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