class library: guard Document-*listener and Document-isListener
[supercollider.git] / SCClassLibrary / Common / GUI / Document.sc
blob5937037d4c6c0ec60e1f90c01518d9fcbaff1072
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                 var index = this.prGetIndexOfListener;
166                 if (index.notNil) {
167                         ^allDocuments[index];
168                 } {
169                         ^nil
170                 }
171         }
173         isListener {
174                 var index = this.class.prGetIndexOfListener;
175                 if (index.notNil) {
176                         ^allDocuments.indexOf(this) == index
177                 } {
178                         ^False
179                 }
180         }
182 // document setup
184         path {
185                 ^this.prGetFileName
186         }
187         path_ { |apath|
188                 this.prSetFileName(apath);
189         }
190         dir { var path = this.path; ^path !? { path.dirname } }
191         name {
192                 ^this.title
193         }
195         name_ { |aname|
196                 this.title_(aname)
197         }
199         title {
200                 ^this.prGetTitle
201         }
203         title_ { | argName |
204                 this.prSetTitle(argName);
205         }
207         background_ { | color |
208                 this.prSetBackgroundColor(color);
209         }
210         background {
211                 var color;
212                 color = Color.new;
213                 this.prGetBackgroundColor(color);
214                 ^color;
215         }
217         selectedBackground_ { | color |
218                 this.prSetSelectedBackgroundColor(color);
219         }
221         selectedBackground {
222                 var color;
223                 color = Color.new;
224                 this.prGetSelectedBackgroundColor(color);
225                 ^color;
226         }
228         *postColor_ { | col |
229                 postColor = col;
230                 ^Document.implementationClass.postColor_(col);
231         }
233         stringColor_ { | color, rangeStart = -1, rangeSize = 0 |
234                 stringColor = color;
235                 this.setTextColor(color,rangeStart, rangeSize);
236         }
237         bounds {
238                 ^this.prGetBounds(Rect.new);
239         }
240         bounds_ { | argBounds |
241                 ^this.prSetBounds(argBounds);
242         }
244 // interaction:
246         close {
247                 this.prclose
248         }
250         front {
251                 ^this.subclassResponsibility(thisMethod)
252         }
254         unfocusedFront {
255                 ^this.subclassResponsibility(thisMethod)
256         }
258         alwaysOnTop_ { |boolean=true|
259                 ^this.subclassResponsibility(thisMethod)
260         }
262         alwaysOnTop {
263                 ^this.subclassResponsibility(thisMethod)
264         }
266         syntaxColorize {
267                 ^this.subclassResponsibility(thisMethod)
268         }
270         selectLine { | line |
271                 this.prSelectLine(line);
272         }
274         selectRange { | start=0, length=0 |
275                 ^this.subclassResponsibility(thisMethod)
276         }
278         editable_ { | abool=true |
279                 editable = abool;
280                 this.prIsEditable_(abool);
281         }
282         removeUndo {
283                 ^this.subclassResponsibility(thisMethod)
284         }
286         promptToSave_ { | bool |
287                 ^this.subclassResponsibility(thisMethod)
288         }
290         promptToSave {
291                 ^this.subclassResponsibility(thisMethod)
292         }
294         underlineSelection {
295                 ^this.subclassResponsibility(thisMethod)
296         }
298         *setTheme { | themeName |
299                 theme = themes[themeName];
300                 if(theme.proto.isNil) {
301                         theme = theme.copy.parent_(themes[\default]);
302                 };
303                 thisProcess.platform.writeClientCSS;
304                 Document.implementationClass.prSetSyntaxColorTheme(
305                         theme.textColor,
306                         theme.classColor,
307                         theme.stringColor,
308                         theme.symbolColor,
309                         theme.commentColor,
310                         theme.numberColor,
311                         theme.specialValsColor,
312                         theme.specialVarsColor,
313                         theme.declColor,
314                         theme.puncColor,
315                         theme.environColor
316                 );
317         }
319 // state info
320         isEdited {
321                 ^this.subclassResponsibility(thisMethod)
322         }
323         isFront {
324                 ^Document.current === this
325         }
327         selectionStart {
328                 ^this.selectedRangeLocation
329         }
331         selectionSize {
332                 ^this.selectedRangeSize
333         }
335         string { | rangestart, rangesize = 1 |
336                 if(rangestart.isNil,{
337                 ^this.text;
338                 });
339                 ^this.rangeText(rangestart, rangesize);
340         }
342         string_ { | string, rangestart = -1, rangesize = 1 |
343                 this.insertTextRange(string, rangestart, rangesize);
344         }
345         selectedString {
346                 ^this.selectedText
347         }
350         font_ { | font, rangestart = -1, rangesize=0 |
351                 this.setFont(font, rangestart, rangesize)
352         }
354         selectedString_ { | txt |
355                 this.prinsertText(txt)
356         }
358         currentLine {
359                 ^this.getSelectedLines(this.selectionStart, 0);
360         }
362         getSelectedLines { | rangestart = -1, rangesize = 0 |
363                 var start, end, str, max;
364                 str = this.string;
365                 max = str.size;
366                 start = rangestart;
367                 end = start + rangesize;
368                 while {
369                         str[start] !== Char.nl and: { start >= 0 }
370                 } { start = start - 1 };
371                 while {
372                         str[end] !== Char.nl and: { end < max }
373                 } { end = end + 1 };
374                 ^str.copyRange(start + 1, end);
375         }
377 //actions:
379         didBecomeKey {
380                 this.class.current = this;
381                 this.saveCurrentEnvironment;
382                 toFrontAction.value(this);
383         }
385         didResignKey {
386                 endFrontAction.value(this);
387                 this.restoreCurrentEnvironment;
388         }
390         makeWikiPage { | wikiWord, extension=(".rtf"), directory |
391                 var filename, file, doc, string, dirName;
392                 directory = directory ? wikiDir;
393                 filename = directory ++ wikiWord ++ extension;
394                 file = File(filename, "w");
395                 if (file.isOpen) {
396                         string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
397                                 "{\\colortbl;\\red255\\green255\\blue255;}\n"
398                                 "Write about " ++ wikiWord ++ " here.\n}";
399                         file.write(string);
400                         file.close;
402                         doc = this.class.open(filename);
403                         doc.path = filename;
404                         doc.selectRange(0,0x7FFFFFFF);
405                         doc.onClose = {
406                                 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
407                                         unixCmd("rm" + filename)
408                                 };
409                         };
410                 } {
411                         // in a second try, check if a path must be created.
412                         // user makes double click on string.
413                         dirName = wikiWord.dirname;
414                         if(dirName != ".") {
415                                 dirName = directory ++ dirName;
416                                 "created directory: % \n".postf(dirName);
417                                 dirName.mkdir;
418                         };
419                 }
420         }
422         openWikiPage {
423                 var selectedText, filename, index, directory;
424                 var extensions = #[".rtf", ".sc", ".scd", ".txt", "", ".rtfd", ".html"];
425                 selectedText = this.selectedText;
426                 index = this.selectionStart;
428                 this.selectRange(index, 0);
430                 // refer to local link with round parens
431                 if(selectedText.first == $( /*)*/ and: {/*(*/ selectedText.last == $) }) {
432                                 selectedText = selectedText[1 .. selectedText.size-2];
433                                 directory = Document.current.path.dirname ++ "/";
434                 } {
435                                 directory = wikiDir;
436                 };
438                 case { selectedText[0] == $* }
439                 {
440                         // execute file
441                         selectedText = selectedText.drop(1);
442                         extensions.do { |ext|
443                                 filename = directory ++ selectedText ++ ext;
444                                 if (File.exists(filename)) {
445                                         // open existing wiki page
446                                         filename.load;
447                                         ^this
448                                 }
449                                 {
450                                 filename = "Help/help-scripts/" ++ selectedText ++ ext;
451                                 if (File.exists(filename)) {
452                                         // open help-script document
453                                         filename.load;
454                                         ^this
455                                 }
456                                 }
457                         };
458                 }
459                 { selectedText.first == $[ and: { selectedText.last == $] }}
460                 {
461                         // open help file
462                         selectedText[1 .. selectedText.size-2].help
463                 }
464                 { selectedText.containsStringAt(0, "http://")
465                         or: { selectedText.containsStringAt(0, "file://") } }
466                 {
467                         // open URL
468                         openOS(selectedText)
469                 }
470                 { selectedText.containsStringAt(selectedText.size-1, "/") }
471                 {
472                         Document(selectedText,
473                                 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
474                         )
475                 }
477                 {
478                         if(index + selectedText.size > this.text.size) { ^this };
479                         extensions.do { |ext|
480                                 filename = directory ++ selectedText ++ ext;
481                                 if (File.exists(filename)) {
482                                         // open existing wiki page
483                                         this.class.open(filename);
484                                         ^this
485                                 }
486                         };
487                         // make a new wiki page
488                         this.makeWikiPage(selectedText, nil, directory);
489                 };
490         }
492         mouseUp{ | x, y, modifiers, buttonNumber, clickCount, clickPos |
493                 mouseUpAction.value(this, x, y, modifiers, buttonNumber, clickCount);           if (wikiBrowse and: { this.linkAtClickPos(clickPos).not }
494                         and: { this.selectUnderlinedText(clickPos) } ) {
495                         ^this.openWikiPage
496                 };
497         }
499         keyDown { | character, modifiers, unicode, keycode |
500                 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
501                 keyDownAction.value(this,character, modifiers, unicode, keycode);
502         }
504         keyUp { | character, modifiers, unicode, keycode |
505                 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
506                 keyUpAction.value(this,character, modifiers, unicode, keycode);
507         }
509         == { | doc |
510                 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
511                         this.path == doc.path
512                 }
513         }
515         hash {
516                 ^(this.path ? this).hash
517         }
519         *defaultUsesAutoInOutdent_ {|bool|
520                 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
521         }
523         usesAutoInOutdent_ {|bool|
524                 this.prUsesAutoInOutdent_(bool)
525         }
527         *prDefaultUsesAutoInOutdent_{|bool|
528                 this.subclassResponsibility(thisMethod);
529         }
531         prUsesAutoInOutdent_{|bool|
532                 ^this.subclassResponsibility(thisMethod);
533         }
536 // private implementation
538         prIsEditable_{ | editable=true |
539                 ^this.subclassResponsibility(thisMethod)
540         }
541         prSetTitle { | argName |
542                 ^this.subclassResponsibility(thisMethod)
543         }
544         prGetTitle {
545                 ^this.subclassResponsibility(thisMethod)
546         }
547         prGetFileName {
548                 ^this.subclassResponsibility(thisMethod)
549         }
550         prSetFileName { | apath |
551                 ^this.subclassResponsibility(thisMethod)
552         }
553         prGetBounds { | argBounds |
554                 ^this.subclassResponsibility(thisMethod)
555         }
557         prSetBounds { | argBounds |
558                 ^this.subclassResponsibility(thisMethod)
559         }
561         *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC, specialValsC, specialVarsC, declC, puncC, environC|
562                 ^this.subclassResponsibility(thisMethod);
563         }
565         // if range is -1 apply to whole doc
566         setFont { | font, rangeStart= -1, rangeSize=100 |
567                 ^this.subclassResponsibility(thisMethod)
568         }
570         setTextColor { | color,  rangeStart = -1, rangeSize = 0 |
571                 ^this.subclassResponsibility(thisMethod)
572         }
574         text {
575                 ^this.subclassResponsibility(thisMethod)
576         }
577         selectedText {
578                 ^this.subclassResponsibility(thisMethod)
579         }
580         selectUnderlinedText { | clickPos |
581                 ^this.subclassResponsibility(thisMethod)
582         }
584         linkAtClickPos { | clickPos |
585                 ^this.subclassResponsibility(thisMethod)
586         }
588         rangeText { | rangestart=0, rangesize=1 |
589                 ^this.subclassResponsibility(thisMethod)
590         }
592         prclose {
593                 ^this.subclassResponsibility(thisMethod)
594         }
596         closed {
597                 onClose.value(this); // call user function
598                 this.restoreCurrentEnvironment;
599                 allDocuments.remove(this);
600                 dataptr = nil;
601         }
603         prinsertText { | dataPtr, txt |
604                 ^this.subclassResponsibility(thisMethod)
605         }
606         insertTextRange { | string, rangestart, rangesize |
607                 ^this.subclassResponsibility(thisMethod)
608         }
610         prAdd {
611                 allDocuments = allDocuments.add(this);
612                 this.editable = true;
613                 if (autoRun) {
614                         if (this.rangeText(0,7) == "/*RUN*/")
615                         {
616                                 this.text.interpret;
617                         }
618                 };
619                 current = this;
620                 initAction.value(this);
622         }
624         //this is called after recompiling the lib
625         *prnumberOfOpen {
626                 ^this.subclassResponsibility(thisMethod)
627         }
628         *numberOfOpen {
629                 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
630                         ^this.prnumberOfOpen
631                 } { ^allDocuments.size };
632                 ^0
633         }
635         *newFromIndex { | idx |
636                 ^super.new.initByIndex(idx)
637         }
638         initByIndex { | idx |
639                 //allDocuments = allDocuments.add(this);
640                 var doc;
641                 doc = this.prinitByIndex(idx);
642                 if(doc.isNil,{^nil});
643                 this.prAdd;
644         }
645         prinitByIndex { | idx |
646                 ^this.subclassResponsibility(thisMethod)
647         }
649         //this is called from the menu: open, new
650         *prGetLast {
651                 ^Document.implementationClass.prBasicNew.initLast
652         }
654         initLast {
655                 ^this.subclassResponsibility(thisMethod)
656         }
658         prGetLastIndex {
659                 ^this.subclassResponsibility(thisMethod)
660         }
662         // private open
663         initFromPath { | path, selectionStart, selectionLength |
664                 var stpath;
665         //      path = apath;
666                 stpath = this.class.standardizePath(path);
667                 this.propen(stpath, selectionStart, selectionLength);
668                 if(dataptr.isNil,{
669                         this.class.allDocuments.do{ |d|
670                                         if(d.path == stpath.absolutePath){
671                                                 ^d
672                                         }
673                                 };
674                         ^nil
675                 });
676                 this.background_(Color.white);
677                 ^this.prAdd;
678         }
679         propen { | path, selectionStart=0, selectionLength=0 |
680                 ^this.subclassResponsibility(thisMethod)
681         }
683         // private newTextWindow
684         initByString{ | argTitle, str, makeListener |
686                 this.prinitByString(argTitle, str, makeListener);
687                 this.background_(Color.white);
688                 if(dataptr.isNil,{^nil});
689                 this.prAdd;
690                 this.title = argTitle;
692         }
693         prinitByString { | title, str, makeListener |
694                 ^this.subclassResponsibility(thisMethod)
695         }
697         // other private
698         // if -1 whole doc
700         prSetBackgroundColor { | color |
701                 ^this.subclassResponsibility(thisMethod)
702         }
703         prGetBackgroundColor { | color |
704                 ^this.subclassResponsibility(thisMethod)
705         }
706         prSetSelectedBackgroundColor { | color |
707                 ^this.subclassResponsibility(thisMethod);
708         }
709         prGetSelectedBackgroundColor { | color |
710                 ^this.subclassResponsibility(thisMethod);
711         }
712         selectedRangeLocation {
713                 ^this.subclassResponsibility(thisMethod)
714         }
715         selectedRangeSize {
716                 ^this.subclassResponsibility(thisMethod)
717         }
719         prSelectLine { | line |
720                 ^this.subclassResponsibility(thisMethod)
721         }
723         *prGetIndexOfListener {
724                 if (this.implementationClass.isNil) {
725                         ^nil
726                 };
728                 if (this.implementationClass.respondsTo(\prGetIndexOfListener)) {
729                         ^this.implementationClass.prGetIndexOfListener
730                 } {
731                         ^nil
732                 }
733         }
735         //---not yet implemented
736         // ~/Documents
737         // /Volumes
738         // Music/Patches
740         //*reviewUnsavedDocumentsWithAlertTitle
741         //*saveAllDocuments
742         //*recentDocumentPaths
743         //save
744         //saveAs
745         //print
746         //
747         //hasPath  was loaded
750 // Environment handling  Document with its own envir must set and restore currentEnvironment on entry and exit.
751 // Requires alteration of *open, *new, closed, didBecomeKey, and didResignKey
753         envir_ { | ev |
754                 envir = ev;
755                 if (this.class.current == this) {
756                         if (savedEnvir.isNil) {
757                                 this.saveCurrentEnvironment
758                         }
759                 }
760         }
762         restoreCurrentEnvironment {
763                 if (envir.notNil) { currentEnvironment = savedEnvir };
764         }
766         saveCurrentEnvironment {
767                 if (envir.notNil) {
768                         savedEnvir = currentEnvironment;
769                         currentEnvironment = envir;
770                 }
771         }
773         *prBasicNew {
774                 ^super.new
775         }