cmake build system: visiblity support for clang
[supercollider.git] / SCClassLibrary / Common / Files / Help.sc
blob3952855c7c49b4bc8acc91d78eb64dba12ca25fd
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         classvar <global, categoriesSkipThese;
21         classvar <filterUserDirEntries, <>cachePath;
23         var     tree, fileslist, <root;
25         *initClass {
26                 var     dir;
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)
34                 };
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);
40                                 };
41                         });
42                 });
43                 global = this.new;
44         }
46         *new { |root|
47                 if(root.isNil and: { global.notNil }) {
48                         ^global
49                 } {
50                         ^super.new.init(root)
51                 }
52         }
54         init { |rootdir|
55                 root = rootdir !? { rootdir.absolutePath };
56                 if(root.isNil or: { root.size == 0 }) {
57                         this.tree;
58                 } {
59                         this.tree(false, false, false, root, false);
60                 }
61         }
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;
67                 var thisHelpExt;
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;
72                 };
73                 if(tree.isNil, { "Help files scanned in % seconds".format({
74                         // Building the tree
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;
79                         if(root.isNil) {
80                                 helpDirs = helpDirs.add( Platform.helpDir );
81                         } {
82                                 helpDirs = helpDirs.add(root);
83                         };
84                         if ( sysext ,{
85                                 helpDirs = helpDirs.add( Platform.systemExtensionDir );
86                         });
87                         if ( userext ,{
88                                 helpDirs = helpDirs.add( Platform.userExtensionDir );
89                         });
91                         // Now check each class's ".categories" response
92                         classes = Object.allSubclasses.difference(categoriesSkipThese).reject({|c| c.asString.beginsWith("Meta_")});
93                         if(root.notNil) {
94                                         // consider only classes whose main definition is in the root folder
95                                 classes = classes.select({ |c| c.filenameSymbol.asString.beginsWith(root) });
96                         };
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 );
105                         };
106                         if(writeCache) {
107                                 {
108                                         this.writeTextArchive;
109                                 }.defer(4.312);
110                                 {
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;
114                                         this.do{|key, value|
115                                                 if("0123456789".includes(key[0]).not and:{key.includes($ ).not}){
116                                                         sclang_completion_dict.add(key);
117                                                 }
118                                         };
119                                         dictfile = File(Platform.userAppSupportDir +/+ "sclang_completion_dict", "w");
120                                         if(dictfile.isOpen){
121                                                 sclang_completion_dict.do{|entry| dictfile.write(entry ++ Char.nl)};
122                                                 dictfile.close;
123                                         };
124                                 }.defer(7.1101);
125                         };
126                 }.bench(false)).postln});
127                 ^tree;
128         }
130         findKeysForValue{|val|
131                 var func, node, keyPath;
132                 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 ));
137                 });
138                 val = val.replace("%20", " ");
139                 func = {|dict, depth = 0|
140                         node = dict.findKeyForValue(val);
141                         node.isNil.if({
142                                 dict.keysValuesDo({|key, item|
143                                         item.isKindOf(Dictionary).if({
144                                                 keyPath = keyPath.copyFromStart(depth - 1).add(key);
145                                                 func.value(item, depth + 1)
146                                         })
147                                 });
148                         }, {^keyPath.add(node)});
149                 };
150                 func.value(this.tree);
151                 ^[];
152         }
154         addUserFilter{ |subpath|
155                 filterUserDirEntries = filterUserDirEntries.add( subpath );
156                 this.forgetTree;
157         }
159         addDirTree{ |helppath,tree|
160                 var helpExtensions = #['html', 'htm', 'scd', 'rtf', 'rtfd'];
161                 var subfileslist;
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;
169                                 };
170                                 if( pathname.fullPath.contains("3vs2").not
171                                         and: { pathname.fullPath.contains("help-scripts").not }
172                                         , {
173                                                 subfileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
174                                                 fileslist[pathname.fileNameWithoutDoubleExtension.asSymbol] = pathname.fullPath;
175                                         })
176                         });
178                 helpRootLen = (helppath.standardizePath).size + 1;
179                 subfileslist.keysValuesDo({ |classsym, path|
181                         subc = path[helpRootLen..].split(Platform.pathSeparator);
182                         if ( helppath == Platform.helpDir,
183                                 {
184                                         subc = subc[0..subc.size-2]; // Ignore "Help" and the filename at the end
185 //                                      subc = path[helpRootLen..].split(Platform.pathSeparator);
186                                 },{
187                                         //helpRootLen = "~".standardizePath;
188                                         if ( helppath == Platform.systemExtensionDir,
189                                                 {
190 //                                                      subc = path[helpRootLen..].split(Platform.pathSeparator);
191                                                         subc = [ "SystemExtensions" ] ++ subc;
192                                                         //subc.postcs;
193                                                 });
194                                         if ( helppath == Platform.userExtensionDir,
195                                                 {
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|
202                                                                         it == spath;
203                                                                 };
204                                                         };
205                                                         // check for double entries (e.g. SwingOSC)
206                                                         subc[..subc.size-2].do{ |it,i|
207                                                                 var subset;
208                                                                 subset = subc[..i-1];
209                                                                 if ( subset.detect( { |jt| jt == it } ).size > 0, {
210                                                                         subc = subc[..i-1] ++ subc[i+1..];
211                                                                 });
212                                                         };
213                                                 });
214                                         if ( helppath == root,
215                                                 {
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|
221                                                                         it == spath;
222                                                                 };
223                                                         };
224                                                         // check for double entries (e.g. SwingOSC)
225                                                         subc[..subc.size-2].do{ |it,i|
226                                                                 var subset;
227                                                                 subset = subc[..i-1];
228                                                                 if ( subset.detect( { |jt| jt == it } ).size > 0, {
229                                                                         subc = subc[..i-1] ++ subc[i+1..];
230                                                                 });
231                                                         };
232                                                 });
233                                         subc = subc[..subc.size-2];
234                                 }
235                         );
236                         if(subc.size > 0) {
237                                 thisHelpExt = helpExtensions.select{ |ext|
238                                         subc.last.endsWith("."++ext)
239                                 };
240                         };
241                         if ( thisHelpExt.size > 0 , {
242                                 subc = subc[..subc.size-2];
243                         });
245                         subc = subc.collect({|i| "[["++i++"]]"});
246                         node = tree;
247                         // Crawl up the tree, creating hierarchy as needed
248                         subc.do({|catname|
249                                 if(node[catname].isNil, {
250                                         node[catname] = Dictionary.new(3);
251                                 });
252                                 node = node[catname];
253                         });
254                         // "node" should now be the tiniest branch
255                         node[classsym.asClass ? classsym] = path;
256                 });
257         }
259         rebuildTree {
260                 this.forgetTree;
261                 if(root.isNil) {
262                         this.tree(allowCached:false);
263                 } {
264                         this.tree(false, false, false, root, false);
265                 }
266         }
268         forgetTree {
269                 tree = nil;
270         }
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 ++ "   ");
278                         }, {
279                                 (prefix + key ++":" + val).postln;
280                         });
281                 });
282         }
284         addCatsToTree { |class, fileslist|
285                 var subc, node;
287                 if(class.categories.isNil.not, {
288                         class.categories.do({|cat|
289                                 subc = cat.split($>).collect({|i| "[["++i++"]]"});
290                                 node = tree;
291                                 // Crawl up the tree, creating hierarchy as needed
292                                 subc.do({|catname|
293                                         if(node[catname].isNil, {
294                                                 node[catname] = Dictionary.new(3);
295                                         });
296                                         node = node[catname];
297                                 });
298                                 // "node" should now be the tiniest branch
299                                 node[class] = fileslist[class.asSymbol] ? "";
300                         });
302                         // Class has been added to list so we're OK
303                         fileslist.removeAt(class.asSymbol);
304                 }); // end if
306         }
308         writeTextArchive{ |path|
309                 var fp;
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);
314                 fp.close;
315         }
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)
323                         }{
324                                 fp.write("%%\n".format($\t.dup(numtabs).join, val.asString));
325                         }
326                 };
327         }
329         readTextArchive{ |path|
330                 var fp, filesliststr;
331                 if(path.isNil){ path = this.cachePath };
332                 fp = File(path, "r");
333                 try{
334                         tree = this.prRecurseTreeFromFile(fp);
335                 }{
336                         "Failure to read tree in Help.treeFromFile(%)".format(path).warn;
337                         this.forgetTree;
338                 };
339                 try{
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;
343                                 this.forgetTree;
344                         }{
345                                 fileslist = filesliststr.interpret;
346                         };
347                 }{
348                         "Failure to read fileslist in Help.treeFromFile(%)".format(path).warn;
349                         this.forgetTree;
350                 };
351                 fp.close;
352         }
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);
359                 numentries.do{
360                         line = fp.getLine[numtabs..];
361                         key = line;
362                         if(key[0]==$[ and:{ key[1]==$[ }){
363                                 // starting with [[ is indicator of being a category
364                                 dict[key] = this.prRecurseTreeFromFile(fp, numtabs+1);
365                         }{
366                                 line = fp.getLine[numtabs..];
367                                 key = key.asSymbol;
368                                 key = key.asClass ? key; // Classes become classes, topics stay as symbols
369                                 dict[key] = line;
370                         }
371                 };
372                 ^dict
373         }
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 );
386         // Now for a GUI
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)
401                 .resize_(5)
402                 .canFocus_(true);
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)
410                                 .resize_(5)
411                                 .visible_(false);
413         // updates the history arrow buttons
414         fBwdFwd = {
415                 bwdButt.enabled = historyIdx > 0;
416                 fwdButt.enabled = historyIdx < (history.size -  1);
417         };
419         fLoadError = { |error|
420                 error.reportError;
421                 "\n\nA discrepancy was found in the help tree.".postln;
422                 if(allowCached) {
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;
425                         this.rebuildTree;
426                         win.close;
427                 } {
428                         "Please report the above error dump on the sc-users mailing list.".postln;
429                 };
430         };
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);
437                 fBwdFwd.value;
438         };
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);
445                 fBwdFwd.value;
446         };
448         // keep this check for compatibility with old versions of swingOSC
449         if( textView.respondsTo( \linkAction ), {
450                 textView
451                         .linkAction_({ arg view, url, descr;
452                                 var path;
453                                 if( url.notEmpty, {
454                                         //fHistoryDo.value( \open, url );
455                                         keys = this.findKeysForValue(url);
456                                         if(keys.size == 0, {
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)});
460                                         });
461                                         if(keys.size > 0, {
462                                                 fSelectTreePath.value(keys.drop(-1), keys.last.asString);
463                                         });
464                                 }, {
465                                         if( descr.beginsWith( "SC://" ), {
466                                                 fHistoryDo.value( \open, descr );
467                                         });
468                                 });
469                         });
470         });
472         lists = Array.newClear(numcols);
473         lists[0] = tree.keys(Array).collect(_.asString).sort;
474         selectednodes = Array.newClear(numcols);
476         // SCListView
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 );
482                 });
483                 view
484                 .resize_(4)
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];
493                                         if( lv2.notNil, {
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 })});
497                                         });
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;
504                                         if(node.isNil, {
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, {
510                                                 {
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 ));
515                                                 }.defer( 0.001 );
516                                                 });
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});
521                             };
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;
528                                         }, {
529                                                 // We have a category on our hands
530                                                 if( lv2.notNil, {
531                                                         lists[ index + 1 ] = node.keys(Array).collect(_.asString).sort({|a,b|
532                                                                         // the outcomes:
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] == $[ /*]*/)) {
538                                                                         a[0] == $[ /*]*/
539                                                                 } {
540                                                                         a < b
541                                                                 }
542                                                         });
543                                                         lv2.items = lists[index+1];
544                                                 });
546                                         });
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 );
551                                         });
552                                         selectednodes[index+2 ..] = nil; // Clear out the now-unselected
553                                 });
554                         });
555                 });
556         });
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|
563                         var nowFocused, lv2;
564                         nowFocused = lv;
565                         switch(unicode,
566                         // cursor left
567                         63234, { if(index > 0, { lv2 = listviews[ index - 1 ]; lv2.focus; nowFocused = lv2 })
568                         },
569                         // cursor right
570                         63235, { if( index < (listviews.size - 1) and: { listviews[ index + 1 ].items.notNil }, {
571                                                 lv2 = listviews[ index + 1 ];
572                                                 try {
573                                                         lv2.value_( if( lv2.respondsTo( \allowsDeselection ).not, - 1 )).valueAction_( 0 ).focus;
574                                                         nowFocused = lv2;
575                                                 }
576                                    })
577                         },
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;
583                                 });
584                         },
585                         //default:
586                         {
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);
590                         });
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));
593                         });
594                 })
595                 .mouseDownAction_({|view, x, y, modifiers, buttonNumber, clickCount|
596                         {
597                         if(lists[index][lv.value][0]==$[, {
598                                 if(scrollView.visibleOrigin.x != (lv.bounds.left - 5), {
599                                         {
600                                         10.do({|i| { scrollView.visibleOrigin_(
601                                                                         Point(((lv.bounds.left - lv.bounds.width)+((10+i)*10)-5), 0))
602                                                                 }.defer;
603                                                 0.02.wait;
604                                         });
605                                         }.fork;
606                                 });
607                         });
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;
613                                 });
614                         });
615                 });
616         });
618         // Add ability to programmatically select an item in a tree
619         fSelectTreePath = { | catpath, leaf |
620                 var foundIndex;
621                 Task{
622                         0.001.wait;
623                         inPathSelect = true;
624                         catpath.do{ |item, index|
625                                 foundIndex = listviews[index].items.indexOfEqual(item);
626                                 if(foundIndex.notNil){
627                                         listviews[index].value_(foundIndex).doAction;
628                                 }{
629                                         "Could not select menu list item % in %".format(item, listviews[index].items).postln;
630                                 };
631                                 0.02.wait;
632                         };
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;
639                         }{
640                                 "Could not select menu list item %".format(leaf).postln;
641                         };
642                         textView.visible = true;
643                         resultsview.visible = false;
644                         fUpdateWinTitle.value;
645                         win.front;
646                 }.play(AppClock);
647         };
649         fUpdateWinTitle = {
650                 win.name_(
651                         (["Help browser"] ++ listviews.collect{|lv| lv.value !? {lv.items[lv.value]} }.reject(_.isNil)).join(" > ") );
652         };
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.
658         }, {
659                 Button.new( win, Rect( 5, /* 534 */ bounds.height - 30, 110, 20 ))
660                         .states_([["Open Help File", Color.black, Color.clear]])
661                         .resize_(7)
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]])
665                         .resize_(7)
666                         .action_({
667                                 if(selecteditem.asSymbol.asClass.notNil, {
668                                         {selecteditem.asSymbol.asClass.openCodeFile }.defer;
669                                 });
670                         });
671         });
672         browseButt = Button.new( win, Rect( 233, /* 534 */ bounds.height - 30, 110, 20 ))
673                 .states_([["Browse Class", Color.black, Color.clear]])
674                 .resize_(7)
675                 .action_({
676                         if(selecteditem.asSymbol.asClass.notNil, {
677                                 {selecteditem.asSymbol.asClass.browse }.defer;
678                         });
679                 });
680         bwdButt = Button.new( win, Rect( 347, /* 534 */ bounds.height - 30, 30, 20 ))
681                 .states_([[ "<" ]])
682                 .resize_(7)
683                 .action_({
684                         if( historyIdx > 0, {
685                                 fHistoryMove.value( -1 );
686                         });
687                 });
688         fwdButt = Button.new( win, Rect( 380, /* 534 */ bounds.height - 30, 30, 20 ))
689                 .states_([[ ">" ]])
690                 .resize_(7)
691                 .action_({
692                         if( historyIdx < (history.size - 1), {
693                                 fHistoryMove.value( 1 );
694                         });
695                 });
696         fBwdFwd.value;
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 != ""){
706                                 // Let's search!
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))
714                                         .resize_(1)
715                                         .align_(\right)
716                                         .string_("% results found for query '%'.".format(results.size, widget.value));
717                                 Button(resultsview, Rect(textViewBounds.width / 2, 0, 100, 30).insetBy(5))
718                                         .resize_(1)
719                                         .states_([["Clear"]])
720                                         .action_({ searchField.valueAction_("") })
721                                         .focus();
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) }
726                                                 );
727                                 };
729                         }{
730                                 // Empty query string, go back to textView
731                                 textView.visible = true;
732                                 resultsview.visible = false;
733                         };
735                 });
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
740                         unicode.switch(
741                                 6, { // f for find
742                                         searchField.focus;
743                                 },
744                                 8, // h for home
745                                 {
746                                         {
747                                                 listviews[0].valueAction_(listviews[0].items.find(["Help"]));
748                                                 scrollView.visibleOrigin_(0@0);
749                                         }.defer(0.001)
750                                 }
751                         );
752                 };
753         };
754         win.view.addAction(helpguikeyacts, \keyUpAction);
756         win.onClose_{
757                 // This is done to prevent Cmd+W winclose from trying to do things in vanishing textviews!
758                 fHistoryDo = {};
759         };
761         win.front;
762         listviews[0].focus;
763         if(listviews[0].items.detect({ |item| item == "Help" }).notNil) {
764                 fSelectTreePath.([], "Help"); // Select the "Help" entry in the root
765                 selecteditem = "Help";
766         } {
767                 selecteditem = listviews[0].items.first;
768                 fSelectTreePath.([], selecteditem);
769         }
771 // end gui
773         all {
774                 //              ^this.new("Help/").dumpToDoc("all-helpfiles");
775                 var doc;
776                 var helpExtensions = ['html', 'htm', 'scd', 'rtf', 'rtfd'];
777                 var str = CollStream.new;
778                 doc = Document.new("all-helpfiles");
779                 [       Platform.helpDir,
780                         Platform.systemExtensionDir,
781                         Platform.userExtensionDir
782                 ].do{ |it|
783                         PathName.new( it ).foldersWithoutSVN.do{ |folderPn|
784                                 str << folderPn.fullPath << Char.nl;
785                                 folderPn.helpFilesDo { |filePn|
786                                         str << Char.tab <<
787                                         filePn.fileNameWithoutExtension  << Char.nl;
788                                 };
789                         }
790                 };
791                 doc.string = str.collection;
792         }
794         // Iterates the tree, finding the help-doc paths and calling action.value(docname, path)
795         do { |action|
796                 this.pr_do(action, this.tree, []);
797         }
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
802                         }{
803                                 action.value(key.asString, val, catpath)
804                         }
805                 }
806         }
808         searchGUI {
809                 this.deprecated(thisMethod, Meta_Help.findRespondingMethodFor(\gui));
810                 ^this.gui
811         }
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|
817                         if(path != ""){
818                                 if(docname.find(query, ignoreCase).notNil){
819                                         results.add(HelpSearchResult(docname, path, 100 / (docname.size - query.size + 1), "", catpath.deepCopy));
820                                 }{
821                                         ext = path.splitext[1];
822                                         // OK, let's open the document, see if it contains the string... HEAVY!
823                                         file = File(path, "r");
824                                         if(file.isOpen){
825                                                 docstr = ext.switch(
826                                                         "html", {file.readAllStringHTML},
827                                                         "htm",  {file.readAllStringHTML},
828                                                         "rtf",  {file.readAllStringRTF},
829                                                                 {file.readAllString}
830                                                         );
831                                                 file.close;
832                                                 pos = docstr.findAll(query, ignoreCase);
833                                                 if(pos.notNil){
834                                                         results.add(HelpSearchResult(docname, path, pos.size, docstr[pos[0] ..  pos[0]+50], catpath.deepCopy));
835                                                 }
836                                         }{
837                                                 "Help.search will rebuild help cache, since an expected file was not found: %".format(path).postln;
838                                                 // EXPERIMENTAL:
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...?
842                                                 this.forgetTree;
843                                                 this.tree(allowCached: false);
844                                                 ^this.search(query, ignoreCase);
845                                         }
846                                 }
847                         }{
848                                 //"empty path: %".format(docname).postln;
849                         }
850                 };
851                 results = results.sort;
853                 ^results
854         }
855         // This iterates the Help.tree to find the file. Can be used instead of platform-specific approaches
856         findHelpFile { |str|
857                 str = str.asString;
858                 this.do { |key, path|
859                         if(key == str and: { path.size > 0 }) { ^path }
860                 };
861                 ^nil
862         }
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|
866                 str = str.asString;
867                 if ( str.isEmpty ) { str = "Help" };
868                 ^Help.findHelpFile( str );
869         }
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) }
880         *all { ^global.all }
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 }
892 } // End class
895 HelpSearchResult {
896         var <>docname, <>path, <>goodness, <>context, <>catpath;
897         *new{|docname, path, goodness, context, catpath|
898                 ^this.newCopyArgs(docname, path, goodness, context, catpath);
899         }
901         asString {
902                 ^ "HelpSearchResult(%, %, %, %)".format(docname, path.basename, goodness, this.contextTrimmed)
903         }
904         // used for sorting:
905         <= { |that|
906                 ^ this.goodness >= that.goodness
907         }
909         contextTrimmed {
910                 ^context.tr($\n, $ ).tr($\t, $ )
911         }
913         drawRow { |parent, bounds, action|
914                 // SCButton
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)
920                                                                                 .insetBy(5, 5))
921                                 .string_(this.contextTrimmed);
923         }
927 + Object {
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   }
947 + Pattern {
948         *categories {   ^ #["Streams-Patterns-Events>Patterns"] }
951 // This allows it to be called from sclang menu item
952 + Process {
953         helpGui {
954                 Help.gui
955         }