MODULE ObxTwins;
(**
	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 Ports, Services, Stores, Models, Views, Controllers, Properties, TextViews;

	CONST
		minVersion = 3; maxVersion = 3;	(* old versions 0 .. 2 of ObxTwin views cannot be read anymore *)
		border = Ports.mm;

	TYPE
		Identity = POINTER TO RECORD (Models.Model) END;	(* dummy model *)

		Context = POINTER TO RECORD (Models.Context)
			base: View;			(* container view *)
			view: Views.View;	(* contained view *)
			l, t, r, b: INTEGER	(* cached bounding box of contained view *)
		END;

		View = POINTER TO RECORD (Views.View)
			top, bottom: Context;
			ident: Identity;	(* temporary dummy model *)
			focus: Context	(* current focus; either top or bottom *)
		END;


	(* Context *)

	PROCEDURE (c: Context) ThisModel (): Models.Model;
	BEGIN
		RETURN NIL	(* don't give the embedded views information about the dummy twin model *)
	END ThisModel;

	PROCEDURE (c: Context) GetSize (OUT w, h: INTEGER);
	BEGIN
		w := c.r - c.l;
		h := c.b - c.t
	END GetSize;

	PROCEDURE (c: Context) MakeVisible (l, t, r, b: INTEGER);
		VAR w, h, sep: INTEGER;
	BEGIN
		IF c.base.top = c THEN	(* top view *)
			c.base.context.MakeVisible(l + border, t + border, r + border, b + border)
		ELSE	(* bottom view *)
			c.base.context.GetSize(w, h); sep := h DIV 3;
			c.base.context.MakeVisible(l + border, t + sep + border, r + border, b + sep + border)
		END
	END MakeVisible;

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

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

	PROCEDURE NewContext (v: Views.View; base: View): Context;
		VAR c: Context;
	BEGIN
		NEW(c);
		c.view := v; c.base := base;
		v.InitContext(c); Stores.Join(v, base);
		RETURN c
	END NewContext;

	PROCEDURE CopyOf (source: Context; shallow: BOOLEAN; base: View): Context;
		VAR v: Views.View;
	BEGIN
		v := Views.CopyOf(source.view,  shallow );
		RETURN NewContext(v, base)
	END CopyOf;


	(* View *)

	PROCEDURE RecalcLayout (v: View);
		VAR w, h, sep: INTEGER; c: Context;
	BEGIN
		v.context.GetSize(w, h);
		sep := h DIV 3;	(* separate the two views at 1/3 of the container's height *)
		c := v.top; c.l := border; c.t := border; c.r := w - border; c.b := sep - border;
		c := v.bottom; c.l := border; c.t := sep + border; c.r := w - border; c.b := h - border
	END RecalcLayout;

	PROCEDURE SetFocus (v: Views.View; x, y: INTEGER): BOOLEAN;
		VAR p: Properties.FocusPref;
	BEGIN	(* determine whether v should be focused when the mouse is clicked at (x, y) in v *)
		p.hotFocus := FALSE;
		p.atLocation := TRUE; p.x := x; p.y := y;
		p.setFocus := FALSE;
		Views.HandlePropMsg(v, p);
		RETURN p.setFocus
	END SetFocus;

	PROCEDURE (v: View) CopyFromModelView (source: Views.View; model: Models.Model);
		VAR shallow: BOOLEAN;
	BEGIN
		WITH source: View DO
			shallow := model = source.ident;	(* shallow copy if both views share the same model *)
			Stores.Join(v, model);
			v.top := CopyOf(source.top, shallow, v);
			v.bottom := CopyOf(source.bottom, shallow, v);
			v.ident := model(Identity);
			v.focus := v.bottom
		END
	END CopyFromModelView;

	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER; h: Views.View;
	BEGIN
		rd.ReadVersion(minVersion, maxVersion, version);
		IF ~rd.cancelled THEN
			Views.ReadView(rd, h); v.top := NewContext(h, v);
			Views.ReadView(rd, h); v.bottom := NewContext(h, v);
			NEW(v.ident); Stores.Join(v, v.ident);
			v.focus := v.bottom
		END
	END Internalize;

	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(maxVersion);
		Views.WriteView(wr, v.top.view);
		Views.WriteView(wr, v.bottom.view)
	END Externalize;

	PROCEDURE (v: View) Neutralize;
	BEGIN
		v.focus.view.Neutralize
	END Neutralize;

	PROCEDURE (v: View) ThisModel (): Models.Model;
	BEGIN
		RETURN v.ident
	END ThisModel;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
		VAR d: INTEGER; c: Context;
	BEGIN
		RecalcLayout(v);
		(* install the subframes for the subviews *)
		c := v.top; Views.InstallFrame(f, c.view, c.l, c.t, 0, v.focus = c);
		c := v.bottom; Views.InstallFrame(f, c.view, c.l, c.t, 1, v.focus = c);
		(* draw frame around the subviews *)
		d := 2 * f.dot;
		c := v.top;
		IF (c.t - d < c.b + d) & (c.l - d < c.r + d) THEN f.DrawRect(c.l - d, c.t - d, c.r + d, c.b + d, f.dot, Ports.black) END;
		c := v.bottom; 
		IF (c.t - d < c.b + d) & (c.l - d < c.r + d) THEN f.DrawRect(c.l - d, c.t - d, c.r + d, c.b + d, f.dot, Ports.black) END
	END Restore;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Views.CtrlMessage;
															VAR focus: Views.View);
		VAR g: Views.Frame; newFocus: Context; mMsg: Controllers.MarkMsg;
			w, h, sep: INTEGER;
	BEGIN
		WITH msg: Controllers.CursorMessage DO
			v.context.GetSize(w, h);
			sep := h DIV 3;
			IF msg.y >= sep THEN newFocus := v.bottom ELSE newFocus := v.top END;
			focus := newFocus.view;
			IF (newFocus # v.focus) & ((msg IS Controllers.TrackMsg) OR (msg IS Controllers.DropMsg)) &
				SetFocus(focus, msg.x, msg.y) THEN
				(* remove marks in old focus *)
				mMsg.show := FALSE; mMsg.focus := TRUE;
				g := Views.ThisFrame(f, v.focus.view); IF g # NIL THEN Views.ForwardCtrlMsg(g, mMsg) END;
				v.focus := newFocus;	(* set new focus *)
				(* set marks in new focus *)
				mMsg.show := TRUE; mMsg.focus := TRUE;
				g := Views.ThisFrame(f, v.focus.view); IF g # NIL THEN Views.ForwardCtrlMsg(g, mMsg) END
			END
		(* the following scrolling-oriented messages are always sent to bottom view, independent of focus *)
		| msg: Controllers.PollSectionMsg DO
			g := Views.ThisFrame(f, v.bottom.view); IF g # NIL THEN Views.ForwardCtrlMsg(g, msg) END;
			IF msg.vertical & ~msg.done THEN
				msg.valid := FALSE; msg.done := TRUE	(* disable default-scrolling *)
			END
		| msg: Controllers.ScrollMsg DO
			g := Views.ThisFrame(f, v.bottom.view); IF g # NIL THEN Views.ForwardCtrlMsg(g, msg) END;
			IF msg.vertical THEN msg.done := TRUE END
		| msg: Controllers.PageMsg DO
			focus := v.bottom.view
		ELSE	(* all other messages are sent to the focus, however *)
			focus := v.focus.view
		END
		(* the assignment to focus signals that the view v wants to forward the message to the
		corresponding embedded view *)
	END HandleCtrlMsg;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Views.PropMessage);
		CONST defW = 80 * Ports.mm; defH = 60 * Ports.mm;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF msg.w = Views.undefined THEN msg.w := defW END;
			IF msg.h = Views.undefined THEN msg.h := defH END
		| msg: Properties.ResizePref DO
			msg.verFitToWin := TRUE	(* make view always as high as window *)
		ELSE
			Views.HandlePropMsg(v.bottom.view, msg)
		END
	END HandlePropMsg;

	PROCEDURE (v: View) ConsiderFocusRequestBy (view: Views.View);
	BEGIN
		IF view = v.top.view THEN
			v.focus := v.top
		ELSIF view = v.bottom.view THEN
			v.focus := v.bottom
		ELSE
			HALT(20)
		END
	END ConsiderFocusRequestBy;

	PROCEDURE NewTwin* (top, bottom: Views.View): Views.View;
		VAR v: View;
	BEGIN
		NEW(v);
		v.top := NewContext(top, v); v.bottom := NewContext(bottom, v);
		NEW(v.ident); Stores.Join(v, v.ident); v.focus := v.bottom;
		RETURN v
	END NewTwin;

	(* example twin view with two embedded text views *)

	PROCEDURE New* (): Views.View;
	BEGIN
		RETURN NewTwin(TextViews.dir.New(NIL), TextViews.dir.New(NIL))
	END New;

	PROCEDURE Deposit*;
	BEGIN
		Views.Deposit(New())
	END Deposit;

END ObxTwins.
