scide: LookupDialog - redo lookup on classes after partial lookup
[supercollider.git] / SCClassLibrary / Common / GUI / Document.sc
blob5f82a5588c97dc6add6ce919f1b175f9f56b0167
1 // Since SC v3.2 dev, Document is an ABSTRACT class. Can't be instantiated directly.
2 // Subclasses provide the editor-specific implementation, e.g. CocoaDocument for the standard Mac interface.
3 // Subclasses also (in their SC code files) add a "implementationClass" method to Document to tell it to use them.
5 Document {
7         classvar <dir="", <wikiDir="", <allDocuments, >current;
8         classvar <>globalKeyDownAction, <> globalKeyUpAction, <>initAction;
10         classvar <>autoRun = true;
11         classvar <>wikiBrowse = true;
13         classvar <>implementationClass;
14         classvar <postColor;
15         classvar <theme, <themes;
17         //don't change the order of these vars:
18         var <dataptr, <>keyDownAction, <>keyUpAction, <>mouseUpAction;
19         var <>toFrontAction, <>endFrontAction, <>onClose, <>mouseDownAction;
21         var <stringColor;
23         var <envir, savedEnvir;
24         var <editable;
26           *initClass{
27                 allDocuments = [];
28           }
30         *startup {
31                 var num, doc;
32                 num = this.numberOfOpen;
33                 num.do { | i |
34                         doc = this.newFromIndex(i);
35                 };
36                 postColor = Color.black;
37                 themes = (
38                         default: (
39                                 classColor: Color(0, 0, 0.75, 1),
40                                 textColor: Color(0, 0, 0, 1),
41                                 stringColor: Color(0.375, 0.375, 0.375, 1),
42                                 commentColor: Color(0.75, 0, 0, 1),
43                                 symbolColor: Color(0, 0.45, 0, 1),
44                                 numberColor: Color(0, 0, 0, 1),
45                                 specialValsColor: Color(0.2, 0.2, 0.75, 1), // true false nil inf
46                                 specialVarsColor: Color(0.4, 0.4, 0.75, 1), // super, thisProcess
47                                 declColor: Color(0, 0, 1, 1), // var, const, args
48                                 puncColor: Color(0, 0, 0, 1),
49                                 environColor: Color(1.0, 0.4, 0, 1)
50                                 )
51                         );
52                 theme = themes.default;
53         }
55         *open { | path, selectionStart=0, selectionLength=0, envir |
56                 var doc, env;
57                 env = currentEnvironment;
58                 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
59                 doc = Document.implementationClass.prBasicNew.initFromPath(path, selectionStart, selectionLength);
60                 if (doc.notNil) {
61                         doc.envir_(envir)
62                 } {
63                         currentEnvironment = env
64                 };
65                 ^doc
66         }
68         *new { | title="Untitled", string="", makeListener=false, envir |
69                 var doc, env;
70                 env = currentEnvironment;
71                 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
72                 doc = Document.implementationClass.new(title, string, makeListener);
73                 if (doc.notNil) {
74                         doc.envir_(envir)
75                 } {
76                         currentEnvironment = env
77                 };
78                 ^doc
79         }
81         *dir_ { | path |
82                 path = path.standardizePath;
83                 if(path == "") { dir = path } {
84                         if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
85                                 dir = path ++ "/"
86                         }
87                 }
88         }
90         *wikiDir_ { | path |
91                 path = path.standardizePath;
92                 if(path == "") {wikiDir = path } {
93                         if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
94                                 wikiDir = path ++ "/"
95                         }
96                 }
97         }
99         *standardizePath { | p |
100                 var pathName;
101                 pathName = PathName(p.standardizePath);
102                 ^if(pathName.isRelativePath) {
103                         dir  ++ pathName.fullPath
104                 } {
105                         pathName.fullPath
106                 }
107         }
108         *abrevPath { | path |
109                 if(path.size < dir.size) { ^path };
110                 if(path.copyRange(0,dir.size - 1) == dir) {
111                         ^path.copyRange(dir.size, path.size - 1)
112                 };
113                 ^path
114         }
116         *openDocuments {
117                 ^allDocuments
118         }
120         *hasEditedDocuments {
121                 allDocuments.do { | doc, i |
122                         if(doc.isEdited) {
123                                 ^true;
124                         }
125                 }
126                 ^false
127         }
129         *closeAll { | leavePostWindowOpen = true |
130                 allDocuments.do { | doc, i |
131                         if(leavePostWindowOpen.not) {
132                                 doc.close;
133                         } {
134                                 if(doc.isListener.not) {
135                                         doc.close;
136                                 }
137                         }
138                 }
139         }
141         *closeAllUnedited { | leavePostWindowOpen = true |
142                 var listenerWindow;
143                 allDocuments.do({ | doc, i |
144                         if(doc.isListener,{
145                                 listenerWindow = doc;
146                         },{
147                                 if(doc.isEdited.not, {
148                                         doc.close;
149                                         });
150                         })
151                 });
152                 if(leavePostWindowOpen.not, {
153                         listenerWindow.close;
154                 })
155         }
157         *current {
158                 if ( thisProcess.platform.hasFeature( \emacs ), {
159                         ^this.implementationClass.current;
160                 });
161                 ^current;
162         }
164         *listener {
165                 ^allDocuments[this.implementationClass.prGetIndexOfListener];
166         }
167         isListener {
168                 ^allDocuments.indexOf(this) == this.class.prGetIndexOfListener
169         }
171 // document setup
173         path {
174                 ^this.prGetFileName
175         }
176         path_ { |apath|
177                 this.prSetFileName(apath);
178         }
179         dir { var path = this.path; ^path !? { path.dirname } }
180         name {
181                 ^this.title
182         }
184         name_ { |aname|
185                 this.title_(aname)
186         }
188         title {
189                 ^this.prGetTitle
190         }
192         title_ { | argName |
193                 this.prSetTitle(argName);
194         }
196         background_ { | color |
197                 this.prSetBackgroundColor(color);
198         }
199         background {
200                 var color;
201                 color = Color.new;
202                 this.prGetBackgroundColor(color);
203                 ^color;
204         }
206         selectedBackground_ { | color |
207                 this.prSetSelectedBackgroundColor(color);
208         }
210         selectedBackground {
211                 var color;
212                 color = Color.new;
213                 this.prGetSelectedBackgroundColor(color);
214                 ^color;
215         }
217         *postColor_ { | col |
218                 postColor = col;
219                 ^Document.implementationClass.postColor_(col);
220         }
222         stringColor_ { | color, rangeStart = -1, rangeSize = 0 |
223                 stringColor = color;
224                 this.setTextColor(color,rangeStart, rangeSize);
225         }
226         bounds {
227                 ^this.prGetBounds(Rect.new);
228         }
229         bounds_ { | argBounds |
230                 ^this.prSetBounds(argBounds);
231         }
233 // interaction:
235         close {
236                 this.prclose
237         }
239         front {
240                 ^this.subclassResponsibility(thisMethod)
241         }
243         unfocusedFront {
244                 ^this.subclassResponsibility(thisMethod)
245         }
247         alwaysOnTop_ { |boolean=true|
248                 ^this.subclassResponsibility(thisMethod)
249         }
251         alwaysOnTop {
252                 ^this.subclassResponsibility(thisMethod)
253         }
255         syntaxColorize {
256                 ^this.subclassResponsibility(thisMethod)
257         }
259         selectLine { | line |
260                 this.prSelectLine(line);
261         }
263         selectRange { | start=0, length=0 |
264                 ^this.subclassResponsibility(thisMethod)
265         }
267         editable_ { | abool=true |
268                 editable = abool;
269                 this.prIsEditable_(abool);
270         }
271         removeUndo {
272                 ^this.subclassResponsibility(thisMethod)
273         }
275         promptToSave_ { | bool |
276                 ^this.subclassResponsibility(thisMethod)
277         }
279         promptToSave {
280                 ^this.subclassResponsibility(thisMethod)
281         }
283         underlineSelection {
284                 ^this.subclassResponsibility(thisMethod)
285         }
287         *setTheme { | themeName |
288                 theme = themes[themeName];
289                 if(theme.proto.isNil) {
290                         theme = theme.copy.parent_(themes[\default]);
291                 };
292                 thisProcess.platform.writeClientCSS;
293                 Document.implementationClass.prSetSyntaxColorTheme(
294                         theme.textColor,
295                         theme.classColor,
296                         theme.stringColor,
297                         theme.symbolColor,
298                         theme.commentColor,
299                         theme.numberColor,
300                         theme.specialValsColor,
301                         theme.specialVarsColor,
302                         theme.declColor,
303                         theme.puncColor,
304                         theme.environColor
305                 );
306         }
308 // state info
309         isEdited {
310                 ^this.subclassResponsibility(thisMethod)
311         }
312         isFront {
313                 ^Document.current === this
314         }
316         selectionStart {
317                 ^this.selectedRangeLocation
318         }
320         selectionSize {
321                 ^this.selectedRangeSize
322         }
324         string { | rangestart, rangesize = 1 |
325                 if(rangestart.isNil,{
326                 ^this.text;
327                 });
328                 ^this.rangeText(rangestart, rangesize);
329         }
331         string_ { | string, rangestart = -1, rangesize = 1 |
332                 this.insertTextRange(string, rangestart, rangesize);
333         }
334         selectedString {
335                 ^this.selectedText
336         }
339         font_ { | font, rangestart = -1, rangesize=0 |
340                 this.setFont(font, rangestart, rangesize)
341         }
343         selectedString_ { | txt |
344                 this.prinsertText(txt)
345         }
347         currentLine {
348                 ^this.getSelectedLines(this.selectionStart, 0);
349         }
351         getSelectedLines { | rangestart = -1, rangesize = 0 |
352                 var start, end, str, max;
353                 str = this.string;
354                 max = str.size;
355                 start = rangestart;
356                 end = start + rangesize;
357                 while {
358                         str[start] !== Char.nl and: { start >= 0 }
359                 } { start = start - 1 };
360                 while {
361                         str[end] !== Char.nl and: { end < max }
362                 } { end = end + 1 };
363                 ^str.copyRange(start + 1, end);
364         }
366 //actions:
368         didBecomeKey {
369                 this.class.current = this;
370                 this.saveCurrentEnvironment;
371                 toFrontAction.value(this);
372         }
374         didResignKey {
375                 endFrontAction.value(this);
376                 this.restoreCurrentEnvironment;
377         }
379         makeWikiPage { | wikiWord, extension=(".rtf"), directory |
380                 var filename, file, doc, string, dirName;
381                 directory = directory ? wikiDir;
382                 filename = directory ++ wikiWord ++ extension;
383                 file = File(filename, "w");
384                 if (file.isOpen) {
385                         string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
386                                 "{\\colortbl;\\red255\\green255\\blue255;}\n"
387                                 "Write about " ++ wikiWord ++ " here.\n}";
388                         file.write(string);
389                         file.close;
391                         doc = this.class.open(filename);
392                         doc.path = filename;
393                         doc.selectRange(0,0x7FFFFFFF);
394                         doc.onClose = {
395                                 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
396                                         unixCmd("rm" + filename)
397                                 };
398                         };
399                 } {
400                         // in a second try, check if a path must be created.
401                         // user makes double click on string.
402                         dirName = wikiWord.dirname;
403                         if(dirName != ".") {
404                                 dirName = directory ++ dirName;
405                                 "created directory: % \n".postf(dirName);
406                                 dirName.mkdir;
407                         };
408                 }
409         }
411         openWikiPage {
412                 var selectedText, filename, index, directory;
413                 var extensions = #[".rtf", ".sc", ".scd", ".txt", "", ".rtfd", ".html"];
414                 selectedText = this.selectedText;
415                 index = this.selectionStart;
417                 this.selectRange(index, 0);
419                 // refer to local link with round parens
420                 if(selectedText.first == $( /*)*/ and: {/*(*/ selectedText.last == $) }) {
421                                 selectedText = selectedText[1 .. selectedText.size-2];
422                                 directory = Document.current.path.dirname ++ "/";
423                 } {
424                                 directory = wikiDir;
425                 };
427                 case { selectedText[0] == $* }
428                 {
429                         // execute file
430                         selectedText = selectedText.drop(1);
431                         extensions.do { |ext|
432                                 filename = directory ++ selectedText ++ ext;
433                                 if (File.exists(filename)) {
434                                         // open existing wiki page
435                                         filename.load;
436                                         ^this
437                                 }
438                                 {
439                                 filename = "Help/help-scripts/" ++ selectedText ++ ext;
440                                 if (File.exists(filename)) {
441                                         // open help-script document
442                                         filename.load;
443                                         ^this
444                                 }
445                                 }
446                         };
447                 }
448                 { selectedText.first == $[ and: { selectedText.last == $] }}
449                 {
450                         // open help file
451                         selectedText[1 .. selectedText.size-2].help
452                 }
453                 { selectedText.containsStringAt(0, "http://")
454                         or: { selectedText.containsStringAt(0, "file://") } }
455                 {
456                         // open URL
457                         openOS(selectedText)
458                 }
459                 { selectedText.containsStringAt(selectedText.size-1, "/") }
460                 {
461                         Document(selectedText,
462                                 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
463                         )
464                 }
466                 {
467                         if(index + selectedText.size > this.text.size) { ^this };
468                         extensions.do { |ext|
469                                 filename = directory ++ selectedText ++ ext;
470                                 if (File.exists(filename)) {
471                                         // open existing wiki page
472                                         this.class.open(filename);
473                                         ^this
474                                 }
475                         };
476                         // make a new wiki page
477                         this.makeWikiPage(selectedText, nil, directory);
478                 };
479         }
481         mouseUp{ | x, y, modifiers, buttonNumber, clickCount, clickPos |
482                 mouseUpAction.value(this, x, y, modifiers, buttonNumber, clickCount);           if (wikiBrowse and: { this.linkAtClickPos(clickPos).not }
483                         and: { this.selectUnderlinedText(clickPos) } ) {
484                         ^this.openWikiPage
485                 };
486         }
488         keyDown { | character, modifiers, unicode, keycode |
489                 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
490                 keyDownAction.value(this,character, modifiers, unicode, keycode);
491         }
493         keyUp { | character, modifiers, unicode, keycode |
494                 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
495                 keyUpAction.value(this,character, modifiers, unicode, keycode);
496         }
498         == { | doc |
499                 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
500                         this.path == doc.path
501                 }
502         }
504         hash {
505                 ^(this.path ? this).hash
506         }
508         *defaultUsesAutoInOutdent_ {|bool|
509                 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
510         }
512         usesAutoInOutdent_ {|bool|
513                 this.prUsesAutoInOutdent_(bool)
514         }
516         *prDefaultUsesAutoInOutdent_{|bool|
517                 this.subclassResponsibility(thisMethod);
518         }
520         prUsesAutoInOutdent_{|bool|
521                 ^this.subclassResponsibility(thisMethod);
522         }
525 // private implementation
527         prIsEditable_{ | editable=true |
528                 ^this.subclassResponsibility(thisMethod)
529         }
530         prSetTitle { | argName |
531                 ^this.subclassResponsibility(thisMethod)
532         }
533         prGetTitle {
534                 ^this.subclassResponsibility(thisMethod)
535         }
536         prGetFileName {
537                 ^this.subclassResponsibility(thisMethod)
538         }
539         prSetFileName { | apath |
540                 ^this.subclassResponsibility(thisMethod)
541         }
542         prGetBounds { | argBounds |
543                 ^this.subclassResponsibility(thisMethod)
544         }
546         prSetBounds { | argBounds |
547                 ^this.subclassResponsibility(thisMethod)
548         }
550         *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC, specialValsC, specialVarsC, declC, puncC, environC|
551                 ^this.subclassResponsibility(thisMethod);
552         }
554         // if range is -1 apply to whole doc
555         setFont { | font, rangeStart= -1, rangeSize=100 |
556                 ^this.subclassResponsibility(thisMethod)
557         }
559         setTextColor { | color,  rangeStart = -1, rangeSize = 0 |
560                 ^this.subclassResponsibility(thisMethod)
561         }
563         text {
564                 ^this.subclassResponsibility(thisMethod)
565         }
566         selectedText {
567                 ^this.subclassResponsibility(thisMethod)
568         }
569         selectUnderlinedText { | clickPos |
570                 ^this.subclassResponsibility(thisMethod)
571         }
573         linkAtClickPos { | clickPos |
574                 ^this.subclassResponsibility(thisMethod)
575         }
577         rangeText { | rangestart=0, rangesize=1 |
578                 ^this.subclassResponsibility(thisMethod)
579         }
581         prclose {
582                 ^this.subclassResponsibility(thisMethod)
583         }
585         closed {
586                 onClose.value(this); // call user function
587                 this.restoreCurrentEnvironment;
588                 allDocuments.remove(this);
589                 dataptr = nil;
590         }
592         prinsertText { | dataPtr, txt |
593                 ^this.subclassResponsibility(thisMethod)
594         }
595         insertTextRange { | string, rangestart, rangesize |
596                 ^this.subclassResponsibility(thisMethod)
597         }
599         prAdd {
600                 allDocuments = allDocuments.add(this);
601                 this.editable = true;
602                 if (autoRun) {
603                         if (this.rangeText(0,7) == "/*RUN*/")
604                         {
605                                 this.text.interpret;
606                         }
607                 };
608                 current = this;
609                 initAction.value(this);
611         }
613         //this is called after recompiling the lib
614         *prnumberOfOpen {
615                 ^this.subclassResponsibility(thisMethod)
616         }
617         *numberOfOpen {
618                 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
619                         ^this.prnumberOfOpen
620                 } { ^allDocuments.size };
621                 ^0
622         }
624         *newFromIndex { | idx |
625                 ^super.new.initByIndex(idx)
626         }
627         initByIndex { | idx |
628                 //allDocuments = allDocuments.add(this);
629                 var doc;
630                 doc = this.prinitByIndex(idx);
631                 if(doc.isNil,{^nil});
632                 this.prAdd;
633         }
634         prinitByIndex { | idx |
635                 ^this.subclassResponsibility(thisMethod)
636         }
638         //this is called from the menu: open, new
639         *prGetLast {
640                 ^Document.implementationClass.prBasicNew.initLast
641         }
643         initLast {
644                 ^this.subclassResponsibility(thisMethod)
645         }
647         prGetLastIndex {
648                 ^this.subclassResponsibility(thisMethod)
649         }
651         // private open
652         initFromPath { | path, selectionStart, selectionLength |
653                 var stpath;
654         //      path = apath;
655                 stpath = this.class.standardizePath(path);
656                 this.propen(stpath, selectionStart, selectionLength);
657                 if(dataptr.isNil,{
658                         this.class.allDocuments.do{ |d|
659                                         if(d.path == stpath.absolutePath){
660                                                 ^d
661                                         }
662                                 };
663                         ^nil
664                 });
665                 this.background_(Color.white);
666                 ^this.prAdd;
667         }
668         propen { | path, selectionStart=0, selectionLength=0 |
669                 ^this.subclassResponsibility(thisMethod)
670         }
672         // private newTextWindow
673         initByString{ | argTitle, str, makeListener |
675                 this.prinitByString(argTitle, str, makeListener);
676                 this.background_(Color.white);
677                 if(dataptr.isNil,{^nil});
678                 this.prAdd;
679                 this.title = argTitle;
681         }
682         prinitByString { | title, str, makeListener |
683                 ^this.subclassResponsibility(thisMethod)
684         }
686         // other private
687         // if -1 whole doc
689         prSetBackgroundColor { | color |
690                 ^this.subclassResponsibility(thisMethod)
691         }
692         prGetBackgroundColor { | color |
693                 ^this.subclassResponsibility(thisMethod)
694         }
695         prSetSelectedBackgroundColor { | color |
696                 ^this.subclassResponsibility(thisMethod);
697         }
698         prGetSelectedBackgroundColor { | color |
699                 ^this.subclassResponsibility(thisMethod);
700         }
701         selectedRangeLocation {
702                 ^this.subclassResponsibility(thisMethod)
703         }
704         selectedRangeSize {
705                 ^this.subclassResponsibility(thisMethod)
706         }
708         prSelectLine { | line |
709                 ^this.subclassResponsibility(thisMethod)
710         }
712         *prGetIndexOfListener {
713                 ^this.subclassResponsibility(thisMethod)
714         }
717         //---not yet implemented
718         // ~/Documents
719         // /Volumes
720         // Music/Patches
722         //*reviewUnsavedDocumentsWithAlertTitle
723         //*saveAllDocuments
724         //*recentDocumentPaths
725         //save
726         //saveAs
727         //print
728         //
729         //hasPath  was loaded
732 // Environment handling  Document with its own envir must set and restore currentEnvironment on entry and exit.
733 // Requires alteration of *open, *new, closed, didBecomeKey, and didResignKey
735         envir_ { | ev |
736                 envir = ev;
737                 if (this.class.current == this) {
738                         if (savedEnvir.isNil) {
739                                 this.saveCurrentEnvironment
740                         }
741                 }
742         }
744         restoreCurrentEnvironment {
745                 if (envir.notNil) { currentEnvironment = savedEnvir };
746         }
748         saveCurrentEnvironment {
749                 if (envir.notNil) {
750                         savedEnvir = currentEnvironment;
751                         currentEnvironment = envir;
752                 }
753         }
755         *prBasicNew {
756                 ^super.new
757         }