Vuo  2.1.0
VuoNodeLibrary.cc
Go to the documentation of this file.
1 
10 #include "VuoNodeLibrary.hh"
11 #include "ui_VuoNodeLibrary.h"
12 
13 #include "VuoEditor.hh"
14 #include "VuoEditorComposition.hh"
15 #include "VuoEditorUtilities.hh"
17 #include "VuoGenericType.hh"
18 #include "VuoCompiler.hh"
20 #include "VuoCompilerPortClass.hh"
21 #include "VuoCompilerType.hh"
22 #include "VuoNodeClass.hh"
24 #include "VuoNodePopover.hh"
25 #include "VuoStringUtilities.hh"
26 #include "VuoType.hh"
27 
28 #ifdef __APPLE__
29 #include <objc/objc-runtime.h>
30 #endif
31 
32 map<pair<QString, QString>, QStringList> VuoNodeLibrary::tokensForNodeClass;
33 map<string, int> VuoNodeLibrary::nodeClassFrequency;
34 set<string> VuoNodeLibrary::stopWords;
35 map<VuoCompilerNodeClass *, int> VuoNodeLibrary::newlyInstalledNodeClasses;
36 
40 VuoNodeLibrary::VuoNodeLibrary(VuoCompiler *compiler, QWidget *parent,
41  nodeLibraryDisplayMode displayMode) :
42  QDockWidget(parent),
43  ui(new Ui::VuoNodeLibrary)
44 {
45  ui->setupUi(this);
46 
47  ui->nodeClassList->verticalScrollBar()->setAttribute(Qt::WA_MacSmallSize);
48  ui->nodeClassList->horizontalScrollBar()->setAttribute(Qt::WA_MacSmallSize);
49  ui->nodePopoverPane->verticalScrollBar()->setAttribute(Qt::WA_MacSmallSize);
50  ui->nodePopoverPane->horizontalScrollBar()->setAttribute(Qt::WA_MacSmallSize);
51 
52  // When the node library is re-sized vertically, stretch the node class list,
53  // not the documentation pane.
54  ui->splitter->setStretchFactor(0, 1);
55  ui->splitter->setStretchFactor(1, 0);
56 
57  this->setFocusProxy(ui->textFilter);
58  setTabOrder(ui->textFilter, ui->nodeClassList);
59  ui->VuoNodeLibraryContents->setFocusProxy(ui->nodeClassList);
60  ui->nodeClassList->setItemDelegate(new VuoNodeClassListItemDelegate(ui->nodeClassList));
61  setHumanReadable(displayMode == VuoNodeLibrary::displayByName);
62 
63  updateListViewTimer = new QTimer(this);
64  updateListViewTimer->setObjectName("VuoNodeLibrary::updateListViewTimer");
65  updateListViewTimer->setSingleShot(true);
66  connect(updateListViewTimer, &QTimer::timeout, this, &VuoNodeLibrary::updateListViewForNewFilterTextNow);
67 
68  connect(ui->nodeClassList, &VuoNodeClassList::itemSelectionChanged, this, static_cast<void (QWidget::*)()>(&QWidget::update));
69  connect(ui->nodeClassList, &VuoNodeClassList::componentsAdded, this, &VuoNodeLibrary::componentsAdded);
71  connect(ui->splitter, &QSplitter::splitterMoved, this, &VuoNodeLibrary::emitNodeDocumentationPanelHeightChanged);
72  connect(ui->textFilter, &VuoNodeLibraryTextFilter::textChanged, this, &VuoNodeLibrary::updateListViewForNewFilterTextOnTimer);
74 
75  ui->textFilter->installEventFilter(this);
76  this->hasBeenShown = false;
77  this->preferredNodeDocumentationPanelHeight = ((VuoEditor *)qApp)->getPreferredNodeDocumentationPanelHeight();
78  this->preferredNodeLibraryWidth = ((VuoEditor *)qApp)->getPreferredNodeLibraryWidth();
79  this->defaultMinimumWidth = minimumWidth();
80  this->defaultMaximumWidth = maximumWidth();
81  fixWidth(false);
82 
83  this->compiler = compiler;
84 
85  populateNodeClassFrequencyMap();
86  populateStopWordList();
87  updateListViewForNewFilterTextNow();
88  updateUI();
89 
90  VuoEditor *editor = (VuoEditor *)qApp;
91  connect(editor, &VuoEditor::darkInterfaceToggled, this, &VuoNodeLibrary::updateColor);
92  updateColor(editor->isInterfaceDark());
93 #ifdef VUO_PRO
94  VuoNodeLibrary_Pro();
95 #endif
96 }
97 
101 void VuoNodeLibrary::cullHiddenNodeClasses(vector<VuoCompilerNodeClass *> &nodeClasses)
102 {
103  for (int i = nodeClasses.size() - 1; i >= 0; --i)
104  {
105  VuoCompilerNodeClass *nodeClass = nodeClasses[i];
106 
107  if (
108  // Exclude specializations of generic node classes from the node library.
109  dynamic_cast<VuoCompilerSpecializedNodeClass *>(nodeClass) ||
110 
112  (VuoStringUtilities::beginsWith(nodeClass->getBase()->getClassName(), "vuo.dictionary") &&
113  VuoStringUtilities::endsWith(nodeClass->getBase()->getClassName(), "VuoText.VuoReal")) ||
114 
115  // Exclude deprecated nodes from the node library.
116  nodeClass->getBase()->getDeprecated()
117  )
118  {
119  nodeClasses.erase(nodeClasses.begin() + i);
120  }
121  }
122 }
123 
127 bool VuoNodeLibrary::eventFilter(QObject *object, QEvent *event)
128 {
129  // Customize handling of keypress events so that the node class list
130  // always receives keypresses of the 'Return' and navigation keys.
131  if (event->type() == QEvent::KeyPress)
132  {
133  QKeyEvent *keyEvent = (QKeyEvent *)(event);
134  if ((keyEvent->key() == Qt::Key_Up) ||
135  (keyEvent->key() == Qt::Key_Down) ||
136  (keyEvent->key() == Qt::Key_PageUp) ||
137  (keyEvent->key() == Qt::Key_PageDown) ||
138  (keyEvent->key() == Qt::Key_Home) ||
139  (keyEvent->key() == Qt::Key_End) ||
140  (keyEvent->key() == Qt::Key_Return))
141  {
142  QApplication::sendEvent(ui->nodeClassList, event);
143  }
144 
145  else
146  {
147  object->removeEventFilter(this);
148  QApplication::sendEvent(object, event);
149  object->installEventFilter(this);
150  }
151 
152  return true;
153  }
154 
155  return QDockWidget::eventFilter(object, event);
156 }
157 
158 
163 {
164  setHumanReadable(true);
165  updateListViewForNewDisplayMode();
166  updateUI();
167 }
168 
173 {
174  setHumanReadable(false);
175  updateListViewForNewDisplayMode();
176  updateUI();
177 }
178 
183 {
184  this->compiler = compiler;
185 }
186 
190 void VuoNodeLibrary::recordNodeClassCapitalizations()
191 {
192  foreach(VuoCompilerNodeClass *nodeClass, loadedNodeClasses)
193  {
194  string className = nodeClass->getBase()->getClassName();
195  capitalizationForNodeClass[QString(className.c_str()).toLower().toUtf8().constData()] = className;
196  }
197 }
198 
203 VuoNodePopover * VuoNodeLibrary::initializeNodePopoverForClass(VuoNodeClass *nodeClass, VuoCompiler *compiler)
204 {
205  VuoNodePopover *popover = new VuoNodePopover(nodeClass, compiler);
206  connect(popover, &VuoNodePopover::popoverDisplayRequested, this, &VuoNodeLibrary::displayPopoverInPane, Qt::QueuedConnection);
209  popover->setTextWidth(ui->nodePopoverPane->viewport()->rect().width());
210  popoverForNodeClass[nodeClass->getClassName()] = popover;
211  return popover;
212 }
213 
218 VuoNodePopover * VuoNodeLibrary::getNodePopoverForClass(VuoNodeClass *nodeClass, VuoCompiler *compiler)
219 {
220  string className = nodeClass->getClassName();
221  map<string, VuoNodePopover *>::iterator popover = popoverForNodeClass.find(className);
222  if (popover != popoverForNodeClass.end())
223  return popover->second;
224  else
225  {
226  VuoNodePopover *newPopover = initializeNodePopoverForClass(nodeClass, compiler);
227  return newPopover;
228  }
229 }
230 
234 void VuoNodeLibrary::populateList(vector<VuoCompilerNodeClass *> nodeClasses, bool resetSelection)
235 {
236  QList<QListWidgetItem *> preselectedItems = ui->nodeClassList->selectedItems();
237  set<string> preselectedClasses;
238 
239  if (!resetSelection)
240  {
241  foreach (QListWidgetItem *item, preselectedItems)
242  preselectedClasses.insert(item->data(VuoNodeClassListItemDelegate::classNameIndex).toString().toUtf8().constData());
243  }
244 
245  ui->nodeClassList->clear();
246  int firstSelectedMatchIndex = -1;
247  int currentItemIndex = 0;
248 
249  foreach (VuoCompilerNodeClass *nodeClass, nodeClasses)
250  {
251  string className = nodeClass->getBase()->getClassName();
252  string humanReadableName = nodeClass->getBase()->getDefaultTitle();
253 
254  QListWidgetItem *item = new QListWidgetItem();
255  item->setData(Qt::DisplayRole, className.c_str()); // used for sort order
256  item->setData(VuoNodeClassListItemDelegate::humanReadableNameIndex, humanReadableName.c_str());
257  item->setData(VuoNodeClassListItemDelegate::classNameIndex, className.c_str()); // node class name, for drag-and-drop operations
258  item->setData(VuoNodeClassListItemDelegate::nodeClassPointerIndex, qVariantFromValue((void *)nodeClass));
259 
260  ui->nodeClassList->addItem(item);
261 
262  if (preselectedClasses.find(className) != preselectedClasses.end())
263  {
264  item->setSelected(true);
265 
266  if (firstSelectedMatchIndex == -1)
267  firstSelectedMatchIndex = currentItemIndex;
268  }
269  else
270  item->setSelected(false);
271 
272  currentItemIndex++;
273  }
274 
275  // Restore the previous selection or set a new one, as appropriate.
276  if (ui->nodeClassList->count() >= 1)
277  {
278  if (firstSelectedMatchIndex >= 0)
279  ui->nodeClassList->setCurrentItem(ui->nodeClassList->item(firstSelectedMatchIndex));
280  else
281  {
282  ui->nodeClassList->item(0)->setSelected(true);
283  ui->nodeClassList->setCurrentItem(ui->nodeClassList->item(0));
284  }
285  }
286 
287  ui->nodeClassList->update();
288 }
289 
293 void VuoNodeLibrary::updateListViewForNewDisplayMode()
294 {
295  updateListView(false);
296 }
297 
301 void VuoNodeLibrary::updateListViewForNewFilterTextOnTimer()
302 {
303  ui->nodeClassList->selectionFinalized = false;
304  updateListViewTimer->start(100);
305 }
306 
310 void VuoNodeLibrary::updateListViewForNewFilterTextNow()
311 {
312  updateListView(true);
313  ui->nodeClassList->selectionFinalized = true;
314 }
315 
319 bool VuoNodeLibrary::nodeHasPortMatchingString(VuoCompilerNodeClass *cnc, string needle, bool isInput)
320 {
321  auto portsToSearch = isInput ? cnc->getBase()->getInputPortClasses() : cnc->getBase()->getOutputPortClasses();
322  for (VuoPortClass *p : portsToSearch)
323  {
324  string name = p->getName();
325  std::transform(name.begin(), name.end(), name.begin(), ::tolower);
326  if (name.find(needle) != string::npos)
327  return true;
328 
329  VuoCompilerPortClass *cpc = dynamic_cast<VuoCompilerPortClass *>(p->getCompiler());
330  if (!cpc)
331  continue;
332  VuoType *dataType = cpc->getDataVuoType();
333  if (!dataType)
334  continue;
335  string key = dataType->getModuleKey();
336  std::transform(key.begin(), key.end(), key.begin(), ::tolower);
337  if (key.find(needle) != string::npos)
338  return true;
339 
340  VuoGenericType *genericType = dynamic_cast<VuoGenericType *>(dataType);
341  if (!genericType)
342  continue;
343  VuoGenericType::Compatibility compatibility;
344  for (string specializedType : genericType->getCompatibleSpecializedTypes(compatibility))
345  {
346  std::transform(specializedType.begin(), specializedType.end(), specializedType.begin(), ::tolower);
347  if (specializedType.find(needle) != string::npos)
348  return true;
349  }
350 
351  }
352  return false;
353 }
354 
360 bool VuoNodeLibrary::nodeHasSourceType(VuoCompilerNodeClass *cnc, string sourceType)
361 {
362  QString actionText, sourcePath;
363  bool nodeClassEditable = VuoEditorUtilities::isNodeClassEditable(cnc->getBase(), actionText, sourcePath);
364 
365  if ((sourceType == ".vuo") && cnc->isSubcomposition())
366  return true;
367  else if ((sourceType == ".fs") && cnc->isIsf())
368  return true;
369  else if ((sourceType == ".c") && nodeClassEditable && !cnc->isSubcomposition() && !cnc->isIsf())
370  return true;
371  else if ((sourceType == ".vuonode") && !cnc->isBuiltIn() && !nodeClassEditable)
372  return true;
373 
374  return false;
375 }
376 
380 void VuoNodeLibrary::updateListView(bool resetSelection)
381 {
382  // Split node class filter text on whitespace.
383  QString textFilter = ui->textFilter->text();
384  ui->nodeClassList->setFilterText(textFilter);
385  QStringList tokenizedTextFilter = textFilter.split(QRegExp("\\s+"), QString::SkipEmptyParts);
386 
387  // Extract the filter predicates.
388  QStringList predicates;
389  for (QString token : tokenizedTextFilter)
390  {
391  if (token.startsWith("in:")
392  || token.startsWith("out:")
393  || token.startsWith("source:"))
394  {
395  predicates << token;
396  tokenizedTextFilter.removeOne(token);
397  }
398  }
399 
400  vector<VuoCompilerNodeClass *> matchingNodeClasses = getMatchingNodeClassesForSearchTerms(tokenizedTextFilter);
401 
402  // If the first search yielded no matches, split the text on '.' as well as whitespace and try again.
403  if ((matchingNodeClasses.size() == 0) && textFilter.contains('.'))
404  {
405  tokenizedTextFilter = textFilter.split(QRegExp("[\\s\\.]+"), QString::SkipEmptyParts);
406  matchingNodeClasses = getMatchingNodeClassesForSearchTerms(tokenizedTextFilter);
407  }
408 
409  // Apply the filter predicates.
410  for (QString predicate : predicates)
411  {
412  string filterValue = predicate.split(':')[1].toLower().toStdString();
413  std::function<bool(VuoCompilerNodeClass *)> predicateFunction;
414  if (predicate.startsWith("in:"))
415  predicateFunction = [=](VuoCompilerNodeClass *cnc) {
416  return !nodeHasPortMatchingString(cnc, filterValue, true);
417  };
418  else if (predicate.startsWith("out:"))
419  predicateFunction = [=](VuoCompilerNodeClass *cnc) {
420  return !nodeHasPortMatchingString(cnc, filterValue, false);
421  };
422  else if (predicate.startsWith("source:"))
423  predicateFunction = [=](VuoCompilerNodeClass *cnc) {
424  return !nodeHasSourceType(cnc, filterValue);
425  };
426 
427  matchingNodeClasses.erase(std::remove_if(matchingNodeClasses.begin(), matchingNodeClasses.end(), predicateFunction), matchingNodeClasses.end());
428  }
429 
430  populateList(matchingNodeClasses, resetSelection);
431 }
432 
437 vector<VuoCompilerNodeClass *> VuoNodeLibrary::getMatchingNodeClassesForSearchTerms(QStringList rawTermList)
438 {
439  QStringList termList = applyFilterTransformations(rawTermList);
440  int numTokens = termList.size();
441  map<int, map<int, vector<VuoCompilerNodeClass *> > > nodeClassesWithTitleAndClassNameMatches;
442 
443  foreach (VuoCompilerNodeClass *nodeClass, loadedNodeClasses)
444  {
445  QString className = nodeClass->getBase()->getClassName().c_str();
446  QString humanReadableName = nodeClass->getBase()->getDefaultTitle().c_str();
447 
448  // Check for the presence of each filter text token within the current
449  // node class's searchable metadata.
450  int numTokensFirstMatchedInTitle = 0;
451  int numTokensFirstMatchedInClassName = 0;
452  bool tokenSearchFailed = false;
453 
454  for (int tokenIndex = 0; (! tokenSearchFailed) && (tokenIndex < numTokens); ++tokenIndex)
455  {
456  QString currentToken = termList.at(tokenIndex);
457  QStringMatcher textFilterPattern(currentToken, Qt::CaseInsensitive);
458  bool foundCurrentTokenInTitle = false;
459  bool foundCurrentTokenInClassName = false;
460  bool foundCurrentTokenInKeywords = false;
461  QStringList nodeTitleTokens = tokenizeNodeName(humanReadableName, "");
462  QStringList nodeClassNameTokens = tokenizeNodeName("", className);
463 
464  for (QStringList::iterator nodeTitleToken = nodeTitleTokens.begin(); (! foundCurrentTokenInTitle) && (nodeTitleToken != nodeTitleTokens.end()); ++nodeTitleToken)
465  {
466  // At least one of the tokens within the human-readable node class title
467  // must begin with the filter text token in order for the node class to be
468  // considered a match based on its title.
469  if (textFilterPattern.indexIn(*nodeTitleToken) == 0)
470  foundCurrentTokenInTitle = true;
471  }
472 
473  if (foundCurrentTokenInTitle)
474  numTokensFirstMatchedInTitle++;
475 
476  if (! foundCurrentTokenInTitle)
477  {
478  for (QStringList::iterator nodeClassNameToken = nodeClassNameTokens.begin(); (! foundCurrentTokenInClassName) && (nodeClassNameToken != nodeClassNameTokens.end()); ++nodeClassNameToken)
479  {
480  // At least one of the tokens within the node class name
481  // must begin with the filter text token in order for the node class to be
482  // considered a match based on its node class name.
483  if (textFilterPattern.indexIn(*nodeClassNameToken) == 0)
484  foundCurrentTokenInClassName = true;
485  }
486  }
487 
488  if (foundCurrentTokenInClassName)
489  numTokensFirstMatchedInClassName++;
490 
491  if (!(foundCurrentTokenInTitle || foundCurrentTokenInClassName))
492  {
493  vector<string> keywords = nodeClass->getBase()->getKeywords();
494  vector<string> automaticKeywords = nodeClass->getAutomaticKeywords();
495  foreach (string automaticKeyword, automaticKeywords)
496  keywords.push_back(automaticKeyword);
497 
498  // Add port display names to searchable keywords.
499  vector<VuoPortClass *> inputPorts = nodeClass->getBase()->getInputPortClasses();
500  foreach (VuoPortClass *port, inputPorts)
501  keywords.push_back(static_cast<VuoCompilerPortClass *>(port->getCompiler())->getDisplayName());
502 
503  vector<VuoPortClass *> outputPorts = nodeClass->getBase()->getOutputPortClasses();
504  foreach (VuoPortClass *port, outputPorts)
505  keywords.push_back(static_cast<VuoCompilerPortClass *>(port->getCompiler())->getDisplayName());
506 
507  // For "Share Value" and "Share List" nodes, add type display names to searchable keywords.
508  if ( (nodeClass->getBase()->getClassName() == "vuo.data.share") ||
509  (nodeClass->getBase()->getClassName() == "vuo.data.share.list"))
510  {
511  map<string, VuoCompilerType *> loadedTypes = compiler->getTypes();
512  for (map<string, VuoCompilerType *>::iterator i = loadedTypes.begin(); i != loadedTypes.end(); ++i)
513  {
514  string typeName = i->first;
515  if ((!VuoType::isListTypeName(typeName)) &&
516  !VuoType::isDictionaryTypeName(typeName) &&
517  (typeName != "VuoMathExpressionList"))
518 
519  {
520  VuoCompilerType *type = i->second;
521  keywords.push_back(type->getBase()->getDefaultTitle());
522  }
523  }
524  }
525 
526  // Split keyphrases on whitespace so that each one is considered an individual searchable keyword.
527  QStringList formattedKeywords;
528  foreach (string keyword, keywords)
529  {
530  QStringList tokenizedKeywords = QString(keyword.c_str()).split(QRegExp("\\s+"), QString::SkipEmptyParts);
531  foreach (QString tokenizedKeyword, tokenizedKeywords)
532  formattedKeywords.push_back(tokenizedKeyword);
533  }
534 
535  for (int i = 0; (!foundCurrentTokenInKeywords) && (i < formattedKeywords.size()); ++i)
536  {
537  QString keyword = formattedKeywords[i];
538 
539  // At least one node class keyword must begin with the filter text token in order for
540  // the node class to be considered a match based on its keywords.
541  if (textFilterPattern.indexIn(keyword) == 0)
542  foundCurrentTokenInKeywords = true;
543  }
544  }
545 
546  if (!(foundCurrentTokenInTitle || foundCurrentTokenInClassName || foundCurrentTokenInKeywords || isStopWord(currentToken.toUtf8().constData())))
547  tokenSearchFailed = true;
548  }
549 
550  if (!tokenSearchFailed)
551  nodeClassesWithTitleAndClassNameMatches[numTokensFirstMatchedInTitle][numTokensFirstMatchedInClassName].push_back(nodeClass);
552  }
553 
554  // List matching node classes:
555  // - Primarily in descending order of number of search token matches within the human-readable node title;
556  // - Secondarily in descending order of number of search token matches within the node class name.
557  vector<VuoCompilerNodeClass *> matchingNodeClasses;
558  for (int i = numTokens; i >= 0; i--)
559  {
560  for (int j = numTokens-i; j >= 0; j--)
561  {
562  vector<VuoCompilerNodeClass *> currentMatches = nodeClassesWithTitleAndClassNameMatches[i][j];
563 
564  // List tertiarily in descending order of approximated frequency of use if the filter text is non-empty
565  // or the library is in "Display by name" mode.
566  if (termList.size() > 0 || getHumanReadable())
567  sort(currentMatches.begin(), currentMatches.end(), nodeClassLessThan);
568 
569  matchingNodeClasses.insert(matchingNodeClasses.end(), currentMatches.begin(), currentMatches.end());
570  }
571  }
572 
573  return matchingNodeClasses;
574 }
575 
580 QStringList VuoNodeLibrary::applyFilterTransformations(QStringList filterTokenList)
581 {
582  QStringList transformedTokenList;
583  foreach (QString token, filterTokenList)
584  {
585  // Apply special handling for arithmetic and comparison operators:
586  // First correct for anticipated variants in user-entered input.
587  if (token == "=<")
588  token = "<=";
589  else if (token == "=>")
590  token = ">=";
591 
592  // Now tweak token lists to ensure that the most relevant nodes are listed first in the search results.
593  if (token == "+")
594  transformedTokenList.append("add");
595  else if (token == "-")
596  transformedTokenList.append("subtract");
597  else if (token == "/")
598  transformedTokenList.append("divide");
599  else if ((token == "*") || (token == "•") || (token == "×"))
600  transformedTokenList.append("multiply");
601  else if (token == "<")
602  transformedTokenList.append("less");
603  else if (token == ">")
604  transformedTokenList.append("greater");
605  else if ((token == "==") || (token == "=") || (token == "≠") || (token == "!=") || (token == "<>"))
606  transformedTokenList.append("equal");
607  else if ((token == "<=") || (token == ">="))
608  transformedTokenList.append("compare");
609  else if (token == "|")
610  transformedTokenList.append("value");
611  else if (token == "%")
612  transformedTokenList.append("limit");
613  else if (token == "^")
614  transformedTokenList.append("exponentiate");
615 
616  transformedTokenList.append(token);
617  }
618 
619  return transformedTokenList;
620 }
621 
626 {
627  // Force a re-paint of each list item, in case the display mode has changed.
628  for (int nodeClassIndex = 0; nodeClassIndex < ui->nodeClassList->count(); ++nodeClassIndex)
629  ui->nodeClassList->setRowHidden(nodeClassIndex, false);
630 
631  ui->nodeClassList->update();
632  update();
633 }
634 
642 void VuoNodeLibrary::getState(QString &filterText, set<string> &selectedNodeClasses, string &documentedNodeClass)
643 {
644  filterText = ui->textFilter->text();
645 
646  selectedNodeClasses.clear();
647  QList<QListWidgetItem *> selectedItems = ui->nodeClassList->selectedItems();
648  foreach (QListWidgetItem *item, selectedItems)
649  selectedNodeClasses.insert(ui->nodeClassList->getNodeClassForItem(item)->getBase()->getClassName());
650 
651  VuoNodePopover *nodeDocumentation = dynamic_cast<VuoNodePopover *>(ui->nodePopoverPane->widget());
652  documentedNodeClass = (nodeDocumentation? nodeDocumentation->getNodeClass()->getClassName() : "");
653 }
654 
658 void VuoNodeLibrary::setState(QString filterText, set<string> selectedNodeClasses, string documentedNodeClass)
659 {
660  disconnect(ui->textFilter, &QLineEdit::textChanged, this, &VuoNodeLibrary::updateListViewForNewFilterTextOnTimer);
661  ui->textFilter->setText(filterText);
662  connect(ui->textFilter, &QLineEdit::textChanged, this, &VuoNodeLibrary::updateListViewForNewFilterTextOnTimer);
663 
664  ui->nodeClassList->disablePopovers();
665  highlightNodeClass("", false, true);
666  foreach (string nodeClassName, selectedNodeClasses)
667  highlightNodeClass(nodeClassName, false, false);
668  ui->nodeClassList->enablePopovers();
669 
670  prepareAndDisplayNodePopoverForClass(documentedNodeClass);
671 }
672 
677 {
678  VuoPanelDocumentation *documentationWidget= dynamic_cast<VuoPanelDocumentation *>(ui->nodePopoverPane->widget());
679  return (documentationWidget? documentationWidget->getSelectedText() : "");
680 }
681 
688 QStringList VuoNodeLibrary::tokenizeNodeName(QString nodeName, QString className)
689 {
690  // Check whether the cache already contains the tokenized node name of interest.
691  pair<QString, QString> namePair = make_pair(nodeName, className);
692  map<pair<QString, QString>, QStringList>::iterator i = VuoNodeLibrary::tokensForNodeClass.find(namePair);
693  if (i != VuoNodeLibrary::tokensForNodeClass.end())
694  return i->second;
695 
696  QStringList tokenizedNodeName = nodeName.split(QRegExp("\\s+"), QString::SkipEmptyParts);
697  QString classNameDelimitedByCaseTransitions = className;
698  classNameDelimitedByCaseTransitions.replace(QRegExp("([a-z0-9])([A-Z])"), "\\1.\\2");
699  QStringList tokenizedClassName = className.split(QRegExp("\\."), QString::SkipEmptyParts);
700  QStringList classNameTokenizedAlsoByCaseTransitions = classNameDelimitedByCaseTransitions.split(QRegExp("\\."), QString::SkipEmptyParts);
701 
702  QStringList classNameSuffixes;
703  if (!className.isEmpty())
704  {
705  QRegExp dotTerminatedPrefix("^.*\\.");
706  dotTerminatedPrefix.setMinimal(true);
707 
708  QString currentClassNameSuffix = className;
709  classNameSuffixes.append(currentClassNameSuffix);
710  while (currentClassNameSuffix.contains('.'))
711  classNameSuffixes.append(currentClassNameSuffix.remove(dotTerminatedPrefix));
712  }
713 
714  tokenizedNodeName.append(tokenizedClassName);
715  tokenizedNodeName.append(classNameTokenizedAlsoByCaseTransitions);
716  tokenizedNodeName.append(classNameSuffixes);
717 
718  // Cache the result for later.
719  VuoNodeLibrary::tokensForNodeClass[namePair] = tokenizedNodeName;
720 
721  return tokenizedNodeName;
722 }
723 
729 {
730  return ((VuoNodeClassListItemDelegate *)(ui->nodeClassList->itemDelegate()))->getHumanReadable();
731 }
732 
738 {
739  ui->textFilter->setFocus();
740  ui->textFilter->selectAll();
741 }
742 
747 {
748  ui->textFilter->setText(text);
749 }
750 
755 void VuoNodeLibrary::setHumanReadable(bool humanReadable)
756 {
757  ((VuoNodeClassListItemDelegate *)(ui->nodeClassList->itemDelegate()))->setHumanReadable(humanReadable);
759 }
760 
764 void VuoNodeLibrary::moveEvent(QMoveEvent *event)
765 {
766  bool disablePropagation = isHidden();
767 
768  if (!disablePropagation)
769  emit nodeLibraryMoved(event->pos());
770 }
771 
775 void VuoNodeLibrary::resizeEvent(QResizeEvent *event)
776 {
777  bool disablePropagation = isHidden();
778 
779  int oldWidth = event->oldSize().width();
780  int newWidth = event->size().width();
781 
782  if (oldWidth != newWidth)
783  {
784  int documentationPaneContentWidth = ui->nodePopoverPane->viewport()->rect().width();
785 
786  if (!disablePropagation)
787  emit nodeDocumentationPanelWidthChanged(documentationPaneContentWidth);
788 
789  // Respond to manual re-sizings by the user after the library has been shown,
790  // but not to automatic re-sizings that occur beforehand.
791  if (hasBeenShown)
792  {
793  if (!disablePropagation)
794  emit nodeLibraryWidthChanged(newWidth);
795 
796  this->preferredNodeLibraryWidth = newWidth;
797  }
798  }
799 
800  int oldHeight = event->oldSize().height();
801  int newHeight = event->size().height();
802 
803  if (oldHeight != newHeight)
804  {
805  if (!disablePropagation)
806  emit nodeLibraryHeightChanged(newHeight);
807 
808  // Respond to manual re-sizings by the user after the library has been shown,
809  // but not to automatic re-sizings that occur beforehand.
810  if (hasBeenShown)
811  {
812  // The documentation panel height may or may not have actually changed --
813  // we emit this signal liberally.
814  if (!disablePropagation)
815  emitNodeDocumentationPanelHeightChanged();
816  }
817  }
818 }
819 
823 void VuoNodeLibrary::emitNodeDocumentationPanelHeightChanged()
824 {
825  QList<int> widgetSizes = ui->splitter->sizes();
826  int newSize = widgetSizes[1];
827 
828  this->preferredNodeDocumentationPanelHeight = newSize;
830 }
831 
836 void VuoNodeLibrary::closeEvent(QCloseEvent *event)
837 {
838  bool disablePropagation = isHidden();
839 
840  if (!disablePropagation)
842 }
843 
847 void VuoNodeLibrary::keyPressEvent(QKeyEvent *event)
848 {
849  if (event->key() == Qt::Key_Escape)
850  {
851  // The 'Escape' key clears the text filter,
852  // or closes the node library if the text filter is already empty.
853  if (ui->textFilter->text().isEmpty())
854  close();
855  else
856  ui->textFilter->clear();
857  }
858 }
859 
866 {
867  ui->nodePopoverPane->takeWidget();
868 }
869 
873 void VuoNodeLibrary::showEvent(QShowEvent *event)
874 {
875  bool disablePropagation = isHidden();
876 
877  // The call to re-size the node library to the preferred width has to be made here rather than in
878  // VuoNodeLibrary::prepareAndMakeVisible(). prepareAndMakeVisible() calls setVisible(true), which
879  // indirectly calls this, but putting the re-sizing call within prepareAndMakeVisible() has no effect.
880  if (preferredNodeLibraryWidth >= 0)
881  {
882  setMinimumWidth(preferredNodeLibraryWidth); // Allow some wiggle room so that the docking area doesn't lose its border.
883  setMaximumWidth(minimumWidth()+1);
884  updateGeometry();
885  }
886 
887  updateSplitterPosition();
888 
889  // The node documentation panel content width must be estimated as if the
890  // vertical scrollbar will be displayed, so that the documentation
891  // text will be fully in view.
892  int documentationPaneContentWidth = ui->nodePopoverPane->viewport()->rect().width() -
893  ui->nodePopoverPane->verticalScrollBar()->sizeHint().width();
894 
895  if (!disablePropagation)
896  emit nodeDocumentationPanelWidthChanged(documentationPaneContentWidth);
897 
898  // The call to display the first popover must come after the width-change
899  // signal is emitted or the popover will be displayed with extra vertical
900  // padding for a few moments.
901  if (!this->hasBeenShown)
902  ui->nodeClassList->enablePopovers();
903 
904  this->hasBeenShown = true;
905 }
906 
912 {
913  setVisible(true);
914 
915  // The call to restore the original minimum and maximum widths has to be made here rather than in
916  // VuoNodeLibrary::showEvent(); otherwise the call that sets the width to the preferred value has no effect.
917  setMinimumWidth(defaultMinimumWidth);
918  setMaximumWidth(defaultMaximumWidth);
919 }
920 
925 {
926  if (fix)
927  {
928  widget()->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
929  widget()->setMinimumWidth(this->preferredNodeLibraryWidth >= 0?
930  this->preferredNodeLibraryWidth :
931  this->defaultMinimumWidth);
932  widget()->setMaximumWidth(this->preferredNodeLibraryWidth >= 0?
933  this->preferredNodeLibraryWidth :
934  this->defaultMinimumWidth);
935  }
936  else
937  {
938  widget()->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
939  widget()->setMinimumWidth(this->defaultMinimumWidth);
940  widget()->setMaximumWidth(this->defaultMaximumWidth);
941  }
942 }
943 
948 void VuoNodeLibrary::updateSplitterPosition()
949 {
950  if (this->preferredNodeDocumentationPanelHeight != -1)
951  {
952  QList<int> oldWidgetSizes = ui->splitter->sizes();
953  QList<int> newWidgetSizes;
954 
955  int totalAvailableSize = 0;
956  foreach(int size, oldWidgetSizes)
957  totalAvailableSize += size;
958 
959  // Splitter widget[0]: Node class list
960  newWidgetSizes.append(totalAvailableSize - this->preferredNodeDocumentationPanelHeight);
961 
962  // Spliter widget[1]: Node documentation pane
963  newWidgetSizes.append(this->preferredNodeDocumentationPanelHeight);
964  ui->splitter->setSizes(newWidgetSizes);
965  }
966 }
967 
973 {
974  string lowercaseNodeClassName = QString(nodeClassName.c_str()).toLower().toUtf8().constData();
975  VuoCompilerNodeClass *nodeClass = compiler->getNodeClass(capitalizationForNodeClass[lowercaseNodeClassName]);
976  if (nodeClass)
978 }
979 
985 {
986  getNodePopoverForClass(nodeClass, compiler)->prepareResourcesAndShow();
987 }
988 
994 {
995  ui->nodeClassList->enablePopoversAndDisplayCurrent();
996 }
997 
1002 void VuoNodeLibrary::displayPopoverInPane(QWidget *panelContentWidget, QString resourceDir)
1003 {
1004  if (ui->nodePopoverPane->widget() == panelContentWidget)
1005  return;
1006 
1007  // Temporarily set the current working directory to the documentation resource directory
1008  // so that relative paths within the popover HTML are resolved correctly.
1009  QString savedWorkingDir = QDir::currentPath();
1010  if (!resourceDir.isEmpty())
1011  QDir::setCurrent(resourceDir);
1012 
1013  // Prevent the previous popover from being deleted when a new one is displayed.
1015 
1016  ui->nodePopoverPane->setWidget(panelContentWidget);
1017 
1018  if (!resourceDir.isEmpty())
1019  QDir::setCurrent(savedWorkingDir);
1020 }
1021 
1027 {
1028  emit nodeLibraryHiddenOrUnhidden(unhidden);
1029 }
1030 
1031 VuoNodeLibrary::~VuoNodeLibrary()
1032 {
1033  emit aboutToBeDestroyed();
1034 
1035  // Prevent the documentation from being destroyed along with the documentation pane,
1036  // since (in the case of composition metadata), the composition window might try to
1037  // use it again later.
1039 
1040  releaseNodePopovers();
1041 
1042  delete ui;
1043 }
1044 
1056 bool VuoNodeLibrary::nodeClassLessThan(VuoCompilerNodeClass *nodeClass1, VuoCompilerNodeClass *nodeClass2)
1057 {
1058  string nodeClassName1 = nodeClass1->getBase()->getClassName();
1059  string nodeClassName2 = nodeClass2->getBase()->getClassName();
1060 
1061  // List subcompositions highlighted during the current editor session first (in reverse chronological highlighting order),
1062  // and subcompositions installed in a previous editor session next (alphabetically).
1063  bool nodeClass1IsSubcomposition = nodeClass1->isSubcomposition();
1064  bool nodeClass2IsSubcomposition = nodeClass2->isSubcomposition();
1065 
1066  if (nodeClass1IsSubcomposition && !nodeClass2IsSubcomposition)
1067  return true;
1068 
1069  else if (nodeClass2IsSubcomposition && !nodeClass1IsSubcomposition)
1070  return false;
1071 
1072  else if (nodeClass1IsSubcomposition && nodeClass2IsSubcomposition)
1073  {
1074  bool nodeClass1InstalledThisSession = newlyInstalledNodeClasses.find(nodeClass1) != newlyInstalledNodeClasses.end();
1075  bool nodeClass2InstalledThisSession = newlyInstalledNodeClasses.find(nodeClass2) != newlyInstalledNodeClasses.end();
1076 
1077  if (nodeClass1InstalledThisSession && !nodeClass2InstalledThisSession)
1078  return true;
1079  else if (nodeClass2InstalledThisSession && !nodeClass1InstalledThisSession)
1080  return false;
1081  else if (nodeClass1InstalledThisSession && nodeClass2InstalledThisSession)
1082  return (newlyInstalledNodeClasses[nodeClass1] > newlyInstalledNodeClasses[nodeClass2]);
1083  else // if (!nodeClass1InstalledThisSession && !nodeClass2InstalledThisSession)
1084  return (nodeClassName1 < nodeClassName2);
1085  }
1086 
1087  // List typecasts last.
1088  bool nodeClass1IsTypecast = nodeClass1->getBase()->isTypecastNodeClass();
1089  bool nodeClass2IsTypecast = nodeClass2->getBase()->isTypecastNodeClass();
1090  if (nodeClass1IsTypecast && !nodeClass2IsTypecast)
1091  return false;
1092  else if (nodeClass2IsTypecast && !nodeClass1IsTypecast)
1093  return true;
1094 
1095  // Otherwise, list by decreasing frequency.
1096  int nodeClass1Frequency = nodeClassFrequency[nodeClassName1];
1097  int nodeClass2Frequency = nodeClassFrequency[nodeClassName2];
1098 
1099  return ((nodeClass1Frequency > nodeClass2Frequency) ||
1100  ((nodeClass1Frequency == nodeClass2Frequency) && (nodeClassName1 < nodeClassName2)));
1101 }
1102 
1109 {
1110  loadedNodeClasses.clear();
1111 
1112  // Clear the popover pane and reset the list of cached popovers.
1113  displayPopoverInPane(NULL, "");
1114  releaseNodePopovers();
1115 
1116  updateNodeClassList(vector<string>(), vector<VuoCompilerNodeClass *>());
1117 }
1118 
1126 void VuoNodeLibrary::updateNodeClassList(const vector<string> &nodeClassesToRemove,
1127  const vector<VuoCompilerNodeClass *> &nodeClassesToAdd)
1128 {
1129  if (nodeClassesToRemove.size() >= 1)
1130  {
1131  // Clear the popover pane and reset the list of cached popovers.
1132  displayPopoverInPane(NULL, "");
1133  releaseNodePopovers();
1134  }
1135 
1136  // Remove the old node classes from the list.
1137  for (int i = loadedNodeClasses.size() - 1; i >= 0; --i)
1138  {
1139  string nodeClassName = loadedNodeClasses[i]->getBase()->getClassName();
1140  if (std::find(nodeClassesToRemove.begin(), nodeClassesToRemove.end(), nodeClassName) != nodeClassesToRemove.end())
1141  loadedNodeClasses.erase(loadedNodeClasses.begin() + i);
1142  }
1143 
1144  // Add the filtered list of new node classes to the list.
1145  vector<VuoCompilerNodeClass *> nodeClassesToAddCopy = nodeClassesToAdd;
1146  cullHiddenNodeClasses(nodeClassesToAddCopy);
1147  foreach (VuoCompilerNodeClass *nodeClassToAdd, nodeClassesToAddCopy)
1148  if (std::find(loadedNodeClasses.begin(), loadedNodeClasses.end(), nodeClassToAdd) == loadedNodeClasses.end())
1149  loadedNodeClasses.push_back(nodeClassToAdd);
1150 
1151  recordNodeClassCapitalizations();
1152 
1153  // Clear the filter text to make sure any newly added nodes are immediately visible in the list.
1154  // @todo https://b33p.net/kosada/node/15217 and https://b33p.net/kosada/node/14657:
1155  // Be more discerning -- this isn't desirable behavior in all circumstances.
1156  bool clearFilterText = false;
1157  if (clearFilterText)
1158  {
1159  // Disable the timer-based refresh prior to changes to the filter text.
1160  disconnect(ui->textFilter, &QLineEdit::textChanged, this, &VuoNodeLibrary::updateListViewForNewFilterTextOnTimer);
1161 
1162  ui->textFilter->setText("");
1163 
1164  // Re-enable the timer-based refresh following changes to the filter text.
1165  connect(ui->textFilter, &QLineEdit::textChanged, this, &VuoNodeLibrary::updateListViewForNewFilterTextOnTimer);
1166  }
1167 
1168  {
1169  ui->nodeClassList->disablePopovers();
1170  updateListView(false);
1171  ui->nodeClassList->enablePopovers();
1172  }
1173 
1174  if (nodeClassesToRemove.size() >= 1)
1175  {
1176  // Re-display something relevant in the documentation panel if we cleared it previously.
1178  }
1179 
1180  updateUI();
1181 }
1182 
1188 void VuoNodeLibrary::highlightNodeClass(string targetNodeClassName, bool highlightAsNewlyInstalled, bool resetPreviousSelection)
1189 {
1190  // Avoid bug where the final set of selected items in the QListWidget is apparently
1191  // sensitive to the order in which their selection status is set:
1192  // First iterate through the entire list and select only the target node class.
1193  bool searchDone = !targetNodeClassName.empty();
1194  for (int i = 0; i < ui->nodeClassList->count() && !searchDone; ++i)
1195  {
1196  QListWidgetItem *currentItem = ui->nodeClassList->item(i);
1197  string currentNodeClassName = currentItem->data(VuoNodeClassListItemDelegate::classNameIndex).toString().toUtf8().constData();
1198  if (currentNodeClassName == targetNodeClassName)
1199  {
1200  ui->nodeClassList->setCurrentItem(currentItem);
1201  currentItem->setSelected(true);
1202  searchDone = true;
1203  }
1204  }
1205 
1206  if (resetPreviousSelection)
1207  {
1208  // Now do a second pass and deselect everything other than the target node class.
1209  for (int i = 0; i < ui->nodeClassList->count(); ++i)
1210  {
1211  QListWidgetItem *currentItem = ui->nodeClassList->item(i);
1212  string currentNodeClassName = currentItem->data(VuoNodeClassListItemDelegate::classNameIndex).toString().toUtf8().constData();
1213  if (currentNodeClassName != targetNodeClassName)
1214  currentItem->setSelected(false);
1215  }
1216  }
1217 
1218  ui->nodeClassList->update();
1219 
1220  if (highlightAsNewlyInstalled)
1221  newlyInstalledNodeClasses[compiler->getNodeClass(targetNodeClassName)] = newlyInstalledNodeClasses.size();
1222 
1223  updateListView(false);
1224 }
1225 
1231 void VuoNodeLibrary::populateNodeClassFrequencyMap()
1232 {
1233 #include "VuoNodeClassFrequencyMap.hh"
1234 }
1235 
1239 void VuoNodeLibrary::populateStopWordList()
1240 {
1241  stopWords.insert("a");
1242  stopWords.insert("an");
1243  stopWords.insert("for");
1244  stopWords.insert("from");
1245  stopWords.insert("in");
1246  stopWords.insert("on");
1247  stopWords.insert("of");
1248  stopWords.insert("the");
1249  stopWords.insert("to");
1250  stopWords.insert("with");
1251 }
1252 
1256 bool VuoNodeLibrary::isStopWord(QString word)
1257 {
1258  return (stopWords.find(word.toLower().toUtf8().constData()) != stopWords.end());
1259 }
1260 
1261 void VuoNodeLibrary::releaseNodePopovers()
1262 {
1263  // Temporarily disabled deletion to work around crash when docking the node library.
1264  // https://b33p.net/kosada/node/15784
1265  for (auto i : popoverForNodeClass)
1266  i.second->cleanup();
1267 // i.second->deleteLater();
1268 
1269  popoverForNodeClass.clear();
1270 }
1271 
1275 void VuoNodeLibrary::updateColor(bool isDark)
1276 {
1277  QString titleTextColor = isDark ? "#303030" : "#808080";
1278  QString titleBackgroundColor = isDark ? "#919191" : "#efefef";
1279  QString dockwidgetBackgroundColor = isDark ? "#505050" : "#efefef";
1280  QString listBackgroundColor = isDark ? "#262626" : "#ffffff";
1281  QString scrollBarColor = isDark ? "#505050" : "#dfdfdf";
1282  QString focusRingColor = isDark ? "#1d6ae5" : "#74acec";
1283 
1284  setStyleSheet(VUO_QSTRINGIFY(
1285  QDockWidget {
1286  titlebar-close-icon: url(:/Icons/dockwidget-close-%3.png);
1287  font-size: 11px;
1288  border: none;
1289  color: %1;
1290  }
1291  QDockWidget::title {
1292  text-align: left;
1293  margin-left: -14px;
1294  background-color: %2;
1295  }
1296 
1297  // Hide it (Qt doesn't seem to support `display: none`).
1298  QDockWidget::float-button {
1299  top: -999px;
1300  left: -999px;
1301  }
1302  )
1303  .arg(titleTextColor)
1304  .arg(titleBackgroundColor)
1305  .arg(isDark ? "dark" : "light")
1306  );
1307 
1308  ui->splitter->setStyleSheet(VUO_QSTRINGIFY(
1309  QSplitter::handle {
1310  background: transparent;
1311  height: 6px;
1312  }
1313  QSplitter {
1314  background-color: %1;
1315  }
1316  )
1317  .arg(dockwidgetBackgroundColor)
1318  );
1319 
1320 
1321  QString nodeClassListStyle = VUO_QSTRINGIFY(
1323  background: %2;
1324  border-radius: 5px;
1325  color: #cacaca;
1326  border: 1px solid %1;
1327  padding: 1px;
1328  margin: 3px 2px 0 3px;
1329  }
1330  VuoNodeClassList:focus {
1331  background: %2;
1332  color: #cacaca;
1333  border: 2px solid %3;
1334  padding: 0;
1335  }
1336  QAbstractScrollArea::corner {
1337  border: none;
1338  }
1339  )
1340  .arg(dockwidgetBackgroundColor)
1341  .arg(listBackgroundColor)
1342  .arg(focusRingColor);
1343 
1345  nodeClassListStyle += VUO_QSTRINGIFY(
1346  QScrollBar {
1347  background: transparent;
1348  height: 10px;
1349  width: 10px;
1350  }
1351  QScrollBar::handle {
1352  background: %1;
1353  border-radius: 3px;
1354  min-width: 20px;
1355  min-height: 20px;
1356  margin: 2px;
1357  }
1358  QAbstractScrollArea::corner,
1359  QScrollBar::add-line,
1360  QScrollBar::sub-line,
1361  QScrollBar::add-page,
1362  QScrollBar::sub-page {
1363  background: transparent;
1364  border: none;
1365  }
1366  )
1367  .arg(scrollBarColor);
1368 
1369  ui->nodeClassList->setStyleSheet(nodeClassListStyle);
1370 
1371 
1372  QString nodePopoverPaneStyle = VUO_QSTRINGIFY(
1373  QScrollArea,
1374  QGraphicsView {
1375  background: %1;
1376  border: none;
1377  border-radius: 5px;
1378  margin: 0 2px 3px 3px;
1379  }
1380  )
1381  .arg(listBackgroundColor);
1382 
1384  nodePopoverPaneStyle += VUO_QSTRINGIFY(
1385  QScrollBar {
1386  background: transparent;
1387  height: 10px;
1388  width: 10px;
1389  }
1390  QScrollBar::handle {
1391  background: %1;
1392  border-radius: 3px;
1393  min-width: 20px;
1394  min-height: 20px;
1395  margin: 2px;
1396  }
1397  QAbstractScrollArea::corner,
1398  QScrollBar::add-line,
1399  QScrollBar::sub-line,
1400  QScrollBar::add-page,
1401  QScrollBar::sub-page {
1402  background: transparent;
1403  border: none;
1404  }
1405  )
1406  .arg(scrollBarColor);
1407 
1408  ui->nodePopoverPane->setStyleSheet(nodePopoverPaneStyle);
1409 
1410  if (isFloating())
1411  {
1412 #ifdef __APPLE__
1413  // This causes Qt to create an NSPanel instead of an NSWindow.
1414  setWindowFlags(windowFlags() | Qt::Dialog);
1415 
1416  id nsView = (id)winId();
1417  id nsWindow = objc_msgSend(nsView, sel_getUid("window"));
1418  unsigned long styleMask = 0;
1419  styleMask |= 1 << 0; // NSWindowStyleMaskTitled
1420  styleMask |= 1 << 1; // NSWindowStyleMaskClosable
1421  styleMask |= 1 << 3; // NSWindowStyleMaskResizable
1422  styleMask |= 1 << 4; // NSWindowStyleMaskUtilityWindow
1423  if (isDark)
1424  styleMask |= 1 << 13; // NSWindowStyleMaskHUDWindow
1425  objc_msgSend(nsWindow, sel_getUid("setStyleMask:"), styleMask);
1426 
1427  // https://b33p.net/kosada/node/15082
1428  objc_msgSend(nsWindow, sel_getUid("setMovableByWindowBackground:"), false);
1429 #endif
1430  }
1431 }