Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / SCClassLibrary / Platform / osx / AutoComplete / AutocompleteBrowsers.sc
blobbaf1f31005a6fd64fbd0ea6b1e38e9c99e889f29
1 AutoCompMethodBrowser {
2         classvar        <>wWidth = 400, <>wHeight = 360;
3         classvar        <methodExclusions, <classExclusions;
5         classvar        <w, textField, listView,                // gui objects
6                 selector,                       // string typed by the user to match to classes or methods
7                 masterList,             // a list of all methods with that selector
8                 reducedList,            // only the ones displayed
9                 skipThis,                       // flag: do (this, xxx) or (xxx) in argList
10                 dropMeta,                       // flag: display Meta_ for metaclasses?
11                 doc,                            // document from which this was created
12                 start, size;            // start and length of identifier in document
14         classvar        overwriteOnCancel = false;
16         *initClass {
17                 methodExclusions = methodExclusions.asArray;
18         }
20                 // prevent certain method selector strings from being gui'ed during typing
21                 // assumes that selectors are spelled correctly!
22                 // startup.rtf should include AutoCompMethodBrowser.exclude([\value, \if, ...]);
23         *exclude { |selectorArray|
24                 methodExclusions = methodExclusions ++ selectorArray.collect({ |sel| sel.asSymbol });
25         }
27         *new { arg start, size, doc;
28                         // may only open a browser if there isn't one open already
29                 (w.isNil and: { this.newCondition(start, size, doc) }).if({
30                         ^this.init(start, size, doc)
31                 }, {
32                         w.notNil.if({ w.front });
33                         ^nil
34                 });
35         }
37         *newCondition { arg start, size, doc;
38                 var     selector;
39                 selector = doc.string(start, size).asSymbol;
40                 ^Document.allowAutoComp and: { methodExclusions.includes(selector).not }
41         }
43         *init { arg argStart, argSize, argDoc;
44                 var     displaySel, temp, initString;
45                 skipThis = dropMeta = false;
46                 selector = argDoc.string(argStart, argSize);
47                         // if no string, abort
48                 (selector.size == 0).if({ ^nil });
49                         // if it's part of a class name,
50                 (selector[0] >= $A and: { selector[0] <= $Z }).if({
51                         AutoCompClassBrowser.classExclusions.includes(selector.asSymbol.asClass).not.if({
52                                         // identify classes containing that string
53                                 masterList = Class.allClasses.select({ |cl|
54                                         cl.isMetaClass and: { cl.name.asString.containsi(selector) }
55                                 }).collect({ |cl|       // then grab their *new methods
56                                         [cl, cl.findRespondingMethodFor(\new), cl.name]  // cl.name used for sorting
57                                 }).reject({ |item| item[1].isNil });
58                                 initString = selector;
59                                 displaySel = "new";
60                                 skipThis = dropMeta = true;
61                                 overwriteOnCancel = true;
62                         });
63                 }, {
64                         masterList = IdentitySet.new;
65                         Class.allClasses.do({ |class|
66                                 class.methods.do({ |method|
67                                         method.name.asString.containsi(selector).if({
68                                                 masterList.add([class, method,
69                                                         class.name ++ "-" ++ method.name
70                                                 ]);
71                                         });
72                                 });
73                         });
74                         initString = "";
75                         displaySel = selector;
76                         overwriteOnCancel = false;
77                 });
78                 (masterList.size > 0).if({
79                                 // this sort will sort both of them (desired)
80                                 // [0] is the ownerclass, [1] the method
81                         masterList = masterList.asArray.sort({ |a, b| a[2] < b[2] });
82                                 // if previous char is a ., then class should not be displayed in argList
83                         (argStart > 0 and: { argDoc.string(argStart-1, 1)[0] == $. }).if({ skipThis = true });
84                         doc = argDoc;
85                         start = argStart;
86                         size = argSize;
87                         this.prInit("." ++ displaySel);
88                         textField.string_(initString);
89                         this.restrictList;
90                 }, {
91                         ^nil
92                 });
93         }
95         *free { |finished = false|
96                 var     string;
97                         // if window is nil, isclosed should be true
98                         // close the window only if it isn't closed
99                 (w.tryPerform(\isClosed) ? true).not.if({
100                                 // if there's typing in the text box and no possible autocomplete,
101                                 // add it into the document
102                         ((string = textField.string).size > 0 and: { finished.not }).if({
103                                         // does the string start with the selector?
104                                         // size var is size of selector
105                                 (string[0..size-1] == selector).if({
106                                                 // if so, drop it so the rest can go in the document
107                                         string = string[size..];
108                                 }, {
109                                                 // if not, and the selector is a class, we need to drop it from doc
110                                         overwriteOnCancel.if({
111                                                 doc.selectRange(start, size+1)
112                                         });
113                                 });
114                                 doc.selectedString_(string);
115                         });
116                         w.close;
117                 });
118                         // garbage; also, w = nil allows next browser to succeed
119                 w = masterList = reducedList = nil;
120         }
122         *finish {
123                 var     selectStart, selectSize, str;
124                         // select the right text in the doc and replace with method template
125                 (reducedList.size > 0).if({
126                         doc.selectRange(start, size+1)  // must replace open paren which size doesn't include
127                                 .selectedString_(str = this.finishString(reducedList[listView.value]));
128                         #selectStart, selectSize = this.finalSelection(str);
129                         doc.selectRange(selectStart, selectSize);       // reposition cursor
130                         textField.string_("");  // .free will do something bad if I don't clear this
131                 });
132                 this.free(true);
133         }
135         *finalSelection { |str|
136                 var  openParen, closeParen;
137                 (openParen = str.detectIndex({ |ch| ch.ascii == 40 })).isNil.if({
138                         ^[start + str.size, 0]
139                 }, {
140                         closeParen = str.detectIndex({ |ch| ch.ascii == 41 });
141                         ^[openParen + 1 + start, closeParen-openParen-1]
142                 });
143         }
145         *finishString { |meth|
146                 ^dropMeta.if({ meth[0].name.asString.copyRange(5, 2000) },
147                                 { meth[1].name })
148                         ++ meth[1].argList(skipThis)
149         }
151         *itemList { arg mList;
152                 ^mList.collect({ |meth|
153                         this.listItem(meth);
154                 });
155         }
157         *listItem { arg meth;
158                 ^dropMeta.if({ meth[0].name.asString.copyRange(5, 2000) },
159                                 { meth[0].name })
160                         ++ "-" ++ meth[1].name ++ meth[1].argList(skipThis)
161         }
163         *restrictList {
164                 var     str, nametemp, keep;
165                 str = textField.string;
166                 reducedList = masterList.select({ |item|
167                         keep = true;
168                         nametemp = this.listItem(item);
169                                 // if str is 0 length, loop doesn't execute and all items will be kept
170                         str.do({ |chr, i|
171                                 (nametemp[i] != chr).if({ keep = false });
172                         });
173                         keep
174                 });
175                 listView.items_(this.itemList(reducedList)).value_(0);
176         }
178         *prInit { arg title;
179                 var boundsTemp, gui;
180                 \CocoaGUI.asClass.notNil.if({
181                         gui = CocoaGUI;  // maybe GUI.current will be supportable later
182                         boundsTemp = gui.window.screenBounds;
183                                 // center the window on screen
184                         w = gui.window.new(title, Rect(
185                                 (boundsTemp.width - wWidth) / 2, (boundsTemp.height - wHeight) / 2,
186                                 wWidth, wHeight
187                         )).onClose_({ this.free });
188                         gui.staticText.new(w, Rect(5, 25, wWidth-10, 20))
189                                 .string_("Type a bit or click and [cr] in the list");
190                         // 3.2 -> 3.3 transition hack - will hardcode SCTextFieldOld later
191                         textField = ('SCTextFieldOld'.asClass ?? { SCTextField })
192                                 .new(w, Rect(5, 50, wWidth - 10, 20)).resize_(2);
193                         listView = gui.listView.new(w, Rect(5, 75, wWidth - 10, wHeight - 80))
194                                 .resize_(5)
195                                 .keyDownAction_({ |listV, char, modifiers, keycode|
196                                         case
197                                                 { (modifiers bitAnd: 10485760 > 0) and: (keycode == 63232) }
198                                                         { listView.value = (listView.value - 1) % reducedList.size }
199                                                 { (modifiers bitAnd: 10485760 > 0) and: (keycode == 63233) }
200                                                         { listView.value = (listView.value + 1) % reducedList.size }                                    { char.ascii == 13 } { GUI.use( gui, {this.finish })}
201                                                 { char.ascii == 27 } { this.free }
202                                 });
204                         textField.keyDownAction_({ |txt, char, modifiers, unicode|
205                                 case
206                                         { (modifiers bitAnd: 10485760 > 0) and: (unicode == 63232) }
207                                                 { listView.value = (listView.value - 1) % reducedList.size }
208                                         { (modifiers bitAnd: 10485760 > 0) and: (unicode == 63233) }
209                                                 { listView.value = (listView.value + 1) % reducedList.size }
210                                         { char.ascii == 13 } { GUI.use( gui, { this.finish })}
211                                         { char.ascii == 27 } { this.free }
212                                                 // default:
213                                         {       txt.defaultKeyDownAction(char, modifiers, unicode);
214                                                 this.restrictList(txt.string);
215                                         };
216                         })
217                                 .action_({ GUI.use( gui, {this.finish })})
218                                 .focus;
219                         w.front;
220                 });
221         }
225 AutoCompClassBrowser : AutoCompMethodBrowser {
226         classvar        savedClass;
228         *initClass {
229                 classExclusions = classExclusions.asArray;
230         }
232                 // an actual array of classes, not symbols: [Nil, Boolean, True, False...]
233                 // unforgiving of typos!
234         *exclude { |classArray|
235                 classExclusions = classExclusions ++ (classArray.collect({ |cl|
236                         [cl, ("Meta_" ++ cl.name).asSymbol.asClass]
237                 }).flat);
238         }
240         *newCondition { arg start, size, doc;
241                 var     class;
242                 class = ("Meta_" ++ doc.string(start, size)).asSymbol;
243                 ^(Document.allowAutoComp and:
244                         { classExclusions.includes(class.asClass).not } and:                    { Class.allClasses.detectIndex({ |cl| cl.name == class }).notNil })
245         }
247         *init { arg argStart, argSize, argDoc;
248                         // newCondition determines that the class exists, so now it should be
249                         // ok to interpret the string to get the class
250                 savedClass = ("Meta_" ++ argDoc.string(argStart, argSize)).interpret;
251                 reducedList = masterList = this.getMethods(savedClass).sort({ |a, b|
252                         a.name < b.name
253                 });
254                 doc = argDoc;
255                 start = argStart;
256                 size = argSize;
257                 this.prInit(savedClass.name);
258                 this.restrictList;
259                 overwriteOnCancel = false;
260         }
262         *getMethods { arg class;
263                 var     list, existsFlag;
264                 list = List.new;
265                         // loop through superclasses, but only Meta_ classes allowed
266                 { class != Class }.while({
267                                 // only if this class is not excluded
268                         classExclusions.includes(class).not.if({
269                                         // for each method
270                                 class.methods.do({ |meth|
271                                                 // does this method name already exist in the list?
272                                                 // this accounts for subclasses overriding superclass methods
273                                         existsFlag = false;
274                                         list.do({ |item| (item.name == meth.name).if({ existsFlag = true }); });
275                                                 // if not, add to the list
276                                         existsFlag.not.if({ list.add(meth); });
277                                 });
278                         });
279                         class = class.superclass;
280                 });
281                 ^list
282         }
284         *finishString { |meth|
285                 var strtemp;
286                 strtemp = savedClass.name ++ "." ++ meth.name ++ meth.argList;
287                 ^strtemp.copyRange(5, strtemp.size-1);
288         }
290         *listItem { arg meth;
291                 ^meth.name ++ meth.argList
292         }
296 // need to think about this some more
297 // what if user deletes chars but doesn't add?
298 // maybe best solution is *free below -- never add chars
299 // ok because user will not type ctrl-. in normal use
301 AutoCompClassSearch : AutoCompClassBrowser {
302         classvar        classBrowser;   // reserve one class browser for autocomplete use
303 //                      userEditedString = false;
305         *newCondition { ^Document.allowAutoComp }       // no restrictions on when a window will be opened
307         *init { arg argStart, argSize, argDoc;
308                 selector = argDoc.string(argStart, argSize);
309                 reducedList = masterList = Class.allClasses.reject({ |cl|
310                                 cl.isMetaClass or: { classExclusions.includes(cl) } })
311                         .select({ |cl| cl.name.asString.containsi(selector) })
312                         .sort({ |a, b| a.name < b.name });
313                 doc = argDoc;
314                 start = argStart;
315                 size = argSize;
316                 this.prInit("Open a class browser");
317                 textField.string_(selector);
318 //              userEditedString = false;
319                 this.restrictList;
320         }
322         *free { super.free(true) }
324         *finish {
325                         // I need to test whether the method chosen is in a superclass of the class
326                         // selected here -- see finishString
327                 savedClass = reducedList[listView.value];
328                 classBrowser = savedClass.browse;
329                         // set enter key action to drop a method template into the document
330                         // methodView's enter key action can change
331                         // based on what you do in the browser;
332                         // save the custom action into the browser instance's environment
333                         // so that this customization persists
334                 classBrowser.views.put(\methodEnterKeyAction, {
335                         var str, newstart, newsize;
336                                 // can't close unless a method is really chosen
337                         if(classBrowser.currentMethod.notNil) {
338                                 doc.selectRange(start, size)
339                                         .selectedString_(str = this.finishString);
340                                 #newstart, newsize = this.finalSelection(str);
341                                 doc.selectRange(newstart, newsize);
342                                 classBrowser.close;
343                         };
344                 });
345                 classBrowser.views.methodView.focus
346                         .enterKeyAction_(classBrowser.views[\methodEnterKeyAction])
347                         .keyDownAction_({ |list, char, modifiers, unicode|
348                                 (char == $^).if({
349                                         classBrowser.views.superButton.doAction;
350                                 }, {
351                                         list.defaultKeyDownAction(char, modifiers, unicode)
352                                 });
353                         });
354                 this.free(true);
355         }
357         *finishString {
358                 var meth, classname;
359                 meth = classBrowser.currentMethod;
360                 if(meth.ownerClass.isMetaClass) {
361                                 // if the method chosen comes from this class or a superclass,
362                                 // use this class, otherwise use the class selected in the class browser
363                         if(savedClass.metaclass == meth.ownerClass
364                                 or: { savedClass.metaclass.superclasses.includes(meth.ownerClass) }) {
365                                 classname = savedClass.name
366                         } {
367                                 classname = meth.ownerClass.name.asString.copyRange(5, 2000)
368                         };
369                         ^classname.asString
370                                 ++ "." ++ meth.name ++ meth.argList
371                 } {
372                         ^meth.name ++ meth.argList
373                 };
374         }
376         *listItem { |cl|
377                 ^cl.name.asString
378         }