/*
	File:		ModalDialogMenuExtensions.c

	Contains:	C language portions of Modal Dialog Menu Access Facillity

	Written by:	Darin Adler

	Copyright:	© 1990-1992 by Apple Computer, Inc., all rights reserved.

	Change History (most recent first):

		<23>	 11/3/92	DTY		Strip out unnecessary includes.
		<22>	 6/10/92	JSM		Moved this file to ModalDialogMenuExtensions.c from
									ModalDialogMenuPatches.c since itŐs used by both the ROM and
									System, keeping all the old revisions.
		<21>	 5/29/92	DCL		Included TextUtils.p. EqualString was moved for the New Inside
									Mac.
		<20>	 3/23/92	JSM		OSEvents.h is obsolete, use Events.h.
		<19>	 2/20/91	KSM		gbm,#KSM-3: Don't check spareflag of a NIL window.
		<18>	 2/10/91	KSM		DBA,#80950: Fix to handle graying of Applications Menu properly.
		<17>	  1/5/91	fjs		(KSM) do not allow non-printing function keys
		<16>	12/18/90	KSM		<rlc> Fix bug where DialogMgr incorrectly sets up the EditField
									of the dialog record causing the Edit Menu to be incorrectly
									enabled.
		<15>	 12/3/90	RLC		<ksm> Change calls to IsWindowModal() to call the newer
									GetWindowModalClass instead.
		<14>	11/26/90	KSM		(dba+dh)Remove extraneous ifdefs around includes.  Also only
									analyze in foregnd.
		<13>	11/14/90	JL		Changing InvalidMenuBar back to InvalMenuBar because Invalid is
									misleading. All InvalÉ calls should be expanded to Invalidate if
									they are expanded at all.
		<12>	11/13/90	KSM		<dba>Fix bug where we checked spareFlag directly instead of
									calling IsWindowModal. IsWindowModal handles nil correctly, and
									is smarter about variation 1 of WDEF 0. Change
									SetSaveMenuEnableState to save the menu state, even if we are
									only setting up the system menus and not disabling the
									application menus. This fixes a bug where the system menus get
									permanently disabled.
		<11>	 11/6/90	KSM		<rlc>Revisit role of SetSaveMenuEnableState and ActiveWindowNeedsHelp
									to handle the additional case of apps using modal window kinds, but
									never calling _ModalDialog.
		<10>	10/29/90	KSM		<dba>Remove some dead code.  Update to look for the modal dialog bit
									before handling menus.  Enable edit menu iff edit text is active.
		 <9>	10/15/90	JSM		Really change InvalMenuBar to InvalidMenuBar.
		 <8>	10/15/90	JL		Changing InvalMenuBar to InvalidMenuBar.
		 <7>	 7/31/90	KSM		Fix edit menu analysis state handling.
		 <6>	 7/13/90	RLC		Fix bug in testing menuselect result, hiword will be zero, but
									entire longword might not be.
		 <5>	 7/13/90	KSM		Fix edit menu enabling check. Remove extra InvalMenuBar call in
									EnableTheEditMenu.
		 <4>	 7/10/90	dba		get rid of C warnings
		 <3>	  7/2/90	KSM		Add edit menu handling.
		 <2>	  6/8/90	KSM		Update to use new IsFrontWindow modal calls.
		 <1>	  6/1/90	KSM		First checked in.

	To Do:
		add a MENU resource to indicate names of Edit menu items
		find items from the Edit menu to enable them
		use knowledge about Dialog Manager to know if Edit menu items should be enabled
			note that we should *only* enable edit menus if the frontmost window is a
			windowKind == dialogKind and there is an appropriate active editText field
			(a visible TextEdit record that is currently active)
		enable help menu
		pass Cut, Copy, Paste, Clear, Select All through dialog filter as command keys
		make a separate patch to Dialog Manager to make it handle command keys
		use a global flag to prevent from saving the menu state twice
		??? save menu state in a global instead of returning it as a function result
		find all the *** and get rid of them
*/

/*
	Modal Dialog Menu Access
	Theory of Operation (Volume 1, Chapter 1, section 1)

	The state of the world is Ňnormal.Ó
	STATE TRANSITION TABLE

	State	Event		Event		Event		MenuSelect	FlushEvents		SysBeep
			=MouseDn	=NullEvt	=Other		(from App)	(from anyone)	(from App)
	-----	---------	---------	---------	-----------	-------------	-------------
		0		1			0			0			0			0				0
		1		0			3			0			0			0				2
		2		0			3			0			0			0				2
		3		0			0			0			0			0				0

	What this means is:
	State 0: This is the normal state of this code.  If we see a mousedown in the menubar, we might
	want to handle the menuselect call, but the app might already be doing this.  So we save this
	event and move to state 1.

	State 1: We have seen a mousedown, but we are waiting to see if the application
	is handling menus during modal dialogs.  If we see the app call menuselect, we know it is
	handling menus and return to state 0.  If we see a flushevents call, we always go to state 0
	and forget any event information we saved coming to this state.  If we see a SysBeep, it might
	be that that ModalDialog filter proc is complaining we clicked out of the dialog, or we may just
	have gotten an extra beep. The SysBeep is "saved" and we go to state 2.  A null event assumes
	that the app had the change to handle the menus, but did not - so we assume it won't, go to
	state 3.

	State 2: We saw a sysbeep.  If we see a null event, we have "eaten" the sysbeep and go to state
	3.  If we see another SysBeep, we Beep (for the one that got us to this state) and save off the
	new SysBeep and stay in this state.

	State 3: We've done our normal code and go back to state 0.  This is implemented in the opposite
	order, in that, we set the state to zero as we proceed on into the regular code.

	------------------------------------------
	In general, new patches do the following:

	FlushEvents:	go to state zero
	MenuSelect:		go to state zero (if the app called it)
	SysBeep:		if state 1, don't beep and go to state 2
					if state 2, beep and stay in state 2
	FilterEvent		if state 0, mousedown goes to state 1
					if state 1, null event goes to state 3, otherwise state 0
					if state 2, null event goes to state 3, otherwise state 0
					if state 3, we'll be on our way to state 0 anyway.


*/

#include <Dialogs.h>
#include <Events.h>
#include <Memory.h>
#include <Menus.h>
#include <Processes.h>
#include <TextUtils.h>
#include <Windows.h>

#include <DialogsPriv.h>
#include <MenuMgrPriv.h>

#define kFunctionKey	0x10
#define kCut			0x78
#define kCopy			0x63
#define kPaste			0x76

// Menu Mgr. data structures

typedef struct
	{
	short		lastMenu;
	short		lastRight;
	short		reserved;
	}
MenuListHeader;

typedef struct
	{
	MenuHandle	handle;
	short		left;
	}
MenuListEntry;


// menu bar iteration

MenuListHeader** GetMenuList(void);
MenuListEntry* GetNextMenu(short* offset);


// other menu operations

Boolean MenuBarEmpty(void);
Boolean PtInMenuBar(Point);
Boolean IsThisASystemMenu(short menuID);
Boolean IsModal(WindowPtr);

MenuHandle FindMenuByTitle(const unsigned char* title);
MenuHandle FindAppleMenu(void);
long GetMenuFlags(MenuHandle);

void DisableAppMenus(void);
void EnableTheEditMenu(void);


// routines to save enable state of all menus in the menu bar

Handle SaveMenusEnableState(void);


// globals (implemented in assembly) to keep our state

enum
	{
	kApplicationHandlesMenus,
	kHandleSystemMenusDisabled,
	kHandleMenusEditMenuDisabled,
	kHandleMenusEditMenuEnabled,
	};
typedef short TAnalyzedWindowState;

typedef struct
	{
	MenuHandle	handle;
	long		savedEnableFlags;
	}
MenuStateEntry;

pascal WindowPtr GetAnalyzedWindow(void);
pascal void SetAnalyzedWindow(WindowPtr);

pascal TAnalyzedWindowState GetAnalyzedWindowState(void);
pascal void SetAnalyzedWindowState(TAnalyzedWindowState);

pascal Handle GetSavedMenuState(void);
pascal void SetSavedMenuState(Handle);


// call to enable all menus for us

pascal void ModalDialogMenuSetup(Boolean nowModal)
	= {0xAA67};


// heuristics used by the patches

pascal TAnalyzedWindowState ActiveWindowNeedsHelp(void);


// routines called by assembly language

pascal Handle SetSaveMenusEnableState(TAnalyzedWindowState state); 	// save state and enable selected menus
pascal void RestoreMenusEnableState(Handle state);
pascal void FilterEvent(EventRecord*);


// implementations

MenuListHeader** GetMenuList(void)
	{
	long result = (* (long*) 0xA1C);
	if (result & 1)
		return 0;
	else
		return (MenuListHeader**) result;
	}

MenuListEntry* GetNextMenu(short* offset)
	{
	MenuListHeader** list = GetMenuList();

	if (list == 0)
		return 0;
	*offset += sizeof(MenuListEntry);
	if (*offset > (**list).lastMenu)
		return 0;
	else
		return (MenuListEntry*) (((char*) *list) + *offset);
	}

Boolean MenuBarEmpty(void)
	{
	short offset = 0;
	return GetNextMenu(&offset) == 0;	// no next menu means no menus in bar
	}


Handle SaveMenusEnableState(void)
	{
	short offset;
	MenuListEntry* mlEntry;
	Handle handle;
	offset = 0;

	// First check to see if we already had saved the state (reentrant check)
	if (GetSavedMenuState())
		return 0;

	// make a handle for saved enable states

	handle = NewHandle(0);
	if (handle == 0)
		return 0;

	// traverse the menus, saving state

	while (mlEntry = GetNextMenu(&offset))
		{
		MenuStateEntry saved;
		MenuHandle menu;

		// make the saved state, and append it to the handle

		menu = mlEntry->handle;
		saved.handle = menu;
		saved.savedEnableFlags = (**menu).enableFlags;
		if (PtrAndHand((Ptr) &saved, handle, sizeof(MenuStateEntry)) != noErr)
			{
			DisposHandle(handle);
			return 0;
			}
		}

	// Be sure to save the state in our global
	SetSavedMenuState(handle);

	return handle;
	}

pascal void RestoreMenusEnableState(Handle state)
	{
	short offset = 0;
	MenuListEntry* mlEntry;

	// traverse the menus, restoring all the enable flags

	while (mlEntry = GetNextMenu(&offset))
		{
		MenuHandle handle = mlEntry->handle;
		MenuStateEntry* stateEntry = (MenuStateEntry*) *state;
		MenuStateEntry* afterLast = (MenuStateEntry*) (((char*) stateEntry) + GetHandleSize(state));

		// search through the state handle for saved state for this menu

		while (stateEntry < afterLast)
			{
			if (stateEntry->handle == handle)
				{
				// found it, jam it in enable flags and continue (unless system menu)

				if (!IsThisASystemMenu((**handle).menuID))
					(**handle).enableFlags = stateEntry->savedEnableFlags;
				break;
				}
			stateEntry++;
			}
		}

	DisposHandle(state);

	// Be sure to clean up our global
	SetSavedMenuState(0);
	ModalDialogMenuSetup(false);		// Tell system menus to return to normal state
	}


void DisableAppMenus(void)
	{
	short offset = 0;
	MenuListEntry* mlEntry;

	// traverse the menus, setting all the enable flags to zero (unless system menu)
	while (mlEntry = GetNextMenu(&offset))
		{
		if (!IsThisASystemMenu((**(mlEntry->handle)).menuID))
			(**(mlEntry->handle)).enableFlags = 0;
		}
	}


Boolean ThereIsAnActiveEditTextFieldInThisWindow(WindowPtr active)
	{
	if (((WindowPeek)active)->windowKind == dialogKind)
		{
		// For some reason, NewDialog does not set up the editField in the DialogRecord.
		// The next DialogMgr call does though, hence this peculiar call to GetDItem.
		// Additionally, call SelIText on a stattext field points editField to that
		// statText field, which belies the name editField.
		short kind;
		Handle h;
		Rect r;
		register short editField = ((DialogPeek)active)->editField;		// Get the edit field locally
		if (editField < 0) editField = 0;								// Force into range
		GetDItem(active, ++editField, &kind, &h, &r);					// Inc. editField before use
		// Check for editField erroneously pointing to a statText field (MPW 3.2 case)
		if ((editField < 0) || ((kind & 0x7f) == editText))
			if (((DialogPeek)active)->editField >= 0)
				if ((**(((DialogPeek)active)->textH)).active != 0)
					return true;
		}
	return false;
	}

pascal Handle SetSaveMenusEnableState(TAnalyzedWindowState state)
	{
	/*
		3 cases:	regular window (do nothing)
					modal window w/o _ModalDialog call (setup system menus)
					_ModalDialog called (setup all menus)
	*/
	Handle result = nil;

	if (state != kApplicationHandlesMenus)
		{
		// These 2 calls need to be in the opposite order in restore
		ModalDialogMenuSetup(true);		// System menus always need to be set up	<18>
		result = SaveMenusEnableState();
		if (result)
			{
			// we have some kind of modal situation; check if we should setup all menus
			if (state != kHandleSystemMenusDisabled)
				{
				// kHandleMenusEditMenuDisabled or kHandleMenusEditMenuEnabled: setup all the menus
				DisableAppMenus();
				if (state == kHandleMenusEditMenuEnabled)
					EnableTheEditMenu();
				}
			}
		}

	return result;
	}

void EnableTheEditMenu()
	{
	MenuListEntry* mlEntry;
	short offset = 0;

	while (mlEntry = GetNextMenu(&offset))
		{
		// Walk across the menus and look for Cut, Copy, and Paste (and maybe Undo) ...
		MenuHandle menu = mlEntry->handle;
		short items = CountMItems(menu);
		short i = (short) ((items - 2) + 1);		// Skip the last 2 items, since we are looking for a cluster of 3
		if (i > (3 + 1)) i = (3 + 1);				// Look at the first 3 items of each menu at most
		while (--i > 0)								// Start at the bottom of the menu, since cmd-X is usually item 3
			{
			short cmdChar;
			GetItemCmd(menu, i, &cmdChar);
			if (cmdChar == 'X')
				{
				long flagsToSet = (7 << i) + 1;		// Compute flagsToSet now before incrementing i
				GetItemCmd(menu, ++i, &cmdChar);	// Check the next item (should be Copy)
				if (cmdChar == 'C')					// We found cmd-C, find Paste
					{
					GetItemCmd(menu, ++i, &cmdChar);
					if (cmdChar == 'V')				// We found cmd-V! This is Edit menu
						(**menu).enableFlags |= flagsToSet;		// So, set the flags
					}
				 return;							// Once we find cmd-X, donŐt look any more
				}
			}
		}
	}

MenuHandle FindMenuByTitle(const unsigned char* title)
	{
	short offset = 0;
	MenuListEntry* mlEntry;

	// traverse the menus, searching for the proper menu item

	while (mlEntry = GetNextMenu(&offset))
		{
		MenuHandle menu = mlEntry->handle;
		if (EqualString((**menu).menuData, title, false, false))
			return menu;
		}
	return 0;
	}

pascal TAnalyzedWindowState ActiveWindowNeedsHelp(void)
	{
	WindowPtr active;
	TAnalyzedWindowState oldState;
	TAnalyzedWindowState newState;
	ProcessSerialNumber ourPSN;
	ProcessSerialNumber currentPSN;
	Boolean inFront;

	if (MenuBarEmpty())		// short-circuit when there are no menus
		return false;

	// short circuit when current application is not frontmost since its menubar is not visible
	inFront = false;
	(void) GetFrontProcess(&ourPSN);
	currentPSN.highLongOfPSN = 0;
	currentPSN.lowLongOfPSN = kCurrentProcess;
	(void) SameProcess(&currentPSN, &ourPSN, &inFront);
	if (inFront == false)
		return false;

	active = FrontWindow();
	oldState = GetAnalyzedWindowState();

	if (active != GetAnalyzedWindow())
		{
		Boolean	calledModalDialog;

		// latch onto the active window

		SetAnalyzedWindow(active);
		SetAnalyzedWindowState(kApplicationHandlesMenus);
		if (active)
			{

			// <18> <KSM The rest of the code in this block is change>
			// Any window that is a modal dialog (WDEF 0 and variant=dBoxProc) window,
			// or was the the active window at the time _ModalDialog was called
			// requires us to disallow switching out (i.e., we must disable the Process menu).
			// IsModal() OR _ModalDialog implies set up System Menus for a modal dialog
			// But ONLY if _ModalDialog was called for this window should we check to see
			// if we should handle the menus on the application's behalf.
	
			// Find out if we called ModalDialog on this window
			calledModalDialog = ((((WindowPeek)active)->spareFlag & systemHandlesMenusMask) != 0);
	
			// Should we disable System Menus (either case)?
			if (IsModal(active) || calledModalDialog) 
				SetAnalyzedWindowState(kHandleSystemMenusDisabled);	// disable System Menus
	
			// Should we handle menus for the application (only if ModalDialog was called)?
			if (calledModalDialog)
				{
				// if application handles menus, Apple menu
				// (or at least About item) will be disabled
	
				MenuHandle appleMenu = FindAppleMenu();
				long appleMenuFlags = GetMenuFlags(appleMenu);
				if (appleMenu && (appleMenuFlags & 1) && (appleMenuFlags & 2))
					{
					// the application doesnŐt handle menus, we will
					if (ThereIsAnActiveEditTextFieldInThisWindow(active))
						SetAnalyzedWindowState(kHandleMenusEditMenuEnabled);
					else
						SetAnalyzedWindowState(kHandleMenusEditMenuDisabled);
					// since we allow choices from the menu bar, unhilite the menu to make that obvious
					HiliteMenu(0);
					}
				}
			}
		}

	newState = GetAnalyzedWindowState();

	// if the state has changed, menu titles may have dimmed/undimmed so redraw them

	if (oldState != newState)
		InvalMenuBar();

	// return true if we are handling menus

	return newState;
	}

pascal void FilterEvent(EventRecord* event)
	{
	if ((event->what == nullEvent) || (event->what == updateEvt))
		(void) ActiveWindowNeedsHelp();
	else if (event->what == mouseDown)
		{
		if (PtInMenuBar(event->where))
			{
			TAnalyzedWindowState state = ActiveWindowNeedsHelp();
			if ((state == kHandleMenusEditMenuEnabled) || (state == kHandleMenusEditMenuDisabled))
				{
				long result;
				register short menuID;
				event->what = nullEvent;

				// track the menus

				result = MenuSelect(event->where);
				if (menuID = (short) (result >> 16))
					{
					short cmdChar;
					GetItemCmd(GetMHandle(menuID), (short) result, &cmdChar);
					// following is a compatibility hack for Word which does not respect enableFlags
 					if (cmdChar != 0)
						{
						EvQElPtr postedEvent;
						if (PPostEvent(keyDown, cmdChar, &postedEvent) != noErr)
							// if the event was not posted, unhilite the menu now
							HiliteMenu(0);
						else
							// otherwise, wait for MenuKey to unhilite it
							postedEvent->evtQModifiers = cmdKey;
						}
					}
				}
			}
		}
	else if (event->what == keyDown)
		{
		if ((event->modifiers & cmdKey) || ((char) event->message == kFunctionKey))
			{
			TAnalyzedWindowState state = ActiveWindowNeedsHelp();
			if ((state == kHandleMenusEditMenuEnabled) || (state == kHandleMenusEditMenuDisabled))
				{
				long result;
				unsigned char key = (char) event->message;

				if (key == kFunctionKey)
					{
					unsigned char virtualKey = (event->message & keyCodeMask) >> 8;
					
					switch (virtualKey)
						{
						case kCut:
							key = 'X';
							break;
							
						case kCopy:
							key = 'C';
							break;
							
						case kPaste:
							key = 'V';
							break;
							
						default:
							key = 0;
							break;
						}
					}
				
				// just hilite the menus when the relevant key is pressed
				
				if (key && (result = MenuKey(key)))
					{
					long dummy;
					Delay(8, &dummy);
					}
				HiliteMenu(0);
				}
			}
		}

	if (event->what != nullEvent)
		SetAnalyzedWindow((WindowPtr) 1);	// set to something that is never a window
	}

long GetMenuFlags(MenuHandle menu)
	{
	if (menu)
		{
		Handle savedFlags = GetSavedMenuState();
		if (savedFlags)
			{
			MenuStateEntry* stateEntry = (MenuStateEntry*) *savedFlags;
			MenuStateEntry* afterLast = (MenuStateEntry*) (((char*) stateEntry) + GetHandleSize(savedFlags));
	
			// search through the state handle for saved state for this menu
	
			while (stateEntry < afterLast)
				{
				if (stateEntry->handle == menu)
					// found it, return saved enable flags
					return stateEntry->savedEnableFlags;
				++stateEntry;
				}
			}
		return (**menu).enableFlags; // didnŐt find a saved state, so return the flags from the menu
		}
	return 0; // didnŐt find a menu, so return nothing enabled
	}

MenuHandle FindAppleMenu(void)
	{
	// *** use MENU resource
	return FindMenuByTitle("\p\024");
	}

Boolean PtInMenuBar(Point pt)
	{
	Rect mbRect;
	(void) GetMBARRect(&mbRect);
	return PtInRect(pt, &mbRect);
	}

Boolean IsThisASystemMenu(short menuID)
	{
	Boolean	isSys;
	if (IsSystemMenu(menuID, &isSys) != noErr)
		return 0;
	return isSys;
	}

Boolean IsModal(WindowPtr active)
	{
	short class;
	if (GetWindowModalClass(active,&class) != noErr)
		return false;
	return (class == dBoxProc);
	}
