1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16 // generic user interface
17 // room for textual deconvolution
18 
19 #include "C4Include.h"
20 #include "gui/C4Gui.h"
21 
22 #include "game/C4Application.h"
23 #include "graphics/C4Draw.h"
24 #include "graphics/C4GraphicsResource.h"
25 #include "gui/C4MouseControl.h"
26 
27 namespace C4GUI
28 {
29 
30 	const char *Edit::CursorRepresentation = "\xC2\xA6"; // U+00A6 BROKEN BAR
31 
32 	namespace
33 	{
IsUtf8ContinuationByte(char c)34 		inline bool IsUtf8ContinuationByte(char c)
35 		{
36 			return (c & 0xC0) == 0x80;
37 		}
IsUtf8StartByte(char c)38 		inline bool IsUtf8StartByte(char c)
39 		{
40 			return (c & 0xC0) == 0xC0;
41 		}
42 	}
43 
44 // ----------------------------------------------------
45 // Edit
46 
Edit(const C4Rect & rtBounds,bool fFocusEdit)47 	Edit::Edit(const C4Rect &rtBounds, bool fFocusEdit) : Control(rtBounds), iCursorPos(0), iSelectionStart(0), iSelectionEnd(0), fLeftBtnDown(false)
48 	{
49 		// create an initial buffer
50 		Text = new char[256];
51 		iBufferSize = 256;
52 		*Text = 0;
53 		// def vals
54 		iMaxTextLength = 255;
55 		iCursorPos = iSelectionStart = iSelectionEnd = 0;
56 		iXScroll = 0;
57 		pFont = &::GraphicsResource.TextFont;
58 		dwBGClr = C4GUI_EditBGColor;
59 		dwFontClr = C4GUI_EditFontColor;
60 		dwBorderColor = 0; // default border
61 		cPasswordMask = 0;
62 		// apply client margin
63 		UpdateOwnPos();
64 		// add context handler
65 		SetContextHandler(new CBContextHandler<Edit>(this, &Edit::OnContext));
66 		// add key handlers
67 		C4CustomKey::Priority eKeyPrio = fFocusEdit ? C4CustomKey::PRIO_FocusCtrl : C4CustomKey::PRIO_Ctrl;
68 		pKeyCursorBack  = RegisterCursorOp(COP_BACK  , K_BACK  , "GUIEditCursorBack", eKeyPrio);
69 		pKeyCursorDel   = RegisterCursorOp(COP_DELETE, K_DELETE, "GUIEditCursorDel",eKeyPrio);
70 		pKeyCursorLeft  = RegisterCursorOp(COP_LEFT  , K_LEFT  , "GUIEditCursorLeft",eKeyPrio);
71 		pKeyCursorRight = RegisterCursorOp(COP_RIGHT , K_RIGHT , "GUIEditCursorRight", eKeyPrio);
72 		pKeyCursorHome  = RegisterCursorOp(COP_HOME  , K_HOME  , "GUIEditCursorHome", eKeyPrio);
73 		pKeyCursorEnd   = RegisterCursorOp(COP_END   , K_END   , "GUIEditCursorEnd", eKeyPrio);
74 		pKeyEnter = new C4KeyBinding(C4KeyCodeEx(K_RETURN), "GUIEditConfirm", KEYSCOPE_Gui,
75 		                             new ControlKeyCB<Edit>(*this, &Edit::KeyEnter), eKeyPrio);
76 		pKeyCopy = new C4KeyBinding(C4KeyCodeEx(K_C, KEYS_Control), "GUIEditCopy", KEYSCOPE_Gui,
77 		                            new ControlKeyCB<Edit>(*this, &Edit::KeyCopy), eKeyPrio);
78 		pKeyPaste = new C4KeyBinding(C4KeyCodeEx(K_V, KEYS_Control), "GUIEditPaste", KEYSCOPE_Gui,
79 		                             new ControlKeyCB<Edit>(*this, &Edit::KeyPaste), eKeyPrio);
80 		pKeyCut = new C4KeyBinding(C4KeyCodeEx(K_X, KEYS_Control), "GUIEditCut", KEYSCOPE_Gui,
81 		                           new ControlKeyCB<Edit>(*this, &Edit::KeyCut), eKeyPrio);
82 		pKeySelAll = new C4KeyBinding(C4KeyCodeEx(K_A, KEYS_Control), "GUIEditSelAll", KEYSCOPE_Gui,
83 		                              new ControlKeyCB<Edit>(*this, &Edit::KeySelectAll), eKeyPrio);
84 	}
85 
~Edit()86 	Edit::~Edit()
87 	{
88 		delete[] Text;
89 		delete pKeyCursorBack;
90 		delete pKeyCursorDel;
91 		delete pKeyCursorLeft;
92 		delete pKeyCursorRight;
93 		delete pKeyCursorHome;
94 		delete pKeyCursorEnd;
95 		delete pKeyEnter;
96 		delete pKeyCopy;
97 		delete pKeyPaste;
98 		delete pKeyCut;
99 		delete pKeySelAll;
100 	}
101 
RegisterCursorOp(CursorOperation op,C4KeyCode key,const char * szName,C4CustomKey::Priority eKeyPrio)102 	class C4KeyBinding *Edit::RegisterCursorOp(CursorOperation op, C4KeyCode key, const char *szName, C4CustomKey::Priority eKeyPrio)
103 	{
104 		// register same op for all shift states; distinction will be done in handling proc
105 		C4CustomKey::CodeList KeyList;
106 		KeyList.emplace_back(key);
107 		KeyList.emplace_back(key, KEYS_Shift);
108 		KeyList.emplace_back(key, KEYS_Control);
109 		KeyList.emplace_back(key, C4KeyShiftState(KEYS_Shift | KEYS_Control));
110 		return new C4KeyBinding(KeyList, szName, KEYSCOPE_Gui, new ControlKeyCBExPassKey<Edit, CursorOperation>(*this, op, &Edit::KeyCursorOp), eKeyPrio);
111 	}
112 
GetDefaultEditHeight()113 	int32_t Edit::GetDefaultEditHeight()
114 	{
115 		// edit height for default font
116 		return GetCustomEditHeight(&::GraphicsResource.TextFont);
117 	}
118 
GetCustomEditHeight(CStdFont * pUseFont)119 	int32_t Edit::GetCustomEditHeight(CStdFont *pUseFont)
120 	{
121 		// edit height for custom font: Make it so edits and wooden labels have same height
122 		return std::max<int32_t>(pUseFont->GetLineHeight()+3, C4GUI_MinWoodBarHgt);
123 	}
124 
ClearText()125 	void Edit::ClearText()
126 	{
127 		// free oversized buffers
128 		if (iBufferSize > 256)
129 		{
130 			delete[] Text;
131 			Text = new char[256];
132 			iBufferSize = 256;
133 		}
134 		// clear text
135 		*Text=0;
136 		// reset cursor and selection
137 		iCursorPos = iSelectionStart = iSelectionEnd = 0;
138 		iXScroll = 0;
139 	}
140 
Deselect()141 	void Edit::Deselect()
142 	{
143 		// reset selection
144 		iSelectionStart = iSelectionEnd = 0;
145 		// cursor might have moved: ensure it is shown
146 		tLastInputTime = C4TimeMilliseconds::Now();
147 	}
148 
DeleteSelection()149 	void Edit::DeleteSelection()
150 	{
151 		// move end text to front
152 		int32_t iSelBegin = std::min(iSelectionStart, iSelectionEnd), iSelEnd = std::max(iSelectionStart, iSelectionEnd);
153 		if (iSelectionStart == iSelectionEnd) return;
154 		memmove(Text + iSelBegin, Text + iSelEnd, strlen(Text + iSelEnd)+1);
155 		// adjust cursor pos
156 		if (iCursorPos > iSelBegin) iCursorPos = std::max(iSelBegin, iCursorPos - iSelEnd + iSelBegin);
157 		// cursor might have moved: ensure it is shown
158 		tLastInputTime = C4TimeMilliseconds::Now();
159 		// nothing selected
160 		iSelectionStart = iSelectionEnd = iSelBegin;
161 	}
162 
InsertText(const char * szText,bool fUser)163 	bool Edit::InsertText(const char *szText, bool fUser)
164 	{
165 		// empty previous selection
166 		if (iSelectionStart != iSelectionEnd) DeleteSelection();
167 		// check buffer length
168 		int32_t iTextLen = SLen(szText);
169 		int32_t iTextEnd = SLen(Text);
170 		bool fBufferOK = (iTextLen + iTextEnd <= (iMaxTextLength-1));
171 		if (!fBufferOK) iTextLen -= iTextEnd+iTextLen - (iMaxTextLength-1);
172 		if (iTextLen <= 0) return false;
173 		// ensure buffer is large enough
174 		EnsureBufferSize(iTextEnd + iTextLen + 1);
175 		// move down text buffer after cursor pos (including trailing zero-char)
176 		int32_t i;
177 		for (i=iTextEnd; i>=iCursorPos; --i) Text[i + iTextLen] = Text[i];
178 		// insert buffer into text
179 		for (i=iTextLen; i; --i) Text[iCursorPos + i - 1] = szText[i - 1];
180 		if (fUser)
181 		{
182 			// advance cursor
183 			iCursorPos += iTextLen;
184 			// cursor moved: ensure it is shown
185 			tLastInputTime = C4TimeMilliseconds::Now();
186 			ScrollCursorInView();
187 		}
188 		// done; return whether everything was inserted
189 		return fBufferOK;
190 	}
191 
GetCharPos(int32_t iControlXPos)192 	int32_t Edit::GetCharPos(int32_t iControlXPos)
193 	{
194 		// client offset
195 		iControlXPos -= rcClientRect.x - rcBounds.x - iXScroll;
196 		// well, not exactly the best idea...maybe add a new fn to the gfx system?
197 		// summing up char widths is no good, because there might be spacings between characters
198 		// 2do: optimize this
199 		if (cPasswordMask)
200 		{
201 			int32_t w, h; char strMask[2] = { cPasswordMask, 0 };
202 			pFont->GetTextExtent(strMask, w, h, false);
203 			return Clamp<int32_t>((iControlXPos + w/2) / std::max<int32_t>(1, w), 0, SLen(Text));
204 		}
205 		int32_t i = 0;
206 		for (int32_t iLastW = 0, w,h; Text[i]; ++i)
207 		{
208 			int oldi = i;
209 			if (IsUtf8StartByte(Text[oldi]))
210 				while (IsUtf8ContinuationByte(Text[++i + 1])) /* EMPTY */;
211 			char c=Text[i+1]; Text[i+1]=0; pFont->GetTextExtent(Text, w, h, false); Text[i+1]=c;
212 			if (w - (w-iLastW)/2 >= iControlXPos) return oldi;
213 			iLastW = w;
214 		}
215 		return i;
216 	}
217 
EnsureBufferSize(int32_t iMinBufferSize)218 	void Edit::EnsureBufferSize(int32_t iMinBufferSize)
219 	{
220 		// realloc buffer if necessary
221 		if (iBufferSize < iMinBufferSize)
222 		{
223 			// get new buffer size (rounded up to multiples of 256)
224 			iMinBufferSize = ((iMinBufferSize - 1) & ~0xff) + 0x100;
225 			// fill new buffer
226 			char *pNewBuffer = new char[iMinBufferSize];
227 			SCopy(Text, pNewBuffer);
228 			// apply new buffer
229 			delete[] Text; Text = pNewBuffer;
230 			iBufferSize = iMinBufferSize;
231 		}
232 	}
233 
ScrollCursorInView()234 	void Edit::ScrollCursorInView()
235 	{
236 		if (rcClientRect.Wdt<5) return;
237 		// get position of cursor
238 		int32_t iScrollOff = std::min<int32_t>(20, rcClientRect.Wdt/3);
239 		int32_t w,h;
240 		if (!cPasswordMask)
241 		{
242 			char c=Text[iCursorPos]; Text[iCursorPos]=0; pFont->GetTextExtent(Text, w, h, false); Text[iCursorPos]=c;
243 		}
244 		else
245 		{
246 			StdStrBuf Buf; Buf.AppendChars(cPasswordMask, iCursorPos);
247 			pFont->GetTextExtent(Buf.getData(), w, h, false);
248 		}
249 		// need to scroll?
250 		while (w-iXScroll < rcClientRect.Wdt/5 && w<iScrollOff+iXScroll && iXScroll > 0)
251 		{
252 			// left
253 			iXScroll = std::max(iXScroll - std::min(100, rcClientRect.Wdt/4), 0);
254 		}
255 		while (w-iXScroll >= rcClientRect.Wdt/5 && w>=rcClientRect.Wdt-iScrollOff+iXScroll)
256 		{
257 			// right
258 			iXScroll += std::min(100, rcClientRect.Wdt/4);
259 		}
260 	}
261 
DoFinishInput(bool fPasting,bool fPastingMore)262 	bool Edit::DoFinishInput(bool fPasting, bool fPastingMore)
263 	{
264 		// do OnFinishInput callback and process result - returns whether pasting operation should be continued
265 		InputResult eResult = OnFinishInput(fPasting, fPastingMore);
266 		switch (eResult)
267 		{
268 		case IR_None: // do nothing and continue pasting
269 			return true;
270 
271 		case IR_CloseDlg: // stop any pastes and close parent dialog successfully
272 		{
273 			Dialog *pDlg = GetDlg();
274 			if (pDlg) pDlg->UserClose(true);
275 			break;
276 		}
277 
278 		case IR_CloseEdit: // stop any pastes and remove this control
279 			delete this;
280 			break;
281 
282 		case IR_Abort: // do nothing and stop any pastes
283 			break;
284 		}
285 		// input has been handled; no more pasting please
286 		return false;
287 	}
288 
Copy()289 	bool Edit::Copy()
290 	{
291 		// get selected range
292 		int32_t iSelBegin = std::min(iSelectionStart, iSelectionEnd), iSelEnd = std::max(iSelectionStart, iSelectionEnd);
293 		if (iSelBegin == iSelEnd) return false;
294 		// allocate a global memory object for the text.
295 		std::string buf(Text+iSelBegin, iSelEnd-iSelBegin);
296 		if (cPasswordMask)
297 			buf.assign(buf.size(), cPasswordMask);
298 
299 		return Application.Copy(buf);
300 	}
301 
Cut()302 	bool Edit::Cut()
303 	{
304 		// copy text
305 		if (!Copy()) return false;
306 		// delete copied text
307 		DeleteSelection();
308 		// done, success
309 		return true;
310 	}
311 
Paste()312 	bool Edit::Paste()
313 	{
314 		bool fSuccess = false;
315 		// check clipboard contents
316 		if(!Application.IsClipboardFull()) return false;
317 		StdCopyStrBuf text(Application.Paste().c_str());
318 		char * szText = text.getMData();
319 		if (text)
320 		{
321 			fSuccess = !!*szText;
322 			// replace any '|'
323 			int32_t iLBPos=0, iLBPos2;
324 			// caution when inserting line breaks: Those must be stripped, and sent as Enter-commands
325 			iLBPos=0;
326 			for (;;)
327 			{
328 				iLBPos = SCharPos(0x0d, szText);
329 				iLBPos2 = SCharPos(0x0a, szText);
330 				if (iLBPos<0 && iLBPos2<0) break; // no more linebreaks
331 				if (iLBPos2>=0 && (iLBPos2<iLBPos || iLBPos<0)) iLBPos = iLBPos2;
332 				if (!iLBPos) { ++szText; continue; } // empty line
333 				szText[iLBPos]=0x00;
334 				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)
335 				szText += iLBPos+1;
336 				iLBPos=0;
337 				if (!DoFinishInput(true, !!*szText))
338 				{
339 					// k, pasted
340 					return true;
341 				}
342 			}
343 			// insert new text (may fail due to overfull buffer, in which case parts of the text will be inserted)
344 			if (*szText) fSuccess = fSuccess && InsertText(szText, true);
345 		}
346 		// return whether insertion was successful
347 		return fSuccess;
348 	}
349 
IsWholeWordSpacer(unsigned char c)350 	bool IsWholeWordSpacer(unsigned char c)
351 	{
352 		// characters that make up a space between words
353 		// the extended characters are all seen a letters, because they vary in different
354 		// charsets (danish, french, etc.) and are likely to represent localized letters
355 		return !Inside<char>(c, 'A', 'Z')
356 		       && !Inside<char>(c, 'a', 'z')
357 		       && !Inside<char>(c, '0', '9')
358 		       && c!='_' && c<127;
359 	}
360 
KeyEnter()361 	bool Edit::KeyEnter()
362 	{
363 		DoFinishInput(false, false);
364 		// whatever happens: Enter key has been processed
365 		return true;
366 	}
367 
KeyCursorOp(const C4KeyCodeEx & key,const CursorOperation & op)368 	bool Edit::KeyCursorOp(const C4KeyCodeEx &key, const CursorOperation &op)
369 	{
370 		bool fShift = !!(key.dwShift & KEYS_Shift);
371 		bool fCtrl = !!(key.dwShift & KEYS_Control);
372 		// any selection?
373 		if (iSelectionStart != iSelectionEnd)
374 		{
375 			// special handling: backspace/del with selection (delete selection)
376 			if (op == COP_BACK || op == COP_DELETE) { DeleteSelection(); return true; }
377 			// no shift pressed: clear selection (even if no cursor movement is done)
378 			if (!fShift) Deselect();
379 		}
380 		// movement or regular/word deletion
381 		int32_t iMoveDir = 0, iMoveLength = 0;
382 		if (op == COP_LEFT && iCursorPos) iMoveDir = -1;
383 		else if (op == COP_RIGHT && (uint32_t)iCursorPos < SLen(Text)) iMoveDir = +1;
384 		else if (op == COP_BACK && iCursorPos && !fShift) iMoveDir = -1;
385 		else if (op == COP_DELETE && (uint32_t)iCursorPos < SLen(Text) && !fShift) iMoveDir = +1;
386 		else if (op == COP_HOME) iMoveLength = -iCursorPos;
387 		else if (op == COP_END) iMoveLength = SLen(Text)-iCursorPos;
388 		if (iMoveDir || iMoveLength)
389 		{
390 			// evaluate move length? (not home+end)
391 			if (iMoveDir)
392 			{
393 				if (fCtrl)
394 				{
395 					// move one word
396 					iMoveLength = 0;
397 					bool fNoneSpaceFound = false, fSpaceFound = false;;
398 					while (iCursorPos + iMoveLength + iMoveDir >= 0 && (uint32_t)(iCursorPos + iMoveLength + iMoveDir) <= SLen(Text))
399 						if (IsWholeWordSpacer(Text[iCursorPos + iMoveLength + (iMoveDir-1)/2]))
400 						{
401 							// stop left of a complete word
402 							if (fNoneSpaceFound && iMoveDir<0) break;
403 							// continue
404 							fSpaceFound = true;
405 							iMoveLength += iMoveDir;
406 						}
407 						else
408 						{
409 							// stop right of spacings complete word
410 							if (fSpaceFound && iMoveDir > 0) break;
411 							// continue
412 							fNoneSpaceFound = true;
413 							iMoveLength += iMoveDir;
414 						}
415 				}
416 				else
417 				{
418 					// Handle UTF-8
419 					iMoveLength = iMoveDir;
420 					while (IsUtf8ContinuationByte(Text[iCursorPos + iMoveLength])) iMoveLength += Sign(iMoveLength);
421 				}
422 			}
423 			// delete stuff
424 			if (op == COP_BACK || op == COP_DELETE)
425 			{
426 				// delete: make backspace command of it
427 				if (op == COP_DELETE) { iCursorPos += iMoveLength; iMoveLength = -iMoveLength; }
428 				// move end of string up
429 				char *c; for (c = Text+iCursorPos; *c; ++c) *(c+iMoveLength) = *c;
430 				// terminate string
431 				*(c+iMoveLength) = 0;
432 				assert(IsValidUtf8(Text));
433 			}
434 			else if (fShift)
435 			{
436 				// shift+arrow key: make/adjust selection
437 				if (iSelectionStart == iSelectionEnd) iSelectionStart = iCursorPos;
438 				iSelectionEnd = iCursorPos + iMoveLength;
439 			}
440 			else
441 				// simple cursor movement: clear any selection
442 				if (iSelectionStart != iSelectionEnd) Deselect();
443 			// adjust cursor pos
444 			iCursorPos += iMoveLength;
445 		}
446 		// show cursor
447 		tLastInputTime = C4TimeMilliseconds::Now();
448 		ScrollCursorInView();
449 		// operation recognized
450 		return true;
451 	}
452 
CharIn(const char * c)453 	bool Edit::CharIn(const char * c)
454 	{
455 		// no control codes
456 		if (((unsigned char)(c[0]))<' ' || c[0]==0x7f) return false;
457 		// no '|'
458 		if (c[0]=='|') return false;
459 		// all extended characters are OK
460 		// insert character at cursor position
461 		return InsertText(c, true);
462 	}
463 
MouseInput(CMouse & rMouse,int32_t iButton,int32_t iX,int32_t iY,DWORD dwKeyParam)464 	void Edit::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam)
465 	{
466 		// inherited first - this may give focus to this element
467 		Control::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
468 		// update dragging area
469 		int32_t iPrevCursorPos = iCursorPos;
470 		// dragging area is updated by drag proc
471 		// process left down and up
472 		switch (iButton)
473 		{
474 		case C4MC_Button_LeftDown:
475 			// mark button as being down
476 			fLeftBtnDown = true;
477 			// set selection start
478 			iSelectionStart = iSelectionEnd = GetCharPos(iX);
479 			// set cursor pos here, too
480 			iCursorPos = iSelectionStart;
481 			// remember drag target
482 			// no dragging movement will be done w/o drag component assigned
483 			// but text selection should work even if the user goes outside the component
484 			if (!rMouse.pDragElement) rMouse.pDragElement = this;
485 			break;
486 
487 		case C4MC_Button_LeftUp:
488 			// only if button was down... (might have dragged here)
489 			if (fLeftBtnDown)
490 			{
491 				// it's now up :)
492 				fLeftBtnDown = false;
493 				// set cursor to this pos
494 				iCursorPos = iSelectionEnd;
495 			}
496 			break;
497 
498 		case C4MC_Button_LeftDouble:
499 		{
500 			// word selection
501 			// get character pos (half-char-offset, use this to allow selection at half-space-offset around a word)
502 			int32_t iCharPos = GetCharPos(iX);
503 			// was space? try left character
504 			if (IsWholeWordSpacer(Text[iCharPos]))
505 			{
506 				if (!iCharPos) break;
507 				if (IsWholeWordSpacer(Text[--iCharPos])) break;
508 			}
509 			// search ending of word left and right
510 			// bounds-check is done by zero-char at end, which is regarded as a spacer
511 			iSelectionStart = iCharPos; iSelectionEnd = iCharPos + 1;
512 			while (iSelectionStart > 0 && !IsWholeWordSpacer(Text[iSelectionStart-1])) --iSelectionStart;
513 			while (!IsWholeWordSpacer(Text[iSelectionEnd])) ++iSelectionEnd;
514 			// set cursor pos to end of selection
515 			iCursorPos = iSelectionEnd;
516 			// ignore last btn-down-selection
517 			fLeftBtnDown = false;
518 		}
519 		break;
520 		case C4MC_Button_MiddleDown:
521 			// set selection start
522 			iSelectionStart = iSelectionEnd = GetCharPos(iX);
523 			// set cursor pos here, too
524 			iCursorPos = iSelectionStart;
525 #ifndef _WIN32
526 			// Insert primary selection
527 			InsertText(Application.Paste(false).c_str(), true);
528 #endif
529 			break;
530 		};
531 		// scroll cursor in view
532 		if (iPrevCursorPos != iCursorPos) ScrollCursorInView();
533 	}
534 
DoDragging(CMouse & rMouse,int32_t iX,int32_t iY,DWORD dwKeyParam)535 	void Edit::DoDragging(CMouse &rMouse, int32_t iX, int32_t iY, DWORD dwKeyParam)
536 	{
537 		// update cursor pos
538 		int32_t iPrevCursorPos = iCursorPos;
539 		iCursorPos = iSelectionEnd = GetCharPos(iX);
540 		// scroll cursor in view
541 		if (iPrevCursorPos != iCursorPos) ScrollCursorInView();
542 	}
543 
OnGetFocus(bool fByMouse)544 	void Edit::OnGetFocus(bool fByMouse)
545 	{
546 		// inherited
547 		Control::OnGetFocus(fByMouse);
548 		// select all
549 		iSelectionStart=0; iSelectionEnd=iCursorPos=SLen(Text);
550 		// begin with a flashing cursor
551 		tLastInputTime = C4TimeMilliseconds::Now();
552 	}
553 
OnLooseFocus()554 	void Edit::OnLooseFocus()
555 	{
556 		// clear selection
557 		iSelectionStart = iSelectionEnd = 0;
558 		// inherited
559 		Control::OnLooseFocus();
560 	}
561 
DrawElement(C4TargetFacet & cgo)562 	void Edit::DrawElement(C4TargetFacet &cgo)
563 	{
564 		// draw background
565 		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);
566 		// draw frame
567 		if (dwBorderColor)
568 		{
569 			int32_t x1=cgo.TargetX+rcBounds.x,y1=cgo.TargetY+rcBounds.y,x2=x1+rcBounds.Wdt,y2=y1+rcBounds.Hgt;
570 			pDraw->DrawFrameDw(cgo.Surface, x1, y1, x2, y2-1, dwBorderColor);
571 			pDraw->DrawFrameDw(cgo.Surface, x1+1, y1+1, x2-1, y2-2, dwBorderColor);
572 		}
573 		else
574 			// default frame color
575 			Draw3DFrame(cgo);
576 		// clipping
577 		int cx0,cy0,cx1,cy1; bool fClip, fOwnClip;
578 		fClip = pDraw->GetPrimaryClipper(cx0,cy0,cx1,cy1);
579 		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;
580 		pDraw->ApplyZoom(nclx1, ncly1);
581 		pDraw->ApplyZoom(nclx2, ncly2);
582 		fOwnClip = pDraw->SetPrimaryClipper(nclx1, ncly1, nclx2, ncly2);
583 		// get usable height of edit field
584 		int32_t iHgt = pFont->GetLineHeight(), iY0;
585 		if (rcClientRect.Hgt <= iHgt)
586 		{
587 			// very narrow edit field: use all of it
588 			iHgt=rcClientRect.Hgt;
589 			iY0=rcClientRect.y;
590 		}
591 		else
592 		{
593 			// normal edit field: center text vertically
594 			iY0 = rcClientRect.y+(rcClientRect.Hgt-iHgt)/2+1;
595 			// don't overdo it with selection mark
596 			iHgt-=2;
597 		}
598 		// get text to draw, apply password mask if neccessary
599 		StdStrBuf Buf; char *pDrawText;
600 		if (cPasswordMask)
601 		{
602 			Buf.AppendChars(cPasswordMask, SLen(Text));
603 			pDrawText = Buf.getMData();
604 		}
605 		else
606 			pDrawText = Text;
607 		// draw selection
608 		if (iSelectionStart != iSelectionEnd)
609 		{
610 			// get selection range
611 			int32_t iSelBegin = std::min(iSelectionStart, iSelectionEnd);
612 			int32_t iSelEnd = std::max(iSelectionStart, iSelectionEnd);
613 			// get offsets in text
614 			int32_t iSelX1, iSelX2, h;
615 			char c = pDrawText[iSelBegin]; pDrawText[iSelBegin]=0; pFont->GetTextExtent(pDrawText, iSelX1, h, false); pDrawText[iSelBegin]=c;
616 			c = pDrawText[iSelEnd]; pDrawText[iSelEnd]=0; pFont->GetTextExtent(pDrawText, iSelX2, h, false); pDrawText[iSelEnd]=c;
617 			iSelX1 -= iXScroll; iSelX2 -= iXScroll;
618 			// draw selection box around it
619 			pDraw->DrawBoxDw(cgo.Surface, cgo.TargetX+rcClientRect.x+iSelX1,cgo.TargetY+iY0,rcClientRect.x+iSelX2-1+cgo.TargetX,iY0+iHgt-1+cgo.TargetY,0x7f7f7f00);
620 		}
621 		// draw edit text
622 		pDraw->TextOut(pDrawText, *pFont, 1.0f, cgo.Surface, rcClientRect.x + cgo.TargetX - iXScroll, iY0 + cgo.TargetY - 1, dwFontClr, ALeft, false);
623 		// draw cursor
624 		bool fBlink = ((tLastInputTime - C4TimeMilliseconds::Now())/500)%2 == 0;
625 		if (HasDrawFocus() && fBlink)
626 		{
627 			char cAtCursor = pDrawText[iCursorPos]; pDrawText[iCursorPos]=0; int32_t w,h,wc;
628 			pFont->GetTextExtent(pDrawText, w, h, false);
629 			pDrawText[iCursorPos] = cAtCursor;
630 			pFont->GetTextExtent(CursorRepresentation, wc, h, false); wc/=2;
631 			pDraw->TextOut(CursorRepresentation, *pFont, 1.5f, cgo.Surface, rcClientRect.x + cgo.TargetX + w - wc - iXScroll, iY0 + cgo.TargetY - h/3, dwFontClr, ALeft, false);
632 		}
633 		// unclip
634 		if (fOwnClip)
635 		{
636 			if (fClip) pDraw->SetPrimaryClipper(cx0,cy0,cx1,cy1);
637 			else pDraw->NoPrimaryClipper();
638 		}
639 	}
640 
SelectAll()641 	void Edit::SelectAll()
642 	{
643 		// safety: no text?
644 		if (!Text) return;
645 		// select all
646 		iSelectionStart = 0;
647 		iSelectionEnd = iCursorPos = SLen(Text);
648 	}
649 
OnContext(C4GUI::Element * pListItem,int32_t iX,int32_t iY)650 	ContextMenu *Edit::OnContext(C4GUI::Element *pListItem, int32_t iX, int32_t iY)
651 	{
652 		// safety: no text?
653 		if (!Text) return nullptr;
654 		// create context menu
655 		ContextMenu *pCtx = new ContextMenu();
656 		// fill with any valid items
657 		// get selected range
658 		uint32_t iSelBegin = std::min(iSelectionStart, iSelectionEnd), iSelEnd = std::max(iSelectionStart, iSelectionEnd);
659 		bool fAnythingSelected = (iSelBegin != iSelEnd);
660 		if (fAnythingSelected)
661 		{
662 			pCtx->AddItem(LoadResStr("IDS_DLG_CUT"), LoadResStr("IDS_DLGTIP_CUT"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxCut));
663 			pCtx->AddItem(LoadResStr("IDS_DLG_COPY"), LoadResStr("IDS_DLGTIP_COPY"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxCopy));
664 		}
665 		if (Application.IsClipboardFull())
666 			pCtx->AddItem(LoadResStr("IDS_DLG_PASTE"), LoadResStr("IDS_DLGTIP_PASTE"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxPaste));
667 
668 		if (fAnythingSelected)
669 			pCtx->AddItem(LoadResStr("IDS_DLG_CLEAR"), LoadResStr("IDS_DLGTIP_CLEAR"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxClear));
670 		if (*Text && (iSelBegin!=0 || iSelEnd!=SLen(Text)))
671 			pCtx->AddItem(LoadResStr("IDS_DLG_SELALL"), LoadResStr("IDS_DLGTIP_SELALL"), Ico_None, new CBMenuHandler<Edit>(this, &Edit::OnCtxSelAll));
672 		// return ctx menu
673 		return pCtx;
674 	}
675 
GetCurrentWord(char * szTargetBuf,int32_t iMaxTargetBufLen)676 	bool Edit::GetCurrentWord(char *szTargetBuf, int32_t iMaxTargetBufLen)
677 	{
678 		// get word before cursor pos (for nick completion)
679 		if (!Text || iCursorPos<=0) return false;
680 		int32_t iPos = iCursorPos;
681 		while (iPos>0)
682 				if (IsWholeWordSpacer(Text[iPos-1])) break; else --iPos;
683 		SCopy(Text + iPos, szTargetBuf, std::min(iCursorPos - iPos, iMaxTargetBufLen));
684 		return !!*szTargetBuf;
685 	}
686 
687 
688 // ----------------------------------------------------
689 // RenameEdit
690 
RenameEdit(Label * pLabel)691 	RenameEdit::RenameEdit(Label *pLabel) : Edit(pLabel->GetBounds(), true), fFinishing(false), pForLabel(pLabel)
692 	{
693 		// ctor - construct for label
694 		assert(pForLabel);
695 		pForLabel->SetVisibility(false);
696 		InsertText(pForLabel->GetText(), true);
697 		// put self into place
698 		Container *pCont = pForLabel->GetParent();
699 		assert(pCont);
700 		pCont->AddElement(this);
701 		Dialog *pDlg = GetDlg();
702 		if (pDlg)
703 		{
704 			pPrevFocusCtrl = pDlg->GetFocus();
705 			pDlg->SetFocus(this, false);
706 		}
707 		else pPrevFocusCtrl=nullptr;
708 		// key binding for rename abort
709 		C4CustomKey::CodeList keys;
710 		keys.emplace_back(K_ESCAPE);
711 		if (Config.Controls.GamepadGuiControl)
712 		{
713 			ControllerKeys::Cancel(keys);
714 		}
715 		pKeyAbort = new C4KeyBinding(keys, "GUIRenameEditAbort", KEYSCOPE_Gui,
716 		                             new ControlKeyCB<RenameEdit>(*this, &RenameEdit::KeyAbort), C4CustomKey::PRIO_FocusCtrl);
717 	}
718 
~RenameEdit()719 	RenameEdit::~RenameEdit()
720 	{
721 		delete pKeyAbort;
722 	}
723 
Abort()724 	void RenameEdit::Abort()
725 	{
726 		OnCancelRename();
727 		FinishRename();
728 	}
729 
OnFinishInput(bool fPasting,bool fPastingMore)730 	Edit::InputResult RenameEdit::OnFinishInput(bool fPasting, bool fPastingMore)
731 	{
732 		// any text?
733 		if (!Text || !*Text)
734 		{
735 			// OK without text is regarded as abort
736 			OnCancelRename();
737 			FinishRename();
738 		}
739 		else switch (OnOKRename(Text))
740 			{
741 			case RR_Invalid:
742 			{
743 				// new name was not accepted: Continue editing
744 				Dialog *pDlg = GetDlg();
745 				if (pDlg) if (pDlg->GetFocus() != this) pDlg->SetFocus(this, false);
746 				SelectAll();
747 				break;
748 			}
749 
750 			case RR_Accepted:
751 				// okay, rename to that text
752 				FinishRename();
753 				break;
754 
755 			case RR_Deleted:
756 				// this is invalid; don't do anything!
757 				break;
758 			}
759 		return IR_Abort;
760 	}
761 
FinishRename()762 	void RenameEdit::FinishRename()
763 	{
764 		// done: restore stuff
765 		fFinishing = true;
766 		pForLabel->SetVisibility(true);
767 		Dialog *pDlg = GetDlg();
768 		if (pDlg && pPrevFocusCtrl) pDlg->SetFocus(pPrevFocusCtrl, false);
769 		delete this;
770 	}
771 
OnLooseFocus()772 	void RenameEdit::OnLooseFocus()
773 	{
774 		Edit::OnLooseFocus();
775 		// callback when control looses focus: OK input
776 		if (!fFinishing) OnFinishInput(false, false);
777 	}
778 
779 
780 
781 // ----------------------------------------------------
782 // LabeledEdit
783 
LabeledEdit(const C4Rect & rcBounds,const char * szName,bool fMultiline,const char * szPrefText,CStdFont * pUseFont,uint32_t dwTextClr)784 	LabeledEdit::LabeledEdit(const C4Rect &rcBounds, const char *szName, bool fMultiline, const char *szPrefText, CStdFont *pUseFont, uint32_t dwTextClr)
785 			: C4GUI::Window()
786 	{
787 		if (!pUseFont) pUseFont = &(::GraphicsResource.TextFont);
788 		SetBounds(rcBounds);
789 		ComponentAligner caMain(GetClientRect(), 0,0, true);
790 		int32_t iLabelWdt=100, iLabelHgt=24;
791 		pUseFont->GetTextExtent(szName, iLabelWdt, iLabelHgt, true);
792 		C4Rect rcLabel, rcEdit;
793 		if (fMultiline)
794 		{
795 			rcLabel = caMain.GetFromTop(iLabelHgt);
796 			caMain.ExpandLeft(-2);
797 			caMain.ExpandTop(-2);
798 			rcEdit = caMain.GetAll();
799 		}
800 		else
801 		{
802 			rcLabel = caMain.GetFromLeft(iLabelWdt);
803 			caMain.ExpandLeft(-2);
804 			rcEdit = caMain.GetAll();
805 		}
806 		AddElement(new Label(szName, rcLabel, ALeft, dwTextClr, pUseFont, false));
807 		AddElement(pEdit = new C4GUI::Edit(rcEdit, false));
808 		pEdit->SetFont(pUseFont);
809 		if (szPrefText) pEdit->InsertText(szPrefText, false);
810 	}
811 
GetControlSize(int * piWdt,int * piHgt,const char * szForText,CStdFont * pForFont,bool fMultiline)812 	bool LabeledEdit::GetControlSize(int *piWdt, int *piHgt, const char *szForText, CStdFont *pForFont, bool fMultiline)
813 	{
814 		CStdFont *pUseFont = pForFont ? pForFont : &(::GraphicsResource.TextFont);
815 		int32_t iLabelWdt=100, iLabelHgt=24;
816 		pUseFont->GetTextExtent(szForText, iLabelWdt, iLabelHgt, true);
817 		int32_t iEditWdt = 100, iEditHgt = Edit::GetCustomEditHeight(pUseFont);
818 		if (fMultiline)
819 		{
820 			iEditWdt += 2; // indent edit a bit
821 			if (piWdt) *piWdt = std::max<int32_t>(iLabelWdt, iEditWdt);
822 			if (piHgt) *piHgt = iLabelHgt + iEditHgt + 2;
823 		}
824 		else
825 		{
826 			iLabelWdt += 2; // add a bit of spacing between label and edit
827 			if (piWdt) *piWdt = iLabelWdt + iEditWdt;
828 			if (piHgt) *piHgt = std::max<int32_t>(iLabelHgt, iEditHgt);
829 		}
830 		return true;
831 	}
832 
833 } // end of namespace
834 
835