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