Move SMaterial std::hash impl to its header
[minetest.git] / irr / src / CGUIEditBox.cpp
blob8d03caf4af5eea2444281a2275654b94bafeed78
1 // Copyright (C) 2002-2012 Nikolaus Gebhardt
2 // This file is part of the "Irrlicht Engine".
3 // For conditions of distribution and use, see copyright notice in irrlicht.h
5 #include "CGUIEditBox.h"
7 #include "IGUISkin.h"
8 #include "IGUIEnvironment.h"
9 #include "IGUIFont.h"
10 #include "IVideoDriver.h"
11 #include "rect.h"
12 #include "os.h"
13 #include "Keycodes.h"
16 todo:
17 optional scrollbars
18 ctrl+left/right to select word
19 double click/ctrl click: word select + drag to select whole words, triple click to select line
20 optional? dragging selected text
21 numerical
24 namespace irr
26 namespace gui
29 //! constructor
30 CGUIEditBox::CGUIEditBox(const wchar_t *text, bool border,
31 IGUIEnvironment *environment, IGUIElement *parent, s32 id,
32 const core::rect<s32> &rectangle) :
33 IGUIEditBox(environment, parent, id, rectangle),
34 OverwriteMode(false), MouseMarking(false),
35 Border(border), Background(true), OverrideColorEnabled(false), MarkBegin(0), MarkEnd(0),
36 OverrideColor(video::SColor(101, 255, 255, 255)), OverrideFont(0), LastBreakFont(0),
37 Operator(0), BlinkStartTime(0), CursorBlinkTime(350), CursorChar(L"_"), CursorPos(0), HScrollPos(0), VScrollPos(0), Max(0),
38 WordWrap(false), MultiLine(false), AutoScroll(true), PasswordBox(false),
39 PasswordChar(L'*'), HAlign(EGUIA_UPPERLEFT), VAlign(EGUIA_CENTER),
40 CurrentTextRect(0, 0, 1, 1), FrameRect(rectangle)
42 #ifdef _DEBUG
43 setDebugName("CGUIEditBox");
44 #endif
46 Text = text;
48 if (Environment)
49 Operator = Environment->getOSOperator();
51 if (Operator)
52 Operator->grab();
54 // this element can be tabbed to
55 setTabStop(true);
56 setTabOrder(-1);
58 calculateFrameRect();
59 breakText();
61 calculateScrollPos();
64 //! destructor
65 CGUIEditBox::~CGUIEditBox()
67 if (OverrideFont)
68 OverrideFont->drop();
70 if (Operator)
71 Operator->drop();
74 //! Sets another skin independent font.
75 void CGUIEditBox::setOverrideFont(IGUIFont *font)
77 if (OverrideFont == font)
78 return;
80 if (OverrideFont)
81 OverrideFont->drop();
83 OverrideFont = font;
85 if (OverrideFont)
86 OverrideFont->grab();
88 breakText();
91 //! Gets the override font (if any)
92 IGUIFont *CGUIEditBox::getOverrideFont() const
94 return OverrideFont;
97 //! Get the font which is used right now for drawing
98 IGUIFont *CGUIEditBox::getActiveFont() const
100 if (OverrideFont)
101 return OverrideFont;
102 IGUISkin *skin = Environment->getSkin();
103 if (skin)
104 return skin->getFont();
105 return 0;
108 //! Sets another color for the text.
109 void CGUIEditBox::setOverrideColor(video::SColor color)
111 OverrideColor = color;
112 OverrideColorEnabled = true;
115 video::SColor CGUIEditBox::getOverrideColor() const
117 return OverrideColor;
120 //! Turns the border on or off
121 void CGUIEditBox::setDrawBorder(bool border)
123 Border = border;
126 //! Checks if border drawing is enabled
127 bool CGUIEditBox::isDrawBorderEnabled() const
129 return Border;
132 //! Sets whether to draw the background
133 void CGUIEditBox::setDrawBackground(bool draw)
135 Background = draw;
138 //! Checks if background drawing is enabled
139 bool CGUIEditBox::isDrawBackgroundEnabled() const
141 return Background;
144 //! Sets if the text should use the override color or the color in the gui skin.
145 void CGUIEditBox::enableOverrideColor(bool enable)
147 OverrideColorEnabled = enable;
150 bool CGUIEditBox::isOverrideColorEnabled() const
152 return OverrideColorEnabled;
155 //! Enables or disables word wrap
156 void CGUIEditBox::setWordWrap(bool enable)
158 WordWrap = enable;
159 breakText();
162 void CGUIEditBox::updateAbsolutePosition()
164 core::rect<s32> oldAbsoluteRect(AbsoluteRect);
165 IGUIElement::updateAbsolutePosition();
166 if (oldAbsoluteRect != AbsoluteRect) {
167 calculateFrameRect();
168 breakText();
169 calculateScrollPos();
173 //! Checks if word wrap is enabled
174 bool CGUIEditBox::isWordWrapEnabled() const
176 return WordWrap;
179 //! Enables or disables newlines.
180 void CGUIEditBox::setMultiLine(bool enable)
182 MultiLine = enable;
183 breakText();
186 //! Checks if multi line editing is enabled
187 bool CGUIEditBox::isMultiLineEnabled() const
189 return MultiLine;
192 void CGUIEditBox::setPasswordBox(bool passwordBox, wchar_t passwordChar)
194 PasswordBox = passwordBox;
195 if (PasswordBox) {
196 PasswordChar = passwordChar;
197 setMultiLine(false);
198 setWordWrap(false);
199 BrokenText.clear();
203 bool CGUIEditBox::isPasswordBox() const
205 return PasswordBox;
208 //! Sets text justification
209 void CGUIEditBox::setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical)
211 HAlign = horizontal;
212 VAlign = vertical;
215 //! called if an event happened.
216 bool CGUIEditBox::OnEvent(const SEvent &event)
218 if (isEnabled()) {
220 switch (event.EventType) {
221 case EET_GUI_EVENT:
222 if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) {
223 if (event.GUIEvent.Caller == this) {
224 MouseMarking = false;
225 setTextMarkers(0, 0);
228 break;
229 case EET_KEY_INPUT_EVENT:
230 if (processKey(event))
231 return true;
232 break;
233 case EET_MOUSE_INPUT_EVENT:
234 if (processMouse(event))
235 return true;
236 break;
237 case EET_STRING_INPUT_EVENT:
238 inputString(*event.StringInput.Str);
239 return true;
240 break;
241 default:
242 break;
246 return IGUIElement::OnEvent(event);
249 bool CGUIEditBox::processKey(const SEvent &event)
251 if (!event.KeyInput.PressedDown)
252 return false;
254 bool textChanged = false;
255 s32 newMarkBegin = MarkBegin;
256 s32 newMarkEnd = MarkEnd;
258 // control shortcut handling
260 if (event.KeyInput.Control) {
261 // german backlash '\' entered with control + '?'
262 if (event.KeyInput.Char == '\\') {
263 inputChar(event.KeyInput.Char);
264 return true;
267 switch (event.KeyInput.Key) {
268 case KEY_KEY_A:
269 // select all
270 newMarkBegin = 0;
271 newMarkEnd = Text.size();
272 break;
273 case KEY_KEY_C:
274 // copy to clipboard
275 if (!PasswordBox && Operator && MarkBegin != MarkEnd) {
276 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
277 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
279 core::stringc s;
280 wStringToUTF8(s, Text.subString(realmbgn, realmend - realmbgn));
281 Operator->copyToClipboard(s.c_str());
283 break;
284 case KEY_KEY_X:
285 // cut to the clipboard
286 if (!PasswordBox && Operator && MarkBegin != MarkEnd) {
287 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
288 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
290 // copy
291 core::stringc sc;
292 wStringToUTF8(sc, Text.subString(realmbgn, realmend - realmbgn));
293 Operator->copyToClipboard(sc.c_str());
295 if (isEnabled()) {
296 // delete
297 core::stringw s;
298 s = Text.subString(0, realmbgn);
299 s.append(Text.subString(realmend, Text.size() - realmend));
300 Text = s;
302 CursorPos = realmbgn;
303 newMarkBegin = 0;
304 newMarkEnd = 0;
305 textChanged = true;
308 break;
309 case KEY_KEY_V:
310 if (!isEnabled())
311 break;
313 // paste from the clipboard
314 if (Operator) {
315 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
316 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
318 // add the string
319 const c8 *p = Operator->getTextFromClipboard();
320 if (p) {
321 irr::core::stringw widep;
322 core::utf8ToWString(widep, p);
324 if (MarkBegin == MarkEnd) {
325 // insert text
326 core::stringw s = Text.subString(0, CursorPos);
327 s.append(widep);
328 s.append(Text.subString(CursorPos, Text.size() - CursorPos));
330 if (!Max || s.size() <= Max) { // thx to Fish FH for fix
331 Text = s;
332 s = widep;
333 CursorPos += s.size();
335 } else {
336 // replace text
338 core::stringw s = Text.subString(0, realmbgn);
339 s.append(widep);
340 s.append(Text.subString(realmend, Text.size() - realmend));
342 if (!Max || s.size() <= Max) { // thx to Fish FH for fix
343 Text = s;
344 s = widep;
345 CursorPos = realmbgn + s.size();
350 newMarkBegin = 0;
351 newMarkEnd = 0;
352 textChanged = true;
354 break;
355 case KEY_HOME:
356 // move/highlight to start of text
357 if (event.KeyInput.Shift) {
358 newMarkEnd = CursorPos;
359 newMarkBegin = 0;
360 CursorPos = 0;
361 } else {
362 CursorPos = 0;
363 newMarkBegin = 0;
364 newMarkEnd = 0;
366 break;
367 case KEY_END:
368 // move/highlight to end of text
369 if (event.KeyInput.Shift) {
370 newMarkBegin = CursorPos;
371 newMarkEnd = Text.size();
372 CursorPos = 0;
373 } else {
374 CursorPos = Text.size();
375 newMarkBegin = 0;
376 newMarkEnd = 0;
378 break;
379 default:
380 return false;
383 // Some special keys - but only handle them if KeyInput.Char is null as on some systems (X11) they might have same key-code as ansi-keys otherwise
384 else if (event.KeyInput.Char == 0) {
385 switch (event.KeyInput.Key) {
386 case KEY_END: {
387 s32 p = Text.size();
388 if (WordWrap || MultiLine) {
389 p = getLineFromPos(CursorPos);
390 p = BrokenTextPositions[p] + (s32)BrokenText[p].size();
391 if (p > 0 && (Text[p - 1] == L'\r' || Text[p - 1] == L'\n'))
392 p -= 1;
395 if (event.KeyInput.Shift) {
396 if (MarkBegin == MarkEnd)
397 newMarkBegin = CursorPos;
399 newMarkEnd = p;
400 } else {
401 newMarkBegin = 0;
402 newMarkEnd = 0;
404 CursorPos = p;
405 BlinkStartTime = os::Timer::getTime();
406 } break;
407 case KEY_HOME: {
409 s32 p = 0;
410 if (WordWrap || MultiLine) {
411 p = getLineFromPos(CursorPos);
412 p = BrokenTextPositions[p];
415 if (event.KeyInput.Shift) {
416 if (MarkBegin == MarkEnd)
417 newMarkBegin = CursorPos;
418 newMarkEnd = p;
419 } else {
420 newMarkBegin = 0;
421 newMarkEnd = 0;
423 CursorPos = p;
424 BlinkStartTime = os::Timer::getTime();
425 } break;
426 case KEY_LEFT:
428 if (event.KeyInput.Shift) {
429 if (CursorPos > 0) {
430 if (MarkBegin == MarkEnd)
431 newMarkBegin = CursorPos;
433 newMarkEnd = CursorPos - 1;
435 } else {
436 newMarkBegin = 0;
437 newMarkEnd = 0;
440 if (CursorPos > 0)
441 CursorPos--;
442 BlinkStartTime = os::Timer::getTime();
443 break;
445 case KEY_RIGHT:
446 if (event.KeyInput.Shift) {
447 if (Text.size() > (u32)CursorPos) {
448 if (MarkBegin == MarkEnd)
449 newMarkBegin = CursorPos;
451 newMarkEnd = CursorPos + 1;
453 } else {
454 newMarkBegin = 0;
455 newMarkEnd = 0;
458 if (Text.size() > (u32)CursorPos)
459 CursorPos++;
460 BlinkStartTime = os::Timer::getTime();
461 break;
462 case KEY_UP:
463 if (MultiLine || (WordWrap && BrokenText.size() > 1)) {
464 s32 lineNo = getLineFromPos(CursorPos);
465 s32 mb = (MarkBegin == MarkEnd) ? CursorPos : (MarkBegin > MarkEnd ? MarkBegin : MarkEnd);
466 if (lineNo > 0) {
467 s32 cp = CursorPos - BrokenTextPositions[lineNo];
468 if ((s32)BrokenText[lineNo - 1].size() < cp)
469 CursorPos = BrokenTextPositions[lineNo - 1] + core::max_((u32)1, BrokenText[lineNo - 1].size()) - 1;
470 else
471 CursorPos = BrokenTextPositions[lineNo - 1] + cp;
474 if (event.KeyInput.Shift) {
475 newMarkBegin = mb;
476 newMarkEnd = CursorPos;
477 } else {
478 newMarkBegin = 0;
479 newMarkEnd = 0;
482 } else {
483 return false;
485 break;
486 case KEY_DOWN:
487 if (MultiLine || (WordWrap && BrokenText.size() > 1)) {
488 s32 lineNo = getLineFromPos(CursorPos);
489 s32 mb = (MarkBegin == MarkEnd) ? CursorPos : (MarkBegin < MarkEnd ? MarkBegin : MarkEnd);
490 if (lineNo < (s32)BrokenText.size() - 1) {
491 s32 cp = CursorPos - BrokenTextPositions[lineNo];
492 if ((s32)BrokenText[lineNo + 1].size() < cp)
493 CursorPos = BrokenTextPositions[lineNo + 1] + core::max_((u32)1, BrokenText[lineNo + 1].size()) - 1;
494 else
495 CursorPos = BrokenTextPositions[lineNo + 1] + cp;
498 if (event.KeyInput.Shift) {
499 newMarkBegin = mb;
500 newMarkEnd = CursorPos;
501 } else {
502 newMarkBegin = 0;
503 newMarkEnd = 0;
506 } else {
507 return false;
509 break;
510 case KEY_INSERT:
511 if (!isEnabled())
512 break;
514 OverwriteMode = !OverwriteMode;
515 break;
516 case KEY_DELETE:
517 if (!isEnabled())
518 break;
520 if (keyDelete()) {
521 BlinkStartTime = os::Timer::getTime();
522 newMarkBegin = 0;
523 newMarkEnd = 0;
524 textChanged = true;
526 break;
527 default:
528 return false;
530 } else {
531 // default keyboard handling
532 switch (event.KeyInput.Key) {
533 case KEY_RETURN:
534 if (MultiLine) {
535 inputChar(L'\n');
536 } else {
537 calculateScrollPos();
538 sendGuiEvent(EGET_EDITBOX_ENTER);
540 return true;
542 case KEY_BACK:
543 if (!isEnabled())
544 break;
546 if (Text.size()) {
547 core::stringw s;
549 if (MarkBegin != MarkEnd) {
550 // delete marked text
551 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
552 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
554 s = Text.subString(0, realmbgn);
555 s.append(Text.subString(realmend, Text.size() - realmend));
556 Text = s;
558 CursorPos = realmbgn;
559 } else {
560 // delete text behind cursor
561 if (CursorPos > 0)
562 s = Text.subString(0, CursorPos - 1);
563 else
564 s = L"";
565 s.append(Text.subString(CursorPos, Text.size() - CursorPos));
566 Text = s;
567 --CursorPos;
570 if (CursorPos < 0)
571 CursorPos = 0;
572 BlinkStartTime = os::Timer::getTime();
573 newMarkBegin = 0;
574 newMarkEnd = 0;
575 textChanged = true;
577 break;
579 case KEY_DELETE:
581 // At least on X11 we get a char with 127 when the delete key is pressed.
582 // We get no char when the delete key on numkeys is pressed with numlock off (handled in the other case calling keyDelete as Char is then 0).
583 // We get a keykode != 127 when delete key on numlock is pressed with numlock on.
584 if (event.KeyInput.Char == 127) {
585 if (!isEnabled())
586 break;
588 if (keyDelete()) {
589 BlinkStartTime = os::Timer::getTime();
590 newMarkBegin = 0;
591 newMarkEnd = 0;
592 textChanged = true;
594 break;
595 } else {
596 inputChar(event.KeyInput.Char);
597 return true;
600 case KEY_ESCAPE:
601 case KEY_TAB:
602 case KEY_SHIFT:
603 case KEY_F1:
604 case KEY_F2:
605 case KEY_F3:
606 case KEY_F4:
607 case KEY_F5:
608 case KEY_F6:
609 case KEY_F7:
610 case KEY_F8:
611 case KEY_F9:
612 case KEY_F10:
613 case KEY_F11:
614 case KEY_F12:
615 case KEY_F13:
616 case KEY_F14:
617 case KEY_F15:
618 case KEY_F16:
619 case KEY_F17:
620 case KEY_F18:
621 case KEY_F19:
622 case KEY_F20:
623 case KEY_F21:
624 case KEY_F22:
625 case KEY_F23:
626 case KEY_F24:
627 // ignore these keys
628 return false;
630 default:
631 inputChar(event.KeyInput.Char);
632 return true;
636 // Set new text markers
637 setTextMarkers(newMarkBegin, newMarkEnd);
639 // break the text if it has changed
640 if (textChanged) {
641 breakText();
642 calculateScrollPos();
643 sendGuiEvent(EGET_EDITBOX_CHANGED);
644 } else {
645 calculateScrollPos();
648 return true;
651 bool CGUIEditBox::keyDelete()
653 if (Text.size() != 0) {
654 core::stringw s;
656 if (MarkBegin != MarkEnd) {
657 // delete marked text
658 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
659 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
661 s = Text.subString(0, realmbgn);
662 s.append(Text.subString(realmend, Text.size() - realmend));
663 Text = s;
665 CursorPos = realmbgn;
666 } else {
667 // delete text before cursor
668 s = Text.subString(0, CursorPos);
669 s.append(Text.subString(CursorPos + 1, Text.size() - CursorPos - 1));
670 Text = s;
673 if (CursorPos > (s32)Text.size())
674 CursorPos = (s32)Text.size();
676 return true;
679 return false;
682 //! draws the element and its children
683 void CGUIEditBox::draw()
685 if (!IsVisible)
686 return;
688 const bool focus = Environment->hasFocus(this);
690 IGUISkin *skin = Environment->getSkin();
691 if (!skin)
692 return;
694 EGUI_DEFAULT_COLOR bgCol = EGDC_GRAY_EDITABLE;
695 if (isEnabled())
696 bgCol = focus ? EGDC_FOCUSED_EDITABLE : EGDC_EDITABLE;
698 if (!Border && Background) {
699 skin->draw2DRectangle(this, skin->getColor(bgCol), AbsoluteRect, &AbsoluteClippingRect);
702 if (Border) {
703 // draw the border
704 skin->draw3DSunkenPane(this, skin->getColor(bgCol), false, Background, AbsoluteRect, &AbsoluteClippingRect);
706 calculateFrameRect();
709 core::rect<s32> localClipRect = FrameRect;
710 localClipRect.clipAgainst(AbsoluteClippingRect);
712 // draw the text
714 IGUIFont *font = getActiveFont();
716 s32 cursorLine = 0;
717 s32 charcursorpos = 0;
719 if (font) {
720 if (LastBreakFont != font) {
721 breakText();
724 // calculate cursor pos
726 core::stringw *txtLine = &Text;
727 s32 startPos = 0;
729 core::stringw s, s2;
731 // get mark position
732 const bool ml = (!PasswordBox && (WordWrap || MultiLine));
733 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
734 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
735 const s32 hlineStart = ml ? getLineFromPos(realmbgn) : 0;
736 const s32 hlineCount = ml ? getLineFromPos(realmend) - hlineStart + 1 : 1;
737 const s32 lineCount = ml ? BrokenText.size() : 1;
739 // Save the override color information.
740 // Then, alter it if the edit box is disabled.
741 const bool prevOver = OverrideColorEnabled;
742 const video::SColor prevColor = OverrideColor;
744 if (Text.size()) {
745 if (!isEnabled() && !OverrideColorEnabled) {
746 OverrideColorEnabled = true;
747 OverrideColor = skin->getColor(EGDC_GRAY_TEXT);
750 for (s32 i = 0; i < lineCount; ++i) {
751 setTextRect(i);
753 // clipping test - don't draw anything outside the visible area
754 core::rect<s32> c = localClipRect;
755 c.clipAgainst(CurrentTextRect);
756 if (!c.isValid())
757 continue;
759 // get current line
760 if (PasswordBox) {
761 if (BrokenText.size() != 1) {
762 BrokenText.clear();
763 BrokenText.push_back(core::stringw());
765 if (BrokenText[0].size() != Text.size()) {
766 BrokenText[0] = Text;
767 for (u32 q = 0; q < Text.size(); ++q) {
768 BrokenText[0][q] = PasswordChar;
771 txtLine = &BrokenText[0];
772 startPos = 0;
773 } else {
774 txtLine = ml ? &BrokenText[i] : &Text;
775 startPos = ml ? BrokenTextPositions[i] : 0;
778 // draw normal text
779 font->draw(txtLine->c_str(), CurrentTextRect,
780 OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_BUTTON_TEXT),
781 false, true, &localClipRect);
783 // draw mark and marked text
784 if (focus && MarkBegin != MarkEnd && i >= hlineStart && i < hlineStart + hlineCount) {
786 s32 mbegin = 0, mend = 0;
787 s32 lineStartPos = 0, lineEndPos = txtLine->size();
789 if (i == hlineStart) {
790 // highlight start is on this line
791 s = txtLine->subString(0, realmbgn - startPos);
792 mbegin = font->getDimension(s.c_str()).Width;
794 // deal with kerning
795 mbegin += font->getKerningWidth(
796 &((*txtLine)[realmbgn - startPos]),
797 realmbgn - startPos > 0 ? &((*txtLine)[realmbgn - startPos - 1]) : 0);
799 lineStartPos = realmbgn - startPos;
801 if (i == hlineStart + hlineCount - 1) {
802 // highlight end is on this line
803 s2 = txtLine->subString(0, realmend - startPos);
804 mend = font->getDimension(s2.c_str()).Width;
805 lineEndPos = (s32)s2.size();
806 } else
807 mend = font->getDimension(txtLine->c_str()).Width;
809 CurrentTextRect.UpperLeftCorner.X += mbegin;
810 CurrentTextRect.LowerRightCorner.X = CurrentTextRect.UpperLeftCorner.X + mend - mbegin;
812 // draw mark
813 skin->draw2DRectangle(this, skin->getColor(EGDC_HIGH_LIGHT), CurrentTextRect, &localClipRect);
815 // draw marked text
816 s = txtLine->subString(lineStartPos, lineEndPos - lineStartPos);
818 if (s.size())
819 font->draw(s.c_str(), CurrentTextRect,
820 OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_HIGH_LIGHT_TEXT),
821 false, true, &localClipRect);
825 // Return the override color information to its previous settings.
826 OverrideColorEnabled = prevOver;
827 OverrideColor = prevColor;
830 // draw cursor
831 if (isEnabled()) {
832 if (WordWrap || MultiLine) {
833 cursorLine = getLineFromPos(CursorPos);
834 txtLine = &BrokenText[cursorLine];
835 startPos = BrokenTextPositions[cursorLine];
837 s = txtLine->subString(0, CursorPos - startPos);
838 charcursorpos = font->getDimension(s.c_str()).Width +
839 font->getKerningWidth(CursorChar.c_str(), CursorPos - startPos > 0 ? &((*txtLine)[CursorPos - startPos - 1]) : 0);
841 if (focus && (CursorBlinkTime == 0 || (os::Timer::getTime() - BlinkStartTime) % (2 * CursorBlinkTime) < CursorBlinkTime)) {
842 setTextRect(cursorLine);
843 CurrentTextRect.UpperLeftCorner.X += charcursorpos;
845 if (OverwriteMode) {
846 core::stringw character = Text.subString(CursorPos, 1);
847 s32 mend = font->getDimension(character.c_str()).Width;
848 // Make sure the cursor box has at least some width to it
849 if (mend <= 0)
850 mend = font->getDimension(CursorChar.c_str()).Width;
851 CurrentTextRect.LowerRightCorner.X = CurrentTextRect.UpperLeftCorner.X + mend;
852 skin->draw2DRectangle(this, skin->getColor(EGDC_HIGH_LIGHT), CurrentTextRect, &localClipRect);
853 font->draw(character.c_str(), CurrentTextRect,
854 OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_HIGH_LIGHT_TEXT),
855 false, true, &localClipRect);
856 } else {
857 font->draw(CursorChar, CurrentTextRect,
858 OverrideColorEnabled ? OverrideColor : skin->getColor(EGDC_BUTTON_TEXT),
859 false, true, &localClipRect);
865 // draw children
866 IGUIElement::draw();
869 //! Sets the new caption of this element.
870 void CGUIEditBox::setText(const wchar_t *text)
872 Text = text;
873 if (u32(CursorPos) > Text.size())
874 CursorPos = Text.size();
875 HScrollPos = 0;
876 breakText();
879 //! Enables or disables automatic scrolling with cursor position
880 //! \param enable: If set to true, the text will move around with the cursor position
881 void CGUIEditBox::setAutoScroll(bool enable)
883 AutoScroll = enable;
886 //! Checks to see if automatic scrolling is enabled
887 //! \return true if automatic scrolling is enabled, false if not
888 bool CGUIEditBox::isAutoScrollEnabled() const
890 return AutoScroll;
893 //! Gets the area of the text in the edit box
894 //! \return Returns the size in pixels of the text
895 core::dimension2du CGUIEditBox::getTextDimension()
897 core::rect<s32> ret;
899 setTextRect(0);
900 ret = CurrentTextRect;
902 for (u32 i = 1; i < BrokenText.size(); ++i) {
903 setTextRect(i);
904 ret.addInternalPoint(CurrentTextRect.UpperLeftCorner);
905 ret.addInternalPoint(CurrentTextRect.LowerRightCorner);
908 return core::dimension2du(ret.getSize());
911 //! Sets the maximum amount of characters which may be entered in the box.
912 //! \param max: Maximum amount of characters. If 0, the character amount is
913 //! infinity.
914 void CGUIEditBox::setMax(u32 max)
916 Max = max;
918 if (Text.size() > Max && Max != 0)
919 Text = Text.subString(0, Max);
922 //! Returns maximum amount of characters, previously set by setMax();
923 u32 CGUIEditBox::getMax() const
925 return Max;
928 //! Set the character used for the cursor.
929 /** By default it's "_" */
930 void CGUIEditBox::setCursorChar(const wchar_t cursorChar)
932 CursorChar[0] = cursorChar;
935 //! Get the character used for the cursor.
936 wchar_t CGUIEditBox::getCursorChar() const
938 return CursorChar[0];
941 //! Set the blinktime for the cursor. 2x blinktime is one full cycle.
942 void CGUIEditBox::setCursorBlinkTime(irr::u32 timeMs)
944 CursorBlinkTime = timeMs;
947 //! Get the cursor blinktime
948 irr::u32 CGUIEditBox::getCursorBlinkTime() const
950 return CursorBlinkTime;
953 bool CGUIEditBox::processMouse(const SEvent &event)
955 switch (event.MouseInput.Event) {
956 case irr::EMIE_LMOUSE_LEFT_UP:
957 if (Environment->hasFocus(this)) {
958 CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y);
959 if (MouseMarking) {
960 setTextMarkers(MarkBegin, CursorPos);
962 MouseMarking = false;
963 calculateScrollPos();
964 return true;
966 break;
967 case irr::EMIE_MOUSE_MOVED: {
968 if (MouseMarking) {
969 CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y);
970 setTextMarkers(MarkBegin, CursorPos);
971 calculateScrollPos();
972 return true;
974 } break;
975 case EMIE_LMOUSE_PRESSED_DOWN:
976 if (!Environment->hasFocus(this)) { // can happen when events are manually send to the element
977 BlinkStartTime = os::Timer::getTime();
978 MouseMarking = true;
979 CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y);
980 setTextMarkers(CursorPos, CursorPos);
981 calculateScrollPos();
982 return true;
983 } else {
984 if (!AbsoluteClippingRect.isPointInside(
985 core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y))) {
986 return false;
987 } else {
988 // move cursor
989 CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y);
991 s32 newMarkBegin = MarkBegin;
992 if (!MouseMarking)
993 newMarkBegin = CursorPos;
995 MouseMarking = true;
996 setTextMarkers(newMarkBegin, CursorPos);
997 calculateScrollPos();
998 return true;
1001 case EMIE_MMOUSE_PRESSED_DOWN: {
1002 if (!AbsoluteClippingRect.isPointInside(core::position2d<s32>(
1003 event.MouseInput.X, event.MouseInput.Y)))
1004 return false;
1006 if (!Environment->hasFocus(this)) {
1007 BlinkStartTime = os::Timer::getTime();
1010 // move cursor and disable marking
1011 CursorPos = getCursorPos(event.MouseInput.X, event.MouseInput.Y);
1012 MouseMarking = false;
1013 setTextMarkers(CursorPos, CursorPos);
1015 // paste from the primary selection
1016 inputString([&] {
1017 irr::core::stringw inserted_text;
1018 if (!Operator)
1019 return inserted_text;
1020 const c8 *inserted_text_utf8 = Operator->getTextFromPrimarySelection();
1021 if (!inserted_text_utf8)
1022 return inserted_text;
1023 core::utf8ToWString(inserted_text, inserted_text_utf8);
1024 return inserted_text;
1025 }());
1027 return true;
1029 default:
1030 break;
1033 return false;
1036 s32 CGUIEditBox::getCursorPos(s32 x, s32 y)
1038 IGUIFont *font = getActiveFont();
1040 const u32 lineCount = (WordWrap || MultiLine) ? BrokenText.size() : 1;
1042 core::stringw *txtLine = 0;
1043 s32 startPos = 0;
1044 x += 3;
1046 for (u32 i = 0; i < lineCount; ++i) {
1047 setTextRect(i);
1048 if (i == 0 && y < CurrentTextRect.UpperLeftCorner.Y)
1049 y = CurrentTextRect.UpperLeftCorner.Y;
1050 if (i == lineCount - 1 && y > CurrentTextRect.LowerRightCorner.Y)
1051 y = CurrentTextRect.LowerRightCorner.Y;
1053 // is it inside this region?
1054 if (y >= CurrentTextRect.UpperLeftCorner.Y && y <= CurrentTextRect.LowerRightCorner.Y) {
1055 // we've found the clicked line
1056 txtLine = (WordWrap || MultiLine) ? &BrokenText[i] : &Text;
1057 startPos = (WordWrap || MultiLine) ? BrokenTextPositions[i] : 0;
1058 break;
1062 if (x < CurrentTextRect.UpperLeftCorner.X)
1063 x = CurrentTextRect.UpperLeftCorner.X;
1065 if (!txtLine)
1066 return 0;
1068 s32 idx = font->getCharacterFromPos(txtLine->c_str(), x - CurrentTextRect.UpperLeftCorner.X);
1070 // click was on or left of the line
1071 if (idx != -1)
1072 return idx + startPos;
1074 // click was off the right edge of the line, go to end.
1075 return txtLine->size() + startPos;
1078 //! Breaks the single text line.
1079 void CGUIEditBox::breakText()
1081 if ((!WordWrap && !MultiLine))
1082 return;
1084 BrokenText.clear(); // need to reallocate :/
1085 BrokenTextPositions.set_used(0);
1087 IGUIFont *font = getActiveFont();
1088 if (!font)
1089 return;
1091 LastBreakFont = font;
1093 core::stringw line;
1094 core::stringw word;
1095 core::stringw whitespace;
1096 s32 lastLineStart = 0;
1097 s32 size = Text.size();
1098 s32 length = 0;
1099 s32 elWidth = RelativeRect.getWidth() - 6;
1100 wchar_t c;
1102 for (s32 i = 0; i < size; ++i) {
1103 c = Text[i];
1104 bool lineBreak = false;
1106 if (c == L'\r') { // Mac or Windows breaks
1107 lineBreak = true;
1108 c = 0;
1109 if (Text[i + 1] == L'\n') { // Windows breaks
1110 // TODO: I (Michael) think that we shouldn't change the text given by the user for whatever reason.
1111 // Instead rework the cursor positioning to be able to handle this (but not in stable release
1112 // branch as users might already expect this behavior).
1113 Text.erase(i + 1);
1114 --size;
1115 if (CursorPos > i)
1116 --CursorPos;
1118 } else if (c == L'\n') { // Unix breaks
1119 lineBreak = true;
1120 c = 0;
1123 // don't break if we're not a multi-line edit box
1124 if (!MultiLine)
1125 lineBreak = false;
1127 if (c == L' ' || c == 0 || i == (size - 1)) {
1128 // here comes the next whitespace, look if
1129 // we can break the last word to the next line
1130 // We also break whitespace, otherwise cursor would vanish beside the right border.
1131 s32 whitelgth = font->getDimension(whitespace.c_str()).Width;
1132 s32 worldlgth = font->getDimension(word.c_str()).Width;
1134 if (WordWrap && length + worldlgth + whitelgth > elWidth && line.size() > 0) {
1135 // break to next line
1136 length = worldlgth;
1137 BrokenText.push_back(line);
1138 BrokenTextPositions.push_back(lastLineStart);
1139 lastLineStart = i - (s32)word.size();
1140 line = word;
1141 } else {
1142 // add word to line
1143 line += whitespace;
1144 line += word;
1145 length += whitelgth + worldlgth;
1148 word = L"";
1149 whitespace = L"";
1151 if (c)
1152 whitespace += c;
1154 // compute line break
1155 if (lineBreak) {
1156 line += whitespace;
1157 line += word;
1158 BrokenText.push_back(line);
1159 BrokenTextPositions.push_back(lastLineStart);
1160 lastLineStart = i + 1;
1161 line = L"";
1162 word = L"";
1163 whitespace = L"";
1164 length = 0;
1166 } else {
1167 // yippee this is a word..
1168 word += c;
1172 line += whitespace;
1173 line += word;
1174 BrokenText.push_back(line);
1175 BrokenTextPositions.push_back(lastLineStart);
1178 // TODO: that function does interpret VAlign according to line-index (indexed line is placed on top-center-bottom)
1179 // but HAlign according to line-width (pixels) and not by row.
1180 // Intuitively I suppose HAlign handling is better as VScrollPos should handle the line-scrolling.
1181 // But please no one change this without also rewriting (and this time fucking testing!!!) autoscrolling (I noticed this when fixing the old autoscrolling).
1182 void CGUIEditBox::setTextRect(s32 line)
1184 if (line < 0)
1185 return;
1187 IGUIFont *font = getActiveFont();
1188 if (!font)
1189 return;
1191 core::dimension2du d;
1193 // get text dimension
1194 const u32 lineCount = (WordWrap || MultiLine) ? BrokenText.size() : 1;
1195 if (WordWrap || MultiLine) {
1196 d = font->getDimension(BrokenText[line].c_str());
1197 } else {
1198 d = font->getDimension(Text.c_str());
1199 d.Height = AbsoluteRect.getHeight();
1201 d.Height += font->getKerningHeight();
1203 // justification
1204 switch (HAlign) {
1205 case EGUIA_CENTER:
1206 // align to h centre
1207 CurrentTextRect.UpperLeftCorner.X = (FrameRect.getWidth() / 2) - (d.Width / 2);
1208 CurrentTextRect.LowerRightCorner.X = (FrameRect.getWidth() / 2) + (d.Width / 2);
1209 break;
1210 case EGUIA_LOWERRIGHT:
1211 // align to right edge
1212 CurrentTextRect.UpperLeftCorner.X = FrameRect.getWidth() - d.Width;
1213 CurrentTextRect.LowerRightCorner.X = FrameRect.getWidth();
1214 break;
1215 default:
1216 // align to left edge
1217 CurrentTextRect.UpperLeftCorner.X = 0;
1218 CurrentTextRect.LowerRightCorner.X = d.Width;
1221 switch (VAlign) {
1222 case EGUIA_CENTER:
1223 // align to v centre
1224 CurrentTextRect.UpperLeftCorner.Y =
1225 (FrameRect.getHeight() / 2) - (lineCount * d.Height) / 2 + d.Height * line;
1226 break;
1227 case EGUIA_LOWERRIGHT:
1228 // align to bottom edge
1229 CurrentTextRect.UpperLeftCorner.Y =
1230 FrameRect.getHeight() - lineCount * d.Height + d.Height * line;
1231 break;
1232 default:
1233 // align to top edge
1234 CurrentTextRect.UpperLeftCorner.Y = d.Height * line;
1235 break;
1238 CurrentTextRect.UpperLeftCorner.X -= HScrollPos;
1239 CurrentTextRect.LowerRightCorner.X -= HScrollPos;
1240 CurrentTextRect.UpperLeftCorner.Y -= VScrollPos;
1241 CurrentTextRect.LowerRightCorner.Y = CurrentTextRect.UpperLeftCorner.Y + d.Height;
1243 CurrentTextRect += FrameRect.UpperLeftCorner;
1246 s32 CGUIEditBox::getLineFromPos(s32 pos)
1248 if (!WordWrap && !MultiLine)
1249 return 0;
1251 s32 i = 0;
1252 while (i < (s32)BrokenTextPositions.size()) {
1253 if (BrokenTextPositions[i] > pos)
1254 return i - 1;
1255 ++i;
1257 return (s32)BrokenTextPositions.size() - 1;
1260 void CGUIEditBox::inputChar(wchar_t c)
1262 if (c == 0)
1263 return;
1264 core::stringw s(&c, 1);
1265 inputString(s);
1268 void CGUIEditBox::inputString(const core::stringw &str)
1270 if (!isEnabled())
1271 return;
1273 core::stringw s;
1274 u32 len = str.size();
1276 if (MarkBegin != MarkEnd) {
1277 // replace marked text
1278 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
1279 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
1281 s = Text.subString(0, realmbgn);
1282 s.append(str);
1283 s.append(Text.subString(realmend, Text.size() - realmend));
1284 Text = s;
1285 CursorPos = realmbgn + len;
1286 } else if (OverwriteMode) {
1287 // check to see if we are at the end of the text
1288 if ((u32)CursorPos + len < Text.size()) {
1289 bool isEOL = false;
1290 s32 EOLPos;
1291 for (u32 i = CursorPos; i < CursorPos + len && i < Max; i++) {
1292 if (Text[i] == L'\n' || Text[i] == L'\r') {
1293 isEOL = true;
1294 EOLPos = i;
1295 break;
1298 if (!isEOL || Text.size() + len <= Max || Max == 0) {
1299 s = Text.subString(0, CursorPos);
1300 s.append(str);
1301 if (isEOL) {
1302 // just keep appending to the current line
1303 // This follows the behavior of other gui libraries behaviors
1304 s.append(Text.subString(EOLPos, Text.size() - EOLPos));
1305 } else {
1306 // replace the next character
1307 s.append(Text.subString(CursorPos + len, Text.size() - CursorPos - len));
1309 Text = s;
1310 CursorPos += len;
1312 } else if (Text.size() + len <= Max || Max == 0) {
1313 // add new character because we are at the end of the string
1314 s = Text.subString(0, CursorPos);
1315 s.append(str);
1316 s.append(Text.subString(CursorPos + len, Text.size() - CursorPos - len));
1317 Text = s;
1318 CursorPos += len;
1320 } else if (Text.size() + len <= Max || Max == 0) {
1321 // add new character
1322 s = Text.subString(0, CursorPos);
1323 s.append(str);
1324 s.append(Text.subString(CursorPos, Text.size() - CursorPos));
1325 Text = s;
1326 CursorPos += len;
1329 BlinkStartTime = os::Timer::getTime();
1330 setTextMarkers(0, 0);
1332 breakText();
1333 calculateScrollPos();
1334 sendGuiEvent(EGET_EDITBOX_CHANGED);
1337 // calculate autoscroll
1338 void CGUIEditBox::calculateScrollPos()
1340 if (!AutoScroll)
1341 return;
1343 IGUIFont *font = getActiveFont();
1344 if (!font)
1345 return;
1347 s32 cursLine = getLineFromPos(CursorPos);
1348 if (cursLine < 0)
1349 return;
1350 setTextRect(cursLine);
1351 const bool hasBrokenText = MultiLine || WordWrap;
1353 // Check horizonal scrolling
1354 // NOTE: Calculations different to vertical scrolling because setTextRect interprets VAlign relative to line but HAlign not relative to row
1356 // get cursor position
1357 // get cursor area
1358 irr::u32 cursorWidth = font->getDimension(CursorChar.c_str()).Width;
1359 core::stringw *txtLine = hasBrokenText ? &BrokenText[cursLine] : &Text;
1360 s32 cPos = hasBrokenText ? CursorPos - BrokenTextPositions[cursLine] : CursorPos; // column
1361 s32 cStart = font->getDimension(txtLine->subString(0, cPos).c_str()).Width; // pixels from text-start
1362 s32 cEnd = cStart + cursorWidth;
1363 s32 txtWidth = font->getDimension(txtLine->c_str()).Width;
1365 if (txtWidth < FrameRect.getWidth()) {
1366 // TODO: Needs a clean left and right gap removal depending on HAlign, similar to vertical scrolling tests for top/bottom.
1367 // This check just fixes the case where it was most noticable (text smaller than clipping area).
1369 HScrollPos = 0;
1370 setTextRect(cursLine);
1373 if (CurrentTextRect.UpperLeftCorner.X + cStart < FrameRect.UpperLeftCorner.X) {
1374 // cursor to the left of the clipping area
1375 HScrollPos -= FrameRect.UpperLeftCorner.X - (CurrentTextRect.UpperLeftCorner.X + cStart);
1376 setTextRect(cursLine);
1378 // TODO: should show more characters to the left when we're scrolling left
1379 // and the cursor reaches the border.
1380 } else if (CurrentTextRect.UpperLeftCorner.X + cEnd > FrameRect.LowerRightCorner.X) {
1381 // cursor to the right of the clipping area
1382 HScrollPos += (CurrentTextRect.UpperLeftCorner.X + cEnd) - FrameRect.LowerRightCorner.X;
1383 setTextRect(cursLine);
1387 // calculate vertical scrolling
1388 if (hasBrokenText) {
1389 irr::u32 lineHeight = font->getDimension(L"A").Height + font->getKerningHeight();
1390 // only up to 1 line fits?
1391 if (lineHeight >= (irr::u32)FrameRect.getHeight()) {
1392 VScrollPos = 0;
1393 setTextRect(cursLine);
1394 s32 unscrolledPos = CurrentTextRect.UpperLeftCorner.Y;
1395 s32 pivot = FrameRect.UpperLeftCorner.Y;
1396 switch (VAlign) {
1397 case EGUIA_CENTER:
1398 pivot += FrameRect.getHeight() / 2;
1399 unscrolledPos += lineHeight / 2;
1400 break;
1401 case EGUIA_LOWERRIGHT:
1402 pivot += FrameRect.getHeight();
1403 unscrolledPos += lineHeight;
1404 break;
1405 default:
1406 break;
1408 VScrollPos = unscrolledPos - pivot;
1409 setTextRect(cursLine);
1410 } else {
1411 // First 2 checks are necessary when people delete lines
1412 setTextRect(0);
1413 if (CurrentTextRect.UpperLeftCorner.Y > FrameRect.UpperLeftCorner.Y && VAlign != EGUIA_LOWERRIGHT) {
1414 // first line is leaving a gap on top
1415 VScrollPos = 0;
1416 } else if (VAlign != EGUIA_UPPERLEFT) {
1417 u32 lastLine = BrokenTextPositions.empty() ? 0 : BrokenTextPositions.size() - 1;
1418 setTextRect(lastLine);
1419 if (CurrentTextRect.LowerRightCorner.Y < FrameRect.LowerRightCorner.Y) {
1420 // last line is leaving a gap on bottom
1421 VScrollPos -= FrameRect.LowerRightCorner.Y - CurrentTextRect.LowerRightCorner.Y;
1425 setTextRect(cursLine);
1426 if (CurrentTextRect.UpperLeftCorner.Y < FrameRect.UpperLeftCorner.Y) {
1427 // text above valid area
1428 VScrollPos -= FrameRect.UpperLeftCorner.Y - CurrentTextRect.UpperLeftCorner.Y;
1429 setTextRect(cursLine);
1430 } else if (CurrentTextRect.LowerRightCorner.Y > FrameRect.LowerRightCorner.Y) {
1431 // text below valid area
1432 VScrollPos += CurrentTextRect.LowerRightCorner.Y - FrameRect.LowerRightCorner.Y;
1433 setTextRect(cursLine);
1439 void CGUIEditBox::calculateFrameRect()
1441 FrameRect = AbsoluteRect;
1442 IGUISkin *skin = 0;
1443 if (Environment)
1444 skin = Environment->getSkin();
1445 if (Border && skin) {
1446 FrameRect.UpperLeftCorner.X += skin->getSize(EGDS_TEXT_DISTANCE_X) + 1;
1447 FrameRect.UpperLeftCorner.Y += skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1;
1448 FrameRect.LowerRightCorner.X -= skin->getSize(EGDS_TEXT_DISTANCE_X) + 1;
1449 FrameRect.LowerRightCorner.Y -= skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1;
1453 //! set text markers
1454 void CGUIEditBox::setTextMarkers(s32 begin, s32 end)
1456 if (begin != MarkBegin || end != MarkEnd) {
1457 MarkBegin = begin;
1458 MarkEnd = end;
1460 if (!PasswordBox && Operator && MarkBegin != MarkEnd) {
1461 // copy to primary selection
1462 const s32 realmbgn = MarkBegin < MarkEnd ? MarkBegin : MarkEnd;
1463 const s32 realmend = MarkBegin < MarkEnd ? MarkEnd : MarkBegin;
1465 core::stringc s;
1466 wStringToUTF8(s, Text.subString(realmbgn, realmend - realmbgn));
1467 Operator->copyToPrimarySelection(s.c_str());
1470 sendGuiEvent(EGET_EDITBOX_MARKING_CHANGED);
1474 //! send some gui event to parent
1475 void CGUIEditBox::sendGuiEvent(EGUI_EVENT_TYPE type)
1477 if (Parent) {
1478 SEvent e;
1479 e.EventType = EET_GUI_EVENT;
1480 e.GUIEvent.Caller = this;
1481 e.GUIEvent.Element = 0;
1482 e.GUIEvent.EventType = type;
1484 Parent->OnEvent(e);
1488 //! Returns whether the element takes input from the IME
1489 bool CGUIEditBox::acceptsIME()
1491 return isEnabled();
1494 } // end namespace gui
1495 } // end namespace irr