MODULE StartMenu;

IMPORT
	Strings, XML, KernelLog, Modules, Inputs, UTF8Strings, XMLObjects,
	MainMenu, WM := WMWindowManager, WMComponents, WMStandardComponents, WMProperties, WMMessages, WMEvents,
	WMRectangles;

CONST
	DefaultPlugin = "BlockStartMenu";
	FancyMenuDesc = "Dummy.XML";
	MaxMenus = 16;
	MaxMenuButtons = 10;

TYPE
	String = Strings.String;
	EventListenerInfo = WMEvents.EventListenerInfo;
	Message = WMMessages.Message;

	Popup = OBJECT (WMComponents.FormWindow)

		PROCEDURE FocusLost;
		BEGIN manager.Remove(SELF) END FocusLost;

		PROCEDURE FocusGot;
		BEGIN manager.SetFocus(SELF) END FocusGot;

		PROCEDURE Handle(VAR msg : Message);
		BEGIN
			Handle^(msg);
			IF (msg.msgType = WMMessages.MsgExt) & (msg.ext = closePopupMsg) THEN FocusLost END;
		END Handle;

	END Popup;

	ClosePopupMsg = OBJECT
	END ClosePopupMsg;

	PluginManager = OBJECT
	VAR
		plugin : Plugin;
		currentPluginName : Strings.String; (* { currentPluginName # NIL } *)
		factory : PluginFactory;
		startIndex : LONGINT;

		PROCEDURE & New*;
		BEGIN
			NEW(factory);
			currentPluginName := Strings.NewString(DefaultPlugin);
			SetPlugin(currentPluginName);
			startIndex := 0;
		END New;

		PROCEDURE SetOriginator(extView : ANY);
		VAR name : Strings.String;
		BEGIN {EXCLUSIVE}
			IF manager = NIL THEN manager := WM.GetDefaultManager() END;
			IF plugin # NIL THEN plugin.Close END;
			name := pluginName.Get();
			IF (name # NIL) & (name^ # currentPluginName^) THEN
				currentPluginName := name;
				SetPlugin(name);
			ELSE
				plugin.Open;
			END;
		END SetOriginator;

		PROCEDURE SetPlugin(name : String);
		BEGIN
			IF plugin # NIL THEN
				plugin.Close;
			END;
			plugin := factory.Get(name);
			startIndex := 0;
			plugin.Open;
		END SetPlugin;

		PROCEDURE Close;
		BEGIN
			IF plugin # NIL THEN plugin.Close END
		END Close;

		PROCEDURE ShiftMenuItems(upwards : BOOLEAN);
		BEGIN
			plugin.ReopenMenuItemsShifted(upwards)
		END ShiftMenuItems;

	END PluginManager;

	PluginFactory = OBJECT

		PROCEDURE Get(type : String) : Plugin;
		VAR a : ANY;
		BEGIN
			IF type^ = "FancyStartMenu" THEN
				a := GenFancyStartMenu(); RETURN a(Plugin)
			ELSIF type^ = "BlockStartMenu" THEN
				a := GenBlockStartMenu(); RETURN a(Plugin)
			ELSE (* return default *)
				a := GenBlockStartMenu(); RETURN a(Plugin)
			END
		END Get;

	END PluginFactory;

	Plugin = OBJECT

		PROCEDURE Open;
		BEGIN HALT(311) END Open;

		PROCEDURE Close;
		BEGIN HALT(311) END Close;

		PROCEDURE Refresh(originator : ANY);
		BEGIN HALT(311) END Refresh;

		PROCEDURE ReopenMenuItemsShifted(upwards : BOOLEAN);
		BEGIN HALT(311) END ReopenMenuItemsShifted;

	END Plugin;

	SubMenuOpener* = OBJECT(WMComponents.Component)
	VAR filename : WMProperties.StringProperty;
		eRun* : EventListenerInfo;

		PROCEDURE &Init*;
		BEGIN
			Init^;
			NEW(filename, NIL, Strings.NewString("Filename"), Strings.NewString("")); properties.Add(filename);
			NEW(eRun, Strings.NewString("Run"), Strings.NewString(""), SELF.Run); eventListeners.Add(eRun);
		END Init;

		PROCEDURE Run*(sender, par : ANY); (** Eventhandler *)
		VAR x, y : LONGINT; filename : String;
			rect : WMRectangles.Rectangle;
		BEGIN
			(* synchronize if not synchronized *)
			IF ~IsCallFromSequencer() THEN sequencer.ScheduleEvent(SELF.Run, sender, par)
			ELSE
				(* actual business logic *)
				filename := SELF.filename.Get();
				IF filename # NIL THEN
					rect := sender(WMComponents.VisualComponent).bounds.Get();
					sender(WMComponents.VisualComponent).ToWMCoordinates(0, 0, x, y);
					OpenPopup(x, y, filename^);
				 END
			END
		END Run;
	END SubMenuOpener;

	MenuButtons =  ARRAY MaxMenuButtons  OF WMStandardComponents.Button;
	SMOArr  =  ARRAY MaxMenuButtons  OF SubMenuOpener;

	FancyStartMenu = OBJECT(Plugin)
	VAR startMenu : WMComponents.FormWindow;
		nofLayouts, nofButtons, nofSMOs : LONGINT;
		menuButtons : MenuButtons;
		smos : SMOArr;

		PROCEDURE CountLayouts() : LONGINT;
		VAR i, n : LONGINT; done : BOOLEAN; s : Strings.String;
		BEGIN
			n := 0; done := FALSE;
			WHILE (i < LEN(layoutNames) - 1) & ~done DO
				s := layoutNames[i].Get();
				IF UTF8Strings.Compare(s^, FancyMenuDesc) # UTF8Strings.CmpEqual THEN
					INC(n)
				ELSE
					done := TRUE
				END;
				INC(i)
			END;
			RETURN n
		END CountLayouts;

		PROCEDURE FindSMO(c : XML.Content; VAR smo : SubMenuOpener);
		VAR enum : XMLObjects.Enumerator; found : BOOLEAN; ptr : ANY;
		BEGIN
			smo := NIL;
			IF c IS WMComponents.VisualComponent THEN
				enum := c(WMComponents.VisualComponent).GetContents();
				found := FALSE;
				WHILE enum.HasMoreElements() & ~found DO
					ptr := enum.GetNext();
					IF ptr IS SubMenuOpener THEN
						smo := ptr(SubMenuOpener);
						found := TRUE
					END
				END
			END;
		END FindSMO;

		PROCEDURE FindMenuButtonsSMO(c : XML.Content; VAR menuButtons : MenuButtons; VAR smos: SMOArr; VAR n, m : LONGINT);
		VAR enum : XMLObjects.Enumerator; i, j : LONGINT; ptr : ANY; s : Strings.String;
			b : WMStandardComponents.Button; smo : SubMenuOpener;
		BEGIN
			IF c IS WMComponents.VisualComponent THEN
				enum := c(WMComponents.VisualComponent).GetContents();
				i := 0; j := 0;
				WHILE enum.HasMoreElements() DO
					ptr := enum.GetNext();
					IF ptr IS WMStandardComponents.Button THEN
						b := ptr(WMStandardComponents.Button);
						s := b.caption.Get();
 						IF (s # NIL) & (UTF8Strings.Compare(s^, "") # UTF8Strings.CmpEqual) THEN
							menuButtons[i] := b; INC(i);
							FindSMO(b, smo);
							IF smo # NIL THEN smos[j] := smo; INC(j) END;
						END;
					END;
				END;
				n := i; m := j
			ELSE
				n := 0; m := 0;
			END;
		END FindMenuButtonsSMO;

		PROCEDURE Open;
		VAR c : XML.Content; width, height : LONGINT; view : WM.ViewPort; s : String;
		BEGIN
			nofLayouts := CountLayouts();
			s := layoutNames[pm.startIndex].Get();
			KernelLog.String("loading "); KernelLog.String(s^); KernelLog.Ln;
			c := WMComponents.Load(s^);
			IF (c # NIL) & (c IS WMComponents.VisualComponent) THEN
				FindMenuButtonsSMO(c, menuButtons, smos, nofButtons, nofSMOs);
				width := c(WMComponents.VisualComponent).bounds.GetWidth();
				height := c(WMComponents.VisualComponent).bounds.GetHeight();
				NEW(startMenu, width, height, TRUE);
				startMenu.SetTitle(Strings.NewString("StartMenu"));
				startMenu.pointerThreshold := 10;
				startMenu.DisableUpdate;
				startMenu.SetContent(c);
				startMenu.EnableUpdate;
				startMenu.Invalidate(startMenu.bounds);
				view := WM.GetDefaultView();
				manager := WM.GetDefaultManager();
				manager.Add(ENTIER(view.range.l), ENTIER(view.range.b) - height + 1, startMenu, {WM.FlagHidden});
			ELSE
				KernelLog.String("XML-file not correctly loaded"); KernelLog.Ln
			END
		END Open;

		PROCEDURE Close;
		BEGIN {EXCLUSIVE}
			IF startMenu # NIL THEN startMenu.Close END
		END Close;

		PROCEDURE Refresh(extView: ANY);
		VAR view : WM.ViewPort;
		BEGIN
			IF (extView # NIL) & (extView IS WM.ViewPort) THEN
				view := extView(WM.ViewPort)
			ELSE
				view := WM.GetDefaultView()
			END;
			manager.SetWindowPos(startMenu, ENTIER(view.range.l), ENTIER(view.range.b) - 255);
		END Refresh;

		PROCEDURE ReplaceMenuButtonsSMO(CONST mb, newmb : MenuButtons; CONST smos, newsmos : SMOArr);
		VAR i : LONGINT; s : Strings.String;
		BEGIN
			FOR i := 0 TO nofButtons - 1 DO
				s := newmb[i].caption.Get();
				mb[i].caption.Set(s);
				s := newsmos[i].filename.Get();
				smos[i].filename.Set(s);
			END;
		END ReplaceMenuButtonsSMO;

		PROCEDURE ReopenMenuItemsShifted(upwards : BOOLEAN);
		VAR old, i : LONGINT; s : String; c : XML.Content;
			newMButtons : MenuButtons;
			newsmos : SMOArr;
			n, m : LONGINT;
		BEGIN
			old := pm.startIndex;
			IF upwards THEN
				pm.startIndex := (pm.startIndex + 1) MOD nofLayouts
			ELSE
				pm.startIndex := (pm.startIndex - 1) MOD nofLayouts
			END;
			IF old # pm.startIndex THEN
				s := layoutNames[pm.startIndex].Get();
				c := WMComponents.Load(s^);
				IF (c # NIL) & (c IS WMComponents.VisualComponent) THEN
					FindMenuButtonsSMO(c, newMButtons, newsmos, n, m);
					IF (nofButtons = nofSMOs) & (nofButtons = n) & (nofSMOs = m) THEN
						ReplaceMenuButtonsSMO(menuButtons, newMButtons, smos, newsmos);
						FOR i := 0 TO nofButtons - 1 DO
							menuButtons[i].Invalidate;
						END;
					ELSE
						KernelLog.String("layout "); KernelLog.String(s^); KernelLog.String(" does not match."); KernelLog.Ln
					END
				ELSE
					KernelLog.String("XML-file not correctly loaded"); KernelLog.Ln
				END
			END
		END ReopenMenuItemsShifted;

	END FancyStartMenu;

	BlockStartMenu = OBJECT(Plugin)
	VAR startMenu : MainMenu.Window;

		PROCEDURE Open;
		BEGIN
			NEW(startMenu);
			startMenu.SetOriginator(NIL);
			startMenu.LoadPages
		END Open;

		PROCEDURE Close;
		BEGIN
			IF startMenu # NIL THEN startMenu.Close END
		END Close;

		PROCEDURE Refresh(extView: ANY);
		BEGIN
			startMenu.SetOriginator(extView);
			startMenu.LoadPages
		END Refresh;

		PROCEDURE ReopenMenuItemsShifted(upwards : BOOLEAN);
		END ReopenMenuItemsShifted;

	END BlockStartMenu;

	(* the starter decouples the sensitive callback from the WindowManager. *)
	Starter = OBJECT
	VAR originator : ANY;

		PROCEDURE &Init*(o : ANY);
		BEGIN
			originator := o
		END Init;

	BEGIN {ACTIVE}
		pm.SetOriginator(originator)
	END Starter;


VAR
	stringPrototype, pluginName : WMProperties.StringProperty;
	layoutNames : ARRAY MaxMenus OF WMProperties.StringProperty;
	manager : WM.WindowManager;
	pm : PluginManager;
	p : Popup;
	closePopupMsg : ClosePopupMsg;

	PROCEDURE OpenPopup*(x, y : LONGINT; CONST filename : ARRAY OF CHAR);
	VAR m : WM.WindowManager;
		 c : XML.Content;
		width, height : LONGINT;
	BEGIN
		c := WMComponents.Load(filename);
		IF (c # NIL) & (c IS WMComponents.VisualComponent) THEN
			width := c(WMComponents.VisualComponent).bounds.GetWidth();
			height := c(WMComponents.VisualComponent).bounds.GetHeight();
			IF width <= 0 THEN width := 10 END; IF height <= 0 THEN height := 10 END;
			NEW(p, width, height, TRUE);
			p.SetContent(c);
			m := WM.GetDefaultManager(); m.Add(x, y-height, p, {WM.FlagHidden}); m.SetFocus(p)
		ELSE
			KernelLog.String(filename); KernelLog.String(" not correctly loaded"); KernelLog.Ln
		END
	END OpenPopup;

	PROCEDURE ClosePopup*;
	VAR msg : WMMessages.Message; manager : WM.WindowManager;
	BEGIN
		msg.msgType := WMMessages.MsgExt; msg.ext := closePopupMsg;
		manager := WM.GetDefaultManager();
		manager.Broadcast(msg);
	END ClosePopup;

	PROCEDURE GenSubMenuOpener*() : XML.Element;
	VAR smo : SubMenuOpener;
	BEGIN
		NEW(smo); RETURN smo
	END GenSubMenuOpener;

	PROCEDURE Open*;
	BEGIN
		pm.SetPlugin(pluginName.Get());
	END Open;

	(* load start menu with buttons shifted to the right *)
	PROCEDURE ShiftMenuItemsRight*;
	BEGIN
		pm.ShiftMenuItems(TRUE);
	END ShiftMenuItemsRight;

	(* load start menu with buttons shifted to the left *)
	PROCEDURE ShiftMenuItemsLeft*;
	BEGIN
		pm.ShiftMenuItems(FALSE);
	END ShiftMenuItemsLeft;

	(* This procedure is directly called by the window manager. It must be safe. *)
	PROCEDURE MessagePreview(VAR m : WMMessages.Message; VAR discard : BOOLEAN);
	VAR starter : Starter;
	BEGIN
		IF m.msgType = WMMessages.MsgKey THEN
			IF (m.y = 0FF1BH) & ((m.flags * Inputs.Ctrl # {}) OR (m.flags * Inputs.Meta # {})) THEN
				NEW(starter, m.originator); discard := TRUE
			END;
		ELSIF (m.msgType = WMMessages.MsgExt) & (m.ext = WMComponents.componentStyleMsg) THEN
			NEW(starter, m.originator);
		END
	END MessagePreview;

	PROCEDURE GenFancyStartMenu() : Plugin;
	VAR menu : FancyStartMenu;
	BEGIN NEW(menu); RETURN menu
	END GenFancyStartMenu;

	PROCEDURE GenBlockStartMenu() : Plugin;
	VAR menu : BlockStartMenu;
	BEGIN NEW(menu); RETURN menu
	END GenBlockStartMenu;

	PROCEDURE InitPrototypes;
	VAR plStartMenu : WMProperties.PropertyList;
		s0, s1 : ARRAY 128 OF CHAR;
		i : LONGINT;
	BEGIN
		NEW(plStartMenu); 	WMComponents.propertyListList.Add("StartMenu", plStartMenu);

		NEW(stringPrototype, NIL, Strings.NewString("Plugin"), Strings.NewString("Plug-in-object that creates start-menu and determines its properties"));
		stringPrototype.Set(Strings.NewString(DefaultPlugin));
		NEW(pluginName, stringPrototype, NIL, NIL); plStartMenu.Add(pluginName);

		FOR i := 0 TO LEN(layoutNames) - 1 DO
			Strings.IntToStr(i, s0);
			COPY("Layout", s1);
			Strings.Append(s1, s0);
			NEW(stringPrototype, NIL, Strings.NewString(s1), Strings.NewString("XML-file that determins content and layout of the fancy start-menu"));
			stringPrototype.Set(Strings.NewString(FancyMenuDesc));
			NEW(layoutNames[i], stringPrototype, NIL, NIL); plStartMenu.Add(layoutNames[i]);
		END;
	END InitPrototypes;

	PROCEDURE Cleanup;
	BEGIN
		IF pm # NIL THEN pm.Close END;
		manager.RemoveMessagePreview(MessagePreview)
	END Cleanup;

	PROCEDURE Fancy*;
	BEGIN
		pluginName.Set(Strings.NewString("FancyStartMenu"));
	END Fancy;

	PROCEDURE Block*;
	BEGIN
		pluginName.Set(Strings.NewString("BlockStartMenu"));
	END Block;

BEGIN
	NEW(pm);
	NEW(closePopupMsg);
	InitPrototypes;
	Modules.InstallTermHandler(Cleanup);
	manager := WM.GetDefaultManager();
	manager.InstallMessagePreview(MessagePreview);
END StartMenu.