ソースを参照

some BlackBox 1.6-rc6 modules added

Alexander Shiryaev 12 年 前
コミット
64f634478d

+ 229 - 0
BlackBox/Std/Mod/Api.txt

@@ -0,0 +1,229 @@
+MODULE StdApi;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Api.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Kernel, Views, Files, Dialog, Converters, Windows, Sequencers, Stores, Meta,
+		Containers, StdDialog, Documents;
+
+	(* Auxiliary procedures *)
+
+	PROCEDURE CheckQualident (VAR str, mod, name: ARRAY OF CHAR);
+		VAR i, j: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; 
+		REPEAT
+			ch := str[i]; mod[i] := ch; INC(i)
+		UNTIL (i = LEN(str)) OR (i = LEN(mod)) OR (ch < "0") OR (ch > "9") & (CAP(ch) < "A") OR (CAP(ch) > "Z");
+		IF ch = "." THEN
+			mod[i - 1] := 0X; j := 0;
+			REPEAT
+				ch := str[i]; name[j] := ch; INC(i); INC(j)
+			UNTIL (i = LEN(str)) OR (j = LEN(name)) OR (ch < "0") OR (ch > "9") & (CAP(ch) < "A") OR (CAP(ch) > "Z");
+			IF ch # 0X THEN mod[0] := 0X; name[0] := 0X END
+		ELSE mod[0] := 0X; name[0] := 0X
+		END
+	END CheckQualident;
+	
+	PROCEDURE PathToSpec (VAR path: ARRAY OF CHAR; VAR loc: Files.Locator; VAR name: Files.Name);
+		VAR i, j: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; j := 0; loc := Files.dir.This("");
+		WHILE (loc.res = 0) & (i < LEN(path) - 1) & (j < LEN(name) - 1) & (path[i] # 0X) DO
+			ch := path[i]; INC(i);
+			IF (j > 0) & ((ch = "/") OR (ch = "\")) THEN
+				name[j] := 0X; j := 0;
+				IF name = "*" THEN
+					IF Dialog.language # "" THEN loc := loc.This(Dialog.language) END
+				ELSE loc := loc.This(name)
+				END
+			ELSE
+				name[j] := ch; INC(j)
+			END
+		END;
+		IF path[i] = 0X THEN name[j] := 0X
+		ELSE loc.res := 1; name := ""
+		END
+	END PathToSpec;
+	
+	PROCEDURE ThisDialog (dialog: ARRAY OF CHAR): Views.View;
+		VAR fname, submod, sub, mod: Files.Name; canCreate: BOOLEAN; conv: Converters.Converter;
+			loc: Files.Locator; file: Files.File; v: Views.View; s: Stores.Store; var: Meta.Item;
+	BEGIN
+		ASSERT(dialog # "", 20);
+		v := NIL; file := NIL; canCreate := FALSE;
+		CheckQualident(dialog, submod, fname);
+		IF submod # "" THEN	(* is qualident *)
+			Meta.LookupPath(dialog, var);
+			IF var.obj = Meta.varObj THEN	(* variable exists *)
+				canCreate := TRUE;
+				Kernel.SplitName(submod, sub, mod);
+				loc := Files.dir.This(sub);
+				IF loc # NIL THEN
+					Kernel.MakeFileName(fname, "");
+					loc := loc.This("Rsrc");
+					IF loc # NIL THEN file := Files.dir.Old(loc, fname, Files.shared) END;
+					IF (file = NIL) & (sub = "") THEN
+						loc := Files.dir.This("System"); ASSERT(loc # NIL, 100);
+						IF loc # NIL THEN
+							loc := loc.This("Rsrc");
+							IF loc # NIL THEN file := Files.dir.Old(loc, fname, Files.shared) END
+						END
+					END
+				END
+			END
+		END;
+		IF (file = NIL) & ~canCreate THEN	(* try file name *)
+			PathToSpec(dialog, loc, fname);
+			IF loc.res = 0 THEN
+				Kernel.MakeFileName(fname, "");
+				file := Files.dir.Old(loc, fname, Files.shared)
+			END
+		END;
+		IF file # NIL THEN
+			Kernel.MakeFileName(fname, "");
+			conv := NIL; Converters.Import(loc, fname, conv, s);
+			IF s # NIL THEN
+				v := s(Views.View)
+			END
+		ELSE Dialog.ShowParamMsg("#System:FileNotFound", dialog, "", "")
+		END;
+		RETURN v
+	END ThisDialog;
+
+	PROCEDURE ThisMask (param: ARRAY OF CHAR): Views.View;
+		VAR v: Views.View; c: Containers.Controller;
+	BEGIN
+		v := ThisDialog(param);
+		IF v # NIL THEN
+			WITH v: Containers.View DO
+				c := v.ThisController();
+				IF c # NIL THEN
+					c.SetOpts(c.opts - {Containers.noFocus} + {Containers.noCaret, Containers.noSelection})
+				ELSE Dialog.ShowMsg("#System:NotEditable")
+				END
+			ELSE Dialog.ShowMsg("#System:ContainerExpected")
+			END
+		END;
+		RETURN v
+	END ThisMask;
+	
+	(* Interface procedures *)
+	
+	PROCEDURE CloseDialog* (OUT closedView: Views.View);
+		CONST canClose = {Windows.neverDirty, Windows.isTool, Windows.isAux};
+		VAR w: Windows.Window; msg: Sequencers.CloseMsg;
+	BEGIN
+		closedView := NIL;
+		w := Windows.dir.First();
+		IF w # NIL THEN
+			IF w.sub THEN
+				closedView := w.frame.view;
+				Windows.dir.Close(w);
+			ELSIF (w.flags * canClose = {}) & w.seq.Dirty() THEN
+				Dialog.ShowMsg("#System:CannotCloseDirtyWindow")
+			ELSE
+				msg.sticky := FALSE; w.seq.Notify(msg);
+				IF ~msg.sticky THEN closedView := w.frame.view; Windows.dir.Close(w) END
+			END
+		END
+	END CloseDialog;
+	
+	PROCEDURE OpenAux* (file, title: ARRAY OF CHAR; OUT v: Views.View);
+		VAR loc: Files.Locator; name: Files.Name; t: Views.Title;
+	BEGIN
+		PathToSpec(file, loc, name);
+		IF loc.res = 0 THEN
+			loc.res := 77; v := Views.OldView(loc, name); loc.res := 0;
+			IF v # NIL THEN t := title$; Views.OpenAux(v, t)
+			ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+			END
+		ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+		END
+	END OpenAux;
+	
+	PROCEDURE OpenAuxDialog* (file, title: ARRAY OF CHAR; OUT v: Views.View);
+		VAR t0: Views.Title; done: BOOLEAN;
+	BEGIN
+		Dialog.MapString(title, t0);
+		Windows.SelectByTitle(NIL, {Windows.isAux}, t0, done);
+		IF ~done THEN
+			v := ThisMask(file);
+			IF v # NIL THEN
+				StdDialog.Open(v, title, NIL, "", NIL, FALSE, TRUE, TRUE, FALSE, TRUE)
+			END
+		END
+	END OpenAuxDialog;
+	
+	PROCEDURE OpenBrowser* (file, title: ARRAY OF CHAR; OUT v: Views.View);
+		VAR loc: Files.Locator; name: Files.Name; t: Views.Title;
+			c: Containers.Controller;
+	BEGIN
+		PathToSpec(file, loc, name);
+		IF loc.res = 0 THEN
+			loc.res := 77; v := Views.OldView(loc, name); loc.res := 0;
+			IF v # NIL THEN
+				WITH v: Containers.View DO
+					c := v.ThisController();
+					IF c # NIL THEN
+						c.SetOpts(c.opts - {Containers.noFocus, Containers.noSelection} + {Containers.noCaret})
+					END
+				ELSE
+				END;
+				t := title$;
+				StdDialog.Open(v, t, NIL, "", NIL, FALSE, TRUE, FALSE, TRUE, FALSE)
+			ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+			END
+		ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+		END
+	END OpenBrowser;
+	
+	PROCEDURE OpenDoc* (file: ARRAY OF CHAR; OUT v: Views.View);
+		VAR loc: Files.Locator; name: Files.Name; conv: Converters.Converter;
+	BEGIN
+		PathToSpec(file, loc, name);
+		IF loc.res = 0 THEN
+			conv := NIL; v := Views.Old(Views.dontAsk, loc, name, conv);
+			IF loc.res = 78 THEN loc := NIL; name := "" END;	(* stationery *)
+			IF v # NIL THEN Views.Open(v, loc, name, conv)
+			ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+			END
+		ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+		END
+	END OpenDoc;
+	
+	PROCEDURE OpenCopyOf* (file: ARRAY OF CHAR; OUT v: Views.View);
+		VAR loc: Files.Locator; name: Files.Name; conv: Converters.Converter;
+	BEGIN
+		PathToSpec(file, loc, name);
+		IF loc.res = 0 THEN
+			conv := NIL; v := Views.Old(Views.dontAsk, loc, name, conv);
+			IF loc.res = 78 THEN loc := NIL; name := "" END;	(* stationary *)
+			IF v # NIL THEN 
+				IF v.context # NIL THEN
+					v := Views.CopyOf(v.context(Documents.Context).ThisDoc(), Views.deep);
+					Stores.InitDomain(v)
+				ELSE v := Views.CopyOf(v, Views.deep)
+				END;
+				Views.Open(v, NIL, "", conv)
+			ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+			END
+		ELSE Dialog.ShowParamMsg("#System:FileNotFound", file, "", "")
+		END
+	END OpenCopyOf;
+	
+	PROCEDURE OpenToolDialog* (file, title: ARRAY OF CHAR; OUT v: Views.View);
+		VAR t0: Views.Title; done: BOOLEAN;
+	BEGIN
+		Dialog.MapString(title, t0);
+		Windows.SelectByTitle(NIL, {Windows.isTool}, t0, done);
+		IF ~done THEN
+			v := ThisMask(file);
+			IF v # NIL THEN
+				StdDialog.Open(v, title, NIL, "", NIL, TRUE, FALSE, TRUE, FALSE, TRUE)
+			END
+		END
+	END OpenToolDialog;
+	
+END StdApi.

+ 243 - 0
BlackBox/Std/Mod/CFrames.txt

@@ -0,0 +1,243 @@
+MODULE StdCFrames;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/CFrames.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT Fonts, Ports, Views, Dates, Dialog;
+
+	CONST lineUp* = 0; lineDown* = 1; pageUp* = 2; pageDown* = 3;
+
+	TYPE
+		Frame* = POINTER TO ABSTRACT RECORD (Views.Frame)
+			disabled*, undef*, readOnly*, noRedraw*: BOOLEAN;
+			font*: Fonts.Font
+		END;
+
+		PushButton* = POINTER TO ABSTRACT RECORD (Frame)
+			label*: ARRAY 256 OF CHAR;
+			default*, cancel*: BOOLEAN;
+			Do*: PROCEDURE (f: PushButton)
+		END;
+
+		CheckBox* = POINTER TO ABSTRACT RECORD (Frame)
+			label*: ARRAY 256 OF CHAR;
+			Get*: PROCEDURE (f: CheckBox; OUT on: BOOLEAN);
+			Set*: PROCEDURE (f: CheckBox; on: BOOLEAN)
+		END;
+
+		RadioButton* = POINTER TO ABSTRACT RECORD (Frame)
+			label*: ARRAY 256 OF CHAR;
+			Get*: PROCEDURE (f: RadioButton; OUT on: BOOLEAN);
+			Set*: PROCEDURE (f: RadioButton; on: BOOLEAN)
+		END;
+
+		ScrollBar* = POINTER TO ABSTRACT RECORD (Frame)
+			Track*: PROCEDURE (f: ScrollBar; dir: INTEGER; VAR pos: INTEGER);
+			Get*: PROCEDURE (f: ScrollBar; OUT size, sect, pos: INTEGER);
+			Set*: PROCEDURE (f: ScrollBar; pos: INTEGER)
+		END;
+
+		Field* = POINTER TO ABSTRACT RECORD (Frame)
+			maxLen*: INTEGER;	(* max num of characters in field (w/o 0X) *)
+			left*, right*, multiLine*, password*: BOOLEAN;
+			Get*: PROCEDURE (f: Field; OUT string: ARRAY OF CHAR);
+			Set*: PROCEDURE (f: Field; IN string: ARRAY OF CHAR);
+			Equal*: PROCEDURE (f: Field; IN s1, s2: ARRAY OF CHAR): BOOLEAN
+		END;
+		
+		UpDownField* = POINTER TO ABSTRACT RECORD (Frame)
+			min*, max*, inc*: INTEGER;
+			Get*: PROCEDURE (f: UpDownField; OUT val: INTEGER);
+			Set*: PROCEDURE (f: UpDownField; val: INTEGER)
+		END;
+			
+		DateField* = POINTER TO ABSTRACT RECORD (Frame)
+			Get*: PROCEDURE (f: DateField; OUT date: Dates.Date);
+			Set*: PROCEDURE (f: DateField; IN date: Dates.Date);
+			GetSel*: PROCEDURE (f: DateField; OUT sel: INTEGER);
+			SetSel*: PROCEDURE (f: DateField; sel: INTEGER)
+		END;
+			
+		TimeField* = POINTER TO ABSTRACT RECORD (Frame)
+			Get*: PROCEDURE (f: TimeField; OUT date: Dates.Time);
+			Set*: PROCEDURE (f: TimeField; IN date: Dates.Time);
+			GetSel*: PROCEDURE (f: TimeField; OUT sel: INTEGER);
+			SetSel*: PROCEDURE (f: TimeField; sel: INTEGER)
+		END;
+			
+		ColorField* = POINTER TO ABSTRACT RECORD (Frame)
+			Get*: PROCEDURE (f: ColorField; OUT col: INTEGER);
+			Set*: PROCEDURE (f: ColorField; col: INTEGER)
+		END;
+		
+		ListBox* = POINTER TO ABSTRACT RECORD (Frame)
+			sorted*: BOOLEAN;
+			Get*: PROCEDURE (f: ListBox; OUT i: INTEGER);
+			Set*: PROCEDURE (f: ListBox; i: INTEGER);
+			GetName*: PROCEDURE (f: ListBox; i: INTEGER; VAR name: ARRAY OF CHAR)
+		END;
+		
+		SelectionBox* = POINTER TO ABSTRACT RECORD (Frame)
+			sorted*: BOOLEAN;
+			Get*: PROCEDURE (f: SelectionBox; i: INTEGER; OUT in: BOOLEAN);
+			Incl*: PROCEDURE (f: SelectionBox; from, to: INTEGER);
+			Excl*: PROCEDURE (f: SelectionBox; from, to: INTEGER);
+			Set*: PROCEDURE (f: SelectionBox; from, to: INTEGER);
+			GetName*: PROCEDURE (f: SelectionBox; i: INTEGER; VAR name: ARRAY OF CHAR)
+		END;
+		
+		ComboBox* = POINTER TO ABSTRACT RECORD (Frame)
+			sorted*: BOOLEAN;
+			Get*: PROCEDURE (f: ComboBox; OUT string: ARRAY OF CHAR);
+			Set*: PROCEDURE (f: ComboBox; IN string: ARRAY OF CHAR);
+			GetName*: PROCEDURE (f: ComboBox; i: INTEGER; VAR name: ARRAY OF CHAR)
+		END;
+		
+		Caption* = POINTER TO ABSTRACT RECORD (Frame)
+			label*: ARRAY 256 OF CHAR;
+			left*, right*: BOOLEAN;
+		END;
+		
+		Group* = POINTER TO ABSTRACT RECORD (Frame)
+			label*: ARRAY 256 OF CHAR
+		END;
+
+		TreeFrame* = POINTER TO ABSTRACT RECORD (Frame)
+			sorted*, haslines*, hasbuttons*, atroot*, foldericons*: BOOLEAN;
+			NofNodes*: PROCEDURE (f: TreeFrame): INTEGER;
+			Child*: PROCEDURE (f: TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+			Parent*: PROCEDURE (f: TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+			Next*:  PROCEDURE (f: TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+			Select*:  PROCEDURE (f: TreeFrame; node: Dialog.TreeNode);
+			Selected*:  PROCEDURE (f: TreeFrame): Dialog.TreeNode;
+			SetExpansion*:  PROCEDURE (f: TreeFrame; tn: Dialog.TreeNode; expanded: BOOLEAN)
+		END;
+
+		Directory* = POINTER TO ABSTRACT RECORD END;
+
+
+	VAR
+		setFocus*: BOOLEAN;
+		defaultFont*, defaultLightFont*: Fonts.Font;
+		dir-, stdDir-: Directory;
+
+
+	(** Frame **)
+
+		
+	PROCEDURE (f: Frame) MouseDown* (x, y: INTEGER; buttons: SET), NEW, EMPTY;
+	PROCEDURE (f: Frame) WheelMove* (x, y: INTEGER; op, nofLines: INTEGER;
+																VAR done: BOOLEAN), NEW, EMPTY;
+	PROCEDURE (f: Frame) KeyDown* (ch: CHAR), NEW, EMPTY;
+	PROCEDURE (f: Frame) Restore* (l, t, r, b: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: Frame) UpdateList*, NEW, EMPTY;
+	PROCEDURE (f: Frame) Mark* (on, focus: BOOLEAN), NEW, EMPTY;
+	PROCEDURE (f: Frame) Edit* (op: INTEGER; VAR v: Views.View; VAR w, h: INTEGER;
+													VAR singleton, clipboard: BOOLEAN), NEW, EMPTY;
+	PROCEDURE (f: Frame) GetCursor* (x, y: INTEGER; modifiers: SET; VAR cursor: INTEGER), NEW, EMPTY;
+
+	PROCEDURE (f: Frame) Update*, NEW, EXTENSIBLE;
+		VAR l, t, r, b: INTEGER; root: Views.RootFrame;
+	BEGIN
+		l := f.l + f.gx; t := f.t + f.gy; r := f.r + f.gx; b := f.b + f.gy;
+		root := Views.RootOf(f);
+		Views.UpdateRoot(root, l, t, r, b, Views.keepFrames);
+		Views.ValidateRoot(root)
+	END Update;
+
+	PROCEDURE (f: Frame) DblClickOk* (x, y: INTEGER): BOOLEAN, NEW, EXTENSIBLE;
+	BEGIN
+		RETURN TRUE
+	END DblClickOk;
+
+
+	(** Field **)
+
+	PROCEDURE (f: Field) Idle* (), NEW, ABSTRACT;
+	PROCEDURE (f: Field) Select* (from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: Field) GetSelection* (OUT from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: Field) Length* (): INTEGER, NEW, ABSTRACT;
+
+	PROCEDURE (f: Field) GetCursor* (x, y: INTEGER; modifiers: SET; VAR cursor: INTEGER), EXTENSIBLE;
+	BEGIN
+		cursor := Ports.textCursor
+	END GetCursor;
+
+
+	(** UpDownField **)
+
+	PROCEDURE (f: UpDownField) Idle*, NEW, ABSTRACT;
+	PROCEDURE (f: UpDownField) Select* (from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: UpDownField) GetSelection* (OUT from, to: INTEGER), NEW, ABSTRACT;
+
+	PROCEDURE (f: UpDownField) GetCursor* (x, y: INTEGER; modifiers: SET;
+																		VAR cursor: INTEGER), EXTENSIBLE;
+	BEGIN
+		cursor := Ports.textCursor
+	END GetCursor;
+
+
+	(** SelectionBox **)
+
+	PROCEDURE (f: SelectionBox) Select* (from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: SelectionBox) GetSelection* (OUT from, to: INTEGER), NEW, ABSTRACT;
+	
+	PROCEDURE (f: SelectionBox) UpdateRange* (op, from, to: INTEGER), NEW, EXTENSIBLE;
+	BEGIN
+		f.Update
+	END UpdateRange;
+
+
+	(** ComboBox **)
+
+	PROCEDURE (f: ComboBox) Idle* (), NEW, ABSTRACT;
+	PROCEDURE (f: ComboBox) Select* (from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: ComboBox) GetSelection* (OUT from, to: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (f: ComboBox) Length* (): INTEGER, NEW, ABSTRACT;
+	
+	(* TreeFrame **)
+	PROCEDURE (f: TreeFrame) GetSize* (OUT w, h: INTEGER), NEW, ABSTRACT;
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) GetPushButtonSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetCheckBoxSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetRadioButtonSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetScrollBarSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetFieldSize* (max: INTEGER; VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetUpDownFieldSize* (max: INTEGER; VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetDateFieldSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetTimeFieldSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetColorFieldSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetListBoxSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetSelectionBoxSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetComboBoxSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetCaptionSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetGroupSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) GetTreeFrameSize* (VAR w, h: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewPushButton* (): PushButton, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewCheckBox* (): CheckBox, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewRadioButton* (): RadioButton, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewScrollBar* (): ScrollBar, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewField* (): Field, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewUpDownField* (): UpDownField, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewDateField* (): DateField, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewTimeField* (): TimeField, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewColorField* (): ColorField, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewListBox* (): ListBox, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewSelectionBox* (): SelectionBox, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewComboBox* (): ComboBox, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewCaption* (): Caption, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewGroup* (): Group, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewTreeFrame* (): TreeFrame, NEW, ABSTRACT;
+
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); dir := d;
+		IF stdDir = NIL THEN stdDir := d END
+	END SetDir;
+
+BEGIN
+	setFocus := FALSE
+END StdCFrames.

+ 183 - 0
BlackBox/Std/Mod/Clocks.txt

@@ -0,0 +1,183 @@
+MODULE StdClocks;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Clocks.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Dates, Math, Domains := Stores, Ports, Stores, Models, Views, Services, Properties,
+		TextModels;
+
+	CONST
+		minSize = 25 * Ports.point; niceSize = 42 * Ports.point;
+		minVersion = 0; maxVersion = 0;
+
+	TYPE
+		StdView = POINTER TO RECORD (Views.View)
+			time: Dates.Time
+		END;
+
+		TickAction = POINTER TO RECORD (Services.Action) END;
+
+		Msg = RECORD (Models.Message)
+			consumed: BOOLEAN;
+			time: Dates.Time
+		END;
+
+	VAR
+		clockTime: Dates.Time;
+		action: TickAction;
+		actionIsAlive: BOOLEAN;
+
+
+	PROCEDURE Cos (r, g: INTEGER): INTEGER;
+	BEGIN
+		RETURN SHORT(ENTIER(r * Math.Cos(2 * Math.Pi() * g / 60) + 0.5))
+	END Cos;
+
+	PROCEDURE Sin (r, g: INTEGER): INTEGER;
+	BEGIN
+		RETURN SHORT(ENTIER(r * Math.Sin(2 * Math.Pi() * g / 60) + 0.5))
+	END Sin;
+
+	PROCEDURE (a: TickAction) Do;
+		VAR msg: Msg; time: Dates.Time;
+	BEGIN
+		Dates.GetTime(time);
+		IF clockTime.second = time.second THEN
+			Services.DoLater(action, Services.Ticks() + Services.resolution DIV 2)
+		ELSE
+			clockTime := time;
+			msg.consumed := FALSE;
+			msg.time := time;
+			Views.Omnicast(msg);
+			IF msg.consumed THEN
+				Services.DoLater(action, Services.Ticks() + Services.resolution DIV 2)
+			ELSE
+				actionIsAlive := FALSE
+			END
+		END
+	END Do;
+
+
+	(* View *)
+
+	PROCEDURE DrawTick (f: Views.Frame; m, d0, d1, s, g: INTEGER; c: Ports.Color);
+	BEGIN
+		f.DrawLine(m + Sin(d0, g), m - Cos(d0, g), m + Sin(d1, g), m - Cos(d1, g), s, c)
+	END DrawTick;
+
+
+	PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		v.Externalize^(wr);
+		wr.WriteVersion(maxVersion);
+		wr.WriteByte(9)
+	END Externalize;
+
+	PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER; format: BYTE;
+	BEGIN
+		v.Internalize^(rd);
+		IF ~rd.cancelled THEN
+			rd.ReadVersion(minVersion, maxVersion, thisVersion);
+			IF ~rd.cancelled THEN
+				rd.ReadByte(format);
+				v.time.second := -1
+			END
+		END
+	END Internalize;
+
+	PROCEDURE (v: StdView) CopyFromSimpleView (source: Views.View);
+	BEGIN
+		WITH source: StdView DO
+			v.time.second := -1
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR c: Models.Context; a: TextModels.Attributes; color: Ports.Color;
+			time: Dates.Time;
+			i, m, d, u, hs, hd1, ms, md1, ss, sd0, sd1,  w, h: INTEGER;
+	BEGIN
+		IF ~actionIsAlive THEN
+			 actionIsAlive := TRUE; Services.DoLater(action, Services.now)
+		END;
+		IF v.time.second = -1 THEN Dates.GetTime(v.time) END;
+		c := v.context; c.GetSize(w, h);
+		WITH c: TextModels.Context DO a := c.Attr(); color := a.color
+		ELSE color := Ports.defaultColor
+		END;
+		u := f.unit;
+		d := h DIV u * u;
+		IF ~ODD(d DIV u) THEN DEC(d, u) END;
+		m := (h - u) DIV 2;
+		IF d >= niceSize - 2 * Ports.point THEN
+			hs := 3 * u; ms := 3 * u; ss := u;
+			hd1 := m * 4 DIV 6; md1 := m * 5 DIV 6; sd0 := -(m DIV 6); sd1 := m - 4 * u;
+			i := 0; WHILE i < 12 DO DrawTick(f, m, m * 11 DIV 12, m, u, i  * 5, color); INC(i) END
+		ELSE
+			hd1 := m * 2 DIV 4; hs := u; ms := u; ss := u;
+			md1 := m * 3 DIV 4; sd0 := 0; sd1 := 3 * u
+		END;
+		time := v.time;
+		f.DrawOval(0, 0, d, d, u, color);
+		DrawTick(f, m, 0, m * 4 DIV 6, hs, time.hour MOD 12 * 5 + time.minute DIV 12, color);
+		DrawTick(f, m, 0, md1, ms, time.minute, color);
+		DrawTick(f, m, sd0, sd1, ss, time.second, color)
+	END Restore;
+
+	PROCEDURE (v: StdView) HandleModelMsg (VAR msg: Models.Message);
+		VAR w, h: INTEGER;
+	BEGIN
+		WITH msg: Msg DO
+			msg.consumed := TRUE;
+			IF v.time.second # msg.time.second THEN	(* execute only once per view *)
+				Views.Update(v, Views.keepFrames);
+				v.time := msg.time
+			END
+		ELSE
+		END
+	END HandleModelMsg;
+
+	PROCEDURE SizePref (v: StdView; VAR p: Properties.SizePref);
+	BEGIN
+		IF (p.w > Views.undefined) & (p.h > Views.undefined) THEN
+			Properties.ProportionalConstraint(1, 1, p.fixedW, p.fixedH, p.w, p.h);
+			IF p.w < minSize THEN p.w := minSize; p.h := minSize END
+		ELSE
+			p.w := niceSize; p.h := niceSize
+		END
+	END SizePref;
+
+	PROCEDURE (v: StdView) HandlePropMsg (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.Preference DO
+			WITH msg: Properties.SizePref DO
+				SizePref(v, msg)
+			ELSE
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+
+
+	(** allocation **)
+
+	PROCEDURE New* (): Views.View;
+		VAR v: StdView;
+	BEGIN
+		NEW(v); v.time.second := -1; RETURN v
+	END New;
+
+	PROCEDURE Deposit*;
+	BEGIN
+		Views.Deposit(New())
+	END Deposit;
+
+
+BEGIN
+	clockTime.second := -1;
+	NEW(action); actionIsAlive := FALSE
+CLOSE
+	IF actionIsAlive THEN Services.RemoveAction(action) END
+END StdClocks.

+ 1016 - 0
BlackBox/Std/Mod/Cmds.txt

@@ -0,0 +1,1016 @@
+MODULE StdCmds;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Cmds.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Fonts, Ports, Services, Stores, Sequencers, Models, Views,
+		Controllers, Containers, Properties, Dialog, Documents, Windows, Strings,
+		StdDialog, StdApi;
+
+	CONST
+		illegalSizeKey = "#System:IllegalFontSize";
+		defaultAllocator = "TextViews.Deposit; StdCmds.Open";
+
+		(* wType, hType *)
+		fix = 0; page = 1; window = 2;
+
+	VAR
+		size*: RECORD 
+			size*: INTEGER
+		END;
+		layout*: RECORD
+			wType*, hType*: INTEGER;
+			width*, height*: REAL;
+			doc: Documents.Document;
+			u: INTEGER
+		END;
+		allocator*: Dialog.String;
+		
+		propEra: INTEGER;	(* (propEra, props) form cache for StdProps() *)
+		props: Properties.StdProp;	(* valid iff propEra = Props.era *)
+		
+		prop: Properties.Property;	(* usef for copy/paste properties *)
+
+	(* auxiliary procedures *)
+	
+	PROCEDURE StdProp (): Properties.StdProp;
+	BEGIN
+		IF propEra # Properties.era THEN
+			Properties.CollectStdProp(props);
+			propEra := Properties.era
+		END;
+		RETURN props
+	END StdProp;
+
+	PROCEDURE Append (VAR s: ARRAY OF CHAR; t: ARRAY OF CHAR);
+		VAR len, i, j: INTEGER; ch: CHAR;
+	BEGIN
+		len := LEN(s);
+		i := 0; WHILE s[i] # 0X DO INC(i) END;
+		j := 0; REPEAT ch := t[j]; s[i] := ch; INC(j); INC(i) UNTIL (ch = 0X) OR (i = len);
+		s[len - 1] := 0X
+	END Append;	
+
+	(* standard commands *)
+
+	PROCEDURE OpenAuxDialog* (file, title: ARRAY OF CHAR);
+		VAR v: Views.View;
+	BEGIN
+		StdApi.OpenAuxDialog(file, title, v)
+	END OpenAuxDialog;
+
+	PROCEDURE OpenToolDialog* (file, title: ARRAY OF CHAR);
+		VAR v: Views.View;
+	BEGIN
+		StdApi.OpenToolDialog(file, title, v)
+	END OpenToolDialog;
+
+	PROCEDURE OpenDoc* (file: ARRAY OF CHAR);
+		VAR v: Views.View;
+	BEGIN
+		StdApi.OpenDoc(file, v)
+	END OpenDoc;
+	
+	PROCEDURE OpenCopyOf* (file: ARRAY OF CHAR);
+		VAR v: Views.View;
+	BEGIN
+		StdApi.OpenCopyOf(file, v)
+	END OpenCopyOf;
+	
+	PROCEDURE OpenAux* (file, title: ARRAY OF CHAR);
+		VAR v: Views.View; 
+	BEGIN
+		StdApi.OpenAux(file, title, v)
+	END OpenAux;
+
+	PROCEDURE OpenBrowser* (file, title: ARRAY OF CHAR);
+		VAR v: Views.View;
+	BEGIN
+		StdApi.OpenBrowser(file, title, v)
+	END OpenBrowser;
+
+	PROCEDURE CloseDialog*;
+		VAR v: Views.View;
+	BEGIN
+		StdApi.CloseDialog(v)
+	END CloseDialog;
+
+
+	PROCEDURE Open*;
+		VAR i: INTEGER; v: Views.View;
+	BEGIN
+		i := Views.Available();
+		IF i > 0 THEN Views.Fetch(v); Views.OpenView(v)
+		ELSE Dialog.ShowMsg("#System:DepositExpected")
+		END
+	END Open;
+
+	PROCEDURE PasteView*;
+		VAR i: INTEGER; v: Views.View;
+	BEGIN
+		i := Views.Available();
+		IF i > 0 THEN
+			Views.Fetch(v);
+			Controllers.PasteView(v, Views.undefined, Views.undefined, FALSE)
+		ELSE Dialog.ShowMsg("#System:DepositExpected")
+		END
+	END PasteView;
+	
+	(* file menu commands *)
+	
+	PROCEDURE New*;
+		VAR res: INTEGER;
+	BEGIN
+		Dialog.Call(allocator, " ", res)
+	END New;
+	
+	
+	(* edit menu commands *)
+	
+	PROCEDURE Undo*;
+		VAR w: Windows.Window;
+	BEGIN
+		w := Windows.dir.Focus(Controllers.frontPath);
+		IF w # NIL THEN w.seq.Undo END
+	END Undo;
+
+	PROCEDURE Redo*;
+		VAR w: Windows.Window;
+	BEGIN
+		w := Windows.dir.Focus(Controllers.frontPath);
+		IF w # NIL THEN w.seq.Redo END
+	END Redo;
+
+	PROCEDURE CopyProp*;
+	BEGIN
+		Properties.CollectProp(prop)
+	END CopyProp;
+	
+	PROCEDURE PasteProp*;
+	BEGIN
+		Properties.EmitProp(NIL, prop)
+	END PasteProp;
+	
+	PROCEDURE Clear*;
+	(** remove the selection of the current focus **)
+		VAR msg: Controllers.EditMsg;
+	BEGIN
+		msg.op := Controllers.cut; msg.view := NIL;
+		msg.clipboard := FALSE;
+		Controllers.Forward(msg)
+	END Clear;
+
+	PROCEDURE SelectAll*;
+	(** select whole content of current focus **)
+		VAR msg: Controllers.SelectMsg;
+	BEGIN
+		msg.set := TRUE; Controllers.Forward(msg)
+	END SelectAll;
+
+	PROCEDURE DeselectAll*;
+	(** select whole content of current focus **)
+		VAR msg: Controllers.SelectMsg;
+	BEGIN
+		msg.set := FALSE; Controllers.Forward(msg)
+	END DeselectAll;
+
+	PROCEDURE SelectDocument*;
+	(** select whole document **)
+		VAR w: Windows.Window; c: Containers.Controller;
+	BEGIN
+		w := Windows.dir.Focus(Controllers.path);
+		IF w # NIL THEN
+			c := w.doc.ThisController();
+			IF (c # NIL) & ~(Containers.noSelection IN c.opts) & (c.Singleton() = NIL) THEN
+				c.SetSingleton(w.doc.ThisView())
+			END
+		END
+	END SelectDocument;
+
+	PROCEDURE SelectNextView*;
+		VAR c: Containers.Controller; v: Views.View;
+	BEGIN
+		c := Containers.Focus();
+		IF (c # NIL) & ~(Containers.noSelection IN c.opts) THEN
+			IF c.HasSelection() THEN v := c.Singleton() ELSE v := NIL END;
+			IF v = NIL THEN
+				c.GetFirstView(Containers.any, v)
+			ELSE
+				c.GetNextView(Containers.any, v);
+				IF v = NIL THEN c.GetFirstView(Containers.any, v) END
+			END;
+			c.SelectAll(FALSE);
+			IF v # NIL THEN c.SetSingleton(v) END
+		ELSE Dialog.ShowMsg("#Dev:NoTargetFocusFound")
+		END
+	END SelectNextView;
+
+	
+	(** font menu commands **)
+
+	PROCEDURE Font* (typeface: Fonts.Typeface);
+	(** set the selection to the given font family **)
+		VAR p: Properties.StdProp;
+	BEGIN
+		NEW(p); p.valid := {Properties.typeface}; p.typeface := typeface;
+		Properties.EmitProp(NIL, p)
+	END Font;
+
+	PROCEDURE DefaultFont*;
+	(** set the selection to the default font family **)
+		VAR p: Properties.StdProp;
+	BEGIN
+		NEW(p); p.valid := {Properties.typeface}; p.typeface := Fonts.default;
+		Properties.EmitProp(NIL, p)
+	END DefaultFont;
+
+
+	(** attributes menu commands **)
+
+	PROCEDURE Plain*;
+	(** reset the font attribute "weight" and all font style attributes of the selection **)
+		VAR p: Properties.StdProp;
+	BEGIN
+		NEW(p); p.valid := {Properties.style, Properties.weight};
+		p.style.val := {}; p.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
+		p.weight := Fonts.normal;
+		Properties.EmitProp(NIL, p)
+	END Plain;
+
+	PROCEDURE Bold*;
+	(** change the font attribute "weight" in the selection;
+	if the selection has a homogeneously bold weight: toggle to normal, else force to bold **)
+		VAR p, p0: Properties.StdProp;
+	BEGIN
+		Properties.CollectStdProp(p0);
+		NEW(p); p.valid := {Properties.weight};
+		IF (Properties.weight IN p0.valid) & (p0.weight # Fonts.normal) THEN
+			p.weight := Fonts.normal
+		ELSE p.weight := Fonts.bold
+		END;
+		Properties.EmitProp(NIL, p)
+	END Bold;
+
+	PROCEDURE Italic*;
+	(** change the font style attribute "italic" in the selection;
+	if the selection is homogeneous wrt this attribute: toggle, else force to italic **)
+		VAR p, p0: Properties.StdProp;
+	BEGIN
+		Properties.CollectStdProp(p0);
+		NEW(p); p.valid := {Properties.style}; p.style.mask := {Fonts.italic};
+		IF (Properties.style IN p0.valid) & (Fonts.italic IN p0.style.val) THEN
+			p.style.val := {}
+		ELSE p.style.val := {Fonts.italic}
+		END;
+		Properties.EmitProp(NIL, p)
+	END Italic;
+
+	PROCEDURE Underline*;
+	(** change the font style attribute "underline" in the selection;
+	if the selection is homogeneous wrt this attribute: toggle, else force to underline **)
+		VAR p, p0: Properties.StdProp;
+	BEGIN
+		Properties.CollectStdProp(p0);
+		NEW(p); p.valid := {Properties.style}; p.style.mask := {Fonts.underline};
+		IF (Properties.style IN p0.valid) & (Fonts.underline IN p0.style.val) THEN
+			p.style.val := {}
+		ELSE p.style.val := {Fonts.underline}
+		END;
+		Properties.EmitProp(NIL, p)
+	END Underline;
+
+	PROCEDURE Strikeout*;
+	(** change the font style attribute "strikeout" in the selection,
+	without changing other attributes;
+	if the selection is homogeneous wrt this attribute: toggle,
+	else force to strikeout **)
+		VAR p, p0: Properties.StdProp;
+	BEGIN
+		Properties.CollectStdProp(p0);
+		NEW(p); p.valid := {Properties.style}; p.style.mask := {Fonts.strikeout};
+		IF (Properties.style IN p0.valid) & (Fonts.strikeout IN p0.style.val) THEN
+			p.style.val := {}
+		ELSE p.style.val := {Fonts.strikeout}
+		END;
+		Properties.EmitProp(NIL, p)
+	END Strikeout;
+
+	PROCEDURE Size* (size: INTEGER);
+	(** set the selection to the given font size **)
+		VAR p: Properties.StdProp;
+	BEGIN
+		NEW(p); p.valid := {Properties.size};
+		p.size := size * Ports.point;
+		Properties.EmitProp(NIL, p)
+	END Size;
+
+	PROCEDURE SetSize*;
+		VAR p: Properties.StdProp;
+	BEGIN
+		IF (0 <= size.size) & (size.size < 32768) THEN
+			NEW(p); p.valid := {Properties.size};
+			p.size := size.size * Fonts.point;
+			Properties.EmitProp(NIL, p)
+		ELSE
+			Dialog.ShowMsg(illegalSizeKey)
+		END
+	END SetSize;
+
+	PROCEDURE InitSizeDialog*;
+		VAR p: Properties.StdProp;
+	BEGIN
+		Properties.CollectStdProp(p);
+		IF Properties.size IN p.valid THEN size.size := p.size DIV Fonts.point END
+	END InitSizeDialog;
+
+	PROCEDURE Color* (color: Ports.Color);
+	(** set the color attributes of the selection **)
+		VAR p: Properties.StdProp;
+	BEGIN
+		NEW(p); p.valid := {Properties.color};
+		p.color.val := color;
+		Properties.EmitProp(NIL, p)
+	END Color;
+
+	PROCEDURE UpdateAll*;	(* for HostCmds.Toggle *)
+		VAR w: Windows.Window; pw, ph: INTEGER; dirty: BOOLEAN; msg: Models.UpdateMsg;
+	BEGIN
+		w := Windows.dir.First();
+		WHILE w # NIL DO
+			IF ~w.sub THEN
+				dirty := w.seq.Dirty();
+				Models.Domaincast(w.doc.Domain(), msg);
+				IF ~dirty THEN w.seq.SetDirty(FALSE) END	(* not perfect: "undoable dirt" ... *)
+			END;
+			w.port.GetSize(pw, ph);
+			w.Restore(0, 0, pw, ph);
+			w := Windows.dir.Next(w)
+		END
+	END UpdateAll;
+
+	PROCEDURE RestoreAll*;
+		VAR w: Windows.Window; pw, ph: INTEGER;
+	BEGIN
+		w := Windows.dir.First();
+		WHILE w # NIL DO
+			w.port.GetSize(pw, ph);
+			w.Restore(0, 0, pw, ph);
+			w := Windows.dir.Next(w)
+		END
+	END RestoreAll;
+	
+	
+	(** document layout dialog **)
+	
+	PROCEDURE SetLayout*;
+		VAR opts: SET; l, t, r, b, r0, b0: INTEGER; c: Containers.Controller; script: Stores.Operation;
+	BEGIN
+		c := layout.doc.ThisController();
+		opts := c.opts - {Documents.pageWidth..Documents.winHeight};
+		IF layout.wType = page THEN INCL(opts, Documents.pageWidth)
+		ELSIF layout.wType = window THEN INCL(opts, Documents.winWidth)
+		END;
+		IF layout.hType = page THEN INCL(opts, Documents.pageHeight)
+		ELSIF layout.hType = window THEN INCL(opts, Documents.winHeight)
+		END;
+		layout.doc.PollRect(l, t, r, b); r0 := r; b0 := b;
+		IF layout.wType = fix THEN r := l + SHORT(ENTIER(layout.width * layout.u)) END;
+		IF layout.hType = fix THEN b := t + SHORT(ENTIER(layout.height * layout.u)) END;
+		IF (opts # c.opts) OR (r # r0) OR (b # b0) THEN
+			Views.BeginScript(layout.doc, "#System:ChangeLayout", script);
+			c.SetOpts(opts);
+			layout.doc.SetRect(l, t, r, b);
+			Views.EndScript(layout.doc, script)
+		END
+	END SetLayout;
+	
+	PROCEDURE InitLayoutDialog*;
+	(* guard: WindowGuard *)
+		VAR w: Windows.Window; c: Containers.Controller; l, t, r, b: INTEGER;
+	BEGIN
+		w := Windows.dir.First();
+		IF w # NIL THEN
+			layout.doc := w.doc;
+			c := w.doc.ThisController();
+			IF Documents.pageWidth IN c.opts THEN layout.wType := page
+			ELSIF Documents.winWidth IN c.opts THEN layout.wType := window
+			ELSE layout.wType := fix
+			END;
+			IF Documents.pageHeight IN c.opts THEN layout.hType := page
+			ELSIF Documents.winHeight IN c.opts THEN layout.hType := window
+			ELSE layout.hType := fix
+			END;
+			IF Dialog.metricSystem THEN layout.u := Ports.mm * 10 ELSE layout.u := Ports.inch END;
+			w.doc.PollRect(l, t, r, b);
+			layout.width := (r - l) DIV (layout.u DIV 100) / 100;
+			layout.height := (b - t) DIV (layout.u DIV 100) / 100
+		END
+	END InitLayoutDialog;
+	
+	PROCEDURE WidthGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF layout.wType # fix THEN par.readOnly := TRUE END
+	END WidthGuard;
+	
+	PROCEDURE HeightGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF layout.hType # fix THEN par.readOnly := TRUE END
+	END HeightGuard;
+	
+	PROCEDURE TypeNotifier* (op, from, to: INTEGER);
+		VAR w, h, l, t, r, b: INTEGER; d: BOOLEAN;
+	BEGIN
+		layout.doc.PollRect(l, t, r, b);
+		IF layout.wType = page THEN
+			layout.doc.PollPage(w, h, l, t, r, b, d)
+		ELSIF layout.wType = window THEN
+			layout.doc.context.GetSize(w, h); r := w - l
+		END;
+		layout.width := (r - l) DIV (layout.u DIV 100) / 100;
+		layout.doc.PollRect(l, t, r, b);
+		IF layout.hType = page THEN
+			layout.doc.PollPage(w, h, l, t, r, b, d)
+		ELSIF layout.hType = window THEN
+			layout.doc.context.GetSize(w, h); b := h - t
+		END;
+		layout.height := (b - t) DIV (layout.u DIV 100) / 100;
+		Dialog.Update(layout)
+	END TypeNotifier;
+	
+
+	(** window menu command **)
+
+	PROCEDURE NewWindow*;
+	(** guard ModelViewGuard **)
+		VAR win: Windows.Window; doc: Documents.Document; v: Views.View; title: Views.Title;
+			seq: ANYPTR; clean: BOOLEAN;
+	BEGIN
+		win := Windows.dir.Focus(Controllers.frontPath);
+		IF win # NIL THEN
+			v := win.doc.ThisView();
+			IF v.Domain() # NIL THEN seq := v.Domain().GetSequencer() ELSE seq := NIL END;
+			clean := (seq # NIL) & ~seq(Sequencers.Sequencer).Dirty();
+			doc := win.doc.DocCopyOf(v);
+			(* Stores.InitDomain(doc, v.Domain()); *)
+			ASSERT(doc.Domain() = v.Domain(), 100);
+			win.GetTitle(title);
+			Windows.dir.OpenSubWindow(Windows.dir.New(), doc, win.flags, title);
+			IF clean THEN seq(Sequencers.Sequencer).SetDirty(FALSE) END
+		END
+	END NewWindow;
+	
+	(* properties *)
+	
+	PROCEDURE GetCmd (name: ARRAY OF CHAR; OUT cmd: ARRAY OF CHAR);
+		VAR i, j: INTEGER; ch, lch: CHAR; key: ARRAY 256 OF CHAR;
+	BEGIN
+		i := 0; ch := name[0]; key[0] := "#"; j := 1;
+		REPEAT
+			key[j] := ch; INC(j); lch := ch; INC(i); ch := name[i]
+		UNTIL (ch = 0X) OR (ch = ".")
+			OR ((ch >= "A") & (ch <= "Z") OR (ch >= "À") & (ch # "×") & (ch <= "Þ"))
+				& ((lch < "A") OR (lch > "Z") & (lch < "À") OR (lch = "×") OR (lch > "Þ"));
+		IF ch = "." THEN
+			key := "#System:" + name
+		ELSE
+			key[j] := ":"; INC(j); key[j] := 0X; j := 0;
+			WHILE ch # 0X DO name[j] := ch; INC(i); INC(j); ch := name[i] END;
+			name[j] := 0X; key := key + name
+		END;
+		Dialog.MapString(key, cmd);
+		IF cmd = name THEN cmd := "" END
+	END GetCmd;
+	
+	PROCEDURE SearchCmd (call: BOOLEAN; OUT found: BOOLEAN);
+		VAR p: Properties.Property; std: BOOLEAN; v: Views.View;  cmd: ARRAY 256 OF CHAR; pos, res: INTEGER;
+	BEGIN
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		v := Containers.FocusSingleton(); found := FALSE;
+		IF v # NIL THEN
+			Services.GetTypeName(v, cmd);
+			GetCmd(cmd, cmd);
+			IF cmd # "" THEN found := TRUE;
+				IF call THEN Dialog.Call(cmd, "", res) END
+			END
+		END;
+		std := FALSE;
+		Properties.CollectProp(p);
+		WHILE p # NIL DO
+			IF p IS Properties.StdProp THEN std := TRUE
+			ELSE
+				Services.GetTypeName(p, cmd);
+				GetCmd(cmd, cmd);
+				IF cmd # "" THEN found := TRUE;
+					IF call THEN Dialog.Call(cmd, "", res) END
+				ELSE
+					Services.GetTypeName(p, cmd);
+					Strings.Find(cmd, "Desc", LEN(cmd$)-4, pos);
+					IF LEN(cmd$)-4 = pos THEN
+						cmd[pos] := 0X; GetCmd(cmd, cmd);
+						IF cmd # "" THEN found := TRUE;
+							IF call THEN Dialog.Call(cmd, "", res) END
+						END
+					END
+				END
+			END;
+			p := p.next
+		END;
+		IF std & ~found THEN
+			Dialog.MapString("#Host:Properties.StdProp", cmd);
+			IF cmd # "Properties.StdProp" THEN found := TRUE;
+				IF call THEN Dialog.Call(cmd, "", res) END
+			END
+		END;
+		IF ~found THEN
+			Dialog.MapString("#System:ShowProp", cmd);
+			IF cmd # "ShowProp" THEN found := TRUE;
+				IF call THEN Dialog.Call(cmd, "", res) END
+			END
+		END;
+		Controllers.ResetCurrentPath
+	END SearchCmd;
+	
+	PROCEDURE ShowProp*;
+		VAR found: BOOLEAN;
+	BEGIN
+		SearchCmd(TRUE, found)
+	END ShowProp;
+	
+	PROCEDURE ShowPropGuard* (VAR par: Dialog.Par);
+		VAR found: BOOLEAN;
+	BEGIN
+		SearchCmd(FALSE, found);
+		IF ~found THEN par.disabled := TRUE END
+	END ShowPropGuard;
+	
+	
+	(* container commands *)
+	
+	PROCEDURE ActFocus (): Containers.Controller;
+		VAR c: Containers.Controller; v: Views.View;
+	BEGIN
+		c := Containers.Focus();
+		IF c # NIL THEN
+			v := c.ThisView();
+			IF v IS Documents.Document THEN
+				v := v(Documents.Document).ThisView();
+				IF v IS Containers.View THEN
+					c := v(Containers.View).ThisController()
+				ELSE c := NIL
+				END
+			END
+		END;
+		RETURN c
+	END ActFocus;
+
+	PROCEDURE ToggleNoFocus*;
+		VAR c: Containers.Controller; v: Views.View;
+	BEGIN
+		c := ActFocus();
+		IF c # NIL THEN
+			v := c.ThisView();
+			IF ~((v IS Documents.Document) OR (Containers.noSelection IN c.opts)) THEN
+				IF Containers.noFocus IN c.opts THEN
+					c.SetOpts(c.opts - {Containers.noFocus})
+				ELSE
+					c.SetOpts(c.opts + {Containers.noFocus})
+				END
+			END
+		END
+	END ToggleNoFocus;
+
+	PROCEDURE OpenAsAuxDialog*;
+	(** create a new sub-window onto the focus view shown in the top window, mask mode **)
+		VAR win: Windows.Window; doc: Documents.Document; v, u: Views.View; title: Views.Title;
+			c: Containers.Controller;
+	BEGIN
+		v := Controllers.FocusView();
+		IF (v # NIL) & (v IS Containers.View) & ~(v IS Documents.Document) THEN
+			win := Windows.dir.Focus(Controllers.frontPath); ASSERT(win # NIL, 100);
+			doc := win.doc.DocCopyOf(v);
+			u := doc.ThisView();
+			c := u(Containers.View).ThisController();
+			c.SetOpts(c.opts - {Containers.noFocus} + {Containers.noCaret, Containers.noSelection});
+			IF v # win.doc.ThisView() THEN
+				c := doc.ThisController();
+				c.SetOpts(c.opts - {Documents.pageWidth, Documents.pageHeight}
+										+ {Documents.winWidth, Documents.winHeight})
+			END;
+			(* Stores.InitDomain(doc, v.Domain()); already done in DocCopyOf *)
+			win.GetTitle(title);
+			Windows.dir.OpenSubWindow(Windows.dir.New(), doc,
+	{Windows.isAux, Windows.neverDirty, Windows.noResize, Windows.noHScroll, Windows.noVScroll},
+															title)
+		ELSE Dialog.Beep
+		END
+	END OpenAsAuxDialog;
+
+	PROCEDURE OpenAsToolDialog*;
+	(** create a new sub-window onto the focus view shown in the top window, mask mode **)
+		VAR win: Windows.Window; doc: Documents.Document; v, u: Views.View; title: Views.Title;
+			c: Containers.Controller;
+	BEGIN
+		v := Controllers.FocusView();
+		IF (v # NIL) & (v IS Containers.View) & ~(v IS Documents.Document) THEN
+			win := Windows.dir.Focus(Controllers.frontPath); ASSERT(win # NIL, 100);
+			doc := win.doc.DocCopyOf(v);
+			u := doc.ThisView();
+			c := u(Containers.View).ThisController();
+			c.SetOpts(c.opts - {Containers.noFocus} + {Containers.noCaret, Containers.noSelection});
+			IF v # win.doc.ThisView() THEN
+				c := doc.ThisController();
+				c.SetOpts(c.opts - {Documents.pageWidth, Documents.pageHeight}
+										+ {Documents.winWidth, Documents.winHeight})
+			END;
+			(* Stores.InitDomain(doc, v.Domain()); already done in DocCopyOf *)
+			win.GetTitle(title);
+			Windows.dir.OpenSubWindow(Windows.dir.New(), doc,
+	{Windows.isTool, Windows.neverDirty, Windows.noResize, Windows.noHScroll, Windows.noVScroll},
+															title)
+		ELSE Dialog.Beep
+		END
+	END OpenAsToolDialog;
+
+	PROCEDURE RecalcFocusSize*;
+		VAR c: Containers.Controller; v: Views.View; bounds: Properties.BoundsPref;
+	BEGIN
+		c := Containers.Focus();
+		IF c # NIL THEN
+			v := c.ThisView();
+			bounds.w := Views.undefined; bounds.h := Views.undefined;
+			Views.HandlePropMsg(v, bounds);
+			v.context.SetSize(bounds.w, bounds.h)
+		END
+	END RecalcFocusSize;
+
+	PROCEDURE RecalcAllSizes*;
+		VAR w: Windows.Window;
+	BEGIN
+		w := Windows.dir.First();
+		WHILE w # NIL DO
+			StdDialog.RecalcView(w.doc.ThisView());
+			w := Windows.dir.Next(w)
+		END
+	END RecalcAllSizes;
+	
+	PROCEDURE SetMode(opts: SET);
+		VAR 
+			c: Containers.Controller; v: Views.View; 
+			gm: Containers.GetOpts; sm: Containers.SetOpts;
+			w: Windows.Window;
+	BEGIN
+		c := Containers.Focus();
+		gm.valid := {};
+		IF (c # NIL) & (c.Singleton() # NIL) THEN
+			v := c.Singleton();
+			Views.HandlePropMsg(v, gm);
+		END;
+		IF gm.valid = {} THEN
+			w := Windows.dir.Focus(Controllers.path);
+			IF (w # NIL) & (w.doc.ThisView() IS Containers.View) THEN v := w.doc.ThisView() ELSE v := NIL END
+		END;
+		IF v # NIL THEN
+			sm.valid := {Containers.noSelection, Containers.noFocus, Containers.noCaret};
+			sm.opts := opts;
+			Views.HandlePropMsg(v, sm);
+		END;
+	END SetMode;
+	
+	PROCEDURE GetMode(OUT found: BOOLEAN; OUT opts: SET);
+		VAR c: Containers.Controller; gm: Containers.GetOpts; w: Windows.Window;
+	BEGIN
+		c := Containers.Focus();
+		gm.valid := {};
+		IF (c # NIL) & (c.Singleton() # NIL) THEN
+			Views.HandlePropMsg(c.Singleton(), gm);
+		END;
+		IF gm.valid = {}  THEN
+			w := Windows.dir.Focus(Controllers.path);
+			IF (w # NIL) & (w.doc.ThisView() IS Containers.View) THEN
+				Views.HandlePropMsg(w.doc.ThisView(), gm);
+			END
+		END;
+		found := gm.valid # {};
+		opts := gm.opts
+	END GetMode;
+	
+	PROCEDURE SetMaskMode*;
+	(* Guard: SetMaskGuard *)
+	BEGIN
+		SetMode({Containers.noSelection, Containers.noCaret})
+	END SetMaskMode;
+
+	PROCEDURE SetEditMode*;
+	(* Guard: SetEditGuard *)
+	BEGIN
+		SetMode({})
+	END SetEditMode;
+
+	PROCEDURE SetLayoutMode*;
+	(* Guard: SetLayoutGuard *)
+	BEGIN
+		SetMode({Containers.noFocus})
+	END SetLayoutMode;
+
+	PROCEDURE SetBrowserMode*;
+	(* Guard: SetBrowserGuard *)
+	BEGIN
+		SetMode({Containers.noCaret})
+	END SetBrowserMode;
+
+
+	(* standard guards *)
+	
+	PROCEDURE ToggleNoFocusGuard* (VAR par: Dialog.Par);
+		VAR c: Containers.Controller; v: Views.View;
+	BEGIN
+		c := ActFocus();
+		IF c # NIL THEN
+			v := c.ThisView();
+			IF ~((v IS Documents.Document) OR (Containers.noSelection IN c.opts)) THEN
+				IF Containers.noFocus IN c.opts THEN par.label := "#System:AllowFocus"
+				ELSE par.label := "#System:PreventFocus"
+				END
+			ELSE par.disabled := TRUE
+			END
+		ELSE par.disabled := TRUE
+		END
+	END ToggleNoFocusGuard;
+
+	PROCEDURE ReadOnlyGuard* (VAR par: Dialog.Par);
+	BEGIN
+		par.readOnly := TRUE
+	END ReadOnlyGuard;
+	
+	PROCEDURE WindowGuard* (VAR par: Dialog.Par);
+		VAR w: Windows.Window;
+	BEGIN
+		w := Windows.dir.First();
+		IF w = NIL THEN par.disabled := TRUE END
+	END WindowGuard;
+
+	PROCEDURE ModelViewGuard* (VAR par: Dialog.Par);
+		VAR w: Windows.Window;
+	BEGIN
+		w := Windows.dir.Focus(Controllers.frontPath);
+		par.disabled := (w = NIL) OR (w.doc.ThisView().ThisModel() = NIL)
+	END ModelViewGuard;
+
+	PROCEDURE SetMaskModeGuard* (VAR par: Dialog.Par);
+		CONST mode = {Containers.noSelection, Containers.noFocus, Containers.noCaret};
+		VAR opts: SET; found: BOOLEAN;
+	BEGIN
+		GetMode(found, opts);
+		IF found THEN
+			par.checked := opts * mode = {Containers.noSelection, Containers.noCaret}
+		ELSE
+			par.disabled := TRUE
+		END
+	END SetMaskModeGuard;
+
+	PROCEDURE SetEditModeGuard* (VAR par: Dialog.Par);
+		CONST mode = {Containers.noSelection, Containers.noFocus, Containers.noCaret};
+		VAR opts: SET; found: BOOLEAN;
+	BEGIN
+		GetMode(found, opts);
+		IF found THEN
+			par.checked := opts * mode = {}
+		ELSE
+			par.disabled := TRUE
+		END
+	END SetEditModeGuard;
+
+	PROCEDURE SetLayoutModeGuard* (VAR par: Dialog.Par);
+		CONST mode = {Containers.noSelection, Containers.noFocus, Containers.noCaret};
+		VAR opts: SET; found: BOOLEAN;
+	BEGIN
+		GetMode(found, opts);
+		IF found THEN
+			par.checked := opts * mode = {Containers.noFocus}
+		ELSE
+			par.disabled := TRUE
+		END
+	END SetLayoutModeGuard;
+
+	PROCEDURE SetBrowserModeGuard* (VAR par: Dialog.Par);
+		CONST mode = {Containers.noSelection, Containers.noFocus, Containers.noCaret};
+		VAR opts: SET; found: BOOLEAN;
+	BEGIN
+		GetMode(found, opts);
+		IF found THEN
+			par.checked := opts * mode = {Containers.noCaret}
+		ELSE
+			par.disabled := TRUE
+		END
+	END SetBrowserModeGuard;
+
+	PROCEDURE SelectionGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ops.valid * {Controllers.cut, Controllers.copy} = {} THEN par.disabled := TRUE END
+	END SelectionGuard;
+
+	PROCEDURE SingletonGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ops.singleton = NIL THEN par.disabled := TRUE END
+	END SingletonGuard;
+	
+	PROCEDURE SelectAllGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ~ops.selectable THEN par.disabled := TRUE END
+	END SelectAllGuard;
+
+	PROCEDURE CaretGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ops.valid * {Controllers.pasteChar .. Controllers.paste} = {} THEN par.disabled := TRUE END
+	END CaretGuard;
+
+	PROCEDURE PasteCharGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ~(Controllers.pasteChar IN ops.valid) THEN par.disabled := TRUE END
+	END PasteCharGuard;
+
+	PROCEDURE PasteLCharGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ~(Controllers.pasteChar IN ops.valid) THEN par.disabled := TRUE END
+	END PasteLCharGuard;
+
+	PROCEDURE PasteViewGuard* (VAR par: Dialog.Par);
+		VAR ops: Controllers.PollOpsMsg;
+	BEGIN
+		Controllers.PollOps(ops);
+		IF ~(Controllers.paste IN ops.valid) THEN par.disabled := TRUE END
+	END PasteViewGuard;
+	
+	PROCEDURE ContainerGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF Containers.Focus() = NIL THEN par.disabled := TRUE END
+	END ContainerGuard;
+	
+	PROCEDURE UndoGuard* (VAR par: Dialog.Par);
+		VAR f: Windows.Window; opName: Stores.OpName;
+	BEGIN
+		Dialog.MapString("#System:Undo", par.label);
+		f := Windows.dir.Focus(Controllers.frontPath);
+		IF (f # NIL) & f.seq.CanUndo() THEN
+			f.seq.GetUndoName(opName);
+			Dialog.MapString(opName, opName);
+			Append(par.label, " ");
+			Append(par.label, opName)
+		ELSE
+			par.disabled := TRUE
+		END
+	END UndoGuard;
+
+	PROCEDURE RedoGuard* (VAR par: Dialog.Par);
+		VAR f: Windows.Window; opName: Stores.OpName;
+	BEGIN
+		Dialog.MapString("#System:Redo", par.label);
+		f := Windows.dir.Focus(Controllers.frontPath);
+		IF (f # NIL) & f.seq.CanRedo() THEN
+			f.seq.GetRedoName(opName);
+			Dialog.MapString(opName, opName);
+			Append(par.label, " ");
+			Append(par.label, opName)
+		ELSE
+			par.disabled := TRUE
+		END
+	END RedoGuard;
+	
+	PROCEDURE PlainGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF props.known * {Properties.style, Properties.weight} # {} THEN
+			par.checked := (Properties.style IN props.valid)
+				& (props.style.val = {}) & ({Fonts.italic, Fonts.underline, Fonts.strikeout} - props.style.mask = {})
+				& (Properties.weight IN props.valid) & (props.weight = Fonts.normal)
+		ELSE
+			par.disabled := TRUE
+		END
+	END PlainGuard;
+
+	PROCEDURE BoldGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.weight IN props.known THEN
+			par.checked := (Properties.weight IN props.valid) & (props.weight = Fonts.bold)
+		ELSE
+			par.disabled := TRUE
+		END
+	END BoldGuard;
+
+	PROCEDURE ItalicGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.style IN props.known THEN
+			par.checked := (Properties.style IN props.valid) & (Fonts.italic IN props.style.val)
+		ELSE
+			par.disabled := TRUE
+		END
+	END ItalicGuard;
+
+	PROCEDURE UnderlineGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.style IN props.known THEN
+			par.checked := (Properties.style IN props.valid) & (Fonts.underline IN props.style.val)
+		ELSE
+			par.disabled := TRUE
+		END
+	END UnderlineGuard;
+
+	PROCEDURE StrikeoutGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.style IN props.known THEN
+			par.checked := (Properties.style IN props.valid) & (Fonts.strikeout IN props.style.val)
+		ELSE
+			par.disabled := TRUE
+		END
+	END StrikeoutGuard;
+
+	PROCEDURE SizeGuard* (size: INTEGER; VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.size IN props.known THEN
+			par.checked := (Properties.size IN props.valid) & (size = props.size DIV Ports.point)
+		ELSE
+			par.disabled := TRUE
+		END
+	END SizeGuard;
+
+	PROCEDURE ColorGuard* (color: INTEGER; VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.color IN props.known THEN
+			par.checked := (Properties.color IN props.valid) & (color = props.color.val)
+		ELSE
+			par.disabled := TRUE
+		END
+	END ColorGuard;
+	
+	PROCEDURE DefaultFontGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF Properties.typeface IN props.known THEN
+			par.checked := (Properties.typeface IN props.valid) & (props.typeface = Fonts.default)
+		ELSE
+			par.disabled := TRUE
+		END
+	END DefaultFontGuard;
+
+	PROCEDURE TypefaceGuard* (VAR par: Dialog.Par);
+		VAR props: Properties.StdProp;
+	BEGIN
+		props := StdProp();
+		IF ~(Properties.typeface IN props.known) THEN par.disabled := TRUE END
+	END TypefaceGuard;
+
+	
+	(* standard notifiers *)
+	
+	PROCEDURE DefaultOnDoubleClick* (op, from, to: INTEGER);
+		VAR  msg: Controllers.EditMsg; c: Containers.Controller;
+	BEGIN
+		IF (op = Dialog.pressed) & (from = 1) THEN
+			Controllers.SetCurrentPath(Controllers.frontPath);
+			c := Containers.Focus();
+			Controllers.ResetCurrentPath;
+			IF {Containers.noSelection, Containers.noCaret} - c.opts = {} THEN
+				msg.op := Controllers.pasteChar;
+				msg.char := 0DX; msg.modifiers := {};
+				Controllers.ForwardVia(Controllers.frontPath, msg)
+			END
+		END
+	END DefaultOnDoubleClick;
+
+
+	PROCEDURE Init;
+	BEGIN
+		allocator := defaultAllocator;
+		propEra := -1
+	END Init;
+
+BEGIN
+	Init
+END StdCmds.

+ 682 - 0
BlackBox/Std/Mod/Coder.txt

@@ -0,0 +1,682 @@
+MODULE StdCoder;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Coder.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Kernel, Files, Converters, Stores, Views, Controllers, Dialog, Documents, Windows,
+		TextModels, TextViews, TextControllers, TextMappers,
+		StdCmds;
+
+	CONST
+		N = 16384;
+		LineLength = 74;
+		OldVersion = 0; ThisVersion = 1;
+		Tag = "StdCoder.Decode";	(* first letter of Tag must not to appear within Tag again *)
+		Separator = "/";
+		View = 1; File = 2; List = 3;
+
+	TYPE
+		FileList = POINTER TO RECORD
+			next: FileList;
+			file: Files.File;
+			type: Files.Type;
+			name:Dialog.String
+		END;
+
+		ParList* = RECORD
+			list*: Dialog.Selection;
+			storeAs*: Dialog.String;
+			files: FileList
+		END;
+
+	VAR
+		par*: ParList;
+		code: ARRAY 64 OF CHAR;
+		revCode: ARRAY 256 OF BYTE;
+		table: ARRAY N OF BYTE;
+		stdDocuType: Files.Type;
+
+	PROCEDURE NofSelections(IN list: Dialog.Selection): INTEGER;
+		VAR i, n: INTEGER;
+	BEGIN
+		i := 0; n := 0;
+		WHILE i # list.len DO
+			IF list.In(i) THEN INC(n) END;
+			INC(i)
+		END;
+		RETURN n
+	END NofSelections;
+
+	PROCEDURE ShowError(n: INTEGER; par: ARRAY OF CHAR);
+	BEGIN
+		Dialog.Beep;
+		CASE n OF
+		 1: Dialog.ShowParamMsg("#Std:bad characters", par, "", "")
+		| 2: Dialog.ShowParamMsg("#Std:checksum error", par, "", "")
+		| 3: Dialog.ShowParamMsg("#Std:incompatible version", par, "", "")
+		| 4: Dialog.ShowParamMsg("#Std:filing error", par, "", "")
+		| 5: Dialog.ShowParamMsg("#Std:directory ^0 not found", par, "", "")
+		| 6: Dialog.ShowParamMsg("#Std:file ^0 not found", par, "", "")
+		| 7: Dialog.ShowParamMsg("#Std:illegal path", par, "", "")
+		| 8: Dialog.ShowParamMsg("#Std:no tag", par, "", "")
+		| 9: Dialog.ShowParamMsg("#Std:disk write protected", par, "", "")
+		| 10: Dialog.ShowParamMsg("#Std:io error", par, "", "")
+		END
+	END ShowError;
+
+	PROCEDURE ShowSizeMsg(x: INTEGER);
+		VAR i, j: INTEGER; ch: CHAR; s: ARRAY 20 OF CHAR;
+	BEGIN
+		ASSERT(x >= 0, 20);
+		i := 0;
+		REPEAT s[i] := CHR(ORD("0") + x MOD 10); INC(i); x := x DIV 10 UNTIL x = 0;
+		s[i] := 0X;
+		DEC(i); j := 0;
+		WHILE j < i DO ch := s[j]; s[j] := s[i]; s[i] := ch; INC(j); DEC(i) END;
+		Dialog.ShowParamStatus("#Std:^0 characters coded", s, "", "")
+	END ShowSizeMsg;
+
+	PROCEDURE Write(dest: TextModels.Writer; x: INTEGER; VAR n: INTEGER);
+	BEGIN
+		dest.WriteChar(code[x]); INC(n);
+		IF n = LineLength THEN dest.WriteChar(0DX); dest.WriteChar(" "); n := 0 END
+	END Write;
+
+	PROCEDURE WriteHeader(dest: TextModels.Writer; VAR n: INTEGER;
+		name: ARRAY OF CHAR; type: BYTE
+	);
+		VAR byte, bit, i: INTEGER; ch: CHAR; tag: ARRAY 16 OF CHAR;
+	BEGIN
+		tag := Tag; i := 0; ch := tag[0];
+		WHILE ch # 0X DO dest.WriteChar(ch); INC(n); INC(i); ch := tag[i] END;
+		dest.WriteChar(" "); INC(n);
+		bit := 0; byte := 0; i := 0;
+		REPEAT
+			ch := name[i]; INC(byte, ASH(ORD(ch), bit)); INC(bit, 8);
+			WHILE bit >= 6 DO Write(dest, byte MOD 64, n); byte := byte DIV 64; DEC(bit, 6) END;
+			INC(i)
+		UNTIL ch = 0X;
+		IF bit # 0 THEN Write(dest, byte, n) END;
+		Write(dest, ThisVersion, n); Write(dest, type, n)
+	END WriteHeader;
+
+	PROCEDURE WriteFileType(dest: TextModels.Writer; VAR n: INTEGER; t: Files.Type);
+		VAR byte, bit, i: INTEGER; ch: CHAR;
+	BEGIN
+		IF t = Kernel.docType THEN t := stdDocuType END;
+		bit := 0; byte := 0; i := 0; dest.WriteChar(" ");
+		REPEAT
+			ch := t[i]; INC(byte, ASH(ORD(ch), bit)); INC(bit, 8);
+			WHILE bit >= 6 DO Write(dest, byte MOD 64, n); byte := byte DIV 64; DEC(bit, 6) END;
+			INC(i)
+		UNTIL ch = 0X;
+		IF bit # 0 THEN Write(dest, byte, n) END
+	END WriteFileType;
+
+	PROCEDURE WriteFile(dest: TextModels.Writer; VAR n: INTEGER; f: Files.File);
+		VAR hash, byte, bit, i, j, sum, len: INTEGER; src: Files.Reader; b: BYTE;
+	BEGIN
+		len := f.Length(); j := len; i := 6;
+		WHILE i # 0 DO Write(dest, j MOD 64, n); j := j DIV 64; DEC(i) END;
+		i := 0;
+		REPEAT table[i] := 0; INC(i) UNTIL i = N;
+		hash := 0; bit := 0; byte := 0; sum := 0; src := f.NewReader(NIL);
+		WHILE len # 0 DO
+			src.ReadByte(b); DEC(len);
+			sum := (sum + b MOD 256) MOD (16 * 1024);
+			IF table[hash] = b THEN INC(bit)	(* 0 bit for correct prediction *)
+			ELSE	(* Incorrect prediction -> 1'xxxx'xxxx bits *)
+				table[hash] := b; INC(byte, ASH(1, bit)); INC(bit);
+				INC(byte, ASH(b MOD 256, bit)); INC(bit, 8)
+			END;
+			WHILE bit >= 6 DO Write(dest, byte MOD 64, n); byte := byte DIV 64; DEC(bit, 6) END;
+			hash := (16 * hash + b MOD 256) MOD N
+		END;
+		IF bit # 0 THEN Write(dest, byte, n) END;
+		i := 6;
+		WHILE i # 0 DO Write(dest, sum MOD 64, n); sum := sum DIV 64; DEC(i) END;
+		IF n # 0 THEN dest.WriteChar(0DX); n := 0 END
+	END WriteFile;
+
+	PROCEDURE Read(src: TextModels.Reader; VAR x: INTEGER; VAR res: INTEGER);
+		VAR ch: CHAR;
+	BEGIN
+		IF res = 0 THEN
+			REPEAT src.ReadChar(ch); x := revCode[ORD(ch)] UNTIL (x >= 0) OR src.eot;
+			IF src.eot THEN res := 1 END
+		END;
+		IF res # 0 THEN x := 0 END
+	END Read;
+
+	PROCEDURE ReadHeader(src: TextModels.Reader; VAR res: INTEGER;
+		VAR name: ARRAY OF CHAR; VAR type: BYTE
+	);
+		VAR x, bit, i, j: INTEGER; ch: CHAR; tag: ARRAY 16 OF CHAR;
+	BEGIN
+		tag := Tag; i := 0;
+		WHILE ~src.eot & (tag[i] # 0X) DO
+			src.ReadChar(ch);
+			IF ch = tag[i] THEN INC(i) ELSIF ch = tag[0] THEN i := 1 ELSE i := 0 END
+		END;
+		IF ~src.eot THEN
+			res := 0; i := 0; bit := 0; x := 0;
+			REPEAT
+				WHILE (res = 0) & (bit < 8) DO Read(src, j, res); INC(x, ASH(j, bit)); INC(bit, 6) END;
+				IF res = 0 THEN
+					ch := CHR(x MOD 256); x := x DIV 256; DEC(bit, 8); name[i] := ch; INC(i)
+				END
+			UNTIL (res # 0) OR (ch = 0X);
+			Read(src, j, res);
+			IF res = 0 THEN
+				IF (j = ThisVersion) OR (j = OldVersion) THEN
+					Read(src, j, res); type := SHORT(SHORT(j))
+				ELSE res := 3
+				END
+			END
+		ELSE res := 8
+		END
+	END ReadHeader;
+
+	PROCEDURE ReadFileType(src: TextModels.Reader; VAR res: INTEGER; VAR ftype: Files.Type);
+		VAR x, bit, i, j: INTEGER; ch: CHAR;
+	BEGIN
+		res := 0; i := 0; bit := 0; x := 0;
+		REPEAT
+			WHILE (res = 0) & (bit < 8) DO Read(src, j, res); INC(x, ASH(j, bit)); INC(bit, 6) END;
+			IF res = 0 THEN ch := CHR(x MOD 256); x := x DIV 256; DEC(bit, 8); ftype[i] := ch; INC(i) END
+		UNTIL (res # 0) OR (ch = 0X);
+		IF ftype = stdDocuType THEN ftype := Kernel.docType END
+	END ReadFileType;
+
+	PROCEDURE ReadFile(src: TextModels.Reader; VAR res: INTEGER; f: Files.File);
+		VAR hash, x, bit, i, j, len, sum, s: INTEGER; byte: BYTE; dest: Files.Writer;
+	BEGIN
+		res := 0; i := 0; len := 0;
+		REPEAT Read(src, x, res); len := len + ASH(x, 6 * i); INC(i) UNTIL (res # 0) OR (i = 6);
+		i := 0;
+		REPEAT table[i] := 0; INC(i) UNTIL i = N;
+		bit := 0; hash := 0; sum := 0; dest := f.NewWriter(NIL);
+		WHILE (res = 0) & (len # 0) DO
+			IF bit = 0 THEN Read(src, x, res); bit := 6 END;
+			IF ODD(x) THEN	(* Incorrect prediction -> 1'xxxx'xxxx *)
+				x := x DIV 2; DEC(bit);
+				WHILE (res = 0) & (bit < 8) DO Read(src, j, res); INC(x, ASH(j, bit)); INC(bit, 6) END;
+				i := x MOD 256;
+				IF i > MAX(BYTE) THEN i := i - 256 END;
+				byte := SHORT(SHORT(i)); x := x DIV 256; DEC(bit, 8);
+				table[hash] := byte
+			ELSE byte := table[hash]; x := x DIV 2; DEC(bit)	(* correct prediction *)
+			END;
+			hash := (16 * hash + byte MOD 256) MOD N;
+			dest.WriteByte(byte); sum := (sum + byte MOD 256) MOD (16 * 1024); DEC(len)
+		END;
+		IF res = 0 THEN
+			i := 0; s := 0;
+			REPEAT Read(src, x, res); s := s + ASH(x, 6 * i); INC(i) UNTIL (res # 0) OR (i = 6);
+			IF (res = 0) & (s # sum) THEN res := 2 END
+		END
+	END ReadFile;
+
+	PROCEDURE ShowText (t: TextModels.Model);
+		VAR l: INTEGER; v: Views.View; wr: TextMappers.Formatter; conv: Converters.Converter;
+	BEGIN
+		l := t.Length();
+		wr.ConnectTo(t); wr.SetPos(l); wr.WriteString(" --- end of encoding ---");
+		ShowSizeMsg(l);
+		v := TextViews.dir.New(t);
+		conv := Converters.list;
+		WHILE (conv # NIL) & (conv.imp # "HostTextConv.ImportText") DO conv := conv.next END;
+		Views.Open(v, NIL, "", conv);
+		Views.SetDirty(v)
+	END ShowText;
+
+	PROCEDURE EncodedView*(v: Views.View): TextModels.Model;
+		VAR n: INTEGER; f: Files.File; wrs: Stores.Writer; t: TextModels.Model; wr: TextModels.Writer;
+	BEGIN
+		f := Files.dir.Temp(); wrs.ConnectTo(f); Views.WriteView(wrs, v);
+		t := TextModels.dir.New(); wr := t.NewWriter(NIL);
+		n := 0; WriteHeader(wr, n, "", View); WriteFileType(wr, n, f.type); WriteFile(wr, n, f);
+		RETURN t
+	END EncodedView;
+
+	PROCEDURE EncodeDocument*;
+		VAR v: Views.View; w: Windows.Window;
+	BEGIN
+		w := Windows.dir.First();
+		IF w # NIL THEN
+			v := w.doc.OriginalView();
+			IF (v.context # NIL) & (v.context IS Documents.Context) THEN
+				v := v.context(Documents.Context).ThisDoc()
+			END;
+			IF v # NIL THEN ShowText(EncodedView(v)) END
+		END
+	END EncodeDocument;
+
+	PROCEDURE EncodeFocus*;
+		VAR v: Views.View;
+	BEGIN
+		v := Controllers.FocusView();
+		IF v # NIL THEN ShowText(EncodedView(v)) END
+	END EncodeFocus;
+
+	PROCEDURE EncodeSelection*;
+		VAR beg, end: INTEGER; t: TextModels.Model; c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c # NIL) & c.HasSelection() THEN
+			c.GetSelection(beg, end);
+			t := TextModels.CloneOf(c.text); t.InsertCopy(0, c.text, beg, end);
+			ShowText(EncodedView(TextViews.dir.New(t)))
+		END
+	END EncodeSelection;
+
+	PROCEDURE EncodeFile*;
+		VAR n: INTEGER; loc: Files.Locator; name: Files.Name; f: Files.File;
+			t: TextModels.Model; wr: TextModels.Writer;
+	BEGIN
+		Dialog.GetIntSpec("", loc, name);
+		IF loc # NIL THEN
+			f := Files.dir.Old(loc, name, TRUE);
+			IF f # NIL THEN
+				t := TextModels.dir.New(); wr := t.NewWriter(NIL);
+				n := 0; WriteHeader(wr, n, name, File); WriteFileType(wr, n, f.type); WriteFile(wr, n, f);
+				ShowText(t)
+			END
+		END
+	END EncodeFile;
+
+	PROCEDURE GetFile(VAR path: ARRAY OF CHAR; VAR loc: Files.Locator; VAR name: Files.Name);
+		VAR i, j: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; ch := path[0]; loc := Files.dir.This("");
+		WHILE (ch # 0X) & (loc # NIL) DO
+			j := 0;
+			WHILE (ch # 0X) & (ch # Separator) DO name[j] := ch; INC(j); INC(i); ch := path[i] END;
+			name[j] := 0X;
+			IF ch = Separator THEN loc := loc.This(name); INC(i); ch := path[i] END;
+			IF loc.res # 0 THEN loc := NIL END
+		END;
+		path[i] := 0X
+	END GetFile;
+
+	PROCEDURE ReadPath(rd: TextModels.Reader; VAR path: ARRAY OF CHAR; VAR len: INTEGER);
+		VAR i, l: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; l := LEN(path) - 1;
+		REPEAT rd.ReadChar(ch) UNTIL rd.eot OR (ch > " ");
+		WHILE ~rd.eot & (ch > " ") & (i < l) DO path[i] := ch; INC(i); rd.ReadChar(ch) END;
+		path[i] := 0X; len := i
+	END ReadPath;
+	
+	PROCEDURE WriteString(w: Files.Writer; IN str: ARRAY OF CHAR; len: INTEGER);
+		VAR i: INTEGER;
+	BEGIN
+		i := 0;
+		WHILE i < len DO
+			IF ORD(str[i]) > MAX(BYTE) THEN w.WriteByte(SHORT(SHORT(ORD(str[i]) - 256)))
+			ELSE w.WriteByte(SHORT(SHORT(ORD(str[i]))))
+			END;
+			INC(i)
+		END
+	END WriteString;
+
+	PROCEDURE EncodeFileList*;
+		TYPE
+			FileList = POINTER TO RECORD
+				next: FileList;
+				f: Files.File
+			END;
+		VAR
+			beg, end, i, j, n: INTEGER; err: BOOLEAN;
+			files, last: FileList;
+			list, f: Files.File; w: Files.Writer; loc: Files.Locator;
+			rd: TextModels.Reader; wr: TextModels.Writer; t: TextModels.Model;
+			c: TextControllers.Controller;
+			name: Files.Name; path, next: ARRAY 2048 OF CHAR;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c # NIL) & c.HasSelection() THEN c.GetSelection(beg, end);
+			rd := c.text.NewReader(NIL); rd.SetPos(beg); err := FALSE;
+			list := Files.dir.Temp(); w := list.NewWriter(NIL); files := NIL; last := NIL;
+			ReadPath(rd, path, i);
+			WHILE (path # "") & (rd.Pos() - i < end) & ~err DO
+				GetFile(path, loc, name);
+				IF loc # NIL THEN
+					f := Files.dir.Old(loc, name, TRUE); err := f = NIL;
+					IF ~err THEN
+						IF last = NIL THEN NEW(last); files := last ELSE NEW(last.next); last := last.next END;
+						last.f := f;
+						ReadPath(rd, next, j);
+						IF (next = "=>") & (rd.Pos() - j < end) THEN
+							ReadPath(rd, next, j);
+							IF next # "" THEN WriteString(w, next, j + 1); ReadPath(rd, next, j)
+							ELSE err := TRUE
+							END
+						ELSE WriteString(w, path, i + 1)
+						END;
+						path := next; i := j
+					END
+				ELSE err := TRUE
+				END
+			END;
+			IF ~err & (files # NIL) THEN
+				t := TextModels.dir.New(); wr := t.NewWriter(NIL);
+				n := 0; WriteHeader(wr, n, "", List);
+				WriteFileType(wr, n, list.type); WriteFile(wr, n, list);
+				WHILE files # NIL DO
+					WriteFileType(wr, n, files.f.type); WriteFile(wr, n, files.f); files := files.next
+				END;
+				ShowText(t)
+			ELSIF err THEN
+				IF path = "" THEN ShowError(7, path)
+				ELSIF loc # NIL THEN ShowError(6, path)
+				ELSE ShowError(5, path)
+				END
+			END
+		END
+	END EncodeFileList;
+
+	PROCEDURE DecodeView(rd: TextModels.Reader; name: Files.Name);
+		VAR res: INTEGER; f: Files.File; ftype: Files.Type; rds: Stores.Reader; v: Views.View;
+	BEGIN
+		ReadFileType(rd, res, ftype);
+		IF res = 0 THEN
+			f := Files.dir.Temp(); ReadFile(rd, res, f);
+			IF res = 0 THEN
+				rds.ConnectTo(f); Views.ReadView(rds, v); Views.Open(v, NIL, name, NIL);
+				Views.SetDirty(v)
+			ELSE ShowError(res, "")
+			END
+		ELSE ShowError(res, "")
+		END
+	END DecodeView;
+
+	PROCEDURE DecodeFile(rd: TextModels.Reader; name: Files.Name);
+		VAR res: INTEGER; ftype: Files.Type; loc: Files.Locator; f: Files.File;
+	BEGIN
+		ReadFileType(rd, res, ftype);
+		IF res = 0 THEN
+			Dialog.GetExtSpec(name, ftype, loc, name);
+			IF loc # NIL THEN
+				f := Files.dir.New(loc, Files.ask);
+				IF f # NIL THEN
+					ReadFile(rd, res, f);
+					IF res = 0 THEN
+						f.Register(name, ftype, Files.ask, res);
+						IF res # 0 THEN ShowError(4, "") END
+					ELSE ShowError(res, "")
+					END
+				ELSIF loc.res = 4 THEN ShowError(9, "")
+				ELSIF loc.res = 5 THEN ShowError(10, "")
+				END
+			END
+		ELSE ShowError(res, "")
+		END
+	END DecodeFile;
+
+	PROCEDURE DecodeFileList (rd: TextModels.Reader; VAR files: FileList; VAR len, res: INTEGER);
+		VAR i, n: INTEGER; b: BYTE; p: FileList;
+			ftype: Files.Type; f: Files.File; frd: Files.Reader; path: Dialog.String;
+	BEGIN
+		ReadFileType(rd, res, ftype);
+		IF res = 0 THEN
+			f := Files.dir.Temp(); ReadFile(rd, res, f);
+			IF res = 0 THEN
+				files := NIL; p := NIL; n := 0;
+				frd := f.NewReader(NIL); frd.ReadByte(b);
+				WHILE ~frd.eof & (res = 0) DO
+					INC(n); i := 0;
+					WHILE ~frd.eof & (b # 0) DO path[i] := CHR(b MOD 256); INC(i); frd.ReadByte(b) END;
+					IF (i > 4) & (path[i - 4] = ".") & (CAP(path[i - 3]) = "O")
+						& (CAP(path[i - 2]) = "D") & (CAP(path[i - 1]) = "C")
+					THEN path[i - 4] := 0X
+					ELSE path[i] := 0X
+					END;
+					IF ~frd.eof THEN
+						IF p = NIL THEN NEW(p); files := p ELSE NEW(p.next); p := p.next END;
+						p.name := path;
+						frd.ReadByte(b)
+					ELSE res := 1
+					END
+				END;
+				p := files; len := n;
+				WHILE (res = 0) & (p # NIL) DO
+					ReadFileType(rd, res, p.type);
+					IF res = 0 THEN p.file := Files.dir.Temp(); ReadFile(rd, res, p.file) END;
+					p := p.next
+				END
+			END
+		END
+	END DecodeFileList;
+
+	PROCEDURE OpenDialog(files: FileList; len: INTEGER);
+		VAR i: INTEGER; p: FileList;
+	BEGIN
+		par.files := files; par.list.SetLen(len);
+		p := files; i := 0;
+		WHILE p # NIL DO par.list.SetItem(i, p.name); INC(i); p := p.next END;
+		par.storeAs := "";
+		Dialog.Update(par); Dialog.UpdateList(par.list);
+		StdCmds.OpenAuxDialog("Std/Rsrc/Coder", "Decode")
+	END OpenDialog;
+
+	PROCEDURE CloseDialog*;
+	BEGIN
+		par.files := NIL; par.list.SetLen(0); par.storeAs := "";
+		Dialog.UpdateList(par.list); Dialog.Update(par)
+	END CloseDialog;
+
+	PROCEDURE Select*(op, from, to: INTEGER);
+		VAR p: FileList; i: INTEGER;
+	BEGIN
+		IF (op = Dialog.included) OR (op = Dialog.excluded) OR (op = Dialog.set) THEN
+			IF NofSelections(par.list) = 1 THEN
+				i := 0; p := par.files;
+				WHILE ~par.list.In(i) DO INC(i); p := p.next END;
+				par.storeAs := p.name
+			ELSE par.storeAs := ""
+			END;
+			Dialog.Update(par)
+		END
+	END Select;
+
+	PROCEDURE CopyFile(from: Files.File; loc: Files.Locator; name: Files.Name; type: Files.Type);
+		CONST BufSize = 4096;
+		VAR res, k, l: INTEGER; f: Files.File; r: Files.Reader; w: Files.Writer;
+			buf: ARRAY BufSize OF BYTE;
+	BEGIN
+		f := Files.dir.New(loc, Files.ask);
+		IF f # NIL THEN
+			r := from.NewReader(NIL); w := f.NewWriter(NIL); l := from.Length();
+			WHILE l # 0 DO
+				IF l <= BufSize THEN k := l ELSE k := BufSize END;
+				r.ReadBytes(buf, 0, k); w.WriteBytes(buf, 0, k);
+				l := l - k
+			END;
+			f.Register(name, type, Files.ask, res);
+			IF res # 0 THEN ShowError(4, "") END
+		ELSIF loc.res = 4 THEN ShowError(9, "")
+		ELSIF loc.res = 5 THEN ShowError(10, "")
+		END
+	END CopyFile;
+
+	PROCEDURE StoreSelection*;
+		VAR i, n: INTEGER; p: FileList; loc: Files.Locator; name: Files.Name;
+	BEGIN
+		n := NofSelections(par.list);
+		IF n > 1 THEN
+			i := 0; p := par.files;
+			WHILE n # 0 DO
+				WHILE ~par.list.In(i) DO INC(i); p := p.next END;
+				GetFile(p.name, loc, name); CopyFile(p.file, loc, name, p.type);
+				DEC(n); INC(i); p := p.next
+			END
+		ELSIF (n = 1) & (par.storeAs # "") THEN
+			i := 0; p := par.files;
+			WHILE ~par.list.In(i) DO INC(i); p := p.next END;
+			GetFile(par.storeAs, loc, name); CopyFile(p.file, loc, name, p.type)
+		END
+	END StoreSelection;
+
+	PROCEDURE StoreSelectionGuard*(VAR p: Dialog.Par);
+		VAR n: INTEGER;
+	BEGIN
+		n := NofSelections(par.list);
+		p.disabled := (n = 0) OR ((n = 1) & (par.storeAs = ""))
+	END StoreSelectionGuard;
+
+	PROCEDURE StoreSingle*;
+		VAR i: INTEGER; p: FileList; loc: Files.Locator; name: Files.Name;
+	BEGIN
+		IF NofSelections(par.list) = 1 THEN
+			i := 0; p := par.files;
+			WHILE ~par.list.In(i) DO INC(i); p := p.next END;
+			GetFile(p.name, loc, name);
+			Dialog.GetExtSpec(name, p.type, loc, name);
+			IF loc # NIL THEN CopyFile(p.file, loc, name, p.type) END
+		END
+	END StoreSingle;
+
+	PROCEDURE StoreSingleGuard*(VAR p: Dialog.Par);
+	BEGIN
+		p.disabled := NofSelections(par.list) # 1
+	END StoreSingleGuard;
+
+	PROCEDURE StoreAllFiles(files: FileList);
+		VAR loc: Files.Locator; name: Files.Name;
+	BEGIN
+		WHILE files # NIL DO
+			GetFile(files.name, loc, name); CopyFile(files.file, loc, name, files.type); files := files.next
+		END
+	END StoreAllFiles;
+
+	PROCEDURE StoreAll*;
+	BEGIN
+		StoreAllFiles(par.files)
+	END StoreAll;
+
+	PROCEDURE DecodeAllFromText*(text: TextModels.Model; beg: INTEGER; ask: BOOLEAN);
+		VAR res, i: INTEGER; type: BYTE; name: Files.Name; rd: TextModels.Reader; files: FileList;
+	BEGIN
+		CloseDialog;
+		rd := text.NewReader(NIL); rd.SetPos(beg);
+		ReadHeader(rd, res, name, type);
+		i := 0;
+		WHILE name[i] # 0X DO INC(i) END;
+		IF (i > 4) & (name[i - 4] = ".") & (CAP(name[i - 3]) = "O")
+			& (CAP(name[i - 2]) = "D") & (CAP(name[i - 1]) = "C")
+		THEN name[i - 4] := 0X
+		END;
+		IF res = 0 THEN
+			IF type = View THEN DecodeView(rd, name)
+			ELSIF type = File THEN DecodeFile(rd, name)
+			ELSIF type = List THEN
+				DecodeFileList(rd, files, i, res);
+				IF res = 0 THEN
+					IF ask THEN OpenDialog(files, i) ELSE StoreAllFiles(files) END
+				ELSE ShowError(res, "")
+				END
+			ELSE ShowError(3, "")
+			END
+		ELSE ShowError(res, "")
+		END
+	END DecodeAllFromText;
+
+	PROCEDURE Decode*;
+		VAR beg, end: INTEGER; c: TextControllers.Controller;
+	BEGIN
+		CloseDialog;
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF c.HasSelection() THEN c.GetSelection(beg, end) ELSE beg := 0 END;
+			DecodeAllFromText(c.text, beg, TRUE)
+		END
+	END Decode;
+
+	PROCEDURE ListFiles(rd: TextModels.Reader; VAR wr: TextMappers.Formatter);
+		VAR i, n, res: INTEGER; b: BYTE;
+			ftype: Files.Type; f: Files.File; frd: Files.Reader; path: Dialog.String;
+	BEGIN
+		ReadFileType(rd, res, ftype);
+		IF res = 0 THEN
+			f := Files.dir.Temp(); ReadFile(rd, res, f);
+			IF res = 0 THEN
+				n := 0;
+				frd := f.NewReader(NIL); frd.ReadByte(b);
+				WHILE ~frd.eof & (res = 0) DO
+					INC(n); i := 0;
+					WHILE ~frd.eof & (b # 0) DO path[i] := CHR(b MOD 256); INC(i); frd.ReadByte(b) END;
+					IF (i > 4) & (path[i - 4] = ".") & (CAP(path[i - 3]) = "O")
+						& (CAP(path[i - 2]) = "D") & (CAP(path[i - 1]) = "C")
+					THEN path[i - 4] := 0X
+					ELSE path[i] := 0X
+					END;
+					IF ~frd.eof THEN wr.WriteString(path); wr.WriteLn; frd.ReadByte(b) ELSE res := 1 END
+				END
+			ELSE ShowError(res, "")
+			END
+		ELSE ShowError(res, "")
+		END
+	END ListFiles;
+
+	PROCEDURE ListSingleton(type, name: ARRAY OF CHAR; VAR wr: TextMappers.Formatter);
+	BEGIN
+		wr.WriteString(type);
+		IF name # "" THEN wr.WriteString(": '"); wr.WriteString(name); wr.WriteChar("'") END;
+		wr.WriteLn
+	END ListSingleton;
+
+	PROCEDURE EncodedInText*(text: TextModels.Model; beg: INTEGER): TextModels.Model;
+		VAR res, i: INTEGER; type: BYTE; name: Files.Name;
+			rd: TextModels.Reader; report: TextModels.Model; wr: TextMappers.Formatter;
+	BEGIN
+		report := TextModels.dir.New(); wr.ConnectTo(report);
+		rd := text.NewReader(NIL); rd.SetPos(beg);
+		ReadHeader(rd, res, name, type);
+		i := 0;
+		WHILE name[i] # 0X DO INC(i) END;
+		IF (i > 4) & (name[i - 4] = ".") & (CAP(name[i - 3]) = "O")
+			& (CAP(name[i - 2]) = "D") & (CAP(name[i - 1]) = "C")
+		THEN name[i - 4] := 0X
+		END;
+		IF res = 0 THEN
+			IF type = View THEN ListSingleton("View", name, wr)
+			ELSIF type = File THEN ListSingleton("File", name, wr)
+			ELSIF type = List THEN ListFiles(rd, wr)
+			ELSE ShowError(3, "")
+			END
+		ELSE ShowError(res, "")
+		END;
+		RETURN report
+	END EncodedInText;
+
+	PROCEDURE ListEncodedMaterial*;
+		VAR beg, end: INTEGER; c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF c.HasSelection() THEN c.GetSelection(beg, end) ELSE beg := 0 END;
+			Views.OpenView(TextViews.dir.New(EncodedInText(c.text, beg)))
+		END
+	END ListEncodedMaterial;
+
+	PROCEDURE InitCodes;
+		VAR i: BYTE; j: INTEGER;
+	BEGIN
+		j := 0;
+		WHILE j # 256 DO revCode[j] := -1; INC(j) END;
+		code[0] := "."; revCode[ORD(".")] := 0; code[1] := ","; revCode[ORD(",")] := 1;
+		i := 2; j := ORD("0");
+		WHILE j <= ORD("9") DO code[i] := CHR(j); revCode[j] := i; INC(i); INC(j) END;
+		j := ORD("A");
+		WHILE j <= ORD("Z") DO code[i] := CHR(j); revCode[j] := i; INC(i); INC(j) END;
+		j := ORD("a");
+		WHILE j <= ORD("z") DO code[i] := CHR(j); revCode[j] := i; INC(i); INC(j) END;
+		ASSERT(i = 64, 60)
+	END InitCodes;
+
+BEGIN
+	InitCodes;
+	stdDocuType[0] := 3X; stdDocuType[1] := 3X; stdDocuType[2] := 3X; stdDocuType[3] := 0X
+END StdCoder.

+ 621 - 0
BlackBox/Std/Mod/Debug.txt

@@ -0,0 +1,621 @@
+MODULE StdDebug;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Debug.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT SYSTEM,
+		Kernel, Strings, Fonts, Services, Ports, Views, Properties, Dialog, Containers, StdFolds,
+		TextModels, TextMappers, TextViews, TextRulers;
+	
+	CONST
+		refViewSize = 9 * Ports.point;
+		
+		heap = 1; source = 2; module = 3; modules = 4;	(* RefView types *)
+		
+	TYPE
+		Name = Kernel.Name;
+
+		ArrayPtr = POINTER TO RECORD
+			last, t, first: INTEGER;	(* gc header *)
+			len: ARRAY 16 OF INTEGER	(* dynamic array length table *)
+		END;
+
+		RefView = POINTER TO RefViewDesc;
+
+		RefViewDesc = RECORD
+			type: SHORTINT;
+			command: SHORTINT;
+			back: RefView;
+			adr: INTEGER;
+			desc: Kernel.Type;
+			ptr: ArrayPtr;
+			name: Name
+		END;
+		
+		Action = POINTER TO RECORD (Services.Action)
+			text: TextModels.Model
+		END;
+		
+		Cluster = POINTER TO RECORD [untagged]	(* must correspond to Kernel.Cluster *)
+			size: INTEGER;
+			next: Cluster
+		END;
+		
+	
+	VAR
+		out: TextMappers.Formatter;
+		path: ARRAY 4 OF Ports.Point;
+		empty: Name;
+
+
+	PROCEDURE NewRuler (): TextRulers.Ruler;
+		CONST mm = Ports.mm;
+		VAR r: TextRulers.Ruler;
+	BEGIN
+		r := TextRulers.dir.New(NIL);
+		TextRulers.SetRight(r, 140 * mm);
+		TextRulers.AddTab(r, 4 * mm); TextRulers.AddTab(r, 34 * mm); TextRulers.AddTab(r, 80 * mm);
+		RETURN r
+	END NewRuler;
+
+	PROCEDURE OpenViewer (t: TextModels.Model; title: Views.Title; ruler:TextRulers.Ruler);
+		VAR v: TextViews.View; c: Containers.Controller;
+	BEGIN
+		Dialog.MapString(title, title);
+		v := TextViews.dir.New(t);
+		v.SetDefaults(ruler, TextViews.dir.defAttr);
+		c := v.ThisController();
+		IF c # NIL THEN
+			c.SetOpts(c.opts - {Containers.noFocus, Containers.noSelection} + {Containers.noCaret})
+		END;
+		Views.OpenAux(v, title)
+	END OpenViewer;
+	
+	PROCEDURE OpenFold (hidden: ARRAY OF CHAR);
+		VAR fold: StdFolds.Fold; t: TextModels.Model; w: TextMappers.Formatter;
+	BEGIN
+		Dialog.MapString(hidden, hidden);
+		t := TextModels.dir.New();
+		w.ConnectTo(t); w.WriteString(hidden);
+		fold := StdFolds.dir.New(StdFolds.expanded, "", t);
+		out.WriteView(fold)
+	END OpenFold;
+	
+	PROCEDURE CloseFold (collaps: BOOLEAN);
+		VAR fold: StdFolds.Fold; m: TextModels.Model;
+	BEGIN
+		fold := StdFolds.dir.New(StdFolds.expanded, "", NIL);
+		out.WriteView(fold);
+		IF collaps THEN fold.Flip(); m := out.rider.Base(); out.SetPos(m.Length()) END
+	END CloseFold;
+	
+	PROCEDURE WriteHex (n: INTEGER);
+	BEGIN
+		out.WriteIntForm(n, TextMappers.hexadecimal, 9, "0", TextMappers.showBase)
+	END WriteHex;
+	
+	PROCEDURE WriteString (adr, len, base: INTEGER; zterm, unicode: BOOLEAN);
+		CONST beg = 0; char = 1; code = 2;
+		VAR ch: CHAR; sc: SHORTCHAR; val, mode: INTEGER; str: ARRAY 16 OF CHAR;
+	BEGIN
+		mode := beg;
+		IF base = 2 THEN SYSTEM.GET(adr, ch); val := ORD(ch) ELSE SYSTEM.GET(adr, sc); val := ORD(sc) END;
+		IF zterm & (val = 0) THEN out.WriteSString('""')
+		ELSE
+			REPEAT
+				IF (val >= ORD(" ")) & (val < 7FH) OR (val > 0A0H) & (val < 100H) OR unicode & (val >= 100H) THEN
+					IF mode # char THEN
+						IF mode = code THEN out.WriteSString(", ") END;
+						out.WriteChar(22X); mode := char
+					END;
+					out.WriteChar(CHR(val))
+				ELSE
+					IF mode = char THEN out.WriteChar(22X) END;
+					IF mode # beg THEN out.WriteSString(", ") END;
+					mode := code; Strings.IntToStringForm(val, Strings.hexadecimal, 1, "0", FALSE, str);
+					IF str[0] > "9" THEN out.WriteChar("0") END;
+					out.WriteString(str); out.WriteChar("X")
+				END;
+				INC(adr, base); DEC(len);
+				IF base = 2 THEN SYSTEM.GET(adr, ch); val := ORD(ch) ELSE SYSTEM.GET(adr, sc); val := ORD(sc) END
+			UNTIL (len = 0) OR zterm & (val = 0)
+		END;
+		IF mode = char THEN out.WriteChar(22X) END
+	END WriteString;
+	
+	PROCEDURE OutString (s: ARRAY OF CHAR);
+		VAR str: Dialog.String;
+	BEGIN
+		Dialog.MapString(s, str);
+		out.WriteString(str)
+	END OutString;
+
+	(* -------------------  variable display ------------------- *)
+	
+	PROCEDURE FormOf (t: Kernel.Type): SHORTCHAR;
+	BEGIN
+		IF SYSTEM.VAL(INTEGER, t) DIV 256 = 0 THEN
+			RETURN SHORT(CHR(SYSTEM.VAL(INTEGER, t)))
+		ELSE
+			RETURN SHORT(CHR(16 + t.id MOD 4))
+		END
+	END FormOf;
+	
+	PROCEDURE LenOf (t: Kernel.Type; ptr: ArrayPtr): INTEGER;
+	BEGIN
+		IF t.size # 0 THEN RETURN t.size
+		ELSIF ptr # NIL THEN RETURN ptr.len[t.id DIV 16 MOD 16 - 1]
+		ELSE RETURN 0
+		END
+	END LenOf;
+	
+	PROCEDURE SizeOf (t: Kernel.Type; ptr: ArrayPtr): INTEGER;
+	BEGIN
+		CASE FormOf(t) OF
+		| 0BX: RETURN 0
+		| 1X, 2X, 4X: RETURN 1
+		| 3X, 5X: RETURN 2
+		| 8X, 0AX: RETURN 8
+		| 11X: RETURN t.size
+		| 12X: RETURN LenOf(t, ptr) * SizeOf(t.base[0], ptr)
+		ELSE RETURN 4
+		END
+	END SizeOf;
+
+	PROCEDURE WriteName (t: Kernel.Type; ptr: ArrayPtr);
+		VAR name: Kernel.Name; f: SHORTCHAR;
+	BEGIN
+		f := FormOf(t);
+		CASE f OF
+		| 0X: OutString("#Dev:Unknown")
+		| 1X: out.WriteSString("BOOLEAN")
+		| 2X: out.WriteSString("SHORTCHAR")
+		| 3X: out.WriteSString("CHAR")
+		| 4X: out.WriteSString("BYTE")
+		| 5X: out.WriteSString("SHORTINT")
+		| 6X: out.WriteSString("INTEGER")
+		| 7X: out.WriteSString("SHORTREAL")
+		| 8X: out.WriteSString("REAL")
+		| 9X: out.WriteSString("SET")
+		| 0AX: out.WriteSString("LONGINT")
+		| 0BX: out.WriteSString("ANYREC")
+		| 0CX: out.WriteSString("ANYPTR")
+		| 0DX: out.WriteSString("POINTER")
+		| 0EX: out.WriteSString("PROCEDURE")
+		| 0FX: out.WriteSString("STRING")
+		| 10X..13X:
+			Kernel.GetTypeName(t, name);
+			IF name = "!" THEN
+				IF f = 11X THEN out.WriteSString("RECORD")
+				ELSIF f = 12X THEN out.WriteSString("ARRAY")
+				ELSE OutString("#Dev:Unknown")
+				END
+			ELSIF (t.id DIV 256 # 0) & (t.mod.refcnt >= 0) THEN
+				out.WriteSString(t.mod.name); out.WriteChar("."); out.WriteSString(name)
+			ELSIF f = 11X THEN
+				out.WriteSString(t.mod.name); out.WriteSString(".RECORD") 
+			ELSIF f = 12X THEN
+				out.WriteSString("ARRAY "); out.WriteInt(LenOf(t, ptr)); t := t.base[0];
+				WHILE (FormOf(t) = 12X) & ((t.id DIV 256 = 0) OR (t.mod.refcnt < 0)) DO
+					out.WriteSString(", "); out.WriteInt(LenOf(t, ptr)); t := t.base[0]
+				END;
+				out.WriteSString(" OF "); WriteName(t, ptr)
+			ELSIF f = 13X THEN
+				out.WriteSString("POINTER")
+			ELSE
+				out.WriteSString("PROCEDURE")
+			END
+		| 20X: out.WriteSString("COM.IUnknown")
+		| 21X: out.WriteSString("COM.GUID")
+		| 22X: out.WriteSString("COM.RESULT")
+		ELSE OutString("#Dev:UnknownFormat"); out.WriteInt(ORD(f))
+		END
+	END WriteName;
+	
+	PROCEDURE WriteGuid (a: INTEGER);
+
+		PROCEDURE Hex (a: INTEGER);
+			VAR x: SHORTCHAR;
+		BEGIN
+			SYSTEM.GET(a, x);
+			out.WriteIntForm(ORD(x), TextMappers.hexadecimal, 2, "0", FALSE)
+		END Hex;
+
+	BEGIN
+		out.WriteChar("{");
+		Hex(a + 3); Hex(a + 2); Hex(a + 1); Hex(a);
+		out.WriteChar("-");
+		Hex(a + 5); Hex(a + 4);
+		out.WriteChar("-");
+		Hex(a + 7); Hex(a + 6);
+		out.WriteChar("-");
+		Hex(a + 8);
+		Hex(a + 9);
+		out.WriteChar("-");
+		Hex(a + 10);
+		Hex(a + 11);
+		Hex(a + 12);
+		Hex(a + 13);
+		Hex(a + 14);
+		Hex(a + 15);
+		out.WriteChar("}")
+	END WriteGuid;
+	
+	PROCEDURE^ ShowVar (ad, ind: INTEGER; f, c: SHORTCHAR; desc: Kernel.Type; ptr: ArrayPtr;
+											back: RefView; VAR name, sel: Name);
+	
+	PROCEDURE ShowRecord (a, ind: INTEGER; desc: Kernel.Type; back: RefView; VAR sel: Name);
+		VAR dir: Kernel.Directory; obj: Kernel.Object; name: Kernel.Name; i, j, n: INTEGER; base: Kernel.Type;
+	BEGIN
+		WriteName(desc, NIL); out.WriteTab;
+		IF desc.mod.refcnt >= 0 THEN
+			OpenFold("#Dev:Fields");
+			n := desc.id DIV 16 MOD 16; j := 0;
+			WHILE j <= n DO
+				base := desc.base[j];
+				IF base # NIL THEN
+					dir := base.fields; i := 0;
+					WHILE i < dir.num DO
+						obj := SYSTEM.VAL(Kernel.Object, SYSTEM.ADR(dir.obj[i]));
+						Kernel.GetObjName(base.mod, obj, name);
+						ShowVar(a + obj.offs, ind, FormOf(obj.struct), 1X, obj.struct, NIL, back, name, sel);
+						INC(i)
+					END
+				END;
+				INC(j)
+			END;
+			out.WriteSString("   "); CloseFold((ind > 1) OR (sel # ""))
+		ELSE
+			OutString("#Dev:Unloaded")
+		END
+	END ShowRecord;
+	
+	PROCEDURE ShowArray (a, ind: INTEGER; desc: Kernel.Type; ptr: ArrayPtr; back: RefView; VAR sel: Name);
+		VAR f: SHORTCHAR; i, n, m, size, len: INTEGER; name: Kernel.Name; eltyp, t: Kernel.Type;
+			vi: SHORTINT; vs: BYTE; str: Dialog.String; high: BOOLEAN;
+	BEGIN
+		WriteName(desc, ptr); out.WriteTab;
+		len := LenOf(desc, ptr); eltyp := desc.base[0]; f := FormOf(eltyp); size := SizeOf(eltyp, ptr);
+		IF (f = 2X) OR (f = 3X) THEN	(* string *)
+			n := 0; m := len; high := FALSE;
+			IF f = 2X THEN
+				REPEAT SYSTEM.GET(a + n, vs); INC(n) UNTIL (n = 32) OR (n = len) OR (vs = 0);
+				REPEAT DEC(m); SYSTEM.GET(a + m, vs) UNTIL (m = 0) OR (vs # 0)
+			ELSE
+				REPEAT
+					SYSTEM.GET(a + n * 2, vi); INC(n);
+					IF vi DIV 256 # 0 THEN high := TRUE END
+				UNTIL (n = len) OR (vi = 0);
+				n := MIN(n, 32);
+				REPEAT DEC(m); SYSTEM.GET(a + m * 2, vi) UNTIL (m = 0) OR (vi # 0)
+			END;
+			WriteString(a, n, size, TRUE, TRUE);
+			INC(m, 2);
+			IF m > len THEN m := len END;
+			IF high OR (m > n) THEN
+				out.WriteSString("   "); OpenFold("...");
+				out.WriteLn;
+				IF high & (n = 32) THEN
+					WriteString(a, m, size, TRUE, TRUE);
+					out.WriteLn; out.WriteLn
+				END;
+				WriteString(a, m, size, FALSE, FALSE);
+				IF m < len THEN out.WriteSString(", ..., 0X") END;
+				out.WriteSString("   "); CloseFold(TRUE)
+			END
+		ELSE
+			t := eltyp;
+			WHILE FormOf(t) = 12X DO t := t.base[0] END;
+			IF FormOf(t) # 0X THEN
+				OpenFold("#Dev:Elements");
+				i := 0;
+				WHILE i < len DO
+					Strings.IntToString(i, str);
+					name := "[" + SHORT(str$) + "]";
+					ShowVar(a, ind, f, 1X, eltyp, ptr, back, name, sel);
+					INC(i); INC(a, size)
+				END;
+				out.WriteSString("   "); CloseFold(TRUE)
+			END
+		END
+	END ShowArray;
+	
+	PROCEDURE ShowProcVar (a: INTEGER);
+		VAR vli, n, ref: INTEGER; m: Kernel.Module; name: Kernel.Name;
+	BEGIN
+		SYSTEM.GET(a, vli);
+		Kernel.SearchProcVar(vli, m, vli);
+		IF m = NIL THEN
+			IF vli = 0 THEN out.WriteSString("NIL")
+			ELSE WriteHex(vli)
+			END
+		ELSE
+			IF m.refcnt >= 0 THEN
+				out.WriteSString(m.name); ref := m.refs;
+				REPEAT Kernel.GetRefProc(ref, n, name) UNTIL (n = 0) OR (vli < n);
+				IF vli < n THEN out.WriteChar("."); out.WriteSString(name) END
+			ELSE
+				OutString("#Dev:ProcInUnloadedMod");
+				out.WriteSString(m.name); out.WriteSString(" !!!")
+			END
+		END
+	END ShowProcVar;
+
+	PROCEDURE ShowPointer (a: INTEGER; f: SHORTCHAR; desc: Kernel.Type; back: RefView; VAR sel: Name);
+		VAR adr, x: INTEGER; ptr: ArrayPtr; c: Cluster; btyp: Kernel.Type;
+	BEGIN
+		SYSTEM.GET(a, adr);
+		IF f = 13X THEN btyp := desc.base[0] ELSE btyp := NIL END;
+		IF adr = 0 THEN out.WriteSString("NIL")
+		ELSIF f = 20X THEN
+			out.WriteChar("["); WriteHex(adr); out.WriteChar("]");
+			out.WriteChar(" "); c := SYSTEM.VAL(Cluster, Kernel.Root());
+			WHILE (c # NIL) & ((adr < SYSTEM.VAL(INTEGER, c)) OR (adr >= SYSTEM.VAL(INTEGER, c) + c.size)) DO c := c.next END;
+			IF c # NIL THEN
+				ptr := SYSTEM.VAL(ArrayPtr, adr)
+			END
+		ELSE
+			IF (f = 13X) OR (f = 0CX) THEN x := adr - 4 ELSE x := adr END;
+			IF ((adr < -4) OR (adr >= 65536)) & Kernel.IsReadable(x, adr + 16) THEN
+				out.WriteChar("["); WriteHex(adr); out.WriteChar("]");
+				IF (f = 13X) OR (f = 0CX) THEN
+					out.WriteChar(" "); c := SYSTEM.VAL(Cluster, Kernel.Root());
+					WHILE (c # NIL) & ((adr < SYSTEM.VAL(INTEGER, c)) OR (adr >= SYSTEM.VAL(INTEGER, c) + c.size)) DO
+						c := c.next
+					END;
+					IF c # NIL THEN
+						ptr := SYSTEM.VAL(ArrayPtr, adr);
+						IF (f = 13X) & (FormOf(btyp) = 12X) THEN	(* array *)
+							adr := SYSTEM.ADR(ptr.len[btyp.id DIV 16 MOD 16]) 
+						END
+					ELSE OutString("#Dev:IllegalPointer")
+					END
+				END
+			ELSE OutString("#Dev:IllegalAddress"); WriteHex(adr)
+			END
+		END
+	END ShowPointer;
+	
+	PROCEDURE ShowSelector (ref: RefView);
+		VAR b: RefView; n: SHORTINT; a, a0: TextModels.Attributes;
+	BEGIN
+		b := ref.back; n := 1;
+		IF b # NIL THEN
+			WHILE (b.name = ref.name) & (b.back # NIL) DO INC(n); b := b.back END;
+			ShowSelector(b);
+			IF n > 1 THEN out.WriteChar("(") END;
+			out.WriteChar(".")
+		END;
+		out.WriteSString(ref.name);
+		IF ref.type = heap THEN out.WriteChar("^") END;
+		IF n > 1 THEN
+			out.WriteChar(")");
+			a0 := out.rider.attr; a := TextModels.NewOffset(a0, 2 * Ports.point);
+			out.rider.SetAttr(a);
+			out.WriteInt(n); out.rider.SetAttr(a0)
+		END
+	END ShowSelector;
+	
+	PROCEDURE ShowVar (ad, ind: INTEGER; f, c: SHORTCHAR; desc: Kernel.Type; ptr: ArrayPtr; back: RefView;
+											VAR name, sel: Name);
+		VAR i, j, vli, a: INTEGER; tsel: Name; a0: TextModels.Attributes;
+			vc: SHORTCHAR; vsi: BYTE; vi: SHORTINT; vr: SHORTREAL; vlr: REAL; vs: SET;
+	BEGIN
+		out.WriteLn; out.WriteTab; i := 0;
+		WHILE i < ind DO out.WriteSString("  "); INC(i) END;
+		a := ad; i := 0; j := 0;
+		IF sel # "" THEN
+			WHILE sel[i] # 0X DO tsel[i] := sel[i]; INC(i) END;
+			IF (tsel[i-1] # ":") & (name[0] # "[") THEN tsel[i] := "."; INC(i) END
+		END;
+		WHILE name[j] # 0X DO tsel[i] := name[j]; INC(i); INC(j) END;
+		tsel[i] := 0X;
+		a0 := out.rider.attr;
+		IF c = 3X THEN	(* varpar *)
+			SYSTEM.GET(ad, a);
+			out.rider.SetAttr(TextModels.NewStyle(a0, {Fonts.italic}))
+		END;
+		IF name[0] # "[" THEN out.WriteChar(".") END;
+		out.WriteSString(name);
+		out.rider.SetAttr(a0); out.WriteTab;
+		IF (c = 3X) & (a >= 0) & (a < 65536) THEN 
+			out.WriteTab; out.WriteSString("NIL VARPAR")
+		ELSIF f = 11X THEN
+			Kernel.GetTypeName(desc, name);
+			IF (c = 3X) & (name[0] # "!") THEN SYSTEM.GET(ad + 4, desc) END;	(* dynamic type *)
+			ShowRecord(a, ind + 1, desc, back, tsel)
+		ELSIF (c = 3X) & (f = 0BX) THEN	(* VAR anyrecord *)
+			SYSTEM.GET(ad + 4, desc);
+			ShowRecord(a, ind + 1, desc, back, tsel)
+		ELSIF f = 12X THEN
+			IF (desc.size = 0) & (ptr = NIL) THEN SYSTEM.GET(ad, a) END;	(* dyn array val par *)
+			IF ptr = NIL THEN ptr := SYSTEM.VAL(ArrayPtr, ad - 8) END;
+			ShowArray(a, ind + 1, desc, ptr, back, tsel)
+		ELSE
+			IF desc = NIL THEN desc := SYSTEM.VAL(Kernel.Type, ORD(f)) END;
+			WriteName(desc, NIL); out.WriteTab;
+			CASE f OF
+			| 0X: (* SYSTEM.GET(a, vli); WriteHex(vli) *)
+			| 1X: SYSTEM.GET(a, vc); 
+				IF vc = 0X THEN out.WriteSString("FALSE")
+				ELSIF vc = 1X THEN out.WriteSString("TRUE")
+				ELSE OutString("#Dev:Undefined"); out.WriteInt(ORD(vc))
+				END
+			| 2X: WriteString(a, 1, 1, FALSE, FALSE)
+			| 3X: WriteString(a, 1, 2, FALSE, TRUE);
+					SYSTEM.GET(a, vi);
+					IF vi DIV 256 # 0 THEN out.WriteString("  "); WriteString(a, 1, 2, FALSE, FALSE) END
+			| 4X: SYSTEM.GET(a, vsi); out.WriteInt(vsi)
+			| 5X: SYSTEM.GET(a, vi); out.WriteInt(vi)
+			| 6X: SYSTEM.GET(a, vli); out.WriteInt(vli)
+			| 7X: SYSTEM.GET(a, vr); out.WriteReal(vr)
+			| 8X: SYSTEM.GET(a, vlr); out.WriteReal(vlr)
+			| 9X: SYSTEM.GET(a, vs); out.WriteSet(vs)
+			| 0AX: SYSTEM.GET(a, vli); SYSTEM.GET(a + 4, i);
+				IF (vli >= 0) & (i = 0) OR (vli < 0) & (i = -1) THEN out.WriteInt(vli)
+				ELSE out.WriteIntForm(i, TextMappers.hexadecimal, 8, "0", TextMappers.hideBase); WriteHex(vli)
+				END
+			| 0CX, 0DX, 13X, 20X: ShowPointer(a, f, desc, back, tsel)
+			| 0EX, 10X: ShowProcVar(a)
+			| 0FX: WriteString(a, 256, 1, TRUE, FALSE)
+			| 21X: WriteGuid(a)
+			| 22X: SYSTEM.GET(a, vli); WriteHex(vli)
+			ELSE 
+			END
+		END
+	END ShowVar;
+	
+
+	PROCEDURE ShowStack;
+		VAR ref, end, i, j, x, a, b, c: INTEGER; m, f: SHORTCHAR; mod: Kernel.Module; name, sel: Kernel.Name;
+			d: Kernel.Type;
+	BEGIN
+		a := Kernel.pc; b := Kernel.fp; c := 100;
+		REPEAT
+			mod := Kernel.modList;
+			WHILE (mod # NIL) & ((a < mod.code) OR (a >= mod.code + mod.csize)) DO mod := mod.next END;
+			IF mod # NIL THEN
+				DEC(a, mod.code);
+				IF mod.refcnt >= 0 THEN
+					out.WriteChar(" "); out.WriteSString(mod.name); ref := mod.refs;
+					REPEAT Kernel.GetRefProc(ref, end, name) UNTIL (end = 0) OR (a < end);
+					IF a < end THEN
+						out.WriteChar("."); out.WriteSString(name);
+						sel := mod.name$; i := 0;
+						WHILE sel[i] # 0X DO INC(i) END;
+						sel[i] := "."; INC(i); j := 0;
+						WHILE name[j] # 0X DO sel[i] := name[j]; INC(i); INC(j) END;
+						sel[i] := ":"; sel[i+1] := 0X;
+						out.WriteSString("   ["); WriteHex(a);
+						out.WriteSString("] ");
+						i := Kernel.SourcePos(mod, 0);
+						IF name # "$$" THEN
+							Kernel.GetRefVar(ref, m, f, d, x, name);
+							WHILE m # 0X DO
+								IF name[0] # "@" THEN ShowVar(b + x, 0, f, m, d, NIL, NIL, name, sel) END;
+								Kernel.GetRefVar(ref, m, f, d, x, name)
+							END
+						END;
+						out.WriteLn
+					ELSE out.WriteSString(".???"); out.WriteLn
+					END
+				ELSE
+					out.WriteChar("("); out.WriteSString(mod.name);
+					out.WriteSString(")   (pc="); WriteHex(a);
+					out.WriteSString(",  fp="); WriteHex(b); out.WriteChar(")");
+					out.WriteLn
+				END
+			ELSE
+				out.WriteSString("<system>   (pc="); WriteHex(a);
+				out.WriteSString(",  fp="); WriteHex(b); out.WriteChar(")");
+				out.WriteLn
+			END;
+			IF (b >= Kernel.fp) & (b < Kernel.stack) THEN
+				SYSTEM.GET(b+4, a);	(* stacked pc *)
+				SYSTEM.GET(b, b);	(* dynamic link *)
+				DEC(a); DEC(c)
+			ELSE c := 0
+			END
+		UNTIL c = 0
+	END ShowStack;
+
+	PROCEDURE (a: Action) Do;	(* delayed trap window open *)
+	BEGIN
+		Kernel.SetTrapGuard(TRUE);
+		OpenViewer(a.text, "#Dev:Trap", NewRuler());
+		Kernel.SetTrapGuard(FALSE);
+	END Do;
+
+	PROCEDURE GetTrapMsg(OUT msg: ARRAY OF CHAR);
+		VAR ref, end, a: INTEGER; mod: Kernel.Module; name: Kernel.Name; head, tail, errstr: ARRAY 32 OF CHAR;
+			key: ARRAY 128 OF CHAR;
+	BEGIN
+		a := Kernel.pc; mod := Kernel.modList;
+		WHILE (mod # NIL) & ((a < mod.code) OR (a >= mod.code + mod.csize)) DO mod := mod.next END;
+		IF mod # NIL THEN
+			DEC(a, mod.code); ref := mod.refs;
+			REPEAT Kernel.GetRefProc(ref, end, name) UNTIL (end = 0) OR (a < end);
+			IF a < end THEN
+				Kernel.SplitName (mod.name$, head, tail);
+				IF head = "" THEN head := "System" END;
+				Strings.IntToString(Kernel.err, errstr);
+				key := tail + "." + name + "." + errstr;
+				Dialog.MapString("#" + head + ":" + key, msg);
+				(* IF key # msg THEN out.WriteString(" " + msg) END; *)
+				IF key = msg THEN msg := "" END;
+			END
+		END
+	END GetTrapMsg;
+
+	PROCEDURE Trap;
+		VAR a0: TextModels.Attributes; action: Action; msg: ARRAY 512 OF CHAR;
+	BEGIN
+		out.ConnectTo(TextModels.dir.New());
+		a0 := out.rider.attr;
+		out.rider.SetAttr(TextModels.NewWeight(a0, Fonts.bold));
+		IF Kernel.err = 129 THEN out.WriteSString("invalid WITH")
+		ELSIF Kernel.err = 130 THEN out.WriteSString("invalid CASE")
+		ELSIF Kernel.err = 131 THEN out.WriteSString("function without RETURN")
+		ELSIF Kernel.err = 132 THEN out.WriteSString("type guard")
+		ELSIF Kernel.err = 133 THEN out.WriteSString("implied type guard")
+		ELSIF Kernel.err = 134 THEN out.WriteSString("value out of range")
+		ELSIF Kernel.err = 135 THEN out.WriteSString("index out of range")
+		ELSIF Kernel.err = 136 THEN out.WriteSString("string too long")
+		ELSIF Kernel.err = 137 THEN out.WriteSString("stack overflow")
+		ELSIF Kernel.err = 138 THEN out.WriteSString("integer overflow")
+		ELSIF Kernel.err = 139 THEN out.WriteSString("division by zero")
+		ELSIF Kernel.err = 140 THEN out.WriteSString("infinite real result")
+		ELSIF Kernel.err = 141 THEN out.WriteSString("real underflow")
+		ELSIF Kernel.err = 142 THEN out.WriteSString("real overflow")
+		ELSIF Kernel.err = 143 THEN out.WriteSString("undefined real result")
+		ELSIF Kernel.err = 144 THEN out.WriteSString("not a number")
+		ELSIF Kernel.err = 200 THEN out.WriteSString("keyboard interrupt")
+		ELSIF Kernel.err = 201 THEN
+			out.WriteSString("NIL dereference")
+		ELSIF Kernel.err = 202 THEN
+			out.WriteSString("illegal instruction: ");
+			out.WriteIntForm(Kernel.val, TextMappers.hexadecimal, 5, "0", TextMappers.showBase)
+		ELSIF Kernel.err = 203 THEN
+			IF (Kernel.val >= -4) & (Kernel.val < 65536) THEN out.WriteSString("NIL dereference (read)")
+			ELSE out.WriteSString("illegal memory read (ad = "); WriteHex(Kernel.val); out.WriteChar(")")
+			END
+		ELSIF Kernel.err = 204 THEN
+			IF (Kernel.val >= -4) & (Kernel.val < 65536) THEN out.WriteSString("NIL dereference (write)")
+			ELSE out.WriteSString("illegal memory write (ad = "); WriteHex(Kernel.val); out.WriteChar(")")
+			END
+		ELSIF Kernel.err = 205 THEN
+			IF (Kernel.val >= -4) & (Kernel.val < 65536) THEN out.WriteSString("NIL procedure call")
+			ELSE out.WriteSString("illegal execution (ad = "); WriteHex(Kernel.val); out.WriteChar(")")
+			END
+		ELSIF Kernel.err = 257 THEN out.WriteSString("out of memory")
+		ELSIF Kernel.err = 10001H THEN out.WriteSString("bus error")
+		ELSIF Kernel.err = 10002H THEN out.WriteSString("address error")
+		ELSIF Kernel.err = 10007H THEN out.WriteSString("fpu error")
+		ELSIF Kernel.err < 0 THEN
+			out.WriteSString("Exception "); out.WriteIntForm(-Kernel.err, TextMappers.hexadecimal, 3, "0", TextMappers.showBase)
+		ELSE
+			out.WriteSString("TRAP "); out.WriteInt(Kernel.err);
+			IF Kernel.err = 126 THEN out.WriteSString("  (not yet implemented)")
+			ELSIF Kernel.err = 125 THEN out.WriteSString("  (call of obsolete procedure)")
+			ELSIF Kernel.err >= 100 THEN out.WriteSString("  (invariant violated)")
+			ELSIF Kernel.err >= 60 THEN out.WriteSString("  (postcondition violated)")
+			ELSIF Kernel.err >= 20 THEN out.WriteSString("  (precondition violated)")
+			END
+		END;
+		GetTrapMsg(msg);
+		IF msg # "" THEN out.WriteLn; out.WriteString(msg) END;
+		out.WriteLn; out.rider.SetAttr(a0);
+		out.WriteLn; ShowStack;
+		NEW(action); action.text := out.rider.Base();
+		Services.DoLater(action, Services.now);
+		out.ConnectTo(NIL)
+	END Trap;
+
+BEGIN
+	Kernel.InstallTrapViewer(Trap);
+	empty := "";
+	path[0].x := refViewSize DIV 2; path[0].y := 0;
+	path[1].x := refViewSize; path[1].y := refViewSize DIV 2;
+	path[2].x := refViewSize DIV 2; path[2].y := refViewSize;
+	path[3].x := 0; path[3].y := refViewSize DIV 2;
+END StdDebug.

+ 297 - 0
BlackBox/Std/Mod/Dialog.txt

@@ -0,0 +1,297 @@
+MODULE StdDialog;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Dialog.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Kernel, Meta, Strings, Files, Stores, Models, Sequencers, Views,
+		Containers, Dialog, Properties, Documents, Converters, Windows;
+
+
+	TYPE
+		Item* = POINTER TO EXTENSIBLE RECORD
+			next*: Item;
+			item-, string-, filter-: POINTER TO ARRAY OF CHAR;
+			shortcut-: ARRAY 8 OF CHAR;
+			privateFilter-, failed, trapped: BOOLEAN;	(* filter call failed, caused a trap *)
+			res: INTEGER	(* result code of failed filter *)
+		END;
+		
+		FilterProcVal = RECORD (Meta.Value) p: Dialog.GuardProc END;
+		FilterProcPVal = RECORD (Meta.Value) p: PROCEDURE(n: INTEGER; VAR p: Dialog.Par) END;
+
+		ViewHook = POINTER TO RECORD (Views.ViewHook) END;
+
+
+	VAR curItem-: Item;	(** IN parameter for item filters **)
+
+
+	PROCEDURE GetSubLoc* (mod: ARRAY OF CHAR; cat: Files.Name;
+											OUT loc: Files.Locator; OUT name: Files.Name);
+		VAR sub: Files.Name; file: Files.File; type: Files.Type;
+	BEGIN
+		IF (cat[0] = "S") & (cat[1] = "y") & (cat[2] = "m") THEN type := Kernel.symType
+		ELSIF (cat[0] = "C") & (cat[1] = "o") & (cat[2] = "d") & (cat[3] = "e") THEN type := Kernel.objType
+		ELSE type := ""
+		END;
+		Kernel.SplitName(mod, sub, name); Kernel.MakeFileName(name, type);
+		loc := Files.dir.This(sub); file := NIL;
+		IF loc # NIL THEN
+			loc := loc.This(cat);
+			IF sub = "" THEN
+				IF loc # NIL THEN
+					file := Files.dir.Old(loc, name, Files.shared);
+					IF file = NIL THEN loc := NIL END
+				END;
+				IF loc = NIL THEN
+					loc := Files.dir.This("System");
+					IF loc # NIL THEN loc := loc.This(cat) END
+				END
+			END
+		END
+	END GetSubLoc;
+
+
+	PROCEDURE Len (VAR str: ARRAY OF CHAR): INTEGER;
+		VAR i: INTEGER;
+	BEGIN
+		i := 0; WHILE str[i] # 0X DO INC(i) END;
+		RETURN i
+	END Len;
+
+	PROCEDURE AddItem* (i: Item; item, string, filter, shortcut: ARRAY OF CHAR);
+		VAR j: INTEGER; ch: CHAR;
+	BEGIN
+		ASSERT(i # NIL, 20);
+		NEW(i.item, Len(item) + 1);
+		NEW(i.string, Len(string) + 1);
+		NEW(i.filter, Len(filter) + 1);
+		ASSERT((i.item # NIL) & (i.string # NIL) & (i.filter # NIL), 100);
+		i.item^ := item$;
+		i.string^ := string$;
+		i.filter^ := filter$;
+		i.shortcut := shortcut$;
+		j := 0; ch := filter[0]; WHILE (ch # ".") & (ch # 0X) DO INC(j); ch := filter[j] END;
+		i.privateFilter := (j > 0) & (ch = 0X);
+		i.failed := FALSE; i.trapped := FALSE
+	END AddItem;
+
+	PROCEDURE ClearGuards* (i: Item);
+	BEGIN
+		i.failed := FALSE; i.trapped := FALSE
+	END ClearGuards;
+
+	PROCEDURE GetGuardProc (name: ARRAY OF CHAR; VAR i: Meta.Item;
+														VAR par: BOOLEAN; VAR n: INTEGER);
+		VAR j, k: INTEGER; num: ARRAY 32 OF CHAR;
+	BEGIN
+		j := 0;
+		WHILE (name[j] # 0X) & (name[j] # "(") DO INC(j) END;
+		IF name[j] = "(" THEN
+			name[j] := 0X; INC(j); k := 0;
+			WHILE (name[j] # 0X) & (name[j] # ")") DO num[k] := name[j]; INC(j); INC(k) END;
+			IF (name[j] = ")") & (name[j+1] = 0X) THEN
+				num[k] := 0X; Strings.StringToInt(num, n, k);
+				IF k = 0 THEN Meta.LookupPath(name, i); par := TRUE
+				ELSE Meta.Lookup("", i)
+				END
+			ELSE Meta.Lookup("", i)
+			END
+		ELSE
+			Meta.LookupPath(name, i); par := FALSE
+		END
+	END GetGuardProc;
+	
+	PROCEDURE CheckFilter* (i: Item; VAR failed, ok: BOOLEAN; VAR par: Dialog.Par);
+		VAR x: Meta.Item; v: FilterProcVal; vp: FilterProcPVal; p: BOOLEAN; n: INTEGER;
+	BEGIN
+		IF ~i.failed THEN
+			curItem := i;
+			par.disabled := FALSE; par.checked := FALSE; par.label := i.item$;
+			par.undef := FALSE; par.readOnly := FALSE;
+			i.failed := TRUE; i.trapped := TRUE;
+			GetGuardProc(i.filter^, x, p, n);
+			IF (x.obj = Meta.procObj) OR (x.obj = Meta.varObj) & (x.typ = Meta.procTyp) THEN
+				IF p THEN
+					x.GetVal(vp, ok);
+					IF ok THEN vp.p(n, par) END
+				ELSE
+					x.GetVal(v, ok);
+					IF ok THEN v.p(par) END
+				END
+			ELSE ok := FALSE
+			END;
+			IF ok THEN i.res := 0 ELSE i.res := 1 END;
+			i.trapped := FALSE; i.failed := ~ok
+		END;
+		failed := i.failed
+	END CheckFilter;
+
+	PROCEDURE HandleItem* (i: Item);
+		VAR res: INTEGER;
+	BEGIN
+		IF ~i.failed THEN
+			Views.ClearQueue; res := 0;
+			Dialog.Call(i.string^, " ", res)
+		ELSIF (i # NIL) & i.failed THEN
+			IF i.trapped THEN
+				Dialog.ShowParamMsg("#System:ItemFilterTrapped", i.string^, i.filter^, "")
+			ELSE
+				Dialog.ShowParamMsg("#System:ItemFilterNotFound", i.string^, i.filter^, "")
+			END
+		END
+	END HandleItem;
+
+	PROCEDURE RecalcView* (v: Views.View);
+	(* recalc size of all subviews of v, then v itself *)
+	VAR m: Models.Model; v1: Views.View; c: Containers.Controller;
+		minW, maxW, minH, maxH, w, h, w0, h0: INTEGER;
+	BEGIN
+		IF v IS Containers.View THEN
+			c := v(Containers.View).ThisController();
+			IF c # NIL THEN
+				v1 := NIL; c.GetFirstView(Containers.any, v1);
+				WHILE v1 # NIL DO
+					RecalcView(v1);
+					c.GetNextView(Containers.any, v1)
+				END
+			END
+		END;
+		IF v.context # NIL THEN
+			m := v.context.ThisModel();
+			IF (m # NIL) & (m IS Containers.Model) THEN
+				m(Containers.Model).GetEmbeddingLimits(minW, maxW, minH, maxH);
+				v.context.GetSize(w0, h0); w := w0; h := h0;
+				Properties.PreferredSize(v, minW, maxW, minH, maxH, w, h, w, h);
+				IF (w # w0) OR (h # h0) THEN v.context.SetSize(w, h) END
+			END
+		END
+	END RecalcView;
+
+
+	PROCEDURE Open* (v: Views.View; title: ARRAY OF CHAR;
+									loc: Files.Locator; name: Files.Name; conv: Converters.Converter;
+									asTool, asAux, noResize, allowDuplicates, neverDirty: BOOLEAN);
+		VAR t: Views.Title; flags, opts: SET; done: BOOLEAN; d: Documents.Document; i: INTEGER;
+			win: Windows.Window; c: Containers.Controller; seq: ANYPTR;
+	BEGIN
+		IF conv = NIL THEN conv := Converters.list END;	(* use document converter *)
+		ASSERT(v # NIL, 20);
+		flags := {}; done := FALSE;
+		IF noResize THEN
+			flags := flags + {Windows.noResize, Windows.noHScroll, Windows.noVScroll}
+		END;
+		IF asTool THEN INCL(flags, Windows.isTool) END;
+		IF asAux THEN INCL(flags, Windows.isAux) END;
+		IF neverDirty THEN INCL(flags, Windows.neverDirty) END;
+		i := 0;
+		WHILE (i < LEN(t) - 1) & (title[i] # 0X) DO t[i] := title[i]; INC(i) END;
+		t[i] := 0X;
+		IF ~allowDuplicates THEN
+			IF ~asTool & ~asAux THEN
+				IF (loc # NIL) & (name # "") THEN Windows.SelectBySpec(loc, name, conv, done) END
+			ELSE
+				IF title # "" THEN Windows.SelectByTitle(v, flags, t, done) END
+			END
+		ELSE
+			INCL(flags, Windows.allowDuplicates)
+		END;
+		IF ~done THEN
+			IF v IS Documents.Document THEN
+				IF v.context # NIL THEN
+					d := Documents.dir.New(
+								Views.CopyOf(v(Documents.Document).ThisView(), Views.shallow), 
+								Views.undefined, Views.undefined)
+				ELSE
+					d := v(Documents.Document)
+				END;
+				ASSERT(d.context = NIL, 22);
+				v := d.ThisView(); ASSERT(v # NIL, 23)
+			ELSIF v.context # NIL THEN
+				ASSERT(v.context IS Documents.Context, 24);
+				d := v.context(Documents.Context).ThisDoc();
+				IF d.context # NIL THEN
+					d := Documents.dir.New(Views.CopyOf(v, Views.shallow), Views.undefined, Views.undefined)
+				END;
+				ASSERT(d.context = NIL, 25)
+				(*IF d.Domain() = NIL THEN Stores.InitDomain(d, v.Domain()) END (for views opened via Views.Old *)
+			ELSE
+				d := Documents.dir.New(v, Views.undefined, Views.undefined)
+			END;
+			IF asTool OR asAux THEN
+				c := d.ThisController();
+				c.SetOpts(c.opts + {Containers.noSelection})
+			END;
+			ASSERT(d.Domain() = v.Domain(), 100);
+			ASSERT(d.Domain() # NIL, 101);
+			seq := d.Domain().GetSequencer();
+			IF neverDirty & (seq # NIL) THEN
+				ASSERT(seq IS Sequencers.Sequencer, 26);
+				seq(Sequencers.Sequencer).SetDirty(FALSE)
+			END;
+			IF neverDirty THEN
+				(* change "fit to page" to "fit to window" in secondary windows *)
+				c := d.ThisController(); opts := c.opts;
+				IF Documents.pageWidth IN opts THEN
+					opts := opts - {Documents.pageWidth} + {Documents.winWidth}
+				END;
+				IF Documents.pageHeight IN opts THEN
+					opts := opts - {Documents.pageHeight} + {Documents.winHeight}
+				END;
+				c.SetOpts(opts)
+			END;
+			win := Windows.dir.New();
+			IF seq # NIL THEN
+				Windows.dir.OpenSubWindow(win, d, flags, t)
+			ELSE
+				Windows.dir.Open(win, d, flags, t, loc, name, conv)
+			END
+		END
+	END Open;
+	
+	PROCEDURE (h: ViewHook) Open (v: Views.View; title: ARRAY OF CHAR;
+								loc: Files.Locator; name: Files.Name; conv: Converters.Converter;
+								asTool, asAux, noResize, allowDuplicates, neverDirty: BOOLEAN);
+	BEGIN
+		Open(v, title, loc, name, conv, asTool, asAux, noResize, allowDuplicates, neverDirty)
+	END Open;
+
+	PROCEDURE (h: ViewHook) OldView (loc: Files.Locator; name: Files.Name;
+																VAR conv: Converters.Converter): Views.View;
+		VAR w: Windows.Window; s: Stores.Store; c: Converters.Converter;
+	BEGIN
+		ASSERT(loc # NIL, 20); ASSERT(name # "", 21);
+		Kernel.MakeFileName(name, ""); s := NIL;
+		IF loc.res # 77 THEN
+			w := Windows.dir.First(); c := conv;
+			IF c = NIL THEN c := Converters.list END;	(* use document converter *)
+			WHILE (w # NIL) & ((w.loc = NIL) OR (w.name = "") OR (w.loc.res = 77) OR
+											~Files.dir.SameFile(loc, name, w.loc, w.name) OR (w.conv # c)) DO
+				w := Windows.dir.Next(w)
+			END;
+			IF w # NIL THEN s := w.doc.ThisView() END
+		END;
+		IF s = NIL THEN
+			Converters.Import(loc, name, conv, s);
+			IF s # NIL THEN RecalcView(s(Views.View)) END
+		END;
+		IF s # NIL THEN RETURN s(Views.View) ELSE RETURN NIL END
+	END OldView;
+
+	PROCEDURE (h: ViewHook) RegisterView (v: Views.View; 
+															loc: Files.Locator; name: Files.Name; conv: Converters.Converter);
+	BEGIN
+		ASSERT(v # NIL, 20); ASSERT(loc # NIL, 21); ASSERT(name # "", 22);
+		Kernel.MakeFileName(name, "");
+		Converters.Export(loc, name, conv, v)
+	END RegisterView;
+
+	PROCEDURE Init;
+		VAR h: ViewHook;
+	BEGIN
+		NEW(h); Views.SetViewHook(h)
+	END Init;
+
+BEGIN
+	Init
+END StdDialog.

+ 223 - 0
BlackBox/Std/Mod/ETHConv.txt

@@ -0,0 +1,223 @@
+MODULE StdETHConv;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/ETHConv.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Fonts, Files, Stores, Ports, Views,
+		TextModels, TextRulers, TextViews,
+		Stamps := StdStamps, Clocks := StdClocks, StdFolds;
+
+	CONST
+		V2Tag = -4095; (* 01 F0 *)
+		V4Tag = 496; (* F0 01 *)
+
+	TYPE
+		FontDesc = RECORD
+			typeface: Fonts.Typeface;
+			size: INTEGER;
+			style: SET;
+			weight: INTEGER
+		END;
+
+	VAR default: Fonts.Font;
+
+	PROCEDURE Split (name: ARRAY OF CHAR; VAR d: FontDesc);
+		VAR i: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; ch := name[0];
+		WHILE (ch < "0") OR (ch >"9") DO
+			d.typeface[i] := ch; INC(i); ch := name[i]
+		END;
+		d.typeface[i] := 0X;
+		d.size := 0;
+		WHILE ("0" <= ch) & (ch <= "9") DO
+			d.size := d.size * 10 + (ORD(ch) - 30H); INC(i); ch := name[i]
+		END;
+		CASE ch OF
+		  "b": d.style := {}; d.weight := Fonts.bold
+		| "i": d.style := {Fonts.italic}; d.weight := Fonts.normal
+		| "j": d.style := {Fonts.italic}; d.weight := Fonts.bold
+		| "m": d.style := {}; d.weight := Fonts.bold
+		ELSE d.style := {}; d.weight := Fonts.normal	(* unknown style *)
+		END
+	END Split;
+
+	PROCEDURE ThisFont (name: ARRAY OF CHAR): Fonts.Font; 
+		VAR d: FontDesc;
+	BEGIN
+		Split(name, d);
+		IF d.typeface = "Syntax" THEN d.typeface := default.typeface END;
+		IF d.size = 10 THEN d.size := default.size
+		ELSE d.size := (d.size - 2) * Ports.point
+		END;
+		RETURN Fonts.dir.This(d.typeface, d.size, d.style, d.weight)
+	END ThisFont;
+
+	PROCEDURE ThisChar (ch: CHAR): CHAR;
+	BEGIN
+		CASE ORD(ch) OF
+		  80H: ch := 0C4X | 81H: ch := 0D6X | 82H: ch := 0DCX
+		| 83H: ch := 0E4X | 84H: ch := 0F6X | 85H: ch := 0FCX
+		| 86H: ch := 0E2X | 87H: ch := 0EAX | 88H: ch := 0EEX | 89H: ch := 0F4X | 8AH: ch := 0FBX
+		| 8BH: ch := 0E0X | 8CH: ch := 0E8X | 8DH: ch := 0ECX | 8EH: ch := 0F2X | 8FH: ch := 0F9X
+		| 90H: ch := 0E9X
+		| 91H: ch := 0EBX | 92H: ch := 0EFX
+		| 93H: ch := 0E7X
+		| 94H: ch := 0E1X
+		| 95H: ch := 0F1X
+		| 9BH: ch := TextModels.hyphen
+		| 9FH: ch := TextModels.nbspace
+		| 0ABH: ch := 0DFX
+		ELSE
+			ch := 0BFX	(* use inverted question mark for unknown character codes *)
+		END;
+		RETURN ch
+	END ThisChar;
+	
+	PROCEDURE ^ LoadTextBlock (r: Stores.Reader; t: TextModels.Model);
+	
+	PROCEDURE StdFold (VAR r: Stores.Reader): Views.View;
+		CONST colLeft = 0; colRight = 1; expRight = 2; expLeft = 3;
+		VAR k: BYTE; state: BOOLEAN; hidden: TextModels.Model; fold: StdFolds.Fold;
+	BEGIN
+		r.ReadByte(k);
+		CASE k MOD 4 OF
+			| colLeft: state := StdFolds.collapsed
+			| colRight: state := StdFolds.collapsed
+			| expRight: state := StdFolds.expanded
+			| expLeft: state := StdFolds.expanded
+		END;
+		IF (k MOD 4 IN {colLeft, expLeft}) & (k < 4) THEN
+			hidden := TextModels.dir.New(); LoadTextBlock(r, hidden);
+		ELSE hidden := NIL;
+		END;
+		fold := StdFolds.dir.New(state, "", hidden);
+		RETURN fold;
+	END StdFold;
+	
+	PROCEDURE LoadTextBlock (r: Stores.Reader; t: TextModels.Model);
+		VAR r0: Stores.Reader; wr: TextModels.Writer;
+			org, len: INTEGER; en, ano, i, n: BYTE; col, voff, ch: CHAR; tag: INTEGER;
+			fname: ARRAY 32 OF CHAR;
+			attr: ARRAY 32 OF TextModels.Attributes;
+			mod, proc: ARRAY 32 OF ARRAY 32 OF CHAR;
+
+		PROCEDURE ReadNum (VAR n: INTEGER);
+			VAR s: BYTE; ch: CHAR; y: INTEGER;
+		BEGIN
+			s := 0; y := 0; r.ReadXChar(ch);
+			WHILE ch >= 80X DO
+				INC(y, ASH(ORD(ch)-128, s)); INC(s, 7); r.ReadXChar(ch)
+			END;
+			n := ASH((ORD(ch) + 64) MOD 128 - 64, s) + y
+		END ReadNum;
+
+		PROCEDURE ReadSet (VAR s: SET);
+			VAR x: INTEGER;
+		BEGIN
+			ReadNum(x); s := BITS(x)
+		END ReadSet;
+
+		PROCEDURE Elem (VAR r: Stores.Reader; span: INTEGER);
+			VAR v: Views.View; end, ew, eh, n, indent: INTEGER; eno, version: BYTE;
+				p: TextRulers.Prop; opts: SET;
+		BEGIN
+			r.ReadInt(ew); r.ReadInt(eh); r.ReadByte(eno);
+			IF eno > en THEN en := eno; r.ReadXString(mod[eno]); r.ReadXString(proc[eno]) END;
+			end := r.Pos() + span;
+			IF (mod[eno] = "ParcElems") OR (mod[eno] = "StyleElems") THEN
+				r.ReadByte(version);
+				NEW(p);
+				p.valid := {TextRulers.first .. TextRulers.tabs};
+				ReadNum(indent); ReadNum(p.left);
+				p.first := p.left + indent;
+				ReadNum(n); p.right := p.left + n;
+				ReadNum(p.lead);
+				ReadNum(p.grid);
+				ReadNum(p.dsc); p.asc := p.grid - p.dsc;
+				ReadSet(opts); p.opts.val := {};
+				IF ~(0 IN opts) THEN p.grid := 1 END;
+				IF 1 IN opts THEN INCL(p.opts.val, TextRulers.leftAdjust) END;
+				IF 2 IN opts THEN INCL(p.opts.val, TextRulers.rightAdjust) END;
+				IF 3 IN opts THEN INCL(p.opts.val, TextRulers.pageBreak) END;
+				INCL(p.opts.val, TextRulers.rightFixed);
+				p.opts.mask := {TextRulers.leftAdjust .. TextRulers.pageBreak, TextRulers.rightFixed};
+				ReadNum(n); p.tabs.len := n;
+				i := 0; WHILE i < p.tabs.len DO ReadNum(p.tabs.tab[i].stop); INC(i) END;
+				v := TextRulers.dir.NewFromProp(p);
+				wr.WriteView(v, ew, eh)
+			ELSIF mod[eno] = "StampElems" THEN
+				v := Stamps.New();
+				wr.WriteView(v, ew, eh)
+			ELSIF mod[eno] = "ClockElems" THEN
+				v := Clocks.New();
+				wr.WriteView(v, ew, eh)
+			ELSIF mod[eno] = "FoldElems" THEN
+				v := StdFold(r);
+				wr.WriteView(v, ew, eh);
+			END;
+			r.SetPos(end)
+		END Elem;
+
+	BEGIN
+		(* skip inner text tags (legacy from V2) *)
+		r.ReadXInt(tag);
+		IF tag # V2Tag THEN r.SetPos(r.Pos()-2) END;
+		(* load text block *)
+		org := r.Pos(); r.ReadInt(len); INC(org, len - 2);
+		r0.ConnectTo(r.rider.Base()); r0.SetPos(org);
+		wr := t.NewWriter(NIL); wr.SetPos(0);
+		n := 0; en := 0; r.ReadByte(ano);
+		WHILE ano # 0 DO
+			IF ano > n THEN
+				n := ano; r.ReadXString(fname);
+				attr[n] := TextModels.NewFont(wr.attr, ThisFont(fname))
+			END;
+			r.ReadXChar(col); r.ReadXChar(voff); r.ReadInt(len);
+			wr.SetAttr(attr[ano]);
+			IF len > 0 THEN
+				WHILE len # 0 DO
+					r0.ReadXChar(ch);
+					IF ch >= 80X THEN ch := ThisChar(ch) END;
+					IF (ch >= " ") OR (ch = TextModels.tab) OR (ch = TextModels.line) THEN
+						wr.WriteChar(ch)
+					END;
+					DEC(len)
+				END
+			ELSE
+				Elem(r, -len); r0.ReadXChar(ch)
+			END;
+			r.ReadByte(ano)
+		END;
+		r.ReadInt(len);
+		r.SetPos(r.Pos() + len);
+	END LoadTextBlock;
+
+	PROCEDURE ImportOberon* (f: Files.File): TextModels.Model;
+		VAR r: Stores.Reader; t: TextModels.Model; tag: INTEGER;
+	BEGIN
+		r.ConnectTo(f); r.SetPos(0);
+		r.ReadXInt(tag);
+		IF tag = ORD("o") + 256 * ORD("B") THEN
+			(* ignore file header of Oberon for Windows and DOSOberon files *)
+			r.SetPos(34); r.ReadXInt(tag)
+		END;
+		ASSERT((tag = V2Tag) OR (tag = V4Tag), 100);
+		t := TextModels.dir.New();
+		LoadTextBlock(r, t);
+		RETURN t;
+	END ImportOberon;
+	
+
+	PROCEDURE ImportETHDoc* (f: Files.File; OUT s: Stores.Store);
+		VAR t: TextModels.Model;
+	BEGIN
+		ASSERT(f # NIL, 20);
+		t := ImportOberon(f);
+		IF t # NIL THEN s := TextViews.dir.New(t) END
+	END ImportETHDoc;
+
+BEGIN
+	default := Fonts.dir.Default()
+END StdETHConv.

+ 779 - 0
BlackBox/Std/Mod/Folds.txt

@@ -0,0 +1,779 @@
+MODULE StdFolds;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Folds.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Domains := Stores, Ports, Stores, Containers, Models, Views, Controllers, Fonts,
+		Properties,Controls,
+		TextModels, TextViews, TextControllers, TextSetters,
+		Dialog, Services;
+
+	CONST
+		expanded* = FALSE; collapsed* = TRUE;
+		minVersion = 0; currentVersion = 0;
+
+		collapseFoldKey = "#Std:Collapse Fold";
+		expandFoldKey = "#Std:Expand Fold";
+		zoomInKey = "#Std:Zoom In";
+		zoomOutKey = "#Std:Zoom Out";
+		expandFoldsKey = "#Std:Expand Folds";
+		collapseFoldsKey = "#Std:Collapse Folds";
+		insertFoldKey = "#Std:Insert Fold";
+		setLabelKey = "#Std:Set Label";
+
+
+	TYPE
+		Label* = ARRAY 32 OF CHAR;
+		
+		Fold* = POINTER TO RECORD (Views.View)
+			leftSide-: BOOLEAN;
+			collapsed-: BOOLEAN;
+			label-: Label; (* valid iff leftSide *)
+			hidden: TextModels.Model (* valid iff leftSide; NIL if no hidden text *)
+		END;
+
+		Directory* = POINTER TO ABSTRACT RECORD END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+		FlipOp = POINTER TO RECORD (Domains.Operation)
+			text: TextModels.Model; (* containing text *)
+			leftpos, rightpos: INTEGER (* position of left and right Fold *)
+		END;
+		
+		SetLabelOp = POINTER TO RECORD (Domains.Operation)
+			text: TextModels.Model; (* containing text *)
+			pos: INTEGER; (* position of fold in text *)
+			oldlabel: Label
+		END;
+		
+		Action = POINTER TO RECORD (Services.Action) END;
+
+		
+	VAR
+		dir-, stdDir-: Directory;
+
+		foldData*: RECORD
+			nested*: BOOLEAN;
+			all*: BOOLEAN;
+			findLabel*: Label;
+			newLabel*: Label
+		END;
+
+		iconFont: Fonts.Typeface;
+		leftExp, rightExp, leftColl, rightColl: ARRAY 8 OF SHORTCHAR;
+		coloredBackg: BOOLEAN;
+		action: Action;
+		fingerprint: INTEGER;	(* for the property inspector *)
+
+		PROCEDURE (d: Directory) New* (collapsed: BOOLEAN; label: Label;
+																	hiddenText: TextModels.Model): Fold, NEW, ABSTRACT;
+
+
+	PROCEDURE GetPair (fold: Fold; VAR l, r: Fold);
+		VAR c: Models.Context; text: TextModels.Model; rd: TextModels.Reader; v: Views.View;
+			nest: INTEGER;
+	BEGIN
+		c := fold.context; l := NIL; r := NIL;
+		WITH c: TextModels.Context DO
+			text := c.ThisModel(); rd := text.NewReader(NIL);
+			IF fold.leftSide THEN l := fold;
+				rd.SetPos(c.Pos()+1); nest := 1;
+				REPEAT rd.ReadView(v);
+					IF (v # NIL) & (v IS Fold) THEN
+						IF v(Fold).leftSide THEN INC(nest) ELSE DEC(nest) END
+					END
+				UNTIL (v = NIL) OR (nest = 0);
+				IF v # NIL THEN r := v(Fold) ELSE r := NIL END
+			ELSE r := fold;
+				rd.SetPos(c.Pos()); nest := 1;
+				REPEAT rd.ReadPrevView(v);
+					IF (v # NIL) & (v IS Fold) THEN
+						IF ~v(Fold).leftSide THEN INC(nest) ELSE DEC(nest) END
+					END
+				UNTIL (v = NIL) OR (nest = 0);
+				IF v # NIL THEN l := v(Fold) ELSE l := NIL END
+			END
+		ELSE (* fold not embedded in a text *)
+		END;
+		ASSERT((l = NIL) OR l.leftSide & (l.hidden # NIL), 100);
+		ASSERT((r = NIL) OR ~r.leftSide & (r.hidden = NIL), 101)
+	END GetPair;
+
+	PROCEDURE (fold: Fold) HiddenText* (): TextModels.Model, NEW;
+		VAR l, r: Fold;
+	BEGIN
+		IF fold.leftSide THEN RETURN fold.hidden
+		ELSE GetPair(fold, l, r);
+			IF l # NIL THEN RETURN l.hidden ELSE RETURN NIL END
+		END
+	END HiddenText;
+
+	PROCEDURE (fold: Fold) MatchingFold* (): Fold, NEW;
+		VAR l, r: Fold;
+	BEGIN
+		GetPair(fold, l, r);
+		IF l # NIL THEN
+			IF fold = l THEN RETURN r ELSE RETURN l END
+		ELSE RETURN NIL
+		END
+	END MatchingFold;
+
+	PROCEDURE GetIcon (fold: Fold; VAR icon: ARRAY OF SHORTCHAR);
+	BEGIN
+		IF fold.leftSide THEN
+			IF fold.collapsed THEN icon := leftColl$ ELSE icon := leftExp$ END
+		ELSE
+			 IF fold.collapsed THEN icon := rightColl$ ELSE icon := rightExp$ END
+		END
+	END GetIcon;
+
+	PROCEDURE CalcSize (f: Fold; VAR w, h: INTEGER);
+		VAR icon: ARRAY 8 OF SHORTCHAR; c: Models.Context; a: TextModels.Attributes; font: Fonts.Font;
+			asc, dsc, fw: INTEGER;
+	BEGIN
+		GetIcon(f, icon);
+		c := f.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			a := c(TextModels.Context).Attr();
+			font := Fonts.dir.This(iconFont, a.font.size, {}, Fonts.normal)
+		ELSE font := Fonts.dir.Default()
+		END;
+		w := font.SStringWidth(icon);
+		font.GetBounds(asc, dsc, fw);
+		h := asc + dsc
+	END CalcSize;
+
+	PROCEDURE Update (f: Fold);
+		VAR w, h: INTEGER;
+	BEGIN
+		CalcSize(f, w, h);
+		f.context.SetSize(w, h);
+		Views.Update(f, Views.keepFrames)
+	END Update;
+
+	PROCEDURE FlipPair (l, r: Fold);
+		VAR text, hidden: TextModels.Model; cl, cr: Models.Context;
+			lpos, rpos: INTEGER;
+	BEGIN
+		IF (l # NIL) & (r # NIL) THEN
+			ASSERT(l.leftSide, 100);
+			ASSERT(~r.leftSide, 101);
+			ASSERT(l.hidden # NIL, 102);
+			ASSERT(r.hidden = NIL, 103);
+			cl := l.context; cr := r.context;
+			text := cl(TextModels.Context).ThisModel();
+			lpos := cl(TextModels.Context).Pos() + 1; rpos := cr(TextModels.Context).Pos();
+			ASSERT(lpos <= rpos, 104);
+			hidden := TextModels.CloneOf(text); 
+			hidden.Insert(0, text, lpos, rpos);
+			text.Insert(lpos, l.hidden, 0, l.hidden.Length());
+			l.hidden := hidden; Stores.Join(l, hidden);
+			l.collapsed := ~l.collapsed;
+			r.collapsed := l.collapsed;
+			Update(l); Update(r);
+			TextControllers.SetCaret(text, lpos)
+		END
+	END FlipPair;
+
+	PROCEDURE (op: FlipOp) Do;
+		VAR rd: TextModels.Reader; left, right: Views.View;
+	BEGIN
+		rd := op.text.NewReader(NIL);
+		rd.SetPos(op.leftpos); rd.ReadView(left);
+		rd.SetPos(op.rightpos); rd.ReadView(right);
+		FlipPair(left(Fold), right(Fold));
+		op.leftpos := left.context(TextModels.Context).Pos();
+		op.rightpos := right.context(TextModels.Context).Pos()
+	END Do;
+
+	PROCEDURE (op: SetLabelOp) Do;
+		VAR rd: TextModels.Reader; fold: Views.View; left, right: Fold; lab: Label;
+	BEGIN
+		rd := op.text.NewReader(NIL);
+		rd.SetPos(op.pos); rd.ReadView(fold);
+		WITH fold: Fold DO
+			GetPair(fold, left, right);
+			IF left # NIL THEN
+				lab := fold.label; left.label := op.oldlabel; op.oldlabel := lab;
+				right.label := left.label
+			END
+		END
+	END Do;
+
+	PROCEDURE SetProp (fold: Fold; p : Properties.Property);
+		VAR op: SetLabelOp; left, right: Fold;
+	BEGIN
+		WHILE p # NIL DO
+			WITH p: Controls.Prop DO
+				IF (Controls.label IN p.valid) & (p.label # fold.label) THEN
+					GetPair(fold, left, right);
+					IF left # NIL THEN
+						NEW(op); op.oldlabel := p.label$;
+						op.text := fold.context(TextModels.Context).ThisModel();
+						op.pos := fold.context(TextModels.Context).Pos();
+						Views.Do(fold, setLabelKey, op)
+					END
+				END
+			ELSE
+			END;
+			p := p.next
+		END
+	END SetProp;
+
+	PROCEDURE (fold: Fold) Flip*, NEW;
+		VAR op: FlipOp; left, right: Fold;
+	BEGIN
+		ASSERT(fold # NIL, 20);
+		NEW(op);
+		GetPair(fold, left, right);
+		IF (left # NIL) & (right # NIL) THEN
+			op.text := fold.context(TextModels.Context).ThisModel();
+			op.leftpos := left.context(TextModels.Context).Pos();
+			op.rightpos := right.context(TextModels.Context).Pos();
+			Views.BeginModification(Views.clean, fold);
+			IF ~left.collapsed THEN Views.Do(fold, collapseFoldKey, op)
+			ELSE Views.Do(fold, expandFoldKey, op)
+			END;
+			Views.EndModification(Views.clean, fold)
+		END
+	END Flip;
+
+	PROCEDURE ReadNext (rd: TextModels.Reader; VAR fold: Fold);
+		VAR v: Views.View;
+	BEGIN
+		REPEAT rd.ReadView(v) UNTIL rd.eot OR (v IS Fold);
+		IF ~rd.eot THEN fold := v(Fold) ELSE fold := NIL END
+	END ReadNext;
+
+	PROCEDURE (fold: Fold) FlipNested*, NEW;
+		VAR text: TextModels.Model; rd: TextModels.Reader; l, r: Fold; level: INTEGER;
+			op: Domains.Operation;
+	BEGIN
+		ASSERT(fold # NIL, 20);
+		GetPair(fold, l, r);
+		IF (l # NIL) & (l.context # NIL) & (l.context IS TextModels.Context) THEN
+			text := l.context(TextModels.Context).ThisModel();
+			Models.BeginModification(Models.clean, text);
+			rd := text.NewReader(NIL);
+			rd.SetPos(l.context(TextModels.Context).Pos());
+			IF l.collapsed THEN
+				Models.BeginScript(text, expandFoldsKey, op);
+				ReadNext(rd, fold); level := 1;
+				WHILE (fold # NIL) & (level > 0) DO
+					IF fold.leftSide & fold.collapsed THEN fold.Flip END;
+					ReadNext(rd, fold);
+					IF fold.leftSide THEN INC(level) ELSE DEC(level) END
+				END
+			ELSE (* l.state = expanded *)
+				Models.BeginScript(text, collapseFoldsKey, op);
+				level := 0;
+				REPEAT ReadNext(rd, fold);
+					IF fold.leftSide THEN INC(level) ELSE DEC(level) END;
+					IF (fold # NIL) & ~fold.leftSide & ~fold.collapsed THEN
+						fold.Flip;
+						rd.SetPos(fold.context(TextModels.Context).Pos()+1)
+					END
+				UNTIL (fold = NIL) OR (level = 0)
+			END;
+			Models.EndScript(text, op);
+			Models.EndModification(Models.clean, text)
+		END
+	END FlipNested;
+
+	PROCEDURE (fold: Fold) HandlePropMsg- (VAR msg: Properties.Message);
+		VAR prop: Controls.Prop; c: Models.Context; a: TextModels.Attributes; asc, w: INTEGER;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			CalcSize(fold, msg.w, msg.h)
+		| msg: Properties.ResizePref DO
+			msg.fixed := TRUE
+		| msg: Properties.FocusPref DO msg.hotFocus := TRUE
+		| msg: Properties.PollMsg DO NEW(prop);
+			prop.known := {Controls.label}; prop.valid := {Controls.label}; prop.readOnly := {};
+			prop.label := fold.label$;
+			msg.prop := prop
+		| msg: Properties.SetMsg DO SetProp(fold, msg.prop)
+		| msg: TextSetters.Pref DO c := fold.context;
+			IF (c # NIL) & (c IS TextModels.Context) THEN
+				a := c(TextModels.Context).Attr();
+				a.font.GetBounds(asc, msg.dsc, w)
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+
+	PROCEDURE Track (fold: Fold; f: Views.Frame; x, y: INTEGER; buttons: SET; VAR hit: BOOLEAN);
+		VAR a: TextModels.Attributes; font: Fonts.Font; c: Models.Context;
+			w, h, asc, dsc, fw: INTEGER; isDown, in, in0: BOOLEAN; modifiers: SET;
+	BEGIN
+		c := fold.context; hit := FALSE;
+		WITH c: TextModels.Context DO
+			a := c.Attr(); font := a.font;
+			c.GetSize(w, h); in0 := FALSE;
+			in := (0 <= x) & (x < w) & (0 <= y) & (y < h);
+			REPEAT
+				IF in # in0 THEN
+					f.MarkRect(0, 0, w, h, Ports.fill, Ports.hilite, FALSE); in0 := in
+				END;
+				f.Input(x, y, modifiers, isDown);
+				in := (0 <= x) & (x < w) & (0 <= y) & (y < h)
+			UNTIL ~isDown;
+			IF in0 THEN hit := TRUE;
+				font.GetBounds(asc, dsc, fw);
+				f.MarkRect(0, 0, w, asc + dsc, Ports.fill, Ports.hilite, FALSE)
+			END
+		ELSE
+		END
+	END Track;
+
+		PROCEDURE (fold: Fold) HandleCtrlMsg* (f: Views.Frame; VAR msg: Views.CtrlMessage;
+																							VAR focus: Views.View);
+			VAR hit: BOOLEAN; pos: INTEGER; l, r: Fold;
+				context: TextModels.Context; text: TextModels.Model;
+		BEGIN
+			WITH msg: Controllers.TrackMsg DO
+				IF fold.context IS TextModels.Context THEN
+					Track(fold, f, msg.x, msg.y, msg.modifiers, hit);
+					IF hit THEN
+						IF Controllers.modify IN msg.modifiers THEN
+							fold.FlipNested
+						ELSE
+							fold.Flip;
+							context := fold.context(TextModels.Context);
+							text := context.ThisModel();
+							IF TextViews.FocusText() = text THEN
+								GetPair(fold, l, r);
+								pos := context.Pos();
+								IF fold = l THEN
+									TextControllers.SetCaret(text, pos + 1)
+								ELSE
+									TextControllers.SetCaret(text, pos)
+								END;
+								TextViews.ShowRange(text, pos, pos + 1, TRUE)
+							END
+						END
+					END
+				END
+			| msg: Controllers.PollCursorMsg DO
+				msg.cursor := Ports.refCursor
+			ELSE
+			END
+		END HandleCtrlMsg;
+
+	PROCEDURE (fold: Fold) Restore* (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR a: TextModels.Attributes; color: Ports.Color; c: Models.Context; font: Fonts.Font;
+			icon: ARRAY 8 OF SHORTCHAR; w, h: INTEGER; asc, dsc, fw: INTEGER;
+	BEGIN
+		GetIcon(fold, icon); c := fold.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			a := fold.context(TextModels.Context).Attr();
+			font := Fonts.dir.This(iconFont, a.font.size, {}, Fonts.normal);
+			color := a.color
+		ELSE font := Fonts.dir.Default(); color := Ports.black
+		END;
+		IF coloredBackg THEN
+			fold.context.GetSize(w, h);
+			f.DrawRect(f.l, f.dot, f.r, h-f.dot, Ports.fill, Ports.grey50);
+			color := Ports.white
+		END;
+		font.GetBounds(asc, dsc, fw);
+		f.DrawSString(0, asc, color, icon, font)
+	END Restore;
+
+	PROCEDURE (fold: Fold) CopyFromSimpleView- (source: Views.View);
+	BEGIN
+		(* fold.CopyFrom^(source); *)
+		WITH source: Fold DO
+			ASSERT(source.leftSide = (source.hidden # NIL), 100);
+			fold.leftSide := source.leftSide;
+			fold.collapsed := source.collapsed;
+			fold.label := source.label;
+			IF source.hidden # NIL THEN
+				fold.hidden := TextModels.CloneOf(source.hidden); Stores.Join(fold.hidden, fold);
+				fold.hidden.InsertCopy(0, source.hidden, 0, source.hidden.Length())
+			END
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (fold: Fold) Internalize- (VAR rd: Stores.Reader);
+		VAR version: INTEGER; store: Stores.Store; xint: INTEGER;
+	BEGIN
+		fold.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, currentVersion, version);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadXInt(xint);fold.leftSide := xint = 0;
+		rd.ReadXInt(xint); fold.collapsed := xint = 0;
+		rd.ReadXString(fold.label);
+		rd.ReadStore(store);
+		IF store # NIL THEN fold.hidden := store(TextModels.Model); Stores.Join(fold.hidden, fold)
+		ELSE fold.hidden := NIL
+		END;
+		fold.leftSide := store # NIL
+	END Internalize;
+
+	PROCEDURE (fold: Fold) Externalize- (VAR wr: Stores.Writer);
+		VAR xint: INTEGER;
+	BEGIN
+		fold.Externalize^(wr);
+		wr.WriteVersion(currentVersion);
+		IF fold.hidden # NIL THEN xint := 0 ELSE xint := 1 END;
+		wr.WriteXInt(xint);
+		IF fold.collapsed THEN xint := 0 ELSE xint := 1 END;
+		wr.WriteXInt(xint);
+		wr.WriteXString(fold.label);
+		wr.WriteStore(fold.hidden)
+	END Externalize;
+
+	(* --------------------- expanding and collapsing in focus text ------------------------ *)
+
+	PROCEDURE ExpandFolds* (text: TextModels.Model; nested: BOOLEAN; IN label: ARRAY OF CHAR);
+		VAR op: Domains.Operation; fold, l, r: Fold; rd: TextModels.Reader;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		Models.BeginModification(Models.clean, text);
+		IF nested THEN Models.BeginScript(text, expandFoldsKey, op)
+		ELSE Models.BeginScript(text, zoomInKey, op)
+		END;
+		rd := text.NewReader(NIL); rd.SetPos(0);
+		ReadNext(rd, fold);
+		WHILE ~rd.eot DO
+			IF fold.leftSide & fold.collapsed THEN
+				IF (label = "") OR (label = fold.label) THEN
+					fold.Flip;
+					IF ~nested THEN 
+						GetPair(fold, l, r);
+						rd.SetPos(r.context(TextModels.Context).Pos())
+					END
+				END
+			END;
+			ReadNext(rd, fold)
+		END;
+		Models.EndScript(text, op);
+		Models.EndModification(Models.clean, text)
+	END ExpandFolds;
+
+	PROCEDURE CollapseFolds* (text: TextModels.Model; nested: BOOLEAN; IN label: ARRAY OF CHAR);
+		VAR op: Domains.Operation; fold, r, l: Fold; rd: TextModels.Reader;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		Models.BeginModification(Models.clean, text);
+		IF nested THEN Models.BeginScript(text, collapseFoldsKey, op)
+		ELSE Models.BeginScript(text, zoomOutKey, op)
+		END;
+		rd := text.NewReader(NIL); rd.SetPos(0);
+		ReadNext(rd, fold);
+		WHILE ~rd.eot DO
+			IF ~fold.leftSide & ~fold.collapsed THEN
+				GetPair(fold, l, r);
+				IF (label = "") OR (label = l.label) THEN
+					fold.Flip;
+					GetPair(l, l, r);
+					rd.SetPos(r.context(TextModels.Context).Pos()+1);
+					IF ~nested THEN REPEAT ReadNext(rd, fold) UNTIL rd.eot OR fold.leftSide
+					ELSE ReadNext(rd, fold)
+					END
+				ELSE ReadNext(rd, fold)
+				END
+			ELSE ReadNext(rd, fold)
+			END
+		END;
+		Models.EndScript(text, op);
+		Models.EndModification(Models.clean, text)
+	END CollapseFolds;
+
+	PROCEDURE ZoomIn*;
+		VAR text: TextModels.Model;
+	BEGIN
+		text := TextViews.FocusText();
+		IF text # NIL THEN ExpandFolds(text, FALSE, "") END
+	END ZoomIn;
+
+	PROCEDURE ZoomOut*;
+		VAR text: TextModels.Model;
+	BEGIN
+		text := TextViews.FocusText();
+		IF text # NIL THEN CollapseFolds(text, FALSE, "") END
+	END ZoomOut;
+
+	PROCEDURE Expand*;
+		VAR text: TextModels.Model;
+	BEGIN
+		text := TextViews.FocusText();
+		IF text # NIL THEN ExpandFolds(text, TRUE, "") END
+	END Expand;
+
+	PROCEDURE Collapse*;
+		VAR text: TextModels.Model;
+	BEGIN
+		text := TextViews.FocusText();
+		IF text # NIL THEN CollapseFolds(text, TRUE, "") END
+	END Collapse;
+
+	(* ---------------------- foldData dialogbox --------------------------- *)
+
+	PROCEDURE FindLabelGuard* (VAR par: Dialog.Par);
+	BEGIN
+		par.disabled := (TextViews.Focus() = NIL) OR foldData.all
+	END FindLabelGuard;
+		
+	PROCEDURE SetLabelGuard* ( VAR p : Dialog.Par );
+		VAR v: Views.View;
+	BEGIN
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		v := Containers.FocusSingleton();
+		p.disabled := (v = NIL) OR ~(v IS Fold) OR ~v(Fold).leftSide;
+		Controllers.ResetCurrentPath()
+	END SetLabelGuard;
+
+	PROCEDURE ExpandLabel*;
+		VAR text: TextModels.Model;
+	BEGIN
+		IF foldData.all & (foldData.findLabel # "") THEN
+			foldData.findLabel := ""; Dialog.Update(foldData)
+		END;
+		text := TextViews.FocusText();
+		IF text # NIL THEN
+			IF ~foldData.all THEN ExpandFolds(text, foldData.nested, foldData.findLabel)
+			ELSE ExpandFolds(text, foldData.nested, "")
+			END
+		END
+	END ExpandLabel;
+
+	PROCEDURE CollapseLabel*;
+		VAR text: TextModels.Model;
+	BEGIN
+		IF foldData.all & (foldData.findLabel # "") THEN
+			foldData.findLabel := ""; Dialog.Update(foldData)
+		END;
+		text := TextViews.FocusText();
+		IF text # NIL THEN
+			IF ~foldData.all THEN CollapseFolds(text, foldData.nested, foldData.findLabel)
+			ELSE CollapseFolds(text, foldData.nested, "")
+			END
+		END
+	END CollapseLabel;
+
+	PROCEDURE FindFold(first: BOOLEAN);
+	VAR c : TextControllers.Controller; r: TextModels.Reader; 
+		v : Views.View; pos, i : INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF first THEN pos := 0 
+			ELSE
+				pos := c.CaretPos();
+				IF pos = TextControllers.none THEN
+					c.GetSelection(i, pos);
+					IF pos = i THEN pos := 0 ELSE INC(pos) END;
+					pos := MIN(pos, c.text.Length()-1)
+				END
+			END;
+			r := c.text.NewReader(NIL); r.SetPos(pos);
+			REPEAT r.ReadView(v)
+			UNTIL r.eot OR ((v IS Fold) & v(Fold).leftSide) & (foldData.all OR (v(Fold).label$ = foldData.findLabel$));
+			IF r.eot THEN
+				c.SetCaret(0); Dialog.Beep
+			ELSE
+				pos := r.Pos();
+				c.view.ShowRange(pos-1, pos, FALSE);
+				c.SetSelection(pos-1, pos);
+				IF LEN(v(Fold).label) > 0 THEN
+					foldData.newLabel := v(Fold).label
+				END;
+				Dialog.Update(foldData)
+			END
+		ELSE
+			Dialog.Beep
+		END
+	END FindFold;
+
+	PROCEDURE FindNextFold*;
+	BEGIN
+		FindFold(FALSE)
+	END FindNextFold;
+	
+	PROCEDURE FindFirstFold*;
+	BEGIN
+		FindFold(TRUE)
+	END FindFirstFold;
+	
+	PROCEDURE SetLabel*;
+		VAR v: Views.View;
+	BEGIN
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		v := Containers.FocusSingleton();
+		IF (v # NIL) & (v IS Fold) & (LEN(foldData.newLabel) > 0) THEN
+			v(Fold).label := foldData.newLabel
+		ELSE
+			Dialog.Beep
+		END;
+		Controllers.ResetCurrentPath()
+	END SetLabel;
+
+	PROCEDURE (a: Action) Do;
+		VAR v: Views.View; fp: INTEGER;
+	BEGIN
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		v := Containers.FocusSingleton();
+		IF (v = NIL) OR ~(v IS Fold) THEN 
+			fingerprint := 0;
+			foldData.newLabel := ""
+		ELSE 
+			fp := Services.AdrOf(v);
+			IF fp # fingerprint THEN 
+				foldData.newLabel := v(Fold).label;
+				fingerprint := fp;
+				Dialog.Update(foldData)
+			END
+		END;
+		Controllers.ResetCurrentPath();
+		Services.DoLater(action, Services.Ticks() + Services.resolution DIV 2)
+	END Do;
+
+	(* ------------------------ inserting folds ------------------------ *)
+		
+	PROCEDURE Overlaps* (text: TextModels.Model; beg, end: INTEGER): BOOLEAN;
+		VAR n, level: INTEGER; rd: TextModels.Reader; v: Views.View;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		ASSERT((beg >= 0) & (end <= text.Length()) & (beg <= end), 21);
+		rd := text.NewReader(NIL); rd.SetPos(beg);
+		n := 0; level := 0;
+		REPEAT rd.ReadView(v);
+			IF ~rd.eot & (rd.Pos() <= end) THEN
+				WITH v: Fold DO INC(n);
+					IF v.leftSide THEN INC(level) ELSE DEC(level) END
+				ELSE
+				END
+			END
+		UNTIL rd.eot OR (level < 0) OR (rd.Pos() >= end);
+		RETURN (level # 0) OR ODD(n)
+	END Overlaps;
+
+	PROCEDURE InsertionAttr (text: TextModels.Model; pos: INTEGER): TextModels.Attributes;
+		VAR rd: TextModels.Reader; ch: CHAR;
+	BEGIN
+		rd := text.NewReader(NIL);
+		rd.SetPos(pos); rd.ReadChar(ch);
+		RETURN rd.attr
+	END InsertionAttr;
+
+	PROCEDURE Insert* (text: TextModels.Model; label: Label; beg, end: INTEGER; collapsed: BOOLEAN);
+		VAR w: TextModels.Writer; fold: Fold; insop: Domains.Operation; a: TextModels.Attributes;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		ASSERT((beg >= 0) & (end <= text.Length()) & (beg <= end), 21);
+		a := InsertionAttr(text, beg);
+		w := text.NewWriter(NIL); w.SetPos(beg);
+		IF a # NIL THEN w.SetAttr(a) END;
+		NEW(fold);
+		fold.leftSide := TRUE; fold.collapsed := collapsed;
+		fold.hidden := TextModels.CloneOf(text); Stores.Join(fold, fold.hidden);
+		fold.label := label$;
+		Models.BeginScript(text, insertFoldKey, insop);
+		w.WriteView(fold, 0, 0);
+		w.SetPos(end+1);
+		a := InsertionAttr(text, end+1);
+		IF a # NIL THEN w.SetAttr(a) END;
+		NEW(fold);
+		fold.leftSide := FALSE; fold.collapsed := collapsed;
+		fold.hidden := NIL; fold.label := "";
+		w.WriteView(fold, 0, 0);
+		Models.EndScript(text, insop)
+	END Insert;
+
+	PROCEDURE CreateGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller; beg, end: INTEGER;
+	BEGIN c := TextControllers.Focus();
+		IF (c # NIL) &  ~(Containers.noCaret IN c.opts) THEN
+			IF c.HasSelection() THEN c.GetSelection(beg, end);
+				IF Overlaps(c.text, beg, end) THEN par.disabled := TRUE END
+			END
+		ELSE par.disabled := TRUE
+		END
+	END CreateGuard;
+
+	PROCEDURE Create* (state: INTEGER);	(* menu cmd parameters don't accept Booleans *)
+		VAR c: TextControllers.Controller; beg, end: INTEGER; collapsed: BOOLEAN;
+	BEGIN
+		collapsed := state = 0;
+		c := TextControllers.Focus();
+		IF (c # NIL) & ~(Containers.noCaret IN c.opts) THEN
+			IF c.HasSelection() THEN c.GetSelection(beg, end);
+				IF ~Overlaps(c.text, beg, end) THEN Insert(c.text, "", beg, end, collapsed) END
+			ELSE beg := c.CaretPos(); Insert(c.text, "", beg, beg, collapsed)
+			END
+		END
+	END Create;
+
+	PROCEDURE InitIcons;
+		VAR font: Fonts.Font;
+
+		PROCEDURE DefaultAppearance;
+		BEGIN
+			font := Fonts.dir.Default(); iconFont := font.typeface$;
+			leftExp := ">"; rightExp := "<";
+			leftColl := "=>"; rightColl := "<=";
+			coloredBackg := TRUE
+		END DefaultAppearance;
+
+	BEGIN
+		IF Dialog.platform = Dialog.linux THEN (* Linux *)
+			DefaultAppearance; 
+			coloredBackg := FALSE
+		ELSIF Dialog.platform DIV 10 = 1 THEN (* Windows *)
+			iconFont := "Wingdings";
+			font := Fonts.dir.This(iconFont, 10*Fonts.point (*arbitrary*), {}, Fonts.normal);
+			IF font.IsAlien() THEN DefaultAppearance
+			ELSE
+				leftExp[0] := SHORT(CHR(240)); leftExp[1] := 0X;
+				rightExp[0] := SHORT(CHR(239)); rightExp[1] := 0X;
+				leftColl[0] := SHORT(CHR(232)); leftColl[1] := 0X;
+				rightColl[0] := SHORT(CHR(231)); rightColl[1] := 0X;
+				coloredBackg := FALSE
+			END
+		ELSIF Dialog.platform DIV 10 = 2 THEN (* Mac *)
+			iconFont := "Chicago";
+			font := Fonts.dir.This(iconFont, 10*Fonts.point (*arbitrary*), {}, Fonts.normal);
+			IF font.IsAlien() THEN DefaultAppearance
+			ELSE
+				leftExp := ">"; rightExp := "<";
+				leftColl := "»"; rightColl := "«";
+				coloredBackg := TRUE
+			END
+		ELSE
+			DefaultAppearance
+		END
+	END InitIcons;
+
+	PROCEDURE (d: StdDirectory) New (collapsed: BOOLEAN; label: Label;
+																hiddenText: TextModels.Model): Fold;
+		VAR fold: Fold;
+	BEGIN
+		NEW(fold); fold.leftSide := hiddenText # NIL; fold.collapsed := collapsed;
+		fold.label := label; fold.hidden := hiddenText; 
+		IF hiddenText # NIL THEN Stores.Join(fold, fold.hidden) END;
+		RETURN fold
+	END  New;
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20);
+		dir := d
+	END SetDir;
+
+	PROCEDURE InitMod;
+		VAR d: StdDirectory;
+	BEGIN
+		foldData.all := TRUE; foldData.nested := FALSE; foldData.findLabel := ""; foldData.newLabel := "";
+		NEW(d); dir := d; stdDir := d;
+		InitIcons;
+		NEW(action); Services.DoLater(action, Services.now);
+	END InitMod;
+
+BEGIN
+	InitMod
+END StdFolds.

+ 436 - 0
BlackBox/Std/Mod/Headers.txt

@@ -0,0 +1,436 @@
+MODULE StdHeaders;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Headers.odc *)
+	(* DO NOT EDIT *)
+
+(* headers / footers support the following macros:
+
+			&p - replaced by current page number as arabic numeral
+			&r - replaced by current page number as roman numeral
+			&R - replaced by current page number as capital roman numeral
+			&a - replaced by current page number as alphanumeric character
+			&A - replaced by current page number as capital alphanumeric character
+			&d - replaced by printing date 
+			&t - replaced by printing time
+			&&- replaced by & character
+			&; - specifies split point
+			&f - filename with path/title
+
+*)
+
+	IMPORT
+		Stores, Ports, Models, Views, Properties, Printing, TextModels, Fonts, Dialog,
+		TextViews, Dates, Windows, Controllers, Containers;
+
+	CONST
+		minVersion = 0; maxVersion = 2;
+		mm = Ports.mm; point = Ports.point;
+		maxWidth = 10000 * mm;
+		alternate* = 0; number* = 1; head* = 2; foot* = 3; showFoot* = 4;
+
+	TYPE
+		Banner* = RECORD
+			left*, right*: ARRAY 128 OF CHAR;
+			gap*: INTEGER
+		END;
+		
+		NumberInfo* = RECORD
+			new*: BOOLEAN;
+			first*: INTEGER
+		END;
+		
+		View = POINTER TO RECORD (Views.View)
+			alternate: BOOLEAN;	(* alternate left/right *)
+			number: NumberInfo;	(* new page number *)
+			head, foot: Banner;
+			font: Fonts.Font;
+			showFoot: BOOLEAN;
+		END;
+
+		Prop* = POINTER TO RECORD (Properties.Property)
+			alternate*, showFoot*: BOOLEAN;
+			number*: NumberInfo;
+			head*, foot*: Banner
+		END;
+
+		ChangeFontOp = POINTER TO RECORD (Stores.Operation)
+			header: View;
+			font: Fonts.Font
+		END;
+
+		ChangeAttrOp = POINTER TO RECORD (Stores.Operation)
+			header: View;
+			alternate, showFoot: BOOLEAN;
+			number: NumberInfo;
+			head, foot: Banner
+		END;
+
+	VAR
+		dialog*: RECORD
+			view: View;
+			alternate*, showFoot*: BOOLEAN;
+			number*: NumberInfo;
+			head*, foot*: Banner;
+		END;
+		
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;
+		PROCEDURE Equal(IN b1, b2: Banner): BOOLEAN;
+		BEGIN
+			RETURN (b1.left = b2.left) & (b1.right = b2.right) & (b1.gap = b2.gap)
+		END Equal;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			IF p.alternate # q.alternate THEN EXCL(valid, alternate) END;
+			IF p.showFoot # q.showFoot THEN EXCL(valid, showFoot) END;
+			IF (p.number.new # q.number.new) OR (p.number.first # q.number.first) THEN EXCL(valid, number) END;
+			IF ~Equal(p.head, q.head) THEN EXCL(valid, head) END;
+			IF ~Equal(p.foot, q.foot) THEN EXCL(valid, foot) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+		
+	(* SetAttrOp *)
+
+	PROCEDURE (op: ChangeFontOp) Do;
+		VAR v: View; font: Fonts.Font; asc, dsc, w: INTEGER; c: Models.Context;
+	BEGIN
+		v := op.header;
+		font := op.font; op.font := v.font; v.font := font;
+		font.GetBounds(asc, dsc, w);
+		c := v.context;
+		c.SetSize(maxWidth, asc + dsc + 2*point);
+		Views.Update(v, Views.keepFrames)
+	END Do;
+
+	PROCEDURE DoChangeFontOp (v: View; font: Fonts.Font);
+		VAR op: ChangeFontOp;
+	BEGIN
+		IF v.font # font THEN
+			NEW(op); op.header := v; op.font := font;
+			Views.Do(v,   "#System:SetProp", op)
+		END
+	END DoChangeFontOp;
+
+	PROCEDURE (op: ChangeAttrOp) Do;
+		VAR v: View; alternate, showFoot: BOOLEAN; number: NumberInfo; head, foot: Banner;
+	BEGIN
+		v := op.header;
+		alternate := op.alternate; showFoot := op.showFoot; number := op.number; head := op.head; foot := op.foot;
+		op.alternate := v.alternate; op.showFoot := v.showFoot; op.number := v.number; op.head := v.head;
+		op.foot := v.foot;
+		v.alternate := alternate; v.showFoot := showFoot; v.number := number; v.head := head; v.foot := foot;
+		Views.Update(v, Views.keepFrames)
+	END Do;
+
+	PROCEDURE DoChangeAttrOp (v: View; alternate, showFoot: BOOLEAN; number: NumberInfo;
+													head, foot: Banner);
+		VAR op: ChangeAttrOp;
+	BEGIN
+		NEW(op); op.header := v; op.alternate := alternate; op.showFoot := showFoot; 
+		op.number := number; op.head := head; op.foot := foot;
+		Views.Do(v,   "#Std:HeaderChange", op)
+	END DoChangeAttrOp;
+
+	PROCEDURE (v: View) CopyFromSimpleView (source: Views.View);
+	BEGIN
+		WITH source: View DO
+			v.alternate := source.alternate;
+			v.number.new := source.number.new; v.number.first := source.number.first;
+			v.head := source.head;
+			v.foot := source.foot;
+			v.font := source.font;
+			v.showFoot := source.showFoot
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		v.Externalize^(wr);
+		wr.WriteVersion(maxVersion);
+		wr.WriteString(v.head.left);
+		wr.WriteString(v.head.right);
+		wr.WriteInt(v.head.gap);
+		wr.WriteString(v.foot.left);
+		wr.WriteString(v.foot.right);
+		wr.WriteInt(v.foot.gap);
+		wr.WriteString(v.font.typeface);
+		wr.WriteInt(v.font.size);
+		wr.WriteSet(v.font.style);
+		wr.WriteInt(v.font.weight);
+		wr.WriteBool(v.alternate);
+		wr.WriteBool(v.number.new);
+		wr.WriteInt(v.number.first);
+		wr.WriteBool(v.showFoot);
+	END Externalize;
+	
+	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
+		VAR version: INTEGER; typeface: Fonts.Typeface; size: INTEGER; style: SET; weight: INTEGER;
+
+	BEGIN
+		v.Internalize^(rd);
+		IF ~rd.cancelled THEN
+			rd.ReadVersion(minVersion, maxVersion, version);
+			IF ~rd.cancelled THEN
+				IF version = 0 THEN
+					rd.ReadXString(v.head.left);
+					rd.ReadXString(v.head.right); 
+					v.head.gap := 5*mm;
+					rd.ReadXString(v.foot.left);
+					rd.ReadXString(v.foot.right);
+					v.foot.gap := 5*mm;
+					rd.ReadXString(typeface);
+					rd.ReadXInt(size); 
+					v.font := Fonts.dir.This(typeface, size * point, {}, Fonts.normal);
+					rd.ReadXInt(v.number.first);
+					rd.ReadBool(v.number.new);
+					rd.ReadBool(v.alternate)
+				ELSE
+					rd.ReadString(v.head.left);
+					rd.ReadString(v.head.right);
+					rd.ReadInt(v.head.gap);
+					rd.ReadString(v.foot.left);
+					rd.ReadString(v.foot.right);
+					rd.ReadInt(v.foot.gap);
+					rd.ReadString(typeface);
+					rd.ReadInt(size);
+					rd.ReadSet(style);
+					rd.ReadInt(weight);
+					v.font := Fonts.dir.This(typeface, size, style, weight);
+					rd.ReadBool(v.alternate);
+					rd.ReadBool(v.number.new);
+					rd.ReadInt(v.number.first);
+					IF version = 2 THEN rd.ReadBool(v.showFoot) ELSE v.showFoot := FALSE END
+				END
+			END
+		END
+	END Internalize;
+
+	PROCEDURE SetProp(v: View; msg: Properties.SetMsg);
+		VAR p: Properties.Property;
+			typeface: Fonts.Typeface; size: INTEGER; style: SET; weight: INTEGER;
+			alt, sf: BOOLEAN; num: NumberInfo; h, f: Banner;
+ 	BEGIN
+		p := msg.prop;
+		WHILE p # NIL DO
+			WITH p: Properties.StdProp DO
+				IF Properties.typeface IN p.valid THEN typeface := p.typeface 
+				ELSE typeface := v.font.typeface
+				END;
+				IF Properties.size IN p.valid THEN size := p.size 
+				ELSE size := v.font.size
+				END;
+				IF Properties.style IN p.valid THEN style := p.style.val
+				ELSE style := v.font.style
+				END;
+				IF Properties.weight IN p.valid THEN weight := p.weight 
+				ELSE weight := v.font.weight
+				END;
+				DoChangeFontOp (v, Fonts.dir.This(typeface, size, style, weight) );
+			| p: Prop DO
+				IF alternate IN p.valid THEN alt := p.alternate ELSE alt := v.alternate END;
+				IF showFoot IN p.valid THEN sf := p.showFoot ELSE sf := v.showFoot END;
+				IF number IN p.valid THEN num := p.number ELSE num := v.number END;
+				IF head IN p.valid THEN h := p.head ELSE h := v.head END;
+				IF foot IN p.valid THEN f := p.foot ELSE f := v.foot END;
+				DoChangeAttrOp(v, alt, sf, num, h, f)
+			ELSE
+			END;
+			p := p.next
+		END
+	END SetProp;
+	
+	PROCEDURE PollProp(v: View; VAR msg: Properties.PollMsg);
+		VAR sp: Properties.StdProp; p: Prop;
+	BEGIN
+		NEW(sp);
+		sp.known := {Properties.size, Properties.typeface, Properties.style, Properties.weight};
+		sp.valid := sp.known;
+		sp.size := v.font.size; sp.typeface := v.font.typeface; 
+		sp.style.val := v.font.style; sp.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
+		sp.weight := v.font.weight;
+		Properties.Insert(msg.prop, sp);
+		NEW(p);
+		p.known := {alternate, number, head, foot, showFoot}; p.valid := p.known;
+		p.head := v.head; p.foot := v.foot;
+		p.alternate := v.alternate;
+		p.showFoot := v.showFoot;
+		p.number := v.number;
+		Properties.Insert(msg.prop, p)
+	END PollProp;
+	
+	PROCEDURE PageMsg(v: View; msg: TextViews.PageMsg);
+	BEGIN
+		IF Printing.par # NIL THEN
+			Dialog.MapString(v.head.left, Printing.par.header.left);
+			Dialog.MapString(v.head.right, Printing.par.header.right);
+			Dialog.MapString(v.foot.left, Printing.par.footer.left);
+			Dialog.MapString(v.foot.right, Printing.par.footer.right);
+			Printing.par.header.font := v.font;
+			Printing.par.footer.font := v.font;
+			Printing.par.page.alternate := v.alternate;
+			IF v.number.new THEN
+				Printing.par.page.first := v.number.first - msg.current
+			END;
+			Printing.par.header.gap := 5*Ports.mm;
+			Printing.par.footer.gap := 5*Ports.mm
+		END
+	END PageMsg;
+
+	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR d, w, h: INTEGER; (*line: Line; *)asc, dsc, x0, x1, y: INTEGER;
+			win: Windows.Window; title: Views.Title; dec: BOOLEAN;
+			pw, ph: INTEGER;
+			date: Dates.Date; time: Dates.Time; pageInfo: Printing.PageInfo; banner: Printing.Banner;
+	BEGIN
+		IF Views.IsPrinterFrame(f) THEN (* am drucken *) END;
+
+		v.font.GetBounds(asc, dsc, w);
+
+		win := Windows.dir.First();
+		WHILE (win # NIL) & (win.doc.Domain() # v.Domain()) DO win := Windows.dir.Next(win) END;
+		IF win = NIL THEN title := "(" + Dialog.appName + ")"
+		ELSE win.GetTitle(title)
+		END;
+		d := f.dot; 
+		v.context.GetSize(w, h);
+		win.doc.PollPage(pw, ph, l, t, r, b, dec);
+		w := r - l;
+
+		f.DrawRect(0, 0, w, h, Ports.fill, Ports.grey25);
+		f.DrawRect(0, 0, w, h, 0, Ports.black);
+		
+		x0 := d; x1 := w-2*d; y := asc + d;
+
+		Dates.GetDate(date);
+		Dates.GetTime(time);
+		pageInfo.alternate := FALSE;
+		pageInfo.title := title;
+		banner.font := v.font;		
+		IF v.showFoot THEN
+			banner.gap := v.foot.gap;
+			Dialog.MapString(v.foot.left, banner.left); Dialog.MapString(v.foot.right, banner.right)
+		ELSE
+			banner.gap := v.head.gap;
+			Dialog.MapString(v.head.left, banner.left); Dialog.MapString(v.head.right, banner.right)
+		END;
+		Printing.PrintBanner(f, pageInfo, banner, date, time, x0, x1, y)
+	END Restore;
+
+	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
+		VAR asc, dsc, w: INTEGER;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			msg.w := maxWidth; 
+			IF msg.h = Views.undefined THEN
+				v.font.GetBounds(asc, dsc, w);
+				msg.h := asc + dsc + 2*point
+			 END
+		| msg: Properties.ResizePref DO
+			msg.fixed := TRUE
+		| msg: TextModels.Pref DO
+			msg.opts := {TextModels.hideable}
+		| msg: Properties.PollMsg DO
+			PollProp(v, msg)
+		| msg: Properties.SetMsg DO
+			SetProp(v, msg)
+		| msg: TextViews.PageMsg DO
+			PageMsg(v, msg)
+		ELSE
+		END
+	END HandlePropMsg;
+	
+	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
+																VAR focus: Views.View);
+	BEGIN
+		WITH msg: Properties.EmitMsg DO Views.HandlePropMsg(v, msg.set)
+		| msg: Properties.CollectMsg DO Views.HandlePropMsg(v, msg.poll)
+		ELSE
+		END
+	END HandleCtrlMsg;
+	
+	PROCEDURE New*(p: Prop; f: Fonts.Font): Views.View;
+		VAR v: View;
+	BEGIN
+		NEW(v);
+		v.head := p.head;
+		v.foot := p.foot;
+		v.number := p.number;
+		v.alternate := p.alternate;
+		v.font := f; 
+		v.showFoot := FALSE;
+		RETURN v;	
+	END New;
+
+	PROCEDURE Deposit*;
+		VAR v: View;
+	BEGIN
+		NEW(v);
+		v.head.left := ""; v.head.right :=  "&d&;&p"; v.head.gap := 5*mm;
+		v.foot.left := ""; v.foot.right := ""; v.foot.gap := 5*mm;
+		v.font := Fonts.dir.Default();
+		v.number.first := 1; v.number.new := FALSE; v.alternate := FALSE; v.showFoot := FALSE;
+		Views.Deposit(v)
+	END Deposit;
+	
+	(* property dialog *)
+	
+	PROCEDURE InitDialog*;
+		VAR  p: Properties.Property;
+	BEGIN
+		Properties.CollectProp(p);
+		WHILE p # NIL DO
+			WITH p: Properties.StdProp DO
+			| p: Prop DO
+				dialog.alternate := p.alternate; dialog.showFoot := p.showFoot;
+				dialog.number := p.number;
+				dialog.head := p.head; dialog.head.gap := dialog.head.gap DIV point;
+				dialog.foot := p.foot; dialog.foot.gap := dialog.foot.gap DIV point;
+				Dialog.Update(dialog)
+			ELSE
+			END;
+			p := p.next
+		END
+	END InitDialog;
+	
+	PROCEDURE Set*;
+		VAR p: Prop;
+	BEGIN
+		NEW(p); p.valid := {alternate, number, head, foot, showFoot};
+		p.alternate := dialog.alternate; p.showFoot := dialog.showFoot;
+		p.number := dialog.number;
+		p.head := dialog.head; p.head.gap := p.head.gap * point;
+		p.foot := dialog.foot; p.foot.gap := p.foot.gap * point;
+		Properties.EmitProp(NIL, p)
+	END Set;
+	
+	PROCEDURE HeaderGuard* (VAR par: Dialog.Par);
+		VAR v: Views.View;
+	BEGIN
+		v := Containers.FocusSingleton();
+		IF (v # NIL) & (v IS View) THEN
+			par.disabled := FALSE;
+			IF (dialog.view = NIL) OR (dialog.view # v) THEN
+				dialog.view := v(View);
+				InitDialog
+			END
+		ELSE
+			par.disabled := TRUE;
+			dialog.view := NIL
+		END
+	END HeaderGuard;
+	
+	PROCEDURE AlternateGuard* (VAR par: Dialog.Par);
+	BEGIN
+		HeaderGuard(par);
+		IF ~par.disabled THEN par.disabled := ~ dialog.alternate END
+	END AlternateGuard;
+	
+	PROCEDURE NewNumberGuard* (VAR par: Dialog.Par);
+	BEGIN
+		HeaderGuard(par);
+		IF ~par.disabled THEN par.disabled := ~ dialog.number.new END
+	END NewNumberGuard;
+	
+END StdHeaders.

+ 893 - 0
BlackBox/Std/Mod/Links.txt

@@ -0,0 +1,893 @@
+MODULE StdLinks;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Links.odc *)
+
+	IMPORT Kernel, Services,
+		Stores, Ports, Fonts,  Models, Views, Controllers, Properties, Dialog, Containers,
+		TextModels, TextMappers, TextViews, TextControllers, TextSetters, TextRulers,
+		Strings, StdCmds;
+
+	CONST
+		kind* = 0; cmd* = 1; close* = 2;	(* constants for Prop.valid *)
+		always* = 0; ifShiftDown* = 1; never* = 2;	(* constants for close attrubute *)
+		minLinkVersion = 0; maxLinkVersion = 1;
+		minTargVersion = 0; maxTargVersion = 0;
+
+	TYPE
+		Directory* = POINTER TO ABSTRACT RECORD END;
+
+		Link* = POINTER TO RECORD (Views.View)
+			leftSide-: BOOLEAN;
+			cmd: POINTER TO ARRAY OF CHAR;
+			close: INTEGER
+		END;
+
+		Target* = POINTER TO RECORD (Views.View)
+			leftSide-: BOOLEAN;
+			ident: POINTER TO ARRAY OF CHAR
+		END;
+
+		Prop* = POINTER TO RECORD (Properties.Property)
+			cmd*: POINTER TO ARRAY OF CHAR;
+			link-: BOOLEAN;
+			close*: INTEGER
+		END;
+		
+		ChangeAttrOp = POINTER TO RECORD (Stores.Operation)
+			v: Views.View;
+			cmd: POINTER TO ARRAY OF CHAR;
+			close: INTEGER;
+			valid: SET
+		END;
+		
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+		TrapCleaner = POINTER TO RECORD (Kernel.TrapCleaner) END;
+
+	VAR
+		dir-, stdDir-: Directory;
+		par-: Link;
+		iconFont: Fonts.Typeface;
+		linkLeft, linkRight, targetLeft, targetRight: ARRAY 8 OF SHORTCHAR;
+		coloredBackg: BOOLEAN;
+		
+		cleaner: TrapCleaner;
+
+		dialog*: RECORD
+			cmd*: ARRAY 512 OF CHAR;
+			type-: ARRAY 32 OF CHAR;
+			close*: Dialog.List;
+			known, valid: SET;
+		END;
+		fingerprint: INTEGER;
+
+	(** Cleaner **)
+
+	PROCEDURE (c: TrapCleaner) Cleanup;
+	BEGIN
+		par := NIL
+	END Cleanup;
+
+	(** Properties **)
+
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			IF (cmd IN valid) & (p.cmd^ # q.cmd^) THEN EXCL(valid, cmd) END;
+			IF (kind IN valid) & (p.link # q.link) THEN EXCL(valid, kind) END;
+			IF (close IN valid) & (p.close # q.close) THEN EXCL (valid, close) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+		
+	PROCEDURE (op: ChangeAttrOp) Do;
+		VAR v: Views.View; s: POINTER TO ARRAY OF CHAR; c: INTEGER;
+	BEGIN
+		v := op.v; 
+		WITH 
+		| v: Link DO 
+			IF cmd IN op.valid THEN s := op.cmd; op.cmd := v.cmd; v.cmd := s END;
+			IF close IN op.valid THEN c := op.close; op.close := v.close; v.close := c END
+		| v: Target DO 
+			IF cmd IN op.valid THEN s := op.cmd; op.cmd := v.ident; v.ident := s END
+		END
+	END Do;
+
+	PROCEDURE DoChangeAttrOp (v: Views.View; s: POINTER TO ARRAY OF CHAR; c: INTEGER; valid: SET);
+		VAR op: ChangeAttrOp;
+	BEGIN
+		NEW(op); op.v := v; op.valid := valid;
+		IF close IN valid THEN 
+		op.close := c END;
+		IF cmd IN valid THEN NEW(op.cmd, LEN(s)+1); op.cmd^ := s$ END;
+		Views.Do(v,   "#Std:LinkChange", op)
+	END DoChangeAttrOp;
+	
+	PROCEDURE SetProp(v: Views.View; msg: Properties.SetMsg);
+		VAR p: Properties.Property;
+ 	BEGIN
+		p := msg.prop;
+		WHILE p # NIL DO
+			WITH p: Prop DO
+				IF (cmd IN p.valid) OR (close IN p.valid) THEN DoChangeAttrOp(v, p.cmd, p.close, p.valid) END
+			ELSE
+			END;
+			p := p.next
+		END
+	END SetProp;
+	
+	PROCEDURE PollProp(v: Views.View; VAR msg: Properties.PollMsg);
+		VAR p: Prop;
+	BEGIN
+		NEW(p);	
+		WITH v: Link DO
+			p.known := {kind, cmd, close}; 
+			p.link := TRUE;
+			p.cmd := v.cmd;
+			p.close := v.close
+		| v: Target DO
+			p.known := {kind, cmd}; 
+			p.link := FALSE;
+			p.cmd := v.ident
+		ELSE
+		END;
+		p.valid := p.known;
+		Properties.Insert(msg.prop, p)
+	END PollProp;
+	
+	PROCEDURE InitDialog*;
+		VAR  p: Properties.Property;
+	BEGIN
+		dialog.cmd := ""; dialog.type := ""; dialog.close.index := -1;
+		dialog.known := {}; dialog.valid := {};
+		Properties.CollectProp(p);
+		WHILE p # NIL DO
+			WITH p: Prop DO
+				dialog.valid := p.valid; dialog.known := p.known;
+				IF cmd IN p.valid THEN
+					dialog.cmd := p.cmd$
+				END;
+				IF kind IN p.valid THEN 
+					IF p.link THEN Dialog.MapString("#Std:Link", dialog.type)
+					ELSE Dialog.MapString("#Std:Target", dialog.type)
+					END
+				END;
+				IF close IN p.valid THEN
+					dialog.close.index := p.close
+				END
+			ELSE
+			END;
+			p := p.next
+		END;
+		Dialog.Update(dialog)
+	END InitDialog;
+	
+	PROCEDURE Set*;
+		VAR p: Prop;
+	BEGIN
+		NEW(p);
+		p.valid := dialog.valid;
+		IF cmd IN p.valid THEN
+			NEW(p.cmd, LEN(dialog.cmd) + 1);
+			p.cmd^ := dialog.cmd$
+		END;
+		p.close := dialog.close.index;
+		Properties.EmitProp(NIL, p);
+		fingerprint := 0	(* force actualization of fields *)
+	END Set;
+	
+	PROCEDURE CmdGuard* (VAR par: Dialog.Par);
+		VAR c: Containers.Controller; v: Views.View; fp: INTEGER;
+	BEGIN
+		IF ~(cmd IN dialog.known) THEN par.disabled := TRUE
+		ELSIF ~(cmd IN dialog.valid) THEN par.undef := TRUE
+		END;
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		fp := 0;
+		c := Containers.Focus();
+		IF c # NIL THEN
+			c.GetFirstView(Containers.selection, v);
+			WHILE v # NIL DO fp := fp + Services.AdrOf(v); c.GetNextView(TRUE, v) END
+		END;
+		IF fp # fingerprint THEN fingerprint := fp; InitDialog END;
+		Controllers.ResetCurrentPath()
+	END CmdGuard;
+	
+	PROCEDURE CloseGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF ~(close IN dialog.known) THEN par.disabled := TRUE
+		ELSIF ~(close IN dialog.valid) THEN par.undef := TRUE
+		END;
+	END CloseGuard;
+	
+	PROCEDURE Notifier* (idx, op, from, to: INTEGER);
+	BEGIN
+		IF op = Dialog.changed THEN INCL(dialog.valid, idx) END
+	END Notifier;
+
+	PROCEDURE (d: Directory) NewLink* (IN cmd: ARRAY OF CHAR): Link, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewTarget* (IN ident: ARRAY OF CHAR): Target, NEW, ABSTRACT;
+
+
+	PROCEDURE InFrame (f: Views.Frame; x, y: INTEGER): BOOLEAN;
+	BEGIN
+		RETURN (f.l <= x) & (x < f.r) & (f.t <= y) & (y < f.b)
+	END InFrame;
+
+	PROCEDURE Mark (f: Views.Frame; show: BOOLEAN);
+	BEGIN
+		f.MarkRect(f.l, f.t, f.r, f.b, Ports.fill, Ports.hilite, show)
+	END Mark;
+
+	PROCEDURE ThisPos (v: TextViews.View; f: Views.Frame; x, y: INTEGER): INTEGER;
+		(* "corrected" v.ThisPos: does not adjust position when crossing 50% boundary of characters *)
+		VAR loc: TextViews.Location; pos: INTEGER;
+	BEGIN
+		pos := v.ThisPos(f, x, y); v.GetThisLocation(f, pos, loc);
+		IF (loc.y <= y) & (y < loc.y + loc.asc + loc.dsc) & (x < loc.x) THEN DEC(pos) END;
+		RETURN pos
+	END ThisPos;
+
+	PROCEDURE GetLinkPair (this: Link; VAR l, r: Link);
+		(* POST: BalancedPair(l, r) & (l # r) & (l = this OR r = this) OR  (l = r = NIL) *)
+		VAR t: TextModels.Model; rd: TextModels.Reader; v: Views.View; level: INTEGER;
+	BEGIN
+		l := NIL; r := NIL; level := 1;
+		IF (this.context # NIL) & (this.context IS TextModels.Context) THEN
+			t := this.context(TextModels.Context).ThisModel();
+			rd := t.NewReader(NIL);
+			IF this.leftSide THEN
+				rd.SetPos(this.context(TextModels.Context).Pos() + 1);
+				REPEAT
+					rd.ReadView(v);
+					IF (v # NIL) & (v IS Link) THEN
+						IF v(Link).leftSide THEN INC(level) ELSE DEC(level) END
+					END
+				UNTIL (v = NIL) OR (level = 0);
+				IF v # NIL THEN l := this; r := v(Link) END
+			ELSE
+				rd.SetPos(this.context(TextModels.Context).Pos());
+				REPEAT
+					rd.ReadPrevView(v);
+					IF (v # NIL) & (v IS Link) THEN
+						IF v(Link).leftSide THEN DEC(level) ELSE INC(level) END
+					END
+				UNTIL (v = NIL) OR (level = 0);
+				IF v # NIL THEN l := v(Link); r := this END
+			END
+		END
+	END GetLinkPair;
+
+	PROCEDURE GetTargetPair (this: Target; VAR l, r: Target);
+		(* POST: BalancedPair(l, r) & (l # r) & (l = this OR r = this) OR  (l = r = NIL) *)
+		VAR t: TextModels.Model; rd: TextModels.Reader; v: Views.View; level: INTEGER;
+	BEGIN
+		l := NIL; r := NIL; level := 1;
+		IF (this.context # NIL) & (this.context IS TextModels.Context) THEN
+			t := this.context(TextModels.Context).ThisModel();
+			rd := t.NewReader(NIL);
+			IF this.leftSide THEN
+				rd.SetPos(this.context(TextModels.Context).Pos() + 1);
+				REPEAT
+					rd.ReadView(v);
+					IF (v # NIL) & (v IS Target) THEN
+						IF v(Target).leftSide THEN INC(level) ELSE DEC(level) END
+					END
+				UNTIL (v = NIL) OR (level = 0);
+				IF v # NIL THEN l := this; r := v(Target) END
+			ELSE
+				rd.SetPos(this.context(TextModels.Context).Pos());
+				REPEAT
+					rd.ReadPrevView(v);
+					IF (v # NIL) & (v IS Target) THEN
+						IF v(Target).leftSide THEN DEC(level) ELSE INC(level) END
+					END
+				UNTIL (v = NIL) OR (level = 0);
+				IF v # NIL THEN l := v(Target); r := this END
+			END
+		END
+	END GetTargetPair;
+
+	PROCEDURE GetRange (l, r: Link; VAR beg, end: INTEGER);
+	BEGIN
+		beg := l.context(TextModels.Context).Pos();
+		end := r.context(TextModels.Context).Pos() + 1
+	END GetRange;
+
+	PROCEDURE MarkRange (v: TextViews.View; f: Views.Frame; beg, end: INTEGER; show: BOOLEAN);
+		VAR b, e: TextViews.Location; r, t: INTEGER;
+	BEGIN
+		ASSERT(beg < end, 20);
+		v.GetThisLocation(f, beg, b); v.GetThisLocation(f, end, e);
+		IF (b.pos < e.pos) OR (b.pos = e.pos) & (b.x < e.x) THEN
+			IF b.start # e.start THEN
+				r := f.r; t := b.y + b.asc + b.dsc;
+				f.MarkRect(b.x, b.y, r, t, Ports.fill, Ports.hilite, show);
+				IF t < e.y THEN f.MarkRect(0, t, r, e.y, Ports.fill, Ports.hilite, show) END;
+				b.x := f.l; b.y := e.y
+			END;
+		    f.MarkRect(b.x, b.y, e.x, e.y + e.asc + e.dsc, Ports.fill, Ports.hilite, show)
+		END
+	END MarkRange;
+
+	PROCEDURE Reveal (left, right: Views.View; str: ARRAY OF CHAR; opname: Stores.OpName);
+		VAR con: TextModels.Context; t: TextModels.Model; pos: INTEGER;
+			w: TextMappers.Formatter; op: Stores.Operation;
+	BEGIN
+		con := left.context(TextModels.Context);
+		t := con.ThisModel(); pos := con.Pos();
+		w.ConnectTo(t); w.SetPos(pos);
+		IF con.Attr() # NIL THEN w.rider.SetAttr(con.Attr()) END;
+		Models.BeginScript(t, opname, op);
+		t.Delete(pos, pos + 1);
+		w.WriteChar("<");
+		IF str # "" THEN w.WriteString(str) END;
+		w.WriteChar(">");
+		con := right.context(TextModels.Context);
+		pos := con.Pos();
+		w.SetPos(pos);
+		IF con.Attr() # NIL THEN w.rider.SetAttr(con.Attr()) END;
+		t.Delete(pos, pos + 1);
+		w.WriteString("<>");
+		Models.EndScript(t, op)
+	END Reveal;
+	
+	PROCEDURE RevealCmd (v: Link);
+		VAR left, right: Link;
+	BEGIN GetLinkPair(v, left, right);
+		IF left # NIL THEN
+			IF v.cmd # NIL THEN Reveal(left, right, v.cmd^, "#StdLinks:Reveal Link Command") 
+			ELSE Reveal(left, right, "", "#StdLinks:Reveal Link Command") 
+			END
+		END
+	END RevealCmd;
+
+	PROCEDURE RevealTarget (targ: Target);
+		VAR  left, right: Target;
+	BEGIN GetTargetPair(targ, left, right);
+		IF left # NIL THEN
+			IF left.ident # NIL THEN Reveal(left, right, left.ident^, "#SdtLinks:Reveal Target Ident")
+			ELSE Reveal(left, right, "", "#SdtLinks:Reveal Target Ident")
+			END
+		END
+	END RevealTarget;
+	
+	PROCEDURE CallCmd (v: Link; close: BOOLEAN);
+		VAR res: INTEGER;
+	BEGIN
+		Kernel.PushTrapCleaner(cleaner); 
+		par := v;
+		IF v.cmd^ # "" THEN 
+			IF close & (v.close = ifShiftDown) OR (v.close = always) THEN
+				StdCmds.CloseDialog
+			END;
+			Dialog.Call(v.cmd^, "#StdLinks:Link Call Failed", res) 
+		END;
+		par := NIL;
+		Kernel.PopTrapCleaner(cleaner)
+	END CallCmd;
+
+	PROCEDURE TrackSingle (f: Views.Frame; VAR in: BOOLEAN);
+		VAR x, y: INTEGER; modifiers: SET; in0, isDown: BOOLEAN;
+	BEGIN
+		in := FALSE;
+		REPEAT
+			f.Input(x, y, modifiers, isDown);
+			in0 := in; in := InFrame(f, x, y);
+			IF in # in0 THEN Mark(f, in) END
+		UNTIL ~isDown;
+		IF in THEN Mark(f, FALSE) END
+	END TrackSingle;
+
+	PROCEDURE TrackRange (v: TextViews.View; f: Views.Frame; l, r: Link; x, y: INTEGER;
+												VAR in: BOOLEAN);
+		VAR pos, beg, end: INTEGER; modifiers: SET; in0, isDown: BOOLEAN;
+	BEGIN
+		in := FALSE;
+		GetRange(l, r, beg, end); pos := ThisPos(v, f, x, y);
+		IF (beg <= pos) & (pos < end) THEN
+			REPEAT
+				f.Input(x, y, modifiers, isDown); pos := ThisPos(v, f, x, y);
+				in0 := in; in := (beg <= pos) & (pos < end);
+				IF in # in0 THEN MarkRange(v, f, beg, end, in) END
+			UNTIL ~isDown;
+			IF in THEN
+				MarkRange(v, f, beg, end, FALSE)
+			END
+		END
+	END TrackRange;
+
+	PROCEDURE Track (v: Link; f: Views.Frame; c: TextControllers.Controller;
+									x, y: INTEGER; modifiers: SET);
+	(* PRE: (c # NIL) & (f.view.ThisModel() = v.context.ThisModel())  OR  (c = NIL) & (f.view = v) *)
+		VAR l, r: Link; in: BOOLEAN;
+	BEGIN
+		GetLinkPair(v, l, r);
+		IF l # NIL THEN
+			IF c # NIL THEN TrackRange(c.view, f, l, r, x, y, in)
+			ELSE TrackSingle(f, in)
+			END;
+			IF in THEN
+				IF (Controllers.modify IN modifiers) & ((c = NIL) OR ~(Containers.noCaret IN c.opts)) THEN
+					RevealCmd(l)
+				ELSE
+					CallCmd(l, Controllers.extend IN modifiers)
+				END
+			END
+		END
+	END Track;
+
+	PROCEDURE TrackTarget (targ: Target; f: Views.Frame; modifiers: SET);
+		VAR in: BOOLEAN;
+	BEGIN
+		TrackSingle(f, in);
+		IF in & (Controllers.modify IN modifiers) THEN RevealTarget(targ) END
+	END TrackTarget;
+
+	PROCEDURE (v: Link) CopyFromSimpleView- (source: Views.View);
+	BEGIN
+		WITH source: Link DO
+			ASSERT(source.leftSide = (source.cmd # NIL), 100);
+			v.leftSide := source.leftSide;
+			v.close := source.close;
+			IF source.cmd # NIL THEN
+				NEW(v.cmd, LEN(source.cmd^));
+				v.cmd^ := source.cmd^$
+			ELSE v.cmd := NIL
+			END
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (t: Target) CopyFromSimpleView- (source: Views.View);
+	BEGIN
+		WITH source: Target DO
+			ASSERT(source.leftSide = (source.ident # NIL), 100);
+			t.leftSide := source.leftSide;
+			IF source.ident # NIL THEN
+				NEW(t.ident, LEN(source.ident^));
+				t.ident^ := source.ident^$
+			ELSE t.ident := NIL
+			END
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (v: Link) Internalize- (VAR rd: Stores.Reader);
+		VAR len: INTEGER; version: INTEGER; pos: INTEGER;
+	BEGIN
+		v.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minLinkVersion, maxLinkVersion, version);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadBool(v.leftSide);
+		rd.ReadInt(len);
+		IF len = 0 THEN v.cmd := NIL
+		ELSE NEW(v.cmd, len); rd.ReadXString(v.cmd^)
+		END;
+		v.leftSide := v.cmd # NIL;
+		IF v.leftSide THEN
+			IF version = 1 THEN
+				rd.ReadInt(v.close)
+			ELSE
+				Strings.Find(v.cmd, "StdLinks.ShowTarget", 0, pos);
+				IF (pos # 0) THEN v.close := ifShiftDown
+				ELSE v.close := never
+				END
+			END
+		END
+	END Internalize;
+
+	PROCEDURE (v: Link) Externalize- (VAR wr: Stores.Writer);
+		VAR pos, version: INTEGER;
+	BEGIN
+		v.Externalize^(wr);
+		IF v.leftSide THEN
+			Strings.Find(v.cmd, "StdLinks.ShowTarget", 0, pos);
+			IF (pos = 0) & (v.close = never) OR (v.close = ifShiftDown) THEN version := 0
+			ELSE version := 1
+			END
+		ELSE
+			version := 0
+		END;
+		wr.WriteVersion(version);
+		wr.WriteBool(v.cmd # NIL);
+		IF v.cmd = NIL THEN wr.WriteInt(0)
+		ELSE wr.WriteInt(LEN(v.cmd^)); wr.WriteXString(v.cmd^)
+		END;
+		IF version = 1 THEN wr.WriteInt(v.close) END
+	END Externalize;
+
+	PROCEDURE (t: Target) Internalize- (VAR rd: Stores.Reader);
+		VAR len: INTEGER; version: INTEGER;
+	BEGIN
+		t.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minTargVersion, maxTargVersion, version);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadBool(t.leftSide);
+		rd.ReadInt(len);
+		IF len = 0 THEN t.ident := NIL
+		ELSE NEW(t.ident, len); rd.ReadXString(t.ident^)
+		END;
+		t.leftSide := t.ident # NIL
+	END Internalize;
+
+	PROCEDURE (t: Target) Externalize- (VAR wr: Stores.Writer);
+	BEGIN
+		t.Externalize^(wr);
+		wr.WriteVersion(maxTargVersion);
+		wr.WriteBool(t.ident # NIL);
+		IF t.ident = NIL THEN wr.WriteInt(0)
+		ELSE wr.WriteInt(LEN(t.ident^)); wr.WriteXString(t.ident^)
+		END
+	END Externalize;
+
+	PROCEDURE RestoreView (v: Views.View; f: Views.Frame; icon: ARRAY OF SHORTCHAR);
+		VAR c: Models.Context; a: TextModels.Attributes; font: Fonts.Font; color: Ports.Color;
+			asc, dsc, w: INTEGER;
+	BEGIN
+		c := v.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			a := c(TextModels.Context).Attr();
+			font := Fonts.dir.This(iconFont, a.font.size, {}, Fonts.normal);
+			color := a.color
+		ELSE font := Fonts.dir.Default(); color := Ports.black
+		END;
+		IF coloredBackg THEN
+		f.DrawRect(f.l, f.t, f.r, f.b, Ports.fill, Ports.grey25) END;
+		font.GetBounds(asc, dsc, w);
+		f.DrawSString(1*Ports.mm DIV 2, asc, color, icon, font)
+	END RestoreView;
+
+	PROCEDURE (v: Link) Restore* (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		IF v.leftSide THEN RestoreView(v, f, linkLeft)
+		ELSE RestoreView(v, f, linkRight)
+		END
+	END Restore;
+
+	PROCEDURE (targ: Target) Restore* (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		IF targ.leftSide THEN RestoreView(targ, f, targetLeft)
+		ELSE RestoreView(targ, f, targetRight)
+		END
+	END Restore;
+
+	PROCEDURE SizePref (v: Views.View; icon: ARRAY OF SHORTCHAR; VAR msg: Properties.SizePref);
+		VAR c: Models.Context; a: TextModels.Attributes; font: Fonts.Font;
+			asc, dsc, w: INTEGER;
+	BEGIN
+		c := v.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			a := c(TextModels.Context).Attr();
+			font := Fonts.dir.This(iconFont, a.font.size, {}, Fonts.normal)
+		ELSE
+			font := Fonts.dir.Default()
+		END;
+		msg.w := font.SStringWidth(icon) + 1*Ports.mm;
+		font.GetBounds(asc, dsc, w);
+		msg.h := asc + dsc
+	END SizePref;
+	
+	PROCEDURE (v: Link) HandlePropMsg- (VAR msg: Properties.Message);
+		VAR a: TextModels.Attributes; c: Models.Context; asc, dsc, w: INTEGER; l, r: Link;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			IF v.leftSide THEN SizePref(v, linkLeft, msg)
+			ELSE SizePref(v, linkRight, msg)
+			END
+		| msg: Properties.FocusPref DO
+			msg.hotFocus := TRUE
+		| msg: Properties.ResizePref DO
+			msg.fixed := TRUE
+		| msg: TextModels.Pref DO
+			msg.opts := {TextModels.hideable}
+		| msg: TextControllers.FilterPref DO
+			msg.filter := TRUE
+		| msg: TextSetters.Pref DO c := v.context;
+			IF (c # NIL) & (c IS TextModels.Context) THEN
+				a := c(TextModels.Context).Attr();
+				a.font.GetBounds(asc, dsc, w);
+				msg.dsc := dsc
+			END
+		| msg: Properties.PollMsg DO
+			IF v.leftSide THEN PollProp(v, msg) 
+			ELSE
+				GetLinkPair(v, l, r);
+				IF l # NIL THEN PollProp(l, msg) END
+			END
+		| msg: Properties.SetMsg DO
+			IF v.leftSide THEN SetProp(v, msg)
+			ELSE GetLinkPair(v, l, r); SetProp(l, msg)
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+	
+	PROCEDURE (targ: Target) HandlePropMsg- (VAR msg: Properties.Message);
+		VAR a: TextModels.Attributes; c: Models.Context; asc, dsc, w: INTEGER;  l, r: Target;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			IF targ.leftSide THEN SizePref(targ, targetLeft, msg)
+			ELSE SizePref(targ, targetRight, msg)
+			END
+		| msg: Properties.FocusPref DO
+			msg.hotFocus := TRUE
+		| msg: Properties.ResizePref DO
+			msg.fixed := TRUE
+		| msg: TextModels.Pref DO
+			msg.opts := {TextModels.hideable}
+		| msg: TextSetters.Pref DO c := targ.context;
+			IF (c # NIL) & (c IS TextModels.Context) THEN
+				a := c(TextModels.Context).Attr();
+				a.font.GetBounds(asc, dsc, w);
+				msg.dsc := dsc
+			END
+		| msg: Properties.PollMsg DO
+			IF targ.leftSide THEN PollProp(targ, msg)
+			ELSE
+				GetTargetPair(targ, l, r);
+				IF l # NIL THEN PollProp(l, msg) END
+			END
+		| msg: Properties.SetMsg DO
+			IF targ.leftSide THEN SetProp(targ, msg)
+			ELSE GetTargetPair(targ, l, r); SetProp(l, msg)
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+
+	PROCEDURE (v: Link) HandleCtrlMsg* (f: Views.Frame;
+		VAR msg: Controllers.Message; VAR focus: Views.View);
+
+		PROCEDURE isHot(c: TextControllers.Controller; x, y: INTEGER; mod: SET): BOOLEAN;
+			VAR pos, beg, end: INTEGER;
+		BEGIN
+			(* ignore alt, cmd, and middle clicks in edit mode *)
+			IF ~(Containers.noCaret IN c.opts) & (mod * {17, 27, 28} # {}) THEN RETURN FALSE END;
+			pos := ThisPos(c.view, f, x, y);
+			(* ignore clicks in selection *)
+			c.GetSelection(beg, end);
+			IF (end > beg) & (pos >= beg) & (pos <= end) THEN RETURN FALSE END;
+			IF v.leftSide THEN RETURN pos >= v.context(TextModels.Context).Pos()
+			ELSE RETURN pos < v.context(TextModels.Context).Pos()
+			END
+		END isHot;
+		
+	BEGIN
+		WITH msg: Controllers.PollCursorMsg DO
+			msg.cursor := Ports.refCursor
+		| msg: TextControllers.FilterPollCursorMsg DO
+			IF isHot(msg.controller, msg.x, msg.y, {}) THEN
+				msg.cursor := Ports.refCursor; msg.done := TRUE
+			END
+		| msg: Controllers.TrackMsg DO
+			Track(v, f, NIL, msg.x, msg.y, msg.modifiers)
+		| msg: TextControllers.FilterTrackMsg DO
+			IF isHot(msg.controller, msg.x, msg.y, msg.modifiers) THEN
+				Track(v, f,  msg.controller, msg.x, msg.y, msg.modifiers);
+				msg.done := TRUE
+			END
+		ELSE
+		END
+	END HandleCtrlMsg;
+
+	PROCEDURE (targ: Target) HandleCtrlMsg* (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		WITH msg: Controllers.TrackMsg DO TrackTarget(targ, f, msg.modifiers)
+		ELSE
+		END
+	END HandleCtrlMsg;
+
+	PROCEDURE (v: Link) GetCmd* (OUT cmd: ARRAY OF CHAR), NEW;
+	BEGIN
+		ASSERT(v.leftSide, 20);
+		ASSERT(v.cmd # NIL, 100);
+		cmd := v.cmd$
+	END GetCmd;
+
+	PROCEDURE (t: Target) GetIdent* (OUT ident: ARRAY OF CHAR), NEW;
+	BEGIN
+		ASSERT(t.leftSide, 20);
+		ASSERT(t.ident # NIL, 100);
+		ident := t.ident$
+	END GetIdent;
+
+	(* --------------- create commands and menu guards ------------------------ *)
+
+	PROCEDURE GetParam (c: TextControllers.Controller; VAR param: ARRAY OF CHAR;
+									VAR lbrBeg, lbrEnd, rbrBeg, rbrEnd: INTEGER);
+		VAR rd: TextModels.Reader; i, beg, end: INTEGER;
+			ch0, ch1, ch2: CHAR;
+	BEGIN
+		param[0] := 0X;
+		IF (c # NIL) & c.HasSelection() THEN
+			c.GetSelection(beg, end);
+			IF end - beg > 4 THEN
+				rd := c.text.NewReader(NIL);
+				rd.SetPos(beg); rd.ReadChar(ch0);
+				rd.SetPos(end-2); rd.ReadChar(ch1); rd.ReadChar(ch2);
+				IF (ch0 = "<") & (ch1 = "<") & (ch2 = ">") THEN
+					rd.SetPos(beg+1); rd.ReadChar(ch0); i := 0;
+					WHILE ~rd.eot & (ch0 # ">") DO
+						IF i < LEN(param) - 1 THEN param[i] := ch0; INC(i) END;
+						rd.ReadChar(ch0)
+					END;
+					param[i] := 0X;
+					lbrBeg := beg; lbrEnd := rd.Pos();
+					rbrBeg := end -2; rbrEnd := end
+				END
+			END
+		END
+	END GetParam;
+	
+	PROCEDURE CreateGuard* (VAR par: Dialog.Par);
+		VAR param: ARRAY 512 OF CHAR; lbrBeg, lbrEnd, rbrBeg, rbrEnd: INTEGER;
+	BEGIN
+		GetParam(TextControllers.Focus(), param, lbrBeg, lbrEnd, rbrBeg, rbrEnd);
+		par.disabled := param = ""
+	END CreateGuard;
+
+	PROCEDURE InsertionAttr (c: TextControllers.Controller; pos: INTEGER): TextModels.Attributes;
+		VAR rd: TextModels.Reader; r: TextRulers.Ruler; a: TextModels.Attributes; ch: CHAR;
+	BEGIN
+		rd := c.text.NewReader(NIL);  a := NIL;
+		rd.SetPos(pos); rd.ReadChar(ch); a := rd.attr;
+		IF a = NIL THEN c.view.PollDefaults(r, a) END;
+		RETURN a
+	END InsertionAttr;
+
+	PROCEDURE CreateLink*;
+		VAR lbrBeg, lbrEnd, rbrBeg, rbrEnd: INTEGER;
+			left, right: Link; c: TextControllers.Controller;
+			cmd: ARRAY 512 OF CHAR;
+			op: Stores.Operation;
+			w: TextModels.Writer; a: TextModels.Attributes;
+	BEGIN
+		c := TextControllers.Focus();
+		GetParam(TextControllers.Focus(), cmd, lbrBeg, lbrEnd, rbrBeg, rbrEnd);
+		IF cmd # "" THEN
+			w := c.text.NewWriter(NIL);
+			Models.BeginScript(c.text, "#StdLinks:Create Link", op);
+			a := InsertionAttr(c, rbrBeg);
+			c.text.Delete(rbrBeg, rbrEnd);
+			right := dir.NewLink("");
+			w.SetPos(rbrBeg);
+			IF a # NIL THEN w.SetAttr(a) END;
+			w.WriteView(right, 0, 0);
+			a := InsertionAttr(c, lbrBeg);
+			c.text.Delete(lbrBeg, lbrEnd);
+			left := dir.NewLink(cmd);
+			w.SetPos(lbrBeg);
+			IF a # NIL THEN w.SetAttr(a) END;
+			w.WriteView(left, 0, 0);
+			Models.EndScript(c.text, op)
+		END
+	END CreateLink;
+
+	PROCEDURE CreateTarget*;
+		VAR lbrBeg, lbrEnd, rbrBeg, rbrEnd: INTEGER;
+			left, right: Target; c: TextControllers.Controller;
+			ident: ARRAY 512 OF CHAR;
+			op: Stores.Operation;
+			w: TextModels.Writer; a: TextModels.Attributes;
+	BEGIN
+		c := TextControllers.Focus();
+		GetParam(TextControllers.Focus(), ident, lbrBeg, lbrEnd, rbrBeg, rbrEnd);
+		IF ident # "" THEN
+			w := c.text.NewWriter(NIL);
+			Models.BeginScript(c.text, "#StdLinks:Create Target", op);
+			a := InsertionAttr(c, rbrBeg);
+			c.text.Delete(rbrBeg, rbrEnd);
+			right := dir.NewTarget("");
+			w.SetPos(rbrBeg);
+			IF a # NIL THEN w.SetAttr(a) END;
+			w.WriteView(right, 0, 0);
+			a := InsertionAttr(c, lbrBeg);
+			c.text.Delete(lbrBeg, lbrEnd);
+			left := dir.NewTarget(ident);
+			w.SetPos(lbrBeg);
+			IF a # NIL THEN w.SetAttr(a) END;
+			w.WriteView(left, 0, 0);
+			Models.EndScript(c.text, op)
+		END
+	END CreateTarget;
+
+	PROCEDURE ShowTarget* (IN ident: ARRAY OF CHAR);
+		VAR c: TextControllers.Controller; rd: TextModels.Reader;
+			v: Views.View; left, right: Target; beg, end: INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			rd := c.text.NewReader(NIL);
+			REPEAT rd.ReadView(v)
+			UNTIL rd.eot OR (v # NIL) & (v IS Target) & v(Target).leftSide & (v(Target).ident^ = ident);
+			IF ~rd.eot THEN
+				GetTargetPair(v(Target), left, right);
+				IF (left # NIL) & (right # NIL) THEN
+					beg := left.context(TextModels.Context).Pos();
+					end := right.context(TextModels.Context).Pos() + 1;
+					c.SetSelection(beg, end);
+					c.view.SetOrigin(beg, 0)
+				ELSE
+					Dialog.ShowParamMsg("target '^0' not found", ident, "", "")
+				END
+			ELSE
+				Dialog.ShowParamMsg("target '^0' not found", ident, "", "")
+			END
+		END
+	END ShowTarget;
+
+
+	(* programming interface *)
+
+	PROCEDURE (d: StdDirectory) NewLink (IN cmd: ARRAY OF CHAR): Link;
+		VAR link: Link; i: INTEGER;
+	BEGIN
+		NEW(link); link.leftSide := cmd # "";
+		IF link.leftSide THEN
+			i := 0; WHILE cmd[i] # 0X DO INC(i) END;
+			NEW(link.cmd, i + 1); link.cmd^ := cmd$
+		ELSE
+			link.cmd := NIL
+		END;
+		link.close := ifShiftDown;
+		RETURN link
+	END NewLink;
+
+	PROCEDURE (d: StdDirectory) NewTarget (IN ident: ARRAY OF CHAR): Target;
+		VAR t: Target; i: INTEGER;
+	BEGIN
+		NEW(t); t.leftSide := ident # "";
+		IF t.leftSide THEN
+			i := 0; WHILE ident[i] # 0X DO INC(i) END;
+			NEW(t.ident, i + 1); t.ident^ := ident$
+		ELSE
+			t.ident := NIL
+		END;
+		RETURN t
+	END NewTarget;
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20);
+		dir := d
+	END SetDir;
+
+	PROCEDURE Init;
+		VAR font: Fonts.Font; d: StdDirectory;
+
+		PROCEDURE DefaultAppearance;
+		BEGIN font := Fonts.dir.Default(); iconFont := font.typeface;
+			linkLeft := "Link"; linkRight := "~";
+			targetLeft := "Targ"; targetRight :=  "~";
+			coloredBackg := TRUE
+		END DefaultAppearance;
+
+	BEGIN
+		NEW(d); dir := d; stdDir := d;
+		IF Dialog.platform DIV 10 = 1 THEN (* Windows *)
+			iconFont := "Wingdings";
+			font := Fonts.dir.This(iconFont, 10*Fonts.point (*arbitrary*), {}, Fonts.normal);
+			IF font.IsAlien() THEN DefaultAppearance
+			ELSE
+				linkLeft[0] := SHORT(CHR(246)); linkLeft[1] := 0X;
+				linkRight[0] := SHORT(CHR(245)); linkRight[1] := 0X;
+				targetLeft[0] := SHORT(CHR(164)); targetLeft[1] := 0X;
+				targetRight[0] := SHORT(CHR(161)); targetRight[1] := 0X;
+				coloredBackg := FALSE
+			END
+		ELSIF Dialog.platform DIV 10 = 2 THEN (* Mac *)
+			DefaultAppearance
+		ELSE
+			DefaultAppearance
+		END;
+		NEW(cleaner);
+		dialog.close.SetResources("#Std:links")
+	END Init;
+
+BEGIN
+	Init
+END StdLinks.

+ 162 - 0
BlackBox/Std/Mod/Logos.txt

@@ -0,0 +1,162 @@
+MODULE StdLogos;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Logos.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT Ports, Stores, Views, Controllers, Properties;
+
+	CONST
+		W = 4;
+		baseSize = 24 * Ports.point;
+
+		colBase = 00202020H;
+
+		changeColorKey = "#System:ChangeColor";
+
+		minVersion = 0; maxVersion = 0;
+
+
+	TYPE
+		View = POINTER TO RECORD (Views.View)
+			c: Ports.Color
+		END;
+		
+		ChangeSizeOp = POINTER TO RECORD (Stores.Operation)
+			view: View;
+			size: INTEGER;
+		END;
+	
+		ChangeColorOp = POINTER TO RECORD (Stores.Operation)
+			view: View;
+			color: Ports.Color
+		END;
+		
+	(* curve painting *)
+
+	PROCEDURE Paint (f: Views.Frame; size: INTEGER; col, bgnd: Ports.Color);
+		VAR i, d, s, g, m, a, b, l, l0, rl, rt, rr, rb: INTEGER; c: Ports.Color;
+	BEGIN
+		s := size DIV 10; d := size DIV 2; g := d DIV 8; m := size * W DIV 2;
+		f.DrawOval(0, s * 2, size * W, size, Ports.fill, col);
+		f.DrawOval(s * W, s * 11 DIV 4, (size - s) * W, size - s * 3 DIV 4, Ports.fill, bgnd);
+		a := m; b := m + d; c := 7 * colBase; i := 0;
+		WHILE i < 4 DO
+			f.DrawOval(a, 0, b, d, Ports.fill, c);
+			INC(a, g); DEC(b, g); DEC(c, colBase); INC(i)
+		END;
+		f.rider.GetRect(rl, rt, rr, rb);
+		l0 := rl; l := (f.gx + m + d DIV 2) DIV f.unit;
+		IF l < rr THEN
+			f.rider.SetRect(l, rt, rr, rb);
+			a := m; b := m + d; c := 0; i := 0;
+			WHILE i < 4 DO
+				f.DrawOval(a, 0, b, d, Ports.fill, c);
+				INC(a, g); DEC(b, g); INC(c, colBase); INC(i)
+			END;
+			f.rider.SetRect(l0, rt, rr, rb)
+		END
+	END Paint;
+
+	(* ChangeOp *)
+
+	PROCEDURE (op: ChangeSizeOp) Do;
+		VAR v: View; size, w: INTEGER;
+	BEGIN
+		v := op.view;
+		size := op.size; v.context.GetSize(w, op.size); v.context.SetSize(size * W, size);
+		Views.Update(v, Views.keepFrames)
+	END Do;
+
+	PROCEDURE (op: ChangeColorOp) Do;
+		VAR v: View; color: Ports.Color;
+	BEGIN
+		v := op.view;
+		color := op.color; op.color := v.c; v.c := color;
+		Views.Update(v, Views.keepFrames)
+	END Do;
+
+	(* View *)
+
+	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		v.Internalize^(rd); IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxVersion, thisVersion); IF rd.cancelled THEN RETURN END;
+		rd.ReadInt(v.c)
+	END Internalize;
+
+	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		v.Externalize^(wr);
+		wr.WriteVersion(maxVersion);
+		wr.WriteInt(v.c)
+	END Externalize;
+
+	PROCEDURE (v: View) CopyFromSimpleView (source: Views.View);
+	BEGIN
+		WITH source: View DO v.c := source.c END
+	END CopyFromSimpleView;
+
+	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR w, h: INTEGER; bgnd: Ports.Color; g: Views.Frame;
+	BEGIN
+		g := f;
+		REPEAT
+			g := Views.HostOf(g);
+			bgnd := Views.transparent;
+			g.view.GetBackground(bgnd)
+		UNTIL bgnd # Views.transparent;
+		v.context.GetSize(w, h);
+		Paint(f, h, v.c, bgnd)
+	END Restore;
+	
+	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
+																VAR focus: Views.View);
+	BEGIN
+		WITH msg: Properties.CollectMsg DO
+			Views.HandlePropMsg(v, msg.poll)
+		| msg: Properties.EmitMsg DO
+			Views.HandlePropMsg(v, msg.set)
+		ELSE	(* ignore other messages *)
+		END
+	END HandleCtrlMsg;
+
+	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
+		VAR q: Properties.Property; p: Properties.StdProp;
+			cop: ChangeColorOp;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			IF (msg.w > Views.undefined) & (msg.h > Views.undefined) THEN
+				(* constrain proposed size *)
+				Properties.ProportionalConstraint(W, 1, msg.fixedW, msg.fixedH, msg.w, msg.h)
+			ELSE
+				(* return default size *)
+				msg.w := W * baseSize; msg.h := baseSize
+			END
+		| msg: Properties.PollMsg DO
+			NEW(p); p.known := {Properties.color}; p.valid := p.known;
+			p.color.val := v.c;
+			msg.prop := p
+		| msg: Properties.SetMsg DO
+			q := msg.prop;
+			WHILE q # NIL DO
+				WITH q: Properties.StdProp DO
+					IF Properties.color IN q.valid THEN
+						NEW(cop); cop.view := v; cop.color := q.color.val;
+						Views.Do(v, changeColorKey, cop)
+					END;
+				ELSE
+				END;
+				q :=q.next
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+	
+	PROCEDURE Deposit*;
+		VAR v: View;
+	BEGIN
+		NEW(v); v.c := Ports.grey50; Views.Deposit(v)
+	END Deposit;
+
+END StdLogos.

+ 853 - 0
BlackBox/Std/Mod/Scrollers.txt

@@ -0,0 +1,853 @@
+MODULE StdScrollers;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Scrollers.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT Dialog, Ports, Services, Stores, Models, Views, Properties, Controllers, StdCFrames;
+	
+	
+	CONST
+		(* properties & options *)
+		horBar* = 0; verBar* = 1; horHide* = 2; verHide* = 3; width* = 4; height* = 5; showBorder* = 6; savePos* = 7;
+		
+
+	TYPE
+		Prop* = POINTER TO RECORD (Properties.Property)
+			horBar*, verBar*: BOOLEAN;
+			horHide*, verHide*: BOOLEAN;
+			width*, height*: INTEGER;
+			showBorder*: BOOLEAN;
+			savePos*: BOOLEAN
+		END;
+		
+		ScrollBar = POINTER TO RECORD (Views.View)
+			v: View;
+			ver: BOOLEAN
+		END;
+		
+		InnerView = POINTER TO RECORD (Views.View)
+			v: View
+		END;
+		
+		View = POINTER TO RECORD (Views.View);
+			view: Views.View;
+			sbW: INTEGER;
+			orgX, orgY: INTEGER;
+			w, h: INTEGER;						(* = 0: adapt to container *)
+			opts: SET;
+			(* not persistent *)
+			hor, ver: ScrollBar;
+			inner: InnerView;
+			rgap, bgap: INTEGER;				(* = 0: no scrollbar *)
+			border: INTEGER;
+			update: Action
+		END;
+		
+		Context = POINTER TO RECORD (Models.Context)
+			v: View;
+			type: INTEGER
+		END;
+		
+		Action = POINTER TO RECORD (Services.Action)
+			v: View
+		END;
+	
+		Op = POINTER TO RECORD (Stores.Operation)
+			v: View;
+			p: Prop
+		END;
+		
+		SOp = POINTER TO RECORD (Stores.Operation)
+			v: View;
+			x, y: INTEGER
+		END;
+		
+		UpdateMsg = RECORD (Views.Message)
+			changed: BOOLEAN
+		END;
+
+	
+	VAR
+		dialog*: RECORD
+			horizontal*, vertical*: RECORD
+				mode*: INTEGER;
+				adapt*: BOOLEAN;
+				size*: REAL
+			END;
+			showBorder*: BOOLEAN;
+			savePos*: BOOLEAN;
+			valid, readOnly: SET
+		END;
+		
+
+	(* tools *)
+	
+	PROCEDURE CheckPos (v: View; VAR x, y: INTEGER);
+		VAR w, h: INTEGER;
+	BEGIN
+		v.context.GetSize(w, h);
+		DEC(w, v.rgap + 2 * v.border);
+		DEC(h, v.bgap + 2 * v.border);
+		IF x > v.w - w THEN x := v.w - w END;
+		IF x < 0 THEN x := 0 END;
+		IF y > v.h - h THEN y := v.h - h END;
+		IF y < 0 THEN y := 0 END
+	END CheckPos;
+	
+	PROCEDURE InnerFrame (v: View; f: Views.Frame): Views.Frame;
+		VAR g, h: Views.Frame;
+	BEGIN
+		g := Views.ThisFrame(f, v.inner);
+		IF g = NIL THEN
+			Views.InstallFrame(f, v.inner, v.border, v.border, 0, TRUE);
+			g := Views.ThisFrame(f, v.inner)
+		END;
+		IF g # NIL THEN
+			h := Views.ThisFrame(g, v.view);
+			IF h = NIL THEN
+				Views.InstallFrame(g, v.view, -v.orgX, -v.orgY, 0, TRUE);
+				h := Views.ThisFrame(g, v.view)
+			END
+		END;
+		RETURN h
+	END InnerFrame;
+
+	PROCEDURE Scroll (v: View; dir: INTEGER; ver: BOOLEAN; p: INTEGER; OUT pos: INTEGER);
+		VAR x, y: INTEGER; last: Stores.Operation; op: SOp;
+	BEGIN
+		x := v.orgX; y := v.orgY;
+		IF ver THEN pos := y ELSE pos := x END;
+		IF dir = StdCFrames.lineUp THEN
+			DEC(pos, 10 * Ports.mm)
+		ELSIF dir = StdCFrames.lineDown THEN
+			INC(pos, 10 * Ports.mm)
+		ELSIF dir = StdCFrames.pageUp THEN
+			DEC(pos, 40 * Ports.mm)
+		ELSIF dir = StdCFrames.pageDown THEN
+			INC(pos, 40 * Ports.mm)
+		ELSIF dir = Controllers.gotoPos THEN
+			pos := p
+		END;
+		IF ver THEN CheckPos(v, x, pos); y := pos
+		ELSE CheckPos(v, pos, y); x := pos
+		END;
+		IF (x # v.orgX) OR (y # v.orgY) THEN
+			last := Views.LastOp(v);
+			IF ~(savePos IN v.opts) OR (last # NIL) & (last IS SOp) THEN
+				v.orgX := x; v.orgY := y;
+				Views.Update(v.view, Views.keepFrames)
+			ELSE
+				NEW(op); op.v := v; op.x := x; op.y := y;
+				Views.Do(v, "#System:Scrolling", op)
+			END
+		END
+	END Scroll;
+	
+	PROCEDURE PollSection (v: View; ver: BOOLEAN; OUT size, sect, pos: INTEGER);
+		VAR w, h: INTEGER;
+	BEGIN
+		v.context.GetSize(w, h);
+		IF ver THEN size := v.h; sect := h - v.bgap - 2 * v.border; pos := v.orgY
+		ELSE size := v.w; sect := w - v.rgap - 2 * v.border; pos := v.orgX
+		END
+	END PollSection;
+	
+	
+	(* SOp *)
+	
+	PROCEDURE (op: SOp) Do;
+		VAR x, y: INTEGER;
+	BEGIN
+		x := op.x; op.x := op.v.orgX; op.v.orgX := x;
+		y := op.y; op.y := op.v.orgY; op.v.orgY := y;
+		Views.Update(op.v.view, Views.keepFrames)
+	END Do;
+	
+
+	(* properties *)
+	
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			IF p.horBar # q.horBar THEN EXCL(valid, horBar) END;
+			IF p.verBar # q.verBar THEN EXCL(valid, verBar) END;
+			IF p.horHide # q.horHide THEN EXCL(valid, horHide) END;
+			IF p.verHide # q.verHide THEN EXCL(valid, verHide) END;
+			IF p.width # q.width THEN EXCL(valid, width) END;
+			IF p.height # q.height THEN EXCL(valid, height) END;
+			IF p.showBorder # q.showBorder THEN EXCL(valid, showBorder) END;
+			IF p.savePos # q.savePos THEN EXCL(valid, savePos) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+	
+	PROCEDURE SetProp (v: View; p: Properties.Property);
+		VAR op: Op;
+	BEGIN
+		WITH p: Prop DO
+			NEW(op); op.v := v; op.p := p;
+			Views.Do(v, "#System:SetProp", op)
+		END
+	END SetProp;
+	
+	PROCEDURE PollProp (v: View; OUT prop: Prop);
+		VAR p: Prop;
+	BEGIN
+		NEW(p);
+		p.valid := {horBar, verBar, horHide, verHide, width, height, showBorder, savePos};
+		p.readOnly := {width, height} - v.opts;
+		p.horBar := horBar IN v.opts;
+		p.verBar := verBar IN v.opts;
+		p.horHide := horHide IN v.opts;
+		p.verHide := verHide IN v.opts;
+		p.width := v.w;
+		p.height := v.h;
+		p.showBorder := showBorder IN v.opts;
+		p.savePos := savePos IN v.opts;
+		p.known := p.valid; prop := p
+	END PollProp;
+	
+	
+	(* Op *)
+	
+	PROCEDURE (op: Op) Do;
+		VAR p: Prop; v: View; valid: SET;
+	BEGIN
+		v := op.v; p := op.p; PollProp(v, op.p); op.p.valid := p.valid;
+		valid := p.valid * ({horBar, verBar, horHide, verHide, showBorder, savePos} + v.opts * {width, height});
+		IF horBar IN valid THEN
+			IF p.horBar THEN INCL(v.opts, horBar) ELSE EXCL(v.opts, horBar) END
+		END;
+		IF verBar IN valid THEN
+			IF p.verBar THEN INCL(v.opts, verBar) ELSE EXCL(v.opts, verBar) END
+		END;
+		IF horHide IN valid THEN
+			IF p.horHide THEN INCL(v.opts, horHide) ELSE EXCL(v.opts, horHide) END
+		END;
+		IF verHide IN valid THEN
+			IF p.verHide THEN INCL(v.opts, verHide) ELSE EXCL(v.opts, verHide) END
+		END;
+		IF width IN valid THEN v.w := p.width END;
+		IF height IN valid THEN v.h := p.height END;
+		IF showBorder IN valid THEN
+			IF p.showBorder THEN INCL(v.opts, showBorder); v.border := 2 * Ports.point
+			ELSE EXCL(v.opts, showBorder); v.border := 0
+			END
+		END;
+		IF savePos IN valid THEN
+			IF p.savePos THEN INCL(v.opts, savePos) ELSE EXCL(v.opts, savePos) END
+		END;
+		Views.Update(v, Views.rebuildFrames)
+	END Do;
+	
+	
+	(* Action *)
+	
+	PROCEDURE (a: Action) Do;
+		VAR msg: UpdateMsg;
+	BEGIN
+		msg.changed := FALSE;
+		Views.Broadcast(a.v, msg);
+		IF msg.changed THEN Views.Update(a.v, Views.keepFrames)
+		ELSE
+			Views.Broadcast(a.v.hor, msg);
+			Views.Broadcast(a.v.ver, msg)
+		END
+	END Do;
+	
+	
+	(* ScrollBars *)
+	
+	PROCEDURE TrackSB (f: StdCFrames.ScrollBar; dir: INTEGER; VAR pos: INTEGER);
+		VAR s: ScrollBar; msg: Controllers.ScrollMsg; pmsg: Controllers.PollSectionMsg; host, inner: Views.Frame;
+	BEGIN
+		s := f.view(ScrollBar); host := Views.HostOf(f);
+		msg.focus := FALSE; msg.vertical := s.ver;
+		msg.op := dir; msg.done := FALSE;
+		inner := InnerFrame(s.v, host);
+		IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+		IF msg.done THEN
+			pmsg.focus := FALSE; pmsg.vertical := s.ver;
+			pmsg.valid := FALSE; pmsg.done := FALSE;
+			inner := InnerFrame(s.v, host);
+			IF inner # NIL THEN Views.ForwardCtrlMsg(inner, pmsg) END;
+			IF pmsg.done THEN
+				pos := pmsg.partPos
+			END
+		ELSE
+			Scroll(s.v, dir, s.ver, 0, pos);
+			Views.ValidateRoot(Views.RootOf(host))
+		END
+	END TrackSB;
+
+	PROCEDURE SetSB (f: StdCFrames.ScrollBar; pos: INTEGER);
+		VAR s: ScrollBar; msg: Controllers.ScrollMsg; p: INTEGER; host, inner: Views.Frame;
+	BEGIN
+		s := f.view(ScrollBar); host := Views.HostOf(f);
+		msg.focus := FALSE; msg.vertical := s.ver;
+		msg.op := Controllers.gotoPos; msg.pos := pos;
+		msg.done := FALSE;
+		inner := InnerFrame(s.v, host);
+		IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+		IF ~msg.done THEN
+			Scroll(s.v, Controllers.gotoPos, s.ver, pos, p);
+			Views.ValidateRoot(Views.RootOf(host))
+		END
+	END SetSB;
+
+	PROCEDURE GetSB (f: StdCFrames.ScrollBar; OUT size, sect, pos: INTEGER);
+		VAR s: ScrollBar; msg: Controllers.PollSectionMsg; host, inner: Views.Frame;
+	BEGIN
+		s := f.view(ScrollBar); host := Views.HostOf(f);
+		msg.focus := FALSE; msg.vertical := s.ver;
+		msg.wholeSize := 1; msg.partSize := 0; msg.partPos := 0;
+		msg.valid := FALSE; msg.done := FALSE;
+		inner := InnerFrame(s.v, host);
+		IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+		IF msg.done THEN
+			IF msg.valid THEN
+				size := msg.wholeSize; sect := msg.partSize; pos := msg.partPos
+			ELSE
+				size := 1; sect := 1; pos := 0
+			END
+		ELSE
+			PollSection(s.v, s.ver, size, sect, pos)
+		END
+	END GetSB;
+
+	PROCEDURE (s: ScrollBar) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.ScrollBar;
+	BEGIN
+		f := StdCFrames.dir.NewScrollBar();
+		f.disabled := FALSE; f.undef := FALSE; f.readOnly := FALSE;
+		f.Track := TrackSB; f.Get := GetSB; f.Set := SetSB;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (s: ScrollBar) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+	
+	PROCEDURE (s: ScrollBar) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		WITH f: StdCFrames.Frame DO
+			WITH msg: Controllers.PollCursorMsg DO
+				f.GetCursor(msg.x, msg.y, msg.modifiers, msg.cursor)
+			| msg: Controllers.TrackMsg DO
+				f.MouseDown(msg.x, msg.y, msg.modifiers)
+			ELSE
+			END
+		END
+	END HandleCtrlMsg;
+	
+	PROCEDURE (s: ScrollBar) HandleViewMsg (f: Views.Frame; VAR msg: Views.Message);
+	BEGIN
+		WITH msg: UpdateMsg DO
+			WITH f: StdCFrames.Frame DO f.Update() END
+		ELSE
+		END
+	END HandleViewMsg;
+
+	
+	(* View *)
+	
+	PROCEDURE Update (v: View; f: Views.Frame);
+		VAR msg: Controllers.PollSectionMsg; w, h: INTEGER; depends: BOOLEAN; inner: Views.Frame;
+	BEGIN
+		v.bgap := 0; v.rgap := 0; depends := FALSE;
+		v.context.GetSize(w, h);
+		DEC(w, 2 * v.border); DEC(h, 2 * v.border);
+		IF horBar IN v.opts THEN
+			IF horHide IN v.opts THEN
+				msg.focus := FALSE; msg.vertical := FALSE;
+				msg.wholeSize := 1; msg.partSize := 0; msg.partPos := 0;
+				msg.valid := FALSE; msg.done := FALSE;
+				inner := InnerFrame(v, f);
+				IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+				IF msg.done THEN
+					IF msg.valid THEN v.bgap := v.sbW END
+				ELSIF v.w > 0 THEN
+					IF w < v.w THEN v.bgap := v.sbW
+					ELSIF w - v.sbW < v.w THEN depends := TRUE
+					END
+				END
+			ELSE v.bgap := v.sbW
+			END
+		END;
+		IF verBar IN v.opts THEN
+			IF verHide IN v.opts THEN
+				msg.focus := FALSE; msg.vertical := TRUE;
+				msg.wholeSize := 1; msg.partSize := 0; msg.partPos := 0;
+				msg.valid := FALSE; msg.done := FALSE;
+				inner := InnerFrame(v, f);
+				IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+				IF msg.done THEN
+					IF msg.valid THEN v.rgap := v.sbW END
+				ELSIF v.h > 0 THEN
+					IF h - v.bgap < v.h THEN v.rgap := v.sbW END
+				END
+			ELSE v.rgap := v.sbW
+			END
+		END;
+		IF depends & (v.rgap > 0) THEN v.bgap := v.sbW END;
+		CheckPos(v, v.orgX, v.orgY)
+	END Update;
+	
+	PROCEDURE Init (v: View; newView: BOOLEAN);
+		CONST min = 2 * Ports.mm; max = MAX(INTEGER); default = 50 * Ports.mm;
+		VAR c: Context; x: INTEGER; msg: Properties.ResizePref;
+	BEGIN
+		IF newView THEN
+			v.opts := v.opts + {horBar, verBar, horHide, verHide};
+			StdCFrames.dir.GetScrollBarSize(x, v.sbW);
+			IF v.view.context # NIL THEN
+				v.view.context.GetSize(v.w, v.h);
+				v.view := Views.CopyOf(v.view, Views.shallow)
+			ELSE
+				v.w := Views.undefined; v.h := Views.undefined;
+				Properties.PreferredSize(v.view, min, max, min, max, default, default, v.w, v.h)
+			END;
+			msg.fixed := FALSE;
+			msg.horFitToWin := FALSE; msg.verFitToWin := FALSE;
+			msg.horFitToPage := FALSE; msg.verFitToPage := FALSE;
+			Views.HandlePropMsg(v.view, msg);
+			IF ~msg.fixed THEN
+				INCL(v.opts, width); INCL(v.opts, height);
+				IF msg.horFitToWin OR msg.horFitToPage THEN v.w := 0 END;
+				IF msg.verFitToWin OR msg.verFitToPage THEN v.h := 0 END
+			END
+		END;
+		v.rgap := 0; v.bgap := 0;
+		IF showBorder IN v.opts THEN v.border := 2 * Ports.point ELSE v.border := 0 END;
+		NEW(v.inner); v.inner.v := v;
+		NEW(c); c.v := v; c.type := 3; v.inner.InitContext(c);
+		NEW(v.hor); v.hor.ver := FALSE; v.hor.v := v;
+		NEW(c); c.v := v; c.type := 2; v.hor.InitContext(c);
+		NEW(v.ver); v.ver.ver := TRUE; v.ver.v := v;
+		NEW(c); c.v := v; c.type := 1; v.ver.InitContext(c);
+		NEW(v.update); v.update.v := v;
+		Stores.Join(v, v.view);
+		Stores.Join(v, v.inner);
+		Stores.Join(v, v.hor);
+		Stores.Join(v, v.ver);
+		Services.DoLater(v.update, Services.now)
+	END Init;
+	
+	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		v.Internalize^(rd);
+		IF ~rd.cancelled THEN
+			rd.ReadVersion(0, 0, thisVersion);
+			IF ~rd.cancelled THEN
+				Views.ReadView(rd, v.view);
+				rd.ReadInt(v.sbW);
+				rd.ReadInt(v.orgX);
+				rd.ReadInt(v.orgY);
+				rd.ReadInt(v.w);
+				rd.ReadInt(v.h);
+				rd.ReadSet(v.opts);
+				Init(v, FALSE)
+			END
+		END
+	END Internalize;
+
+	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		v.Externalize^(wr);
+		wr.WriteVersion(0);
+		Views.WriteView(wr, v.view);
+		wr.WriteInt(v.sbW);
+		IF savePos IN v.opts THEN
+			wr.WriteInt(v.orgX);
+			wr.WriteInt(v.orgY)
+		ELSE
+			wr.WriteInt(0);
+			wr.WriteInt(0)
+		END;
+		wr.WriteInt(v.w);
+		wr.WriteInt(v.h);
+		wr.WriteSet(v.opts);
+	END Externalize;
+
+	PROCEDURE (v: View) ThisModel(): Models.Model;
+	BEGIN
+		RETURN v.view.ThisModel()
+	END ThisModel;
+	
+	PROCEDURE (v: View) CopyFromModelView (source: Views.View; model: Models.Model);
+	BEGIN
+		WITH source: View DO
+			IF model = NIL THEN v.view := Views.CopyOf(source.view, Views.deep)
+			ELSE v.view := Views.CopyWithNewModel(source.view, model)
+			END;
+			v.sbW := source.sbW;
+			v.orgX := source.orgX;
+			v.orgY := source.orgY;
+			v.w := source.w;
+			v.h := source.h;
+			v.opts := source.opts;
+		END;
+		Init(v, FALSE)
+	END CopyFromModelView;
+	
+	PROCEDURE (v: View) InitContext (context: Models.Context);
+		VAR c: Context;
+	BEGIN
+		v.InitContext^(context);
+		IF v.view.context = NIL THEN
+			NEW(c); c.v := v; c.type := 0; v.view.InitContext(c)
+		END
+	END InitContext;
+
+	PROCEDURE (v: View) Neutralize;
+	BEGIN
+		v.view.Neutralize
+	END Neutralize;
+
+	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR w, h: INTEGER;
+	BEGIN
+		v.context.GetSize(w, h);
+		IF showBorder IN v.opts THEN
+			v.border := 2 * f.dot;
+			f.DrawRect(0, f.dot, w, v.border, Ports.fill, Ports.black);
+			f.DrawRect(f.dot, 0, v.border, h, Ports.fill, Ports.black);
+			f.DrawRect(0, h - v.border, w, h - f.dot, Ports.fill, Ports.grey25);
+			f.DrawRect(w - v.border, 0, w - f.dot, h, Ports.fill, Ports.grey25);
+			f.DrawRect(0, 0, w, f.dot, Ports.fill, Ports.grey50);
+			f.DrawRect(0, 0, f.dot, h, Ports.fill, Ports.grey50);
+			f.DrawRect(0, h - f.dot, w, h, Ports.fill, Ports.white);
+			f.DrawRect(w - f.dot, 0, w, h, Ports.fill, Ports.white)
+		END;
+		Views.InstallFrame(f, v.inner, v.border, v.border, 0, TRUE);
+		IF v.bgap > 0 THEN Views.InstallFrame(f, v.hor, v.border, h - v.border - v.bgap, 0, FALSE) END;
+		IF v.rgap > 0 THEN Views.InstallFrame(f, v.ver, w - v.border - v.rgap, v.border, 0, FALSE) END
+	END Restore;
+	
+	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View);
+		VAR w, h, p, n: INTEGER;smsg: Controllers.ScrollMsg; inner: Views.Frame;
+	BEGIN
+		WITH msg: Controllers.WheelMsg DO
+			smsg.focus := FALSE; smsg.op := msg.op; smsg.pos := 0; smsg.done := FALSE; n := msg.nofLines;
+			IF (v.rgap > 0) OR (v.bgap > 0) THEN
+				smsg.vertical := v.rgap > 0;
+				REPEAT
+					smsg.done := FALSE;
+					inner := InnerFrame(v, f);
+					IF inner # NIL THEN Views.ForwardCtrlMsg(inner, smsg) END;
+					IF ~smsg.done THEN
+						Scroll(v, smsg.op, smsg.vertical, 0, p);
+						Views.ValidateRoot(Views.RootOf(f))
+					END;
+					DEC(n)
+				UNTIL n <= 0;
+				msg.done := TRUE
+			ELSE
+				focus := v.inner
+			END
+		| msg: Controllers.CursorMessage DO
+			v.context.GetSize(w, h);
+			IF msg.x > w - v.border - v.rgap THEN
+				IF msg.y <= h - v.border - v.bgap THEN focus := v.ver END
+			ELSIF msg.y > h - v.border - v.bgap THEN focus := v.hor
+			ELSE focus := v.inner
+			END
+		| msg: Controllers.PollSectionMsg DO
+			inner := InnerFrame(v, f);
+			IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+			IF ~msg.done THEN
+				PollSection(v, msg.vertical, msg.wholeSize, msg.partSize, msg.partPos);
+				msg.valid := msg.partSize < msg.wholeSize;
+				msg.done := TRUE
+			END
+		| msg: Controllers.ScrollMsg DO
+			inner := InnerFrame(v, f);
+			IF inner # NIL THEN Views.ForwardCtrlMsg(inner, msg) END;
+			IF ~msg.done THEN
+				Scroll(v, msg.op, msg.vertical, msg.pos, p);
+				Views.ValidateRoot(Views.RootOf(f));
+				msg.done := TRUE
+			END
+		ELSE focus := v.inner
+		END;
+		IF ~(msg IS Controllers.TickMsg) THEN
+			Services.DoLater(v.update, Services.now)
+		END
+	END HandleCtrlMsg;
+
+	PROCEDURE (v: View) HandleViewMsg (f: Views.Frame; VAR msg: Views.Message);
+		VAR b, r: INTEGER;
+	BEGIN
+		WITH msg: UpdateMsg DO
+			b := v.bgap; r := v.rgap;
+			Update(v, f);
+			IF (v.bgap # b) OR (v.rgap # r) THEN msg.changed := TRUE END
+		ELSE
+		END
+	END HandleViewMsg;
+
+	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
+		VAR w, h: INTEGER; p: Properties.Property; prop: Prop; fv: Views.View;
+	BEGIN
+		WITH msg: Properties.FocusPref DO
+			v.context.GetSize(w, h);
+			Views.HandlePropMsg(v.view, msg);
+			IF msg.atLocation THEN
+				IF (msg.x > w - v.border - v.rgap) & (msg.y > h - v.border - v.bgap) THEN
+					msg.hotFocus := FALSE; msg.setFocus := FALSE
+				ELSIF ((msg.x > w - v.border - v.rgap) OR (msg.y > h - v.border - v.bgap)) & ~msg.setFocus THEN
+					msg.hotFocus := TRUE
+				END
+			END
+		| msg: Properties.SizePref DO
+			IF (v.w > 0) & (v.h > 0) THEN
+				IF msg.w = Views.undefined THEN msg.w := 50 * Ports.mm END;
+				IF msg.h = Views.undefined THEN msg.h := 50 * Ports.mm END
+			ELSE
+				IF msg.w > v.rgap THEN DEC(msg.w, v.rgap + 2 * v.border) END;
+				IF msg.h > v.bgap THEN DEC(msg.h, v.bgap + 2 * v.border) END;
+				Views.HandlePropMsg(v.view, msg);
+				IF msg.w > 0 THEN INC(msg.w, v.rgap + 2 * v.border) END;
+				IF msg.h > 0 THEN INC(msg.h, v.bgap + 2 * v.border) END
+			END;
+			IF msg.w < 3 * v.sbW THEN msg.w := 3 * v.sbW END;
+			IF msg.h < 3 * v.sbW THEN msg.h := 3 * v.sbW END
+		| msg: Properties.ResizePref DO
+			Views.HandlePropMsg(v.view, msg);
+			IF v.w > 0 THEN
+				msg.fixed := FALSE;
+				msg.horFitToWin := TRUE;
+				msg.horFitToPage := FALSE
+			END;
+			IF v.h > 0 THEN
+				msg.fixed := FALSE;
+				msg.verFitToWin := TRUE;
+				msg.verFitToPage := FALSE
+			END
+		| msg: Properties.BoundsPref DO
+			Views.HandlePropMsg(v.view, msg);
+			INC(msg.w, 2 * v.border);
+			INC(msg.h, 2 * v.border);
+			IF (horBar IN v.opts) & ~(horHide IN v.opts) THEN INC(msg.w, v.sbW) END;
+			IF (verBar IN v.opts) & ~(verHide IN v.opts) THEN INC(msg.h, v.sbW) END
+		| msg: Properties.PollMsg DO
+			Views.HandlePropMsg(v.view, msg);
+			PollProp(v, prop); Properties.Insert(msg.prop, prop)
+		| msg: Properties.SetMsg DO
+			p := msg.prop; WHILE (p # NIL) & ~(p IS Prop) DO p := p.next END;
+			IF p # NIL THEN SetProp(v, p) END;
+			Views.HandlePropMsg(v.view, msg);
+		| msg: Properties.ControlPref DO
+			fv := msg.focus;
+			IF fv = v THEN msg.focus := v.view END;
+			Views.HandlePropMsg(v.view, msg);
+			msg.focus := fv
+		ELSE
+			Views.HandlePropMsg(v.view, msg);
+		END;
+	END HandlePropMsg;
+	
+	
+	(* InnerView *)
+	
+	PROCEDURE (v: InnerView) GetBackground (VAR color: Ports.Color);
+	BEGIN
+		color := Ports.background
+	END GetBackground;
+
+	PROCEDURE (v: InnerView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		Views.InstallFrame(f, v.v.view, -v.v.orgX, -v.v.orgY, 0, TRUE)
+	END Restore;
+	
+	PROCEDURE (v: InnerView) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		focus := v.v.view
+	END HandleCtrlMsg;
+
+
+	(* Context *)
+	
+	PROCEDURE (c: Context) MakeVisible (l, t, r, b: INTEGER);
+		VAR w, h, x, y: INTEGER;
+	BEGIN
+		IF ~(savePos IN c.v.opts) THEN
+			c.v.context.GetSize(w, h);
+			x := c.v.orgX; y := c.v.orgY;
+			IF c.v.w > 0 THEN
+				DEC(w, c.v.rgap + 2 * c.v.border);
+				IF r > x + w - Ports.point THEN x := r - w + Ports.point END;
+				IF l < x + Ports.point THEN x := l - Ports.point END;
+			END;
+			IF c.v.h > 0 THEN
+				DEC(h, c.v.bgap + 2 * c.v.border);
+				IF b > y + h - Ports.point THEN y := b - h + Ports.point END;
+				IF t < y + Ports.point THEN y := t - Ports.point END;
+			END;
+			IF (x # c.v.orgX) OR (y # c.v.orgY) THEN
+				CheckPos(c.v, x, y); c.v.orgX := x; c.v.orgY := y; 
+				Views.Update(c.v.view, Views.keepFrames)
+			END;
+			Services.DoLater(c.v.update, Services.now)
+		END
+	END MakeVisible;
+	
+	PROCEDURE (c: Context) Consider (VAR p: Models.Proposal);
+	BEGIN
+		c.v.context.Consider(p)
+	END Consider;
+	
+	PROCEDURE (c: Context) Normalize (): BOOLEAN;
+	BEGIN
+		RETURN ~(savePos IN c.v.opts)
+	END Normalize;
+	
+	PROCEDURE (c: Context) GetSize (OUT w, h: INTEGER);
+	BEGIN
+		c.v.context.GetSize(w, h);
+		DEC(w, c.v.rgap + 2 * c.v.border);
+		DEC(h, c.v.bgap + 2 * c.v.border);
+		IF c.type = 0 THEN
+			IF c.v.w > 0 THEN w := c.v.w END;
+			IF c.v.h > 0 THEN h := c.v.h END
+		ELSIF c.type = 1 THEN
+			w := c.v.rgap
+		ELSIF c.type = 2 THEN
+			h := c.v.bgap
+		END
+	END GetSize;
+	
+	PROCEDURE (c: Context) SetSize (w, h: INTEGER);
+		VAR w0, h0, w1, h1: INTEGER;
+	BEGIN
+		ASSERT(c.type = 0, 100);
+		c.v.context.GetSize(w0, h0); w1 := w0; h1 := h0;
+		IF c.v.w > 0 THEN c.v.w := w
+		ELSE w1 := w + c.v.rgap + 2 * c.v.border
+		END;
+		IF c.v.h > 0 THEN c.v.h := h
+		ELSE h1 := h + c.v.bgap + 2 * c.v.border
+		END;
+		IF (w1 # w0) OR (h1 # h0) THEN
+			c.v.context.SetSize(w1, h1)
+		END
+	END SetSize;
+	
+	PROCEDURE (c: Context) ThisModel (): Models.Model;
+	BEGIN
+		RETURN NIL
+	END ThisModel;
+	
+	
+	(* dialog *)
+	
+	PROCEDURE InitDialog*;
+		VAR  p: Properties.Property; u: INTEGER;
+	BEGIN
+		Properties.CollectProp(p);
+		WHILE (p # NIL) & ~(p IS Prop) DO p := p.next END;
+		IF p # NIL THEN
+			WITH p: Prop DO
+				IF Dialog.metricSystem THEN u := Ports.mm DIV 10 ELSE u := Ports.inch DIV 100 END;
+				dialog.valid := p.valid;
+				dialog.readOnly := p.readOnly;
+				IF ~p.horBar THEN dialog.horizontal.mode := 0
+				ELSIF p.horHide THEN dialog.horizontal.mode := 1
+				ELSE dialog.horizontal.mode := 2
+				END;
+				IF ~p.verBar THEN dialog.vertical.mode := 0
+				ELSIF p.verHide THEN dialog.vertical.mode := 1
+				ELSE dialog.vertical.mode := 2
+				END;
+				dialog.horizontal.size := p.width DIV u / 100;
+				dialog.vertical.size := p.height DIV u / 100;
+				dialog.horizontal.adapt := p.width = 0;
+				dialog.vertical.adapt := p.height = 0;
+				dialog.showBorder := p.showBorder;
+				dialog.savePos := p.savePos
+			END
+		END
+	END InitDialog;
+	
+	PROCEDURE Set*;
+		VAR p: Prop; u: INTEGER;
+	BEGIN
+		IF Dialog.metricSystem THEN u := 10 * Ports.mm ELSE u := Ports.inch END;
+		NEW(p); p.valid := dialog.valid;
+		p.horBar := dialog.horizontal.mode # 0;
+		p.verBar := dialog.vertical.mode # 0;
+		p.horHide := dialog.horizontal.mode = 1;
+		p.verHide := dialog.vertical.mode = 1;
+		IF ~dialog.horizontal.adapt THEN p.width := SHORT(ENTIER(dialog.horizontal.size * u)) END;
+		IF ~dialog.vertical.adapt THEN p.height := SHORT(ENTIER(dialog.vertical.size * u)) END;
+		p.showBorder := dialog.showBorder;
+		p.savePos := dialog.savePos;
+		Properties.EmitProp(NIL, p)
+	END Set;
+	
+	PROCEDURE DialogGuard* (VAR par: Dialog.Par);
+		VAR  p: Properties.Property;
+	BEGIN
+		Properties.CollectProp(p);
+		WHILE (p # NIL) & ~(p IS Prop) DO p := p.next END;
+		IF p = NIL THEN par.disabled := TRUE END
+	END DialogGuard;
+	
+	PROCEDURE HorAdaptGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF width IN dialog.readOnly THEN par.readOnly := TRUE END
+	END HorAdaptGuard;
+	
+	PROCEDURE VerAdaptGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF height IN dialog.readOnly THEN par.readOnly := TRUE END
+	END VerAdaptGuard;
+	
+	PROCEDURE WidthGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF dialog.horizontal.adapt THEN par.disabled := TRUE
+		ELSIF width IN dialog.readOnly THEN par.readOnly := TRUE
+		END
+	END WidthGuard;
+	
+	PROCEDURE HeightGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF dialog.vertical.adapt THEN par.disabled := TRUE
+		ELSIF height IN dialog.readOnly THEN par.readOnly := TRUE
+		END
+	END HeightGuard;
+	
+	
+	(* commands *)	
+	
+	PROCEDURE AddScroller*;
+		VAR poll: Controllers.PollOpsMsg; v: View; replace: Controllers.ReplaceViewMsg;
+	BEGIN
+		Controllers.PollOps(poll);
+		IF (poll.singleton # NIL) & ~(poll.singleton IS View) THEN
+			NEW(v); v.view := poll.singleton; Init(v, TRUE);
+			replace.old := poll.singleton; replace.new := v;
+			Controllers.Forward(replace)
+		ELSE Dialog.Beep
+		END
+	END AddScroller;
+
+	PROCEDURE RemoveScroller*;
+		VAR poll: Controllers.PollOpsMsg; replace: Controllers.ReplaceViewMsg;
+	BEGIN
+		Controllers.PollOps(poll);
+		IF (poll.singleton # NIL) & (poll.singleton IS View) THEN
+			replace.old := poll.singleton;
+			replace.new := Views.CopyOf(poll.singleton(View).view, Views.shallow);
+			Controllers.Forward(replace)
+		ELSE Dialog.Beep
+		END
+	END RemoveScroller;
+
+END StdScrollers.

+ 436 - 0
BlackBox/Std/Mod/Stamps.txt

@@ -0,0 +1,436 @@
+MODULE StdStamps;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/Stamps.odc *)
+	(* DO NOT EDIT *)
+
+(*
+	StdStamps are used to keep track of document changes, in particular program texts.
+	StdStamps carry a sequence number and a fingerprint of the document with them.
+	Each time the document (and therefore its fingerprint) is changed and stored,
+	the sequence number is incremented. (When determining the fingerprint of the
+	document, whitespace is ignored, except in string literals.)
+	
+	Each StdStamp also keeps track of the history of most recent changes.
+	For the last maxHistoryEntries sequence numbers, the date and time,
+	and an optional one-line comment is stored. To avoid too many entries in the history
+	while working on a module, the most recent history entry is overwritten upon the
+	generation of a new sequence number if the current date is the same as the date in
+	the history entry.
+
+*)
+
+	IMPORT
+		SYSTEM, (* SYSTEM.ROT only, for fingerprint calculation *)
+		Strings, Dates, StdCmds,
+		Ports, Models, Stores, Containers, Properties, Views, Controllers, Fonts,
+		TextModels, TextSetters, TextMappers, TextViews, TextRulers;
+
+	CONST
+		setCommentKey = "#Std:Set Comment";
+		maxHistoryEntries = 25;
+		minVersion = 0; origStampVersion = 0; thisVersion = 2;
+		
+	TYPE
+		History = ARRAY maxHistoryEntries OF RECORD
+			fprint, snr: INTEGER;	(* fingerprint, sequence number *)
+			date: INTEGER;			(* days since 1/1/1 *)
+			time: INTEGER;			(* min + 64 * hour *)
+			comment: POINTER TO ARRAY OF CHAR;	(* nil if no comment  *)
+		END;
+			
+		StdView = POINTER TO RECORD (Views.View)
+			(*--snr: LONGINT;*)			
+			nentries: INTEGER;	(* number of entries in history *)
+			history: History;			(* newest entry in history[0] *)
+			cache: ARRAY 64 OF CHAR;
+		END;
+
+		SetCmtOp = POINTER TO RECORD (Stores.Operation)
+			stamp: StdView;
+			oldcomment: POINTER TO ARRAY OF CHAR;
+		END;
+
+	VAR
+		comment*: RECORD
+			s*: ARRAY 64 OF CHAR;
+		END;
+
+
+	PROCEDURE (op: SetCmtOp) Do;
+		VAR temp: POINTER TO ARRAY OF CHAR;
+	BEGIN
+		temp := op.stamp.history[0].comment;
+		op.stamp.history[0].comment := op.oldcomment;
+		op.oldcomment := temp;
+	END Do;
+
+	PROCEDURE Format (v: StdView);
+		VAR s: ARRAY 64 OF CHAR; d: Dates.Date; t: INTEGER;
+	BEGIN
+		t := v.history[0].time;
+		Dates.DayToDate(v.history[0].date, d);
+		Dates.DateToString(d, Dates.plainAbbreviated, s); v.cache := s$;
+		Strings.IntToStringForm(v.history[0].snr, Strings.decimal, 4, "0", FALSE, s);
+		v.cache := v.cache + " (" + s + ")"
+	END Format;
+
+
+	PROCEDURE FontContext (v: StdView): Fonts.Font;
+		VAR c: Models.Context;
+	BEGIN
+		c := v.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			RETURN c(TextModels.Context).Attr().font;
+		ELSE
+			RETURN Fonts.dir.Default()
+		END;
+	END FontContext;
+
+	PROCEDURE CalcFP (t: TextModels.Model): INTEGER;
+		CONST sglQuote = "'"; dblQuote = '"';
+		VAR fp: INTEGER;  rd: TextModels.Reader; ch, quoteChar: CHAR; 
+	BEGIN
+		quoteChar := 0X; fp := 0;
+		rd := t.NewReader(NIL); rd.ReadChar(ch);
+		WHILE ~rd.eot DO
+			IF ch = quoteChar THEN quoteChar := 0X;
+			ELSIF (quoteChar = 0X) & ((ch = dblQuote) OR (ch = sglQuote)) THEN quoteChar := ch;
+			END;
+			IF (quoteChar = 0X) & (21X <= ch) & (ch # 8BX) & (ch # 8FX) & (ch # 0A0X) (* not in string literal *)
+				OR (quoteChar # 0X) & (20X <= ch) (* within string literal *)
+			THEN
+				fp := SYSTEM.ROT(fp, 1) + 13 * ORD(ch);
+			END;
+			rd.ReadChar(ch);
+		END;
+		RETURN fp;
+	END CalcFP;
+
+	PROCEDURE Update (v: StdView; forcenew: BOOLEAN);
+		VAR fp: INTEGER; i: INTEGER; ndays: INTEGER; d: Dates.Date; t: Dates.Time;
+	BEGIN
+		IF (v.context # NIL) & (v.context IS TextModels.Context) THEN
+			fp := CalcFP(v.context(TextModels.Context).ThisModel());
+			IF (fp # v.history[0].fprint) OR forcenew THEN
+				Dates.GetDate(d); Dates.GetTime(t);
+				ndays := Dates.Day(d);
+				IF (ndays # v.history[0].date) OR forcenew THEN
+					(* move down entries in history list *)
+					i := maxHistoryEntries-1;
+					WHILE i > 0 DO
+						v.history[i] := v.history[i-1];
+						DEC(i);
+					END;
+					v.history[0].comment := NIL;
+				END;
+				IF v.nentries < maxHistoryEntries THEN INC(v.nentries) END;
+				INC(v.history[0].snr);
+				v.history[0].fprint := fp;
+				v.history[0].date := ndays;
+				v.history[0].time := t.minute + t.hour*64;
+				Format(v);
+				Views.Update(v, Views.keepFrames);
+			END;
+		END;
+	END Update;
+
+	PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer);
+		VAR i, len: INTEGER;
+	BEGIN
+		Update(v, FALSE);
+		v.Externalize^(wr);
+		wr.WriteVersion(thisVersion);
+		(*--wr.WriteLInt(v.snr);*)
+		wr.WriteXInt(v.nentries);
+		FOR i := 0 TO v.nentries-1 DO
+			wr.WriteInt(v.history[i].fprint);
+			wr.WriteInt(v.history[i].snr);
+			wr.WriteInt(v.history[i].date);
+			wr.WriteXInt(v.history[i].time);
+			IF v.history[i].comment # NIL THEN
+				len := LEN(v.history[i].comment$);
+				wr.WriteXInt(len);
+				wr.WriteXString(v.history[i].comment^);
+			ELSE wr.WriteXInt(0);
+			END
+		END;
+	END Externalize;
+
+	PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader);
+		VAR version: INTEGER; format: BYTE; i, len: INTEGER;
+			d: Dates.Date; t: Dates.Time;
+	BEGIN
+		v.Internalize^(rd);
+		IF ~rd.cancelled THEN
+			rd.ReadVersion(minVersion, thisVersion, version);
+			IF ~rd.cancelled THEN
+				IF version = origStampVersion THEN (* deal with old StdStamp format *)
+					(* would like to calculate fingerprint, but hosting model not available at this time *)
+					v.history[0].fprint := 0;
+					v.history[0].snr := 1; v.nentries := 1;
+					rd.ReadXInt(d.year); rd.ReadXInt(d.month); rd.ReadXInt(d.day);
+					rd.ReadXInt(t.hour); rd.ReadXInt(t.minute); rd.ReadXInt(t.second);
+					rd.ReadByte(format); (* format not used anymore *)
+					v.history[0].date := Dates.Day(d);
+					v.history[0].time := t.minute + t.hour*64;
+				ELSE
+					IF version = 1 THEN rd.ReadInt(v.history[0].snr) END; (* red text: to be removed soon *)
+					rd.ReadXInt(v.nentries);
+					FOR i := 0 TO v.nentries-1 DO
+						rd.ReadInt(v.history[i].fprint);
+						IF version > 1 THEN rd.ReadInt(v.history[i].snr)
+						ELSIF (* (version = 1) & *) i > 0 THEN v.history[i].snr := v.history[i-1].snr - 1;
+						END; (* red text: to be removed soon *)
+						rd.ReadInt(v.history[i].date);
+						rd.ReadXInt(v.history[i].time);
+						rd.ReadXInt(len);
+						IF len > 0 THEN
+							NEW(v.history[i].comment, len + 1);
+							rd.ReadXString(v.history[i].comment^);
+						ELSE v.history[i].comment := NIL;
+						END
+					END;
+				END;
+				Format(v);
+			END
+		END
+	END Internalize;
+
+	PROCEDURE (v: StdView) CopyFromSimpleView (source: Views.View);
+		VAR i: INTEGER;
+	BEGIN
+		(* v.CopyFrom^(source); *)
+		WITH source: StdView DO
+			(*--v.snr := source.snr;*)
+			v.nentries := source.nentries;
+			v.history := source.history;
+			v.cache := source.cache;
+			FOR i := 0 TO v.nentries - 1 DO
+				IF source.history[i].comment # NIL THEN
+					NEW(v.history[i].comment, LEN(source.history[i].comment$) + 1);
+					v.history[i].comment^ := source.history[i].comment^$;
+				END
+			END
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR a: TextModels.Attributes; color: Ports.Color; c: Models.Context; font: Fonts.Font;
+			asc, dsc, fw: INTEGER;
+	BEGIN
+		c := v.context;
+		IF (c # NIL) & (c IS TextModels.Context) THEN
+			a := v.context(TextModels.Context).Attr();
+			font := a.font;
+			color := a.color;
+		ELSE font := Fonts.dir.Default(); color := Ports.black;
+		END;
+		font.GetBounds(asc, dsc, fw);
+		f.DrawLine(f.l, asc + f.dot, f.r, asc + f.dot, 1, Ports.grey25 );
+		f.DrawString(0, asc, color, v.cache, font);
+	END Restore;
+
+	PROCEDURE SizePref (v: StdView; VAR p: Properties.SizePref);
+		VAR font: Fonts.Font; asc, dsc, w: INTEGER; d: Dates.Date; s: ARRAY 64 OF CHAR;
+	BEGIN
+		font := FontContext(v);
+		font.GetBounds(asc, dsc, w);
+		d.day := 28; d.month := 1; d.year := 2222; p.w := 0;
+		WHILE d.month <= 12 DO
+			Dates.DateToString(d, Dates.plainAbbreviated, s);
+			s := s + " (0000)";
+			w := font.StringWidth(s);
+			IF w > p.w THEN p.w := w END;
+			INC(d.month)
+		END;
+		p.h := asc + dsc;
+	END SizePref;
+
+	PROCEDURE (v: StdView) HandlePropMsg (VAR msg: Properties.Message);
+		VAR font: Fonts.Font; asc, w: INTEGER;
+	BEGIN
+		WITH msg: Properties.Preference DO
+			WITH msg: Properties.SizePref DO
+				SizePref(v, msg)
+			| msg: Properties.ResizePref DO
+				msg.fixed := TRUE
+			| msg: Properties.FocusPref DO
+				msg.hotFocus := TRUE
+			| msg: TextSetters.Pref DO
+				font := FontContext(v);
+				font.GetBounds(asc, msg.dsc, w);
+			ELSE
+			END
+		ELSE
+		END
+	END HandlePropMsg;
+
+	PROCEDURE NewRuler (): TextRulers.Ruler;
+		CONST mm = Ports.mm;
+		VAR r: TextRulers.Ruler;
+	BEGIN
+		r := TextRulers.dir.New(NIL);
+		TextRulers.SetRight(r, 140 * mm);
+		TextRulers.AddTab(r, 15 * mm); TextRulers.AddTab(r, 35 * mm); TextRulers.AddTab(r, 75 * mm);
+		RETURN r
+	END NewRuler;
+
+	PROCEDURE ShowHistory (v: StdView);
+		VAR text: TextModels.Model; f: TextMappers.Formatter;
+			i: INTEGER; d: Dates.Date; s: ARRAY 64 OF CHAR;
+			tv: TextViews.View; attr: TextModels.Attributes;
+	BEGIN
+		text := TextModels.dir.New();
+		f.ConnectTo(text);
+		attr := f.rider.attr;
+		f.rider.SetAttr(TextModels.NewStyle(attr, {Fonts.italic}));
+		f.WriteString("seq nr."); f.WriteTab;
+		f.WriteString("fingerprint"); f.WriteTab;
+		f.WriteString("date and time"); f.WriteTab;
+		f.WriteString("comment"); f.WriteLn;
+		f.rider.SetAttr(attr); f.WriteLn;
+		(*--n := v.snr;*)
+		FOR i := 0 TO v.nentries-1 DO
+			f.WriteIntForm(v.history[i].snr, 10, 4, "0", FALSE);
+			(*--DEC(n);*)
+			f.WriteTab;
+			f.WriteIntForm(v.history[i].fprint, TextMappers.hexadecimal, 8, "0", FALSE);
+			f.WriteTab;
+			Dates.DayToDate(v.history[i].date, d);
+			Dates.DateToString(d, Dates.plainAbbreviated, s);
+			f.WriteString(s);
+			f.WriteString("  ");
+			f.WriteIntForm(v.history[i].time DIV 64, 10, 2, "0", FALSE);
+			f.WriteString(":");
+			f.WriteIntForm(v.history[i].time MOD 64, 10, 2, "0", FALSE);
+			IF v.history[i].comment # NIL THEN
+				f.WriteTab;
+				f.WriteString( v.history[i].comment^);
+			END;
+			f.WriteLn;
+		END;
+		tv := TextViews.dir.New(text);
+		tv.SetDefaults(NewRuler(), TextViews.dir.defAttr);
+		tv.ThisController().SetOpts({Containers.noFocus, Containers.noCaret});
+		Views.OpenAux(tv, "History");
+	END ShowHistory;
+
+	PROCEDURE Track (v: StdView; f: Views.Frame; x, y: INTEGER; buttons: SET);
+		VAR c: Models.Context; w, h: INTEGER; isDown, in, in0: BOOLEAN; m: SET;
+	BEGIN
+		c := v.context; c.GetSize(w, h); in0 := FALSE; in := TRUE;
+		REPEAT
+			IF in # in0 THEN
+				f.MarkRect(0, 0, w, h, Ports.fill, Ports.invert, Ports.show); in0 := in
+			END;
+			f.Input(x, y, m, isDown);
+			in := (0 <= x) & (x < w) & (0 <= y) & (y < h)
+		UNTIL ~isDown;
+		IF in0 THEN
+			f.MarkRect(0, 0, w, h, Ports.fill, Ports.invert, Ports.hide);
+			IF Controllers.modify IN m THEN
+				IF v.history[0].comment # NIL THEN comment.s := v.history[0].comment^$;
+				ELSE comment.s := "";
+				END;
+				StdCmds.OpenToolDialog("Std/Rsrc/Stamps", "Comment");
+			ELSE ShowHistory(v);
+			END
+		END
+	END Track;
+
+	PROCEDURE (v: StdView) HandleCtrlMsg (
+				f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View);
+	BEGIN
+		WITH msg: Controllers.TrackMsg DO
+			Track(v, f, msg.x, msg.y, msg.modifiers)
+		| msg: Controllers.PollCursorMsg DO
+			msg.cursor := Ports.refCursor
+		ELSE
+		END
+	END HandleCtrlMsg;
+
+
+	(* ------------ programming interface: ---------------------- *)
+
+	PROCEDURE GetFirstInText* (t: TextModels.Model): Views.View;
+		VAR r: TextModels.Reader; v: Views.View;
+	BEGIN
+		IF t # NIL THEN
+			r := t.NewReader(NIL);
+			REPEAT r.ReadView(v) UNTIL (v = NIL) OR (v IS StdView);
+			RETURN v;
+		ELSE RETURN NIL;
+		END;
+	END GetFirstInText;
+
+	PROCEDURE IsStamp* (v: Views.View): BOOLEAN;
+	BEGIN
+		RETURN v IS StdView;
+	END IsStamp;
+
+	PROCEDURE GetInfo* (v: Views.View; VAR snr, historylen: INTEGER);
+	BEGIN
+		ASSERT(v IS StdView, 20);
+		WITH v: StdView DO
+			snr := v.history[0].snr; historylen := v.nentries;
+		END
+	END GetInfo;
+
+	PROCEDURE GetData* (v: Views.View; entryno: INTEGER;
+				VAR fprint: INTEGER; VAR date: Dates.Date; VAR time: Dates.Time);
+	BEGIN
+		ASSERT(v IS StdView, 20);
+		WITH v: StdView DO
+			IF entryno <= v.nentries THEN
+				fprint := v.history[entryno].fprint;
+				Dates.DayToDate(v.history[entryno].date, date);
+				time.minute := v.history[entryno].time MOD 64;
+				time.minute := v.history[entryno].time DIV 64;
+				time.second := 0;
+			END
+		END
+	END GetData;
+
+	(** Insert new history entry with comment in v. *)
+	PROCEDURE Stamp* (v: Views.View; comment: ARRAY OF CHAR);
+	BEGIN
+		ASSERT(v IS StdView, 20);
+		WITH v: StdView DO
+			Update(v, TRUE);
+			NEW(v.history[0].comment, LEN(comment$) + 1);
+			v.history[0].comment^ := comment$;
+		END
+	END Stamp;
+
+	PROCEDURE New* (): Views.View;
+		VAR v: StdView; d: Dates.Date; t: Dates.Time;
+	BEGIN
+		NEW(v); v.history[0].snr := 0; v.nentries := 0;
+		v.history[0].fprint := 0;
+		Dates.GetDate(d); Dates.GetTime(t);
+		v.history[0].date := Dates.Day(d);
+		v.history[0].time := t.minute + t.hour*64;
+		Format(v);
+		RETURN v;
+	END New;
+
+	PROCEDURE SetComment*;
+		VAR v: Views.View; op: SetCmtOp;
+	BEGIN
+		v := GetFirstInText(TextViews.FocusText());
+		IF v # NIL THEN
+			WITH v: StdView DO
+				NEW(op); op.stamp := v;
+				NEW(op.oldcomment, LEN(comment.s$) + 1);
+				op.oldcomment^ := comment.s$;
+				Views.Do(v, setCommentKey, op);
+			END
+		END
+	END SetComment;
+
+	PROCEDURE Deposit*;
+	BEGIN
+		Views.Deposit(New())
+	END Deposit;
+
+END StdStamps.

+ 133 - 0
BlackBox/Std/Mod/ViewSizer.txt

@@ -0,0 +1,133 @@
+MODULE StdViewSizer;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Std/Mod/ViewSizer.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT Services, Ports, Dialog, Views, Containers, Properties;
+
+	CONST width = 1; height = 2;
+
+	VAR
+		size*: RECORD
+			typeName-: Dialog.String;
+			w*, h*: REAL;
+			proportional*, fixedW, fixedH: BOOLEAN;
+			unit, scaleW, scaleH, lastChanged: INTEGER;
+			unitText: ARRAY 6 OF CHAR;
+			view: Views.View;
+			container: Containers.Controller
+		END;
+
+	PROCEDURE ConnectDialog (v: Views.View; c: Containers.Controller);
+		VAR pref: Properties.ResizePref;
+	BEGIN
+		IF (v # NIL) & (v.context # NIL) THEN
+			IF Dialog.metricSystem THEN size.unit := Ports.mm * 10; size.unitText := "cm"
+			ELSE size.unit := Ports.inch; size.unitText := "inch"
+			END;
+			size.view := v; size.container := c;
+			Services.GetTypeName(v, size.typeName);
+			v.context.GetSize(size.scaleW, size.scaleH);
+			size.w := size.scaleW / size.unit; size.h := size.scaleH / size.unit;
+			pref.fixed := FALSE;
+			pref.horFitToPage := FALSE; pref.verFitToPage := FALSE;
+			pref.horFitToWin := FALSE; pref.verFitToWin := FALSE;
+			Views.HandlePropMsg(v, pref);
+			size.fixedW := pref.fixed;
+			size.fixedH := pref.fixed;
+			size.proportional := FALSE
+		ELSE
+			size.view := NIL; size.container := c; size.typeName := ""
+		END;
+		Dialog.Update(size)
+	END ConnectDialog;
+
+	PROCEDURE SetViewSize*;
+	BEGIN
+		IF size.view # NIL THEN
+			size.view.context.SetSize(SHORT(ENTIER(size.w * size.unit + 0.5)),
+												SHORT(ENTIER(size.h * size.unit + 0.5)));
+			IF size.container # NIL THEN size.container.SetSingleton(size.view) END;
+			ConnectDialog(size.view, size.container)
+		ELSE Dialog.Beep
+		END
+	END SetViewSize;
+
+	PROCEDURE InitDialog*;
+		VAR v: Views.View; c: Containers.Controller;
+	BEGIN
+		c := Containers.Focus();
+		IF c # NIL THEN v := c.Singleton() ELSE v := NIL END;
+		IF (v # size.view) OR (c # size.container) THEN ConnectDialog(v, c) END
+	END InitDialog;
+
+	PROCEDURE ResetDialog*;
+		VAR proportional: BOOLEAN; v: Views.View;
+	BEGIN
+		proportional := size.proportional; v := size.view;
+		size.view := NIL; InitDialog;
+		IF proportional & (v = size.view) THEN size.proportional := TRUE; Dialog.Update(size) END
+	END ResetDialog;
+
+	PROCEDURE WidthGuard* (VAR par: Dialog.Par);
+	BEGIN
+		InitDialog;
+		par.disabled := size.view = NIL;
+		par.readOnly := size.fixedW
+	END WidthGuard;
+
+	PROCEDURE HeightGuard* (VAR par: Dialog.Par);
+	BEGIN
+		InitDialog;
+		par.disabled := size.view = NIL;
+		par.readOnly := size.fixedH
+	END HeightGuard;
+
+	PROCEDURE ProportionGuard* (VAR par: Dialog.Par);
+	BEGIN
+		par.disabled := (size.view = NIL) OR size.fixedW OR size.fixedH OR (size.scaleW = 0) OR (size.scaleH = 0)
+	END ProportionGuard;
+
+	PROCEDURE UnitGuard* (VAR par: Dialog.Par);
+	BEGIN
+		IF size.view # NIL THEN par.label := size.unitText$ ELSE par.label := "" END
+	END UnitGuard;
+
+	PROCEDURE AdjustDialogToPref (fixedW, fixedH: BOOLEAN);
+		VAR w, h: INTEGER; w0, h0: REAL; pref: Properties.SizePref;
+	BEGIN
+		w := SHORT(ENTIER(size.w * size.unit + 0.5)); h := SHORT(ENTIER(size.h * size.unit + 0.5));
+		IF size.proportional & (w > 0) & (h > 0) & (size.scaleW > 0) & (size.scaleH > 0) THEN
+			Properties.ProportionalConstraint(size.scaleW, size.scaleH, fixedW, fixedH, w, h)
+		END;
+		pref.w := w; pref.h := h; pref.fixedW := fixedW; pref.fixedH := fixedH;
+		Views.HandlePropMsg(size.view, pref);
+		IF ~fixedW THEN w0 := pref.w / size.unit ELSE w0 := size.w END;
+		IF ~fixedH THEN h0 := pref.h / size.unit ELSE h0 := size.h END;
+		IF (w0 # size.w) OR (h0 # size.h) THEN size.w := w0; size.h := h0; Dialog.Update(size) END
+	END AdjustDialogToPref;
+
+	PROCEDURE WNotifier* (op, from, to: INTEGER);
+	BEGIN
+		IF size.w > 0 THEN AdjustDialogToPref(TRUE, FALSE); size.lastChanged := width
+		ELSIF size.w # 0 THEN Dialog.Beep
+		END
+	END WNotifier;
+
+	PROCEDURE HNotifier* (op, from, to: INTEGER);
+	BEGIN
+		IF size.h > 0 THEN AdjustDialogToPref(FALSE, TRUE); size.lastChanged := height
+		ELSIF size.h # 0 THEN Dialog.Beep
+		END
+	END HNotifier;
+
+	PROCEDURE ProportionNotifier* (op, from, to: INTEGER);
+	BEGIN
+		IF (op = Dialog.changed) & size.proportional THEN
+			IF size.lastChanged = width THEN AdjustDialogToPref(TRUE, FALSE)
+			ELSIF size.lastChanged = height THEN AdjustDialogToPref(FALSE, TRUE)
+			END
+		END
+	END ProportionNotifier;
+
+END StdViewSizer.

+ 3163 - 0
BlackBox/System/Mod/Controls.txt

@@ -0,0 +1,3163 @@
+MODULE Controls;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 System/Mod/Controls.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Kernel, Dates, Dialog, Meta, Services, Stores, Views, Properties, 
+		Strings, Fonts, Ports, Controllers, Windows, StdCFrames;
+
+	CONST
+		(** elements of Property.valid **)
+		opt0* = 0; opt1* = 1; opt2* = 2; opt3* = 3; opt4* = 4;
+		link* = 5; label* = 6; guard* = 7; notifier* = 8; level* = 9;
+
+		default* = opt0; cancel* = opt1;
+		left* = opt0; right* = opt1; multiLine* = opt2; password* = opt3;
+		sorted* = opt0;
+		haslines* = opt1; hasbuttons* = opt2; atroot* = opt3; foldericons* = opt4;
+
+		minVersion = 0; maxBaseVersion = 4;
+		pbVersion = 0; cbVersion = 0; rbVersion = 0; fldVersion = 0; 
+		dfldVersion = 0; tfldVersion = 0; cfldVersion = 0;
+		lbxVersion = 0; sbxVersion = 0; cbxVersion = 0; capVersion = 1; grpVersion = 0; 
+		tfVersion = 0; 
+
+		rdel = 07X; ldel = 08X;  tab = 09X; ltab = 0AX; lineChar = 0DX; esc = 01BX;
+		arrowLeft = 1CX; arrowRight = 1DX; arrowUp = 1EX; arrowDown = 1FX;
+
+		update = 2;	(* notify options *)
+		listUpdate = 3;
+		guardCheck = 4;
+		flushCaches = 5;	(* re-map labels for flushed string resources, after a language change *)
+		
+		maxAdr = 8;
+
+	TYPE
+		Prop* = POINTER TO RECORD (Properties.Property)
+			opt*: ARRAY 5 OF BOOLEAN;
+			link*: Dialog.String;
+			label*: Dialog.String;
+			guard*: Dialog.String;
+			notifier*: Dialog.String;
+			level*: INTEGER
+		END;
+		
+		Directory* = POINTER TO ABSTRACT RECORD END;
+
+		Control* = POINTER TO ABSTRACT RECORD (Views.View)
+			item-: Meta.Item;
+			disabled-, undef-, readOnly-, customFont-: BOOLEAN;
+			font-: Fonts.Font;
+			label-: Dialog.String;
+			prop-: Prop;
+			adr: ARRAY maxAdr OF INTEGER;
+			num: INTEGER;
+			stamp: INTEGER;
+			shortcut: CHAR;
+			guardErr, notifyErr: BOOLEAN
+		END;
+
+		DefaultsPref* = RECORD (Properties.Preference)
+			disabled*: BOOLEAN;	(** OUT, preset to ~c.item.Valid() *)
+			undef*: BOOLEAN;	(** OUT, preset to FALSE *)
+			readOnly*: BOOLEAN	(** OUT, preset to c.item.vis = readOnly *)
+		END;
+
+		PropPref* = RECORD (Properties.Preference)
+			valid*: SET	(** OUT, preset to {link, label, guard, notifier, customFont} *)
+		END;
+
+		PushButton = POINTER TO RECORD (Control) END;
+
+		CheckBox = POINTER TO RECORD (Control) END;
+
+		RadioButton = POINTER TO RECORD (Control) END;
+
+		Field = POINTER TO RECORD (Control)
+			maxLen: INTEGER
+		END;
+
+		UpDownField = POINTER TO RECORD (Control)
+			min, max, inc: INTEGER
+		END;
+
+		DateField = POINTER TO RECORD (Control) 
+			selection: INTEGER	(* 0: no selection, 1..n-1: this part selected, -1: part n selected *)
+		END;
+
+		TimeField = POINTER TO RECORD (Control) 
+			selection: INTEGER
+		END;
+
+		ColorField = POINTER TO RECORD (Control) END;
+
+		ListBox = POINTER TO RECORD (Control) END;
+
+		SelectionBox = POINTER TO RECORD (Control) END;
+
+		ComboBox = POINTER TO RECORD (Control) END;
+
+		Caption = POINTER TO RECORD (Control) END;
+
+		Group = POINTER TO RECORD (Control) END;
+
+		TreeControl = POINTER TO RECORD (Control) END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+		Op = POINTER TO RECORD (Stores.Operation)
+			ctrl: Control;
+			prop: Prop
+		END;
+
+		FontOp = POINTER TO RECORD (Stores.Operation)
+			ctrl: Control;
+			font: Fonts.Font;
+			custom: BOOLEAN
+		END;
+
+		NotifyMsg = RECORD (Views.NotifyMsg)
+			frame: Views.Frame;
+			op, from, to: INTEGER
+		END;
+
+		UpdateCachesMsg = RECORD (Views.UpdateCachesMsg) END;
+
+		SelectPtr = POINTER TO Dialog.Selection;
+		
+		ProcValue = RECORD (Meta.Value) p*: PROCEDURE END;
+		SelectValue = RECORD (Meta.Value) p*: SelectPtr END;
+		GuardProcVal = RECORD (Meta.Value) p*: Dialog.GuardProc END;
+		NotifyProcValOld = RECORD (Meta.Value) p*: PROCEDURE (op, from, to: INTEGER) END;
+		GuardProcPVal = RECORD (Meta.Value) p*: PROCEDURE(n: INTEGER; VAR p: Dialog.Par) END;
+		NotifyProcPVal = RECORD (Meta.Value) p*: PROCEDURE(n, op, f, t: INTEGER) END;
+
+		Param = RECORD from, to, i: INTEGER; n: Dialog.String END;
+		
+		TVParam = RECORD l: INTEGER; e: BOOLEAN; nodeIn, nodeOut: Dialog.TreeNode END; 
+
+		Action = POINTER TO RECORD (Services.Action) 
+			w: Windows.Window;
+			resolution, cnt: INTEGER
+		END;
+
+		TrapCleaner = POINTER TO RECORD (Kernel.TrapCleaner) END;
+
+	VAR
+		dir-, stdDir-: Directory;
+		par-: Control;
+		stamp: INTEGER;
+		action: Action;
+		cleaner:  TrapCleaner;
+		cleanerInstalled: INTEGER;
+
+
+	(** Cleaner **)
+
+	PROCEDURE (c: TrapCleaner) Cleanup;
+	BEGIN
+		par := NIL;
+		cleanerInstalled := 0
+	END Cleanup;
+
+
+	PROCEDURE (c: Control) Update- (f: Views.Frame; op, from, to: INTEGER), NEW, EMPTY;
+	PROCEDURE (c: Control) UpdateList- (f: Views.Frame), NEW, EMPTY;
+	PROCEDURE (c: Control) CheckLink- (VAR ok: BOOLEAN), NEW, EMPTY;
+	PROCEDURE (c: Control) HandlePropMsg2- (VAR p: Views.PropMessage), NEW, EMPTY;
+	PROCEDURE (c: Control) HandleViewMsg2- (f: Views.Frame; VAR msg: Views.Message), NEW, EMPTY;
+	PROCEDURE (c: Control) HandleCtrlMsg2- (f: Views.Frame; VAR msg: Views.CtrlMessage;
+																	VAR focus: Views.View), NEW, EMPTY;
+	PROCEDURE (c: Control) Externalize2- (VAR wr: Stores.Writer), NEW, EMPTY;
+	PROCEDURE (c: Control) Internalize2- (VAR rd: Stores.Reader), NEW, EMPTY;
+
+
+	(* auxiliary procedures *)
+
+	PROCEDURE IsShortcut (ch: CHAR; c: Control): BOOLEAN;
+	BEGIN
+		IF (ch >= "a") & (ch <= "z") OR (ch >= 0E0X) THEN ch := CAP(ch) END;
+		RETURN ch = c.shortcut
+	END IsShortcut;
+
+	PROCEDURE ExtractShortcut (c: Control);
+		VAR label: Dialog.String; i: INTEGER; ch, sCh: CHAR;
+	BEGIN
+		Dialog.MapString(c.label, label);
+		i := 0; ch := label[0]; sCh := "&";
+		WHILE sCh = "&" DO
+			WHILE (ch # 0X) & (ch # "&") DO INC(i); ch := label[i] END;
+			IF ch = 0X THEN sCh := 0X
+			ELSE INC(i); sCh := label[i]; INC(i); ch := label[i]
+			END
+		END;
+		IF (sCh >= "a") & (sCh <= "z") OR (sCh >= 0E0X) THEN sCh := CAP(sCh) END;
+		c.shortcut := sCh
+	END ExtractShortcut;
+
+	PROCEDURE GetGuardProc (name: ARRAY OF CHAR; VAR i: Meta.Item; VAR err: BOOLEAN;
+												VAR par: BOOLEAN; VAR n: INTEGER);
+		VAR j, k, e: INTEGER; num: ARRAY 32 OF CHAR;
+	BEGIN
+		j := 0;
+		WHILE (name[j] # 0X) & (name[j] # "(") DO INC(j) END;
+		IF name[j] = "(" THEN
+			INC(j); k := 0;
+			WHILE (name[j] # 0X) & (name[j] # ")") DO num[k] := name[j]; INC(j); INC(k) END;
+			IF (name[j] = ")") & (name[j+1] = 0X) THEN
+				num[k] := 0X; Strings.StringToInt(num, n, e);
+				IF e = 0 THEN
+					name[j - k - 1] := 0X;
+					Meta.LookupPath(name, i); par := TRUE
+				ELSE
+					IF ~err THEN
+						Dialog.ShowParamMsg("#System:SyntaxErrorIn", name, "", "");
+						err := TRUE
+					END;
+					Meta.Lookup("", i);
+					RETURN
+				END
+			ELSE
+				IF ~err THEN
+					Dialog.ShowParamMsg("#System:SyntaxErrorIn", name, "", "");
+					err := TRUE
+				END;
+				Meta.Lookup("", i);
+				RETURN
+			END
+		ELSE
+			Meta.LookupPath(name, i); par := FALSE
+		END;
+		IF (i.obj = Meta.procObj) OR (i.obj = Meta.varObj) & (i.typ = Meta.procTyp) THEN (*ok *)
+		ELSE
+			IF ~err THEN
+				IF i.obj = Meta.undef THEN
+					Dialog.ShowParamMsg("#System:NotFound", name, "", "")
+				ELSE
+					Dialog.ShowParamMsg("#System:HasWrongType", name, "", "")
+				END;
+				err := TRUE
+			END;
+			Meta.Lookup("", i)
+		END
+	END GetGuardProc;
+	
+	PROCEDURE CallGuard (c: Control);
+		VAR ok, up: BOOLEAN; n: INTEGER; dpar: Dialog.Par; p: Control;
+			v: GuardProcVal; vp: GuardProcPVal; i: Meta.Item; pref: DefaultsPref;
+	BEGIN
+		Controllers.SetCurrentPath(Controllers.targetPath);
+		pref.disabled := ~c.item.Valid();
+		pref.undef := FALSE;
+		pref.readOnly := c.item.vis = Meta.readOnly;
+		Views.HandlePropMsg(c, pref);
+		c.disabled := pref.disabled;
+		c.undef := pref.undef;
+		c.readOnly := pref.readOnly;
+		c.label := c.prop.label$;
+		IF ~c.disabled & (c.prop.guard # "") & ~c.guardErr THEN
+			IF cleanerInstalled = 0 THEN Kernel.PushTrapCleaner(cleaner) END;
+			INC(cleanerInstalled);
+			p := par; par := c;
+			dpar.disabled := FALSE; dpar.undef := FALSE;
+			dpar.readOnly := c.readOnly;
+			dpar.checked := FALSE; dpar.label := c.label$;
+			GetGuardProc(c.prop.guard, i, c.guardErr, up, n);
+			IF i.obj # Meta.undef THEN
+				IF up THEN	(* call with numeric parameter *)
+					i.GetVal(vp, ok);
+					IF ok THEN vp.p(n, dpar) END
+				ELSE
+					i.GetVal(v, ok);
+					IF ok THEN v.p(dpar) END
+				END;
+				IF ok THEN
+					c.disabled := dpar.disabled;
+					c.undef := dpar.undef;
+					IF dpar.readOnly THEN c.readOnly := TRUE END;
+					IF dpar.label # c.label THEN c.label := dpar.label END
+				ELSIF ~c.guardErr THEN
+					Dialog.ShowParamMsg("#System:HasWrongType", c.prop.guard, "", "");
+					c.guardErr := TRUE
+				END
+			END;
+			par := p;
+			DEC(cleanerInstalled);
+			IF cleanerInstalled = 0 THEN Kernel.PopTrapCleaner(cleaner) END
+		END;
+		ExtractShortcut(c);
+		Controllers.ResetCurrentPath()
+	END CallGuard;
+
+	PROCEDURE CallNotifier (c: Control; op, from, to: INTEGER);
+		VAR ok, up: BOOLEAN; n: INTEGER; vold: NotifyProcValOld; vp: NotifyProcPVal;
+			i: Meta.Item; p: Control;
+	BEGIN
+		IF c.prop.notifier # "" THEN
+			IF cleanerInstalled = 0 THEN Kernel.PushTrapCleaner(cleaner) END;
+			INC(cleanerInstalled);
+			p := par; par := c;
+			IF c.prop.notifier[0] = "!" THEN
+				IF op = Dialog.pressed THEN
+					c.prop.notifier[0] := " ";
+					Dialog.ShowStatus(c.prop.notifier);
+					c.prop.notifier[0] := "!"
+				ELSIF op = Dialog.released THEN
+					Dialog.ShowStatus("")
+				END
+			ELSE
+				GetGuardProc(c.prop.notifier, i, c.notifyErr, up, n);
+				IF i.obj # Meta.undef THEN
+					IF up THEN	(* call with numeric parameter *)
+						i.GetVal(vp, ok);
+						IF ok THEN vp.p(n, op, from, to) END
+					ELSE
+						i.GetVal(vold, ok);
+						IF ok THEN vold.p(op, from, to) END
+					END;
+					IF ~ok & ~c.notifyErr THEN
+						Dialog.ShowParamMsg("#System:HasWrongType", c.prop.notifier, "", "");
+						c.notifyErr := TRUE
+					END
+				END
+			END;
+			par := p;
+			DEC(cleanerInstalled);
+			IF cleanerInstalled = 0 THEN Kernel.PopTrapCleaner(cleaner) END
+		END
+	END CallNotifier;
+
+	PROCEDURE DCHint (modifiers: SET): INTEGER;
+	BEGIN
+		IF Controllers.doubleClick IN modifiers THEN RETURN 1
+		ELSE RETURN 0
+		END
+	END DCHint;
+
+	PROCEDURE Notify* (c: Control; f: Views.Frame; op, from, to: INTEGER);
+		VAR msg: NotifyMsg;
+	BEGIN
+		IF ~c.readOnly & ~ c.disabled THEN
+			CallNotifier(c, op, from, to);
+			IF op >= Dialog.changed THEN
+				msg.id0 := c.item.adr; msg.id1 := msg.id0 + c.item.Size(); msg.frame := f;
+				msg.op := op; msg.from := from; msg.to := to;
+				msg.opts := {update, guardCheck};
+				Views.Omnicast(msg)
+			END
+		END
+	END Notify;
+
+	PROCEDURE NotifyFlushCaches*;
+		VAR msg: NotifyMsg;
+	BEGIN
+		msg.opts := {flushCaches}; msg.id0 := 0; msg.id1 := 0;
+		Views.Omnicast(msg)
+	END NotifyFlushCaches;
+	
+	PROCEDURE GetName (VAR path, name: ARRAY OF CHAR; VAR i: INTEGER);
+		VAR j: INTEGER; ch: CHAR;
+	BEGIN
+		j := 0; ch := path[i];
+		WHILE (j < LEN(name) - 1) & ((ch >= "0") & (ch <= "9") OR (CAP(ch) >= "A") & (CAP(ch) <= "Z")
+												OR (ch >= 0C0X) & (ch # "×") & (ch # "÷") & (ch <= 0FFX) OR (ch = "_")) DO
+			name[j] := ch; INC(i); INC(j); ch := path[i]
+		END;
+		IF (ch = 0X) OR (ch = ".") OR (ch = "[") OR (ch = "^") THEN name[j] := 0X
+		ELSE name[0] := 0X
+		END
+	END GetName;
+
+	PROCEDURE LookupPath (path: ARRAY OF CHAR; VAR i: Meta.Item;
+												VAR adr: ARRAY OF INTEGER; VAR num: INTEGER);
+		VAR j, n: INTEGER; name: Meta.Name; ch: CHAR;
+	BEGIN
+		path[LEN(path) - 1] := 0X; j := 0; num := 0;
+		GetName(path, name, j); Meta.Lookup(name, i);
+		IF (i.obj = Meta.modObj) & (path[j] = ".") THEN
+			INC(j); GetName(path, name, j);
+			i.Lookup(name, i); ch := path[j]; INC(j);
+			WHILE i.obj = Meta.varObj DO
+				adr[num] := i.adr;
+				IF num < LEN(adr) - 1 THEN INC(num) END;
+				IF ch = 0X THEN RETURN 
+				ELSIF i.typ = Meta.ptrTyp THEN
+					IF ch = "^" THEN ch := path[j]; INC(j) END;
+					i.Deref(i)
+				ELSIF (i.typ = Meta.recTyp) & (ch = ".") THEN
+					GetName(path, name, j); i.Lookup(name, i);
+					ch := path[j]; INC(j)
+				ELSIF (i.typ = Meta.arrTyp) & (ch = "[") THEN
+					ch := path[j]; INC(j); n := 0;
+					WHILE (ch >= "0") & (ch <= "9") DO n := 10 * n + ORD(ch) - ORD("0"); ch := path[j]; INC(j) END;
+					IF ch = "]" THEN ch := path[j]; INC(j); i.Index(n, i) ELSE Meta.Lookup("", i) END
+				ELSE Meta.Lookup("", i)
+				END
+			END
+		ELSE
+			Meta.LookupPath(path, i); num := 0;
+			IF i.obj = Meta.varObj THEN adr[0] := i.adr; num := 1
+			ELSIF i.obj # Meta.procObj THEN Meta.Lookup("", i)
+			END
+		END
+	END LookupPath;
+
+	PROCEDURE Sort (VAR adr: ARRAY OF INTEGER; num: INTEGER);
+		VAR i, j, p: INTEGER;
+	BEGIN
+		i := 1;
+		WHILE i < num DO
+			p := adr[i]; j := i;
+			WHILE (j >= 1) & (adr[j - 1] > p) DO adr[j] := adr[j - 1]; DEC(j) END;
+			adr[j] := p; INC(i)
+		END
+	END Sort;
+
+	PROCEDURE GetTypeName (IN item: Meta.Item; OUT name: Meta.Name);
+		VAR mod: Meta.Name;
+	BEGIN
+		IF (item.typ = Meta.recTyp) THEN
+			item.GetTypeName(mod, name);
+			IF (mod = "Dialog") OR (mod = "Dates") THEN (* ok *)
+			ELSE name := ""
+			END
+		ELSE name := ""
+		END
+	END GetTypeName;
+
+	PROCEDURE OpenLink* (c: Control; p: Prop);
+		VAR ok: BOOLEAN;
+	BEGIN
+		ASSERT(c # NIL, 20); ASSERT(p # NIL, 21);
+		c.num := 0;
+		c.prop := Properties.CopyOf(p)(Prop);
+		IF c.font = NIL THEN
+			IF c.customFont THEN c.font := StdCFrames.defaultLightFont
+			ELSE c.font := StdCFrames.defaultFont
+			END
+		END;
+		c.guardErr := FALSE; c.notifyErr := FALSE;
+		LookupPath(p.link, c.item, c.adr, c.num);
+		IF c.item.obj = Meta.varObj THEN
+			Sort(c.adr, c.num);
+			ok := TRUE; c.CheckLink(ok);
+			IF ~ok THEN
+				Meta.Lookup("", c.item);
+				Dialog.ShowParamMsg("#System:HasWrongType", p.link, "", "")
+			END
+		ELSE
+			Meta.Lookup("", c.item); c.num := 0
+		END;
+		CallGuard(c);
+		c.stamp := stamp
+	END OpenLink;
+
+
+	(** Prop **)
+
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			IF p.link # q.link THEN EXCL(valid, link) END;
+			IF p.label # q.label THEN EXCL(valid, label) END;
+			IF p.guard # q.guard THEN EXCL(valid, guard) END;
+			IF p.notifier # q.notifier THEN EXCL(valid, notifier) END;
+			IF p.level # q.level THEN EXCL(valid, level) END;
+			IF p.opt[0] # q.opt[0] THEN EXCL(valid, opt0) END;
+			IF p.opt[1] # q.opt[1] THEN EXCL(valid, opt1) END;
+			IF p.opt[2] # q.opt[2] THEN EXCL(valid, opt2) END;
+			IF p.opt[3] # q.opt[3] THEN EXCL(valid, opt3) END;
+			IF p.opt[4] # q.opt[4] THEN EXCL(valid, opt4) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+
+
+	(* Control *)
+
+	PROCEDURE (c: Control) CopyFromSimpleView2- (source: Control), NEW, EMPTY;
+
+	PROCEDURE (c: Control) CopyFromSimpleView- (source: Views.View);
+	BEGIN
+		WITH source: Control DO
+			c.item := source.item;
+			c.adr := source.adr;
+			c.num := source.num;
+			c.disabled := source.disabled;
+			c.undef := source.undef;
+			c.readOnly := source.readOnly;
+			c.shortcut := source.shortcut;
+			c.customFont := source.customFont;
+			c.font := source.font;
+			c.label := source.label$;
+			c.prop := Properties.CopyOf(source.prop)(Prop);
+			c.CopyFromSimpleView2(source)
+		END
+	END CopyFromSimpleView;
+
+	PROCEDURE (c: Control) Internalize- (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER; x, def, canc, sort: BOOLEAN;
+	BEGIN
+		c.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxBaseVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		NEW(c.prop);
+		IF thisVersion >= 3 THEN
+			rd.ReadString(c.prop.link);
+			rd.ReadString(c.prop.label);
+			rd.ReadString(c.prop.guard);
+			rd.ReadString(c.prop.notifier);
+			rd.ReadInt(c.prop.level);
+			rd.ReadBool(c.customFont);
+			rd.ReadBool(c.prop.opt[0]);
+			rd.ReadBool(c.prop.opt[1]);
+			rd.ReadBool(c.prop.opt[2]);
+			rd.ReadBool(c.prop.opt[3]);
+			rd.ReadBool(c.prop.opt[4]);
+			IF c.customFont & (thisVersion = 4) THEN
+				Views.ReadFont(rd, c.font)
+			END
+		ELSE
+			rd.ReadXString(c.prop.link);
+			rd.ReadXString(c.prop.label);
+			rd.ReadXString(c.prop.guard);
+			c.prop.notifier := "";
+			c.prop.opt[2] := FALSE;
+			c.prop.opt[3] := FALSE;
+			c.prop.opt[4] := FALSE;
+			sort := FALSE;
+			IF thisVersion = 2 THEN
+				rd.ReadXString(c.prop.notifier);
+				rd.ReadBool(sort);
+				rd.ReadBool(c.prop.opt[multiLine])
+			ELSIF thisVersion = 1 THEN
+				rd.ReadXString(c.prop.notifier);
+				rd.ReadBool(sort)
+			END;
+			rd.ReadBool(x);	(* free, was sed for prop.element *)
+			rd.ReadBool(def); 
+			rd.ReadBool(canc);
+			rd.ReadXInt(c.prop.level);
+			rd.ReadBool(c.customFont);
+			c.prop.opt[default] := def OR sort OR (c IS Field);
+			c.prop.opt[cancel] := canc
+		END;
+		c.Internalize2(rd);
+		OpenLink(c, c.prop)
+	END Internalize;
+
+	PROCEDURE (c: Control) Externalize- (VAR wr: Stores.Writer);
+	BEGIN
+		c.Externalize^(wr);
+		wr.WriteVersion(maxBaseVersion);
+		wr.WriteString(c.prop.link);
+		wr.WriteString(c.prop.label);
+		wr.WriteString(c.prop.guard);
+		wr.WriteString(c.prop.notifier);
+		wr.WriteInt(c.prop.level);
+		wr.WriteBool(c.customFont);
+		wr.WriteBool(c.prop.opt[0]);
+		wr.WriteBool(c.prop.opt[1]);
+		wr.WriteBool(c.prop.opt[2]);
+		wr.WriteBool(c.prop.opt[3]);
+		wr.WriteBool(c.prop.opt[4]);
+		IF c.customFont THEN Views.WriteFont(wr, c.font) END;
+		c.Externalize2(wr)
+	END Externalize;
+
+	PROCEDURE (c: Control) HandleViewMsg- (f: Views.Frame; VAR msg: Views.Message);
+		VAR disabled, undef, readOnly, done, allDone: BOOLEAN; i: INTEGER; lbl: Dialog.String;
+	BEGIN
+		WITH msg: Views.NotifyMsg DO
+			done := FALSE; allDone := FALSE;
+			IF guardCheck IN msg.opts THEN
+				(* should call c.Update for each frame but Views.Update only once *)
+				WITH f: StdCFrames.Caption DO lbl := f.label$
+				| f: StdCFrames.PushButton DO lbl := f.label$
+				| f: StdCFrames.RadioButton DO lbl := f.label$
+				| f: StdCFrames.CheckBox DO lbl := f.label$
+				| f: StdCFrames.Group DO lbl := f.label$
+				ELSE lbl := c.label$
+				END;
+				WITH f: StdCFrames.Frame DO
+					disabled := f.disabled; undef := f.undef; readOnly := f.readOnly
+				ELSE
+					disabled := c.disabled; undef := c.undef; readOnly := c.readOnly
+				END;
+				CallGuard(c);
+				IF (c.disabled # disabled) OR (c.undef # undef)
+				OR (c.readOnly # readOnly) OR (c.label # lbl) THEN
+					WITH f: StdCFrames.Frame DO
+						IF f.noRedraw THEN
+							f.disabled := c.disabled;
+							f.undef := c.undef;
+							f.readOnly := c.readOnly;
+							c.Update(f, 0, 0, 0); done := TRUE
+						ELSE Views.Update(c, Views.rebuildFrames); allDone := TRUE
+						END
+					ELSE Views.Update(c, Views.keepFrames); done := TRUE
+					END
+				END
+			END;
+			IF flushCaches IN msg.opts THEN
+				Views.Update(c, Views.rebuildFrames)
+			END;
+			i := 0; WHILE (i < c.num) & (c.adr[i] < msg.id0) DO INC(i) END;
+			IF (i < c.num) & (c.adr[i] < msg.id1) & ~allDone THEN
+				IF (update IN msg.opts) & ~done THEN
+					WITH msg: NotifyMsg DO
+						IF msg.frame # f THEN	(* don't update origin frame *)
+							c.Update(f, msg.op, msg.from, msg.to)
+						END
+					ELSE
+						c.Update(f, 0, 0, 0)
+					END
+				END;
+				IF listUpdate IN msg.opts THEN
+					c.UpdateList(f)
+				END
+			END
+		| msg: Views.UpdateCachesMsg DO
+			IF c.stamp # stamp THEN
+				OpenLink(c, c.prop);
+				IF msg IS UpdateCachesMsg THEN
+					Views.Update(c, Views.rebuildFrames)
+				END
+			END
+		ELSE
+		END;
+		c.HandleViewMsg2(f, msg)
+	END HandleViewMsg;
+
+	PROCEDURE (c: Control) HandleCtrlMsg* (f: Views.Frame; VAR msg: Controllers.Message;
+																							VAR focus: Views.View);
+		VAR sp: Properties.SizeProp; p: Control; dcOk: BOOLEAN;
+	BEGIN
+		IF cleanerInstalled = 0 THEN Kernel.PushTrapCleaner(cleaner) END;
+		INC(cleanerInstalled);
+		p := par; par := c;
+		WITH msg: Properties.PollPickMsg DO
+			msg.dest := f
+		| msg: Properties.PickMsg DO
+			NEW(sp); sp.known := {Properties.width, Properties.height}; sp.valid := sp.known;
+			c.context.GetSize(sp.width, sp.height);
+			Properties.Insert(msg.prop, sp)
+		| msg: Controllers.TrackMsg DO
+			IF ~c.disabled THEN
+				dcOk := TRUE;
+				IF f IS StdCFrames.Frame THEN dcOk := f(StdCFrames.Frame).DblClickOk(msg.x, msg.y) END;
+				IF (DCHint(msg.modifiers) = 1)  & dcOk THEN
+					(* double click *)
+					Notify(c, f, Dialog.pressed, 1, 0)
+				ELSE
+					Notify(c, f, Dialog.pressed, 0, 0)
+				END
+			END
+		ELSE
+		END;
+		c.HandleCtrlMsg2(f, msg, focus);
+		WITH msg: Controllers.TrackMsg DO
+			IF ~c.disabled THEN
+				Notify(c, f, Dialog.released, 0, 0)
+			END
+		ELSE
+		END;
+		par := p;
+		DEC(cleanerInstalled);
+		IF cleanerInstalled = 0 THEN Kernel.PopTrapCleaner(cleaner) END
+	END HandleCtrlMsg;
+
+	PROCEDURE (c: Control) HandlePropMsg- (VAR msg: Properties.Message);
+		VAR fpref: Properties.FocusPref; stp: Properties.StdProp;
+			cp: Prop; ppref: PropPref; op: Op; valid: SET; p: Properties.Property;
+			fop: FontOp; face: Fonts.Typeface; size, weight: INTEGER; style: SET;
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN
+				fpref.hotFocus := FALSE; fpref.setFocus := FALSE; fpref.atLocation := FALSE;
+				Views.HandlePropMsg(c, fpref);
+				IF fpref.setFocus THEN msg.getFocus := TRUE END
+			END
+		| msg: Properties.PollMsg DO
+			ppref.valid := {link, label, notifier, guard};
+			Views.HandlePropMsg(c, ppref);
+			cp := Properties.CopyOf(c.prop)(Prop);
+			cp.valid := ppref.valid; cp.known := cp.valid; cp.readOnly := {};
+			Properties.Insert(msg.prop, cp);
+			NEW(stp);
+			stp.valid := {Properties.typeface..Properties.weight};
+			stp.known := stp.valid;
+			IF c.customFont THEN stp.typeface := c.font.typeface$
+			ELSE stp.typeface := Fonts.default
+			END;
+			stp.size := c.font.size; stp.style.val := c.font.style; stp.weight := c.font.weight;
+			stp.style.mask := {Fonts.italic, Fonts.strikeout, Fonts.underline};
+			Properties.Insert(msg.prop, stp)
+		| msg: Properties.SetMsg DO
+			p := msg.prop; op := NIL; fop := NIL;
+			WHILE (p # NIL) & (op = NIL) DO
+				WITH p: Prop DO
+					ppref.valid := {link, label, notifier, guard};
+					Views.HandlePropMsg(c, ppref);
+					valid := p.valid * ppref.valid;
+					IF valid # {} THEN
+						NEW(op); 
+						op.ctrl := c; 
+						op.prop := Properties.CopyOf(p)(Prop); op.prop.valid := valid
+					END
+				| p: Properties.StdProp DO
+					valid := p.valid * {Properties.typeface..Properties.weight};
+					IF valid # {} THEN
+						NEW(fop); fop.ctrl := c;
+						face := c.font.typeface$; size := c.font.size; style := c.font.style; weight := c.font.weight;
+						IF Properties.typeface IN p.valid THEN face := p.typeface$;
+							IF face = Fonts.default THEN face := StdCFrames.defaultFont.typeface END
+						END;
+						IF Properties.size IN p.valid THEN size := p.size END;
+						IF Properties.style IN p.valid THEN
+							style := (p.style.val * p.style.mask) + (style - p.style.mask)
+						END;
+						IF Properties.weight IN p.valid THEN weight := p.weight END;
+						fop.custom := TRUE;
+						fop.font := Fonts.dir.This(face, size, style, weight);
+						IF (fop.font.typeface = StdCFrames.defaultFont.typeface)
+						& (fop.font.size = StdCFrames.defaultFont.size)
+						& (fop.font.style = StdCFrames.defaultFont.style)
+						& (fop.font.weight = StdCFrames.defaultFont.weight) THEN
+							fop.custom := FALSE;
+							fop.font := StdCFrames.defaultFont
+						END
+					END
+				ELSE
+				END;
+				p := p.next
+			END;
+			IF op # NIL THEN Views.Do(c, "#System:SetProp", op) END;
+			IF fop # NIL THEN Views.Do(c, "#System:SetProp", fop) END
+		| msg: Properties.TypePref DO
+			IF Services.Is(c, msg.type) THEN msg.view := c END
+		ELSE
+		END;
+		c.HandlePropMsg2(msg)
+	END HandlePropMsg;
+
+
+	(* Op *)
+
+	PROCEDURE (op: Op) Do;
+		VAR c: Control; prop: Prop;
+	BEGIN
+		c := op.ctrl;
+		prop := Properties.CopyOf(c.prop)(Prop);
+		prop.valid := op.prop.valid;	(* fields to be restored *)
+		IF link IN op.prop.valid THEN c.prop.link := op.prop.link END;
+		IF label IN op.prop.valid THEN c.prop.label := op.prop.label END;
+		IF guard IN op.prop.valid THEN c.prop.guard := op.prop.guard END;
+		IF notifier IN op.prop.valid THEN c.prop.notifier := op.prop.notifier END;
+		IF level IN op.prop.valid THEN c.prop.level := op.prop.level END;
+		IF opt0 IN op.prop.valid THEN c.prop.opt[0] := op.prop.opt[0] END;
+		IF opt1 IN op.prop.valid THEN c.prop.opt[1] := op.prop.opt[1] END;
+		IF opt2 IN op.prop.valid THEN c.prop.opt[2] := op.prop.opt[2] END;
+		IF opt3 IN op.prop.valid THEN c.prop.opt[3] := op.prop.opt[3] END;
+		IF opt4 IN op.prop.valid THEN c.prop.opt[4] := op.prop.opt[4] END;
+		IF c.prop.guard # prop.guard THEN c.guardErr := FALSE END;
+		IF c.prop.notifier # prop.notifier THEN c.notifyErr := FALSE END;
+		IF c.prop.link # prop.link THEN OpenLink(c, c.prop) ELSE CallGuard(c) END;
+		op.prop := prop;
+		Views.Update(c, Views.rebuildFrames)
+	END Do;
+
+	PROCEDURE (op: FontOp) Do;
+		VAR c: Control; custom: BOOLEAN; font: Fonts.Font;
+	BEGIN
+		c := op.ctrl;
+		custom := c.customFont; c.customFont := op.custom; op.custom := custom;
+		font := c.font; c.font := op.font; op.font := font;
+		Views.Update(c, Views.rebuildFrames)
+	END Do;
+
+
+	(* ------------------------- standard controls ------------------------- *)
+
+	PROCEDURE CatchCtrlMsg (c: Control; f: Views.Frame; VAR msg: Controllers.Message;
+																				VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled THEN
+			WITH f: StdCFrames.Frame DO
+				WITH msg: Controllers.PollCursorMsg DO
+					f.GetCursor(msg.x, msg.y, msg.modifiers, msg.cursor)
+				| msg: Controllers.PollOpsMsg DO
+					msg.valid := {Controllers.pasteChar}
+				| msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				| msg: Controllers.MarkMsg DO
+					f.Mark(msg.show, msg.focus)
+				|msg: Controllers.WheelMsg DO
+					f.WheelMove(msg.x, msg.y, msg.op, msg.nofLines, msg.done)
+				ELSE
+				END
+			END
+		END
+	END CatchCtrlMsg;
+	
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) NewPushButton* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewCheckBox* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewRadioButton* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewField* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewUpDownField* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewDateField* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewTimeField* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewColorField* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewListBox* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewSelectionBox* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewComboBox* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewCaption* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewGroup* (p: Prop): Control, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) NewTreeControl* (p: Prop): Control, NEW, ABSTRACT;
+
+
+	(* PushButton *)
+
+	PROCEDURE Call (c: PushButton);
+		VAR res: INTEGER; p: Control; ok: BOOLEAN; msg: Views.NotifyMsg;
+	BEGIN
+		IF c.item.Valid() & ((c.item.obj = Meta.procObj) OR (c.item.obj = Meta.varObj) & (c.item.typ = Meta.procTyp)) THEN
+			IF cleanerInstalled = 0 THEN Kernel.PushTrapCleaner(cleaner) END;
+			INC(cleanerInstalled);
+			p := par; c.item.Call(ok); par := p;
+			DEC(cleanerInstalled);
+			IF cleanerInstalled = 0 THEN Kernel.PopTrapCleaner(cleaner) END;
+			IF ~ok THEN Dialog.ShowMsg("#System:BehaviorNotAccessible") END
+		ELSIF c.prop.link # "" THEN
+			IF cleanerInstalled = 0 THEN Kernel.PushTrapCleaner(cleaner) END;
+			INC(cleanerInstalled);
+			p := par; par := c; Dialog.Call(c.prop.link, " ", res); par := p;
+			DEC(cleanerInstalled);
+			IF cleanerInstalled = 0 THEN Kernel.PopTrapCleaner(cleaner) END
+		ELSE Dialog.ShowMsg("#System:NoBehaviorBound")
+		END;
+		msg.opts := {guardCheck};
+		Views.Omnicast(msg)
+	END Call;
+	
+	PROCEDURE Do (f: StdCFrames.PushButton);
+	BEGIN
+		Call(f.view(PushButton))
+	END Do;
+
+	PROCEDURE (c: PushButton) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, pbVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: PushButton) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(pbVersion)
+	END Externalize2;
+
+	PROCEDURE (c: PushButton) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.PushButton;
+	BEGIN
+		f := StdCFrames.dir.NewPushButton();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.label := c.label$;
+		f.default := c.prop.opt[default];
+		f.cancel := c.prop.opt[cancel];
+		f.Do := Do;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: PushButton) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: PushButton) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																				VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled THEN
+			WITH f: StdCFrames.Frame DO
+				WITH msg: Controllers.EditMsg DO
+					IF (msg.op = Controllers.pasteChar)
+						& ((msg.char = lineChar)
+							OR (msg.char = " ")
+							OR (msg.char = esc) & c.prop.opt[cancel]
+							OR IsShortcut(msg.char, c)) THEN f.KeyDown(msg.char) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: PushButton) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			msg.accepts := ~c.disabled & ((msg.char = lineChar) & c.prop.opt[default]
+				OR (msg.char = esc) & c.prop.opt[cancel]
+				OR IsShortcut(msg.char, c))
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~ c.readOnly THEN
+				msg.hotFocus := TRUE; msg.setFocus := StdCFrames.setFocus
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetPushButtonSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, default, cancel}
+		| msg: DefaultsPref DO
+			IF c.prop.link # "" THEN msg.disabled := FALSE END
+		ELSE
+		END
+	END HandlePropMsg2;
+	
+	PROCEDURE (c: PushButton) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.PushButton).label := c.label$;
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+	PROCEDURE (c: PushButton) CheckLink (VAR ok: BOOLEAN);
+	BEGIN
+		ok := c.item.typ = Meta.procTyp
+	END CheckLink;
+
+
+	(* CheckBox *)
+
+	PROCEDURE GetCheckBox (f: StdCFrames.CheckBox; OUT x: BOOLEAN);
+		VAR c: CheckBox;
+	BEGIN
+		x := FALSE;
+		c := f.view(CheckBox);
+		IF c.item.Valid() THEN
+			IF c.item.typ = Meta.boolTyp THEN x := c.item.BoolVal()
+			ELSIF c.item.typ = Meta.setTyp THEN x := c.prop.level IN c.item.SetVal()
+			END
+		END
+	END GetCheckBox;
+
+	PROCEDURE SetCheckBox (f: StdCFrames.CheckBox; x: BOOLEAN);
+		VAR c: CheckBox; s: SET;
+	BEGIN
+		c := f.view(CheckBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF c.item.typ = Meta.boolTyp THEN
+				c.item.PutBoolVal(x); Notify(c, f, Dialog.changed, 0, 0)
+			ELSIF c.item.typ = Meta.setTyp THEN
+				s := c.item.SetVal();
+				IF x THEN INCL(s, c.prop.level) ELSE EXCL(s, c.prop.level) END;
+				c.item.PutSetVal(s);
+				IF x THEN Notify(c, f, Dialog.included, c.prop.level, c.prop.level)
+				ELSE Notify(c, f, Dialog.excluded, c.prop.level, c.prop.level)
+				END
+			END
+		END
+	END SetCheckBox;
+
+	PROCEDURE (c: CheckBox) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, cbVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: CheckBox) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(cbVersion)
+	END Externalize2;
+
+	PROCEDURE (c: CheckBox) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.CheckBox;
+	BEGIN
+		f :=  StdCFrames.dir.NewCheckBox();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.label := c.label$;
+		f.Get := GetCheckBox;
+		f.Set := SetCheckBox;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: CheckBox) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: CheckBox) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.Frame DO
+				WITH msg: Controllers.EditMsg DO
+					IF (msg.op = Controllers.pasteChar)
+						& ((msg.char = " ") OR IsShortcut(msg.char, c)) THEN f.KeyDown(msg.char) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: CheckBox) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				IF (msg.char = tab) OR (msg.char = ltab) THEN
+					(* tabs set focus to first checkbox only *)
+					IF (msg.focus # NIL) & (msg.focus IS CheckBox)
+							& (msg.focus(CheckBox).item.adr = c.item.adr) THEN
+						msg.getFocus := FALSE
+					END
+				ELSIF (msg.char >= arrowLeft) & (msg.char <= arrowDown) THEN
+					(* arrows set focus to next checkbox bound to same variable *)
+					msg.getFocus := StdCFrames.setFocus
+						& (msg.focus # NIL)
+						& (msg.focus IS CheckBox)
+						& (msg.focus(CheckBox).item.adr = c.item.adr);
+					msg.accepts := msg.getFocus & (msg.focus # c)
+				ELSIF IsShortcut(msg.char, c) THEN
+					msg.accepts := TRUE; msg.getFocus := StdCFrames.setFocus
+				ELSIF msg.char # " " THEN
+					msg.accepts := FALSE
+				END
+			END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.hotFocus := TRUE; msg.setFocus := StdCFrames.setFocus
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetCheckBoxSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, level}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: CheckBox) CheckLink (VAR ok: BOOLEAN);
+	BEGIN
+		ok := (c.item.typ = Meta.boolTyp) OR (c.item.typ = Meta.setTyp)
+	END CheckLink;
+
+	PROCEDURE (c: CheckBox) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		IF (op = 0) OR (c.item.typ = Meta.boolTyp) OR (c.prop.level = to) THEN
+			f(StdCFrames.CheckBox).label := c.label$;
+			f(StdCFrames.Frame).Update
+		END
+	END Update;
+	
+
+	(* RadioButton *)
+
+	PROCEDURE GetRadioButton (f: StdCFrames.RadioButton; OUT x: BOOLEAN);
+		VAR c: RadioButton;
+	BEGIN
+		x := FALSE;
+		c := f.view(RadioButton);
+		IF c.item.Valid() THEN
+			IF c.item.typ = Meta.boolTyp THEN x := c.item.BoolVal() = (c.prop.level # 0)
+			ELSE x := c.item.IntVal() = c.prop.level
+			END
+		END
+	END GetRadioButton;
+
+	PROCEDURE SetRadioButton (f: StdCFrames.RadioButton; x: BOOLEAN);
+		VAR c: RadioButton; old: INTEGER;
+	BEGIN
+		IF x THEN
+			c := f.view(RadioButton);
+			IF c.item.Valid() & ~c.readOnly THEN
+				IF c.item.typ = Meta.boolTyp THEN
+					IF c.item.BoolVal() THEN old := 1 ELSE old := 0 END;
+					IF c.prop.level # old THEN
+						c.item.PutBoolVal(c.prop.level # 0);
+						Notify(c, f, Dialog.changed, old, c.prop.level)
+					END
+				ELSE
+					old := c.item.IntVal();
+					IF c.prop.level # old THEN
+						c.item.PutIntVal(c.prop.level);
+						Notify(c, f, Dialog.changed, old, c.prop.level)
+					END
+				END
+			END
+		END
+	END SetRadioButton;
+
+	PROCEDURE (c: RadioButton) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, rbVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: RadioButton) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(rbVersion)
+	END Externalize2;
+
+	PROCEDURE (c: RadioButton) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.RadioButton;
+	BEGIN
+		f := StdCFrames.dir.NewRadioButton();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.label := c.label$;
+		f.Get := GetRadioButton;
+		f.Set := SetRadioButton;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: RadioButton) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: RadioButton) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																			VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.Frame DO
+				WITH msg: Controllers.EditMsg DO
+					IF (msg.op = Controllers.pasteChar)
+						& ((msg.char <= " ") OR IsShortcut(msg.char, c)) THEN f.KeyDown(msg.char) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: RadioButton) HandlePropMsg2 (VAR msg: Properties.Message);
+		VAR hot: BOOLEAN;
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				IF (msg.char = tab) OR (msg.char = ltab) THEN
+					(* tabs set focus to active radio button only *)
+					IF c.item.Valid() THEN
+						IF c.item.typ = Meta.boolTyp THEN hot := c.item.BoolVal() = (c.prop.level # 0)
+						ELSE hot := c.item.IntVal() = c.prop.level
+						END
+					ELSE hot := FALSE
+					END;
+					IF ~hot THEN msg.getFocus := FALSE END
+				ELSIF (msg.char >= arrowLeft) & (msg.char <= arrowDown) THEN
+					(* arrows set focus to next radio button bound to same variable *)
+					msg.getFocus := StdCFrames.setFocus
+						& (msg.focus # NIL) & (msg.focus IS RadioButton)
+						& (msg.focus(RadioButton).item.adr = c.item.adr);
+					msg.accepts := msg.getFocus & (msg.focus # c)
+				ELSIF IsShortcut(msg.char, c) THEN
+					msg.accepts := TRUE; msg.getFocus := StdCFrames.setFocus
+				ELSIF msg.char # " " THEN
+					msg.accepts := FALSE
+				END
+			END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.hotFocus := TRUE; msg.setFocus := StdCFrames.setFocus
+ 			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetRadioButtonSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, level}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: RadioButton) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		IF name = "List" THEN c.item.Lookup("index", c.item) END;
+		ok := (c.item.typ >= Meta.byteTyp) & (c.item.typ <= Meta.intTyp) OR (c.item.typ = Meta.boolTyp)
+	END CheckLink;
+
+	PROCEDURE (c: RadioButton) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		IF (op = 0) OR (c.prop.level = to) OR (c.prop.level = from) THEN
+			f(StdCFrames.RadioButton).label := c.label$;
+			f(StdCFrames.Frame).Update
+		END
+	END Update;
+	
+
+	(* Field *)
+
+	PROCEDURE LongToString (x: LONGINT; OUT s: ARRAY OF CHAR);
+		VAR d: ARRAY 24 OF CHAR; i, j: INTEGER;
+	BEGIN
+		IF x = MIN(LONGINT) THEN
+			s := "-9223372036854775808"
+		ELSE
+			i := 0; j := 0;
+			IF x < 0 THEN s[0] := "-"; i := 1; x := -x END;
+			REPEAT d[j] := CHR(x MOD 10 + ORD("0")); INC(j); x := x DIV 10 UNTIL x = 0;
+			WHILE j > 0 DO DEC(j); s[i] := d[j]; INC(i) END;
+			s[i] := 0X
+		END
+	END LongToString;
+
+	PROCEDURE StringToLong (IN s: ARRAY OF CHAR; OUT x: LONGINT; OUT res: INTEGER);
+		VAR i, sign, d: INTEGER;
+	BEGIN
+		i := 0; sign := 1; x := 0; res := 0;
+		WHILE s[i] = " " DO INC(i) END;
+		IF s[i] = "-" THEN sign := -1; INC(i) END;
+		WHILE s[i] = " " DO INC(i) END;
+		IF s[i] = 0X THEN res := 2 END;
+		WHILE (s[i] >= "0") & (s[i] <= "9") DO
+			d := ORD(s[i]) - ORD("0"); INC(i);
+			IF x <= (MAX(LONGINT) - d) DIV 10 THEN x := 10 * x + d
+			ELSE res := 1
+			END
+		END;
+		x := x * sign;
+		IF s[i] # 0X THEN res := 2 END
+	END StringToLong;
+
+	PROCEDURE FixToInt (fix: ARRAY OF CHAR; OUT int: ARRAY OF CHAR; scale: INTEGER);
+		VAR i, j: INTEGER;
+	BEGIN
+		IF scale > 24 THEN scale := 24 ELSIF scale < 0 THEN scale := 0 END;
+		i := 0; j := 0;
+		WHILE (fix[i] # ".") & (fix[i] # 0X) DO int[j] := fix[i]; INC(i); INC(j) END;
+		IF fix[i] = "." THEN INC(i) END;
+		WHILE (scale > 0) & (fix[i] >= "0") & (fix[i] <= "9") DO int[j] := fix[i]; INC(i); INC(j); DEC(scale) END;
+		WHILE scale > 0 DO int[j] := "0"; INC(j); DEC(scale) END;
+		int[j] := 0X
+	END FixToInt;
+
+	PROCEDURE IntToFix (int: ARRAY OF CHAR; OUT fix: ARRAY OF CHAR; scale: INTEGER);
+		VAR i, j, n: INTEGER;
+	BEGIN
+		IF scale > 24 THEN scale := 24 ELSIF scale < 0 THEN scale := 0 END;
+		n := LEN(int$); i := 0; j := 0;
+		WHILE int[i] < "0" DO fix[j] := int[i]; INC(i); INC(j); DEC(n) END;
+		IF n > scale THEN
+			WHILE n > scale DO fix[j] := int[i]; INC(i); INC(j); DEC(n) END
+		ELSE
+			fix[j] := "0"; INC(j)
+		END;
+		fix[j] := "."; INC(j);
+		WHILE n < scale DO fix[j] := "0"; INC(j); DEC(scale) END;
+		WHILE n > 0 DO  fix[j] := int[i]; INC(i); INC(j); DEC(n) END;
+		fix[j] := 0X
+	END IntToFix;
+
+	PROCEDURE GetField (f: StdCFrames.Field; OUT x: ARRAY OF CHAR);
+		VAR c: Field; ok: BOOLEAN; b, v: Meta.Item; mod, name: Meta.Name;
+	BEGIN
+		x := "";
+		c := f.view(Field);
+		IF c.item.Valid() THEN
+			IF c.item.typ = Meta.arrTyp THEN
+				c.item.GetStringVal(x, ok)
+			ELSIF c.item.typ IN {Meta.byteTyp, Meta.sIntTyp, Meta.intTyp} THEN
+				Strings.IntToString(c.item.IntVal(), x);
+				IF c.prop.level > 0 THEN IntToFix(x, x, c.prop.level) END
+			ELSIF c.item.typ = Meta.longTyp THEN
+				LongToString(c.item.LongVal(), x);
+				IF c.prop.level > 0 THEN IntToFix(x, x, c.prop.level) END
+			ELSIF c.item.typ = Meta.sRealTyp THEN
+				IF c.prop.level <= 0 THEN
+					Strings.RealToStringForm(c.item.RealVal(), 7, 0, c.prop.level, " ", x)
+				ELSE
+					Strings.RealToStringForm(c.item.RealVal(), c.prop.level, 0, 1, " ", x)
+				END
+			ELSIF c.item.typ = Meta.realTyp THEN
+				IF c.prop.level <= 0 THEN
+					Strings.RealToStringForm(c.item.RealVal(), 16, 0, c.prop.level, " ", x)
+				ELSE
+					Strings.RealToStringForm(c.item.RealVal(), c.prop.level, 0, 1, " ", x)
+				END
+			ELSIF c.item.typ = Meta.recTyp THEN
+				c.item.GetTypeName(mod, name);
+				IF mod = "Dialog" THEN
+					IF name = "Currency" THEN
+						c.item.Lookup("val", v); c.item.Lookup("scale", b);
+						LongToString(v.LongVal(), x); IntToFix(x, x, b.IntVal())
+					ELSE (* Combo *)
+						c.item.Lookup("item", v); (* Combo *)
+						IF v.typ = Meta.arrTyp THEN v.GetStringVal(x, ok) END
+					END
+				END
+			END
+		ELSE
+			x := c.label$
+		END
+	END GetField;
+
+	PROCEDURE SetField (f: StdCFrames.Field; IN x: ARRAY OF CHAR);
+		VAR c: Field; ok: BOOLEAN; i, res, old: INTEGER; r, or: REAL; b, v: Meta.Item;
+			mod, name: Meta.Name; long, long0: LONGINT;
+			s: ARRAY 1024 OF CHAR;
+	BEGIN
+		c := f.view(Field);
+		IF c.item.Valid() & ~c.readOnly THEN
+			CASE c.item.typ OF
+			| Meta.arrTyp:
+				c.item.GetStringVal(s, ok);
+				IF ~ok OR (s$ # x$) THEN
+					c.item.PutStringVal(x, ok);
+					IF ok THEN Notify(c, f, Dialog.changed, 0, 0) ELSE Dialog.Beep END
+				END
+			| Meta.byteTyp:
+				IF x = "" THEN i := 0; res := 0
+				ELSIF c.prop.level > 0 THEN FixToInt(x, s, c.prop.level); Strings.StringToInt(s, i, res)
+				ELSE Strings.StringToInt(x, i, res)
+				END;
+				IF (res = 0) & (i >= MIN(BYTE)) & (i <= MAX(BYTE)) THEN
+					old := c.item.IntVal();
+					IF i # old THEN c.item.PutIntVal(i); Notify(c, f, Dialog.changed, old, i) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.sIntTyp:
+				IF x = "" THEN i := 0; res := 0
+				ELSIF c.prop.level > 0 THEN FixToInt(x, s, c.prop.level); Strings.StringToInt(s, i, res)
+				ELSE Strings.StringToInt(x, i, res)
+				END;
+				IF (res = 0) & (i >= MIN(SHORTINT)) & (i <= MAX(SHORTINT)) THEN
+					old := c.item.IntVal();
+					IF i # old THEN c.item.PutIntVal(i); Notify(c, f, Dialog.changed, old, i) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.intTyp:
+				IF x = "" THEN i := 0; res := 0
+				ELSIF c.prop.level > 0 THEN FixToInt(x, s, c.prop.level); Strings.StringToInt(s, i, res)
+				ELSE Strings.StringToInt(x, i, res)
+				END;
+				IF res = 0 THEN
+					old := c.item.IntVal();
+					IF i # old THEN c.item.PutIntVal(i); Notify(c, f, Dialog.changed, old, i) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.longTyp:
+				IF x = "" THEN long := 0; res := 0
+				ELSE FixToInt(x, s, c.prop.level); StringToLong(s, long, res)
+				END;
+				IF res = 0 THEN
+					long0 := c.item.LongVal();
+					IF long # long0 THEN c.item.PutLongVal(long); Notify(c, f, Dialog.changed, 0, 0) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.sRealTyp:
+				IF (x = "") OR (x = "-") THEN r := 0; res := 0 ELSE Strings.StringToReal(x, r, res) END;
+				IF (res = 0) & (r >= MIN(SHORTREAL)) & (r <= MAX(SHORTREAL)) THEN
+					or := c.item.RealVal();
+					IF r # or THEN c.item.PutRealVal(r); Notify(c, f, Dialog.changed, 0, 0) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.realTyp:
+				IF (x = "") OR (x = "-") THEN r := 0; res := 0 ELSE Strings.StringToReal(x, r, res) END;
+				IF res = 0 THEN
+					or := c.item.RealVal();
+					IF r # or THEN c.item.PutRealVal(r); Notify(c, f, Dialog.changed, 0, 0) END
+				ELSIF x # "-" THEN
+					Dialog.Beep
+				END
+			| Meta.recTyp:
+				c.item.GetTypeName(mod, name);
+				IF mod = "Dialog" THEN
+					IF name = "Currency" THEN
+						c.item.Lookup("val", v); c.item.Lookup("scale", b);
+						IF x = "" THEN long := 0; res := 0
+						ELSE FixToInt(x, s, b.IntVal()); StringToLong(s, long, res)
+						END;
+						IF res = 0 THEN
+							long0 := v.LongVal();
+							IF long # long0 THEN v.PutLongVal(long); Notify(c, f, Dialog.changed, 0, 0) END
+						ELSIF x # "-" THEN
+							Dialog.Beep
+						END
+					ELSE	(* name = "Combo" *)
+						c.item.Lookup("item", v);
+						IF v.typ = Meta.arrTyp THEN
+							v.GetStringVal(s, ok);
+							IF ~ok OR (s$ # x$) THEN
+								v.PutStringVal(x, ok);
+								IF ok THEN Notify(c, f, Dialog.changed, 0, 0) ELSE Dialog.Beep END
+							END
+						END
+					END
+				END
+			END
+		END
+	END SetField;
+
+	PROCEDURE EqualField (f: StdCFrames.Field; IN s1, s2: ARRAY OF CHAR): BOOLEAN;
+		VAR c: Field; i1, i2, res1, res2: INTEGER; r1, r2: REAL; l1, l2: LONGINT;
+			mod, name: Meta.Name; t1, t2: ARRAY 64 OF CHAR; b: Meta.Item;
+	BEGIN
+		c := f.view(Field);
+		CASE c.item.typ OF
+		| Meta.arrTyp:
+			RETURN s1 = s2
+		| Meta.byteTyp, Meta.sIntTyp, Meta.intTyp:
+			IF c.prop.level > 0 THEN
+				FixToInt(s1, t1, c.prop.level); Strings.StringToInt(t1, i1, res1);
+				FixToInt(s2, t2, c.prop.level); Strings.StringToInt(t2, i2, res2)
+			ELSE
+				Strings.StringToInt(s1, i1, res1);
+				Strings.StringToInt(s2, i2, res2)
+			END;
+			RETURN (res1 = 0) & (res2 = 0) & (i1 = i2) 
+		| Meta.longTyp:
+			IF c.prop.level > 0 THEN
+				FixToInt(s1, t1, c.prop.level); StringToLong(t1, l1, res1);
+				FixToInt(s2, t2, c.prop.level); StringToLong(t2, l2, res2)
+			ELSE
+				StringToLong(s1, l1, res1);
+				StringToLong(s2, l2, res2)
+			END;
+			RETURN (res1 = 0) & (res2 = 0) & (l1 = l2) 
+		| Meta.sRealTyp, Meta.realTyp:
+			Strings.StringToReal(s1, r1, res1);
+			Strings.StringToReal(s2, r2, res2);
+			RETURN (res1 = 0) & (res2 = 0) & (r1 = r2) 
+		| Meta.recTyp:
+			c.item.GetTypeName(mod, name);
+			IF mod = "Dialog" THEN
+				IF name = "Currency" THEN
+					c.item.Lookup("scale", b); i1 := b.IntVal();
+					FixToInt(s1, t1, i1); StringToLong(t1, l1, res1);
+					FixToInt(s2, t2, i1); StringToLong(t2, l2, res2);
+					RETURN (res1 = 0) & (res2 = 0) & (l1 =l2)
+				ELSE (* name = "Combo" *)
+					RETURN s1 = s2
+				END
+			END
+		ELSE RETURN s1 = s2
+		END
+	END EqualField;
+
+	PROCEDURE (c: Field) CopyFromSimpleView2 (source: Control);
+	BEGIN
+		WITH source: Field DO c.maxLen := source.maxLen END
+	END CopyFromSimpleView2;
+
+	PROCEDURE (c: Field) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, fldVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: Field) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(fldVersion)
+	END Externalize2;
+
+	PROCEDURE (c: Field) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.Field;
+	BEGIN
+		f := StdCFrames.dir.NewField();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.maxLen := c.maxLen;
+		f.left := c.prop.opt[left];
+		f.right := c.prop.opt[right];
+		f.multiLine := c.prop.opt[multiLine];
+		f.password := c.prop.opt[password];
+		f.Get := GetField;
+		f.Set := SetField;
+		f.Equal := EqualField;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: Field) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: Field) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+		VAR ch: CHAR; mod, name: Meta.Name;
+	BEGIN
+		WITH f: StdCFrames.Field DO
+			IF ~c.disabled & ~c.readOnly THEN
+				WITH msg: Controllers.PollOpsMsg DO
+					msg.selectable := TRUE;
+					(* should ask Frame if there is a selection for cut or copy! *)
+					msg.valid := {Controllers.pasteChar, Controllers.cut, Controllers.copy, Controllers.paste}
+				| msg: Controllers.TickMsg DO
+					f.Idle
+				| msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						ch := msg.char;
+						IF (ch = ldel) OR (ch = rdel) OR (ch >= 10X) & (ch <= 1FX)
+							OR ("0" <= ch) & (ch <= "9") OR (ch = "+") OR (ch = "-")
+							OR (c.item.typ = Meta.arrTyp)
+							OR (c.item.typ IN {Meta.sRealTyp, Meta.realTyp}) & ((ch = ".") OR (ch = "E"))
+							OR (c.prop.level > 0) & (ch = ".")
+							THEN f.KeyDown(ch)
+						ELSIF c.item.typ = Meta.recTyp THEN
+							c.item.GetTypeName(mod, name);
+							IF (mod = "Dialog") & (name = "Combo") OR (ch = ".") THEN
+								f.KeyDown(ch)
+							ELSE Dialog.Beep
+							END
+						ELSE Dialog.Beep
+						END
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				| msg: Controllers.SelectMsg DO
+					IF msg.set THEN f.Select(0, MAX(INTEGER))
+					ELSE f.Select(-1, -1)
+					END
+				| msg: Controllers.MarkMsg DO
+					f.Mark(msg.show, msg.focus);
+					IF ~msg.show & msg.focus THEN f.Update END;
+					IF msg.show & msg.focus THEN f.Select(0, MAX(INTEGER)) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			ELSIF ~c.disabled THEN
+				WITH msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				ELSE
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: Field) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF msg.char = lineChar THEN msg.accepts := c.prop.opt[multiLine] & (msg.focus = c)
+			ELSIF msg.char = esc THEN msg.accepts := FALSE
+			END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			ELSIF~c.disabled THEN
+				msg.hotFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetFieldSize(c.maxLen, msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, level, notifier, left, right, multiLine, password}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: Field) CheckLink (VAR ok: BOOLEAN);
+		VAR t: INTEGER; name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name); t := c.item.typ;
+		IF (t = Meta.arrTyp) & (c.item.BaseTyp() = Meta.charTyp) THEN c.maxLen := SHORT(c.item.Len() - 1)
+		ELSIF t = Meta.byteTyp THEN c.maxLen := 6
+		ELSIF t = Meta.sIntTyp THEN c.maxLen := 9
+		ELSIF t = Meta.intTyp THEN c.maxLen := 13
+		ELSIF t = Meta.longTyp THEN c.maxLen := 24
+		ELSIF t = Meta.sRealTyp THEN c.maxLen := 16
+		ELSIF t = Meta.realTyp THEN c.maxLen := 24
+		ELSIF name = "Combo" THEN c.maxLen := 64
+		ELSIF name = "Currency" THEN c.maxLen := 16
+		ELSE ok := FALSE
+		END
+	END CheckLink;
+
+	PROCEDURE (c: Field) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* UpDownField *)
+
+	PROCEDURE GetUpDownField (f: StdCFrames.UpDownField; OUT val: INTEGER);
+		VAR c: UpDownField;
+	BEGIN
+		val := 0;
+		c := f.view(UpDownField);
+		IF c.item.Valid() THEN val := c.item.IntVal() END
+	END GetUpDownField;
+
+	PROCEDURE SetUpDownField (f: StdCFrames.UpDownField; val: INTEGER);
+		VAR c: UpDownField; old: INTEGER;
+	BEGIN
+		c := f.view(UpDownField);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF (val >= c.min) & (val <= c.max) THEN
+				old := c.item.IntVal();
+				IF old # val THEN c.item.PutIntVal(val); Notify(c, f, Dialog.changed, old, val) END
+			ELSE Dialog.Beep
+			END
+		END
+	END SetUpDownField;
+	
+	PROCEDURE (c: UpDownField) CopyFromSimpleView2 (source: Control);
+	BEGIN
+		WITH source: UpDownField DO
+			c.min := source.min;
+			c.max := source.max;
+			c.inc := source.inc
+		END
+	END CopyFromSimpleView2;
+
+	PROCEDURE (c: UpDownField) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, fldVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: UpDownField) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(fldVersion)
+	END Externalize2;
+
+	PROCEDURE (c: UpDownField) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.UpDownField;
+	BEGIN
+		f := StdCFrames.dir.NewUpDownField();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.min := c.min;
+		f.max := c.max;
+		f.inc := c.inc;
+		f.Get := GetUpDownField;
+		f.Set := SetUpDownField;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: UpDownField) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: UpDownField) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+		VAR ch: CHAR;
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.UpDownField DO
+				WITH msg: Controllers.PollOpsMsg DO
+					msg.selectable := TRUE;
+					(* should ask view if there is a selection for cut or copy! *)
+					msg.valid := {Controllers.pasteChar, Controllers.cut, Controllers.copy, Controllers.paste}
+				| msg: Controllers.TickMsg DO
+					f.Idle
+				| msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						ch := msg.char;
+						IF (ch = ldel) OR (ch = rdel) OR (ch >= 10X) & (ch <= 1FX)
+							OR ("0" <= ch) & (ch <= "9") OR (ch = "+") OR (ch = "-")
+							OR (c.item.typ = Meta.arrTyp)
+							THEN f.KeyDown(ch)
+						ELSE Dialog.Beep
+						END
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				| msg: Controllers.SelectMsg DO
+					IF msg.set THEN f.Select(0, MAX(INTEGER))
+					ELSE f.Select(-1, -1)
+					END
+				| msg: Controllers.MarkMsg DO
+					f.Mark(msg.show, msg.focus);
+					IF msg.show & msg.focus THEN f.Select(0, MAX(INTEGER)) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: UpDownField) HandlePropMsg2 (VAR msg: Properties.Message);
+		VAR m: INTEGER; n: INTEGER;
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			m := -c.min;
+			IF c.max > m THEN m := c.max END;
+			n := 3;
+			WHILE m > 99 DO INC(n); m := m DIV 10 END;
+			StdCFrames.dir.GetUpDownFieldSize(n, msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: UpDownField) CheckLink (VAR ok: BOOLEAN);
+	BEGIN
+		IF c.item.typ = Meta.byteTyp THEN c.min := MIN(BYTE); c.max := MAX(BYTE)
+		ELSIF c.item.typ = Meta.sIntTyp THEN c.min := MIN(SHORTINT); c.max := MAX(SHORTINT)
+		ELSIF c.item.typ = Meta.intTyp THEN c.min := MIN(INTEGER); c.max := MAX(INTEGER)
+		ELSE ok := FALSE
+		END;
+		c.inc := 1
+	END CheckLink;
+
+	PROCEDURE (c: UpDownField) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* DateField *)
+
+	PROCEDURE GetDateField (f: StdCFrames.DateField; OUT date: Dates.Date);
+		VAR c: DateField; v: Meta.Item;
+	BEGIN
+		date.year := 1; date.month := 1; date.day := 1;
+		c := f.view(DateField);
+		IF c.item.Valid() THEN
+			c.item.Lookup("year", v); IF v.typ = Meta.intTyp THEN date.year := SHORT(v.IntVal()) END;
+			c.item.Lookup("month", v); IF v.typ = Meta.intTyp THEN date.month := SHORT(v.IntVal()) END;
+			c.item.Lookup("day", v); IF v.typ = Meta.intTyp THEN date.day := SHORT(v.IntVal()) END
+		END
+	END GetDateField;
+	
+	PROCEDURE SetDateField(f:  StdCFrames.DateField; IN date: Dates.Date);
+		VAR c: DateField; v: Meta.Item;
+	BEGIN
+		c := f.view(DateField);
+		IF c.item.Valid() & ~c.readOnly THEN
+			c.item.Lookup("year", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.year) END;
+			c.item.Lookup("month", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.month) END;
+			c.item.Lookup("day", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.day) END;
+			Notify(c, f, Dialog.changed, 0, 0)
+		END
+	END SetDateField;
+	
+	PROCEDURE GetDateFieldSelection (f: StdCFrames.DateField; OUT sel: INTEGER);
+	BEGIN
+		sel := f.view(DateField).selection
+	END GetDateFieldSelection;
+	
+	PROCEDURE SetDateFieldSelection (f: StdCFrames.DateField; sel: INTEGER);
+	BEGIN
+		f.view(DateField).selection := sel
+	END SetDateFieldSelection;
+	
+	PROCEDURE (c: DateField) CopyFromSimpleView2 (source: Control);
+	BEGIN
+		WITH source: DateField DO c.selection := source.selection END
+	END CopyFromSimpleView2;
+
+	PROCEDURE (c: DateField) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, dfldVersion, thisVersion);
+		c.selection := 0
+	END Internalize2;
+
+	PROCEDURE (c: DateField) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(dfldVersion)
+	END Externalize2;
+
+	PROCEDURE (c: DateField) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.DateField;
+	BEGIN
+		f := StdCFrames.dir.NewDateField();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.Get := GetDateField;
+		f.Set := SetDateField;
+		f.GetSel := GetDateFieldSelection;
+		f.SetSel := SetDateFieldSelection;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: DateField) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: DateField) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.DateField DO
+				WITH msg: Controllers.PollOpsMsg DO
+					msg.valid := {Controllers.pasteChar, Controllers.copy}
+				| msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						f.KeyDown(msg.char)
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				| msg: Controllers.TickMsg DO
+					IF f.mark THEN
+						IF c.selection = 0 THEN c.selection := 1; Views.Update(c, Views.keepFrames) END
+					END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: DateField) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN
+				msg.accepts := FALSE
+			ELSIF (msg.char = tab) OR (msg.char = ltab) THEN
+				msg.accepts := ((msg.focus # c) & (~c.disabled & ~c.readOnly)) OR 
+					(msg.focus = c) & ((msg.char = tab) & (c.selection # -1) OR (msg.char = ltab) & (c.selection # 1));
+				msg.getFocus := msg.accepts
+			END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetDateFieldSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: DateField) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "Date"
+	END CheckLink;
+
+	PROCEDURE (c: DateField) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* TimeField *)
+
+	PROCEDURE GetTimeField (f: StdCFrames.TimeField; OUT time: Dates.Time);
+		VAR c: TimeField; v: Meta.Item;
+	BEGIN
+		time.hour := 0; time.minute := 0; time.second := 0;
+		c := f.view(TimeField);
+		IF c.item.Valid() THEN
+			c.item.Lookup("hour", v); IF v.typ = Meta.intTyp THEN time.hour := SHORT(v.IntVal()) END;
+			c.item.Lookup("minute", v); IF v.typ = Meta.intTyp THEN time.minute := SHORT(v.IntVal()) END;
+			c.item.Lookup("second", v); IF v.typ = Meta.intTyp THEN time.second := SHORT(v.IntVal()) END
+		END
+	END GetTimeField;
+	
+	PROCEDURE SetTimeField(f:  StdCFrames.TimeField; IN date: Dates.Time);
+		VAR c: TimeField; v: Meta.Item;
+	BEGIN
+		c := f.view(TimeField);
+		IF c.item.Valid() & ~c.readOnly THEN
+			c.item.Lookup("hour", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.hour) END;
+			c.item.Lookup("minute", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.minute) END;
+			c.item.Lookup("second", v); IF v.typ = Meta.intTyp THEN v.PutIntVal(date.second) END;
+			Notify(c, f, Dialog.changed, 0, 0)
+		END
+	END SetTimeField;
+	
+	PROCEDURE GetTimeFieldSelection (f: StdCFrames.TimeField; OUT sel: INTEGER);
+	BEGIN
+		sel := f.view(TimeField).selection
+	END GetTimeFieldSelection;
+	
+	PROCEDURE SetTimeFieldSelection (f: StdCFrames.TimeField; sel: INTEGER);
+	BEGIN
+		f.view(TimeField).selection := sel
+	END SetTimeFieldSelection;
+	
+	PROCEDURE (c: TimeField) CopyFromSimpleView2 (source: Control);
+	BEGIN
+		WITH source: TimeField DO c.selection := source.selection END
+	END CopyFromSimpleView2;
+
+	PROCEDURE (c: TimeField) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, tfldVersion, thisVersion);
+		c.selection := 0
+	END Internalize2;
+
+	PROCEDURE (c: TimeField) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(tfldVersion)
+	END Externalize2;
+
+	PROCEDURE (c: TimeField) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.TimeField;
+	BEGIN
+		f := StdCFrames.dir.NewTimeField();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.Get := GetTimeField;
+		f.Set := SetTimeField;
+		f.GetSel := GetTimeFieldSelection;
+		f.SetSel := SetTimeFieldSelection;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: TimeField) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: TimeField) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.TimeField DO
+				WITH msg: Controllers.PollOpsMsg DO
+					msg.valid := {Controllers.pasteChar, Controllers.copy}
+				| msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						f.KeyDown(msg.char)
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				| msg: Controllers.TickMsg DO
+					IF f.mark THEN
+						IF c.selection = 0 THEN c.selection := 1; Views.Update(c, Views.keepFrames) END
+					END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: TimeField) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN
+				msg.accepts := FALSE
+			ELSIF (msg.char = tab) OR (msg.char = ltab) THEN
+				msg.accepts := (msg.focus # c) OR 
+					((msg.char = tab) & (c.selection # -1)) OR ((msg.char = ltab) & (c.selection # 1))
+			END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetTimeFieldSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: TimeField) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "Time"
+	END CheckLink;
+
+	PROCEDURE (c: TimeField) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* ColorField *)
+
+	PROCEDURE GetColorField (f: StdCFrames.ColorField; OUT col: INTEGER);
+		VAR c: ColorField; v: Meta.Item;
+	BEGIN
+		col := Ports.defaultColor;
+		c := f.view(ColorField);
+		IF c.item.Valid() THEN
+			IF c.item.typ = Meta.intTyp THEN
+				col := c.item.IntVal()
+			ELSE
+				c.item.Lookup("val", v); IF v.typ = Meta.intTyp THEN col := v.IntVal() END
+			END
+		END
+	END GetColorField;
+	
+	PROCEDURE SetColorField(f:  StdCFrames.ColorField; col: INTEGER);
+		VAR c: ColorField; v: Meta.Item; old: INTEGER;
+	BEGIN
+		c := f.view(ColorField);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF c.item.typ = Meta.intTyp THEN
+				old := c.item.IntVal();
+				IF old # col THEN c.item.PutIntVal(col); Notify(c, f, Dialog.changed, old, col) END
+			ELSE
+				c.item.Lookup("val", v); 
+				IF v.typ = Meta.intTyp THEN 
+					old := v.IntVal();
+					IF old # col THEN v.PutIntVal(col); Notify(c, f, Dialog.changed, old, col) END
+				END
+			END
+		END
+	END SetColorField;
+	
+	PROCEDURE (c: ColorField) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, cfldVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: ColorField) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(cfldVersion)
+	END Externalize2;
+
+	PROCEDURE (c: ColorField) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.ColorField;
+	BEGIN
+		f := StdCFrames.dir.NewColorField();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.Get := GetColorField;
+		f.Set := SetColorField;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: ColorField) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: ColorField) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		IF ~c.disabled & ~c.readOnly THEN
+			WITH f: StdCFrames.ColorField DO
+				WITH msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						f.KeyDown(msg.char)
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: ColorField) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			msg.accepts := ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c)
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.hotFocus := TRUE; msg.setFocus := StdCFrames.setFocus
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetColorFieldSize(msg.w, msg.h)
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: ColorField) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := (name = "Color") OR (c.item.typ = Meta.intTyp)
+	END CheckLink;
+
+	PROCEDURE (c: ColorField) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* ListBox *)
+
+	PROCEDURE GetListBox (f: StdCFrames.ListBox; OUT i: INTEGER);
+		VAR c: ListBox; v: Meta.Item;
+	BEGIN
+		i := -1;
+		c := f.view(ListBox);
+		IF c.item.Valid() THEN
+			c.item.Lookup("index", v);
+			IF v.typ = Meta.intTyp THEN i := v.IntVal() END
+		END
+	END GetListBox;
+
+	PROCEDURE SetListBox (f: StdCFrames.ListBox; i: INTEGER);
+		VAR c: ListBox; v: Meta.Item; old: INTEGER;
+	BEGIN
+		c := f.view(ListBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			c.item.Lookup("index", v);
+			IF v.typ = Meta.intTyp THEN
+				old := v.IntVal();
+				IF i # old THEN v.PutIntVal(i); Notify(c, f, Dialog.changed, old, i) END
+			END
+		END
+	END SetListBox;
+	
+	PROCEDURE GetFName (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: Param DO
+			WITH rec: Dialog.List DO rec.GetItem(par.i, par.n)
+			| rec: Dialog.Selection DO rec.GetItem(par.i, par.n)
+			| rec: Dialog.Combo DO rec.GetItem(par.i, par.n)
+			ELSE par.n := ""
+			END
+		END
+	END GetFName;
+	
+	PROCEDURE GetListName (f: StdCFrames.ListBox; i: INTEGER; VAR name: ARRAY OF CHAR);
+		VAR c: ListBox; par: Param;
+	BEGIN
+		par.n := "";
+		c := f.view(ListBox);
+		IF c.item.Valid() THEN
+			par.i := i;
+			c.item.CallWith(GetFName, par)
+		END;
+		name := par.n$
+	END GetListName;
+
+	PROCEDURE (c: ListBox) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, lbxVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: ListBox) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(lbxVersion)
+	END Externalize2;
+
+	PROCEDURE (c: ListBox) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.ListBox;
+	BEGIN
+		f := StdCFrames.dir.NewListBox();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.sorted := c.prop.opt[sorted];
+		f.Get := GetListBox;
+		f.Set := SetListBox;
+		f.GetName := GetListName;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: ListBox) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: ListBox) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																		VAR focus: Views.View);
+	BEGIN
+		WITH f: StdCFrames.ListBox DO
+			IF ~c.disabled & ~c.readOnly THEN
+				WITH msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN f.KeyDown(msg.char) END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			ELSIF ~c.disabled THEN
+				WITH msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				ELSE
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: ListBox) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			ELSIF~c.disabled THEN
+				msg.hotFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetListBoxSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, sorted}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: ListBox) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "List"
+	END CheckLink;
+
+	PROCEDURE (c: ListBox) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+	PROCEDURE (c: ListBox) UpdateList (f: Views.Frame);
+	BEGIN
+		f(StdCFrames.Frame).UpdateList
+	END UpdateList;
+	
+
+	(* SelectionBox *)
+
+	PROCEDURE InLargeSet (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: Param DO
+			WITH rec: Dialog.Selection DO
+				IF rec.In(par.i) THEN par.i := 1 ELSE par.i := 0 END
+			ELSE par.i := 0
+			END
+		END
+	END InLargeSet;
+	
+	PROCEDURE GetSelectionBox (f: StdCFrames.SelectionBox; i: INTEGER; OUT in: BOOLEAN);
+		VAR c: SelectionBox; lv: SelectValue; par: Param;
+	BEGIN
+		in := FALSE;
+		c := f.view(SelectionBox);
+		IF c.item.Valid() THEN
+			IF c.item.Is(lv) THEN
+				par.i := i;
+				c.item.CallWith(InLargeSet, par);
+				in := par.i # 0
+			END
+		END
+	END GetSelectionBox;
+
+	PROCEDURE InclLargeSet (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: Param DO
+			WITH rec: Dialog.Selection DO
+				IF (par.from # par.to) OR ~rec.In(par.from) THEN
+					rec.Incl(par.from, par.to); par.i := 1
+				ELSE par.i := 0
+				END
+			ELSE par.i := 0
+			END
+		END
+	END InclLargeSet;
+	
+	PROCEDURE InclSelectionBox (f: StdCFrames.SelectionBox; from, to: INTEGER);
+		VAR c: SelectionBox; lv: SelectValue; par: Param;
+	BEGIN
+		c := f.view(SelectionBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF c.item.Is(lv) THEN
+				par.from := from; par.to := to;
+				c.item.CallWith(InclLargeSet, par);
+				IF par.i # 0 THEN Notify(c, f, Dialog.included, from, to) END
+			END
+		END
+	END InclSelectionBox;
+	
+	PROCEDURE ExclLargeSet (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: Param DO
+			WITH rec: Dialog.Selection DO
+				IF (par.from # par.to) OR rec.In(par.from) THEN
+					rec.Excl(par.from, par.to); par.i := 1
+				ELSE par.i := 0
+				END
+			ELSE par.i := 0
+			END
+		END
+	END ExclLargeSet;
+	
+	PROCEDURE ExclSelectionBox (f: StdCFrames.SelectionBox; from, to: INTEGER);
+		VAR c: SelectionBox; lv: SelectValue; par: Param;
+	BEGIN
+		c := f.view(SelectionBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF c.item.Is(lv) THEN
+				par.from := from; par.to := to;
+				c.item.CallWith(ExclLargeSet, par);
+				IF par.i # 0 THEN Notify(c, f, Dialog.excluded, from, to) END
+			END
+		END
+	END ExclSelectionBox;
+	
+	PROCEDURE SetSelectionBox (f: StdCFrames.SelectionBox; from, to: INTEGER);
+		VAR c: SelectionBox; lv: SelectValue; par: Param;
+	BEGIN
+		c := f.view(SelectionBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			IF c.item.Is(lv) THEN
+				par.from := 0; par.to := MAX(INTEGER);
+				c.item.CallWith(ExclLargeSet, par);
+				par.from := from; par.to := to;
+				c.item.CallWith(InclLargeSet, par);
+				Notify(c, f, Dialog.set, from, to)
+			END
+		END
+	END SetSelectionBox;
+	
+	PROCEDURE GetSelName (f: StdCFrames.SelectionBox; i: INTEGER; VAR name: ARRAY OF CHAR);
+		VAR c: SelectionBox; par: Param;
+	BEGIN
+		par.n := "";
+		c := f.view(SelectionBox);
+		IF c.item.Valid() THEN
+			par.i := i;
+			c.item.CallWith(GetFName, par)
+		END;
+		name := par.n$
+	END GetSelName;
+
+	PROCEDURE (c: SelectionBox) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, sbxVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: SelectionBox) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(sbxVersion)
+	END Externalize2;
+
+	PROCEDURE (c: SelectionBox) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.SelectionBox;
+	BEGIN
+		f := StdCFrames.dir.NewSelectionBox();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.sorted := c.prop.opt[sorted];
+		f.Get := GetSelectionBox;
+		f.Incl := InclSelectionBox;
+		f.Excl := ExclSelectionBox;
+		f.Set := SetSelectionBox;
+		f.GetName := GetSelName;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: SelectionBox) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: SelectionBox) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																				VAR focus: Views.View);
+	BEGIN
+		WITH f: StdCFrames.SelectionBox DO
+			IF ~c.disabled & ~c.readOnly THEN
+				WITH msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN f.KeyDown(msg.char) END
+				| msg: Controllers.SelectMsg DO
+					IF msg.set THEN f.Select(0, MAX(INTEGER))
+					ELSE f.Select(-1, -1)
+					END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			ELSIF ~c.disabled THEN
+				WITH msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				ELSE
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: SelectionBox) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) OR msg.getFocus THEN
+				msg.getFocus := StdCFrames.setFocus
+			END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			ELSIF~c.disabled THEN
+				msg.hotFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetSelectionBoxSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, sorted}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: SelectionBox) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "Selection"
+	END CheckLink;
+
+	PROCEDURE (c: SelectionBox) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		IF (op >= Dialog.included) & (op <= Dialog.set) THEN
+			f(StdCFrames.SelectionBox).UpdateRange(op, from, to)
+		ELSE
+			f(StdCFrames.Frame).Update
+		END
+	END Update;
+	
+	PROCEDURE (c: SelectionBox) UpdateList (f: Views.Frame);
+	BEGIN
+		f(StdCFrames.Frame).UpdateList
+	END UpdateList;
+	
+
+	(* ComboBox *)
+
+	PROCEDURE GetComboBox (f: StdCFrames.ComboBox; OUT x: ARRAY OF CHAR);
+		VAR c: ComboBox; ok: BOOLEAN; v: Meta.Item;
+	BEGIN
+		x := "";
+		c := f.view(ComboBox);
+		IF c.item.Valid() THEN
+			c.item.Lookup("item", v);
+			IF v.typ = Meta.arrTyp THEN v.GetStringVal(x, ok) END
+		END
+	END GetComboBox;
+
+	PROCEDURE SetComboBox (f: StdCFrames.ComboBox; IN x: ARRAY OF CHAR);
+		VAR c: ComboBox; ok: BOOLEAN; v: Meta.Item; s: ARRAY 1024 OF CHAR;
+	BEGIN
+		c := f.view(ComboBox);
+		IF c.item.Valid() & ~c.readOnly THEN
+			c.item.Lookup("item", v);
+			IF v.typ = Meta.arrTyp THEN
+				v.GetStringVal(s, ok);
+				IF ~ok OR (s$ # x$) THEN
+					v.PutStringVal(x, ok);
+					IF ok THEN Notify(c, f, Dialog.changed, 0, 0) END
+				END
+			END
+		END
+	END SetComboBox;
+
+	PROCEDURE GetComboName (f: StdCFrames.ComboBox; i: INTEGER; VAR name: ARRAY OF CHAR);
+		VAR c: ComboBox; par: Param;
+	BEGIN
+		par.n := "";
+		c := f.view(ComboBox);
+		IF c.item.Valid() THEN
+			par.i := i;
+			c.item.CallWith(GetFName, par)
+		END;
+		name := par.n$
+	END GetComboName;
+
+	PROCEDURE (c: ComboBox) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, cbxVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: ComboBox) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(cbxVersion)
+	END Externalize2;
+
+	PROCEDURE (c: ComboBox) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.ComboBox;
+	BEGIN
+		f := StdCFrames.dir.NewComboBox();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.sorted := c.prop.opt[sorted];
+		f.Get := GetComboBox;
+		f.Set := SetComboBox;
+		f.GetName := GetComboName;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: ComboBox) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: ComboBox) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																			VAR focus: Views.View);
+	BEGIN
+		WITH f: StdCFrames.ComboBox DO
+			IF ~c.disabled & ~c.readOnly THEN
+				WITH msg: Controllers.PollOpsMsg DO
+					msg.selectable := TRUE;
+					(* should ask Frame if there is a selection for cut or copy! *)
+					msg.valid := {Controllers.pasteChar, Controllers.cut, Controllers.copy, Controllers.paste}
+				| msg: Controllers.TickMsg DO
+					f.Idle
+				| msg: Controllers.EditMsg DO
+					IF msg.op = Controllers.pasteChar THEN
+						f.KeyDown(msg.char)
+					ELSE
+						f.Edit(msg.op, msg.view, msg.w, msg.h, msg.isSingle, msg.clipboard)
+					END
+				| msg: Controllers.SelectMsg DO
+					IF msg.set THEN f.Select(0, MAX(INTEGER))
+					ELSE f.Select(-1, -1)
+					END
+				| msg: Controllers.MarkMsg DO
+					f.Mark(msg.show, msg.focus);
+					IF msg.show & msg.focus THEN f.Select(0, MAX(INTEGER)) END
+				| msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: ComboBox) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) THEN msg.getFocus := TRUE END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetComboBoxSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, sorted}
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: ComboBox) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "Combo"
+	END CheckLink;
+
+	PROCEDURE (c: ComboBox) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+	PROCEDURE (c: ComboBox) UpdateList (f: Views.Frame);
+	BEGIN
+		f(StdCFrames.Frame).UpdateList
+	END UpdateList;
+	
+
+	(* Caption *)
+
+	PROCEDURE (c: Caption) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, capVersion, thisVersion);
+		IF thisVersion < 1 THEN c.prop.opt[left] := TRUE END
+	END Internalize2;
+
+	PROCEDURE (c: Caption) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		(* Save old version for captions that are compatible with the old version *)
+		IF c.prop.opt[left] THEN wr.WriteVersion(0) ELSE wr.WriteVersion(capVersion) END
+	END Externalize2;
+
+	PROCEDURE (c: Caption) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.Caption;
+	BEGIN
+		f := StdCFrames.dir.NewCaption();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.label := c.label$;
+		f.left := c.prop.opt[left];
+		f.right := c.prop.opt[right];
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: Caption) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: Caption) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			StdCFrames.dir.GetCaptionSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, left, right}
+		| msg: DefaultsPref DO
+			IF c.prop.link = "" THEN msg.disabled := FALSE END
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: Caption) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Caption).label := c.label$;
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* Group *)
+
+	PROCEDURE (c: Group) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, grpVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: Group) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(grpVersion)
+	END Externalize2;
+
+	PROCEDURE (c: Group) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.Group;
+	BEGIN
+		f := StdCFrames.dir.NewGroup();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.label := c.label$;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: Group) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+
+	PROCEDURE (c: Group) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			StdCFrames.dir.GetGroupSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard}
+		| msg: DefaultsPref DO
+			IF c.prop.link = "" THEN msg.disabled := FALSE END
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: Group) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Group).label := c.label$;
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+	
+	(* TreeControl *)
+	
+	PROCEDURE (c: TreeControl) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		rd.ReadVersion(minVersion, tfVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (c: TreeControl) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		wr.WriteVersion(tfVersion)
+	END Externalize2;
+
+	PROCEDURE TVNofNodesF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO par.l := rec.NofNodes()
+			ELSE par.l := 0
+			END
+		END
+	END TVNofNodesF;
+	
+	PROCEDURE TVNofNodes (f: StdCFrames.TreeFrame): INTEGER;
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.l := 0;
+		IF c.item.Valid() THEN c.item.CallWith(TVNofNodesF, par) END;
+		RETURN par.l
+	END TVNofNodes;
+	
+	PROCEDURE TVChildF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO par.nodeOut := rec.Child(par.nodeIn, Dialog.firstPos)
+			ELSE par.nodeOut := NIL
+			END
+		END
+	END TVChildF;
+	
+	PROCEDURE TVChild (f: StdCFrames.TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.nodeIn := node; par.nodeOut := NIL;
+		IF c.item.Valid() THEN c.item.CallWith(TVChildF, par) END;
+		RETURN par.nodeOut
+	END TVChild;
+	
+	PROCEDURE TVParentF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO par.nodeOut := rec.Parent(par.nodeIn)
+			ELSE par.nodeOut := NIL
+			END
+		END
+	END TVParentF;
+	
+	PROCEDURE TVParent (f: StdCFrames.TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.nodeIn := node; par.nodeOut := NIL;
+		IF c.item.Valid() THEN c.item.CallWith(TVParentF, par) END;
+		RETURN par.nodeOut
+	END TVParent;
+	
+	PROCEDURE TVNextF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO par.nodeOut := rec.Next(par.nodeIn)
+			ELSE par.nodeOut := NIL
+			END
+		END
+	END TVNextF;
+	
+	PROCEDURE TVNext (f: StdCFrames.TreeFrame; node: Dialog.TreeNode): Dialog.TreeNode;
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.nodeIn := node; par.nodeOut := NIL;
+		IF c.item.Valid() THEN c.item.CallWith(TVNextF, par) END;
+		RETURN par.nodeOut
+	END TVNext;
+	
+	PROCEDURE TVSelectF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO rec.Select(par.nodeIn) END
+		END
+	END TVSelectF;
+	
+	PROCEDURE TVSelect (f: StdCFrames.TreeFrame; node: Dialog.TreeNode);
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.nodeIn := node;
+		IF c.item.Valid() THEN 
+			c.item.CallWith(TVSelectF, par);
+			Notify(c, f, Dialog.changed, 0, 0)
+		END
+	END TVSelect;
+	
+	PROCEDURE TVSelectedF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			WITH rec: Dialog.Tree DO par.nodeOut := rec.Selected()
+			ELSE par.nodeOut := NIL
+			END
+		END
+	END TVSelectedF;
+	
+	PROCEDURE TVSelected (f: StdCFrames.TreeFrame): Dialog.TreeNode;
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.nodeOut := NIL;
+		IF c.item.Valid() THEN c.item.CallWith(TVSelectedF, par) END;
+		RETURN par.nodeOut
+	END TVSelected;
+	
+	PROCEDURE TVSetExpansionF (VAR rec, par: ANYREC);
+	BEGIN
+		WITH par: TVParam DO 
+			par.nodeIn.SetExpansion(par.e)
+		END
+	END TVSetExpansionF;
+	
+	PROCEDURE TVSetExpansion (f: StdCFrames.TreeFrame; tn: Dialog.TreeNode; expanded: BOOLEAN);
+		VAR c: TreeControl; par: TVParam;
+	BEGIN
+		c := f.view(TreeControl); par.e := expanded; par.nodeIn := tn;
+		IF c.item.Valid() THEN c.item.CallWith(TVSetExpansionF, par) END
+	END TVSetExpansion;
+
+	PROCEDURE (c: TreeControl) GetNewFrame (VAR frame: Views.Frame);
+		VAR f: StdCFrames.TreeFrame;
+	BEGIN
+		f := StdCFrames.dir.NewTreeFrame();
+		f.disabled := c.disabled;
+		f.undef := c.undef;
+		f.readOnly := c.readOnly;
+		f.font := c.font;
+		f.sorted := c.prop.opt[sorted];
+		f.haslines := c.prop.opt[haslines];
+		f.hasbuttons := c.prop.opt[hasbuttons];
+		f.atroot := c.prop.opt[atroot];
+		f.foldericons := c.prop.opt[foldericons];
+		f.NofNodes := TVNofNodes;
+		f.Child := TVChild;
+		f.Parent := TVParent;
+		f.Next := TVNext;
+		f.Select := TVSelect;
+		f.Selected := TVSelected;
+		f.SetExpansion := TVSetExpansion;
+		frame := f
+	END GetNewFrame;
+
+	PROCEDURE (c: TreeControl) UpdateList (f: Views.Frame);
+	BEGIN
+		f(StdCFrames.Frame).UpdateList()
+	END UpdateList;
+	
+	PROCEDURE (c: TreeControl) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		WITH f: StdCFrames.Frame DO f.Restore(l, t, r, b) END
+	END Restore;
+	
+	PROCEDURE (c: TreeControl) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
+																			VAR focus: Views.View);
+	BEGIN
+		WITH f: StdCFrames.TreeFrame DO
+			IF ~c.disabled & ~c.readOnly THEN
+				WITH msg: Controllers.EditMsg DO
+					IF (msg.op = Controllers.pasteChar) THEN 
+						f.KeyDown(msg.char)
+					END
+				ELSE
+					CatchCtrlMsg(c, f, msg, focus)
+				END
+			ELSIF ~c.disabled THEN
+				WITH msg: Controllers.TrackMsg DO
+					f.MouseDown(msg.x, msg.y, msg.modifiers)
+				ELSE
+				END
+			END
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (c: TreeControl) HandlePropMsg2 (VAR msg: Properties.Message);
+	BEGIN			
+		WITH msg: Properties.ControlPref DO
+			IF (msg.char = lineChar) OR (msg.char = esc) THEN msg.accepts := FALSE END;
+			IF ~c.disabled & ~c.readOnly & IsShortcut(msg.char, c) OR msg.getFocus THEN
+				msg.getFocus := StdCFrames.setFocus
+			END
+		| msg: Properties.FocusPref DO
+			IF ~c.disabled & ~c.readOnly THEN
+				msg.setFocus := TRUE
+			ELSIF~c.disabled THEN
+				msg.hotFocus := TRUE
+			END
+		| msg: Properties.SizePref DO
+			StdCFrames.dir.GetTreeFrameSize(msg.w, msg.h)
+		| msg: PropPref DO
+			msg.valid := {link, label, guard, notifier, sorted, haslines, hasbuttons, atroot, foldericons}
+		| msg: Properties.ResizePref DO
+			msg.horFitToWin := TRUE; msg.verFitToWin := TRUE
+		ELSE
+		END
+	END HandlePropMsg2;
+
+	PROCEDURE (c: TreeControl) CheckLink (VAR ok: BOOLEAN);
+		VAR name: Meta.Name;
+	BEGIN
+		GetTypeName(c.item, name);
+		ok := name = "Tree"
+	END CheckLink;
+
+	PROCEDURE (c: TreeControl) Update (f: Views.Frame; op, from, to: INTEGER);
+	BEGIN
+		f(StdCFrames.Frame).Update
+	END Update;
+	
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) NewPushButton (p: Prop): Control;
+		VAR c: PushButton;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewPushButton;
+
+	PROCEDURE (d: StdDirectory) NewCheckBox (p: Prop): Control;
+		VAR c: CheckBox;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewCheckBox;
+
+	PROCEDURE (d: StdDirectory) NewRadioButton (p: Prop): Control;
+		VAR c: RadioButton;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewRadioButton;
+
+	PROCEDURE (d: StdDirectory) NewField (p: Prop): Control;
+		VAR c: Field;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewField;
+	
+	PROCEDURE (d: StdDirectory) NewUpDownField (p: Prop): Control;
+		VAR c: UpDownField;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewUpDownField;
+	
+	PROCEDURE (d: StdDirectory) NewDateField (p: Prop): Control;
+		VAR c: DateField;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewDateField;
+
+	PROCEDURE (d: StdDirectory) NewTimeField (p: Prop): Control;
+		VAR c: TimeField;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewTimeField;
+	
+	PROCEDURE (d: StdDirectory) NewColorField (p: Prop): Control;
+		VAR c: ColorField;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewColorField;
+
+	PROCEDURE (d: StdDirectory) NewListBox (p: Prop): Control;
+		VAR c: ListBox;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewListBox;
+
+	PROCEDURE (d: StdDirectory) NewSelectionBox (p: Prop): Control;
+		VAR c: SelectionBox;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewSelectionBox;
+
+	PROCEDURE (d: StdDirectory) NewComboBox (p: Prop): Control;
+		VAR c: ComboBox;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewComboBox;
+
+	PROCEDURE (d: StdDirectory) NewCaption (p: Prop): Control;
+		VAR c: Caption;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewCaption;
+
+	PROCEDURE (d: StdDirectory) NewGroup (p: Prop): Control;
+		VAR c: Group;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewGroup;
+
+	PROCEDURE (d: StdDirectory) NewTreeControl (p: Prop): Control;
+		VAR c: TreeControl;
+	BEGIN
+		NEW(c); OpenLink(c, p); RETURN c
+	END NewTreeControl;
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); dir := d
+	END SetDir;
+
+	PROCEDURE InitProp (VAR p: Prop);
+	BEGIN
+		NEW(p);
+		p.link := ""; p.label := ""; p.guard := ""; p.notifier := "";
+		p.level := 0;
+		p.opt[0] := FALSE; p.opt[1] := FALSE;
+		p.opt[2] := FALSE; p.opt[3] := FALSE;
+		p.opt[4] := FALSE
+	END InitProp;
+
+	PROCEDURE DepositPushButton*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.label := "#System:untitled";
+		Views.Deposit(dir.NewPushButton(p))
+	END DepositPushButton;
+
+	PROCEDURE DepositCheckBox*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.label := "#System:untitled";
+		Views.Deposit(dir.NewCheckBox(p))
+	END DepositCheckBox;
+
+	PROCEDURE DepositRadioButton*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.label := "#System:untitled";
+		Views.Deposit(dir.NewRadioButton(p))
+	END DepositRadioButton;
+
+	PROCEDURE DepositField*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p); p.opt[left] := TRUE;
+		Views.Deposit(dir.NewField(p))
+	END DepositField;
+	
+	PROCEDURE DepositUpDownField*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewUpDownField(p))
+	END DepositUpDownField;
+	
+	PROCEDURE DepositDateField*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewDateField(p))
+	END DepositDateField;
+
+	PROCEDURE DepositTimeField*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewTimeField(p))
+	END DepositTimeField;
+
+	PROCEDURE DepositColorField*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewColorField(p))
+	END DepositColorField;
+
+	PROCEDURE DepositListBox*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewListBox(p))
+	END DepositListBox;
+
+	PROCEDURE DepositSelectionBox*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewSelectionBox(p))
+	END DepositSelectionBox;
+
+	PROCEDURE DepositComboBox*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		Views.Deposit(dir.NewComboBox(p))
+	END DepositComboBox;
+
+	PROCEDURE DepositCancelButton*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.link := "StdCmds.CloseDialog"; p.label := "#System:Cancel"; p.opt[cancel] := TRUE;
+		Views.Deposit(dir.NewPushButton(p))
+	END DepositCancelButton;
+
+	PROCEDURE DepositCaption*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p); p.opt[left] := TRUE;
+		p.label := "#System:Caption";
+		Views.Deposit(dir.NewCaption(p))
+	END DepositCaption;
+
+	PROCEDURE DepositGroup*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.label := "#System:Caption";
+		Views.Deposit(dir.NewGroup(p))
+	END DepositGroup;
+	
+	PROCEDURE DepositTreeControl*;
+		VAR p: Prop;
+	BEGIN
+		InitProp(p);
+		p.opt[haslines] := TRUE; p.opt[hasbuttons] := TRUE; p.opt[atroot] := TRUE; p.opt[foldericons] := TRUE;
+		Views.Deposit(dir.NewTreeControl(p))
+	END DepositTreeControl;
+
+	PROCEDURE Relink*;
+		VAR msg: UpdateCachesMsg;
+	BEGIN
+		INC(stamp);
+		Views.Omnicast(msg)
+	END Relink;
+
+
+	PROCEDURE Init;
+		VAR d: StdDirectory;
+	BEGIN
+		par := NIL; stamp := 0;
+		NEW(d); stdDir := d; dir := d;
+		NEW(cleaner); cleanerInstalled := 0
+	END Init;
+
+
+	(* check guards action *)
+
+	PROCEDURE (a: Action) Do;
+		VAR msg: Views.NotifyMsg;
+	BEGIN
+		IF Windows.dir # NIL THEN
+			IF a.w # NIL THEN
+				INC(a.cnt);
+				msg.id0 := 0; msg.id1 := 0; msg.opts := {guardCheck};
+				IF a.w.seq # NIL THEN a.w.seq.Handle(msg) END;
+				a.w := Windows.dir.Next(a.w);
+				WHILE (a.w # NIL) & a.w.sub DO a.w := Windows.dir.Next(a.w) END
+			ELSE
+				IF a.cnt = 0 THEN a.resolution := Services.resolution
+				ELSE a.resolution := Services.resolution DIV a.cnt DIV 2
+				END;
+				a.cnt := 0;
+				a.w := Windows.dir.First();
+				WHILE (a.w # NIL) & a.w.sub DO a.w := Windows.dir.Next(a.w) END
+			END
+		END;
+		Services.DoLater(a, Services.Ticks() + a.resolution)
+	END Do;
+
+BEGIN
+	Init;
+	NEW(action); action.w := NIL; action.cnt := 0; Services.DoLater(action, Services.now)
+CLOSE
+	Services.RemoveAction(action)
+END Controls.

+ 87 - 0
BlackBox/System/Mod/In.txt

@@ -0,0 +1,87 @@
+MODULE In;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 System/Mod/In.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT TextMappers, TextControllers;
+
+	VAR
+		Done-: BOOLEAN;
+		s: TextMappers.Scanner;
+
+	PROCEDURE Open*;
+		VAR c: TextControllers.Controller; beg, end: INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF c.HasSelection() THEN c.GetSelection(beg, end) ELSE beg := 0 END;
+			s.ConnectTo(c.text); s.SetPos(beg); s.rider.Read; Done := TRUE
+		ELSE
+			s.ConnectTo(NIL); Done := FALSE
+		END
+	END Open;
+
+	PROCEDURE Char* (OUT ch: CHAR);
+	BEGIN
+		IF Done THEN
+			IF s.rider.eot THEN Done := FALSE
+			ELSE ch := s.rider.char; s.rider.Read
+			END
+		END
+	END Char;
+
+	PROCEDURE Int* (OUT i: INTEGER);
+	BEGIN
+		IF Done THEN
+			s.Scan;
+			IF (s.type = TextMappers.int) THEN
+				i := s.int
+			ELSE Done := FALSE
+			END
+		END
+	END Int;
+
+	PROCEDURE LongInt* (OUT l: LONGINT);
+	BEGIN
+		IF Done THEN
+			s.Scan;
+			IF (s.type = TextMappers.lint) OR (s.type = TextMappers.int) THEN
+				l := s.lint
+			ELSE Done := FALSE
+			END
+		END
+	END LongInt;
+
+	PROCEDURE Real* (OUT x: REAL);
+	BEGIN
+		IF Done THEN
+			s.Scan;
+			IF s.type = TextMappers.real THEN
+				x := SHORT(s.real)
+			ELSIF s.type = TextMappers.int THEN
+				x := s.int
+			ELSE Done := FALSE
+			END
+		END
+	END Real;
+
+	PROCEDURE Name* (OUT name: ARRAY OF CHAR);
+	BEGIN
+		IF Done THEN
+			s.Scan;
+			TextMappers.ScanQualIdent(s, name, Done)
+		END
+	END Name;
+
+	PROCEDURE String* (OUT str: ARRAY OF CHAR);
+	BEGIN
+		IF Done THEN
+			s.Scan;
+			IF s.type = TextMappers.string THEN
+				str := s.string$
+			ELSE Done := FALSE
+			END
+		END
+	END String;
+
+END In.

+ 860 - 0
BlackBox/Text/Mod/Cmds.txt

@@ -0,0 +1,860 @@
+MODULE TextCmds;
+
+	(* THIS IS TEXT COPY OF BlackBox Text/Mod/Cmds.odc *)
+	(* DO NOT EDIT *)
+
+(* could eliminate ReplList/ReplOp and use Models.Begin/EndScript instead (as already done for shifting) *)
+(* move ListAlienViews to StdCmds and generalize accordingly? *)
+
+
+	IMPORT
+		Strings, Ports, Stores, Models, Views, Controllers, Properties, Dialog, Containers,
+		TextModels, TextMappers, TextRulers, TextSetters, TextViews, TextControllers;
+
+	CONST
+		(* ShiftOp.left *)
+		left = TRUE; right = FALSE;
+
+		(* PreparePat, FindPat *)
+		leftTerm = 3X; rightTerm = 4X;
+
+		(* DoReplace mode *)
+		replace = 0; replaceAndFind = 1; replaceAll = 2;
+
+		(* FindIn first *)
+		first = TRUE; again = FALSE;
+
+		mm = Ports.mm; point = Ports.point; maxPat = 256;
+		viewcode = TextModels.viewcode;
+		tab = TextModels.tab; line = TextModels.line; para = TextModels.para;
+		nbspace = TextModels.nbspace; digitspace = TextModels.digitspace;
+		hyphen = TextModels.hyphen;
+		nbhyphen = TextModels.nbhyphen; softhyphen = TextModels.softhyphen;
+
+		posKey = "#Text:Position";
+		searchAliensKey = "#Text:SearchForAlienViews";	(* dormant code option *)
+		alienTypeKey = "#Text:AlienViewType";
+		noAliensKey = "#Text:NoAlienViewsFound";
+		noRulerKey = "#Text:NoRulerSelected";
+		noMatchKey = "#Text:SelectionDoesNotMatch";
+		noTargetKey = "#Text:NoTargetFound";
+		noSelectionKey = "#Text:NoSelectionFound";
+		noPatternKey = "#Text:PatternNotSpecified";
+		notFoundKey = "#Text:PatternNotFound";	(* not used *)
+		replacingKey = "#System:Replacing";
+		shiftingKey = "#Text:Shifting";
+		showMarksKey = "#Text:ShowMarks";
+		hideMarksKey = "#Text:HideMarks";
+		replaceSelectionKey = "#Text:ReplaceAllInSelection";
+		replaceAllKey = "#Text:ReplaceAll";
+
+
+	TYPE
+		FindSpec = RECORD
+			valid, ignoreCase, wordBeginsWith, wordEndsWith, reverse: BOOLEAN;
+			start: INTEGER;
+			find: ARRAY maxPat OF CHAR
+		END;
+
+		ReplList = POINTER TO RECORD
+			next: ReplList;
+			beg, end: INTEGER;
+			buf: TextModels.Model
+		END;
+
+		ReplOp = POINTER TO RECORD (Stores.Operation)
+			text: TextModels.Model;
+			list, last: ReplList;
+			find: FindSpec
+		END;
+
+
+	VAR
+		find*: RECORD
+			find*: ARRAY maxPat OF CHAR;
+			replace*: ARRAY maxPat OF CHAR;
+			ignoreCase*, wordBeginsWith*, wordEndsWith*: BOOLEAN;
+			reverseOrientation*: BOOLEAN
+		END;
+
+		ruler*: RECORD
+			pageBreaks*: RECORD
+				notInside*, joinPara*: BOOLEAN
+			END
+		END;
+
+
+	PROCEDURE Show (t: TextModels.Model; beg, end: INTEGER);
+	BEGIN
+		TextViews.ShowRange(t, beg, end, TextViews.focusOnly);
+		IF beg = end THEN
+			TextControllers.SetCaret(t, beg)
+		ELSE
+			TextControllers.SetSelection(t, beg, end)
+		END
+	END Show;
+
+	PROCEDURE NoShow (t: TextModels.Model; pos: INTEGER);
+	BEGIN
+		TextControllers.SetSelection(t, pos, pos);
+		TextControllers.SetCaret(t, pos)
+	END NoShow;
+
+	PROCEDURE Ruler (): TextRulers.Ruler;
+		VAR r: TextRulers.Ruler;
+	BEGIN
+		r := TextRulers.dir.New(NIL);
+		TextRulers.AddTab(r, 4*mm); TextRulers.AddTab(r, 20*mm);
+		RETURN r
+	END Ruler;
+
+
+	(* search & replace *)
+
+	PROCEDURE LeftTerminator (ch: CHAR): BOOLEAN;
+	BEGIN
+		IF ch < 100X THEN
+			CASE ch OF
+				viewcode, tab, line, para, " ",
+				"(", "[", "{", "=",
+				hyphen, softhyphen: RETURN TRUE
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN TRUE
+		END
+	END LeftTerminator;
+
+	PROCEDURE RightTerminator (ch: CHAR): BOOLEAN;
+	BEGIN
+		IF ch < 100X THEN
+			CASE ch OF
+				0X, viewcode, tab, line, para, " ",
+				"!", "(", ")", ",", ".", ":", ";", "?", "[", "]", "{", "}",
+				hyphen, softhyphen: RETURN TRUE
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN TRUE
+		END
+	END RightTerminator;
+
+	PROCEDURE PreparePat (spec: FindSpec;
+								VAR pat: ARRAY OF CHAR; VAR n: INTEGER;
+								VAR wordBeg, wordEnd: BOOLEAN);
+		VAR i: INTEGER; ch: CHAR;
+	BEGIN
+		i := 0; ch := spec.find[0];
+		wordBeg := spec.wordBeginsWith & ~LeftTerminator(ch);
+		IF wordBeg THEN pat[0] := leftTerm; n := 1 ELSE n := 0 END;
+		WHILE ch # 0X DO
+			IF ch # softhyphen THEN
+				IF spec.ignoreCase THEN pat[n] := Strings.Upper(ch) ELSE pat[n] := ch END;
+				INC(n)
+			END;
+			INC(i); ch := spec.find[i]
+		END;
+		wordEnd := spec.wordEndsWith & ~RightTerminator(pat[n - 1]);
+		IF wordEnd THEN pat[n] := rightTerm; INC(n) END
+	END PreparePat;
+
+	PROCEDURE FindPat (t: TextModels.Model; spec: FindSpec; VAR beg, end: INTEGER);
+	(* post: beg < end => t[beg, end) = spec.find, start <= beg; else beg = end *)
+		VAR r: TextModels.Reader; start: INTEGER;
+			i, j, b, e, n: INTEGER; ch0, ch, ch1: CHAR; wordBeg, wordEnd, icase: BOOLEAN;
+			pat, ref: ARRAY maxPat OF CHAR;	(* ref [b..e) is readback buffer *)
+			pos0, pos1, absStart: INTEGER;
+			orientation: INTEGER;
+	BEGIN
+		IF spec.reverse THEN
+			orientation := -1; absStart := t.Length();
+			PreparePat(spec, ref, n, wordEnd, wordBeg);
+			i := n; j := 0; REPEAT DEC(i); pat[j] := ref[i]; INC(j) UNTIL i = 0	(* Just reverse the pattern... *)
+		ELSE
+			orientation := 1; absStart := 0;
+			PreparePat(spec, pat, n, wordBeg, wordEnd)
+		END;
+		start := spec.start; icase := spec.ignoreCase;
+		r := t.NewReader(NIL); i := 0;
+		IF wordBeg THEN
+			IF start # absStart THEN
+				DEC(start, orientation)
+			ELSE
+				r.SetPos(absStart);
+				IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
+				IF ~LeftTerminator(ch) THEN i := 1 END
+			END
+		END;
+		r.SetPos(start); IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
+		pos0 := start; pos1 := start;
+		IF icase THEN ch := Strings.Upper(ch) END;
+		ref[0] := ch; ch0 := ch; j := 0; b := 0; e := 1;
+		WHILE ~r.eot & (i < n) DO
+			ch1 := pat[i];
+			IF (ch1 = ch)
+			OR (ch1 = leftTerm) & LeftTerminator(ch)
+			OR (ch1 = rightTerm) & RightTerminator(ch) THEN
+				INC(i); j := (j + 1) MOD maxPat
+			ELSIF ch = softhyphen THEN
+				j := (j + 1) MOD maxPat
+			ELSE
+				i := 0; INC(pos0, orientation); b := (b + 1) MOD maxPat; j := b
+			END;
+			IF j # e THEN
+				ch := ref[j]
+			ELSE
+				INC(pos1, orientation);
+				IF spec.reverse THEN r.ReadPrevChar(ch) ELSE r.ReadChar(ch) END;
+				IF icase THEN ch := Strings.Upper(ch) END;
+				ref[j] := ch; e := (e + 1) MOD maxPat
+			END
+		END;
+		IF wordEnd & ~((i + 1 = n) & r.eot) THEN DEC(pos1, orientation) END;
+		IF (n > 0) & ((i = n) OR wordEnd & (i + 1 = n) & r.eot) THEN
+			IF wordBeg & ((pos0 # absStart) OR LeftTerminator(ch0)) THEN INC(pos0, orientation) END
+		ELSE
+			pos0 := pos1
+		END;
+		IF spec.reverse THEN
+			beg := pos1; end := pos0
+		ELSE
+			beg := pos0; end := pos1
+		END
+	END FindPat;
+
+	PROCEDURE OverrideSpecWithOption (VAR spec: FindSpec; option: ARRAY OF CHAR);
+		VAR i: INTEGER; choice: BOOLEAN; ch: CHAR;
+	BEGIN
+		choice := TRUE; i := 0; ch := option[i];
+		WHILE ch # 0X DO
+			CASE option[i] OF
+				'~': choice := ~choice
+			|    'I', 'i': spec.ignoreCase := choice; choice := TRUE
+			|    'B', 'b': spec.wordBeginsWith := choice; choice := TRUE
+			|    'E', 'e': spec.wordEndsWith := choice; choice := TRUE
+			|    'R', 'r': spec.reverse := choice; choice := TRUE
+			ELSE choice := TRUE
+			END;
+			INC(i); ch := option[i]
+		END
+	END OverrideSpecWithOption;
+
+	PROCEDURE SetSpec (VAR spec: FindSpec; pos0, pos1: INTEGER; option: ARRAY OF CHAR);
+	BEGIN
+		ASSERT(find.find # "", 20);
+		spec.valid := TRUE;
+		spec.ignoreCase := find.ignoreCase;
+		spec.wordBeginsWith := find.wordBeginsWith;
+		spec.wordEndsWith := find.wordEndsWith;
+		spec.reverse := find.reverseOrientation;
+		OverrideSpecWithOption(spec, option);
+		IF spec.reverse THEN spec.start := pos1
+		ELSE spec.start := pos0
+		END;
+		spec.find := find.find$
+	END SetSpec;
+
+	PROCEDURE SetFindSpec (c: TextControllers.Controller; first: BOOLEAN; option: ARRAY OF CHAR;
+		VAR spec: FindSpec
+	);
+		VAR (*start,*) pos0, pos1, beg, end: INTEGER;
+	BEGIN
+		IF first THEN pos0 := 0; pos1 := pos0
+		ELSIF c.HasCaret() THEN pos0 := c.CaretPos(); pos1 := pos0
+		ELSIF c.HasSelection() THEN c.GetSelection(beg, end); pos0 := beg + 1; pos1 := end - 1
+		ELSE pos0 := 0; pos1 := pos0
+		END;
+		SetSpec(spec, pos0, pos1, option);
+		IF spec.reverse THEN
+			IF spec.start = 0 THEN spec.start := c.text.Length() END
+		ELSE
+			IF spec.start = c.text.Length() THEN spec.start := 0 END
+		END
+	END SetFindSpec;
+
+
+	PROCEDURE ReplBuf (target: TextModels.Model; pos: INTEGER): TextModels.Model;
+		VAR buf: TextModels.Model; attr: TextModels.Attributes; rd: TextModels.Reader;
+			out: TextModels.Writer; i: INTEGER;
+	BEGIN
+		rd := target.NewReader(NIL); rd.SetPos(pos); rd.ReadRun(attr);
+		buf := TextModels.CloneOf(target); out := buf.NewWriter(NIL); out.SetPos(0);
+		IF attr # NIL THEN out.SetAttr(attr) END;
+		i := 0; WHILE find.replace[i] # 0X DO out.WriteChar(find.replace[i]); INC(i) END;
+		RETURN buf
+	END ReplBuf;
+
+
+	(* operations *)
+
+	PROCEDURE (op: ReplOp) Do;
+		VAR u, v: ReplList; text, save: TextModels.Model; beg, end, delta, len: INTEGER;
+	BEGIN
+		text := op.text;
+		u := op.list; v := NIL; delta := 0;
+		WHILE u # NIL DO
+			INC(u.beg, delta); INC(u.end, delta);
+			IF u.end > u.beg THEN
+				save := TextModels.CloneOf(text); save.Insert(0, text, u.beg, u.end);
+				DEC(delta, u.end - u.beg)
+			ELSE
+				save := NIL
+			END;
+			IF u.buf # NIL THEN
+				len := u.buf.Length();
+				text.Insert(u.beg, u.buf, 0, len);
+				u.end := u.beg + len;
+				INC(delta, len)
+			ELSE
+				u.end := u.beg
+			END;
+			u.buf := save;
+			v := u; u := u.next
+		END;
+		IF op.find.valid THEN
+			FindPat(text, op.find, beg, end); op.find.valid := FALSE;
+			IF beg = end THEN Dialog.Beep END
+		ELSIF v # NIL THEN
+			beg := v.beg; end := v.end
+		ELSE
+			beg := 0; end := 0
+		END;
+		IF end > beg THEN Show(text, beg, end) ELSE NoShow(text, beg) END
+	END Do;
+
+	PROCEDURE AddRepl (op: ReplOp; beg, end: INTEGER; reverse: BOOLEAN);
+		VAR u: ReplList;
+	BEGIN
+		NEW(u); u.beg := beg; u.end := end; u.buf := ReplBuf(op.text, beg);
+		IF reverse THEN	(* append *)
+			u.next := op.list; op.list := u
+		ELSE	(* prepend *)
+			IF op.list = NIL THEN op.list := u ELSE op.last.next := u END;
+			op.last := u
+		END
+	END AddRepl;
+
+	PROCEDURE DoReplaceThis ( 
+		t: TextModels.Model; mode: INTEGER;
+		firstBeg, firstEnd: INTEGER;
+		rngBeg, rngEnd: INTEGER;
+		option: ARRAY OF CHAR
+	);
+		VAR op: ReplOp; spec: FindSpec; beg, end, len: INTEGER;
+	BEGIN
+		NEW(op); op.text := t; op.list := NIL;
+		beg := firstBeg; end := firstEnd;
+		IF mode IN {replace, replaceAndFind} THEN
+			AddRepl(op, firstBeg, firstEnd, spec.reverse)
+		END;
+		IF mode = replaceAndFind THEN
+			SetSpec(op.find, firstBeg + (* LEN(find.replace$) *) ReplBuf(t, 0).Length(), firstBeg, option)
+		ELSE
+			op.find.valid := FALSE
+		END;
+		IF mode = replaceAll THEN
+			len := LEN(find.find$);
+			SetSpec(spec, 0, t.Length(), option);
+			WHILE (rngBeg <= beg) & (beg < end) & (end <= rngEnd)  DO
+				AddRepl(op, beg, end, spec.reverse);
+				IF spec.reverse THEN spec.start := beg ELSE spec.start := beg + len END;
+				FindPat(t, spec, beg, end)
+			END
+		END;
+		Models.Do(t, replacingKey, op)
+	END DoReplaceThis;
+
+	PROCEDURE DoReplace (c: TextControllers.Controller; mode: INTEGER; option: ARRAY OF CHAR);
+		VAR t: TextModels.Model; spec: FindSpec;
+			selBeg, selEnd, beg, end, len0: INTEGER; hasSel0: BOOLEAN;
+	BEGIN
+		IF c # NIL THEN
+			t := c.text; len0 := t.Length(); hasSel0 := c.HasSelection();
+			IF hasSel0 THEN
+				c.GetSelection(selBeg, selEnd);
+				IF selEnd < len0 THEN
+					SetSpec(spec, selBeg, selEnd + 1, option)
+				ELSE SetSpec(spec, selBeg, selEnd, option)
+				END
+			ELSE
+				selBeg := 0; selEnd := len0;
+				SetFindSpec(c, (* again *) mode = replaceAll, option, spec)
+			END;
+			FindPat(t, spec, beg, end);
+			IF mode = replaceAll THEN
+				IF (selBeg <= beg) & (beg < end) & (end <= selEnd) THEN
+					DoReplaceThis(t, mode, beg, end, selBeg, selEnd, option);
+					IF hasSel0 THEN Show(c.text, selBeg, selEnd + t.Length() - len0) END
+				ELSE NoShow(c.text, 0); Dialog.Beep
+				END
+			ELSIF hasSel0 THEN
+				IF (beg = selBeg) & (end = selEnd) THEN
+					DoReplaceThis(t, mode, selBeg, selEnd, 0, len0, option)
+				ELSE Dialog.ShowParamMsg(noMatchKey, spec.find, "", "")
+				END
+			ELSE Dialog.ShowMsg(noSelectionKey)
+			END
+		ELSE Dialog.ShowMsg(noTargetKey)
+		END
+	END DoReplace;
+
+	PROCEDURE DoShift (c: TextControllers.Controller; left: BOOLEAN);
+		VAR script: Stores.Operation;
+			t: TextModels.Model; st: TextSetters.Setter;
+			rd: TextModels.Reader; wr: TextModels.Writer;
+			box: TextSetters.LineBox; beg, pos, end: INTEGER; ch: CHAR;
+	BEGIN
+		IF (c # NIL) & (c.HasSelection() OR c.HasCaret()) THEN
+			t := c.text;
+			IF c.HasSelection() THEN c.GetSelection(beg, end) ELSE beg := c.CaretPos(); end := beg END;
+			st := c.view.ThisSetter(); beg := st.ThisSequence(beg); pos := beg;
+			rd := t.NewReader(NIL); rd.SetPos(pos);
+			IF ~left THEN wr := t.NewWriter(NIL) END;
+			Models.BeginScript(t, shiftingKey, script);
+			REPEAT
+				rd.ReadChar(ch);
+				IF rd.view # NIL THEN
+					st.GetLine(pos, box);
+					IF box.rbox THEN ch := para END
+				END;
+				IF left & ((ch = tab) OR (ch = " ") OR (ch = digitspace) OR (ch = nbspace)) THEN
+					t.Delete(pos, pos + 1); rd.SetPos(pos); DEC(end)
+				ELSIF ~left & (ch # line) & (ch # para) THEN
+					wr.SetPos(pos);
+					IF (ch = " ") OR (ch = digitspace) OR (ch = nbspace) THEN
+						wr.WriteChar(ch)
+					ELSE wr.WriteChar(tab)
+					END;
+					INC(pos); INC(end)
+				ELSE INC(pos)
+				END;
+				WHILE ~rd.eot & (ch # line) & (ch # para) DO
+					INC(pos); rd.ReadChar(ch)
+				END
+			UNTIL rd.eot OR (pos >= end);
+			Models.EndScript(t, script);
+			IF end > beg THEN TextControllers.SetSelection(t, beg, end) END
+		END
+	END DoShift;
+
+	(** commands **)
+
+	PROCEDURE ListAlienViews*;
+		VAR t: TextModels.Model; v: TextViews.View; wr: TextMappers.Formatter;
+			rd: TextModels.Reader; view: Views.View;
+			type: Stores.TypeName; none: BOOLEAN;
+	BEGIN
+		t := TextViews.FocusText();
+		IF t # NIL THEN
+			wr.ConnectTo(TextModels.dir.New());
+			rd := t.NewReader(NIL); rd.ReadView(view); none := TRUE;
+			WHILE view # NIL DO
+				IF view IS Views.Alien THEN
+					IF none THEN
+						wr.WriteTab; wr.WriteMsg(posKey);
+						wr.WriteTab; wr.WriteMsg(alienTypeKey); wr.WriteLn
+					END;
+					none := FALSE;
+					type := view(Views.Alien).store.path[0]$;
+					wr.WriteTab;
+					wr.WriteIntForm(rd.Pos() - 1,
+						TextMappers.decimal, 5, nbspace, TextMappers.hideBase);
+					wr.WriteTab; wr.WriteString(type); wr.WriteLn
+				END;
+				rd.ReadView(view)
+			END;
+			IF none THEN wr.WriteString(noAliensKey); wr.WriteLn END;
+			v := TextViews.dir.New(wr.rider.Base());
+			v.SetDefaults(Ruler(), TextViews.dir.defAttr);
+			Views.OpenView(v)
+		END
+	END ListAlienViews;
+
+
+	PROCEDURE ToggleMarksGuard* (VAR par: Dialog.Par);
+		VAR v: TextViews.View;
+	BEGIN
+		v := TextViews.Focus();
+		IF (v # NIL) & v.HidesMarks() THEN par.label := showMarksKey
+		ELSE par.label := hideMarksKey
+		END
+	END ToggleMarksGuard;
+
+	PROCEDURE ToggleMarks*;
+		VAR v: TextViews.View;
+	BEGIN
+		v := TextViews.Focus();
+		IF v # NIL THEN v.DisplayMarks(~v.HidesMarks()) END
+	END ToggleMarks;
+
+	PROCEDURE ShowMarks*;
+		VAR v: TextViews.View;
+	BEGIN
+		v := TextViews.Focus();
+		IF (v # NIL) & v.HidesMarks() THEN v.DisplayMarks(TextViews.show) END
+	END ShowMarks;
+
+	PROCEDURE HideMarks*;
+		VAR v: TextViews.View;
+	BEGIN
+		v := TextViews.Focus();
+		IF (v # NIL) & ~v.HidesMarks() THEN v.DisplayMarks(TextViews.hide) END
+	END HideMarks;
+
+	PROCEDURE MakeDefaultRulerGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller; v: Views.View;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			v := c.Singleton();
+			IF (v = NIL) OR ~(v IS TextRulers.Ruler) THEN par.disabled := TRUE END
+		ELSE par.disabled := TRUE
+		END
+	END MakeDefaultRulerGuard;
+
+	PROCEDURE MakeDefaultRuler*;
+		VAR c: TextControllers.Controller; rd: TextModels.Reader;
+			r: TextRulers.Ruler; a: TextModels.Attributes;
+			beg, end: INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF c.HasSelection() THEN
+				c.GetSelection(beg, end);
+				rd := c.text.NewReader(NIL); rd.SetPos(beg); rd.Read;
+				IF (rd.view # NIL) & (rd.view IS TextRulers.Ruler) THEN
+					c.view.PollDefaults(r, a);
+					c.view.SetDefaults(rd.view(TextRulers.Ruler), a)
+				ELSE Dialog.ShowMsg(noRulerKey)
+				END
+			ELSE Dialog.ShowMsg(noSelectionKey)
+			END
+		ELSE Dialog.ShowMsg(noTargetKey)
+		END
+	END MakeDefaultRuler;
+
+	PROCEDURE MakeDefaultAttributes*;
+		VAR c: TextControllers.Controller; rd: TextModels.Reader;
+			r: TextRulers.Ruler; a: TextModels.Attributes;
+			beg, end: INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			IF c.HasSelection() THEN
+				c.GetSelection(beg, end);
+				rd := c.text.NewReader(NIL); rd.SetPos(beg); rd.Read;
+				c.view.PollDefaults(r, a);
+				c.view.SetDefaults(r, rd.attr)
+			ELSE Dialog.ShowMsg(noSelectionKey)
+			END
+		ELSE Dialog.ShowMsg(noTargetKey)
+		END
+	END MakeDefaultAttributes;
+
+	PROCEDURE ShiftLeft*;
+	BEGIN
+		DoShift(TextControllers.Focus(), left)
+	END ShiftLeft;
+
+	PROCEDURE ShiftRight*;
+	BEGIN
+		DoShift(TextControllers.Focus(), right)
+	END ShiftRight;
+
+
+	PROCEDURE Subscript*;
+		VAR q, p0: Properties.Property; p: TextModels.Prop;
+	BEGIN
+		Properties.CollectProp(q);
+		p0 := q; WHILE (p0 # NIL) & ~(p0 IS TextModels.Prop) DO p0 := p0.next END;
+		NEW(p); p.valid := {TextModels.offset};
+		IF (p0 # NIL) & (TextModels.offset IN p0.valid) THEN
+			p.offset := p0(TextModels.Prop).offset - point
+		ELSE p.offset := -point
+		END;
+		Properties.EmitProp(NIL, p)
+	END Subscript;
+
+	PROCEDURE Superscript*;
+		VAR q, p0: Properties.Property; p: TextModels.Prop;
+	BEGIN
+		Properties.CollectProp(q);
+		p0 := q; WHILE (p0 # NIL) & ~(p0 IS TextModels.Prop) DO p0 := p0.next END;
+		NEW(p); p.valid := {TextModels.offset};
+		IF (p0 # NIL) & (TextModels.offset IN p0.valid) THEN
+			p.offset := p0(TextModels.Prop).offset + point
+		ELSE p.offset := point
+		END;
+		Properties.EmitProp(NIL, p)
+	END Superscript;
+
+
+	PROCEDURE ForceToNewLine (c: TextControllers.Controller);
+		VAR st: TextSetters.Setter; pos, start: INTEGER; msg: Controllers.EditMsg;
+	BEGIN
+		IF c.HasCaret() THEN
+			pos := c.CaretPos();
+			st := c.view.ThisSetter(); start := st.ThisLine(pos);
+			IF pos # start THEN
+				msg.op := Controllers.pasteChar; msg.char := line;
+				Controllers.Forward(msg)
+			END
+		END
+	END ForceToNewLine;
+
+	PROCEDURE InsertParagraph*;
+		VAR c: TextControllers.Controller; script: Stores.Operation; msg: Controllers.EditMsg;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			Models.BeginScript(c.text, "#Text:InsertParagraph", script);
+			ForceToNewLine(c);
+			msg.op := Controllers.pasteChar; msg.char := para;
+			Controllers.Forward(msg);
+			Models.EndScript(c.text, script)
+		END
+	END InsertParagraph;
+
+	PROCEDURE InsertRuler*;
+		VAR c: TextControllers.Controller; script: Stores.Operation;
+			rd: TextModels.Reader; r: TextRulers.Ruler;
+			pos, end: INTEGER;
+	BEGIN
+		c := TextControllers.Focus();
+		IF c # NIL THEN
+			r := NIL;
+			IF c.HasSelection() THEN
+				c.GetSelection(pos, end);
+				rd := c.text.NewReader(NIL); rd.SetPos(pos); rd.Read;
+				IF (rd.view # NIL) & (rd.view IS TextRulers.Ruler) THEN
+					r := rd.view(TextRulers.Ruler)
+				END
+			ELSE pos := c.CaretPos()
+			END;
+			IF r = NIL THEN r := TextViews.ThisRuler(c.view, pos) END;
+			r := TextRulers.CopyOf(r, Views.deep);
+			Models.BeginScript(c.text, "#Text:InsertRuler", script);
+			ForceToNewLine(c);
+			c.view.DisplayMarks(TextViews.show);
+			Controllers.PasteView(r, Views.undefined, Views.undefined, FALSE);
+			Models.EndScript(c.text, script)
+		END
+	END InsertRuler;
+
+	PROCEDURE InsertSoftHyphen*;
+		VAR msg: Controllers.EditMsg;
+	BEGIN
+		msg.op := Controllers.pasteChar; msg.char := softhyphen;
+		Controllers.Forward(msg)
+	END InsertSoftHyphen;
+
+	PROCEDURE InsertNBHyphen*;
+		VAR msg: Controllers.EditMsg;
+	BEGIN
+		msg.op := Controllers.pasteChar; msg.char := nbhyphen;
+		Controllers.Forward(msg)
+	END InsertNBHyphen;
+
+	PROCEDURE InsertNBSpace*;
+		VAR msg: Controllers.EditMsg;
+	BEGIN
+		msg.op := Controllers.pasteChar; msg.char := nbspace;
+		Controllers.Forward(msg)
+	END InsertNBSpace;
+
+	PROCEDURE InsertDigitSpace*;
+		VAR msg: Controllers.EditMsg;
+	BEGIN
+		msg.op := Controllers.pasteChar; msg.char := digitspace;
+		Controllers.Forward(msg)
+	END InsertDigitSpace;
+
+
+	PROCEDURE GetFindPattern (c: TextControllers.Controller);
+		VAR r: TextModels.Reader; beg, end: INTEGER; i: INTEGER; ch: CHAR;
+			new: ARRAY maxPat OF CHAR;
+	BEGIN
+		IF (c # NIL) & c.HasSelection() THEN
+			c.GetSelection(beg, end);
+			r := c.text.NewReader(NIL); r.SetPos(beg); r.ReadChar(ch); i := 0;
+			WHILE (r.Pos() <= end) & (i < maxPat - 1) DO
+				new[i] := ch; INC(i); r.ReadChar(ch)
+			END;
+			new[i] := 0X;
+			IF (new # "") & (new # find.find) THEN
+				find.find := new$;
+				find.ignoreCase := FALSE;
+				find.wordBeginsWith := FALSE; find.wordEndsWith := FALSE;
+				Dialog.Update(find)
+			END
+		END
+	END GetFindPattern;
+
+	PROCEDURE FindIn (c: TextControllers.Controller; first: BOOLEAN; option: ARRAY OF CHAR);
+		VAR spec: FindSpec; beg, end: INTEGER;
+	BEGIN
+		IF c # NIL THEN
+			IF find.find # "" THEN
+				SetFindSpec(c, first, option, spec);
+				FindPat(c.text, spec, beg, end);
+				IF end > beg THEN Show(c.text, beg, end) ELSE NoShow(c.text, 0); Dialog.Beep END
+			ELSE Dialog.ShowMsg(noPatternKey)
+			END
+		ELSE Dialog.ShowMsg(noTargetKey)
+		END
+	END FindIn;
+
+
+	PROCEDURE FindGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (find.find = "") THEN par.disabled := TRUE END
+	END FindGuard;
+
+	PROCEDURE FindFirst* (option: ARRAY OF CHAR);
+	BEGIN
+		FindIn(TextControllers.Focus(), first, option)
+	END FindFirst;
+
+	PROCEDURE FindAgainGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (~c.HasSelection() & (find.find = "")) THEN par.disabled := TRUE END
+	END FindAgainGuard;
+
+	PROCEDURE FindAgain* (option: ARRAY OF CHAR);
+	BEGIN
+		FindIn(TextControllers.Focus(), again, option)
+	END FindAgain;
+
+
+	PROCEDURE ReplaceGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (Containers.noCaret IN c.opts) OR ~c.HasSelection() OR (find.find = "") THEN
+			par.disabled := TRUE
+		END
+	END ReplaceGuard;
+
+	PROCEDURE Replace* (option: ARRAY OF CHAR);
+	BEGIN
+		DoReplace(TextControllers.Focus(), replace, option)
+	END Replace;
+
+	PROCEDURE ReplaceAndFindNext* (option: ARRAY OF CHAR);
+	BEGIN
+		DoReplace(TextControllers.Focus(), replaceAndFind, option)
+	END ReplaceAndFindNext;
+
+
+	PROCEDURE ReplaceAllGuard* (VAR par: Dialog.Par);
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (Containers.noCaret IN c.opts) OR (find.find = "") THEN
+			par.disabled := TRUE
+		ELSE
+			IF c.HasSelection() THEN par.label := replaceSelectionKey ELSE par.label := replaceAllKey END
+		END
+	END ReplaceAllGuard;
+
+	PROCEDURE ReplaceAll* (option: ARRAY OF CHAR);
+	BEGIN
+		DoReplace(TextControllers.Focus(), replaceAll, option)
+	END ReplaceAll;
+
+
+	PROCEDURE SetNormalOrientation*;
+	BEGIN
+		find.reverseOrientation := FALSE;
+		Dialog.Update(find)
+	END SetNormalOrientation;
+
+	PROCEDURE SetReverseOrientation*;
+	BEGIN
+		find.reverseOrientation := TRUE;
+		Dialog.Update(find)
+	END SetReverseOrientation;
+
+	PROCEDURE InitFindDialog*;
+	BEGIN
+		GetFindPattern(TextControllers.Focus())
+	END InitFindDialog;
+
+
+	(** ruler dialog **)
+
+	PROCEDURE InitRulerDialog*;
+		VAR v: Views.View; ra: TextRulers.Attributes;
+	BEGIN
+		v := Controllers.FocusView();
+		IF v # NIL THEN
+			WITH v: TextRulers.Ruler DO
+				ra := v.style.attr;
+				ruler.pageBreaks.notInside := TextRulers.noBreakInside IN ra.opts;
+				ruler.pageBreaks.joinPara := TextRulers.parJoin IN ra.opts
+			ELSE
+			END
+		END
+	END InitRulerDialog;
+
+	PROCEDURE SetRuler*;
+		VAR v: Views.View; p: TextRulers.Prop;
+	BEGIN
+		v := Controllers.FocusView();
+		IF v # NIL THEN
+			WITH v: TextRulers.Ruler DO
+				NEW(p); p.valid := {TextRulers.opts};
+				p.opts.mask := {TextRulers.noBreakInside, TextRulers.parJoin};
+				p.opts.val := {};
+				IF ruler.pageBreaks.notInside THEN INCL(p.opts.val, TextRulers.noBreakInside) END;
+				IF ruler.pageBreaks.joinPara THEN INCL(p.opts.val, TextRulers.parJoin) END;
+				Properties.EmitProp(NIL, p)
+			ELSE
+			END
+		END
+	END SetRuler;
+
+
+	(** standard text-related guards **)
+
+	PROCEDURE FocusGuard* (VAR par: Dialog.Par);
+	(** in non-TextView menus; otherwise implied by menu type **)
+	BEGIN
+		IF TextViews.Focus() = NIL THEN par.disabled := TRUE END
+	END FocusGuard;
+
+	PROCEDURE EditGuard* (VAR par: Dialog.Par);
+	(** in non-TextView menus; otherwise use "StdCmds.EditGuard" **)
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (Containers.noCaret IN c.opts) THEN par.disabled := TRUE END
+	END EditGuard;
+
+	PROCEDURE SelectionGuard* (VAR par: Dialog.Par);
+	(** in non-TextView menus; otherwise use "StdCmds.SelectionGuard" **)
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR ~c.HasSelection() THEN par.disabled := TRUE END
+	END SelectionGuard;
+
+	PROCEDURE EditSelectionGuard* (VAR par: Dialog.Par);
+	(** in non-TextView menus; otherwise use "StdCmds.SelectionGuard" **)
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (Containers.noCaret IN c.opts) OR ~c.HasSelection() THEN par.disabled := TRUE END
+	END EditSelectionGuard;
+
+	PROCEDURE SingletonGuard* (VAR par: Dialog.Par);
+	(** in non-TextView menus; otherwise use "StdCmds.SingletonGuard" **)
+		VAR c: TextControllers.Controller;
+	BEGIN
+		c := TextControllers.Focus();
+		IF (c = NIL) OR (c.Singleton() = NIL) THEN par.disabled := TRUE END
+	END SingletonGuard;
+
+END TextCmds.

+ 1633 - 0
BlackBox/Text/Mod/Controllers.txt

@@ -0,0 +1,1633 @@
+MODULE TextControllers;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Controllers.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT
+		Services, Stores, Ports, Models, Views, Dialog, Controllers, Properties, Containers,
+		TextModels, TextRulers, TextSetters, TextViews;
+
+	CONST
+		noAutoScroll* = 16; noAutoIndent* = 17;
+
+		(** Controller.SetCaret pos; Controller.SetSelection beg, end **)
+		none* = -1;
+
+		(* Track mode *)
+		chars = 0; words = 1; lines = 2;	(* plus "none", defined above *)
+
+		enter = 3X; rdel = 7X; ldel = 8X;
+		aL = 1CX; aR = 1DX; aU = 1EX; aD = 1FX;
+		pL = 10X; pR = 11X; pU = 12X; pD = 13X;
+		dL = 14X; dR = 15X; dU = 16X; dD = 17X;
+
+		viewcode = TextModels.viewcode;
+		tab = TextModels.tab; line = TextModels.line; para = TextModels.para;
+
+		point = Ports.point; mm = Ports.mm; inch16 = Ports.inch DIV 16;
+
+		boundCaret = TRUE;
+		lenCutoff = 2000;	(* max run length inspected to fetch properties *)
+
+		attrChangeKey = "#Text:AttributeChange";
+		resizingKey = "#System:Resizing";
+		insertingKey = "#System:Inserting";
+		deletingKey = "#System:Deleting";
+		movingKey = "#System:Moving";
+		copyingKey = "#System:Copying";
+		linkingKey = "#System:Linking";
+		replacingKey = "#System:Replacing";
+
+		minVersion = 0; maxVersion = 0; maxStdVersion = 0;
+
+
+	TYPE
+		Controller* = POINTER TO ABSTRACT RECORD (Containers.Controller)
+			view-: TextViews.View;
+			text-: TextModels.Model	(** view # NIL => text = view.ThisText() **)
+		END;
+
+		Directory* = POINTER TO ABSTRACT RECORD (Containers.Directory) END;
+
+
+		FilterPref* = RECORD (Properties.Preference)
+			controller*: Controller;	(** IN, set to text controller asking for filter **)
+			frame*: Views.Frame;	(** IN, set to frame of controlled text view **)
+			x*, y*: INTEGER;	(** IN, set to coordinates of cursor in frame space **)
+			filter*: BOOLEAN	(** preset to FALSE **)
+		END;
+
+		FilterPollCursorMsg* = RECORD (Controllers.Message)
+			controller*: Controller;	(** IN, set to text controller asking for filter **)
+			x*, y*: INTEGER;
+			cursor*: INTEGER;	(** as for Controllers.PollCursorMsg **)
+			done*: BOOLEAN	(** OUT; initialized to FALSE **)
+		END;
+
+		FilterTrackMsg* = RECORD (Controllers.Message)
+			controller*: Controller;	(** IN, set to text controller asking for filter **)
+			x*, y*: INTEGER;
+			modifiers*: SET;	(** as for Controllers.TrackMsg **)
+			done*: BOOLEAN	(** OUT; initialized to FALSE **)
+		END;
+
+
+		StdCtrl = POINTER TO RECORD (Controller)
+			(* general state *)
+			cachedRd: TextModels.Reader;
+			cachedWr: TextModels.Writer;
+			insAttr: TextModels.Attributes;  (* preset attrs for next typed char *)
+			autoBeg, autoEnd: INTEGER;	(* lazy auto-scrolling;
+																invalid if (-1, .); initially (MAX(LONGINT), 0) *)
+			(* caret *)
+			carPos: INTEGER;	(* HasCaret()  iff  0 <= carPos <= text.Length() *)
+			carLast: INTEGER;	(* used to recover caret at meaningful position *)
+			carX, lastX: INTEGER;	(* arrow up/down anti-aliasing *)
+			carTick: LONGINT;	(* next tick to invert flashing caret mark *)
+			carVisible: BOOLEAN;	(* caret currently visible - used for flashing caret *)
+			(* selection *)
+			selBeg, selEnd: INTEGER;	(* HasSel()  iff  0 <= selBeg < selEnd <= text.Length() *)
+			aliasSelBeg, aliasSelEnd: INTEGER;	(* need lazy synchronization? *)
+			selPin0, selPin1: INTEGER;	(* anchor points of selection *)
+			(* most recent scroll-while-tracking step *)
+			lastStep: LONGINT
+		END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+
+		(* messages *)
+
+		ModelMessage* = ABSTRACT RECORD (Models.Message) END;
+			(** messages to control virtual model extensions, such as marks **)
+
+		SetCaretMsg* = EXTENSIBLE RECORD (ModelMessage)
+			pos*: INTEGER
+		END;
+
+		SetSelectionMsg* = EXTENSIBLE RECORD (ModelMessage)
+			beg*, end*: INTEGER
+		END;
+
+
+		ViewMessage = ABSTRACT RECORD (Views.Message) END;
+
+		CaretMsg = RECORD (ViewMessage)
+			show: BOOLEAN
+		END;
+
+		SelectionMsg = RECORD (ViewMessage)
+			beg, end: INTEGER;
+			show: BOOLEAN
+		END;
+
+
+		(* miscellaneous *)
+
+		TrackState = RECORD
+			x, y: INTEGER;
+			toggle: BOOLEAN
+		END;
+
+
+	VAR
+		dir-, stdDir-: Directory;
+
+
+	PROCEDURE CachedReader (c: StdCtrl): TextModels.Reader;
+		VAR rd: TextModels.Reader;
+	BEGIN
+		rd := c.text.NewReader(c.cachedRd); c.cachedRd := NIL; RETURN rd
+	END CachedReader;
+
+	PROCEDURE CacheReader (c: StdCtrl; rd: TextModels.Reader);
+	BEGIN
+		c.cachedRd := rd
+	END CacheReader;
+
+
+	PROCEDURE CachedWriter (c: StdCtrl; attr: TextModels.Attributes): TextModels.Writer;
+		VAR wr: TextModels.Writer;
+	BEGIN
+		wr := c.text.NewWriter(c.cachedWr); wr.SetAttr(attr);
+		c.cachedRd := NIL; RETURN wr
+	END CachedWriter;
+
+	PROCEDURE CacheWriter (c: StdCtrl; wr: TextModels.Writer);
+	BEGIN
+		c.cachedWr := wr
+	END CacheWriter;
+
+
+	(** Controller **)
+
+	PROCEDURE (c: Controller) Internalize2- (VAR rd: Stores.Reader), EXTENSIBLE;
+		VAR v: INTEGER;
+	BEGIN
+		(* c.Internalize^(rd); *)
+		rd.ReadVersion(minVersion, maxVersion, v)
+	END Internalize2;
+
+	PROCEDURE (c: Controller) Externalize2- (VAR wr: Stores.Writer), EXTENSIBLE;
+	BEGIN
+		(* c.Externalize^(wr); *)
+		wr.WriteVersion(maxVersion)
+	END Externalize2;
+
+	PROCEDURE (c: Controller) InitView2* (v: Views.View), EXTENSIBLE;
+	BEGIN
+		ASSERT((v = NIL) # (c.view = NIL), 21);
+		IF c.view = NIL THEN ASSERT(v IS TextViews.View, 22) END;
+		(* c.InitView^(v); *)
+		IF v # NIL THEN c.view := v(TextViews.View); c.text := c.view.ThisModel()
+		ELSE c.view := NIL; c.text := NIL
+		END
+	END InitView2;
+
+	PROCEDURE (c: Controller) ThisView* (): TextViews.View, EXTENSIBLE;
+	BEGIN
+		RETURN c.view
+	END ThisView;
+
+
+	(** caret **)
+
+	PROCEDURE (c: Controller) CaretPos* (): INTEGER, NEW, ABSTRACT;
+	PROCEDURE (c: Controller) SetCaret* (pos: INTEGER), NEW, ABSTRACT;
+	(** pre: pos = none  OR  0 <= pos <= c.text.Length() **)
+	(** post: c.carPos = pos **)
+
+
+	(** selection **)
+
+	PROCEDURE (c: Controller) GetSelection* (OUT beg, end: INTEGER), NEW, ABSTRACT;
+	(** post: beg = end  OR  0 <= beg <= end <= c.text.Length() **)
+
+	PROCEDURE (c: Controller) SetSelection* (beg, end: INTEGER), NEW, ABSTRACT;
+	(** pre: beg = end  OR  0 <= beg < end <= c.text.Length() **)
+	(** post: c.selBeg = beg, c.selEnd = end **)
+
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) NewController* (opts: SET): Controller, ABSTRACT;
+
+	PROCEDURE (d: Directory) New* (): Controller, EXTENSIBLE;
+	BEGIN
+		RETURN d.NewController({})
+	END New;
+
+
+	(** miscellaneous **)
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); dir := d
+	END SetDir;
+
+	PROCEDURE Install*;
+	BEGIN
+		TextViews.SetCtrlDir(dir)
+	END Install;
+
+
+	PROCEDURE Focus* (): Controller;
+		VAR v: Views.View; c: Containers.Controller;
+	BEGIN
+		v := Controllers.FocusView();
+		IF (v # NIL) & (v IS TextViews.View) THEN
+			c := v(TextViews.View).ThisController();
+			IF (c # NIL) & (c IS Controller) THEN RETURN c(Controller)
+			ELSE RETURN NIL
+			END
+		ELSE RETURN NIL
+		END
+	END Focus;
+
+
+	PROCEDURE SetCaret* (text: TextModels.Model; pos: INTEGER);
+	(** pre: text # NIL,  pos = none  OR  0 <= pos <= text.Length() **)
+		VAR cm: SetCaretMsg;
+	BEGIN
+		ASSERT(text # NIL, 20); ASSERT(none <= pos, 21); ASSERT(pos <= text.Length(), 22);
+		cm.pos := pos; Models.Broadcast(text, cm)
+	END SetCaret;
+
+	PROCEDURE SetSelection* (text: TextModels.Model; beg, end: INTEGER);
+	(** pre: text # NIL,  beg = end  OR  0 <= beg < end <= text.Length() **)
+		VAR sm: SetSelectionMsg;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		IF beg # end THEN
+			ASSERT(0 <= beg, 21); ASSERT(beg < end, 22); ASSERT(end <= text.Length(), 23)
+		END;
+		sm.beg := beg; sm.end := end; Models.Broadcast(text, sm)
+	END SetSelection;
+
+
+	(* support for cursor/selection/focus marking *)
+
+	PROCEDURE BlinkCaret (c: StdCtrl; f: Views.Frame; tick: INTEGER);
+		VAR vis: BOOLEAN;
+	BEGIN
+		IF (c.carPos # none) & f.front & (tick >= c.carTick) THEN
+			IF c.carVisible THEN
+				c.MarkCaret(f, Containers.hide); c.carVisible := FALSE
+			ELSE
+				c.carVisible := TRUE; c.MarkCaret(f, Containers.show)
+			END;
+			c.carTick := tick + Dialog.caretPeriod
+		END
+	END BlinkCaret;
+
+	PROCEDURE FlipCaret (c: StdCtrl; show: BOOLEAN);
+		VAR msg: CaretMsg;
+	BEGIN
+		msg.show := show;
+		Views.Broadcast(c.view, msg)
+	END FlipCaret;
+
+	PROCEDURE CheckCaret (c: StdCtrl);
+		VAR text: TextModels.Model; len, pos: INTEGER;
+	BEGIN
+		IF ~(Containers.noCaret IN c.opts) THEN
+			IF (c.carPos = none) & ~(boundCaret & (c.selBeg # c.selEnd)) & (c.ThisFocus() = NIL) THEN
+				text := c.text; len := text.Length(); pos := c.carLast; 
+				IF pos < 0 THEN pos := 0 ELSIF pos > len THEN pos := len END;
+				(* c.carVisible := FALSE; c.carTick := 0;	(* force visible mark *) *)
+				SetCaret(text, pos)
+			END
+		ELSE c.carPos := none
+		END
+	END CheckCaret;
+
+
+
+	PROCEDURE HiliteRect (f: Views.Frame; l, t, r, b, s: INTEGER; show: BOOLEAN);
+	BEGIN
+		IF s = Ports.fill THEN
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.hilite, show)
+		ELSE
+			f.MarkRect(l, t, r - s, t + s, s, Ports.hilite, show);
+			f.MarkRect(l, t + s, l + s, b - s, s, Ports.hilite, show);
+			f.MarkRect(l + s, b - s, r, b, s, Ports.hilite, show);
+			f.MarkRect(r - s, t + s, r, b - s, s, Ports.hilite, show)
+		END
+	END HiliteRect;
+
+	PROCEDURE MarkSelRange (c: StdCtrl; f: Views.Frame; b, e: TextViews.Location;
+		front, show: BOOLEAN
+	);
+		VAR fw, ff, r, t: INTEGER;
+	BEGIN
+		IF front THEN fw := 0; ff := Ports.fill ELSE fw := f.dot; ff := fw END;
+		IF b.start # e.start THEN
+			r := f.r; t := b.y + b.asc + b.dsc;
+			HiliteRect(f, b.x, b.y, r + fw, t + fw, ff, show);
+			IF t < e.y THEN HiliteRect(f, 0, t, r + fw, e.y + fw, ff, show) END;
+			b.x := f.l; b.y := e.y
+		END;
+		HiliteRect(f, b.x, b.y, e.x + fw, e.y + e.asc + e.dsc + fw, ff, show)
+	END MarkSelRange;
+
+	PROCEDURE MarkSelection (c: StdCtrl; f: Views.Frame; beg, end: INTEGER; show: BOOLEAN);
+		VAR b, e: TextViews.Location; s: Views.View; 
+	BEGIN
+		IF (beg # end) & f.mark THEN
+			ASSERT(beg < end, 20);
+			s := c.Singleton();
+			IF s # NIL THEN
+				IF beg + 1 = end THEN Containers.MarkSingleton(c, f, show) END
+			ELSE
+				c.view.GetThisLocation(f, beg, b); c.view.GetThisLocation(f, end, e);
+				IF (b.pos < e.pos) OR (b.pos = e.pos) & (b.x < e.x) THEN
+					MarkSelRange(c, f, b, e, f.front, show)
+				END
+			END
+		END
+	END MarkSelection;
+
+	PROCEDURE FlipSelection (c: StdCtrl; beg, end: INTEGER; show: BOOLEAN);
+		VAR msg: SelectionMsg;
+	BEGIN
+		msg.beg := beg; msg.end := end; msg.show := show;
+		Views.Broadcast(c.view, msg)
+	END FlipSelection;
+
+
+	PROCEDURE InitMarks (c: StdCtrl);
+	BEGIN
+		c.autoBeg := MAX(INTEGER); c.autoEnd := 0;
+		c.carPos := none; c.carVisible := FALSE; c.carLast := none; c.carTick := 0; c.carX := -1;
+		c.selBeg := none; c.selEnd := none;
+		c.lastStep := 0
+	END InitMarks;
+
+	PROCEDURE AutoShowRange (c: StdCtrl; beg, end: INTEGER);
+	BEGIN
+		IF (beg <= c.autoBeg) & (c.autoEnd <= end) THEN
+			c.autoBeg := beg; c.autoEnd := end	(* new range includes old range: expand *)
+		ELSE
+			c.autoBeg := -1	(* schizopheric scroll request -> don't scroll at all *)
+		END
+	END AutoShowRange;
+
+	PROCEDURE UpdateMarks (c: StdCtrl; op: INTEGER; beg, end, delta: INTEGER);
+	(* ensure that marks are valid after updates *)
+	BEGIN
+		CASE op OF
+		  TextModels.insert:
+			c.carLast := end; c.selBeg := end; c.selEnd := end; beg := end
+		| TextModels.delete:
+			c.carLast := beg; c.selBeg := beg; c.selEnd := beg; end := beg
+		| TextModels.replace:
+		ELSE
+			HALT(100)
+		END;
+		AutoShowRange(c, beg, end)
+	END UpdateMarks;
+
+
+	(* support for smart cut/copy/paste and attributing *)
+
+	PROCEDURE LegalChar (ch: CHAR): BOOLEAN;
+	BEGIN
+		IF ch < 100X THEN
+			CASE ORD(ch) OF
+				ORD(viewcode),
+				ORD(tab), ORD(line), ORD(para),
+				ORD(" ") .. 7EH, 80H .. 0FFH: RETURN TRUE
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN TRUE
+		END
+	END LegalChar;
+
+	PROCEDURE LeftTerminator (ch: CHAR): BOOLEAN;
+	BEGIN
+		IF ch < 100X THEN
+			CASE ch OF
+				viewcode, tab, line, para, '"', "'", "(", "[", "{": RETURN TRUE
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN TRUE
+		END
+	END LeftTerminator;
+
+	PROCEDURE RightTerminator (ch, ch1: CHAR): BOOLEAN;
+	BEGIN
+		IF ch < 100X THEN
+			CASE ch OF
+			  0X, viewcode, tab, line, para,
+			  "!", '"', "'", "(", ")", ",", ";", "?", "[", "]", "{", "}": RETURN TRUE
+			| ".", ":":
+				CASE ch1 OF
+				  0X, viewcode, tab, line, para, " ": RETURN TRUE
+				ELSE RETURN FALSE
+				END
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN TRUE
+		END
+	END RightTerminator;
+
+	PROCEDURE ReadLeft (rd: TextModels.Reader; pos: INTEGER; OUT ch: CHAR);
+	BEGIN
+		IF pos > 0 THEN rd.SetPos(pos - 1); rd.ReadChar(ch)
+		ELSE rd.SetPos(pos); ch := " "
+		END
+	END ReadLeft;
+
+	PROCEDURE SmartRange (c: StdCtrl; VAR beg, end: INTEGER);
+	(* if possible and whole words are covered,
+	extend [beg, end) to encompass either a leading or a trailing blank *)
+		VAR rd: TextModels.Reader; we, be: INTEGER; ch, ch0, ch1: CHAR; rightTerm: BOOLEAN;
+	BEGIN
+(*
+disable intelligent delete/cut/move for now
+		rd := CachedReader(c); ReadLeft(rd, beg, ch0); rd.ReadChar(ch);
+		IF ((ch0 <= " ") OR LeftTerminator(ch0)) & (ch # " ") THEN
+			(* range covers beg of word *)
+			we := beg; be := beg;
+			WHILE (ch # 0X) & (be <= end) DO
+				ch1 := ch; rd.ReadChar(ch); INC(be);
+				IF (ch1 # " ") & ((be <= end) OR ~RightTerminator(ch1, ch)) THEN we := be END
+			END;
+			rightTerm := RightTerminator(ch1, ch);
+			IF (beg < we) & (we = end) & ((we < be) OR rightTerm) THEN
+				(* range covers end of word *)
+				IF (we < be) & (ch1 = " ") THEN
+					INC(end)	(* include trailing blank *)
+				ELSIF (beg > 0) & rightTerm & (ch0 = " ") THEN
+					DEC(beg)	(* include leading blank *)
+				END
+			END
+		END;
+		CacheReader(c, rd)
+*)
+	END SmartRange;
+
+	PROCEDURE OnlyWords (c: StdCtrl; beg, end: INTEGER): BOOLEAN;
+		VAR rd: TextModels.Reader; we, be: INTEGER; ch, ch0, ch1: CHAR;
+			rightTerm, words: BOOLEAN;
+	BEGIN
+		words := FALSE;
+		rd := CachedReader(c); ReadLeft(rd, beg, ch0); rd.ReadChar(ch);
+		IF ((ch0 <= " ") OR LeftTerminator(ch0)) & (ch # " ") THEN	(* range covers beg of word *)
+			we := beg; be := beg;
+			WHILE (ch # 0X) & (be <= end) DO
+				ch1 := ch; rd.ReadChar(ch); INC(be);
+				IF (ch1 # " ") & ((be <= end) OR ~RightTerminator(ch1, ch)) THEN
+					we := be
+				END
+			END;
+			rightTerm := RightTerminator(ch1, ch);
+			IF (beg < we) & (we = end) & ((we < be) OR rightTerm) THEN	(* range covers end of word *)
+				words := TRUE
+			END
+		END;
+		CacheReader(c, rd);
+		RETURN words
+	END OnlyWords;
+
+	PROCEDURE GetTargetField (t: TextModels.Model; pos: INTEGER;
+		VAR touchL, touchM, touchR: BOOLEAN
+	);
+		VAR rd: TextModels.Reader; ch0, ch1: CHAR; leftTerm, rightTerm: BOOLEAN;
+	BEGIN
+		rd := t.NewReader(NIL); ReadLeft(rd, pos, ch0); rd.ReadChar(ch1);
+		leftTerm := (ch0 <= " ") OR LeftTerminator(ch0);
+		rightTerm := (ch1 <= " ") OR RightTerminator(ch1, 0X);
+		touchL := ~leftTerm & rightTerm;
+		touchM := ~leftTerm & ~rightTerm;
+		touchR := leftTerm & ~rightTerm
+	END GetTargetField;
+
+	PROCEDURE LeftExtend (t: TextModels.Model; attr: TextModels.Attributes);
+		VAR wr: TextModels.Writer;
+	BEGIN
+		wr := t.NewWriter(NIL); wr.SetAttr(attr); wr.SetPos(0); wr.WriteChar(" ")
+	END LeftExtend;
+
+	PROCEDURE RightExtend (t: TextModels.Model; attr: TextModels.Attributes);
+		VAR wr: TextModels.Writer;
+	BEGIN
+		wr := t.NewWriter(NIL); wr.SetPos(t.Length()); wr.SetAttr(attr); wr.WriteChar(" ")
+	END RightExtend;
+
+	PROCEDURE MergeAdjust (target, inset: TextModels.Model; pos: INTEGER; OUT start: INTEGER);
+		VAR rd: TextModels.Reader; a: TextModels.Attributes; ch, ch1: CHAR;
+			touchL, touchM, touchR: BOOLEAN;
+	BEGIN
+		start := pos;
+(*
+disable intelligent paste for now
+		GetTargetField(target, pos, touchL, touchM, touchR);
+		IF touchL THEN
+			rd := inset.NewReader(NIL); rd.SetPos(0);
+			rd.ReadChar(ch); a := rd.attr; rd.ReadChar(ch1);
+			IF (ch > " ") & ~RightTerminator(ch, ch1) THEN LeftExtend(inset, a); INC(start) END
+		END;
+		IF touchR & (inset.Length() > 0) THEN
+			rd := inset.NewReader(rd); rd.SetPos(inset.Length() - 1); rd.ReadChar(ch);
+			IF (ch > " ") & ~LeftTerminator(ch) THEN RightExtend(inset, rd.attr) END
+		END
+*)
+	END MergeAdjust;
+
+
+	PROCEDURE InsertionAttr (c: StdCtrl): TextModels.Attributes;
+		VAR rd: TextModels.Reader; r: TextRulers.Ruler; a: TextModels.Attributes; ch: CHAR;
+	BEGIN
+		a := c.insAttr;
+		IF a = NIL THEN
+			rd := CachedReader(c); a := NIL;
+			IF c.carPos # none THEN
+				ReadLeft(rd, c.carPos, ch); a := rd.attr;
+				IF ((ch <= " ") OR (ch = TextModels.nbspace)) & (c.carPos < c.text.Length()) THEN
+					rd.ReadChar(ch);
+					IF ch > " " THEN a := rd.attr END
+				END
+			ELSIF boundCaret & (c.selBeg # c.selEnd) THEN
+				rd.SetPos(c.selBeg); rd.ReadChar(ch); a := rd.attr;
+				c.insAttr := a
+			END;
+			IF a = NIL THEN c.view.PollDefaults(r, a) END;
+			CacheReader(c, rd)
+		END;
+		RETURN a
+	END InsertionAttr;
+
+
+	PROCEDURE GetTargetRange (c: StdCtrl; OUT beg, end: INTEGER);
+	BEGIN
+		IF boundCaret & (c.selBeg # c.selEnd) THEN
+			beg := c.selBeg; end := c.selEnd
+		ELSE
+			beg := c.carPos; end := beg
+		END
+	END GetTargetRange;
+
+
+	PROCEDURE DoEdit (name: Stores.OpName;
+		c: StdCtrl; beg, end: INTEGER;
+		attr: TextModels.Attributes; ch: CHAR; view: Views.View; w, h: INTEGER;
+		buf: TextModels.Model; bufbeg, bufend: INTEGER;	(* buf # NIL & bufend < 0: bufend = buf.Length() *)
+		pos: INTEGER
+	);
+		VAR script: Stores.Operation; wr: TextModels.Writer; cluster: BOOLEAN;
+	BEGIN
+		IF (beg < end)	(* something to delete *)
+		OR (attr # NIL)	(* something new to write *)
+		OR (buf # NIL)	(* something new to insert *)
+		THEN
+			cluster := (beg < end) OR (attr = NIL) OR (view # NIL);
+			(* don't script when typing a single character -> TextModels will bunch if possible *)
+			(* ~cluster => name is reverted to #System.Inserting by TextModels *)
+			IF cluster THEN Models.BeginScript(c.text, name, script) END;
+			IF beg < end THEN
+				c.text.Delete(beg, end);
+				IF pos > beg THEN DEC(pos, end - beg) END
+			END;
+			IF attr # NIL THEN
+				ASSERT(buf = NIL, 20);
+				wr := CachedWriter(c, attr); wr.SetPos(pos);
+				IF view # NIL THEN wr.WriteView(view, w, h) ELSE wr.WriteChar(ch) END;
+				CacheWriter(c, wr)
+			ELSIF buf # NIL THEN
+				IF bufend < 0 THEN bufend := buf.Length() END;
+				c.text.Insert(pos, buf, bufbeg, bufend)
+			END;
+			IF cluster THEN Models.EndScript(c.text, script) END;
+			CheckCaret(c)
+		END
+	END DoEdit;
+
+
+	(* editing *)
+
+	PROCEDURE ThisPos (v: TextViews.View; f: Views.Frame; x, y: INTEGER): INTEGER;
+		VAR loc: TextViews.Location; pos: INTEGER;
+	BEGIN
+		pos := v.ThisPos(f, x, y); v.GetThisLocation(f, pos, loc);
+		IF (loc.view # NIL) & (x > (loc.l + loc.r) DIV 2) THEN INC(pos) END;
+		RETURN pos
+	END ThisPos;
+
+	PROCEDURE ShowPos (c: StdCtrl; beg, end: INTEGER);
+	BEGIN
+		IF ~(noAutoScroll IN c.opts) THEN
+			c.view.ShowRange(beg, end, TextViews.focusOnly)
+		END
+	END ShowPos;
+
+
+	PROCEDURE Indentation (c: StdCtrl; pos: INTEGER): TextModels.Model;
+	(* pre: c.carPos # none *)
+		VAR st: TextSetters.Setter; buf: TextModels.Model; rd: TextModels.Reader;
+			wr: TextModels.Writer; ch: CHAR; spos: INTEGER;
+	BEGIN
+		buf := NIL;
+		rd := CachedReader(c);
+		st := c.view.ThisSetter(); spos := st.ThisSequence(pos); rd.SetPos(spos); rd.ReadChar(ch);
+		IF (ch = tab) & (spos < pos) THEN
+			buf := TextModels.CloneOf(c.text); wr := buf.NewWriter(NIL); wr.SetPos(buf.Length());
+			wr.SetAttr(InsertionAttr(c));
+			wr.WriteChar(line);
+			REPEAT wr.WriteChar(tab); rd.ReadChar(ch) UNTIL (ch # tab) OR (rd.Pos() > pos)
+		END;
+		CacheReader(c, rd);
+		RETURN buf
+	END Indentation;
+
+	PROCEDURE InsertChar (c: StdCtrl; ch: CHAR);
+		VAR buf: TextModels.Model; attr: TextModels.Attributes;
+			beg, end: INTEGER; legal: BOOLEAN; name: Stores.OpName;
+	BEGIN
+		attr := NIL; buf := NIL;
+		IF ch < 100X THEN legal := LegalChar(ch) ELSE legal := TRUE END;	(* should check Unicode *)
+		IF (ch = ldel) OR (ch = rdel) THEN name := deletingKey ELSE name := replacingKey END;
+		IF boundCaret & (c.selBeg # c.selEnd) & (legal OR (ch = ldel) OR (ch = rdel) OR (ch = enter)) THEN
+			beg := c.selBeg; end := c.selEnd;
+			IF (ch = ldel) OR (ch = rdel) THEN SmartRange(c, beg, end); ch := 0X END
+		ELSE
+			beg := c.carPos; end := beg
+		END;
+		IF (c.carPos # none) OR boundCaret & (c.selBeg # c.selEnd) THEN
+			IF (ch = line) OR (ch = enter) THEN
+				IF noAutoIndent IN c.opts THEN buf := NIL ELSE buf := Indentation(c, beg) END;
+				IF buf = NIL THEN ch := line; legal := TRUE ELSE ch := 0X; legal := FALSE END
+			END;
+			IF legal THEN
+				attr := InsertionAttr(c)
+			ELSIF (ch = ldel) & (c.carPos > 0) THEN
+				beg := c.carPos - 1; end := c.carPos
+			ELSIF (ch = rdel) & (c.carPos < c.text.Length()) THEN
+				beg := c.carPos; end := c.carPos + 1
+			END
+		END;
+		DoEdit(name, c, beg, end, attr, ch, NIL, 0, 0, buf, 0, -1, beg)
+	END InsertChar;
+
+	PROCEDURE InsertText (c: StdCtrl; beg, end: INTEGER; text: TextModels.Model; OUT start: INTEGER);
+		VAR buf: TextModels.Model;
+	BEGIN
+		buf := TextModels.CloneOf(text); buf.InsertCopy(0, text, 0, text.Length());
+		IF beg = end THEN MergeAdjust(c.text, buf, beg, start) ELSE start := beg END;
+		DoEdit(insertingKey, c, beg, end, NIL, 0X, NIL, 0, 0, buf, 0, -1, beg)
+	END InsertText;
+
+	PROCEDURE InsertView (c: StdCtrl; beg, end: INTEGER; v: Views.View; w, h: INTEGER);
+	BEGIN
+		DoEdit(insertingKey, c, beg, end, InsertionAttr(c), 0X, v, w, h, NIL, 0, 0, beg)
+	END InsertView;
+
+
+	PROCEDURE InSubFrame (f, f1: Views.Frame; x, y: INTEGER): BOOLEAN;
+	BEGIN
+		INC(x, f.gx - f1.gx); INC(y, f.gy - f1.gy);
+		RETURN (f1.l <= x) & (x < f1.r) & (f1.t <= y) & (y < f1.b)
+	END InSubFrame;
+
+	PROCEDURE InFrame (f: Views.Frame; x, y: INTEGER): BOOLEAN;
+	BEGIN
+		RETURN (f.l <= x) & (x < f.r) & (f.t <= y) & (y < f.b)
+	END InFrame;
+
+
+	(* filtered tracking *)
+
+	PROCEDURE IsFilter (v: Views.View; c: StdCtrl; f: Views.Frame; x, y: INTEGER): BOOLEAN;
+		VAR pref: FilterPref;
+	BEGIN
+		pref.controller := c; pref.frame := f; pref.x := x; pref.y := y;
+		pref.filter := FALSE;
+		Views.HandlePropMsg(v, pref);
+		RETURN pref.filter
+	END IsFilter;
+
+	PROCEDURE FindFilter (c: StdCtrl; f: Views.Frame; x, y: INTEGER; OUT filter: Views.View);
+		CONST catchRange = 1000;
+		VAR rd: TextModels.Reader; pos, beg, end: INTEGER; isF: BOOLEAN;
+	BEGIN
+		c.view.GetRange(f, beg, end); DEC(beg, catchRange);
+		pos := c.view.ThisPos(f, x, y);
+		IF pos < c.text.Length() THEN INC(pos) END;	(* let filter handle itself *)
+		rd := CachedReader(c); rd.SetPos(pos);
+		REPEAT
+			rd.ReadPrevView(filter);
+			isF := (filter # NIL) & IsFilter(filter, c, f, x, y);
+		UNTIL isF OR rd.eot OR (rd.Pos() < beg);
+		IF ~isF THEN filter := NIL END;
+		CacheReader(c, rd)
+	END FindFilter;
+
+	PROCEDURE FilteredPollCursor (c: StdCtrl; f: Views.Frame;
+		VAR msg: Controllers.PollCursorMsg; VAR done: BOOLEAN
+	);
+		VAR filter, focus: Views.View; x, y: INTEGER; modifiers: SET; isDown: BOOLEAN; fmsg: FilterPollCursorMsg;
+	BEGIN
+		FindFilter(c, f, msg.x, msg.y, filter);
+		IF filter # NIL THEN
+			(* f.Input(x, y, modifiers, isDown); *)
+			fmsg.x := msg.x; fmsg.y := msg.y; fmsg.cursor := msg.cursor;
+			fmsg.controller := c; fmsg.done := FALSE;
+			(*Views.ForwardCtrlMsg(f, fmsg) - does not work f.view # filter !!*)
+			focus := NIL;
+			filter.HandleCtrlMsg(f, fmsg, focus);
+			IF fmsg.done THEN msg.cursor := fmsg.cursor END;
+			done := fmsg.done
+		END
+	END FilteredPollCursor;
+
+	PROCEDURE FilteredTrack (c: StdCtrl; f: Views.Frame;
+		VAR msg: Controllers.TrackMsg; VAR done: BOOLEAN
+	);
+		VAR filter, focus: Views.View; fmsg: FilterTrackMsg;
+	BEGIN
+		FindFilter(c, f, msg.x, msg.y, filter);
+		IF filter # NIL THEN
+			fmsg.x := msg.x; fmsg.y := msg.y; fmsg.modifiers := msg.modifiers;
+			fmsg.controller := c; fmsg.done := FALSE;
+			(*Views.ForwardCtrlMsg(f, fmsg) - does not work f.view # filter !!*)
+			focus := NIL; filter.HandleCtrlMsg(f, fmsg, focus);
+			done := fmsg.done
+		END
+	END FilteredTrack;
+
+
+	(* StdCtrl *)
+
+	PROCEDURE (c: StdCtrl) Internalize2 (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		c.Internalize2^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		InitMarks(c)
+	END Internalize2;
+
+	PROCEDURE (c: StdCtrl) Externalize2 (VAR wr: Stores.Writer);
+	BEGIN
+		c.Externalize2^(wr);
+		wr.WriteVersion(maxStdVersion)
+	END Externalize2;
+
+	PROCEDURE (c: StdCtrl) CopyFrom (source: Stores.Store);
+	BEGIN
+		c.CopyFrom^(source); InitMarks(c)
+	END CopyFrom;
+
+	PROCEDURE (c: StdCtrl) Neutralize2;
+	BEGIN
+		(* c.Neutralize^; *)
+		c.SetCaret(none)
+	END Neutralize2;
+
+	PROCEDURE (c: StdCtrl) GetContextType (OUT type: Stores.TypeName);
+	BEGIN
+		type := "TextViews.View"
+	END GetContextType;
+
+	PROCEDURE (c: StdCtrl) GetValidOps (OUT valid: SET);
+	BEGIN
+		valid := {};
+		IF (c.carPos # none) OR (boundCaret & (c.selBeg # c.selEnd)) THEN
+			valid := valid + {Controllers.pasteChar, Controllers.paste}
+		END;
+		IF c.selBeg # c.selEnd THEN
+			valid := valid + {Controllers.cut, Controllers.copy}
+		END
+	END GetValidOps;
+
+	PROCEDURE (c: StdCtrl) NativeModel (m: Models.Model): BOOLEAN;
+	BEGIN
+		ASSERT(m # NIL, 20);
+		RETURN m IS TextModels.Model
+	END NativeModel;
+
+	PROCEDURE (c: StdCtrl) NativeView (v: Views.View): BOOLEAN;
+	BEGIN
+		ASSERT(v # NIL, 20);
+		RETURN v IS TextViews.View
+	END NativeView;
+
+	PROCEDURE (c: StdCtrl) NativeCursorAt (f: Views.Frame; x, y: INTEGER): INTEGER;
+	BEGIN
+		RETURN Ports.textCursor
+	END NativeCursorAt;
+
+	PROCEDURE (c: StdCtrl) PollNativeProp (selection: BOOLEAN;
+		VAR p: Properties.Property; VAR truncated: BOOLEAN
+	);
+		VAR beg, end: INTEGER;
+	BEGIN
+		IF selection & (c.selBeg = c.selEnd) THEN
+			p := InsertionAttr(c).Prop(); truncated := FALSE
+		ELSE
+			IF selection THEN beg := c.selBeg; end := c.selEnd
+			ELSE beg := 0; end := c.text.Length()
+			END;
+(*
+			truncated := (end - beg > lenCutoff);
+			IF truncated THEN end := beg + lenCutoff END;
+*)
+			p := c.text.Prop(beg, end)
+		END
+	END PollNativeProp;
+
+	PROCEDURE (c: StdCtrl) SetNativeProp (selection: BOOLEAN; old, p: Properties.Property);
+		VAR t: TextModels.Model; beg, end: INTEGER;
+	BEGIN
+		t := c.text;
+		IF selection THEN beg := c.selBeg; end := c.selEnd ELSE beg := 0; end := t.Length() END;
+		IF beg < end THEN
+			t.Modify(beg, end, old, p);
+			IF selection THEN c.SetSelection(beg, end) END
+		ELSIF selection THEN
+			c.insAttr := TextModels.ModifiedAttr(InsertionAttr(c), p)
+		END
+	END SetNativeProp;
+
+	PROCEDURE (c: StdCtrl) MakeViewVisible (v: Views.View);
+		VAR pos: INTEGER;
+	BEGIN
+		ASSERT(v # NIL, 20);
+		ASSERT(v.context # NIL, 21);
+		ASSERT(v.context.ThisModel() = c.text, 22);
+		pos := v.context(TextModels.Context).Pos();
+		ShowPos(c, pos, pos + 1)
+	END MakeViewVisible;
+
+	PROCEDURE (c: StdCtrl) GetFirstView (selection: BOOLEAN; OUT v: Views.View);
+		VAR rd: TextModels.Reader; beg, end: INTEGER;
+	BEGIN
+		IF selection THEN beg := c.selBeg; end := c.selEnd
+		ELSE beg := 0; end := c.text.Length()
+		END;
+		IF beg < end THEN
+			rd := CachedReader(c); rd.SetPos(beg); rd.ReadView(v);
+			IF rd.Pos() > end THEN v := NIL END;
+			CacheReader(c, rd)
+		ELSE v := NIL
+		END
+	END GetFirstView;
+
+	PROCEDURE (c: StdCtrl) GetNextView (selection: BOOLEAN; VAR v: Views.View);
+		VAR con: Models.Context; rd: TextModels.Reader; beg, end, pos: INTEGER;
+	BEGIN
+		ASSERT(v # NIL, 20); con := v.context;
+		ASSERT(con # NIL, 21); ASSERT(con.ThisModel() = c.text, 22);
+		IF selection THEN beg := c.selBeg; end := c.selEnd
+		ELSE beg := 0; end := c.text.Length()
+		END;
+		pos := con(TextModels.Context).Pos();
+		IF (beg <= pos) & (pos < end) THEN
+			rd := CachedReader(c); rd.SetPos(pos + 1); rd.ReadView(v);
+			IF rd.Pos() > end THEN v := NIL END;
+			CacheReader(c, rd)
+		ELSE v := NIL
+		END
+	END GetNextView;
+
+	PROCEDURE (c: StdCtrl) GetPrevView (selection: BOOLEAN; VAR v: Views.View);
+		VAR con: Models.Context; rd: TextModels.Reader; beg, end, pos: INTEGER;
+	BEGIN
+		ASSERT(v # NIL, 20); con := v.context;
+		ASSERT(con # NIL, 21); ASSERT(con.ThisModel() = c.text, 22);
+		IF selection THEN beg := c.selBeg; end := c.selEnd
+		ELSE beg := 0; end := c.text.Length()
+		END;
+		pos := con(TextModels.Context).Pos();
+		IF (beg < pos) & (pos <= end) THEN
+			rd := CachedReader(c); rd.SetPos(pos); rd.ReadPrevView(v);
+			IF rd.Pos() < beg THEN v := NIL END;
+			CacheReader(c, rd)
+		ELSE v := NIL
+		END
+	END GetPrevView;
+
+	PROCEDURE (c: StdCtrl) GetSelectionBounds (f: Views.Frame; OUT x, y, w, h: INTEGER);
+		VAR b, e: TextViews.Location;
+	BEGIN
+		c.GetSelectionBounds^(f, x, y, w, h);
+		IF w = Views.undefined THEN
+			c.view.GetThisLocation(f, c.selBeg, b);
+			c.view.GetThisLocation(f, c.selEnd, e);
+			IF b.start = e.start THEN x := b.x; w := e.x - b.x;
+			ELSE x := f.l; w := f.r - f.l;
+			END;
+			y := b.y; h := e.y + e.asc + e.dsc - b.y
+		END
+	END GetSelectionBounds;
+
+	PROCEDURE (c: StdCtrl) MarkPickTarget (source, f: Views.Frame;
+		sx, sy, x, y: INTEGER; show: BOOLEAN
+	);
+		VAR b, e: TextViews.Location; pos: INTEGER;
+	BEGIN
+		pos := c.view.ThisPos(f, x, y);
+		IF pos < c.text.Length() THEN
+			c.view.GetThisLocation(f, pos, b);
+			c.view.GetThisLocation(f, pos + 1, e);
+			IF (b.pos < e.pos) OR (b.pos = e.pos) & (b.x < e.x) THEN
+				MarkSelRange(c, f, b, e, TRUE, show)
+			END
+		END
+	END MarkPickTarget;
+
+	PROCEDURE (c: StdCtrl) MarkDropTarget (source, f: Views.Frame;
+		sx, sy, dx, dy, w, h, rx, ry: INTEGER; type: Stores.TypeName; isSingle, show: BOOLEAN
+	);
+		VAR loc: TextViews.Location; pos: INTEGER;
+	BEGIN
+		pos := c.view.ThisPos(f, dx, dy);
+		IF (source # NIL) & ((source.view = f.view) OR (source.view.ThisModel() = f.view.ThisModel()))
+		& (c.selBeg < pos) & (pos < c.selEnd) THEN
+			pos := c.selBeg
+		END;
+		c.view.GetThisLocation(f, pos, loc);
+		f.MarkRect(loc.x, loc.y, loc.x + f.unit, loc.y + loc.asc + loc.dsc, Ports.fill, Ports.invert, show);
+		IF (isSingle OR ~Services.Extends(type, "TextViews.View")) & (w > 0) & (h > 0) THEN
+			DEC(dx, rx); DEC(dy, ry);
+			f.MarkRect(dx, dy, dx + w, dy + h, 0, Ports.dim25, show)
+		END
+	END MarkDropTarget;
+
+
+	PROCEDURE GetThisLine (c: StdCtrl; pos: INTEGER; OUT beg, end: INTEGER);
+		VAR st: TextSetters.Setter;
+	BEGIN
+		st := c.view.ThisSetter();
+		beg := st.ThisLine(pos); end := st.NextLine(beg);
+		IF end = beg THEN end := c.text.Length() END;
+	END GetThisLine;
+
+	PROCEDURE GetThisChunk (c: StdCtrl; f: Views.Frame;
+		VAR s: TrackState; OUT beg, end: INTEGER; OUT mode: INTEGER
+	);
+		VAR v: TextViews.View; b, e: TextViews.Location;
+			st: TextSetters.Setter; ruler: TextRulers.Ruler; ra: TextRulers.Attributes;
+			pos, r: INTEGER;
+	BEGIN
+		v := c.view; st := v.ThisSetter(); pos := ThisPos(v, f, s.x, s.y);
+		ruler := TextViews.ThisRuler(v, pos); ra := ruler.style.attr;
+		r := ra.right; IF ~(TextRulers.rightFixed IN ra.opts) OR (r > f.r) THEN r := f.r END;
+		st.GetWord(pos, beg, end);
+		v.GetThisLocation(f, beg, b); v.GetThisLocation(f, end, e);
+		IF (s.x < f.l) OR (s.x >= r) THEN	(* outside of line box: whole line *)
+			GetThisLine(c, pos, beg, end);
+			mode := lines
+		ELSIF (s.y < b.y) OR (s.y < b.y + b.asc + b.dsc) & (s.x < b.x)
+		OR (s.y >= e.y) & (s.x >= e.x) OR (s.y >= e.y + e.asc + e.dsc) THEN
+			(* outside of word: single char *)
+			beg := ThisPos(v, f, s.x, s.y); v.GetThisLocation(f, beg, b);
+			IF (b.x > s.x) & (beg > 0) THEN DEC(beg) END;
+			IF beg < c.text.Length() THEN end := beg + 1 ELSE end := beg END;
+			mode := words
+		ELSE	(* whole word *)
+			mode := words
+		END
+	END GetThisChunk;
+
+	PROCEDURE SetSel (c: StdCtrl; beg, end: INTEGER);
+	(* pre: ~(Containers.noSelection IN c.opts) *)
+	BEGIN
+		IF beg >= end THEN c.SetCaret(beg) ELSE c.SetSelection(beg, end) END
+	END SetSel;
+
+	PROCEDURE PrepareToTrack (c: StdCtrl; f: Views.Frame;
+		VAR s: TrackState; mode: INTEGER;
+		VAR pin0, pin1, pos: INTEGER
+	);
+		VAR loc: TextViews.Location; beg, end: INTEGER; m: INTEGER;
+	BEGIN
+		pos := ThisPos(c.view, f, s.x, s.y);
+		IF mode IN {chars, words, lines} THEN
+			GetThisChunk(c, f, s, pin0, pin1, m)
+		ELSE pin0 := pos; pin1 := pos
+		END;
+		IF s.toggle & ((c.selBeg # c.selEnd) OR boundCaret & (c.carPos # none))
+		& ~(Containers.noSelection IN c.opts) THEN	(* modify existing selection *)
+			IF c.selBeg # c.selEnd THEN
+				beg := c.selBeg; end := c.selEnd
+			ELSE
+				beg := c.carPos; end := beg; c.selPin0 := beg; c.selPin1 := beg
+			END;
+			IF pin1 > c.selPin0 THEN
+				end := pin1; pin0 := beg
+			ELSIF pin0 < c.selPin1 THEN
+				beg := pin0; pin0 := end
+			END;
+			SetSel(c, beg, end);
+			pin1 := pin0
+		ELSIF mode IN {chars, words, lines} THEN
+			SetSel(c, pin0, pin1);
+			pos := pin1
+		ELSE
+			SetCaret(c.text, pos)
+		END;
+		c.lastStep := Services.Ticks()
+	END PrepareToTrack;
+
+	PROCEDURE ScrollDelay (d: INTEGER): INTEGER;
+		VAR second, delay: INTEGER;
+	BEGIN
+		second := Services.resolution;
+		IF d < 2 * mm THEN delay := second DIV 2
+		ELSIF d < 4 * mm THEN delay := second DIV 3
+		ELSIF d < 6 * mm THEN delay := second DIV 5
+		ELSIF d < 8 * mm THEN delay := second DIV 10
+		ELSE delay := second DIV 20
+		END;
+		RETURN delay
+	END ScrollDelay;
+
+	PROCEDURE ScrollWhileTracking (c: StdCtrl; f: Views.Frame; VAR x0, y0, x, y: INTEGER);
+	(* currently, there are no provisions to scroll while tracking inside an embedded view *)
+		VAR now: LONGINT; (* normalize: BOOLEAN; *) scr: Controllers.ScrollMsg;
+	BEGIN
+		(* normalize := c.view.context.Normalize(); *)
+		now := Services.Ticks();
+		IF x < f.l THEN x0 := x; x := f.l ELSIF x > f.r THEN x0 := x; x := f.r END;
+		IF (y < f.t) (* & normalize*) THEN
+			IF c.lastStep + ScrollDelay(f.t - y) <= now THEN
+				c.lastStep := now;
+				scr.focus := TRUE; scr.vertical := TRUE; scr.op := Controllers.decLine;
+				scr.done := FALSE;
+				Controllers.ForwardVia(Controllers.frontPath, scr)
+			END
+		ELSIF (y > f.b) (* & normalize *) THEN
+			IF c.lastStep + ScrollDelay(y - f.b) <= now THEN
+				c.lastStep := now;
+				scr.focus := TRUE; scr.vertical := TRUE; scr.op := Controllers.incLine;
+				scr.done := FALSE;
+				Controllers.ForwardVia(Controllers.frontPath, scr)
+			END
+		ELSE
+			y0 := y
+		END
+	END ScrollWhileTracking;
+
+	PROCEDURE (c: StdCtrl) TrackMarks (f: Views.Frame; x, y: INTEGER; units, extend, add: BOOLEAN);
+		VAR s: TrackState; pos, beg, end, pin0, pin1, p, p1: INTEGER;
+			modifiers: SET; mode, m: INTEGER; isDown, noSel: BOOLEAN;
+	BEGIN
+		IF c.opts * Containers.mask # Containers.mask THEN	(* track caret or selection *)
+			s.x := x; s.y := y; s.toggle := extend;
+			noSel := Containers.noSelection IN c.opts;
+			IF units & ~noSel THEN	(* select units, i.e. words or lines *)
+				GetThisChunk(c, f, s, beg, end, mode)
+			ELSE	(* set caret or selection *)
+				mode := none
+			END;
+			PrepareToTrack(c, f, s, mode, pin0, pin1, p); x := s.x; y := s.y;
+			beg := pin0; end := pin1;
+			IF p < pin0 THEN beg := p ELSIF p > pin1 THEN end := p END;
+			p := -1;
+			f.Input(s.x, s.y, modifiers, isDown);
+			WHILE isDown DO
+(*
+			REPEAT
+				f.Input(s.x, s.y, modifiers, isDown);
+*)
+				IF (s.x # x) OR (s.y # y) THEN
+					ScrollWhileTracking(c, f, x, y, s.x, s.y);
+					p1 := ThisPos(c.view, f, s.x, s.y);
+					IF p1 # p THEN
+						p := p1;
+						IF mode IN {words, lines} THEN
+							IF mode = words THEN
+								GetThisChunk(c, f, s, beg, end, m)
+							ELSE
+								GetThisLine(c, p, beg, end)
+							END;
+							IF p > pin0 THEN pos := end ELSE pos := beg END
+						ELSE pos := p
+						END;
+						beg := pin0; end := pin1;
+						IF noSel THEN
+							c.SetCaret(pos)
+						ELSE
+							IF pos < pin0 THEN beg := pos ELSIF pos > pin1 THEN end := pos END;
+							SetSel(c, beg, end);
+							IF c.selPin0 = c.selPin1 THEN
+								IF pos < pin0 THEN c.selPin0 := pos; c.selPin1 := pin1
+								ELSIF pos > pin1 THEN c.selPin0 := pin0; c.selPin1 := pos
+								END
+							END
+						END
+					END
+				END;
+				f.Input(s.x, s.y, modifiers, isDown)
+			END
+(*
+			UNTIL ~isDown
+*)
+		END
+	END TrackMarks;
+
+	PROCEDURE (c: StdCtrl) Resize (v: Views.View; l, t, r, b: INTEGER);
+		VAR con: Models.Context;
+	BEGIN
+		ASSERT(v # NIL, 20); con := v.context;
+		ASSERT(con # NIL, 21); ASSERT(con.ThisModel() = c.text, 22);
+		con.SetSize(r - l, b - t)
+	END Resize;
+
+	PROCEDURE (c: StdCtrl) DeleteSelection;
+		VAR beg, end: INTEGER;
+	BEGIN
+		beg := c.selBeg; end := c.selEnd;
+		IF beg # end THEN
+			SmartRange(c, beg, end);
+			DoEdit(deletingKey, c, beg, end, NIL, 0X, NIL, 0, 0, NIL, 0, 0, 0)
+		END
+	END DeleteSelection;
+
+	PROCEDURE (c: StdCtrl) MoveLocalSelection (f, dest: Views.Frame; x, y, dx, dy: INTEGER);
+		VAR buf: TextModels.Model; pos, beg0, end0, beg, end, start, len: INTEGER;
+	BEGIN
+		pos := dest.view(TextViews.View).ThisPos(dest, dx, dy);
+(* smart move disabled for now --> use true move instead of copy
+		beg0 := c.selBeg; end0 := c.selEnd; beg := beg0; end := end0;
+		SmartRange(c, beg, end);
+		IF (beg < pos) & (pos < end) THEN pos := beg END;
+		buf := TextModels.CloneOf(c.text); buf.CopyFrom(0, c.text, beg0, end0);
+		IF OnlyWords(c, beg0, end0) THEN MergeAdjust(c.text, buf, pos, start) ELSE start := pos END;
+		len := end0 - beg0;
+		IF start >= end THEN DEC(start, end - beg) END;
+		IF pos # beg THEN
+			DoEdit(movingKey, c, beg, end, NIL, 0X, NIL, 0, 0, buf, pos);
+			SetSelection(c.text, start, start + len);
+			AutoShowRange(c, start, start + len)
+		END
+*)
+		beg := c.selBeg; end := c.selEnd;
+		IF (pos < beg) OR (pos > end) THEN
+			len := end - beg; start := pos;
+			IF start >= end THEN DEC(start, len) END;
+			DoEdit(movingKey, c, 0, 0, NIL, 0X, NIL, 0, 0, c.text, beg, end, pos);
+			SetSelection(c.text, start, start + len);
+			AutoShowRange(c, start, start + len)
+		END
+	END MoveLocalSelection;
+
+	PROCEDURE (c: StdCtrl) CopyLocalSelection (f, dest: Views.Frame; x, y, dx, dy: INTEGER);
+		VAR buf: TextModels.Model; pos, beg, end, start, len: INTEGER;
+	BEGIN
+		pos := dest.view(TextViews.View).ThisPos(dest, dx, dy);
+		beg := c.selBeg; end := c.selEnd;
+		IF (beg < pos) & (pos < end) THEN pos := beg END;
+		buf := TextModels.CloneOf(c.text); buf.InsertCopy(0, c.text, beg, end);
+		IF OnlyWords(c, beg, end) THEN MergeAdjust(c.text, buf, pos, start) ELSE start := pos END;
+		len := end - beg;
+		DoEdit(copyingKey, c, 0, 0, NIL, 0X, NIL, 0, 0, buf, 0, -1, pos);
+		SetSelection(c.text, start, start + len);
+		AutoShowRange(c, start, start + len)
+	END CopyLocalSelection;
+
+	PROCEDURE (c: StdCtrl) SelectionCopy (): Containers.Model;
+		VAR t: TextModels.Model;
+	BEGIN
+		IF c.selBeg # c.selEnd THEN
+			t := TextModels.CloneOf(c.text); t.InsertCopy(0, c.text, c.selBeg, c.selEnd);
+		ELSE t := NIL
+		END;
+		RETURN t
+	END SelectionCopy;
+
+	PROCEDURE (c: StdCtrl) NativePaste (m: Models.Model; f: Views.Frame);
+		VAR beg, end, start: INTEGER;
+	BEGIN
+		WITH m: TextModels.Model DO
+			GetTargetRange(c, beg, end);
+			IF beg # none THEN InsertText(c, beg, end, m, start) END
+		END
+	END NativePaste;
+
+	PROCEDURE (c: StdCtrl) ArrowChar (f: Views.Frame; ch: CHAR; units, select: BOOLEAN);
+		VAR st: TextSetters.Setter; v: TextViews.View; loc: TextViews.Location;
+			org, len,  p, pos,  b, e,  beg, end,  d, d0, edge,  x, dy: INTEGER;
+			change, rightEdge, rightDir: BOOLEAN;
+			scroll: Controllers.ScrollMsg;
+	BEGIN
+		c.insAttr := NIL;
+		Models.StopBunching(c.text);
+		v := c.view; st := v.ThisSetter();
+		change := select OR (c.selBeg = c.selEnd);
+		IF c.selBeg # c.selEnd THEN beg := c.selBeg; end := c.selEnd
+		ELSE beg := c.carPos; end := beg; c.carLast := beg
+		END;
+		len := c.text.Length();
+		rightDir := (ch = aR) OR (ch = pR) OR (ch = dR) OR (ch = aD) OR (ch = pD) OR (ch = dD);
+		rightEdge := 	change & (c.carLast < end)
+							OR rightDir & (~change OR (beg = end) & (c.carLast = end));
+		IF rightEdge THEN edge := end ELSE edge := beg END;
+		ShowPos(c, edge, edge);
+		b := beg; e := end; d := edge; d0 := edge;
+		CASE ch OF
+		| aL:
+			IF units THEN
+				p := d; e := d;
+				WHILE (p > 0) & ((edge = d) OR (edge = e)) DO DEC(p); st.GetWord(p, edge, e) END;
+			ELSIF change THEN DEC(edge)
+			END
+		| pL, dL:
+			v.GetThisLocation(f, edge, loc); edge := loc.start
+		| aR:
+			IF units THEN
+				p := d; e := edge;
+				WHILE (p < len) & ((edge <= d) OR (edge = e)) DO INC(p); st.GetWord(p, edge, e) END
+			ELSIF change THEN INC(edge)
+			END
+		| pR, dR:
+			v.GetThisLocation(f, edge, loc); p := st.NextLine(loc.start);
+			IF p = loc.start THEN p := len ELSE DEC(p) END;
+			IF p > edge THEN edge := p END
+		| aU:
+			IF units THEN
+				p := st.ThisSequence(edge);
+				IF p < edge THEN edge := p ELSE edge := st.PreviousSequence(edge) END
+			ELSE
+				v.PollOrigin(org, dy); v.GetThisLocation(f, edge, loc);
+				IF c.lastX >= 0 THEN x := c.lastX ELSE x := loc.x END;
+				c.carX := x;
+				IF loc.start > 0 THEN
+					edge := v.ThisPos(f, x, loc.y - 1);
+					IF (edge >= loc.start) & (org > 0) THEN
+						v.SetOrigin(org - 1, 0);
+						v.GetThisLocation(f, edge, loc);
+						edge := v.ThisPos(f, x, loc.y - 1)
+					END
+				END
+			END
+		| pU:
+			v.PollOrigin(org, dy);
+			IF edge > org THEN edge := org
+			ELSIF org > 0 THEN
+				scroll.focus := TRUE; scroll.vertical := TRUE; scroll.op := Controllers.decPage;
+				scroll.done := FALSE;
+				Views.ForwardCtrlMsg(f, scroll);
+				v.PollOrigin(edge, dy)
+			END
+		| dU:
+			edge := 0
+		| aD:
+			IF units THEN
+				p := st.NextSequence(st.ThisSequence(edge));
+				IF p > edge THEN edge := p ELSE edge := st.NextSequence(p) END
+			ELSE
+				v.GetThisLocation(f, edge, loc);
+				IF c.lastX >= 0 THEN x := c.lastX ELSE x := loc.x END;
+				c.carX := x;
+				edge := v.ThisPos(f, x, loc.y + loc.asc + loc.dsc + 1)
+			END
+		| pD:
+			v.GetRange(f, b, e);
+			IF e < len THEN
+				scroll.focus := TRUE; scroll.vertical := TRUE; scroll.op := Controllers.incPage;
+				scroll.done := FALSE;
+				Views.ForwardCtrlMsg(f, scroll);
+				v.GetRange(f, edge, e)
+			ELSE edge := len
+			END
+		| dD:
+			edge := len
+		END;
+		IF rightEdge THEN end := edge ELSE beg := edge END;
+		IF ~select THEN
+			IF rightDir THEN beg := edge ELSE end := edge END
+		END;
+		IF beg < 0 THEN beg := 0 ELSIF beg > len THEN beg := len END;
+		IF end < beg THEN end := beg ELSIF end > len THEN end := len END;
+		IF beg = end THEN
+			ShowPos(c, beg, end)
+		ELSE
+			IF rightEdge THEN ShowPos(c, end - 1, end) ELSE ShowPos(c, beg, beg + 1) END
+		END;
+		SetSel(c, beg, end)
+	END ArrowChar;
+
+	PROCEDURE (c: StdCtrl) ControlChar (f: Views.Frame; ch: CHAR);
+	BEGIN
+		InsertChar(c, ch)
+	END ControlChar;
+
+	PROCEDURE (c: StdCtrl) PasteChar (ch: CHAR);
+	BEGIN
+		InsertChar(c, ch)
+	END PasteChar;
+
+	PROCEDURE (c: StdCtrl) PasteView (f: Views.Frame; v: Views.View; w, h: INTEGER);
+		VAR t: TextModels.Model; pos, start, beg, end, len: INTEGER;
+	BEGIN
+		GetTargetRange(c, beg, end);
+		IF beg # none THEN InsertView(c, beg, end, v, w, h) END
+	END PasteView;
+
+	PROCEDURE (c: StdCtrl) Drop (src, f: Views.Frame; sx, sy, x, y, w, h, rx, ry: INTEGER;
+		v: Views.View; isSingle: BOOLEAN
+	);
+		VAR t: TextModels.Model; pos, start, beg, end, len: INTEGER;
+	BEGIN
+		pos := ThisPos(c.view, f, x, y);
+		WITH v: TextViews.View DO t := v.ThisModel() ELSE t := NIL END;
+		IF (t # NIL) & ~isSingle THEN
+			InsertText(c, pos, pos, t, start); len := t.Length()
+		ELSE
+			InsertView(c, pos, pos, v, w, h); start := pos; len := 1
+		END;
+		SetSelection(c.text, start, start + len);
+		AutoShowRange(c, start, start + len)
+	END Drop;
+
+	PROCEDURE (c: StdCtrl) PickNativeProp (f: Views.Frame; x, y: INTEGER; VAR p: Properties.Property);
+		VAR rd: TextModels.Reader;
+	BEGIN
+		rd := CachedReader(c); rd.SetPos(ThisPos(c.view, f, x, y)); rd.Read;
+		IF ~rd.eot THEN p := rd.attr.Prop() ELSE p := NIL END;
+		CacheReader(c, rd)
+	END PickNativeProp;
+
+	PROCEDURE (c: StdCtrl) HandleModelMsg (VAR msg: Models.Message);
+		VAR done: BOOLEAN;
+	BEGIN
+		c.HandleModelMsg^(msg);
+		IF msg.model = c.text THEN
+			WITH msg: Models.UpdateMsg DO
+				WITH msg: TextModels.UpdateMsg DO
+					CASE msg.op OF
+					  TextModels.insert, TextModels.delete, TextModels.replace:
+						UpdateMarks(c, msg.op, msg.beg, msg.end, msg.delta)
+					ELSE	(* unknown text op happened *)
+						c.view.Neutralize
+					END
+				ELSE	(* unknown text update happened *)
+					c.view.Neutralize
+				END
+			| msg: ModelMessage DO
+				WITH msg: SetCaretMsg DO
+					c.SetCaret(msg.pos)
+				| msg: SetSelectionMsg DO
+					c.SetSelection(msg.beg, msg.end)
+				ELSE
+				END
+			ELSE
+			END
+		END
+	END HandleModelMsg;
+
+	PROCEDURE (c: StdCtrl) HandleViewMsg (f: Views.Frame; VAR msg: Views.Message);
+	BEGIN
+		c.HandleViewMsg^(f, msg);
+		IF msg.view = c.view THEN
+			WITH msg: ViewMessage DO
+				WITH msg: CaretMsg DO
+					c.MarkCaret(f, msg.show)
+				| msg: SelectionMsg DO
+					MarkSelection(c, f, msg.beg, msg.end, msg.show)
+				END
+			ELSE
+			END
+		END
+	END HandleViewMsg;
+
+	PROCEDURE (c: StdCtrl) HandleCtrlMsg (f: Views.Frame;
+		VAR msg: Controllers.Message; VAR focus: Views.View
+	);
+		VAR g: Views.Frame; beg, end: INTEGER; done: BOOLEAN;
+	BEGIN
+		IF (msg IS Controllers.MarkMsg) OR (msg IS Controllers.TickMsg) THEN
+			beg := c.autoBeg; end := c.autoEnd;
+			c.autoBeg := MAX(INTEGER); c.autoEnd := 0
+		END;
+		WITH msg: Controllers.TickMsg DO
+			IF ~(noAutoScroll IN c.opts)
+				& (0 <= beg) & (beg <= end) & (end <= c.text.Length())
+				& c.view.context.Normalize()
+			THEN
+				c.view.ShowRange(beg, end, TextViews.focusOnly)
+			END;
+			IF focus = NIL THEN
+				CheckCaret(c); BlinkCaret(c, f, msg.tick);
+				IF (c.selBeg # c.aliasSelBeg) OR (c.selEnd # c.aliasSelEnd) THEN
+					(* lazy update of text-synchronous alias marks *)
+					c.aliasSelBeg := c.selBeg; c.aliasSelEnd := c.selEnd;
+					SetSelection(c.text, c.selBeg, c.selEnd)
+				END
+			END
+		| msg: Controllers.MarkMsg DO
+			c.carX := -1;
+			IF msg.show THEN c.carVisible := TRUE; c.carTick := 0 END
+		| msg: Controllers.TrackMsg DO
+			c.insAttr := NIL; c.carX := -1; Models.StopBunching(c.text)
+		| msg: Controllers.EditMsg DO
+			c.lastX := c.carX; c.carX := -1;
+			IF focus = NIL THEN CheckCaret(c) END
+		| msg: Controllers.ReplaceViewMsg DO
+			c.carX := -1
+		| msg: Controllers.TransferMessage DO
+			c.carX := -1
+		| msg: Properties.EmitMsg DO
+			c.carX := -1
+		ELSE
+		END;
+		done := FALSE;
+		WITH msg: Controllers.CursorMessage DO
+			IF TRUE (* Containers.noCaret IN c.opts *) THEN	(* mask or browser mode *)
+				g := Views.FrameAt(f, msg.x, msg.y);
+				IF (g = NIL) OR IsFilter(g.view, c, f, msg.x, msg.y) THEN
+					WITH msg: Controllers.PollCursorMsg DO
+						FilteredPollCursor(c, f, msg, done)
+					| msg: Controllers.TrackMsg DO
+						FilteredTrack(c, f, msg, done)
+					ELSE
+					END
+				END
+			END
+		ELSE
+		END;
+		IF ~done THEN c.HandleCtrlMsg^(f, msg, focus) END
+	END HandleCtrlMsg;
+
+
+	(* caret *)
+
+	PROCEDURE (c: StdCtrl) HasCaret (): BOOLEAN;
+	BEGIN
+		RETURN c.carPos # none
+	END HasCaret;
+
+	PROCEDURE (c: StdCtrl) MarkCaret (f: Views.Frame; show: BOOLEAN);
+		CONST carW = 1; carMinH = 7;	(* in frame dots *)
+		VAR loc: TextViews.Location; pos, beg, end,  u, x, y, w, h: INTEGER; fm: INTEGER;
+	BEGIN
+		pos := c.carPos;
+		IF (pos # none) & f.mark & (f.front & c.carVisible OR ~f.front) THEN
+			c.view.GetRange(f, beg, end);
+			IF (beg <= pos) & (pos <= end) THEN
+				u := f.dot;
+				c.view.GetThisLocation(f, pos, loc);
+				IF f.front THEN fm := Ports.invert ELSE fm := Ports.dim50 END;
+				x := loc.x; y := loc.y; h := loc.asc + loc.dsc;
+				IF Dialog.thickCaret THEN w := 2 * carW * u ELSE w := carW * u END;
+				IF x >= f.r - w THEN DEC(x, w) END;
+				IF h < carMinH * u THEN h := carMinH * u END;	(* special caret in lines of (almost) zero height *)
+				f.MarkRect(x, y, x + w, y + h, Ports.fill, fm, show)
+			END
+		END
+	END MarkCaret;
+
+	PROCEDURE (c: StdCtrl) CaretPos (): INTEGER;
+	BEGIN
+		RETURN c.carPos
+	END CaretPos;
+
+	PROCEDURE (c: StdCtrl) SetCaret (pos: INTEGER);
+	BEGIN
+		ASSERT(none <= pos, 20); ASSERT(pos <= c.text.Length(), 21);
+		c.insAttr := NIL;
+		IF pos # c.carPos THEN
+			IF (pos # none) & (c.carPos = none) THEN
+				IF boundCaret THEN c.SetSelection(none, none) END;
+				c.SetFocus(NIL)
+			END;
+
+			IF Containers.noCaret IN c.opts THEN pos := none END;
+			IF c.carPos # none THEN
+				c.carLast := c.carPos; FlipCaret(c, Containers.hide)
+			END;
+			c.carPos := pos;
+			IF pos # none THEN
+				c.carVisible := TRUE; c.carTick := Services.Ticks() + Dialog.caretPeriod; FlipCaret(c, Containers.show)
+			END
+		END
+	END SetCaret;
+
+
+	(* selection *)
+
+	PROCEDURE (c: StdCtrl) HasSelection (): BOOLEAN;
+	BEGIN
+		RETURN c.selBeg # c.selEnd
+	END HasSelection;
+
+	PROCEDURE (c: StdCtrl) Selectable (): BOOLEAN;
+	BEGIN
+		RETURN c.text.Length() > 0
+	END Selectable;
+
+	PROCEDURE (c: StdCtrl) SetSingleton (s: Views.View);
+		VAR s0: Views.View;
+	BEGIN
+		s0 := c.Singleton();
+		c.SetSingleton^(s);
+		s := c.Singleton();
+		IF s # s0 THEN
+			c.insAttr := NIL;
+			IF s # NIL THEN
+				c.selBeg := s.context(TextModels.Context).Pos(); c.selEnd := c.selBeg + 1;
+				c.selPin0 := c.selBeg; c.selPin1 := c.selEnd
+			ELSE c.selBeg := none; c.selEnd := none
+			END
+		END
+	END SetSingleton;
+
+	PROCEDURE (c: StdCtrl) SelectAll (select: BOOLEAN);
+	(** extended by subclass to include intrinsic selections **)
+	BEGIN
+		IF select THEN c.SetSelection(0, c.text.Length()) ELSE c.SetSelection(none, none) END
+	END SelectAll;
+
+	PROCEDURE (c: StdCtrl) InSelection (f: Views.Frame; x, y: INTEGER): BOOLEAN;
+	(* pre: c.selBeg # c.selEnd *)
+	(* post: (x, y) in c.selection *)
+		VAR b, e: TextViews.Location; y0, y1, y2, y3: INTEGER;
+	BEGIN
+		c.view.GetThisLocation(f, c.selBeg, b); y0 := b.y; y1 := y0 + b.asc + b.dsc;
+		c.view.GetThisLocation(f, c.selEnd, e); y2 := e.y; y3 := y2 + e.asc + e.dsc;
+		RETURN ((y >= y0) & (x >= b.x) OR (y >= y1)) & ((y < y2) OR (y < y3) & (x < e.x))
+	END InSelection;
+
+	PROCEDURE (c: StdCtrl) MarkSelection (f: Views.Frame; show: BOOLEAN);
+	BEGIN
+		MarkSelection(c, f, c.selBeg, c.selEnd, show)
+	END MarkSelection;
+
+	PROCEDURE (c: StdCtrl) GetSelection (OUT beg, end: INTEGER);
+	BEGIN
+		beg := c.selBeg; end := c.selEnd
+	END GetSelection;
+
+	PROCEDURE (c: StdCtrl) SetSelection (beg, end: INTEGER);
+		VAR t: TextModels.Model; rd: TextModels.Reader;
+			beg0, end0, p: INTEGER; singleton: BOOLEAN;
+	BEGIN
+		t := c.text; ASSERT(t # NIL, 20);
+		IF Containers.noSelection IN c.opts THEN end := beg
+		ELSIF beg # end THEN
+			ASSERT(0 <= beg, 21); ASSERT(beg < end, 22); ASSERT(end <= t.Length(), 23)
+		END;
+		beg0 := c.selBeg; end0 := c.selEnd;
+		c.insAttr := NIL;
+		IF (beg # beg0) OR (end # end0) THEN
+			IF (beg # end) & (c.selBeg = c.selEnd) THEN
+				IF boundCaret THEN
+					IF c.carPos = end THEN p := c.carPos ELSE p := beg END;
+					c.SetCaret(none); c.carLast := p
+				END;
+				c.SetFocus(NIL);
+				c.selPin0 := beg; c.selPin1 := end
+			ELSIF boundCaret & (beg = end) THEN
+				c.selPin1 := c.selPin0	(* clear selection anchors *)
+			END;
+			IF beg + 1 = end THEN
+				rd := CachedReader(c);
+				rd.SetPos(beg); rd.Read; singleton := rd.view # NIL;
+				CacheReader(c, rd)
+			ELSE singleton := FALSE
+			END;
+			IF singleton THEN	(* native or singleton -> singleton *)
+				IF rd.view # c.Singleton() THEN c.SetSingleton(rd.view) END
+			ELSIF c.Singleton() # NIL THEN	(* singleton -> native *)
+				c.SetSingleton(NIL);
+				c.selBeg := beg; c.selEnd := end;
+				FlipSelection(c, beg, end, Containers.show)
+			ELSE	(* native -> native *)
+				c.selBeg := beg; c.selEnd := end;
+				IF (beg0 <= beg) & (end <= end0) THEN	(* reduce *)
+					p := end0; end0 := beg; beg := end; end := p
+				ELSIF (beg <= beg0) & (end0 <= end) THEN	(* extend *) 
+					p := end; end := beg0; beg0 := end0; end0 := p
+				ELSIF (beg <= beg0) & (beg0 <= end) THEN	(* shift left *)
+					p := end; end := beg0; beg0 := p
+				ELSIF (end >= end0) & (beg <= end0) THEN	(* shift right *)
+					p := end0; end0 := beg; beg := p
+				END;
+				IF beg0 < end0 THEN FlipSelection(c, beg0, end0, Containers.show) END;
+				IF beg < end THEN FlipSelection(c, beg, end, Containers.show) END
+			END
+		END
+	END SetSelection;
+
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) NewController (opts: SET): Controller;
+		VAR c: StdCtrl;
+	BEGIN
+		NEW(c); c.SetOpts(opts); InitMarks(c); RETURN c
+	END NewController;
+
+
+	PROCEDURE Init;
+		VAR d: StdDirectory;
+	BEGIN
+		NEW(d); dir := d; stdDir := d
+	END Init;
+
+BEGIN
+	Init
+END TextControllers.

+ 596 - 0
BlackBox/Text/Mod/Mappers.txt

@@ -0,0 +1,596 @@
+MODULE TextMappers;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Mappers.odc *)
+	(* DO NOT EDIT *)
+
+	IMPORT Strings, Views, Dialog, TextModels;
+
+	CONST
+		(** Scanner.opts **)
+		returnCtrlChars* = 1;
+		returnQualIdents* = 2; returnViews* = 3;
+		interpretBools* = 4; interpretSets* = 5;
+		maskViews* = 6;
+
+		(** Scanner.type **)
+		char* = 1; string* = 3; int* = 4; real* = 5;
+		bool* = 6;	(** iff interpretBools IN opts **)
+		set* = 7;	(** iff interpretSets IN opts **)
+		view* = 8;	(** iff returnViews IN opts **)
+		tab* = 9; line* = 10; para* = 11;	(** iff returnCtrlChars IN opts **)
+		lint* = 16;
+		eot* = 30;
+		invalid* = 31;	(** last Scan hit lexically invalid sequence **)
+
+		(** Formatter.WriteIntForm base **)
+		charCode* = Strings.charCode; decimal* = Strings.decimal; hexadecimal* = Strings.hexadecimal;
+
+		(** Formatter.WriteIntForm showBase **)
+		hideBase* = Strings.hideBase; showBase* = Strings.showBase;
+
+		VIEW = TextModels.viewcode;
+		TAB = TextModels.tab; LINE = TextModels.line; PARA = TextModels.para;
+
+		acceptUnderscores = TRUE;
+
+	TYPE
+		String* = ARRAY 256 OF CHAR;
+
+		Scanner* = RECORD
+			opts-: SET;
+			rider-: TextModels.Reader;	(** prefetch state for single character look-ahead **)
+
+			type*: INTEGER;
+			start*, lines*, paras*: INTEGER;	(** update by Skip **)
+
+			char*: CHAR;	(** valid iff type = char **)
+			int*: INTEGER;	(** valid iff type = int **)
+			base*: INTEGER;	(** valid iff type IN {int, lint} **)
+			lint*: LONGINT;	(** valid iff type IN {int, lint} **)
+			real*: REAL;	(** valid iff type = real **)
+			bool*: BOOLEAN;	(** valid iff type = bool **)
+			set*: SET;	(** valid iff type = set **)
+			len*: INTEGER;	(** valid iff type IN {string, int, lint} **)
+			string*: String;	(** valid iff type IN {string, int, lint, bool, char} **)
+			view*: Views.View; w*, h*: INTEGER	(** valid iff type = view **)
+		END;
+
+		Formatter* = RECORD
+			rider-: TextModels.Writer
+		END;
+
+
+	(** Scanner **)
+
+	PROCEDURE ^ (VAR s: Scanner) SetPos* (pos: INTEGER), NEW;
+	PROCEDURE ^ (VAR s: Scanner) SetOpts* (opts: SET), NEW;
+	PROCEDURE ^ (VAR s: Scanner) Skip* (OUT ch: CHAR), NEW;
+	PROCEDURE ^ (VAR s: Scanner) Scan*, NEW;
+
+
+	PROCEDURE Get (VAR s: Scanner; OUT ch: CHAR);
+	BEGIN
+		s.rider.ReadChar(ch)
+	END Get;
+
+	PROCEDURE Real (VAR s: Scanner);
+		VAR res: INTEGER; ch: CHAR;
+	BEGIN
+		s.type := real;
+		s.string[s.len] := "."; INC(s.len); Get(s, ch);
+		WHILE ("0" <= ch) & (ch <= "9") & (s.len < LEN(s.string) - 1) DO
+			s.string[s.len] := ch; INC(s.len); Get(s, ch)
+		END;
+		IF (ch = "E") OR (ch = "D") THEN
+			s.string[s.len] := ch; INC(s.len); Get(s, ch);
+			IF (ch = "-") OR (ch = "+") THEN s.string[s.len] := ch; INC(s.len); Get(s,ch) END;
+			WHILE ("0" <= ch) & (ch <= "9") & (s.len < LEN(s.string) - 1) DO
+				s.string[s.len] := ch; INC(s.len); Get(s, ch)
+			END
+		END;
+		s.string[s.len] := 0X;
+		Strings.StringToReal(s.string, s.real, res);
+		IF res # 0 THEN s.type := invalid END
+	END Real;
+
+	PROCEDURE Integer (VAR s: Scanner);
+		VAR n, k, res: INTEGER; ch: CHAR; hex: BOOLEAN;
+	BEGIN
+		s.type := int; hex := FALSE; ch := s.rider.char;
+		IF ch = "%" THEN
+			s.string[s.len] := "%"; INC(s.len); Get(s, ch); n:= 0;
+			IF ("0" <= ch) & (ch <= "9") THEN
+				k := ORD(ch) - ORD("0");
+				REPEAT
+					n := 10*n + k; s.string[s.len] := ch; INC(s.len);
+					Get(s, ch); k := ORD(ch) - ORD("0")
+				UNTIL (ch < "0") OR (ch > "9") OR (n > (MAX(INTEGER) - k) DIV 10) OR (s.len = LEN(s.string));
+				IF ("0" <= ch) & (ch <= "9") THEN s.type := invalid ELSE s.base := n END
+			ELSE s.type := invalid
+			END
+		ELSIF (ch = "H") OR (ch = "X") THEN
+			hex := TRUE; s.base := 16;
+			s.string[s.len] := ch; INC(s.len); Get(s, ch)
+		ELSE
+			s.base := 10
+		END;
+		s.string[s.len] := 0X;
+		IF s.type # invalid THEN
+			Strings.StringToInt(s.string, s.int, res);
+			IF res = 0 THEN s.type := int;
+				IF hex THEN (* Strings.StringToLInt(s.string, s.lint, res); ASSERT(res = 0, 100); *)
+					IF s.int < 0 THEN s.lint := s.int + (LONG(MAX(INTEGER)) + 1) * 2
+					ELSE s.lint := s.int
+					END
+				ELSE s.lint := s.int
+				END
+			ELSIF res = 1 THEN (* INTEGER overflow *)
+				Strings.StringToLInt(s.string, s.lint, res);
+				IF res = 0 THEN s.type := lint ELSE s.type := invalid END
+			ELSE	(* syntax error *)
+				s.type := invalid
+			END
+		END
+	END Integer;
+
+	PROCEDURE Number (VAR s: Scanner; neg: BOOLEAN);
+		VAR m: INTEGER; ch: CHAR;
+	BEGIN
+		s.len := 0; m := 0; ch := s.rider.char;
+		IF neg THEN s.string[s.len] := "-"; INC(s.len) END;
+		REPEAT
+			IF (m > 0) OR (ch # "0") THEN	(* ignore leading zeroes *)
+				s.string[s.len] := ch; INC(s.len); INC(m)
+			END;
+			Get(s, ch)
+		UNTIL (ch < "0") OR (ch > "9") & (ch < "A") OR (ch > "F")
+			OR (s.len = LEN(s.string) - 1) OR s.rider.eot;
+		IF (s.len = 0) OR (s.len = 1) & (s.string[0] = "-") THEN	(* compensate for ignoring leading zeroes *)
+			s.string[s.len] := "0"; INC(s.len)
+		END;
+		s.string[s.len] := 0X;
+		IF ch = "." THEN Real(s) ELSE Integer(s) END
+	END Number;
+
+	PROCEDURE Cardinal (VAR s: Scanner; OUT n: INTEGER);
+		VAR k: INTEGER; ch: CHAR;
+	BEGIN
+		n := 0; s.Skip(ch);
+		IF ("0" <= ch) & (ch <= "9") THEN
+			k := ORD(ch) - ORD("0");
+			REPEAT
+				n := n * 10 + k;
+				Get(s, ch); k := ORD(ch) - ORD("0")
+			UNTIL (ch < "0") OR (ch > "9") OR (n > (MAX(INTEGER) - k) DIV 10);
+			IF ("0" <= ch) & (ch <= "9") THEN s.type := invalid END
+		ELSE s.type := invalid
+		END
+	END Cardinal;
+
+	PROCEDURE Set (VAR s: Scanner);
+		VAR n, m: INTEGER; ch: CHAR;
+	BEGIN
+		s.type := set; Get(s, ch); s.Skip(ch); s.set := {};
+		WHILE ("0" <= ch) & (ch <= "9") & (s.type = set) DO
+			Cardinal(s, n); s.Skip(ch);
+			IF (MIN(SET) <= n) & (n <= MAX(SET)) THEN
+				INCL(s.set, n);
+				IF ch = "," THEN
+					Get(s, ch); s.Skip(ch)
+				ELSIF ch = "." THEN
+					Get(s, ch);
+					IF ch = "." THEN
+						Get(s, ch); s.Skip(ch); Cardinal(s, m); s.Skip(ch);
+						IF ch = "," THEN Get(s, ch); s.Skip(ch) END;
+						IF (n <= m) & (m <= MAX(SET)) THEN
+							WHILE m > n DO INCL(s.set, m); DEC(m) END
+						ELSE s.type := invalid
+						END
+					ELSE s.type := invalid
+					END
+				END
+			ELSE s.type := invalid
+			END
+		END;
+		IF s.type = set THEN
+			s.Skip(ch);
+			IF ch = "}" THEN Get(s, ch) ELSE s.type := invalid END
+		END
+	END Set;
+
+	PROCEDURE Boolean (VAR s: Scanner);
+		VAR ch: CHAR;
+	BEGIN
+		s.type := bool; Get(s, ch);
+		IF (ch = "T") OR (ch = "F") THEN
+			s.Scan;
+			IF (s.type = string) & (s.string = "TRUE") THEN s.type := bool; s.bool := TRUE
+			ELSIF (s.type = string) & (s.string = "FALSE") THEN s.type := bool; s.bool := FALSE
+			ELSE s.type := invalid
+			END
+		ELSE s.type := invalid
+		END
+	END Boolean;
+
+	PROCEDURE Name (VAR s: Scanner);
+		VAR max: INTEGER; ch: CHAR;
+	BEGIN
+		s.type := string; s.len := 0; ch := s.rider.char; max := LEN(s.string);
+		REPEAT
+			s.string[s.len] := ch; INC(s.len); Get(s, ch)
+		UNTIL
+			~(	("0" <= ch) & (ch <= "9")
+			  OR ("A" <= CAP(ch)) & (CAP(ch) <= "Z")
+			  OR (0C0X <= ch) & (ch <= 0FFX) & (ch # 0D7X) & (ch # 0F7X)
+			  OR acceptUnderscores & (ch = "_"))
+		OR (s.len = max);
+		IF (returnQualIdents IN s.opts) & (ch = ".") & (s.len < max) THEN
+			REPEAT
+				s.string[s.len] := ch; INC(s.len); Get(s, ch)
+			UNTIL
+			~(	("0" <= ch) & (ch <= "9")
+			  OR ("A" <= CAP(ch)) & (CAP(ch) <= "Z")
+			  OR (0C0X <= ch) & (ch <= 0FFX) & (ch # 0D7X) & (ch # 0F7X)
+			  OR acceptUnderscores & (ch = "_") )
+			OR (s.len = max)
+		END;
+		IF s.len = max THEN DEC(s.len); s.type := invalid END;	(* ident too long *)
+		s.string[s.len] := 0X
+	END Name;
+
+	PROCEDURE DoubleQuotedString (VAR s: Scanner);
+		VAR max, pos: INTEGER; ch: CHAR;
+	BEGIN
+		pos := s.rider.Pos();
+		s.type := string; s.len := 0; max := LEN(s.string) - 1; Get(s, ch);
+		WHILE (ch # '"') & (ch # 0X) & (s.len < max) DO
+			s.string[s.len] := ch; INC(s.len);
+			Get(s, ch)
+		END;
+		s.string[s.len] := 0X;
+		IF ch = '"' THEN Get(s, ch)
+		ELSE s.type := invalid; s.rider.SetPos(pos (* s.rider.Pos() - s.len - 1 *)); Get(s, ch)
+		END
+	END DoubleQuotedString;
+
+	PROCEDURE SingleQuotedString (VAR s: Scanner);
+		VAR max, pos: INTEGER; ch: CHAR;
+	BEGIN
+		pos := s.rider.Pos();
+		s.type := string; s.len := 0; max := LEN(s.string) - 1; Get(s, ch);
+		WHILE (ch # "'") & (ch # 0X) & (s.len < max) DO
+			s.string[s.len] := ch; INC(s.len);
+			Get(s, ch)
+		END;
+		s.string[s.len] := 0X;
+		IF s.len = 1 THEN s.type := char; s.char := s.string[0] END;
+		IF ch = "'" THEN Get(s, ch)
+		ELSE s.type := invalid; s.rider.SetPos(pos (* s.rider.Pos() - s.len - 1 *)); Get(s, ch)
+		END
+	END SingleQuotedString;
+
+	PROCEDURE Char (VAR s: Scanner);
+		VAR ch: CHAR;
+	BEGIN
+		ch := s.rider.char;
+		IF ch # 0X THEN
+			s.type := char; s.char := ch; s.string[0] := ch; s.string[1] := 0X; Get(s, ch)
+		ELSE s.type := invalid
+		END
+	END Char;
+
+	PROCEDURE View (VAR s: Scanner);
+		VAR ch: CHAR;
+	BEGIN
+		s.type := view; s.view := s.rider.view; s.w := s.rider.w; s.h := s.rider.h;
+		IF maskViews IN s.opts THEN
+			IF s.rider.char # TextModels.viewcode THEN
+				s.type := char; s.char := s.rider.char; s.string[0] := s.char; s.string[1] := 0X
+			END
+		END;
+		Get(s, ch)
+	END View;
+
+
+	PROCEDURE (VAR s: Scanner) ConnectTo* (text: TextModels.Model), NEW;
+	BEGIN
+		IF text # NIL THEN
+			s.rider := text.NewReader(s.rider); s.SetPos(0); s.SetOpts({})
+		ELSE
+			s.rider := NIL
+		END
+	END ConnectTo;
+
+	PROCEDURE (VAR s: Scanner) SetPos* (pos: INTEGER), NEW;
+	BEGIN
+		s.rider.SetPos(pos); s.start := pos;
+		s.lines := 0; s.paras := 0; s.type := invalid
+	END SetPos;
+
+	PROCEDURE (VAR s: Scanner) SetOpts* (opts: SET), NEW;
+	BEGIN
+		s.opts := opts
+	END SetOpts;
+
+	PROCEDURE (VAR s: Scanner) Pos* (): INTEGER, NEW;
+	BEGIN
+		RETURN s.rider.Pos()
+	END Pos;
+
+	PROCEDURE (VAR s: Scanner) Skip* (OUT ch: CHAR), NEW;
+		VAR c, v: BOOLEAN;
+	BEGIN
+		IF s.opts * {returnCtrlChars, returnViews} = {} THEN
+			ch := s.rider.char;
+			WHILE ((ch <= " ") OR (ch = TextModels.digitspace) OR (ch = TextModels.nbspace))
+			& ~s.rider.eot DO
+				IF ch = LINE THEN INC(s.lines)
+				ELSIF ch = PARA THEN INC(s.paras)
+				END;
+				Get(s, ch)
+			END
+		ELSE
+			c := returnCtrlChars IN s.opts;
+			v := returnViews IN s.opts;
+			ch := s.rider.char;
+			WHILE ((ch <= " ") OR (ch = TextModels.digitspace) OR (ch = TextModels.nbspace))
+			& ~s.rider.eot
+			& (~c OR (ch # TAB) & (ch # LINE) & (ch # PARA))
+			& (~v OR (ch # VIEW) OR (s.rider.view = NIL)) DO
+				IF ch = LINE THEN INC(s.lines)
+				ELSIF ch = PARA THEN INC(s.paras)
+				END;
+				Get(s, ch)
+			END
+		END;
+		IF ~s.rider.eot THEN s.start := s.rider.Pos() - 1
+		ELSE s.start := s.rider.Base().Length(); s.type := eot
+		END
+	END Skip;
+
+	PROCEDURE (VAR s: Scanner) Scan*, NEW;
+		VAR sign, neg: BOOLEAN; ch: CHAR;
+	BEGIN
+		s.Skip(ch);
+		IF s.type # eot THEN
+			neg := (ch = "-"); sign := neg OR (ch = "+");
+			IF sign THEN s.char := ch; Get(s, ch) END;
+			IF ("0" <= ch) & (ch <= "9") THEN Number(s, neg)
+			ELSIF sign THEN s.type := char;	(* return prefetched sign w/o trailing number *)
+				s.string[0] := s.char; s.string[1] := 0X
+			ELSE
+				CASE ch OF
+				| "A" .. "Z", "a" .. "z", 0C0X .. 0D6X, 0D8X .. 0F6X, 0F8X .. 0FFX: Name(s)
+				| '"': DoubleQuotedString(s)
+				| "'": SingleQuotedString(s)
+				| TAB: s.type := tab; Get(s, ch)
+				| LINE: s.type := line; Get(s, ch)
+				| PARA: s.type := para; Get(s, ch)
+				| VIEW:
+					IF s.rider.view # NIL THEN View(s) ELSE Char(s) END
+				| "{":
+					IF interpretSets IN s.opts THEN Set(s) ELSE Char(s) END
+				| "$":
+					IF interpretBools IN s.opts THEN Boolean(s) ELSE Char(s) END
+				| "_":
+					IF acceptUnderscores THEN Name(s) ELSE Char(s) END
+				ELSE Char(s)
+				END
+			END
+		END
+	END Scan;
+
+
+	(** scanning utilities **)
+
+	PROCEDURE IsQualIdent* (IN s: ARRAY OF CHAR): BOOLEAN;
+		VAR i: INTEGER; ch: CHAR;
+	BEGIN
+		ch := s[0]; i := 1;
+		IF ("A" <= CAP(ch)) & (CAP(ch) <= "Z")
+		OR (0C0X <= ch) & (ch <= 0FFX) & (ch # 0D0X) & (ch # 0D7X) & (ch # 0F7X) THEN
+			REPEAT
+				ch := s[i]; INC(i)
+			UNTIL
+				~(	("0" <= ch) & (ch <= "9")
+				  OR ("A" <= CAP(ch)) & (CAP(ch) <= "Z")
+				  OR (0C0X <= ch) & (ch <= 0FFX) & (ch # 0D0X) & (ch # 0D7X) & (ch # 0F7X)
+				  OR (ch = "_") );
+			IF ch = "." THEN
+				INC(i);
+				REPEAT
+					ch := s[i]; INC(i)
+				UNTIL
+					~(	("0" <= ch) & (ch <= "9")
+					  OR ("A" <= CAP(ch)) & (CAP(ch) <= "Z")
+					  OR (0C0X <= ch) & (ch <= 0FFX) & (ch # 0D0X) & (ch # 0D7X) & (ch # 0F7X)
+					  OR (ch = "_") );
+				RETURN ch = 0X
+			ELSE
+				RETURN FALSE
+			END
+		ELSE
+			RETURN FALSE
+		END
+	END IsQualIdent;
+
+	PROCEDURE ScanQualIdent* (VAR s: Scanner; OUT x: ARRAY OF CHAR; OUT done: BOOLEAN);
+		VAR mod: String; i, j, len, start: INTEGER; ch: CHAR;
+	BEGIN
+		done := FALSE;
+		IF s.type = string THEN
+			IF IsQualIdent(s.string) THEN
+				IF s.len < LEN(x) THEN
+					x := s.string$; done := TRUE
+				END
+			ELSE
+				mod := s.string; len := s.len; start := s.start;
+				s.Scan;
+				IF (s.type = char) & (s.char = ".") THEN
+					s.Scan;
+					IF (s.type = string) & (len + 1 + s.len < LEN(x)) THEN
+						i := 0; ch := mod[0]; WHILE ch # 0X DO x[i] := ch; INC(i); ch := mod[i] END;
+						x[i] := "."; INC(i);
+						j := 0; ch := s.string[0];
+						WHILE ch # 0X DO x[i] := ch; INC(i); INC(j); ch := s.string[j] END;
+						x[i] := 0X; done := TRUE
+					END
+				END;
+				IF ~done THEN s.SetPos(start); s.Scan() END
+			END
+		END
+	END ScanQualIdent;
+
+
+	(** Formatter **)
+
+	PROCEDURE ^ (VAR f: Formatter) SetPos* (pos: INTEGER), NEW;
+	PROCEDURE ^ (VAR f: Formatter) WriteIntForm* (x: LONGINT;
+														base, minWidth: INTEGER; fillCh: CHAR; showBase: BOOLEAN), NEW;
+	PROCEDURE ^ (VAR f: Formatter) WriteRealForm* (x: REAL;
+														precision, minW, expW: INTEGER; fillCh: CHAR), NEW;
+	PROCEDURE ^ (VAR f: Formatter) WriteViewForm* (v: Views.View; w, h: INTEGER), NEW;
+
+
+	PROCEDURE (VAR f: Formatter) ConnectTo* (text: TextModels.Model), NEW;
+	BEGIN
+		IF text # NIL THEN
+			f.rider := text.NewWriter(f.rider); f.SetPos(text.Length())
+		ELSE
+			f.rider := NIL
+		END
+	END ConnectTo;
+
+	PROCEDURE (VAR f: Formatter) SetPos* (pos: INTEGER), NEW;
+	BEGIN
+		f.rider.SetPos(pos)
+	END SetPos;
+
+	PROCEDURE (VAR f: Formatter) Pos* (): INTEGER, NEW;
+	BEGIN
+		RETURN f.rider.Pos()
+	END Pos;
+
+
+	PROCEDURE (VAR f: Formatter) WriteChar* (x: CHAR), NEW;
+	BEGIN
+		IF (x >= " ") & (x # 7FX) THEN
+			f.rider.WriteChar(x)
+		ELSE
+			f.rider.WriteChar(" ");
+			f.WriteIntForm(ORD(x), charCode, 3, "0", showBase);
+			f.rider.WriteChar(" ")
+		END
+	END WriteChar;
+
+	PROCEDURE (VAR f: Formatter) WriteInt* (x: LONGINT), NEW;
+	BEGIN
+		f.WriteIntForm(x, decimal, 0, TextModels.digitspace, hideBase)
+	END WriteInt;
+
+	PROCEDURE (VAR f: Formatter) WriteSString* (x: ARRAY OF SHORTCHAR), NEW;
+		VAR i: INTEGER;
+	BEGIN
+		i := 0; WHILE x[i] # 0X DO f.WriteChar(x[i]); INC(i) END
+	END WriteSString;
+
+	PROCEDURE (VAR f: Formatter) WriteString* (x: ARRAY OF CHAR), NEW;
+		VAR i: INTEGER;
+	BEGIN
+		i := 0; WHILE x[i] # 0X DO f.WriteChar(x[i]); INC(i) END
+	END WriteString;
+
+	PROCEDURE (VAR f: Formatter) WriteReal* (x: REAL), NEW;
+		VAR m: ARRAY 256 OF CHAR;
+	BEGIN
+		Strings.RealToString(x, m); f.WriteString(m)
+	END WriteReal;
+
+	PROCEDURE (VAR f: Formatter) WriteBool* (x: BOOLEAN), NEW;
+	BEGIN
+		IF x THEN f.WriteString("$TRUE") ELSE f.WriteString("$FALSE") END
+	END WriteBool;
+
+	PROCEDURE (VAR f: Formatter) WriteSet* (x: SET), NEW;
+		VAR i: INTEGER;
+	BEGIN
+		f.WriteChar("{"); i := MIN(SET);
+		WHILE x # {} DO
+			IF i IN x THEN f.WriteInt(i); EXCL(x, i);
+				IF (i + 2 <= MAX(SET)) & (i+1 IN x) & (i+2 IN x) THEN f.WriteString("..");
+					x := x - {i+1, i+2}; INC(i, 3);
+					WHILE (i <= MAX(SET)) & (i IN x) DO EXCL(x, i); INC(i) END;
+					f.WriteInt(i-1)
+				END;
+				IF x # {} THEN f.WriteString(", ") END
+			END;
+			INC(i)
+		END;
+		f.WriteChar("}")
+	END WriteSet;
+
+	PROCEDURE (VAR f: Formatter) WriteTab*, NEW;
+	BEGIN
+		f.rider.WriteChar(TAB)
+	END WriteTab;
+
+	PROCEDURE (VAR f: Formatter) WriteLn*, NEW;
+	BEGIN
+		f.rider.WriteChar(LINE)
+	END WriteLn;
+
+	PROCEDURE (VAR f: Formatter) WritePara*, NEW;
+	BEGIN
+		f.rider.WriteChar(PARA)
+	END WritePara;
+
+	PROCEDURE (VAR f: Formatter) WriteView* (v: Views.View), NEW;
+	BEGIN
+		f.WriteViewForm(v, Views.undefined, Views.undefined)
+	END WriteView;
+
+
+	PROCEDURE (VAR f: Formatter) WriteIntForm* (x: LONGINT;
+		base, minWidth: INTEGER; fillCh: CHAR; showBase: BOOLEAN
+	), NEW;
+		VAR s: ARRAY 80 OF CHAR;
+	BEGIN
+		Strings.IntToStringForm(x, base, minWidth, fillCh, showBase, s);
+		f.WriteString(s)
+	END WriteIntForm;
+
+	PROCEDURE (VAR f: Formatter) WriteRealForm* (x: REAL;
+		precision, minW, expW: INTEGER; fillCh: CHAR
+	), NEW;
+		VAR s: ARRAY 256 OF CHAR;
+	BEGIN
+		Strings.RealToStringForm(x, precision, minW, expW, fillCh, s); f.WriteString(s)
+	END WriteRealForm;
+
+
+	PROCEDURE (VAR f: Formatter) WriteViewForm* (v: Views.View; w, h: INTEGER), NEW;
+	BEGIN
+		f.rider.WriteView(v, w, h)
+	END WriteViewForm;
+
+	PROCEDURE (VAR f: Formatter) WriteParamMsg* (msg, p0, p1, p2: ARRAY OF CHAR), NEW;
+		VAR s: ARRAY 256 OF CHAR; i: INTEGER; ch: CHAR;
+	BEGIN
+		Dialog.MapParamString(msg, p0, p1, p2, s);
+		i := 0; ch := s[0];
+		WHILE ch # 0X DO
+			IF ch = LINE THEN f.WriteLn
+			ELSIF ch = PARA THEN f.WritePara
+			ELSIF ch = TAB THEN f.WriteTab
+			ELSIF ch >= " " THEN f.WriteChar(ch)
+			END;
+			INC(i); ch := s[i]
+		END
+	END WriteParamMsg;
+
+	PROCEDURE (VAR f: Formatter) WriteMsg* (msg: ARRAY OF CHAR), NEW;
+	BEGIN
+		f.WriteParamMsg(msg, "", "", "")
+	END WriteMsg;
+
+END TextMappers.

+ 2085 - 0
BlackBox/Text/Mod/Models.txt

@@ -0,0 +1,2085 @@
+MODULE TextModels;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Models.odc *)
+	(* DO NOT EDIT *)
+
+(* re-check alien attributes: project to base attributes? *)
+(* support *lists* of attribute extensions? *)
+(* support for enumeration of texts within embedded views 
+	- generally: support for enumeration of X-views within a recursive scheme?
+	- however: Containers already provides a general iteration scheme
+		-> could add recursion support to Reader later
+*)
+
+	IMPORT
+		Files, Services, Fonts, Ports, Stores, Models, Views, Properties, Containers;
+
+	(* text file format:
+
+	text = 0					  CHAR
+			textoffset			INTEGER (> 0)
+			{ run }
+			-1					CHAR
+			{ char }
+
+	run = attrno				 BYTE (0..32)
+			[ attr ]			    attr.Internalize
+			( piece | lpiece | viewref )
+
+	piece = length			   INTEGER (> 0)
+
+	lpiece = -length		     INTEGER (< 0, length MOD 2 = 0)
+
+	viewref = 0				   INTEGER
+			w					  INTEGER
+			h					   INTEGER
+			view				   view.Internalize
+	*)
+
+	CONST
+		(* unicode* = 1X; *)
+		viewcode* = 2X;	(** code for embedded views **)
+		tab* = 9X; line* = 0DX; para* = 0EX;	(** tabulator; line and paragraph separator **)
+		zwspace* = 8BX; nbspace* = 0A0X; digitspace* = 8FX;
+		hyphen* = 90X; nbhyphen* = 91X; softhyphen* = 0ADX;
+
+		(** Pref.opts, options of text-aware views **)
+		maskChar* = 0; hideable* = 1;
+
+		(** Prop.known/valid/readOnly **)
+		offset* = 0; code* = 1;
+
+		(** InfoMsg.op **)
+		store* = 0;
+
+		(** UpdateMsg.op **)
+		replace* = 0; insert* = 1; delete* = 2;
+
+		(* EditOp.mode *)
+		deleteRange = 0; moveBuf = 1; writeSChar = 2; writeChar = 3; writeView = 4;
+
+		dictSize = 32;
+
+		point = Ports.point;
+		defW = 64 * point; defH = 32 * point;
+
+		(* embedding limits - don't increase maxHeight w/o checking TextViews.StdView *)
+		minWidth = 5 * point; maxWidth = MAX(INTEGER) DIV 2;
+		minHeight = 5 * point; maxHeight = 1500 * point;
+
+		minVersion = 0; maxAttrVersion = 0; maxModelVersion = 0;
+		noLCharStdModelVersion = 0; maxStdModelVersion = 1;
+
+		cacheWidth = 8; cacheLen = 4096; cacheLine = 128;
+
+	TYPE
+		Model* = POINTER TO ABSTRACT RECORD (Containers.Model) END;
+
+		Attributes* = POINTER TO EXTENSIBLE RECORD (Stores.Store)
+			init-: BOOLEAN;	(* immutable once init is set *)
+			color-: Ports.Color;
+			font-: Fonts.Font;
+			offset-: INTEGER
+		END;
+
+		AlienAttributes* = POINTER TO RECORD (Attributes)
+			store-: Stores.Alien
+		END;
+
+		Prop* = POINTER TO RECORD (Properties.Property)
+			offset*: INTEGER;
+			code*: CHAR
+		END;
+
+
+		Context* = POINTER TO ABSTRACT RECORD (Models.Context) END;
+
+		Pref* = RECORD (Properties.Preference)
+			opts*: SET;	(** preset to {} **)
+			mask*: CHAR	(** valid if maskChar IN opts **)
+		END;
+
+
+		Reader* = POINTER TO ABSTRACT RECORD
+			eot*: BOOLEAN;
+			attr*: Attributes;
+			char*: CHAR;
+			view*: Views.View;
+			w*, h*: INTEGER
+		END;
+
+		Writer* = POINTER TO ABSTRACT RECORD
+			attr-: Attributes
+		END;
+
+
+		InfoMsg* = RECORD (Models.Message)
+			op*: INTEGER
+		END;
+
+		UpdateMsg* = RECORD (Models.UpdateMsg)
+			op*: INTEGER;
+			beg*, end*, delta*: INTEGER	(** range: [beg, end); length = length' + delta **)
+		END;
+
+
+		Directory* = POINTER TO ABSTRACT RECORD
+			attr-: Attributes
+		END;
+
+
+		Run = POINTER TO EXTENSIBLE RECORD
+			prev, next: Run;
+			len: INTEGER;
+			attr: Attributes
+		END;
+
+		LPiece = POINTER TO EXTENSIBLE RECORD (Run)
+			file: Files.File;
+			org: INTEGER
+		END;
+
+		Piece = POINTER TO RECORD (LPiece) END;	(* u IS Piece => CHAR run *)
+
+		ViewRef = POINTER TO RECORD (Run)	(* u IS ViewRef => View run *)
+			w, h: INTEGER;
+			view: Views.View	(* embedded view *)
+		END;
+
+
+		PieceCache = RECORD
+			org: INTEGER;
+			prev: Run	(* Org(prev.next) = org *)
+		END;
+
+		SpillFile = POINTER TO RECORD
+			file: Files.File;	(* valid if file # NIL *)
+			len: INTEGER;	(* len = file.Length() *)
+			writer: Files.Writer	(* writer.Base() = file *)
+		END;
+
+		AttrDict = RECORD
+			len: BYTE;
+			attr: ARRAY dictSize OF Attributes
+		END;
+
+		StdModel = POINTER TO RECORD (Model)
+			len: INTEGER;	(* len = sum(u : [trailer.next, trailer) : u.len) *)
+			id: INTEGER;  (* unique (could use SYSTEM.ADR instead ...) *)
+			era: INTEGER;	(* stable era >= k *)
+			trailer: Run;	(* init => trailer # NIL *)
+			pc: PieceCache;
+			spill: SpillFile;	(* spill file, created lazily, shared with clones *)
+			rd: Reader	(* reader cache *)
+		END;
+
+		StdContext = POINTER TO RECORD (Context)
+			text: StdModel;
+			ref: ViewRef
+		END;
+
+		StdReader = POINTER TO RECORD (Reader)
+			base: StdModel;	(* base = Base() *)
+			pos: INTEGER;	(* pos = Pos() *)
+			era: INTEGER;
+			run: Run;	(* era = base.era => Pos(run) + off = pos *)
+			off: INTEGER;	(* era = base.era => 0 <= off < run.len *)
+			reader: Files.Reader	(* file reader cache *)
+		END;
+
+		StdWriter = POINTER TO RECORD (Writer)
+			base: StdModel;	(* base = Base() *)
+			(* hasSequencer := base.Domain() = NIL  OR  base.Domain().GetSequencer() = NIL *)
+			pos: INTEGER;	(* pos = Pos() *)
+			era: INTEGER;	(* relevant iff hasSequencer *)
+			run: Run	(* hasSequencer  &  era = base.era  =>  Pos(run) = pos *)
+		END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+
+		MoveOp = POINTER TO RECORD (Stores.Operation)	(* MoveStretchFrom *)
+			(* move src.[beg, end) to dest.pos *)
+			src: StdModel;
+			beg, end: INTEGER;
+			dest: StdModel;
+			pos: INTEGER
+		END;
+
+		EditOp = POINTER TO RECORD (Stores.Operation)	(* CopyStretchFrom, Delete, WriteXXX *)
+			mode: INTEGER;
+			canBunch: BOOLEAN;
+			text: StdModel;
+			beg, end: INTEGER;	(* op = deleteRange: move text.[beg, end) to <first, last> *)
+			pos: INTEGER;
+			first, last: Run;	(* op = moveBuf: move <first, last> to text.pos;
+											op = writeView: insert <first> at text.pos*)
+			len: INTEGER;	(* op = moveBuf: length of <first, last>;
+											op = write[L]Char: length of spill file before writing new [long] char *)
+			attr: Attributes	(* op = write[L]Char *)
+		END;
+
+		AttrList = POINTER TO RECORD
+			next: AttrList;
+			len: INTEGER;
+			attr: Attributes
+		END;
+
+		SetAttrOp = POINTER TO RECORD (Stores.Operation)	(* SetAttr, Modify *)
+			text: StdModel;
+			beg: INTEGER;
+			list: AttrList
+		END;
+
+		ResizeViewOp = POINTER TO RECORD (Stores.Operation)	(* ResizeView *)
+			text: StdModel;
+			pos: INTEGER;
+			ref: ViewRef;
+			w, h: INTEGER
+		END;
+
+		ReplaceViewOp = POINTER TO RECORD (Stores.Operation)	(* ReplaceView *)
+			text: StdModel;
+			pos: INTEGER;
+			ref: ViewRef;
+			new: Views.View
+		END;
+
+		TextCache = RECORD
+			id: INTEGER;  (* id of the text block served by this cache block *)
+			beg, end: INTEGER;  (* [beg .. end) cached, 0 <= end - beg < cacheLen *)
+			buf: ARRAY cacheLen OF BYTE  (* [beg MOD cacheLen .. end MOD cacheLen) *)
+		END;
+		Cache = ARRAY cacheWidth OF TextCache;
+
+	VAR
+		dir-, stdDir-: Directory;
+
+		stdProp: Properties.StdProp;	(* temp for NewColor, ... NewWeight *)
+		prop: Prop;	(* temp for NewOffset *)
+		nextId: INTEGER;
+		cache: Cache;
+
+
+	(** Model **)
+
+	PROCEDURE (m: Model) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+		VAR thisVersion: INTEGER;
+	BEGIN
+		m.Internalize^(rd); IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxModelVersion, thisVersion)
+	END Internalize;
+
+	PROCEDURE (m: Model) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	BEGIN
+		m.Externalize^(wr);
+		wr.WriteVersion(maxModelVersion)
+	END Externalize;
+
+
+	PROCEDURE (m: Model) Length* (): INTEGER, NEW, ABSTRACT;
+	PROCEDURE (m: Model) NewReader* (old: Reader): Reader, NEW, ABSTRACT;
+	PROCEDURE (m: Model) NewWriter* (old: Writer): Writer, NEW, ABSTRACT;
+	PROCEDURE (m: Model) InsertCopy* (pos: INTEGER; m0: Model; beg0, end0: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (m: Model) Insert* (pos: INTEGER; m0: Model; beg0, end0: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (m: Model) Delete* (beg, end: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (m: Model) SetAttr* (beg, end: INTEGER; attr: Attributes), NEW, ABSTRACT;
+	PROCEDURE (m: Model) Prop* (beg, end: INTEGER): Properties.Property, NEW, ABSTRACT;
+	PROCEDURE (m: Model) Modify* (beg, end: INTEGER; old, p: Properties.Property), NEW, ABSTRACT;
+	PROCEDURE (m: Model) ReplaceView* (old, new: Views.View), ABSTRACT;
+
+	PROCEDURE (m: Model) Append* (m0: Model), NEW, ABSTRACT;
+(*
+	BEGIN
+		ASSERT(m # m0, 20);
+		m.Insert(m.Length(), m0, 0, m0.Length())
+	END Append;
+*)
+	PROCEDURE (m: Model) Replace* (beg, end: INTEGER; m0: Model; beg0, end0: INTEGER),
+		NEW, ABSTRACT;
+(*
+		VAR script: Stores.Operation; delta: INTEGER;
+	BEGIN
+		Models.BeginScript(m, "#System:Replacing", script);
+		m.Delete(beg, end);
+		IF beg0 > 
+		m.Insert(beg, m0, beg0, end0);
+		Models.EndScript(m, script)
+	END Replace;
+*)
+
+	(** Attributes **)
+
+	PROCEDURE (a: Attributes) CopyFrom- (source: Stores.Store), EXTENSIBLE;
+	(** pre: ~a.init, source.init **)
+	(** post: a.init **)
+	BEGIN
+		WITH source: Attributes DO
+			ASSERT(~a.init, 20); ASSERT(source.init, 21); a.init := TRUE;
+			a.color := source.color; a.font := source.font; a.offset := source.offset
+		END
+	END CopyFrom;
+
+	PROCEDURE (a: Attributes) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+	(** pre: ~a.init **)
+	(** post: a.init **)
+		VAR thisVersion: INTEGER;
+			fprint: INTEGER; face: Fonts.Typeface; size: INTEGER; style: SET; weight: INTEGER;
+	BEGIN
+		ASSERT(~a.init, 20); a.init := TRUE;
+		a.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxAttrVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadInt(a.color);
+		rd.ReadInt(fprint);
+		rd.ReadXString(face); rd.ReadInt(size); rd.ReadSet(style); rd.ReadXInt(weight);
+		a.font := Fonts.dir.This(face, size, style, weight);
+		IF a.font.IsAlien() THEN Stores.Report("#System:AlienFont", face, "", "")
+(*
+		ELSIF a.font.Fingerprint() # fprint THEN Stores.Report("#System:AlienFontVersion", face, "", "")
+*)
+		END;
+		rd.ReadInt(a.offset)
+	END Internalize;
+
+	PROCEDURE (a: Attributes) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	(** pre: a.init **)
+		VAR f: Fonts.Font;
+	BEGIN
+		ASSERT(a.init, 20);
+		a.Externalize^(wr);
+		wr.WriteVersion(maxAttrVersion);
+		wr.WriteInt(a.color);
+		f := a.font;
+(*
+		wr.WriteInt(f.Fingerprint());
+*)
+		wr.WriteInt(0);
+		wr.WriteXString(f.typeface); wr.WriteInt(f.size); wr.WriteSet(f.style); wr.WriteXInt(f.weight);
+		wr.WriteInt(a.offset)
+	END Externalize;
+
+	PROCEDURE (a: Attributes) InitFromProp* (p: Properties.Property), NEW, EXTENSIBLE;
+	(** pre: ~a.init **)
+	(** post: a.init, x IN p.valid => x set in a, else x defaults in a **)
+		VAR def: Fonts.Font; face: Fonts.Typeface; size: INTEGER; style: SET; weight: INTEGER;
+	BEGIN
+		ASSERT(~a.init, 20); a.init := TRUE;
+		def := Fonts.dir.Default();
+		face := def.typeface$; size := def.size; style := def.style; weight := def.weight;
+		a.color := Ports.defaultColor; a.offset := 0;
+		WHILE p # NIL DO
+			WITH p: Properties.StdProp DO
+				IF Properties.color IN p.valid THEN a.color := p.color.val END;
+				IF Properties.typeface IN p.valid THEN face := p.typeface END;
+				IF (Properties.size IN p.valid)
+				& (Ports.point <= p.size) & (p.size <= 32767 * Ports.point) THEN
+					size := p.size
+				END;
+				IF Properties.style IN p.valid THEN
+					style := style - p.style.mask + p.style.val * p.style.mask
+				END;
+				IF (Properties.weight IN p.valid) & (1 <= p.weight) & (p.weight <= 1000) THEN
+					weight := p.weight
+				END
+			| p: Prop DO
+				IF offset IN p.valid THEN a.offset := p.offset END
+			ELSE
+			END;
+			p := p.next
+		END;
+		a.font := Fonts.dir.This(face, size, style, weight)
+	END InitFromProp;
+
+	PROCEDURE (a: Attributes) Equals* (b: Attributes): BOOLEAN, NEW, EXTENSIBLE;
+	(** pre: a.init, b.init **)
+	BEGIN
+		ASSERT(a.init, 20); ASSERT((b # NIL) & b.init, 21);
+		RETURN (a = b)
+			OR   (Services.SameType(a, b))
+				& (a.color = b.color) & (a.font = b.font) & (a.offset = b.offset)
+	END Equals;
+
+	PROCEDURE (a: Attributes) Prop* (): Properties.Property, NEW, EXTENSIBLE;
+	(** pre: a.init **)
+		VAR p: Properties.Property; sp: Properties.StdProp; tp: Prop;
+	BEGIN
+		ASSERT(a.init, 20);
+		NEW(sp);
+		sp.known := {Properties.color .. Properties.weight}; sp.valid := sp.known;
+		sp.color.val := a.color;
+		sp.typeface := a.font.typeface$;
+		sp.size := a.font.size;
+		sp.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
+		sp.style.val := a.font.style * sp.style.mask;
+		sp.weight := a.font.weight;
+		NEW(tp);
+		tp.known := {offset}; tp.valid := tp.known;
+		tp.offset := a.offset;
+		Properties.Insert(p, tp); Properties.Insert(p, sp);
+		RETURN p
+	END Prop;
+
+	PROCEDURE (a: Attributes) ModifyFromProp- (p: Properties.Property), NEW, EXTENSIBLE;
+	(** pre: ~a.init **)
+		VAR face: Fonts.Typeface; size: INTEGER; style: SET; weight: INTEGER;
+			valid: SET;
+	BEGIN
+		face := a.font.typeface; size := a.font.size; 
+		style := a.font.style; weight := a.font.weight;
+		WHILE p # NIL DO
+			valid := p.valid;
+			WITH p: Properties.StdProp DO
+				IF Properties.color IN valid THEN a.color := p.color.val END;
+				IF Properties.typeface IN valid THEN
+					face := p.typeface
+				END;
+				IF (Properties.size IN valid)
+				& (Ports.point <= p.size) & (p.size <= 32767 * Ports.point) THEN
+					size := p.size
+				ELSE EXCL(valid, Properties.size)
+				END;
+				IF Properties.style IN valid THEN
+					style := style - p.style.mask + p.style.val * p.style.mask
+				END;
+				IF (Properties.weight IN valid) & (1 <= p.weight) & (p.weight <= 1000) THEN
+					weight := p.weight
+				ELSE EXCL(valid, Properties.weight)
+				END;
+				IF valid - {Properties.typeface .. Properties.weight} # valid THEN
+					a.font := Fonts.dir.This(face, size, style, weight)
+				END
+			| p: Prop DO
+				IF offset IN valid THEN a.offset := p.offset END
+			ELSE
+			END;
+			p := p.next
+		END
+	END ModifyFromProp;
+
+
+	PROCEDURE ReadAttr* (VAR rd: Stores.Reader; VAR a: Attributes);
+		VAR st: Stores.Store; alien: AlienAttributes;
+	BEGIN
+		rd.ReadStore(st); ASSERT(st # NIL, 20);
+		IF st IS Stores.Alien THEN
+			NEW(alien); alien.store := st(Stores.Alien); Stores.Join(alien, alien.store);
+			alien.InitFromProp(NIL); a := alien;
+			Stores.Report("#Text:AlienAttributes", "", "", "")
+		ELSE a := st(Attributes)
+		END
+	END ReadAttr;
+
+	PROCEDURE WriteAttr* (VAR wr: Stores.Writer; a: Attributes);
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		WITH a: AlienAttributes DO wr.WriteStore(a.store) ELSE wr.WriteStore(a) END
+	END WriteAttr;
+
+	PROCEDURE ModifiedAttr* (a: Attributes; p: Properties.Property): Attributes;
+	(** pre: a.init **)
+	(** post: x IN p.valid => x in new attr set to value in p, else set to value in a **)
+		VAR h: Attributes;
+	BEGIN
+		ASSERT(a.init, 20);
+		h := Stores.CopyOf(a)(Attributes); h.ModifyFromProp(p);
+		RETURN h
+	END ModifiedAttr;
+
+
+	(** AlienAttributes **)
+
+	PROCEDURE (a: AlienAttributes) Externalize- (VAR wr: Stores.Writer);
+	BEGIN
+		HALT(100)
+	END Externalize;
+
+	PROCEDURE (a: AlienAttributes) CopyFrom- (source: Stores.Store);
+	BEGIN
+		a.CopyFrom^(source);
+		a.store := Stores.CopyOf(source(AlienAttributes).store)(Stores.Alien);
+		Stores.Join(a, a.store)
+	END CopyFrom;
+
+	PROCEDURE (a: AlienAttributes) Prop* (): Properties.Property;
+	BEGIN
+		RETURN NIL
+	END Prop;
+
+	PROCEDURE (a: AlienAttributes) ModifyFromProp- (p: Properties.Property);
+	END ModifyFromProp;
+
+
+	(** Prop **)
+
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			IF p.offset # q.offset THEN EXCL(valid, offset) END;
+			IF p.code # q.code THEN EXCL(valid, code) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+
+
+	(** Context **)
+
+	PROCEDURE (c: Context) ThisModel* (): Model, ABSTRACT;
+	PROCEDURE (c: Context) Pos* (): INTEGER, NEW, ABSTRACT;
+	PROCEDURE (c: Context) Attr* (): Attributes, NEW, ABSTRACT;
+
+
+	(** Reader **)
+
+	PROCEDURE (rd: Reader) Base* (): Model, NEW, ABSTRACT;
+	PROCEDURE (rd: Reader) SetPos* (pos: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (rd: Reader) Pos* (): INTEGER, NEW, ABSTRACT;
+	PROCEDURE (rd: Reader) Read*, NEW, ABSTRACT;
+	PROCEDURE (rd: Reader) ReadPrev*, NEW, ABSTRACT;
+
+	PROCEDURE (rd: Reader) ReadChar* (OUT ch: CHAR), NEW, ABSTRACT;
+(*
+	BEGIN
+		rd.Read; ch := rd.char
+	END ReadChar;
+*)
+	PROCEDURE (rd: Reader) ReadPrevChar* (OUT ch: CHAR), NEW, ABSTRACT;
+(*
+	BEGIN
+		rd.ReadPrev; ch := rd.char
+	END ReadPrevChar;
+*)
+	PROCEDURE (rd: Reader) ReadView* (OUT v: Views.View), NEW, ABSTRACT;
+(*
+	BEGIN
+		REPEAT rd.Read UNTIL (rd.view # NIL) OR rd.eot;
+		v := rd.view
+	END ReadView;
+*)
+	PROCEDURE (rd: Reader) ReadPrevView* (OUT v: Views.View), NEW, ABSTRACT;
+(*
+	BEGIN
+		REPEAT rd.ReadPrev UNTIL (rd.view # NIL) OR rd.eot;
+		v := rd.view
+	END ReadPrevView;
+*)
+	PROCEDURE (rd: Reader) ReadRun* (OUT attr: Attributes), NEW, ABSTRACT;
+	(** post: rd.eot OR a # NIL, rd.view = ViewAt(rd.Pos() - 1) **)
+(*
+		VAR a: Attributes;
+	BEGIN
+		a := rd.attr;
+		REPEAT rd.Read UNTIL (rd.attr # a) OR (rd.view # NIL) OR rd.eot;
+		IF rd.eot THEN attr := NIL ELSE attr := rd.attr END
+	END ReadRun;
+*)
+	PROCEDURE (rd: Reader) ReadPrevRun* (OUT attr: Attributes), NEW, ABSTRACT;
+	(** post: rd.eot OR a # NIL, rd.view = ViewAt(rd.Pos()) **)
+(*
+		VAR a: Attributes;
+	BEGIN
+		a := rd.attr;
+		REPEAT rd.ReadPrev UNTIL (rd.attr # a) OR (rd.view # NIL) OR rd.eot;
+		IF rd.eot THEN attr := NIL ELSE attr := rd.attr END
+	END ReadPrevRun;
+*)
+
+	(** Writer **)
+
+	PROCEDURE (wr: Writer) Base* (): Model, NEW, ABSTRACT;
+	PROCEDURE (wr: Writer) SetPos* (pos: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (wr: Writer) Pos* (): INTEGER, NEW, ABSTRACT;
+	(* PROCEDURE (wr: Writer) WriteSChar* (ch: SHORTCHAR), NEW, ABSTRACT; *)
+	PROCEDURE (wr: Writer) WriteChar* (ch: CHAR), NEW, ABSTRACT;
+	PROCEDURE (wr: Writer) WriteView* (view: Views.View; w, h: INTEGER), NEW, ABSTRACT;
+
+	PROCEDURE (wr: Writer) SetAttr* (attr: Attributes), NEW(*, EXTENSIBLE*);
+	BEGIN
+		ASSERT(attr # NIL, 20); ASSERT(attr.init, 21); wr.attr := attr
+	END SetAttr;
+
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) New* (): Model, NEW, ABSTRACT;
+
+	PROCEDURE (d: Directory) NewFromString* (s: ARRAY OF CHAR): Model, NEW, EXTENSIBLE;
+		VAR m: Model; w: Writer; i: INTEGER;
+	BEGIN
+		m := d.New(); w := m.NewWriter(NIL);
+		i := 0; WHILE s[i] # 0X DO w.WriteChar(s[i]); INC(i) END;
+		RETURN m
+	END NewFromString;
+
+	PROCEDURE (d: Directory) SetAttr* (attr: Attributes), NEW, EXTENSIBLE;
+	BEGIN
+		ASSERT(attr.init, 20); d.attr := attr
+	END SetAttr;
+
+
+	(* StdModel - foundation *)
+
+	PROCEDURE OpenSpill (s: SpillFile);
+	BEGIN
+		s.file := Files.dir.Temp(); s.len := 0;
+		s.writer := s.file.NewWriter(NIL)
+	END OpenSpill;
+
+	PROCEDURE Find (t: StdModel; VAR pos: INTEGER; VAR u: Run; VAR off: INTEGER);
+	(* post: 0 <= pos <= t.len, 0 <= off < u.len, Pos(u) + off = pos *)
+	(* Read/Write rely on Find to force pos into the legal range *)
+		VAR v: Run; m: INTEGER;
+	BEGIN
+		IF pos < 0 THEN pos := 0 END;
+		IF pos >= t.len THEN
+			u := t.trailer; off := 0; t.pc.prev := t.trailer; t.pc.org := 0
+		ELSE
+			v := t.pc.prev.next; m := pos - t.pc.org;
+			IF m >= 0 THEN
+				WHILE m >= v.len DO DEC(m, v.len); v := v.next END
+			ELSE
+				WHILE m < 0 DO v := v.prev; INC(m, v.len) END
+			END;
+			u := v; off := m; t.pc.prev := v.prev; t.pc.org := pos - m
+		END
+	END Find;
+
+	PROCEDURE Split (off: INTEGER; VAR u, un: Run);
+	(* pre: 0 <= off <= u.len *)
+	(* post: u.len = off, u.len + un.len = u'.len, Pos(u) + u.len = Pos(un) *)
+		VAR lp: LPiece; sp: Piece;
+	BEGIN
+		IF off = 0 THEN un := u; u := un.prev	(* "split" at left edge of run *)
+		ELSIF off < u.len THEN	(* u.len > 1 => u IS LPiece; true split *)
+			WITH u: Piece DO
+				NEW(sp); sp^ := u^; INC(sp.org, off);
+				un := sp
+			ELSE	(* u IS LPiece) & ~(u IS Piece) *)
+				NEW(lp);
+				lp.prev := u.prev; lp.next := u.next; lp.len := u.len; lp.attr := u.attr;
+				lp.file := u(LPiece).file; lp.org := u(LPiece).org;
+				INC(lp.org, 2 * off);
+				un := lp
+			END;
+			DEC(un.len, off); DEC(u.len, un.len);
+			un.prev := u; un.next := u.next; un.next.prev := un; u.next := un
+		ELSIF off = u.len THEN un := u.next	(* "split" at right edge of run *)
+		ELSE HALT(100)
+		END
+	END Split;
+
+	PROCEDURE Merge (t: StdModel; u: Run; VAR v: Run);
+		VAR p, q: LPiece;
+	BEGIN
+		WITH u: Piece DO
+			IF (v IS Piece) & ((u.attr = v.attr) OR u.attr.Equals(v.attr)) THEN
+				p := u; q := v(Piece);
+				IF (p.file = q.file) & (p.org + p.len = q.org) THEN
+					IF t.pc.prev = p THEN INC(t.pc.org, q.len)
+					ELSIF t.pc.prev = q THEN t.pc.prev := t.trailer; t.pc.org := 0
+					END;
+					INC(p.len, q.len); v := v.next
+				END
+			END
+		| u: LPiece DO	(* ~(u IS Piece) *)
+			IF (v IS LPiece) & ~(v IS Piece) & ((u.attr = v.attr) OR u.attr.Equals(v.attr)) THEN
+				p := u(LPiece); q := v(LPiece);
+				IF (p.file = q.file) & (p.org + 2 * p.len = q.org) THEN
+					IF t.pc.prev = p THEN INC(t.pc.org, q.len)
+					ELSIF t.pc.prev = q THEN t.pc.prev := t.trailer; t.pc.org := 0
+					END;
+					INC(p.len, q.len); v := v.next
+				END
+			END
+		ELSE	(* ignore: can't merge ViewRef runs *)
+		END
+	END Merge;
+
+	PROCEDURE Splice (un, v, w: Run);	(* (u, un) -> (u, v ... w, un) *)
+		VAR u: Run;
+	BEGIN
+		IF v # w.next THEN	(* non-empty stretch v ... w *)
+			u := un.prev;
+			u.next := v; v.prev := u; un.prev := w; w.next := un
+		END
+	END Splice;
+
+	PROCEDURE NewContext (r: ViewRef; text: StdModel): StdContext;
+		VAR c: StdContext;
+	BEGIN
+		NEW(c); c.text := text; c.ref := r; 
+		Stores.Join(text, r.view);
+		RETURN c
+	END NewContext;
+
+	PROCEDURE CopyOfPiece (p: LPiece): LPiece;
+		VAR lp: LPiece; sp: Piece;
+	BEGIN
+		WITH p: Piece DO NEW(sp); sp^ := p^; RETURN sp
+		ELSE
+			NEW(lp);
+			lp.prev := p.prev; lp.next := p.next; lp.len := p.len; lp.attr := p.attr;
+			lp.file := p(LPiece).file; lp.org := p(LPiece).org; 
+			RETURN lp
+		END
+	END CopyOfPiece;
+
+	PROCEDURE CopyOfViewRef (r: ViewRef; text: StdModel): ViewRef;
+		VAR v: ViewRef;
+	BEGIN
+		NEW(v); v^ := r^;
+		v.view := Views.CopyOf(r.view, Views.deep);
+		v.view.InitContext(NewContext(v, text));
+		RETURN v
+	END CopyOfViewRef;
+
+	PROCEDURE InvalCache (t: StdModel; pos: INTEGER);
+		VAR n: INTEGER;
+	BEGIN
+		n := t.id MOD cacheWidth;
+		IF cache[n].id = t.id THEN
+			IF pos <= cache[n].beg THEN cache[n].beg := 0; cache[n].end := 0
+			ELSIF pos < cache[n].end THEN cache[n].end := pos
+			END
+		END
+	END InvalCache;
+
+	PROCEDURE StdInit (t: StdModel);
+		VAR u: Run;
+	BEGIN
+		IF t.trailer = NIL THEN
+			NEW(u); u.len := MAX(INTEGER); u.attr := NIL; u.next := u; u.prev := u;
+			t.len := 0; t.id := nextId; INC(nextId); t.era := 0; t.trailer := u;
+			t.pc.prev := u; t.pc.org := 0;
+			IF t.spill = NIL THEN NEW(t.spill) END
+		END
+	END StdInit;
+
+	PROCEDURE CopyOf (src: StdModel; beg, end: INTEGER; dst: StdModel): StdModel;
+		VAR buf: StdModel; u, v, r, z, zn: Run; ud, vd: INTEGER;
+	BEGIN
+		ASSERT(beg < end, 20);
+		buf := Containers.CloneOf(dst)(StdModel); 
+		ASSERT(buf.Domain() = NIL, 100);
+		Find(src, beg, u, ud); Find(src, end, v, vd);
+		z := buf.trailer; r := u;
+		WHILE r # v DO
+			WITH r: LPiece DO	(* Piece or LPiece *)
+				zn := CopyOfPiece(r); DEC(zn.len, ud);
+				IF zn IS Piece THEN INC(zn(LPiece).org, ud) ELSE INC(zn(LPiece).org, 2 * ud) END
+			| r: ViewRef DO
+				zn := CopyOfViewRef(r, buf)
+			ELSE (* ignore *)
+			END;
+			z.next := zn; zn.prev := z; z := zn; r := r.next; ud := 0
+		END;
+		IF vd > 0 THEN	(* v IS LPiece *)
+			zn := CopyOfPiece(v(LPiece)); zn.len := vd - ud;
+			IF zn IS Piece THEN INC(zn(LPiece).org, ud) ELSE INC(zn(LPiece).org, 2 * ud) END;
+			z.next := zn; zn.prev := z; z := zn
+		END;
+		z.next := buf.trailer; buf.trailer.prev := z;
+		buf.len := end - beg;
+		RETURN buf
+	END CopyOf;
+
+	PROCEDURE ProjectionOf (src: Model; beg, end: INTEGER; dst: StdModel): StdModel;
+	(* rider-conversion to eliminate covariance conflicts in binary operations *)
+		VAR buf: StdModel; rd: Reader; wr: Writer;
+	BEGIN
+		rd := src.NewReader(NIL); rd.SetPos(beg);
+		buf := Containers.CloneOf(dst)(StdModel); ASSERT(buf.Domain() = NIL, 100);
+		wr := buf.NewWriter(NIL);
+		WHILE beg < end DO
+			INC(beg);
+			rd.Read; wr.SetAttr(rd.attr);
+			IF rd.view # NIL THEN
+				wr.WriteView(Views.CopyOf(rd.view, Views.deep), rd.w, rd.h)
+			ELSE
+				wr.WriteChar(rd.char)
+			END
+		END;
+		RETURN buf
+	END ProjectionOf;
+
+	PROCEDURE Move (src: StdModel; beg, end: INTEGER; dest: StdModel; pos: INTEGER);
+		VAR pc: PieceCache; view: Views.View;
+			u, un, v, vn, w, wn: Run; ud, vd, wd: INTEGER;
+			(*initDom: BOOLEAN; newDom, dom: Stores.Domain;*)
+			upd: UpdateMsg; neut: Models.NeutralizeMsg;
+	BEGIN
+		Models.Broadcast(src, neut);
+		Find(src, beg, u, ud); Split(ud, u, un); pc := src.pc;
+		Find(src, end, v, vd); Split(vd, v, vn); src.pc := pc;
+		Merge(src, u, vn); u.next := vn; vn.prev := u;
+		DEC(src.len, end - beg);
+		InvalCache(src, beg);
+		INC(src.era);
+		upd.op := delete; upd.beg := beg; upd.end := beg + 1; upd.delta := beg - end;
+		Models.Broadcast(src, upd);
+		IF src = dest THEN
+			IF pos > end THEN DEC(pos, end - beg) END
+		ELSE
+			(*newDom := dest.Domain(); initDom := (src.Domain() = NIL) & (newDom # NIL);*)
+			w := un;
+			WHILE w # vn DO
+				(*
+				IF initDom THEN
+					dom := w.attr.Domain();
+					IF (dom # NIL) & (dom # newDom) THEN w.attr := Stores.CopyOf(w.attr)(Attributes) END;
+					Stores.InitDomain(w.attr, newDom)
+				END;
+				*)
+				IF ~Stores.Joined(dest, w.attr) THEN
+					IF ~Stores.Unattached(w.attr) THEN w.attr := Stores.CopyOf(w.attr)(Attributes) END;
+					Stores.Join(dest, w.attr)
+				END;
+				WITH w: ViewRef DO
+					view := w.view;
+					(*IF initDom THEN Stores.InitDomain(view, newDom) END;*)
+					Stores.Join(dest, view);
+					view.context(StdContext).text := dest
+				ELSE
+				END;
+				w := w.next
+			END
+		END;
+		Find(dest, pos, w, wd); Split(wd, w, wn); Splice(wn, un, v);
+		v := wn.prev; Merge(dest, v, wn); v.next := wn; wn.prev := v;
+		wn := w.next; Merge(dest, w, wn); w.next := wn; wn.prev := w;
+		INC(dest.len, end - beg);
+		InvalCache(dest, pos);
+		INC(dest.era);
+		upd.op := insert; upd.beg := pos; upd.end := pos + end - beg; upd.delta := end - beg;
+		Models.Broadcast(dest, upd)
+	END Move;
+
+
+	(* StdModel - operations *)
+
+	PROCEDURE (op: MoveOp) Do;
+		VAR src, dest: StdModel; beg, end, pos: INTEGER; neut: Models.NeutralizeMsg;
+	BEGIN
+		src := op.src; beg := op.beg; end := op.end; dest := op.dest; pos := op.pos;
+		IF src = dest THEN
+			IF pos < beg THEN
+				op.pos := end; op.beg := pos; op.end := pos + end - beg
+			ELSE
+				op.pos := beg; op.beg := pos - (end - beg); op.end := pos
+			END
+		ELSE
+			Models.Broadcast(op.src, neut);	(* destination is neutralized by sequencer *)
+			op.dest := src; op.src := dest;
+			op.pos := beg; op.beg := pos; op.end := pos + end - beg
+		END;
+		Move(src, beg, end, dest, pos)
+	END Do;
+
+	PROCEDURE DoMove (name: Stores.OpName;
+		src: StdModel; beg, end: INTEGER;
+		dest: StdModel; pos: INTEGER
+	);
+		VAR op: MoveOp;
+	BEGIN
+		IF (beg < end) & ((src # dest) OR ~((beg <= pos) & (pos <= end))) THEN
+			NEW(op);
+			op.src := src; op.beg := beg; op.end := end;
+			op.dest := dest; op.pos := pos;
+			Models.Do(dest, name, op)
+		END
+	END DoMove;
+
+
+	PROCEDURE (op: EditOp) Do;
+		VAR text: StdModel; (*newDom, dom: Stores.Domain;*) pc: PieceCache;
+			u, un, v, vn: Run; sp: Piece; lp: LPiece; r: ViewRef;
+			ud, vd, beg, end, pos, len: INTEGER; w, h: INTEGER;
+			upd: UpdateMsg;
+	BEGIN
+		text := op.text;
+		CASE op.mode OF
+		  deleteRange:
+			beg := op.beg; end := op.end; len := end - beg;
+			Find(text, beg, u, ud); Split(ud, u, un); pc := text.pc;
+			Find(text, end, v, vd); Split(vd, v, vn); text.pc := pc;
+			Merge(text, u, vn); u.next := vn; vn.prev := u;
+			DEC(text.len, len);
+			InvalCache(text, beg);
+			INC(text.era);
+			op.mode := moveBuf; op.canBunch := FALSE;
+			op.pos := beg; op.first := un; op.last := v; op.len := len;
+			upd.op := delete; upd.beg := beg; upd.end := beg + 1; upd.delta := -len;
+			Models.Broadcast(text, upd)
+		| moveBuf:
+			pos := op.pos;
+			Find(text, pos, u, ud); Split(ud, u, un); Splice(un, op.first, op.last);
+			INC(text.len, op.len);
+			InvalCache(text, pos);
+			INC(text.era);
+			op.mode := deleteRange;
+			op.beg := pos; op.end := pos + op.len;
+			upd.op := insert; upd.beg := pos; upd.end := pos + op.len; upd.delta := op.len;
+			Models.Broadcast(text, upd)
+		| writeSChar:
+			pos := op.pos;
+			InvalCache(text, pos);
+			Find(text, pos, u, ud); Split(ud, u, un);
+			IF (u.attr = op.attr) & (u IS Piece) & (u(Piece).file = text.spill.file)
+			& (u(Piece).org + u.len = op.len) THEN
+				INC(u.len);
+				IF text.pc.org >= pos THEN INC(text.pc.org) END
+			ELSE
+				(*
+				newDom := text.Domain();
+				IF newDom # NIL THEN
+					dom := op.attr.Domain();
+					IF (dom # NIL) & (dom # newDom) THEN
+						op.attr := Stores.CopyOf(op.attr)(Attributes)
+					END;
+					Stores.InitDomain(op.attr, newDom)
+				END;
+				*)
+				IF ~Stores.Joined(text, op.attr) THEN
+					IF ~Stores.Unattached(op.attr) THEN op.attr := Stores.CopyOf(op.attr)(Attributes) END;
+					Stores.Join(text, op.attr)
+				END;
+				NEW(sp); u.next := sp; sp.prev := u; sp.next := un; un.prev := sp;
+				sp.len := 1; sp.attr := op.attr;
+				sp.file := text.spill.file; sp.org := op.len;
+				IF text.pc.org > pos THEN INC(text.pc.org) END
+			END;
+			INC(text.len); INC(text.era);
+			op.mode := deleteRange;
+			upd.op := insert; upd.beg := pos; upd.end := pos + 1; upd.delta := 1;
+			Models.Broadcast(text, upd)
+		| writeChar:
+			pos := op.pos;
+			InvalCache(text, pos);
+			Find(text, pos, u, ud); Split(ud, u, un);
+			IF (u.attr = op.attr) & (u IS LPiece) & ~(u IS Piece) & (u(LPiece).file = text.spill.file)
+			& (u(LPiece).org + 2 * u.len = op.len) THEN
+				INC(u.len);
+				IF text.pc.org >= pos THEN INC(text.pc.org) END
+			ELSE
+				(*
+				newDom := text.Domain();
+				IF newDom # NIL THEN
+					dom := op.attr.Domain();
+					IF (dom # NIL) & (dom # newDom) THEN
+						op.attr := Stores.CopyOf(op.attr)(Attributes)
+					END;
+					Stores.InitDomain(op.attr, newDom)
+				END;
+				*)
+				IF ~Stores.Joined(text, op.attr) THEN
+					IF ~Stores.Unattached(op.attr) THEN op.attr := Stores.CopyOf(op.attr)(Attributes) END;
+					Stores.Join(text, op.attr)
+				END;
+				NEW(lp); u.next := lp; lp.prev := u; lp.next := un; un.prev := lp;
+				lp.len := 1; lp.attr := op.attr;
+				lp.file := text.spill.file; lp.org := op.len;
+				IF text.pc.org > pos THEN INC(text.pc.org) END
+			END;
+			INC(text.len); INC(text.era);
+			op.mode := deleteRange;
+			upd.op := insert; upd.beg := pos; upd.end := pos + 1; upd.delta := 1;
+			Models.Broadcast(text, upd)
+		| writeView:
+			pos := op.pos; r := op.first(ViewRef);
+			InvalCache(text, pos);
+			Find(text, pos, u, ud); Split(ud, u, un);
+			u.next := r; r.prev := u; r.next := un; un.prev := r;
+			INC(text.len); INC(text.era);
+			r.view.InitContext(NewContext(r, text));
+			(* Stores.InitDomain(r.view, text.Domain()); *)
+			Stores.Join(text, r.view);
+			w := r.w; h := r.h; r.w := defW; r.h := defH;
+			Properties.PreferredSize(r.view, minWidth, maxWidth, minHeight, maxHeight, defW, defH,
+				w, h
+			);
+			r.w := w; r.h := h;
+			op.mode := deleteRange;
+			upd.op := insert; upd.beg := pos; upd.end := pos + 1; upd.delta := 1;
+			Models.Broadcast(text, upd)
+		END
+	END Do;
+
+	PROCEDURE GetWriteOp (t: StdModel; pos: INTEGER; VAR op: EditOp; VAR bunch: BOOLEAN);
+		VAR last: Stores.Operation;
+	BEGIN
+		last := Models.LastOp(t);
+		IF (last # NIL) & (last IS EditOp) THEN
+			op := last(EditOp);
+			bunch := op.canBunch & (op.end = pos)
+		ELSE bunch := FALSE
+		END;
+		IF bunch THEN
+			INC(op.end)
+		ELSE
+			NEW(op); op.canBunch := TRUE;
+			op.text := t; op.beg := pos; op.end := pos + 1
+		END;
+		op.pos := pos
+	END GetWriteOp;
+
+
+	PROCEDURE SetPreferredSize (t: StdModel; v: Views.View);
+		VAR minW, maxW, minH, maxH, w, h: INTEGER;
+	BEGIN
+		t.GetEmbeddingLimits(minW, maxW, minH, maxH);
+		v.context.GetSize(w, h);
+		Properties.PreferredSize(v, minW, maxW, minH, maxH, w, h, w, h);
+		v.context.SetSize(w, h)
+	END SetPreferredSize;
+
+	PROCEDURE (op: SetAttrOp) Do;
+		VAR t: StdModel; attr: Attributes; z: AttrList; (*checkDom: BOOLEAN;*)
+			pc: PieceCache; u, un, v, vn: Run; ud, vd, pos, next: INTEGER;
+			upd: UpdateMsg;
+	BEGIN
+		t := op.text; z := op.list; pos := op.beg; (*checkDom := t.Domain() # NIL;*)
+		WHILE z # NIL DO
+			next := pos + z.len;
+			IF z.attr # NIL THEN
+				Find(t, pos, u, ud); Split(ud, u, un); pc := t.pc;
+				Find(t, next, v, vd); Split(vd, v, vn); t.pc := pc;
+				attr := un.attr;
+				WHILE un # vn DO
+					un.attr := z.attr;
+					(*
+					IF checkDom & (un.attr.Domain() # t.Domain()) THEN
+						IF un.attr.Domain() # NIL THEN un.attr := Stores.CopyOf(un.attr)(Attributes) END;
+						Stores.InitDomain(un.attr, t.Domain())
+					END;
+					*)
+					IF ~Stores.Joined(t, un.attr) THEN
+						IF ~Stores.Unattached(un.attr) THEN un.attr := Stores.CopyOf(un.attr)(Attributes) END;
+						Stores.Join(t, un.attr)
+					END;
+					Merge(t, u, un);
+					WITH un: ViewRef DO SetPreferredSize(t, un.view) ELSE END;
+					IF u.next = un THEN u := un; un := un.next ELSE u.next := un; un.prev := u END
+				END;
+				Merge(t, u, un); u.next := un; un.prev := u;
+				z.attr := attr
+			END;
+			pos := next; z := z.next
+		END;
+		INC(t.era);
+		upd.op := replace; upd.beg := op.beg; upd.end := pos; upd.delta := 0;
+		Models.Broadcast(t, upd)
+	END Do;
+
+
+	PROCEDURE (op: ResizeViewOp) Do;
+		VAR r: ViewRef; w, h: INTEGER; upd: UpdateMsg;
+	BEGIN
+		r := op.ref;
+		w := op.w; h := op.h;  op.w := r.w; op.h := r.h;  r.w := w; r.h := h;
+		INC(op.text.era);
+		upd.op := replace; upd.beg := op.pos; upd.end := op.pos + 1; upd.delta := 0;
+		Models.Broadcast(op.text, upd)
+	END Do;
+
+
+	PROCEDURE (op: ReplaceViewOp) Do;
+		VAR new: Views.View; upd: UpdateMsg;
+	BEGIN
+		new := op.new; op.new := op.ref.view; op.ref.view := new;
+		INC(op.text.era);
+		upd.op := replace; upd.beg := op.pos; upd.end := op.pos + 1; upd.delta := 0;
+		Models.Broadcast(op.text, upd)
+	END Do;
+
+
+	(* StdModel *)
+
+	PROCEDURE (t: StdModel) InitFrom (source: Containers.Model);
+	BEGIN
+		WITH source: StdModel DO
+			ASSERT(source.trailer # NIL, 20);
+			t.spill := source.spill;	(* reduce no of temp files: share spill files among clones *)
+			StdInit(t)
+		END
+	END InitFrom;
+
+	PROCEDURE WriteCharacters (t: StdModel; VAR wr: Stores.Writer);
+		VAR r: Files.Reader; u: Run; len: INTEGER;
+(*
+			sp: Properties.StorePref;
+*)
+			buf: ARRAY 1024 OF BYTE;
+	BEGIN
+		r := NIL;
+		u := t.trailer.next;
+		WHILE u # t.trailer DO
+			WITH u: Piece DO
+				r := u.file.NewReader(r); r.SetPos(u.org);
+				len := u.len;
+				WHILE len > LEN(buf) DO
+					r.ReadBytes(buf, 0, LEN(buf)); wr.rider.WriteBytes(buf, 0, LEN(buf));
+					DEC(len, LEN(buf))
+				END;
+				r.ReadBytes(buf, 0, len); wr.rider.WriteBytes(buf, 0, len)
+			| u: LPiece DO	(* ~(u IS Piece) *)
+				r := u.file.NewReader(r); r.SetPos(u.org);
+				len := 2 * u.len;
+				WHILE len > LEN(buf) DO
+					r.ReadBytes(buf, 0, LEN(buf)); wr.rider.WriteBytes(buf, 0, LEN(buf));
+					DEC(len, LEN(buf))
+				END;
+				r.ReadBytes(buf, 0, len); wr.rider.WriteBytes(buf, 0, len)
+			| u: ViewRef DO
+(*
+				sp.view := u.view; Views.HandlePropMsg(u.view, sp);
+				IF sp.view # NIL THEN wr.WriteSChar(viewcode) END
+*)
+				IF Stores.ExternalizeProxy(u.view) # NIL THEN
+					wr.WriteSChar(viewcode)
+				END
+			END;
+			u := u.next
+		END
+	END WriteCharacters;
+
+	PROCEDURE WriteAttributes (VAR wr: Stores.Writer; t: StdModel;
+		a: Attributes; VAR dict: AttrDict
+	);
+		VAR k, len: BYTE;
+	BEGIN
+		len := dict.len; k := 0; WHILE (k # len) & ~a.Equals(dict.attr[k]) DO INC(k) END;
+		wr.WriteByte(k);
+		IF k = len THEN
+			IF len < dictSize THEN dict.attr[len] := a; INC(dict.len) END;
+			(* ASSERT(Stores.Joined(t, a)); but bkwd-comp: *)
+			(* IF a.Domain() # d THEN always copy: bkwd-comp hack to avoid link *)
+				a := Stores.CopyOf(a)(Attributes); (* Stores.InitDomain(a, d); *) Stores.Join(t, a);
+			(* END; *)
+			WriteAttr(wr, a)
+		END
+	END WriteAttributes;
+
+	PROCEDURE (t: StdModel) Externalize (VAR wr: Stores.Writer);
+		VAR (*dom: Stores.Domain;*) u, v, un: Run;
+			attr: Attributes; dict: AttrDict;
+			org, runlen, pos: INTEGER; lchars: BOOLEAN;
+			inf: InfoMsg;
+	BEGIN
+		t.Externalize^(wr);
+		StdInit(t); (*dom := t.Domain();*)
+		wr.WriteVersion(0);
+		wr.WriteInt(0); org := wr.Pos();
+		u := t.trailer.next; v := t.trailer; dict.len := 0; lchars := FALSE;
+		WHILE u # v DO
+			attr := u.attr;
+			WITH u: Piece DO
+				runlen := u.len; un := u.next;
+				WHILE (un IS Piece) & un.attr.Equals(attr) DO
+					INC(runlen, un.len); un := un.next
+				END;
+				WriteAttributes(wr, t, attr, dict); wr.WriteInt(runlen)
+			| u: LPiece DO	(* ~(u IS Piece) *)
+				runlen := 2 * u.len; un := u.next;
+				WHILE (un IS LPiece) & ~(un IS Piece) & un.attr.Equals(attr) DO
+					INC(runlen, 2 * un.len); un := un.next
+				END;
+				WriteAttributes(wr, t, attr, dict); wr.WriteInt(-runlen);
+				lchars := TRUE
+			| u: ViewRef DO
+				IF Stores.ExternalizeProxy(u.view) # NIL THEN
+					WriteAttributes(wr, t, attr, dict); wr.WriteInt(0);
+					wr.WriteInt(u.w); wr.WriteInt(u.h); Views.WriteView(wr, u.view)
+				END;
+				un := u.next
+			END;
+			u := un
+		END;
+		wr.WriteByte(-1);
+		pos := wr.Pos();
+		wr.SetPos(org - 5);
+		IF lchars THEN wr.WriteVersion(maxStdModelVersion)
+		ELSE wr.WriteVersion(noLCharStdModelVersion)	(* version 0 did not support LONGCHAR *)
+		END;
+		wr.WriteInt(pos - org);
+		wr.SetPos(pos);
+		WriteCharacters(t, wr);
+		inf.op := store; Models.Broadcast(t, inf)
+	END Externalize;
+
+	PROCEDURE (t: StdModel) Internalize (VAR rd: Stores.Reader);
+		VAR u, un: Run; sp: Piece; lp: LPiece; v: ViewRef;
+			org, len: INTEGER; ano: BYTE; thisVersion: INTEGER;
+			attr: Attributes; dict: AttrDict;
+	BEGIN
+		ASSERT(t.Domain() = NIL, 20); ASSERT(t.len = 0, 21);
+		t.Internalize^(rd); IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdModelVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		StdInit(t);
+		dict.len := 0; u := t.trailer;
+		rd.ReadInt(len); org := rd.Pos() + len;
+		rd.ReadByte(ano);
+		WHILE ano # -1 DO
+			IF ano = dict.len THEN
+				ReadAttr(rd, attr); Stores.Join(t, attr);
+				IF dict.len < dictSize THEN dict.attr[dict.len] := attr; INC(dict.len) END
+			ELSE
+				attr := dict.attr[ano]
+			END;
+			rd.ReadInt(len);
+			IF len > 0 THEN	(* piece *)
+				NEW(sp); sp.len := len; sp.attr := attr;
+				sp.file := rd.rider.Base(); sp.org := org; un := sp;
+				INC(org, len)
+			ELSIF len < 0 THEN	(* longchar piece *)
+				len := -len; ASSERT(~ODD(len), 100);
+				NEW(lp); lp.len := len DIV 2; lp.attr := attr;
+				lp.file := rd.rider.Base(); lp.org := org; un := lp;
+				INC(org, len)
+			ELSE	(* len = 0  =>  embedded view *)
+				NEW(v); v.len := 1; v.attr := attr;
+				rd.ReadInt(v.w); rd.ReadInt(v.h); Views.ReadView(rd, v.view);
+				v.view.InitContext(NewContext(v, t));
+				un := v; INC(org)
+			END;
+			INC(t.len, un.len); u.next := un; un.prev := u; u := un;
+			rd.ReadByte(ano)
+		END;
+		rd.SetPos(org);
+		u.next := t.trailer; t.trailer.prev := u
+	END Internalize;
+
+(*
+	PROCEDURE (t: StdModel) PropagateDomain;
+		VAR u: Run; dom: Stores.Domain;
+	BEGIN
+		IF t.Domain() # NIL THEN
+			u := t.trailer.next;
+			WHILE u # t.trailer DO
+				dom := u.attr.Domain();
+				IF (dom # NIL) & (dom # t.Domain()) THEN u.attr := Stores.CopyOf(u.attr)(Attributes) END;
+				Stores.InitDomain(u.attr, t.Domain());
+				WITH u: ViewRef DO Stores.InitDomain(u.view, t.Domain()) ELSE END;
+				u := u.next
+			END
+		END
+	END PropagateDomain;
+*)
+
+	PROCEDURE (t: StdModel) GetEmbeddingLimits (OUT minW, maxW, minH, maxH: INTEGER);
+	BEGIN
+		minW := minWidth; maxW := maxWidth; minH := minHeight; maxH := maxHeight
+	END GetEmbeddingLimits;
+
+
+	PROCEDURE (t: StdModel) Length (): INTEGER;
+	BEGIN
+		StdInit(t);
+		RETURN t.len
+	END Length;
+
+	PROCEDURE (t: StdModel) NewReader (old: Reader): Reader;
+		VAR rd: StdReader;
+	BEGIN
+		StdInit(t);
+		IF (old # NIL) & (old IS StdReader) THEN rd := old(StdReader) ELSE NEW(rd) END;
+		IF rd.base # t THEN
+			rd.base := t; rd.era := -1; rd.SetPos(0)
+		ELSIF rd.pos > t.len THEN
+			rd.SetPos(t.len)
+		END;
+		rd.eot := FALSE;
+		RETURN rd
+	END NewReader;
+
+	PROCEDURE (t: StdModel) NewWriter (old: Writer): Writer;
+		VAR wr: StdWriter;
+	BEGIN
+		StdInit(t);
+		IF (old # NIL) & (old IS StdWriter) THEN wr := old(StdWriter) ELSE NEW(wr) END;
+		IF (wr.base # t) OR (wr.pos > t.len) THEN
+			wr.base := t; wr.era := -1; wr.SetPos(t.len)
+		END;
+		wr.SetAttr(dir.attr);
+		RETURN wr
+	END NewWriter;
+
+	PROCEDURE (t: StdModel) InsertCopy (pos: INTEGER; t0: Model; beg0, end0: INTEGER);
+		VAR buf: StdModel;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= pos, 21); ASSERT(pos <= t.len, 22);
+		ASSERT(0 <= beg0, 23); ASSERT(beg0 <= end0, 24); ASSERT(end0 <= t0.Length(), 25);
+		IF beg0 < end0 THEN
+			WITH t0: StdModel DO buf := CopyOf(t0, beg0, end0, t)
+			ELSE buf := ProjectionOf(t0, beg0, end0, t)
+			END;
+			(* IF t.Domain() # NIL THEN Stores.InitDomain(buf,t.Domain()) END; *)
+			Stores.Join(t, buf);
+			DoMove("#System:Copying", buf, 0, buf.len, t, pos)
+		END
+	END InsertCopy;
+
+	PROCEDURE (t: StdModel) Insert (pos: INTEGER; t0: Model; beg, end: INTEGER);
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= pos, 21); ASSERT(pos <= t.len, 22);
+		ASSERT(0 <= beg, 23); ASSERT(beg <= end, 24); ASSERT(end <= t0.Length(), 25);
+		IF beg < end THEN
+			IF (t.Domain() # NIL) & (t0 IS StdModel) & (t0.Domain() = t.Domain()) THEN
+				DoMove("#System:Moving", t0(StdModel), beg, end, t, pos)
+			ELSE	(* moving across domains *)
+				t.InsertCopy(pos, t0, beg, end); t0.Delete(beg, end)
+			END
+		END
+	END Insert;
+
+	PROCEDURE (t: StdModel) Append (t0: Model);
+		VAR len0: INTEGER;
+	BEGIN
+		StdInit(t);
+		ASSERT(t # t0, 20);
+		len0 := t0.Length();
+		IF len0 > 0 THEN
+			IF (t.Domain() # NIL) & (t0 IS StdModel) & (t0.Domain() = t.Domain()) THEN
+				DoMove("#Text:Appending", t0(StdModel), 0, len0, t, t.len)
+			ELSE	(* moving across domains *)
+				t.InsertCopy(t.len, t0, 0, len0); t0.Delete(0, len0)
+			END
+		END
+	END Append;
+
+	PROCEDURE (t: StdModel) Delete (beg, end: INTEGER);
+		VAR op: EditOp;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= beg, 20); ASSERT(beg <= end, 21); ASSERT(end <= t.len, 22);
+		IF beg < end THEN
+			NEW(op); op.mode := deleteRange; op.canBunch := FALSE;
+			op.text := t; op.beg := beg; op.end := end;
+			Models.Do(t, "#System:Deleting", op)
+		END
+	END Delete;
+
+	PROCEDURE (t: StdModel) SetAttr (beg, end: INTEGER; attr: Attributes);
+		VAR op: SetAttrOp; zp, z: AttrList;
+			u, v, w: Run; ud, vd: INTEGER; modified: BOOLEAN;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= beg, 20); ASSERT(beg <= end, 21); ASSERT(end <= t.len, 22);
+		IF beg < end THEN
+			NEW(op); op.text := t; op.beg := beg;
+			Find(t, beg, u, ud); Find(t, end, v, vd);
+			IF vd > 0 THEN w := v.next ELSE w := v END;
+			zp := NIL; modified := FALSE;
+			WHILE u # w DO
+				IF u = v THEN INC(ud, v.len - vd) END;
+				NEW(z); z.len := u.len - ud; z.attr := attr;
+				IF zp = NIL THEN op.list := z ELSE zp.next:= z END;
+				zp := z;
+				modified := modified OR ~u.attr.Equals(attr);
+				u := u.next; ud := 0
+			END;
+			IF modified THEN Models.Do(t, "#Text:AttributeChange", op) END
+		END
+	END SetAttr;
+
+	PROCEDURE (t: StdModel) Prop (beg, end: INTEGER): Properties.Property;
+		VAR p, q: Properties.Property; tp: Prop;
+			u, v, w: Run; ud, vd: INTEGER; equal: BOOLEAN;
+			rd: Reader;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= beg, 20); ASSERT(beg <= end, 21); ASSERT(end <= t.len, 22);
+		IF beg < end THEN
+			Find(t, beg, u, ud); Find(t, end, v, vd);
+			IF vd > 0 THEN w := v.next ELSE w := v END;
+			p := u.attr.Prop();
+			u := u.next;
+			WHILE u # w DO
+				Properties.Intersect(p, u.attr.Prop(), equal);
+				u := u.next
+			END;
+			IF beg + 1 = end THEN
+				t.rd := t.NewReader(t.rd); rd := t.rd;
+				rd.SetPos(beg); rd.Read;
+				IF (rd.view = NIL) OR (rd.char # viewcode) THEN
+					q := p; WHILE (q # NIL) & ~(q IS Prop) DO q := q.next END;
+					IF q # NIL THEN
+						tp := q(Prop)
+					ELSE NEW(tp); Properties.Insert(p, tp)
+					END;
+					INCL(tp.valid, code); INCL(tp.known, code); INCL(tp.readOnly, code);
+					tp.code := rd.char
+				END
+			END
+		ELSE p := NIL
+		END;
+		RETURN p
+	END Prop;
+
+	PROCEDURE (t: StdModel) Modify (beg, end: INTEGER; old, p: Properties.Property);
+		VAR op: SetAttrOp; zp, z: AttrList;
+			u, v, w: Run; ud, vd: INTEGER; equal, modified: BOOLEAN;
+			q: Properties.Property;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= beg, 20); ASSERT(beg <= end, 21); ASSERT(end <= t.len, 22);
+		IF (beg < end) & (p # NIL) THEN
+			NEW(op); op.text := t; op.beg := beg;
+			Find(t, beg, u, ud); Find(t, end, v, vd);
+			IF vd > 0 THEN w := v.next ELSE w := v END;
+			zp := NIL; modified := FALSE;
+			WHILE u # w DO
+				IF u = v THEN INC(ud, v.len - vd) END;
+				IF old # NIL THEN
+					q := u.attr.Prop();
+					Properties.Intersect(q, old, equal);	(* q := q * old *)
+					Properties.Intersect(q, old, equal)	(* equal := q = old *)
+				END;
+				NEW(z); z.len := u.len - ud;
+				IF (old = NIL) OR equal THEN
+					z.attr := ModifiedAttr(u.attr, p);
+					modified := modified OR ~u.attr.Equals(z.attr)
+				END;
+				IF zp = NIL THEN op.list := z ELSE zp.next := z END;
+				zp := z;
+				u := u.next; ud := 0
+			END;
+			IF modified THEN Models.Do(t, "#System:Modifying", op) END
+		END
+	END Modify;
+
+	PROCEDURE (t: StdModel) ReplaceView (old, new: Views.View);
+		VAR c: StdContext; op: ReplaceViewOp;
+	BEGIN
+		StdInit(t);
+		ASSERT(old.context # NIL, 20); ASSERT(old.context IS StdContext, 21);
+		ASSERT(old.context(StdContext).text = t, 22);
+		ASSERT((new.context = NIL) OR (new.context = old.context), 24);
+		IF new # old THEN
+			c := old.context(StdContext);
+			IF new.context = NIL THEN new.InitContext(c) END;
+			(* Stores.InitDomain(new, t.Domain()); *)
+			Stores.Join(t, new);
+			NEW(op); op.text := t; op.pos := c.Pos(); op.ref := c.ref; op.new := new;
+			Models.Do(t, "#System:Replacing", op)
+		END
+	END ReplaceView;
+
+	PROCEDURE (t: StdModel) CopyFrom- (source: Stores.Store);
+	BEGIN
+		StdInit(t);
+		WITH source: StdModel DO t.InsertCopy(0, source, 0, source.len) END
+	END CopyFrom;
+
+	PROCEDURE (t: StdModel) Replace (beg, end: INTEGER; t0: Model; beg0, end0: INTEGER);
+		VAR script: Stores.Operation;
+	BEGIN
+		StdInit(t);
+		ASSERT(0 <= beg, 20); ASSERT(beg <= end, 21); ASSERT(end <= t.len, 22);
+		ASSERT(0 <= beg0, 23); ASSERT(beg0 <= end0, 24); ASSERT(end0 <= t0.Length(), 25);
+		ASSERT(t # t0, 26);
+		Models.BeginScript(t, "#System:Replacing", script);
+		t.Delete(beg, end); t.Insert(beg, t0, beg0, end0);
+		Models.EndScript(t, script)
+	END Replace;
+
+
+	(* StdContext *)
+
+	PROCEDURE (c: StdContext) ThisModel (): Model;
+	BEGIN
+		RETURN c.text
+	END ThisModel;
+
+	PROCEDURE (c: StdContext) GetSize (OUT w, h: INTEGER);
+	BEGIN
+		w := c.ref.w; h := c.ref.h
+	END GetSize;
+
+	PROCEDURE (c: StdContext) SetSize (w, h: INTEGER);
+		VAR t: StdModel; r: ViewRef; op: ResizeViewOp;
+	BEGIN
+		t := c.text; r := c.ref;
+		IF w = Views.undefined THEN w := r.w END;
+		IF h = Views.undefined THEN h := r.h END;
+		Properties.PreferredSize(r.view, minWidth, maxWidth, minHeight, maxHeight, r.w, r.h, w, h);
+		IF (w # r.w) OR (h # r.h) THEN
+			NEW(op); op.text := t; op.pos := c.Pos(); op.ref := r; op.w := w; op.h := h;
+			Models.Do(t, "#System:Resizing", op)
+		END
+	END SetSize;
+
+	PROCEDURE (c: StdContext) Normalize (): BOOLEAN;
+	BEGIN
+		RETURN FALSE
+	END Normalize;
+
+	PROCEDURE (c: StdContext) Pos (): INTEGER;
+		VAR t: StdModel; u, r, w: Run; pos: INTEGER;
+	BEGIN
+		t := c.text; r := c.ref;
+		IF t.pc.prev.next # r THEN
+			u := t.trailer.next; w := t.trailer; pos := 0;
+			WHILE (u # r) & (u # w) DO INC(pos, u.len); u := u.next END;
+			ASSERT(u = r, 20);
+			t.pc.prev := r.prev; t.pc.org := pos
+		END;
+		RETURN t.pc.org
+	END Pos;
+
+	PROCEDURE (c: StdContext) Attr (): Attributes;
+	BEGIN
+		RETURN c.ref.attr
+	END Attr;
+
+
+	(* StdReader *)
+
+	PROCEDURE RemapView (rd: StdReader);
+		VAR p: Pref;
+	BEGIN
+		p.opts := {}; Views.HandlePropMsg(rd.view, p);
+		IF maskChar IN p.opts THEN rd.char := p.mask ELSE rd.char := viewcode END
+	END RemapView;
+
+	PROCEDURE Reset (rd: StdReader);
+		VAR t: StdModel;
+	BEGIN
+		t := rd.base;
+		Find(t, rd.pos, rd.run, rd.off); rd.era := t.era
+	END Reset;
+
+
+	PROCEDURE (rd: StdReader) Base (): Model;
+	BEGIN
+		RETURN rd.base
+	END Base;
+
+	PROCEDURE (rd: StdReader) SetPos (pos: INTEGER);
+	BEGIN
+		ASSERT(pos >= 0, 20); ASSERT(rd.base # NIL, 21); ASSERT(pos <= rd.base.len, 22);
+		rd.eot := FALSE; rd.attr := NIL; rd.char := 0X; rd.view := NIL;
+		IF (rd.pos # pos) OR (rd.run = rd.base.trailer) THEN
+			rd.pos := pos; rd.era := -1
+		END
+	END SetPos;
+
+	PROCEDURE (rd: StdReader) Pos (): INTEGER;
+	BEGIN
+		RETURN rd.pos
+	END Pos;
+
+	PROCEDURE (rd: StdReader) Read;
+		VAR t: StdModel; u: Run; n, pos, len: INTEGER; lc: ARRAY 2 OF BYTE;
+	BEGIN
+		t := rd.base;
+		n := t.id MOD cacheWidth;
+		IF rd.era # t.era THEN Reset(rd) END;
+		u := rd.run;
+		WITH u: Piece DO
+			rd.attr := u.attr;
+			pos := rd.pos MOD cacheLen;
+			IF ~((cache[n].id = t.id) & (cache[n].beg <= rd.pos) & (rd.pos < cache[n].end)) THEN
+				(* cache miss *)
+				IF cache[n].id # t.id THEN cache[n].id := t.id; cache[n].beg := 0; cache[n].end := 0 END;
+				len := cacheLine;
+				IF len > cacheLen - pos THEN len := cacheLen - pos END;
+				IF len > u.len - rd.off THEN len := u.len - rd.off END;
+				rd.reader := u.file.NewReader(rd.reader); rd.reader.SetPos(u.org + rd.off);
+				rd.reader.ReadBytes(cache[n].buf, pos, len);
+				IF rd.pos = cache[n].end THEN
+cache[n].end := rd.pos + len;
+(*
+					INC(cache[n].end, len);
+*)
+					IF cache[n].end - cache[n].beg >= cacheLen THEN
+						cache[n].beg := cache[n].end - (cacheLen - 1)
+					END
+				ELSE cache[n].beg := rd.pos; cache[n].end := rd.pos + len
+				END
+			END;
+			rd.char := CHR(cache[n].buf[pos] MOD 256); rd.view := NIL;
+			INC(rd.pos); INC(rd.off);
+			IF rd.off = u.len THEN rd.run := u.next; rd.off := 0 END
+		| u: LPiece DO	(* ~(u IS Piece) *)
+			rd.attr := u.attr;
+			rd.reader := u.file.NewReader(rd.reader); rd.reader.SetPos(u.org + rd.off * 2);
+			rd.reader.ReadBytes(lc, 0, 2);
+			rd.char := CHR(lc[0] MOD 256 + 256 * (lc[1] + 128)); rd.view := NIL;
+			IF (cache[n].id = t.id) & (rd.pos = cache[n].end) THEN
+cache[n].end := cache[n].end + 1;
+IF cache[n].end - cache[n].beg >= cacheLen THEN cache[n].beg := cache[n].beg + 1 END;
+(*
+				INC(cache[n].end);
+				IF cache[n].end - cache[n].beg >= cacheLen THEN INC(cache[n].beg) END
+*)
+			END;
+			INC(rd.pos); INC(rd.off);
+			IF rd.off = u.len THEN rd.run := u.next; rd.off := 0 END
+		| u: ViewRef DO
+			rd.attr := u.attr;
+			rd.view := u.view; rd.w := u.w; rd.h := u.h; RemapView(rd);
+			IF (cache[n].id = t.id) & (rd.pos = cache[n].end) THEN
+cache[n].end := cache[n].end + 1;
+IF cache[n].end - cache[n].beg >= cacheLen THEN cache[n].beg := cache[n].beg + 1 END;
+(*
+				INC(cache[n].end);
+				IF cache[n].end - cache[n].beg >= cacheLen THEN INC(cache[n].beg) END
+*)
+			END;
+			INC(rd.pos); rd.run := u.next; rd.off := 0
+		ELSE
+			rd.eot := TRUE; rd.attr := NIL; rd.char := 0X; rd.view := NIL
+		END
+	END Read;
+
+	PROCEDURE (rd: StdReader) ReadPrev;
+		VAR t: StdModel; u: Run; n, pos, len: INTEGER; lc: ARRAY 2 OF BYTE;
+	BEGIN
+		t := rd.base;
+		n := t.id MOD cacheWidth;
+		IF rd.era # t.era THEN Reset(rd) END;
+		IF rd.off > 0 THEN DEC(rd.off)
+		ELSIF rd.pos > 0 THEN
+			rd.run := rd.run.prev; rd.off := rd.run.len - 1
+		ELSE rd.run := t.trailer
+		END;
+		u := rd.run;
+		WITH u: Piece DO
+			rd.attr := u.attr;
+			DEC(rd.pos);
+			pos := rd.pos MOD cacheLen;
+			IF ~((cache[n].id = t.id) & (cache[n].beg <= rd.pos) & (rd.pos < cache[n].end)) THEN
+				(* cache miss *)
+				IF cache[n].id # t.id THEN cache[n].id := t.id; cache[n].beg := 0; cache[n].end := 0 END;
+				len := cacheLine;
+				IF len > pos + 1 THEN len := pos + 1 END;
+				IF len > rd.off + 1 THEN len := rd.off + 1 END;
+				rd.reader := u.file.NewReader(rd.reader);
+				rd.reader.SetPos(u.org + rd.off - (len - 1));
+				rd.reader.ReadBytes(cache[n].buf, pos - (len - 1), len);
+				IF rd.pos = cache[n].beg - 1 THEN
+cache[n].beg := cache[n] .beg - len;
+(*
+					DEC(cache[n].beg, len);
+*)
+					IF cache[n].end - cache[n].beg >= cacheLen THEN
+						cache[n].end := cache[n].beg + (cacheLen - 1)
+					END
+				ELSE cache[n].beg := rd.pos - (len - 1); cache[n].end := rd.pos + 1
+				END
+			END;
+			rd.char := CHR(cache[n].buf[pos] MOD 256); rd.view := NIL
+		| u: LPiece DO	(* ~(u IS Piece) *)
+			rd.attr := u.attr;
+			rd.reader := u.file.NewReader(rd.reader);
+			rd.reader.SetPos(u.org + 2 * rd.off);
+			rd.reader.ReadBytes(lc, 0, 2);
+			rd.char := CHR(lc[0] MOD 256 + 256 * (lc[1] + 128)); rd.view := NIL;
+			IF (cache[n].id = t.id) & (rd.pos = cache[n].beg) THEN
+cache[n].beg := cache[n].beg - 1;
+IF cache[n].end - cache[n].beg >= cacheLen THEN cache[n].end := cache[n].end - 1 END
+(*
+				DEC(cache[n].beg);
+				IF cache[n].end - cache[n].beg >= cacheLen THEN DEC(cache[n].end) END
+*)
+			END;
+			DEC(rd.pos)
+		| u: ViewRef DO
+			rd.attr := u.attr;
+			rd.view := u.view; rd.w := u.w; rd.h := u.h; RemapView(rd);
+			IF (cache[n].id = t.id) & (rd.pos = cache[n].beg) THEN
+cache[n].beg := cache[n].beg - 1;
+IF cache[n].end - cache[n].beg >= cacheLen THEN cache[n].end := cache[n].end - 1 END
+(*
+				DEC(cache[n].beg);
+				IF cache[n].end - cache[n].beg >= cacheLen THEN DEC(cache[n].end) END
+*)
+			END;
+			DEC(rd.pos)
+		ELSE
+			rd.eot := TRUE; rd.attr := NIL; rd.char := 0X; rd.view := NIL
+		END
+	END ReadPrev;
+
+	PROCEDURE (rd: StdReader) ReadChar (OUT ch: CHAR);
+	BEGIN
+		rd.Read; ch := rd.char
+	END ReadChar;
+
+	PROCEDURE (rd: StdReader) ReadPrevChar (OUT ch: CHAR);
+	BEGIN
+		rd.ReadPrev; ch := rd.char
+	END ReadPrevChar;
+
+	PROCEDURE (rd: StdReader) ReadView (OUT v: Views.View);
+		VAR t: StdModel; u: Run;
+	BEGIN
+		t := rd.base;
+		IF rd.era # t.era THEN Reset(rd) END;
+		DEC(rd.pos, rd.off);
+		u := rd.run;
+		WHILE u IS LPiece DO INC(rd.pos, u.len); u := u.next END;
+		WITH u: ViewRef DO
+			INC(rd.pos); rd.run := u.next; rd.off := 0;
+			rd.attr := u.attr; rd.view := u.view; rd.w := u.w; rd.h := u.h; RemapView(rd)
+		ELSE	(* u = t.trailer *)
+			ASSERT(u = t.trailer, 100);
+			rd.run := u; rd.off := 0;
+			rd.eot := TRUE; rd.attr := NIL; rd.char := 0X; rd.view := NIL
+		END;
+		v := rd.view
+	END ReadView;
+
+	PROCEDURE (rd: StdReader) ReadPrevView (OUT v: Views.View);
+		VAR t: StdModel; u: Run;
+	BEGIN
+		t := rd.base;
+		IF rd.era # t.era THEN Reset(rd) END;
+		DEC(rd.pos, rd.off);
+		u := rd.run.prev;
+		WHILE u IS LPiece DO DEC(rd.pos, u.len); u := u.prev END;
+		rd.run := u; rd.off := 0;
+		WITH u: ViewRef DO
+			DEC(rd.pos);
+			rd.attr := u.attr; rd.view := u.view; rd.w := u.w; rd.h := u.h; RemapView(rd)
+		ELSE	(* u = t.trailer *)
+			ASSERT(u = t.trailer, 100);
+			rd.eot := TRUE; rd.attr := NIL; rd.char := 0X; rd.view := NIL
+		END;
+		v := rd.view
+	END ReadPrevView;
+
+	PROCEDURE (rd: StdReader) ReadRun (OUT attr: Attributes);
+		VAR t: StdModel; a0: Attributes; u, trailer: Run; pos: INTEGER;
+	BEGIN
+		t := rd.base;
+		IF rd.era # t.era THEN Reset(rd) END;
+		a0 := rd.attr; u := rd.run; pos := rd.pos - rd.off; trailer := t.trailer;
+		WHILE (u.attr = a0) & ~(u IS ViewRef) & (u # trailer) DO
+			INC(pos, u.len); u := u.next
+		END;
+		rd.run := u; rd.pos := pos; rd.off := 0;
+		rd.Read;
+		attr := rd.attr
+	END ReadRun;
+	
+	PROCEDURE (rd: StdReader) ReadPrevRun (OUT attr: Attributes);
+		VAR t: StdModel; a0: Attributes; u, trailer: Run; pos: INTEGER;
+	BEGIN
+		t := rd.base;
+		IF rd.era # t.era THEN Reset(rd) END;
+		a0 := rd.attr; u := rd.run; pos := rd.pos - rd.off; trailer := t.trailer;
+		IF u # trailer THEN u := u.prev; DEC(pos, u.len) END;
+		WHILE (u.attr = a0) & ~(u IS ViewRef) & (u # trailer) DO
+			u := u.prev; DEC(pos, u.len)
+		END;
+		IF u # trailer THEN
+			rd.run := u.next; rd.pos := pos + u.len; rd.off := 0
+		ELSE
+			rd.run := trailer; rd.pos := 0; rd.off := 0
+		END;
+		rd.ReadPrev;
+		attr := rd.attr
+	END ReadPrevRun;
+
+
+	(* StdWriter *)
+
+	PROCEDURE WriterReset (wr: StdWriter);
+		VAR t: StdModel; u: Run; uo: INTEGER;
+	BEGIN
+		t := wr.base;
+		Find(t, wr.pos, u, uo); Split(uo, u, wr.run); wr.era := t.era
+	END WriterReset;
+
+	PROCEDURE (wr: StdWriter) Base (): Model;
+	BEGIN
+		RETURN wr.base
+	END Base;
+
+	PROCEDURE (wr: StdWriter) SetPos (pos: INTEGER);
+	BEGIN
+		ASSERT(pos >= 0, 20); ASSERT(wr.base # NIL, 21); ASSERT(pos <= wr.base.len, 22);
+		IF wr.pos # pos THEN
+			wr.pos := pos; wr.era := -1
+		END
+	END SetPos;
+
+	PROCEDURE (wr: StdWriter) Pos (): INTEGER;
+	BEGIN
+		RETURN wr.pos
+	END Pos;
+
+	PROCEDURE WriteSChar (wr: StdWriter; ch: SHORTCHAR);
+		VAR t: StdModel; u, un: Run; p: Piece; pos, spillPos: INTEGER;
+			op: EditOp; bunch: BOOLEAN;
+	BEGIN
+		t := wr.base; pos := wr.pos;
+		IF t.spill.file = NIL THEN OpenSpill(t.spill) END;
+		t.spill.writer.WriteByte(SHORT(ORD(ch))); spillPos := t.spill.len; t.spill.len := spillPos + 1;
+		IF (t.Domain() = NIL) OR (t.Domain().GetSequencer() = NIL) THEN
+			(* optimized for speed - writing to unbound text *)
+			InvalCache(t, pos);
+			IF wr.era # t.era THEN WriterReset(wr) END;
+			un := wr.run; u := un.prev;
+			IF (u.attr # NIL) & u.attr.Equals(wr.attr) & (u IS Piece) & (u(Piece).file = t.spill.file)
+			& (u(Piece).org + u.len = spillPos) THEN
+				INC(u.len);
+				IF t.pc.org >= pos THEN INC(t.pc.org) END
+			ELSE
+				NEW(p); u.next := p; p.prev := u; p.next := un; un.prev := p;
+				p.len := 1; p.attr := wr.attr;
+				p.file := t.spill.file; p.org := spillPos;
+				IF t.pc.org > pos THEN INC(t.pc.org) END;
+				IF ~Stores.Joined(t, p.attr) THEN
+					IF ~Stores.Unattached(p.attr) THEN p.attr := Stores.CopyOf(p.attr)(Attributes) END;
+					Stores.Join(t, p.attr)
+				END
+			END;
+			INC(t.era); INC(t.len);
+			INC(wr.era)
+		ELSE
+			GetWriteOp(t, pos, op, bunch);
+			IF (op.attr = NIL) OR ~op.attr.Equals(wr.attr) THEN op.attr := wr.attr END;
+			op.mode := writeSChar; (*op.attr := wr.attr;*) op.len := spillPos;
+			IF bunch THEN Models.Bunch(t) ELSE Models.Do(t, "#System:Inserting", op) END
+		END;
+		wr.pos := pos + 1
+	END WriteSChar;
+
+	PROCEDURE (wr: StdWriter) WriteChar (ch: CHAR);
+		VAR t: StdModel; u, un: Run; lp: LPiece; pos, spillPos: INTEGER;
+			fw: Files.Writer; op: EditOp; bunch: BOOLEAN;
+	BEGIN
+		IF (ch >= 20X) & (ch < 7FX)
+		OR (ch = tab) OR (ch = line) OR (ch = para)
+		OR (ch = zwspace) OR (ch = digitspace)
+		OR (ch = hyphen) OR (ch = nbhyphen) OR (ch >= 0A0X) & (ch < 100X) THEN
+			WriteSChar(wr, SHORT(ch))	(* could inline! *)
+		ELSIF ch = 200BX THEN wr.WriteChar(zwspace)
+		ELSIF ch = 2010X THEN wr.WriteChar(hyphen)
+		ELSIF ch = 2011X THEN wr.WriteChar(nbhyphen)
+		ELSIF ch >= 100X THEN
+			t := wr.base; pos := wr.pos;
+			IF t.spill.file = NIL THEN OpenSpill(t.spill) END;
+			fw := t.spill.writer;
+			fw.WriteByte(SHORT(SHORT(ORD(ch))));
+			fw.WriteByte(SHORT(SHORT(ORD(ch) DIV 256 - 128)));
+			spillPos := t.spill.len; t.spill.len := spillPos + 2;
+			IF (t.Domain() = NIL) OR (t.Domain().GetSequencer() = NIL) THEN
+				(* optimized for speed - writing to unbound text *)
+				InvalCache(t, pos);
+				IF wr.era # t.era THEN WriterReset(wr) END;
+				un := wr.run; u := un.prev;
+				IF (u.attr # NIL) & u.attr.Equals(wr.attr) & (u IS LPiece) & ~(u IS Piece) & (u(LPiece).file = t.spill.file)
+				& (u(LPiece).org + 2 * u.len = spillPos) THEN
+					INC(u.len);
+					IF t.pc.org >= pos THEN INC(t.pc.org) END
+				ELSE
+					NEW(lp); u.next := lp; lp.prev := u; lp.next := un; un.prev := lp;
+					lp.len := 1; lp.attr := wr.attr;
+					lp.file := t.spill.file; lp.org := spillPos;
+					IF t.pc.org > pos THEN INC(t.pc.org) END;
+					IF ~Stores.Joined(t, lp.attr) THEN
+						IF ~Stores.Unattached(lp.attr) THEN lp.attr := Stores.CopyOf(lp.attr)(Attributes) END;
+						Stores.Join(t, lp.attr)
+					END
+				END;
+				INC(t.era); INC(t.len);
+				INC(wr.era)
+			ELSE
+				GetWriteOp(t, pos, op, bunch);
+				IF (op.attr = NIL) OR ~op.attr.Equals(wr.attr) THEN op.attr := wr.attr END;
+				op.mode := writeChar; (*op.attr := wr.attr;*) op.len := spillPos;
+				IF bunch THEN Models.Bunch(t) ELSE Models.Do(t, "#System:Inserting", op) END
+			END;
+			wr.pos := pos + 1
+		END
+	END WriteChar;
+
+	PROCEDURE (wr: StdWriter) WriteView (view: Views.View; w, h: INTEGER);
+		VAR t: StdModel; u, un: Run; r: ViewRef; pos: INTEGER;
+			op: EditOp; bunch: BOOLEAN;
+	BEGIN
+		ASSERT(view # NIL, 20); ASSERT(view.context = NIL, 21);
+		t := wr.base; pos := wr.pos;
+		Stores.Join(t, view);
+		IF (t.Domain() = NIL) OR (t.Domain().GetSequencer() = NIL) THEN	
+																					(* optimized for speed - writing to unbound text *)
+			IF wr.era # t.era THEN WriterReset(wr) END;
+			InvalCache(t, pos);
+			NEW(r); r.len := 1; r.attr := wr.attr; r.view := view; r.w := defW; r.h := defH;
+			un := wr.run; u := un.prev; u.next := r; r.prev := u; r.next := un; un.prev := r;
+			IF t.pc.org > pos THEN INC(t.pc.org) END;
+			INC(t.era); INC(t.len);
+			view.InitContext(NewContext(r, t));
+			Properties.PreferredSize(view, minWidth, maxWidth, minHeight, maxHeight, defW, defH,
+				w, h
+			);
+			r.w := w; r.h := h;
+			INC(wr.era)
+		ELSE
+			NEW(r); r.len := 1; r.attr := wr.attr; r.view := view; r.w := w; r.h := h;
+			GetWriteOp(t, pos, op, bunch);
+			op.mode := writeView; op.first := r;
+			IF bunch THEN Models.Bunch(t) ELSE Models.Do(t, "#System:Inserting", op) END
+		END;
+		INC(wr.pos)
+	END WriteView;
+
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) New (): Model;
+		VAR t: StdModel;
+	BEGIN
+		NEW(t); StdInit(t); RETURN t
+	END New;
+
+
+	(** miscellaneous procedures **)
+(*
+	PROCEDURE DumpRuns* (t: Model);
+		VAR u: Run; n, i, beg, end: INTEGER; name: ARRAY 64 OF CHAR; r: Files.Reader; b: BYTE;
+	BEGIN
+		Sub.synch := FALSE;
+		WITH t: StdModel DO
+			u := t.trailer.next;
+			REPEAT
+				WITH u: Piece DO
+					Sub.String("short");
+					Sub.Int(u.len);
+					Sub.Char(" "); Sub.IntForm(SYSTEM.ADR(u.file^), 16, 8, "0", FALSE);
+					Sub.Int(u.org); Sub.Char(" ");
+					r := u.file.NewReader(NIL); r.SetPos(u.org); i := 0;
+					WHILE i < 16 DO r.ReadByte(b); Sub.Char(CHR(b)); INC(i) END;
+					Sub.Ln
+				| u: LPiece DO	(* ~(u IS Piece) *)
+					Sub.String("long");
+					Sub.Int(-u.len);
+					Sub.Char(" "); Sub.IntForm(SYSTEM.ADR(u.file^), 16, 8, "0", FALSE);
+					Sub.Int(u.org); Sub.Char(" ");
+					r := u.file.NewReader(NIL); r.SetPos(u.org); i := 0;
+					WHILE i < 16 DO r.ReadByte(b); Sub.Char(CHR(b)); INC(i) END;
+					Sub.Ln
+				| u: ViewRef DO
+					Sub.String("view");
+					Services.GetTypeName(u.view, name);
+					Sub.String(name); Sub.Int(u.w); Sub.Int(u.h); Sub.Ln
+				ELSE
+					Sub.Char("?"); Sub.Ln
+				END;
+				u := u.next
+			UNTIL u = t.trailer;
+			n := t.id MOD cacheWidth;
+			IF cache[n].id = t.id THEN
+				beg := cache[n].beg; end := cache[n].end;
+				Sub.Int(beg); Sub.Int(end); Sub.Ln;
+				Sub.Char("{");
+				WHILE beg < end DO Sub.Char(CHR(cache[n].buf[beg MOD cacheLen])); INC(beg) END;
+				Sub.Char("}"); Sub.Ln
+			ELSE Sub.String("not cached"); Sub.Ln
+			END
+		END
+	END DumpRuns;
+*)
+
+	PROCEDURE NewColor* (a: Attributes; color: Ports.Color): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.color}; stdProp.color.val := color;
+		RETURN ModifiedAttr(a, stdProp)
+	END NewColor;
+
+	PROCEDURE NewFont* (a: Attributes; font: Fonts.Font): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.typeface .. Properties.weight};
+		stdProp.typeface := font.typeface$;
+		stdProp.size := font.size;
+		stdProp.style.val := font.style;
+		stdProp.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
+		stdProp.weight := font.weight;
+		RETURN ModifiedAttr(a, stdProp)
+	END NewFont;
+
+	PROCEDURE NewOffset* (a: Attributes; offset: INTEGER): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		prop.valid := {0 (*global constant offset masked by param :-( *)}; prop.offset := offset;
+		RETURN ModifiedAttr(a, prop)
+	END NewOffset;
+
+	PROCEDURE NewTypeface* (a: Attributes; typeface: Fonts.Typeface): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.typeface}; stdProp.typeface := typeface;
+		RETURN ModifiedAttr(a, stdProp)
+	END NewTypeface;
+
+	PROCEDURE NewSize* (a: Attributes; size: INTEGER): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.size}; stdProp.size := size;
+		RETURN ModifiedAttr(a, stdProp)
+	END NewSize;
+
+	PROCEDURE NewStyle* (a: Attributes; style: SET): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.style}; stdProp.style.val := style; stdProp.style.mask := -{};
+		RETURN ModifiedAttr(a, stdProp)
+	END NewStyle;
+
+	PROCEDURE NewWeight* (a: Attributes; weight: INTEGER): Attributes;
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		stdProp.valid := {Properties.weight}; stdProp.weight := weight;
+		RETURN ModifiedAttr(a, stdProp)
+	END NewWeight;
+
+
+	PROCEDURE WriteableChar* (ch: CHAR): BOOLEAN;
+	(* must be identical to test in (StdWriter)WriteChar - inlined there for efficiency *)
+	BEGIN
+		RETURN
+			(ch >= 20X) & (ch < 7FX) OR
+			(ch = tab) OR (ch = line) OR (ch = para) OR
+			(ch = zwspace) OR (ch = digitspace) OR
+			(ch = hyphen) OR (ch = nbhyphen) OR
+			(ch >= 0A0X)	(* need to augment with test for valid Unicode *)
+	END WriteableChar;
+
+
+	PROCEDURE CloneOf* (source: Model): Model;
+	BEGIN
+		ASSERT(source # NIL, 20);
+		RETURN Containers.CloneOf(source)(Model)
+	END CloneOf;
+
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); ASSERT(d.attr # NIL, 21); ASSERT(d.attr.init, 22);
+		dir := d
+	END SetDir;
+
+
+	PROCEDURE Init;
+		VAR d: StdDirectory; a: Attributes;
+	BEGIN
+		NEW(a); a.InitFromProp(NIL);
+		NEW(stdProp); stdProp.known := -{};
+		NEW(prop); prop.known := -{};
+		NEW(d); stdDir := d; dir := d; d.SetAttr(a)
+	END Init;
+
+BEGIN
+	Init
+END TextModels.

+ 1676 - 0
BlackBox/Text/Mod/Rulers.txt

@@ -0,0 +1,1676 @@
+(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Rulers.odc *)
+(* DO NOT EDIT *)
+
+MODULE TextRulers;
+
+(**
+	project	= "BlackBox"
+	organization	= "www.oberon.ch"
+	contributors	= "Oberon microsystems"
+	version	= "System/Rsrc/About"
+	copyright	= "System/Rsrc/About"
+	license	= "Docu/BB-License"
+	changes	= ""
+	issues	= ""
+
+**)
+
+	(* re-check alien attributes: consider projection semantics *)
+
+	IMPORT
+		Kernel, Strings, Services, Fonts, Ports, Stores,
+		Models, Views, Controllers, Properties, Dialog,
+		TextModels;
+
+	CONST
+		(** Attributes.valid, Prop.known/valid **)	(* Mark.kind *)
+		first* = 0; left* = 1; right* = 2; lead* = 3; asc* = 4; dsc* = 5; grid* = 6;
+		opts* = 7; tabs* = 8;
+		(* additional values for icons held by Mark.kind *)
+		invalid = -1;
+		firstIcon = 10; lastIcon = 25;
+		rightToggle = 10;
+		gridDec = 12; gridVal = 13; gridInc = 14;
+		leftFlush = 16; centered = 17; rightFlush = 18; justified = 19;
+		leadDec = 21; leadVal = 22; leadInc = 23;
+		pageBrk = 25;
+
+		modeIcons = {leftFlush .. justified};
+		validIcons = {rightToggle, gridDec .. gridInc, leftFlush .. justified, leadDec .. leadInc, pageBrk};
+		fieldIcons = {gridVal, leadVal};
+
+		(** Attributes.opts **)
+		leftAdjust* = 0; rightAdjust* = 1;
+				(** both: fully justified; none: centered **)
+		noBreakInside* = 2; pageBreak* = 3; parJoin* = 4;
+				(** pageBreak of this ruler overrides parJoin request of previous ruler **)
+		rightFixed* = 5;	(** has fixed right border **)
+
+		options = {leftAdjust .. rightFixed};	(* options mask *)
+		adjMask = {leftAdjust, rightAdjust};
+
+		(** Attributes.tabType[i] **)
+		maxTabs* = 32;
+		centerTab* = 0; rightTab* = 1;
+			(** both: (reserved); none: leftTab **)
+		barTab* = 2;
+
+		tabOptions = {centerTab .. barTab};	(* mask for presently valid options *)
+
+		mm = Ports.mm; inch16 = Ports.inch DIV 16; point = Ports.point;
+		tabBarHeight = 11 * point; scaleHeight = 10 * point; iconBarHeight = 14 * point;
+		rulerHeight = tabBarHeight + scaleHeight + iconBarHeight;
+		iconHeight = 10 * point; iconWidth = 12 * point; iconGap = 2 * point;
+		iconPin = rulerHeight - (iconBarHeight - iconHeight) DIV 2;
+
+		rulerChangeKey = "#Text:RulerChange";
+
+		minVersion = 0;
+		maxAttrVersion = 2; maxStyleVersion = 0; maxStdStyleVersion = 0;
+		maxRulerVersion = 0; maxStdRulerVersion = 0;
+
+
+	TYPE
+		Tab* = RECORD
+			stop*: INTEGER;
+			type*: SET
+		END;
+
+		TabArray* = RECORD	(* should be POINTER TO ARRAY OF Tab -- but cannot protect *)
+			len*: INTEGER;
+			tab*: ARRAY maxTabs OF Tab
+		END;
+
+		Attributes* = POINTER TO EXTENSIBLE RECORD (Stores.Store)
+			init-: BOOLEAN;	(* immutable once init holds *)
+			first-, left-, right-, lead-, asc-, dsc-, grid-: INTEGER;
+			opts-: SET;
+			tabs-: TabArray
+		END;
+
+		AlienAttributes* = POINTER TO RECORD (Attributes)
+			store-: Stores.Alien
+		END;
+
+		Style* = POINTER TO ABSTRACT RECORD (Models.Model)
+			attr-: Attributes
+		END;
+
+		Ruler* = POINTER TO ABSTRACT RECORD (Views.View)
+			style-: Style
+		END;
+
+
+		Prop* = POINTER TO RECORD (Properties.Property)
+			first*, left*, right*, lead*, asc*, dsc*, grid*: INTEGER;
+			opts*: RECORD val*, mask*: SET END;
+			tabs*: TabArray
+		END;
+
+
+		UpdateMsg* = RECORD (Models.UpdateMsg)
+			(** domaincast upon style update **)
+			style*: Style;
+			oldAttr*: Attributes
+		END;
+
+
+		Directory* = POINTER TO ABSTRACT RECORD
+			attr-: Attributes
+		END;
+
+
+		StdStyle = POINTER TO RECORD (Style) END;
+
+		StdRuler = POINTER TO RECORD (Ruler)
+			sel: INTEGER;	(* sel # invalid => sel = kind of selected mark *)
+			px, py: INTEGER	(* sel # invalid => px, py of selected mark *)
+		END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+		Mark = RECORD
+			ruler: StdRuler;
+			l, r, t, b: INTEGER;
+			px, py, px0, py0, x, y: INTEGER;
+			kind, index: INTEGER;
+			type: SET;	(* valid if kind = tabs *)
+			tabs: TabArray;	(* if valid: tabs[index].type = type *)
+			dirty: BOOLEAN
+		END;
+
+		SetAttrOp = POINTER TO RECORD (Stores.Operation)
+			style: Style;
+			attr: Attributes
+		END;
+
+		NeutralizeMsg = RECORD (Views.Message) END;
+
+
+	VAR
+		dir-, stdDir-: Directory;
+
+		def: Attributes;
+		prop: Prop;	(* recycled *)
+		globRd: TextModels.Reader;	(* cache for temp reader; beware of reentrance *)
+		font: Fonts.Font;
+
+		marginGrid, minTabWidth, tabGrid: INTEGER;
+
+
+	PROCEDURE ^ DoSetAttrOp (s: Style; attr: Attributes);
+
+	PROCEDURE CopyTabs (IN src: TabArray; OUT dst: TabArray);
+	(* a TabArray is a 256 byte structure - copying of used parts is much faster than ":= all" *)
+		VAR i, n: INTEGER;
+	BEGIN
+		n := src.len; dst.len := n;
+		i := 0; WHILE i < n DO dst.tab[i] := src.tab[i]; INC(i) END
+	END CopyTabs;
+
+
+	(** Attributes **)
+
+	PROCEDURE (a: Attributes) CopyFrom- (source: Stores.Store), EXTENSIBLE;
+	BEGIN
+		WITH source: Attributes DO
+			ASSERT(~a.init, 20); ASSERT(source.init, 21);
+			a.init := TRUE;
+			a.first := source.first; a.left := source.left; a.right := source.right;
+			a.lead := source.lead; a.asc := source.asc; a.dsc := source.dsc; a.grid := source.grid;
+			a.opts := source.opts;
+			CopyTabs(source.tabs, a.tabs)
+		END
+	END CopyFrom;
+
+	PROCEDURE (a: Attributes) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	(** pre: a.init **)
+		VAR i: INTEGER; typedTabs: BOOLEAN;
+	BEGIN
+		ASSERT(a.init, 20);
+		a.Externalize^(wr);
+		i := 0; WHILE (i < a.tabs.len) & (a.tabs.tab[i].type = {}) DO INC(i) END;
+		typedTabs := i < a.tabs.len;
+		IF typedTabs THEN
+			wr.WriteVersion(maxAttrVersion)
+		ELSE
+			wr.WriteVersion(1)	(* versions before 2 had only leftTabs *)
+		END;
+		wr.WriteInt(a.first); wr.WriteInt(a.left); wr.WriteInt(a.right);
+		wr.WriteInt(a.lead); wr.WriteInt(a.asc); wr.WriteInt(a.dsc); wr.WriteInt(a.grid);
+		wr.WriteSet(a.opts);
+		wr.WriteXInt(a.tabs.len);
+		i := 0; WHILE i < a.tabs.len DO wr.WriteInt(a.tabs.tab[i].stop); INC(i) END;
+		IF typedTabs THEN
+			i := 0; WHILE i < a.tabs.len DO wr.WriteSet(a.tabs.tab[i].type); INC(i) END
+		END
+	END Externalize;
+
+	PROCEDURE (a: Attributes) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+	(** pre: ~a.init **)
+	(** post: a.init **)
+		VAR thisVersion, i, n, trash: INTEGER; trashSet: SET;
+	BEGIN
+		ASSERT(~a.init, 20); a.init := TRUE;
+		a.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxAttrVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadInt(a.first); rd.ReadInt(a.left); rd.ReadInt(a.right);
+		rd.ReadInt(a.lead); rd.ReadInt(a.asc); rd.ReadInt(a.dsc); rd.ReadInt(a.grid);
+		rd.ReadSet(a.opts);
+		rd.ReadXInt(n); a.tabs.len := MIN(n, maxTabs);
+		i := 0; WHILE i < a.tabs.len DO rd.ReadInt(a.tabs.tab[i].stop); INC(i) END;
+		WHILE i < n DO rd.ReadInt(trash); INC(i) END;
+		IF thisVersion = 0 THEN	(* convert from v0 rightFixed to v1 ~rightFixed default *)
+			INCL(a.opts, rightFixed)
+		END;
+		IF thisVersion >= 2 THEN
+			i := 0; WHILE i < a.tabs.len DO rd.ReadSet(a.tabs.tab[i].type); INC(i) END;
+			WHILE i < n DO rd.ReadSet(trashSet); INC(i) END
+		ELSE
+			i := 0; WHILE i < a.tabs.len DO a.tabs.tab[i].type := {}; INC(i) END
+		END
+	END Internalize;
+
+	PROCEDURE Set (p: Prop; opt: INTEGER; VAR x: INTEGER; min, max, new: INTEGER);
+	BEGIN
+		IF opt IN p.valid THEN x := MAX(min, MIN(max, new)) END
+	END Set;
+
+	PROCEDURE ModifyFromProp (a: Attributes; p: Properties.Property);
+		CONST maxW = 10000*mm; maxH = 32767 * point;
+		VAR i: INTEGER; type, mask: SET;
+	BEGIN
+		WHILE p # NIL DO
+			WITH p: Prop DO
+				Set(p, first, a.first, 0, maxW, p.first);
+				Set(p, left, a.left, 0, maxW, p.left);
+				Set(p, right, a.right, MAX(a.left, a.first), maxW, p.right);
+				Set(p, lead, a.lead, 0, maxH, p.lead);
+				Set(p, asc, a.asc, 0, maxH, p.asc);
+				Set(p, dsc, a.dsc, 0, maxH - a.asc, p.dsc);
+				Set(p, grid, a.grid, 1, maxH, p.grid);
+				IF opts IN p.valid THEN
+					a.opts := a.opts * (-p.opts.mask) + p.opts.val * p.opts.mask
+				END;
+				IF (tabs IN p.valid) & (p.tabs.len >= 0) THEN
+					IF (p.tabs.len > 0) & (p.tabs.tab[0].stop >= 0) THEN
+						i := 0; a.tabs.len := MIN(p.tabs.len, maxTabs);
+						REPEAT
+							a.tabs.tab[i].stop := p.tabs.tab[i].stop;
+							type := p.tabs.tab[i].type; mask := tabOptions;
+							IF type * {centerTab, rightTab} = {centerTab, rightTab} THEN
+								mask := mask - {centerTab, rightTab}
+							END;
+							a.tabs.tab[i].type := a.tabs.tab[i].type * (-mask) + type * mask;
+							INC(i)
+						UNTIL (i = a.tabs.len) OR (p.tabs.tab[i].stop < p.tabs.tab[i - 1].stop);
+						a.tabs.len := i
+					ELSE a.tabs.len := 0
+					END
+				END
+			ELSE
+			END;
+			p := p.next
+		END
+	END ModifyFromProp;
+
+	PROCEDURE (a: Attributes) ModifyFromProp- (p: Properties.Property), NEW, EXTENSIBLE;
+	BEGIN
+		ModifyFromProp(a, p)
+	END ModifyFromProp;
+
+	PROCEDURE (a: Attributes) InitFromProp* (p: Properties.Property), NEW, EXTENSIBLE;
+	(** pre: ~a.init **)
+	(** post: (a.init, p # NIL & x IN p.valid) => x set in a, else x defaults in a **)
+	BEGIN
+		ASSERT(~a.init, 20);
+		a.init := TRUE;
+		a.first := def.first; a.left := def.left; a.right := def.right;
+		a.lead := def.lead; a.asc := def.asc; a.dsc := def.dsc; a.grid := def.grid;
+		a.opts := def.opts;
+		CopyTabs(def.tabs, a.tabs);
+		ModifyFromProp(a, p)
+	END InitFromProp;
+
+	PROCEDURE (a: Attributes) Equals* (b: Attributes): BOOLEAN, NEW, EXTENSIBLE;
+	(** pre: a.init, b.init **)
+		VAR i: INTEGER;
+	BEGIN
+		ASSERT(a.init, 20); ASSERT(b.init, 21);
+		IF a # b THEN
+			i := 0;
+			WHILE (i < a.tabs.len)
+			 & (a.tabs.tab[i].stop = b.tabs.tab[i].stop)
+			 & (a.tabs.tab[i].type = b.tabs.tab[i].type) DO
+				INC(i)
+			END;
+			RETURN (Services.SameType(a, b))
+				& (a.first = b.first) & (a.left = b.left) & (a.right = b.right)
+				& (a.lead = b.lead) & (a.asc = b.asc) & (a.dsc = b.dsc) & (a.grid = b.grid)
+				& (a.opts = b.opts) & (a.tabs.len = b.tabs.len) & (i = a.tabs.len)
+		ELSE RETURN TRUE
+		END
+	END Equals;
+
+	PROCEDURE (a: Attributes) Prop* (): Properties.Property, NEW, EXTENSIBLE;
+	(** pre: a.init **)
+	(** post: x attr in a => x IN p.valid, m set to value of attr in a **)
+		VAR p: Prop;
+	BEGIN
+		ASSERT(a.init, 20);
+		NEW(p);
+		p.known := {first .. tabs}; p.valid := p.known;
+		p.first := a.first; p.left := a.left; p.right := a.right;
+		p.lead := a.lead; p.asc := a.asc; p.dsc := a.dsc; p.grid := a.grid;
+		p.opts.val := a.opts; p.opts.mask := options;
+		CopyTabs(a.tabs, p.tabs);
+		RETURN p
+	END Prop;
+
+
+	PROCEDURE ReadAttr* (VAR rd: Stores.Reader; OUT a: Attributes);
+		VAR st: Stores.Store; alien: AlienAttributes;
+	BEGIN
+		rd.ReadStore(st);
+		ASSERT(st # NIL, 100);
+		IF st IS Stores.Alien THEN
+			NEW(alien); alien.store := st(Stores.Alien); Stores.Join(alien, alien.store);
+			alien.InitFromProp(NIL); a := alien
+		ELSE a := st(Attributes)
+		END
+	END ReadAttr;
+
+	PROCEDURE WriteAttr* (VAR wr: Stores.Writer; a: Attributes);
+	BEGIN
+		ASSERT(a # NIL, 20); ASSERT(a.init, 21);
+		WITH a: AlienAttributes DO wr.WriteStore(a.store) ELSE wr.WriteStore(a) END
+	END WriteAttr;
+
+	PROCEDURE ModifiedAttr* (a: Attributes; p: Properties.Property): Attributes;
+	(** pre: a.init **)
+	(** post: x IN p.valid => x in new attr set to value in p, else set to value in a **)
+		VAR h: Attributes;
+	BEGIN
+		ASSERT(a.init, 20);
+		h := Stores.CopyOf(a)(Attributes); h.ModifyFromProp(p);
+		RETURN h
+	END ModifiedAttr;
+
+
+	(** AlienAttributes **)
+
+	PROCEDURE (a: AlienAttributes) Externalize- (VAR wr: Stores.Writer);
+	BEGIN
+		HALT(100)
+	END Externalize;
+
+	PROCEDURE (a: AlienAttributes) Internalize- (VAR rd: Stores.Reader);
+	BEGIN
+		HALT(100)
+	END Internalize;
+
+	PROCEDURE (a: AlienAttributes) InitFromProp* (p: Properties.Property);
+	BEGIN
+		a.InitFromProp^(NIL)
+	END InitFromProp;
+
+	PROCEDURE (a: AlienAttributes) ModifyFromProp- (p: Properties.Property);
+	BEGIN
+		(* a.InitFromProp^(NIL) *)
+		a.InitFromProp(NIL)
+	END ModifyFromProp;
+
+
+	(** Style **)
+
+(*
+	PROCEDURE (s: Style) PropagateDomain-, EXTENSIBLE;
+		VAR dom: Stores.Domain;
+	BEGIN
+		ASSERT(s.attr # NIL, 20);
+		dom := s.attr.Domain();
+		IF (dom # NIL) & (dom # s.Domain()) THEN s.attr := Stores.CopyOf(s.attr)(Attributes) END;
+		Stores.InitDomain(s.attr, s.Domain())
+	END PropagateDomain;
+*)
+
+	PROCEDURE (s: Style) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	BEGIN
+		s.Externalize^(wr);
+		wr.WriteVersion(maxStyleVersion);
+		WriteAttr(wr, s.attr)
+	END Externalize;
+
+	PROCEDURE (s: Style) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+		VAR thisVersion: INTEGER;
+	BEGIN
+		s.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStyleVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		ReadAttr(rd, s.attr); Stores.Join(s, s.attr)
+	END Internalize;
+
+	PROCEDURE (s: Style) SetAttr* (attr: Attributes), NEW, EXTENSIBLE;
+	(** pre: attr.init **)
+	(** post: s.attr = attr OR s.attr.Equals(attr) **)
+	BEGIN
+		ASSERT(attr.init, 20);
+		DoSetAttrOp(s, attr)
+	END SetAttr;
+
+	PROCEDURE (s: Style) CopyFrom- (source: Stores.Store), EXTENSIBLE;
+	BEGIN
+		WITH source: Style DO
+			ASSERT(source.attr # NIL, 21);
+			s.SetAttr(Stores.CopyOf(source.attr)(Attributes))
+				(* bkwd-comp hack to avoid link *)
+				(* copy would not be necessary if Attributes were immutable (and assigned to an Immutable Domain) *)
+		END
+	END CopyFrom;
+	
+(*
+	PROCEDURE (s: Style) InitFrom- (source: Models.Model), EXTENSIBLE;
+	BEGIN
+		WITH source: Style DO
+			ASSERT(source.attr # NIL, 21);
+			s.SetAttr(Stores.CopyOf(source.attr)(Attributes))
+				(* bkwd-comp hack to avoid link *)
+		END
+	END InitFrom;
+*)
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) SetAttr* (attr: Attributes), NEW, EXTENSIBLE;
+	(** pre: attr.init **)
+	(** post: d.attr = ModifiedAttr(attr, p)
+		[ p.valid = {opts, tabs}, p.tabs.len = 0, p.opts.mask = {noBreakInside.. parJoin}, p.opts.val = {} ]
+	**)
+		VAR p: Prop;
+	BEGIN
+		ASSERT(attr.init, 20);
+		IF attr.tabs.len > 0 THEN
+			NEW(p);
+			p.valid := {opts, tabs};
+			p.opts.mask := {noBreakInside, pageBreak, parJoin}; p.opts.val := {};
+			p.tabs.len := 0;
+			attr := ModifiedAttr(attr, p)
+		END;
+		d.attr := attr
+	END SetAttr;
+
+	PROCEDURE (d: Directory) NewStyle* (attr: Attributes): Style, NEW, ABSTRACT;
+	PROCEDURE (d: Directory) New* (style: Style): Ruler, NEW, ABSTRACT;
+
+	PROCEDURE (d: Directory) NewFromProp* (p: Prop): Ruler, NEW, EXTENSIBLE;
+	BEGIN
+		RETURN d.New(d.NewStyle(ModifiedAttr(d.attr, p)))
+	END NewFromProp;
+
+
+	PROCEDURE Deposit*;
+	BEGIN
+		Views.Deposit(dir.New(NIL))
+	END Deposit;
+
+
+	(** Ruler **)
+
+	PROCEDURE (r: Ruler) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		r.Externalize^(wr);
+		wr.WriteVersion(maxRulerVersion); wr.WriteStore(r.style)
+	END Externalize;
+	
+	PROCEDURE (r: Ruler) InitStyle* (s: Style), NEW;
+	(** pre: r.style = NIL, s # NIL, style.attr # NIL **)
+	(** post: r.style = s **)
+	BEGIN
+		ASSERT((r.style = NIL) OR (r.style = s), 20);
+		ASSERT(s # NIL, 21); ASSERT(s.attr # NIL, 22);
+		r.style := s; Stores.Join(r, s)
+	END InitStyle;
+	
+
+	PROCEDURE (r: Ruler) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+		VAR st: Stores.Store; thisVersion: INTEGER;
+	BEGIN
+		r.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxRulerVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadStore(st);
+		IF st IS Stores.Alien THEN rd.TurnIntoAlien(Stores.alienComponent); RETURN END;
+		r.InitStyle(st(Style))
+	END Internalize;
+
+(*
+	PROCEDURE (r: Ruler) InitModel* (m: Models.Model), EXTENSIBLE;
+	(** pre: r.style = NIL, m # NIL, style.attr # NIL, m IS Style **)
+	(** post: r.style = m **)
+	BEGIN
+		WITH m: Style DO
+			ASSERT((r.style = NIL) OR (r.style = m), 20);
+			ASSERT(m # NIL, 21); ASSERT(m.attr # NIL, 22);
+			r.style := m
+		ELSE HALT(23)
+		END
+	END InitModel;
+*)
+
+(*
+	PROCEDURE (r: Ruler) PropagateDomain-, EXTENSIBLE;
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		Stores.InitDomain(r.style, r.Domain())
+	END PropagateDomain;
+*)
+
+	PROCEDURE CopyOf* (r: Ruler; shallow: BOOLEAN): Ruler;
+		VAR v: Views.View;
+	BEGIN
+		ASSERT(r # NIL, 20);
+		v := Views.CopyOf(r, shallow); RETURN v(Ruler)
+	END CopyOf;
+
+
+	(** Prop **)
+
+	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
+		VAR valid: SET;  i: INTEGER; c, m: SET; eq: BOOLEAN;
+	BEGIN
+		WITH q: Prop DO
+			valid := p.valid * q.valid; equal := TRUE;
+			i := 0;
+			WHILE (i < p.tabs.len)
+			& (p.tabs.tab[i].stop = q.tabs.tab[i].stop)
+			& (p.tabs.tab[i].type = q.tabs.tab[i].type)
+			DO
+				INC(i)
+			END;
+			IF p.first # q.first THEN EXCL(valid, first) END;
+			IF p.left # q.left THEN EXCL(valid, left) END;
+			IF p.right # q.right THEN EXCL(valid, right) END;
+			IF p.lead # q.lead THEN EXCL(valid, lead) END;
+			IF p.asc # q.asc THEN EXCL(valid, asc) END;
+			IF p.dsc # q.dsc THEN EXCL(valid, dsc) END;
+			IF p.grid # q.grid THEN EXCL(valid, grid) END;
+			Properties.IntersectSelections(p.opts.val, p.opts.mask, q.opts.val, q.opts.mask, c, m, eq);
+			IF m = {} THEN EXCL(valid, opts)
+			ELSIF (opts IN valid) & ~eq THEN p.opts.mask := m; equal := FALSE
+			END;
+			IF (p.tabs.len # q.tabs.len) OR (q.tabs.len # i) THEN EXCL(valid, tabs) END;
+			IF p.valid # valid THEN p.valid := valid; equal := FALSE END
+		END
+	END IntersectWith;
+
+
+	(** ruler construction **)
+
+(*property-based facade procedures *)
+
+	PROCEDURE SetFirst* (r: Ruler; x: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {first}; prop.first := x;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetFirst;
+
+	PROCEDURE SetLeft* (r: Ruler; x: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {left}; prop.left := x;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetLeft;
+
+	PROCEDURE SetRight* (r: Ruler; x: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {right}; prop.right := x;
+		prop.opts.mask := {rightFixed}; prop.opts.val := {};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetRight;
+
+	PROCEDURE SetFixedRight* (r: Ruler; x: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {right, opts}; prop.right := x;
+		prop.opts.mask := {rightFixed}; prop.opts.val := {rightFixed};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetFixedRight;
+
+
+	PROCEDURE SetLead* (r: Ruler; h: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {lead}; prop.lead := h;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetLead;
+
+	PROCEDURE SetAsc* (r: Ruler; h: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {asc}; prop.asc := h;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetAsc;
+
+	PROCEDURE SetDsc* (r: Ruler; h: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {dsc}; prop.dsc := h;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetDsc;
+
+	PROCEDURE SetGrid* (r: Ruler; h: INTEGER);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {grid}; prop.grid := h;
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetGrid;
+
+
+	PROCEDURE SetLeftFlush* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {leftAdjust, rightAdjust}; prop.opts.val := {leftAdjust};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetLeftFlush;
+
+	PROCEDURE SetRightFlush* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {leftAdjust, rightAdjust}; prop.opts.val := {rightAdjust};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetRightFlush;
+
+	PROCEDURE SetCentered* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {leftAdjust, rightAdjust}; prop.opts.val := {};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetCentered;
+
+	PROCEDURE SetJustified* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {leftAdjust, rightAdjust}; prop.opts.val := {leftAdjust, rightAdjust};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetJustified;
+
+
+	PROCEDURE SetNoBreakInside* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {noBreakInside}; prop.opts.val := {noBreakInside};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetNoBreakInside;
+
+	PROCEDURE SetPageBreak* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {pageBreak}; prop.opts.val := {pageBreak};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetPageBreak;
+
+	PROCEDURE SetParJoin* (r: Ruler);
+	BEGIN
+		ASSERT(r.style # NIL, 20);
+		prop.valid := {opts};
+		prop.opts.mask := {parJoin}; prop.opts.val := {parJoin};
+		r.style.SetAttr(ModifiedAttr(r.style.attr, prop))
+	END SetParJoin;
+
+
+	PROCEDURE AddTab* (r: Ruler; x: INTEGER);
+		VAR ra: Attributes; i: INTEGER;
+	BEGIN
+		ASSERT(r.style # NIL, 20); ra := r.style.attr; i := ra.tabs.len; ASSERT(i < maxTabs, 21);
+		ASSERT((i = 0) OR (ra.tabs.tab[i - 1].stop < x), 22);
+		prop.valid := {tabs};
+		CopyTabs(ra.tabs, prop.tabs);
+		prop.tabs.tab[i].stop := x; prop.tabs.tab[i].type := {}; INC(prop.tabs.len);
+		r.style.SetAttr(ModifiedAttr(ra, prop))
+	END AddTab;
+
+	PROCEDURE MakeCenterTab* (r: Ruler);
+		VAR ra: Attributes; i: INTEGER;
+	BEGIN
+		ASSERT(r.style # NIL, 20); ra := r.style.attr; i := ra.tabs.len; ASSERT(i > 0, 21);
+		prop.valid := {tabs};
+		CopyTabs(ra.tabs, prop.tabs);
+		prop.tabs.tab[i - 1].type := prop.tabs.tab[i - 1].type + {centerTab} - {rightTab};
+		r.style.SetAttr(ModifiedAttr(ra, prop))
+	END MakeCenterTab;
+
+	PROCEDURE MakeRightTab* (r: Ruler);
+		VAR ra: Attributes; i: INTEGER;
+	BEGIN
+		ASSERT(r.style # NIL, 20); ra := r.style.attr; i := ra.tabs.len; ASSERT(i > 0, 21);
+		prop.valid := {tabs};
+		CopyTabs(ra.tabs, prop.tabs);
+		prop.tabs.tab[i - 1].type := prop.tabs.tab[i - 1].type - {centerTab} + {rightTab};
+		r.style.SetAttr(ModifiedAttr(ra, prop))
+	END MakeRightTab;
+
+	PROCEDURE MakeBarTab* (r: Ruler);
+		VAR ra: Attributes; i: INTEGER;
+	BEGIN
+		ASSERT(r.style # NIL, 20); ra := r.style.attr; i := ra.tabs.len; ASSERT(i > 0, 21);
+		prop.valid := {tabs};
+		CopyTabs(ra.tabs, prop.tabs);
+		prop.tabs.tab[i - 1].type := prop.tabs.tab[i - 1].type + {barTab};
+		r.style.SetAttr(ModifiedAttr(ra, prop))
+	END MakeBarTab;
+
+
+	(* SetAttrOp *)
+
+	PROCEDURE (op: SetAttrOp) Do;
+		VAR s: Style; attr: Attributes; upd: UpdateMsg;
+	BEGIN
+		s := op.style;
+		attr := s.attr; s.attr := op.attr; op.attr := attr;
+		(*Stores.InitDomain(s.attr, s.Domain());*) (* Stores.Join(s, s.attr); *)
+		ASSERT((s.attr=NIL) OR Stores.Joined(s, s.attr), 100);
+		upd.style := s; upd.oldAttr := attr; Models.Domaincast(s.Domain(), upd)
+	END Do;
+
+	PROCEDURE DoSetAttrOp (s: Style; attr: Attributes);
+		VAR op: SetAttrOp;
+	BEGIN
+		IF (s.attr # attr) OR ~s.attr.Equals(attr) THEN
+			(* IF attr.Domain() # s.Domain() THEN attr := Stores.CopyOf(attr)(Attributes) END; *)
+			IF ~Stores.Joined(s, attr) THEN
+				IF ~Stores.Unattached(attr) THEN attr := Stores.CopyOf(attr)(Attributes) END;
+				Stores.Join(s, attr)
+			END;
+			NEW(op); op.style := s; op.attr := attr;
+			Models.Do(s, rulerChangeKey, op)
+		END
+	END DoSetAttrOp;
+
+
+	(* grid definitions *)
+
+	PROCEDURE MarginGrid (x: INTEGER): INTEGER;
+	BEGIN
+		RETURN (x + marginGrid DIV 2) DIV marginGrid * marginGrid
+	END MarginGrid;
+
+	PROCEDURE TabGrid (x: INTEGER): INTEGER;
+	BEGIN
+		RETURN (x + tabGrid DIV 2) DIV tabGrid * tabGrid
+	END TabGrid;
+
+
+	(* nice graphical primitives *)
+
+	PROCEDURE DrawCenteredInt (f: Views.Frame; x, y, n: INTEGER);
+		VAR sw: INTEGER; s: ARRAY 32 OF CHAR;
+	BEGIN
+		Strings.IntToString(n, s); sw := font.StringWidth(s);
+		f.DrawString(x - sw DIV 2, y, Ports.defaultColor, s, font)
+	END DrawCenteredInt;
+
+	PROCEDURE DrawNiceRect (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR u: INTEGER;
+	BEGIN
+		u := f.dot;
+		f.DrawRect(l, t, r - u, b - u, 0, Ports.defaultColor);
+		f.DrawLine(l + u, b - u, r - u, b - u, u, Ports.grey25);
+		f.DrawLine(r - u, t + u, r - u, b - u, u, Ports.grey25)
+	END DrawNiceRect;
+
+	PROCEDURE DrawScale (f: Views.Frame; l, t, r, b, clipL, clipR: INTEGER);
+		VAR u, h, x, px, sw: INTEGER; i, n, d1, d2: INTEGER; s: ARRAY 32 OF CHAR;
+	BEGIN
+		f.DrawRect(l, t, r, b, Ports.fill, Ports.grey12);
+		u := f.dot;
+		IF Dialog.metricSystem THEN d1 := 2; d2 := 10 ELSE d1 := 2; d2 := 16 END;
+		DEC(b, point);
+		sw := 2*u + font.StringWidth("8888888888");
+		x := l + tabGrid; i := 0; n := 0;
+		WHILE x <= r DO
+			INC(i); px := TabGrid(x);
+			IF i = d2 THEN
+				h := 6*point; i := 0; INC(n);
+				IF (px >= clipL - sw) & (px < clipR) THEN
+					Strings.IntToString(n, s);
+					f.DrawString(px - 2*u - font.StringWidth(s), b - 3*point, Ports.defaultColor, s, font)
+				END
+			ELSIF i MOD d1 = 0 THEN
+				h := 2*point
+			ELSE
+				h := 0
+			END;
+			IF (px >= clipL) & (px < clipR) & (h > 0) THEN
+				f.DrawLine(px, b, px, b - h, 0, Ports.defaultColor)
+			END;
+			INC(x, tabGrid)
+		END
+	END DrawScale;
+
+	PROCEDURE InvertTabMark (f: Views.Frame; l, t, r, b: INTEGER; type: SET; show: BOOLEAN);
+		VAR u, u2, u3, yc, i, ih: INTEGER;
+	BEGIN
+		u := f.dot; u2 := 2*u; u3 := 3*u;
+		IF ~ODD((r - l) DIV u) THEN DEC(r, u) END;
+		yc := l + (r - l) DIV u DIV 2 * u;
+		IF barTab IN type THEN
+			f.MarkRect(yc, b - u3, yc + u, b - u2, Ports.fill, Ports.invert, show);
+			f.MarkRect(yc, b - u, yc + u, b, Ports.fill, Ports.invert, show)
+		END;
+		IF centerTab IN type THEN
+			f.MarkRect(l + u, b - u2, r - u, b - u, Ports.fill, Ports.invert, show)
+		ELSIF rightTab IN type THEN
+			f.MarkRect(l, b - u2, yc + u, b - u, Ports.fill, Ports.invert, show)
+		ELSE
+			f.MarkRect(yc, b - u2, r, b - u, Ports.fill, Ports.invert, show)
+		END;
+		DEC(b, u3); INC(l, u2); DEC(r, u2);
+		ih := (r - l) DIV 2;
+		i := b - t; t := b - u;
+		WHILE (i > 0) & (r > l) DO
+			DEC(i, u);
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.invert, show);
+			IF i <= ih THEN INC(l, u); DEC(r, u) END;
+			DEC(t, u); DEC(b, u)
+		END
+	END InvertTabMark;
+
+	PROCEDURE InvertFirstMark (f: Views.Frame; l, t, r, b: INTEGER; show: BOOLEAN);
+		VAR u, i, ih: INTEGER;
+	BEGIN
+		u := f.dot;
+		i := b - t; t := b - u;
+		ih := r - l;
+		WHILE (i > 0) & (r > l) DO
+			DEC(i, u);
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.invert, show);
+			IF i <= ih THEN DEC(r, u) END;
+			DEC(t, u); DEC(b, u)
+		END
+	END InvertFirstMark;
+
+	PROCEDURE InvertLeftMark (f: Views.Frame; l, t, r, b: INTEGER; show: BOOLEAN);
+		VAR u, i, ih: INTEGER;
+	BEGIN
+		u := f.dot;
+		i := b - t; b := t + u;
+		ih := r - l;
+		WHILE (i > 0) & (r > l) DO
+			DEC(i, u);
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.invert, show);
+			IF i <= ih THEN DEC(r, u) END;
+			INC(t, u); INC(b, u)
+		END
+	END InvertLeftMark;
+
+	PROCEDURE InvertRightMark (f: Views.Frame; l, t, r, b: INTEGER; show: BOOLEAN);
+		VAR u, i, ih: INTEGER;
+	BEGIN
+		u := f.dot;
+		IF ~ODD((b - t) DIV u) THEN INC(t, u) END;
+		ih := r - l; l := r - u;
+		i := b - t; b := t + u;
+		WHILE (i > 0) & (i > ih) DO
+			DEC(i, u);
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.invert, show);
+			DEC(l, u);
+			INC(t, u); INC(b, u)
+		END;
+		WHILE (i > 0) & (r > l) DO
+			DEC(i, u);
+			f.MarkRect(l, t, r, b, Ports.fill, Ports.invert, show);
+			INC(l, u);
+			INC(t, u); INC(b, u)
+		END
+	END InvertRightMark;
+
+
+	(* marks *)
+
+	PROCEDURE SetMark (VAR m: Mark; r: StdRuler; px, py: INTEGER; kind, index: INTEGER);
+	BEGIN
+		m.ruler := r; m.kind := kind;
+		m.px := px; m.py := py;
+		CASE kind OF
+		  first:
+			m.l := px; m.r := m.l + 4*point;
+			m.b := py - 7*point; m.t := m.b - 4*point
+		| left:
+			m.l := px; m.r := m.l + 4*point;
+			m.b := py - 2*point; m.t := m.b - 4*point
+		| right:
+			m.r := px; m.l := m.r - 4*point;
+			m.b := py - 3*point; m.t := m.b - 7*point
+		| tabs:
+			m.l := px - 4*point; m.r := m.l + 9*point;
+			m.b := py - 5*point; m.t := m.b - 6*point;
+			m.type := r.style.attr.tabs.tab[index].type
+		| firstIcon .. lastIcon:
+			m.l := px; m.r := px + iconWidth;
+			m.t := py; m.b := py + iconHeight
+		ELSE HALT(100)
+		END
+	END SetMark;
+
+	PROCEDURE Try (VAR m: Mark; r: StdRuler; px, py, x, y: INTEGER; kind, index: INTEGER);
+	BEGIN
+		IF m.kind = invalid THEN
+			SetMark(m, r, px, py, kind, index);
+			IF (m.l - point <= x) & (x < m.r + point) & (m.t - point <= y) & (y < m.b + point) THEN
+				m.px0 := m.px; m.py0 := m.py; m.x := x; m.y := y;
+				IF kind = tabs THEN
+					m.index := index; CopyTabs(r.style.attr.tabs, m.tabs)
+				END
+			ELSE
+				m.kind := invalid
+			END
+		END
+	END Try;
+
+	PROCEDURE InvertMark (VAR m: Mark; f: Views.Frame; show: BOOLEAN);
+	(* pre: kind # invalid *)
+	BEGIN
+		CASE m.kind OF
+		  first: InvertFirstMark(f, m.l, m.t, m.r, m.b, show)
+		| left: InvertLeftMark(f, m.l, m.t, m.r, m.b, show)
+		| right: InvertRightMark(f, m.l, m.t, m.r, m.b, show)
+		| tabs: InvertTabMark(f, m.l, m.t, m.r, m.b, m.type, show)
+		END
+	END InvertMark;
+
+	PROCEDURE HiliteMark (VAR m: Mark; f: Views.Frame; show: BOOLEAN);
+	BEGIN
+		f.MarkRect(m.l, m.t, m.r - point, m.b - point, Ports.fill, Ports.hilite, show)
+	END HiliteMark;
+
+	PROCEDURE HiliteThisMark (r: StdRuler; f: Views.Frame; kind: INTEGER; show: BOOLEAN);
+		VAR m: Mark; px,  w, h: INTEGER;
+	BEGIN
+		IF (kind # invalid) & (kind IN validIcons) THEN
+			px := iconGap + (kind - firstIcon) * (iconWidth + iconGap);
+			r.context.GetSize(w, h);
+			SetMark(m, r, px, h - iconPin, kind, -1);
+			HiliteMark(m, f, show)
+		END
+	END HiliteThisMark;
+
+	PROCEDURE DrawMark (VAR m: Mark; f: Views.Frame);
+	(* pre: kind # invalid *)
+		VAR a: Attributes; l, t, r, b, y, d, e, asc, dsc, fw: INTEGER; i: INTEGER;
+			w: ARRAY 4 OF INTEGER;
+	BEGIN
+		a := m.ruler.style.attr;
+		l := m.l + 2 * point; t := m.t + 2 * point; r := m.r - 4 * point; b := m.b - 3 * point;
+		font.GetBounds(asc, dsc, fw);
+		y := (m.t + m.b + asc) DIV 2;
+		w[0] := (r - l) DIV 2; w[1] := r - l; w[2] := (r - l) DIV 3; w[3] := (r - l) * 2 DIV 3;
+		CASE m.kind OF
+		  rightToggle:
+			IF rightFixed IN a.opts THEN
+				d := 0; y := (t + b) DIV 2 - point; e := (l + r) DIV 2 + point;
+				WHILE t < y DO
+					f.DrawLine(e - d, t, e, t, point, Ports.defaultColor); INC(d, point); INC(t, point)
+				END;
+				WHILE t < b DO
+					f.DrawLine(e - d, t, e, t, point, Ports.defaultColor); DEC(d, point); INC(t, point)
+				END
+			ELSE
+				DEC(b, point);
+				f.DrawLine(l, t, r, t, point, Ports.defaultColor);
+				f.DrawLine(l, b, r, b, point, Ports.defaultColor);
+				f.DrawLine(l, t, l, b, point, Ports.defaultColor);
+				f.DrawLine(r, t, r, b, point, Ports.defaultColor)
+			END
+		| gridDec:
+			WHILE t < b DO f.DrawLine(l, t, r, t, point, Ports.defaultColor); INC(t, 2 * point) END
+		| gridVal:
+			DrawCenteredInt(f, (l + r) DIV 2, y, a.grid DIV point)
+		| gridInc:
+			WHILE t < b DO f.DrawLine(l, t, r, t, point, Ports.defaultColor); INC(t, 3 * point) END
+		| leftFlush:
+			i := 0;
+			WHILE t < b DO
+				d := w[i]; i := (i + 1) MOD LEN(w);
+				f.DrawLine(l, t, l + d, t, point, Ports.defaultColor); INC(t, 2 * point)
+			END
+		| centered:
+			i := 0;
+			WHILE t < b DO
+				d := (r - l - w[i]) DIV 2; i := (i + 1) MOD LEN(w);
+				f.DrawLine(l + d, t, r - d, t, point, Ports.defaultColor); INC(t, 2 * point)
+			END
+		| rightFlush:
+			i := 0;
+			WHILE t < b DO
+				d := w[i]; i := (i + 1) MOD LEN(w);
+				f.DrawLine(r - d, t, r, t, point, Ports.defaultColor); INC(t, 2 * point)
+			END
+		| justified:
+			WHILE t < b DO f.DrawLine(l, t, r, t, point, Ports.defaultColor); INC(t, 2 * point) END
+		| leadDec:
+			f.DrawLine(l, t, l, t + point, point, Ports.defaultColor); INC(t, 2 * point);
+			WHILE t < b DO f.DrawLine(l, t, r, t, point, Ports.defaultColor); INC(t, 2 * point) END
+		| leadVal:
+			DrawCenteredInt(f, (l + r) DIV 2, y, m.ruler.style.attr.lead DIV point)
+		| leadInc:
+			f.DrawLine(l, t, l, t + 3 * point, point, Ports.defaultColor); INC(t, 4 * point);
+			WHILE t < b DO f.DrawLine(l, t, r, t, point, Ports.defaultColor); INC(t, 2 * point) END
+		| pageBrk:
+			DEC(b, point);
+			IF pageBreak IN a.opts THEN
+				y := (t + b) DIV 2 - point;
+				f.DrawLine(l, t, l, y, point, Ports.defaultColor);
+				f.DrawLine(r, t, r, y, point, Ports.defaultColor);
+				f.DrawLine(l, y, r, y, point, Ports.defaultColor);
+				INC(y, 2 * point);
+				f.DrawLine(l, y, r, y, point, Ports.defaultColor);
+				f.DrawLine(l, y, l, b, point, Ports.defaultColor);
+				f.DrawLine(r, y, r, b, point, Ports.defaultColor)
+			ELSE
+				f.DrawLine(l, t, l, b, point, Ports.defaultColor);
+				f.DrawLine(r, t, r, b, point, Ports.defaultColor)
+			END
+		ELSE
+			HALT(100)
+		END;
+		IF ~(m.kind IN {gridVal, leadVal}) THEN
+			DrawNiceRect(f, m.l, m.t, m.r, m.b)
+		END
+	END DrawMark;
+
+	PROCEDURE GetMark (VAR m: Mark; r: StdRuler; f: Views.Frame;
+		x, y: INTEGER; canCreate: BOOLEAN
+	);
+	(* pre: ~canCreate OR (f # NIL) *)
+		VAR a: Attributes; px,  w, h: INTEGER; i: INTEGER;
+	BEGIN
+		m.kind := invalid; m.dirty := FALSE;
+		a := r.style.attr;
+		r.context.GetSize(w, h);
+
+		(* first try scale *)
+		Try(m, r, a.first, h, x, y, first, 0);
+		Try(m, r, a.left, h, x, y, left, 0);
+		IF rightFixed IN a.opts THEN
+			Try(m, r, a.right, h, x, y, right, 0)
+		END;
+		i := 0;
+		WHILE (m.kind = invalid) & (i < a.tabs.len) DO
+			Try(m, r, a.tabs.tab[i].stop, h, x, y, tabs, i);
+			INC(i)
+		END;
+		IF (m.kind = invalid) & (y >= h - tabBarHeight) & (a.tabs.len < maxTabs) THEN
+			i := 0; px := TabGrid(x);
+			WHILE (i < a.tabs.len) & (a.tabs.tab[i].stop < px) DO INC(i) END;
+			IF (i = 0) OR (px - a.tabs.tab[i - 1].stop >= minTabWidth) THEN
+				IF (i = a.tabs.len) OR (a.tabs.tab[i].stop - px >= minTabWidth) THEN
+					IF canCreate THEN	(* set new tab stop, initially at end of list *)
+						m.kind := tabs; m.index := a.tabs.len; m.dirty := TRUE;
+						CopyTabs(a.tabs, m.tabs); m.tabs.len := a.tabs.len + 1;
+						m.tabs.tab[a.tabs.len].stop := px; m.tabs.tab[a.tabs.len].type := {};
+						a.tabs.tab[a.tabs.len].stop := px; a.tabs.tab[a.tabs.len].type := {};
+						SetMark(m, r, px, h, tabs, m.index); InvertMark(m, f, Ports.show);
+						m.px0 := m.px; m.py0 := m.py; m.x := x; m.y := y
+					END
+				END
+			END
+		END;
+
+		(* next try icon bar *)
+		px := iconGap; i := firstIcon;
+		WHILE i <= lastIcon DO
+			IF i IN validIcons THEN
+				Try(m, r, px, h - iconPin, x, y, i, 0)
+			END;
+			INC(px, iconWidth + iconGap); INC(i)
+		END
+	END GetMark;
+
+	PROCEDURE SelectMark (r: StdRuler; f: Views.Frame; IN m: Mark);
+	BEGIN
+		r.sel := m.kind; r.px := m.px; r.py := m.py
+	END SelectMark;
+
+	PROCEDURE DeselectMark (r: StdRuler; f: Views.Frame);
+	BEGIN
+		HiliteThisMark(r, f, r.sel, Ports.hide); r.sel := invalid
+	END DeselectMark;
+
+
+	(* mark interaction *)
+
+	PROCEDURE Mode (r: StdRuler): INTEGER;
+		VAR a: Attributes; i: INTEGER;
+	BEGIN
+		a := r.style.attr;
+		IF a.opts * adjMask = {leftAdjust} THEN
+			i := leftFlush
+		ELSIF a.opts * adjMask = {} THEN
+			i := centered
+		ELSIF a.opts * adjMask = {rightAdjust} THEN
+			i := rightFlush
+		ELSE (* a.opts * adjMask = adjMask *)
+			i := justified
+		END;
+		RETURN i
+	END Mode;
+
+	PROCEDURE GrabMark (VAR m: Mark; r: StdRuler; f: Views.Frame; x, y: INTEGER);
+	BEGIN
+		GetMark(m, r, f, x, y, TRUE);
+		DeselectMark(r, f);
+		IF m.kind = Mode(r) THEN m.kind := invalid END
+	END GrabMark;
+
+	PROCEDURE TrackMark (VAR m: Mark; f: Views.Frame; x, y: INTEGER; modifiers: SET);
+		VAR px, py,  w, h: INTEGER;
+	BEGIN
+		IF m.kind # invalid THEN
+			px := m.px + x - m.x; py := m.py + y - m.y;
+			IF m.kind = tabs THEN
+				px := TabGrid(px)
+			ELSIF m.kind IN validIcons THEN
+				IF (m.l <= x) & (x < m.r) THEN px := 1 ELSE px := 0 END
+			ELSE
+				px := MarginGrid(px)
+			END;
+			IF m.kind IN {right, tabs} THEN
+				m.ruler.context.GetSize(w, h);
+				IF (0 <= y) & (y < h + scaleHeight) OR (Controllers.extend IN modifiers) THEN 
+					py := h
+				ELSE
+					py := -1	(* moved mark out of ruler: delete tab stop or fixed right margin *)
+				END
+			ELSIF m.kind IN validIcons THEN
+				IF (m.t <= y) & (y < m.b) THEN py := 1 ELSE py := 0 END
+			ELSE
+				py := MarginGrid(py)
+			END;
+			IF (m.kind IN {right, tabs}) & ((m.px # px) OR (m.py # py)) THEN
+				INC(m.x, px - m.px); INC(m.y, py - m.py);
+				InvertMark(m, f, Ports.hide); SetMark(m, m.ruler, px, py, m.kind, m.index);
+				InvertMark(m, f, Ports.show);
+				m.dirty := TRUE
+			ELSIF (m.kind IN {first, left}) & (m.px # px) THEN
+				INC(m.x, px - m.px);
+				InvertMark(m, f, Ports.hide); SetMark(m, m.ruler, px, m.py, m.kind, m.index);
+				InvertMark(m, f, Ports.show)
+			ELSIF (m.kind IN validIcons) & (m.px * m.py # px * py) THEN
+				HiliteMark(m, f, Ports.show);
+				IF m.kind IN modeIcons THEN HiliteThisMark(m.ruler, f, Mode(m.ruler), Ports.hide) END;
+				m.px := px; m.py := py
+			END
+		END
+	END TrackMark;
+
+	PROCEDURE ShiftMarks (a: Attributes; p: Prop; mask: SET; x0, dx: INTEGER);
+		VAR new: SET; i, j, t0, t1: INTEGER; tab0, tab1: TabArray;
+	BEGIN
+		new := mask - p.valid;
+		IF first IN new THEN p.first := a.first END;
+		IF tabs IN new THEN CopyTabs(a.tabs, p.tabs) END;
+		p.valid := p.valid + mask;
+		IF first IN mask THEN INC(p.first, dx) END;
+		IF tabs IN mask THEN
+			i := 0;
+			WHILE (i < p.tabs.len) & (p.tabs.tab[i].stop < x0) DO tab0.tab[i] := p.tabs.tab[i]; INC(i) END;
+			t0 := i;
+			t1 := 0;
+			WHILE i < p.tabs.len DO
+				tab1.tab[t1].stop := p.tabs.tab[i].stop + dx;
+				tab1.tab[t1].type := p.tabs.tab[i].type;
+				INC(t1); INC(i)
+			END;
+			i := 0; j := 0; p.tabs.len := 0;
+			WHILE i < t0 DO	(* merge sort *)
+				WHILE (j < t1) & (tab1.tab[j].stop < tab0.tab[i].stop) DO
+					p.tabs.tab[p.tabs.len] := tab1.tab[j]; INC(p.tabs.len); INC(j)
+				END;
+				IF (j < t1) & (tab1.tab[j].stop = tab0.tab[i].stop) THEN INC(j) END;
+				p.tabs.tab[p.tabs.len] := tab0.tab[i]; INC(p.tabs.len); INC(i)
+			END;
+			WHILE j < t1 DO
+				p.tabs.tab[p.tabs.len] := tab1.tab[j]; INC(p.tabs.len); INC(j)
+			END
+		END
+	END ShiftMarks;
+
+	PROCEDURE ShiftDependingMarks (VAR m: Mark; p: Prop);
+		VAR a: Attributes; dx: INTEGER;
+	BEGIN
+		a := m.ruler.style.attr; dx := m.px - m.px0;
+		CASE m.kind OF
+		  first: ShiftMarks(a, p, {tabs}, 0, dx)
+		| left: ShiftMarks(a, p, {first, tabs}, 0, dx)
+		| tabs: ShiftMarks(a, p, {tabs}, m.px0, dx)
+		ELSE
+		END
+	END ShiftDependingMarks;
+
+	PROCEDURE AdjustMarks (VAR m: Mark; f: Views.Frame; modifiers: SET);
+		VAR r: StdRuler; a: Attributes; p: Prop;
+			g: INTEGER; i, j: INTEGER; shift: BOOLEAN; type: SET;
+	BEGIN
+		r := m.ruler;
+		IF  (m.kind # invalid) & (m.kind IN validIcons)
+				& (m.px = 1) & (m.py = 1)
+		OR (m.kind # invalid) & ~(m.kind IN validIcons)
+				& ((m.px # m.px0) OR (m.py # m.py0)
+					OR (m.kind = tabs) (*(m.tabs.len # r.style.attr.tabs.len)*) )
+		THEN
+			a := r.style.attr; NEW(p);
+			p.valid := {};
+			shift := (Controllers.modify IN modifiers) & (m.tabs.len = r.style.attr.tabs.len);
+			CASE m.kind OF
+			  first:
+				p.valid := {first}; p.first := m.px
+			| left:
+				p.valid := {left}; p.left := m.px
+			| right:
+				IF m.py >= 0 THEN
+					p.valid := {right}; p.right := m.px
+				ELSE
+					p.valid := {opts}; p.opts.val := {}; p.opts.mask := {rightFixed}
+				END
+			| tabs:
+				IF ~m.dirty THEN
+					p.valid := {tabs}; CopyTabs(m.tabs, p.tabs);
+					i := m.index; type := m.tabs.tab[i].type;
+					IF shift THEN
+						type := type * {barTab};
+						IF type = {} THEN type := {barTab}
+						ELSE type := {}
+						END;
+						p.tabs.tab[i].type := p.tabs.tab[i].type - {barTab} + type
+					ELSE
+						type := type * {centerTab, rightTab};
+						IF type = {} THEN type := {centerTab}
+						ELSIF type = {centerTab} THEN type := {rightTab}
+						ELSE type := {}
+						END;
+						p.tabs.tab[i].type := p.tabs.tab[i].type - {centerTab, rightTab} + type
+					END
+				ELSIF ~shift THEN
+					p.valid := {tabs}; p.tabs.len := m.tabs.len - 1;
+					i := 0;
+					WHILE i < m.index DO p.tabs.tab[i] := m.tabs.tab[i]; INC(i) END;
+					INC(i);
+					WHILE i < m.tabs.len DO p.tabs.tab[i - 1] := m.tabs.tab[i]; INC(i) END;
+					i := 0;
+					WHILE (i < p.tabs.len) & (p.tabs.tab[i].stop < m.px) DO INC(i) END;
+					IF (m.px >= MIN(a.first, a.left)) & (m.px <= f.r) & (m.py >= 0)
+					 & ((i = 0) OR (m.px - p.tabs.tab[i - 1].stop >= minTabWidth))
+					 & ((i = p.tabs.len) OR (p.tabs.tab[i].stop - m.px >= minTabWidth)) THEN
+						j := p.tabs.len;
+						WHILE j > i DO p.tabs.tab[j] := p.tabs.tab[j - 1]; DEC(j) END;
+						p.tabs.tab[i].stop := m.px; p.tabs.tab[i].type := m.tabs.tab[m.index].type;
+						INC(p.tabs.len)
+					END;
+					i := 0;
+					WHILE (i < p.tabs.len)
+					 & (p.tabs.tab[i].stop = a.tabs.tab[i].stop)
+					 & (p.tabs.tab[i].type = a.tabs.tab[i].type) DO
+						INC(i)
+					END;
+					IF (i = p.tabs.len) & (p.tabs.len = a.tabs.len) THEN RETURN END	(* did not change *)
+				END
+			| rightToggle:
+				p.valid := {right, opts};
+				IF ~(rightFixed IN a.opts) THEN
+					p.right := f.r DIV marginGrid * marginGrid
+				END;
+				p.opts.val := a.opts / {rightFixed}; p.opts.mask := {rightFixed}
+			| gridDec:
+				p.valid := {asc, grid}; g := a.grid - point;
+				IF g = 0 THEN p.grid := 1; p.asc := 0 ELSE p.grid := g; p.asc := g - a.dsc END
+			| gridVal:
+				SelectMark(r, f, m); RETURN
+			| gridInc:
+				p.valid := {asc, grid}; g := a.grid + point; DEC(g, g MOD point);
+				p.grid := g; p.asc := g - a.dsc
+			| leftFlush:
+				p.valid := {opts}; p.opts.val := {leftAdjust}; p.opts.mask := adjMask
+			| centered:
+				p.valid := {opts}; p.opts.val := {}; p.opts.mask := adjMask
+			| rightFlush:
+				p.valid := {opts}; p.opts.val := {rightAdjust}; p.opts.mask := adjMask
+			| justified:
+				p.valid := {opts}; p.opts.val := adjMask; p.opts.mask := adjMask
+			| leadDec:
+				p.valid := {lead}; p.lead := a.lead - point
+			| leadVal:
+				SelectMark(r, f, m); RETURN
+			| leadInc:
+				p.valid := {lead}; p.lead := a.lead + point
+			| pageBrk:
+				p.valid := {opts}; p.opts.val := a.opts / {pageBreak}; p.opts.mask := {pageBreak}
+			ELSE HALT(100)
+			END;
+			IF shift THEN ShiftDependingMarks(m, p) END;
+			IF m.kind IN validIcons - modeIcons THEN HiliteMark(m, f, Ports.hide) END;
+
+			r.style.SetAttr(ModifiedAttr(a, p))
+		END
+	END AdjustMarks;
+
+
+	(* primitivies for standard ruler *)
+
+	PROCEDURE Track (r: StdRuler; f: Views.Frame; IN msg: Controllers.TrackMsg);
+		VAR m: Mark; x, y, res: INTEGER; modifiers: SET; isDown: BOOLEAN;
+			cmd: ARRAY 128 OF CHAR;
+	BEGIN
+		GrabMark(m, r, f, msg.x, msg.y);
+		REPEAT
+			f.Input(x, y, modifiers, isDown); TrackMark(m, f, x, y, modifiers)
+		UNTIL ~isDown;
+		AdjustMarks(m, f, modifiers);
+		IF Controllers.doubleClick IN msg.modifiers THEN
+			CASE m.kind OF
+			| invalid:
+				Dialog.MapString("#Text:OpenRulerDialog", cmd); Dialog.Call(cmd, "", res)
+			| gridVal, leadVal:
+				Dialog.MapString("#Text:OpenSizeDialog", cmd); Dialog.Call(cmd, "", res)
+			ELSE
+			END
+		END
+	END Track;
+
+	PROCEDURE Edit (r: StdRuler; f: Views.Frame; VAR msg: Controllers.EditMsg);
+		VAR v: Views.View;
+	BEGIN
+		CASE msg.op OF
+		  Controllers.copy:
+			msg.view := Views.CopyOf(r, Views.deep);
+			msg.isSingle := TRUE
+		| Controllers.paste:
+			v := msg.view;
+			WITH v: Ruler DO r.style.SetAttr(v.style.attr) ELSE END
+		ELSE
+		END
+	END Edit;
+
+	PROCEDURE PollOps (r: StdRuler; f: Views.Frame; VAR msg: Controllers.PollOpsMsg);
+	BEGIN
+		msg.type := "TextRulers.Ruler";
+		msg.pasteType := "TextRulers.Ruler";
+		msg.selectable := FALSE;
+		msg.valid := {Controllers.copy, Controllers.paste}
+	END PollOps;
+
+	PROCEDURE SetProp (r: StdRuler; VAR msg: Properties.SetMsg; VAR requestFocus: BOOLEAN);
+		VAR a1: Attributes; px, py, g: INTEGER; sel: INTEGER;
+			p: Properties.Property; sp: Properties.StdProp; rp: Prop;
+	BEGIN
+		p := msg.prop; sel := r.sel; px := r.px; py := r.py;
+		IF sel # invalid THEN
+			WHILE (p # NIL) & ~(p IS Properties.StdProp) DO p := p.next END;
+			IF p # NIL THEN
+				sp := p(Properties.StdProp);
+				IF (r.sel = leadVal) & (Properties.size IN sp.valid) THEN
+					NEW(rp); rp.valid := {lead};
+					rp.lead := sp.size
+				ELSIF (r.sel = gridVal) & (Properties.size IN sp.valid) THEN
+					g := sp.size; DEC(g, g MOD point);
+					NEW(rp); rp.valid := {asc, grid};
+					IF g = 0 THEN rp.asc := 0; rp.grid := 1
+					ELSE rp.asc := g - r.style.attr.dsc; rp.grid := g
+					END
+				ELSE
+					rp := NIL
+				END
+			END;
+			p := rp
+		END;
+		a1 := ModifiedAttr(r.style.attr, p);
+		IF ~a1.Equals(r.style.attr) THEN
+			r.style.SetAttr(a1);
+			IF requestFocus & (r.sel = invalid) THEN	(* restore mark selection *)
+				r.sel := sel; r.px := px; r.py := py 
+			END
+		ELSE requestFocus := FALSE
+		END
+	END SetProp;
+
+	PROCEDURE PollProp (r: StdRuler; VAR msg: Properties.PollMsg);
+		VAR p: Properties.StdProp;
+	BEGIN
+		CASE r.sel OF
+		  invalid:
+			msg.prop := r.style.attr.Prop()
+		| leadVal:
+			NEW(p); p.known := {Properties.size}; p.valid := p.known;
+			p.size := r.style.attr.lead;
+			msg.prop := p
+		| gridVal:
+			NEW(p); p.known := {Properties.size}; p.valid := p.known;
+			p.size := r.style.attr.grid;
+			msg.prop := p
+		ELSE HALT(100)
+		END
+	END PollProp;
+
+
+	(* StdStyle *)
+
+	PROCEDURE (r: StdStyle) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		r.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdStyleVersion, thisVersion)
+	END Internalize;
+
+	PROCEDURE (r: StdStyle) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		r.Externalize^(wr);
+		wr.WriteVersion(maxStdStyleVersion)
+	END Externalize;
+(*	
+	PROCEDURE (r: StdStyle) CopyFrom (source: Stores.Store);
+	BEGIN
+		r.SetAttr(source(StdStyle).attr)
+	END CopyFrom;
+*)
+
+	(* StdRuler *)
+
+	PROCEDURE (r: StdRuler) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		r.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdRulerVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		r.sel := invalid
+	END Internalize;
+
+	PROCEDURE (r: StdRuler) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		r.Externalize^(wr);
+		wr.WriteVersion(maxStdRulerVersion)
+	END Externalize;
+
+	PROCEDURE (r: StdRuler) ThisModel (): Models.Model;
+	BEGIN
+		RETURN r.style
+	END ThisModel;
+
+	PROCEDURE (r: StdRuler) CopyFromModelView (source: Views.View; model: Models.Model);
+	BEGIN
+		r.sel := invalid; r.InitStyle(model(Style))
+	END CopyFromModelView;
+
+	PROCEDURE (ruler: StdRuler) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR a: Attributes; m: Mark; u, scale, tabBar,  px,  w, h: INTEGER; i: INTEGER;
+	BEGIN
+		u := f.dot; a := ruler.style.attr;
+		ruler.context.GetSize(w, h);
+		tabBar := h - tabBarHeight; scale := tabBar - scaleHeight;
+		w := MIN(f.r + 10 * mm, 10000 * mm);	(* high-level clipping *)
+		f.DrawLine(0, scale - u, w - u, scale - u, u, Ports.grey25);
+		f.DrawLine(0, tabBar - u, w - u, tabBar - u, u, Ports.grey50);
+		DrawScale(f, 0, scale, w, tabBar, l, r);
+		DrawNiceRect(f, 0, h - rulerHeight, w, h);
+		SetMark(m, ruler, a.first, h, first, -1); InvertMark(m, f, Ports.show);
+		SetMark(m, ruler, a.left, h, left, -1); InvertMark(m, f, Ports.show);
+		IF rightFixed IN a.opts THEN
+			SetMark(m, ruler, a.right, h, right, -1); InvertMark(m, f, Ports.show)
+		END;
+		i := 0;
+		WHILE i < a.tabs.len DO
+			SetMark(m, ruler, a.tabs.tab[i].stop, h, tabs, i); InvertMark(m, f, Ports.show); INC(i)
+		END;
+		px := iconGap; i := firstIcon;
+		WHILE i <= lastIcon DO
+			IF i IN validIcons THEN
+				SetMark(m, ruler, px, h - iconPin, i, -1); DrawMark(m, f)
+			END;
+			INC(px, iconWidth + iconGap); INC(i)
+		END;
+		HiliteThisMark(ruler, f, Mode(ruler), Ports.show)
+	END Restore;
+
+	PROCEDURE (ruler: StdRuler) RestoreMarks (f: Views.Frame; l, t, r, b: INTEGER);
+	BEGIN
+		HiliteThisMark(ruler, f, ruler.sel, Ports.show)
+	END RestoreMarks;
+
+	PROCEDURE (r: StdRuler) GetBackground (VAR color: Ports.Color);
+	BEGIN
+		color := Ports.background
+	END GetBackground;
+
+	PROCEDURE (r: StdRuler) Neutralize;
+		VAR msg: NeutralizeMsg;
+	BEGIN
+		Views.Broadcast(r, msg)
+	END Neutralize;
+
+	PROCEDURE (r: StdRuler) HandleModelMsg (VAR msg: Models.Message);
+	BEGIN
+		WITH msg: UpdateMsg DO
+			Views.Update(r, Views.keepFrames)
+		ELSE
+		END
+	END HandleModelMsg;
+
+	PROCEDURE (r: StdRuler) HandleViewMsg (f: Views.Frame; VAR msg: Views.Message);
+	BEGIN
+		WITH msg: NeutralizeMsg DO
+			DeselectMark(r, f)
+		ELSE
+		END
+	END HandleViewMsg;
+
+	PROCEDURE (r: StdRuler) HandleCtrlMsg (f: Views.Frame;
+		VAR msg: Controllers.Message; VAR focus: Views.View
+	);
+		VAR requestFocus: BOOLEAN;
+	BEGIN
+		WITH msg: Controllers.TrackMsg DO
+			Track(r, f, msg)
+		| msg: Controllers.EditMsg DO
+			Edit(r, f, msg)
+		| msg: Controllers.MarkMsg DO
+			r.RestoreMarks(f, f.l, f.t, f.r, f.b)
+		| msg: Controllers.SelectMsg DO
+			IF ~msg.set THEN DeselectMark(r, f) END
+		| msg: Controllers.PollOpsMsg DO
+			PollOps(r, f, msg)
+		| msg: Properties.CollectMsg DO
+			PollProp(r, msg.poll)
+		| msg: Properties.EmitMsg DO
+			requestFocus := f.front;
+			SetProp(r, msg.set, requestFocus);
+			msg.requestFocus := requestFocus
+		ELSE
+		END
+	END HandleCtrlMsg;
+
+	PROCEDURE (r: StdRuler) HandlePropMsg (VAR msg: Properties.Message);
+		VAR m: Mark; requestFocus: BOOLEAN; w, h: INTEGER;
+	BEGIN
+		WITH msg: Properties.SizePref DO
+			msg.w := 10000 * Ports.mm; msg.h := rulerHeight
+		| msg: Properties.ResizePref DO
+			msg.fixed := TRUE
+		| msg: Properties.FocusPref DO
+			IF msg.atLocation THEN
+				r.context.GetSize(w, h);
+				GetMark(m, r, NIL, msg.x, msg.y, FALSE);
+				msg.hotFocus := (m.kind # invalid) & ~(m.kind IN fieldIcons) OR (msg.y >= h - tabBarHeight);
+				msg.setFocus := ~msg.hotFocus
+			END
+		| msg: TextModels.Pref DO
+			msg.opts := {TextModels.maskChar, TextModels.hideable};
+			msg.mask := TextModels.para
+		| msg: Properties.SetMsg DO
+			requestFocus := FALSE;
+			SetProp(r, msg, requestFocus)
+		| msg: Properties.PollMsg DO
+			PollProp(r, msg)
+		ELSE
+		END
+	END HandlePropMsg;
+
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) NewStyle (attr: Attributes): Style;
+		VAR s: StdStyle;
+	BEGIN
+		IF attr = NIL THEN attr := d.attr END;
+		NEW(s); s.SetAttr(attr); RETURN s
+	END NewStyle;
+
+	PROCEDURE (d: StdDirectory) New (style: Style): Ruler;
+		VAR r: StdRuler;
+	BEGIN
+		IF style = NIL THEN style := d.NewStyle(NIL) END;
+		NEW(r); r.InitStyle(style); r.sel := invalid; RETURN r
+	END New;
+
+
+	(** miscellaneous **)
+
+	PROCEDURE GetValidRuler* (text: TextModels.Model; pos, hint: INTEGER;
+		VAR ruler: Ruler; VAR rpos: INTEGER
+	);
+		(** pre: (hint < 0   OR   (ruler, rpos) is first ruler before hint  &  0 <= pos <= t.Length() **)
+		(** post: hint < rpos <= pos & rpos = Pos(ruler) & (no ruler in (rpos, pos])
+				OR   ((ruler, rpos) unmodified)
+		**)
+		VAR view: Views.View;
+	BEGIN
+		IF pos < text.Length() THEN INC(pos) END;	(* let a ruler dominate its own position *)
+		IF pos < hint THEN hint := -1 END;
+		globRd := text.NewReader(globRd); globRd.SetPos(pos);
+		REPEAT
+			globRd.ReadPrevView(view)
+		UNTIL globRd.eot OR (view IS Ruler) OR (globRd.Pos() < hint);
+		IF (view # NIL) & (view IS Ruler) THEN
+			ruler := view(Ruler); rpos := globRd.Pos()
+		END
+	END GetValidRuler;
+
+	PROCEDURE SetDir* (d: Directory);
+	(** pre: d # NIL, d.attr # NIL **)
+	(** post: dir = d **)
+	BEGIN
+		ASSERT(d # NIL, 20); ASSERT(d.attr.init, 21); dir := d
+	END SetDir;
+
+
+	PROCEDURE Init;
+		VAR d: StdDirectory; fnt: Fonts.Font; asc, dsc, w: INTEGER;
+	BEGIN
+		IF Dialog.metricSystem THEN
+			marginGrid := 1*mm; minTabWidth := 1*mm; tabGrid := 1*mm
+		ELSE
+			marginGrid := inch16; minTabWidth := inch16; tabGrid := inch16
+		END;
+
+		fnt := Fonts.dir.Default();
+		font := Fonts.dir.This(fnt.typeface, 7*point, {}, Fonts.normal);	(* font for ruler scales *)
+		NEW(prop);
+		prop.valid := {first .. tabs};
+		prop.first := 0; prop.left := 0;
+		IF Dialog.metricSystem THEN
+			prop.right := 165*mm
+		ELSE
+			prop.right := 104*inch16
+		END;
+		fnt.GetBounds(asc, dsc, w);
+		prop.lead := 0; prop.asc := asc; prop.dsc := dsc; prop.grid := 1;
+		prop.opts.val := {leftAdjust}; prop.opts.mask := options;
+		prop.tabs.len := 0;
+
+		NEW(def); def.InitFromProp(prop);
+		NEW(d); d.attr := def; dir := d; stdDir := d
+	END Init;
+
+	PROCEDURE Cleaner;
+	BEGIN
+		globRd := NIL
+	END Cleaner;
+
+BEGIN
+	Init;
+	Kernel.InstallCleaner(Cleaner)
+CLOSE
+	Kernel.RemoveCleaner(Cleaner)
+END TextRulers.

+ 1313 - 0
BlackBox/Text/Mod/Setters.txt

@@ -0,0 +1,1313 @@
+MODULE TextSetters;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Setters.odc *)
+	(* DO NOT EDIT *)
+
+	(* correct NextPage postcond in docu *)
+	(* make s.r, s.rd reducible? *)
+	(* paraShutoff needs to be controlled by an approx flag to certain ops (later ...?) *)
+
+	IMPORT
+		Fonts, Ports, Printers, Stores, Models, Views, Properties,
+		TextModels, TextRulers;
+
+	CONST
+		(** Pref.opts, options of setter-aware views; 0 overrides 1 **)
+		lineBreak* = 0; wordJoin* = 1; wordPart* = 2; flexWidth* = 3;
+
+		tab = TextModels.tab; line = TextModels.line; para = TextModels.para;
+		zwspace = TextModels.zwspace; nbspace = TextModels.nbspace;
+		hyphen = TextModels.hyphen; nbhyphen = TextModels.nbhyphen;
+		digitspace = TextModels.digitspace;
+		softhyphen = TextModels.softhyphen;
+
+		mm = Ports.mm;
+		minTabWidth = 2 * Ports.point; stdTabWidth = 4 * mm;
+		leftLineGap = 2 * Ports.point; rightLineGap = 3 * Ports.point;
+		adjustMask = {TextRulers.leftAdjust, TextRulers.rightAdjust};
+		centered = {}; leftFlush = {TextRulers.leftAdjust}; rightFlush = {TextRulers.rightAdjust};
+		blocked = adjustMask;
+
+		boxCacheLen = 64;
+		seqCacheLen = 16;
+
+		paraShutoff = MAX(INTEGER);	(* longest stretch read backwards to find start of paragraph *)
+															(* unsafe: disabled *)
+		cachedRulers = FALSE;	(* caching ruler objects trades speed against GC effectiveness *)
+		periodInWords = FALSE;
+		colonInWords = FALSE;
+
+		minVersion = 0; maxVersion = 0; maxStdVersion = 0;
+
+
+	TYPE
+		Pref* = RECORD (Properties.Preference)
+			opts*: SET;
+			endW*: INTEGER;	(** preset (to width of view) **)
+			dsc*: INTEGER	(** preset (to dominating line descender) **)
+		END;
+
+
+		Reader* = POINTER TO ABSTRACT RECORD
+			r-: TextModels.Reader;	(** look-ahead state **)
+			(** unit **)
+			string*: ARRAY 64 OF CHAR;	(** single chars in string[0] **)
+			view*: Views.View;
+			(** unit props **)
+			textOpts*: SET;
+			mask*: CHAR;
+			setterOpts*: SET;
+			w*, endW*, h*, dsc*: INTEGER;
+			attr*: TextModels.Attributes;
+			(** reading state **)
+			eot*: BOOLEAN;
+			pos*: INTEGER;
+			x*: INTEGER;	(** to be advanced by client! **)
+			adjStart*: INTEGER;
+			spaces*: INTEGER;
+			tabIndex*: INTEGER;	(** tabs being processed; initially -1 **)
+			tabType*: SET;	(** type of tab being processed; initially {} **)
+			(** line props **)
+			vw*: INTEGER;
+			hideMarks*: BOOLEAN;
+			ruler*: TextRulers.Ruler;
+			rpos*: INTEGER
+		END;
+
+		Setter* = POINTER TO ABSTRACT RECORD (Stores.Store)
+			text-: TextModels.Model;	(** connected iff text # NIL **)
+			defRuler-: TextRulers.Ruler;
+			vw-: INTEGER;
+			hideMarks-: BOOLEAN
+		END;
+
+
+		LineBox* = RECORD
+			len*: INTEGER;
+			ruler*: TextRulers.Ruler;
+			rpos*: INTEGER;
+			left*, right*, asc*, dsc*: INTEGER;
+			rbox*, bop*, adj*, eot*: BOOLEAN;	(** adj => adjW > 0; adj & blocked => spaces > 0 **)
+			views*: BOOLEAN;
+			skipOff*: INTEGER;	(** chars in [skipOff, len) take endW **)
+			adjOff*: INTEGER;	(** offset of last block in box - adjust only this block **)
+			spaces*: INTEGER;	(** valid, > 0 if adj & blocked **)
+			adjW*: INTEGER;	(** valid if adj - to be distributed over spaces **)
+			tabW*: ARRAY TextRulers.maxTabs OF INTEGER	(** delta width of tabs (<= 0) **)
+		END;
+
+
+		Directory* = POINTER TO ABSTRACT RECORD END;
+
+
+		Worder = RECORD
+			box: LineBox; next: INTEGER;
+			i: INTEGER
+		END;
+
+		StdReader = POINTER TO RECORD (Reader) END;
+
+		StdSetter = POINTER TO RECORD (Setter)
+			rd: Reader;	(* subject to reduction? *)
+			r: TextModels.Reader;	(* subject to reduction? *)
+			ruler: TextRulers.Ruler;
+			rpos: INTEGER;
+			key: INTEGER
+		END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+
+	VAR
+		dir-, stdDir-: Directory;
+
+		nextKey: INTEGER;
+		boxIndex, seqIndex: INTEGER;
+		boxCache: ARRAY boxCacheLen OF RECORD
+			key: INTEGER;	(* valid iff key > 0 *)
+			start: INTEGER;
+			line: LineBox	(* inv ruler = NIL *)
+		END;
+		seqCache: ARRAY seqCacheLen OF RECORD
+			key: INTEGER;	(* valid iff key > 0 *)
+			start, pos: INTEGER	(* sequence [start, end), end >= pos *)
+		END;
+
+
+	(** Reader **)
+
+	PROCEDURE (rd: Reader) Set* (
+		old: TextModels.Reader;
+		text: TextModels.Model; x, pos: INTEGER;
+		ruler: TextRulers.Ruler; rpos: INTEGER; vw: INTEGER; hideMarks: BOOLEAN
+	), NEW, EXTENSIBLE;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		ASSERT(ruler # NIL, 22);
+		rd.r := text.NewReader(old); rd.r.SetPos(pos); rd.r.Read;
+		rd.string[0] := 0X; rd.view := NIL;
+		rd.textOpts := {};
+		rd.setterOpts := {}; rd.w := 0; rd.endW := 0; rd.h := 0; rd.dsc := 0;
+		rd.attr := NIL;
+		rd.eot := FALSE; rd.pos := pos; rd.x := x;
+		rd.tabIndex := -1; rd.tabType := {};
+		rd.adjStart := pos; rd.spaces := 0;
+		rd.ruler := ruler; rd.rpos := rpos; rd.vw := vw; rd.hideMarks := hideMarks
+	END Set;
+
+	PROCEDURE (rd: Reader) Read*, NEW, EXTENSIBLE;
+	(** pre: rd set **)
+	(** post: rd.pos = rd.pos' + Length(rd.string) **)
+	BEGIN
+		rd.string[0] := rd.r.char; rd.string[1] := 0X;
+		rd.view := rd.r.view;
+		rd.textOpts := {};
+		rd.setterOpts := {};
+		rd.w := rd.r.w; rd.endW := rd.w; rd.h := rd.r.h; rd.dsc := 0;
+		rd.attr := rd.r.attr;
+		rd.eot := rd.r.eot;
+		INC(rd.pos);
+		rd.r.Read
+	END Read;
+
+	PROCEDURE (rd: Reader) AdjustWidth* (start, pos: INTEGER; IN box: LineBox;
+		VAR w: INTEGER
+	), NEW, ABSTRACT;
+
+	PROCEDURE (rd: Reader) SplitWidth* (w: INTEGER): INTEGER, NEW, ABSTRACT;
+
+
+	(** Setter **)
+
+	PROCEDURE (s: Setter) CopyFrom- (source: Stores.Store), EXTENSIBLE;
+	BEGIN
+		WITH source: Setter DO
+			s.text := source.text; s.defRuler := source.defRuler;
+			s.vw := source.vw;  s.hideMarks := source.hideMarks
+		END
+	END CopyFrom;
+
+	PROCEDURE (s: Setter) Internalize- (VAR rd: Stores.Reader), EXTENSIBLE;
+		VAR thisVersion: INTEGER;
+	BEGIN
+		s.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxVersion, thisVersion)
+	END Internalize;
+
+	PROCEDURE (s: Setter) Externalize- (VAR wr: Stores.Writer), EXTENSIBLE;
+	BEGIN
+		s.Externalize^(wr);
+		wr.WriteVersion(maxVersion)
+	END Externalize;
+
+	PROCEDURE (s: Setter) ConnectTo* (text: TextModels.Model;
+		defRuler: TextRulers.Ruler; vw: INTEGER; hideMarks: BOOLEAN
+	), NEW, EXTENSIBLE;
+	BEGIN
+		IF text # NIL THEN
+			s.text := text; s.defRuler := defRuler; s.vw := vw; s.hideMarks := hideMarks
+		ELSE
+			s.text := NIL; s.defRuler := NIL
+		END
+	END ConnectTo;
+
+
+	PROCEDURE (s: Setter) ThisPage* (pageH: INTEGER; pageNo: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, 0 <= pageNo **)
+	(** post: (result = -1) & (pageNo >= maxPageNo) OR (result = pageStart(pageNo)) **)
+
+	PROCEDURE (s: Setter) NextPage* (pageH: INTEGER; start: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, ThisPage(pageH, pageNo) = start [with pageNo = NumberOfPageAt(start)] **)
+	(** post: (result = start) & last-page(start) OR result = next-pageStart(start) **)
+
+
+	PROCEDURE (s: Setter) ThisSequence* (pos: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, 0 <= pos <= s.text.Length() **)
+	(** post: (result = 0) OR (char(result - 1) IN {line, para}) **)
+
+	PROCEDURE (s: Setter) NextSequence* (start: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, ThisSequence(start) = start **)
+	(** post: (result = start) & last-line(start) OR (ThisSequence(t, result - 1) = start) **)
+
+	PROCEDURE (s: Setter) PreviousSequence* (start: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, ThisSequence(t, start) = start **)
+	(** post: (result = 0) & (start = 0) OR (result = ThisSequence(t, start - 1)) **)
+
+
+	PROCEDURE (s: Setter) ThisLine* (pos: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, 0 <= pos <= s.text.Length() **)
+	(** post: result <= pos, (pos < NextLine(result)) OR last-line(result) **)
+
+	PROCEDURE (s: Setter) NextLine* (start: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, ThisLine(start) = start **)
+	(** post: (result = 0) & (start = 0) OR
+				(result = start) & last-line(start) OR
+				(ThisLine(result - 1) = start) **)
+
+	PROCEDURE (s: Setter) PreviousLine* (start: INTEGER): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, ThisLine(start) = start **)
+	(** post: (result = 0) & (start = 0) OR (result = ThisLine(start - 1)) **)
+
+
+	PROCEDURE (s: Setter) GetWord* (pos: INTEGER; OUT beg, end: INTEGER), NEW, ABSTRACT;
+	(** pre: connected, 0 <= pos <= s.text.Length() **)
+	(** post: c set, beg <= pos <= end **)
+
+	PROCEDURE (s: Setter) GetLine* (start: INTEGER; OUT box: LineBox), NEW, ABSTRACT;
+	(** pre: connected, ThisLine(start) = start, 0 <= start <= s.text.Length() **)
+	(** post: (c, box) set (=> box.ruler # NIL), (box.len > 0) OR box.eot,
+		 0 <= box.left <= box.right <= ruler.right **)
+
+	PROCEDURE (s: Setter) GetBox* (start, end, maxW, maxH: INTEGER;
+		OUT w, h: INTEGER
+	), NEW, ABSTRACT;
+	(** pre: connected, ThisLine(start) = start, 0 <= start <= end <= s.text.Length() **)
+	(** post: c set, maxW > undefined => w <= maxW, maxH > undefined => h <= maxH **)
+
+
+	PROCEDURE (s: Setter) NewReader* (old: Reader): Reader, NEW, ABSTRACT;
+	(** pre: connected **)
+
+
+	PROCEDURE (s: Setter) GridOffset* (dsc: INTEGER; IN box: LineBox): INTEGER, NEW, ABSTRACT;
+	(** pre: connected, dsc >= 0: dsc is descender of previous line; dsc = -1 for first line **)
+	(** post: dsc + GridOffset(dsc, box) + box.asc = k*ruler.grid (k >= 0) >= ruler.asc + ruler.grid **)
+
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) New* (): Setter, NEW, ABSTRACT;
+
+
+	(* line box cache *)
+
+	PROCEDURE InitCache;
+		VAR i: INTEGER;
+	BEGIN
+		nextKey := 1; boxIndex := 0; seqIndex := 0;
+		i := 0; WHILE i < boxCacheLen DO boxCache[i].key := -1; INC(i) END;
+		i := 0; WHILE i < seqCacheLen DO seqCache[i].key := -1; INC(i) END
+	END InitCache;
+
+	PROCEDURE ClearCache (key: INTEGER);
+		VAR i, j: INTEGER;
+	BEGIN
+		i := 0; j := boxIndex;
+		WHILE i < boxCacheLen DO
+			IF boxCache[i].key = key THEN boxCache[i].key := -1; j := i END;
+			INC(i)
+		END;
+		boxIndex := j;
+		i := 0; j := seqIndex;
+		WHILE i < seqCacheLen DO
+			IF seqCache[i].key = key THEN seqCache[i].key := -1; j := i END;
+			INC(i)
+		END;
+		seqIndex := j
+	END ClearCache;
+
+
+	PROCEDURE CacheIndex (key, start: INTEGER): INTEGER;
+		VAR i: INTEGER;
+	BEGIN
+RETURN -1;
+		i := 0;
+		WHILE (i < boxCacheLen) & ~((boxCache[i].key = key) & (boxCache[i].start = start)) DO
+			INC(i)
+		END;
+		IF i = boxCacheLen THEN i := -1 END;
+		RETURN i
+	END CacheIndex;
+
+	PROCEDURE GetFromCache (s: StdSetter; i: INTEGER; VAR l: LineBox);
+	BEGIN
+		l := boxCache[i].line;
+		IF ~cachedRulers THEN
+			IF l.rpos >= 0 THEN
+				s.r := s.text.NewReader(s.r); s.r.SetPos(l.rpos); s.r.Read;
+				l.ruler := s.r.view(TextRulers.Ruler)
+			ELSE l.ruler := s.defRuler
+			END
+		END
+	END GetFromCache;
+
+	PROCEDURE AddToCache (key, start: INTEGER; VAR l: LineBox);
+		VAR i: INTEGER;
+	BEGIN
+		i := boxIndex; boxIndex := (i + 1) MOD boxCacheLen;
+		boxCache[i].key := key; boxCache[i].start := start; boxCache[i].line := l;
+		IF ~cachedRulers THEN
+			boxCache[i].line.ruler := NIL
+		END
+	END AddToCache;
+
+
+	PROCEDURE CachedSeqStart (key, pos: INTEGER): INTEGER;
+		VAR start: INTEGER; i: INTEGER;
+	BEGIN
+		i := 0;
+		WHILE (i < seqCacheLen)
+		& ~((seqCache[i].key = key) & (seqCache[i].start <= pos) & (pos <= seqCache[i].pos)) DO
+			INC(i)
+		END;
+		IF i < seqCacheLen THEN start := seqCache[i].start ELSE start := -1 END;
+		RETURN start
+	END CachedSeqStart;
+
+	PROCEDURE AddSeqStartToCache (key, pos, start: INTEGER);
+		VAR i: INTEGER;
+	BEGIN
+		i := 0;
+		WHILE (i < seqCacheLen) & ~((seqCache[i].key = key) & (seqCache[i].start = start)) DO
+			INC(i)
+		END;
+		IF i < seqCacheLen THEN
+			IF seqCache[i].pos < pos THEN seqCache[i].pos := pos END
+		ELSE
+			i := seqIndex; seqIndex := (i + 1) MOD seqCacheLen;
+			seqCache[i].key := key; seqCache[i].pos := pos; seqCache[i].start := start
+		END
+	END AddSeqStartToCache;
+
+
+	(* StdReader *)
+
+(*
+	PROCEDURE WordPart (ch, ch1: CHAR): BOOLEAN;
+	(* needs more work ... put elsewhere? *)
+	BEGIN
+		CASE ORD(ch) OF
+		  ORD("0") .. ORD("9"), ORD("A") .. ORD("Z"), ORD("a") .. ORD("z"),
+		  ORD(digitspace), ORD(nbspace), ORD(nbhyphen), ORD("_"),
+		  0C0H .. 0C6H, 0E0H .. 0E6H,	(* ~ A *)
+		  0C7H, 0E7H,	(* ~ C *)
+		  0C8H .. 0CBH, 0E8H .. 0EBH,	(* ~ E *)
+		  0CCH .. 0CFH, 0ECH .. 0EFH,	(* ~ I *)
+		  0D1H, 0F1H,	(* ~ N *)
+		  0D2H .. 0D6H, 0D8H, 0F2H .. 0F6H, 0F8H,	(* ~ O *)
+		  0D9H .. 0DCH, 0F9H .. 0FCH,	(* ~ U *)
+		  0DDH, 0FDH, 0FFH,	(* ~ Y *)
+		  0DFH:	(* ~ ss *)
+			RETURN TRUE
+		| ORD("."), ORD(":"):
+			IF (ch = ".") & periodInWords OR (ch = ":") & colonInWords THEN
+				CASE ch1 OF
+				  0X, TextModels.viewcode, tab, line, para, " ":
+					RETURN FALSE
+				ELSE RETURN TRUE
+				END
+			ELSE RETURN FALSE
+			END
+		ELSE RETURN FALSE
+		END
+	END WordPart;
+*)
+
+	PROCEDURE WordPart (ch, ch1: CHAR): BOOLEAN;
+		(* Same as .net function System.Char.IsLetterOrDigit(ch)
+				+ digit space, nonbreaking space, nonbreaking hyphen, & underscore
+			ch1 unused *)
+		VAR low: INTEGER;
+	BEGIN
+		low := ORD(ch) MOD 256;
+		CASE ORD(ch) DIV 256 OF
+		| 001H, 015H, 034H..04CH, 04EH..09EH, 0A0H..0A3H, 0ACH..0D6H, 0F9H, 0FCH: RETURN TRUE
+		| 000H: CASE low OF
+			| 030H..039H, 041H..05AH, 061H..07AH, 0AAH, 0B5H, 0BAH, 0C0H..0D6H, 0D8H..0F6H, 0F8H..0FFH,
+				ORD(digitspace), ORD(nbspace), ORD(nbhyphen), ORD("_"): RETURN TRUE
+			ELSE
+			END
+		| 002H: CASE low OF
+			| 000H..041H, 050H..0C1H, 0C6H..0D1H, 0E0H..0E4H, 0EEH: RETURN TRUE
+			ELSE
+			END
+		| 003H: CASE low OF
+			| 07AH, 086H, 088H..08AH, 08CH, 08EH..0A1H, 0A3H..0CEH, 0D0H..0F5H, 0F7H..0FFH: RETURN TRUE
+			ELSE
+			END
+		| 004H: CASE low OF
+			| 000H..081H, 08AH..0CEH, 0D0H..0F9H: RETURN TRUE
+			ELSE
+			END
+		| 005H: CASE low OF
+			| 000H..00FH, 031H..056H, 059H, 061H..087H, 0D0H..0EAH, 0F0H..0F2H: RETURN TRUE
+			ELSE
+			END
+		| 006H: CASE low OF
+			| 021H..03AH, 040H..04AH, 060H..069H, 06EH..06FH, 071H..0D3H, 0D5H, 0E5H..0E6H, 0EEH..0FCH, 0FFH: RETURN TRUE
+			ELSE
+			END
+		| 007H: CASE low OF
+			| 010H, 012H..02FH, 04DH..06DH, 080H..0A5H, 0B1H: RETURN TRUE
+			ELSE
+			END
+		| 009H: CASE low OF
+			| 004H..039H, 03DH, 050H, 058H..061H, 066H..06FH, 07DH, 085H..08CH, 08FH..090H, 093H..0A8H, 0AAH..0B0H, 0B2H, 0B6H..0B9H, 0BDH, 0CEH, 0DCH..0DDH, 0DFH..0E1H, 0E6H..0F1H: RETURN TRUE
+			ELSE
+			END
+		| 00AH: CASE low OF
+			| 005H..00AH, 00FH..010H, 013H..028H, 02AH..030H, 032H..033H, 035H..036H, 038H..039H, 059H..05CH, 05EH, 066H..06FH, 072H..074H, 085H..08DH, 08FH..091H, 093H..0A8H, 0AAH..0B0H, 0B2H..0B3H, 0B5H..0B9H, 0BDH, 0D0H, 0E0H..0E1H, 0E6H..0EFH: RETURN TRUE
+			ELSE
+			END
+		| 00BH: CASE low OF
+			| 005H..00CH, 00FH..010H, 013H..028H, 02AH..030H, 032H..033H, 035H..039H, 03DH, 05CH..05DH, 05FH..061H, 066H..06FH, 071H, 083H, 085H..08AH, 08EH..090H, 092H..095H, 099H..09AH, 09CH, 09EH..09FH, 0A3H..0A4H, 0A8H..0AAH, 0AEH..0B9H, 0E6H..0EFH: RETURN TRUE
+			ELSE
+			END
+		| 00CH: CASE low OF
+			| 005H..00CH, 00EH..010H, 012H..028H, 02AH..033H, 035H..039H, 060H..061H, 066H..06FH, 085H..08CH, 08EH..090H, 092H..0A8H, 0AAH..0B3H, 0B5H..0B9H, 0BDH, 0DEH, 0E0H..0E1H, 0E6H..0EFH: RETURN TRUE
+			ELSE
+			END
+		| 00DH: CASE low OF
+			| 005H..00CH, 00EH..010H, 012H..028H, 02AH..039H, 060H..061H, 066H..06FH, 085H..096H, 09AH..0B1H, 0B3H..0BBH, 0BDH, 0C0H..0C6H: RETURN TRUE
+			ELSE
+			END
+		| 00EH: CASE low OF
+			| 001H..030H, 032H..033H, 040H..046H, 050H..059H, 081H..082H, 084H, 087H..088H, 08AH, 08DH, 094H..097H, 099H..09FH, 0A1H..0A3H, 0A5H, 0A7H, 0AAH..0ABH, 0ADH..0B0H, 0B2H..0B3H, 0BDH, 0C0H..0C4H, 0C6H, 0D0H..0D9H, 0DCH..0DDH: RETURN TRUE
+			ELSE
+			END
+		| 00FH: CASE low OF
+			| 000H, 020H..029H, 040H..047H, 049H..06AH, 088H..08BH: RETURN TRUE
+			ELSE
+			END
+		| 010H: CASE low OF
+			| 000H..021H, 023H..027H, 029H..02AH, 040H..049H, 050H..055H, 0A0H..0C5H, 0D0H..0FAH, 0FCH: RETURN TRUE
+			ELSE
+			END
+		| 011H: CASE low OF
+			| 000H..059H, 05FH..0A2H, 0A8H..0F9H: RETURN TRUE
+			ELSE
+			END
+		| 012H: CASE low OF
+			| 000H..048H, 04AH..04DH, 050H..056H, 058H, 05AH..05DH, 060H..088H, 08AH..08DH, 090H..0B0H, 0B2H..0B5H, 0B8H..0BEH, 0C0H, 0C2H..0C5H, 0C8H..0D6H, 0D8H..0FFH: RETURN TRUE
+			ELSE
+			END
+		| 013H: CASE low OF
+			| 000H..010H, 012H..015H, 018H..05AH, 080H..08FH, 0A0H..0F4H: RETURN TRUE
+			ELSE
+			END
+		| 014H: IF low >= 001H THEN RETURN TRUE END
+		| 016H: CASE low OF
+			| 000H..06CH, 06FH..076H, 081H..09AH, 0A0H..0EAH: RETURN TRUE
+			ELSE
+			END
+		| 017H: CASE low OF
+			| 000H..00CH, 00EH..011H, 020H..031H, 040H..051H, 060H..06CH, 06EH..070H, 080H..0B3H, 0D7H, 0DCH, 0E0H..0E9H: RETURN TRUE
+			ELSE
+			END
+		| 018H: CASE low OF
+			| 010H..019H, 020H..077H, 080H..0A8H: RETURN TRUE
+			ELSE
+			END
+		| 019H: CASE low OF
+			| 000H..01CH, 046H..06DH, 070H..074H, 080H..0A9H, 0C1H..0C7H, 0D0H..0D9H: RETURN TRUE
+			ELSE
+			END
+		| 01AH: IF low < 017H THEN RETURN TRUE END
+		| 01DH: IF low < 0C0H THEN RETURN TRUE END
+		| 01EH: CASE low OF
+			| 000H..09BH, 0A0H..0F9H: RETURN TRUE
+			ELSE
+			END
+		| 01FH: CASE low OF
+			| 000H..015H, 018H..01DH, 020H..045H, 048H..04DH, 050H..057H, 059H, 05BH, 05DH, 05FH..07DH, 080H..0B4H, 0B6H..0BCH, 0BEH, 0C2H..0C4H, 0C6H..0CCH, 0D0H..0D3H, 0D6H..0DBH, 0E0H..0ECH, 0F2H..0F4H, 0F6H..0FCH: RETURN TRUE
+			ELSE
+			END
+		| 020H: CASE low OF
+			| 071H, 07FH, 090H..094H: RETURN TRUE
+			ELSE
+			END
+		| 021H: CASE low OF
+			| 002H, 007H, 00AH..013H, 015H, 019H..01DH, 024H, 026H, 028H, 02AH..02DH, 02FH..031H, 033H..039H, 03CH..03FH, 045H..049H: RETURN TRUE
+			ELSE
+			END
+		| 02CH: CASE low OF
+			| 000H..02EH, 030H..05EH, 080H..0E4H: RETURN TRUE
+			ELSE
+			END
+		| 02DH: CASE low OF
+			| 000H..025H, 030H..065H, 06FH, 080H..096H, 0A0H..0A6H, 0A8H..0AEH, 0B0H..0B6H, 0B8H..0BEH, 0C0H..0C6H, 0C8H..0CEH, 0D0H..0D6H, 0D8H..0DEH: RETURN TRUE
+			ELSE
+			END
+		| 030H: CASE low OF
+			| 005H..006H, 031H..035H, 03BH..03CH, 041H..096H, 09DH..09FH, 0A1H..0FAH, 0FCH..0FFH: RETURN TRUE
+			ELSE
+			END
+		| 031H: CASE low OF
+			| 005H..02CH, 031H..08EH, 0A0H..0B7H, 0F0H..0FFH: RETURN TRUE
+			ELSE
+			END
+		| 04DH: IF low < 0B6H THEN RETURN TRUE END
+		| 09FH: IF low < 0BCH THEN RETURN TRUE END
+		| 0A4H: IF low < 08DH THEN RETURN TRUE END
+		| 0A8H: CASE low OF
+			| 000H..001H, 003H..005H, 007H..00AH, 00CH..022H: RETURN TRUE
+			ELSE
+			END
+		| 0D7H: IF low < 0A4H THEN RETURN TRUE END
+		| 0FAH: CASE low OF
+			| 000H..02DH, 030H..06AH, 070H..0D9H: RETURN TRUE
+			ELSE
+			END
+		| 0FBH: CASE low OF
+			| 000H..006H, 013H..017H, 01DH, 01FH..028H, 02AH..036H, 038H..03CH, 03EH, 040H..041H, 043H..044H, 046H..0B1H, 0D3H..0FFH: RETURN TRUE
+			ELSE
+			END
+		| 0FDH: CASE low OF
+			| 000H..03DH, 050H..08FH, 092H..0C7H, 0F0H..0FBH: RETURN TRUE
+			ELSE
+			END
+		| 0FEH: CASE low OF
+			| 070H..074H, 076H..0FCH: RETURN TRUE
+			ELSE
+			END
+		| 0FFH: CASE low OF
+			| 010H..019H, 021H..03AH, 041H..05AH, 066H..0BEH, 0C2H..0C7H, 0CAH..0CFH, 0D2H..0D7H, 0DAH..0DCH: RETURN TRUE
+			ELSE
+			END
+		ELSE
+		END;
+		RETURN FALSE		
+	END WordPart;
+
+(*
+	PROCEDURE ExtendToEOL (x, right: INTEGER): INTEGER;
+	BEGIN
+		IF right - x > 5 * mm THEN RETURN right - x ELSE RETURN 5 * mm END
+	END ExtendToEOL;
+*)
+
+	PROCEDURE Right (ra: TextRulers.Attributes; vw: INTEGER): INTEGER;
+	BEGIN
+		IF TextRulers.rightFixed IN ra.opts THEN
+			RETURN ra.right
+		ELSE
+			RETURN vw
+		END
+	END Right;
+
+	PROCEDURE GetViewPref (rd: StdReader);
+		CONST maxH = 1600 * Ports.point;
+		VAR ra: TextRulers.Attributes; tp: TextModels.Pref; sp: Pref;
+	BEGIN
+		ra := rd.ruler.style.attr;
+		tp.opts := {}; Views.HandlePropMsg(rd.view, tp);
+		rd.textOpts := tp.opts; rd.mask := tp.mask;
+		sp.opts := {}; sp.dsc := ra.dsc; sp.endW := rd.w; Views.HandlePropMsg(rd.view, sp);
+		rd.setterOpts := sp.opts; rd.dsc := sp.dsc; rd.endW := sp.endW;
+		IF rd.w >= 10000 * mm THEN rd.w := 10000 * mm END;
+		IF (TextModels.hideable IN tp.opts) & rd.hideMarks THEN
+			rd.h := 0; sp.dsc := 0;
+(*
+rd.w := 0;
+*)
+			IF ~( (rd.view IS TextRulers.Ruler)
+					OR (TextModels.maskChar IN rd.textOpts) & (rd.mask = para) ) THEN
+				rd.w := 0
+			END
+(**)
+		ELSIF rd.h > maxH THEN rd.h := maxH
+		END;
+		IF TextModels.maskChar IN rd.textOpts THEN
+			rd.string[0] := rd.mask; rd.string[1] := 0X
+		ELSE rd.string[0] := TextModels.viewcode
+		END
+	END GetViewPref;
+
+	PROCEDURE GatherString (rd: StdReader);
+		VAR i, len: INTEGER; ch: CHAR;
+	BEGIN
+		i := 1; len := LEN(rd.string) - 1; ch := rd.r.char;
+		WHILE (i < len)
+			& (rd.r.view = NIL) & (rd.r.attr = rd.attr)
+			& (	 (" " < ch) & (ch <= "~") & (ch # "-")
+				OR  (ch = digitspace)
+				OR  (ch >= nbspace) & (ch < 100X) & (ch # softhyphen)
+				)
+		DO	(* rd.r.char > " " => ~rd.eot *)
+			rd.string[i] := ch; INC(i);
+			rd.eot := rd.r.eot;
+			rd.r.Read; ch := rd.r.char; INC(rd.pos)
+		END;
+		rd.string[i] := 0X; rd.setterOpts := {wordJoin};
+		IF i = 1 THEN
+			IF WordPart(rd.string[0], 0X) THEN INCL(rd.setterOpts, wordPart) END
+		END;
+		rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
+	END GatherString;
+
+	PROCEDURE SpecialChar (rd: StdReader);
+		VAR ra: TextRulers.Attributes; i, tabs, spaceW, dW: INTEGER; type: SET;
+	BEGIN
+		ra := rd.ruler.style.attr;
+		CASE ORD(rd.string[0]) OF
+		  ORD(tab):
+			rd.textOpts := {TextModels.hideable};
+			rd.endW := minTabWidth;
+			rd.adjStart := rd.pos; rd.spaces := 0;
+			(*
+			i := 0; WHILE (i < ra.tabs.len) & (ra.tabs.tab[i].stop < rd.x + minTabWidth) DO INC(i) END;
+			*)
+			i := rd.tabIndex + 1;
+			IF i < ra.tabs.len THEN
+				type := ra.tabs.tab[i].type;
+				rd.w := MAX(minTabWidth, ra.tabs.tab[i].stop - rd.x);
+				IF TextRulers.barTab IN type THEN
+					IF TextRulers.rightTab IN type THEN
+						rd.w := MAX(minTabWidth, rd.w - leftLineGap)
+					ELSIF ~(TextRulers.centerTab IN type) THEN
+						INC(rd.w, rightLineGap)
+					END
+				END;
+				rd.tabIndex := i; rd.tabType := type
+			ELSE	(* for "reasonable" fonts: round to closest multiple of spaces of this font *)
+				spaceW := rd.attr.font.SStringWidth(" ");
+				IF (1 <= spaceW) & (spaceW <= stdTabWidth) THEN
+					rd.w := (stdTabWidth + spaceW DIV 2) DIV spaceW * spaceW
+				ELSE
+					rd.w := stdTabWidth
+				END;
+				rd.tabIndex := TextRulers.maxTabs; rd.tabType := {}
+			END
+		| ORD(line):
+			rd.setterOpts := {lineBreak}; rd.w := 0; rd.endW := 0
+		| ORD(para):
+(*
+			IF rd.hideMarks THEN
+				rd.w := 0; rd.h := 0; rd.dsc := 0
+			ELSE
+				rd.w := ExtendToEOL(rd.x, Right(ra, rd.vw)) + 1
+			END;
+			INC(rd.h, ra.lead);
+			rd.textOpts := {TextModels.hideable};
+			rd.endW := rd.w
+*)
+(*
+			rd.setterOpts := {lineBreak};
+*)
+			IF rd.hideMarks THEN rd.h := 0; rd.dsc := 0 END;
+			INC(rd.h, ra.lead); rd.textOpts := {TextModels.hideable};
+			IF (rd.view = NIL) OR ~(rd.view IS TextRulers.Ruler) THEN
+				rd.w := 10000 * Ports.mm (* ExtendToEOL(rd.x, Right(ra, rd.vw)) + 1 *)
+			END;
+			rd.endW := rd.w
+(**)
+		| ORD(" "):
+			rd.setterOpts := {flexWidth};
+			rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := 0; INC(rd.spaces)
+		| ORD(zwspace):
+			rd.w := 0; rd.endW := 0
+		| ORD(digitspace):
+			rd.setterOpts := {wordPart};
+			rd.w := rd.attr.font.StringWidth("0"); rd.endW := rd.w
+		| ORD("-"):
+			rd.setterOpts := {};
+			rd.w := rd.attr.font.StringWidth("-"); rd.endW := rd.w
+		| ORD(hyphen):
+			rd.setterOpts := {};
+			rd.string[0] := "-" (*softhyphen*);
+			rd.w := rd.attr.font.StringWidth("-" (*softhyphen*)); rd.endW := rd.w
+		| ORD(nbhyphen):
+			rd.setterOpts := {wordJoin, wordPart};
+			rd.string[0] := "-" (*softhyphen*);
+			rd.w := rd.attr.font.StringWidth("-" (*softhyphen*)); rd.endW := rd.w
+		| ORD(softhyphen):
+			rd.setterOpts := {wordPart}; rd.textOpts := {TextModels.hideable};
+			rd.string[0] := "-";
+			rd.endW := rd.attr.font.StringWidth("-" (*softhyphen*));
+			IF rd.hideMarks THEN rd.w := 0 ELSE rd.w := rd.endW END
+		ELSE
+			rd.setterOpts := {wordJoin};
+			IF WordPart(rd.string[0], rd.r.char) THEN INCL(rd.setterOpts, wordPart) END;
+			rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
+		END
+	END SpecialChar;
+(*
+	PROCEDURE LongChar (rd: StdReader);
+		VAR ra: TextRulers.Attributes;
+	BEGIN
+		ra := rd.ruler.style.attr;
+		rd.setterOpts := {wordJoin, wordPart};
+		rd.w := rd.attr.font.StringWidth(rd.string); rd.endW := rd.w
+	END LongChar;
+*)
+
+	PROCEDURE (rd: StdReader) Read;
+	(* pre: connected *)
+		VAR ra: TextRulers.Attributes; asc, dsc, w: INTEGER; ch: CHAR;
+	BEGIN
+		rd.Read^;
+		IF ~rd.eot THEN
+			IF rd.view = NIL THEN
+				rd.attr.font.GetBounds(asc, dsc, w);
+				rd.h := asc + dsc; rd.dsc := dsc
+			ELSE
+				GetViewPref(rd)
+			END;
+			IF (rd.view = NIL) OR (TextModels.maskChar IN rd.textOpts) THEN
+				ch := rd.string[0];
+				IF (rd.view = NIL)
+				 & (	(" " < ch) & (ch < "~") & (ch # "-")
+				 	OR (ch = digitspace)
+				 	OR (ch >= nbspace) & (ch # softhyphen)
+				  	)
+				THEN
+					GatherString(rd)
+				ELSE
+					SpecialChar(rd)
+				END
+			END
+		ELSE
+			ra := rd.ruler.style.attr;
+			rd.w := 0; rd.endW := 0; rd.h := ra.asc + ra.dsc; rd.dsc := ra.dsc
+		END
+	END Read;
+
+	PROCEDURE (rd: StdReader) AdjustWidth (start, pos: INTEGER; IN box: LineBox; VAR w: INTEGER);
+		VAR i: INTEGER; form: SET;
+	BEGIN
+		IF box.adj & (pos >= start + box.adjOff) THEN
+			form := box.ruler.style.attr.opts * adjustMask;
+			IF (form = blocked) & (rd.string[0] = " ") THEN
+				INC(w, box.adjW DIV box.spaces)
+			ELSIF (form # blocked) & (rd.string[0] = tab) THEN
+				INC(w, box.adjW)	(* is this correct ??? *)
+			END
+		END;
+		i := rd.tabIndex;	(* rd.string[0] = tab  =>  i >= 0 *)
+		IF (rd.string[0] = tab) & (i < box.ruler.style.attr.tabs.len) THEN
+			w := box.tabW[i]
+		END
+	END AdjustWidth;
+
+	PROCEDURE (rd: StdReader) SplitWidth (w: INTEGER): INTEGER;
+	BEGIN
+		IF (rd.string[1] = 0X) & (rd.view = NIL) THEN
+			RETURN (w + 1) DIV 2
+		ELSE RETURN w
+		END
+	END SplitWidth;
+
+
+	(* Worder *)
+
+	PROCEDURE SetWorder (VAR w: Worder; s: StdSetter; pos: INTEGER; OUT start: INTEGER);
+		CONST wordCutoff = LEN(s.rd.string);
+	BEGIN
+		start := s.ThisSequence(pos);
+		IF pos - start >= wordCutoff THEN
+			start := pos; WHILE pos - start < wordCutoff DO start := s.PreviousLine(start) END
+		END;
+		s.GetLine(start, w.box); w.next := start + w.box.len;
+		s.rd.Set(s.r, s.text, w.box.left, start, w.box.ruler, w.box.rpos, s.vw, s.hideMarks);
+		w.i := 0; s.rd.string[0] := 0X
+	END SetWorder;
+
+	PROCEDURE StepWorder (VAR w: Worder; s: StdSetter; VAR part: BOOLEAN);
+		VAR rd: Reader;
+	BEGIN
+		rd := s.rd;
+		IF rd.string[w.i] = 0X THEN
+			IF rd.pos < w.next THEN
+				rd.Read; w.i := 0
+			ELSE
+				IF ~w.box.eot THEN
+					s.GetLine(w.next, w.box);
+					s.rd.Set(s.r, s.text, w.box.left, w.next, w.box.ruler, w.box.rpos, s.vw, s.hideMarks);
+					rd.Read; w.i := 0;
+					INC(w.next, w.box.len)
+				ELSE
+					rd.string[0] := 0X
+				END
+			END
+		END;
+		IF rd.string[0] = 0X THEN	(* end of text *)
+			part := TRUE
+		ELSIF rd.string[1] = 0X THEN	(* special character *)
+			part := wordPart IN rd.setterOpts; INC(w.i)
+		ELSE	(* gathered sString *)
+			part := WordPart(rd.string[w.i], rd.string[w.i + 1]); INC(w.i)
+		END
+	END StepWorder;
+
+
+	(* StdSetter *)
+
+	PROCEDURE (s: StdSetter) CopyFrom (source: Stores.Store);
+	BEGIN
+		s.CopyFrom^(source);
+		WITH source: StdSetter DO
+			s.ruler := source.ruler; s.rpos := source.rpos; s.key := source.key;
+			s.rd := NIL; s.r := NIL
+		END
+	END CopyFrom;
+
+	PROCEDURE (s: StdSetter) Externalize (VAR wr: Stores.Writer);
+	BEGIN
+		s.Externalize^(wr);
+		wr.WriteVersion(maxStdVersion)
+	END Externalize;
+
+	PROCEDURE (s: StdSetter) Internalize (VAR rd: Stores.Reader);
+		VAR thisVersion: INTEGER;
+	BEGIN
+		s.Internalize^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		s.text := NIL; s.defRuler := NIL; s.ruler := NIL; s.rd := NIL; s.r := NIL
+	END Internalize;
+
+
+	PROCEDURE (s: StdSetter) ConnectTo (text: TextModels.Model;
+		defRuler: TextRulers.Ruler; vw: INTEGER; hideMarks: BOOLEAN
+	);
+	BEGIN
+		s.ConnectTo^(text, defRuler, vw, hideMarks);
+		ClearCache(s.key);
+		IF text # NIL THEN
+			s.ruler := defRuler; s.rpos := -1; s.key := nextKey; INC(nextKey)
+		ELSE
+			s.ruler := NIL
+		END
+	END ConnectTo;
+
+
+	PROCEDURE (s: StdSetter) ThisPage (pageH: INTEGER; pageNo: INTEGER): INTEGER;
+	(* pre: connected, 0 <= pageNo *)
+	(* post: (result = -1) & (pageNo >= maxPageNo) OR (result = pageStart(pageNo)) *)
+		VAR start, prev: INTEGER;
+	BEGIN
+		ASSERT(s.text # NIL, 20); ASSERT(pageNo >= 0, 21);
+		start := 0;
+		WHILE pageNo > 0 DO
+			prev := start; DEC(pageNo); start := s.NextPage(pageH, start);
+			IF start = prev THEN start := -1; pageNo := 0 END
+		END;
+		RETURN start
+	END ThisPage;
+
+	PROCEDURE (s: StdSetter) NextPage (pageH: INTEGER; start: INTEGER): INTEGER;
+	(* pre: connected, ThisPage(pageH, x) = start *)
+	(* post: (result = s.text.Length()) OR result = next-pageStart(start) *)
+		CONST
+			noBreakInside = TextRulers.noBreakInside;
+			pageBreak = TextRulers.pageBreak;
+			parJoin = TextRulers.parJoin;
+			regular = 0; protectInside = 1; joinFirst = 2; joinNext = 3; confirmSpace = 4;	(* state *)
+		VAR
+			box: LineBox; ra: TextRulers.Attributes;
+			h, asc, dsc, backup, pos, state: INTEGER; isRuler: BOOLEAN;
+
+		PROCEDURE FetchNextLine;
+		BEGIN
+			s.GetLine(pos, box);
+			IF box.len > 0 THEN
+				ra := box.ruler.style.attr; isRuler := box.rpos = pos;
+				asc := box.asc + s.GridOffset(dsc, box); dsc := box.dsc; h := asc + dsc
+			END
+		END FetchNextLine;
+
+		PROCEDURE HandleRuler;
+			CONST norm = 0; nbi = 1; pj = 2;
+			VAR strength: INTEGER;
+		BEGIN
+			IF isRuler & (pos > start) & ~(pageBreak IN ra.opts) THEN
+				IF parJoin IN ra.opts THEN strength := pj
+				ELSIF noBreakInside IN ra.opts THEN strength := nbi
+				ELSE strength := norm
+				END;
+				CASE state OF
+				| regular:
+					CASE strength OF
+					| norm:
+					| nbi: state := protectInside; backup := pos
+					| pj: state := joinFirst; backup := pos
+					END
+				| protectInside:
+					CASE strength OF
+					| norm: state := regular
+					| nbi: backup := pos
+					| pj: state := joinFirst; backup := pos
+					END
+				| joinFirst:
+					CASE strength OF
+					| norm: state := confirmSpace
+					| nbi: state := protectInside
+					| pj: state := joinNext
+					END
+				| joinNext:
+					CASE strength OF
+					| norm: state := confirmSpace
+					| nbi: state := protectInside
+					| pj:
+					END
+				| confirmSpace:
+					CASE strength OF
+					| norm: state := regular
+					| nbi: state := protectInside; backup := pos
+					| pj: state := joinFirst; backup := pos
+					END
+				END
+			END
+		END HandleRuler;
+
+		PROCEDURE IsEmptyLine (): BOOLEAN;
+		BEGIN
+			RETURN (box.right = box.left)  OR  s.hideMarks & isRuler & ~(pageBreak IN ra.opts)
+		END IsEmptyLine;
+
+	BEGIN
+		ASSERT(s.text # NIL, 20);
+		ASSERT(0 <= start, 21); ASSERT(start <= s.text.Length(), 22);
+		pos := start; dsc := -1;
+		FetchNextLine;
+		IF box.len > 0 THEN
+			state := regular;
+			REPEAT	(* at least one line per page *)
+				HandleRuler; DEC(pageH, h); INC(pos, box.len);
+				IF (state = confirmSpace) & ~IsEmptyLine() THEN state := regular END;
+				FetchNextLine
+			UNTIL (box.len = 0)  OR  (pageH - h < 0)  OR  isRuler & (pageBreak IN ra.opts);
+			IF ~isRuler OR ~(pageBreak IN ra.opts) THEN
+				WHILE (box.len > 0) & IsEmptyLine() DO	(* skip empty lines at top of page *)
+					HandleRuler; INC(pos, box.len); FetchNextLine
+				END
+			END;
+			HandleRuler;
+			IF (state # regular) & ~(isRuler & (pageBreak IN ra.opts) OR (box.len = 0)) THEN pos := backup END
+		END;
+		RETURN pos
+	END NextPage;
+
+
+	PROCEDURE (s: StdSetter) NextSequence (start: INTEGER): INTEGER;
+	(* pre: connected, ThisSequence(start) = start *)
+	(* post: (result = start) & last-line(start) OR (ThisSequence(t, result - 1) = start) *)
+		VAR rd: TextModels.Reader; ch: CHAR;
+	BEGIN
+		ASSERT(s.text # NIL, 20);
+		s.r := s.text.NewReader(s.r); rd := s.r; rd.SetPos(start);
+		REPEAT rd.ReadChar(ch) UNTIL rd.eot OR (ch = line) OR (ch = para);
+		IF rd.eot THEN RETURN start ELSE RETURN rd.Pos() END
+	END NextSequence;
+
+	PROCEDURE (s: StdSetter) ThisSequence (pos: INTEGER): INTEGER;
+	(* pre: connected, 0 <= pos <= t.Length() *)
+	(* post: (result = 0) OR (char(result - 1) IN {line, para}) *)
+		VAR rd: TextModels.Reader; start, limit: INTEGER; ch: CHAR;
+	BEGIN
+		ASSERT(s.text # NIL, 20); ASSERT(0 <= pos, 21); ASSERT(pos <= s.text.Length(), 22);
+		IF pos = 0 THEN
+			RETURN 0
+		ELSE
+			start := CachedSeqStart(s.key, pos);
+			IF start < 0 THEN
+				s.r := s.text.NewReader(s.r); rd := s.r; rd.SetPos(pos);
+				limit := paraShutoff;
+				REPEAT rd.ReadPrevChar(ch); DEC(limit)
+				UNTIL rd.eot OR (ch = line) OR (ch = para) OR (limit = 0);
+				IF rd.eot THEN start := 0 ELSE start := rd.Pos() + 1 END;
+				AddSeqStartToCache(s.key, pos, start)
+			END;
+			RETURN start
+		END
+	END ThisSequence;
+
+	PROCEDURE (s: StdSetter) PreviousSequence (start: INTEGER): INTEGER;
+	(* pre: connected, ThisSequence(t, start) = start *) 
+	(* post: (result = 0) & (start = 0) OR (result = ThisSequence(t, start - 1)) *)
+	BEGIN
+		IF start <= 1 THEN RETURN 0 ELSE RETURN s.ThisSequence(start - 1) END
+	END PreviousSequence;
+
+
+	PROCEDURE (s: StdSetter) ThisLine (pos: INTEGER): INTEGER;
+	(* pre: connected *)
+		VAR start, next: INTEGER;
+	BEGIN
+		next := s.ThisSequence(pos);
+		REPEAT start := next; next := s.NextLine(start) UNTIL (next > pos) OR (next = start);
+		RETURN start
+	END ThisLine;
+
+	PROCEDURE (s: StdSetter) NextLine (start: INTEGER): INTEGER;
+	(* pre: connected, ThisLine(start) = start *)
+	(* post: (result = 0) & (start = 0) OR
+				(result = start) & last-line(start) OR
+				(ThisLine(result - 1) = start) *)
+		VAR box: LineBox; len: INTEGER; i: INTEGER; eot: BOOLEAN;
+	BEGIN
+		i := CacheIndex(s.key, start);
+		IF i >= 0 THEN
+			len := boxCache[i].line.len; eot := boxCache[i].line.eot
+		ELSE
+			s.GetLine(start, box); len := box.len; eot := box.eot
+		END;
+		IF ~eot THEN RETURN start + len ELSE RETURN start END
+	END NextLine;
+
+	PROCEDURE (s: StdSetter) PreviousLine (start: INTEGER): INTEGER;
+	(* pre: connected, ThisLine(start) = start *)
+	(* post: (result = 0) & (start = 0) OR (result = ThisLine(start - 1)) *)
+	BEGIN
+		IF start <= 1 THEN start := 0 ELSE start := s.ThisLine(start - 1) END;
+		RETURN start
+	END PreviousLine;
+
+
+	PROCEDURE (s: StdSetter) GetWord (pos: INTEGER; OUT beg, end: INTEGER);
+	(* pre: connected, 0 <= pos <= s.text.Length() *)
+	(* post: beg <= pos <= end *)
+		CONST wordCutoff = LEN(s.rd.string);
+		VAR w: Worder; part: BOOLEAN;
+	BEGIN
+		ASSERT(s.text # NIL, 20); ASSERT(0 <= pos, 21); ASSERT(pos <= s.text.Length(), 22);
+		SetWorder(w, s, pos, beg); end := beg;
+		REPEAT
+			StepWorder(w, s, part); INC(end);
+			IF ~part THEN beg := end END
+		UNTIL end >= pos;
+		DEC(end);
+		REPEAT
+			StepWorder(w, s, part); INC(end)
+		UNTIL ~part OR (s.rd.string[0] = 0X) OR (end - beg > wordCutoff)
+	END GetWord;
+
+	PROCEDURE (s: StdSetter) GetLine (start: INTEGER; OUT box: LineBox);
+		VAR rd: Reader; ra: TextRulers.Attributes; brk: LineBox;
+			d, off, right, w: INTEGER; i, tabsN: INTEGER; form: SET; adj: BOOLEAN; ch: CHAR;
+
+		PROCEDURE TrueW (VAR b: LineBox; w: INTEGER): INTEGER;
+			VAR i: INTEGER; type: SET;
+		BEGIN
+			i := rd.tabIndex;
+			IF (0 <= i ) & (i < TextRulers.maxTabs) & (rd.string[0] # tab) THEN
+				type := rd.tabType * {TextRulers.centerTab, TextRulers.rightTab};
+				IF type = {TextRulers.centerTab} THEN
+					DEC(w, b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w DIV 2))
+				ELSIF type = {TextRulers.rightTab} THEN
+					DEC(w, b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w))
+				END
+			END;
+			RETURN w
+		END TrueW;
+
+		PROCEDURE Enclose (VAR b: LineBox; w: INTEGER);
+			VAR off, i, d: INTEGER; type: SET;
+		BEGIN
+			b.len := rd.pos - start; INC(b.right, w);
+			off := rd.attr.offset; i := rd.tabIndex;
+			IF rd.h - rd.dsc + off > b.asc THEN b.asc := rd.h - rd.dsc + off END;
+			IF rd.dsc - off > b.dsc THEN b.dsc := rd.dsc - off END;
+			IF rd.view # NIL THEN b.views := TRUE END;
+			IF (0 <= i ) & (i < TextRulers.maxTabs) THEN
+				IF rd.string[0] = tab THEN
+					b.tabW[i] := w
+				ELSE
+					type := rd.tabType * {TextRulers.centerTab, TextRulers.rightTab};
+					IF type = {TextRulers.centerTab} THEN
+						d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w DIV 2);
+						DEC(b.tabW[i], d); DEC(b.right, d)
+					ELSIF type = {TextRulers.rightTab} THEN
+						d := b.tabW[i] - MAX(minTabWidth, b.tabW[i] - w);
+						DEC(b.tabW[i], d); DEC(b.right, d)
+					END
+				END
+			END
+		END Enclose;
+
+	BEGIN
+		ASSERT(s.text # NIL, 20); ASSERT(0 <= start, 21); ASSERT(start <= s.text.Length(), 22);
+		i := CacheIndex(s.key, start);
+		IF i >= 0 THEN
+			GetFromCache(s, i, box)
+		ELSE
+			TextRulers.GetValidRuler(s.text, start, s.rpos, s.ruler, s.rpos);
+			IF s.rpos > start THEN s.ruler := s.defRuler; s.rpos := -1 END;
+			box.ruler := s.ruler; box.rpos := s.rpos;
+			ra := s.ruler.style.attr; tabsN := ra.tabs.len; right := Right(ra, s.vw);
+			s.r := s.text.NewReader(s.r);
+			IF start = 0 THEN s.r.SetPos(start); ch := para
+			ELSE s.r.SetPos(start - 1); s.r.ReadChar(ch)
+			END;
+			s.r.Read;
+
+(*
+			IF s.r.char = para THEN box.rbox := ~s.hideMarks; box.bop := s.hideMarks; box.left := 0
+			ELSIF ch = para THEN box.rbox := FALSE; box.bop := TRUE; box.left := ra.first
+			ELSE box.rbox := FALSE; box.bop := FALSE; box.left := ra.left
+			END;
+*)
+			IF s.r.char = para THEN box.rbox := TRUE; box.bop := FALSE; box.left := 0
+			ELSIF ch = para THEN box.rbox := FALSE; box.bop := TRUE; box.left := ra.first
+			ELSE box.rbox := FALSE; box.bop := FALSE; box.left := ra.left
+			END;
+(**)
+			box.views := FALSE;
+			box.asc := 0; box.dsc := 0; box.right := box.left;
+			box.len := 0; box.adjOff := 0; box.spaces := 0;
+			brk.right := 0;
+	
+			s.rd := s.NewReader(s.rd); rd := s.rd;
+			rd.Set(s.r, s.text, box.left, start, box.ruler, box.rpos, s.vw, s.hideMarks);
+			rd.Read;
+			WHILE ~rd.eot & (box.right + (*rd.w*) TrueW(box, rd.w) <= right)
+			& ~(lineBreak IN rd.setterOpts) DO
+				IF ~(wordJoin IN rd.setterOpts) & (box.right + rd.endW <= right) THEN
+					(*brk := box;*)
+					brk.len := box.len; brk.ruler := box.ruler; brk.rpos := box.rpos;
+					brk.left := box.left; brk.right := box.right; brk.asc := box.asc; brk.dsc := box.dsc;
+					brk.rbox := box.rbox; brk.bop := box.bop; brk.adj := box.adj; brk.eot := box.eot;
+					brk.views := box.views; brk.skipOff := box.skipOff; brk.adjOff := box.adjOff;
+					brk.spaces := box.spaces; brk.adjW := box.adjW;
+					i := 0; WHILE i < tabsN DO brk.tabW[i] := box.tabW[i]; INC(i) END;
+					(*---*)
+					Enclose(brk, rd.endW);
+					brk.eot := rd.r.eot	(* rd.r.eot one ahead of rd.eot *)
+				END;
+				box.adjOff := rd.adjStart - start; box.spaces := rd.spaces;
+				Enclose(box, rd.w);
+				rd.x := box.right; rd.Read
+			END;
+			IF (lineBreak IN rd.setterOpts) (* & ~box.rbox *) THEN Enclose(box, 0) END;
+			box.eot := rd.eot; adj := FALSE; box.skipOff := box.len;
+			IF box.right + rd.w > right THEN	(* rd.w > 0 => ~rd.eot & ~(lineBreak IN setterOpts) *)
+				IF ~(wordJoin IN rd.setterOpts) & (box.right + rd.endW <= right) THEN
+					IF rd.string[0] = " " THEN DEC(box.spaces) END;
+					Enclose(box, rd.endW);
+					adj := TRUE
+				ELSIF brk.right > 0 THEN
+					(*box := brk;*)
+					box.len := brk.len; box.ruler := brk.ruler; box.rpos := brk.rpos;
+					box.left := brk.left; box.right := brk.right; box.asc := brk.asc; box.dsc := brk.dsc;
+					box.rbox := brk.rbox; box.bop := brk.bop; box.adj := brk.adj; box.eot := brk.eot;
+					box.views := brk.views; box.skipOff := brk.skipOff; box.adjOff := brk.adjOff;
+					box.spaces := brk.spaces; box.adjW := brk.adjW;
+					i := 0; WHILE i < tabsN DO box.tabW[i] := brk.tabW[i]; INC(i) END;
+					(*---*)
+					box.skipOff := box.len - 1; adj := TRUE
+				ELSIF box.right = box.left THEN
+					Enclose(box, rd.w)	(* force at least one per line *)
+				END
+			ELSIF (box.right = box.left) & box.eot THEN
+				box.asc := ra.asc; box.dsc := ra.dsc	(* force empty line to ruler's default height *)
+			END;
+	
+			box.adj := FALSE;
+			d := right - box.right;
+			IF d > 0 THEN
+				form := ra.opts * adjustMask;
+				IF form = blocked THEN
+					IF adj & (box.spaces > 0) THEN
+						box.right := right; box.adj := TRUE; box.adjW := d
+					END
+				ELSIF form = rightFlush THEN
+					IF box.adjOff > 0 THEN
+						box.adjW := d; box.adj := TRUE
+					ELSE
+						INC(box.left, d)
+					END;
+					box.right := right
+				ELSIF form = centered THEN
+					IF box.adjOff > 0 THEN
+						box.adjW := d DIV 2; box.adj := TRUE
+					ELSE
+						INC(box.left, d DIV 2)
+					END;
+					INC(box.right, d DIV 2)
+				END
+			END;
+
+			AddToCache(s.key, start, box)
+		END;
+
+		ASSERT(box.eot OR (box.len > 0), 100)
+	END GetLine;
+
+
+	PROCEDURE (s: StdSetter) GetBox (start, end, maxW, maxH: INTEGER; OUT w, h: INTEGER);
+		VAR box: LineBox; asc, dsc: INTEGER;
+	BEGIN
+		ASSERT(s.text # NIL, 20);
+		ASSERT(0 <= start, 21);
+		ASSERT(start <= end, 22);
+		ASSERT(end <= s.text.Length(), 23);
+		w := 0; h := 0; dsc := -1;
+		IF maxW <= Views.undefined THEN maxW := MAX(INTEGER) END;
+		IF maxH <= Views.undefined THEN maxH := MAX(INTEGER) END;
+		WHILE (start < end) & (h < maxH) DO
+			s.GetLine(start, box);
+			IF box.rbox THEN w := MAX(w, Right(box.ruler.style.attr, s.vw))
+			ELSE w := MAX(w, box.right)
+			END;
+			asc := box.asc + s.GridOffset(dsc, box); dsc := box.dsc;
+			INC(start, box.len); INC(h, asc + dsc)
+		END;
+		w := MIN(w, maxW); h := MIN(h, maxH)
+	END GetBox;
+
+
+	PROCEDURE (s: StdSetter) NewReader (old: Reader): Reader;
+	(* pre: connected *)
+		VAR rd: StdReader;
+	BEGIN
+		ASSERT(s.text # NIL, 20);
+		IF (old # NIL) & (old IS StdReader) THEN RETURN old
+		ELSE NEW(rd); RETURN rd
+		END
+	END NewReader;
+
+
+	PROCEDURE (s: StdSetter) GridOffset (dsc: INTEGER; IN box: LineBox): INTEGER;
+		VAR ra: TextRulers.Attributes; h, h0: INTEGER;
+		(* minimal possible line spacing h0, minimal legal line spacing h *)
+	BEGIN
+		IF ~box.rbox THEN
+			ra := box.ruler.style.attr;
+			IF dsc < 0 THEN
+RETURN 0	(* no longer try to correct first line's grid position -- should be done when printing... *)
+(*
+				h0 := box.asc; h := ra.asc;
+				IF h < h0 THEN	(* override legal spacing if to small *)
+					h := h - (h - h0) DIV ra.grid * ra.grid	(* adjust to next larger grid line *)
+				END;
+				RETURN h - h0
+*)
+			ELSE
+				h0 := box.asc + dsc; h := ra.asc + ra.dsc;
+				IF h < h0 THEN h := h0 END;	(* override legal spacing if to small *)
+				RETURN - (-h) DIV ra.grid * ra.grid - h0	(* adjust to next larger grid line *)
+			END
+		ELSE
+			RETURN 0
+		END
+	END GridOffset;
+
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) New (): Setter;
+		VAR s: StdSetter;
+	BEGIN
+		NEW(s); s.text := NIL; RETURN s
+	END New;
+
+
+	(** miscellaneous **)
+
+	PROCEDURE Init;
+		VAR d: StdDirectory;
+	BEGIN
+		InitCache;
+		NEW(d); dir := d; stdDir := d
+	END Init;
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); dir := d
+	END SetDir;
+
+BEGIN
+	Init
+END TextSetters.

+ 1579 - 0
BlackBox/Text/Mod/Views.txt

@@ -0,0 +1,1579 @@
+MODULE TextViews;
+
+	(* THIS IS TEXT COPY OF BlackBox 1.6-rc6 Text/Mod/Views.odc *)
+	(* DO NOT EDIT *)
+
+(* could use +, $ in DrawLine cache implementation *)
+
+	IMPORT
+		Services, Fonts, Ports, Stores,
+		Models, Views, Controllers, Properties, Dialog, Printing, Containers,
+		TextModels, TextRulers, TextSetters;
+
+	CONST
+		(** v.DisplayMarks hide *)
+		show* = FALSE; hide* = TRUE;
+
+		(** v.ShowRange focusOnly **)
+		any* = FALSE; focusOnly* = TRUE;
+
+		parasign = 0B6X;	(* paragraph sign, to mark non-ruler paragraph breaks *)
+
+		mm = Ports.mm; inch16 = Ports.inch DIV 16; point = Ports.point;
+		maxScrollHeight = 16 * point; maxScrollSteps = 100; fuseScrollHeight = maxScrollHeight DIV 2;
+		maxHeight = maxScrollHeight * maxScrollSteps;
+		adjustMask = {TextRulers.leftAdjust, TextRulers.rightAdjust};
+
+		(* SetOp.mode *)
+		setMarks = 0; setSetter = 1; setDefs = 2;
+
+		scrollingKey = "#System:Scrolling";
+		viewSettingKey = "#System:ViewSetting";
+
+		minVersion = 0; maxVersion = 0; maxStdVersion = 0;
+
+
+	TYPE
+		View* = POINTER TO ABSTRACT RECORD (Containers.View) END;
+
+		Directory* = POINTER TO ABSTRACT RECORD
+			defAttr-: TextModels.Attributes
+		END;
+
+
+		Location* = RECORD
+			(** start of line and position of location **)
+			start*, pos*: INTEGER;
+			(** coordinates of location **)
+			x*, y*: INTEGER;
+			(** line dimensions at location **)
+			asc*, dsc*: INTEGER;
+			(** if view at location: **)
+			view*: Views.View;
+			l*, t*, r*, b*: INTEGER
+		END;
+
+
+		PositionMsg* = RECORD (Models.Message)
+			focusOnly*: BOOLEAN;
+			beg*, end*: INTEGER
+		END;
+
+
+		PageMsg* = RECORD (Properties.Message)
+			current*: INTEGER
+		END;
+
+
+		Line = POINTER TO RECORD
+			next: Line;
+			start, asc, h: INTEGER;
+			attr: TextRulers.Attributes;	(* attr = box.ruler.style.attr *)
+			box: TextSetters.LineBox	(* caching of box.rpos not consistent *)
+		END;
+
+		StdView = POINTER TO RECORD (View)
+			(* model *)
+			text: TextModels.Model;
+			org: INTEGER;
+			dy: INTEGER;	(* 0 <= dy < Height(first line) *)
+			defRuler: TextRulers.Ruler;
+			defAttr: TextModels.Attributes;
+			hideMarks: BOOLEAN;
+			(* general state *)
+			cachedRd: TextSetters.Reader;
+			(* line grid cache *)
+			trailer: Line;	(* trailer # NIL => trailer.eot, trailer.next # trailer *)
+			bot: INTEGER;	(* max(f : f seen by Restore : f.b) *)
+			(* setter *)
+			setter, setter0: TextSetters.Setter	(* setter # setter0 lazily detects setter change *)
+		END;
+
+		StdDirectory = POINTER TO RECORD (Directory) END;
+
+		ScrollOp = POINTER TO RECORD (Stores.Operation)
+			v: StdView;
+			org, dy: INTEGER;
+			bunchOrg, bunchDy: INTEGER;
+			bunch: BOOLEAN;	(* bunch => bunchOrg, bunchDy valid *)
+			silent: BOOLEAN	(* original caller of Do(op) already handled situation *)
+		END;
+
+		SetOp = POINTER TO RECORD (Stores.Operation)
+			mode: INTEGER;
+			view: StdView;
+			hideMarks: BOOLEAN;
+			setter: TextSetters.Setter;
+			defRuler: TextRulers.Ruler;
+			defAttr: TextModels.Attributes
+		END;
+
+		FindAnyFrameMsg = RECORD (Views.Message)
+			(* find frame with smallest height (frame.b - frame.t) that displays view; NIL if none found *)
+			frame: Views.Frame	(* OUT, initially NIL *)
+		END;
+
+		FindFocusFrameMsg = RECORD (Controllers.Message)
+			(* find outermost focus frame displaying view; NIL if none found *)
+			view: Views.View;	(* IN *)
+			frame: Views.Frame	(* OUT, initially NIL *)
+		END;
+
+
+	VAR
+		ctrlDir-: Containers.Directory;
+		dir-, stdDir-: Directory;
+
+
+	(* forward used in GetStart, UpdateView, ShowRangeIn *)
+	PROCEDURE ^ DoSetOrigin (v: StdView; org, dy: INTEGER; silent: BOOLEAN);
+
+
+	(** View **)
+
+	PROCEDURE (v: View) Internalize2- (VAR rd: Stores.Reader), EXTENSIBLE;
+	(** pre: ~v.init **)
+	(** post: v.init **)
+		VAR thisVersion: INTEGER;
+	BEGIN
+		(*v.Internalize^(rd);*)
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxVersion, thisVersion)
+	END Internalize2;
+
+	PROCEDURE (v: View) Externalize2- (VAR wr: Stores.Writer), EXTENSIBLE;
+	(** pre: v.init **)
+	BEGIN
+		(*v.Externalize^(wr);*)
+		wr.WriteVersion(maxVersion)
+	END Externalize2;
+
+	PROCEDURE (v: View) ThisModel* (): TextModels.Model, EXTENSIBLE;
+		VAR m: Containers.Model;
+	BEGIN
+		m := v.ThisModel^();
+		IF m # NIL THEN
+			RETURN m(TextModels.Model)
+		ELSE
+			RETURN NIL
+		END
+	END ThisModel;
+
+	PROCEDURE (v: View) DisplayMarks* (hide: BOOLEAN), NEW, ABSTRACT;
+	PROCEDURE (v: View) HidesMarks* (): BOOLEAN, NEW, ABSTRACT;
+	PROCEDURE (v: View) SetSetter* (setter: TextSetters.Setter), NEW, ABSTRACT;
+	PROCEDURE (v: View) ThisSetter* (): TextSetters.Setter, NEW, ABSTRACT;
+	PROCEDURE (v: View) SetOrigin* (org, dy: INTEGER), NEW, ABSTRACT;
+	(** post: org = ThisLine(org) => v.org = org, v.dy = dy; else v.org = ThisLine(org), v.dy = 0 **)
+
+	PROCEDURE (v: View) PollOrigin* (OUT org, dy: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (v: View) SetDefaults* (r: TextRulers.Ruler; a: TextModels.Attributes),
+		NEW, ABSTRACT;
+	(** pre: r.init, a.init **)
+
+	PROCEDURE (v: View) PollDefaults* (OUT r: TextRulers.Ruler; OUT a: TextModels.Attributes),
+		NEW, ABSTRACT;
+	PROCEDURE (v: View) GetThisLocation* (f: Views.Frame; pos: INTEGER; OUT loc: Location),
+		NEW, ABSTRACT;
+
+	PROCEDURE (v: View) GetRect* (f: Views.Frame; view: Views.View; OUT l, t, r, b: INTEGER);
+		VAR con: Models.Context; loc: Location; pos: INTEGER;
+	BEGIN
+		con := view.context;
+		ASSERT(con # NIL, 20); ASSERT(con.ThisModel() = v.ThisModel(), 21);
+		pos := con(TextModels.Context).Pos();
+		v.GetThisLocation(f, pos, loc);
+		IF loc.view = view THEN
+			l := loc.l; t := loc.t; r := loc.r; b := loc.b
+		ELSE
+			l := MAX(INTEGER); t := MAX(INTEGER); r := l; b := t
+		END
+	END GetRect;
+
+	PROCEDURE (v: View) GetRange* (f: Views.Frame; OUT beg, end: INTEGER), NEW, ABSTRACT;
+	(** post: beg = beg of first visible line, end = end of last visible line **)
+
+	PROCEDURE (v: View) ThisPos* (f: Views.Frame; x, y: INTEGER): INTEGER, NEW, ABSTRACT;
+	PROCEDURE (v: View) ShowRangeIn* (f: Views.Frame; beg, end: INTEGER), NEW, ABSTRACT;
+	PROCEDURE (v: View) ShowRange* (beg, end: INTEGER; focusOnly: BOOLEAN), NEW, ABSTRACT;
+	(** post: in all frames (resp. in front or otherwise target frame if focusOnly):
+		if possible, first visible pos <= k <= last visible pos,
+		with k = beg if beg = end and beg <= k < end otherwise **)
+
+
+	(** Directory **)
+
+	PROCEDURE (d: Directory) Set* (defAttr: TextModels.Attributes), NEW, EXTENSIBLE;
+	BEGIN
+		ASSERT(defAttr # NIL, 20); ASSERT(defAttr.init, 21);
+		d.defAttr := defAttr
+	END Set;
+
+	PROCEDURE (d: Directory) New* (text: TextModels.Model): View, NEW, ABSTRACT;
+
+
+	(** miscellaneous **)
+
+	PROCEDURE SetCtrlDir* (d: Containers.Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); ctrlDir := d
+	END SetCtrlDir;
+
+	PROCEDURE SetDir* (d: Directory);
+	BEGIN
+		ASSERT(d # NIL, 20); dir := d
+	END SetDir;
+
+
+	PROCEDURE Focus* (): View;
+		VAR v: Views.View;
+	BEGIN
+		v := Controllers.FocusView();
+		IF (v # NIL) & (v IS View) THEN RETURN v(View) ELSE RETURN NIL END
+	END Focus;
+
+	PROCEDURE FocusText* (): TextModels.Model;
+		VAR v: View;
+	BEGIN
+		v := Focus();
+		IF v # NIL THEN RETURN v.ThisModel() ELSE RETURN NIL END
+	END FocusText;
+
+	PROCEDURE Deposit*;
+	BEGIN
+		Views.Deposit(dir.New(NIL))
+	END Deposit;
+
+
+	PROCEDURE ShowRange* (text: TextModels.Model; beg, end: INTEGER; focusOnly: BOOLEAN);
+	(** post: in all front or target frames displaying a view displaying t:
+		if possible, first visible pos <= k <= last visible pos,
+		with k = beg if beg = end and beg <= k < end otherwise **)
+		VAR pm: PositionMsg;
+	BEGIN
+		ASSERT(text # NIL, 20);
+		pm.beg := beg; pm.end := end; pm.focusOnly := focusOnly;
+		Models.Broadcast(text, pm)
+	END ShowRange;
+
+
+	PROCEDURE ThisRuler* (v: View; pos: INTEGER): TextRulers.Ruler;
+		VAR r: TextRulers.Ruler; a: TextModels.Attributes; rpos: INTEGER;
+	BEGIN
+		v.PollDefaults(r, a); rpos := -1; TextRulers.GetValidRuler(v.ThisModel(), pos, -1, r, rpos);
+		RETURN r
+	END ThisRuler;
+
+
+	(* auxiliary procedures *)
+
+	PROCEDURE GetReader (v: StdView; start: INTEGER; IN box: TextSetters.LineBox
+	): TextSetters.Reader;
+		VAR st: TextSetters.Setter; rd: TextSetters.Reader;
+	BEGIN
+		ASSERT(box.ruler # NIL, 100);
+		st := v.ThisSetter();
+		rd := v.cachedRd; v.cachedRd := NIL;	(* reader recycling *)
+		rd := st.NewReader(rd);
+		rd.Set(rd.r, v.text, box.left, start, box.ruler, box.rpos, st.vw, st.hideMarks);
+		RETURN rd
+	END GetReader;
+
+	PROCEDURE CacheReader (v: StdView; rd: TextSetters.Reader);
+	BEGIN
+		ASSERT(v.cachedRd = NIL, 20);
+		v.cachedRd := rd
+	END CacheReader;
+
+
+	(* line descriptors *)
+
+	PROCEDURE SetLineAsc (st: TextSetters.Setter; t: Line; dsc: INTEGER);
+	(* pre: dsc: descender of previous line (-1 if first line) *)
+	BEGIN
+		t.asc := t.box.asc + st.GridOffset(dsc, t.box);
+		t.h := t.asc + t.box.dsc
+	END SetLineAsc;
+
+	PROCEDURE NewLine (st: TextSetters.Setter; start, dsc: INTEGER): Line;
+	(* pre: start: start of line to measure; dsc: descender of previous line (-1 if first line) *)
+		VAR t: Line;
+	BEGIN
+		NEW(t); st.GetLine(start, t.box);
+		t.start := start; SetLineAsc(st, t, dsc);
+		t.attr := t.box.ruler.style.attr;
+		RETURN t
+	END NewLine;
+
+	PROCEDURE AddLine (st: TextSetters.Setter; VAR t: Line; VAR start, y: INTEGER);
+	BEGIN
+		t.next := NewLine(st, start, t.box.dsc); t := t.next;
+		INC(start, t.box.len); INC(y, t.h)
+	END AddLine;
+
+	PROCEDURE InitLines (v: StdView);
+		VAR asc, dsc, w: INTEGER; t0, t: Line; start, y: INTEGER;
+	BEGIN
+		v.defAttr.font.GetBounds(asc, dsc, w);
+		NEW(t0); start := v.org; y := v.dy;
+		t0.box.dsc := -1;	(* dsc = -1: trailer.next is first line *)
+		t := t0; AddLine(v.ThisSetter(), t, start, y); t.next := t0;	(* at least one valid line desc *)
+		t0.start := start; t0.asc := asc; t0.h := asc + dsc;	(* trailer.(asc, h) for caret display following last line *)
+		t0.attr := NIL;
+		t0.box.eot := TRUE; t0.box.len := 0;
+		t0.box.ruler := NIL;
+		t0.box.left := -1;	(* make trailer async to every other line *)
+		v.trailer := t0; v.bot := 0
+	END InitLines;
+
+	PROCEDURE ExtendLines (v: StdView; bot: INTEGER);
+		VAR st: TextSetters.Setter; t0, t: Line; start, y: INTEGER;
+	BEGIN
+		IF bot >= v.bot THEN
+			t0 := v.trailer; start := t0.start;
+			y := v.dy; t := t0; WHILE t.next # t0 DO t := t.next; INC(y, t.h) END;
+			IF (y < bot) & ~t.box.eot THEN
+				st := v.ThisSetter();
+				REPEAT AddLine(st, t, start, y) UNTIL (y >= bot) OR t.box.eot;
+				t.next := t0; t0.start := start
+			END;
+			v.bot := bot
+		END
+	END ExtendLines;
+
+	PROCEDURE ReduceLines (v: StdView; bot: INTEGER);
+		VAR t0, t: Line; y: INTEGER;
+	BEGIN
+		IF bot <= v.bot THEN
+			t0 := v.trailer; y := v.dy;
+			t := t0; WHILE (t.next # t0) & (y < bot) DO t := t.next; INC(y, t.h) END;
+			t0.start := t.next.start; t.next := t0;
+			v.bot := bot
+		END
+	END ReduceLines;
+
+	PROCEDURE ValidateLines (v: StdView; bot: INTEGER);
+		VAR st: TextSetters.Setter; w, h, len: INTEGER;
+	BEGIN
+		IF v.setter # NIL THEN
+			v.context.GetSize(w, h);	(* possibly adapt to changed width *)
+			IF v.setter.vw # w THEN v.setter0 := NIL; v.trailer := NIL END
+		END;
+		len := v.text.Length();
+		IF (v.org > len) OR (v.trailer # NIL) & (v.trailer.start > len) THEN v.trailer := NIL END;
+		IF v.trailer = NIL THEN
+			IF v.org > len THEN v.org := len END;
+			st := v.ThisSetter(); v.org := st.ThisLine(v.org);
+			InitLines(v)
+		END;
+		ExtendLines(v, bot)
+	END ValidateLines;
+
+	PROCEDURE PrependLines (v: StdView);
+		VAR st: TextSetters.Setter;  t0, t1, t: Line; start, y: INTEGER;
+	BEGIN
+		t0 := v.trailer; start := v.org; y := v.dy;
+		IF t0.start # start THEN
+			st := v.ThisSetter();
+			t := t0; t1 := t0.next;
+			WHILE (t1.start # start) & (y < v.bot) DO AddLine(st, t, start, y) END;
+			IF y >= v.bot THEN
+				t.next := t0; t0.start := start
+			ELSE
+				t.next := t1;
+				IF t1 # v.trailer THEN SetLineAsc(st, t1, t.box.dsc) END
+			END
+		END
+	END PrependLines;
+
+
+	(* update frame after insert/delete/replace *)
+
+	PROCEDURE ThisViewLine (v: StdView; y: INTEGER): Line;
+	(* pre: 0 <= y < v.bot *)
+		VAR t: Line; py: INTEGER;
+	BEGIN
+		t := v.trailer.next; py := v.dy;
+		WHILE ~t.box.eot & (py + t.h < y) DO INC(py, t.h); t := t.next END;
+		RETURN t
+	END ThisViewLine;
+
+	PROCEDURE LocateThisLine (v: StdView; start: INTEGER; OUT t: Line; OUT y: INTEGER);
+		VAR t1: Line;
+	BEGIN
+		t := v.trailer.next; y := v.dy;
+		t1 := v.trailer.next;
+		WHILE t.start # start DO INC(y, t.h); t := t.next; ASSERT(t # t1, 100) END
+	END LocateThisLine;
+
+	PROCEDURE GetStart (st: TextSetters.Setter; v: StdView; beg: INTEGER; OUT start: INTEGER);
+	(* find start of line containing beg after text change; tuned using valid line descs *)
+		VAR s, t: Line;
+	BEGIN
+		s := v.trailer; t := s.next;
+		WHILE (t # v.trailer) & (t.start + t.box.len < beg) DO s := t; t := s.next END;
+		IF s # v.trailer THEN	(* at least first line desc possibly still valid *)
+			start := st.NextLine(s.start);	(* NextLine can be much cheaper than ThisLine *)
+			IF start # t.start THEN
+				GetStart(st, v, s.start, start)
+			ELSIF ~t.box.eot & (start + t.box.len = beg) & (st.NextLine(start) = beg) THEN
+				start := beg
+			END
+		ELSE
+			IF v.org <= v.text.Length() THEN
+				start := st.ThisLine(v.org)
+			ELSE
+				start := st.ThisLine(v.text.Length())
+			END;
+			IF start < v.org THEN
+				DoSetOrigin(v, start, 0, TRUE)
+			ELSIF start > v.org THEN
+				start := v.org
+			END
+		END
+	END GetStart;
+
+	PROCEDURE GetStringStart (v: StdView; t: Line; pos: INTEGER; OUT p1, x: INTEGER);
+		VAR rd: TextSetters.Reader;
+	BEGIN
+		p1 := t.start; x := t.box.left;
+		IF t.box.views THEN
+			rd := GetReader(v, p1, t.box); rd.Read;
+			WHILE ~rd.eot & (rd.pos <= pos) DO
+				rd.AdjustWidth(t.start, p1, t.box, rd.w); INC(rd.x, rd.w);
+				IF rd.view # NIL THEN p1 := rd.pos; x := rd.x END;
+				rd.Read
+			END;
+			CacheReader(v, rd)
+		END
+	END GetStringStart;
+
+	PROCEDURE InSynch (t0, t1: Line): BOOLEAN;
+	BEGIN
+		RETURN (t0.start = t1.start) & (t0.asc = t1.asc) & (t0.attr = t1.attr)
+			& (t0.box.left = t1.box.left) & (t0.box.asc = t1.box.asc) & (t0.box.dsc = t1.box.dsc)
+			& (t0.box.rbox = t1.box.rbox) & (t0.box.bop = t1.box.bop)
+	END InSynch;
+
+	PROCEDURE RebuildView (v: StdView);
+	BEGIN
+		v.setter0 := NIL;
+		IF v.trailer # NIL THEN v.trailer := NIL; v.bot := 0; Views.Update(v, Views.rebuildFrames) END
+	END RebuildView;
+
+	PROCEDURE UpdateIn (v: StdView; l, t, b: INTEGER);
+	BEGIN
+		Views.UpdateIn(v, l, t, MAX(INTEGER), b, Views.rebuildFrames)
+	END UpdateIn;
+
+	PROCEDURE UpdateFrames (v: StdView; t0, t1, u: Line; beg, y0, yu: INTEGER);
+		VAR t, te: Line; b, x, b0, b1, top, bot: INTEGER;
+	BEGIN
+		IF ((beg < t0.next.start) OR t0.box.eot) & ~t0.box.adj
+		 & ((beg < t1.next.start) OR t1.box.eot) & ~t1.box.adj
+		 & InSynch(t0, t1) THEN
+			GetStringStart(v, t1, beg, beg, x)
+		ELSE
+			beg := t1.start
+		END;
+		b := y0; t := t0; WHILE t # u DO INC(b, t.h); t := t.next END;
+		IF b = yu THEN
+			te := u
+		ELSE	(* t = u *)
+			te := v.trailer;
+			b0 := b; WHILE t # v.trailer DO INC(b0, t.h); t := t.next END;
+			IF yu < b THEN ExtendLines(v, v.bot) ELSE ReduceLines(v, v.bot) END;
+			b1 := y0; t := t1; WHILE t # v.trailer DO INC(b1, t.h); t := t.next END;
+			IF b1 < b0 THEN UpdateIn(v, 0, b1, b0) END	(* erase trailer *)
+		END;
+		IF t1.start < beg THEN	(* conserve head of t1 *)
+			UpdateIn(v, x, y0, y0 + t1.h);	(* redraw tail of t1 *)
+			top := y0 + t1.h
+		ELSE
+			top := y0
+		END;
+		bot := y0; REPEAT INC(bot, t1.h); t1 := t1.next UNTIL t1 = te;
+		IF top < bot THEN UpdateIn(v, 0, top, bot) END	(* redraw affected lines *)
+	END UpdateFrames;
+
+	PROCEDURE UpdateView (v: StdView; beg, end, delta: INTEGER);
+		VAR st: TextSetters.Setter; r: TextRulers.Ruler; rpos: INTEGER;
+			s0, t0, t, tn, u: Line; start, y, y0: INTEGER;
+	BEGIN
+		IF v.trailer # NIL THEN
+			v.setter0 := NIL; st := v.ThisSetter();
+			IF (beg <= v.trailer.start) & ((end >= v.org) OR (end - delta >= v.org)) THEN
+				GetStart(st, v, beg, start);
+				y0 := v.dy; s0 := v.trailer;
+				WHILE s0.next.start < start DO s0 := s0.next; INC(y0, s0.h) END;
+	
+				t := s0.next; WHILE (t # v.trailer) & (t.start < end) DO t := t.next END;
+				IF (t = v.trailer.next) & (t.start >= end) THEN
+					REPEAT
+						INC(t.start, delta);
+						IF t.box.rpos >= end THEN INC(t.box.rpos, delta) END;
+						t := t.next
+					UNTIL t = v.trailer.next
+				ELSE
+					WHILE (t # v.trailer.next) & (t.start >= end) DO
+						INC(t.start, delta);
+						IF t.box.rpos >= end THEN INC(t.box.rpos, delta) END;
+						t := t.next
+					END
+				END;
+				tn := s0; y := y0; t0 := s0.next; u := t0;
+				REPEAT
+					t := tn; AddLine(st, tn, start, y); (* start = end(tn), y = bot(tn) *)
+					WHILE (u # v.trailer) & (u.start < tn.start) DO u := u.next END
+				UNTIL tn.box.eot OR (y > v.bot)
+					OR (tn.start >= end) & (u.start = tn.start) & (u.box.len = tn.box.len)
+						& (u.asc = tn.asc) & (u.attr = tn.attr) & (u.box.dsc = tn.box.dsc)
+						& (u.box.rpos = tn.box.rpos);	(* can be expensive ... *)
+				IF tn.box.eot OR (y > v.bot) THEN
+					t := tn; u := v.trailer; v.trailer.start := start
+				ELSE
+					DEC(y, tn.h)
+				END;
+				t.next := u;
+				IF (s0 # v.trailer) & (s0.next # v.trailer) THEN s0.box.eot := FALSE END;
+				ASSERT(v.trailer.start <= v.text.Length(), 100);
+				UpdateFrames(v, t0, s0.next, u, beg, y0, y)
+			ELSIF end <= v.org THEN
+				INC(v.org, delta);
+(*
+				IF end < v.org - delta - 500 THEN start := v.org ELSE start := st.ThisLine(v.org) END;
+				(* this is not safe; even a change 500 characters away could force the view's origin to a
+				new position in order to maintain the invariant that the origin always falls on a line start;
+				however, ThisLine can be quite expensive -- can we rely on TextSetters cache ? *)
+*)
+				start := st.ThisLine(v.org);
+				r := v.defRuler; rpos := -1; TextRulers.GetValidRuler(v.text, start, -1, r, rpos);
+				IF (v.org = start) & (v.trailer.next.attr = r.style.attr) THEN
+					t := v.trailer;
+					REPEAT
+						t := t.next; INC(t.start, delta);
+						IF t.box.rpos < start THEN t.box.rpos := rpos ELSE INC(t.box.rpos, delta) END
+					UNTIL t = v.trailer
+				ELSE
+					DoSetOrigin(v, start, 0, TRUE); RebuildView(v)
+				END
+			END
+		END
+	END UpdateView;
+
+	PROCEDURE StyleUpdate (v: StdView; oldAttr: TextRulers.Attributes);
+		VAR t: Line; beg: INTEGER; first: BOOLEAN;
+	BEGIN
+		IF v.trailer # NIL THEN
+			t := v.trailer.next; first := TRUE;
+			WHILE t # v.trailer DO
+				WHILE (t # v.trailer) & (t.attr # oldAttr) DO t := t.next END;
+				IF t # v.trailer THEN
+					IF first THEN v.Neutralize; first := FALSE END;
+					beg := t.start; t := t.next;
+					WHILE (t # v.trailer) & (t.attr = oldAttr) DO t := t.next END;
+					UpdateView(v, beg, t.start, 0)
+				END
+			END
+		END
+	END StyleUpdate;
+
+
+	(* line drawing *)
+
+	PROCEDURE DrawLine (v: StdView;
+		start: INTEGER; IN box: TextSetters.LineBox;
+		f: Views.Frame; l, r, y, t: INTEGER; pageF: BOOLEAN
+	);
+	(* pre: area cleared *)
+	(* [l,r) for high-level clipping to tune update after small change *)
+		CONST cacheLen = 128;
+		VAR rd: TextSetters.Reader; ra: TextRulers.Attributes;
+			v1: Views.View; c: Containers.Controller;
+			py, end, skip: INTEGER;
+			cache: RECORD	(* initially: long = TRUE, len = 0 *)
+				x, y: INTEGER; color: Ports.Color; font: Fonts.Font;
+				len: INTEGER;
+				buf: ARRAY cacheLen OF CHAR
+			END;
+
+		PROCEDURE FlushCaches;
+		BEGIN
+			IF cache.len > 0 THEN
+				cache.buf[cache.len] := 0X;
+				f.DrawString(cache.x, cache.y, cache.color, cache.buf, cache.font)
+			END;
+			cache.len := 0
+		END FlushCaches;
+
+		PROCEDURE CacheString (x, y: INTEGER; c: INTEGER; IN s: ARRAY OF CHAR;
+			f: Fonts.Font
+		);
+			VAR i, j, len: INTEGER;
+		BEGIN
+			len := 0; WHILE s[len] # 0X DO INC(len) END;
+			IF (cache.len + len >= cacheLen) OR (cache.y # y) OR (cache.color # c) OR (cache.font # f) THEN
+				FlushCaches
+			END;
+			ASSERT(cache.len + len < cacheLen, 100);
+			IF cache.len = 0 THEN cache.x := x; cache.y := y; cache.color := c; cache.font := f END;
+			i := 0; j := cache.len;
+			WHILE i < len DO cache.buf[j] := s[i]; INC(i); INC(j) END;
+			cache.len := j
+		END CacheString;
+
+(*
+		PROCEDURE CacheString (x, y: INTEGER; c: INTEGER; IN s: ARRAY OF CHAR;
+			f: Fonts.Font
+		);
+			VAR i, j, len: INTEGER;
+		BEGIN
+			(* flush first, then promote *)
+			len := 0; WHILE s[len] # 0X DO INC(len) END;
+			IF (cache.len + len >= cacheLen) OR (cache.y # y) OR (cache.color # c) OR (cache.font # f) THEN
+				FlushCaches
+			END;
+			IF (cache.len > 0) & cache.short THEN	(* promote short chars to chars *)
+				i := 0; WHILE i < cache.len DO cache.buf[i] := cache.sbuf[i]; INC(i) END
+			END;
+			cache.short := FALSE;
+			ASSERT(cache.len + len < cacheLen, 100);
+			IF cache.len = 0 THEN cache.x := x; cache.y := y; cache.color := c; cache.font := f END;
+			i := 0; j := cache.len;
+			WHILE i < len DO cache.buf[j] := s[i]; INC(i); INC(j) END;
+			cache.len := j
+		END CacheString;
+*)
+
+	BEGIN
+		IF box.len > 0 THEN
+			cache.len := 0;
+			end := start + box.len; skip := start + box.skipOff;
+			rd := GetReader(v, start, box); rd.Read;
+			WHILE ~rd.eot & (rd.pos <= end) & (rd.x < r) DO
+				IF rd.pos > skip THEN rd.w := rd.endW END;
+				rd.AdjustWidth(start, rd.pos, box, rd.w);
+				IF rd.x + rd.w > l THEN
+					v1 := rd.view;
+					IF v1 # NIL THEN
+						FlushCaches;
+						IF ~((TextModels.hideable IN rd.textOpts) & v.hideMarks) THEN
+							c := v.ThisController();
+							Views.InstallFrame(f, v1,
+								rd.x, y - rd.attr.offset + rd.dsc - rd.h, 0,
+								(c # NIL) & (v1 = c.ThisFocus()) )
+						END
+					ELSIF (rd.h > 0) & (rd.w > 0) THEN
+						IF box.rbox & ~v.hideMarks THEN rd.string[0] := parasign END;	(* ¶ sign *)
+						py := y - rd.attr.offset;
+						IF rd.string[0] > " " THEN
+							CacheString(rd.x, py, rd.attr.color, rd.string, rd.attr.font);
+							IF ~v.hideMarks & (TextModels.hideable IN rd.textOpts) THEN
+								f.DrawRect(rd.x, py - box.asc + f.dot,
+									MIN(rd.x + rd.w, f.r), py + box.dsc - f.dot, 0, Ports.grey25)
+							END
+						ELSIF rd.string[0] # 0X THEN
+							FlushCaches;
+							IF ~v.hideMarks & (TextModels.hideable IN rd.textOpts) THEN
+								f.DrawRect(rd.x, py - box.asc + f.dot, rd.x + rd.w, py + box.dsc - f.dot, 0, Ports.grey25)
+							END
+						ELSE FlushCaches
+						END
+					END
+				END;
+				INC(rd.x, rd.w); rd.Read
+			END;
+			FlushCaches;
+			CacheReader(v, rd)
+		END;
+		IF v.hideMarks & ~pageF THEN
+			ra := box.ruler.style.attr;
+			IF TextRulers.pageBreak IN ra.opts THEN
+				IF (box.rpos = start) & (ra.lead >= f.dot) THEN
+					f.DrawLine(l, t, r - f.dot, t, 0, Ports.grey50)
+				ELSIF (box.rpos = start - 1) & (ra.lead < f.dot) THEN
+					f.DrawLine(l, t, r - f.dot, t, 0, Ports.grey50)
+				END
+			END
+		END
+	END DrawLine;
+
+	PROCEDURE DrawDecorations (v: StdView; u: Line; f: Views.Frame; l, t, r, b: INTEGER);
+		VAR a: TextRulers.Attributes; i,  x: INTEGER; col: Ports.Color;
+			st: TextSetters.Setter; srd: TextSetters.Reader; rd: TextModels.Reader;
+	BEGIN
+		IF t < b THEN
+			i := 0; a := u.attr; srd := NIL;
+			WHILE i < a.tabs.len DO
+				IF TextRulers.barTab IN a.tabs.tab[i].type THEN
+					x := a.tabs.tab[i].stop;
+					IF (l <= x) & (x < r) THEN
+						IF u.box.rpos = -1 THEN col := v.defAttr.color
+						ELSIF srd = NIL THEN
+							st := v.ThisSetter();
+							srd := v.cachedRd; v.cachedRd := NIL;
+							srd := st.NewReader(srd);
+							srd.Set(srd.r, v.text, 0, 0, v.defRuler, 0, st.vw, st.hideMarks); rd := srd.r;
+							rd.SetPos(u.box.rpos); rd.Read; col := rd.attr.color
+						END;
+						f.DrawLine(x, t, x, b - f.dot, 0, col)
+					END
+				END;
+				INC(i)
+			END;
+			IF srd # NIL THEN CacheReader(v, srd) END
+		END
+	END DrawDecorations;
+
+
+	(* focus-message handling *)
+
+	PROCEDURE PollSection (v: StdView; f: Views.Frame; VAR msg: Controllers.PollSectionMsg);
+		CONST ms = maxScrollSteps; mh = maxScrollHeight;
+		VAR t: Line; steps, step: INTEGER;
+	BEGIN
+		IF msg.vertical THEN
+			ValidateLines(v, f.b); t := v.trailer.next;
+			IF t.h > 0 THEN
+				steps := -((-t.h) DIV mh); step := -(v.dy DIV mh)
+			ELSE steps := 1; step := 0
+			END;
+			msg.wholeSize := v.text.Length() * ms;
+			msg.partPos := v.org * ms + t.box.len * ms * step DIV steps;
+			msg.partSize := 0;
+			msg.valid := (v.org > 0) OR (t.h > mh) OR (t.next # v.trailer);
+			msg.done := TRUE
+		END
+	END PollSection;
+
+	PROCEDURE Scroll (v: StdView; f: Views.Frame; VAR msg: Controllers.ScrollMsg);
+		VAR st: TextSetters.Setter; box, box0: TextSetters.LineBox;
+			t, t1, trailer: Line; org, len, dy, h, h1, sh, steps, step: INTEGER;
+			poll: Controllers.PollSectionMsg;
+	BEGIN
+		IF msg.vertical THEN
+			poll.vertical := TRUE;
+			PollSection(v, f, poll)
+		END;
+		IF msg.vertical & poll.valid THEN
+			org := v.org; dy := v.dy; st := v.ThisSetter(); trailer := v.trailer;
+			CASE msg.op OF
+			  Controllers.decLine:
+				IF dy <= -(maxScrollHeight + fuseScrollHeight) THEN
+					INC(dy, maxScrollHeight)
+				ELSIF dy < 0 THEN
+					dy := 0
+				ELSIF org > 0 THEN
+					org := st.PreviousLine(org); st.GetLine(org, box);
+					h1 := box.asc + box.dsc + st.GridOffset(-1, box);
+					IF h1 > maxScrollHeight + fuseScrollHeight THEN
+						sh := h1 - h1 MOD maxScrollHeight;
+						IF h1 - sh < fuseScrollHeight THEN DEC(sh, maxScrollHeight) END;
+						dy := -sh
+					ELSE dy := 0
+					END
+				END
+			| Controllers.incLine:
+				t := trailer.next;
+				IF t.h + dy > maxScrollHeight + fuseScrollHeight THEN
+					DEC(dy, maxScrollHeight)
+				ELSIF ~t.box.eot THEN
+					org := t.next.start; dy := 0
+				END
+			| Controllers.decPage:
+				sh := f.b; DEC(sh, maxScrollHeight + sh MOD maxScrollHeight);
+				IF dy <= -(sh + fuseScrollHeight) THEN
+					INC(dy, sh)
+				ELSE
+					t := trailer.next;
+					h := maxScrollHeight - dy;
+					IF t.h < h THEN h := t.h END;
+					box0 := t.box; h1:= h - st.GridOffset(-1, box0);
+					WHILE (org > 0) & (h + fuseScrollHeight < f.b) DO
+						org := st.PreviousLine(org); st.GetLine(org, box);
+						h1 := box.asc + box.dsc;
+						INC(h, h1 + st.GridOffset(box.dsc, box0));
+						box0 := box
+					END;
+					h1 := h1 + st.GridOffset(-1, box0);
+					sh := h1 - (h - f.b); DEC(sh, sh MOD maxScrollHeight);
+					IF h1 - sh >= fuseScrollHeight THEN dy := -sh ELSE dy := 0 END
+				END;
+				IF (org > v.org) OR (org = v.org) & (dy <= v.dy) THEN	(* guarantee progress *)
+					org := st.PreviousLine(org); st.GetLine(org, box);
+					h1 := box.asc + box.dsc + st.GridOffset(-1, box);
+					IF h1 > maxScrollHeight + fuseScrollHeight THEN
+						dy := - (h1 DIV maxScrollHeight * maxScrollHeight)
+					ELSE
+						dy := 0
+					END
+				END
+			| Controllers.incPage:
+				t := trailer.next;
+				sh := f.b; DEC(sh, maxScrollHeight + sh MOD maxScrollHeight);
+				IF t.h + dy > sh + fuseScrollHeight THEN
+					DEC(dy, sh)
+				ELSE
+					t := ThisViewLine(v, f.b); LocateThisLine(v, t.start, t1, h);
+					IF (h + t.h >= f.b) & (t.h <= maxScrollHeight) THEN
+						org := st.PreviousLine(t.start)
+					ELSE org := t.start
+					END;
+					IF h + t.h - f.b > maxScrollHeight THEN
+						sh := f.b - h; DEC(sh, maxScrollHeight + sh MOD maxScrollHeight);
+						IF sh >= fuseScrollHeight THEN dy := -sh ELSE dy := 0 END
+					ELSE
+						dy := 0
+					END
+				END;
+				IF (org < v.org) OR (org = v.org) & (dy >= v.dy) THEN	(* guarantee progress *)
+					IF t.h + dy > maxScrollHeight + fuseScrollHeight THEN
+						DEC(dy, maxScrollHeight)
+					ELSE
+						org := t.next.start; dy := 0
+					END
+				END
+			| Controllers.gotoPos:
+				org := st.ThisLine(msg.pos DIV maxScrollSteps); st.GetLine(org, box);
+				sh := box.asc + box.dsc + st.GridOffset(-1, box);
+				steps := -((-sh) DIV maxScrollHeight);
+				IF (steps > 0) & (box.len > 0) THEN
+					step := steps * (msg.pos - org * maxScrollSteps) DIV (maxScrollSteps * box.len);
+(*
+					step := steps * (msg.pos MOD maxScrollSteps) DIV maxScrollSteps;
+*)
+					dy := -(step * maxScrollHeight)
+				ELSE
+					dy := 0
+				END
+			ELSE
+			END;
+			len := v.text.Length();
+			IF org > len THEN org := len; dy := 0 END;
+			v.SetOrigin(org, dy);
+			msg.done := TRUE
+		END
+	END Scroll;
+
+	PROCEDURE NotifyViewsOnPage (v: StdView; beg, end, pageNo: INTEGER);
+		VAR st: TextSetters.Setter; rd: TextSetters.Reader; r: TextModels.Reader;
+			view: Views.View; current: INTEGER;
+			page: PageMsg;
+	BEGIN
+		IF pageNo >= 0 THEN current := pageNo
+		ELSIF Printing.par # NIL THEN current := Printing.Current() (* Printing.par.page.current *) + 1
+		ELSE current := -1
+		END;
+		IF current >= 0 THEN
+			st := v.ThisSetter();
+			rd := v.cachedRd; v.cachedRd := NIL;	(* reader recycling *)
+			rd := st.NewReader(rd);
+			rd.Set(rd.r, v.text, 0, 0, v.defRuler, 0, st.vw, st.hideMarks);
+			r := rd.r; r.SetPos(beg); r.ReadView(view);
+			WHILE (r.Pos() <= end) & ~r.eot DO
+				page.current := current; Views.HandlePropMsg(view, page); r.ReadView(view)
+			END;
+			CacheReader(v, rd)
+		END
+	END NotifyViewsOnPage;
+
+	PROCEDURE Page (v: StdView; pageH: INTEGER; op, pageY: INTEGER; OUT done, eoy: BOOLEAN);
+		VAR st: TextSetters.Setter; org, prev, page: INTEGER;
+	BEGIN
+		IF ~v.hideMarks & ((v.context = NIL) OR v.context.Normalize()) THEN
+			v.DisplayMarks(hide)
+		END;
+		st := v.ThisSetter();
+		IF op = Controllers.nextPageY THEN
+			done := TRUE; org := st.NextPage(pageH, v.org); eoy := (org = v.text.Length());
+			IF ~eoy THEN NotifyViewsOnPage(v, org, st.NextPage(pageH, org), -1) END
+		ELSIF op =  Controllers.gotoPageY THEN
+			ASSERT(pageY >= 0, 20);
+			done := TRUE; org := 0; eoy := FALSE; page := 0;
+			WHILE (page < pageY) & ~eoy DO
+				prev := org; org := st.NextPage(pageH, org); eoy := org = prev;
+				IF ~eoy THEN NotifyViewsOnPage(v, prev, org, page) END;
+				INC(page)
+			END;
+			IF ~eoy THEN NotifyViewsOnPage(v, org, st.NextPage(pageH, org), page) END
+		ELSE
+			done := FALSE
+		END;
+		IF done & ~eoy THEN v.org := org; v.dy := 0; v.trailer := NIL; v.bot := 0 END
+	END Page;
+
+
+	PROCEDURE ShowAdjusted (v: StdView; shift: INTEGER; rebuild: BOOLEAN);
+	BEGIN
+		IF shift # 0 THEN Views.Scroll(v, 0, shift)
+		ELSIF rebuild THEN UpdateIn(v, 0, 0, MAX(INTEGER))
+		END;
+		Views.RestoreDomain(v.Domain())
+	END ShowAdjusted;
+
+	PROCEDURE AdjustLines (v: StdView; org, dy: INTEGER;
+		OUT shift: INTEGER; OUT rebuild: BOOLEAN
+	);
+	(* post: shift = 0  OR  ~rebuild *)
+		VAR d: Stores.Domain; c: Containers.Controller; t, t0, t1: Line; org0, dy0, y: INTEGER;
+	BEGIN
+		d := v.Domain(); t0 := v.trailer; org0 := v.org; rebuild := FALSE; shift := 0;
+		IF (d # NIL) & ((org # org0) OR (dy # v.dy)) THEN
+			Views.RestoreDomain(d);	(* make sure that pixels are up-to-date before scrolling *)
+			c := v.ThisController();
+			IF c # NIL THEN
+				Containers.FadeMarks(c, Containers.hide)	(* fade marks with overhang *)
+			END
+		END;
+		IF (t0 # NIL) & (org = org0) & (dy # v.dy) THEN	(* sub-line shift *)
+			shift := dy - v.dy;
+		ELSIF (t0 # NIL) & (org > org0) & (org < t0.start) THEN	(* shift up *)
+			LocateThisLine(v, org, t, y); t0.next := t;
+			shift := dy - y
+		ELSIF (t0 # NIL) & (org < org0) THEN	(* shift down *)
+			t1 := t0.next; dy0 := v.dy + t1.asc; v.org := org; v.dy := dy;
+			IF t1.start = org0 THEN	(* new lines need to be prepended *)
+				PrependLines(v)	(* may change t1.asc *)
+			END;
+			ASSERT(t0.next.start = org, 100);
+			IF org0 < t0.start THEN	(* former top still visible -> shift down *)
+				LocateThisLine(v, org0, t, y); shift := y - (dy0 - t1.asc)
+			ELSE	(* rebuild all *)
+				rebuild := TRUE
+			END
+		ELSIF (t0 = NIL) OR (org # org0) OR (dy # v.dy) THEN	(* rebuild all *)
+			rebuild := TRUE
+		END;
+		v.org := org; v.dy := dy;
+		IF rebuild THEN	(* rebuild all *)
+			v.trailer := NIL; ValidateLines(v, v.bot)
+		ELSIF shift < 0 THEN	(* shift up *)
+			INC(v.bot, shift); ExtendLines(v, v.bot - shift)
+		ELSIF shift > 0 THEN	(* shift down *)
+			INC(v.bot, shift); ReduceLines(v, v.bot - shift)
+		END
+	END AdjustLines;
+
+	PROCEDURE Limit (v: StdView; bot: INTEGER; allLines: BOOLEAN): INTEGER;
+		CONST minH = 12 * point;
+		VAR s, t: Line; pos, y: INTEGER;
+	BEGIN
+		s := v.trailer.next; t := s; y := v.dy;
+		WHILE ~t.box.eot & (y + t.h <= bot) DO INC(y, t.h); s := t; t := t.next END;
+		IF ~allLines & (bot - y < t.h) & (bot - y < minH) THEN t := s END;
+		pos := t.start + t.box.len;
+(*
+		IF t.box.eot THEN INC(pos) END;
+*)
+		RETURN pos
+	END Limit;
+
+
+	(* ScrollOp *)
+
+	PROCEDURE (op: ScrollOp) Do;
+		VAR org0, dy0, org, dy, shift: INTEGER; rebuild: BOOLEAN;
+	BEGIN
+		IF op.bunch THEN org := op.bunchOrg; dy := op.bunchDy
+		ELSE org := op.org; dy := op.dy
+		END;
+		org0 := op.v.org; dy0 := op.v.dy;
+		IF op.silent THEN
+			op.v.org := org; op.v.dy := dy; op.silent := FALSE
+		ELSE
+			AdjustLines(op.v, org, dy, shift, rebuild); ShowAdjusted(op.v, shift, rebuild)
+		END;
+		IF op.bunch THEN op.bunch := FALSE ELSE op.org := org0; op.dy := dy0 END
+	END Do;
+
+	PROCEDURE DoSetOrigin (v: StdView; org, dy: INTEGER; silent: BOOLEAN);
+	(* pre: org = v.ThisSetter().ThisLine(org) *)
+		VAR con: Models.Context; last: Stores.Operation; op: ScrollOp;
+			shift: INTEGER; rebuild: BOOLEAN;
+	BEGIN
+		IF (org # v.org) OR (dy # v.dy) THEN
+			con := v.context;
+			IF con # NIL THEN
+				IF (v.Domain() = NIL) OR con.Normalize() THEN
+					IF silent THEN
+						v.org := org; v.dy := dy
+					ELSE
+						AdjustLines(v, org, dy, shift, rebuild); ShowAdjusted(v, shift, rebuild)
+					END
+				ELSE
+					last := Views.LastOp(v);
+					IF (last # NIL) & (last IS ScrollOp) THEN
+						op := last(ScrollOp);
+						op.bunch := TRUE; op.bunchOrg := org; op.bunchDy := dy;
+						op.silent := silent;
+						Views.Bunch(v)
+					ELSE
+						NEW(op); op.v := v; op.org := org; op.dy := dy;
+						op.bunch := FALSE;
+						op.silent := silent;
+						Views.Do(v, scrollingKey, op)
+					END
+				END
+			ELSE
+				v.org := org; v.dy := dy
+			END
+		END
+	END DoSetOrigin;
+
+
+	(* SetOp *)
+
+	PROCEDURE (op: SetOp) Do;
+		VAR v: StdView; m: BOOLEAN;
+			a: TextModels.Attributes; r: TextRulers.Ruler; s: TextSetters.Setter;
+	BEGIN
+		v := op.view;
+		CASE op.mode OF
+		  setMarks:
+			m := v.hideMarks; v.hideMarks := op.hideMarks; op.hideMarks := m
+		| setSetter:
+			s := v.setter;
+			IF s # NIL THEN s.ConnectTo(NIL, NIL, 0, FALSE) END;
+			v.setter := op.setter; op.setter := s
+		| setDefs:
+			r := v.defRuler; a := v.defAttr;
+			v.defRuler := op.defRuler; v.defAttr := op.defAttr;
+			op.defRuler := r; op.defAttr := a;
+(*
+			IF (v.defAttr.Domain() # NIL) & (v.defAttr.Domain() # v.Domain()) THEN
+				v.defAttr := Stores.CopyOf(v.defAttr)(TextModels.Attributes)
+			END;
+			Stores.Join(v, v.defAttr);
+*)
+			IF v.defAttr # NIL THEN (* could be for undo operations *)
+				IF ~Stores.Joined(v, v.defAttr) THEN
+					IF ~Stores.Unattached(v.defAttr) THEN
+						v.defAttr := Stores.CopyOf(v.defAttr)(TextModels.Attributes)
+					END;
+					Stores.Join(v, v.defAttr)
+				END;
+			END;
+			
+			IF v.defRuler # NIL THEN Stores.Join(v, v.defRuler) END;
+		END;
+		RebuildView(v)
+	END Do;
+
+	PROCEDURE DoSet (op: SetOp; mode: INTEGER; v: StdView);
+	BEGIN
+		op.mode := mode; op.view := v; Views.Do(v, viewSettingKey, op)
+	END DoSet;
+
+
+	(* StdView *)
+
+	PROCEDURE (v: StdView) Internalize2 (VAR rd: Stores.Reader);
+		VAR st: Stores.Store; r: TextRulers.Ruler; a: TextModels.Attributes;
+			org, dy: INTEGER; thisVersion: INTEGER; hideMarks: BOOLEAN;
+	BEGIN
+		v.Internalize2^(rd);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadVersion(minVersion, maxStdVersion, thisVersion);
+		IF rd.cancelled THEN RETURN END;
+		rd.ReadBool(hideMarks);
+		rd.ReadStore(st); ASSERT(st # NIL, 100);
+		IF ~(st IS TextRulers.Ruler) THEN
+			rd.TurnIntoAlien(Stores.alienComponent);
+			Stores.Report("#Text:AlienDefaultRuler", "", "", "");
+			RETURN
+		END;
+		r := st(TextRulers.Ruler);
+		TextModels.ReadAttr(rd, a);
+		rd.ReadInt(org); rd.ReadInt(dy);
+		v.DisplayMarks(hideMarks);
+		v.setter := TextSetters.dir.New(); v.setter0 := NIL;
+		v.SetDefaults(r, a); v.SetOrigin(org, dy);
+		v.trailer := NIL; v.bot := 0
+	END Internalize2;
+
+	PROCEDURE (v: StdView) Externalize2 (VAR wr: Stores.Writer);
+		VAR org, dy: INTEGER; hideMarks: BOOLEAN;
+			a: Stores.Store;
+	BEGIN
+		v.Externalize2^(wr);
+		IF (v.context = NIL) OR v.context.Normalize() THEN
+			org := 0; dy := 0; hideMarks := TRUE
+		ELSE
+			org := v.org; dy := v.dy; hideMarks := v.hideMarks
+		END;
+		wr.WriteVersion(maxStdVersion);
+		wr.WriteBool(hideMarks);
+		a := Stores.CopyOf(v.defAttr); (*Stores.InitDomain(a, v.Domain());*) Stores.Join(v, a);
+			(* bkwd-comp hack: avoid link => so that pre release 1.3 Internalize can still read texts *)
+		wr.WriteStore(v.defRuler);
+		wr.WriteStore(a);
+		wr.WriteInt(org); wr.WriteInt(dy)
+	END Externalize2;
+
+	PROCEDURE (v: StdView) CopyFromModelView2 (source: Views.View; model: Models.Model);
+		VAR s: TextSetters.Setter; r: TextRulers.Ruler;
+	BEGIN
+		(* v.CopyFromModelView^(source, model); *)
+		WITH source: StdView DO
+			s := Stores.CopyOf(source.setter)(TextSetters.Setter);
+			v.setter := s; v.setter0 := NIL;
+			r := TextRulers.CopyOf(source.defRuler, Views.deep);
+			v.DisplayMarks(source.HidesMarks());
+			v.SetDefaults(r, source.defAttr);
+			v.trailer := NIL; v.bot := 0;
+			IF v.text = source.text THEN
+				v.org := source.org; v.dy := source.dy
+			END
+		END
+	END CopyFromModelView2;
+
+	PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
+		VAR st: TextSetters.Setter; u0, u: Line;
+			y0, y, w, h: INTEGER; end: INTEGER; pageF: BOOLEAN;
+	BEGIN
+		ASSERT(v.context # NIL, 20);
+		IF v.setter # NIL THEN v.context.GetSize(w, h) END;
+		IF (v.setter = NIL) OR (v.setter.vw # w) THEN
+			Views.RemoveFrames(f, l, t, r, b)
+		END;
+		ValidateLines(v, b);
+		u := v.trailer.next; y := v.dy;
+		pageF := Views.IsPrinterFrame(f) & v.context.Normalize();
+		IF pageF THEN	(* on page-formatted frames do not display truncated lines at bottom *)
+			st := v.ThisSetter(); end := st.NextPage(f.b - f.t, v.org)
+		END;
+		WHILE (u # v.trailer) & (y + u.h <= t) DO INC(y, u.h); u := u.next END;
+		y0 := y; u0 := u;
+		IF (u = v.trailer.next) & (y < b) THEN	(* at least one line per page *)
+			ASSERT((u.box.len > 0) OR u.box.eot OR (u.next = v.trailer), 100);
+			DrawLine(v, u.start, u.box, f, l, r, y + u.asc, y + u.h - u.box.dsc - u.box.asc, pageF);
+			INC(y, u.h); u := u.next
+		END;
+		WHILE (u # v.trailer) & (y < b) & (~pageF OR (u.start < end)) DO
+			ASSERT((u.box.len > 0) OR u.box.eot OR (u.next = v.trailer), 101);
+			IF u.box.ruler # u0.box.ruler THEN
+				DrawDecorations(v, u0, f, l, y0, r, y); u0 := u; y0 := y
+			END;
+			DrawLine(v, u.start, u.box, f, l, r, y + u.asc, y + u.h - u.box.dsc - u.box.asc, pageF);
+			INC(y, u.h); u := u.next
+		END;
+		IF y0 # y THEN DrawDecorations(v, u0, f, l, y0, r, y) END
+	END Restore;
+
+	PROCEDURE (v: StdView) DisplayMarks (hide: BOOLEAN);
+		VAR op: SetOp; c: Containers.Controller;
+	BEGIN
+		IF v.hideMarks # hide THEN
+			c := v.ThisController();
+			IF c # NIL THEN Containers.FadeMarks(c, Containers.hide) END;
+			IF (v.context # NIL) & ~v.context.Normalize() THEN
+				NEW(op); op.hideMarks := hide; DoSet(op, setMarks, v)
+			ELSE
+				v.hideMarks := hide; RebuildView(v)
+			END
+		END
+	END DisplayMarks;
+
+	PROCEDURE (v: StdView) HidesMarks (): BOOLEAN;
+	BEGIN
+		RETURN v.hideMarks
+	END HidesMarks;
+
+	PROCEDURE (v: StdView) SetSetter (setter: TextSetters.Setter);
+		VAR op: SetOp;
+	BEGIN
+		ASSERT(setter # NIL, 20);
+		IF v.setter # setter THEN
+			IF v.setter # NIL THEN
+				NEW(op); op.setter := setter; DoSet(op, setSetter, v)
+			ELSE v.setter := setter
+			END
+		END
+	END SetSetter;
+
+	PROCEDURE (v: StdView) ThisSetter (): TextSetters.Setter;
+		VAR st: TextSetters.Setter; w, h: INTEGER;
+	BEGIN
+		st := v.setter; ASSERT(st # NIL, 20);
+		IF st # v.setter0 THEN
+			IF v.context # NIL THEN
+				v.context.GetSize(w, h)
+			ELSE
+				IF Dialog.metricSystem THEN
+					w := 165*mm
+				ELSE
+					w := 104*inch16
+				END
+			END;
+			st.ConnectTo(v.text, v.defRuler, w, v.hideMarks);
+			v.setter0 := st
+		END;
+		RETURN st
+	END ThisSetter;
+
+	PROCEDURE (d: StdView) AcceptableModel (m: Containers.Model): BOOLEAN;
+	BEGIN
+		RETURN m IS TextModels.Model
+	END AcceptableModel;
+	
+	PROCEDURE (v: StdView) InitModel2 (m: Containers.Model);
+	BEGIN
+		ASSERT(m IS TextModels.Model, 23);
+		v.text := m(TextModels.Model)
+	END InitModel2;
+
+	PROCEDURE (v: StdView) SetOrigin (org, dy: INTEGER);
+		VAR st: TextSetters.Setter; start: INTEGER;
+	BEGIN
+		ASSERT(v.text # NIL, 20);
+		st := v.ThisSetter(); start := st.ThisLine(org);
+		IF start # org THEN org := start; dy := 0 END;
+		DoSetOrigin(v, org, dy, FALSE)
+	END SetOrigin;
+
+	PROCEDURE (v: StdView) PollOrigin (OUT org, dy: INTEGER);
+	BEGIN
+		org := v.org; dy := v.dy
+	END PollOrigin;
+
+	PROCEDURE (v: StdView) SetDefaults (r: TextRulers.Ruler; a: TextModels.Attributes);
+		VAR op: SetOp;
+	BEGIN
+		ASSERT(r # NIL, 20); ASSERT(r.style.attr.init, 21);
+		ASSERT(a # NIL, 22); ASSERT(a.init, 23);
+		IF (v.defRuler # r) OR (v.defAttr # a) THEN
+(*
+			(*IF (v.context # NIL) & (r # v.defRuler) THEN*)
+			IF (v.Domain() # NIL) & (r # v.defRuler) THEN
+				Stores.InitDomain(r, v.Domain())
+			END;
+*)
+			IF r # v.defRuler THEN Stores.Join(v, r) END;
+			NEW(op); op.defRuler := r; op.defAttr := a; DoSet(op, setDefs, v)
+		END
+	END SetDefaults;
+
+	PROCEDURE (v: StdView) PollDefaults (OUT r: TextRulers.Ruler; OUT a: TextModels.Attributes);
+	BEGIN
+		r := v.defRuler; a := v.defAttr
+	END PollDefaults;
+
+(*
+	PROCEDURE (v: StdView) PropagateDomain;
+		VAR m: Models.Model;
+	BEGIN
+		ASSERT(v.setter # NIL, 20); ASSERT(v.text # NIL, 21);
+		ASSERT(v.defRuler # NIL, 22); ASSERT(v.defAttr # NIL, 23);
+		v.PropagateDomain^;
+		m := v.ThisModel();
+		IF m # NIL THEN Stores.InitDomain(m, v.Domain()) END;
+		Stores.InitDomain(v.defRuler, v.Domain())
+	END PropagateDomain;
+	*)
+(*
+	PROCEDURE (v: StdView) Flush, NEW;
+	BEGIN
+		v.trailer := NIL; v.bot := 0; v.setter0 := NIL
+	END Flush;
+*)
+	PROCEDURE (v: StdView) HandleModelMsg2 (VAR msg: Models.Message);
+	BEGIN
+		IF msg.model = v.text THEN
+			WITH msg: Models.UpdateMsg DO
+				WITH msg: TextModels.UpdateMsg DO
+					IF msg.op IN {TextModels.insert, TextModels.delete, TextModels.replace} THEN
+						UpdateView(v, msg.beg, msg.end, msg.delta)
+					ELSE	(* unknown text op happened *)
+						RebuildView(v)
+					END
+				ELSE	(* unknown text update happened *)
+					RebuildView(v)
+				END
+			| msg: PositionMsg DO
+				v.ShowRange(msg.beg, msg.end, msg.focusOnly)
+			ELSE
+			END
+		ELSE	(* domaincast received *)
+			WITH msg: TextRulers.UpdateMsg DO
+				StyleUpdate(v, msg.oldAttr)
+			| msg: Models.UpdateMsg DO	(* forced rebuild *)
+				RebuildView(v)
+			ELSE
+			END
+		END
+	END HandleModelMsg2;
+
+	PROCEDURE (v: StdView) HandleViewMsg2 (f: Views.Frame; VAR msg: Views.Message);
+	BEGIN
+		IF msg.view = v THEN
+			WITH msg: FindAnyFrameMsg DO
+				IF (msg.frame = NIL) OR (msg.frame.b - msg.frame.t > f.b - f.t) THEN msg.frame := f END
+			ELSE
+			END
+		ELSE
+			WITH msg: Views.UpdateCachesMsg DO	(* display view in new frame *)
+				IF Views.Era(v) # Models.Era(v.text) THEN
+					(* view/setter caches outdated - possible if v previous to this notification had no frame open *)
+					v.setter0 := NIL; v.trailer := NIL; v.bot := 0
+				END
+			ELSE
+			END
+		END
+	END HandleViewMsg2;
+
+	PROCEDURE (v: StdView) HandleCtrlMsg2 (f: Views.Frame;
+		VAR msg: Controllers.Message; VAR focus: Views.View
+	);
+	BEGIN
+		WITH msg: Controllers.PollSectionMsg DO
+			IF (focus = NIL) OR ~msg.focus THEN
+				PollSection(v, f, msg);
+				focus := NIL
+			END
+		| msg: FindFocusFrameMsg DO
+			IF (msg.view = v) & (msg.frame = NIL) THEN msg.frame := f END
+		| msg: Controllers.ScrollMsg DO
+			IF (focus = NIL) OR ~msg.focus THEN
+				Scroll(v, f, msg);
+				focus := NIL
+			END
+		| msg: Controllers.PageMsg DO
+			Page(v, f.b - f.t, msg.op, msg.pageY, msg.done, msg.eoy);
+			focus := NIL
+		ELSE
+		END
+	END HandleCtrlMsg2;
+
+	PROCEDURE (v: StdView) HandlePropMsg2 (VAR p: Properties.Message);
+		CONST minW = 5 * point; maxW = maxHeight; minH = 5 * point; maxH = maxHeight;
+		VAR st: TextSetters.Setter;
+	BEGIN
+		WITH p: Properties.SizePref DO
+			IF p.w = Views.undefined THEN p.w := v.defRuler.style.attr.right END;
+			IF p.h = Views.undefined THEN p.h := MAX(INTEGER) END
+		| p: Properties.BoundsPref DO
+			st := v.ThisSetter();
+			st.GetBox(0, v.text.Length(), maxW, maxH, p.w, p.h);
+			IF p.w < minW THEN p.w := minW END;
+			IF p.h < minH THEN p.h := minH END
+		| p: Properties.ResizePref DO
+			p.fixed := FALSE;
+			p.horFitToPage := ~(TextRulers.rightFixed IN v.defRuler.style.attr.opts);
+			p.verFitToWin := TRUE
+		| p: Properties.TypePref DO
+			IF Services.Is(v, p.type) THEN p.view := v END
+		| p: Containers.DropPref DO
+			p.okToDrop := TRUE
+		ELSE
+		END
+	END HandlePropMsg2;
+
+
+	PROCEDURE (v: StdView) GetThisLocation (f: Views.Frame; pos: INTEGER; OUT loc: Location);
+	(* pre: f must be displayed *)
+	(* if position lies outside view, the next best location inside will be taken *)
+		VAR rd: TextSetters.Reader; t: Line; p1, y, w, h: INTEGER;
+	BEGIN
+		ValidateLines(v, f.b);
+		y := v.dy;
+		IF pos < v.org THEN
+			t := v.trailer.next;
+			loc.start := t.start; loc.pos := t.start;
+			loc.x := 0; loc.y := y; loc.asc := t.asc; loc.dsc := t.h - t.asc; loc.view := NIL;
+			RETURN
+		ELSIF pos < v.trailer.start THEN
+			t := v.trailer.next;
+			WHILE ~t.box.eot & ~((t.start <= pos) & (pos < t.next.start)) DO
+				INC(y, t.h); t := t.next
+			END
+		ELSE	(* pos >= v.trailer.start *)
+			t := v.trailer.next; WHILE ~t.box.eot DO INC(y, t.h); t := t.next END;
+			IF t = v.trailer THEN
+				loc.start := t.start; loc.pos := t.start;
+				loc.x := 0; loc.y := y; loc.asc := t.asc; loc.dsc := t.h - t.asc; loc.view := NIL;
+				RETURN
+			END
+		END;
+		p1 := t.start;
+		rd := GetReader(v, p1, t.box); rd.Read;
+		WHILE rd.pos < pos DO
+			p1 := rd.pos; rd.AdjustWidth(t.start, p1, t.box, rd.w); INC(rd.x, rd.w); rd.Read
+		END;
+		IF LEN(rd.string$) > 1 THEN	(* collated subsequence *)
+			rd.x := f.CharPos(rd.x, pos - p1, rd.string, rd.attr.font);
+			IF rd.pos = pos THEN rd.Read END
+		ELSIF rd.pos = pos THEN
+			rd.AdjustWidth(t.start, pos, t.box, rd.w); INC(rd.x, rd.w); rd.Read
+		ELSE
+			ASSERT(p1 = pos, 100)
+		END;
+		loc.view := rd.view;
+		loc.start := t.start; loc.pos := pos;
+		loc.x := rd.x; loc.y := y; loc.asc := t.asc; loc.dsc := t.h - t.asc;
+		IF loc.view # NIL THEN
+			v.context.GetSize(w, h);
+			IF rd.x + rd.w > w THEN rd.w := w - rd.x END;
+			loc.l := rd.x; loc.t := y - rd.attr.offset + t.asc + rd.dsc - rd.h;
+			loc.r := loc.l + rd.w; loc.b := loc.t + rd.h
+		END;
+		CacheReader(v, rd)
+	END GetThisLocation;
+
+	PROCEDURE (v: StdView) GetRange (f: Views.Frame; OUT beg, end: INTEGER);
+		VAR t: Line;
+	BEGIN
+		ValidateLines(v, f.b);
+		t := ThisViewLine(v, f.t); beg := t.start; end := Limit(v, f.b, TRUE)
+	END GetRange;
+
+	PROCEDURE (v: StdView) ThisPos (f: Views.Frame; x, y: INTEGER): INTEGER;
+	(* pre: f must be displayed *)
+	(* post: f.org <= result <= v.text.Length() *)
+		VAR rd: TextSetters.Reader; t: Line; p1, end, py: INTEGER;
+	BEGIN
+		ValidateLines(v, f.b);
+		t := v.trailer.next; py := v.dy;
+		WHILE ~t.box.eot & (py + t.h <= y) DO INC(py, t.h); t := t.next END;
+		p1 := t.start; end := p1 + t.box.len;
+		IF py + t.h > y THEN
+			IF (end > p1) & (y >= v.dy) THEN
+				IF t.box.eot THEN INC(end) END;
+				rd := GetReader(v, p1, t.box);
+				rd.Read; rd.AdjustWidth(t.start, rd.pos, t.box, rd.w);
+				WHILE (rd.x + rd.SplitWidth(rd.w) < x) & (rd.pos < end) DO
+					p1 := rd.pos; INC(rd.x, rd.w);
+					rd.Read; rd.AdjustWidth(t.start, rd.pos, t.box, rd.w)
+				END;
+				IF LEN(rd.string$) > 1 THEN	(* collated subsequence *)
+					INC(p1, f.CharIndex(rd.x, x, rd.string, rd.attr.font))
+				END;
+				CacheReader(v, rd)
+			END
+		ELSE p1 := end
+		END;
+		RETURN p1
+	END ThisPos;
+
+	PROCEDURE (v: StdView) ShowRangeIn (f: Views.Frame; beg, end: INTEGER);
+		CONST minH = 12 * point;
+		VAR c: Models.Context; st: TextSetters.Setter; t, t1: Line;
+			org0, last, len,  org, dy,  p, q: INTEGER; y, h, mh: INTEGER;
+			box, box0: TextSetters.LineBox; loc, loc1: Location;
+			focus: BOOLEAN;
+	BEGIN
+		focus := f = Controllers.FocusFrame();
+		c := v.context;
+		st := v.ThisSetter(); ValidateLines(v, f.b); org0 := v.org;
+		last := Limit(v, f.b, FALSE); len := v.text.Length();
+		IF last = len THEN p := st.ThisLine(last); LocateThisLine(v, p, t1, y); h := f.b - y END;
+		IF (beg > last)
+		OR (beg = last) & ((last < len) OR (len > 0) & (h < t1.h) & (h < minH))
+		OR (end < org0)
+		OR (beg < end) & (end = org0) THEN
+			org := -1; dy := 0;
+			IF beg <= org0 THEN	(* try to adjust by scrolling up *)
+				p := st.PreviousLine(v.org);
+				IF p <= beg THEN	(* reveal one line at top *)
+					org := p; st.GetLine(org, box);
+					h := box.asc + box.dsc + st.GridOffset(-1, box);
+					IF h > maxScrollHeight + fuseScrollHeight THEN
+						dy := -(h - h MOD maxScrollHeight);
+						IF h + dy < fuseScrollHeight THEN INC(dy, maxScrollHeight) END
+					END
+				END
+			END;
+			IF (org = -1) & (beg >= last) THEN	(* try to adjust by scrolling down *)
+				p := st.ThisLine(last); q := st.NextLine(p); st.GetLine(q, box);
+				IF (beg < q + box.len) OR (p = q) THEN	(* reveal one line at bottom *)
+					LocateThisLine(v, p, t1, y);
+					h := box.asc + box.dsc + st.GridOffset(t1.box.dsc, box);
+					IF h > maxScrollHeight + fuseScrollHeight THEN h := maxScrollHeight END;
+					mh := y + t1.h - f.b + h;
+					t := v.trailer.next; h := v.dy;
+					WHILE (t # v.trailer) & (h < mh) DO INC(h, t.h); t := t.next END;
+					IF t.start > v.org THEN org := t.start END
+				END
+			END;
+			IF org = -1 THEN	(* adjust by moving into "nice" position *)
+				mh := f.b DIV 3;
+				org := st.ThisLine(beg); st.GetLine(org, box0);
+				h := box0.asc + box0.dsc + st.GridOffset(-1, box0); p := org;
+				WHILE (p > 0) & (h < mh) DO
+					DEC(h, st.GridOffset(-1, box0)); org := p;
+					p := st.PreviousLine(org); st.GetLine(p, box);
+					INC(h, box.asc + box.dsc + st.GridOffset(box.dsc, box0));
+					box0 := box
+				END;
+				IF (org = len) & (len > 0) THEN org := st.PreviousLine(org) END
+			END;
+			DoSetOrigin(v, org, dy, FALSE)
+		END;
+		IF focus THEN
+			f := Controllers.FocusFrame();
+			IF (f # NIL) & (f.view = v) THEN
+			
+				v.GetThisLocation(f, beg, loc);
+				v.GetThisLocation(f, end, loc1);
+				IF (loc.y = loc1.y) & (loc.x <= loc1.x) THEN
+					c.MakeVisible(loc.x, loc.y, loc1.x, loc1.y)
+				END
+			ELSE
+				HALT(100); (* this should not happen *)
+			END
+		END;
+(*
+		IF c IS Documents.Context THEN
+			v.GetThisLocation(f, beg, loc);
+			v.GetThisLocation(f, end, loc1);
+			IF (loc.y = loc1.y) & (loc.x <= loc1.x) THEN
+				Documents.MakeVisible(c(Documents.Context).ThisDoc(), f, loc.x, loc.y, loc1.x, loc1.y)
+			END
+		END
+*)
+	END ShowRangeIn;
+
+	PROCEDURE (v: StdView) ShowRange (beg, end: INTEGER; focusOnly: BOOLEAN);
+		VAR fmsg: FindFocusFrameMsg; amsg: FindAnyFrameMsg; f: Views.Frame;
+	BEGIN
+		IF focusOnly THEN
+			fmsg.view := v; fmsg.frame := NIL; Controllers.Forward(fmsg); f := fmsg.frame
+		ELSE
+			amsg.frame := NIL; Views.Broadcast(v, amsg); f := amsg.frame
+		END;
+		IF f # NIL THEN v.ShowRangeIn(f, beg, end) END
+	END ShowRange;
+
+
+	(* StdDirectory *)
+
+	PROCEDURE (d: StdDirectory) New (text: TextModels.Model): View;
+		VAR v: StdView; c: Controllers.Controller; r: TextRulers.Ruler;
+	BEGIN
+		r := TextRulers.dir.New(NIL);
+		IF text = NIL THEN text := TextModels.dir.New() END;
+		(* IF text.Domain() # NIL THEN Stores.InitDomain(r, text.Domain()) END; *)
+		Stores.Join(text, r);
+		NEW(v); v.hideMarks := FALSE; v.bot := 0; v.org := 0; v.dy := 0;
+		v.InitModel(text);
+		v.SetDefaults(r, d.defAttr);
+		v.SetSetter(TextSetters.dir.New());
+		v.DisplayMarks(hide);
+		IF ctrlDir # NIL THEN v.SetController(ctrlDir.New()) END;
+		(* Stores.InitDomain(v, text.Domain()); *)
+		Stores.Join(v, text);
+		RETURN v
+	END New;
+
+
+	PROCEDURE Init;
+		VAR d: StdDirectory; a: TextModels.Attributes; res: INTEGER;
+	BEGIN
+		Dialog.Call("TextControllers.Install", "#Text:CntrlInstallFailed", res);
+		NEW(a); a.InitFromProp(NIL);	(* use defaults *)
+		NEW(d); d.defAttr := a;
+		stdDir := d; dir := d
+	END Init;
+
+BEGIN
+	Init
+END TextViews.

+ 28 - 0
BlackBox/build

@@ -33,6 +33,34 @@ LindevCompiler.Compile('System/Mod', 'Containers.txt')
 LindevCompiler.Compile('System/Mod', 'Documents.txt')
 LindevCompiler.Compile('System/Mod', 'Windows.txt')
 
+LindevCompiler.Compile('Text/Mod', 'Models.txt')
+LindevCompiler.Compile('Text/Mod', 'Mappers.txt')
+LindevCompiler.Compile('Text/Mod', 'Rulers.txt')
+LindevCompiler.Compile('Text/Mod', 'Setters.txt')
+LindevCompiler.Compile('Text/Mod', 'Views.txt')
+LindevCompiler.Compile('Text/Mod', 'Controllers.txt')
+LindevCompiler.Compile('Text/Mod', 'Cmds.txt')
+
+LindevCompiler.Compile('System/Mod', 'In.txt')
+
+LindevCompiler.Compile('Std/Mod', 'Dialog.txt')
+LindevCompiler.Compile('Std/Mod', 'Api.txt')
+LindevCompiler.Compile('Std/Mod', 'CFrames.txt')
+LindevCompiler.Compile('System/Mod', 'Controls.txt')
+LindevCompiler.Compile('Std/Mod', 'Cmds.txt')
+LindevCompiler.Compile('Std/Mod', 'Logos.txt')
+LindevCompiler.Compile('Std/Mod', 'Scrollers.txt')
+LindevCompiler.Compile('Std/Mod', 'ViewSizer.txt')
+
+LindevCompiler.Compile('Std/Mod', 'Clocks.txt')
+LindevCompiler.Compile('Std/Mod', 'Coder.txt')
+LindevCompiler.Compile('Std/Mod', 'Folds.txt')
+LindevCompiler.Compile('Std/Mod', 'Debug.txt')
+LindevCompiler.Compile('Std/Mod', 'Stamps.txt')
+LindevCompiler.Compile('Std/Mod', 'ETHConv.txt')
+LindevCompiler.Compile('Std/Mod', 'Headers.txt')
+LindevCompiler.Compile('Std/Mod', 'Links.txt')
+
 LindevCompiler.Compile('Lin/Mod', 'Obsd.linHostFiles.txt')
 LindevCompiler.Compile('Std/Mod', 'Loader.txt')
 LindevCompiler.Compile('System/Mod', 'Console.txt')

+ 3 - 27
README

@@ -16,38 +16,14 @@ How to build:
 Files:
 	original:
 		BlackBox:
-			Std/Mod/Loader.odc
-			Std/Mod/Interpreter.odc
+			Std/*
+			Text/*
 			Dev/Rsrc/Errors.odc
 			Docu/BB-License.odc
 			Docu/BB-Licensing-Policy.odc
 			Docu/BB-Open-Source-License.odc
 			System/Mod/
-				Containers.odc
-				Controllers.odc
-				Converters.odc
-				Dates.odc
-				Dialog.odc
-				Documents.odc
-				Files.odc
-				Fonts.odc
-				Integers.odc
-				Log.odc
-				Math.odc
-				Mechanisms.odc
-				Meta.odc
-				Models.odc
-				Ports.odc
-				Printers.odc
-				Printing.odc
-				Properties.odc
-				SMath.odc
-				Sequencers.odc
-				Services.odc
-				Stores.odc
-				Strings.odc
-				Views.odc
-				Windows.odc
+				all except Console
 		OpenBUGS:
 			Dev/Mod/ElfLinker16.odc
 			Dev/Docu/ElfLinker.odc