/******************************************************************************
 JXTextMenuData.cc

	Stores a string and an image (optional) for each menu item.

	BASE CLASS = JXMenuData

	Copyright © 1996 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JXTextMenuData.h>
#include <JXTextMenu.h>
#include <JXTextMenuTable.h>
#include <JXWindow.h>
#include <JXImage.h>
#include <JXColormap.h>
#include <jXGlobals.h>
#include <jXKeysym.h>
#include <JString.h>
#include <JFontManager.h>
#include <JMinMax.h>
#include <iostream.h>
#include <ctype.h>
#include <jAssert.h>

// JBroadcaster message types

const JCharacter* JXTextMenuData::kImageChanged  = "ImageChanged::JXTextMenuData";

/******************************************************************************
 Constructor

 ******************************************************************************/

JXTextMenuData::JXTextMenuData
	(
	JXTextMenu* menu
	)
	:
	JXMenuData(),
	itsMenu( menu ),
	itsFontMgr( menu->GetFontManager() )
{
	itsTextItemData = new JArray<TextItemData>;
	assert( itsTextItemData != NULL );

	itsNeedGeomRecalcFlag = kTrue;
	itsMaxImageWidth      = 1;
	itsMaxTextWidth       = 1;
	itsMaxShortcutWidth   = 1;
	itsHasNMShortcutsFlag = kFalse;
	itsCompressHeightFlag = kFalse;

	itsItemHeights = new JRunArray<JCoordinate>;
	assert( itsItemHeights != NULL );
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JXTextMenuData::~JXTextMenuData()
{
	DeleteAll();
	delete itsTextItemData;

	delete itsItemHeights;
}

/******************************************************************************
 InsertItem

 ******************************************************************************/

void
JXTextMenuData::InsertItem
	(
	const JIndex		index,
	const JCharacter*	str,
	const JBoolean		isCheckbox,
	const JBoolean		isRadio,
	const JCharacter*	shortcuts,
	const JCharacter*	nmShortcut,
	const JCharacter*	id
	)
{
	TextItemData itemData;

	itemData.text = new JString(str);
	assert( itemData.text != NULL );

	itemData.fontSize = kJXDefaultFontSize;
	itemData.fontID =
		itsFontMgr->GetFontID(JGetDefaultFontName(), itemData.fontSize,
							  itemData.fontStyle);

	itsTextItemData->InsertElementAtIndex(index, itemData);

	JXMenuData::InsertItem(index, isCheckbox, isRadio, shortcuts, id);

	const JString* s;
	GetItemShortcuts(index, &s);
	itemData.ulIndex = JXWindow::GetULShortcutIndex(*(itemData.text), s);
	itsTextItemData->SetElement(index, itemData);

	(itsMenu->GetWindow())->MenuItemInserted(itsMenu, index);
	itsNeedGeomRecalcFlag = kTrue;

	SetNMShortcut(index, nmShortcut);	// parse it and register it
}

/******************************************************************************
 DeleteItem (virtual)

 ******************************************************************************/

void
JXTextMenuData::DeleteItem
	(
	const JIndex index
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	CleanOutTextItem(&itemData);
	itsTextItemData->RemoveElement(index);

	JXMenuData::DeleteItem(index);

	(itsMenu->GetWindow())->MenuItemRemoved(itsMenu, index);
	itsNeedGeomRecalcFlag = kTrue;
}

/******************************************************************************
 DeleteAll (virtual)

 ******************************************************************************/

void
JXTextMenuData::DeleteAll()
{
	const JSize itemCount = itsTextItemData->GetElementCount();
	for (JIndex i=1; i<=itemCount; i++)
		{
		TextItemData itemData = itsTextItemData->GetElement(i);
		CleanOutTextItem(&itemData);
		}
	itsTextItemData->RemoveAll();

	JXMenuData::DeleteAll();

	(itsMenu->GetWindow())->ClearAllMenuShortcuts(itsMenu);
	itsNeedGeomRecalcFlag = kTrue;
}

/******************************************************************************
 CleanOutTextItem (private)

 ******************************************************************************/

void
JXTextMenuData::CleanOutTextItem
	(
	TextItemData* itemData
	)
{
	delete (itemData->text);
	itemData->text = NULL;

	delete (itemData->nmShortcut);
	itemData->nmShortcut = NULL;

	if (itemData->ownsImage)
		{
		delete (itemData->image);
		}
	itemData->image = NULL;
}

/******************************************************************************
 GetText

 ******************************************************************************/

const JString&
JXTextMenuData::GetText
	(
	const JIndex	index,
	JIndex*			ulIndex,
	JFontID*		id,
	JSize*			size,
	JFontStyle*		style
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);
	*id    = itemData.fontID;
	*size  = itemData.fontSize;
	*style = itemData.fontStyle;

	*ulIndex = itemData.ulIndex;
	return *(itemData.text);
}

/******************************************************************************
 SetText

 ******************************************************************************/

void
JXTextMenuData::SetText
	(
	const JIndex		index,
	const JCharacter*	str
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	*(itemData.text) = str;

	const JString* shortcuts;
	GetItemShortcuts(index, &shortcuts);
	itemData.ulIndex = JXWindow::GetULShortcutIndex(*(itemData.text), shortcuts);
	itsTextItemData->SetElement(index, itemData);

	itsNeedGeomRecalcFlag = kTrue;
}

/******************************************************************************
 SetMenuItems

	%% is not supported.  The shortcuts can be listed after %h
	(e.g. "Quit%hQq"), but they cannot include %.

	The vertical bar '|' separates menu items.

 ******************************************************************************/

void
JXTextMenuData::SetMenuItems
	(
	const JCharacter* menuStr
	)
{
	DeleteAll();

	JSize count   = 0;
	JString str   = menuStr;
	JBoolean done = kFalse;
	while (!done)
		{
		JIndex sepIndex;
		JString itemText;
		const JBoolean found = str.LocateSubstring("|", &sepIndex);
		if (found)
			{
			assert( sepIndex > 1 );
			itemText = str.GetSubstring(1, sepIndex-1);
			str.RemoveSubstring(1, sepIndex);
			}
		else
			{
			itemText = str;
			done     = kTrue;
			}

		JBoolean isActive, hasSeparator, isCheckbox, isRadio;
		JString shortcuts, nmShortcut, id;
		ParseMenuItemStr(&itemText, &isActive, &hasSeparator,
						 &isCheckbox, &isRadio, &shortcuts, &nmShortcut, &id);

		count++;
		InsertItem(count, itemText, isCheckbox, isRadio, shortcuts, nmShortcut, id);
		if (!isActive)
			{
			DisableItem(count);
			}
		if (hasSeparator)
			{
			ShowSeparatorAfter(count);
			}
		}
}

/******************************************************************************
 ParseMenuItemStr (private)

	%d = disabled, %l = separator line after item,
	%b = checkbox, %r = radio button,
	%h<chars> = specify shortcuts, %k<chars> = specify non-menu shortcut string,
	%i<chars> = internal identifier

 ******************************************************************************/

void
JXTextMenuData::ParseMenuItemStr
	(
	JString*	text,
	JBoolean*	isActive,
	JBoolean*	hasSeparator,
	JBoolean*	isCheckbox,
	JBoolean*	isRadio,
	JString*	shortcuts,
	JString*	nmShortcut,
	JString*	id
	)
	const
{
	*isActive     = kTrue;
	*hasSeparator = kFalse;
	*isCheckbox   = kFalse;
	*isRadio      = kFalse;

	shortcuts->Clear();
	nmShortcut->Clear();
	id->Clear();

	JIndex opIndex;
	while (text->LocateLastSubstring("%", &opIndex))
		{
		const JSize textLength = text->GetLength();
		assert( opIndex < textLength );
		JString op = text->GetSubstring(opIndex+1, textLength);
		text->RemoveSubstring(opIndex, textLength);

		const JCharacter opc = op.GetFirstCharacter();
		if (opc == 'd')
			{
			*isActive = kFalse;
			}
		else if (opc == 'l')
			{
			*hasSeparator = kTrue;
			}
		else if (opc == 'b' || opc == 'B')
			{
			*isCheckbox = kTrue;
			}
		else if (opc == 'r' || opc == 'R')
			{
			*isCheckbox = kTrue;
			*isRadio    = kTrue;
			}

		else if (opc == 'h' && shortcuts->IsEmpty())
			{
			*shortcuts = op.GetSubstring(2, op.GetLength());
			shortcuts->TrimWhitespace();
			}
		else if (opc == 'h')
			{
			cerr << "Tried to use %h more than once in '" << *text << '\'' << endl;
			}

		else if (opc == 'k' && nmShortcut->IsEmpty())
			{
			*nmShortcut = op.GetSubstring(2, op.GetLength());
			nmShortcut->TrimWhitespace();
			}
		else if (opc == 'k')
			{
			cerr << "Tried to use %k more than once in '" << *text << '\'' << endl;
			}

		else if (opc == 'i' && id->IsEmpty())
			{
			*id = op.GetSubstring(2, op.GetLength());
			id->TrimWhitespace();
			}
		else if (opc == 'i')
			{
			cerr << "Tried to use %i more than once in '" << *text << '\'' << endl;
			}

		else
			{
			cerr << "Unsupported option %" << op << " in '" << *text << '\'' << endl;
			}
		}

	text->TrimWhitespace();
}

/******************************************************************************
 Get font

 ******************************************************************************/

const JCharacter*
JXTextMenuData::GetFontName
	(
	const JIndex index
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);
	return itsFontMgr->GetFontName(itemData.fontID);
}

void
JXTextMenuData::GetFont
	(
	const JIndex	index,
	JString*		name,
	JSize*			size,
	JFontStyle*		style
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);
	*name  = itsFontMgr->GetFontName(itemData.fontID);
	*size  = itemData.fontSize;
	*style = itemData.fontStyle;
}

/******************************************************************************
 Set font

 ******************************************************************************/

void
JXTextMenuData::SetFontName
	(
	const JIndex		index,
	const JCharacter*	name
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	const JFontID newID   = itsFontMgr->GetFontID(name, itemData.fontSize,
												  itemData.fontStyle);
	if (newID != itemData.fontID)
		{
		itemData.fontID = newID;
		itsTextItemData->SetElement(index, itemData);

		itsNeedGeomRecalcFlag = kTrue;
		}
}

void
JXTextMenuData::SetFontSize
	(
	const JIndex	index,
	const JSize		size
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	if (size != itemData.fontSize)
		{
		itemData.fontSize = size;

		itemData.fontID = itsFontMgr->UpdateFontID(itemData.fontID, itemData.fontSize,
												   itemData.fontStyle);
		itsTextItemData->SetElement(index, itemData);

		itsNeedGeomRecalcFlag = kTrue;
		}
}

void
JXTextMenuData::SetFontStyle
	(
	const JIndex		index,
	const JFontStyle&	style
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	if (style != itemData.fontStyle)
		{
		itemData.fontStyle = style;

		itemData.fontID = itsFontMgr->UpdateFontID(itemData.fontID, itemData.fontSize,
												   itemData.fontStyle);
		itsTextItemData->SetElement(index, itemData);

		itsNeedGeomRecalcFlag = kTrue;
		}
}

void
JXTextMenuData::SetFont
	(
	const JIndex		index,
	const JCharacter*	name,
	const JSize			size,
	const JFontStyle&	style
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	itemData.fontID    = itsFontMgr->GetFontID(name, size, style);
	itemData.fontSize  = size;
	itemData.fontStyle = style;
	itsTextItemData->SetElement(index, itemData);

	itsNeedGeomRecalcFlag = kTrue;
}

void
JXTextMenuData::SetFont
	(
	const JIndex		index,
	const JFontID		id,
	const JSize			size,
	const JFontStyle&	style
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	itemData.fontID    = id;
	itemData.fontSize  = size;
	itemData.fontStyle = style;
	itsTextItemData->SetElement(index, itemData);

	itsNeedGeomRecalcFlag = kTrue;
}

/******************************************************************************
 GetImage

 ******************************************************************************/

JBoolean
JXTextMenuData::GetImage
	(
	const JIndex	index,
	const JXImage**	image
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);
	if (itemData.image != NULL)
		{
		*image = itemData.image;
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 SetImage

 ******************************************************************************/

void
JXTextMenuData::SetImage
	(
	const JIndex	index,
	JXImage*		image,
	const JBoolean	menuOwnsImage
	)
{
	if (image == NULL)
		{
		ClearImage(index);
		return;
		}

	TextItemData itemData = itsTextItemData->GetElement(index);

	if (itemData.image == NULL ||
		(itemData.image->GetBounds()) != image->GetBounds())
		{
		itsNeedGeomRecalcFlag = kTrue;
		}

	if (itemData.ownsImage)
		{
		delete itemData.image;
		}

	itemData.image     = image;
	itemData.ownsImage = menuOwnsImage;
	itsTextItemData->SetElement(index, itemData);

	Broadcast(ImageChanged(index));
}

/******************************************************************************
 ClearImage

 ******************************************************************************/

void
JXTextMenuData::ClearImage
	(
	const JIndex index
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	if (itemData.image != NULL)
		{
		if (itemData.ownsImage)
			{
			delete itemData.image;
			}
		itemData.image = NULL;
		itsTextItemData->SetElement(index, itemData);

		itsNeedGeomRecalcFlag = kTrue;
		}
}

/******************************************************************************
 GetNMShortcut

 ******************************************************************************/

JBoolean
JXTextMenuData::GetNMShortcut
	(
	const JIndex	index,
	JString*		str
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);
	if (itemData.nmShortcut != NULL)
		{
		*str = *(itemData.nmShortcut);
		return kTrue;
		}
	else
		{
		str->Clear();
		return kFalse;
		}
}

/******************************************************************************
 GetNMShortcut

 ******************************************************************************/

JBoolean
JXTextMenuData::GetNMShortcut
	(
	const JIndex	index,
	JString*		str,
	JFontID*		id,
	JSize*			size,
	JFontStyle*		style
	)
	const
{
	const TextItemData itemData = itsTextItemData->GetElement(index);

	if (itemData.nmShortcut != NULL)
		{
		*str = *(itemData.nmShortcut);

		*size  = itemData.fontSize;
		*style = JFontStyle();
		*id    = itsFontMgr->UpdateFontID(itemData.fontID, *size, *style);

		return kTrue;
		}
	else
		{
		str->Clear();
		return kFalse;
		}
}

/******************************************************************************
 SetNMShortcut

 ******************************************************************************/

void
JXTextMenuData::SetNMShortcut
	(
	const JIndex		index,
	const JCharacter*	str
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);

	const JBoolean strEmpty = JStringEmpty(str);

	JBoolean changed = kFalse;
	if (!strEmpty && itemData.nmShortcut == NULL)
		{
		itemData.nmShortcut = new JString(str);
		assert( itemData.nmShortcut != NULL );
		itsTextItemData->SetElement(index, itemData);
		changed = kTrue;
		}
	else if (!strEmpty)
		{
		*(itemData.nmShortcut) = str;
		changed = kTrue;
		}
	else if (itemData.nmShortcut != NULL)
		{
		delete itemData.nmShortcut;
		itemData.nmShortcut = NULL;
		itsTextItemData->SetElement(index, itemData);
		changed = kTrue;
		}

	if (changed)
		{
		itsNeedGeomRecalcFlag = kTrue;

		JXWindow* window = itsMenu->GetWindow();
		window->ClearMenuShortcut(itsMenu, index);

		int key;
		JXKeyModifiers modifiers;
		if (itemData.nmShortcut != NULL &&
			ParseNMShortcut(*(itemData.nmShortcut), &key, &modifiers))
			{
			window->InstallMenuShortcut(itsMenu, index, key, modifiers);
			}
		}
}

/******************************************************************************
 ParseNMShortcut (private)

	Convert prepended "Ctrl-", "Meta-", "Ctrl-Shift-", and "Meta-Shift-"
	into modifier flags.  If the rest of the string is a single character,
	then we return this in key.  We also translate the strings "dash", "minus",
	"plus", "period", "comma", "return", and "tab" because the actual characters
	can confuse the user.  (e.g. Use "Ctrl-dash" instead of "Ctrl--")  We also
	decode "F1" through "F35" to be the function keys that X provides.

	"Ctrl-Shift-" and "Meta-Shift-" are allowed only for alphabetic and control
	characters because of differences between keyboards.

	Note that it is a bad idea to use "return" as a shortcut if the window
	contains a multi-line text editor.  "Meta-tab" is not available, because
	JXWindow catches it.

 ******************************************************************************/

struct JXTMModifierConversion
{
	const JCharacter*	str;
	JSize				strLength;
	JBoolean			ctrl, meta, shift;
};

static const JXTMModifierConversion kNMModConv[] =
{
	{"Ctrl-Shift-", 11, kTrue,  kFalse, kTrue },
	{"Meta-Shift-", 11, kFalse, kTrue,  kTrue },
	{"Ctrl-",        5, kTrue,  kFalse, kFalse},
	{"Meta-",        5, kFalse, kTrue,  kFalse}
};

const JSize kNMModConvCount = sizeof(kNMModConv)/sizeof(JXTMModifierConversion);

struct JXTMLinuxIntelModifierTranslation
{
	const JCharacter*	oldPrefix;
	JSize				oldLength;
	const JCharacter*	newPrefix;
};

/*
static const JXTMLinuxIntelModifierTranslation kNMLIModTrans[] =
{
	{"Meta-Shift-", 11, "Alt-Shift-"},
	{"Meta-",        5, "Alt-"      }
};

const JSize kNMLIModTransCount = sizeof(kNMLIModTrans)/sizeof(JXTMLinuxIntelModifierTranslation);
*/

struct JXTMKeySymConversion
{
	const JCharacter*	str;
	JCharacter			key;
};

static const JXTMKeySymConversion kNMKeyConv[] =
{
	{"dash",      '-'},
	{"minus",     '-'},
	{"plus",      '+'},
	{"period",    '.'},
	{"comma",     ','},
	{"return",    '\r'},
	{"tab",       '\t'},
	{"backspace", '\b'}
};

const JSize kNMKeyConvCount = sizeof(kNMKeyConv)/sizeof(JXTMKeySymConversion);

JBoolean
JXTextMenuData::ParseNMShortcut
	(
	const JString&	str,
	int*			key,
	JXKeyModifiers*	modifiers
	)
{
JIndex i;

	JString keyStr(str);
	modifiers->Clear();

	// decode modifiers

	for (i=0; i<kNMModConvCount; i++)
		{
		if (keyStr.BeginsWith(kNMModConv[i].str) &&
			keyStr.GetLength() > kNMModConv[i].strLength)
			{
			keyStr.RemoveSubstring(1, kNMModConv[i].strLength);
			modifiers->SetState(kJXControlKeyIndex, kNMModConv[i].ctrl);
			modifiers->SetState(kJXMetaKeyIndex,    kNMModConv[i].meta);
			modifiers->SetState(kJXShiftKeyIndex,   kNMModConv[i].shift);
			break;
			}
		}

	// translate known name to single character

	for (i=0; i<kNMKeyConvCount; i++)
		{
		if (JStringCompare(keyStr, kNMKeyConv[i].str, kFalse) == 0)
			{
			const JCharacter s[2] = { kNMKeyConv[i].key, '\0' };
			keyStr = s;
			break;
			}
		}

	// check for single character

	const int c1 = (unsigned char) keyStr.GetFirstCharacter();
	if (keyStr.GetLength() == 1)
		{
		assert( !modifiers->shift() || isalpha(c1) || iscntrl(c1) );

		*key = (modifiers->shift() ? toupper(c1) : tolower(c1));
		return kTrue;
		}

	// check for function key

	if (c1 == 'F')
		{
		const JString fnStr = keyStr.GetSubstring(2, keyStr.GetLength());
		JSize fnIndex;
		if (fnStr.ConvertToUInt(&fnIndex) && 1 <= fnIndex && fnIndex <= 35)
			{
			*key = XK_F1 + fnIndex-1;
			return kTrue;
			}
		}

	// give up

	return kFalse;
}

/******************************************************************************
 TranslateModifierName (static)

 ******************************************************************************/

void
JXTextMenuData::TranslateModifierName
	(
	JString* nmShortcut
	)
{
#ifdef _J_LINUX_INTEL
/*
	for (JIndex i=0; i<kNMLIModTransCount; i++)
		{
		if (nmShortcut->BeginsWith(kNMLIModTrans[i].oldPrefix, kNMLIModTrans[i].oldLength))
			{
			nmShortcut->ReplaceSubstring(1, kNMLIModTrans[i].oldLength,
										 kNMLIModTrans[i].newPrefix);
			break;
			}
		}
*/
#endif
}

/******************************************************************************
 ShowSeparatorAfter (protected)

 ******************************************************************************/

void
JXTextMenuData::ShowSeparatorAfter
	(
	const JIndex	index,
	const JBoolean	show
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	if (itemData.separator != show)
		{
		itemData.separator = show;
		itsTextItemData->SetElement(index, itemData);

		itsNeedGeomRecalcFlag = kTrue;
		}
}

/******************************************************************************
 ItemShortcutsChanged (virtual protected)

	Derived classes can override this to update underlining, etc.

	Note that shortcuts can be NULL.

 ******************************************************************************/

void
JXTextMenuData::ItemShortcutsChanged
	(
	const JIndex	index,
	const JString*	shortcuts
	)
{
	TextItemData itemData = itsTextItemData->GetElement(index);
	itemData.ulIndex =
		JXWindow::GetULShortcutIndex(*(itemData.text), shortcuts);
	itsTextItemData->SetElement(index, itemData);
}

/******************************************************************************
 ConfigureTable

	Called by JXTextMenuTable constructor.

 ******************************************************************************/

void
JXTextMenuData::ConfigureTable
	(
	JXTextMenuTable* table
	)
{
	const JBoolean hasCheckboxes = HasCheckboxes();
	const JBoolean hasSubmenus   = HasSubmenus();

	if (itsNeedGeomRecalcFlag)
		{
		itsNeedGeomRecalcFlag = kFalse;
		itsMaxImageWidth      = 1;
		itsMaxTextWidth       = 1;
		itsMaxShortcutWidth   = JXMenuTable::kSubmenuColWidth;
		itsHasNMShortcutsFlag = kFalse;
		itsItemHeights->RemoveAll();

		const JSize itemCount = itsTextItemData->GetElementCount();
		for (JIndex i=1; i<=itemCount; i++)
			{
			const TextItemData itemData = itsTextItemData->GetElement(i);
			JCoordinate h =
				(itsCompressHeightFlag && !hasCheckboxes && !hasSubmenus) ?
				0 : JXMenuTable::kMinRowHeight;

			if (itemData.text != NULL)
				{
				const JCoordinate th =
					itsFontMgr->GetLineHeight(itemData.fontID, itemData.fontSize,
											  itemData.fontStyle);
				h = JMax(h, th);
				const JCoordinate tw = 2*JXTextMenuTable::kHMarginWidth +
					itsFontMgr->GetStringWidth(itemData.fontID, itemData.fontSize,
											   itemData.fontStyle, *(itemData.text));
				itsMaxTextWidth = JMax(itsMaxTextWidth, tw);
				}

			if (itemData.image != NULL)
				{
				h = JMax(h, (itemData.image)->GetHeight());
				itsMaxImageWidth = JMax(itsMaxImageWidth, (itemData.image)->GetWidth());
				}

			if (itemData.nmShortcut != NULL)
				{
				JFontStyle style;
				itsHasNMShortcutsFlag = kTrue;
				const JCoordinate th =
					itsFontMgr->GetLineHeight(itemData.fontID, itemData.fontSize, style);
				h = JMax(h, th);
				const JCoordinate tw = JXTextMenuTable::kHNMSMarginWidth +
					JXTextMenuTable::kHMarginWidth +
					itsFontMgr->GetStringWidth(itemData.fontID, itemData.fontSize,
											   style, *(itemData.nmShortcut));
				itsMaxShortcutWidth = JMax(itsMaxShortcutWidth, tw);
				}

			h += 2*(JXTextMenuTable::kHilightBorderWidth + 1);
			if (HasSeparator(i))
				{
				h += JXTextMenuTable::kSeparatorHeight;
				}

			table->SetRowHeight(i,h);
			itsItemHeights->AppendElement(h);
			}
		}
	else
		{
		JRunArrayIterator<JCoordinate> iter(itsItemHeights);
		JCoordinate h;
		JIndex i=0;
		while(iter.Next(&h))
			{
			i++;
			table->SetRowHeight(i,h);
			}
		}

	// set the column widths

	if (hasCheckboxes)
		{
		table->SetColWidth(1, JXMenuTable::kCheckboxColWidth +
							  JXTextMenuTable::kHilightBorderWidth);
		}
	else
		{
		table->SetColWidth(1, JXTextMenuTable::kHilightBorderWidth);
		}

	table->SetColWidth(2, itsMaxImageWidth);
	table->SetColWidth(3, itsMaxTextWidth);

	if (itsHasNMShortcutsFlag || hasSubmenus)
		{
		table->SetColWidth(4, itsMaxShortcutWidth +
							  JXTextMenuTable::kHilightBorderWidth);
		}
	else
		{
		table->SetColWidth(4, JXTextMenuTable::kHilightBorderWidth);
		}

	// set a sensible scroll step

	const JCoordinate scrollStep =
		(itsFontMgr->GetLineHeight(JGetDefaultFontName(), kJXDefaultFontSize, JFontStyle())
		+ 2*(JXTextMenuTable::kHilightBorderWidth + 1));
	table->SetDefaultRowHeight(scrollStep);
}

#define JTemplateType JXTextMenuData::TextItemData
#include <JArray.tmpls>
#undef JTemplateType
