/******************************************************************************
 JXTEBase.cc

	We implement the routines required by JTextEditor that only depend on JX.

	We paste the following data types:
		XA_STRING, TEXT, text/x-jxstyled0

	We accept drops of the following data types:
		text/plain, text/x-jxstyled0

	To paste other formats, override TEGetExternalClipboard().

	To accept drops of other formats that can be pasted (and should therefore
		display an insertion caret), override TEXWillAcceptDrop() and
		TEXConvertDropData().

	To accept drops of other formats that cannot be pasted, override
		WillAcceptDrop() and -all- four HandleDND*() functions.

	BASE CLASS = JXScrollableWidget, JTextEditor

	Copyright © 1996-99 by John Lindal. All rights reserved.

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

#include <JXTextClipboard.h>
#include <JXTEBlinkCaretTask.h>
#include <JXGoToLineDialog.h>
#include <JXDisplay.h>
#include <JXWindow.h>
#include <JXMenuBar.h>
#include <JXTextMenu.h>
#include <JXScrollbar.h>
#include <JXWindowPainter.h>
#include <JXPSPrinter.h>
#include <JXPTPrinter.h>
#include <JXSelectionManager.h>
#include <JXDNDManager.h>
#include <JXColormap.h>
#include <jXGlobals.h>
#include <jXActionDefs.h>
#include <jXKeysym.h>

#include <JString.h>
#include <jASCIIConstants.h>
#include <jStrStreamUtil.h>
#include <jFileUtil.h>
#include <jProcessUtil.h>
#include <jStreamUtil.h>
#include <jTime.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <jAssert.h>

JXTEBase::PartialWordModifier JXTEBase::itsPWMod = JXTEBase::kCtrlAltPWMod;
JBoolean JXTEBase::itsWindowsHomeEndFlag         = kFalse;

static const JCharacter* kStyledTextXAtomName = "text/x-jxstyled0";

static const JCharacter* kDNDActionCopyDescrip = "copy the text";
static const JCharacter* kDNDActionMoveDescrip = "move the text";

// JError data

const JCharacter* JXTEBase::kNoData            = "NoData::JXTEBase";
const JCharacter* JXTEBase::kDataNotCompatible = "DataNotCompatible::JXTEBase";

const JCharacter* JXTEBase::kNoDataMsg =
	"The X Clipboard is empty.";

const JCharacter* JXTEBase::kDataNotCompatibleMsg =
	"Unable to paste the current contents of the X Clipboard.";

static const JCharacter* kDNCAtomMsg = "  (Unknown type:  ";

// Edit menu

static const JCharacter* kEditMenuTitleStr    = "Edit";
static const JCharacter* kEditMenuShortcutStr = "#E";

static const JCharacter* kMacEditMenuStr =
	"    Undo       %k Meta-Z       %i" kJXUndoAction
	"  | Redo       %k Meta-Shift-Z %i" kJXRedoAction
	"%l| Cut        %k Meta-X       %i" kJXCutAction
	"  | Copy       %k Meta-C       %i" kJXCopyAction
	"  | Paste      %k Meta-V       %i" kJXPasteAction
	"  | Clear                      %i" kJXClearAction
	"%l| Select all %k Meta-A       %i" kJXSelectAllAction;

static const JCharacter* kWinEditMenuStr =
	"    Undo       %h uz %k Ctrl-Z       %i" kJXUndoAction
	"  | Redo       %h r  %k Ctrl-Shift-Z %i" kJXRedoAction
	"%l| Cut        %h tx %k Ctrl-X       %i" kJXCutAction
	"  | Copy       %h c  %k Ctrl-C       %i" kJXCopyAction
	"  | Paste      %h pv %k Ctrl-V       %i" kJXPasteAction
	"  | Clear      %h l                  %i" kJXClearAction
	"%l| Select all %h a  %k Ctrl-A       %i" kJXSelectAllAction;

static const JCharacter* kCleanRightMarginStr           = "Clean right margin";
static const JCharacter* kCleanRightMarginMacShortcut   = "Meta-return";
static const JCharacter* kCleanRightMarginWinNMShortcut = "Ctrl-return";
static const JCharacter* kCleanRightMarginWinShortcuts  = "m";
static const JCharacter* kCleanRightMarginAction        = "CleanRightMarginCmd::JXTEBase";

static const JCharacter* kCoerceRightMarginStr           = "Coerce right margin";
static const JCharacter* kCoerceRightMarginMacShortcut   = "Meta-Shift-return";
static const JCharacter* kCoerceRightMarginWinNMShortcut = "Ctrl-Shift-return";
static const JCharacter* kCoerceRightMarginWinShortcuts  = "n";
static const JCharacter* kCoerceRightMarginAction        = "CoerceRightMarginCmd::JXTEBase";

static const JCharacter* kShiftSelLeftStr            = "Shift left";
static const JCharacter* kShiftSelLeftMacShortcut    = "Meta-[";
static const JCharacter* kShiftSelLeftWinNMShortcut  = "Ctrl-[";
static const JCharacter* kShiftSelLeftWinShortcuts   = "e";
static const JCharacter* kShiftSelLeftAction         = "ShiftSelLeftCmd::JXTEBase";

static const JCharacter* kShiftSelRightStr           = "Shift right";
static const JCharacter* kShiftSelRightMacShortcut   = "Meta-]";
static const JCharacter* kShiftSelRightWinNMShortcut = "Ctrl-]";
static const JCharacter* kShiftSelRightWinShortcuts  = "i";
static const JCharacter* kShiftSelRightAction        = "ShiftSelRightCmd::JXTEBase";

static const JCharacter* kToggleReadOnlyStr          = "Read only";
static const JCharacter* kToggleReadOnlyWinShortcuts = "o";
static const JCharacter* kToggleReadOnlyAction       = "ToggleReadOnlyCmd::JXTEBase";

struct EditMenuItemInfo
{
	JTextEditor::CmdIndex	cmd;
	const JCharacter*		id;
};

static const EditMenuItemInfo kEditMenuItemInfo[] =
{
	{ JTextEditor::kUndoCmd,      kJXUndoAction      },
	{ JTextEditor::kRedoCmd,      kJXRedoAction      },
	{ JTextEditor::kCutCmd,       kJXCutAction       },
	{ JTextEditor::kCopyCmd,      kJXCopyAction      },
	{ JTextEditor::kPasteCmd,     kJXPasteAction     },
	{ JTextEditor::kDeleteSelCmd, kJXClearAction     },
	{ JTextEditor::kSelectAllCmd, kJXSelectAllAction },

	{ JTextEditor::kCleanRightMarginCmd,  kCleanRightMarginAction  },
	{ JTextEditor::kCoerceRightMarginCmd, kCoerceRightMarginAction },
	{ JTextEditor::kShiftSelLeftCmd,      kShiftSelLeftAction      },
	{ JTextEditor::kShiftSelRightCmd,     kShiftSelRightAction     },
	{ JTextEditor::kToggleReadOnlyCmd,    kToggleReadOnlyAction    }
};
const JSize kEditMenuItemCount = sizeof(kEditMenuItemInfo)/sizeof(EditMenuItemInfo);

// used when setting images

enum
{
	kUndoIndex = 1, kRedoIndex,
	kCutIndex, kCopyIndex, kPasteIndex, kClearIndex,
	kSelectAllIndex
};

/******************************************************************************
 Constructor (protected)

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

JXTEBase::JXTEBase
	(
	const Type			type,
	const JBoolean		breakCROnly,
	const JBoolean		pasteStyledText,
	JXScrollbarSet*		scrollbarSet,
	JXContainer*		enclosure,
	const HSizingOption	hSizing,
	const VSizingOption	vSizing,
	const JCoordinate	x,
	const JCoordinate	y,
	const JCoordinate	w,
	const JCoordinate	h
	)
	:
	JXScrollableWidget(scrollbarSet, enclosure, hSizing, vSizing, x,y, w,h),
	JTextEditor(type, breakCROnly, pasteStyledText,
				GetFontManager(), GetColormap(),
				(GetColormap())->GetRedColor(),					// caret
				(GetColormap())->GetDefaultSelectionColor(),	// selection filled
				(GetColormap())->GetBlueColor(),				// selection outline
				(GetColormap())->GetBlackColor(),				// drag
				GetApertureWidth()),

	itsWillPasteCustomFlag( kFalse )
{
	itsEditMenu  = NULL;
	itsMenuStyle = JXMenu::kMacintoshStyle;

	itsCanAdjustMarginsFlag  = kFalse;
	itsCanToggleReadOnlyFlag = kFalse;

	itsPSPrinter      = NULL;
	itsPSPrintName    = NULL;
	itsPTPrinter      = NULL;
	itsPTPrintName    = NULL;

	itsGoToLineDialog = NULL;

	itsDNDDragInfo    = NULL;
	itsDNDDropInfo    = NULL;

	itsBlinkTask = new JXTEBlinkCaretTask(this);
	assert( itsBlinkTask != NULL );
	TECaretShouldBlink(kTrue);

	itsMinWidth = itsMinHeight = 0;
	RecalcAll(kTrue);

	if (type == kStaticText)
		{
		WantInput(kFalse);
		itsStyledTextXAtom = None;
		}
	else
		{
		WantInput(kTrue);
		SetDefaultCursor(kJXTextEditCursor);

		// targets for clipboard

		JXSelectionManager* selMgr = GetSelectionManager();

		AddSelectionTarget(kJXClipboardName, XA_STRING);
		AddSelectionTarget(kJXClipboardName, selMgr->GetTextXAtom());

		itsStyledTextXAtom = AddSelectionTarget(kJXClipboardName, kStyledTextXAtomName);

		// targets for DND

		const Atom dndName = (GetDNDManager())->GetDNDSelectionName();

		AddSelectionTarget(dndName, selMgr->GetMimePlainTextXAtom());
		AddSelectionTarget(dndName, itsStyledTextXAtom);
		}
}

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

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

JXTEBase::~JXTEBase()
{
	delete itsPSPrintName;
	delete itsPTPrintName;
	delete itsBlinkTask;
}

/******************************************************************************
 Draw (virtual protected)

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

void
JXTEBase::Draw
	(
	JXWindowPainter&	p,
	const JRect&		rect
	)
{
	TEDraw(p, rect);
}

/******************************************************************************
 HandleMouseEnter

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

void
JXTEBase::HandleMouseEnter()
{
	ShowCursor();
	itsPrevMousePt = JPoint(0,0);

	JXScrollableWidget::HandleMouseEnter();
}

/******************************************************************************
 HandleMouseHere

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

void
JXTEBase::HandleMouseHere
	(
	const JPoint&			pt,
	const JXKeyModifiers&	modifiers
	)
{
	if (pt != itsPrevMousePt)
		{
		ShowCursor();
		}
	itsPrevMousePt = pt;

	JXScrollableWidget::HandleMouseHere(pt, modifiers);
}

/******************************************************************************
 HandleMouseLeave

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

void
JXTEBase::HandleMouseLeave()
{
	ShowCursor();
	JXScrollableWidget::HandleMouseLeave();
}

/******************************************************************************
 HandleMouseDown (virtual protected)

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

void
JXTEBase::HandleMouseDown
	(
	const JPoint&			pt,
	const JXMouseButton		button,
	const JSize				clickCount,
	const JXButtonStates&	buttonStates,
	const JXKeyModifiers&	modifiers
	)
{
	const Type type = GetType();
	if (button > kJXRightButton)
		{
		ScrollForWheel(button, modifiers);
		}
	else if (type != kStaticText)
		{
		ShowCursor();

		if (button == kJXMiddleButton && type == kFullEditor)
			{
			TEHandleMouseDown(pt, 1, kFalse, kFalse);
			TEHandleMouseUp(kFalse);
			Paste();
			}
		else if (button != kJXMiddleButton)
			{
			const JBoolean extendSelection = JI2B(
				button == kJXRightButton || modifiers.shift() );
			const JBoolean partialWord = JI2B(
				(itsPWMod == kCtrlAltPWMod &&
				 modifiers.control() && modifiers.meta()) ||
				(itsPWMod != kCtrlAltPWMod &&
				 modifiers.GetState(kJXMod2KeyIndex + itsPWMod - kMod2PWMod)));
			TEHandleMouseDown(pt, clickCount, extendSelection, partialWord);
			}
		}
}

/******************************************************************************
 HandleMouseDrag (virtual protected)

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

void
JXTEBase::HandleMouseDrag
	(
	const JPoint&			pt,
	const JXButtonStates&	buttonStates,
	const JXKeyModifiers&	modifiers
	)
{
	DNDDragInfo info(&pt, &buttonStates, &modifiers);
	itsDNDDragInfo = &info;

	TEHandleMouseDrag(pt);

	itsDNDDragInfo = NULL;
}

/******************************************************************************
 HandleMouseUp (virtual protected)

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

void
JXTEBase::HandleMouseUp
	(
	const JPoint&			pt,
	const JXMouseButton		button,
	const JXButtonStates&	buttonStates,
	const JXKeyModifiers&	modifiers
	)
{
	TEHandleMouseUp(modifiers.meta());
}

/******************************************************************************
 HitSamePart (virtual protected)

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

JBoolean
JXTEBase::HitSamePart
	(
	const JPoint& pt1,
	const JPoint& pt2
	)
	const
{
	return TEHitSamePart(pt1, pt2);
}

/******************************************************************************
 TEDisplayBusyCursor (virtual protected)

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

void
JXTEBase::TEDisplayBusyCursor()
	const
{
	(JXGetApplication())->DisplayBusyCursor();
}

/******************************************************************************
 TEBeginDND (virtual protected)

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

JBoolean
JXTEBase::TEBeginDND()
{
	assert( itsDNDDragInfo != NULL );

	return BeginDND(*(itsDNDDragInfo->pt), *(itsDNDDragInfo->buttonStates),
					*(itsDNDDragInfo->modifiers));
}

/******************************************************************************
 DNDFinish (virtual protected)

	Force the dragged text to be copied.

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

void
JXTEBase::DNDFinish
	(
	const JXContainer* target
	)
{
	TEDNDFinished();

	if (target != this)
		{
		const JString* text;
		const JBoolean ok = GetDragClip(&text);
		assert( ok );
		}
}

/******************************************************************************
 GetDNDAction (virtual protected)

	This is called repeatedly during the drag so the drop action can be
	changed based on the current target, buttons, and modifier keys.

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

Atom
JXTEBase::GetDNDAction
	(
	const JXContainer*		target,
	const JXButtonStates&	buttonStates,
	const JXKeyModifiers&	modifiers
	)
{
	const Type type = GetType();
	if (type == kFullEditor && modifiers.control())
		{
		return (GetDNDManager())->GetDNDActionAskXAtom();
		}
	else if (type == kFullEditor &&
			 ((target == this && !modifiers.meta()) ||
			  (target != this &&  modifiers.meta())))
		{
		return (GetDNDManager())->GetDNDActionMoveXAtom();
		}
	else
		{
		return (GetDNDManager())->GetDNDActionCopyXAtom();
		}
}

/******************************************************************************
 GetDNDAskActions (virtual protected)

	This is called when the value returned by GetDNDAction() changes to
	XdndActionAsk.  If GetDNDAction() repeatedly returns XdndActionAsk,
	this function is not called again because it is assumed that the
	actions are the same within a single DND session.

	This function must place at least 2 elements in askActionList and
	askDescriptionList.

	The first element should be the default action.

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

void
JXTEBase::GetDNDAskActions
	(
	const JXButtonStates&	buttonStates,
	const JXKeyModifiers&	modifiers,
	JArray<Atom>*			askActionList,
	JPtrArray<JString>*		askDescriptionList
	)
{
	assert( GetType() == kFullEditor );

	JXDNDManager* dndMgr = GetDNDManager();
	askActionList->AppendElement(dndMgr->GetDNDActionCopyXAtom());
	askActionList->AppendElement(dndMgr->GetDNDActionMoveXAtom());

	JString* s = new JString(kDNDActionCopyDescrip);
	assert( s != NULL );
	askDescriptionList->Append(s);

	s = new JString(kDNDActionMoveDescrip);
	assert( s != NULL );
	askDescriptionList->Append(s);
}

/******************************************************************************
 LostSelectionOwnership (virtual protected)

	When we lose the selection, we toss our buffer.

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

void
JXTEBase::LostSelectionOwnership
	(
	const Atom selectionName
	)
{
	if (selectionName == kJXClipboardName)
		{
		TEClearClipboard();
		}
	else if (selectionName == (GetDNDManager())->GetDNDSelectionName())
		{
		TEClearDragClip();
		}

	JXScrollableWidget::LostSelectionOwnership(selectionName);
}

/******************************************************************************
 WillAcceptDrop (virtual protected)

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

JBoolean
JXTEBase::WillAcceptDrop
	(
	const JArray<Atom>&	typeList,
	Atom*				action,
	const Time			time,
	const JXWidget*		source
	)
{
	itsWillPasteCustomFlag = kFalse;

	if (GetType() != kFullEditor)
		{
		return kFalse;
		}
	else if (source == this)
		{
		return kTrue;
		}
	else if (TEXWillAcceptDrop(typeList, *action, time, source))
		{
		itsWillPasteCustomFlag = kTrue;
		return kTrue;
		}
	else
		{
		JXDNDManager* dndMgr = GetDNDManager();
		JBoolean canGetStyledText, canGetText;
		Atom textType;
		return JI2B(
			(*action == dndMgr->GetDNDActionCopyXAtom() ||
			 *action == dndMgr->GetDNDActionMoveXAtom() ||
			 *action == dndMgr->GetDNDActionAskXAtom()) &&
			GetAvailDataTypes(typeList, &canGetStyledText, &canGetText, &textType));
		}
}

/******************************************************************************
 TEXWillAcceptDrop (virtual protected)

	Derived classes can override this to accept other data types that
	can be pasted.  Returning kTrue guarantees that TEXConvertDropData()
	will be called, and this can be used to accept a different data type
	even when one of the default types (e.g. text/plain) is available.

	Data types that cannot be pasted must be implemented by overriding
	WillAcceptDrop() and -all- four HandleDND*() functions.

	When overriding this function, derived classes must also override
	TEXConvertDropData() to process the actual data that is dropped.

	source is non-NULL if the drag is between widgets in the same program.
	This provides a way for compound documents to identify drags between their
	various parts.

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

JBoolean
JXTEBase::TEXWillAcceptDrop
	(
	const JArray<Atom>&	typeList,
	const Atom			action,
	const Time			time,
	const JXWidget*		source
	)
{
	return kFalse;
}

/******************************************************************************
 HandleDNDEnter (virtual protected)

	This is called when the mouse enters the widget.

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

void
JXTEBase::HandleDNDEnter()
{
	TEHandleDNDEnter();
}

/******************************************************************************
 HandleDNDHere (virtual protected)

	This is called while the mouse is inside the widget.

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

void
JXTEBase::HandleDNDHere
	(
	const JPoint&	pt,
	const JXWidget*	source
	)
{
	TEHandleDNDHere(pt, JI2B(source == this));
}

/******************************************************************************
 HandleDNDLeave (virtual protected)

	This is called when the mouse leaves the widget without dropping data.

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

void
JXTEBase::HandleDNDLeave()
{
	TEHandleDNDLeave();
}

/******************************************************************************
 HandleDNDDrop (virtual protected)

	This is called when the data is dropped.  The data is accessed via
	the selection manager, just like Paste.

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

void
JXTEBase::HandleDNDDrop
	(
	const JPoint&		pt,
	const JArray<Atom>&	typeList,
	const Atom			action,
	const Time			time,
	const JXWidget*		source
	)
{
	DNDDropInfo info(&typeList, action, time);
	itsDNDDropInfo = &info;

	JXDNDManager* dndMgr  = GetDNDManager();
	const Atom copyAction = dndMgr->GetDNDActionCopyXAtom();
	if (!itsWillPasteCustomFlag &&
		action == dndMgr->GetDNDActionAskXAtom())
		{
		JArray<Atom> actionList;
		JPtrArray<JString> descriptionList;
		descriptionList.SetCleanUpAction(JPtrArrayT::kDeleteAllAsObjects);
		if (!dndMgr->GetAskActions(&actionList, &descriptionList))
			{
			itsDNDDropInfo->action = copyAction;
			}
		else if (!dndMgr->ChooseDropAction(actionList, descriptionList,
										   &(itsDNDDropInfo->action)))
			{
			return;
			}
		}

	TEHandleDNDDrop(pt, JI2B(source == this),
					JI2B(itsDNDDropInfo->action == copyAction));

	itsDNDDropInfo = NULL;
}

/******************************************************************************
 TEPasteDropData (virtual protected)

	Get the data that was dropped and paste it in.

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

void
JXTEBase::TEPasteDropData()
{
	assert( itsDNDDropInfo != NULL );

	JXDNDManager* dndMgr     = GetDNDManager();
	const Atom selectionName = dndMgr->GetDNDSelectionName();

	JString text;
	JRunArray<Font> style;
	if (itsWillPasteCustomFlag &&
		TEXConvertDropData(*(itsDNDDropInfo->typeList), itsDNDDropInfo->action,
						   itsDNDDropInfo->time, &text, &style))
		{
		if (style.IsEmpty())
			{
			Paste(text);
			}
		else
			{
			Paste(text, &style);
			}
		}
	else if (!itsWillPasteCustomFlag &&
			 GetSelectionData(selectionName, *(itsDNDDropInfo->typeList),
							  itsDNDDropInfo->time, &text, &style) == kJNoError)
		{
		if (style.IsEmpty())
			{
			Paste(text);
			}
		else
			{
			Paste(text, &style);
			}

		if (itsDNDDropInfo->action == dndMgr->GetDNDActionMoveXAtom())
			{
			(GetSelectionManager())->
				SendDeleteSelectionRequest(selectionName, itsDNDDropInfo->time,
										   GetWindow());
			}
		}
}

/******************************************************************************
 TEXConvertDropData (virtual protected)

	Derived classes can override this to convert other data types into
	text and styles that can be pasted.  Return kTrue to paste the contents
	of text and style.  To paste unstyled text, leave style empty.

	This function will only be called if the derived class implemented
	TEXWillAcceptDrop() and returned kTrue.

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

JBoolean
JXTEBase::TEXConvertDropData
	(
	const JArray<Atom>&	typeList,
	const Atom			action,
	const Time			time,
	JString*			text,
	JRunArray<Font>*	style
	)
{
	return kFalse;
}

/******************************************************************************
 Activate (virtual)

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

void
JXTEBase::Activate()
{
	JXScrollableWidget::Activate();
	if (IsActive() && HasFocus())
		{
		TEActivate();
		if ((GetWindow())->HasFocus())
			{
			itsBlinkTask->Reset();
			(JXGetApplication())->InstallIdleTask(itsBlinkTask);
			TEActivateSelection();
			}
		}
}

/******************************************************************************
 Deactivate (virtual)

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

void
JXTEBase::Deactivate()
{
	JXScrollableWidget::Deactivate();
	if (!IsActive())
		{
		TEDeactivate();
		(JXGetApplication())->RemoveIdleTask(itsBlinkTask);
		TEDeactivateSelection();
		}
}

/******************************************************************************
 HandleFocusEvent (virtual protected)

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

void
JXTEBase::HandleFocusEvent()
{
	JXScrollableWidget::HandleFocusEvent();
	TEActivate();
	if (IsActive() && (GetWindow())->HasFocus())
		{
		itsBlinkTask->Reset();
		(JXGetApplication())->InstallIdleTask(itsBlinkTask);
		TEActivateSelection();
		}
}

/******************************************************************************
 HandleUnfocusEvent (virtual protected)

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

void
JXTEBase::HandleUnfocusEvent()
{
	JXScrollableWidget::HandleUnfocusEvent();
	TEDeactivate();
	(JXGetApplication())->RemoveIdleTask(itsBlinkTask);
	TEDeactivateSelection();
}

/******************************************************************************
 HandleWindowFocusEvent (virtual protected)

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

void
JXTEBase::HandleWindowFocusEvent()
{
	JXScrollableWidget::HandleWindowFocusEvent();
	TEActivateSelection();
	if (IsActive() && HasFocus())
		{
		itsBlinkTask->Reset();
		(JXGetApplication())->InstallIdleTask(itsBlinkTask);
		}
}

/******************************************************************************
 HandleWindowUnfocusEvent (virtual protected)

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

void
JXTEBase::HandleWindowUnfocusEvent()
{
	JXScrollableWidget::HandleWindowUnfocusEvent();
	TEHideCaret();
	TEDeactivateSelection();
	(JXGetApplication())->RemoveIdleTask(itsBlinkTask);
}

/******************************************************************************
 TECaretShouldBlink (virtual protected)

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

void
JXTEBase::TECaretShouldBlink
	(
	const JBoolean blink
	)
{
	itsBlinkTask->ShouldBlink(blink);
}

/******************************************************************************
 HandleKeyPress (virtual)

	We handle all the edit shortcuts here because we won't always have
	an Edit menu.

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

void
JXTEBase::HandleKeyPress
	(
	const int				origKey,
	const JXKeyModifiers&	origModifiers
	)
{
	int key                  = origKey;
	JXKeyModifiers modifiers = origModifiers;
	if (itsWindowsHomeEndFlag)
		{
		RemapWindowsHomeEnd(&key, &modifiers);
		}

	const JBoolean controlOn = modifiers.control();
	const JBoolean metaOn    = modifiers.meta();
	const JBoolean shiftOn   = modifiers.shift();

	const Type type = GetType();
	if (type == kFullEditor && !controlOn && !metaOn &&
		((GetDisplay())->GetLatestButtonStates()).AllOff())
		{
		HideCursor();
		}

	JBoolean processed = kFalse;

	if (type == kFullEditor &&
		(((key == 'z' || key == 'Z') && !controlOn &&  metaOn && !shiftOn) ||
		 (key == JXCtrl('Z')         &&  controlOn && !metaOn && !shiftOn)))
		{
		Undo();
		processed = kTrue;
		}
	else if (type == kFullEditor &&
			 (((key == 'x' || key == 'X') && !controlOn &&  metaOn && !shiftOn) ||
			  (key == JXCtrl('X')         &&  controlOn && !metaOn && !shiftOn)))
		{
		Cut();
		processed = kTrue;
		}
	else if (((key == 'c' || key == 'C') && !controlOn &&  metaOn && !shiftOn) ||
			 (key == JXCtrl('C')         &&  controlOn && !metaOn && !shiftOn))
		{
		Copy();
		processed = kTrue;
		}
	else if (type == kFullEditor &&
			 (((key == 'v' || key == 'V') && !controlOn &&  metaOn && !shiftOn) ||
			  (key == JXCtrl('V')         &&  controlOn && !metaOn && !shiftOn)))
		{
		Paste();
		processed = kTrue;
		}
	else if (((key == 'a' || key == 'A') && !controlOn &&  metaOn && !shiftOn) ||
			 (key == JXCtrl('A')         &&  controlOn && !metaOn && !shiftOn))
		{
		SelectAll();
		processed = kTrue;
		}

	else if (key == kJLeftArrow || key == kJRightArrow ||
			 key == kJUpArrow   || key == kJDownArrow)
		{
		CaretMotion motion = kMoveByCharacter;
		if ((itsPWMod == kCtrlAltPWMod && controlOn && metaOn) ||
			(itsPWMod != kCtrlAltPWMod &&
			 modifiers.GetState(kJXMod2KeyIndex + itsPWMod - kMod2PWMod)))
			{
			motion = kMoveByPartialWord;
			}
		else if (controlOn)
			{
			motion = kMoveByWord;
			}
		else if (metaOn)
			{
			motion = kMoveByLine;
			}

		if (type == kFullEditor || shiftOn || motion != kMoveByCharacter)
			{
			processed = TEHandleKeyPress(key, shiftOn, motion);
			}
		else
			{
			processed = kFalse;
			}
		}

	else if (type == kFullEditor &&
			 0 < key && key <= 255 &&
			 !controlOn && !metaOn)
		{
		processed = TEHandleKeyPress(key, shiftOn, kMoveByCharacter);
		}

	if (!processed)
		{
		int k = key;
		if (k == ' ')
			{
			k = XK_Page_Down;
			}
		else if (k == kJDeleteKey)
			{
			k = XK_Page_Up;
			}

		JXScrollableWidget::HandleKeyPress(k, modifiers);
		}
}

/******************************************************************************
 RemapWindowsHomeEnd (private)

	Home/End		=> beginning/end of line
	Ctrl-Home/End	=> scroll to top/bottom

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

void
JXTEBase::RemapWindowsHomeEnd
	(
	int*			key,
	JXKeyModifiers*	modifiers
	)
	const
{
	if ((*key == XK_Home || *key == XK_KP_Home ||
		 *key == XK_End  || *key == XK_KP_End) &&
		modifiers->control())
		{
		modifiers->SetState(kJXControlKeyIndex, kFalse);
		}
	else if (*key == XK_Home || *key == XK_KP_Home)
		{
		*key = kJLeftArrow;
		modifiers->SetState(kJXMetaKeyIndex, kTrue);
		}
	else if (*key == XK_End || *key == XK_KP_End)
		{
		*key = kJRightArrow;
		modifiers->SetState(kJXMetaKeyIndex, kTrue);
		}
}

/******************************************************************************
 AdjustCursor (virtual protected)

	Show the default cursor during drag-and-drop.

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

void
JXTEBase::AdjustCursor
	(
	const JPoint&			pt,
	const JXKeyModifiers&	modifiers
	)
{
	if (TEWillDragAndDrop(pt, kFalse, modifiers.meta()))
		{
		DisplayCursor(kJXDefaultCursor);
		}
	else
		{
		JXScrollableWidget::AdjustCursor(pt, modifiers);
		}
}

/******************************************************************************
 ApertureResized (virtual protected)

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

void
JXTEBase::ApertureResized
	(
	const JCoordinate dw,
	const JCoordinate dh
	)
{
	JXScrollableWidget::ApertureResized(dw,dh);
	TESetBoundsWidth(GetApertureWidth());
	TESetGUIBounds(itsMinWidth, itsMinHeight, -1);
}

/******************************************************************************
 TERefresh (virtual protected)

	Not inline because it is virtual.

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

void
JXTEBase::TERefresh()
{
	Refresh();
}

/******************************************************************************
 TERefreshRect (virtual protected)

	Not inline because it is virtual.

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

void
JXTEBase::TERefreshRect
	(
	const JRect& rect
	)
{
	RefreshRect(rect);
}

/******************************************************************************
 TEUpdateDisplay (virtual protected)

	Not inline because it is virtual.

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

void
JXTEBase::TEUpdateDisplay()
{
	(GetWindow())->Update();
}

/******************************************************************************
 TERedraw (virtual protected)

	Not inline because it is virtual.

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

void
JXTEBase::TERedraw()
{
	Redraw();
}

/******************************************************************************
 TESetGUIBounds (virtual protected)

	Keep the bounds at least as large as the aperture.

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

void
JXTEBase::TESetGUIBounds
	(
	const JCoordinate w,
	const JCoordinate h,
	const JCoordinate changeY
	)
{
	itsMinWidth  = w;
	itsMinHeight = h;

	const JRect ap = GetApertureGlobal();

	JCoordinate width = ap.width();
	if (width < w)
		{
		width = w;
		}

	JCoordinate height = ap.height();
	if (height < h)
		{
		height = h;
		}

	JXScrollbar *hScrollbar, *vScrollbar;
	if (changeY >= 0 && GetScrollbars(&hScrollbar, &vScrollbar))
		{
		const JCoordinate origH = GetBoundsHeight();
		if (height < origH)
			{
			vScrollbar->PrepareForLowerMaxValue(changeY, origH - height);
			}
		else if (height > origH)
			{
			vScrollbar->PrepareForHigherMaxValue(changeY, height - origH);
			}
		}

	SetBounds(width, height);
}

/******************************************************************************
 TEScrollToRect (virtual protected)

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

JBoolean
JXTEBase::TEScrollToRect
	(
	const JRect&	rect,
	const JBoolean	centerInDisplay
	)
{
	if (centerInDisplay && rect.right <= GetApertureWidth())
		{
		JRect r = rect;
		r.left  = 0;
		return ScrollToRectCentered(r, kFalse);
		}
	else if (centerInDisplay)
		{
		return ScrollToRectCentered(rect, kFalse);
		}
	else
		{
		return ScrollToRect(rect);
		}
}

/******************************************************************************
 TEScrollForDrag (virtual protected)

	Not inline because it is virtual.

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

JBoolean
JXTEBase::TEScrollForDrag
	(
	const JPoint& pt
	)
{
	return ScrollForDrag(pt);
}

/******************************************************************************
 TESetVertScrollStep (virtual protected)

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

void
JXTEBase::TESetVertScrollStep
	(
	const JCoordinate vStep
	)
{
	SetVertStepSize(vStep);
	SetVertPageStepContext(vStep);
}

/******************************************************************************
 TEClipboardChanged (virtual protected)

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

void
JXTEBase::TEClipboardChanged()
{
	if (!BecomeSelectionOwner(kJXClipboardName))
		{
		(JGetUserNotification())->ReportError("Unable to copy to the X Clipboard.");
		}
	else if (this != (GetDisplay())->GetTextClipboard())
		{
		(GetDisplay())->UpdateTextClipboard();
		}
}

/******************************************************************************
 TEOwnsClipboard (virtual protected)

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

JBoolean
JXTEBase::TEOwnsClipboard()
	const
{
	return OwnsSelection(kJXClipboardName);
}

/******************************************************************************
 TEGetExternalClipboard (virtual protected)

	Returns kTrue if there is something pasteable on the system clipboard.

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

JBoolean
JXTEBase::TEGetExternalClipboard
	(
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	const JError err = GetSelectionData(kJXClipboardName, CurrentTime, text, style);
	if (err.OK())
		{
		return kTrue;
		}
	else
		{
		if (err != kNoData)
			{
			err.ReportError();
			}
		return kFalse;
		}
}

/******************************************************************************
 GetAvailDataTypes (private)

	Returns kTrue if it can find a data type that we understand.

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

JBoolean
JXTEBase::GetAvailDataTypes
	(
	const JArray<Atom>&	typeList,
	JBoolean*			canGetStyledText,
	JBoolean*			canGetText,
	Atom*				textType
	)
	const
{
	*canGetStyledText = kFalse;
	*canGetText       = kFalse;
	*textType         = None;

	JXSelectionManager* selMgr = GetSelectionManager();

	const JSize typeCount = typeList.GetElementCount();
	for (JIndex i=1; i<=typeCount; i++)
		{
		const Atom type = typeList.GetElement(i);
		if (type == XA_STRING ||
			type == selMgr->GetMimePlainTextXAtom() ||
			(!(*canGetText) && type == selMgr->GetTextXAtom()))
			{
			*canGetText = kTrue;
			*textType   = type;
			}

		// By checking WillPasteStyledText(), we avoid wasting time
		// parsing style information.

		else if (type == itsStyledTextXAtom && WillPasteStyledText())
			{
			*canGetStyledText = kTrue;
			}
		}

	return JConvertToBoolean( *canGetStyledText || *canGetText );
}

/******************************************************************************
 GetSelectionData (private)

	Returns kTrue if there is something pasteable in the given selection.

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

JError
JXTEBase::GetSelectionData
	(
	const Atom			selectionName,
	const Time			time,
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	JArray<Atom> typeList;
	if ((GetSelectionManager())->
			GetAvailableTypes(selectionName, time, GetWindow(), &typeList))
		{
		return GetSelectionData(selectionName, typeList, time, text, style);
		}
	else
		{
		return NoData();
		}
}

/******************************************************************************
 GetSelectionData (private)

	Returns kTrue if there is something pasteable in the given selection.

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

JError
JXTEBase::GetSelectionData
	(
	const Atom			selectionName,
	const JArray<Atom>&	typeList,
	const Time			time,
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	text->Clear();
	style->RemoveAll();

	JBoolean canGetStyledText, canGetText;
	Atom textType;
	if (GetAvailDataTypes(typeList, &canGetStyledText, &canGetText, &textType))
		{
		JXSelectionManager* selMgr = GetSelectionManager();
		const JXWindow* window     = GetWindow();

		JBoolean gotData = kFalse;
		JError err       = JNoError();

		Atom returnType, textReturnType;
		unsigned char* data = NULL;
		JSize dataLength;
		JXSelectionManager::DeleteMethod delMethod;

		if (!gotData && canGetStyledText &&
			selMgr->GetSelectionData(selectionName, time, window, itsStyledTextXAtom,
									 &returnType, &data, &dataLength, &delMethod))
			{
			if (returnType == itsStyledTextXAtom)
				{
				gotData = kTrue;
				jistrstream(input, reinterpret_cast<char*>(data), dataLength);
				if (!ReadPrivateFormat(input, this, text, style))
					{
					err = DataNotCompatible();
					}
				}
			selMgr->DeleteSelectionData(&data, delMethod);
			}

		if (!gotData && canGetText &&
			selMgr->GetSelectionData(selectionName, time, window, textType,
									 &textReturnType, &data, &dataLength, &delMethod))
			{
			if (textReturnType == XA_STRING ||
				textReturnType == selMgr->GetMimePlainTextXAtom())
				{
				gotData = kTrue;
				*text = JString(reinterpret_cast<JCharacter*>(data), dataLength);
				}
			selMgr->DeleteSelectionData(&data, delMethod);
			}

		if (!gotData)
			{
			err = DataNotCompatible(canGetText ? textReturnType : None, GetDisplay());
			}

		if (err.OK())
			{
			err = FilterSelectionData(text, style);
			}

		return err;
		}
	else
		{
		return NoData();
		}
}

/******************************************************************************
 FilterSelectionData (virtual protected)

	Derived classes can override this to change the data or return an error.

	*** Note that style will be empty if the data was plain text.

	They must call the inherited version first because we remove illegal
	characters and convert \r and \r\n to \n.

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

JError
JXTEBase::FilterSelectionData
	(
	JString*			text,
	JRunArray<Font>*	style
	)
	const
{
	RemoveIllegalChars(text, style);

	JSize length = text->GetLength();

	// convert \r\n to \n

	if (length > 0)		// avoid unsigned rollover
		{
		for (JIndex i=length-1; i>=1; i--)
			{
			if (text->GetCharacter(i  ) == '\r' &&
				text->GetCharacter(i+1) == '\n')
				{
				text->RemoveSubstring(i,i);
				length--;
				}
			}
		}

	// convert \r to \n

	for (JIndex i=1; i<=length; i++)
		{
		if (text->GetCharacter(i) == '\r')
			{
			text->SetCharacter(i, '\n');
			}
		}

	return JNoError();
}

/******************************************************************************
 ConvertSelection (virtual protected)

	Convert selection to the specified type and return kTrue,
	or return kFalse if the conversion cannot be accomplished.

	*returnType must be actual data type.  For example, when "TEXT" is
	requested, one often returns XA_STRING.

	*data must be allocated with "new unsigned char[]" and will be deleted
	by the caller.  *dataLength must be set to the length of *data.

	*bitsPerBlock must be set to the number of bits per element of data.
	e.g.	If data is text, *bitsPerBlock=8.
			If data is an int, *bitsPerBlock=sizeof(int)*8

	Since X performs byte swapping when *bitsPerBlock > 8, mixed data is
	packed one byte at a time to insure that it can be correctly decoded.

	We hard-code the versions passed to WritePrivateFormat() because
	each version should have a different X atom.  This way, older versions
	can request styled text from newer versions.

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

JBoolean
JXTEBase::ConvertSelection
	(
	const Atom		name,
	const Atom		type,
	Atom*			returnType,
	unsigned char**	data,
	JSize*			dataLength,
	JSize*			bitsPerBlock
	)
{
	const Atom dndName = (GetDNDManager())->GetDNDSelectionName();
	*bitsPerBlock = 8;

	JXSelectionManager* selMgr = GetSelectionManager();
	const Atom mimeText        = selMgr->GetMimePlainTextXAtom();

	if (type == XA_STRING || type == mimeText || type == selMgr->GetTextXAtom())
		{
		const JString* text;
		if ((name == kJXClipboardName && !GetInternalClipboard(&text)) ||
			(name == dndName          && !GetDragClip(&text)) ||
			(name != kJXClipboardName && name != dndName))
			{
			return kFalse;
			}

		*returnType = (type == mimeText) ? mimeText : XA_STRING;
		*dataLength = text->GetLength();
		*data       = new unsigned char[ *dataLength ];
		if (*data != NULL)
			{
			memcpy(*data, *text, *dataLength);
			return kTrue;
			}
		}

	else if (type == itsStyledTextXAtom)
		{
		ostrstream output;
		if ((name == kJXClipboardName && !WriteClipboardPrivateFormat(output, 1)) ||
			(name == dndName          && !WriteDragClipPrivateFormat(output, 1)) ||
			(name != kJXClipboardName && name != dndName))
			{
			return kFalse;
			}

		output << ends;

		char* str   = output.str();
		*data       = reinterpret_cast<unsigned char*>(str);
		*dataLength = strlen(str);
		*returnType = itsStyledTextXAtom;
		return kTrue;
		}

	else if (GetType() == kFullEditor &&
			 type == selMgr->GetDeleteSelectionXAtom() && name == dndName)
		{
		DeleteSelection();

		*data       = new unsigned char[1];
		*dataLength = 0;
		*returnType = selMgr->GetNULLXAtom();
		return kTrue;
		}

	return kFalse;
}

/******************************************************************************
 DataNotCompatible::DataNotCompatible

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

JXTEBase::DataNotCompatible::DataNotCompatible
	(
	const Atom	type,
	JXDisplay*	display
	)
	:
	JError(kDataNotCompatible, kDataNotCompatibleMsg)
{
	if (type != None)
		{
		assert( display != NULL );

		JString msg = kDataNotCompatibleMsg;
		msg += kDNCAtomMsg;
		msg += XGetAtomName(*display, type);
		msg += ")";
		SetMessage(msg, kTrue);
		}
}

/******************************************************************************
 AppendEditMenu

	Call this to let us create the Edit menu for text editing.

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

#include <jx_edit_undo.xpm>
#include <jx_edit_redo.xpm>
#include <jx_edit_cut.xpm>
#include <jx_edit_copy.xpm>
#include <jx_edit_paste.xpm>
#include <jx_edit_clear.xpm>
#include <jx_edit_clean_right_margin.xpm>
#include <jx_edit_coerce_right_margin.xpm>
#include <jx_edit_shift_left.xpm>
#include <jx_edit_shift_right.xpm>
#include <jx_edit_read_only.xpm>

JXTextMenu*
JXTEBase::AppendEditMenu
	(
	JXMenuBar*			menuBar,
	const JXMenu::Style	menuStyle,
	const JBoolean		showAdjustMarginsCmds,
	const JBoolean		allowAdjustMargins,
	const JBoolean		showToggleReadOnly,
	const JBoolean		allowToggleReadOnly
	)
{
	assert( itsEditMenu == NULL );

	// create basic menu

	itsMenuStyle = menuStyle;

	itsEditMenu = menuBar->AppendTextMenu(kEditMenuTitleStr);
	if (itsMenuStyle == JXMenu::kMacintoshStyle)
		{
		itsEditMenu->SetMenuItems(kMacEditMenuStr);
		}
	else
		{
		itsEditMenu->SetShortcuts(kEditMenuShortcutStr);
		itsEditMenu->SetMenuItems(kWinEditMenuStr);
		}

	itsEditMenu->SetItemImage(kUndoIndex,  jx_edit_undo);
	itsEditMenu->SetItemImage(kRedoIndex,  jx_edit_redo);
	itsEditMenu->SetItemImage(kCutIndex,   jx_edit_cut);
	itsEditMenu->SetItemImage(kCopyIndex,  jx_edit_copy);
	itsEditMenu->SetItemImage(kPasteIndex, jx_edit_paste);
	itsEditMenu->SetItemImage(kClearIndex, jx_edit_clear);

	itsEditMenu->SetUpdateAction(JXMenu::kDisableAll);
	ListenTo(itsEditMenu);

	// append margin adjustment commands

	if (showAdjustMarginsCmds)
		{
		itsCanAdjustMarginsFlag = allowAdjustMargins;

		const JSize itemCount = itsEditMenu->GetItemCount();
		itsEditMenu->ShowSeparatorAfter(itemCount);
		itsEditMenu->AppendItem(kCleanRightMarginStr, kFalse, kFalse, NULL, NULL, kCleanRightMarginAction);
		itsEditMenu->AppendItem(kCoerceRightMarginStr, kFalse, kFalse, NULL, NULL, kCoerceRightMarginAction);
		itsEditMenu->AppendItem(kShiftSelLeftStr, kFalse, kFalse, NULL, NULL, kShiftSelLeftAction);
		itsEditMenu->AppendItem(kShiftSelRightStr, kFalse, kFalse, NULL, NULL, kShiftSelRightAction);

		if (itsMenuStyle == JXMenu::kMacintoshStyle)
			{
			itsEditMenu->SetItemNMShortcut(itemCount+1, kCleanRightMarginMacShortcut);
			itsEditMenu->SetItemNMShortcut(itemCount+2, kCoerceRightMarginMacShortcut);
			itsEditMenu->SetItemNMShortcut(itemCount+3, kShiftSelLeftMacShortcut);
			itsEditMenu->SetItemNMShortcut(itemCount+4, kShiftSelRightMacShortcut);
			}
		else
			{
			itsEditMenu->SetItemNMShortcut(itemCount+1, kCleanRightMarginWinNMShortcut);
			itsEditMenu->SetItemShortcuts (itemCount+1, kCleanRightMarginWinShortcuts);

			itsEditMenu->SetItemNMShortcut(itemCount+2, kCoerceRightMarginWinNMShortcut);
			itsEditMenu->SetItemShortcuts (itemCount+2, kCoerceRightMarginWinShortcuts);

			itsEditMenu->SetItemNMShortcut(itemCount+3, kShiftSelLeftWinNMShortcut);
			itsEditMenu->SetItemShortcuts (itemCount+3, kShiftSelLeftWinShortcuts);

			itsEditMenu->SetItemNMShortcut(itemCount+4, kShiftSelRightWinNMShortcut);
			itsEditMenu->SetItemShortcuts (itemCount+4, kShiftSelRightWinShortcuts);
			}

		itsEditMenu->SetItemImage(itemCount+1, jx_edit_clean_right_margin);
		itsEditMenu->SetItemImage(itemCount+2, jx_edit_coerce_right_margin);
		itsEditMenu->SetItemImage(itemCount+3, jx_edit_shift_left);
		itsEditMenu->SetItemImage(itemCount+4, jx_edit_shift_right);
		}

	// append "read only" checkbox

	if (showToggleReadOnly)
		{
		itsCanToggleReadOnlyFlag = allowToggleReadOnly;

		const JSize itemCount = itsEditMenu->GetItemCount();
		itsEditMenu->ShowSeparatorAfter(itemCount);
		itsEditMenu->AppendItem(kToggleReadOnlyStr, kTrue, kFalse, NULL, NULL, kToggleReadOnlyAction);

		if (itsMenuStyle == JXMenu::kWindowsStyle)
			{
			itsEditMenu->SetItemShortcuts(itemCount+1, kToggleReadOnlyWinShortcuts);
			}

		itsEditMenu->SetItemImage(itemCount+1, jx_edit_read_only);
		}

	return itsEditMenu;
}

/******************************************************************************
 ShareEditMenu

	Call this to let us share the Edit menu with other objects.

	The JXTextMenu passed to the second version must have ID's
	assigned to the standard items:
		Undo, Redo, Cut, Copy, Paste, Clear, Select All

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

JXTextMenu*
JXTEBase::ShareEditMenu
	(
	JXTEBase*		te,
	const JBoolean	allowAdjustMargins,
	const JBoolean	allowToggleReadOnly
	)
{
	assert( itsEditMenu == NULL && te->itsEditMenu != NULL );

	itsEditMenu  = te->itsEditMenu;
	itsMenuStyle = te->itsMenuStyle;

	itsCanAdjustMarginsFlag   = allowAdjustMargins;
	itsCanToggleReadOnlyFlag  = allowToggleReadOnly;

	ListenTo(itsEditMenu);
	return itsEditMenu;
}

void
JXTEBase::ShareEditMenu
	(
	JXTextMenu*			menu,
	const JXMenu::Style	menuStyle,
	const JBoolean		allowAdjustMargins,
	const JBoolean		allowToggleReadOnly
	)
{
	assert( itsEditMenu == NULL && menu != NULL );

	itsEditMenu  = menu;
	itsMenuStyle = menuStyle;

	itsCanAdjustMarginsFlag   = allowAdjustMargins;
	itsCanToggleReadOnlyFlag  = allowToggleReadOnly;

	ListenTo(itsEditMenu);
}

/******************************************************************************
 Receive (virtual protected)

	Listen for menu update requests and menu selections.

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

void
JXTEBase::Receive
	(
	JBroadcaster*	sender,
	const Message&	message
	)
{
	if (sender == itsEditMenu && message.Is(JXMenu::kNeedsUpdate))
		{
		if (HasFocus())
			{
			UpdateEditMenu();
			}
		}
	else if (sender == itsEditMenu && message.Is(JXMenu::kItemSelected))
		{
		if (HasFocus())
			{
			const JXMenu::ItemSelected* selection =
				dynamic_cast(const JXMenu::ItemSelected*, &message);
			assert( selection != NULL );
			HandleEditMenu(selection->GetIndex());
			}
		}

	else if (sender == itsPSPrinter &&
			 message.Is(JPrinter::kPrintSetupFinished))
		{
		const JPrinter::PrintSetupFinished* info =
			dynamic_cast(const JPrinter::PrintSetupFinished*, &message);
		assert( info != NULL );
		if (info->Successful())
			{
			SetPSPrintFileName(itsPSPrinter->GetFileName());
			Print(*itsPSPrinter);
			}
		StopListening(itsPSPrinter);
		}

	else if (sender == itsPTPrinter &&
			 message.Is(JPrinter::kPrintSetupFinished))
		{
		const JPrinter::PrintSetupFinished* info =
			dynamic_cast(const JPrinter::PrintSetupFinished*, &message);
		assert( info != NULL );
		if (info->Successful())
			{
			SetPTPrintFileName(itsPTPrinter->GetFileName());
			itsPTPrinter->Print(GetText());
			}
		StopListening(itsPTPrinter);
		}

	else if (sender == itsGoToLineDialog && message.Is(JXDialogDirector::kDeactivated))
		{
		const JXDialogDirector::Deactivated* info =
			dynamic_cast(const JXDialogDirector::Deactivated*, &message);
		assert( info != NULL );
		if (info->Successful())
			{
			GoToLine(itsGoToLineDialog->GetLineIndex());
			}
		itsGoToLineDialog = NULL;
		}

	else
		{
		JXScrollableWidget::Receive(sender, message);
		JTextEditor::Receive(sender, message);
		}
}

/******************************************************************************
 UpdateEditMenu (private)

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

void
JXTEBase::UpdateEditMenu()
{
	assert( itsEditMenu != NULL );

	JString crmActionText, crm2ActionText;
	JBoolean isReadOnly;
	const JArray<JBoolean> enableFlags =
		GetCmdStatus(&crmActionText, &crm2ActionText, &isReadOnly);

	const JSize count = itsEditMenu->GetItemCount();
	for (JIndex i=1; i<=count; i++)
		{
		CmdIndex cmd;
		if (EditMenuIndexToCmd(i, &cmd))
			{
			JBoolean enable = kTrue;
			if (cmd == kCleanRightMarginCmd)
				{
				itsEditMenu->SetItemText(i, crmActionText);
				enable = itsCanAdjustMarginsFlag;
				}
			else if (cmd == kCoerceRightMarginCmd)
				{
				itsEditMenu->SetItemText(i, crm2ActionText);
				enable = itsCanAdjustMarginsFlag;
				}
			else if (cmd == kShiftSelLeftCmd ||
					 cmd == kShiftSelRightCmd)
				{
				enable = itsCanAdjustMarginsFlag;
				}
			else if (cmd == kToggleReadOnlyCmd)
				{
				if (isReadOnly)
					{
					itsEditMenu->CheckItem(i);
					}
				enable = itsCanToggleReadOnlyFlag;
				}

			itsEditMenu->SetItemEnable(i, JI2B(enable && enableFlags.GetElement(cmd)));
			}
		}
}

/******************************************************************************
 HandleEditMenu (private)

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

void
JXTEBase::HandleEditMenu
	(
	const JIndex index
	)
{
	assert( itsEditMenu != NULL );

	CmdIndex cmd;
	if (!EditMenuIndexToCmd(index, &cmd))
		{
		return;
		}

	if (cmd == kUndoCmd)
		{
		Undo();
		}
	else if (cmd == kRedoCmd)
		{
		Redo();
		}

	else if (cmd == kCutCmd)
		{
		Cut();
		}
	else if (cmd == kCopyCmd)
		{
		Copy();
		}
	else if (cmd == kPasteCmd)
		{
		Paste();
		}
	else if (cmd == kDeleteSelCmd)
		{
		DeleteSelection();
		}

	else if (cmd == kSelectAllCmd)
		{
		SelectAll();
		}

	else if (cmd == kCleanRightMarginCmd)
		{
		JIndexRange range;
		CleanRightMargin(kFalse, &range);
		}
	else if (cmd == kCoerceRightMarginCmd)
		{
		JIndexRange range;
		CleanRightMargin(kTrue, &range);
		}
	else if (cmd == kShiftSelLeftCmd)
		{
		TabSelectionLeft();
		}
	else if (cmd == kShiftSelRightCmd)
		{
		TabSelectionRight();
		}

	else if (cmd == kToggleReadOnlyCmd)
		{
		const Type type = GetType();
		if (type == kFullEditor)
			{
			SetType(kSelectableText);
			}
		else if (type == kSelectableText)
			{
			SetType(kFullEditor);
			}
		// don't change kStaticText
		}
}

/******************************************************************************
 Edit menu index <-> cmd

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

#define ClassName    JXTEBase
#define IndexToCmdFn EditMenuIndexToCmd
#define CmdToIndexFn EditMenuCmdToIndex
#define MenuVar      itsEditMenu
#define CmdCount     kEditMenuItemCount
#define CmdIDList    kEditMenuItemInfo
#include <JXMenuItemIDUtil.th>
#undef ClassName
#undef IndexToCmdFn
#undef CmdToIndexFn
#undef MenuVar
#undef CmdCount
#undef CmdIDList

/******************************************************************************
 SetPSPrinter

	Call this to provide a JXPSPrinter object for this text.  This object
	does *not* take ownership of the printer object.

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

void
JXTEBase::SetPSPrinter
	(
	JXPSPrinter* p
	)
{
	if (itsPSPrinter != NULL)
		{
		StopListening(itsPSPrinter);
		}
	itsPSPrinter = p;
}

/******************************************************************************
 GetPSPrintFileName

	Call this to get the file name used in the Print Setup dialog.

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

const JString&
JXTEBase::GetPSPrintFileName()
	const
{
	if (itsPSPrintName == NULL)
		{
		JXTEBase* me = const_cast<JXTEBase*>(this);
		me->itsPSPrintName = new JString;
		assert( itsPSPrintName != NULL );
		}

	return *itsPSPrintName;
}

/******************************************************************************
 SetPSPrintFileName

	Call this to set the file name used in the Print Setup dialog.

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

void
JXTEBase::SetPSPrintFileName
	(
	const JCharacter* fileName
	)
{
	GetPSPrintFileName();		// create itsPSPrintName
	*itsPSPrintName = fileName;
}

/******************************************************************************
 HandlePSPageSetup

	You must call SetPSPrinter() before using this routine.

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

void
JXTEBase::HandlePSPageSetup()
{
	assert( itsPSPrinter != NULL );

	itsPSPrinter->BeginUserPageSetup();
}

/******************************************************************************
 PrintPS

	You must call SetPSPrinter() before using this routine.

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

void
JXTEBase::PrintPS()
{
	assert( itsPSPrinter != NULL );

	itsPSPrinter->SetFileName(GetPSPrintFileName());
	itsPSPrinter->BeginUserPrintSetup();
	ListenTo(itsPSPrinter);
}

/******************************************************************************
 Print footer (virtual protected)

	Overrides of JTextEditor functions.

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

JCoordinate
JXTEBase::GetPrintFooterHeight
	(
	JPagePrinter& p
	)
	const
{
	return JRound(1.5 * p.GetLineHeight());
}

void
JXTEBase::DrawPrintFooter
	(
	JPagePrinter&		p,
	const JCoordinate	footerHeight
	)
{
	JRect pageRect = p.GetPageRect();
	pageRect.top   = pageRect.bottom - footerHeight;

	JString pageNumberStr(p.GetPageIndex(), 0);
	pageNumberStr.Prepend("Page ");

	p.String(pageRect, pageNumberStr,
			 JPainter::kHAlignCenter, JPainter::kVAlignBottom);
}

/******************************************************************************
 SetPTPrinter

	Call this to provide a JXPTPrinter object for this text.  This object
	does *not* take ownership of the printer object.

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

void
JXTEBase::SetPTPrinter
	(
	JXPTPrinter* p
	)
{
	if (itsPTPrinter != NULL)
		{
		StopListening(itsPTPrinter);
		}
	itsPTPrinter = p;
}

/******************************************************************************
 GetPTPrintFileName

	Call this to get the file name used in the Print Setup dialog.

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

const JString&
JXTEBase::GetPTPrintFileName()
	const
{
	if (itsPTPrintName == NULL)
		{
		JXTEBase* me = const_cast<JXTEBase*>(this);
		me->itsPTPrintName = new JString;
		assert( itsPTPrintName != NULL );
		}

	return *itsPTPrintName;
}

/******************************************************************************
 SetPTPrintFileName

	Call this to set the file name used in the Print Setup dialog.

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

void
JXTEBase::SetPTPrintFileName
	(
	const JCharacter* fileName
	)
{
	GetPTPrintFileName();		// create itsPTPrintName
	*itsPTPrintName = fileName;
}

/******************************************************************************
 HandlePTPageSetup

	You must call SetPTPrinter() before using this routine.

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

void
JXTEBase::HandlePTPageSetup()
{
	assert( itsPTPrinter != NULL );

	itsPTPrinter->BeginUserPageSetup();
}

/******************************************************************************
 PrintPT

	You must call SetPTPrinter() before using this routine.

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

void
JXTEBase::PrintPT()
{
	assert( itsPTPrinter != NULL );

	itsPTPrinter->SetFileName(GetPTPrintFileName());
	itsPTPrinter->BeginUserPrintSetup();
	ListenTo(itsPTPrinter);
}

/******************************************************************************
 AskForLine

	Opens dialog window to ask user which line to go to.

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

void
JXTEBase::AskForLine()
{
	assert( itsGoToLineDialog == NULL );

	const JIndex charIndex = GetInsertionIndex();
	const JIndex lineIndex = GetLineForChar(charIndex);
	const JSize lineCount  = GetLineCount();

	JXDirector* sup = (GetWindow())->GetDirector();
	itsGoToLineDialog = new JXGoToLineDialog(sup, lineIndex, lineCount);
	assert( itsGoToLineDialog != NULL );
	itsGoToLineDialog->BeginDialog();
	ListenTo(itsGoToLineDialog);
}
