2 Help.tree and Help.gui - a scheme to allow UGens, no wait I mean ALL classes,
3 to be "self-classified" and provide a pleasant-ish browsing interface. No wait,
4 let's put all help docs into the tree too! Yeah!
7 with lots of input from Scott Wilson
8 and crossplatform tips from nescivi
20 classvar <global, categoriesSkipThese;
21 classvar <filterUserDirEntries, <>cachePath;
23 var tree, fileslist, <root;
27 cachePath = Platform.userAppSupportDir +/+ "SC_helptree.cache.txt";
28 categoriesSkipThese = [
29 Filter, BufInfoUGenBase, InfoUGenBase, MulAdd, BinaryOpUGen,
30 UnaryOpUGen, BasicOpUGen, MultiOutUGen, ChaosGen,
31 OutputProxy, AbstractOut, AbstractIn, Object, Class, UGen];
32 if(\SCImageFilter.asClass.notNil) {
33 categoriesSkipThese = categoriesSkipThese.add(\SCImageFilter.asClass)
35 filterUserDirEntries = [ "Extensions", "SuperCollider", "SuperCollider3", "Help", "svn", "share", "classes", "trunk", "Downloads", "Application Support" ];
36 [Platform.systemExtensionDir, Platform.userExtensionDir].do({ |path|
37 path.split(Platform.pathSeparator).do({ |dirname|
38 if(filterUserDirEntries.detect(_ == dirname).isNil) {
39 filterUserDirEntries = filterUserDirEntries.add(dirname);
47 if(root.isNil and: { global.notNil }) {
55 root = rootdir !? { rootdir.absolutePath };
56 if(root.isNil or: { root.size == 0 }) {
59 this.tree(false, false, false, root, false);
63 tree { |sysext=true, userext=true, allowCached=true, root, writeCache=true|
64 var classes, node, subc, helpRootLen;
65 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
66 var helpDirs = Array.new;
68 if(allowCached and: {tree.isNil and: {File.exists(this.cachePath)}}){
69 "Help tree read from cache in % seconds".format({
70 this.readTextArchive; // Attempt to read cached version
71 }.bench(false)).postln;
73 if(tree.isNil, { "Help files scanned in % seconds".format({
76 // Help file paths - will be used for categorising, if categories is nil or if not a class's helpfile.
77 // Otherwise they'll be stored for quick access to helpfile.
78 fileslist = IdentityDictionary.new;
80 helpDirs = helpDirs.add( Platform.helpDir );
82 helpDirs = helpDirs.add(root);
85 helpDirs = helpDirs.add( Platform.systemExtensionDir );
88 helpDirs = helpDirs.add( Platform.userExtensionDir );
91 // Now check each class's ".categories" response
92 classes = Object.allSubclasses.difference(categoriesSkipThese).reject({|c| c.asString.beginsWith("Meta_")});
94 // consider only classes whose main definition is in the root folder
95 classes = classes.select({ |c| c.filenameSymbol.asString.beginsWith(root) });
97 tree = Dictionary.new(8);
98 classes.do({|class| this.addCatsToTree(class, fileslist)});
100 // Now add the remaining ones to the tree - they're everything except the classes which
101 // have declared their own categorisation(s).
103 helpDirs.do{ |helpDir|
104 this.addDirTree( helpDir,tree );
108 this.writeTextArchive;
111 var sclang_completion_dict, dictfile;
112 // Also piggyback on the tree struct to write out an auto-completion dictionary:
113 sclang_completion_dict = SortedList.new;
115 if("0123456789".includes(key[0]).not and:{key.includes($ ).not}){
116 sclang_completion_dict.add(key);
119 dictfile = File(Platform.userAppSupportDir +/+ "sclang_completion_dict", "w");
121 sclang_completion_dict.do{|entry| dictfile.write(entry ++ Char.nl)};
126 }.bench(false)).postln});
130 findKeysForValue{|val|
131 var func, node, keyPath;
133 if( val.beginsWith( "SC://" ), { ^keyPath });
134 // only file scheme urls in tree
135 if( val.beginsWith( "file:" ), {
136 val = val.copyToEnd( if( val.copyRange( 5, 6 ) == "//", 7, 5 ));
138 val = val.replace("%20", " ");
139 func = {|dict, depth = 0|
140 node = dict.findKeyForValue(val);
142 dict.keysValuesDo({|key, item|
143 item.isKindOf(Dictionary).if({
144 keyPath = keyPath.copyFromStart(depth - 1).add(key);
145 func.value(item, depth + 1)
148 }, {^keyPath.add(node)});
150 func.value(this.tree);
154 addUserFilter{ |subpath|
155 filterUserDirEntries = filterUserDirEntries.add( subpath );
159 addDirTree{ |helppath,tree|
160 var helpExtensions = #['html', 'htm', 'scd', 'rtf', 'rtfd'];
162 var node, subc, helpRootLen, thisHelpExt;
164 subfileslist = IdentityDictionary.new;
166 PathName.new(helppath.standardizePath).helpFilesDo({|pathname|
167 if( pathname.fullPath.contains("ignore")){
168 "Not ignoring: %".format(pathname.fullPath).postln;
170 if( pathname.fullPath.contains("3vs2").not
171 and: { pathname.fullPath.contains("help-scripts").not }
173 subfileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
174 fileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
178 helpRootLen = (helppath.standardizePath).size + 1;
179 subfileslist.keysValuesDo({ |classsym, path|
181 subc = path[helpRootLen..].split(Platform.pathSeparator);
182 if ( helppath == Platform.helpDir,
184 subc = subc[0..subc.size-2]; // Ignore "Help" and the filename at the end
185 // subc = path[helpRootLen..].split(Platform.pathSeparator);
187 //helpRootLen = "~".standardizePath;
188 if ( helppath == Platform.systemExtensionDir,
190 // subc = path[helpRootLen..].split(Platform.pathSeparator);
191 subc = [ "SystemExtensions" ] ++ subc;
194 if ( helppath == Platform.userExtensionDir,
196 helpRootLen = "~/".absolutePath.size; // + 1;
197 subc = path[helpRootLen..].split(Platform.pathSeparator);
198 subc = [ "UserExtensions" ] ++ subc;
199 // check for common superfluous names that may confuse the categorisation;
200 filterUserDirEntries.do{ |spath|
201 subc = subc.reject{ |it|
205 // check for double entries (e.g. SwingOSC)
206 subc[..subc.size-2].do{ |it,i|
208 subset = subc[..i-1];
209 if ( subset.detect( { |jt| jt == it } ).size > 0, {
210 subc = subc[..i-1] ++ subc[i+1..];
214 if ( helppath == root,
216 // exactly the same as the previous if,
217 // but it isn't right to change helpRootLen
218 // check for common superfluous names that may confuse the categorisation;
219 filterUserDirEntries.do{ |spath|
220 subc = subc.reject{ |it|
224 // check for double entries (e.g. SwingOSC)
225 subc[..subc.size-2].do{ |it,i|
227 subset = subc[..i-1];
228 if ( subset.detect( { |jt| jt == it } ).size > 0, {
229 subc = subc[..i-1] ++ subc[i+1..];
233 subc = subc[..subc.size-2];
237 thisHelpExt = helpExtensions.select{ |ext|
238 subc.last.endsWith("."++ext)
241 if ( thisHelpExt.size > 0 , {
242 subc = subc[..subc.size-2];
245 subc = subc.collect({|i| "[["++i++"]]"});
247 // Crawl up the tree, creating hierarchy as needed
249 if(node[catname].isNil, {
250 node[catname] = Dictionary.new(3);
252 node = node[catname];
254 // "node" should now be the tiniest branch
255 node[classsym.asClass ? classsym] = path;
262 this.tree(allowCached:false);
264 this.tree(false, false, false, root, false);
272 dumpTree { |node, prefix=""|
273 node = node ?? {this.tree};
274 node.keysValuesDo({ |key, val|
275 if(val.isKindOf(Dictionary), {
276 (prefix + key).postln;
277 this.dumpTree(val, prefix ++ " ");
279 (prefix + key ++":" + val).postln;
284 addCatsToTree { |class, fileslist|
287 if(class.categories.isNil.not, {
288 class.categories.do({|cat|
289 subc = cat.split($>).collect({|i| "[["++i++"]]"});
291 // Crawl up the tree, creating hierarchy as needed
293 if(node[catname].isNil, {
294 node[catname] = Dictionary.new(3);
296 node = node[catname];
298 // "node" should now be the tiniest branch
299 node[class] = fileslist[class.asSymbol] ? "";
302 // Class has been added to list so we're OK
303 fileslist.removeAt(class.asSymbol);
308 writeTextArchive{ |path|
310 if(path.isNil){ path = this.cachePath };
311 fp = File(path, "w");
312 this.prRecurseTreeToFile(fp, this.tree); // Must use this.tree - will create if not exists
313 fp.write(fileslist.asTextArchive);
317 prRecurseTreeToFile{ | fp, treeBit, numtabs=0 |
318 fp.write("%%\n".format($\t.dup(numtabs).join, treeBit.size));
319 treeBit.keysValuesDo{| key, val |
320 fp.write("%%\n".format($\t.dup(numtabs).join, key.asString));
321 if(val.isKindOf(Dictionary)){
322 this.prRecurseTreeToFile(fp, val, numtabs + 1)
324 fp.write("%%\n".format($\t.dup(numtabs).join, val.asString));
329 readTextArchive{ |path|
330 var fp, filesliststr;
331 if(path.isNil){ path = this.cachePath };
332 fp = File(path, "r");
334 tree = this.prRecurseTreeFromFile(fp);
336 "Failure to read tree in Help.treeFromFile(%)".format(path).warn;
340 filesliststr = fp.readAllString;
341 if(filesliststr.size < 10){ // Old version of cache didn't store it...
342 "Help.tree cache has no fileslisttree. Will regenerate it".warn;
345 fileslist = filesliststr.interpret;
348 "Failure to read fileslist in Help.treeFromFile(%)".format(path).warn;
354 prRecurseTreeFromFile{ | fp, numtabs=0 |
355 var line, numentries, dict, key;
356 line = fp.getLine[numtabs..];
357 numentries = line.asInteger;
358 dict = Dictionary.new(numentries);
360 line = fp.getLine[numtabs..];
362 if(key[0]==$[ and:{ key[1]==$[ }){
363 // starting with [[ is indicator of being a category
364 dict[key] = this.prRecurseTreeFromFile(fp, numtabs+1);
366 line = fp.getLine[numtabs..];
368 key = key.asClass ? key; // Classes become classes, topics stay as symbols
375 gui { |sysext=true,userext=true, allowCached=true|
376 var classes, win, lists, listviews, numcols=7, selecteditem, node, newlist, curkey;
377 var selectednodes, scrollView, compView, textView, keys;
378 var classButt, browseButt, bwdButt, fwdButt;
379 var isClass, history = [], historyIdx = 0, fBwdFwd, fHistoryDo, fHistoryMove;
380 var screenBounds, bounds, textViewBounds, results, resultsview, statictextloc;
381 var searchField, helpguikeyacts, fSelectTreePath, inPathSelect = false, fUpdateWinTitle, fLoadError;
383 // Call to ensure the tree has been built
384 this.tree( sysext, userext, allowCached );
387 screenBounds = Window.screenBounds;
388 bounds = Rect(128, 264, 1040, 564);
389 bounds = bounds.center_(screenBounds.center);
390 bounds = bounds.sect(screenBounds.insetBy(15));
391 win = Window.new("Help browser", bounds); // SCWindow
392 // scrollView and compView hold the category-browsing list widgets
393 scrollView = ScrollView.new(win, Rect(5, 0, 425, 529)).hasBorder_(true).resize_(4);
394 compView = CompositeView.new(scrollView, Rect(0, 0, numcols * 200, /*504*/ bounds.height-60));
395 // textView displays a help file "inline"
396 textViewBounds = Rect(435, 0, /*620*/bounds.width-435, /*554*/ bounds.height-35);
397 textView = TextView.new(win, textViewBounds)
398 .hasVerticalScroller_(true)
399 .hasHorizontalScroller_(false)
400 .autohidesScrollers_(false)
404 if(GUI.current.id == \swing, { textView.editable_( false ).canFocus_( true ) });
406 textView.bounds = textView.bounds; // hack to fix origin on first load
408 // hidden at first, this will receive search results when the search field is activated
409 resultsview = ScrollView(win, textViewBounds)
413 // updates the history arrow buttons
415 bwdButt.enabled = historyIdx > 0;
416 fwdButt.enabled = historyIdx < (history.size - 1);
419 fLoadError = { |error|
421 "\n\nA discrepancy was found in the help tree.".postln;
423 "rm \"%\"".format(cachePath).unixCmd;
424 "The help tree cache may be out of sync with the file system. Rebuilding cache. Please reopen the Help GUI when this is finished.".postln;
428 "Please report the above error dump on the sc-users mailing list.".postln;
432 // cuts the redo history, adds and performs a new text open action
433 fHistoryDo = { arg selector, argum;
434 history = history.copyFromStart( historyIdx ).add([ selector, argum ]);
435 historyIdx = history.size - 1;
436 try({ textView.perform( selector, argum ) }, fLoadError);
440 // moves relatively in the history, and performs text open action
441 fHistoryMove = { arg incr; var entry;
442 historyIdx = historyIdx + incr;
443 entry = history[ historyIdx ];
444 try({ textView.perform( entry[ 0 ], entry[ 1 ]) }, fLoadError);
448 // keep this check for compatibility with old versions of swingOSC
449 if( textView.respondsTo( \linkAction ), {
451 .linkAction_({ arg view, url, descr;
454 //fHistoryDo.value( \open, url );
455 keys = this.findKeysForValue(url);
457 ("Invalid hyperlink:" + url + "Please repair this.\nSearching help directories for alternative.").warn;
458 url = Help.findHelpFile(url.basename.splitext.first);
459 url.notNil.if({keys = this.findKeysForValue(url)});
462 fSelectTreePath.value(keys.drop(-1), keys.last.asString);
465 if( descr.beginsWith( "SC://" ), {
466 fHistoryDo.value( \open, descr );
472 lists = Array.newClear(numcols);
473 lists[0] = tree.keys(Array).collect(_.asString).sort;
474 selectednodes = Array.newClear(numcols);
477 listviews = (0..numcols-1).collect({ arg index; var view;
478 view = ListView( compView, Rect( 5 + (index * 200), 4, 190, /* 504 */ bounds.height - 60 ));
479 //view.items = []; // trick me into drawing correctly in scrollview
480 if( view.respondsTo( \allowsDeselection ), {
481 view.allowsDeselection_( true ).value_( nil );
485 .action_({ arg lv; var lv2;
486 if( lv.value.notNil, {
487 // We've clicked on a category or on a class
489 if((lv.items.size != 0), {
490 lv2 = if( index < (listviews.size - 1), { listviews[ index + 1 ]});
492 selecteditem = lists[index][lv.value];
494 // Clear the GUI for the subsequent panels
495 listviews[index+1..].do({ arg lv; lv.items=#[];
496 if( lv.respondsTo( \allowsDeselection ), { lv.value = nil })});
499 // Get the current node, from the parent node
500 node = try { if(index==0, tree, {selectednodes[index-1]})[selecteditem] };
501 curkey = selecteditem;
502 selectednodes[index] = node;
505 // We have a "leaf" (class or helpdoc), since no keys found
507 if( (index + 1 < lists.size), { lists[index+1] = #[] });
509 if(inPathSelect.not, {
511 // Note: the "isClosed" check is to prevent errors caused by event triggering while user closing window
512 if(textView.isClosed.not){textView.visible = true};
513 if(resultsview.isClosed.not){resultsview.visible = false};
514 fHistoryDo.value( \open, fileslist.at( selecteditem.asSymbol ) ? fileslist.at( \Help ));
517 isClass = selecteditem.asSymbol.asClass.notNil;
518 // Note: "Help" class is not the class that matches "Help.html", so avoid potential confusion via special case
519 if(classButt.notNil){
520 classButt.enabled_((selecteditem!="Help") and: {isClass});
522 browseButt.enabled_((selecteditem!="Help") and: {isClass});
523 // The "selectednodes" entry for the leaf, is the path to the helpfile (or "")
524 selectednodes[index] = try { if(index==0, {tree}, {selectednodes[index-1]})
525 [curkey.asSymbol.asClass ? curkey.asSymbol]};
527 fUpdateWinTitle.value;
529 // We have a category on our hands
531 lists[ index + 1 ] = node.keys(Array).collect(_.asString).sort({|a,b|
533 // a and b both start with open-bracket:
534 // test result should be a < b
535 // or one starts with open-bracket and the other doesn't (xor)
536 // test result should be whether it's a that has the bracket
537 if(a[0] == $[ /*]*/ xor: (b[0] == $[ /*]*/)) {
543 lv2.items = lists[index+1];
548 if( (index + 1) < listviews.size, {
549 listviews[index+1].value = if( listviews[index+1].respondsTo( \allowsDeselection ).not, 1 );
550 listviews[index+1].valueAction_( 0 );
552 selectednodes[index+2 ..] = nil; // Clear out the now-unselected
558 listviews[0].items = lists[0];
560 // Add keyboard navigation between columns
561 listviews.do({ |lv, index| // SCView
562 lv.keyDownAction_({|view,char,modifiers,unicode,keycode|
567 63234, { if(index > 0, { lv2 = listviews[ index - 1 ]; lv2.focus; nowFocused = lv2 })
570 63235, { if( index < (listviews.size - 1) and: { listviews[ index + 1 ].items.notNil }, {
571 lv2 = listviews[ index + 1 ];
573 lv2.value_( if( lv2.respondsTo( \allowsDeselection ).not, - 1 )).valueAction_( 0 ).focus;
578 13, { // Hit RETURN to open source or helpfile
579 // The class name, or helpfile name we're after
581 if(lv.value.notNil and: {if(index==0, tree, {selectednodes[index-1]})[lists[index][lv.value]].isNil}, {
582 { selecteditem.openHelpFile }.defer;
587 // Returning nil is supposed to be sufficient to trigger the default action,
588 // but on my SC this doesn't happen.
589 view.defaultKeyDownAction(char,modifiers,unicode);
591 if(scrollView.visibleOrigin.x > nowFocused.bounds.left or: {scrollView.visibleOrigin.x + scrollView.bounds.width > nowFocused.bounds.left}, {
592 scrollView.visibleOrigin_(Point(nowFocused.bounds.left - 5, 0));
595 .mouseDownAction_({|view, x, y, modifiers, buttonNumber, clickCount|
597 if(lists[index][lv.value][0]==$[, {
598 if(scrollView.visibleOrigin.x != (lv.bounds.left - 5), {
600 10.do({|i| { scrollView.visibleOrigin_(
601 Point(((lv.bounds.left - lv.bounds.width)+((10+i)*10)-5), 0))
608 }.defer(0.01); // defer because .action above needs to register the new index
610 if(clickCount == 2, {
611 if(lv.value.notNil and: { try { if(index==0, tree, {selectednodes[index-1]})[lists[index][lv.value]] }.isNil}, {
612 { selecteditem.openHelpFile }.defer;
618 // Add ability to programmatically select an item in a tree
619 fSelectTreePath = { | catpath, leaf |
624 catpath.do{ |item, index|
625 foundIndex = listviews[index].items.indexOfEqual(item);
626 if(foundIndex.notNil){
627 listviews[index].value_(foundIndex).doAction;
629 "Could not select menu list item % in %".format(item, listviews[index].items).postln;
633 inPathSelect = false;
634 foundIndex = listviews[catpath.size].items.indexOfEqual(leaf);
635 if(foundIndex.notNil){
636 listviews[catpath.size].value_(foundIndex).doAction;
637 // history = history.drop(-1);
638 // historyIdx = history.size - 1;
640 "Could not select menu list item %".format(leaf).postln;
642 textView.visible = true;
643 resultsview.visible = false;
644 fUpdateWinTitle.value;
651 (["Help browser"] ++ listviews.collect{|lv| lv.value !? {lv.items[lv.value]} }.reject(_.isNil)).join(" > ") );
654 Platform.case(\windows, {
655 // TEMPORARY WORKAROUND:
656 // At present, opening text windows from GUI code can cause crashes on Psycollider
657 // (thread safety issue?). To work around this we just remove those buttons.
659 Button.new( win, Rect( 5, /* 534 */ bounds.height - 30, 110, 20 ))
660 .states_([["Open Help File", Color.black, Color.clear]])
662 .action_({{ selecteditem.openHelpFile }.defer;});
663 classButt = Button.new( win, Rect( 119, /* 534 */ bounds.height - 30, 110, 20 ))
664 .states_([["Open Class File", Color.black, Color.clear]])
667 if(selecteditem.asSymbol.asClass.notNil, {
668 {selecteditem.asSymbol.asClass.openCodeFile }.defer;
672 browseButt = Button.new( win, Rect( 233, /* 534 */ bounds.height - 30, 110, 20 ))
673 .states_([["Browse Class", Color.black, Color.clear]])
676 if(selecteditem.asSymbol.asClass.notNil, {
677 {selecteditem.asSymbol.asClass.browse }.defer;
680 bwdButt = Button.new( win, Rect( 347, /* 534 */ bounds.height - 30, 30, 20 ))
684 if( historyIdx > 0, {
685 fHistoryMove.value( -1 );
688 fwdButt = Button.new( win, Rect( 380, /* 534 */ bounds.height - 30, 30, 20 ))
692 if( historyIdx < (history.size - 1), {
693 fHistoryMove.value( 1 );
698 // textfield for searching:
699 statictextloc = Rect(10, 10, textViewBounds.width-20, 200);
700 StaticText.new(win, Rect(435, bounds.height-35, 100 /* bounds.width-435 */, 35))
701 .align_(\right).resize_(7).string_("Search help files:");
702 searchField = TextField.new(win, Rect(535, bounds.height-35, bounds.width-535-35, 35).insetBy(8))
703 .resize_(8).action_({|widget|
705 if(widget.value != ""){
707 // hide the textView, show the resultsview, do a query
708 textView.visible = false;
709 resultsview.visible = true;
710 resultsview.removeAll;
711 results = this.search(widget.value);
712 // Now add the results!
713 StaticText(resultsview, Rect(0, 0, textViewBounds.width / 2, 30))
716 .string_("% results found for query '%'.".format(results.size, widget.value));
717 Button(resultsview, Rect(textViewBounds.width / 2, 0, 100, 30).insetBy(5))
719 .states_([["Clear"]])
720 .action_({ searchField.valueAction_("") })
722 results.do{|res, index|
723 res.drawRow(resultsview, Rect(0, index*30 + 30, textViewBounds.width, 30),
724 // Add an action that uses the gui itself:
725 { fSelectTreePath.(res.catpath, res.docname) }
730 // Empty query string, go back to textView
731 textView.visible = true;
732 resultsview.visible = false;
737 // Handle some "global" (for the Help gui) key actions
738 helpguikeyacts = {|view, char, modifiers, unicode, keycode|
739 if((modifiers & (262144 | 1048576)) != 0){ // cmd or control key is pressed
747 listviews[0].valueAction_(listviews[0].items.find(["Help"]));
748 scrollView.visibleOrigin_(0@0);
754 win.view.addAction(helpguikeyacts, \keyUpAction);
757 // This is done to prevent Cmd+W winclose from trying to do things in vanishing textviews!
763 if(listviews[0].items.detect({ |item| item == "Help" }).notNil) {
764 fSelectTreePath.([], "Help"); // Select the "Help" entry in the root
765 selecteditem = "Help";
767 selecteditem = listviews[0].items.first;
768 fSelectTreePath.([], selecteditem);
774 // ^this.new("Help/").dumpToDoc("all-helpfiles");
776 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
777 var str = CollStream.new;
778 doc = Document.new("all-helpfiles");
780 Platform.systemExtensionDir,
781 Platform.userExtensionDir
783 PathName.new( it ).foldersWithoutSVN.do{ |folderPn|
784 str << folderPn.fullPath << Char.nl;
785 folderPn.helpFilesDo { |filePn|
787 filePn.fileNameWithoutExtension << Char.nl;
791 doc.string = str.collection;
794 // Iterates the tree, finding the help-doc paths and calling action.value(docname, path)
796 this.pr_do(action, this.tree, []);
798 pr_do { |action, curdict, catpath|
799 curdict.keysValuesDo{|key, val|
800 if(val.class == Dictionary){
801 this.pr_do(action, val, catpath ++ [key]) // recurse
803 action.value(key.asString, val, catpath)
809 this.deprecated(thisMethod, Meta_Help.findRespondingMethodFor(\gui));
813 // Returns an array of hits as HelpSearchResult instances
814 search { |query, ignoreCase=true|
815 var results = List.new, file, ext, docstr, pos;
816 this.do{ |docname, path, catpath|
818 if(docname.find(query, ignoreCase).notNil){
819 results.add(HelpSearchResult(docname, path, 100 / (docname.size - query.size + 1), "", catpath.deepCopy));
821 ext = path.splitext[1];
822 // OK, let's open the document, see if it contains the string... HEAVY!
823 file = File(path, "r");
826 "html", {file.readAllStringHTML},
827 "htm", {file.readAllStringHTML},
828 "rtf", {file.readAllStringRTF},
832 pos = docstr.findAll(query, ignoreCase);
834 results.add(HelpSearchResult(docname, path, pos.size, docstr[pos[0] .. pos[0]+50], catpath.deepCopy));
837 "Help.search will rebuild help cache, since an expected file was not found: %".format(path).postln;
839 // File:isOpen failure means that the help tree cache is out of date.
840 // So let's explicitly destroy and recreate it.
841 // There may be a danger of infinite loops if some weird filesystem stuff is happening...?
843 this.tree(allowCached: false);
844 ^this.search(query, ignoreCase);
848 //"empty path: %".format(docname).postln;
851 results = results.sort;
855 // This iterates the Help.tree to find the file. Can be used instead of platform-specific approaches
858 this.do { |key, path|
859 if(key == str and: { path.size > 0 }) { ^path }
864 // does the same as findHelpFile, but replaces the string with "Help" if the string is empty. This makes it possible in sced to open the main help if nothing is selected.
865 findHelpFileOrElse { |str|
867 if ( str.isEmpty ) { str = "Help" };
868 ^Help.findHelpFile( str );
871 // class method interface
873 *tree { |sysext = true, userext = true, allowCached = true| ^global.tree(sysext, userext, allowCached) }
874 *addUserFilter { |subpath| ^global.addUserFilter(subpath) }
875 *addDirTree { |helppath, tree| ^global.addDirTree(helppath, tree) }
876 *rebuildTree { ^global.rebuildTree }
877 *forgetTree { ^global.forgetTree }
878 *dumpTree { |node, prefix = ""| ^global.dumpTree(node, prefix) }
879 *addCatsToTree { |class, fileslist| ^global.addCatsToTree(class, fileslist) }
881 *do { |action| ^global.do(action) }
882 *pr_do { |action, curdict| ^global.pr_do(action, curdict) }
883 *searchGUI { ^global.searchGUI }
884 *search { |query, ignoreCase| ^global.search(query, ignoreCase) }
885 *findHelpFile { |str| ^global.findHelpFile(str) }
886 *makeHelp { |undocumentedObject, path| ^global.makeHelp(undocumentedObject, path) }
887 *makeAutoHelp { |andocumentedClass, path| ^global.makeAutoHelp(andocumentedClass, path) }
889 // instance-based getters
890 filterUserDirEntries { ^filterUserDirEntries }
891 cachePath { ^cachePath }
896 var <>docname, <>path, <>goodness, <>context, <>catpath;
897 *new{|docname, path, goodness, context, catpath|
898 ^this.newCopyArgs(docname, path, goodness, context, catpath);
902 ^ "HelpSearchResult(%, %, %, %)".format(docname, path.basename, goodness, this.contextTrimmed)
906 ^ this.goodness >= that.goodness
910 ^context.tr($\n, $ ).tr($\t, $ )
913 drawRow { |parent, bounds, action|
915 Button.new(parent, bounds.copy.setExtent(bounds.width * 0.3, bounds.height).insetBy(5, 5))
916 .states_([[docname]]).action_(action ? { path.openHTMLFile });
918 StaticText.new(parent, bounds.copy.setExtent(bounds.width * 0.7, bounds.height)
919 .moveBy(bounds.width * 0.3, 0)
921 .string_(this.contextTrimmed);
929 // Classes may override this to specify where they fit in a thematic classification,
930 // if they want to classify differently than the help doc location would indicate.
932 // Multiple categorisations are allowed (hence the array).
934 // Extension libs (which won't automatically get allocated, since their help won't be in the main
935 // help tree) SHOULD override this to specify where best to fit.
936 // (Note: *Please* use the "Libraries" and/or "UGens" main classifications, those are the best
937 // places for users to find extensions docs. Don't add new "root" help categories, that's
938 // not good for GUI usability.)
940 // Each categorisation should be a string using ">" marks to separate categories from subcategories.
941 // For examples see (e.g.) SinOsc, Gendy1, LPF, Integrator, EnvGen
942 //*categories { ^ #["Unclassified"] }
943 *categories { ^ nil }
948 *categories { ^ #["Streams-Patterns-Events>Patterns"] }
951 // This allows it to be called from sclang menu item