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