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