1 // A thorough refactoring of JMc's original class browser
2 // Previously a state was a Class or an array representing a search result
3 // Now it's an Environment holding more info
7 classvar updateProtos, searchMenuKeys, viewList, buttonSet, gray;
10 // historyPos == 0 means one item in history[0]
11 // so it must be negative for empty history
12 currentState, history, historyPos = -1,
16 ^super.new.init(class)
20 // views that can change current class or method
21 // should always keep the state environment variables up-to-date
22 current { ^currentState }
23 currentClass { ^currentState.currentClass }
24 currentMethod { ^currentState.currentMethod }
29 hvBold12 = Font.sansSerif( 12 ).boldVariant;
33 views = Environment(parent: updateProtos, know: true).make {
35 ~window = gui.window.new("class browser", Rect(128, (GUI.window.screenBounds.height - 638).clip(0, 320), 720, 600))
36 .onClose_({ this.free });
38 ~window.view.decorator = FlowLayout(~window.view.bounds);
40 ~currentClassNameView = gui.textField.new(~window, Rect(0,0, 308, 32));
41 ~currentClassNameView.font = Font.sansSerif( 18 ).boldVariant;
42 ~currentClassNameView.align = \center;
44 ~currentClassNameView.action = {
45 this.makeState(views.currentClassNameView.string.asSymbol.asClass, \class);
48 ~superClassNameView = gui.staticText.new(~window, Rect(0,0, 256, 32));
49 ~superClassNameView.font = hvBold12 = Font.sansSerif( 12 ).boldVariant;
51 ~window.view.decorator.nextLine;
53 ~bakButton = gui.button.new(~window, Rect(0,0, 24, 24));
54 ~bakButton.states = [["<"]];
57 historyPos = historyPos - 1;
62 ~fwdButton = gui.button.new(~window, Rect(0,0, 24, 24));
63 ~fwdButton.states = [[">"]];
65 if (historyPos < (history.size - 1)) {
66 historyPos = historyPos + 1;
72 ~superButton = gui.button.new(~window, Rect(0,0, 50, 24));
73 ~superButton.states = [["super"]];
75 ~superButton.action = {
76 if(currentState.currentClass.notNil) {
77 this.makeState( currentState.currentClass.superclass );
81 ~metaButton = gui.button.new(~window, Rect(0,0, 50, 24));
82 ~metaButton.states = [["meta"]];
84 ~metaButton.action = {
85 if(currentState.currentClass.notNil) {
86 this.makeState( currentState.currentClass.class );
91 ~helpButton = gui.button.new(~window, Rect(0,0, 50, 24));
92 ~helpButton.states = [["help"]];
94 ~helpButton.action = {
95 if(currentState.currentClass.notNil) {
96 currentState.currentClass.help;
100 ~classSourceButton = gui.button.new(~window, Rect(0,0, 90, 24));
101 ~classSourceButton.states = [["class source"]];
103 ~classSourceButton.action = {
104 if(currentState.currentClass.notNil) {
105 currentState.currentClass.openCodeFile;
109 ~methodSourceButton = gui.button.new(~window, Rect(0,0, 90, 24));
110 ~methodSourceButton.states = [["method source"]];
111 ~methodSourceButton.action = {
112 if(currentState.currentMethod.notNil) {
113 currentState.currentMethod.openCodeFile;
117 ~implementationButton = gui.button.new(~window, Rect(0,0, 100, 24));
118 ~implementationButton.states = [["implementations"]];
119 ~implementationButton.action = {
120 if(currentState.currentMethod.notNil) {
121 thisProcess.interpreter.cmdLine = currentState.currentMethod.name.asString;
122 thisProcess.methodTemplates;
126 ~refsButton = gui.button.new(~window, Rect(0,0, 70, 24));
127 ~refsButton.states = [["references"]];
128 ~refsButton.action = {
129 if(currentState.currentMethod.notNil) {
130 thisProcess.interpreter.cmdLine = currentState.currentMethod.name.asString;
131 thisProcess.methodReferences;
135 if(this.respondsTo(\openSVN)) {
136 ~svnButton = gui.button.new(~window, Rect(0,0, 32, 24));
137 ~svnButton.states = [["svn"]];
138 ~svnButton.action = {
139 var filename, svnAddr;
140 if(currentState.currentMethod.notNil) {
141 svnAddr = "http://supercollider.svn.sourceforge.net/viewvc/supercollider/trunk/common/build/";
142 filename = currentState.currentClass.filenameSymbol.asString;
143 svnAddr = svnAddr ++ filename.drop(filename.find("SCClassLibrary"));
144 svnAddr = svnAddr ++ "?view=log";
145 this.openSVN( svnAddr );
150 ~window.view.decorator.nextLine;
152 GUI.staticText.new(~window, Rect(0, 0, 65, 20)).string_("Search for");
153 ~searchField = GUI.textField.new(~window, Rect(0, 0, 235, 20))
155 this.searchClasses(views.searchField.string,
156 views.searchMenu.value, views.matchCaseButton.value);
158 GUI.staticText.new(~window, Rect(0, 0, 15, 20)).string_("in").align_(\center);
159 ~searchMenu = GUI.popUpMenu.new(~window, Rect(0, 0, 200, 20));
160 ~matchCaseButton = GUI.button.new(~window, Rect(0, 0, 100, 20))
161 .states_([["Case insensitive"], ["Match case"]]);
163 ~searchButton = GUI.button.new(~window, Rect(0, 0, 40, 20))
166 this.searchClasses(views.searchField.string,
167 views.searchMenu.value, views.matchCaseButton.value);
170 ~window.view.decorator.nextLine;
172 ~filenameView = gui.staticText.new(~window, Rect(0,0, 600, 18));
173 ~filenameView.font = Font.sansSerif( 10 );
175 ~window.view.decorator.nextLine;
176 gui.staticText.new(~window, Rect(0,0, 180, 24))
177 .font_(hvBold12).align_(\center).string_("class vars");
178 gui.staticText.new(~window, Rect(0,0, 180, 24))
179 .font_(hvBold12).align_(\center).string_("instance vars");
180 ~window.view.decorator.nextLine;
182 ~classVarView = gui.listView.new(~window, Rect(0,0, 180, 130));
183 ~instVarView = gui.listView.new(~window, Rect(0,0, 180, 130));
184 ~classVarView.value = 0;
185 ~instVarView.value = 0;
187 ~window.view.decorator.nextLine;
189 ~subclassTitle = gui.staticText.new(~window, Rect(0,0, 220, 24))
190 .font_(hvBold12).align_(\center).string_("subclasses (press return)");
191 ~methodTitle = gui.staticText.new(~window, Rect(0,0, 240, 24))
192 .font_(hvBold12).align_(\center).string_("methods");
193 gui.staticText.new(~window, Rect(0,0, 200, 24))
194 .font_(hvBold12).align_(\center).string_("arguments");
195 ~window.view.decorator.nextLine;
197 ~subclassView = gui.listView.new(~window, Rect(0,0, 220, 260));
198 ~methodView = gui.listView.new(~window, Rect(0,0, 240, 260));
199 ~argView = gui.listView.new(~window, Rect(0,0, 200, 260));
200 ~subclassView.resize = 4;
201 ~methodView.resize = 4;
204 ~window.view.decorator.nextLine;
206 [~classVarView, ~instVarView, ~subclassView, ~methodView, ~argView].do (
207 _.beginDragAction_({nil})
211 this.addInstanceActions
212 .makeState(class, \class);
217 makeState { |result, state = \class, addHistory = true, extraValuesDict|
218 currentState = Environment(proto: views, parent: updateProtos, know: true).make {
220 if(~result.isNil or: { result.isArray and: { result.isEmpty } }) {
221 ~state = ("empty" ++ state).asSymbol
225 if(extraValuesDict.respondsTo(\keysValuesDo)) {
226 currentEnvironment.putAll(extraValuesDict);
228 ~state.envirGet[\init].value;
229 ~subclassViewIndex = ~subclassViewIndex.value ? 0;
230 ~methodViewIndex = ~methodViewIndex.value ? 0;
234 if (history.size > 1000) { history = history.drop(1) };
235 historyPos = historyPos + 1;
236 history = history.extend(historyPos).add(currentState);
242 // note, don't use makeState because we already have the complete state saved
243 currentState = history[historyPos];
248 var updaters, bstate, view;
250 updaters = ~state.envirGet;
251 viewList.do { |viewname|
252 updaters[viewname].value(viewname.envirGet);
254 buttonSet.do { |viewname|
255 viewname.envirGet.tryPerform(\enabled_,
256 updaters[\buttonsDisabled].includes(viewname).not);
263 if(views.window.isClosed.not) { views.window.close };
264 currentState = history = views = nil; // dump garbage
267 // workaround for the fact that you can't get a non-metaclass from a metaclass
270 if(method.isNil) { ^nil };
271 ^if((class = method.ownerClass).isMetaClass) {
272 class.name.asString[5..].asSymbol.asClass;
276 getClass { |method| ^this.class.getClass(method) }
278 searchClasses { |string, rootNumber, matchCase, addHistory = true|
279 var pool, rootdir, result,
280 searchType = searchMenuKeys[rootNumber] ? \classSearch,
282 string = string ?? { "" };
283 matchCase = matchCase > 0;
286 isClassSearch = true;
287 pool = Class.allClasses
290 isClassSearch = false;
291 pool = Class.allClasses;
294 isClassSearch = true;
295 pool = this.currentClass.allSubclasses
298 isClassSearch = false;
299 pool = this.currentClass.allSubclasses ++ this.currentClass;
300 if(this.currentClass.isMetaClass.not) {
301 pool = pool ++ pool.collect(_.class);
305 isClassSearch = true;
306 rootdir = PathName(currentState.currentClass.filenameSymbol.asString).pathOnly;
307 pool = Class.allClasses.select({ |class|
308 PathName(class.filenameSymbol.asString).pathOnly == rootdir
313 pool.reject(_.isMetaClass).sort({ |a, b| a.name < b.name }),
316 ^this // just to force early exit
319 case { string.isEmpty } {
320 result = pool.sort({ |a, b| a.name < b.name });
323 result = (if(matchCase) {
324 pool.select({ |class|
325 class.isMetaClass.not and: { class.name.asString.contains(string) }
328 pool.select({ |class|
329 class.isMetaClass.not and: { class.name.asString.containsi(string) }
331 }).sort({ |a, b| a.name < b.name });
333 { // default case - method search
337 result = result.addAll(class.methods.select({ |method|
338 method.name.asString.contains(string)
343 result = result.addAll(class.methods.select({ |method|
344 method.name.asString.containsi(string)
348 result = result.sort({ |a, b|
349 if(a.name == b.name) { a.ownerClass.name < b.ownerClass.name }
353 this.makeState(result, searchType, addHistory, extraValuesDict: (
354 searchString: string,
355 searchMenuValue: rootNumber,
356 caseButtonValue: matchCase.binaryValue
362 gray = Color.grey(0.5);
364 searchMenuKeys = #[classSearch, methodSearch, classSearch, methodSearch, classSearch];
365 viewList = #[currentClassNameView, superClassNameView, filenameView,
366 classVarView, instVarView, subclassTitle, methodTitle, subclassView,
367 methodView, argView, searchMenu];
368 buttonSet = IdentitySet[\superButton, \metaButton, \helpButton, \classSourceButton,
369 \methodSourceButton, \implementationButton, \refsButton, \svnButton];
370 // updateProtos holds instructions to update the GUI
371 // for each kind of browser result to display
375 ~setSubclassArray.value;
376 ~setMethodArray.value;
377 ~setCurrentClass.value(~result);
378 ~setCurrentMethod.value(~methodArray[0]);
380 // ~result must be a Class
381 currentClassNameView: { |v| v.string = ~currentClass.name.asString },
382 superClassNameView: { |v|
383 v.string = "superclass: " ++ ~currentClass.superclass.tryPerform(\name).asString
385 filenameView: { |v| v.string = ~currentClass.filenameSymbol.asString },
387 v.items = ~currentClass.classVarNames.asArray.collectAs
388 ({|name| name.asString }, Array).sort
391 v.items = ~currentClass.instVarNames.asArray.collectAs
392 ({|name| name.asString }, Array).sort
395 v.string_("subclasses (press return)")
398 ~subclassView.items_(~subclassArray.collect({|class| class.name.asString }))
399 .value_(~subclassViewIndex ? 0)
400 .action_(~subclassViewNormalAction)
401 .enterKeyAction_(~navigateToSubclassAction)
402 .mouseDownAction_(~listViewDoubleClickAction);
404 methodTitle: { |v| v.string_("methods") },
406 var colorFunc = { |class, name|
407 if(class.findOverriddenMethod(name.asSymbol).isNil) {
413 var classMethodColors = ~classMethodNames.collect { |name|
414 colorFunc.value(~currentClass.class, name)
416 var methodColors = ~methodNames.collect { |name|
417 colorFunc.value(~currentClass, name)
419 var methodNames = ~classMethodNames ++ ~methodNames;
420 var colors = classMethodColors ++ methodColors;
422 ~methodView.items_(methodNames)
423 .value_(~methodViewIndex ? 0)
424 .action_(~displayCurrentMethodArgsAction)
425 .mouseDownAction_(~listViewDoubleClickAction)
426 .tryPerform(\colors_, colors)
429 if (~currentMethod.isNil or: { ~currentMethod.argNames.isNil }) {
430 ~argView.items = ["this"];
432 ~argView.items = ~currentMethod.argNames.collectAs( {|name, i|
434 defval = ~currentMethod.prototypeFrame[i];
435 if (defval.isNil) { name.asString }{
436 name.asString ++ " = " ++ defval.asString
442 v.items = ["All classes",
444 "Subclasses of " ++ ~currentClass.name,
445 "% + subclass methods".format(~currentClass.name),
447 ++ PathName(~currentClass.filenameSymbol.asString)
451 buttonsDisabled: IdentitySet.new // all buttons OK
453 // this could happen if the user types a wrong class name into the class name box
455 currentClassNameView: { |v| v.string = "Class does not exist" },
456 superClassNameView: { |v| v.string = "" },
457 filenameView: { |v| v.string = "" },
458 classVarView: { |v| v.items_(#[]) },
459 instVarView: { |v| v.items_(#[]) },
460 subclassTitle: { |v| v.string = "no subclasses" },
461 methodTitle: { |v| v.string = "no methods" },
462 subclassView: { |v| v.items_(#[]) },
463 methodView: { |v| v.items_(#[]) },
464 argView: { |v| v.items_(#[]) },
465 buttonsDisabled: buttonSet // all buttons disabled
468 // init: set current class and current method
470 ~currentClass = ~result[0];
471 ~subclassArray = ~result;
472 ~setMethodArray.value(~currentClass);
473 ~currentMethod = ~methodArray[0];
475 // result must be an array of classes
476 currentClassNameView: { |v| v.string = "Class Search: " ++ ~searchString },
477 superClassNameView: { |v| v.string = "" },
478 filenameView: { |v| ~class[\filenameView].value(v) },
479 classVarView: { |v| ~class[\classVarView].value(v) },
480 instVarView: { |v| ~class[\instVarView].value(v) },
481 subclassTitle: { |v| v.string = "matching classes (press return)" },
482 methodTitle: { |v| v.string = "methods" },
484 v.items_(~result.collect(_.name))
485 .value_(~subclassViewIndex ? 0)
486 .action_(~classSearchSubclassViewAction)
487 .enterKeyAction_(~navigateToCurrentClassAction);
489 methodView: { |v| ~class[\methodView].value(v) },
490 argView: { |v| ~class[\argView].value(v) },
491 buttonsDisabled: #[] // all buttons OK
494 currentClassNameView: { |v| v.string = "Class Search: " ++ ~searchString },
495 superClassNameView: { |v| v.string = "No results found!" },
496 filenameView: { |v| v.string = "" },
497 classVarView: { |v| v.items_(#[]) },
498 instVarView: { |v| v.items_(#[]) },
499 subclassTitle: { |v| v.string = "no classes" },
500 methodTitle: { |v| v.string = "no methods" },
501 subclassView: { |v| v.items_(#[]) },
502 methodView: { |v| v.items_(#[]) },
503 argView: { |v| v.items_(#[]) },
504 buttonsDisabled: buttonSet // all buttons disabled
508 ~methodArray = ~result;
509 ~currentMethod = ~methodArray[0];
510 ~methodViewIndex = 0;
511 ~setClassArrayFromMethodSearch.value(~result);
512 ~currentClass = this.getClass(~currentMethod);
513 ~subclassViewIndex = ~subclassArray.indexOf(~currentClass);
515 // result must be an array of methods
516 currentClassNameView: { |v| v.string = "Method Search: " ++ ~searchString },
517 superClassNameView: { |v| v.string = "" },
518 filenameView: { |v| v.string = ~currentMethod.filenameSymbol },
519 classVarView: { |v| ~class[\classVarView].value(v) },
520 instVarView: { |v| ~class[\instVarView].value(v) },
521 subclassTitle: { |v| v.string = "classes (press return)" },
522 methodTitle: { |v| v.string = "matching methods (press return)" },
524 v.items_(~subclassArray.collect({ |class| class.name }))
525 .value_(~subclassViewIndex ? 0)
527 .enterKeyAction_(~navigateToSubclassAction);
530 v.items_(~methodArray.collect({ |method|
531 method.name ++ " (" ++ method.ownerClass ++ ")"
532 })).action_(~methodSearchMethodViewAction)
533 .value_(~methodViewIndex ? 0)
534 .enterKeyAction_(~navigateToCurrentClassAction)
536 argView: { |v| ~class[\argView].value(v) },
537 buttonsDisabled: #[] // all buttons OK
540 currentClassNameView: { |v| v.string = "Method Search: " ++ ~searchString },
541 superClassNameView: { |v| v.string = "No results found!" },
542 filenameView: { |v| v.string = "" },
543 classVarView: { |v| v.items_(#[]) },
544 instVarView: { |v| v.items_(#[]) },
545 subclassTitle: { |v| v.string = "no classes" },
546 methodTitle: { |v| v.string = "no methods" },
547 subclassView: { |v| v.items_(#[]) },
548 methodView: { |v| v.items_(#[]) },
549 argView: { |v| v.items_(#[]) },
550 buttonsDisabled: buttonSet // all buttons disabled
553 // support funcs shared in common
554 setCurrentClass: { |class|
555 ~currentClass = class ?? { ~subclassArray[~subclassView.value ? 0] };
557 setCurrentMethod: { |method|
558 ~currentMethod = method ?? { ~methodArray[~methodView.value ? 0] };
560 setSubclassArray: { |class|
561 class = class ? ~result;
562 if (class.subclasses.isNil) { ~subclassArray = []; } {
563 ~subclassArray = class.subclasses.copy.sort {|a,b| a.name <= b.name };
566 setMethodArray: { |class|
567 class = class ? ~result;
568 if (class.class.methods.isNil) {
569 ~classMethodArray = [];
570 ~classMethodNames = [];
572 ~classMethodArray = class.class.methods.asArray.copy.sort
573 {|a,b| a.name <= b.name };
574 ~classMethodNames = ~classMethodArray.collect {|method|
575 "*" ++ method.name.asString
578 if (class.methods.isNil) {
582 ~methodArray = class.methods.asArray.copy.sort
583 {|a,b| a.name <= b.name };
584 ~methodNames = ~methodArray.collect {|method|
588 ~methodArray = ~classMethodArray ++ ~methodArray;
590 setClassArrayFromMethodSearch: { |result|
591 var classSet = IdentitySet.new, class;
592 ~result.do({ |method|
593 classSet.add(this.getClass(method));
595 ~subclassArray = classSet.asArray.sort({ |a, b| a.name < b.name });
601 // because these use instance variables, they cannot be defined in the above class method
602 // each ClassBrowser instance gets a separate dictionary with the proper object scope
605 // these are called from GUI, cannot assume currentState is already in force
606 navigateToSubclassAction: {
607 var prevState = currentState, prevMethod = prevState.currentMethod;
608 this.makeState(currentState.subclassArray[currentState.subclassView.value], \class,
609 // methodViewIndex func runs in context
610 // of the new state environment
611 // might be nil unless subclass also implements
612 extraValuesDict: (methodViewIndex: {
613 if(prevMethod.isNil) { 0 } {
614 ~methodArray.detectIndex({ |item|
615 item.name == prevMethod.name
620 navigateToCurrentClassAction: {
621 var prevState = currentState;
622 this.makeState(currentState.currentClass, \class,
623 extraValuesDict: (methodViewIndex: {
624 ~methodArray.indexOf(prevState.currentMethod)
627 subclassViewNormalAction: { |view|
628 currentState.subclassViewIndex = view.value;
630 displayCurrentMethodArgsAction: { |view|
632 ~methodViewIndex = view.value;
633 ~setCurrentMethod.value;
634 ~class[\argView].value(~argView)
637 methodEnterKeyAction: {
638 currentState.currentMethod !? { currentState.currentMethod.openCodeFile };
640 classSearchSubclassViewAction: { |view|
642 ~subclassViewIndex = view.value;
643 ~setCurrentClass.value;
644 ~setMethodArray.value(~currentClass);
645 ~methodViewIndex = 0;
646 ~class[\filenameView].value(~filenameView);
647 ~class[\methodView].value(~methodView);
648 ~class[\argView].value(~argView);
649 ~class[\classVarView].value(~classVarView);
650 ~class[\instVarView].value(~instVarView);
653 methodSearchMethodViewAction: { |view|
656 ~methodViewIndex = view.value;
657 ~currentMethod = ~methodArray[~methodView.value ? 0];
658 ~currentClass = this.getClass(~currentMethod);
659 ~methodSearch[\filenameView].value(~filenameView);
660 ~class[\argView].value(~argView);
661 ~class[\classVarView].value(~classVarView);
662 ~class[\instVarView].value(~instVarView);
663 if((index = ~subclassArray.indexOf(~currentClass)).notNil) {
664 ~subclassView.value = index;
665 ~subclassViewIndex = index;
669 listViewDoubleClickAction: { |view, x, y, mods, buttonNumber, clickCount|
670 if(clickCount == 2) { view.enterKeyAction.value }