Std/Mod/Tables.odc

MODULE StdTables;
(**
	project	= "BlackBox"
	organization	= "www.oberon.ch"
	contributors	= "Oberon microsystems"
	version	= "System/Rsrc/About"
	copyright	= "System/Rsrc/About"
	license	= "Docu/BB-License"
	changes	= "
	- YYYYMMDD, nn, ...
	"
	issues	= "
	- ...
	"

**)

	IMPORT
		Services, Fonts, Ports, Dialog, Meta, Stores, Models, Views, Controllers, Containers, Properties, Controls,
		TextModels, TextViews, HostPorts;

	CONST
		minVersion = 0;
		tabVersion = 2;
		maxCol = 256;
		defColW = 30 * Ports.mm;
		left = -1; right = -2; center = -3;	(* adjustment modes *)
		move = 1; adjust = 2; field = 3;	(* cursor modes *)

		tab = 09X; ltab = 0AX; lineChar = 0DX; esc = 1BX;

		line* = 0DX;			(* line feed characher for labels *)
		deselect* = -1; select* = -2; changed* = -3; (* notifier op-values *)
		layoutEditable* = 0; dataEditable* = 1; selectionStyle* = 2; (* values for property validity in Prop *)
		noSelect* = 0; cellSelect* = 1; rowSelect* = 2; colSelect* = 3; crossSelect* = 4; (* selection style values *)

	TYPE
		Table* = RECORD
			rows-, cols-: INTEGER;
			selection: Selection;
			labels: POINTER TO ARRAY OF Dialog.String;
			data: POINTER TO ARRAY OF ARRAY OF Dialog.String;
			weights: POINTER TO ARRAY OF ARRAY OF INTEGER;
			styles: POINTER TO ARRAY OF ARRAY OF SET;
			colors: POINTER TO ARRAY OF ARRAY OF Ports.Color
		END;

		Selection = POINTER TO RECORD
			on: BOOLEAN;
			row, col: INTEGER	(** only valid if on = TRUE **)
		END;

		Prop* = POINTER TO RECORD (Properties.Property)
			layoutEditable*, dataEditable*: BOOLEAN;
			selectionStyle*: INTEGER
		END;

		Control = POINTER TO RECORD (Controls.Control)
			(* persistent *)
			sprop: Properties.StdProp;	(* font attributes *)
			tprop: Prop;	(* table attributes *)
			columns: INTEGER;	(* width[0..columns-1] and mode[0..columns-1] are defined *)
			width: ARRAY maxCol OF INTEGER;
			mode: ARRAY maxCol OF INTEGER;

			(* not persistent *)
			fldFont, titFont: Fonts.Font;	(* cell fonts *)
			rowHeight, labelHeight, baseOff: INTEGER;	(* height of rows, height of the label row and offset for font *)
			orgRow, orgX: INTEGER;	(* scroll state, orgX in coordinates, orgRow in rows *)

			selRow, selCol: INTEGER;	(* selected field *)
			showSelection: BOOLEAN;
			hasSelection: BOOLEAN;	(* control has a selected field *)

			x, y, w, h: INTEGER;
			field: Views.View	(* textfield used to enter cell content *)
		END;

		Directory* = POINTER TO ABSTRACT RECORD END;
		StdDirectory = POINTER TO RECORD (Directory) END;

		PropOp = POINTER TO RECORD (Stores.Operation)	(* typeface, style and size *)
			tab: Control;
			sprop: Properties.StdProp;
			tprop: Prop
		END;

		FormatOp = POINTER TO RECORD (Stores.Operation)	(* cell format *)
			tab: Control;
			col, width, mode: INTEGER
		END;

		TableValue = RECORD (Meta.Value)
			t: Table
		END;

		Context = POINTER TO RECORD (Models.Context)
			w, h: INTEGER;
			base: Views.View
		END;

		Action = POINTER TO RECORD (Services.Action) END;

	VAR
		dir-, stdDir-: Directory;
		text*: Dialog.String;	(* used to edit cell. Only one cell can be active at once *)
		dlg*: RECORD
			layoutEditable*, dataEditable*: BOOLEAN;
			selectionStyle*: Dialog.List;
			fingerprint: INTEGER;
			known, valid: SET
		END;
		action: Action;
		
	PROCEDURE CountLines (IN s: Dialog.String): INTEGER;
		VAR i, r, l: INTEGER;
	BEGIN
		r := 1; l := LEN(s$);
		FOR i := 0 TO l - 1 DO IF s[i] = line THEN INC(r) END END;
		RETURN r
	END CountLines;


	(* Prop *)

	PROCEDURE (p: Prop) IntersectWith* (q: Properties.Property; OUT equal: BOOLEAN);
		VAR valid: SET; p0: Prop;
	BEGIN
		p0 := q(Prop);
		valid := p.valid * p0.valid; equal := TRUE;
		IF p.layoutEditable # p0.layoutEditable THEN EXCL(valid, layoutEditable) END;
		IF p.dataEditable # p0.dataEditable THEN EXCL(valid, dataEditable) END;
		IF p.selectionStyle # p0.selectionStyle THEN EXCL(valid, selectionStyle) END;
		IF valid # p.valid THEN p.valid := valid; equal := FALSE END
	END IntersectWith;

	(* Table *)

	PROCEDURE (VAR tab: Table) SetSize* (rows, cols: INTEGER), NEW;
		VAR i, j, fr, fc: INTEGER;
			labels: POINTER TO ARRAY OF Dialog.String;
			data: POINTER TO ARRAY OF ARRAY OF Dialog.String;
			weights: POINTER TO ARRAY OF ARRAY OF INTEGER;
			styles: POINTER TO ARRAY OF ARRAY OF SET;
			colors: POINTER TO ARRAY OF ARRAY OF Ports.Color;
	BEGIN
		ASSERT((rows >= 0) & (cols >= 0), 20); 
		ASSERT((cols > 0) OR ((cols = 0) & (rows = 0)), 21); 
		tab.rows := rows; tab.cols := cols;
		IF rows > 0 THEN
			data := tab.data; NEW(tab.data, rows, cols); weights := tab.weights; NEW(tab.weights, rows, cols);
			styles := tab.styles; NEW(tab.styles, rows, cols); colors := tab.colors; NEW(tab.colors, rows, cols);
			IF data # NIL THEN
				FOR i := 0 TO MIN(rows, LEN(data, 0)) - 1 DO
					FOR j := 0 TO MIN(cols, LEN(data, 1)) - 1 DO
						tab.data[i, j] := data[i, j]; tab.weights[i, j] := weights[i, j]; tab.styles[i, j] := styles[i, j];
						tab.colors[i, j] := colors[i, j]
					END
				END
			END;
			(* set defaults *)
			IF data = NIL THEN fr := 0; fc := 0 ELSE fr := LEN(data, 0); fc := LEN(data, 1) END; 
			FOR i := fr TO LEN(tab.data, 0) - 1 DO
				FOR j := fc TO LEN(tab.data, 1) - 1 DO
					tab.weights[i, j] := Fonts.normal; tab.styles[i, j] := {}; tab.colors[i, j] := Ports.black 
				END
			END
		ELSE
			tab.data := NIL
		END;
		IF cols > 0 THEN
			labels := tab.labels; NEW(tab.labels, cols);
			IF labels # NIL THEN
				FOR i := 0 TO MIN(cols, LEN(labels)) - 1 DO
					tab.labels[i] := labels[i]
				END
			END
		ELSE
			tab.labels := NIL
		END;
		IF tab.selection = NIL THEN
			NEW(tab.selection)
		ELSE
			tab.selection.on := FALSE
		END
	END SetSize;

	PROCEDURE (VAR tab: Table) SetItem* (row, col: INTEGER; (*IN*) item: Dialog.String), NEW;
	BEGIN
		ASSERT(tab.data # NIL, 20);
		tab.data[row, col] := item
	END SetItem;

	PROCEDURE (VAR tab: Table) GetItem* (row, col: INTEGER; OUT item: Dialog.String), NEW;
	BEGIN
		ASSERT(tab.data # NIL, 20);
		item := tab.data[row, col]
	END GetItem;

	PROCEDURE (VAR tab: Table) SetLabel* (col: INTEGER; (*IN*)label: Dialog.String), NEW;
	BEGIN
		ASSERT(tab.labels # NIL, 20);
		tab.labels[col] := label
	END SetLabel;

	PROCEDURE (VAR tab: Table) GetLabel* (col: INTEGER; OUT label: Dialog.String), NEW;
	BEGIN
		ASSERT(tab.labels # NIL, 20);
		label := tab.labels[col]
	END GetLabel;

	PROCEDURE (VAR tab: Table) HasSelection* (): BOOLEAN, NEW;
	BEGIN
		RETURN (tab.selection # NIL) & (tab.selection.on)
	END HasSelection;

	PROCEDURE (VAR tab: Table) GetSelection* (OUT row, col: INTEGER), NEW;
	BEGIN
		ASSERT(tab.selection # NIL, 20);
		ASSERT(tab.selection.on, 21);
		row := tab.selection.row; col := tab.selection.col
	END GetSelection;

	PROCEDURE (VAR tab: Table) Select* (row, col: INTEGER), NEW;
	BEGIN
		ASSERT(tab.selection # NIL, 20);
		tab.selection.on := TRUE; tab.selection.row := row; tab.selection.col := col
	END Select;

	PROCEDURE (VAR tab: Table) Deselect*, NEW;
	BEGIN
		ASSERT(tab.selection # NIL, 20);
		tab.selection.on := FALSE		
	END Deselect;

	PROCEDURE (VAR tab: Table) SetAttr* (l, t, r, b: INTEGER; style: SET; weight: INTEGER; color: Ports.Color), NEW;
		VAR i, j: INTEGER;
	BEGIN
		ASSERT(tab.data # NIL, 20);
		FOR i := t TO b DO
			FOR j := l TO r DO
				tab.weights[i, j] := weight; tab.styles[i, j] := style; tab.colors[i, j] := color
			END
		END
	END SetAttr;

	PROCEDURE (VAR tab: Table) GetAttr* (row, col: INTEGER; OUT style: SET; OUT weight: INTEGER;
																OUT color: Ports.Color), NEW;
	BEGIN
		ASSERT(tab.data # NIL, 20);
		weight := tab.weights[row, col]; style := tab.styles[row, col]; color := tab.colors[row, col]
	END GetAttr;


	(* Context *)

	PROCEDURE (c: Context) GetSize (OUT w, h: INTEGER);
	BEGIN
		w := c.w; h := c.h
	END GetSize;

	PROCEDURE (c: Context) Normalize (): BOOLEAN;
	BEGIN
		RETURN c.base.context.Normalize()
	END Normalize;

	PROCEDURE (c: Context) Consider (VAR p: Models.Proposal);
	BEGIN
		c.base.context.Consider(p)
	END Consider;

	PROCEDURE (c: Context) ThisModel (): Models.Model;
	BEGIN
		RETURN NIL
	END ThisModel;

	PROCEDURE NewField(t: Control; col, w, h: INTEGER): Views.View;
		VAR c: Context; p: Controls.Prop; v: Views.View; prop: Properties.StdProp; setMsg: Properties.SetMsg;
	BEGIN
		NEW(p); p.link := "StdTables.text";
		IF t.mode[col] = left THEN p.opt[Controls.left] := TRUE
		ELSIF t.mode[col] = right THEN p.opt[Controls.right] := TRUE
		END;
		(* bug in controls, thus adjusting has to be set to left mode *)
		p.opt[Controls.left] := TRUE;
		p.opt[Controls.right] := FALSE;
		v := Controls.dir.NewField(p);

		NEW(c); c.w := w; c.h := h; c.base := t;
		v.InitContext(c);

		NEW(prop);
		prop.typeface := t.fldFont.typeface;
		prop.size := t.fldFont.size - Fonts.point;
		prop.style.val := t.fldFont.style;
		prop.style.mask := t.fldFont.style;
		prop.weight := t.fldFont.weight;
		prop.valid := {Properties.typeface..Properties.weight};
		prop.known := prop.valid;
		setMsg.prop := prop;
		Views.HandlePropMsg(v, setMsg);
		RETURN v
	END NewField;

	PROCEDURE SendNotifyMsg (c: Control);
		VAR msg: Views.NotifyMsg;
	BEGIN
		msg.id0 := c.item.adr; msg.id1 := msg.id0 + c.item.Size();
		msg.opts := {2, 4}; (* update, guardcheck *)
		Views.Omnicast(msg)
	END SendNotifyMsg;

	PROCEDURE GetSize(c: Control; OUT rows, cols: INTEGER);
		VAR item: Meta.Item;
	BEGIN
		IF c.item.Valid() THEN
			item := c.item;
			c.item.Lookup("rows", item); rows := item.IntVal();
			c.item.Lookup("cols", item); cols := item.IntVal()
		ELSE
			rows := 0; cols := 0
		END
	END GetSize;

	PROCEDURE GetLabel(c: Control; col: INTEGER; OUT val: Dialog.String; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok); t.t.GetLabel(col, val)
	END GetLabel;

	PROCEDURE GetAttr(c: Control; row, col: INTEGER; 
								OUT style: SET; OUT weight: INTEGER; OUT color: Ports.Color; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok); t.t.GetAttr(row, col, style, weight, color)
	END GetAttr;

	PROCEDURE GetText(c: Control; row, col: INTEGER; OUT val: Dialog.String; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok); t.t.GetItem(row, col, val)
	END GetText;

	PROCEDURE SetText(c: Control; f: Views.Frame; row, col: INTEGER; IN val: Dialog.String; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok);
		IF ok THEN
			t.t.SetItem(row, col, val);
			(* Notify(c, row, col, {}, changed); *)
			SendNotifyMsg(c);
			Controls.Notify(c, f, changed, row, col)
		END
	END SetText;

	PROCEDURE GetSelection(c: Control; OUT on: BOOLEAN; OUT row, col: INTEGER; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok);
		IF ok THEN
			on := t.t.HasSelection();
			IF on THEN t.t.GetSelection(row, col) ELSE row := -1; col := -1 END
		END
	END GetSelection;

	PROCEDURE SetSelection(c: Control; f: Views.Frame; on: BOOLEAN; row, col: INTEGER; OUT ok: BOOLEAN);
		VAR t: TableValue;
	BEGIN
		ASSERT(c.item.Valid(), 20);
		c.item.GetVal(t, ok);
		IF ok THEN
			IF on THEN t.t.Select(row, col); Controls.Notify(c, f, select, row, col)
			ELSE t.t.Deselect; Controls.Notify(c, f, deselect, row, col)
			END;
			SendNotifyMsg(c)
		END
	END SetSelection;


	PROCEDURE SetupControl (t: Control);
		VAR i, asc, dsc, w: INTEGER;
	BEGIN
		t.fldFont := Fonts.dir.This(t.sprop.typeface, t.sprop.size, t.sprop.style.val, Fonts.normal);
		t.titFont := Fonts.dir.This(t.sprop.typeface, t.sprop.size, t.sprop.style.val, Fonts.bold);
		t.rowHeight := 3 * t.sprop.size DIV 2;
		t.fldFont.GetBounds(asc, dsc, w);
		t.rowHeight := asc + dsc + 4 * Ports.point;
		t.baseOff := (t.rowHeight - asc - dsc) DIV 2 + asc;
		i := t.columns;
		WHILE i < maxCol DO t.width[i] := defColW; t.mode[i] := center; INC(i) END;
		IF t.field # NIL THEN
			t.field.context(Context).h := t.rowHeight
		END;
		t.labelHeight := t.rowHeight 
	END SetupControl;


	(** Directory **)

	PROCEDURE (d: Directory) NewControl* (p: Controls.Prop): Views.View, NEW, ABSTRACT;


	(* PropOp *)

	PROCEDURE (op: PropOp) Do;
		VAR c: Control; sprop: Properties.StdProp; tprop: Prop;
	BEGIN
		ASSERT((op.sprop # NIL) OR (op.tprop # NIL), 20);
		c := op.tab;
		IF op.sprop # NIL THEN
			sprop := Properties.CopyOf(c.sprop)(Properties.StdProp);
			sprop.valid := op.sprop.valid;	(* fields to be restored *)
			IF Properties.typeface IN sprop.valid THEN c.sprop.typeface := op.sprop.typeface END;
			IF Properties.size IN sprop.valid THEN c.sprop.size := op.sprop.size END;
			IF Properties.style IN sprop.valid THEN
				c.sprop.style.mask := c.sprop.style.mask + op.sprop.style.mask;
				c.sprop.style.val := c.sprop.style.val - op.sprop.style.mask + op.sprop.style.val
			END;
			IF sprop.valid # {} THEN SetupControl(c) END;
			op.sprop := sprop
		END;
		IF op.tprop # NIL THEN
			tprop := Properties.CopyOf(c.tprop)(Prop);
			tprop.valid := op.tprop.valid;	(* fields to be restored *)
			IF layoutEditable IN tprop.valid THEN c.tprop.layoutEditable := op.tprop.layoutEditable END;
			IF dataEditable IN tprop.valid THEN c.tprop.dataEditable := op.tprop.dataEditable END;
			IF selectionStyle IN tprop.valid THEN c.tprop.selectionStyle := op.tprop.selectionStyle END;
			op.tprop := tprop
		END;
		Views.Update(c, Views.rebuildFrames)
	END Do;


	(* FormatOp *)

	PROCEDURE (op: FormatOp) Do;
		VAR t: Control; c, w, m: INTEGER;
	BEGIN
		t := op.tab; c := op.col; w := op.width; m := op.mode;
		op.width := t.width[c]; op.mode := t.mode[c];
		t.width[c] := w; t.mode[c] := m;
		IF c >= t.columns THEN t.columns := c + 1 END;
		Views.Update(t, Views.keepFrames)
	END Do;


	(* properties *)

	PROCEDURE PollProp (c: Control; VAR list: Properties.Property);
		VAR p: Properties.Property;
	BEGIN
		p := Properties.CopyOf(c.sprop);
		p.valid := {Properties.typeface, Properties.size, Properties.style, Properties.weight};
		p.known := p.valid; p.readOnly := {Properties.weight};
		Properties.Insert(list, p);
		p := Properties.CopyOf(c.tprop);
		p.valid := {layoutEditable, dataEditable, selectionStyle}; p.known := p.valid; p.readOnly := {};
		Properties.Insert(list, p)
	END PollProp;

	PROCEDURE SetProp (c: Control; p: Properties.Property);
		VAR op: PropOp; valid: SET;
	BEGIN
		op := NIL;
		WHILE p # NIL DO
			WITH p: Properties.StdProp DO
				valid := p.valid * {Properties.typeface, Properties.size, Properties.style};
				IF valid # {} THEN
					IF op = NIL THEN NEW(op); op.tab := c END;
					op.sprop := Properties.CopyOf(p)(Properties.StdProp);
					op.sprop.valid := valid
				END
			| p: Prop DO
				valid := p.valid * {layoutEditable, dataEditable, selectionStyle};
				IF valid # {} THEN
					IF op = NIL THEN NEW(op); op.tab := c END;
					op.tprop := Properties.CopyOf(p)(Prop);
					op.tprop.valid := valid
				END
			ELSE
			END;
			p := p.next
		END;
		IF op # NIL THEN Views.Do(c, "#System:SetProp", op) END
	END SetProp;


	(* Control *)

	PROCEDURE DrawBorder (f: Views.Frame; x, y, w, h: INTEGER);
	BEGIN
		f.DrawRect(x, 	y, 	x+f.dot, 	y+h, Ports.fill, Ports.white);
		f.DrawRect(x+f.dot, 	y+0, 	x+2 * f.dot,	y+ h, Ports.fill, Ports.grey25);
		f.DrawRect(x+0, 	y+0, 	x+w, 	y+f.dot, Ports.fill, Ports.white);
		f.DrawRect(x+f.dot, 	y+f.dot, 	x+w, 	y+2 * f.dot, Ports.fill, Ports.grey25);
		f.DrawRect(x+w - f.dot, 	y+0, 	x+w, 	y+h, Ports.fill, Ports.grey50);
		f.DrawRect(x+w - 2 * f.dot, 	y+f.dot, 	x+w - f.dot, 	y+h - f.dot, Ports.fill, Ports.black);
		f.DrawRect(x+0, 	y+h - f.dot, 	x+w, 	y+h, Ports.fill, Ports.grey50);
		f.DrawRect(x+f.dot, 	y+h - 2 * f.dot, 	x+w - f.dot, 	y+h - f.dot, Ports.fill, Ports.black)
	END DrawBorder;

	PROCEDURE DrawLabel (f: Views.Frame; x, y, w, x0, y0, w0, h0, mode: INTEGER; 
									VAR is: ARRAY OF CHAR; font: Fonts.Font; rowHeight: INTEGER);
		VAR dx, i, j, si, sw, rw: INTEGER; s: Dialog.String;
	BEGIN
		DEC(w, 4 * f.dot); INC(x, 2 * f.dot);
		j := 0; y := y - rowHeight;
		WHILE is[j] # 0X DO
			si := 0;
			WHILE (is[j] # 0X) & (is[j] # line) DO s[si] := is[j]; INC(si); INC(j) END;
			IF is[j] = line THEN INC(j) END;
			s[si] := 0X; y := y + rowHeight;
			sw := font.StringWidth(s);
			IF sw > w THEN
				rw := w - font.StringWidth("...");
				IF (rw >= 0) & (LEN(s) >= 4) THEN
					i := f.CharIndex(0, rw, s, font);
					IF i > 0 THEN DEC(i) END;
					IF i > LEN(s) - 4 THEN i := LEN(s) - 4 END;
					s[i] := "."; s[i+1] := "."; s[i+2] := "."; s[i+3] := 0X;
					sw := font.StringWidth(s)
				ELSE sw := 0
				END
			END;
			IF sw > 0 THEN
				dx := x;
				IF mode = center THEN dx := x +  (w - sw) DIV 2
				ELSIF mode = right THEN dx := x + w - sw
				END;
				f.DrawString(dx, y, Ports.black, s, font)
			END
		END;
		DrawBorder(f, x0, y0, w0, h0)
	END DrawLabel;

	PROCEDURE DrawField (f: Views.Frame; x, y, w, mode: INTEGER; VAR s: ARRAY OF CHAR; font: Fonts.Font;
											color: Ports.Color);
		VAR i, sw, rw: INTEGER;
	BEGIN
		DEC(w, 4 * f.dot); INC(x, 2 * f.dot);
		sw := font.StringWidth(s);
		IF sw > w THEN
			rw := w - font.StringWidth("...");
			IF (rw >= 0) & (LEN(s) >= 4) THEN
				i := f.CharIndex(0, rw, s, font);
				IF i > 0 THEN DEC(i) END;
				IF i > LEN(s) - 4 THEN i := LEN(s) - 4 END;
				s[i] := "."; s[i+1] := "."; s[i+2] := "."; s[i+3] := 0X;
				sw := font.StringWidth(s)
			ELSE sw := 0
			END
		END;
		IF sw > 0 THEN
			IF mode = center THEN INC(x, (w - sw) DIV 2)
			ELSIF mode = right THEN INC(x, w - sw)
			END;
			f.DrawString(x, y, color, s, font)
		END
	END DrawField;

	PROCEDURE GetRect(t: Control; f: Views.Frame; row, col: INTEGER; OUT x, y, w, h: INTEGER;
										OUT visible: BOOLEAN);
		VAR c: INTEGER;
	BEGIN
		c := 0; x := 2 * f.dot - t.orgX;
		WHILE c < col DO INC(x, t.width[c]); INC(c) END;
		IF row >= t.orgRow THEN
			visible := TRUE;
			IF row = -1 THEN
				h := t.labelHeight
			ELSE
				h := t.rowHeight
			END;
			y := (row - t.orgRow) * t.rowHeight + t.labelHeight + 3 * f.dot;
			w := t.width[col] 
		ELSE
			visible := FALSE
		END
	END GetRect;


	PROCEDURE DrawSelection (tab: Control; f: Views.Frame; selRow, selCol: INTEGER; show: BOOLEAN);
		VAR c, l, t, r, b, cols, rows: INTEGER;
	BEGIN
		IF (selRow >= tab.orgRow) OR (tab.tprop.selectionStyle IN {colSelect, crossSelect}) THEN
			GetSize(tab, rows, cols);
			IF (0 <= selRow) & (selRow < rows) & (0 <= selCol) & (selCol < cols) THEN
				IF tab.tprop.selectionStyle = cellSelect THEN	(* mark selected cell *)
					l := 2 * f.dot - tab.orgX; c := 0;
					WHILE c < selCol DO INC(l, tab.width[c]); INC(c) END;
					r := l + tab.width[selCol];					
					t := (selRow - tab.orgRow) * tab.rowHeight + tab.labelHeight + 3 * f.dot;
					b := t + tab.rowHeight;
					f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
				ELSIF tab.tprop.selectionStyle = rowSelect THEN	(* mark selected row *)
					l := 2 * f.dot - tab.orgX; r := l; c := 0;
					WHILE c < cols DO INC(r, tab.width[c]); INC(c) END;
					t := (selRow - tab.orgRow) * tab.rowHeight + tab.labelHeight + 3 * f.dot;
					b := t + tab.rowHeight;
					f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
				ELSIF tab.tprop.selectionStyle = colSelect THEN	(* mark selected column *)
					l := 2 * f.dot - tab.orgX; c := 0;
					WHILE c < selCol DO INC(l, tab.width[c]); INC(c) END;
					r := l + tab.width[selCol];
					t := tab.labelHeight + 3 * f.dot;
					b := t + tab.rowHeight * (rows - tab.orgRow);
					f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
				ELSIF tab.tprop.selectionStyle = crossSelect THEN	(* mark both the row and column *)
					IF selRow >= tab.orgRow THEN
						l := 2 * f.dot - tab.orgX; r := l; c := 0;
						t := (selRow - tab.orgRow) * tab.rowHeight + tab.labelHeight + 3 * f.dot;
						b := t + tab.rowHeight;
						IF selCol > 0 THEN
							WHILE c < selCol DO INC(r, tab.width[c]); INC(c) END;
							f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
						END;
						IF selCol + 1 < cols THEN
							r := r + tab.width[selCol]; l := r; c := selCol + 1;
							WHILE c < cols DO INC(r, tab.width[c]); INC(c) END;
							f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
						END
					END;
					l := 2 * f.dot - tab.orgX; c := 0;
					WHILE c < selCol DO INC(l, tab.width[c]); INC(c) END;
					r := l + tab.width[selCol];
					t := tab.labelHeight + 3 * f.dot;
					b := t + tab.rowHeight * (rows - tab.orgRow);
					f.MarkRect(l + f.dot, t + f.dot, r - 2 * f.dot, b - 2 * f.dot, Ports.fill, Ports.hilite, show)
				END
			END
		END
	END DrawSelection;

	PROCEDURE Select (c: Control; f: Views.Frame; on: BOOLEAN);
		VAR ok: BOOLEAN;
	BEGIN
		IF on # c.hasSelection THEN
			c.hasSelection := on; c.showSelection := on;
			SetSelection(c, f, on, c.selRow, c.selCol, ok)
		END
	END Select;

	PROCEDURE ViewFromSelection (t: Control): Views.View;
		VAR str: Dialog.String; ok: BOOLEAN;
	BEGIN
		IF t.hasSelection & t.showSelection THEN
			GetText(t, t.selRow, t.selCol, str, ok);
			IF ok THEN
				RETURN TextViews.dir.New(TextModels.dir.NewFromString(str))
			ELSE
				RETURN NIL
			END
		ELSE RETURN NIL
		END
	END ViewFromSelection;

	PROCEDURE GetControlSize (t: Control; dot: INTEGER; OUT w, h: LONGINT);
		VAR r, c, i: INTEGER;
	BEGIN
		IF ~t.disabled & t.item.Valid() THEN
			w := 0; h := 0;
			GetSize(t, r, c);
			i := 0;
			WHILE (i < c) & (i < maxCol) DO INC(w, t.width[i]); INC(i) END;
			INC(w, 3 * dot);
			h := LONG(r - t.orgRow) * t.rowHeight + t.labelHeight
		ELSE
			w := Views.undefined; h := Views.undefined
		END
	END GetControlSize;

	PROCEDURE CheckPos (t: Control; x, y, dot: INTEGER; VAR col, type, p: INTEGER);
		VAR c, a: INTEGER; w, h: LONGINT;
	BEGIN
		GetControlSize(t, dot, w, h);
		INC(x, t.orgX);
		IF (x >= 0) & (x <= w) & (y >= 0) & (y <= h) THEN
			c := 0; w := 0; type := 0; INC(x, dot);
			WHILE (c < maxCol) & (x >= w + t.width[c]) DO INC(w, t.width[c]); INC(c) END;
			IF (x <= w + 3 * dot) & (c > 0) THEN
				col := c - 1; p := SHORT(w) + dot - t.orgX; type := move
			ELSIF y - dot < t.labelHeight THEN
				type := adjust;
				col := c; a := t.width[c] DIV 3;
				IF x < w + a THEN p := left
				ELSIF x > w + a * 2 THEN p := right
				ELSE p := center
				END
			ELSE
				col := c; p := (y - t.labelHeight - dot) DIV t.rowHeight + t.orgRow - 1 + 1; type := field
			END
		ELSE type := 0
		END
	END CheckPos;

	PROCEDURE MoveLine (t: Control; f: Views.Frame; col, x0: INTEGER);
		VAR w, h, x, y, x1, limit: INTEGER; m: SET; isDown: BOOLEAN; op: FormatOp;
	BEGIN
		t.context.GetSize(w, h);
		x := x0; limit := x0 - t.width[col] + 2 * f.dot;
		REPEAT
			f.Input(x1, y, m, isDown);
			IF x1 < limit THEN x1 := limit END;
			IF x1 # x THEN
				f.MarkRect(x, 0, x + f.dot, h, Ports.fill, Ports.invert, FALSE);
				x := x1;
				f.MarkRect(x, 0, x + f.dot, h, Ports.fill, Ports.invert, TRUE)
			END
		UNTIL ~isDown;
		NEW(op); op.tab := t; op.col := col;
		op.width := t.width[col] + x - x0; op.mode := t.mode[col];
		Views.Do(t, "#System:SetLayout", op)
	END MoveLine;

	PROCEDURE ChangeAdjust (t: Control; col, mode: INTEGER);
		VAR op: FormatOp;
	BEGIN
		NEW(op); op.tab := t; op.col := col;
		op.width := t.width[col]; op.mode := mode;
		Views.Do(t, "#System:SetLayout", op)
	END ChangeAdjust;

	PROCEDURE ControlSections (t: Control; f: Views.Frame; vertical: BOOLEAN; OUT size, part, pos: INTEGER);
		VAR r, c, w, max: INTEGER;
	BEGIN
		size := 0; part := 0; pos := 0; GetSize(t, r, c);
		IF vertical THEN
			size := r;
			part := (f.b - (f.t + t.labelHeight + 3*f.dot)) DIV t.rowHeight;
			pos := t.orgRow
		ELSE
			w := 0; max := MIN(c, maxCol);
			c := 0;
			WHILE (c < max) DO INC(w, t.width[c]); INC(c) END;
			size := w + 3 * f.dot;
			part := f.r - f.l;
			pos := t.orgX
		END
	END ControlSections;

	PROCEDURE ScrollControl (t: Control; f: Views.Frame; op, pos: INTEGER; vertical: BOOLEAN);
		VAR size, part, p, delta, l, t0, r, b: INTEGER;
	BEGIN
		IF vertical THEN
			ControlSections(t, f, TRUE, size, part, p);
			delta := part - 1;
			IF delta < 1 THEN delta := 1 END;
			CASE op OF
			| Controllers.decLine: DEC(p)
			| Controllers.incLine: INC(p)
			| Controllers.decPage: DEC(p, delta)
			| Controllers.incPage: INC(p, delta)
			| Controllers.gotoPos: p := pos
			END;
			IF p > size - part THEN p := size - part END;
			IF p < 0 THEN p := 0 END;
			delta := (f.gy + t.labelHeight + 3 * f.dot) DIV f.unit;
			f.rider.GetRect(l, t0, r, b);
			IF b > delta THEN
				IF t0 < delta THEN f.rider.SetRect(l, delta, r, b) END;
				Views.Scroll(t, 0, (t.orgRow - p) * t.rowHeight);
				IF f.rider # NIL THEN
					f.rider.SetRect(l, t0, r, b)
				END
			END;
			t.orgRow := p
		ELSE
			ControlSections(t, f, FALSE, size, part, p);
			delta := part - defColW;
			IF delta < defColW THEN delta := defColW END;
			CASE op OF
			| Controllers.decLine: DEC(p, defColW)
			| Controllers.incLine: INC(p, defColW)
			| Controllers.decPage: DEC(p, delta)
			| Controllers.incPage: INC(p, delta)
			| Controllers.gotoPos: p := pos
			END;
			IF p >= size - part THEN p := size - part END;
			IF p < 0 THEN p := 0 END;
			Views.Scroll(t, t.orgX - p, 0);
			t.orgX := p
		END
	END ScrollControl;

	PROCEDURE HandleChar (t: Control; f: Views.Frame; ch: CHAR);
	BEGIN
		CASE ch OF
		| 10X: ScrollControl(t, f, Controllers.decPage, 0, FALSE)
		| 11X: ScrollControl(t, f, Controllers.incPage, 0, FALSE)
		| 12X: ScrollControl(t, f, Controllers.decPage, 0, TRUE)
		| 13X: ScrollControl(t, f, Controllers.incPage, 0, TRUE)
		| 14X: ScrollControl(t, f, Controllers.gotoPos, 0, FALSE)
		| 15X: ScrollControl(t, f, Controllers.gotoPos, MAX(INTEGER), FALSE)
		| 16X: ScrollControl(t, f, Controllers.gotoPos, 0, TRUE)
		| 17X: ScrollControl(t, f, Controllers.gotoPos, MAX(INTEGER), TRUE)
		| 1CX: ScrollControl(t, f, Controllers.decLine, 0, FALSE)
		| 1DX: ScrollControl(t, f, Controllers.incLine, 0, FALSE)
		| 1EX: ScrollControl(t, f, Controllers.decLine, 0, TRUE)
		| 1FX: ScrollControl(t, f, Controllers.incLine, 0, TRUE)
		| 07X, 08X, 1BX: Select(t, f, FALSE)	(* rdel, del, esc *)
		ELSE
		END
	END HandleChar;


	PROCEDURE (t: Control) Internalize2 (VAR rd: Stores.Reader);
		VAR thisVersion, i: INTEGER; lockedLayout, lockedData, markRow, markCol: BOOLEAN;
	BEGIN
		rd.ReadVersion(minVersion, tabVersion, thisVersion);
		IF ~rd.cancelled THEN
			IF thisVersion = tabVersion THEN (* current table version *)
				NEW(t.tprop);
				rd.ReadBool(t.tprop.layoutEditable);
				rd.ReadBool(t.tprop.dataEditable);
				rd.ReadInt(t.tprop.selectionStyle)
			ELSIF thisVersion = 1 THEN (* intermediate Table version (after 1.4 Beta, before 1.4 final) *)
				rd.ReadBool(lockedLayout);
				rd.ReadBool(lockedData);
				rd.ReadBool(markRow);
				rd.ReadBool(markCol);
				NEW(t.tprop);
				t.tprop.layoutEditable := ~lockedLayout; t.tprop.dataEditable := ~lockedData;
				IF markRow THEN
					IF markCol THEN t.tprop.selectionStyle := noSelect ELSE t.tprop.selectionStyle := rowSelect END
				ELSE
					IF markCol THEN t.tprop.selectionStyle := colSelect ELSE t.tprop.selectionStyle := cellSelect END
				END
			ELSE (* old version, 1.4 Beta *)
				t.tprop.layoutEditable := TRUE; t.tprop.dataEditable := TRUE; t.tprop.selectionStyle := cellSelect
			END;
			NEW(t.sprop);
			rd.ReadString(t.sprop.typeface);
			rd.ReadInt(t.sprop.size);
			rd.ReadSet(t.sprop.style.val);
			t.sprop.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
			t.sprop.weight := Fonts.normal;
			rd.ReadInt(t.columns); i := 0;
			WHILE i < t.columns DO
				rd.ReadInt(t.width[i]);
				rd.ReadInt(t.mode[i]);
				INC(i)
			END;
			SetupControl(t)
		END
	END Internalize2;

	PROCEDURE (t: Control) Externalize2 (VAR wr: Stores.Writer);
		VAR i: INTEGER;
	BEGIN
		wr.WriteVersion(tabVersion);
		wr.WriteBool(t.tprop.layoutEditable);
		wr.WriteBool(t.tprop.dataEditable);
		wr.WriteInt(t.tprop.selectionStyle);
		wr.WriteString(t.sprop.typeface);
		wr.WriteInt(t.sprop.size);
		wr.WriteSet(t.sprop.style.val);
		wr.WriteInt(t.columns); i := 0;
		WHILE i < t.columns DO
			wr.WriteInt(t.width[i]);
			wr.WriteInt(t.mode[i]);
			INC(i)
		END
	END Externalize2;

	PROCEDURE (t: Control) CopyFromSimpleView2 (source: Controls.Control);
	BEGIN
		WITH source: Control DO
			t.sprop := Properties.CopyOf(source.sprop)(Properties.StdProp);
			t.tprop := Properties.CopyOf(source.tprop)(Prop);
			t.columns := source.columns;
			t.width := source.width;
			t.mode := source.mode;
			SetupControl(t)
		END
	END CopyFromSimpleView2;

	PROCEDURE (t: Control) GetBackground (VAR color: Ports.Color);
	BEGIN
		color := Ports.background
	END GetBackground;

	PROCEDURE Paint (c: Control; f: Views.Frame; l, t, r, b: INTEGER);
		VAR width, w, h, rows, cols, row, col, x, y, rowHeight, dl, weight: INTEGER; font: Fonts.Font;
			str: Dialog.String; ok: BOOLEAN;style: SET; color: Ports.Color;
	BEGIN
		c.context.GetSize(w, h);
		IF ~c.disabled & c.item.Valid() THEN
			GetSize(c, rows, cols); 
			DEC(c.rowHeight, c.rowHeight MOD f.unit);
			row := -1; y := 2 * f.dot; font := c.titFont;
			WHILE (row < rows) & (y < b) DO
				IF row = - 1 THEN
					rowHeight := 1;
					FOR col := 0 TO cols - 1 DO
						GetLabel(c, col, str, ok); dl := CountLines(str);
						IF dl > rowHeight THEN rowHeight := dl END
					END;
					rowHeight := rowHeight * c.rowHeight;
					c.labelHeight := rowHeight
				ELSE 
					rowHeight := c.rowHeight
				END;
				IF ~Views.IsPrinterFrame(f) OR (y + rowHeight < b) THEN
					col := 0; x := 2 * f.dot - c.orgX;
					WHILE (col < cols) & (col < maxCol) & (x < r) DO
						width := c.width[col];
						IF x + width >= 0 THEN
							f.DrawRect(x - f.dot, y, x, y + rowHeight, Ports.fill, Ports.black);	(* left vertical separation line *)
							IF ~Views.IsPrinterFrame(f) OR (x + width < r) THEN
								IF row = -1 THEN
									GetLabel(c, col, str, ok);
									f.DrawRect(x, y, x + width - f.dot, y + rowHeight - f.dot, Ports.fill, Ports.grey25);
										(* background *)
									DrawLabel(f, x, y + c.baseOff, width - f.dot, x, y, width, rowHeight,
														c.mode[col], str, font, c.rowHeight)
								ELSE
									GetText(c, row, col, str, ok); GetAttr(c, row, col, style, weight, color, ok);
									f.DrawRect(x, y, x + width - f.dot, y + rowHeight - f.dot, Ports.fill, Ports.white);
										(* background *)
									DrawField(f, x, y + c.baseOff, width - f.dot, c.mode[col], str, 
													Fonts.dir.This(font.typeface, font.size, style, weight), color)
								END
							END
						END;
						INC(col); INC(x, width)
					END;
					f.DrawRect(x - f.dot, y, x, y + rowHeight, Ports.fill, Ports.black);	(* right vertical separator line *)
					IF Views.IsPrinterFrame(f) & (x >= r) THEN DEC(x, width) END;

					IF row = -1 THEN row := c.orgRow; font := c.fldFont; INC(y, f.dot)
					ELSE INC(row)
					END
				END;
				INC(y, rowHeight);
				f.DrawRect(f.dot - c.orgX, y - f.dot, x, y, Ports.fill, Ports.black)	(* bottom line *)
			END;
			IF f.dot - c.orgX < x THEN
				f.DrawRect(f.dot - c.orgX, f.dot, x, 2 * f.dot, Ports.fill, Ports.black);
				f.DrawRect(f.dot - c.orgX, c.labelHeight + f.dot, x, c.labelHeight + 2 * f.dot, Ports.fill, Ports.black)
			END
		ELSE
			f.DrawRect(f.dot, f.dot, w - f.dot, h - f.dot, Ports.fill, Ports.grey25);
			f.DrawRect(0, 0, w, h, f.dot, Ports.black)
		END
	END Paint;

	PROCEDURE (c: Control) Restore (f: Views.Frame; l, t, r, b: INTEGER);
		VAR visible: BOOLEAN; g: Views.Frame;
	BEGIN
		Paint(c, f, l, t, r, b);
		IF c.field # NIL THEN
			GetRect(c, f, c.selRow, c.selCol, c.x, c.y, c.w, c.h, visible);
			IF visible THEN Views.InstallFrame(f, c.field, c.x, c.y, 0, TRUE)
			ELSE
				g := Views.ThisFrame(f, c.field); IF g # NIL THEN Views.RemoveFrame(f, g) END
			END
		END
	END Restore;
	
	PROCEDURE (c: Control) Update (f: Views.Frame; op, from, to: INTEGER);
	BEGIN
		Views.Update(c, Views.keepFrames)
	END Update;

	PROCEDURE (c: Control) RestoreMarks (f: Views.Frame; l, t, r, b: INTEGER);
		VAR row, col: INTEGER; ok, on: BOOLEAN;
	BEGIN
		IF c.item.Valid() THEN
			GetSelection(c, on, row, col, ok);
			IF ok THEN
				c.hasSelection := on;
				IF on THEN
					c.selRow := row; c.selCol := col;
					c.showSelection := c.field = NIL;
					IF c.showSelection THEN DrawSelection(c, f, c.selRow, c.selCol, TRUE) END
				END
			END
		END
	END RestoreMarks;

	PROCEDURE AddEditField (t: Control; f: Views.Frame; row, col: INTEGER);
		VAR g: Views.Frame; mMsg: Controllers.MarkMsg; ok, visible: BOOLEAN;
	BEGIN
		GetRect(t, f, row, col, t.x, t.y, t.w, t.h, visible);
		IF visible THEN
			GetText(t, row, col, text, ok);
			t.field := NewField(t, col, t.w, t.h); Stores.Join(t, t.field);

			Views.InstallFrame(f, t.field, t.x, t.y, 0, TRUE);
			mMsg.show := TRUE; mMsg.focus := TRUE;
			g := Views.ThisFrame(f, t.field);
			IF g # NIL THEN Views.ForwardCtrlMsg(g, mMsg) END
		END
	END AddEditField;

	PROCEDURE DisableEditField (t: Control; f: Views.Frame; update: BOOLEAN);
		VAR g: Views.Frame; mMsg: Controllers.MarkMsg; ok: BOOLEAN;
	BEGIN
		IF t.field # NIL THEN
			mMsg.show := FALSE; mMsg.focus := FALSE;
			g := Views.ThisFrame(f, t.field);
			IF g # NIL THEN Views.ForwardCtrlMsg(g, mMsg); Views.RemoveFrame(f, g) END;
			t.field := NIL;
			IF update THEN SetText(t, f, t.selRow, t.selCol, text, ok) END;
			t.showSelection := TRUE
		END
	END DisableEditField;

	PROCEDURE MoveEditField (t: Control; f: Views.Frame; right: BOOLEAN);
		VAR rows, cols: INTEGER; ok: BOOLEAN;
	BEGIN
		DisableEditField(t, f, TRUE);
		GetSize(t, rows, cols);
		Select(t, f, FALSE);
		IF right THEN
			t.selCol := (t.selCol + 1) MOD cols;
			IF t.selCol = 0 THEN t.selRow := (t.selRow + 1) MOD rows END
		ELSE
			t.selCol := (t.selCol - 1) MOD cols;
			IF t.selCol = cols - 1 THEN t.selRow := (t.selRow - 1) MOD rows END
		END;
		t.hasSelection := TRUE; t.showSelection := FALSE;
		SetSelection(t, f, TRUE, t.selRow, t.selCol, ok);
		AddEditField(t, f, t.selRow, t.selCol)
	END MoveEditField;

	PROCEDURE TrackMouse (t: Control; f: Views.Frame; VAR r, c, type: INTEGER);
		VAR x, y, selr, selc: INTEGER; m: SET; isDown, sel: BOOLEAN;
	BEGIN
		selr := t.selRow; selc := t.selCol; sel := t.hasSelection;
		REPEAT
			f.Input(x, y, m, isDown);
			CheckPos(t, x, y, f.dot, c, type, r);
			IF sel & ((type # field) OR (selr # r) OR (selc # c)) THEN
				DrawSelection(t, f, selr, selc, FALSE)
			END;
			IF (type = field) & (~sel OR (selr # r) OR (selc # c)) THEN
				DrawSelection(t, f, r, c, TRUE)
			END;
			sel := type = field; selr := r; selc := c
		UNTIL ~isDown;
		IF ~sel & t.hasSelection THEN
			DrawSelection(t, f, t.selRow, t.selCol, TRUE)
		END
	END TrackMouse;

	PROCEDURE (t: Control) HandleCtrlMsg2 (f: Views.Frame; VAR msg: Controllers.Message;
																	VAR focus: Views.View);
		VAR p, col, type: INTEGER; x, y: INTEGER; m: SET; isDown: BOOLEAN;
			ok, rebuild: BOOLEAN; c, w, size, part, pos: INTEGER;
	BEGIN
		IF ~t.disabled & t.item.Valid() THEN
			WITH msg: Controllers.PollOpsMsg DO
				IF t.field # NIL THEN focus := t.field
				ELSIF t.hasSelection THEN msg.valid := {Controllers.copy}
				END
			| msg: Controllers.PollCursorMsg DO
				IF (t.field # NIL) & (msg.x >= t.x) & (msg.x <= t.x + t.w) & (msg.y >= t.y) & (msg.y <= t.y + t.h) THEN
					focus := t.field
				ELSE
					CheckPos(t, msg.x, msg.y, f.dot, col, type, p);
					IF (type = move) & t.tprop.layoutEditable THEN msg.cursor := HostPorts.resizeHCursor
					ELSIF (type = adjust) & t.tprop.layoutEditable THEN msg.cursor := Ports.refCursor
					ELSIF  ~t.readOnly & (type = field) THEN msg.cursor := Ports.tableCursor
					END
				END
			| msg: Controllers.MarkMsg DO
				IF t.field # NIL THEN DisableEditField(t, f, TRUE); Views.Update(t, Views.rebuildFrames) END
			| msg: Controllers.SelectMsg DO
				IF t.field # NIL THEN focus := t.field
				ELSIF ~msg.set THEN Select(t, f, FALSE)
				END
			| msg: Controllers.EditMsg DO
				IF t.field # NIL THEN
					IF msg.op = Controllers.pasteChar THEN
						IF (msg.char = lineChar) OR (msg.char = esc) THEN
							DisableEditField(t, f, msg.char # esc); Views.Update(t, Views.rebuildFrames)
						ELSIF (msg.char = tab) OR (msg.char = ltab) THEN
							MoveEditField(t, f, msg.char = tab); Views.Update(t, Views.rebuildFrames)
						ELSE
							focus := t.field
						END	
					ELSE
						focus := t.field
					END
				ELSE
					IF (msg.op = Controllers.pasteChar) & (msg.char = lineChar) &
						(t.tprop.selectionStyle = cellSelect) & t.hasSelection THEN
						t.showSelection := FALSE;
						SetSelection(t, f, TRUE, t.selRow, t.selCol, ok);
						AddEditField(t, f, t.selRow, t.selCol); Views.Update(t, Views.keepFrames)
					ELSIF msg.op = Controllers.pasteChar THEN
						HandleChar(t, f, msg.char)
					ELSIF msg.op = Controllers.copy THEN
						msg.view := ViewFromSelection(t);
						msg.w := 0; msg.h := 0; msg.isSingle := FALSE
					END
				END
			| msg: Controllers.TrackMsg DO
				IF (t.field # NIL) & (msg.x >= t.x) & (msg.x <= t.x + t.w) & (msg.y >= t.y) & (msg.y <= t.y + t.h) THEN
						focus := t.field
				ELSE
					rebuild := t.field # NIL; DisableEditField(t, f, TRUE);	(* update below after notifications *)
					CheckPos(t, msg.x, msg.y, f.dot, col, type, p);
					IF type = move THEN
						IF t.tprop.layoutEditable THEN
							MoveLine(t, f, col, p)
						END
					ELSIF type = adjust THEN
						IF t.tprop.layoutEditable THEN
							ChangeAdjust(t, col, p);
							REPEAT f.Input(x, y, m, isDown) UNTIL ~isDown
						END
					ELSIF ~t.readOnly & (type = field) THEN
						TrackMouse(t, f, p, col, type);
						IF type = field THEN
							IF ~t.readOnly & t.tprop.dataEditable
								& ((Controllers.doubleClick IN msg.modifiers) OR (t.tprop.selectionStyle = noSelect))
							THEN
								Select(t, f, FALSE);
								t.selRow := p; t.selCol := col; t.hasSelection := TRUE; t.showSelection := FALSE;
								SetSelection(t, f, TRUE, t.selRow, t.selCol, ok);
								AddEditField(t, f, p, col); Views.Update(t, Views.keepFrames)
							ELSE
								IF (t.selRow # p) OR (t.selCol # col) OR ~t.hasSelection THEN
									Select(t, f, FALSE);
									t.selRow := p; t.selCol := col;
									Select(t, f, TRUE)
								ELSIF t.tprop.selectionStyle = cellSelect THEN Select(t, f, FALSE)
								END
							END
						END
					END;
					IF rebuild THEN Views.Update(t, Views.rebuildFrames) END
				END
			| msg: Controllers.PollSectionMsg DO
				ControlSections(t, f, msg.vertical, msg.wholeSize, msg.partSize, msg.partPos);
				IF (msg.partPos > 0) & (msg.partPos > msg.wholeSize - msg.partSize) THEN
					ScrollControl(t, f, Controllers.gotoPos, msg.wholeSize - msg.partSize, msg.vertical);
					ControlSections(t, f, msg.vertical, msg.wholeSize, msg.partSize, msg.partPos)
				END;
				msg.valid := msg.partSize < msg.wholeSize;
				msg.done := TRUE
			| msg: Controllers.ScrollMsg DO
				ScrollControl(t, f, msg.op, msg.pos, msg.vertical);
				msg.done := TRUE
			| msg: Controllers.PageMsg DO
				IF msg.op IN {Controllers.nextPageY, Controllers.gotoPageY} THEN	(* vertical *)
					ControlSections(t, f, TRUE, size, part, pos);
					IF msg.op = Controllers.nextPageY THEN
						t.orgRow := pos + part
					ELSE
						t.orgRow := msg.pageY * part
					END;
					msg.done := TRUE;
					msg.eoy :=t.orgRow >= size
				ELSE (* horizontal *)
					ControlSections(t, f, FALSE, size, part, pos);
					IF msg.op = Controllers.nextPageX THEN
						t.orgX := pos + part
					ELSE
						t.orgX := msg.pageX * part
					END;
					IF (t.orgX > 0) & (t.orgX < size) THEN
						c := 0; w := 0;
						WHILE w < t.orgX DO INC(w, t.width[c]); INC(c) END;
						t.orgX := w - t.width[c-1] + 1*f.dot
					END;
					msg.done := TRUE;
					msg.eox :=t.orgX >= size
				END
			ELSE
			END
		END
	END HandleCtrlMsg2;

	PROCEDURE (t: Control) HandlePropMsg2 (VAR msg: Properties.Message);
		VAR w, h: LONGINT;
	BEGIN
		WITH msg: Properties.FocusPref DO
			IF ~t.disabled THEN msg.setFocus := TRUE END
		| msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) & (msg.h = Views.undefined) THEN
				GetControlSize(t, Ports.point, w, h);
				msg.w := SHORT(MIN(w, MAX(INTEGER)));
				msg.h := SHORT(MIN(h, MAX(INTEGER)))
			END;
			IF msg.w = Views.undefined THEN msg.w := 80 * Ports.mm END;
			IF msg.h = Views.undefined THEN msg.h := 30 * Ports.mm END
		| msg: Properties.ResizePref DO
			msg.horFitToWin := TRUE;
			msg.verFitToWin := TRUE
		| msg: Controls.PropPref DO
			msg.valid := {Controls.link, Controls.label, Controls.guard, Controls.notifier}
		| msg: Properties.PollMsg DO
			PollProp(t, msg.prop)
		| msg: Properties.SetMsg DO
			SetProp(t, msg.prop)
		| msg: Properties.ControlPref DO
			IF (t.field # NIL) & ((msg.char = lineChar) OR (msg.char = esc) OR (msg.char = tab) OR (msg.char = ltab))
				OR (t.field = NIL) & (t.tprop.selectionStyle = cellSelect) & t.hasSelection & (msg.char = lineChar)
			THEN
				msg.accepts := TRUE
			END
		ELSE
			IF t.field # NIL THEN Views.HandlePropMsg(t.field, msg) END
		END
	END HandlePropMsg2;

	PROCEDURE (c: Control) CheckLink (VAR ok: BOOLEAN);
		VAR item: Meta.Item; mod: Meta.Name; name: Meta.Name;
	BEGIN
		item := c.item;
		IF (item.typ = Meta.recTyp) THEN
			item.GetTypeName(mod, name);
			IF (mod = "StdTables") & (name = "Table") THEN
				ok := TRUE
			ELSE
				ok := FALSE
			END
		ELSE ok := FALSE
		END
	END CheckLink;


	(* Property Dialog *)


	PROCEDURE PollPropForDlg;
		VAR q: Properties.Property; p: Prop;
	BEGIN
		dlg.layoutEditable := TRUE; dlg.dataEditable := TRUE; dlg.selectionStyle.index := cellSelect;
		dlg.known := {}; dlg.valid := {};
		Properties.CollectProp(q);
		WHILE (q # NIL) & ~(q IS Prop) DO q := q.next END;
		IF q # NIL THEN
			p := q(Prop);
			dlg.known := p.known; dlg.valid := p.valid;
			IF layoutEditable IN p.valid THEN dlg.layoutEditable := p.layoutEditable END;
			IF dataEditable IN p.valid THEN dlg.dataEditable := p.dataEditable END;
			IF selectionStyle IN p.valid THEN dlg.selectionStyle.index := p.selectionStyle END
		END;
		Dialog.Update(dlg)
	END PollPropForDlg;

	PROCEDURE SetPropFromDlg;
		VAR p: Prop;
	BEGIN
		NEW(p);
		p.known := dlg.known;
		p.valid := dlg.valid;
		p.layoutEditable := dlg.layoutEditable;
		p.dataEditable := dlg.dataEditable;
		p.selectionStyle := dlg.selectionStyle.index;
		Properties.EmitProp(NIL, p)
	END SetPropFromDlg;

	PROCEDURE (a: Action) Do;
		VAR c: Containers.Controller; v: Views.View; fp: INTEGER;
	BEGIN
		Controllers.SetCurrentPath(Controllers.targetPath);
		c := Containers.Focus(); fp := 0;
		IF c # NIL THEN
			c.GetFirstView(Containers.selection, v);
			WHILE v # NIL DO fp := fp + Services.AdrOf(v); c.GetNextView(Containers.selection, v) END
		END;
		IF fp # dlg.fingerprint THEN PollPropForDlg; dlg.fingerprint := fp END;
		Controllers.ResetCurrentPath();
		Services.DoLater(a, Services.Ticks() + Services.resolution DIV 2)
	END Do;

	PROCEDURE InitDialog*;
	BEGIN
		IF action = NIL THEN NEW(action); Services.DoLater(action, Services.now) END;
		Controllers.SetCurrentPath(Controllers.targetPath);
		PollPropForDlg;
		Controllers.ResetCurrentPath()
	END InitDialog;

	PROCEDURE Set*;
	BEGIN
		Controllers.SetCurrentPath(Controllers.targetPath);
		SetPropFromDlg; PollPropForDlg;
		Controllers.ResetCurrentPath()
	END Set;

	PROCEDURE Guard* (idx: INTEGER; VAR par: Dialog.Par);
	BEGIN
		IF ~(idx IN dlg.known) THEN par.disabled := TRUE
		ELSIF ~(idx IN dlg.valid) THEN par.undef := TRUE
		END
	END Guard;

	PROCEDURE Notifier* (idx, op, from, to: INTEGER);
	BEGIN
		IF op = Dialog.changed THEN INCL(dlg.valid, idx) END
	END Notifier;


	(* StdDirectory *)

	PROCEDURE InitStdProp (OUT p: Properties.StdProp);
		VAR f: Fonts.Font;
	BEGIN
		NEW(p);
		f := Fonts.dir.Default();
		p.typeface := f.typeface;
		p.size := f.size;
		p.style.val := f.style;
		p.style.mask := {Fonts.italic, Fonts.underline, Fonts.strikeout};
		p.weight := Fonts.normal
	END InitStdProp;

	PROCEDURE InitTableProp (OUT p: Prop);
	BEGIN
		NEW(p);
		p.layoutEditable := TRUE; p.dataEditable := TRUE; p.selectionStyle := cellSelect
	END InitTableProp;

	PROCEDURE (d: StdDirectory) NewControl (p: Controls.Prop): Views.View;
		VAR c: Control;
	BEGIN
		NEW(c); Controls.OpenLink(c, p); InitStdProp(c.sprop); InitTableProp(c.tprop); SetupControl(c); RETURN c
	END NewControl;


	PROCEDURE SetDir* (d: Directory);
	BEGIN
		ASSERT(d # NIL, 20); dir := d
	END SetDir;

	PROCEDURE DepositControl*;
		VAR p: Controls.Prop;
	BEGIN
		NEW(p);
		p.link := ""; p.label := ""; p.guard := ""; p.notifier := "";
		p.level := 0; p.opt[Controls.sorted] := FALSE;
		Views.Deposit(dir.NewControl(p))
	END DepositControl;

	PROCEDURE SetupSelectionStyleList (VAR l: Dialog.List);
	BEGIN
		l.SetLen(5);
		l.SetItem(noSelect, "No Selection");
		l.SetItem(cellSelect, "Select Cell");
		l.SetItem(rowSelect, "Select Row");
		l.SetItem(colSelect, "Select Column");
		l.SetItem(crossSelect, "Select Row & Column")
	END SetupSelectionStyleList;

	PROCEDURE Init;
		VAR d: StdDirectory;
	BEGIN
		NEW(d); stdDir := d; dir := d;
		SetupSelectionStyleList(dlg.selectionStyle)
	END Init;

BEGIN
	Init
CLOSE
	Services.RemoveAction(action)
END StdTables.