Vuo  2.1.0
VuoEditor.cc
Go to the documentation of this file.
1 
10 #include "VuoEditor.hh"
11 
12 #include "VuoCodeWindow.hh"
13 #include "VuoCompilerDriver.hh"
14 #include "VuoCompilerIssue.hh"
15 #include "VuoComposition.hh"
18 #include "VuoEditorAboutBox.hh"
19 #include "VuoEditorComposition.hh"
20 #include "VuoEditorUtilities.hh"
21 #include "VuoEditorWindow.hh"
22 #include "VuoErrorDialog.hh"
23 #include "VuoException.hh"
24 #include "VuoModuleManager.hh"
25 #include "VuoNodeClass.hh"
26 #include "VuoNodeSet.hh"
27 #include "VuoStringUtilities.hh"
28 #include "VuoRendererCommon.hh"
29 #include "VuoRendererFonts.hh"
30 #include "VuoCompilerException.hh"
31 #include "VuoCompilerNodeClass.hh"
32 #include "VuoExampleMenu.hh"
33 #include "VuoNodePopover.hh"
34 #include "VuoOsStatus.h"
35 #include "VuoProtocol.hh"
36 #include "VuoRecentFileMenu.hh"
39 
40 #include <langinfo.h>
41 #include <sstream>
42 #include <fstream>
43 
44 #include <Carbon/Carbon.h>
45 #include <IOKit/IOKitLib.h>
46 
47 const QString VuoEditor::recentFileListSettingsKey = "recentFileList";
48 const QString VuoEditor::subcompositionPrefixSettingsKey = "subcompositionPrefix";
49 const QString VuoEditor::nodeLibraryDisplayModeSettingsKey = "nodeLibraryDisplayMode";
50 const QString VuoEditor::nodeLibraryVisibilityStateSettingsKey = "nodeLibraryVisible";
51 const QString VuoEditor::nodeLibraryDockingStateSettingsKey = "nodeLibraryDocked";
52 const QString VuoEditor::nodeLibraryFloatingPositionSettingsKey = "nodeLibraryFloatingPosition";
53 const QString VuoEditor::nodeLibraryWidthSettingsKey = "nodeLibraryWidth";
54 const QString VuoEditor::nodeLibraryHeightSettingsKey = "nodeLibraryHeight";
55 const QString VuoEditor::nodeDocumentationPanelHeightSettingsKey = "nodeDocumentationPanelHeight";
56 const QString VuoEditor::shaderDocumentationVisibilitySettingsKey = "shaderDocumentationVisible";
57 const QString VuoEditor::gridTypeSettingsKey = "canvasGridType";
58 const QString VuoEditor::gridOpacitySettingsKey = "canvasGridOpacity";
59 const qreal VuoEditor::defaultGridOpacity = 1;
60 const QString VuoEditor::snapToGridSettingsKey = "canvasGridSnap";
61 const QString VuoEditor::darkInterfaceSettingsKey = "darkInterface";
62 const QString VuoEditor::canvasOpacitySettingsKey = "canvasOpacity";
63 const QString VuoEditor::movieExportWidthSettingsKey = "movieExportWidth";
64 const QString VuoEditor::movieExportHeightSettingsKey = "movieExportHeight";
65 const QString VuoEditor::movieExportTimeSettingsKey = "movieExportTime";
66 const QString VuoEditor::movieExportDurationSettingsKey = "movieExportDuration";
67 const QString VuoEditor::movieExportFramerateSettingsKey = "movieExportFramerate";
68 const QString VuoEditor::movieExportSpatialSupersampleSettingsKey = "movieExportSpatialSupersample";
69 const QString VuoEditor::movieExportTemporalSupersampleSettingsKey = "movieExportTemporalSupersample";
70 const QString VuoEditor::movieExportShutterAngleSettingsKey = "movieExportShutterAngle";
71 const QString VuoEditor::movieExportImageFormatSettingsKey = "movieExportImageFormat";
72 const QString VuoEditor::movieExportQualitySettingsKey = "movieExportQuality";
73 
74 const QString VuoEditor::vuoHelpBookScheme = "vuo-help";
75 const QString VuoEditor::vuoNodeSetDocumentationScheme = "vuo-nodeset";
76 const QString VuoEditor::vuoNodeDocumentationScheme = "vuo-node";
77 const QString VuoEditor::vuoExampleCompositionScheme = "vuo-example";
78 const QString VuoEditor::vuoExampleHighlightedNodeClassQueryItem = "nodeClass";
79 const QString VuoEditor::vuoCompositionFileExtension = "vuo";
80 const QString VuoEditor::vuoNodeClassFileExtension = "vuonode";
81 const QString VuoEditor::vuoTutorialURL = "https://vuo.org/tutorials";
82 
83 string VuoEditor::documentationGenerationDirectory = "";
84 
88 VuoEditor::VuoEditor(int &argc, char *argv[])
89  : QApplication(argc,argv)
90 {
91  // Load stored application settings.
92  QCoreApplication::setOrganizationName("Vuo");
93  QCoreApplication::setOrganizationDomain("vuo.org");
94  QCoreApplication::setApplicationName("Editor");
95  settings = new QSettings();
96 
97  loadTranslations();
98 
99  VuoEditorWindow::untitledComposition = QObject::tr("Untitled Composition");
100 
101 
102  // Controls the 2-finger-scroll speed.
103  // Defaults to 3 which (after https://b33p.net/kosada/node/13245) feels a little too fast.
104  // 2 seems to better match Preview.app.
105  setWheelScrollLines(2);
106 
107 
108  uiInitialized = false;
110 
111  setQuitOnLastWindowClosed(false);
112  aboutBox = nullptr;
113  ownedNodeLibrary = NULL;
114  documentationQueue = dispatch_queue_create("org.vuo.editor.documentation", NULL);
115  userSubscriptionLevel = VuoEditor::CommunityUser;
116  reportAbsenceOfUpdates = false;
117  networkManager = NULL;
118  compiler = nullptr;
119  moduleManager = nullptr;
120  subcompositionRouter = new VuoSubcompositionMessageRouter();
121 
122  // Initialize default settings.
123  const VuoNodeLibrary::nodeLibraryDisplayMode defaultNodeLibraryDisplayMode = VuoNodeLibrary::displayByName;
124  const bool defaultNodeLibraryVisibilityState = true; // Node library is visible by default.
125  const bool defaultNodeLibraryDockingState = true; // When visible, node library is docked by default.
126  const int defaultNodeLibraryDocumentationPanelHeight = 280;
127 
128  qApp->setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents, false);
129  qApp->setAttribute(Qt::AA_SynthesizeTouchForUnhandledMouseEvents, false);
130 
131  qApp->setAttribute(Qt::AA_UseHighDpiPixmaps);
132  QToolTip::setFont(VuoRendererFonts::getSharedFonts()->portPopoverFont());
133 
134  // Disable macOS's restore-open-documents-on-launch feature, since it doesn't work with Qt anyway,
135  // and since it pops up a meaningless "Do you want to try to reopen its windows again?" window after a crash.
136  settings->setValue("ApplePersistenceIgnoreStateQuietly", true);
137 
138  QStringList storedRecentFileList = settings->value(recentFileListSettingsKey).toStringList();
139 
140  menuOpenRecent = new VuoRecentFileMenu();
141  menuOpenRecent->setRecentFiles(storedRecentFileList);
142 
143 
144 #if VUO_PRO
145  VuoEditor_Pro0();
146 #endif
147 
148  subcompositionPrefix = settings->contains(subcompositionPrefixSettingsKey)?
149  settings->value(subcompositionPrefixSettingsKey).toString() :
150  "";
151 
152  canvasOpacity = settings->contains(canvasOpacitySettingsKey)?
153  settings->value(canvasOpacitySettingsKey).toInt() :
154  255;
155 
156  nodeLibraryDisplayMode = settings->contains(nodeLibraryDisplayModeSettingsKey)?
157  (VuoNodeLibrary::nodeLibraryDisplayMode)settings->value(nodeLibraryDisplayModeSettingsKey).toInt() :
158  defaultNodeLibraryDisplayMode;
159 
160  nodeLibraryCurrentlyVisible = settings->contains(nodeLibraryVisibilityStateSettingsKey)?
161  settings->value(nodeLibraryVisibilityStateSettingsKey).toBool() :
162  defaultNodeLibraryVisibilityState;
163 
164  nodeLibraryCurrentlyDocked = settings->contains(nodeLibraryDockingStateSettingsKey)?
165  settings->value(nodeLibraryDockingStateSettingsKey).toBool() :
166  defaultNodeLibraryDockingState;
167 
168  if (settings->contains(nodeLibraryFloatingPositionSettingsKey))
169  {
170  settingsContainedNodeLibraryFloatingPosition = true;
171  nodeLibraryFloatingPosition = settings->value(nodeLibraryFloatingPositionSettingsKey).toPoint();
172  }
173  else
174  {
175  settingsContainedNodeLibraryFloatingPosition = false;
176  nodeLibraryFloatingPosition = QPoint();
177  }
178 
179  if (settings->contains(nodeLibraryWidthSettingsKey))
180  nodeLibraryWidth = settings->value(nodeLibraryWidthSettingsKey).toInt();
181  else
182  nodeLibraryWidth = -1;
183 
184  if (settings->contains(nodeLibraryHeightSettingsKey))
185  nodeLibraryHeight = settings->value(nodeLibraryHeightSettingsKey).toInt();
186  else
187  nodeLibraryHeight = -1;
188 
189  if (settings->contains(nodeDocumentationPanelHeightSettingsKey))
190  nodeDocumentationPanelHeight = settings->value(nodeDocumentationPanelHeightSettingsKey).toInt();
191  else
192  nodeDocumentationPanelHeight = defaultNodeLibraryDocumentationPanelHeight;
193 
194  previousVisibleNodeLibraryStateWasDocked = nodeLibraryCurrentlyDocked;
195  currentFloatingNodeLibrary = NULL;
196 
197  shaderDocumentationVisible = settings->contains(shaderDocumentationVisibilitySettingsKey)?
198  settings->value(shaderDocumentationVisibilitySettingsKey).toBool() :
199  true;
200 
201  applyStoredMovieExportSettings();
202 
203  // Disable the Mac OS 10.7+ restore-open-documents-on-launch feature, since it doesn't work with Qt anyway,
204  // and since it pops up a meaningless "Do you want to try to reopen its windows again?" window after a crash.
205  // (Reverted for now, since it it causes "ApplePersistenceIgnoreState: Existing state will not be touched. New state will be written to…" console messages.
206 // settings->setValue("ApplePersistenceIgnoreState", true);
207 
208  compiler = new VuoCompiler();
209  moduleManager = new VuoModuleManager(compiler);
210 
211 #if VUO_PRO
212  VuoEditor_Pro1();
213 #endif
214 
215  builtInDriversQueue = dispatch_queue_create("org.vuo.editor.drivers", NULL);
216  dispatch_async(builtInDriversQueue, ^{
217  initializeBuiltInDrivers();
218  });
219 
220  compiler->prepareForFastBuild();
221 
222  // If operating in documentation-generation mode, generate the node documentation and exit.
223  if (!documentationGenerationDirectory.empty())
224  {
225  dispatch_async(dispatch_get_main_queue(), ^{
226  generateAllNodeSetHtmlDocumentation(documentationGenerationDirectory);
227  quitCleanly();
228  });
229  return;
230  }
231 
232  // Register the URL handler for the "vuo-help" scheme, used for displaying a section of the user manual.
233  QDesktopServices::setUrlHandler(vuoHelpBookScheme, this, "openHelpBookPageFromUrl");
234 
235  // Register the URL handler for the "vuo-nodeset" scheme, used for displaying node set documentation.
236  QDesktopServices::setUrlHandler(vuoNodeSetDocumentationScheme, this, "showNodeSetDocumentationFromUrl");
237 
238  // Register the URL handler for the "vuo-node" scheme, used for displaying node documentation.
239  QDesktopServices::setUrlHandler(vuoNodeDocumentationScheme, this, "showNodeDocumentationFromUrl");
240 
241  // Register the URL handler for the "vuo-example" scheme, used for opening example compositions.
242  QDesktopServices::setUrlHandler(vuoExampleCompositionScheme, this, "openExampleCompositionFromUrl");
243 
244  // Construct the basic menu bar here, so that it's visible when no document windows are open.
245  menuBar = new QMenuBar();
246  {
247  menuFile = new QMenu(menuBar);
248  menuFile->setSeparatorsCollapsible(false);
249  menuFile->setTitle(tr("&File"));
250 
251  menuFile->addAction(tr("&New Composition"), this, SLOT(newComposition()), QKeySequence("Ctrl+N"));
252 
253  menuNewCompositionWithTemplate = new QMenu(tr("New Composition from Template"));
254  menuNewCompositionWithTemplate->setSeparatorsCollapsible(false);
255  populateNewCompositionWithTemplateMenu(menuNewCompositionWithTemplate);
256  menuFile->addMenu(menuNewCompositionWithTemplate);
257 
258  QMenu *menuNewShader = new QMenu(tr("New Shader"));
259  menuNewShader->setSeparatorsCollapsible(false);
260  populateNewShaderMenu(menuNewShader);
261  menuFile->addMenu(menuNewShader);
262 
263  menuFile->addSeparator();
264  menuFile->addAction(tr("&Open…"), this, SLOT(openFile()), QKeySequence("Ctrl+O"));
265 
266  // "Open Recent" menu
267  connect(menuOpenRecent, &VuoRecentFileMenu::recentFileSelected, this, &VuoEditor::openUrl);
269  menuFile->addMenu(menuOpenRecent);
270  connect(menuFile, &QMenu::aboutToShow, this, &VuoEditor::pruneAllOpenRecentFileMenus);
271 
272  // "Open Example" menu
273  menuOpenExample = new VuoExampleMenu(menuFile, compiler);
274  connect(menuOpenExample, &VuoExampleMenu::exampleSelected, this, &VuoEditor::openUrl);
275  menuFile->addMenu(menuOpenExample);
276 
277  // Connect the "Quit" menu item action to our customized quit method. On Mac OS X, this menu
278  // item will automatically be moved from the "File" menu to the Application menu.
279  menuFile->addAction(tr("Quit"), this, SLOT(quitCleanly()), QKeySequence("Ctrl+Q"));
280 
281  // "About" menu item
282  menuFile->addAction(tr("About Vuo…"), this, SLOT(about()));
283 
284  // Workaround for bug preventing the "Quit" and "About" menu items from appearing within the Application menu:
285  // Add them again to the "File" menu, this time with Qt's automatic menu merging behavior disabled;
286  // this at least gains us a functional keyboard shortcut for the "Quit" item. (@todo: Fix properly; see
287  // https://b33p.net/kosada/node/5260 ).
288  QAction *aboutAction = new QAction(NULL);
289  aboutAction->setText(tr("About Vuo…"));
290  connect(aboutAction, &QAction::triggered, this, &VuoEditor::about);
291  aboutAction->setMenuRole(QAction::NoRole); // Disable automatic menu merging for this item.
292  menuFile->addSeparator();
293  menuFile->addAction(aboutAction);
294 
295  QAction *quitAction = new QAction(NULL);
296  quitAction->setText(tr("&Quit Vuo"));
297  quitAction->setShortcut(QKeySequence("Ctrl+Q"));
298  connect(quitAction, &QAction::triggered, this, &VuoEditor::quitCleanly);
299  quitAction->setMenuRole(QAction::NoRole); // Disable automatic menu merging for this item.
300  menuFile->addSeparator();
301  menuFile->addAction(quitAction);
302 
303  menuBar->addAction(menuFile->menuAction());
304 
305 #if VUO_PRO
306  enableMenuItems(menuFile, canCloseWelcomeWindow());
307 #endif
308  }
309 
310  {
311  menuView = new QMenu(menuBar);
312  menuView->setSeparatorsCollapsible(false);
313  menuView->setTitle(tr("&View"));
314 
315  // "Show Node Library"
316  showNodeLibraryAction = new QAction(NULL);
317  showNodeLibraryAction->setText(tr("Show Node &Library"));
318  showNodeLibraryAction->setShortcut(QKeySequence("Ctrl+Return"));
319  connect(showNodeLibraryAction, &QAction::triggered, this, &VuoEditor::showNodeLibrary);
320  menuView->addAction(showNodeLibraryAction);
321 
322 
323  // "Grid" submenu
324  {
325  // "Snap" menu item
326  snapToGridAction = new QAction(NULL);
327  snapToGridAction->setText(tr("Snap"));
328  snapToGridAction->setCheckable(true);
329  bool snapToGrid = (settings->contains(snapToGridSettingsKey)?
330  settings->value(snapToGridSettingsKey).toBool() :
331  true);
332  VuoRendererItem::setSnapToGrid(snapToGrid);
333  snapToGridAction->setChecked(snapToGrid);
334  connect(snapToGridAction, &QAction::toggled, this, &VuoEditor::updateSnapToGrid);
335 
336  // "Line"/"Points" menu items
337  int gridOpacity;
338  if (settings->contains(gridOpacitySettingsKey))
339  gridOpacity = settings->value(gridOpacitySettingsKey).toInt();
340  else
341  {
342  gridOpacity = VuoEditor::defaultGridOpacity;
343  settings->setValue(gridOpacitySettingsKey, gridOpacity);
344  }
345 
347 
348  int gridType = (settings->contains(gridTypeSettingsKey)
349  ? settings->value(gridTypeSettingsKey).toInt()
350  : VuoRendererComposition::LineGrid);
352 
353  showGridLinesAction = new QAction(NULL);
354  showGridLinesAction->setText(tr("Lines"));
355  showGridLinesAction->setCheckable(true);
356  showGridLinesAction->setChecked(gridType == VuoRendererComposition::LineGrid);
357  connect(showGridLinesAction, &QAction::triggered, this, &VuoEditor::showGridLinesToggled);
358 
359  showGridPointsAction = new QAction(NULL);
360  showGridPointsAction->setText(tr("Points"));
361  showGridPointsAction->setCheckable(true);
362  showGridPointsAction->setChecked(gridType == VuoRendererComposition::PointGrid);
363  connect(showGridPointsAction, &QAction::triggered, this, &VuoEditor::showGridPointsToggled);
364  }
365 
366 
367  // "Canvas Transparency"
368  canvasTransparencyNoneAction = new QAction(NULL);
369  canvasTransparencyNoneAction->setText(tr("None"));
370  canvasTransparencyNoneAction->setData(255);
371  canvasTransparencyNoneAction->setCheckable(true);
372  canvasTransparencyNoneAction->setShortcut(QKeySequence("Ctrl+1"));
373 
374  canvasTransparencySlightAction = new QAction(NULL);
375  canvasTransparencySlightAction->setText(tr("Slightly Transparent"));
376  canvasTransparencySlightAction->setData(230);
377  canvasTransparencySlightAction->setCheckable(true);
378  canvasTransparencySlightAction->setShortcut(QKeySequence("Ctrl+2"));
379 
380  canvasTransparencyHighAction = new QAction(NULL);
381  canvasTransparencyHighAction->setText(tr("Very Transparent"));
382  canvasTransparencyHighAction->setData(191);
383  canvasTransparencyHighAction->setCheckable(true);
384  canvasTransparencyHighAction->setShortcut(QKeySequence("Ctrl+3"));
385 
386  canvasTransparencyOptions = new QActionGroup(this);
390 
391  foreach (QAction *action, canvasTransparencyOptions->actions())
392  {
393  if (action->data().toInt() == canvasOpacity)
394  {
395  action->setChecked(true);
396  action->setEnabled(false);
397  break;
398  }
399  }
400 
401  // updateCanvasOpacity() needs to happen before canvasOpacityChanged(),
402  // since slots attached to the latter rely on VuoEditor::getCanvasOpacity() which is updated by the former.
403  connect(canvasTransparencyOptions, &QActionGroup::triggered, this, &VuoEditor::updateCanvasOpacity);
404  connect(canvasTransparencyOptions, &QActionGroup::triggered, this, &VuoEditor::canvasOpacityChanged);
405 
406  menuBar->addAction(menuView->menuAction());
407  }
408 
409  {
410  // @todo: For some reason the following doesn't work; no "Window" menu item is displayed.
411  menuWindow = new QMenu(menuBar);
412  menuWindow->setSeparatorsCollapsible(false);
413  menuWindow->setTitle(tr("&Window"));
414 
415  populateWindowMenu(menuWindow, nullptr);
416 
417  connect(menuWindow, &QMenu::aboutToShow, this, &VuoEditor::updateUI);
418 
419  menuBar->addAction(menuWindow->menuAction());
420  }
421 
422  {
423  menuHelp = new QMenu(menuBar);
424  menuHelp->setSeparatorsCollapsible(false);
425  menuHelp->setTitle(tr("&Help"));
426  populateHelpMenu(menuHelp);
427 
428  // Prevent "Help > Search" from triggering lookup of all customized example composition titles at once.
429  connect(menuHelp, &QMenu::aboutToShow, menuOpenExample, &VuoExampleMenu::disableExampleTitleLookup);
430  connect(menuHelp, &QMenu::aboutToHide, menuOpenExample, &VuoExampleMenu::enableExampleTitleLookup);
431 
432  connect(menuHelp, &QMenu::aboutToShow, [this] { populateHelpMenu(menuHelp); });
433 
434  menuBar->addAction(menuHelp->menuAction());
435  }
436 
437 #ifdef __APPLE__
438  {
439  // Construct an OS X dock context menu.
440  // Items included in this menu will supplement the default OS X dock context menu items.
441  dockContextMenu = new QMenu();
442  dockContextMenu->setSeparatorsCollapsible(false);
443  dockContextMenu->setAsDockMenu();
444 
445  connect(dockContextMenu, &QMenu::aboutToShow, this, &VuoEditor::updateUI);
446  }
447 #endif
448 
449  initializeTopLevelNodeLibrary(compiler,
450  nodeLibraryDisplayMode,
451  settingsContainedNodeLibraryFloatingPosition,
452  nodeLibraryFloatingPosition,
453  nodeLibraryWidth,
454  nodeLibraryHeight);
455  if (!nodeLibraryCurrentlyDocked)
456  designateNewFloatingNodeLibrary(ownedNodeLibrary);
457 
458  moduleManager->setNodeLibrary(ownedNodeLibrary);
459 
460 
461  // @todo https://b33p.net/kosada/node/14299 The following line causes nodes to be listed in the
462  // floating node library immediately on launch (good), but then to be duplicated moments later (bad).
463  //moduleManager->updateWithAlreadyLoadedModules();
464  conformToGlobalNodeLibraryVisibility(
465  getGlobalNodeLibraryStateForAttributes(nodeLibraryCurrentlyVisible, nodeLibraryCurrentlyDocked),
466  currentFloatingNodeLibrary);
467  ownedNodeLibrary->displayPopoverForCurrentNodeClass();
468 
469  // Update the global node library display mode whenever it is changed for the top-level node library.
470  connect(ownedNodeLibrary, &VuoNodeLibrary::changedIsHumanReadable, static_cast<VuoEditor *>(qApp), &VuoEditor::updateNodeLibraryDisplayMode);
471 
472  // Update the "Show/Hide Node Library" menu item when the node library visibility or display mode is changed.
473  connect(ownedNodeLibrary, &VuoNodeLibrary::visibilityChanged, this, &VuoEditor::updateUI);
474  enableGlobalStateConformanceToLibrary(ownedNodeLibrary);
475  connect(this, &VuoEditor::globalNodeLibraryStateChanged, this, &VuoEditor::conformToGlobalNodeLibraryVisibility);
476 
477 #ifndef VUO_PRO
478  QApplication::processEvents(); // Force processing of any incoming QEvent::FileOpen event, which may populate queuedCompositionsToOpen.
479  if (queuedCompositionsToOpen.empty())
480  newComposition();
481 #endif
482 
483  // Initialize a file dialogue with its default directory set to something reasonable, so that
484  // file dialogues initialized subsequently within the same editor session will default to
485  // this location rather than to the executable directory.
486  // See https://b33p.net/kosada/node/7302 .
487  QFileDialog d;
488  if (d.history().isEmpty())
489  d.setDirectory(getDefaultCompositionStorageDirectory());
490 
491  uiInitialized = true;
493  updateUI();
494 }
495 
500 {
501  dispatch_sync(builtInDriversQueue, ^{});
502 
503  menuBar->deleteLater();
504  delete subcompositionRouter;
505  moduleManager->deleteWhenReady(); // deletes compiler
506 }
507 
511 QString VuoEditor::qtFindTranslation(const QLocale &locale, const QString &filename, const QString &prefix, const QString &directory, const QString &suffix)
512 {
513  QString path;
514  if (QFileInfo(filename).isRelative()) {
515  path = directory;
516  if (!path.isEmpty() && !path.endsWith(QLatin1Char('/')))
517  path += QLatin1Char('/');
518  }
519 
520  QFileInfo fi;
521  QString realname;
522  QStringList fuzzyLocales;
523 
524  // see http://www.unicode.org/reports/tr35/#LanguageMatching for inspiration
525 
526  QStringList languages = locale.uiLanguages();
527  for (int i = languages.size()-1; i >= 0; --i) {
528  QString lang = languages.at(i);
529  QString lowerLang = lang.toLower();
530  if (lang != lowerLang)
531  languages.insert(i+1, lowerLang);
532  }
533 
534  // try explicit locales names first
535  foreach (QString localeName, languages) {
536  localeName.replace(QLatin1Char('-'), QLatin1Char('_'));
537 
538  realname = path + filename + prefix + localeName + (suffix.isNull() ? QLatin1String(".qm") : suffix);
539  fi.setFile(realname);
540  if (fi.isReadable() && fi.isFile())
541  return realname;
542 
543  realname = path + filename + prefix + localeName;
544  fi.setFile(realname);
545  if (fi.isReadable() && fi.isFile())
546  return realname;
547 
548  fuzzyLocales.append(localeName);
549  }
550 
551  // start guessing
552  foreach (QString localeName, fuzzyLocales) {
553  for (;;) {
554  int rightmost = localeName.lastIndexOf(QLatin1Char('_'));
555  // no truncations? fail
556  if (rightmost <= 0)
557  break;
558  localeName.truncate(rightmost);
559 
560  realname = path + filename + prefix + localeName + (suffix.isNull() ? QLatin1String(".qm") : suffix);
561  fi.setFile(realname);
562  if (fi.isReadable() && fi.isFile())
563  return realname;
564 
565  realname = path + filename + prefix + localeName;
566  fi.setFile(realname);
567  if (fi.isReadable() && fi.isFile())
568  return realname;
569  }
570  }
571 
572  if (!suffix.isNull()) {
573  realname = path + filename + suffix;
574  fi.setFile(realname);
575  if (fi.isReadable() && fi.isFile())
576  return realname;
577  }
578 
579  realname = path + filename + prefix;
580  fi.setFile(realname);
581  if (fi.isReadable() && fi.isFile())
582  return realname;
583 
584  realname = path + filename;
585  fi.setFile(realname);
586  if (fi.isReadable() && fi.isFile())
587  return realname;
588 
589  return QString();
590 }
591 
595 void VuoEditor::loadTranslations()
596 {
597  if (settings->value("translation/enable", true).toBool())
598  {
599  QLocale locale = QLocale::system();
600  if (VuoIsDebugEnabled())
601  {
602  VUserLog("C locale:");
603  VUserLog(" NL codeset : %s", nl_langinfo(CODESET));
604  VUserLog(" LANG : %s", getenv("LANG"));
605  VUserLog(" LC_ALL : %s", getenv("LC_ALL"));
606 
607  VUserLog("Apple locale:");
608  CFStringRef appleLocale = (CFStringRef)CFPreferencesCopyAppValue(CFSTR("AppleLocale"), kCFPreferencesCurrentApplication);
609  VUserLog(" ISO (pref) : %s", VuoStringUtilities::makeFromCFString(appleLocale).c_str());
610  CFRelease(appleLocale);
611 
612  CFLocaleRef localeCF = CFLocaleCopyCurrent();
613  VUserLog(" ISO (CF) : %s", VuoStringUtilities::makeFromCFString(CFLocaleGetIdentifier(localeCF)).c_str());
614  CFRelease(localeCF);
615 
616  auto logCFArray = ^(const char *label, CFArrayRef array){
617  CFIndex count = CFArrayGetCount(array);
618  vector<string> s;
619  for (CFIndex i = 0; i < count; ++i)
620  s.push_back(VuoStringUtilities::makeFromCFString(CFArrayGetValueAtIndex(array, i)));
621  VUserLog(" %s: %s", label, VuoStringUtilities::join(s, ',').c_str());
622  CFRelease(array);
623  };
624 
625  logCFArray("UI languages (pref)", (CFArrayRef)CFPreferencesCopyAppValue(CFSTR("AppleLanguages"), kCFPreferencesCurrentApplication));
626 
627  logCFArray("UI languages (CF) ", CFLocaleCopyPreferredLanguages());
628 
629  VUserLog("Qt locale:");
630  VUserLog(" ISO : %s", locale.name().toUtf8().data());
631  VUserLog(" BCP47 : %s", locale.bcp47Name().toUtf8().data());
632  VUserLog(" country : %s (%s)", QLocale::countryToString(locale.country()).toUtf8().data(), locale.nativeCountryName().toUtf8().data());
633  VUserLog(" language : %s (%s)", QLocale::languageToString(locale.language()).toUtf8().data(), locale.nativeLanguageName().toUtf8().data());
634  VUserLog(" UI languages : %s", locale.uiLanguages().join(',').toUtf8().data());
635  VUserLog(" script : %s", QLocale::scriptToString(locale.script()).toUtf8().data());
636  }
637 
638  QString translationsPath = QDir(QString::fromStdString(VuoFileUtilities::getVuoFrameworkPath() + "/../../Resources/Translations")).canonicalPath();
639  QList<QString> contexts;
640  contexts << "qtbase" << "vuo";
641  QString prefix("_");
642  QString suffix(".qm");
643  foreach (QString context, contexts)
644  {
645  VDebugLog("Context %s:", context.toUtf8().data());
646 
647  QTranslator *t = new QTranslator();
648  QString translationFilename = qtFindTranslation(locale, context, prefix, translationsPath, suffix);
649  if (translationFilename.isEmpty())
650  {
651  VDebugLog(" No translation file found.");
652  continue;
653  }
654 
655  bool loaded = t->load(locale, context, prefix, translationsPath, suffix);
656  VDebugLog(" Loading '%s': %s", translationFilename.toUtf8().data(), loaded ? "ok" : "error");
657  if (!loaded)
658  continue;
659 
660  bool installed = installTranslator(t);
661  VDebugLog(" Installing '%s': %s", translationFilename.toUtf8().data(), installed ? "ok" : "error");
662  }
663  }
664  else
665  VDebugLog("Disabling translations since preference `translation.enable` is false.");
666 }
667 
672  {
673  vector<QString> queuedCompositionsToOpenSnapshot(queuedCompositionsToOpen);
674  queuedCompositionsToOpen.clear();
675 
676  // Iterate through a copy of the queue rather than the original queue,
677  // because calls to openUrl() might add the file back into the original queue,
678  // so we need the clear() call to have happened before that.
679  foreach (QString queuedUrl, queuedCompositionsToOpenSnapshot)
680  openUrl(queuedUrl);
681  }
682 
688  {
689 #if VUO_PRO
690  enableMenuItems(menuFile, canCloseWelcomeWindow());
691 
692  if (!queuedCompositionsToOpen.empty())
693  closeWelcomeWindow();
694 #endif
695  }
696 
702  void VuoEditor::enableMenuItems(QMenu *menu, bool enable)
703  {
704  if (!menu)
705  return;
706 
707  foreach (QAction *action, menu->actions())
708  {
709  // Apply recursively to submenus.
710  if (action->menu())
711  {
712  VuoExampleMenu *exampleMenu = dynamic_cast<VuoExampleMenu *>(action->menu());
713  if (exampleMenu)
714  exampleMenu->enableMenuItems(enable);
715  else
716  enableMenuItems(action->menu(), enable);
717  }
718  else
719  {
720  bool isQuitOrHelp = action->text().contains("Quit") || action->text().contains("About");
721  bool isTemplateHeader = (menu == menuNewCompositionWithTemplate) &&
722  action->data().value<QString>().isEmpty() &&
723  !action->data().value<void *>();
724  if (!(isQuitOrHelp || isTemplateHeader))
725  action->setEnabled(enable);
726  }
727  }
728  }
729 
734 {
735  menuOpenRecent->pruneNonexistentFiles();
736  return menuOpenRecent->getRecentFiles();
737 }
738 
743 {
744  VUserLog("Conditionally quit");
745 
746  // Write any unsaved settings changes to permanent storage.
747  settings->sync();
748 
749  // Prevent node library visibility changes from impacting application-global settings
750  // from this point forward, since we are about to forcibly close them.
751  if (ownedNodeLibrary)
752  disableGlobalStateConformanceToLibrary(ownedNodeLibrary);
753 
755  disableGlobalStateConformanceToLibrary(window->getOwnedNodeLibrary());
756 
757  // Try to close each window sequentially, in the order they're stacked.
758  windowsRemainingAfterQuitRequested = VuoEditorUtilities::getOpenEditingWindowsStacked();
759  if (windowsRemainingAfterQuitRequested.empty())
760  reallyQuit();
761  else
762  // Start the chain reaction.
763  windowsRemainingAfterQuitRequested[0]->close();
764 }
765 
773 {
774  if (windowsRemainingAfterQuitRequested.empty())
775  // The user hasn't requested to quit, so do nothing.
776  return;
777 
778  // Restore node library connections.
779  foreach (QMainWindow *window, windowsRemainingAfterQuitRequested)
780  {
781  VuoEditorWindow *compositionWindow = dynamic_cast<VuoEditorWindow *>(window);
782  if (compositionWindow)
783  enableGlobalStateConformanceToLibrary(compositionWindow->getOwnedNodeLibrary());
784  }
785 
786  if (ownedNodeLibrary)
787  enableGlobalStateConformanceToLibrary(ownedNodeLibrary);
788 
789  windowsRemainingAfterQuitRequested.clear();
790 }
791 
798 void VuoEditor::continueQuit(QMainWindow *window)
799 {
800  if (windowsRemainingAfterQuitRequested.empty())
801  // The user hasn't requested to quit, so do nothing.
802  return;
803 
804  windowsRemainingAfterQuitRequested.removeOne(window);
805 
806  if (windowsRemainingAfterQuitRequested.empty())
807  reallyQuit();
808  else
809  // Continue the chain reaction.
810  windowsRemainingAfterQuitRequested[0]->close();
811 }
812 
816 void VuoEditor::reallyQuit()
817 {
818  VUserLog("Quit");
819 
820  try
821  {
823  }
824  catch (...)
825  {
826  // Do nothing; it doesn't matter if this sometimes fails.
827  }
828 
829  QApplication::quit();
830 }
831 
836 {
837  return nodeLibraryCurrentlyDocked;
838 }
839 
845 void VuoEditor::enableGlobalStateConformanceToLibrary(VuoNodeLibrary *library)
846 {
847  connect(library, &VuoNodeLibrary::nodeLibraryHiddenOrUnhidden, this, &VuoEditor::updateGlobalNodeLibraryVisibilityState);
848  connect(library, &VuoNodeLibrary::topLevelChanged, this, &VuoEditor::updateGlobalNodeLibraryDockedState);
849  connect(library, &VuoNodeLibrary::nodeLibraryMoved, this, &VuoEditor::updateGlobalNodeLibraryFloatingPosition);
850  connect(library, &VuoNodeLibrary::nodeLibraryWidthChanged, this, &VuoEditor::updateGlobalNodeLibraryWidth);
851  connect(library, &VuoNodeLibrary::nodeLibraryHeightChanged, this, &VuoEditor::updateGlobalNodeLibraryHeight);
852  connect(library, &VuoNodeLibrary::nodeDocumentationPanelHeightChanged, this, &VuoEditor::updateGlobalNodeDocumentationPanelHeight);
853 }
854 
860 void VuoEditor::disableGlobalStateConformanceToLibrary(VuoNodeLibrary *library)
861 {
862  disconnect(library, &VuoNodeLibrary::nodeLibraryHiddenOrUnhidden, this, &VuoEditor::updateGlobalNodeLibraryVisibilityState);
863  disconnect(library, &VuoNodeLibrary::topLevelChanged, this, &VuoEditor::updateGlobalNodeLibraryDockedState);
864  disconnect(library, &VuoNodeLibrary::nodeLibraryMoved, this, &VuoEditor::updateGlobalNodeLibraryFloatingPosition);
865  disconnect(library, &VuoNodeLibrary::nodeLibraryWidthChanged, this, &VuoEditor::updateGlobalNodeLibraryWidth);
866  disconnect(library, &VuoNodeLibrary::nodeLibraryHeightChanged, this, &VuoEditor::updateGlobalNodeLibraryHeight);
867  disconnect(library, &VuoNodeLibrary::nodeDocumentationPanelHeightChanged, this, &VuoEditor::updateGlobalNodeDocumentationPanelHeight);
868 }
869 
873 void VuoEditor::initializeTopLevelNodeLibrary(VuoCompiler *nodeLibraryCompiler,
874  VuoNodeLibrary::nodeLibraryDisplayMode nodeLibraryDisplayMode,
875  bool setFloatingPosition,
876  QPoint floatingPosition,
877  int nodeLibraryWidth,
878  int nodeLibraryHeight)
879 {
880  ownedNodeLibrary = new VuoNodeLibrary(nodeLibraryCompiler, NULL, nodeLibraryDisplayMode);
881  ownedNodeLibrary->setObjectName("Top-level node library");
882 
883  ownedNodeLibrary->setFloating(true);
884 
885  if (setFloatingPosition)
886  ownedNodeLibrary->move(floatingPosition);
887 
888  if (nodeLibraryHeight >= 0)
889  ownedNodeLibrary->resize(ownedNodeLibrary->rect().width(), nodeLibraryHeight);
890 }
891 
896 {
897  if (!aboutBox)
898  aboutBox = new VuoEditorAboutBox();
899 
900  aboutBox->showNormal();
901  aboutBox->raise();
902  aboutBox->activateWindow();
903 }
904 
909 {
910  m->addAction(canvasTransparencyNoneAction);
911  m->addAction(canvasTransparencySlightAction);
912  m->addAction(canvasTransparencyHighAction);
913 }
914 
918 void VuoEditor::populateWindowMenu(QMenu *m, QMainWindow *currentWindow)
919 {
920  if (currentWindow)
921  {
922  m->addAction(tr("Minimize"), currentWindow, &QMainWindow::showMinimized, QKeySequence("Ctrl+M"));
923  m->addAction(tr("Zoom"), currentWindow, &QMainWindow::showMaximized);
924 
925  m->addSeparator();
926  }
927 
928 #if VUO_PRO
929  populateWindowMenu_Pro(m);
930 #endif
931 
932  QList<QMainWindow *> openWindows = VuoEditorUtilities::getOpenEditingWindows();
933  if (! openWindows.empty())
934  {
935  m->addSeparator();
936 
937  for (QMainWindow *openWindow : openWindows)
938  {
939  QAction *raiseDocumentAction = VuoEditorUtilities::getRaiseDocumentActionForWindow(openWindow);
940  m->addAction(raiseDocumentAction);
941 
942  if (currentWindow)
943  raiseDocumentAction->setChecked(openWindow == currentWindow);
944  else if (openWindow->isMinimized())
945  raiseDocumentAction->setChecked(false);
946  }
947  }
948 }
949 
954 {
955  m->clear();
956 
957  // Vuo Manual
958  QAction *vuoManualAction = new QAction(m);
959  vuoManualAction->setText(tr("Vuo Manual"));
960  vuoManualAction->setData(QUrl(getVuoManualURL()));
961  connect(vuoManualAction, &QAction::triggered, this, &VuoEditor::openHelpBook);
962  m->addAction(vuoManualAction);
963 
964  // Vuo Manual (PDF)
965  QAction *vuoManualPDFAction = new QAction(m);
966  vuoManualPDFAction->setText(tr("Vuo Manual (PDF)"));
967  vuoManualPDFAction->setData(QUrl(getVuoManualURL()));
968  connect(vuoManualPDFAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
969  m->addAction(vuoManualPDFAction);
970 
971  // Video Tutorials
972  QAction *videoTutorialsAction = new QAction(m);
973  videoTutorialsAction->setText(tr("Video Tutorials"));
974  videoTutorialsAction->setData(QUrl(vuoTutorialURL));
975  connect(videoTutorialsAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
976  m->addAction(videoTutorialsAction);
977 
978  m->addSeparator();
979 
980  // Search vuo.org
981  QAction *searchVuoOrgAction = new QAction(m);
982  searchVuoOrgAction->setText(tr("Search vuo.org"));
983  searchVuoOrgAction->setData(QUrl("https://vuo.org/search"));
984  connect(searchVuoOrgAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
985  m->addAction(searchVuoOrgAction);
986 
987  m->addSeparator();
988 
989  // View Community Activity
990  QAction *communityActivityAction = new QAction(m);
991  communityActivityAction->setText(tr("View Community Activity"));
992  communityActivityAction->setData(QUrl("https://vuo.org/community"));
993  connect(communityActivityAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
994  m->addAction(communityActivityAction);
995 
996  // Share a Composition
997  QAction *shareCompositionAction = new QAction(m);
998  shareCompositionAction->setText(tr("Share a Composition"));
999  shareCompositionAction->setData(QUrl("https://vuo.org/node/add/vuo-composition"));
1000  connect(shareCompositionAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1001  m->addAction(shareCompositionAction);
1002 
1003  // Start a Discussion
1004  QAction *startDiscussionAction = new QAction(m);
1005  startDiscussionAction->setText(tr("Start a Discussion"));
1006  startDiscussionAction->setData(QUrl("https://vuo.org/node/add/discussion"));
1007  connect(startDiscussionAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1008  m->addAction(startDiscussionAction);
1009 
1010  // Report a Bug
1011  QAction *reportBugAction = new QAction(m);
1012  reportBugAction->setText(tr("Report a Bug"));
1013  reportBugAction->setData(QUrl("https://vuo.org/bug"));
1014  connect(reportBugAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1015  m->addAction(reportBugAction);
1016 
1017  // Request a Feature
1018  QAction *requestFeatureAction = new QAction(m);
1019  requestFeatureAction->setText(tr("Request a Feature"));
1020  requestFeatureAction->setData(QUrl("https://vuo.org/feature-request"));
1021  connect(requestFeatureAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1022  m->addAction(requestFeatureAction);
1023 
1024  m->addSeparator();
1025 
1026  // Help Us Improve Vuo
1027  QAction *improveVuoAction = new QAction(m);
1028  improveVuoAction->setText(tr("Help Us Improve Vuo"));
1029  improveVuoAction->setData(QUrl("https://vuo.org/community-edition"));
1030  connect(improveVuoAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1031  m->addAction(improveVuoAction);
1032 
1033  // Contact Team Vuo
1034  QAction *contactTeamVuoAction = new QAction(m);
1035  contactTeamVuoAction->setText(tr("Contact Team Vuo"));
1036  contactTeamVuoAction->setData(QUrl("https://vuo.org/contact"));
1037  connect(contactTeamVuoAction, &QAction::triggered, this, &VuoEditor::openExternalUrlFromSenderData);
1038  m->addAction(contactTeamVuoAction);
1039 
1040 #if VUO_PRO
1041  VuoEditor::populateHelpMenu_Pro(m);
1042 #endif
1043 }
1044 
1049 {
1050  OSStatus ret = AHGotoPage(CFSTR("org.vuo.Editor.help"), NULL, NULL);
1051  if (ret)
1052  {
1053  char *description = VuoOsStatus_getText(ret);
1054  VUserLog("Error: Couldn't open Vuo Manual in HelpViewer.app: %s", description);
1055  free(description);
1056  }
1057 }
1058 
1064 void VuoEditor::openHelpBookPageFromUrl(const QUrl &url)
1065 {
1066  CFStringRef relativePath = CFStringCreateWithCString(NULL, url.url(QUrl::RemoveScheme).toUtf8().constData(), kCFStringEncodingUTF8);
1067  OSStatus ret = AHGotoPage(CFSTR("org.vuo.Editor.help"), relativePath, NULL);
1068  if (ret)
1069  {
1070  char *description = VuoOsStatus_getText(ret);
1071  VUserLog("Error: Couldn't open Vuo Manual in HelpViewer.app: %s", description);
1072  free(description);
1073  }
1074  CFRelease(relativePath);
1075 }
1076 
1085 VuoEditorWindow * VuoEditor::createEditorWindow(QString filename, bool existingComposition, VuoProtocol *activeProtocol)
1086 {
1087  string compositionAsString = (existingComposition ? VuoFileUtilities::readFileToString(filename.toStdString()) : "");
1088  VuoEditorWindow *w = createEditorWindow(filename, filename, compositionAsString, activeProtocol);
1089  return w;
1090 }
1091 
1101 VuoEditorWindow * VuoEditor::createEditorWindow(QString documentIdentifier, QString filename, const string &compositionAsString,
1102  VuoProtocol *activeProtocol, string nodeClassToHighlight)
1103 {
1104  VuoEditorWindow *w;
1105  try
1106  {
1107  // @todo Take into account the current node library visibility state in deciding
1108  // whether to dock a node library to the newly opened window.
1109  // See: https://b33p.net/kosada/node/3464, https://b33p.net/kosada/node/3002,
1110  // https://b33p.net/kosada/node/3854, https://b33p.net/kosada/node/3087.
1111  w = new VuoEditorWindow(documentIdentifier, filename,
1112  compositionAsString,
1113  nodeLibraryDisplayMode,
1114  getGlobalNodeLibraryStateForAttributes(nodeLibraryCurrentlyVisible, nodeLibraryCurrentlyDocked),
1115  currentFloatingNodeLibrary,
1116  activeProtocol,
1117  nodeClassToHighlight);
1118  }
1119  catch (const VuoCompilerException &e)
1120  {
1121  VuoErrorDialog::show(NULL, tr("%1 can't be opened").arg(QFileInfo(filename).fileName()), e.what());
1122  return NULL;
1123  }
1124 
1125  documentIdentifierAssigned[filename.isEmpty() ? documentIdentifier : filename] = w;
1126 
1127  const int defaultWindowXOffset = 40;
1128  const int defaultWindowYOffset = 40;
1129 
1131  if (!activeWindow)
1132  {
1133  // If this will be the only window…
1134 
1135  if (ownedNodeLibrary && nodeLibraryCurrentlyVisible && !nodeLibraryCurrentlyDocked)
1136  {
1137  // If the node library is visible and floating, make sure the new window doesn't cover it up.
1138  int initialWindowXOffset = defaultWindowXOffset;
1139  int initialWindowYOffset = defaultWindowYOffset;
1140 
1141  int nodeLibraryLeftPos = ownedNodeLibrary->mapToGlobal(ownedNodeLibrary->rect().topLeft()).x();
1142  int nodeLibraryRightPos = ownedNodeLibrary->mapToGlobal(ownedNodeLibrary->rect().topRight()).x();
1143  int nodeLibraryCenterPos = 0.5*(nodeLibraryLeftPos + nodeLibraryRightPos);
1144 
1145  int availableSpaceLeftBoundary = QApplication::desktop()->availableGeometry(w).left();
1146  int availableSpaceRightBoundary = QApplication::desktop()->availableGeometry(w).right();
1147 
1148  bool nodeLibraryCloserToLeft = (nodeLibraryCenterPos - availableSpaceLeftBoundary) <
1149  (availableSpaceRightBoundary - nodeLibraryCenterPos);
1150 
1151  const int horizontalBuffer = 10;
1152  bool spaceForWindowToRightOfNodeLibrary = (availableSpaceRightBoundary - nodeLibraryRightPos) >=
1153  (w->geometry().width() + horizontalBuffer);
1154 
1155  // Leave enough space to the left of the initial window to display the floating library, if applicable
1156  // and it makes sense to do so.
1157  if ((nodeLibraryCloserToLeft && spaceForWindowToRightOfNodeLibrary))
1158  initialWindowXOffset = nodeLibraryRightPos + horizontalBuffer;
1159 
1160  // If the floating node library's top edge is higher than the default top edge position
1161  // for composition windows, vertically align the first composition window with the top edge
1162  // of the node library instead of using the default.
1163  // ownedNodeLibrary->titleBarWidget() doesn't reliably return non-NULL; hard-code title bar height instead of
1164  // actually retrieving it.
1165  const int titleBarHeight = 16;
1166  int nodeLibraryTopPos = ownedNodeLibrary->mapToGlobal(ownedNodeLibrary->rect().topLeft()).y() - titleBarHeight;
1167 
1168  if (nodeLibraryTopPos < defaultWindowYOffset)
1169  initialWindowYOffset = nodeLibraryTopPos;
1170 
1171  w->move(initialWindowXOffset, initialWindowYOffset);
1172  }
1173  else
1174  {
1175  // If the node library is hidden or docked, just let the window system position the new window.
1176  }
1177 
1178  yScreenSpaceShortage = qMax(0, (w->geometry().bottom()) - QApplication::desktop()->availableGeometry(w).bottom() + 5);
1179  }
1180  else
1181  {
1182  // If there's already a window open, offset the new window relative to the active window,
1183  // to make it obvious that there are multiple windows.
1184  w->move(activeWindow->pos() + QPoint(defaultWindowXOffset, defaultWindowYOffset));
1185  }
1186 
1187  // Workaround for bug that meant that maximizing or unmaximizing a window required a subsequent click to
1188  // the window before further interaction with it was possible, if the window height was limited by
1189  // available screen real estate. https://b33p.net/kosada/node/6914
1190  w->resize(w->width(), w->height()-yScreenSpaceShortage);
1191 
1192  // Keep the floating node library's node class list synchronized with the currently active composition window.
1193  // Make sure to set this up before the window's show() call.
1194  connect(w, &VuoEditorWindow::windowActivated, this, &VuoEditor::updateFloatingNodeLibraryModules);
1195  connect(w, &VuoEditorWindow::windowDeactivated, this, &VuoEditor::updateFloatingNodeLibraryModules);
1196 
1197  w->show();
1198 
1199 #if VUO_PRO
1200  createEditorWindow_Pro(w);
1201 #endif
1202 
1203  // Update the application UI (Open Recent menu, Window menu, macOS dock context menu)
1204  // whenever an editor window is created, destroyed, or modifies its "Document" list.
1206 
1207  // Update the global node library display mode whenever it is changed for any node library instance.
1208  connect(w->getOwnedNodeLibrary(), &VuoNodeLibrary::changedIsHumanReadable, static_cast<VuoEditor *>(qApp), &VuoEditor::updateNodeLibraryDisplayMode);
1209 
1210  // Keep node library visibility state synchronized among editor windows.
1211  enableGlobalStateConformanceToLibrary(w->getOwnedNodeLibrary());
1212 
1213  try
1214  {
1215  VuoCompilerIssues *issues = new VuoCompilerIssues();
1217  delete issues;
1218  }
1219  catch (const VuoCompilerException &e)
1220  {
1221  QString summary = tr("This composition contains nodes that aren't installed.");
1222 
1223  QString details = tr("<p>If you save the composition while it contains these nodes, some information will be lost.</p>"
1224  "<p>Try searching for these nodes in the <a href=\"%1\">Node Gallery</a>. "
1225  "Or if you don't need them, just delete them from the composition.</p>")
1226  .arg("https://vuo.org/nodes")
1227  + QString::fromStdString(e.getIssues()->getHint(true));
1228 
1229  QString disclosureDetails = QString::fromStdString(e.getIssues()->getShortDescription(false));
1230 
1231  VuoErrorDialog::show(w, summary, details, disclosureDetails);
1232  }
1233 
1234  return w;
1235 }
1236 
1240 void VuoEditor::registerOpenDocument(QMainWindow *window)
1241 {
1242  // Not sure if this is necessary, since menus that display currently-open documents already get updated on QMenu::aboutToShow.
1243  updateUI();
1244  connect(window, &QMainWindow::destroyed, this, &VuoEditor::updateUI);
1245 
1246  // Keep the list of recent documents synchronized among the "File > Open Recent" menu of each editor window.
1248  connect(VuoEditorUtilities::getFileMenuForWindow(window), &QMenu::aboutToShow, this, &VuoEditor::pruneAllOpenRecentFileMenus);
1250 }
1251 
1256 {
1257  if (!compiler)
1258  {
1259  // Wait for the constructor to finish.
1260  dispatch_async(dispatch_get_main_queue(), ^{
1261  // Don't bother opening the standard new untitled composition during startup
1262  // if another composition has opened in the meantime
1263  // (by dropping a composition file on the Vuo.app icon,
1264  // or double-clicking a composition file in Finder after double-clicking Vuo.app,
1265  // or by passing a filename as a command-line argument).
1267  return;
1268 #if VUO_PRO
1269  if (!closeWelcomeWindow())
1270  return;
1271 #endif
1272  QString identifier = assignUntitledDocumentIdentifier();
1273  VUserLog("%s: New empty composition", identifier.toUtf8().data());
1274  createEditorWindow(identifier, "", "", NULL);
1275  });
1276  return;
1277  }
1278 
1279 #if VUO_PRO
1280  if (!closeWelcomeWindow())
1281  return;
1282 #endif
1283 
1284  QString identifier = assignUntitledDocumentIdentifier();
1285  VUserLog("%s: New empty composition", identifier.toUtf8().data());
1286  createEditorWindow(identifier, "", "", NULL);
1287 }
1288 
1293 VuoEditorWindow * VuoEditor::newCompositionWithContent(string content, string compositionDir)
1294 {
1295  QString identifier = assignUntitledDocumentIdentifier();
1296  VUserLog("%s: New Composition with content", identifier.toUtf8().data());
1297  return createEditorWindow(identifier, QString::fromStdString(compositionDir), content, NULL);
1298 }
1299 
1305 {
1306  QAction *sender = (QAction *)QObject::sender();
1307  VuoProtocol *selectedProtocol = static_cast<VuoProtocol *>(sender->data().value<void *>());
1308 
1309  closeUnmodifiedUntitledComposition();
1310 #if VUO_PRO
1311  if (!closeWelcomeWindow())
1312  return;
1313 #endif
1314 
1315  QString identifier = assignUntitledDocumentIdentifier();
1316  VUserLog("%s: New Composition with %s protocol", identifier.toUtf8().data(), selectedProtocol->getName().c_str());
1317  VuoEditorWindow *w = createEditorWindow(identifier, false, selectedProtocol);
1318 
1319  if (selectedProtocol && w && w->getCurrentNodeLibrary())
1320  {
1321  string nodeLibraryFilterText = getFilterTextForTemplate(selectedProtocol->getId());
1322  if (!nodeLibraryFilterText.empty())
1323  {
1324  w->getCurrentNodeLibrary()->searchForText(nodeLibraryFilterText.c_str());
1326  }
1327  }
1328 }
1329 
1335 {
1336  QAction *sender = (QAction *)QObject::sender();
1337  QString selectedTemplate = static_cast<QString>(sender->data().value<QString>());
1338  string templatePath = VuoFileUtilities::getVuoFrameworkPath() + "/Resources/" + selectedTemplate.toUtf8().constData() + ".vuo";
1339 
1340  closeUnmodifiedUntitledComposition();
1341 #if VUO_PRO
1342  if (!closeWelcomeWindow())
1343  return;
1344 #endif
1345 
1346  string compositionAsString = VuoFileUtilities::readFileToString(templatePath);
1347  QString identifier = assignUntitledDocumentIdentifier();
1348  VUserLog("%s: New Composition with %s template", identifier.toUtf8().data(), selectedTemplate.toUtf8().data());
1349  VuoEditorWindow *w = createEditorWindow(identifier, "", compositionAsString, NULL);
1350 
1351  if (w && w->getCurrentNodeLibrary())
1352  {
1353  string nodeLibraryFilterText = getFilterTextForTemplate(selectedTemplate.toUtf8().constData());
1354  if (!nodeLibraryFilterText.empty())
1355  {
1356  w->getCurrentNodeLibrary()->searchForText(nodeLibraryFilterText.c_str());
1358  }
1359  }
1360 }
1361 
1369 string VuoEditor::getFilterTextForTemplate(string templateID)
1370 {
1371  map<string, string> filterTextForTemplate;
1372 
1373  // Protocols
1374  filterTextForTemplate[VuoProtocol::imageFilter] = "image";
1375  filterTextForTemplate[VuoProtocol::imageGenerator] = "image";
1376  filterTextForTemplate[VuoProtocol::imageTransition] = "image";
1377 
1378  // Window templates
1379  filterTextForTemplate["imageTemplate"] = "vuo.image";
1380  filterTextForTemplate["layersTemplate"] = "vuo.layer shape";
1381  filterTextForTemplate["sceneTemplate"] = "vuo.scene shape";
1382 
1383  // Export templates
1384  filterTextForTemplate["movie"] = filterTextForTemplate[VuoProtocol::imageGenerator];
1385  filterTextForTemplate["screensaver"] = filterTextForTemplate[VuoProtocol::imageGenerator];
1386 
1387  filterTextForTemplate["FFGLSource"] = filterTextForTemplate[VuoProtocol::imageGenerator];
1388  filterTextForTemplate["FFGLEffect"] = filterTextForTemplate[VuoProtocol::imageFilter];
1389  filterTextForTemplate["FFGLBlendMode"] = filterTextForTemplate[VuoProtocol::imageTransition];
1390 
1391  filterTextForTemplate["FxPlugGenerator"] = filterTextForTemplate[VuoProtocol::imageGenerator];
1392  filterTextForTemplate["FxPlugEffect"] = filterTextForTemplate[VuoProtocol::imageFilter];
1393  filterTextForTemplate["FxPlugTransition"] = filterTextForTemplate[VuoProtocol::imageTransition];
1394 
1395  map<string, string>::iterator i = filterTextForTemplate.find(templateID);
1396  if (i != filterTextForTemplate.end())
1397  return i->second;
1398  else
1399  return "";
1400 }
1401 
1405 QString VuoEditor::assignUntitledDocumentIdentifier(void)
1406 {
1407  QString uniqueDocumentIdentifier = VuoEditorWindow::untitledComposition;
1408  int documentIdentifierInstanceNum = 1;
1409 
1410  while(documentIdentifierAssigned[uniqueDocumentIdentifier])
1411  {
1412  std::ostringstream oss;
1413  oss << " " << ++documentIdentifierInstanceNum;
1414  uniqueDocumentIdentifier = VuoEditorWindow::untitledComposition + QString::fromStdString(oss.str());
1415  }
1416 
1417  return uniqueDocumentIdentifier;
1418 }
1419 
1424 {
1425  QFileDialog d(NULL, "", "", "Vuo Composition (*.vuo);;Fragment Shader (*.fs)");
1426  d.setFileMode(QFileDialog::ExistingFiles);
1427 
1428  // At Qt 5.2.1, the file dialogue's history() always seems to be empty.
1429  // See https://b33p.net/kosada/node/7302 .
1430  //if (d.history().isEmpty())
1431  // d.setDirectory(getDefaultCompositionStorageDirectory());
1432 
1433  QStringList fileNames;
1434  if (d.exec() == QDialog::Accepted)
1435  fileNames = d.selectedFiles();
1436 
1437  foreach (QString fileName, fileNames)
1438  openFileWithName(fileName);
1439 }
1440 
1444 QMainWindow * VuoEditor::openFileWithName(QString filename, bool addToRecentFileMenu)
1445 {
1446  closeUnmodifiedUntitledComposition();
1447 #if VUO_PRO
1448  if (!closeWelcomeWindow())
1449  {
1450  QString fileURL = QString("file://").append(filename);
1451  queuedCompositionsToOpen.push_back(QString("file://").append(filename));
1452  return NULL;
1453  }
1454 #endif
1455 
1456  QMainWindow *existing = VuoEditorUtilities::existingWindowWithFile(filename);
1457  if (existing)
1458  {
1460  return existing;
1461  }
1462 
1463  if (! VuoFileUtilities::fileIsReadable(filename.toUtf8().constData()))
1464  {
1465  VuoErrorDialog::show(NULL, tr("You do not have permission to open the document \"%1\".").arg(QFileInfo(filename).fileName()), "");
1466  return NULL;
1467  }
1468 
1469  string dir, file, ext;
1470  VuoFileUtilities::splitPath(filename.toStdString(), dir, file, ext);
1471  bool isComposition = VuoFileUtilities::isCompositionExtension(ext);
1472 
1473  QMainWindow *window = nullptr;
1474  if (isComposition)
1475  {
1476  VUserLog("%s.%s: Open", file.c_str(), ext.c_str());
1477  window = createEditorWindow(filename, true);
1478  if (!window)
1479  return nullptr;
1480 
1481  dynamic_cast<VuoEditorWindow *>(window)->setIncludeInRecentFileMenu(addToRecentFileMenu);
1482  }
1483  else
1484  {
1485  try
1486  {
1487  window = new VuoCodeWindow(filename.toStdString());
1488  if (!window)
1489  return nullptr;
1490 
1491  dynamic_cast<VuoCodeWindow *>(window)->setIncludeInRecentFileMenu(addToRecentFileMenu);
1492  }
1493  catch (VuoException &e)
1494  {
1495  VuoErrorDialog::show(NULL, tr("Couldn't open the shader."), e.what());
1496  return nullptr;
1497  }
1498 
1499  window->show();
1500  }
1501 
1502  if (addToRecentFileMenu)
1504 
1505  return window;
1506 }
1507 
1515 void VuoEditor::openExampleComposition(QString filename, VuoNodeSet *nodeSet, string nodeClassToHighlight)
1516 {
1517  VUserLog("%s: Open", filename.toUtf8().data());
1518 
1519  string filenameStr = filename.toStdString();
1520  string workingDir = VuoFileUtilities::getTmpDir();
1521  string compositionContents = nodeSet->getExampleCompositionContents(filenameStr);
1522  QString compositionPath = QString::fromStdString(workingDir + "/" + filenameStr);
1523 
1524  nodeSet->extractExampleCompositionResources(workingDir);
1525  closeUnmodifiedUntitledComposition();
1526 #if VUO_PRO
1527  if (!closeWelcomeWindow())
1528  return;
1529 #endif
1530  createEditorWindow(compositionPath, compositionPath, compositionContents, NULL, nodeClassToHighlight);
1531 
1532  QString exampleCompositionUrl = QString(VuoEditor::vuoExampleCompositionScheme)
1533  .append("://")
1534  .append(nodeSet->getName().c_str())
1535  .append("/")
1536  .append(filename);
1537 
1538  addFileToAllOpenRecentFileMenus(exampleCompositionUrl);
1539 }
1540 
1546 void VuoEditor::closeUnmodifiedUntitledComposition()
1547 {
1549  if (untitled)
1550  untitled->close();
1551 }
1552 
1557 {
1558  // If there is an editor window open and the user has somehow accessed the
1559  // application-global menu anyway, trigger the equivalent menu item of the
1560  // topmost editor window instead.
1562  if (topmostWindow)
1563  topmostWindow->on_showNodeLibrary_triggered();
1564 
1565  // Unhiding the top-level node class library triggers node-library-floating mode globally.
1566  else
1567  {
1568  if ((ownedNodeLibrary->isHidden()))
1569  {
1570  VUserLog("Show node library");
1571 
1572  designateNewFloatingNodeLibrary(ownedNodeLibrary);
1573  updateGlobalNodeLibraryState(true, false);
1574  }
1575 
1576  ownedNodeLibrary->focusTextFilter();
1577  }
1578 }
1579 
1583 void VuoEditor::conformToGlobalNodeLibraryVisibility(VuoNodeLibrary::nodeLibraryState visibility,
1584  VuoNodeLibrary *floater)
1585 {
1586  if ((visibility == VuoNodeLibrary::nodeLibraryHidden) ||
1587  (visibility == VuoNodeLibrary::nodeLibraryDocked))
1588  {
1589  ownedNodeLibrary->releaseDocumentationWidget();
1590  ownedNodeLibrary->setVisible(false);
1591  }
1592 
1593  else if (visibility == VuoNodeLibrary::nodeLibraryFloating)
1594  {
1595  // If our own node library was the one that initiated global
1596  // floating-node-library mode by being undocked, let it float.
1597  // It is now the single application-wide floating library.
1598  if (ownedNodeLibrary == floater)
1599  {
1600  if (! ownedNodeLibrary->isFloating())
1601  ownedNodeLibrary->setFloating(true);
1602 
1603  //ownedNodeLibrary->setVisible(true);
1604  ownedNodeLibrary->prepareAndMakeVisible();
1605  ownedNodeLibrary->setFocus();
1606  ownedNodeLibrary->displayPopoverForCurrentNodeClass();
1607  }
1608  }
1609 
1610  updateUI();
1611 }
1612 
1616 void VuoEditor::updateUI()
1617 {
1618  if (!uiInitialized)
1619  return;
1620 
1621  QList<QMainWindow *> openWindows = VuoEditorUtilities::getOpenEditingWindowsStacked();
1622  QMainWindow *frontWindow = (openWindows.empty() ? nullptr : openWindows.front());
1623 
1624  // Update the list of open documents in the "Window" menu, including the checkmarks for the actions.
1625  menuWindow->clear();
1626  populateWindowMenu(menuWindow, frontWindow);
1627 
1628 #ifdef __APPLE__
1629  // Update the list of open documents in the OS X dock context menu, using the same actions.
1630  dockContextMenu->clear();
1631  for (QMainWindow *openWindow : VuoEditorUtilities::getOpenEditingWindows())
1632  dockContextMenu->addAction(VuoEditorUtilities::getRaiseDocumentActionForWindow(openWindow));
1633 #endif
1634 
1635  // Update the "enabled" status of the canvas transparency menu items.
1636  foreach (QAction *action, canvasTransparencyOptions->actions())
1637  action->setEnabled(!action->isChecked());
1638 }
1639 
1643 bool VuoEditor::event(QEvent * e)
1644 {
1645  switch (e->type())
1646  {
1647  case QEvent::FileOpen:
1648  openUrl(static_cast<QFileOpenEvent *>(e)->url().toString());
1649  return true;
1650 
1651  case QEvent::ApplicationActivate:
1652  emit activeApplicationStateChanged(true);
1653  return true;
1654 
1655  case QEvent::ApplicationDeactivate:
1656  emit activeApplicationStateChanged(false);
1657  return true;
1658 
1659  // macOS Dock context menu > Quit
1660  case QEvent::Close:
1661  e->ignore();
1662  quitCleanly();
1663  return true;
1664 
1665  default:
1666  return QApplication::event(e);
1667  }
1668 }
1669 
1675 {
1676  QAction *sender = (QAction *)QObject::sender();
1677  QDesktopServices::openUrl(sender->data().toUrl());
1678 }
1679 
1685 {
1686  QAction *sender = (QAction *)QObject::sender();
1687  QString filePath = sender->data().value<QString>();
1688 
1689  if (! filePath.startsWith("file://"))
1690  filePath = QString("file://").append(filePath);
1691 
1692  openUrl(filePath);
1693 }
1694 
1700 {
1701  QAction *sender = (QAction *)QObject::sender();
1702  QString filePath = sender->data().value<QString>();
1703  moveFileToTrash(filePath);
1704 }
1705 
1709 void VuoEditor::moveFileToTrash(QString filePath)
1710 {
1711  try
1712  {
1713  VuoFileUtilities::moveFileToTrash(filePath.toUtf8().constData());
1714  }
1715  catch (const VuoException &e)
1716  {
1717  if (VuoFileUtilities::fileExists(filePath.toUtf8().constData()))
1718  VuoErrorDialog::show(NULL, VuoEditor::tr("Couldn't move file to the Trash"), e.what());
1719  }
1720 }
1721 
1728 void VuoEditor::openUrl(const QString &url)
1729 {
1730  QUrl fileUrl(url);
1731 
1732  if (fileUrl.isValid() && fileUrl.scheme() == VuoEditor::vuoExampleCompositionScheme)
1733  {
1734  if (!uiInitialized)
1735  queuedCompositionsToOpen.push_back(url);
1736  else
1738  }
1739  else
1740  {
1741  string dir, file, ext;
1742  VuoFileUtilities::splitPath(fileUrl.toLocalFile().toUtf8().constData(), dir, file, ext);
1743 
1744  if (QString(ext.c_str()) == VuoEditor::vuoNodeClassFileExtension)
1745  {
1746  string installedNodeDir = VuoFileUtilities::getUserModulesPath();
1747  string sourceNodePath = dir + "/" + file + "." + ext;
1748  string targetNodePath = installedNodeDir + "/" + file + "." + ext;
1749 
1750  VuoFileUtilities::makeDir(installedNodeDir);
1751 
1752  if (VuoFileUtilities::fileExists(targetNodePath))
1753  {
1754  QMessageBox messageBox;
1755  string errorSummary = "A node named \"" + file + "\" already exists. Do you want to replace it with the one you're installing?";
1756 
1757  // On OS X, this combination of flags displays the minimize, maximimize, and close buttons, but all in a disabled state.
1758  messageBox.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowMaximizeButtonHint);
1759 
1760  messageBox.setTextFormat(Qt::RichText);
1761  messageBox.setStandardButtons(QMessageBox::Discard | QMessageBox::Cancel);
1762  messageBox.setButtonText(QMessageBox::Discard, tr("Replace"));
1763  messageBox.setButtonText(QMessageBox::Cancel, tr("Cancel"));
1764  messageBox.setDefaultButton(QMessageBox::Discard);
1765  messageBox.setText(errorSummary.c_str());
1766  messageBox.setStyleSheet("#qt_msgbox_informativelabel, QMessageBoxDetailsText { font-weight: normal; font-size: 11pt; }");
1767 
1768  // Give the "Cancel" button keyboard focus (without "Default" status) so that it can be activated by spacebar.
1769  static_cast<QPushButton *>(messageBox.button(QMessageBox::Cancel))->setAutoDefault(false);
1770  messageBox.button(QMessageBox::Cancel)->setFocus();
1771 
1772  messageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
1773 
1774  if (messageBox.exec() == QMessageBox::Discard)
1775  {
1776  // Move pre-existing module to the trash.
1777  try
1778  {
1779  VuoFileUtilities::moveFileToTrash(targetNodePath);
1780  }
1781  catch (VuoException &e)
1782  {
1783  if (VuoFileUtilities::fileExists(targetNodePath))
1784  {
1785  VuoErrorDialog::show(NULL, tr("There was a problem installing the node."), e.what());
1786  return;
1787  }
1788  }
1789 
1790  }
1791  else
1792  return;
1793  }
1794 
1795  // Install the node class in the "User Modules" directory.
1796  try
1797  {
1798  VUserLog("Install node %s.%s", file.c_str(), ext.c_str());
1799 
1800  VuoFileUtilities::copyFile(sourceNodePath, targetNodePath);
1801  }
1802  catch (VuoException &e)
1803  {
1804  VuoErrorDialog::show(NULL, tr("There was a problem installing the node."), e.what());
1805  return;
1806  }
1807 
1808  if (VuoFileUtilities::fileExists(targetNodePath))
1809  {
1810  QMessageBox installationSuccessMessageBox;
1811  installationSuccessMessageBox.setText(tr("Your node has been installed!"));
1812  installationSuccessMessageBox.setIconPixmap(VuoEditorUtilities::vuoLogoForDialogs());
1813  installationSuccessMessageBox.exec();
1814  }
1815  }
1816 
1818  {
1819  if (!uiInitialized)
1820  queuedCompositionsToOpen.push_back(url);
1821  else
1822  openFileWithName(fileUrl.toLocalFile());
1823  }
1824 
1825  else
1826  QDesktopServices::openUrl(url);
1827  }
1828 }
1829 
1834 {
1835  shaderDocumentationVisible = isVisible;
1836  settings->setValue(shaderDocumentationVisibilitySettingsKey, shaderDocumentationVisible);
1837 }
1838 
1843 {
1844  return shaderDocumentationVisible;
1845 }
1846 
1850 void VuoEditor::applyStoredMovieExportSettings()
1851 {
1852  movieExportWidth = (settings->contains(movieExportWidthSettingsKey)? settings->value(movieExportWidthSettingsKey).toInt() : 1024);
1853  movieExportHeight = (settings->contains(movieExportHeightSettingsKey)? settings->value(movieExportHeightSettingsKey).toInt() : 768);
1854  movieExportTime = (settings->contains(movieExportTimeSettingsKey)? settings->value(movieExportTimeSettingsKey).toDouble() : 0.);
1855  movieExportDuration = (settings->contains(movieExportDurationSettingsKey)? settings->value(movieExportDurationSettingsKey).toDouble() : 10.);
1856  movieExportFramerate = (settings->contains(movieExportFramerateSettingsKey)? settings->value(movieExportFramerateSettingsKey).toDouble() : 30.);
1857  movieExportSpatialSupersample = (settings->contains(movieExportSpatialSupersampleSettingsKey)? settings->value(movieExportSpatialSupersampleSettingsKey).toInt() : 1.);
1858  movieExportTemporalSupersample = (settings->contains(movieExportTemporalSupersampleSettingsKey)? settings->value(movieExportTemporalSupersampleSettingsKey).toInt() : 1.);
1859  movieExportShutterAngle = (settings->contains(movieExportShutterAngleSettingsKey)? static_cast<float>(settings->value(movieExportShutterAngleSettingsKey).toDouble()) : 360.);
1860  movieExportImageFormat = (settings->contains(movieExportImageFormatSettingsKey)? settings->value(movieExportImageFormatSettingsKey).toString() : "H.264");
1861  movieExportQuality = (settings->contains(movieExportQualitySettingsKey)? settings->value(movieExportQualitySettingsKey).toDouble() : 1.);
1862 }
1863 
1867 void VuoEditor::updateGlobalMovieExportSettings(int width,
1868  int height,
1869  double time,
1870  double duration,
1871  double framerate,
1872  int spatialSupersample,
1873  int temporalSupersample,
1874  float shutterAngle,
1875  QString imageFormat,
1876  double quality)
1877 {
1878  if (width != movieExportWidth)
1879  {
1880  movieExportWidth = width;
1881  settings->setValue(movieExportWidthSettingsKey, movieExportWidth);
1882  }
1883 
1884  if (height != movieExportHeight)
1885  {
1886  movieExportHeight = height;
1887  settings->setValue(movieExportHeightSettingsKey, movieExportHeight);
1888  }
1889 
1890  if (time != movieExportTime)
1891  {
1892  movieExportTime = time;
1893  settings->setValue(movieExportTimeSettingsKey, movieExportTime);
1894  }
1895 
1896  if (duration != movieExportDuration)
1897  {
1898  movieExportDuration = duration;
1899  settings->setValue(movieExportDurationSettingsKey, movieExportDuration);
1900  }
1901 
1902  if (framerate != movieExportFramerate)
1903  {
1904  movieExportFramerate = framerate;
1905  settings->setValue(movieExportFramerateSettingsKey, movieExportFramerate);
1906  }
1907 
1908  if (spatialSupersample != movieExportSpatialSupersample)
1909  {
1910  movieExportSpatialSupersample = spatialSupersample;
1911  settings->setValue(movieExportSpatialSupersampleSettingsKey, movieExportSpatialSupersample);
1912  }
1913 
1914  if (temporalSupersample != movieExportTemporalSupersample)
1915  {
1916  movieExportTemporalSupersample = temporalSupersample;
1917  settings->setValue(movieExportTemporalSupersampleSettingsKey, movieExportTemporalSupersample);
1918  }
1919 
1920  if (shutterAngle != movieExportShutterAngle)
1921  {
1922  movieExportShutterAngle = shutterAngle;
1923  settings->setValue(movieExportShutterAngleSettingsKey, static_cast<double>(movieExportShutterAngle));
1924  }
1925 
1926  if (imageFormat != movieExportImageFormat)
1927  {
1928  movieExportImageFormat = imageFormat;
1929  settings->setValue(movieExportImageFormatSettingsKey, movieExportImageFormat);
1930  }
1931 
1932  if (quality != movieExportQuality)
1933  {
1934  movieExportQuality = quality;
1935  settings->setValue(movieExportQualitySettingsKey, movieExportQuality);
1936  }
1937 }
1938 
1943  int &height,
1944  double &time,
1945  double &duration,
1946  double &framerate,
1947  int &spatialSupersample,
1948  int &temporalSupersample,
1949  float &shutterAngle,
1950  QString &imageFormat,
1951  double &quality)
1952 {
1953  width = movieExportWidth;
1954  height = movieExportHeight;
1955  time = movieExportTime;
1956  duration = movieExportDuration;
1957  framerate = movieExportFramerate;
1958  spatialSupersample = movieExportSpatialSupersample;
1959  temporalSupersample = movieExportTemporalSupersample;
1960  shutterAngle = movieExportShutterAngle;
1961  imageFormat = movieExportImageFormat;
1962  quality = movieExportQuality;
1963 }
1964 
1965 
1970 VuoNodeLibrary::nodeLibraryState VuoEditor::getGlobalNodeLibraryStateForAttributes(bool visible, bool docked)
1971 {
1972  return ((! visible)? VuoNodeLibrary::nodeLibraryHidden :
1973  (docked? VuoNodeLibrary::nodeLibraryDocked :
1974  VuoNodeLibrary::nodeLibraryFloating));
1975 }
1976 
1982 void VuoEditor::updateNodeLibraryDisplayMode(bool humanReadable)
1983 {
1984  nodeLibraryDisplayMode = (humanReadable? VuoNodeLibrary::displayByName : VuoNodeLibrary::displayByClass);
1985  settings->setValue(nodeLibraryDisplayModeSettingsKey, nodeLibraryDisplayMode);
1986 
1987  // Exception to the non-propagation policy for this setting:
1988  // If the top-level node library is not currently visible, do synchronize its display mode so that
1989  // it is up-to-date if/when it next appears.
1990  if ((ownedNodeLibrary->isHidden()) && (ownedNodeLibrary->getHumanReadable() != humanReadable))
1991  {
1992  ownedNodeLibrary->setHumanReadable(humanReadable);
1993  ownedNodeLibrary->updateUI();
1994  updateUI();
1995  }
1996 }
1997 
2001 void VuoEditor::updateGlobalNodeLibraryFloatingPosition(QPoint newPos)
2002 {
2003  nodeLibraryFloatingPosition = newPos;
2004  settings->setValue(nodeLibraryFloatingPositionSettingsKey, nodeLibraryFloatingPosition);
2005 }
2006 
2010 void VuoEditor::updateGlobalNodeLibraryWidth(int newWidth)
2011 {
2012  nodeLibraryWidth = newWidth;
2013  settings->setValue(nodeLibraryWidthSettingsKey, nodeLibraryWidth);
2014 }
2015 
2019 void VuoEditor::updateGlobalNodeLibraryHeight(int newHeight)
2020 {
2021  nodeLibraryHeight = newHeight;
2022  settings->setValue(nodeLibraryHeightSettingsKey, nodeLibraryHeight);
2023 }
2024 
2028 void VuoEditor::updateGlobalNodeDocumentationPanelHeight(int newSize)
2029 {
2030  nodeDocumentationPanelHeight = newSize;
2031  settings->setValue(nodeDocumentationPanelHeightSettingsKey, nodeDocumentationPanelHeight);
2032 }
2033 
2037 void VuoEditor::updateGlobalNodeLibraryVisibilityState(bool visible)
2038 {
2039  VUserLog("%s node library", visible ? "Show" : "Hide");
2040 
2041  // If transitioning to a visible state, revert to the docking state from the last time the node library was visible.
2042  bool updatedDockingState = ((visible && (! nodeLibraryCurrentlyVisible))? previousVisibleNodeLibraryStateWasDocked : nodeLibraryCurrentlyDocked);
2043  updateGlobalNodeLibraryState(visible, updatedDockingState);
2044 }
2045 
2049 void VuoEditor::updateGlobalNodeLibraryDockedState(bool floating)
2050 {
2051  VUserLog("%s node library", floating ? "Detach" : "Attach");
2052 
2053  VuoNodeLibrary *sender = (VuoNodeLibrary *)QObject::sender();
2054  VuoNodeLibrary *floater = (floating? sender : NULL);
2055  designateNewFloatingNodeLibrary(floater);
2056  updateGlobalNodeLibraryState(this->nodeLibraryCurrentlyVisible, (! floating));
2057 
2058  if (floating)
2059  updateFloatingNodeLibraryModules();
2060  else
2061  updateDockedNodeLibraryModules();
2062 }
2063 
2069 void VuoEditor::updateGlobalNodeLibraryState(bool visible, bool docked)
2070 {
2071  VuoNodeLibrary::nodeLibraryState currentNodeLibraryState = getGlobalNodeLibraryStateForAttributes(nodeLibraryCurrentlyVisible, nodeLibraryCurrentlyDocked);
2072  VuoNodeLibrary::nodeLibraryState updatedNodeLibraryState = getGlobalNodeLibraryStateForAttributes(visible, docked);
2073 
2074  if (currentNodeLibraryState != VuoNodeLibrary::nodeLibraryHidden)
2075  previousVisibleNodeLibraryStateWasDocked = nodeLibraryCurrentlyDocked;
2076 
2077  nodeLibraryCurrentlyVisible = visible;
2078  nodeLibraryCurrentlyDocked = docked;
2079  if (docked)
2080  currentFloatingNodeLibrary = NULL;
2081 
2082  settings->setValue(nodeLibraryVisibilityStateSettingsKey, nodeLibraryCurrentlyVisible);
2083  settings->setValue(nodeLibraryDockingStateSettingsKey, nodeLibraryCurrentlyDocked);
2084 
2085  emit globalNodeLibraryStateChanged(updatedNodeLibraryState, currentFloatingNodeLibrary, false);
2086 }
2087 
2092 void VuoEditor::updateFloatingNodeLibraryModules()
2093 {
2094  if (!currentFloatingNodeLibrary)
2095  return;
2096 
2098  if (activeWindow)
2099  {
2100  VuoModuleManager *activeCompositionModuleManager = activeWindow->getComposition()->getModuleManager();
2101  if (activeCompositionModuleManager->getNodeLibrary() != currentFloatingNodeLibrary)
2102  {
2103  // Tell other open editor windows to stop sending updates to the floating node library, if they were doing so.
2104  QList<VuoEditorWindow *> openWindows = VuoEditorUtilities::getOpenCompositionEditingWindows();
2105  foreach (VuoEditorWindow *window, openWindows)
2106  {
2107  if (window != activeWindow)
2108  {
2109  VuoModuleManager *windowModuleManager = window->getComposition()->getModuleManager();
2110  if (windowModuleManager->getNodeLibrary() == currentFloatingNodeLibrary)
2111  windowModuleManager->setNodeLibrary(NULL);
2112  }
2113  }
2114 
2115  // Tell the top-level app to stop sending updates to the floating node library, if it was doing so.
2116  if (moduleManager->getNodeLibrary() == currentFloatingNodeLibrary)
2117  moduleManager->setNodeLibrary(NULL);
2118  }
2119  }
2120 
2121  VuoModuleManager *updatedModuleManager = (activeWindow? activeWindow->getComposition()->getModuleManager() : this->moduleManager);
2122  if (updatedModuleManager->getNodeLibrary() != currentFloatingNodeLibrary)
2123  {
2124  // Document the node library's current state (selected and documented items, filter text) before resetting it.
2125  QString origFilterText;
2126  set<string> origSelectedNodeClasses;
2127  string origDocumentedNodeClass;
2128  currentFloatingNodeLibrary->getState(origFilterText, origSelectedNodeClasses, origDocumentedNodeClass);
2129 
2130  // Populate the node library with its new content.
2131  currentFloatingNodeLibrary->clearNodeClassList();
2132  updatedModuleManager->setNodeLibrary(currentFloatingNodeLibrary);
2133  updatedModuleManager->updateWithAlreadyLoadedModules();
2134 
2135  // Now restore the node library's original state.
2136  currentFloatingNodeLibrary->setState(origFilterText, origSelectedNodeClasses, origDocumentedNodeClass);
2137 
2138  // If the documentation pane displayed previously is no longer relevant, display
2139  // whatever documentation is most appropriate now.
2140  QString newFilterText;
2141  set<string> newSelectedNodeClasses;
2142  string newDocumentedNodeClass;
2143  currentFloatingNodeLibrary->getState(newFilterText, newSelectedNodeClasses, newDocumentedNodeClass);
2144  if (newDocumentedNodeClass.empty())
2145  {
2146  if (activeWindow)
2147  activeWindow->displayAppropriateDocumentation();
2148  else
2149  currentFloatingNodeLibrary->displayPopoverForCurrentNodeClass();
2150  }
2151  }
2152 }
2153 
2159 {
2160  if (moduleManager->getNodeLibrary())
2161  {
2162  VuoModuleManager::CallbackType subcompositionCreated = ^void (void) {
2163  moduleManager->getNodeLibrary()->highlightNodeClass(nodeClassName);
2164  };
2165  moduleManager->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionCreated);
2166  }
2167 
2168  QList<VuoEditorWindow *> openWindows = VuoEditorUtilities::getOpenCompositionEditingWindows();
2169  foreach (VuoEditorWindow *window, openWindows)
2170  {
2171  VuoModuleManager *windowModuleManager = window->getComposition()->getModuleManager();
2172  if (windowModuleManager->getNodeLibrary())
2173  {
2174  VuoModuleManager::CallbackType subcompositionCreated = ^void (void) {
2175  windowModuleManager->getNodeLibrary()->highlightNodeClass(nodeClassName);
2176  };
2177  windowModuleManager->doNextTimeNodeClassIsLoaded(nodeClassName, subcompositionCreated);
2178  }
2179  }
2180 }
2181 
2186 void VuoEditor::updateDockedNodeLibraryModules()
2187 {
2188  if (currentFloatingNodeLibrary)
2189  return;
2190 
2191  // Tell each open editor window to send updates to the node library that it owns and not to any others.
2192  QList<VuoEditorWindow *> openWindows = VuoEditorUtilities::getOpenCompositionEditingWindows();
2193  foreach (VuoEditorWindow *window, openWindows)
2194  {
2195  VuoModuleManager *windowModuleManager = window->getComposition()->getModuleManager();
2196  VuoNodeLibrary *windowOwnedNodeLibrary = window->getOwnedNodeLibrary();
2197 
2198  if (windowModuleManager->getNodeLibrary() != windowOwnedNodeLibrary)
2199  {
2200  windowOwnedNodeLibrary->clearNodeClassList();
2201  windowModuleManager->setNodeLibrary(windowOwnedNodeLibrary);
2202  windowModuleManager->updateWithAlreadyLoadedModules();
2203  }
2204  }
2205 
2206  // Tell the top-level app to send updates to the node library that it owns and not to any others.
2207  if (moduleManager->getNodeLibrary() != ownedNodeLibrary)
2208  {
2209  ownedNodeLibrary->clearNodeClassList();
2210  moduleManager->setNodeLibrary(ownedNodeLibrary);
2211  moduleManager->updateWithAlreadyLoadedModules();
2212  }
2213 }
2214 
2219 void VuoEditor::designateNewFloatingNodeLibrary(VuoNodeLibrary *library)
2220 {
2221  if (currentFloatingNodeLibrary && (currentFloatingNodeLibrary != ownedNodeLibrary))
2222  disconnect(currentFloatingNodeLibrary, &VuoNodeLibrary::aboutToBeDestroyed, this, &VuoEditor::assignTopLevelLibraryAsReplacementFloater);
2223 
2224  if (library && (library != ownedNodeLibrary))
2225  connect(library, &VuoNodeLibrary::aboutToBeDestroyed, this, &VuoEditor::assignTopLevelLibraryAsReplacementFloater);
2226 
2227  currentFloatingNodeLibrary = library;
2228 }
2229 
2234 void VuoEditor::assignTopLevelLibraryAsReplacementFloater()
2235 {
2236  currentFloatingNodeLibrary = ownedNodeLibrary;
2237 
2238  emit globalNodeLibraryStateChanged(getGlobalNodeLibraryStateForAttributes(nodeLibraryCurrentlyVisible, nodeLibraryCurrentlyDocked),
2239  currentFloatingNodeLibrary, true);
2240 }
2241 
2248 {
2249  menuOpenRecent->addFile(filePath);
2251 }
2252 
2258 {
2259  this->closedFiles.push_back(filePath.toUtf8().constData());
2260 }
2261 
2268 {
2269  menuOpenRecent->clearRecentFileListActionTriggered();
2271 }
2272 
2279 {
2280  menuOpenRecent->pruneNonexistentFiles();
2282 }
2283 
2289 {
2290  for (QMainWindow *openWindow : VuoEditorUtilities::getOpenEditingWindows())
2292 
2293  settings->setValue(recentFileListSettingsKey, menuOpenRecent->getRecentFiles());
2294 }
2295 
2301 {
2302  QString bundledManual = QDir::cleanPath(QApplication::applicationDirPath().append("/../Resources"))
2303  .append(QDir::separator())
2304  .append("vuo-")
2305  .append(VUO_VERSION_AND_BUILD_STRING)
2306  .append("--manual.pdf");
2307 
2308  if (QFile(bundledManual).exists())
2309  return QString("file://").append(bundledManual);
2310  else
2311  return "https://vuo.org/manual.pdf";
2312 }
2313 
2318 {
2319  return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
2320 }
2321 
2327 void VuoEditor::generateAllNodeSetHtmlDocumentation(string saveDirectory)
2328 {
2329  const bool publishInternalVuoLinks = true;
2330  VuoFileUtilities::makeDir(saveDirectory);
2331 
2332 
2333  QString indexFilename(QString::fromStdString(saveDirectory) + "/index.html");
2334  QFile indexFile(indexFilename);
2335  if (!indexFile.open(QFile::WriteOnly | QFile::Truncate))
2336  throw VuoException(("Couldn't open " + indexFilename).toUtf8().data());
2337 
2338  QTextStream indexWriter(&indexFile);
2339  indexWriter << VUO_QSTRINGIFY(
2340  <html>
2341  <head>
2342  <title>Vuo %1 node documentation</title>
2343  <style>
2344  * { font-size: 13pt; font-family: 'PT Sans',Avenir,'Trebuchet MS',Tahoma,sans-serif; }
2345  body { padding: 5em; }
2346  h1 { font-size: 24pt; color: #aaa; font-weight: normal; }
2347  a, a:visited { font-weight: bold; color: #69a; }
2348  p { margin-top: 0; color: #aaa; }
2349  p a, p a:visited { font-weight: normal; color: #aaa; }
2350  li { list-style: none; }
2351  </style>
2352  </head>
2353  <body>
2354  <h1>Vuo %1 node documentation</h1>
2355  <ul>
2356  )
2357  .arg(VUO_VERSION_STRING);
2358 
2359  map<string, VuoCompilerNodeClass *> nodeClassesMap = compiler->getNodeClasses();
2360  vector<VuoCompilerNodeClass *> nodeClasses;
2361  for (map<string, VuoCompilerNodeClass *>::iterator i = nodeClassesMap.begin(); i != nodeClassesMap.end(); ++i)
2362  nodeClasses.push_back(i->second);
2364 
2365  set<string> nodeSetNames;
2366  foreach (VuoCompilerNodeClass *nodeClass, nodeClasses)
2367  if (nodeClass->getBase()->getNodeSet())
2368  nodeSetNames.insert(nodeClass->getBase()->getNodeSet()->getName());
2369 
2370  foreach (string nodeSetName, nodeSetNames)
2371  {
2372  string nodeSetSaveDirectory = saveDirectory + "/" + nodeSetName;
2373  VuoFileUtilities::makeDir(nodeSetSaveDirectory);
2374 
2375  string saveNodeSetHtml = nodeSetSaveDirectory + "/index.html";
2376  VuoNodeSet *nodeSet = compiler->getNodeSetForName(nodeSetName);
2377 
2378  generateMainHtmlPageForNodeSet(nodeSet, saveNodeSetHtml, publishInternalVuoLinks);
2379  nodeSet->extractDocumentationResources(nodeSetSaveDirectory);
2380  generateNodeClassHtmlPagesForNodeSet(nodeSet, nodeSetSaveDirectory, publishInternalVuoLinks);
2381 
2382  string description = nodeSet->getDescription();
2383  string firstLineOfDescription = description.substr(0, description.find('\n') - 1);
2384  string filteredDescription = VuoStringUtilities::generateHtmlFromMarkdownLine(VuoRendererCommon::externalizeVuoNodeLinks(compiler, QString::fromStdString(firstLineOfDescription), false).toStdString());
2385  indexWriter << VUO_QSTRINGIFY(
2386  <li><a href="%1/">%2</a><p>%3</p></li>
2387  )
2388  .arg(QString::fromStdString(nodeSetName))
2389  .arg(VuoEditorComposition::formatNodeSetNameForDisplay(QString::fromStdString(nodeSetName)))
2390  .arg(QString::fromStdString(filteredDescription));
2391  }
2392 
2393  indexWriter << VUO_QSTRINGIFY(
2394  </ul>
2395  </body>
2396  </html>
2397  );
2398 }
2399 
2408 {
2409  const bool publishInternalVuoLinks = false;
2410 
2411  string nodeSetName = url.host().toUtf8().constData();
2412  VuoNodeSet *nodeSet = compiler->getNodeSetForName(nodeSetName);
2413 
2414  if (!nodeSet)
2415  return;
2416 
2417  // If the node set resource directory did not already exist, create and populate it now.
2418  string preexistingResourceDir = getResourceDirectoryForNodeSet(nodeSetName);
2419  string tmpSaveDir = (!preexistingResourceDir.empty()? preexistingResourceDir : VuoFileUtilities::makeTmpDir(nodeSetName));
2420  string tmpSaveNodeSetHtml = tmpSaveDir + "/index.html";
2421 
2422  if (tmpSaveDir != preexistingResourceDir)
2423  {
2424  generateMainHtmlPageForNodeSet(nodeSet, tmpSaveNodeSetHtml, publishInternalVuoLinks);
2425 
2426  // Extract resources referenced by the documentation.
2427  nodeSet->extractDocumentationResources(tmpSaveDir);
2428  }
2429 
2430  // Open the node set html file using an external browser.
2431  if (QDesktopServices::openUrl(QString("file://").append(tmpSaveNodeSetHtml.c_str())))
2432  emit activeApplicationStateChanged(false);
2433 
2434  // Save documentation for each member node class to its own html file.
2435  if (tmpSaveDir != preexistingResourceDir)
2436  {
2437  generateNodeClassHtmlPagesForNodeSet(nodeSet, tmpSaveDir, publishInternalVuoLinks);
2438  setResourceDirectoryForNodeSet(nodeSet->getName().c_str(), tmpSaveDir);
2439  }
2440 }
2441 
2450 {
2451  string nodeClassName = url.host().toUtf8().constData();
2454  NULL);
2455 
2456  if (topmostNodeLibrary)
2457  topmostNodeLibrary->prepareAndDisplayNodePopoverForClass(nodeClassName);
2458 }
2459 
2466 QString VuoEditor::generateHtmlDocumentationStyles(bool forBrowser, bool isDark)
2467 {
2468  return VUO_QSTRINGIFY(<style>
2469  table, th, td {
2470  border: 1px solid #ccc;
2471  border-collapse: collapse;
2472  padding: %1;
2473  }
2474  code, kbd, pre {
2475  font-family: 'Monaco';
2476  font-size: 12px;
2477  background-color: %2;
2478  padding: 0 0.4em;
2479  }
2480  pre {
2481  padding: 1em;
2482  white-space: pre-wrap;
2483  }
2484  </style>)
2485  .arg(forBrowser ? "0.4em" : "0")
2486  .arg(isDark ? "#383838" : "#ececec");
2487 }
2488 
2493 void VuoEditor::generateMainHtmlPageForNodeSet(VuoNodeSet *nodeSet, string saveFileName, bool publishInternalVuoLinks)
2494 {
2495  string nodeSetName = nodeSet->getName();
2496  vector<string> nodeSetClassNames = nodeSet->getNodeClassNames();
2497 
2498  // Metadata and node set description
2499  //: Appears in the webpage title on node set documentation pages.
2500  QString htmlHeader = "<html><head><meta charset=\"utf-8\"><title>" + tr("Vuo Node Set Documentation") + ": "
2501  + QString::fromStdString(nodeSetName)
2502  + "</title>"
2504  + "</head><body>";
2505 
2506  QString nodeSetDisplayName = VuoEditorComposition::formatNodeSetNameForDisplay(QString::fromStdString(nodeSetName));
2507  QString title = QString("<h2>").append(nodeSetDisplayName);
2508  if (nodeSetDisplayName != nodeSetName.c_str())
2509  title.append(" (").append(nodeSetName.c_str()).append(")");
2510  title.append("</h2>");
2511 
2512  string nodeSetDocumentationContent = nodeSet->getDescription();
2513  QString filteredNodeSetDocumentationContent = VuoStringUtilities::generateHtmlFromMarkdown(publishInternalVuoLinks?
2514  VuoRendererCommon::externalizeVuoNodeLinks(compiler, QString::fromStdString(nodeSetDocumentationContent), false).toStdString() :
2515  removeVuoLinks(nodeSetDocumentationContent)).c_str();
2516 
2517 
2518  QString htmlFooter = "</body></html>";
2519 
2520  // Example compositions
2521  vector<string> nodeSetExampleCompositionFileNames = nodeSet->getExampleCompositionFileNames();
2522  QString nodeSetExampleCompositionText = "";
2523 
2524  foreach (string compositionFileName, nodeSetExampleCompositionFileNames)
2525  {
2526  string compositionAsString = nodeSet->getExampleCompositionContents(compositionFileName);
2527  VuoCompositionMetadata metadata(compositionAsString);
2528 
2529  string name = metadata.getCustomizedName();
2530  if (name.empty())
2531  name = VuoEditorComposition::formatCompositionFileNameForDisplay(compositionFileName.c_str()).toUtf8().constData();
2532 
2533  string description = metadata.getDescription();
2534  string filteredDescription = (publishInternalVuoLinks? VuoRendererCommon::externalizeVuoNodeLinks(compiler, QString::fromStdString(description), false).toStdString() : removeVuoLinks(description));
2535  QString compositionDescription = VuoStringUtilities::generateHtmlFromMarkdownLine(filteredDescription).c_str();
2536 
2537  nodeSetExampleCompositionText.append("<li>")
2538  .append("<a href=\"")
2540  .append("://")
2541  .append(nodeSetName.c_str())
2542  .append("/")
2543  .append(compositionFileName.c_str())
2544  .append("\"><font size=+1>")
2545  .append(name.c_str())
2546  .append("</font></a>");
2547 
2548  if (!compositionDescription.isEmpty())
2549  nodeSetExampleCompositionText.append(": ").append(compositionDescription);
2550 
2551  nodeSetExampleCompositionText.append("</li>\n");
2552  }
2553 
2554  if (nodeSetExampleCompositionText.size() > 0)
2555  //: Appears on node set documentation webpages.
2556  nodeSetExampleCompositionText = "<BR><HR><h3>" + tr("Example composition(s)", "", nodeSetExampleCompositionFileNames.size()) + ":</h3>\n<ul>\n" + nodeSetExampleCompositionText + "</ul>";
2557 
2558  // Node classes
2559  QString nodeSetClassesText = "";
2560 
2561  // Sort by default node title.
2562  std::sort(nodeSetClassNames.begin(), nodeSetClassNames.end(), [=](const string nodeClassName1, const string nodeClassName2) {
2563  VuoCompilerNodeClass *nodeClass1 = compiler->getNodeClass(nodeClassName1);
2564  VuoCompilerNodeClass *nodeClass2 = compiler->getNodeClass(nodeClassName2);
2565  string nodeClass1Title = nodeClass1? nodeClass1->getBase()->getDefaultTitle() : "";
2566  string nodeClass2Title = nodeClass2? nodeClass2->getBase()->getDefaultTitle() : "";
2567 
2568  return nodeClass1Title < nodeClass2Title;
2569  });
2570 
2571  foreach (string nodeClassName, nodeSetClassNames)
2572  {
2573  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName);
2574  if (!nodeClass || nodeClass->getBase()->getDeprecated())
2575  continue;
2576 
2577  QString nodeClassTitle = nodeClass->getBase()->getDefaultTitle().c_str();
2578  QString nodeClassDescription = VuoStringUtilities::generateHtmlFromMarkdown(nodeClass->getBase()->getDescription()).c_str();
2579  QString nodeClassProNodeIndicator;
2580 #if VUO_PRO
2581  if (nodeClass->getBase()->isPro())
2582  //: Appears on node set documentation webpages.
2583  nodeClassProNodeIndicator = " <b>[<a href=\"https://vuo.org/pro-nodes\">" + tr("Pro node") + "</a>]</b>";
2584 #endif
2585 
2586  QString nodeClassDocumentationLink = QString((nodeClassName + ".html").c_str());
2587 
2588  // Strip HTML and extract the first sentence of the node class description for display.
2589  nodeClassDescription.remove(QRegExp("<[^>]*>"));
2590  nodeClassDescription.replace(QRegExp("\\.\\s.*"), ".");
2591 
2592  nodeSetClassesText.append("<li>")
2593  .append("<a href=\"")
2594  .append(nodeClassDocumentationLink)
2595  .append("\">")
2596  .append("<font size=+1>")
2597  .append(nodeClassTitle)
2598  .append("</font>")
2599  .append("</a>")
2600  .append(" (")
2601  .append(nodeClassName.c_str())
2602  .append(")");
2603 
2604  nodeSetClassesText.append(nodeClassProNodeIndicator);
2605 
2606  if (!nodeClassDescription.isEmpty())
2607  nodeSetClassesText.append(": ").append(nodeClassDescription);
2608 
2609  nodeSetClassesText.append("</li>\n");
2610  }
2611 
2612  if (nodeSetClassesText.size() > 0)
2613  //: Appears on node set documentation webpages.
2614  nodeSetClassesText = "<BR><HR><h3>" + tr("Node(s)", "", nodeSetClassNames.size()) + ":</h3>\n<ul>\n" + nodeSetClassesText + "</ul>";
2615 
2616  // Save the node set documentation to an html file.
2617  ofstream savedNodeSetFile(saveFileName.c_str(), ios::trunc);
2618 
2619  savedNodeSetFile << htmlHeader.append("\n\n").toUtf8().constData();
2620  savedNodeSetFile << title.append("\n\n").toUtf8().constData();
2621  savedNodeSetFile << filteredNodeSetDocumentationContent.append("\n\n").toUtf8().constData();
2622  savedNodeSetFile << nodeSetExampleCompositionText.append("\n\n").toUtf8().constData();
2623  savedNodeSetFile << nodeSetClassesText.append("\n\n").toUtf8().constData();
2624  savedNodeSetFile << htmlFooter.append("\n\n").toUtf8().constData();
2625 
2626  savedNodeSetFile.close();
2627 }
2628 
2640 void VuoEditor::generateNodeClassHtmlPagesForNodeSet(VuoNodeSet *nodeSet, string saveDir, bool publishInternalVuoLinks)
2641 {
2642  string nodeSetName = nodeSet->getName();
2643  vector<string> nodeSetClassNames = nodeSet->getNodeClassNames();
2644  foreach (string nodeClassName, nodeSetClassNames)
2645  {
2646  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(nodeClassName);
2647  if (!nodeClass || nodeClass->getBase()->getDeprecated())
2648  continue;
2649 
2650  string nodeClassTitle = nodeClass->getBase()->getDefaultTitle();
2651  string nodeClassDescription = nodeClass->getBase()->getDescription();
2652  string filteredNodeClassDescription = (publishInternalVuoLinks? VuoRendererCommon::externalizeVuoNodeLinks(compiler, QString::fromStdString(nodeClassDescription), false).toStdString() :
2653  removeVuoLinks(nodeClassDescription));
2654 
2655  vector<string> manualKeywords = nodeClass->getBase()->getKeywords();
2656  vector<string> automaticKeywords = nodeClass->getAutomaticKeywords();
2657 
2658  set<string> sortedUniqueKeywords;
2659  foreach (string keyword, manualKeywords)
2660  sortedUniqueKeywords.insert(keyword);
2661 
2662  foreach (string keyword, automaticKeywords)
2663  sortedUniqueKeywords.insert(keyword);
2664 
2665  // Metadata and node class description
2666  //: Appears in the webpage title on node documentation pages.
2667  QString nodeClassHtmlHeader = "<html><head><meta charset=\"utf-8\"><title>" + tr("Vuo Node Documentation") + ": "
2668  + QString::fromStdString(nodeClassTitle)
2669  + " (" + QString::fromStdString(nodeClassName) + ")"
2670  + "</title>"
2672  + "</head><body>";
2673  QString nodeClassDocumentationTitle = QString("<h2>")
2674  .append(nodeClassTitle.c_str())
2675  .append(" (").append(nodeClassName.c_str()).append(")")
2676  .append("</h2>");
2677  QString nodeClassDocumentationContent = VuoStringUtilities::generateHtmlFromMarkdown(filteredNodeClassDescription).c_str();
2678  //: Appears on node documentation webpages.
2679  QString nodeClassKeywordsIntro = "<p><b>" + tr("Keyword(s)", "", sortedUniqueKeywords.size()) + "</b>: ";
2680  QString nodeClassKeywordsText = nodeClassKeywordsIntro;
2681  foreach (string keyword, sortedUniqueKeywords)
2682  {
2683  if (nodeClassKeywordsText != nodeClassKeywordsIntro)
2684  nodeClassKeywordsText.append(", ");
2685 
2686  nodeClassKeywordsText.append("<i>").append(keyword.c_str()).append("</i>");
2687  }
2688  nodeClassKeywordsText.append("</p>");
2689 
2690  // Rendering of model node
2692  VuoRendererNode *modelNode = new VuoRendererNode(nodeClass->newNode(), NULL);
2693  modelNode->setAlwaysDisplayPortNames(true);
2694  composition->addNode(modelNode->getBase());
2695  composition->createAndConnectInputAttachments(modelNode, compiler);
2696 
2697  QSizeF size = composition->itemsBoundingRect().size().toSize();
2698  QSizeF retinaSize(size.width()*2, size.height()*2);
2699  QPixmap pixmap(retinaSize.toSize());
2700  pixmap.fill(Qt::transparent);
2701 
2702  QPainter painter(&pixmap);
2703  painter.setRenderHint(QPainter::Antialiasing, true);
2704  painter.setRenderHint(QPainter::HighQualityAntialiasing, true);
2705  painter.setRenderHint(QPainter::TextAntialiasing, true);
2706 
2707  composition->render(&painter);
2708 
2709  string nodeClassRenderedPreviewFileName = nodeClassName + ".png";
2710  string tmpSaveNodeClassImage = saveDir + "/" + nodeClassRenderedPreviewFileName;
2711  QFile file(tmpSaveNodeClassImage.c_str());
2712  file.open(QIODevice::WriteOnly);
2713  pixmap.save(&file, "PNG");
2714 
2715  delete modelNode;
2716  delete composition;
2717 
2718  QString nodeClassRenderedPreview = QString("<img src=\"%1\" width=\"%2\" height=\"%3\" />")
2719  .arg(nodeClassRenderedPreviewFileName.c_str())
2720  .arg(size.width())
2721  .arg(size.height());
2722 
2723  // Node class example compositions
2724  vector<string> nodeClassExampleCompositionFileNames = nodeClass->getBase()->getExampleCompositionFileNames();
2725  QString nodeClassExampleCompositionText = "";
2726 
2727  foreach (string compositionFileName, nodeClassExampleCompositionFileNames)
2728  {
2729  string compositionAsString = nodeSet->getExampleCompositionContents(compositionFileName);
2730  VuoCompositionMetadata metadata(compositionAsString);
2731 
2732  string name = metadata.getCustomizedName();
2733  if (name.empty())
2734  name = VuoEditorComposition::formatCompositionFileNameForDisplay(compositionFileName.c_str()).toUtf8().constData();
2735 
2736  // Override @c publishInternalVuoLinks here, and always filter out links to node classes from
2737  // example composition descriptions, since those example descriptions will almost always reference the same
2738  // node class whose documentation references the example.
2739  string description = metadata.getDescription();
2740  string filteredDescription = removeVuoLinks(description);
2741  QString compositionDescription = VuoStringUtilities::generateHtmlFromMarkdownLine(filteredDescription).c_str();
2742 
2743  nodeClassExampleCompositionText.append("<li>")
2744  .append("<a href=\"")
2746  .append("://")
2747  .append(nodeSetName.c_str())
2748  .append("/")
2749  .append(compositionFileName.c_str())
2750  .append("?")
2752  .append("=")
2753  .append(nodeClass->getBase()->getClassName().c_str())
2754  .append("\"><font size=+1>")
2755  .append(name.c_str())
2756  .append("</font></a>");
2757 
2758  if (!compositionDescription.isEmpty())
2759  nodeClassExampleCompositionText.append(": ").append(compositionDescription);
2760 
2761  nodeClassExampleCompositionText.append("</li>\n");
2762  }
2763 
2764  if (nodeClassExampleCompositionFileNames.size() > 0)
2765  //: Appears on node documentation webpages.
2766  nodeClassExampleCompositionText = "<HR><h3>" + tr("Example composition(s)", "", nodeClassExampleCompositionFileNames.size()) + ":</h3>\n<ul>\n" + nodeClassExampleCompositionText + "</ul>";
2767 
2768  // Footer
2769  QString nodeClassProNodeIndicator;
2770 #if VUO_PRO
2771  if (nodeClass->getBase()->isPro())
2772  nodeClassProNodeIndicator = QString(VuoNodePopover::installedProNodeText).append("<br><br>");
2773 #endif
2774  //: Appears on node documentation webpages.
2775  QString nodeClassSetReference = "<HR>" + nodeClassProNodeIndicator + tr("Back to %1 node set documentation.")
2776  .arg("<a href=\"index.html\">" + QString::fromStdString(nodeSetName) + "</a>");
2777  QString nodeClassHtmlFooter = "</body></html>";
2778 
2779  string saveNodeClassHtml = saveDir + "/" + nodeClassName + ".html";
2780  ofstream savedNodeClassFile(saveNodeClassHtml.c_str(), ios::trunc);
2781 
2782  savedNodeClassFile << nodeClassHtmlHeader.append("\n\n").toUtf8().constData();
2783  savedNodeClassFile << nodeClassDocumentationTitle.append("\n\n").toUtf8().constData();
2784  savedNodeClassFile << nodeClassRenderedPreview.append("\n\n").toUtf8().constData();
2785  savedNodeClassFile << nodeClassDocumentationContent.append("\n\n").toUtf8().constData();
2786  savedNodeClassFile << nodeClassKeywordsText.append("\n\n").toUtf8().constData();
2787  savedNodeClassFile << nodeClassExampleCompositionText.append("\n\n").toUtf8().constData();
2788  savedNodeClassFile << nodeClassSetReference.append("\n\n").toUtf8().constData();
2789  savedNodeClassFile << nodeClassHtmlFooter.append("\n\n").toUtf8().constData();
2790 
2791  savedNodeClassFile.close();
2792  }
2793 }
2794 
2795 
2800 string VuoEditor::removeVuoLinks(string markdownText)
2801 {
2802  QString filteredText(markdownText.c_str());
2803  QRegularExpression vuoNodeLink("\\[([^\\]]+)\\](\\(vuo-node://([^\\]]+)\\))");
2804  filteredText.replace(vuoNodeLink, "`\\1`");
2805 
2806  QRegularExpression vuoNodeSetLink("\\[([^\\]]+)\\](\\(vuo-nodeset://([^\\]]+)\\))");
2807  filteredText.replace(vuoNodeSetLink, "`\\1`");
2808 
2809  return filteredText.toUtf8().constData();
2810 }
2811 
2820 {
2821  string nodeSetName = url.host().toUtf8().constData();
2822  string nodeClassToHighlight = "";
2823 
2824  QUrlQuery query(url.query());
2825  // Check whether the url contains the expected query parameters.
2826  if (query.hasQueryItem(vuoExampleHighlightedNodeClassQueryItem))
2827  nodeClassToHighlight = query.queryItemValue(vuoExampleHighlightedNodeClassQueryItem, QUrl::FullyDecoded).toUtf8().constData();
2828 
2829  string compositionFileName = VuoStringUtilities::substrAfter(url.path().toUtf8().constData(), "/");
2830 
2831  VuoNodeSet *nodeSet = compiler->getNodeSetForName(nodeSetName);
2832  if (!nodeSet)
2833  return;
2834 
2835  // Open the composition.
2836  openExampleComposition(compositionFileName.c_str(), nodeSet, nodeClassToHighlight);
2837 }
2838 
2844 {
2845  QClipboard *clipboard = QApplication::clipboard();
2846  const QMimeData *mimeData = clipboard->mimeData();
2847 
2848  if (!mimeData->hasFormat("text/plain") || mimeData->text().isNull())
2849  return "";
2850 
2851  return mimeData->text();
2852 }
2853 
2859 {
2860  return this->nodeDocumentationPanelHeight;
2861 }
2862 
2868 {
2869  return this->nodeLibraryWidth;
2870 }
2871 
2880 {
2881  if (resourceDirectoryForNodeSet.find(nodeSetName) != resourceDirectoryForNodeSet.end())
2882  return resourceDirectoryForNodeSet[nodeSetName];
2883  else
2884  return "";
2885 }
2886 
2891 void VuoEditor::setResourceDirectoryForNodeSet(string nodeSetName, string directory)
2892 {
2893  resourceDirectoryForNodeSet[nodeSetName] = directory;
2894 }
2895 
2900 {
2901  return subcompositionRouter;
2902 }
2903 
2907 void VuoEditor::initializeBuiltInDrivers()
2908 {
2909  // Image filter driver
2911  string imageFilterDriverPath = VuoFileUtilities::getVuoFrameworkPath() + "/Resources/" + "imageFilterDriver.vuo";
2912  string imageFilterDriverAsString = (VuoFileUtilities::fileExists(imageFilterDriverPath)?
2913  VuoFileUtilities::readFileToString(imageFilterDriverPath) :
2914  "");
2915 
2916  if (!imageFilterDriverAsString.empty())
2917  {
2918  VuoCompilerDriver *imageFilterDriver = new VuoCompilerDriver(compiler, imageFilterDriverAsString);
2919  if (imageFilterDriver->isValidDriverForProtocol(imageFilterProtocol))
2920  builtInDriverForProtocol[imageFilterProtocol] = imageFilterDriver;
2921  }
2922 
2923  // Image generator driver
2925  string imageGeneratorDriverPath = VuoFileUtilities::getVuoFrameworkPath() + "/Resources/" + "imageGeneratorDriver.vuo";
2926  string imageGeneratorDriverAsString = (VuoFileUtilities::fileExists(imageGeneratorDriverPath)?
2927  VuoFileUtilities::readFileToString(imageGeneratorDriverPath) :
2928  "");
2929 
2930  if (!imageGeneratorDriverAsString.empty())
2931  {
2932  VuoCompilerDriver *imageGeneratorDriver = new VuoCompilerDriver(compiler, imageGeneratorDriverAsString);
2933  if (imageGeneratorDriver->isValidDriverForProtocol(imageGeneratorProtocol))
2934  builtInDriverForProtocol[imageGeneratorProtocol] = imageGeneratorDriver;
2935  }
2936 
2937  // Image transition driver
2939  string imageTransitionDriverPath = VuoFileUtilities::getVuoFrameworkPath() + "/Resources/" + "imageTransitionDriver.vuo";
2940  string imageTransitionDriverAsString = (VuoFileUtilities::fileExists(imageTransitionDriverPath)?
2941  VuoFileUtilities::readFileToString(imageTransitionDriverPath) :
2942  "");
2943 
2944  if (!imageTransitionDriverAsString.empty())
2945  {
2946  VuoCompilerDriver *imageTransitionDriver = new VuoCompilerDriver(compiler, imageTransitionDriverAsString);
2947  if (imageTransitionDriver->isValidDriverForProtocol(imageTransitionProtocol))
2948  builtInDriverForProtocol[imageTransitionProtocol] = imageTransitionDriver;
2949  }
2950 
2951  // Extract built-in images to be used by protocol drivers.
2952  VuoNodeSet *imageNodeSet = compiler->getNodeSetForName("vuo.image");
2953  if (imageNodeSet)
2955 }
2956 
2961 {
2962  dispatch_sync(builtInDriversQueue, ^{});
2963 
2964  map<VuoProtocol *, VuoCompilerDriver *>::iterator driver = builtInDriverForProtocol.find(protocol);
2965  if (driver != builtInDriverForProtocol.end())
2966  return driver->second;
2967  else
2968  return NULL;
2969 }
2970 
2975 {
2976  // Sub-item indentation.
2977  // Use transparent icons instead of whitespace padding,
2978  // so that users can more easily customize the menu item shortcuts via System Preferences.
2979  QPixmap emptyPixmap(32, 32);
2980  emptyPixmap.fill(Qt::transparent);
2981  QIcon indentIcon(emptyPixmap);
2982 
2983  // "Window" subcategories
2984  QAction *windowHeading = new QAction(this);
2985  windowHeading->setText(tr("Window"));
2986  windowHeading->setEnabled(false);
2987  m->addAction(windowHeading);
2988 
2989  // Image template
2990  {
2991  QAction *templateAction = new QAction(this);
2992  templateAction->setText(tr("Image"));
2993  templateAction->setData("imageTemplate");
2994  templateAction->setIcon(indentIcon);
2995  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
2996  m->addAction(templateAction);
2997  }
2998 
2999  // Layers template
3000  {
3001  QAction *templateAction = new QAction(this);
3002  templateAction->setText(tr("Layers"));
3003  templateAction->setData("layersTemplate");
3004  templateAction->setIcon(indentIcon);
3005  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3006  m->addAction(templateAction);
3007  }
3008 
3009  // Scene template
3010  {
3011  QAction *templateAction = new QAction(this);
3012  templateAction->setText(tr("Scene"));
3013  templateAction->setData("sceneTemplate");
3014  templateAction->setIcon(indentIcon);
3015  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3016  m->addAction(templateAction);
3017  }
3018 
3019  // "Protocol" subcategories
3020  QAction *protocolHeading = new QAction(this);
3021  protocolHeading->setText(tr("Protocol"));
3022  protocolHeading->setEnabled(false);
3023  m->addSeparator();
3024  m->addAction(protocolHeading);
3025 
3026  // Protocols
3027  foreach (VuoProtocol *protocol, VuoProtocol::getProtocols())
3028  {
3029  QAction *protocolAction = new QAction(this);
3030  protocolAction->setText(tr(protocol->getName().c_str()));
3031  protocolAction->setData(qVariantFromValue(static_cast<void *>(protocol)));
3032  protocolAction->setIcon(indentIcon);
3033  connect(protocolAction, &QAction::triggered, this, &VuoEditor::newCompositionWithProtocol);
3034  m->addAction(protocolAction);
3035  }
3036 
3037  // "Export" subcategories
3038  {
3039  m->addSeparator();
3040 
3041  QAction *heading = new QAction(this);
3042  heading->setText(tr("Export"));
3043  heading->setEnabled(false);
3044  m->addAction(heading);
3045 
3046  {
3047  QAction *templateAction = new QAction(this);
3048  templateAction->setText(tr("Movie"));
3049  templateAction->setData("movie");
3050  templateAction->setIcon(indentIcon);
3051  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3052  m->addAction(templateAction);
3053  }
3054 
3055  {
3056  QAction *templateAction = new QAction(this);
3057  templateAction->setText(tr("Screen Saver"));
3058  templateAction->setData("screensaver");
3059  templateAction->setIcon(indentIcon);
3060  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3061  m->addAction(templateAction);
3062  }
3063 
3064  {
3065  QAction *heading = new QAction(this);
3066  heading->setText(tr("FFGL"));
3067  heading->setIcon(indentIcon);
3068  heading->setEnabled(false);
3069  m->addAction(heading);
3070 
3071  {
3072  QAction *templateAction = new QAction(this);
3073  templateAction->setText(" " + tr("Source"));
3074  templateAction->setData("FFGLSource");
3075  templateAction->setIcon(indentIcon);
3076  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3077  m->addAction(templateAction);
3078  }
3079 
3080  {
3081  QAction *templateAction = new QAction(this);
3082  templateAction->setText(" " + tr("Effect"));
3083  templateAction->setData("FFGLEffect");
3084  templateAction->setIcon(indentIcon);
3085  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3086  m->addAction(templateAction);
3087  }
3088 
3089  {
3090  QAction *templateAction = new QAction(this);
3091  templateAction->setText(" " + tr("Blend Mode"));
3092  templateAction->setData("FFGLBlendMode");
3093  templateAction->setIcon(indentIcon);
3094  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3095  m->addAction(templateAction);
3096  }
3097  }
3098 
3099  {
3100  QAction *heading = new QAction(this);
3101  heading->setText(tr("FxPlug"));
3102  heading->setIcon(indentIcon);
3103  heading->setEnabled(false);
3104  m->addAction(heading);
3105 
3106  {
3107  QAction *templateAction = new QAction(this);
3108  templateAction->setText(" " + tr("Generator"));
3109  templateAction->setData("FxPlugGenerator");
3110  templateAction->setIcon(indentIcon);
3111  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3112  m->addAction(templateAction);
3113  }
3114 
3115  {
3116  QAction *templateAction = new QAction(this);
3117  templateAction->setText(" " + tr("Effect"));
3118  templateAction->setData("FxPlugEffect");
3119  templateAction->setIcon(indentIcon);
3120  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3121  m->addAction(templateAction);
3122  }
3123 
3124  {
3125  QAction *templateAction = new QAction(this);
3126  templateAction->setText(" " + tr("Transition"));
3127  templateAction->setData("FxPlugTransition");
3128  templateAction->setIcon(indentIcon);
3129  connect(templateAction, &QAction::triggered, this, &VuoEditor::newCompositionWithTemplate);
3130  m->addAction(templateAction);
3131  }
3132  }
3133  }
3134 }
3135 
3140 {
3141  {
3142  QAction *action = new QAction(m);
3143  action->setText(tr("Image Filter"));
3144  action->setData("GLSLImageFilter");
3145  connect(action, &QAction::triggered, [=] {
3146  closeUnmodifiedUntitledComposition();
3147 #if VUO_PRO
3148  if (!closeWelcomeWindow())
3149  return;
3150 #endif
3152  });
3153  m->addAction(action);
3154  }
3155 
3156  {
3157  QAction *action = new QAction(m);
3158  action->setText(tr("Image Generator"));
3159  action->setData("GLSLImageGenerator");
3160  connect(action, &QAction::triggered, [=] {
3161  closeUnmodifiedUntitledComposition();
3162 #if VUO_PRO
3163  if (!closeWelcomeWindow())
3164  return;
3165 #endif
3167  });
3168  m->addAction(action);
3169  }
3170 
3171  {
3172  QAction *action = new QAction(m);
3173  action->setText(tr("Image Transition"));
3174  action->setData("GLSLImageTransition");
3175  connect(action, &QAction::triggered, [=] {
3176  closeUnmodifiedUntitledComposition();
3177 #if VUO_PRO
3178  if (!closeWelcomeWindow())
3179  return;
3180 #endif
3182  });
3183  m->addAction(action);
3184  }
3185 }
3186 
3192 {
3193  return documentationQueue;
3194 }
3195 
3200 {
3201 #if VUO_PRO
3202  return darkInterfaceAction->isChecked();
3203 #else
3204  return false;
3205 #endif
3206 }
3207 
3213 {
3214  return this->canvasOpacity;
3215 }
3216 
3220 void VuoEditor::showGridLinesToggled(bool show)
3221 {
3223  if (show)
3224  {
3225  showGridPointsAction->setChecked(false);
3226  type = VuoRendererComposition::LineGrid;
3227  }
3228  else
3229  type = VuoRendererComposition::NoGrid;
3230 
3231  settings->setValue(gridTypeSettingsKey, type);
3233 
3234  emit showGridToggled();
3235 }
3236 
3240 void VuoEditor::showGridPointsToggled(bool show)
3241 {
3243  if (show)
3244  {
3245  showGridLinesAction->setChecked(false);
3246  type = VuoRendererComposition::PointGrid;
3247  }
3248  else
3249  type = VuoRendererComposition::NoGrid;
3250 
3251  settings->setValue(gridTypeSettingsKey, type);
3253 
3254  emit showGridToggled();
3255 }
3256 
3260 void VuoEditor::updateSnapToGrid(bool snap)
3261 {
3262  settings->setValue(snapToGridSettingsKey, snap);
3263 
3265 }
3266 
3270 void VuoEditor::updateColor(bool isDark)
3271 {
3272  settings->setValue(darkInterfaceSettingsKey, isDark);
3273 
3275 }
3276 
3281 void VuoEditor::updateCanvasOpacity(QAction *setOpacityAction)
3282 {
3283  updateCanvasOpacityTo(setOpacityAction->data().toInt());
3284  updateUI();
3285 }
3286 
3291 void VuoEditor::updateCanvasOpacityTo(int opacity)
3292 {
3293  settings->setValue(canvasOpacitySettingsKey, opacity);
3294  canvasOpacity = opacity;
3295 }
3296 
3305 {
3306  VuoEditor::documentationGenerationDirectory = dir;
3307 }
3308 
3315 {
3316  map<QString, QString> modelExampleCompositionsAndNodeSets;
3317  modelExampleCompositionsAndNodeSets["LaserDiscoball.vuo"] = "vuo.audio";
3318  modelExampleCompositionsAndNodeSets["Tschuri.vuo"] = "vuo.image";
3319  modelExampleCompositionsAndNodeSets["SlitscanMixingInk.vuo"] = "vuo.layer";
3320  modelExampleCompositionsAndNodeSets["DrawInSpace.vuo"] = "vuo.scene";
3321  return modelExampleCompositionsAndNodeSets;
3322 }
3323 
3329 {
3330  map<QString, QString> modelExampleCompositionsAndNodeSets;
3331 
3332  if (protocol->getId() == VuoProtocol::imageFilter)
3333  {
3334  modelExampleCompositionsAndNodeSets["PixellateImageRadially.vuo"] = "vuo.image";
3335  }
3336 
3337  else if (protocol->getId() == VuoProtocol::imageGenerator)
3338  {
3339  modelExampleCompositionsAndNodeSets["GenerateCheckerboardImage.vuo"] = "vuo.image";
3340  modelExampleCompositionsAndNodeSets["MakeDriftingClouds.vuo"] = "vuo.image";
3341  modelExampleCompositionsAndNodeSets["MakeOvalPatterns.vuo"] = "vuo.image";
3342  modelExampleCompositionsAndNodeSets["RippleImageGradients.vuo"] = "vuo.image";
3343  modelExampleCompositionsAndNodeSets["SpinKaleidoscope.vuo"] = "vuo.event";
3344  }
3345 
3346  else if (protocol->getId() == VuoProtocol::imageTransition)
3347  modelExampleCompositionsAndNodeSets["BlendImages.vuo"] = "vuo.image";
3348 
3349  return modelExampleCompositionsAndNodeSets;
3350 }
3351 
3355 QString VuoEditor::getURLForExampleComposition(QString compositionName, QString nodeSetName)
3356 {
3357  QString compositionURL = QString(VuoEditor::vuoExampleCompositionScheme)
3358  .append("://")
3359  .append(nodeSetName)
3360  .append("/")
3361  .append(compositionName);
3362  return compositionURL;
3363 }
3364 
3369 {
3370 #if VUO_PRO
3371  return getStoredUserName_Pro();
3372 #else
3373  return getenv("USER");
3374 #endif
3375 }
3376 
3381 {
3382 #if VUO_PRO
3383  return getStoredUserProfileURL_Pro();
3384 #else
3385  return "";
3386 #endif
3387 }
3388 
3394 {
3395  settings->setValue(subcompositionPrefixSettingsKey, prefix);
3396  subcompositionPrefix = prefix;
3397 }
3398 
3399 
3404 {
3405  return (!subcompositionPrefix.isEmpty()? subcompositionPrefix : getDefaultSubcompositionPrefix());
3406 }
3407 
3413 {
3414  return getUserName().c_str();
3415 }
3416 
3421 {
3422  menuOpenRecent->openMostRecentFile();
3423 }
3424 
3429 {
3430  menuOpenExample->openRandomExample();
3431 }