MODULE FormControllers;
(**
	project	= "BlackBox"
	organization	= "www.oberon.ch"
	contributors	= "Oberon microsystems"
	version	= "System/Rsrc/About"
	copyright	= "System/Rsrc/About"
	license	= "Docu/BB-License"
	purpose	= ""
	changes	= "
	- 20080904, mf, StdController.IsSelected: conformance with contract improved
	- 20160719, center #117, fixes for non-local drop behavior
	"
	issues	= "
	- ...
	"

**)

	IMPORT
		Services, Ports, Stores, Models, Views, Controllers, Properties, Containers,
		FormModels, FormViews;

	CONST
		noSelection* = Containers.noSelection; noFocus* = Containers.noFocus;
		rdel = 7X; ldel = 8X;
		arrowLeft = 1CX; arrowRight = 1DX; arrowUp = 1EX; arrowDown = 1FX;
		(* range of currently supported versions *)
		minVersion = 0; maxBaseVersion = 0; maxStdVersion = 0;

	TYPE
		Controller* = POINTER TO ABSTRACT RECORD (Containers.Controller)
			form-: FormModels.Model;
			view-: FormViews.View
		END;

		Directory* = POINTER TO ABSTRACT RECORD (Containers.Directory) END;

		List* = POINTER TO RECORD
			next*: List;
			view*: Views.View
		END;


		StdController = POINTER TO RECORD (Controller)
			sel: List;	(* (sel = NIL) OR (c.ThisFocus() = NIL) *)
			reader: FormModels.Reader;
			lastX, lastY: INTEGER
		END;

		StdDirectory = POINTER TO RECORD (Directory) END;

		MarkMsg = RECORD (Views.Message)
			list: List;
			show: BOOLEAN
		END;

		PollFocusMsg = RECORD (Controllers.PollFocusMsg)
			c: Controller
		END;


	VAR dir-, stdDir-: Directory;


	(** Controller **)

	PROCEDURE (c: Controller) Internalize2- (VAR rd: Stores.Reader), EXTENSIBLE;
		VAR thisVersion: INTEGER;
	BEGIN
		rd.ReadVersion(minVersion, maxBaseVersion, thisVersion)
	END Internalize2;

	PROCEDURE (c: Controller) Externalize2- (VAR wr: Stores.Writer), EXTENSIBLE;
	BEGIN
		wr.WriteVersion(maxBaseVersion)
	END Externalize2;

	PROCEDURE (c: Controller) InitView2* (view: Views.View), EXTENSIBLE;
	BEGIN
		IF view # NIL THEN
			ASSERT(view IS FormViews.View, 25);
			c.view := view(FormViews.View); c.form := c.view.ThisModel()
		ELSE
			c.form := NIL; c.view := NIL
		END
	END InitView2;

	PROCEDURE (c: Controller) ThisView* (): FormViews.View, EXTENSIBLE;
	BEGIN
		RETURN c.view
	END ThisView;

	PROCEDURE (c: Controller) Select* (view: Views.View), NEW, ABSTRACT;

	PROCEDURE (c: Controller) Deselect* (view: Views.View), NEW, ABSTRACT;

	PROCEDURE (c: Controller) IsSelected* (view: Views.View): BOOLEAN, NEW, ABSTRACT;

	PROCEDURE (c: Controller) GetSelection* (): List, NEW, ABSTRACT;

	PROCEDURE (c: Controller) SetSelection* (l: List), NEW, ABSTRACT;


	(** Directory **)

	PROCEDURE (d: Directory) NewController* (opts: SET): Controller, ABSTRACT;

	PROCEDURE (d: Directory) New* (): Controller, EXTENSIBLE;
	BEGIN
		RETURN d.NewController({})
	END New;


	(* auxiliary procedures *)

	PROCEDURE MarkList (c: StdController; f: Views.Frame; h: List; show: BOOLEAN);
		VAR l, t, r, b, s: INTEGER;
	BEGIN
		IF ~(Containers.noSelection IN c.opts) THEN
			s := 2 * f.unit;
			WHILE h # NIL DO
				c.view.GetRect(f, h.view, l, t, r, b);
				f.MarkRect(l - s, t - s, r + s, b + s, s, Ports.hilite, show);
				h := h.next
			END
		END
	END MarkList;

	PROCEDURE Toggle (c: StdController; view: Views.View);
	BEGIN
		IF c.IsSelected(view) THEN c.Deselect(view) ELSE c.Select(view) END
	END Toggle;


	(* StdController *)

	PROCEDURE (c: StdController) Internalize2 (VAR rd: Stores.Reader);
		VAR thisVersion: INTEGER;
	BEGIN
		c.Internalize2^(rd);
		IF ~rd.cancelled THEN
			rd.ReadVersion(minVersion, maxStdVersion, thisVersion);
			c.sel := NIL; c.lastX := -1
		END
	END Internalize2;

	PROCEDURE (c: StdController) Externalize2 (VAR wr: Stores.Writer);
	BEGIN
		c.Externalize2^(wr);
		wr.WriteVersion(maxStdVersion)
	END Externalize2;

	PROCEDURE (c: StdController) CopyFrom (source: Stores.Store);
	BEGIN
		c.CopyFrom^(source);
		c.sel := NIL; c.lastX := -1
	END CopyFrom;

	PROCEDURE (c: StdController) RestoreMarks2 (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		IF (c.lastX <= f.l ) OR (c.lastX >= f.r) OR (c.lastY <= f.t) OR (c.lastY >= f.b) THEN
			c.lastX := (f.l + f.r) DIV 2; c.lastY := (f.t + f.b) DIV 2
		END
	END RestoreMarks2;

	PROCEDURE (c: StdController) HandleViewMsg (f: Views.Frame; VAR msg: Views.Message);
	BEGIN
		WITH msg: MarkMsg DO
			MarkList(c, f, msg.list, msg.show)
		ELSE
			c.HandleViewMsg^(f, msg)
		END
	END HandleViewMsg;

	PROCEDURE (c: StdController) HandleCtrlMsg (f: Views.Frame; VAR msg: Views.CtrlMessage;
																			VAR focus: Views.View);
	BEGIN
		WITH msg: PollFocusMsg DO
			c.HandleCtrlMsg^(f, msg, focus);
			msg.c := c
		ELSE
			c.HandleCtrlMsg^(f, msg, focus)
		END
	END HandleCtrlMsg;


	(* subclass hooks *)

	PROCEDURE (c: StdController) GetContextType (OUT type: Stores.TypeName);
	BEGIN
		type := "FormViews.View"
	END GetContextType;

	PROCEDURE (c: StdController) GetValidOps (OUT valid: SET);
	BEGIN
		valid := {Controllers.pasteChar, Controllers.paste};
		IF c.sel # NIL THEN valid := valid + {Controllers.cut, Controllers.copy} END
	END GetValidOps;

	PROCEDURE (c: StdController) NativeModel (model: Models.Model): BOOLEAN;
	BEGIN
		ASSERT(model # NIL, 20);
		RETURN model IS FormModels.Model
	END NativeModel;

	PROCEDURE (c: StdController) NativeView (view: Views.View): BOOLEAN;
	BEGIN
		ASSERT(view # NIL, 20);
		RETURN view IS FormViews.View
	END NativeView;

	PROCEDURE (c: StdController) NativeCursorAt (f: Views.Frame; x, y: INTEGER): INTEGER;
	BEGIN
		ASSERT(f # NIL, 20);
		RETURN Ports.graphicsCursor
	END NativeCursorAt;

	PROCEDURE (c: StdController) MakeViewVisible (v: Views.View);
		VAR l, t, r, b: INTEGER;
	BEGIN
		ASSERT(v # NIL, 20);
		ASSERT(v.context # NIL, 21);
		ASSERT(v.context.ThisModel() = c.form, 22);
		v.context(FormModels.Context).GetRect(l, t, r, b);
		c.view.context.MakeVisible(l, t, r, b)
	END MakeViewVisible;

	PROCEDURE (c: StdController) GetFirstView (selection: BOOLEAN; OUT v: Views.View);
		VAR rd: FormModels.Reader;
	BEGIN
		IF selection THEN
			IF c.sel # NIL THEN v := c.sel.view ELSE v := NIL END
		ELSE
			rd := c.form.NewReader(c.reader); c.reader := rd;
			rd.ReadView(v)
		END
	END GetFirstView;

	PROCEDURE (c: StdController) GetNextView (selection: BOOLEAN; VAR v: Views.View);
		VAR h: List; rd: FormModels.Reader;
	BEGIN	(* could be optimized *)
		ASSERT(v # NIL, 20);
		IF selection THEN
			h := c.sel; WHILE (h # NIL) & (h.view # v) DO h := h.next END; ASSERT(h # NIL, 21);
			IF h.next # NIL THEN v := h.next.view ELSE v := NIL END
		ELSE
			rd := c.form.NewReader(c.reader); c.reader := rd;
			rd.Set(v); rd.ReadView(v)
		END
	END GetNextView;

	PROCEDURE (c: StdController) GetSelectionBounds (f: Views.Frame; OUT x, y, w, h: INTEGER);
		VAR g: Views.Frame; sel: List; l, t, r, b, gw, gh, border: INTEGER;
	BEGIN
		IF c.Singleton() # NIL THEN
			c.GetSelectionBounds^(f, x, y, w, h)
		ELSE
			l := MAX(INTEGER); t := MAX(INTEGER); r := MIN(INTEGER); b := MIN(INTEGER);
			sel := c.sel;
			WHILE sel # NIL DO
				g := Views.ThisFrame(f, sel.view);
				IF g # NIL THEN
					sel.view.context.GetSize(gw, gh);
					IF g.gx < l THEN l := g.gx END;
					IF g.gy < t THEN t := g.gy END;
					IF g.gx + gw > r THEN r := g.gx + gw END;
					IF g.gy + gh > b THEN b := g.gy + gh END;
				END;
				sel := sel.next
			END;
			IF (l < r) & (t < b) THEN
				border := c.view.Border();
				x := l - f.gx - border; y := t - f.gy - border;
				w := r - l + 2 * border; h := b - t + 2 * border
			ELSE
				x := 0; y := 0; w := 0; h := 0
			END
		END
	END GetSelectionBounds;
	
	PROCEDURE (c: StdController) MarkDropTarget (src, dst: Views.Frame;
																			sx, sy, dx, dy, w, h, rx, ry: INTEGER;
																			type: Stores.TypeName;
																			isSingle, show: BOOLEAN);
		CONST dm = 4 * Ports.point; dp = 18 * Ports.point;
		VAR vx, vy, l, t, r, b: INTEGER; sc: Containers.Controller; s: Views.View;
	BEGIN	(* cf. Drop *)
		IF ~isSingle & (src # NIL) & (src.view IS FormViews.View) THEN	(* mark local form selection *)
			vx := dx - sx; vy := dy - sy;
			sc := src.view(FormViews.View).ThisController();
			IF sc # NIL THEN
				WITH sc: Controller DO
					sc.GetFirstView(Containers.selection, s);
					WHILE s # NIL DO
						s.context(FormModels.Context).GetRect(l, t, r, b); w := r - l; h := b - t;
						INC(l, vx); INC(t, vy); FormViews.RoundToGrid(c.view, l, t);
						dst.MarkRect(l, t, l + w, t + h, 0, Ports.invert, show);
						sc.GetNextView(Containers.selection, s)
					END
				END
			END
		ELSIF (w > 0) & (h > 0) THEN	(* mark non-local form or singleton selection *)
			vx := dx - rx; vy := dy - ry;
			FormViews.RoundToGrid(c.view, vx, vy);
			IF ~isSingle & Services.Extends(type, "FormViews.View") THEN	(* general form selection *)
				dst.MarkRect(vx - dm, vy, vx + dp, vy + dst.unit, 0, Ports.invert, show);
				dst.MarkRect(vx, vy - dm, vx + dst.unit, vy + dp, 0, Ports.invert, show);
				INC(vx, w); INC(vy, h);
				dst.MarkRect(vx - dp, vy, vx + dm, vy + dst.unit, 0, Ports.invert, show);
				dst.MarkRect(vx, vy - dp, vx + dst.unit, vy + dm, 0, Ports.invert, show)
			ELSE	(* singleton selection *)
				dst.MarkRect(vx, vy, vx + w, vy + h, 0, Ports.invert, show)
			END
		ELSE	(* cross-hair mark for non-form, non-singleton selections *)
			FormViews.RoundToGrid(c.view, dx, dy);
			dst.MarkRect(dx - dm, dy, dx + dp, dy + dst.unit, 0, Ports.invert, show);
			dst.MarkRect(dx, dy - dm, dx + dst.unit, dy + dp, 0, Ports.invert, show)
		END
	END MarkDropTarget;

	PROCEDURE (c: StdController) TrackMarks (f: Views.Frame; x, y: INTEGER;
																			units, extend, add: BOOLEAN);
		VAR dx, dy, x0, y0, dx0, dy0: INTEGER; isDown: BOOLEAN; h: Views.View; m: SET;

		PROCEDURE InvertRect (f: Views.Frame; x, y, dx, dy: INTEGER);
			VAR l, t, r, b: INTEGER;
		BEGIN
			IF dx >= 0 THEN l := x; r := x + dx ELSE l := x + dx; r := x END;
			IF dy >= 0 THEN t := y; b := y + dy ELSE t := y + dy; b := y END;
			f.MarkRect(l, t, r, b, 0, Ports.dim50, TRUE)
		END InvertRect;

		PROCEDURE SelectArea (c: StdController; l, t, r, b: INTEGER; toggle: BOOLEAN);
			VAR h: INTEGER; rd: FormModels.Reader; v: Views.View; p, q: List; empty: BOOLEAN;
		BEGIN
			IF l > r THEN h := l; l := r; r := h END;
			IF t > b THEN h := t; t := b; b := h END;
			rd := c.form.NewReader(c.reader); c.reader := rd;
			rd.ReadView(v); p := NIL; empty := c.sel = NIL;
			WHILE v # NIL DO
				IF (rd.l < r) & (rd.t < b) & (rd.r > l) & (rd.b > t) THEN
					IF toggle THEN Toggle(c, v)
					ELSIF ~empty THEN c.Select(v)
					ELSE NEW(q); q.next := p; p := q; q.view := v
					END
				END;
				rd.ReadView(v)
			END;
			(* this optimization prevents the appearance of a temporary singleton *)
			IF ~toggle & empty THEN c.SetSelection(p) END
		END SelectArea;

	BEGIN
		dx := 0; dy := 0;	(* vector from (x, y) *)
		InvertRect(f, x, y, dx, dy);
		REPEAT
			f.Input(x0, y0, m, isDown);
			dx0 := x0 - x; dy0 := y0 - y;
			IF (dx0 # dx) OR (dy0 # dy) THEN
				InvertRect(f, x, y, dx, dy);
				dx := dx0; dy := dy0;
				InvertRect(f, x, y, dx, dy)
			END
		UNTIL ~isDown;
		InvertRect(f, x, y, dx, dy);
		c.lastX := x0; c.lastY := y0;
		IF (dx # 0) OR (dy # 0) THEN
			SelectArea(c, x, y, x + dx, y + dy, extend OR add)
		ELSE
			h := c.form.ViewAt(x, y);
			IF h # NIL THEN
				IF extend OR add THEN Toggle(c, h) ELSE c.Select(h) END
			END
		END
	END TrackMarks;

	PROCEDURE (c: StdController) Resize (view: Views.View; l, t, r, b: INTEGER);
	BEGIN
		c.form.Resize(view, l, t, r, b)
	END Resize;

	PROCEDURE (c: StdController) DeleteSelection;
		VAR script: Stores.Operation; h: List;
	BEGIN
		Models.BeginScript(c.form, "#System:Deletion", script);
		h := c.sel; WHILE h # NIL DO c.form.Delete(h.view); h := h.next END;
		Models.EndScript(c.form, script)
	END DeleteSelection;

	PROCEDURE MoveRelativeLocalSel (c: StdController; src, dst: Views.Frame;
															dx, dy: INTEGER; grid: BOOLEAN);
		VAR script: Stores.Operation; sel, h: List; l, t, r, b, newl, newt: INTEGER;
	BEGIN
		IF (dx # 0) OR (dy # 0) THEN
			sel := c.GetSelection();
			Models.BeginScript(c.form, "#System:Moving", script);
			h := sel;
			WHILE h # NIL DO
				h.view.context(FormModels.Context).GetRect(l, t, r, b);
				newl := l + dx; newt := t + dy;
				IF grid THEN FormViews.RoundToGrid(c.view, newl, newt) END;
				c.form.Move(h.view, newl - l, newt - t);
				h := h.next
			END;
			Models.EndScript(c.form, script);
			c.SetSelection(sel)
		END
	END MoveRelativeLocalSel;

	PROCEDURE (c: StdController) MoveLocalSelection (src, dst: Views.Frame; sx, sy,
																									dx, dy: INTEGER);
	BEGIN
		MoveRelativeLocalSel(c, src, dst,  dx - sx, dy - sy, TRUE)
	END MoveLocalSelection;

	PROCEDURE (c: StdController) CopyLocalSelection (src, dst: Views.Frame; sx, sy,
																						dx, dy: INTEGER);
		VAR script: Stores.Operation; q: Views.View; h, s, t: List;
	BEGIN
		dx := dx - sx; dy := dy - sy;
		IF (dx # 0) OR (dy # 0) THEN
			FormViews.RoundToGrid(c.view, dx, dy);
			Models.BeginScript(c.form, "#System:Copying", script);
			h := c.GetSelection(); s := NIL;
			WHILE h # NIL DO
				q := h.view; c.form.Copy(q, dx, dy);
				NEW(t); t.next := s; s := t; t.view := q;
				h := h.next
			END;
			Models.EndScript(c.form, script);
			c.SetSelection(s)
		END
	END CopyLocalSelection;

	PROCEDURE (c: StdController) SelectionCopy (): Containers.Model;
		VAR f: FormModels.Model; rd: FormModels.Reader; wr: FormModels.Writer;
			v: Views.View; dx, dy: INTEGER;

		PROCEDURE GetOffset (p: List; border: INTEGER; OUT dx, dy: INTEGER);
			VAR l, t, vl, vt, vr, vb: INTEGER;
		BEGIN
			IF p # NIL THEN
				l := MAX(INTEGER); t := MAX(INTEGER);
				WHILE p # NIL DO
					p.view.context(FormModels.Context).GetRect(vl, vt, vr, vb);
					IF vl < l THEN l := vl END;
					IF vt < t THEN t := vt END;
					p := p.next
				END;
				dx := l - border; dy := t - border
			ELSE
				dx := 0; dy := 0
			END
		END GetOffset;

	BEGIN
		f := FormModels.CloneOf(c.form);
		GetOffset(c.sel, c.view.Border(), dx, dy);
		rd := c.form.NewReader(NIL); wr := f.NewWriter(NIL);
		rd.ReadView(v);
		WHILE v # NIL DO
			IF c.IsSelected(v) THEN
				wr.WriteView(Views.CopyOf(v, Views.deep), rd.l - dx, rd.t - dy, rd.r - dx, rd.b - dy)
			END;
			rd.ReadView(v)
		END;
		RETURN f
	END SelectionCopy;

	PROCEDURE (c: StdController) NativePaste (m: Models.Model; f: Views.Frame);
		VAR x, y, cw, ch: INTEGER; v: Views.View; rd: FormModels.Reader;
			wr: FormModels.Writer; n, i: INTEGER; script: Stores.Operation;
	BEGIN
		x := c.lastX; y := c.lastY;
		c.view.context.GetSize(cw, ch);
		IF (x <= f.l) OR (x >= f.r) OR (y <= f.t) OR (y >= f.b) THEN
			x := (f.l + f.r) DIV 2; y := (f.r + f.b) DIV 2
		END;
		c.lastX := x; c.lastY := y;
		FormViews.RoundToGrid(c.view, x, y);
		WITH m: FormModels.Model DO
			Models.BeginScript(c.form, "#System:Insertion", script);
			rd := m.NewReader(NIL); wr := c.form.NewWriter(NIL); wr.Set(c.form.Top());
			rd.ReadView(v); n := 0;
			WHILE v # NIL DO
				wr.WriteView(Views.CopyOf(v, Views.shallow), x + rd.l, y + rd.t, x + rd.r, y + rd.b);
				INC(n);
				rd.ReadView(v)
			END;
			Models.EndScript(c.form, script);
			(* n views have been inserted at the top => select them *)
			c.SelectAll(Containers.deselect);
			i := c.form.NofViews() - n; ASSERT(i >= 0, 100);
			rd := c.form.NewReader(rd);
			WHILE i # 0 DO rd.ReadView(v); DEC(i) END;	(* skip old views *)
			WHILE n # 0 DO rd.ReadView(v); c.Select(v); DEC(n) END
		END
	END NativePaste;

	PROCEDURE (c: StdController) ArrowChar (f: Views.Frame; ch: CHAR; units,  select: BOOLEAN);
		VAR d: INTEGER;
	BEGIN
		IF units THEN d := c.view.Grid() ELSE d := f.unit END;
		IF ch = arrowLeft THEN
			MoveRelativeLocalSel(c, f, f, -d, 0, units)
		ELSIF ch = arrowRight THEN
			MoveRelativeLocalSel(c, f, f, +d, 0, units)
		ELSIF ch = arrowUp THEN
			MoveRelativeLocalSel(c, f, f, 0, -d, units)
		ELSIF ch = arrowDown THEN
			MoveRelativeLocalSel(c, f, f, 0, +d, units)
		END
	END ArrowChar;

	PROCEDURE (c: StdController) ControlChar (f: Views.Frame; ch: CHAR);
	BEGIN
		IF (ch = ldel) OR (ch = rdel) THEN c.DeleteSelection END
	END ControlChar;

	PROCEDURE (c: StdController) PasteChar (ch: CHAR);
	END PasteChar;

	PROCEDURE (c: StdController) PasteView (f: Views.Frame; v: Views.View; w, h: INTEGER);
		VAR minW, maxW, minH, maxH, x, y: INTEGER;
	BEGIN
		x := c.lastX; y := c.lastY;
		IF (x <= f.l) OR (x >= f.r) OR (y <= f.t) OR (y >= f.b) THEN
			x := (f.l + f.r) DIV 2; y := (f.t + f.b) DIV 2
		END;
		c.lastX := x; c.lastY := y;
		FormViews.RoundToGrid(c.view, x, y);
		c.form.GetEmbeddingLimits(minW, maxW, minH, maxH);
		Properties.PreferredSize(v, minW, maxW, minH, maxH, minW, minH, w, h);
		c.form.Insert(v, x, y, x + w, y + h); c.Select(v)
	END PasteView;

	PROCEDURE (c: StdController) Drop (src, dst: Views.Frame; sx, sy, dx, dy,
															w, h, rx, ry: INTEGER; v: Views.View; isSingle: BOOLEAN);
		VAR minW, maxW, minH, maxH, l, t, sw, sh: INTEGER;
			s: Views.View; p, q: List;
			m: FormModels.Model; rd: FormModels.Reader; wr: FormModels.Writer;
			script: Stores.Operation;
	BEGIN	(* cf. MarkDropTarget *)
		DEC(dx, rx); DEC(dy, ry);
		IF ~isSingle & (v IS FormViews.View) THEN
			Models.BeginScript(c.form, "#System:Insertion", script);
			m := v(FormViews.View).ThisModel();
			rd := m.NewReader(NIL); wr := c.form.NewWriter(NIL);
			rd.ReadView(s); p := NIL;
			WHILE s # NIL DO
				l := rd.l + dx; t := rd.t + dy; sw := rd.r - rd.l; sh := rd.b - rd.t;
				FormViews.RoundToGrid(c.view, l, t);
				s := Views.CopyOf(s, Views.shallow);
				wr.WriteView(s, l, t, l + sw, t + sh);
				NEW(q); q.next := p; p := q; q.view := s;
				rd.ReadView(s)
			END;
			Models.EndScript(c.form, script); (* this line was added *)
			c.SetSelection(p)
		ELSE
			FormViews.RoundToGrid(c.view, dx, dy);
			c.form.GetEmbeddingLimits(minW, maxW, minH, maxH);
			Properties.PreferredSize(v, minW, maxW, minH, maxH, minW, minH, w, h);
			c.form.Insert(v, dx, dy, dx + w, dy + h); c.Select(v)
		END
	END Drop;


	(* selection *)

	PROCEDURE (c: StdController) HasSelection (): BOOLEAN;
	BEGIN
		RETURN c.sel # NIL
	END HasSelection;

	PROCEDURE (c: StdController) Selectable (): BOOLEAN;
	BEGIN
		RETURN c.form.NofViews() # 0
	END Selectable;

	PROCEDURE (c: StdController) SetSingleton (s: Views.View);
		VAR l: List;
	BEGIN
		c.SetSingleton^(s);
		IF s # NIL THEN NEW(l); l.view := s; c.sel := l ELSE c.sel := NIL END
	END SetSingleton;

	PROCEDURE (c: StdController) SelectAll (select: BOOLEAN);
		VAR s: FormModels.Reader; v: Views.View; l, h: List; msg: MarkMsg;
	BEGIN
		IF select THEN
			ASSERT(~(Containers.noSelection IN c.opts), 20);
			c.SetFocus(NIL);
			s := c.form.NewReader(c.reader); c.reader := s;
			s.Set(NIL); s.ReadView(v);
			IF c.form.NofViews() = 1 THEN
				ASSERT(v # NIL, 100);
				c.SetSingleton(v)
			ELSE
				IF (c.sel # NIL) & (c.sel.next = NIL) THEN c.SetSingleton(NIL) END;
				l := NIL;
				WHILE v # NIL DO
					IF ~c.IsSelected(v) THEN NEW(h); h.next := l; l := h; h.view := v END;
					s.ReadView(v)
				END;
				msg.list := l;
				h := c.sel; WHILE (h # NIL) & (h.next # NIL) DO h := h.next END;
				IF h = NIL THEN c.sel := l ELSE h.next := l END;
				IF msg.list # NIL THEN msg.show := TRUE; Views.Broadcast(c.view, msg) END
			END
		ELSIF c.sel # NIL THEN
			IF c.sel.next = NIL THEN	(* singleton *)
				c.SetSingleton(NIL)
			ELSE
				msg.list := c.sel; c.sel := NIL; 
				msg.show := FALSE; Views.Broadcast(c.view, msg)
			END
		END
	END SelectAll;

	PROCEDURE (c: StdController) InSelection (f: Views.Frame; x, y: INTEGER): BOOLEAN;
		VAR g: Views.Frame;
	BEGIN
		g := Views.FrameAt(f, x, y);
		IF g # NIL THEN
			RETURN c.IsSelected(g.view)
		ELSE
			RETURN FALSE
		END
	END InSelection;

	PROCEDURE (c: StdController) MarkSelection (f: Views.Frame; show: BOOLEAN);
	BEGIN
		IF c.sel = NIL THEN	(* skip *)
		ELSIF c.sel.next = NIL THEN
			Containers.MarkSingleton(c, f, show)
		ELSE
			MarkList(c, f, c.sel, show)
		END
	END MarkSelection;


	(* caret *)

	PROCEDURE (c: StdController) HasCaret (): BOOLEAN;
	BEGIN
		RETURN TRUE
	END HasCaret;

	PROCEDURE (c: StdController) MarkCaret (f: Views.Frame; show: BOOLEAN);
	END MarkCaret;


	(* FormController protocol *)

	PROCEDURE (c: StdController) Select (view: Views.View);
		VAR l, h, sel: List; msg: MarkMsg;
	BEGIN
		ASSERT(view # NIL, 20); ASSERT(view.context.ThisModel() = c.form, 21);
		ASSERT(~(Containers.noSelection IN c.opts), 22);
		l := c.sel; WHILE (l # NIL) & (l.view # view) DO l := l.next END;
		IF l = NIL THEN	(* view is not yet selected *)
			sel := c.sel;
			IF sel = NIL THEN
				c.SetSingleton(view)
			ELSE
				NEW(l); l.view := view;
				IF sel.next = NIL THEN
					c.SetSingleton(NIL); ASSERT(c.sel = NIL, 100);
					l.next := sel; c.sel := l;
					msg.list := l
				ELSE
					l.next := sel; c.sel := l;
					NEW(h); h.view := view;
					msg.list := h
				END;
				msg.show := TRUE; Views.Broadcast(c.view, msg)
			END
		END
	END Select;

	PROCEDURE (c: StdController) Deselect (view: Views.View);
		VAR l, h: List; msg: MarkMsg;
	BEGIN
		ASSERT(view # NIL, 20); ASSERT(view.context.ThisModel() = c.form, 21);
		l := c.sel; h := NIL; WHILE (l # NIL) & (l.view # view) DO h := l; l := l.next END;
		IF l # NIL THEN	(* l is selection node of view, h its predecessor *)
			IF (h = NIL) & (l.next = NIL) THEN	(* singleton *)
				c.SetSingleton(NIL)
			ELSE
				IF h = NIL THEN c.sel := l.next ELSE h.next := l.next END;
				msg.list:= l; l.next := NIL; msg.show := FALSE; Views.Broadcast(c.view, msg);
				IF (c.sel # NIL) & (c.sel.next = NIL) THEN	(* singleton *)
					view := c.sel.view;
					msg.list := c.sel; c.sel := NIL; 
					msg.show := TRUE; Views.Broadcast(c.view, msg);
					c.SetSingleton(view)
				END
			END
		END
	END Deselect;

	PROCEDURE (c: StdController) IsSelected (view: Views.View): BOOLEAN;
		VAR l: List;
	BEGIN
		IF view # NIL THEN
			ASSERT(view.context.ThisModel() = c.form, 20);
			l := c.sel; WHILE (l # NIL) & (l.view # view) DO l := l.next END;
			RETURN l # NIL
		ELSE
			RETURN FALSE
		END
	END IsSelected;

	PROCEDURE (c: StdController) GetSelection (): List;
		VAR l, h, s: List;
	BEGIN
		l := NIL; s := c.sel;
		WHILE s # NIL DO NEW(h); h.next := l; l := h; h.view := s.view; s := s.next END;
		RETURN l
	END GetSelection;

	PROCEDURE (c: StdController) SetSelection (l: List);
		VAR msg: MarkMsg;
	BEGIN
		c.SelectAll(FALSE); ASSERT(c.sel = NIL, 100);
		IF l = NIL THEN	(* skip *)
		ELSIF l.next = NIL THEN
			c.SetSingleton(l.view)
		ELSE
			msg.list := l; c.sel := l; 
			msg.show := TRUE; Views.Broadcast(c.view, msg)
		END
	END SetSelection;


	(* StdDirectory *)

	PROCEDURE (d: StdDirectory) NewController (opts: SET): Controller;
		VAR c: StdController;
	BEGIN
		NEW(c); c.SetOpts(opts); RETURN c
	END NewController;


	(** miscellaneous **)

	PROCEDURE Focus* (): Controller;
		VAR msg: PollFocusMsg; 
	BEGIN
		msg.c := NIL;
		Controllers.Forward(msg);
		RETURN msg.c
	END Focus;
	
	PROCEDURE Insert* (c: Controller; view: Views.View; l, t, r, b: INTEGER);
		VAR w, h: INTEGER;
	BEGIN
		w := r - l; h := b - t;
		FormViews.RoundToGrid(c.view, l, t);
		c.form.Insert(view, l, t, l + w, t + h)
	END Insert;

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

	PROCEDURE Install*;
	BEGIN
		FormViews.SetCtrlDir(dir)
	END Install;

	PROCEDURE Init;
		VAR d: StdDirectory;
	BEGIN
		NEW(d); SetDir(d); stdDir := d
	END Init;

BEGIN
	Init
END FormControllers.
