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 var index = this.prGetIndexOfListener;
167 ^allDocuments[index];
174 var index = this.class.prGetIndexOfListener;
176 ^allDocuments.indexOf(this) == index
188 this.prSetFileName(apath);
190 dir { var path = this.path; ^path !? { path.dirname } }
204 this.prSetTitle(argName);
207 background_ { | color |
208 this.prSetBackgroundColor(color);
213 this.prGetBackgroundColor(color);
217 selectedBackground_ { | color |
218 this.prSetSelectedBackgroundColor(color);
224 this.prGetSelectedBackgroundColor(color);
228 *postColor_ { | col |
230 ^Document.implementationClass.postColor_(col);
233 stringColor_ { | color, rangeStart = -1, rangeSize = 0 |
235 this.setTextColor(color,rangeStart, rangeSize);
238 ^this.prGetBounds(Rect.new);
240 bounds_ { | argBounds |
241 ^this.prSetBounds(argBounds);
251 ^this.subclassResponsibility(thisMethod)
255 ^this.subclassResponsibility(thisMethod)
258 alwaysOnTop_ { |boolean=true|
259 ^this.subclassResponsibility(thisMethod)
263 ^this.subclassResponsibility(thisMethod)
267 ^this.subclassResponsibility(thisMethod)
270 selectLine { | line |
271 this.prSelectLine(line);
274 selectRange { | start=0, length=0 |
275 ^this.subclassResponsibility(thisMethod)
278 editable_ { | abool=true |
280 this.prIsEditable_(abool);
283 ^this.subclassResponsibility(thisMethod)
286 promptToSave_ { | bool |
287 ^this.subclassResponsibility(thisMethod)
291 ^this.subclassResponsibility(thisMethod)
295 ^this.subclassResponsibility(thisMethod)
298 *setTheme { | themeName |
299 theme = themes[themeName];
300 if(theme.proto.isNil) {
301 theme = theme.copy.parent_(themes[\default]);
303 thisProcess.platform.writeClientCSS;
304 Document.implementationClass.prSetSyntaxColorTheme(
311 theme.specialValsColor,
312 theme.specialVarsColor,
321 ^this.subclassResponsibility(thisMethod)
324 ^Document.current === this
328 ^this.selectedRangeLocation
332 ^this.selectedRangeSize
335 string { | rangestart, rangesize = 1 |
336 if(rangestart.isNil,{
339 ^this.rangeText(rangestart, rangesize);
342 string_ { | string, rangestart = -1, rangesize = 1 |
343 this.insertTextRange(string, rangestart, rangesize);
350 font_ { | font, rangestart = -1, rangesize=0 |
351 this.setFont(font, rangestart, rangesize)
354 selectedString_ { | txt |
355 this.prinsertText(txt)
359 ^this.getSelectedLines(this.selectionStart, 0);
362 getSelectedLines { | rangestart = -1, rangesize = 0 |
363 var start, end, str, max;
367 end = start + rangesize;
369 str[start] !== Char.nl and: { start >= 0 }
370 } { start = start - 1 };
372 str[end] !== Char.nl and: { end < max }
374 ^str.copyRange(start + 1, end);
380 this.class.current = this;
381 this.saveCurrentEnvironment;
382 toFrontAction.value(this);
386 endFrontAction.value(this);
387 this.restoreCurrentEnvironment;
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");
396 string = "{\\rtf1\\mac\\ansicpg10000\\cocoartf102\\n{\\fonttbl}\n"
397 "{\\colortbl;\\red255\\green255\\blue255;}\n"
398 "Write about " ++ wikiWord ++ " here.\n}";
402 doc = this.class.open(filename);
404 doc.selectRange(0,0x7FFFFFFF);
406 if(doc.string == ("Write about " ++ wikiWord ++ " here.")) {
407 unixCmd("rm" + filename)
411 // in a second try, check if a path must be created.
412 // user makes double click on string.
413 dirName = wikiWord.dirname;
415 dirName = directory ++ dirName;
416 "created directory: % \n".postf(dirName);
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 ++ "/";
438 case { selectedText[0] == $* }
441 selectedText = selectedText.drop(1);
442 extensions.do { |ext|
443 filename = directory ++ selectedText ++ ext;
444 if (File.exists(filename)) {
445 // open existing wiki page
450 filename = "Help/help-scripts/" ++ selectedText ++ ext;
451 if (File.exists(filename)) {
452 // open help-script document
459 { selectedText.first == $[ and: { selectedText.last == $] }}
462 selectedText[1 .. selectedText.size-2].help
464 { selectedText.containsStringAt(0, "http://")
465 or: { selectedText.containsStringAt(0, "file://") } }
470 { selectedText.containsStringAt(selectedText.size-1, "/") }
472 Document(selectedText,
473 pathMatch(directory ++ selectedText).collect({ |it|it.basename ++ "\n"}).join
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);
487 // make a new wiki page
488 this.makeWikiPage(selectedText, nil, directory);
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) } ) {
499 keyDown { | character, modifiers, unicode, keycode |
500 this.class.globalKeyDownAction.value(this,character, modifiers, unicode, keycode);
501 keyDownAction.value(this,character, modifiers, unicode, keycode);
504 keyUp { | character, modifiers, unicode, keycode |
505 this.class.globalKeyUpAction.value(this,character, modifiers, unicode, keycode);
506 keyUpAction.value(this,character, modifiers, unicode, keycode);
510 ^if(this.path.isNil or: { doc.path.isNil }) { doc === this } {
511 this.path == doc.path
516 ^(this.path ? this).hash
519 *defaultUsesAutoInOutdent_ {|bool|
520 Document.implementationClass.prDefaultUsesAutoInOutdent_(bool)
523 usesAutoInOutdent_ {|bool|
524 this.prUsesAutoInOutdent_(bool)
527 *prDefaultUsesAutoInOutdent_{|bool|
528 this.subclassResponsibility(thisMethod);
531 prUsesAutoInOutdent_{|bool|
532 ^this.subclassResponsibility(thisMethod);
536 // private implementation
538 prIsEditable_{ | editable=true |
539 ^this.subclassResponsibility(thisMethod)
541 prSetTitle { | argName |
542 ^this.subclassResponsibility(thisMethod)
545 ^this.subclassResponsibility(thisMethod)
548 ^this.subclassResponsibility(thisMethod)
550 prSetFileName { | apath |
551 ^this.subclassResponsibility(thisMethod)
553 prGetBounds { | argBounds |
554 ^this.subclassResponsibility(thisMethod)
557 prSetBounds { | argBounds |
558 ^this.subclassResponsibility(thisMethod)
561 *prSetSyntaxColorTheme{ |textC, classC, stringC, symbolC, commentC, numberC, specialValsC, specialVarsC, declC, puncC, environC|
562 ^this.subclassResponsibility(thisMethod);
565 // if range is -1 apply to whole doc
566 setFont { | font, rangeStart= -1, rangeSize=100 |
567 ^this.subclassResponsibility(thisMethod)
570 setTextColor { | color, rangeStart = -1, rangeSize = 0 |
571 ^this.subclassResponsibility(thisMethod)
575 ^this.subclassResponsibility(thisMethod)
578 ^this.subclassResponsibility(thisMethod)
580 selectUnderlinedText { | clickPos |
581 ^this.subclassResponsibility(thisMethod)
584 linkAtClickPos { | clickPos |
585 ^this.subclassResponsibility(thisMethod)
588 rangeText { | rangestart=0, rangesize=1 |
589 ^this.subclassResponsibility(thisMethod)
593 ^this.subclassResponsibility(thisMethod)
597 onClose.value(this); // call user function
598 this.restoreCurrentEnvironment;
599 allDocuments.remove(this);
603 prinsertText { | dataPtr, txt |
604 ^this.subclassResponsibility(thisMethod)
606 insertTextRange { | string, rangestart, rangesize |
607 ^this.subclassResponsibility(thisMethod)
611 allDocuments = allDocuments.add(this);
612 this.editable = true;
614 if (this.rangeText(0,7) == "/*RUN*/")
620 initAction.value(this);
624 //this is called after recompiling the lib
626 ^this.subclassResponsibility(thisMethod)
629 thisProcess.platform.when(\_NumberOfOpenTextWindows) {
631 } { ^allDocuments.size };
635 *newFromIndex { | idx |
636 ^super.new.initByIndex(idx)
638 initByIndex { | idx |
639 //allDocuments = allDocuments.add(this);
641 doc = this.prinitByIndex(idx);
642 if(doc.isNil,{^nil});
645 prinitByIndex { | idx |
646 ^this.subclassResponsibility(thisMethod)
649 //this is called from the menu: open, new
651 ^Document.implementationClass.prBasicNew.initLast
655 ^this.subclassResponsibility(thisMethod)
659 ^this.subclassResponsibility(thisMethod)
663 initFromPath { | path, selectionStart, selectionLength |
666 stpath = this.class.standardizePath(path);
667 this.propen(stpath, selectionStart, selectionLength);
669 this.class.allDocuments.do{ |d|
670 if(d.path == stpath.absolutePath){
676 this.background_(Color.white);
679 propen { | path, selectionStart=0, selectionLength=0 |
680 ^this.subclassResponsibility(thisMethod)
683 // private newTextWindow
684 initByString{ | argTitle, str, makeListener |
686 this.prinitByString(argTitle, str, makeListener);
687 this.background_(Color.white);
688 if(dataptr.isNil,{^nil});
690 this.title = argTitle;
693 prinitByString { | title, str, makeListener |
694 ^this.subclassResponsibility(thisMethod)
700 prSetBackgroundColor { | color |
701 ^this.subclassResponsibility(thisMethod)
703 prGetBackgroundColor { | color |
704 ^this.subclassResponsibility(thisMethod)
706 prSetSelectedBackgroundColor { | color |
707 ^this.subclassResponsibility(thisMethod);
709 prGetSelectedBackgroundColor { | color |
710 ^this.subclassResponsibility(thisMethod);
712 selectedRangeLocation {
713 ^this.subclassResponsibility(thisMethod)
716 ^this.subclassResponsibility(thisMethod)
719 prSelectLine { | line |
720 ^this.subclassResponsibility(thisMethod)
723 *prGetIndexOfListener {
724 if (this.implementationClass.isNil) {
728 if (this.implementationClass.respondsTo(\prGetIndexOfListener)) {
729 ^this.implementationClass.prGetIndexOfListener
735 //---not yet implemented
740 //*reviewUnsavedDocumentsWithAlertTitle
742 //*recentDocumentPaths
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
755 if (this.class.current == this) {
756 if (savedEnvir.isNil) {
757 this.saveCurrentEnvironment
762 restoreCurrentEnvironment {
763 if (envir.notNil) { currentEnvironment = savedEnvir };
766 saveCurrentEnvironment {
768 savedEnvir = currentEnvironment;
769 currentEnvironment = envir;