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.
7 classvar <dir="", <wikiDir="", <allDocuments, >current;
8 classvar <>globalKeyDownAction, <> globalKeyUpAction, <>initAction;
10 classvar <>autoRun = true;
11 classvar <>wikiBrowse = true;
13 classvar <>implementationClass;
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;
23 var <envir, savedEnvir;
32 num = this.numberOfOpen;
34 doc = this.newFromIndex(i);
36 postColor = Color.black;
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)
52 theme = themes.default;
55 *open { | path, selectionStart=0, selectionLength=0, envir |
57 env = currentEnvironment;
58 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
59 doc = Document.implementationClass.prBasicNew.initFromPath(path, selectionStart, selectionLength);
63 currentEnvironment = env
68 *new { | title="Untitled", string="", makeListener=false, envir |
70 env = currentEnvironment;
71 if(this.current.notNil) { this.current.restoreCurrentEnvironment };
72 doc = Document.implementationClass.new(title, string, makeListener);
76 currentEnvironment = env
82 path = path.standardizePath;
83 if(path == "") { dir = path } {
84 if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
91 path = path.standardizePath;
92 if(path == "") {wikiDir = path } {
93 if(pathMatch(path).isEmpty) { ("there is no such path:" + path).postln } {
99 *standardizePath { | p |
101 pathName = PathName(p.standardizePath);
102 ^if(pathName.isRelativePath) {
103 dir ++ pathName.fullPath
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)
120 *hasEditedDocuments {
121 allDocuments.do { | doc, i |
129 *closeAll { | leavePostWindowOpen = true |
130 allDocuments.do { | doc, i |
131 if(leavePostWindowOpen.not) {
134 if(doc.isListener.not) {
141 *closeAllUnedited { | leavePostWindowOpen = true |
143 allDocuments.do({ | doc, i |
145 listenerWindow = doc;
147 if(doc.isEdited.not, {
152 if(leavePostWindowOpen.not, {
153 listenerWindow.close;
158 if ( thisProcess.platform.hasFeature( \emacs ), {
159 ^this.implementationClass.current;
165 ^allDocuments[this.implementationClass.prGetIndexOfListener];
168 ^allDocuments.indexOf(this) == this.class.prGetIndexOfListener
177 this.prSetFileName(apath);
179 dir { var path = this.path; ^path !? { path.dirname } }
193 this.prSetTitle(argName);
196 background_ { | color |
197 this.prSetBackgroundColor(color);
202 this.prGetBackgroundColor(color);
206 selectedBackground_ { | color |
207 this.prSetSelectedBackgroundColor(color);
213 this.prGetSelectedBackgroundColor(color);
217 *postColor_ { | col |
219 ^Document.implementationClass.postColor_(col);
222 stringColor_ { | color, rangeStart = -1, rangeSize = 0 |
224 this.setTextColor(color,rangeStart, rangeSize);
227 ^this.prGetBounds(Rect.new);
229 bounds_ { | argBounds |
230 ^this.prSetBounds(argBounds);
240 ^this.subclassResponsibility(thisMethod)
244 ^this.subclassResponsibility(thisMethod)
247 alwaysOnTop_ { |boolean=true|
248 ^this.subclassResponsibility(thisMethod)
252 ^this.subclassResponsibility(thisMethod)
256 ^this.subclassResponsibility(thisMethod)
259 selectLine { | line |
260 this.prSelectLine(line);
263 selectRange { | start=0, length=0 |
264 ^this.subclassResponsibility(thisMethod)
267 editable_ { | abool=true |
269 this.prIsEditable_(abool);
272 ^this.subclassResponsibility(thisMethod)
275 promptToSave_ { | bool |
276 ^this.subclassResponsibility(thisMethod)
280 ^this.subclassResponsibility(thisMethod)
284 ^this.subclassResponsibility(thisMethod)
287 *setTheme { | themeName |
288 theme = themes[themeName];
289 if(theme.proto.isNil) {
290 theme = theme.copy.parent_(themes[\default]);
292 thisProcess.platform.writeClientCSS;
293 Document.implementationClass.prSetSyntaxColorTheme(
300 theme.specialValsColor,
301 theme.specialVarsColor,
310 ^this.subclassResponsibility(thisMethod)
313 ^Document.current === this
317 ^this.selectedRangeLocation
321 ^this.selectedRangeSize
324 string { | rangestart, rangesize = 1 |
325 if(rangestart.isNil,{
328 ^this.rangeText(rangestart, rangesize);
331 string_ { | string, rangestart = -1, rangesize = 1 |
332 this.insertTextRange(string, rangestart, rangesize);
339 font_ { | font, rangestart = -1, rangesize=0 |
340 this.setFont(font, rangestart, rangesize)
343 selectedString_ { | txt |
344 this.prinsertText(txt)
348 ^this.getSelectedLines(this.selectionStart, 0);
351 getSelectedLines { | rangestart = -1, rangesize = 0 |
352 var start, end, str, max;
356 end = start + rangesize;
358 str[start] !== Char.nl and: { start >= 0 }
359 } { start = start - 1 };
361 str[end] !== Char.nl and: { end < max }
363 ^str.copyRange(start + 1, end);
369 this.class.current = this;
370 this.saveCurrentEnvironment;
371 toFrontAction.value(this);
375 endFrontAction.value(this);
376 this.restoreCurrentEnvironment;
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");
385 string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
386 "{\\colortbl;\\red255\\green255\\blue255;}\n"
387 "Write about " ++ wikiWord ++ " here.\n}";
391 doc = this.class.open(filename);
393 doc.selectRange(0,0x7FFFFFFF);
395 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
396 unixCmd("rm" + filename)
400 // in a second try, check if a path must be created.
401 // user makes double click on string.
402 dirName = wikiWord.dirname;
404 dirName = directory ++ dirName;
405 "created directory: % \n".postf(dirName);
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 ++ "/";
427 case { selectedText[0] == $* }
430 selectedText = selectedText.drop(1);
431 extensions.do { |ext|
432 filename = directory ++ selectedText ++ ext;
433 if (File.exists(filename)) {
434 // open existing wiki page
439 filename = "Help/help-scripts/" ++ selectedText ++ ext;
440 if (File.exists(filename)) {
441 // open help-script document
448 { selectedText.first == $[ and: { selectedText.last == $] }}
451 selectedText[1 .. selectedText.size-2].help
453 { selectedText.containsStringAt(0, "http://")
454 or: { selectedText.containsStringAt(0, "file://") } }
459 { selectedText.containsStringAt(selectedText.size-1, "/") }
461 Document(selectedText,
462 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
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);
476 // make a new wiki page
477 this.makeWikiPage(selectedText, nil, directory);
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) } ) {
488 keyDown { | character, modifiers, unicode, keycode |
489 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
490 keyDownAction.value(this,character, modifiers, unicode, keycode);
493 keyUp { | character, modifiers, unicode, keycode |
494 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
495 keyUpAction.value(this,character, modifiers, unicode, keycode);
499 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
500 this.path == doc.path
505 ^(this.path ? this).hash
508 *defaultUsesAutoInOutdent_ {|bool|
509 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
512 usesAutoInOutdent_ {|bool|
513 this.prUsesAutoInOutdent_(bool)
516 *prDefaultUsesAutoInOutdent_{|bool|
517 this.subclassResponsibility(thisMethod);
520 prUsesAutoInOutdent_{|bool|
521 ^this.subclassResponsibility(thisMethod);
525 // private implementation
527 prIsEditable_{ | editable=true |
528 ^this.subclassResponsibility(thisMethod)
530 prSetTitle { | argName |
531 ^this.subclassResponsibility(thisMethod)
534 ^this.subclassResponsibility(thisMethod)
537 ^this.subclassResponsibility(thisMethod)
539 prSetFileName { | apath |
540 ^this.subclassResponsibility(thisMethod)
542 prGetBounds { | argBounds |
543 ^this.subclassResponsibility(thisMethod)
546 prSetBounds { | argBounds |
547 ^this.subclassResponsibility(thisMethod)
550 *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC, specialValsC, specialVarsC, declC, puncC, environC|
551 ^this.subclassResponsibility(thisMethod);
554 // if range is -1 apply to whole doc
555 setFont { | font, rangeStart= -1, rangeSize=100 |
556 ^this.subclassResponsibility(thisMethod)
559 setTextColor { | color, rangeStart = -1, rangeSize = 0 |
560 ^this.subclassResponsibility(thisMethod)
564 ^this.subclassResponsibility(thisMethod)
567 ^this.subclassResponsibility(thisMethod)
569 selectUnderlinedText { | clickPos |
570 ^this.subclassResponsibility(thisMethod)
573 linkAtClickPos { | clickPos |
574 ^this.subclassResponsibility(thisMethod)
577 rangeText { | rangestart=0, rangesize=1 |
578 ^this.subclassResponsibility(thisMethod)
582 ^this.subclassResponsibility(thisMethod)
586 onClose.value(this); // call user function
587 this.restoreCurrentEnvironment;
588 allDocuments.remove(this);
592 prinsertText { | dataPtr, txt |
593 ^this.subclassResponsibility(thisMethod)
595 insertTextRange { | string, rangestart, rangesize |
596 ^this.subclassResponsibility(thisMethod)
600 allDocuments = allDocuments.add(this);
601 this.editable = true;
603 if (this.rangeText(0,7) == "/*RUN*/")
609 initAction.value(this);
613 //this is called after recompiling the lib
615 ^this.subclassResponsibility(thisMethod)
618 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
620 } { ^allDocuments.size };
624 *newFromIndex { | idx |
625 ^super.new.initByIndex(idx)
627 initByIndex { | idx |
628 //allDocuments = allDocuments.add(this);
630 doc = this.prinitByIndex(idx);
631 if(doc.isNil,{^nil});
634 prinitByIndex { | idx |
635 ^this.subclassResponsibility(thisMethod)
638 //this is called from the menu: open, new
640 ^Document.implementationClass.prBasicNew.initLast
644 ^this.subclassResponsibility(thisMethod)
648 ^this.subclassResponsibility(thisMethod)
652 initFromPath { | path, selectionStart, selectionLength |
655 stpath = this.class.standardizePath(path);
656 this.propen(stpath, selectionStart, selectionLength);
658 this.class.allDocuments.do{ |d|
659 if(d.path == stpath.absolutePath){
665 this.background_(Color.white);
668 propen { | path, selectionStart=0, selectionLength=0 |
669 ^this.subclassResponsibility(thisMethod)
672 // private newTextWindow
673 initByString{ | argTitle, str, makeListener |
675 this.prinitByString(argTitle, str, makeListener);
676 this.background_(Color.white);
677 if(dataptr.isNil,{^nil});
679 this.title = argTitle;
682 prinitByString { | title, str, makeListener |
683 ^this.subclassResponsibility(thisMethod)
689 prSetBackgroundColor { | color |
690 ^this.subclassResponsibility(thisMethod)
692 prGetBackgroundColor { | color |
693 ^this.subclassResponsibility(thisMethod)
695 prSetSelectedBackgroundColor { | color |
696 ^this.subclassResponsibility(thisMethod);
698 prGetSelectedBackgroundColor { | color |
699 ^this.subclassResponsibility(thisMethod);
701 selectedRangeLocation {
702 ^this.subclassResponsibility(thisMethod)
705 ^this.subclassResponsibility(thisMethod)
708 prSelectLine { | line |
709 ^this.subclassResponsibility(thisMethod)
712 *prGetIndexOfListener {
713 ^this.subclassResponsibility(thisMethod)
717 //---not yet implemented
722 //*reviewUnsavedDocumentsWithAlertTitle
724 //*recentDocumentPaths
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
737 if (this.class.current == this) {
738 if (savedEnvir.isNil) {
739 this.saveCurrentEnvironment
744 restoreCurrentEnvironment {
745 if (envir.notNil) { currentEnvironment = savedEnvir };
748 saveCurrentEnvironment {
750 savedEnvir = currentEnvironment;
751 currentEnvironment = envir;