class library: guard Document-*listener and Document-isListener
[supercollider.git] / SCClassLibrary / deprecated / deprecated-3.6-help / Help.sc
blob88b270727722e1be8e25c3299debab995aefaf25
1 /*
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!
6 By Dan Stowell, 2007
7 with lots of input from Scott Wilson
8 and crossplatform tips from nescivi
10 Try it:
11 Help.gui
13 Help.dumpTree
15 see also:
16 Class.browse
19 + Help {
20         *initClass {
21                 var     dir;
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)
29                 };
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);
35                                 };
36                         });
37                 });
38                 global = this.new;
39         }
41         *new { |root|
42                 if(root.isNil and: { global.notNil }) {
43                         ^global
44                 } {
45                         ^super.new.init(root)
46                 }
47         }
49         init { |rootdir|
50                 root = rootdir !? { rootdir.absolutePath };
51                 if(root.isNil or: { root.size == 0 }) {
52                         this.tree;
53                 } {
54                         this.tree(false, false, false, root, false);
55                 }
56         }
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;
62                 var thisHelpExt;
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;
67                 };
68                 if(tree.isNil, { "Help files scanned in % seconds".format({
69                         // Building the tree
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;
74                         if(root.isNil) {
75                                 helpDirs = helpDirs.add( Platform.helpDir );
76                         } {
77                                 helpDirs = helpDirs.add(root);
78                         };
79                         if ( sysext ,{
80                                 helpDirs = helpDirs.add( Platform.systemExtensionDir );
81                         });
82                         if ( userext ,{
83                                 helpDirs = helpDirs.add( Platform.userExtensionDir );
84                         });
86                         // Now check each class's ".categories" response
87                         classes = Object.allSubclasses.difference(categoriesSkipThese).reject({|c| c.asString.beginsWith("Meta_")});
88                         if(root.notNil) {
89                                         // consider only classes whose main definition is in the root folder
90                                 classes = classes.select({ |c| c.filenameSymbol.asString.beginsWith(root) });
91                         };
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 );
100                         };
101                         if(writeCache) {
102                                 {
103                                         this.writeTextArchive;
104                                 }.defer(4.312);
105                                 {
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;
109                                         this.do{|key, value|
110                                                 if("0123456789".includes(key[0]).not and:{key.includes($ ).not}){
111                                                         sclang_completion_dict.add(key);
112                                                 }
113                                         };
114                                         dictfile = File(Platform.userAppSupportDir +/+ "sclang_completion_dict", "w");
115                                         if(dictfile.isOpen){
116                                                 sclang_completion_dict.do{|entry| dictfile.write(entry ++ Char.nl)};
117                                                 dictfile.close;
118                                         };
119                                 }.defer(7.1101);
120                         };
121                 }.bench(false)).postln});
122                 ^tree;
123         }
125         findKeysForValue{|val|
126                 var func, node, keyPath;
127                 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 ));
132                 });
133                 val = val.replace("%20", " ");
134                 func = {|dict, depth = 0|
135                         node = dict.findKeyForValue(val);
136                         node.isNil.if({
137                                 dict.keysValuesDo({|key, item|
138                                         item.isKindOf(Dictionary).if({
139                                                 keyPath = keyPath.copyFromStart(depth - 1).add(key);
140                                                 func.value(item, depth + 1)
141                                         })
142                                 });
143                         }, {^keyPath.add(node)});
144                 };
145                 func.value(this.tree);
146                 ^[];
147         }
149         addUserFilter{ |subpath|
150                 filterUserDirEntries = filterUserDirEntries.add( subpath );
151                 this.forgetTree;
152         }
154         addDirTree{ |helppath,tree|
155                 var helpExtensions = #['html', 'htm', 'scd', 'rtf', 'rtfd'];
156                 var subfileslist;
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;
164                                 };
165                                 if( pathname.fullPath.contains("3vs2").not
166                                         and: { pathname.fullPath.contains("help-scripts").not }
167                                         , {
168                                                 subfileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
169                                                 fileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
170                                         })
171                         });
173                 helpRootLen = (helppath.standardizePath).size + 1;
174                 subfileslist.keysValuesDo({ |classsym, path|
176                         subc = path[helpRootLen..].split(Platform.pathSeparator);
177                         if ( helppath == Platform.helpDir,
178                                 {
179                                         subc = subc[0..subc.size-2]; // Ignore "Help" and the filename at the end
180 //                                      subc = path[helpRootLen..].split(Platform.pathSeparator);
181                                 },{
182                                         //helpRootLen = "~".standardizePath;
183                                         if ( helppath == Platform.systemExtensionDir,
184                                                 {
185 //                                                      subc = path[helpRootLen..].split(Platform.pathSeparator);
186                                                         subc = [ "SystemExtensions" ] ++ subc;
187                                                         //subc.postcs;
188                                                 });
189                                         if ( helppath == Platform.userExtensionDir,
190                                                 {
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|
197                                                                         it == spath;
198                                                                 };
199                                                         };
200                                                         // check for double entries (e.g. SwingOSC)
201                                                         subc[..subc.size-2].do{ |it,i|
202                                                                 var subset;
203                                                                 subset = subc[..i-1];
204                                                                 if ( subset.detect( { |jt| jt == it } ).size > 0, {
205                                                                         subc = subc[..i-1] ++ subc[i+1..];
206                                                                 });
207                                                         };
208                                                 });
209                                         if ( helppath == root,
210                                                 {
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|
216                                                                         it == spath;
217                                                                 };
218                                                         };
219                                                         // check for double entries (e.g. SwingOSC)
220                                                         subc[..subc.size-2].do{ |it,i|
221                                                                 var subset;
222                                                                 subset = subc[..i-1];
223                                                                 if ( subset.detect( { |jt| jt == it } ).size > 0, {
224                                                                         subc = subc[..i-1] ++ subc[i+1..];
225                                                                 });
226                                                         };
227                                                 });
228                                         subc = subc[..subc.size-2];
229                                 }
230                         );
231                         if(subc.size > 0) {
232                                 thisHelpExt = helpExtensions.select{ |ext|
233                                         subc.last.endsWith("."++ext)
234                                 };
235                         };
236                         if ( thisHelpExt.size > 0 , {
237                                 subc = subc[..subc.size-2];
238                         });
240                         subc = subc.collect({|i| "[["++i++"]]"});
241                         node = tree;
242                         // Crawl up the tree, creating hierarchy as needed
243                         subc.do({|catname|
244                                 if(node[catname].isNil, {
245                                         node[catname] = Dictionary.new(3);
246                                 });
247                                 node = node[catname];
248                         });
249                         // "node" should now be the tiniest branch
250                         node[classsym.asClass ? classsym] = path;
251                 });
252         }
254         rebuildTree {
255                 this.forgetTree;
256                 if(root.isNil) {
257                         this.tree(allowCached:false);
258                 } {
259                         this.tree(false, false, false, root, false);
260                 }
261         }
263         forgetTree {
264                 tree = nil;
265         }
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 ++ "   ");
273                         }, {
274                                 (prefix + key ++":" + val).postln;
275                         });
276                 });
277         }
279         addCatsToTree { |class, fileslist|
280                 var subc, node;
282                 if(class.categories.isNil.not, {
283                         class.categories.do({|cat|
284                                 subc = cat.split($>).collect({|i| "[["++i++"]]"});
285                                 node = tree;
286                                 // Crawl up the tree, creating hierarchy as needed
287                                 subc.do({|catname|
288                                         if(node[catname].isNil, {
289                                                 node[catname] = Dictionary.new(3);
290                                         });
291                                         node = node[catname];
292                                 });
293                                 // "node" should now be the tiniest branch
294                                 node[class] = fileslist[class.asSymbol] ? "";
295                         });
297                         // Class has been added to list so we're OK
298                         fileslist.removeAt(class.asSymbol);
299                 }); // end if
301         }
303         writeTextArchive{ |path|
304                 var fp;
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);
309                 fp.close;
310         }
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)
318                         }{
319                                 fp.write("%%\n".format($\t.dup(numtabs).join, val.asString));
320                         }
321                 };
322         }
324         readTextArchive{ |path|
325                 var fp, filesliststr;
326                 if(path.isNil){ path = this.cachePath };
327                 fp = File(path, "r");
328                 try{
329                         tree = this.prRecurseTreeFromFile(fp);
330                 }{
331                         "Failure to read tree in Help.treeFromFile(%)".format(path).warn;
332                         this.forgetTree;
333                 };
334                 try{
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;
338                                 this.forgetTree;
339                         }{
340                                 fileslist = filesliststr.interpret;
341                         };
342                 }{
343                         "Failure to read fileslist in Help.treeFromFile(%)".format(path).warn;
344                         this.forgetTree;
345                 };
346                 fp.close;
347         }
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);
354                 numentries.do{
355                         line = fp.getLine[numtabs..];
356                         key = line;
357                         if(key[0]==$[ and:{ key[1]==$[ }){
358                                 // starting with [[ is indicator of being a category
359                                 dict[key] = this.prRecurseTreeFromFile(fp, numtabs+1);
360                         }{
361                                 line = fp.getLine[numtabs..];
362                                 key = key.asSymbol;
363                                 key = key.asClass ? key; // Classes become classes, topics stay as symbols
364                                 dict[key] = line;
365                         }
366                 };
367                 ^dict
368         }
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 );
381         // Now for a GUI
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)
396                 .resize_(5)
397                 .canFocus_(true);
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)
405                                 .resize_(5)
406                                 .visible_(false);
408         // updates the history arrow buttons
409         fBwdFwd = {
410                 bwdButt.enabled = historyIdx > 0;
411                 fwdButt.enabled = historyIdx < (history.size -  1);
412         };
414         fLoadError = { |error|
415                 error.reportError;
416                 "\n\nA discrepancy was found in the help tree.".postln;
417                 if(allowCached) {
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;
420                         this.rebuildTree;
421                         win.close;
422                 } {
423                         "Please report the above error dump on the sc-users mailing list.".postln;
424                 };
425         };
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);
432                 fBwdFwd.value;
433         };
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);
440                 fBwdFwd.value;
441         };
443         // keep this check for compatibility with old versions of swingOSC
444         if( textView.respondsTo( \linkAction ), {
445                 textView
446                         .linkAction_({ arg view, url, descr;
447                                 var path;
448                                 if( url.notEmpty, {
449                                         //fHistoryDo.value( \open, url );
450                                         keys = this.findKeysForValue(url);
451                                         if(keys.size == 0, {
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)});
455                                         });
456                                         if(keys.size > 0, {
457                                                 fSelectTreePath.value(keys.drop(-1), keys.last.asString);
458                                         });
459                                 }, {
460                                         if( descr.beginsWith( "SC://" ), {
461                                                 fHistoryDo.value( \open, descr );
462                                         });
463                                 });
464                         });
465         });
467         lists = Array.newClear(numcols);
468         lists[0] = tree.keys(Array).collect(_.asString).sort;
469         selectednodes = Array.newClear(numcols);
471         // SCListView
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 );
477                 });
478                 view
479                 .resize_(4)
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];
488                                         if( lv2.notNil, {
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 })});
492                                         });
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;
499                                         if(node.isNil, {
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, {
505                                                 {
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 ));
510                                                 }.defer( 0.001 );
511                                                 });
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});
516                             };
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;
523                                         }, {
524                                                 // We have a category on our hands
525                                                 if( lv2.notNil, {
526                                                         lists[ index + 1 ] = node.keys(Array).collect(_.asString).sort({|a,b|
527                                                                         // the outcomes:
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] == $[ /*]*/)) {
533                                                                         a[0] == $[ /*]*/
534                                                                 } {
535                                                                         a < b
536                                                                 }
537                                                         });
538                                                         lv2.items = lists[index+1];
539                                                 });
541                                         });
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 );
546                                         });
547                                         selectednodes[index+2 ..] = nil; // Clear out the now-unselected
548                                 });
549                         });
550                 });
551         });
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|
558                         var nowFocused, lv2;
559                         nowFocused = lv;
560                         switch(unicode,
561                         // cursor left
562                         63234, { if(index > 0, { lv2 = listviews[ index - 1 ]; lv2.focus; nowFocused = lv2 })
563                         },
564                         // cursor right
565                         63235, { if( index < (listviews.size - 1) and: { listviews[ index + 1 ].items.notNil }, {
566                                                 lv2 = listviews[ index + 1 ];
567                                                 try {
568                                                         lv2.value_( if( lv2.respondsTo( \allowsDeselection ).not, - 1 )).valueAction_( 0 ).focus;
569                                                         nowFocused = lv2;
570                                                 }
571                                    })
572                         },
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;
578                                 });
579                         },
580                         //default:
581                         {
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);
585                         });
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));
588                         });
589                 })
590                 .mouseDownAction_({|view, x, y, modifiers, buttonNumber, clickCount|
591                         {
592                         if(lists[index][lv.value][0]==$[, {
593                                 if(scrollView.visibleOrigin.x != (lv.bounds.left - 5), {
594                                         {
595                                         10.do({|i| { scrollView.visibleOrigin_(
596                                                                         Point(((lv.bounds.left - lv.bounds.width)+((10+i)*10)-5), 0))
597                                                                 }.defer;
598                                                 0.02.wait;
599                                         });
600                                         }.fork;
601                                 });
602                         });
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;
608                                 });
609                         });
610                 });
611         });
613         // Add ability to programmatically select an item in a tree
614         fSelectTreePath = { | catpath, leaf |
615                 var foundIndex;
616                 Task{
617                         0.001.wait;
618                         inPathSelect = true;
619                         catpath.do{ |item, index|
620                                 foundIndex = listviews[index].items.indexOfEqual(item);
621                                 if(foundIndex.notNil){
622                                         listviews[index].value_(foundIndex).doAction;
623                                 }{
624                                         "Could not select menu list item % in %".format(item, listviews[index].items).postln;
625                                 };
626                                 0.02.wait;
627                         };
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;
634                         }{
635                                 "Could not select menu list item %".format(leaf).postln;
636                         };
637                         textView.visible = true;
638                         resultsview.visible = false;
639                         fUpdateWinTitle.value;
640                         win.front;
641                 }.play(AppClock);
642         };
644         fUpdateWinTitle = {
645                 win.name_(
646                         (["Help browser"] ++ listviews.collect{|lv| lv.value !? {lv.items[lv.value]} }.reject(_.isNil)).join(" > ") );
647         };
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.
653         }, {
654                 Button.new( win, Rect( 5, /* 534 */ bounds.height - 30, 110, 20 ))
655                         .states_([["Open Help File", Color.black, Color.clear]])
656                         .resize_(7)
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]])
660                         .resize_(7)
661                         .action_({
662                                 if(selecteditem.asSymbol.asClass.notNil, {
663                                         {selecteditem.asSymbol.asClass.openCodeFile }.defer;
664                                 });
665                         });
666         });
667         browseButt = Button.new( win, Rect( 233, /* 534 */ bounds.height - 30, 110, 20 ))
668                 .states_([["Browse Class", Color.black, Color.clear]])
669                 .resize_(7)
670                 .action_({
671                         if(selecteditem.asSymbol.asClass.notNil, {
672                                 {selecteditem.asSymbol.asClass.browse }.defer;
673                         });
674                 });
675         bwdButt = Button.new( win, Rect( 347, /* 534 */ bounds.height - 30, 30, 20 ))
676                 .states_([[ "<" ]])
677                 .resize_(7)
678                 .action_({
679                         if( historyIdx > 0, {
680                                 fHistoryMove.value( -1 );
681                         });
682                 });
683         fwdButt = Button.new( win, Rect( 380, /* 534 */ bounds.height - 30, 30, 20 ))
684                 .states_([[ ">" ]])
685                 .resize_(7)
686                 .action_({
687                         if( historyIdx < (history.size - 1), {
688                                 fHistoryMove.value( 1 );
689                         });
690                 });
691         fBwdFwd.value;
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 != ""){
701                                 // Let's search!
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))
709                                         .resize_(1)
710                                         .align_(\right)
711                                         .string_("% results found for query '%'.".format(results.size, widget.value));
712                                 Button(resultsview, Rect(textViewBounds.width / 2, 0, 100, 30).insetBy(5))
713                                         .resize_(1)
714                                         .states_([["Clear"]])
715                                         .action_({ searchField.valueAction_("") })
716                                         .focus();
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) }
721                                                 );
722                                 };
724                         }{
725                                 // Empty query string, go back to textView
726                                 textView.visible = true;
727                                 resultsview.visible = false;
728                         };
730                 });
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
735                         unicode.switch(
736                                 6, { // f for find
737                                         searchField.focus;
738                                 },
739                                 8, // h for home
740                                 {
741                                         {
742                                                 listviews[0].valueAction_(listviews[0].items.find(["Help"]));
743                                                 scrollView.visibleOrigin_(0@0);
744                                         }.defer(0.001)
745                                 }
746                         );
747                 };
748         };
749         win.view.addAction(helpguikeyacts, \keyUpAction);
751         win.onClose_{
752                 // This is done to prevent Cmd+W winclose from trying to do things in vanishing textviews!
753                 fHistoryDo = {};
754         };
756         win.front;
757         listviews[0].focus;
758         if(listviews[0].items.detect({ |item| item == "Help" }).notNil) {
759                 fSelectTreePath.([], "Help"); // Select the "Help" entry in the root
760                 selecteditem = "Help";
761         } {
762                 selecteditem = listviews[0].items.first;
763                 fSelectTreePath.([], selecteditem);
764         }
766 // end gui
768         all {
769                 //              ^this.new("Help/").dumpToDoc("all-helpfiles");
770                 var doc;
771                 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
772                 var str = CollStream.new;
773                 doc = Document.new("all-helpfiles");
774                 [       Platform.helpDir,
775                         Platform.systemExtensionDir,
776                         Platform.userExtensionDir
777                 ].do{ |it|
778                         PathName.new( it ).foldersWithoutSVN.do{ |folderPn|
779                                 str << folderPn.fullPath << Char.nl;
780                                 folderPn.helpFilesDo { |filePn|
781                                         str << Char.tab <<
782                                         filePn.fileNameWithoutExtension  << Char.nl;
783                                 };
784                         }
785                 };
786                 doc.string = str.collection;
787         }
789         // Iterates the tree, finding the help-doc paths and calling action.value(docname, path)
790         do { |action|
791                 this.pr_do(action, this.tree, []);
792         }
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
797                         }{
798                                 action.value(key.asString, val, catpath)
799                         }
800                 }
801         }
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|
807                         if(path != ""){
808                                 if(docname.find(query, ignoreCase).notNil){
809                                         results.add(HelpSearchResult(docname, path, 100 / (docname.size - query.size + 1), "", catpath.deepCopy));
810                                 }{
811                                         ext = path.splitext[1];
812                                         // OK, let's open the document, see if it contains the string... HEAVY!
813                                         file = File(path, "r");
814                                         if(file.isOpen){
815                                                 docstr = ext.switch(
816                                                         "html", {file.readAllStringHTML},
817                                                         "htm",  {file.readAllStringHTML},
818                                                         "rtf",  {file.readAllStringRTF},
819                                                                 {file.readAllString}
820                                                         );
821                                                 file.close;
822                                                 pos = docstr.findAll(query, ignoreCase);
823                                                 if(pos.notNil){
824                                                         results.add(HelpSearchResult(docname, path, pos.size, docstr[pos[0] ..  pos[0]+50], catpath.deepCopy));
825                                                 }
826                                         }{
827                                                 "Help.search will rebuild help cache, since an expected file was not found: %".format(path).postln;
828                                                 // EXPERIMENTAL:
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...?
832                                                 this.forgetTree;
833                                                 this.tree(allowCached: false);
834                                                 ^this.search(query, ignoreCase);
835                                         }
836                                 }
837                         }{
838                                 //"empty path: %".format(docname).postln;
839                         }
840                 };
841                 results = results.sort;
843                 ^results
844         }
845         // This iterates the Help.tree to find the file. Can be used instead of platform-specific approaches
846         findHelpFile { |str|
847                 str = str.asString;
848                 this.do { |key, path|
849                         if(key == str and: { path.size > 0 }) { ^path }
850                 };
851                 ^nil
852         }
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|
856                 str = str.asString;
857                 if ( str.isEmpty ) { str = "Help" };
858                 ^Help.findHelpFile( str );
859         }
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) }
870         *all { ^global.all }
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 }
882 } // End class
887 + Object {
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   }
907 + Pattern {
908         *categories {   ^ #["Streams-Patterns-Events>Patterns"] }
911 // This allows it to be called from sclang menu item
912 + Process {
913         helpGui {
914                 Help.gui
915         }