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
22 cachePath = Platform.userAppSupportDir +/+ "SC_helptree.cache.txt";
23 categoriesSkipThese = [
24 Filter, BufInfoUGenBase, InfoUGenBase, MulAdd, BinaryOpUGen,
25 UnaryOpUGen, BasicOpUGen, MultiOutUGen, ChaosGen,
26 OutputProxy, AbstractOut, AbstractIn, Object, Class, UGen];
27 if(\SCImageFilter.asClass.notNil) {
28 categoriesSkipThese = categoriesSkipThese.add(\SCImageFilter.asClass)
30 filterUserDirEntries = [ "Extensions", "SuperCollider", "SuperCollider3", "Help", "svn", "share", "classes", "trunk", "Downloads", "Application Support" ];
31 [Platform.systemExtensionDir, Platform.userExtensionDir].do({ |path|
32 path.split(Platform.pathSeparator).do({ |dirname|
33 if(filterUserDirEntries.detect(_ == dirname).isNil) {
34 filterUserDirEntries = filterUserDirEntries.add(dirname);
42 if(root.isNil and: { global.notNil }) {
50 root = rootdir !? { rootdir.absolutePath };
51 if(root.isNil or: { root.size == 0 }) {
54 this.tree(false, false, false, root, false);
58 tree { |sysext=true, userext=true, allowCached=true, root, writeCache=true|
59 var classes, node, subc, helpRootLen;
60 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
61 var helpDirs = Array.new;
63 if(allowCached and: {tree.isNil and: {File.exists(this.cachePath)}}){
64 "Help tree read from cache in % seconds".format({
65 this.readTextArchive; // Attempt to read cached version
66 }.bench(false)).postln;
68 if(tree.isNil, { "Help files scanned in % seconds".format({
71 // Help file paths - will be used for categorising, if categories is nil or if not a class's helpfile.
72 // Otherwise they'll be stored for quick access to helpfile.
73 fileslist = IdentityDictionary.new;
75 helpDirs = helpDirs.add( Platform.helpDir );
77 helpDirs = helpDirs.add(root);
80 helpDirs = helpDirs.add( Platform.systemExtensionDir );
83 helpDirs = helpDirs.add( Platform.userExtensionDir );
86 // Now check each class's ".categories" response
87 classes = Object.allSubclasses.difference(categoriesSkipThese).reject({|c| c.asString.beginsWith("Meta_")});
89 // consider only classes whose main definition is in the root folder
90 classes = classes.select({ |c| c.filenameSymbol.asString.beginsWith(root) });
92 tree = Dictionary.new(8);
93 classes.do({|class| this.addCatsToTree(class, fileslist)});
95 // Now add the remaining ones to the tree - they're everything except the classes which
96 // have declared their own categorisation(s).
98 helpDirs.do{ |helpDir|
99 this.addDirTree( helpDir,tree );
103 this.writeTextArchive;
106 var sclang_completion_dict, dictfile;
107 // Also piggyback on the tree struct to write out an auto-completion dictionary:
108 sclang_completion_dict = SortedList.new;
110 if("0123456789".includes(key[0]).not and:{key.includes($ ).not}){
111 sclang_completion_dict.add(key);
114 dictfile = File(Platform.userAppSupportDir +/+ "sclang_completion_dict", "w");
116 sclang_completion_dict.do{|entry| dictfile.write(entry ++ Char.nl)};
121 }.bench(false)).postln});
125 findKeysForValue{|val|
126 var func, node, keyPath;
128 if( val.beginsWith( "SC://" ), { ^keyPath });
129 // only file scheme urls in tree
130 if( val.beginsWith( "file:" ), {
131 val = val.copyToEnd( if( val.copyRange( 5, 6 ) == "//", 7, 5 ));
133 val = val.replace("%20", " ");
134 func = {|dict, depth = 0|
135 node = dict.findKeyForValue(val);
137 dict.keysValuesDo({|key, item|
138 item.isKindOf(Dictionary).if({
139 keyPath = keyPath.copyFromStart(depth - 1).add(key);
140 func.value(item, depth + 1)
143 }, {^keyPath.add(node)});
145 func.value(this.tree);
149 addUserFilter{ |subpath|
150 filterUserDirEntries = filterUserDirEntries.add( subpath );
154 addDirTree{ |helppath,tree|
155 var helpExtensions = #['html', 'htm', 'scd', 'rtf', 'rtfd'];
157 var node, subc, helpRootLen, thisHelpExt;
159 subfileslist = IdentityDictionary.new;
161 PathName.new(helppath.standardizePath).helpFilesDo({|pathname|
162 if( pathname.fullPath.contains("ignore")){
163 "Not ignoring: %".format(pathname.fullPath).postln;
165 if( pathname.fullPath.contains("3vs2").not
166 and: { pathname.fullPath.contains("help-scripts").not }
168 subfileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
169 fileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
173 helpRootLen = (helppath.standardizePath).size + 1;
174 subfileslist.keysValuesDo({ |classsym, path|
176 subc = path[helpRootLen..].split(Platform.pathSeparator);
177 if ( helppath == Platform.helpDir,
179 subc = subc[0..subc.size-2]; // Ignore "Help" and the filename at the end
180 // subc = path[helpRootLen..].split(Platform.pathSeparator);
182 //helpRootLen = "~".standardizePath;
183 if ( helppath == Platform.systemExtensionDir,
185 // subc = path[helpRootLen..].split(Platform.pathSeparator);
186 subc = [ "SystemExtensions" ] ++ subc;
189 if ( helppath == Platform.userExtensionDir,
191 helpRootLen = "~/".absolutePath.size; // + 1;
192 subc = path[helpRootLen..].split(Platform.pathSeparator);
193 subc = [ "UserExtensions" ] ++ subc;
194 // check for common superfluous names that may confuse the categorisation;
195 filterUserDirEntries.do{ |spath|
196 subc = subc.reject{ |it|
200 // check for double entries (e.g. SwingOSC)
201 subc[..subc.size-2].do{ |it,i|
203 subset = subc[..i-1];
204 if ( subset.detect( { |jt| jt == it } ).size > 0, {
205 subc = subc[..i-1] ++ subc[i+1..];
209 if ( helppath == root,
211 // exactly the same as the previous if,
212 // but it isn't right to change helpRootLen
213 // check for common superfluous names that may confuse the categorisation;
214 filterUserDirEntries.do{ |spath|
215 subc = subc.reject{ |it|
219 // check for double entries (e.g. SwingOSC)
220 subc[..subc.size-2].do{ |it,i|
222 subset = subc[..i-1];
223 if ( subset.detect( { |jt| jt == it } ).size > 0, {
224 subc = subc[..i-1] ++ subc[i+1..];
228 subc = subc[..subc.size-2];
232 thisHelpExt = helpExtensions.select{ |ext|
233 subc.last.endsWith("."++ext)
236 if ( thisHelpExt.size > 0 , {
237 subc = subc[..subc.size-2];
240 subc = subc.collect({|i| "[["++i++"]]"});
242 // Crawl up the tree, creating hierarchy as needed
244 if(node[catname].isNil, {
245 node[catname] = Dictionary.new(3);
247 node = node[catname];
249 // "node" should now be the tiniest branch
250 node[classsym.asClass ? classsym] = path;
257 this.tree(allowCached:false);
259 this.tree(false, false, false, root, false);
267 dumpTree { |node, prefix=""|
268 node = node ?? {this.tree};
269 node.keysValuesDo({ |key, val|
270 if(val.isKindOf(Dictionary), {
271 (prefix + key).postln;
272 this.dumpTree(val, prefix ++ " ");
274 (prefix + key ++":" + val).postln;
279 addCatsToTree { |class, fileslist|
282 if(class.categories.isNil.not, {
283 class.categories.do({|cat|
284 subc = cat.split($>).collect({|i| "[["++i++"]]"});
286 // Crawl up the tree, creating hierarchy as needed
288 if(node[catname].isNil, {
289 node[catname] = Dictionary.new(3);
291 node = node[catname];
293 // "node" should now be the tiniest branch
294 node[class] = fileslist[class.asSymbol] ? "";
297 // Class has been added to list so we're OK
298 fileslist.removeAt(class.asSymbol);
303 writeTextArchive{ |path|
305 if(path.isNil){ path = this.cachePath };
306 fp = File(path, "w");
307 this.prRecurseTreeToFile(fp, this.tree); // Must use this.tree - will create if not exists
308 fp.write(fileslist.asTextArchive);
312 prRecurseTreeToFile{ | fp, treeBit, numtabs=0 |
313 fp.write("%%\n".format($\t.dup(numtabs).join, treeBit.size));
314 treeBit.keysValuesDo{| key, val |
315 fp.write("%%\n".format($\t.dup(numtabs).join, key.asString));
316 if(val.isKindOf(Dictionary)){
317 this.prRecurseTreeToFile(fp, val, numtabs + 1)
319 fp.write("%%\n".format($\t.dup(numtabs).join, val.asString));
324 readTextArchive{ |path|
325 var fp, filesliststr;
326 if(path.isNil){ path = this.cachePath };
327 fp = File(path, "r");
329 tree = this.prRecurseTreeFromFile(fp);
331 "Failure to read tree in Help.treeFromFile(%)".format(path).warn;
335 filesliststr = fp.readAllString;
336 if(filesliststr.size < 10){ // Old version of cache didn't store it...
337 "Help.tree cache has no fileslisttree. Will regenerate it".warn;
340 fileslist = filesliststr.interpret;
343 "Failure to read fileslist in Help.treeFromFile(%)".format(path).warn;
349 prRecurseTreeFromFile{ | fp, numtabs=0 |
350 var line, numentries, dict, key;
351 line = fp.getLine[numtabs..];
352 numentries = line.asInteger;
353 dict = Dictionary.new(numentries);
355 line = fp.getLine[numtabs..];
357 if(key[0]==$[ and:{ key[1]==$[ }){
358 // starting with [[ is indicator of being a category
359 dict[key] = this.prRecurseTreeFromFile(fp, numtabs+1);
361 line = fp.getLine[numtabs..];
363 key = key.asClass ? key; // Classes become classes, topics stay as symbols
370 gui { |sysext=true,userext=true, allowCached=true|
371 var classes, win, lists, listviews, numcols=7, selecteditem, node, newlist, curkey;
372 var selectednodes, scrollView, compView, textView, keys;
373 var classButt, browseButt, bwdButt, fwdButt;
374 var isClass, history = [], historyIdx = 0, fBwdFwd, fHistoryDo, fHistoryMove;
375 var screenBounds, bounds, textViewBounds, results, resultsview, statictextloc;
376 var searchField, helpguikeyacts, fSelectTreePath, inPathSelect = false, fUpdateWinTitle, fLoadError;
378 // Call to ensure the tree has been built
379 this.tree( sysext, userext, allowCached );
382 screenBounds = Window.screenBounds;
383 bounds = Rect(128, 264, 1040, 564);
384 bounds = bounds.center_(screenBounds.center);
385 bounds = bounds.sect(screenBounds.insetBy(15));
386 win = Window.new("Help browser", bounds); // SCWindow
387 // scrollView and compView hold the category-browsing list widgets
388 scrollView = ScrollView.new(win, Rect(5, 0, 425, 529)).hasBorder_(true).resize_(4);
389 compView = CompositeView.new(scrollView, Rect(0, 0, numcols * 200, /*504*/ bounds.height-60));
390 // textView displays a help file "inline"
391 textViewBounds = Rect(435, 0, /*620*/bounds.width-435, /*554*/ bounds.height-35);
392 textView = TextView.new(win, textViewBounds)
393 .hasVerticalScroller_(true)
394 .hasHorizontalScroller_(false)
395 .autohidesScrollers_(false)
399 if(GUI.current.id == \swing, { textView.editable_( false ).canFocus_( true ) });
401 textView.bounds = textView.bounds; // hack to fix origin on first load
403 // hidden at first, this will receive search results when the search field is activated
404 resultsview = ScrollView(win, textViewBounds)
408 // updates the history arrow buttons
410 bwdButt.enabled = historyIdx > 0;
411 fwdButt.enabled = historyIdx < (history.size - 1);
414 fLoadError = { |error|
416 "\n\nA discrepancy was found in the help tree.".postln;
418 "rm \"%\"".format(cachePath).unixCmd;
419 "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;
423 "Please report the above error dump on the sc-users mailing list.".postln;
427 // cuts the redo history, adds and performs a new text open action
428 fHistoryDo = { arg selector, argum;
429 history = history.copyFromStart( historyIdx ).add([ selector, argum ]);
430 historyIdx = history.size - 1;
431 try({ textView.perform( selector, argum ) }, fLoadError);
435 // moves relatively in the history, and performs text open action
436 fHistoryMove = { arg incr; var entry;
437 historyIdx = historyIdx + incr;
438 entry = history[ historyIdx ];
439 try({ textView.perform( entry[ 0 ], entry[ 1 ]) }, fLoadError);
443 // keep this check for compatibility with old versions of swingOSC
444 if( textView.respondsTo( \linkAction ), {
446 .linkAction_({ arg view, url, descr;
449 //fHistoryDo.value( \open, url );
450 keys = this.findKeysForValue(url);
452 ("Invalid hyperlink:" + url + "Please repair this.\nSearching help directories for alternative.").warn;
453 url = Help.findHelpFile(url.basename.splitext.first);
454 url.notNil.if({keys = this.findKeysForValue(url)});
457 fSelectTreePath.value(keys.drop(-1), keys.last.asString);
460 if( descr.beginsWith( "SC://" ), {
461 fHistoryDo.value( \open, descr );
467 lists = Array.newClear(numcols);
468 lists[0] = tree.keys(Array).collect(_.asString).sort;
469 selectednodes = Array.newClear(numcols);
472 listviews = (0..numcols-1).collect({ arg index; var view;
473 view = ListView( compView, Rect( 5 + (index * 200), 4, 190, /* 504 */ bounds.height - 60 ));
474 //view.items = []; // trick me into drawing correctly in scrollview
475 if( view.respondsTo( \allowsDeselection ), {
476 view.allowsDeselection_( true ).value_( nil );
480 .action_({ arg lv; var lv2;
481 if( lv.value.notNil, {
482 // We've clicked on a category or on a class
484 if((lv.items.size != 0), {
485 lv2 = if( index < (listviews.size - 1), { listviews[ index + 1 ]});
487 selecteditem = lists[index][lv.value];
489 // Clear the GUI for the subsequent panels
490 listviews[index+1..].do({ arg lv; lv.items=#[];
491 if( lv.respondsTo( \allowsDeselection ), { lv.value = nil })});
494 // Get the current node, from the parent node
495 node = try { if(index==0, tree, {selectednodes[index-1]})[selecteditem] };
496 curkey = selecteditem;
497 selectednodes[index] = node;
500 // We have a "leaf" (class or helpdoc), since no keys found
502 if( (index + 1 < lists.size), { lists[index+1] = #[] });
504 if(inPathSelect.not, {
506 // Note: the "isClosed" check is to prevent errors caused by event triggering while user closing window
507 if(textView.isClosed.not){textView.visible = true};
508 if(resultsview.isClosed.not){resultsview.visible = false};
509 fHistoryDo.value( \open, fileslist.at( selecteditem.asSymbol ) ? fileslist.at( \Help ));
512 isClass = selecteditem.asSymbol.asClass.notNil;
513 // Note: "Help" class is not the class that matches "Help.html", so avoid potential confusion via special case
514 if(classButt.notNil){
515 classButt.enabled_((selecteditem!="Help") and: {isClass});
517 browseButt.enabled_((selecteditem!="Help") and: {isClass});
518 // The "selectednodes" entry for the leaf, is the path to the helpfile (or "")
519 selectednodes[index] = try { if(index==0, {tree}, {selectednodes[index-1]})
520 [curkey.asSymbol.asClass ? curkey.asSymbol]};
522 fUpdateWinTitle.value;
524 // We have a category on our hands
526 lists[ index + 1 ] = node.keys(Array).collect(_.asString).sort({|a,b|
528 // a and b both start with open-bracket:
529 // test result should be a < b
530 // or one starts with open-bracket and the other doesn't (xor)
531 // test result should be whether it's a that has the bracket
532 if(a[0] == $[ /*]*/ xor: (b[0] == $[ /*]*/)) {
538 lv2.items = lists[index+1];
543 if( (index + 1) < listviews.size, {
544 listviews[index+1].value = if( listviews[index+1].respondsTo( \allowsDeselection ).not, 1 );
545 listviews[index+1].valueAction_( 0 );
547 selectednodes[index+2 ..] = nil; // Clear out the now-unselected
553 listviews[0].items = lists[0];
555 // Add keyboard navigation between columns
556 listviews.do({ |lv, index| // SCView
557 lv.keyDownAction_({|view,char,modifiers,unicode,keycode|
562 63234, { if(index > 0, { lv2 = listviews[ index - 1 ]; lv2.focus; nowFocused = lv2 })
565 63235, { if( index < (listviews.size - 1) and: { listviews[ index + 1 ].items.notNil }, {
566 lv2 = listviews[ index + 1 ];
568 lv2.value_( if( lv2.respondsTo( \allowsDeselection ).not, - 1 )).valueAction_( 0 ).focus;
573 13, { // Hit RETURN to open source or helpfile
574 // The class name, or helpfile name we're after
576 if(lv.value.notNil and: {if(index==0, tree, {selectednodes[index-1]})[lists[index][lv.value]].isNil}, {
577 { selecteditem.help }.defer;
582 // Returning nil is supposed to be sufficient to trigger the default action,
583 // but on my SC this doesn't happen.
584 view.defaultKeyDownAction(char,modifiers,unicode);
586 if(scrollView.visibleOrigin.x > nowFocused.bounds.left or: {scrollView.visibleOrigin.x + scrollView.bounds.width > nowFocused.bounds.left}, {
587 scrollView.visibleOrigin_(Point(nowFocused.bounds.left - 5, 0));
590 .mouseDownAction_({|view, x, y, modifiers, buttonNumber, clickCount|
592 if(lists[index][lv.value][0]==$[, {
593 if(scrollView.visibleOrigin.x != (lv.bounds.left - 5), {
595 10.do({|i| { scrollView.visibleOrigin_(
596 Point(((lv.bounds.left - lv.bounds.width)+((10+i)*10)-5), 0))
603 }.defer(0.01); // defer because .action above needs to register the new index
605 if(clickCount == 2, {
606 if(lv.value.notNil and: { try { if(index==0, tree, {selectednodes[index-1]})[lists[index][lv.value]] }.isNil}, {
607 { selecteditem.help }.defer;
613 // Add ability to programmatically select an item in a tree
614 fSelectTreePath = { | catpath, leaf |
619 catpath.do{ |item, index|
620 foundIndex = listviews[index].items.indexOfEqual(item);
621 if(foundIndex.notNil){
622 listviews[index].value_(foundIndex).doAction;
624 "Could not select menu list item % in %".format(item, listviews[index].items).postln;
628 inPathSelect = false;
629 foundIndex = listviews[catpath.size].items.indexOfEqual(leaf);
630 if(foundIndex.notNil){
631 listviews[catpath.size].value_(foundIndex).doAction;
632 // history = history.drop(-1);
633 // historyIdx = history.size - 1;
635 "Could not select menu list item %".format(leaf).postln;
637 textView.visible = true;
638 resultsview.visible = false;
639 fUpdateWinTitle.value;
646 (["Help browser"] ++ listviews.collect{|lv| lv.value !? {lv.items[lv.value]} }.reject(_.isNil)).join(" > ") );
649 Platform.case(\windows, {
650 // TEMPORARY WORKAROUND:
651 // At present, opening text windows from GUI code can cause crashes on Psycollider
652 // (thread safety issue?). To work around this we just remove those buttons.
654 Button.new( win, Rect( 5, /* 534 */ bounds.height - 30, 110, 20 ))
655 .states_([["Open Help File", Color.black, Color.clear]])
657 .action_({{ selecteditem.help }.defer;});
658 classButt = Button.new( win, Rect( 119, /* 534 */ bounds.height - 30, 110, 20 ))
659 .states_([["Open Class File", Color.black, Color.clear]])
662 if(selecteditem.asSymbol.asClass.notNil, {
663 {selecteditem.asSymbol.asClass.openCodeFile }.defer;
667 browseButt = Button.new( win, Rect( 233, /* 534 */ bounds.height - 30, 110, 20 ))
668 .states_([["Browse Class", Color.black, Color.clear]])
671 if(selecteditem.asSymbol.asClass.notNil, {
672 {selecteditem.asSymbol.asClass.browse }.defer;
675 bwdButt = Button.new( win, Rect( 347, /* 534 */ bounds.height - 30, 30, 20 ))
679 if( historyIdx > 0, {
680 fHistoryMove.value( -1 );
683 fwdButt = Button.new( win, Rect( 380, /* 534 */ bounds.height - 30, 30, 20 ))
687 if( historyIdx < (history.size - 1), {
688 fHistoryMove.value( 1 );
693 // textfield for searching:
694 statictextloc = Rect(10, 10, textViewBounds.width-20, 200);
695 StaticText.new(win, Rect(435, bounds.height-35, 100 /* bounds.width-435 */, 35))
696 .align_(\right).resize_(7).string_("Search help files:");
697 searchField = TextField.new(win, Rect(535, bounds.height-35, bounds.width-535-35, 35).insetBy(8))
698 .resize_(8).action_({|widget|
700 if(widget.value != ""){
702 // hide the textView, show the resultsview, do a query
703 textView.visible = false;
704 resultsview.visible = true;
705 resultsview.removeAll;
706 results = this.search(widget.value);
707 // Now add the results!
708 StaticText(resultsview, Rect(0, 0, textViewBounds.width / 2, 30))
711 .string_("% results found for query '%'.".format(results.size, widget.value));
712 Button(resultsview, Rect(textViewBounds.width / 2, 0, 100, 30).insetBy(5))
714 .states_([["Clear"]])
715 .action_({ searchField.valueAction_("") })
717 results.do{|res, index|
718 res.drawRow(resultsview, Rect(0, index*30 + 30, textViewBounds.width, 30),
719 // Add an action that uses the gui itself:
720 { fSelectTreePath.(res.catpath, res.docname) }
725 // Empty query string, go back to textView
726 textView.visible = true;
727 resultsview.visible = false;
732 // Handle some "global" (for the Help gui) key actions
733 helpguikeyacts = {|view, char, modifiers, unicode, keycode|
734 if((modifiers & (262144 | 1048576)) != 0){ // cmd or control key is pressed
742 listviews[0].valueAction_(listviews[0].items.find(["Help"]));
743 scrollView.visibleOrigin_(0@0);
749 win.view.addAction(helpguikeyacts, \keyUpAction);
752 // This is done to prevent Cmd+W winclose from trying to do things in vanishing textviews!
758 if(listviews[0].items.detect({ |item| item == "Help" }).notNil) {
759 fSelectTreePath.([], "Help"); // Select the "Help" entry in the root
760 selecteditem = "Help";
762 selecteditem = listviews[0].items.first;
763 fSelectTreePath.([], selecteditem);
769 // ^this.new("Help/").dumpToDoc("all-helpfiles");
771 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
772 var str = CollStream.new;
773 doc = Document.new("all-helpfiles");
775 Platform.systemExtensionDir,
776 Platform.userExtensionDir
778 PathName.new( it ).foldersWithoutSVN.do{ |folderPn|
779 str << folderPn.fullPath << Char.nl;
780 folderPn.helpFilesDo { |filePn|
782 filePn.fileNameWithoutExtension << Char.nl;
786 doc.string = str.collection;
789 // Iterates the tree, finding the help-doc paths and calling action.value(docname, path)
791 this.pr_do(action, this.tree, []);
793 pr_do { |action, curdict, catpath|
794 curdict.keysValuesDo{|key, val|
795 if(val.class == Dictionary){
796 this.pr_do(action, val, catpath ++ [key]) // recurse
798 action.value(key.asString, val, catpath)
803 // Returns an array of hits as HelpSearchResult instances
804 search { |query, ignoreCase=true|
805 var results = List.new, file, ext, docstr, pos;
806 this.do{ |docname, path, catpath|
808 if(docname.find(query, ignoreCase).notNil){
809 results.add(HelpSearchResult(docname, path, 100 / (docname.size - query.size + 1), "", catpath.deepCopy));
811 ext = path.splitext[1];
812 // OK, let's open the document, see if it contains the string... HEAVY!
813 file = File(path, "r");
816 "html", {file.readAllStringHTML},
817 "htm", {file.readAllStringHTML},
818 "rtf", {file.readAllStringRTF},
822 pos = docstr.findAll(query, ignoreCase);
824 results.add(HelpSearchResult(docname, path, pos.size, docstr[pos[0] .. pos[0]+50], catpath.deepCopy));
827 "Help.search will rebuild help cache, since an expected file was not found: %".format(path).postln;
829 // File:isOpen failure means that the help tree cache is out of date.
830 // So let's explicitly destroy and recreate it.
831 // There may be a danger of infinite loops if some weird filesystem stuff is happening...?
833 this.tree(allowCached: false);
834 ^this.search(query, ignoreCase);
838 //"empty path: %".format(docname).postln;
841 results = results.sort;
845 // This iterates the Help.tree to find the file. Can be used instead of platform-specific approaches
848 this.do { |key, path|
849 if(key == str and: { path.size > 0 }) { ^path }
854 // 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.
855 findHelpFileOrElse { |str|
857 if ( str.isEmpty ) { str = "Help" };
858 ^Help.findHelpFile( str );
861 // class method interface
863 *tree { |sysext = true, userext = true, allowCached = true| ^global.tree(sysext, userext, allowCached) }
864 *addUserFilter { |subpath| ^global.addUserFilter(subpath) }
865 *addDirTree { |helppath, tree| ^global.addDirTree(helppath, tree) }
866 *rebuildTree { ^global.rebuildTree }
867 *forgetTree { ^global.forgetTree }
868 *dumpTree { |node, prefix = ""| ^global.dumpTree(node, prefix) }
869 *addCatsToTree { |class, fileslist| ^global.addCatsToTree(class, fileslist) }
871 *do { |action| ^global.do(action) }
872 *pr_do { |action, curdict| ^global.pr_do(action, curdict) }
873 *searchGUI { ^global.searchGUI }
874 *search { |query, ignoreCase| ^global.search(query, ignoreCase) }
875 *findHelpFile { |str| ^global.findHelpFile(str) }
876 *makeHelp { |undocumentedObject, path| ^global.makeHelp(undocumentedObject, path) }
877 *makeAutoHelp { |andocumentedClass, path| ^global.makeAutoHelp(andocumentedClass, path) }
879 // instance-based getters
880 filterUserDirEntries { ^filterUserDirEntries }
881 cachePath { ^cachePath }
889 // Classes may override this to specify where they fit in a thematic classification,
890 // if they want to classify differently than the help doc location would indicate.
892 // Multiple categorisations are allowed (hence the array).
894 // Extension libs (which won't automatically get allocated, since their help won't be in the main
895 // help tree) SHOULD override this to specify where best to fit.
896 // (Note: *Please* use the "Libraries" and/or "UGens" main classifications, those are the best
897 // places for users to find extensions docs. Don't add new "root" help categories, that's
898 // not good for GUI usability.)
900 // Each categorisation should be a string using ">" marks to separate categories from subcategories.
901 // For examples see (e.g.) SinOsc, Gendy1, LPF, Integrator, EnvGen
902 //*categories { ^ #["Unclassified"] }
903 *categories { ^ nil }
908 *categories { ^ #["Streams-Patterns-Events>Patterns"] }
911 // This allows it to be called from sclang menu item