julian+alberto classlib merges for 3.4 (10125, 10130, 10133)
[supercollider.git] / common / build / SCClassLibrary / Common / GUI / Document.sc
blob16a1c503aefe96bb1ecd9a9af3bd17b5626f77dd
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                                 )
46                         );
47                 theme = themes.default;
48         }
50         *open { | path, selectionStart=0, selectionLength=0, envir |
51                 var doc, env;
52                 env = currentEnvironment;
53                 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
54                 doc = Document.implementationClass.prBasicNew.initFromPath(path, selectionStart, selectionLength);
55                 if (doc.notNil) {
56                         doc.envir_(envir)
57                 } {
58                         currentEnvironment = env
59                 };
60                 ^doc
61         }
63         *new { | title="Untitled", string="", makeListener=false, envir |
64                 var doc, env;
65                 env = currentEnvironment;
66                 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
67                 doc = Document.implementationClass.new(title, string, makeListener);
68                 if (doc.notNil) {
69                         doc.envir_(envir)
70                 } {
71                         currentEnvironment = env
72                 };
73                 ^doc
74         }
76         *dir_ { | path |
77                 path = path.standardizePath;
78                 if(path == "") { dir = path } {
79                         if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
80                                 dir = path ++ "/"
81                         }
82                 }
83         }
85         *wikiDir_ { | path |
86                 path = path.standardizePath;
87                 if(path == "") {wikiDir = path } {
88                         if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
89                                 wikiDir = path ++ "/"
90                         }
91                 }
92         }
94         *standardizePath { | p |
95                 var pathName;
96                 pathName = PathName(p.standardizePath);
97                 ^if(pathName.isRelativePath) {
98                         dir  ++ pathName.fullPath
99                 } {
100                         pathName.fullPath
101                 }
102         }
103         *abrevPath { | path |
104                 if(path.size < dir.size) { ^path };
105                 if(path.copyRange(0,dir.size - 1) == dir) {
106                         ^path.copyRange(dir.size, path.size - 1)
107                 };
108                 ^path
109         }
111         *openDocuments {
112                 ^allDocuments
113         }
115         *hasEditedDocuments {
116                 allDocuments.do { | doc, i |
117                         if(doc.isEdited) {
118                                 ^true;
119                         }
120                 }
121                 ^false
122         }
124         *closeAll { | leavePostWindowOpen = true |
125                 allDocuments.do { | doc, i |
126                         if(leavePostWindowOpen.not) {
127                                 doc.close;
128                         } {
129                                 if(doc.isListener.not) {
130                                         doc.close;
131                                 }
132                         }
133                 }
134         }
136         *closeAllUnedited { | leavePostWindowOpen = true |
137                 var listenerWindow;
138                 allDocuments.do({ | doc, i |
139                         if(doc.isListener,{
140                                 listenerWindow = doc;
141                         },{
142                                 if(doc.isEdited.not, {
143                                         doc.close;
144                                         });
145                         })
146                 });
147                 if(leavePostWindowOpen.not, {
148                         listenerWindow.close;
149                 })
150         }
152         *current {
153                 if ( thisProcess.platform.hasFeature( \emacs ), {
154                         ^this.implementationClass.current;
155                 });
156                 ^current;
157         }
159         *listener {
160                 ^allDocuments[this.implementationClass.prGetIndexOfListener];
161         }
162         isListener {
163                 ^allDocuments.indexOf(this) == this.class.prGetIndexOfListener
164         }
166 // document setup
168         path {
169                 ^this.prGetFileName
170         }
171         path_ { |apath|
172                 this.prSetFileName(apath);
173         }
174         dir { var path = this.path; ^path !? { path.dirname } }
175         name {
176                 ^this.title
177         }
179         name_ { |aname|
180                 this.title_(aname)
181         }
183         title {
184                 ^this.prGetTitle
185         }
187         title_ { | argName |
188                 this.prSetTitle(argName);
189         }
191         background_ { | color |
192                 this.prSetBackgroundColor(color);
193         }
194         background {
195                 var color;
196                 color = Color.new;
197                 this.prGetBackgroundColor(color);
198                 ^color;
199         }
201         selectedBackground_ { | color |
202                 this.prSetSelectedBackgroundColor(color);
203         }
205         selectedBackground {
206                 var color;
207                 color = Color.new;
208                 this.prGetSelectedBackgroundColor(color);
209                 ^color;
210         }
212         *postColor_ { | col |
213                 postColor = col;
214                 ^Document.implementationClass.postColor_(col);
215         }
217         stringColor_ { | color, rangeStart = -1, rangeSize = 0 |
218                 stringColor = color;
219                 this.setTextColor(color,rangeStart, rangeSize);
220         }
221         bounds {
222                 ^this.prGetBounds(Rect.new);
223         }
224         bounds_ { | argBounds |
225                 ^this.prSetBounds(argBounds);
226         }
228 // interaction:
230         close {
231                 this.prclose
232         }
234         front {
235                 ^this.subclassResponsibility(thisMethod)
236         }
238         unfocusedFront {
239                 ^this.subclassResponsibility(thisMethod)
240         }
242         alwaysOnTop_ { |boolean=true|
243                 ^this.subclassResponsibility(thisMethod)
244         }
246         alwaysOnTop {
247                 ^this.subclassResponsibility(thisMethod)
248         }
250         syntaxColorize {
251                 ^this.subclassResponsibility(thisMethod)
252         }
254         selectLine { | line |
255                 this.prSelectLine(line);
256         }
258         selectRange { | start=0, length=0 |
259                 ^this.subclassResponsibility(thisMethod)
260         }
262         editable_ { | abool=true |
263                 editable = abool;
264                 this.prIsEditable_(abool);
265         }
266         removeUndo {
267                 ^this.subclassResponsibility(thisMethod)
268         }
270         promptToSave_ { | bool |
271                 ^this.subclassResponsibility(thisMethod)
272         }
274         promptToSave {
275                 ^this.subclassResponsibility(thisMethod)
276         }
278         underlineSelection {
279                 ^this.subclassResponsibility(thisMethod)
280         }
282         *setTheme { | themeName |
283                 theme = themes[themeName];
284                 Document.implementationClass.prSetSyntaxColorTheme(
285                         theme.textColor,
286                         theme.classColor,
287                         theme.stringColor,
288                         theme.symbolColor,
289                         theme.commentColor,
290                         theme.numberColor
291                 );
292         }
294 // state info
295         isEdited {
296                 ^this.subclassResponsibility(thisMethod)
297         }
298         isFront {
299                 ^Document.current === this
300         }
302         selectionStart {
303                 ^this.selectedRangeLocation
304         }
306         selectionSize {
307                 ^this.selectedRangeSize
308         }
310         string { | rangestart, rangesize = 1 |
311                 if(rangestart.isNil,{
312                 ^this.text;
313                 });
314                 ^this.rangeText(rangestart, rangesize);
315         }
317         string_ { | string, rangestart = -1, rangesize = 1 |
318                 this.insertTextRange(string, rangestart, rangesize);
319         }
320         selectedString {
321                 ^this.selectedText
322         }
325         font_ { | font, rangestart = -1, rangesize=0 |
326                 this.setFont(font, rangestart, rangesize)
327         }
329         selectedString_ { | txt |
330                 this.prinsertText(txt)
331         }
333         currentLine {
334                 ^this.getSelectedLines(this.selectionStart, 0);
335         }
336         
337         getSelectedLines { | rangestart = -1, rangesize = 0 |
338                 var start, end, str, max;
339                 str = this.string;
340                 max = str.size;
341                 start = rangestart;
342                 end = start + rangesize;
343                 while {
344                         str[start] !== Char.nl and: { start >= 0 }
345                 } { start = start - 1 };
346                 while {
347                         str[end] !== Char.nl and: { end < max }
348                 } { end = end + 1 };
349                 ^str.copyRange(start + 1, end);
350         }
352 //actions:
354         didBecomeKey {
355                 this.class.current = this;
356                 this.saveCurrentEnvironment;
357                 toFrontAction.value(this);
358         }
360         didResignKey {
361                 endFrontAction.value(this);
362                 this.restoreCurrentEnvironment;
363         }
365         makeWikiPage { | wikiWord, extension=(".rtf"), directory |
366                 var filename, file, doc, string, dirName;
367                 directory = directory ? wikiDir;
368                 filename = directory ++ wikiWord ++ extension;
369                 file = File(filename, "w");
370                 if (file.isOpen) {
371                         string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
372                                 "{\\colortbl;\\red255\\green255\\blue255;}\n"
373                                 "Write about " ++ wikiWord ++ " here.\n}";
374                         file.write(string);
375                         file.close;
377                         doc = this.class.open(filename);
378                         doc.path = filename;
379                         doc.selectRange(0,0x7FFFFFFF);
380                         doc.onClose = {
381                                 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
382                                         unixCmd("rm" + filename)
383                                 };
384                         };
385                 } {
386                         // in a second try, check if a path must be created.
387                         // user makes double click on string.
388                         dirName = wikiWord.dirname;
389                         if(dirName != ".") {
390                                 dirName = directory ++ dirName;
391                                 "created directory: % \n".postf(dirName);
392                                 unixCmd("mkdir -p" + dirName);
393                         };
394                 }
395         }
397         openWikiPage {
398                 var selectedText, filename, index, directory;
399                 var extensions = #[".rtf", ".sc", ".scd", ".txt", "", ".rtfd", ".html"];
400                 selectedText = this.selectedText;
401                 index = this.selectionStart;
403                 this.selectRange(index, 0);
405                 // refer to local link with round parens
406                 if(selectedText.first == $( /*)*/ and: {/*(*/ selectedText.last == $) }) {
407                                 selectedText = selectedText[1 .. selectedText.size-2];
408                                 directory = Document.current.path.dirname ++ "/";
409                 } {
410                                 directory = wikiDir;
411                 };
413                 case { selectedText[0] == $* }
414                 {
415                         // execute file
416                         selectedText = selectedText.drop(1);
417                         extensions.do { |ext|
418                                 filename = directory ++ selectedText ++ ext;
419                                 if (File.exists(filename)) {
420                                         // open existing wiki page
421                                         filename.load;
422                                         ^this
423                                 }
424                                 {
425                                 filename = "Help/help-scripts/" ++ selectedText ++ ext;
426                                 if (File.exists(filename)) {
427                                         // open help-script document
428                                         filename.load;
429                                         ^this
430                                 }
431                                 }
432                         };
433                 }
434                 { selectedText.first == $[ and: { selectedText.last == $] }}
435                 {
436                         // open help file
437                         selectedText[1 .. selectedText.size-2].openHelpFile
438                 }
439                 { selectedText.containsStringAt(0, "http://")
440                         or: { selectedText.containsStringAt(0, "file://") } }
441                 {
442                         // open URL
443                         ("open " ++ selectedText).unixCmd;
444                 }
445                 { selectedText.containsStringAt(selectedText.size-1, "/") }
446                 {
447                         Document(selectedText,
448                                 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
449                         )
450                 }
452                 {
453                         if(index + selectedText.size > this.text.size) { ^this };
454                         extensions.do { |ext|
455                                 filename = directory ++ selectedText ++ ext;
456                                 if (File.exists(filename)) {
457                                         // open existing wiki page
458                                         this.class.open(filename);
459                                         ^this
460                                 }
461                         };
462                         // make a new wiki page
463                         this.makeWikiPage(selectedText, nil, directory);
464                 };
465         }
467         mouseUp{ | x, y, modifiers, buttonNumber, clickCount, clickPos |
468                 mouseUpAction.value(this, x, y, modifiers, buttonNumber, clickCount);           if (wikiBrowse and: { this.linkAtClickPos(clickPos).not }
469                         and: { this.selectUnderlinedText(clickPos) } ) {
470                         ^this.openWikiPage
471                 };
472         }
474         keyDown { | character, modifiers, unicode, keycode |
475                 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
476                 keyDownAction.value(this,character, modifiers, unicode, keycode);
477         }
479         keyUp { | character, modifiers, unicode, keycode |
480                 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
481                 keyUpAction.value(this,character, modifiers, unicode, keycode);
482         }
484         == { | doc |
485                 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
486                         this.path == doc.path
487                 }
488         }
489         
490         *defaultUsesAutoInOutdent_ {|bool|
491                 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
492         }
493         
494         usesAutoInOutdent_ {|bool|
495                 this.prUsesAutoInOutdent_(bool)
496         }
497         
498         *prDefaultUsesAutoInOutdent_{|bool|
499                 this.subclassResponsibility(thisMethod);
500         }
501         
502         prUsesAutoInOutdent_{|bool|
503                 ^this.subclassResponsibility(thisMethod);
504         }
507 // private implementation
509         prIsEditable_{ | editable=true |
510                 ^this.subclassResponsibility(thisMethod)
511         }
512         prSetTitle { | argName |
513                 ^this.subclassResponsibility(thisMethod)
514         }
515         prGetTitle {
516                 ^this.subclassResponsibility(thisMethod)
517         }
518         prGetFileName {
519                 ^this.subclassResponsibility(thisMethod)
520         }
521         prSetFileName { | apath |
522                 ^this.subclassResponsibility(thisMethod)
523         }
524         prGetBounds { | argBounds |
525                 ^this.subclassResponsibility(thisMethod)
526         }
528         prSetBounds { | argBounds |
529                 ^this.subclassResponsibility(thisMethod)
530         }
532         *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC|
533                 ^this.subclassResponsibility(thisMethod);
534         }
536         // if range is -1 apply to whole doc
537         setFont { | font, rangeStart= -1, rangeSize=100 |
538                 ^this.subclassResponsibility(thisMethod)
539         }
541         setTextColor { | color,  rangeStart = -1, rangeSize = 0 |
542                 ^this.subclassResponsibility(thisMethod)
543         }
545         text {
546                 ^this.subclassResponsibility(thisMethod)
547         }
548         selectedText {
549                 ^this.subclassResponsibility(thisMethod)
550         }
551         selectUnderlinedText { | clickPos |
552                 ^this.subclassResponsibility(thisMethod)
553         }
555         linkAtClickPos { | clickPos |
556                 ^this.subclassResponsibility(thisMethod)
557         }
559         rangeText { | rangestart=0, rangesize=1 |
560                 ^this.subclassResponsibility(thisMethod)
561         }
563         prclose {
564                 ^this.subclassResponsibility(thisMethod)
565         }
567         closed {
568                 onClose.value(this); // call user function
569                 this.restoreCurrentEnvironment;
570                 allDocuments.remove(this);
571                 dataptr = nil;
572         }
574         prinsertText { | dataPtr, txt |
575                 ^this.subclassResponsibility(thisMethod)
576         }
577         insertTextRange { | string, rangestart, rangesize |
578                 ^this.subclassResponsibility(thisMethod)
579         }
581         prAdd {
582                 allDocuments = allDocuments.add(this);
583                 this.editable = true;
584                 if (autoRun) {
585                         if (this.rangeText(0,7) == "/*RUN*/")
586                         {
587                                 this.text.interpret;
588                         }
589                 };
590                 current = this;
591                 initAction.value(this);
593         }
595         //this is called after recompiling the lib
596         *prnumberOfOpen {
597                 ^this.subclassResponsibility(thisMethod)
598         }
599         *numberOfOpen {
600                 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
601                         ^this.prnumberOfOpen
602                 } { ^allDocuments.size };
603                 ^0
604         }
606         *newFromIndex { | idx |
607                 ^super.new.initByIndex(idx)
608         }
609         initByIndex { | idx |
610                 //allDocuments = allDocuments.add(this);
611                 var doc;
612                 doc = this.prinitByIndex(idx);
613                 if(doc.isNil,{^nil});
614                 this.prAdd;
615         }
616         prinitByIndex { | idx |
617                 ^this.subclassResponsibility(thisMethod)
618         }
620         //this is called from the menu: open, new
621         *prGetLast {
622                 ^Document.implementationClass.prBasicNew.initLast
623         }
625         initLast {
626                 ^this.subclassResponsibility(thisMethod)
627         }
629         prGetLastIndex {
630                 ^this.subclassResponsibility(thisMethod)
631         }
633         // private open
634         initFromPath { | path, selectionStart, selectionLength |
635                 var stpath;
636         //      path = apath;
637                 stpath = this.class.standardizePath(path);
638                 this.propen(stpath, selectionStart, selectionLength);
639                 if(dataptr.isNil,{
640                         this.class.allDocuments.do{ |d|
641                                         if(d.path == stpath.absolutePath){
642                                                 ^d
643                                         }
644                                 };
645                         ^nil
646                 });
647                 this.background_(Color.white);
648                 ^this.prAdd;
649         }
650         propen { | path, selectionStart=0, selectionLength=0 |
651                 ^this.subclassResponsibility(thisMethod)
652         }
654         // private newTextWindow
655         initByString{ | argTitle, str, makeListener |
657                 this.prinitByString(argTitle, str, makeListener);
658                 this.background_(Color.white);
659                 if(dataptr.isNil,{^nil});
660                 this.prAdd;
661                 this.title = argTitle;
663         }
664         prinitByString { | title, str, makeListener |
665                 ^this.subclassResponsibility(thisMethod)
666         }
668         // other private
669         // if -1 whole doc
671         prSetBackgroundColor { | color |
672                 ^this.subclassResponsibility(thisMethod)
673         }
674         prGetBackgroundColor { | color |
675                 ^this.subclassResponsibility(thisMethod)
676         }
677         prSetSelectedBackgroundColor { | color |
678                 ^this.subclassResponsibility(thisMethod);
679         }
680         prGetSelectedBackgroundColor { | color |
681                 ^this.subclassResponsibility(thisMethod);
682         }
683         selectedRangeLocation {
684                 ^this.subclassResponsibility(thisMethod)
685         }
686         selectedRangeSize {
687                 ^this.subclassResponsibility(thisMethod)
688         }
690         prSelectLine { | line |
691                 ^this.subclassResponsibility(thisMethod)
692         }
694         *prGetIndexOfListener {
695                 ^this.subclassResponsibility(thisMethod)
696         }
699         //---not yet implemented
700         // ~/Documents
701         // /Volumes
702         // Music/Patches
704         //*reviewUnsavedDocumentsWithAlertTitle
705         //*saveAllDocuments
706         //*recentDocumentPaths
707         //save
708         //saveAs
709         //print
710         //
711         //hasPath  was loaded
714 // Environment handling  Document with its own envir must set and restore currentEnvironment on entry and exit.
715 // Requires alteration of *open, *new, closed, didBecomeKey, and didResignKey
717         envir_ { | ev |
718                 envir = ev;
719                 if (this.class.current == this) {
720                         if (savedEnvir.isNil) {
721                                 this.saveCurrentEnvironment
722                         }
723                 }
724         }
726         restoreCurrentEnvironment {
727                 if (envir.notNil) { currentEnvironment = savedEnvir };
728         }
730         saveCurrentEnvironment {
731                 if (envir.notNil) {
732                         savedEnvir = currentEnvironment;
733                         currentEnvironment = envir;
734                 }
735         }
737         *prBasicNew {
738                 ^super.new
739         }