1 #include "gui/interface/Textbox.h"
2 
3 #include "Config.h"
4 #include "Platform.h"
5 #include "Format.h"
6 #include "PowderToy.h"
7 
8 #include "graphics/Graphics.h"
9 
10 #include "gui/interface/Point.h"
11 #include "gui/interface/Keys.h"
12 #include "gui/interface/Mouse.h"
13 
14 #include "ContextMenu.h"
15 
16 using namespace ui;
17 
Textbox(Point position,Point size,String textboxText,String textboxPlaceholder)18 Textbox::Textbox(Point position, Point size, String textboxText, String textboxPlaceholder):
19 	Label(position, size, ""),
20 	ReadOnly(false),
21 	inputType(All),
22 	limit(String::npos),
23 	keyDown(0),
24 	characterDown(0),
25 	mouseDown(false),
26 	masked(false),
27 	border(true)
28 {
29 	placeHolder = textboxPlaceholder;
30 
31 	SetText(textboxText);
32 	cursor = text.length();
33 
34 	menu->RemoveItem(0);
35 	menu->AddItem(ContextMenuItem("Cut", 1, true));
36 	menu->AddItem(ContextMenuItem("Copy", 0, true));
37 	menu->AddItem(ContextMenuItem("Paste", 2, true));
38 }
39 
SetHidden(bool hidden)40 void Textbox::SetHidden(bool hidden)
41 {
42 	menu->RemoveItem(0);
43 	menu->RemoveItem(1);
44 	menu->RemoveItem(2);
45 	menu->AddItem(ContextMenuItem("Cut", 1, !hidden));
46 	menu->AddItem(ContextMenuItem("Copy", 0, !hidden));
47 	menu->AddItem(ContextMenuItem("Paste", 2, true));
48 
49 	masked = hidden;
50 }
51 
SetPlaceholder(String text)52 void Textbox::SetPlaceholder(String text)
53 {
54 	placeHolder = text;
55 }
56 
SetText(String newText)57 void Textbox::SetText(String newText)
58 {
59 	backingText = newText;
60 
61 	if(masked)
62 	{
63 		String maskedText = newText;
64 		std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
65 		Label::SetText(maskedText);
66 	}
67 	else
68 		Label::SetText(newText);
69 
70 	cursor = newText.length();
71 
72 	if(cursor)
73 	{
74 		textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
75 	}
76 	else
77 	{
78 		cursorPositionY = cursorPositionX = 0;
79 	}
80 }
81 
GetInputType()82 Textbox::ValidInput Textbox::GetInputType()
83 {
84 	return inputType;
85 }
86 
SetInputType(ValidInput input)87 void Textbox::SetInputType(ValidInput input)
88 {
89 	inputType = input;
90 }
91 
SetLimit(size_t limit)92 void Textbox::SetLimit(size_t limit)
93 {
94 	this->limit = limit;
95 }
96 
GetLimit()97 size_t Textbox::GetLimit()
98 {
99 	return limit;
100 }
101 
GetText()102 String Textbox::GetText()
103 {
104 	return backingText;
105 }
106 
OnContextMenuAction(int item)107 void Textbox::OnContextMenuAction(int item)
108 {
109 	switch(item)
110 	{
111 	case 0:
112 		copySelection();
113 		break;
114 	case 1:
115 		cutSelection();
116 		break;
117 	case 2:
118 		pasteIntoSelection();
119 		break;
120 	}
121 }
122 
resetCursorPosition()123 void Textbox::resetCursorPosition()
124 {
125 	textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
126 }
127 
TabFocus()128 void Textbox::TabFocus()
129 {
130 	GetParentWindow()->FocusComponent(this);
131 	selectAll();
132 }
133 
cutSelection()134 void Textbox::cutSelection()
135 {
136 	if (HasSelection())
137 	{
138 		if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
139 			return;
140 		String toCopy = backingText.Between(getLowerSelectionBound(), getHigherSelectionBound());
141 		ClipboardPush(format::CleanString(toCopy, false, true, false).ToUtf8());
142 		backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
143 		cursor = getLowerSelectionBound();
144 	}
145 	else
146 	{
147 		if (!backingText.length())
148 			return;
149 		ClipboardPush(format::CleanString(backingText, false, true, false).ToUtf8());
150 		backingText.clear();
151 		cursor = 0;
152 	}
153 	ClearSelection();
154 
155 	if(masked)
156 	{
157 		String maskedText = backingText;
158 		std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
159 		Label::SetText(maskedText);
160 	}
161 	else
162 	{
163 		text = backingText;
164 	}
165 
166 	updateTextWrapper();
167 	updateSelection();
168 	TextPosition(displayTextWrapper.WrappedText());
169 
170 	if(cursor)
171 	{
172 		textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
173 	}
174 	else
175 	{
176 		cursorPositionY = cursorPositionX = 0;
177 	}
178 	if (actionCallback.change)
179 		actionCallback.change();
180 }
181 
pasteIntoSelection()182 void Textbox::pasteIntoSelection()
183 {
184 	String newText = format::CleanString(ClipboardPull().FromUtf8(), true, true, inputType != Multiline, inputType == Number || inputType == Numeric);
185 	if (HasSelection())
186 	{
187 		if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
188 			return;
189 		backingText.EraseBetween(getLowerSelectionBound(), getHigherSelectionBound());
190 		cursor = getLowerSelectionBound();
191 	}
192 
193 	int regionWidth = Size.X;
194 	if (Appearance.icon)
195 		regionWidth -= 13;
196 	regionWidth -= Appearance.Margin.Left;
197 	regionWidth -= Appearance.Margin.Right;
198 
199 	if (limit != String::npos)
200 	{
201 		newText = newText.Substr(0, limit-backingText.length());
202 	}
203 	if (!multiline && Graphics::textwidth(backingText + newText) > regionWidth)
204 	{
205 		int pLimit = regionWidth - Graphics::textwidth(backingText);
206 		int pWidth = 0;
207 		auto it = newText.begin();
208 		while (it != newText.end())
209 		{
210 			auto w = Graphics::CharWidth(*it);
211 			if (pWidth + w > pLimit)
212 			{
213 				break;
214 			}
215 			pWidth += w;
216 			++it;
217 		}
218 		newText = String(newText.begin(), it);
219 	}
220 
221 	backingText.Insert(cursor, newText);
222 	cursor = cursor+newText.length();
223 	ClearSelection();
224 
225 	if(masked)
226 	{
227 		String maskedText = backingText;
228 		std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
229 		Label::SetText(maskedText);
230 	}
231 	else
232 	{
233 		text = backingText;
234 	}
235 
236 	updateTextWrapper();
237 	updateSelection();
238 	TextPosition(displayTextWrapper.WrappedText());
239 
240 	if(cursor)
241 	{
242 		textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
243 	}
244 	else
245 	{
246 		cursorPositionY = cursorPositionX = 0;
247 	}
248 	if (actionCallback.change)
249 		actionCallback.change();
250 }
251 
CharacterValid(int character)252 bool Textbox::CharacterValid(int character)
253 {
254 	switch(inputType)
255 	{
256 		case Numeric:
257 			if (character == '-' && cursor == 0 && backingText[0] != '-')
258 				return true;
259 		case Number:
260 			return (character >= '0' && character <= '9');
261 		case Multiline:
262 			if (character == '\n')
263 				return true;
264 		case All:
265 		default:
266 			return (character >= ' ' && character < 127);
267 	}
268 	return false;
269 }
270 
271 // TODO: proper unicode validation
StringValid(String text)272 bool Textbox::StringValid(String text)
273 {
274 	for (String::value_type c : text)
275 		if (!CharacterValid(c))
276 			return false;
277 	return true;
278 }
279 
Tick(float dt)280 void Textbox::Tick(float dt)
281 {
282 	Label::Tick(dt);
283 	if (!IsFocused())
284 	{
285 		keyDown = 0;
286 		characterDown = 0;
287 	}
288 	unsigned long time_pls = Platform::GetTime();
289 	if ((keyDown || characterDown) && repeatTime <= time_pls)
290 	{
291 		//OnVKeyPress(keyDown, characterDown, false, false, false);
292 		repeatTime = Platform::GetTime()+30;
293 	}
294 }
295 
OnKeyRelease(int key,int scan,bool repeat,bool shift,bool ctrl,bool alt)296 void Textbox::OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
297 {
298 	keyDown = 0;
299 	characterDown = 0;
300 }
301 
OnKeyPress(int key,int scan,bool repeat,bool shift,bool ctrl,bool alt)302 void Textbox::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
303 {
304 	characterDown = scan;
305 	keyDown = key;
306 	repeatTime = Platform::GetTime()+300;
307 	OnVKeyPress(key, scan, repeat, shift, ctrl, alt);
308 }
309 
OnVKeyPress(int key,int scan,bool repeat,bool shift,bool ctrl,bool alt)310 void Textbox::OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt)
311 {
312 	bool changed = false;
313 	if (ctrl && scan == SDL_SCANCODE_C && !masked && !repeat)
314 	{
315 		copySelection();
316 		return;
317 	}
318 	if (ctrl && scan == SDL_SCANCODE_V && !ReadOnly)
319 	{
320 		pasteIntoSelection();
321 		return;
322 	}
323 	if (ctrl && scan == SDL_SCANCODE_X && !masked && !repeat && !ReadOnly)
324 	{
325 		cutSelection();
326 		return;
327 	}
328 	if (ctrl && scan == SDL_SCANCODE_A)
329 	{
330 		selectAll();
331 		return;
332 	}
333 
334 	try
335 	{
336 		switch(key)
337 		{
338 		case SDLK_HOME:
339 			cursor = 0;
340 			ClearSelection();
341 			break;
342 		case SDLK_END:
343 			cursor = backingText.length();
344 			ClearSelection();
345 			break;
346 		case SDLK_LEFT:
347 			if(cursor > 0)
348 				cursor--;
349 			ClearSelection();
350 			break;
351 		case SDLK_RIGHT:
352 			if (cursor < (int)backingText.length())
353 				cursor++;
354 			ClearSelection();
355 			break;
356 		case SDLK_DELETE:
357 			if(ReadOnly)
358 				break;
359 			if (HasSelection())
360 			{
361 				if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
362 					return;
363 				backingText.Erase(getLowerSelectionBound(), getHigherSelectionBound());
364 				cursor = getLowerSelectionBound();
365 				changed = true;
366 			}
367 			else if (backingText.length() && cursor < (int)backingText.length())
368 			{
369 				if (ctrl)
370 				{
371 					size_t stopChar;
372 					stopChar = backingText.SplitByNot(" .,!?\n", cursor).PositionBefore();
373 					stopChar = backingText.SplitByAny(" .,!?\n", stopChar).PositionBefore();
374 					backingText.EraseBetween(cursor, stopChar);
375 				}
376 				else
377 					backingText.erase(cursor, 1);
378 				changed = true;
379 			}
380 			ClearSelection();
381 			break;
382 		case SDLK_BACKSPACE:
383 			if (ReadOnly)
384 				break;
385 			if (HasSelection())
386 			{
387 				if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
388 					return;
389 				backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
390 				cursor = getLowerSelectionBound();
391 				changed = true;
392 			}
393 			else if (backingText.length() && cursor > 0)
394 			{
395 				if (ctrl)
396 				{
397 					size_t stopChar;
398 					stopChar = backingText.SplitFromEndByNot(" .,!?\n", cursor).PositionBefore();
399 					if (stopChar == backingText.npos)
400 						stopChar = -1;
401 					else
402 						stopChar = backingText.SplitFromEndByAny(" .,!?\n", stopChar).PositionBefore();
403 					backingText.EraseBetween(stopChar+1, cursor);
404 					cursor = stopChar+1;
405 				}
406 				else
407 				{
408 					backingText.erase(cursor-1, 1);
409 					cursor--;
410 				}
411 				changed = true;
412 			}
413 			ClearSelection();
414 			break;
415 		case SDLK_RETURN:
416 			OnTextInput("\n");
417 			break;
418 		}
419 	}
420 	catch (std::out_of_range &e)
421 	{
422 		cursor = 0;
423 		backingText = "";
424 	}
425 	AfterTextChange(changed);
426 }
427 
AfterTextChange(bool changed)428 void Textbox::AfterTextChange(bool changed)
429 {
430 	if (cursor > (int)backingText.length())
431 		cursor = backingText.length();
432 
433 	if (changed)
434 	{
435 		if (inputType == Number)
436 		{
437 			//Remove extra preceding 0's
438 			while(backingText[0] == '0' && backingText.length()>1)
439 				backingText.erase(backingText.begin());
440 		}
441 
442 		if (masked)
443 		{
444 			String maskedText = backingText;
445 			std::fill(maskedText.begin(), maskedText.end(), 0xE00D);
446 			Label::SetText(maskedText);
447 		}
448 		else
449 		{
450 			text = backingText;
451 		}
452 	}
453 
454 	updateTextWrapper();
455 	updateSelection();
456 	TextPosition(displayTextWrapper.WrappedText());
457 
458 	if(cursor)
459 	{
460 		textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
461 	}
462 	else
463 	{
464 		cursorPositionY = cursorPositionX = 0;
465 	}
466 	if (changed && actionCallback.change)
467 		actionCallback.change();
468 }
469 
OnTextInput(String text)470 void Textbox::OnTextInput(String text)
471 {
472 	if (StringValid(text) && !ReadOnly)
473 	{
474 		if (HasSelection())
475 		{
476 			if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
477 				return;
478 			backingText.erase(backingText.begin()+getLowerSelectionBound(), backingText.begin()+getHigherSelectionBound());
479 			cursor = getLowerSelectionBound();
480 		}
481 
482 		int regionWidth = Size.X;
483 		if (Appearance.icon)
484 			regionWidth -= 13;
485 		regionWidth -= Appearance.Margin.Left;
486 		regionWidth -= Appearance.Margin.Right;
487 		if ((limit==String::npos || backingText.length() < limit) && (Graphics::textwidth(backingText + text) <= regionWidth || multiline))
488 		{
489 			if (cursor == (int)backingText.length())
490 			{
491 				backingText += text;
492 			}
493 			else
494 			{
495 				backingText.Insert(cursor, text);
496 			}
497 			cursor++;
498 		}
499 		ClearSelection();
500 		AfterTextChange(true);
501 	}
502 }
503 
OnMouseClick(int x,int y,unsigned button)504 void Textbox::OnMouseClick(int x, int y, unsigned button)
505 {
506 
507 	if (button != SDL_BUTTON_RIGHT)
508 	{
509 		mouseDown = true;
510 		auto index = textWrapper.Point2Index(x-textPosition.X, y-textPosition.Y);
511 		cursor = index.raw_index;
512 		if(cursor)
513 		{
514 			textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
515 		}
516 		else
517 		{
518 			cursorPositionY = cursorPositionX = 0;
519 		}
520 	}
521 	Label::OnMouseClick(x, y, button);
522 }
523 
OnMouseUp(int x,int y,unsigned button)524 void Textbox::OnMouseUp(int x, int y, unsigned button)
525 {
526 	mouseDown = false;
527 	Label::OnMouseUp(x, y, button);
528 }
529 
OnMouseMoved(int localx,int localy,int dx,int dy)530 void Textbox::OnMouseMoved(int localx, int localy, int dx, int dy)
531 {
532 	if(mouseDown)
533 	{
534 		auto index = textWrapper.Point2Index(localx-textPosition.X, localy-textPosition.Y);
535 		cursor = index.raw_index;
536 		if(cursor)
537 		{
538 			textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
539 		}
540 		else
541 		{
542 			cursorPositionY = cursorPositionX = 0;
543 		}
544 	}
545 	Label::OnMouseMoved(localx, localy, dx, dy);
546 }
547 
Draw(const Point & screenPos)548 void Textbox::Draw(const Point& screenPos)
549 {
550 	Label::Draw(screenPos);
551 
552 	Graphics * g = GetGraphics();
553 	if(IsFocused())
554 	{
555 		if(border) g->drawrect(screenPos.X, screenPos.Y, Size.X, Size.Y, 255, 255, 255, 255);
556 		g->draw_line(screenPos.X+textPosition.X+cursorPositionX, screenPos.Y-2+textPosition.Y+cursorPositionY, screenPos.X+textPosition.X+cursorPositionX, screenPos.Y+9+textPosition.Y+cursorPositionY, 255, 255, 255, 255);
557 	}
558 	else
559 	{
560 		if(!text.length())
561 		{
562 			g->drawtext(screenPos.X+textPosition.X, screenPos.Y+textPosition.Y, placeHolder, textColour.Red, textColour.Green, textColour.Blue, 170);
563 		}
564 		if(border) g->drawrect(screenPos.X, screenPos.Y, Size.X, Size.Y, 160, 160, 160, 255);
565 	}
566 	if(Appearance.icon)
567 		g->draw_icon(screenPos.X+iconPosition.X, screenPos.Y+iconPosition.Y, Appearance.icon);
568 }
569