deprecate SCViewHolder-layRight
[supercollider.git] / SCClassLibrary / Common / GUI / Document.sc
blobbc532eb88e1f58a15338ed82e34c9ebc63b419fc
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                 thisProcess.platform.writeClientCSS;
290                 Document.implementationClass.prSetSyntaxColorTheme(
291                         theme.textColor,
292                         theme.classColor,
293                         theme.stringColor,
294                         theme.symbolColor,
295                         theme.commentColor,
296                         theme.numberColor,
297                         theme.specialValsColor,
298                         theme.specialVarsColor,
299                         theme.declColor,
300                         theme.puncColor,
301                         theme.environColor
302                 );
303         }
305 // state info
306         isEdited {
307                 ^this.subclassResponsibility(thisMethod)
308         }
309         isFront {
310                 ^Document.current === this
311         }
313         selectionStart {
314                 ^this.selectedRangeLocation
315         }
317         selectionSize {
318                 ^this.selectedRangeSize
319         }
321         string { | rangestart, rangesize = 1 |
322                 if(rangestart.isNil,{
323                 ^this.text;
324                 });
325                 ^this.rangeText(rangestart, rangesize);
326         }
328         string_ { | string, rangestart = -1, rangesize = 1 |
329                 this.insertTextRange(string, rangestart, rangesize);
330         }
331         selectedString {
332                 ^this.selectedText
333         }
336         font_ { | font, rangestart = -1, rangesize=0 |
337                 this.setFont(font, rangestart, rangesize)
338         }
340         selectedString_ { | txt |
341                 this.prinsertText(txt)
342         }
344         currentLine {
345                 ^this.getSelectedLines(this.selectionStart, 0);
346         }
348         getSelectedLines { | rangestart = -1, rangesize = 0 |
349                 var start, end, str, max;
350                 str = this.string;
351                 max = str.size;
352                 start = rangestart;
353                 end = start + rangesize;
354                 while {
355                         str[start] !== Char.nl and: { start >= 0 }
356                 } { start = start - 1 };
357                 while {
358                         str[end] !== Char.nl and: { end < max }
359                 } { end = end + 1 };
360                 ^str.copyRange(start + 1, end);
361         }
363 //actions:
365         didBecomeKey {
366                 this.class.current = this;
367                 this.saveCurrentEnvironment;
368                 toFrontAction.value(this);
369         }
371         didResignKey {
372                 endFrontAction.value(this);
373                 this.restoreCurrentEnvironment;
374         }
376         makeWikiPage { | wikiWord, extension=(".rtf"), directory |
377                 var filename, file, doc, string, dirName;
378                 directory = directory ? wikiDir;
379                 filename = directory ++ wikiWord ++ extension;
380                 file = File(filename, "w");
381                 if (file.isOpen) {
382                         string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
383                                 "{\\colortbl;\\red255\\green255\\blue255;}\n"
384                                 "Write about " ++ wikiWord ++ " here.\n}";
385                         file.write(string);
386                         file.close;
388                         doc = this.class.open(filename);
389                         doc.path = filename;
390                         doc.selectRange(0,0x7FFFFFFF);
391                         doc.onClose = {
392                                 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
393                                         unixCmd("rm" + filename)
394                                 };
395                         };
396                 } {
397                         // in a second try, check if a path must be created.
398                         // user makes double click on string.
399                         dirName = wikiWord.dirname;
400                         if(dirName != ".") {
401                                 dirName = directory ++ dirName;
402                                 "created directory: % \n".postf(dirName);
403                                 dirName.mkdir;
404                         };
405                 }
406         }
408         openWikiPage {
409                 var selectedText, filename, index, directory;
410                 var extensions = #[".rtf", ".sc", ".scd", ".txt", "", ".rtfd", ".html"];
411                 selectedText = this.selectedText;
412                 index = this.selectionStart;
414                 this.selectRange(index, 0);
416                 // refer to local link with round parens
417                 if(selectedText.first == $( /*)*/ and: {/*(*/ selectedText.last == $) }) {
418                                 selectedText = selectedText[1 .. selectedText.size-2];
419                                 directory = Document.current.path.dirname ++ "/";
420                 } {
421                                 directory = wikiDir;
422                 };
424                 case { selectedText[0] == $* }
425                 {
426                         // execute file
427                         selectedText = selectedText.drop(1);
428                         extensions.do { |ext|
429                                 filename = directory ++ selectedText ++ ext;
430                                 if (File.exists(filename)) {
431                                         // open existing wiki page
432                                         filename.load;
433                                         ^this
434                                 }
435                                 {
436                                 filename = "Help/help-scripts/" ++ selectedText ++ ext;
437                                 if (File.exists(filename)) {
438                                         // open help-script document
439                                         filename.load;
440                                         ^this
441                                 }
442                                 }
443                         };
444                 }
445                 { selectedText.first == $[ and: { selectedText.last == $] }}
446                 {
447                         // open help file
448                         selectedText[1 .. selectedText.size-2].openHelpFile
449                 }
450                 { selectedText.containsStringAt(0, "http://")
451                         or: { selectedText.containsStringAt(0, "file://") } }
452                 {
453                         // open URL
454                         openOS(selectedText)
455                 }
456                 { selectedText.containsStringAt(selectedText.size-1, "/") }
457                 {
458                         Document(selectedText,
459                                 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
460                         )
461                 }
463                 {
464                         if(index + selectedText.size > this.text.size) { ^this };
465                         extensions.do { |ext|
466                                 filename = directory ++ selectedText ++ ext;
467                                 if (File.exists(filename)) {
468                                         // open existing wiki page
469                                         this.class.open(filename);
470                                         ^this
471                                 }
472                         };
473                         // make a new wiki page
474                         this.makeWikiPage(selectedText, nil, directory);
475                 };
476         }
478         mouseUp{ | x, y, modifiers, buttonNumber, clickCount, clickPos |
479                 mouseUpAction.value(this, x, y, modifiers, buttonNumber, clickCount);           if (wikiBrowse and: { this.linkAtClickPos(clickPos).not }
480                         and: { this.selectUnderlinedText(clickPos) } ) {
481                         ^this.openWikiPage
482                 };
483         }
485         keyDown { | character, modifiers, unicode, keycode |
486                 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
487                 keyDownAction.value(this,character, modifiers, unicode, keycode);
488         }
490         keyUp { | character, modifiers, unicode, keycode |
491                 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
492                 keyUpAction.value(this,character, modifiers, unicode, keycode);
493         }
495         == { | doc |
496                 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
497                         this.path == doc.path
498                 }
499         }
501         hash {
502                 ^(this.path ? this).hash
503         }
505         *defaultUsesAutoInOutdent_ {|bool|
506                 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
507         }
509         usesAutoInOutdent_ {|bool|
510                 this.prUsesAutoInOutdent_(bool)
511         }
513         *prDefaultUsesAutoInOutdent_{|bool|
514                 this.subclassResponsibility(thisMethod);
515         }
517         prUsesAutoInOutdent_{|bool|
518                 ^this.subclassResponsibility(thisMethod);
519         }
522 // private implementation
524         prIsEditable_{ | editable=true |
525                 ^this.subclassResponsibility(thisMethod)
526         }
527         prSetTitle { | argName |
528                 ^this.subclassResponsibility(thisMethod)
529         }
530         prGetTitle {
531                 ^this.subclassResponsibility(thisMethod)
532         }
533         prGetFileName {
534                 ^this.subclassResponsibility(thisMethod)
535         }
536         prSetFileName { | apath |
537                 ^this.subclassResponsibility(thisMethod)
538         }
539         prGetBounds { | argBounds |
540                 ^this.subclassResponsibility(thisMethod)
541         }
543         prSetBounds { | argBounds |
544                 ^this.subclassResponsibility(thisMethod)
545         }
547         *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC, specialValsC, specialVarsC, declC, puncC, environC|
548                 ^this.subclassResponsibility(thisMethod);
549         }
551         // if range is -1 apply to whole doc
552         setFont { | font, rangeStart= -1, rangeSize=100 |
553                 ^this.subclassResponsibility(thisMethod)
554         }
556         setTextColor { | color,  rangeStart = -1, rangeSize = 0 |
557                 ^this.subclassResponsibility(thisMethod)
558         }
560         text {
561                 ^this.subclassResponsibility(thisMethod)
562         }
563         selectedText {
564                 ^this.subclassResponsibility(thisMethod)
565         }
566         selectUnderlinedText { | clickPos |
567                 ^this.subclassResponsibility(thisMethod)
568         }
570         linkAtClickPos { | clickPos |
571                 ^this.subclassResponsibility(thisMethod)
572         }
574         rangeText { | rangestart=0, rangesize=1 |
575                 ^this.subclassResponsibility(thisMethod)
576         }
578         prclose {
579                 ^this.subclassResponsibility(thisMethod)
580         }
582         closed {
583                 onClose.value(this); // call user function
584                 this.restoreCurrentEnvironment;
585                 allDocuments.remove(this);
586                 dataptr = nil;
587         }
589         prinsertText { | dataPtr, txt |
590                 ^this.subclassResponsibility(thisMethod)
591         }
592         insertTextRange { | string, rangestart, rangesize |
593                 ^this.subclassResponsibility(thisMethod)
594         }
596         prAdd {
597                 allDocuments = allDocuments.add(this);
598                 this.editable = true;
599                 if (autoRun) {
600                         if (this.rangeText(0,7) == "/*RUN*/")
601                         {
602                                 this.text.interpret;
603                         }
604                 };
605                 current = this;
606                 initAction.value(this);
608         }
610         //this is called after recompiling the lib
611         *prnumberOfOpen {
612                 ^this.subclassResponsibility(thisMethod)
613         }
614         *numberOfOpen {
615                 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
616                         ^this.prnumberOfOpen
617                 } { ^allDocuments.size };
618                 ^0
619         }
621         *newFromIndex { | idx |
622                 ^super.new.initByIndex(idx)
623         }
624         initByIndex { | idx |
625                 //allDocuments = allDocuments.add(this);
626                 var doc;
627                 doc = this.prinitByIndex(idx);
628                 if(doc.isNil,{^nil});
629                 this.prAdd;
630         }
631         prinitByIndex { | idx |
632                 ^this.subclassResponsibility(thisMethod)
633         }
635         //this is called from the menu: open, new
636         *prGetLast {
637                 ^Document.implementationClass.prBasicNew.initLast
638         }
640         initLast {
641                 ^this.subclassResponsibility(thisMethod)
642         }
644         prGetLastIndex {
645                 ^this.subclassResponsibility(thisMethod)
646         }
648         // private open
649         initFromPath { | path, selectionStart, selectionLength |
650                 var stpath;
651         //      path = apath;
652                 stpath = this.class.standardizePath(path);
653                 this.propen(stpath, selectionStart, selectionLength);
654                 if(dataptr.isNil,{
655                         this.class.allDocuments.do{ |d|
656                                         if(d.path == stpath.absolutePath){
657                                                 ^d
658                                         }
659                                 };
660                         ^nil
661                 });
662                 this.background_(Color.white);
663                 ^this.prAdd;
664         }
665         propen { | path, selectionStart=0, selectionLength=0 |
666                 ^this.subclassResponsibility(thisMethod)
667         }
669         // private newTextWindow
670         initByString{ | argTitle, str, makeListener |
672                 this.prinitByString(argTitle, str, makeListener);
673                 this.background_(Color.white);
674                 if(dataptr.isNil,{^nil});
675                 this.prAdd;
676                 this.title = argTitle;
678         }
679         prinitByString { | title, str, makeListener |
680                 ^this.subclassResponsibility(thisMethod)
681         }
683         // other private
684         // if -1 whole doc
686         prSetBackgroundColor { | color |
687                 ^this.subclassResponsibility(thisMethod)
688         }
689         prGetBackgroundColor { | color |
690                 ^this.subclassResponsibility(thisMethod)
691         }
692         prSetSelectedBackgroundColor { | color |
693                 ^this.subclassResponsibility(thisMethod);
694         }
695         prGetSelectedBackgroundColor { | color |
696                 ^this.subclassResponsibility(thisMethod);
697         }
698         selectedRangeLocation {
699                 ^this.subclassResponsibility(thisMethod)
700         }
701         selectedRangeSize {
702                 ^this.subclassResponsibility(thisMethod)
703         }
705         prSelectLine { | line |
706                 ^this.subclassResponsibility(thisMethod)
707         }
709         *prGetIndexOfListener {
710                 ^this.subclassResponsibility(thisMethod)
711         }
714         //---not yet implemented
715         // ~/Documents
716         // /Volumes
717         // Music/Patches
719         //*reviewUnsavedDocumentsWithAlertTitle
720         //*saveAllDocuments
721         //*recentDocumentPaths
722         //save
723         //saveAs
724         //print
725         //
726         //hasPath  was loaded
729 // Environment handling  Document with its own envir must set and restore currentEnvironment on entry and exit.
730 // Requires alteration of *open, *new, closed, didBecomeKey, and didResignKey
732         envir_ { | ev |
733                 envir = ev;
734                 if (this.class.current == this) {
735                         if (savedEnvir.isNil) {
736                                 this.saveCurrentEnvironment
737                         }
738                 }
739         }
741         restoreCurrentEnvironment {
742                 if (envir.notNil) { currentEnvironment = savedEnvir };
743         }
745         saveCurrentEnvironment {
746                 if (envir.notNil) {
747                         savedEnvir = currentEnvironment;
748                         currentEnvironment = envir;
749                 }
750         }
752         *prBasicNew {
753                 ^super.new
754         }