/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2003-2007  Sven Eberhardt
 * Copyright (c) 2005, 2007, 2009-2011  Günther Brammer
 * Copyright (c) 2007  Peter Wortmann
 * Copyright (c) 2010  Benjamin Herr
 * Copyright (c) 2011  Nicolas Hake
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de
 *
 * Portions might be copyrighted by other authors who have contributed
 * to OpenClonk.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 * See isc_license.txt for full license and disclaimer.
 *
 * "Clonk" is a registered trademark of Matthes Bender.
 * See clonk_trademark_license.txt for full license.
 */
// generic user interface
// room for textual deconvolution

#include <C4Include.h>
#include <C4Gui.h>

#include <C4FullScreen.h>
#include <C4LoaderScreen.h>
#include <C4Application.h>
#include <C4MouseControl.h>
#include <C4GraphicsResource.h>

namespace C4GUI
{

	const char *Edit::CursorRepresentation = "\xC2\xA6"; // U+00A6 BROKEN BAR

	namespace
	{
		inline bool IsUtf8ContinuationByte(char c)
		{
			return (c & 0xC0) == 0x80;
		}
		inline bool IsUtf8StartByte(char c)
		{
			return (c & 0xC0) == 0xC0;
		}
	}

// ----------------------------------------------------
// Edit

	Edit::Edit(const C4Rect &rtBounds, bool fFocusEdit) : Control(rtBounds), iCursorPos(0), iSelectionStart(0), iSelectionEnd(0), fLeftBtnDown(false)
	{
		// create an initial buffer
		Text = new char[256];
		iBufferSize = 256;
		*Text = 0;
		// def vals
		iMaxTextLength = 255;
		iCursorPos = iSelectionStart = iSelectionEnd = 0;
		iXScroll = 0;
		pFont = &::GraphicsResource.TextFont;
		dwBGClr = C4GUI_EditBGColor;
		dwFontClr = C4GUI_EditFontColor;
		dwBorderColor = 0; // default border
		cPasswordMask = 0;
		// apply client margin
		UpdateOwnPos();
		// add context handler
		SetContextHandler(new CBContextHandler<Edit>(this, &Edit::OnContext));
		// add key handlers
		C4CustomKey::Priority eKeyPrio = fFocusEdit ? C4CustomKey::PRIO_FocusCtrl : C4CustomKey::PRIO_Ctrl;
		pKeyCursorBack  = RegisterCursorOp(COP_BACK  , K_BACK  , "GUIEditCursorBack", eKeyPrio);
		pKeyCursorDel   = RegisterCursorOp(COP_DELETE, K_DELETE, "GUIEditCursorDel",eKeyPrio);
		pKeyCursorLeft  = RegisterCursorOp(COP_LEFT  , K_LEFT  , "GUIEditCursorLeft",eKeyPrio);
		pKeyCursorRight = RegisterCursorOp(COP_RIGHT , K_RIGHT , "GUIEditCursorRight", eKeyPrio);
		pKeyCursorHome  = RegisterCursorOp(COP_HOME  , K_HOME  , "GUIEditCursorHome", eKeyPrio);
		pKeyCursorEnd   = RegisterCursorOp(COP_END   , K_END   , "GUIEditCursorEnd", eKeyPrio);
		pKeyEnter = new C4KeyBinding(C4KeyCodeEx(K_RETURN), "GUIEditConfirm", KEYSCOPE_Gui,
		                             new ControlKeyCB<Edit>(*this, &Edit::KeyEnter), eKeyPrio);
		pKeyCopy = new C4KeyBinding(C4KeyCodeEx(KEY_C, KEYS_Control), "GUIEditCopy", KEYSCOPE_Gui,
		                            new ControlKeyCB<Edit>(*this, &Edit::KeyCopy), eKeyPrio);
		pKeyPaste = new C4KeyBinding(C4KeyCodeEx(KEY_V, KEYS_Control), "GUIEditPaste", KEYSCOPE_Gui,
		                             new ControlKeyCB<Edit>(*this, &Edit::KeyPaste), eKeyPrio);
		pKeyCut = new C4KeyBinding(C4KeyCodeEx(KEY_X, KEYS_Control), "GUIEditCut", KEYSCOPE_Gui,
		                           new ControlKeyCB<Edit>(*this, &Edit::KeyCut), eKeyPrio);
		pKeySelAll = new C4KeyBinding(C4KeyCodeEx(KEY_A, KEYS_Control), "GUIEditSelAll", KEYSCOPE_Gui,
		                              new ControlKeyCB<Edit>(*this, &Edit::KeySelectAll), eKeyPrio);
	}

	Edit::~Edit()
	{
		delete[] Text;
		delete pKeyCursorBack;
		delete pKeyCursorDel;
		delete pKeyCursorLeft;
		delete pKeyCursorRight;
		delete pKeyCursorHome;
		delete pKeyCursorEnd;
		delete pKeyEnter;
		delete pKeyCopy;
		delete pKeyPaste;
		delete pKeyCut;
		delete pKeySelAll;
	}

	class C4KeyBinding *Edit::RegisterCursorOp(CursorOperation op, C4KeyCode key, const char *szName, C4CustomKey::Priority eKeyPrio)
	{
		// register same op for all shift states; distinction will be done in handling proc
		C4CustomKey::CodeList KeyList;
		KeyList.push_back(C4KeyCodeEx(key));
		KeyList.push_back(C4KeyCodeEx(key, KEYS_Shift));
		KeyList.push_back(C4KeyCodeEx(key, KEYS_Control));
		KeyList.push_back(C4KeyCodeEx(key, C4KeyShiftState(KEYS_Shift | KEYS_Control)));
		return new C4KeyBinding(KeyList, szName, KEYSCOPE_Gui, new ControlKeyCBExPassKey<Edit, CursorOperation>(*this, op, &Edit::KeyCursorOp), eKeyPrio);
	}

	int32_t Edit::GetDefaultEditHeight()
	{
		// edit height for default font
		return GetCustomEditHeight(&::GraphicsResource.TextFont);
	}

	int32_t Edit::GetCustomEditHeight(CStdFont *pUseFont)
	{
		// edit height for custom font: Make it so edits and wooden labels have same height
		return Max<int32_t>(pUseFont->GetLineHeight()+3, C4GUI_MinWoodBarHgt);
	}

	void Edit::ClearText()
	{
		// free oversized buffers
		if (iBufferSize > 256)
		{
			delete[] Text;
			Text = new char[256];
			iBufferSize = 256;
		}
		// clear text
		*Text=0;
		// reset cursor and selection
		iCursorPos = iSelectionStart = iSelectionEnd = 0;
		iXScroll = 0;
	}

	void Edit::Deselect()
	{
		// reset selection
		iSelectionStart = iSelectionEnd = 0;
		// cursor might have moved: ensure it is shown
		dwLastInputTime=GetTime();
	}

	void Edit::DeleteSelection()
	{
		// move end text to front
		int32_t iSelBegin = Min(iSelectionStart, iSelectionEnd), iSelEnd = Max(iSelectionStart, iSelectionEnd);
		if (iSelectionStart == iSelectionEnd) return;
		memmove(Text + iSelBegin, Text + iSelEnd, strlen(Text + iSelEnd)+1);
		// adjust cursor pos
		if (iCursorPos > iSelBegin) iCursorPos = Max(iSelBegin, iCursorPos - iSelEnd + iSelBegin);
		// cursor might have moved: ensure it is shown
		dwLastInputTime=GetTime();
		// nothing selected
		iSelectionStart = iSelectionEnd = iSelBegin;
	}

	bool Edit::InsertText(const char *szText, bool fUser)
	{
		// empty previous selection
		if (iSelectionStart != iSelectionEnd) DeleteSelection();
		// check buffer length
		int32_t iTextLen = SLen(szText);
		int32_t iTextEnd = SLen(Text);
		bool fBufferOK = (iTextLen + iTextEnd <= (iMaxTextLength-1));
		if (!fBufferOK) iTextLen -= iTextEnd+iTextLen - (iMaxTextLength-1);
		if (iTextLen <= 0) return false;
		// ensure buffer is large enough
		EnsureBufferSize(iTextEnd + iTextLen + 1);
		// move down text buffer after cursor pos (including trailing zero-char)
		int32_t i;
		for (i=iTextEnd; i>=iCursorPos; --i) Text[i + iTextLen] = Text[i];
		// insert buffer into text
		for (i=iTextLen; i; --i) Text[iCursorPos + i - 1] = szText[i - 1];
		if (fUser)
		{
			// advance cursor
			iCursorPos += iTextLen;
			// cursor moved: ensure it is shown
			dwLastInputTime=GetTime();
			ScrollCursorInView();
		}
		// done; return whether everything was inserted
		return fBufferOK;
	}

	int32_t Edit::GetCharPos(int32_t iControlXPos)
	{
		// client offset
		iControlXPos -= rcClientRect.x - rcBounds.x - iXScroll;
		// well, not exactly the best idea...maybe add a new fn to the gfx system?
		// summing up char widths is no good, because there might be spacings between characters
		// 2do: optimize this
		if (cPasswordMask)
		{
			int32_t w, h; char strMask[2] = { cPasswordMask, 0 };
			pFont->GetTextExtent(strMask, w, h, false);
			return BoundBy<int32_t>((iControlXPos + w/2) / w, 0, SLen(Text));
		}
		int32_t i = 0;
		for (int32_t iLastW = 0, w,h; Text[i]; ++i)
		{
			int oldi = i;
			if (IsUtf8StartByte(Text[oldi]))
				while (IsUtf8ContinuationByte(Text[++i + 1])) /* EMPTY */;
			char c=Text[i+1]; Text[i+1]=0; pFont->GetTextExtent(Text, w, h, false); Text[i+1]=c;
			if (w - (w-iLastW)/2 >= iControlXPos) return oldi;
			iLastW = w;
		}
		return i;
	}

	void Edit::EnsureBufferSize(int32_t iMinBufferSize)
	{
		// realloc buffer if necessary
		if (iBufferSize < iMinBufferSize)
		{
			// get new buffer size (rounded up to multiples of 256)
			iMinBufferSize = ((iMinBufferSize - 1) & ~0xff) + 0x100;
			// fill new buffer
			char *pNewBuffer = new char[iMinBufferSize];
			SCopy(Text, pNewBuffer);
			// apply new buffer
			delete[] Text; Text = pNewBuffer;
			iBufferSize = iMinBufferSize;
		}
	}

	void Edit::ScrollCursorInView()
	{
		if (rcClientRect.Wdt<5) return;
		// get position of cursor
		int32_t iScrollOff = Min<int32_t>(20, rcClientRect.Wdt/3);
		int32_t w,h;
		if (!cPasswordMask)
		{
			char c=Text[iCursorPos]; Text[iCursorPos]=0; pFont->GetTextExtent(Text, w, h, false); Text[iCursorPos]=c;
		}
		else
		{
			StdStrBuf Buf; Buf.AppendChars(cPasswordMask, iCursorPos);
			pFont->GetTextExtent(Buf.getData(), w, h, false);
		}
		// need to scroll?
		while (w-iXScroll < rcClientRect.Wdt/5 && w<iScrollOff+iXScroll && iXScroll > 0)
		{
			// left
			iXScroll = Max(iXScroll - Min(100, rcClientRect.Wdt/4), 0);
		}
		while (w-iXScroll >= rcClientRect.Wdt/5 && w>=rcClientRect.Wdt-iScrollOff+iXScroll)
		{
			// right
			iXScroll += Min(100, rcClientRect.Wdt/4);
		}
	}

	bool Edit::DoFinishInput(bool fPasting, bool fPastingMore)
	{
		// do OnFinishInput callback and process result - returns whether pasting operation should be continued
		InputResult eResult = OnFinishInput(fPasting, fPastingMore);
		switch (eResult)
		{
		case IR_None: // do nothing and continue pasting
			return true;

		case IR_CloseDlg: // stop any pastes and close parent dialog successfully
		{
			Dialog *pDlg = GetDlg();
			if (pDlg) pDlg->UserClose(true);
			break;
		}

		case IR_CloseEdit: // stop any pastes and remove this control
			delete this;
			break;

		case IR_Abort: // do nothing and stop any pastes
			break;
		}
		// input has been handled; no more pasting please
		return false;
	}

	bool Edit::Copy()
	{
		// get selected range
		int32_t iSelBegin = Min(iSelectionStart, iSelectionEnd), iSelEnd = Max(iSelectionStart, iSelectionEnd);
		if (iSelBegin == iSelEnd) return false;
		StdStrBuf buf;
		// allocate a global memory object for the text.
		buf.Append(Text+iSelBegin, iSelEnd-iSelBegin);
		if (cPasswordMask)
			memset(buf.getMData(), cPasswordMask, buf.getLength());

		return Application.Copy(buf);
	}

	bool Edit::Cut()
	{
		// copy text
		if (!Copy()) return false;
		// delete copied text
		DeleteSelection();
		// done, success
		return true;
	}

	bool Edit::Paste()
	{
		bool fSuccess = false;
		// check clipboard contents
		if(!Application.IsClipboardFull()) return false;
		StdStrBuf text(Application.Paste());
		char * szText = text.getMData();
		if (text)
		{
			fSuccess = !!*szText;
			// replace any '|'
			int32_t iLBPos=0, iLBPos2;
			// caution when inserting line breaks: Those must be stripped, and sent as Enter-commands
			iLBPos=0;
			for (;;)
			{
				iLBPos = SCharPos(0x0d, szText);
				iLBPos2 = SCharPos(0x0a, szText);
				if (iLBPos<0 && iLBPos2<0) break; // no more linebreaks
				if (iLBPos2>=0 && (iLBPos2<iLBPos || iLBPos<0)) iLBPos = iLBPos2;
				if (!iLBPos) { ++szText; continue; } // empty line
				szText[iLBPos]=0x00;
				if (!InsertText(szText, true)) fSuccess=false; // if the buffer was too long, still try to insert following stuff (don't abort just b/c one line was too long)
				szText += iLBPos+1;
				iLBPos=0;
				if (!DoFinishInput(true, !!*szText))
				{
					// k, pasted
					return true;
				}
			}
			// insert new text (may fail due to overfull buffer, in which case parts of the text will be inserted)
			if (*szText) fSuccess = fSuccess && InsertText(szText, true);
		}
		// return whether insertion was successful
		return fSuccess;
	}

	bool IsWholeWordSpacer(unsigned char c)
	{
		// characters that make up a space between words
		// the extended characters are all seen a letters, because they vary in different
		// charsets (danish, french, etc.) and are likely to represent localized letters
		return !Inside<char>(c, 'A', 'Z')
		       && !Inside<char>(c, 'a', 'z')
		       && !Inside<char>(c, '0', '9')
		       && c!='_' && c<127;
	}

	bool Edit::KeyEnter()
	{
		DoFinishInput(false, false);
		// whatever happens: Enter key has been processed
		return true;
	}

	bool Edit::KeyCursorOp(const C4KeyCodeEx &key, const CursorOperation &op)
	{
		bool fShift = !!(key.dwShift & KEYS_Shift);
		bool fCtrl = !!(key.dwShift & KEYS_Control);
		// any selection?
		if (iSelectionStart != iSelectionEnd)
		{
			// special handling: backspace/del with selection (delete selection)
			if (op == COP_BACK || op == COP_DELETE) { DeleteSelection(); return true; }
			// no shift pressed: clear selection (even if no cursor movement is done)
			if (!fShift) Deselect();
		}
		// movement or regular/word deletion
		int32_t iMoveDir = 0, iMoveLength = 0;
		if (op == COP_LEFT && iCursorPos) iMoveDir = -1;
		else if (op == COP_RIGHT && (uint32_t)iCursorPos < SLen(Text)) iMoveDir = +1;
		else if (op == COP_BACK && iCursorPos && !fShift) iMoveDir = -1;
		else if (op == COP_DELETE && (uint32_t)iCursorPos < SLen(Text) && !fShift) iMoveDir = +1;
		else if (op == COP_HOME) iMoveLength = -iCursorPos;
		else if (op == COP_END) iMoveLength = SLen(Text)-iCursorPos;
		if (iMoveDir || iMoveLength)
		{
			// evaluate move length? (not home+end)
			if (iMoveDir)
			{
				if (fCtrl)
				{
					// move one word
					iMoveLength = 0;
					bool fNoneSpaceFound = false, fSpaceFound = false;;
					while (iCursorPos + iMoveLength + iMoveDir >= 0 && (uint32_t)(iCursorPos + iMoveLength + iMoveDir) <= SLen(Text))
						if (IsWholeWordSpacer(Text[iCursorPos + iMoveLength + (iMoveDir-1)/2]))
						{
							// stop left of a complete word
							if (fNoneSpaceFound && iMoveDir<0) break;
							// continue
							fSpaceFound = true;
							iMoveLength += iMoveDir;
						}
						else
						{
							// stop right of spacings complete word
							if (fSpaceFound && iMoveDir > 0) break;
							// continue
							fNoneSpaceFound = true;
							iMoveLength += iMoveDir;
						}
				}
				else
				{
					// Handle UTF-8
					iMoveLength = iMoveDir;
					while (IsUtf8ContinuationByte(Text[iCursorPos + iMoveLength])) iMoveLength += Sign(iMoveLength);
				}
			}
			// delete stuff
			if (op == COP_BACK || op == COP_DELETE)
			{
				// delete: make backspace command of it
				if (op == COP_DELETE) { iCursorPos += iMoveLength; iMoveLength = -iMoveLength; }
				// move end of string up
				char *c; for (c = Text+iCursorPos; *c; ++c) *(c+iMoveLength) = *c;
				// terminate string
				*(c+iMoveLength) = 0;
				assert(IsValidUtf8(Text));
			}
			else if (fShift)
			{
				// shift+arrow key: make/adjust selection
				if (iSelectionStart == iSelectionEnd) iSelectionStart = iCursorPos;
				iSelectionEnd = iCursorPos + iMoveLength;
			}
			else
				// simple cursor movement: clear any selection
				if (iSelectionStart != iSelectionEnd) Deselect();
			// adjust cursor pos
			iCursorPos += iMoveLength;
		}
		// show cursor
		dwLastInputTime=GetTime();
		ScrollCursorInView();
		// operation recognized
		return true;
	}

	bool Edit::CharIn(const char * c)
	{
		// no control codes
		if (((unsigned char)(c[0]))<' ' || c[0]==0x7f) return false;
		// no '|'
		if (c[0]=='|') return false;
		// all extended characters are OK
		// insert character at cursor position
		return InsertText(c, true);
	}

	void Edit::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam)
	{
		// inherited first - this may give focus to this element
		Control::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
		// update dragging area
		int32_t iPrevCursorPos = iCursorPos;
		// dragging area is updated by drag proc
		// process left down and up
		switch (iButton)
		{
		case C4MC_Button_LeftDown:
			// mark button as being down
			fLeftBtnDown = true;
			// set selection start
			iSelectionStart = iSelectionEnd = GetCharPos(iX);
			// set cursor pos here, too
			iCursorPos = iSelectionStart;
			// remember drag target
			// no dragging movement will be done w/o drag component assigned
			// but text selection should work even if the user goes outside the component
			if (!rMouse.pDragElement) rMouse.pDragElement = this;
			break;

		case C4MC_Button_LeftUp:
			// only if button was down... (might have dragged here)
			if (fLeftBtnDown)
			{
				// it's now up :)
				fLeftBtnDown = false;
				// set cursor to this pos
				iCursorPos = iSelectionEnd;
			}
			break;

		case C4MC_Button_LeftDouble:
		{
			// word selection
			// get character pos (half-char-offset, use this to allow selection at half-space-offset around a word)
			int32_t iCharPos = GetCharPos(iX);
			// was space? try left character
			if (IsWholeWordSpacer(Text[iCharPos]))
			{
				if (!iCharPos) break;
				if (IsWholeWordSpacer(Text[--iCharPos])) break;
			}
			// search ending of word left and right
			// bounds-check is done by zero-char at end, which is regarded as a spacer
			iSelectionStart = iCharPos; iSelectionEnd = iCharPos + 1;
			while (iSelectionStart > 0 && !IsWholeWordSpacer(Text[iSelectionStart-1])) --iSelectionStart;
			while (!IsWholeWordSpacer(Text[iSelectionEnd])) ++iSelectionEnd;
			// set cursor pos to end of selection
			iCursorPos = iSelectionEnd;
			// ignore last btn-down-selection
			fLeftBtnDown = false;
		}
		break;
		case C4MC_Button_MiddleDown:
			// set selection start
			iSelectionStart = iSelectionEnd = GetCharPos(iX);
			// set cursor pos here, too
			iCursorPos = iSelectionStart;
#ifndef _WIN32
			// Insert primary selection
			InsertText(Application.Paste(false).getData(), true);
#endif
			break;
		};
		// scroll cursor in view
		if (iPrevCursorPos != iCursorPos) ScrollCursorInView();
	}

	void Edit::DoDragging(CMouse &rMouse, int32_t iX, int32_t iY, DWORD dwKeyParam)
	{
		// update cursor pos
		int32_t iPrevCursorPos = iCursorPos;
		iCursorPos = iSelectionEnd = GetCharPos(iX);
		// scroll cursor in view
		if (iPrevCursorPos != iCursorPos) ScrollCursorInView();
	}

	void Edit::OnGetFocus(bool fByMouse)
	{
		// inherited
		Control::OnGetFocus(fByMouse);
		// select all
		iSelectionStart=0; iSelectionEnd=iCursorPos=SLen(Text);
		// begin with a flashing cursor
		dwLastInputTime=GetTime();
	}

	void Edit::OnLooseFocus()
	{
		// clear selection
		iSelectionStart = iSelectionEnd = 0;
		// inherited
		Control::OnLooseFocus();
	}

	void Edit::DrawElement(C4TargetFacet &cgo)
	{
		// draw background
		pDraw->DrawBoxDw(cgo.Surface, cgo.TargetX+rcBounds.x,cgo.TargetY+rcBounds.y,rcBounds.x+rcBounds.Wdt+cgo.TargetX-1,rcClientRect.y+rcClientRect.Hgt+cgo.TargetY, dwBGClr);
		// draw frame
		if (dwBorderColor)
		{
			int32_t x1=cgo.TargetX+rcBounds.x,y1=cgo.TargetY+rcBounds.y,x2=x1+rcBounds.Wdt,y2=y1+rcBounds.Hgt;
			pDraw->DrawFrameDw(cgo.Surface, x1, y1, x2, y2-1, dwBorderColor);
			pDraw->DrawFrameDw(cgo.Surface, x1+1, y1+1, x2-1, y2-2, dwBorderColor);
		}
		else
			// default frame color
			Draw3DFrame(cgo);
		// clipping
		int cx0,cy0,cx1,cy1; bool fClip, fOwnClip;
		fClip = pDraw->GetPrimaryClipper(cx0,cy0,cx1,cy1);
		float nclx1 = rcClientRect.x+cgo.TargetX-2, ncly1 = rcClientRect.y+cgo.TargetY, nclx2 = rcClientRect.x+rcClientRect.Wdt+cgo.TargetX+1, ncly2 = rcClientRect.y+rcClientRect.Hgt+cgo.TargetY;
		pDraw->ApplyZoom(nclx1, ncly1);
		pDraw->ApplyZoom(nclx2, ncly2);
		fOwnClip = pDraw->SetPrimaryClipper(nclx1, ncly1, nclx2, ncly2);
		// get usable height of edit field
		int32_t iHgt = pFont->GetLineHeight(), iY0;
		if (rcClientRect.Hgt <= iHgt)
		{
			// very narrow edit field: use all of it
			iHgt=rcClientRect.Hgt;
			iY0=rcClientRect.y;
		}
		else
		{
			// normal edit field: center text vertically
			iY0 = rcClientRect.y+(rcClientRect.Hgt-iHgt)/2+1;
			// don't overdo it with selection mark
			iHgt-=2;
		}
		// get text to draw, apply password mask if neccessary
		StdStrBuf Buf; char *pDrawText;
		if (cPasswordMask)
		{
			Buf.AppendChars(cPasswordMask, SLen(Text));
			pDrawText = Buf.getMData();
		}
		else
			pDrawText = Text;
		// draw selection
		if (iSelectionStart != iSelectionEnd)
		{
			// get selection range
			int32_t iSelBegin = Min(iSelectionStart, iSelectionEnd);
			int32_t iSelEnd = Max(iSelectionStart, iSelectionEnd);
			// get offsets in text
			int32_t iSelX1, iSelX2, h;
			char c = pDrawText[iSelBegin]; pDrawText[iSelBegin]=0; pFont->GetTextExtent(pDrawText, iSelX1, h, false); pDrawText[iSelBegin]=c;
			c = pDrawText[iSelEnd]; pDrawText[iSelEnd]=0; pFont->GetTextExtent(pDrawText, iSelX2, h, false); pDrawText[iSelEnd]=c;
			iSelX1 -= iXScroll; iSelX2 -= iXScroll;
			// draw selection box around it
			pDraw->DrawBoxDw(cgo.Surface, cgo.TargetX+rcClientRect.x+iSelX1,cgo.TargetY+iY0,rcClientRect.x+iSelX2-1+cgo.TargetX,iY0+iHgt-1+cgo.TargetY,0x7f7f7f00);
		}
		// draw edit text
		pDraw->TextOut(pDrawText, *pFont, 1.0f, cgo.Surface, rcClientRect.x + cgo.TargetX - iXScroll, iY0 + cgo.TargetY - 1, dwFontClr, ALeft, false);
		// draw cursor
		if (HasDrawFocus() && !(((dwLastInputTime-GetTime())/500)%2))
		{
			char cAtCursor = pDrawText[iCursorPos]; pDrawText[iCursorPos]=0; int32_t w,h,wc;
			pFont->GetTextExtent(pDrawText, w, h, false);
			pDrawText[iCursorPos] = cAtCursor;
			pFont->GetTextExtent(CursorRepresentation, wc, h, false); wc/=2;
			pDraw->TextOut(CursorRepresentation, *pFont, 1.5f, cgo.Surface, rcClientRect.x + cgo.TargetX + w - wc - iXScroll, iY0 + cgo.TargetY - h/3, dwFontClr, ALeft, false);
		}
		// unclip
		if (fOwnClip)
		{
			if (fClip) pDraw->SetPrimaryClipper(cx0,cy0,cx1,cy1);
			else pDraw->NoPrimaryClipper();
		}
	}

	void Edit::SelectAll()
	{
		// safety: no text?
		if (!Text) return;
		// select all
		iSelectionStart = 0;
		iSelectionEnd = iCursorPos = SLen(Text);
	}

	ContextMenu *Edit::OnContext(C4GUI::Element *pListItem, int32_t iX, int32_t iY)
	{
		// safety: no text?
		if (!Text) return NULL;
		// create context menu
		ContextMenu *pCtx = new ContextMenu();
		// fill with any valid items
		// get selected range
		uint32_t iSelBegin = Min(iSelectionStart, iSelectionEnd), iSelEnd = Max(iSelectionStart, iSelectionEnd);
		bool fAnythingSelected = (iSelBegin != iSelEnd);
		if (fAnythingSelected)
		{
			pCtx->AddItem(LoadResStr("IDS_DLG_CUT"), LoadResStr("IDS_DLGTIP_CUT"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxCut));
			pCtx->AddItem(LoadResStr("IDS_DLG_COPY"), LoadResStr("IDS_DLGTIP_COPY"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxCopy));
		}
		if (Application.IsClipboardFull())
			pCtx->AddItem(LoadResStr("IDS_DLG_PASTE"), LoadResStr("IDS_DLGTIP_PASTE"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxPaste));

		if (fAnythingSelected)
			pCtx->AddItem(LoadResStr("IDS_DLG_CLEAR"), LoadResStr("IDS_DLGTIP_CLEAR"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxClear));
		if (*Text && (iSelBegin!=0 || iSelEnd!=SLen(Text)))
			pCtx->AddItem(LoadResStr("IDS_DLG_SELALL"), LoadResStr("IDS_DLGTIP_SELALL"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxSelAll));
		// return ctx menu
		return pCtx;
	}

	bool Edit::GetCurrentWord(char *szTargetBuf, int32_t iMaxTargetBufLen)
	{
		// get word before cursor pos (for nick completion)
		if (!Text || iCursorPos<=0) return false;
		int32_t iPos = iCursorPos;
		while (iPos>0)
				if (IsWholeWordSpacer(Text[iPos-1])) break; else --iPos;
		SCopy(Text + iPos, szTargetBuf, Min(iCursorPos - iPos, iMaxTargetBufLen));
		return !!*szTargetBuf;
	}


// ----------------------------------------------------
// RenameEdit

	RenameEdit::RenameEdit(Label *pLabel) : Edit(pLabel->GetBounds(), true), fFinishing(false), pForLabel(pLabel)
	{
		// ctor - construct for label
		assert(pForLabel);
		pForLabel->SetVisibility(false);
		InsertText(pForLabel->GetText(), true);
		// put self into place
		Container *pCont = pForLabel->GetParent();
		assert(pCont);
		pCont->AddElement(this);
		Dialog *pDlg = GetDlg();
		if (pDlg)
		{
			pPrevFocusCtrl = pDlg->GetFocus();
			pDlg->SetFocus(this, false);
		}
		else pPrevFocusCtrl=NULL;
		// key binding for rename abort
		C4CustomKey::CodeList keys;
		keys.push_back(C4KeyCodeEx(K_ESCAPE));
		if (Config.Controls.GamepadGuiControl)
		{
			keys.push_back(C4KeyCodeEx(KEY_Gamepad(0, KEY_JOY_AnyHighButton)));
		}
		pKeyAbort = new C4KeyBinding(keys, "GUIRenameEditAbort", KEYSCOPE_Gui,
		                             new ControlKeyCB<RenameEdit>(*this, &RenameEdit::KeyAbort), C4CustomKey::PRIO_FocusCtrl);
	}

	RenameEdit::~RenameEdit()
	{
		delete pKeyAbort;
	}

	void RenameEdit::Abort()
	{
		OnCancelRename();
		FinishRename();
	}

	Edit::InputResult RenameEdit::OnFinishInput(bool fPasting, bool fPastingMore)
	{
		// any text?
		if (!Text || !*Text)
		{
			// OK without text is regarded as abort
			OnCancelRename();
			FinishRename();
		}
		else switch (OnOKRename(Text))
			{
			case RR_Invalid:
			{
				// new name was not accepted: Continue editing
				Dialog *pDlg = GetDlg();
				if (pDlg) if (pDlg->GetFocus() != this) pDlg->SetFocus(this, false);
				SelectAll();
				break;
			}

			case RR_Accepted:
				// okay, rename to that text
				FinishRename();
				break;

			case RR_Deleted:
				// this is invalid; don't do anything!
				break;
			}
		return IR_Abort;
	}

	void RenameEdit::FinishRename()
	{
		// done: restore stuff
		fFinishing = true;
		pForLabel->SetVisibility(true);
		Dialog *pDlg = GetDlg();
		if (pDlg && pPrevFocusCtrl) pDlg->SetFocus(pPrevFocusCtrl, false);
		delete this;
	}

	void RenameEdit::OnLooseFocus()
	{
		Edit::OnLooseFocus();
		// callback when control looses focus: OK input
		if (!fFinishing) OnFinishInput(false, false);
	}



// ----------------------------------------------------
// LabeledEdit

	LabeledEdit::LabeledEdit(const C4Rect &rcBounds, const char *szName, bool fMultiline, const char *szPrefText, CStdFont *pUseFont, uint32_t dwTextClr)
			: C4GUI::Window()
	{
		if (!pUseFont) pUseFont = &(::GraphicsResource.TextFont);
		SetBounds(rcBounds);
		ComponentAligner caMain(GetClientRect(), 0,0, true);
		int32_t iLabelWdt=100, iLabelHgt=24;
		pUseFont->GetTextExtent(szName, iLabelWdt, iLabelHgt, true);
		C4Rect rcLabel, rcEdit;
		if (fMultiline)
		{
			rcLabel = caMain.GetFromTop(iLabelHgt);
			caMain.ExpandLeft(-2);
			caMain.ExpandTop(-2);
			rcEdit = caMain.GetAll();
		}
		else
		{
			rcLabel = caMain.GetFromLeft(iLabelWdt);
			caMain.ExpandLeft(-2);
			rcEdit = caMain.GetAll();
		}
		AddElement(new Label(szName, rcLabel, ALeft, dwTextClr, pUseFont, false));
		AddElement(pEdit = new C4GUI::Edit(rcEdit, false));
		pEdit->SetFont(pUseFont);
		if (szPrefText) pEdit->InsertText(szPrefText, false);
	}

	bool LabeledEdit::GetControlSize(int *piWdt, int *piHgt, const char *szForText, CStdFont *pForFont, bool fMultiline)
	{
		CStdFont *pUseFont = pForFont ? pForFont : &(::GraphicsResource.TextFont);
		int32_t iLabelWdt=100, iLabelHgt=24;
		pUseFont->GetTextExtent(szForText, iLabelWdt, iLabelHgt, true);
		int32_t iEditWdt = 100, iEditHgt = Edit::GetCustomEditHeight(pUseFont);
		if (fMultiline)
		{
			iEditWdt += 2; // indent edit a bit
			if (piWdt) *piWdt = Max<int32_t>(iLabelWdt, iEditWdt);
			if (piHgt) *piHgt = iLabelHgt + iEditHgt + 2;
		}
		else
		{
			iLabelWdt += 2; // add a bit of spacing between label and edit
			if (piWdt) *piWdt = iLabelWdt + iEditWdt;
			if (piHgt) *piHgt = Max<int32_t>(iLabelHgt, iEditHgt);
		}
		return true;
	}

} // end of namespace

