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