1 /***************************************************************************
2  *      Mechanized Assault and Exploration Reloaded Projectfile            *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
18  ***************************************************************************/
19 
20 #include <algorithm>
21 
22 #include "ui/graphical/menu/widgets/lineedit.h"
23 #include "ui/graphical/menu/widgets/tools/validator.h"
24 #include "ui/graphical/application.h"
25 #include "ui/graphical/window.h"
26 #include "settings.h"
27 #include "video.h"
28 #include "input/mouse/mouse.h"
29 #include "input/keyboard/keyboard.h"
30 #include "utility/log.h"
31 #include "keys.h"
32 
33 //------------------------------------------------------------------------------
cLineEdit(const cBox<cPosition> & area,eLineEditFrameType frameType_,eUnicodeFontType fontType_)34 cLineEdit::cLineEdit (const cBox<cPosition>& area, eLineEditFrameType frameType_, eUnicodeFontType fontType_) :
35 	cClickableWidget (area),
36 	cursorVisibleTime (800),
37 	cursorInvisibleTime (500),
38 	text (""),
39 	fontType (fontType_),
40 	frameType (frameType_),
41 	cursorPos (0),
42 	startOffset (0),
43 	endOffset (0),
44 	readOnly (false),
45 	hasKeyFocus (false),
46 	showCursor (false)
47 {
48 	createBackground();
49 }
50 
51 //------------------------------------------------------------------------------
~cLineEdit()52 cLineEdit::~cLineEdit()
53 {}
54 
55 //------------------------------------------------------------------------------
getText()56 const std::string& cLineEdit::getText()
57 {
58 	return text;
59 }
60 
61 //------------------------------------------------------------------------------
setText(std::string text_)62 void cLineEdit::setText (std::string text_)
63 {
64 	std::swap (text, text_);
65 	if (validator)
66 	{
67 		const auto state = validator->validate (text);
68 		if (state != eValidatorState::Valid)
69 		{
70 			validator->fixup (text);
71 		}
72 	}
73 	resetTextPosition();
74 	textSet();
75 }
76 
77 //------------------------------------------------------------------------------
setReadOnly(bool readOnly_)78 void cLineEdit::setReadOnly (bool readOnly_)
79 {
80 	readOnly = readOnly_;
81 	setConsumeClick (!readOnly);
82 }
83 
84 //------------------------------------------------------------------------------
setValidator(std::unique_ptr<cValidator> validator_)85 void cLineEdit::setValidator (std::unique_ptr<cValidator> validator_)
86 {
87 	validator = std::move (validator_);
88 }
89 
90 //------------------------------------------------------------------------------
finishEditing()91 void cLineEdit::finishEditing()
92 {
93 	auto application = getActiveApplication();
94 	if (application)
95 	{
96 		application->releaseKeyFocus (*this);
97 	}
98 	else
99 	{
100 		finishEditingInternal();
101 	}
102 }
103 
104 //------------------------------------------------------------------------------
finishEditingInternal()105 void cLineEdit::finishEditingInternal()
106 {
107 	SDL_StopTextInput();
108 
109 	if (validator)
110 	{
111 		const auto state = validator->validate (text);
112 		if (state != eValidatorState::Valid)
113 		{
114 			validator->fixup (text);
115 			resetTextPosition();
116 		}
117 		editingFinished (state);
118 	}
119 	else
120 	{
121 		editingFinished (eValidatorState::Valid);
122 	}
123 }
124 //------------------------------------------------------------------------------
draw(SDL_Surface & destination,const cBox<cPosition> & clipRect)125 void cLineEdit::draw (SDL_Surface& destination, const cBox<cPosition>& clipRect)
126 {
127 	if (surface != nullptr)
128 	{
129 		SDL_Rect position = getArea().toSdlRect();
130 		SDL_BlitSurface (surface.get(), nullptr, &destination, &position);
131 	}
132 
133 	const auto offsetRect = getTextDrawOffset();
134 	const auto cursorXOffset = font->getFontSize (fontType) == FONT_SIZE_SMALL ? -1 : 0;
135 
136 	const cPosition textPosition = getPosition() + offsetRect;
137 
138 	font->showText (textPosition.x(), textPosition.y(), text.substr (startOffset, endOffset - startOffset), fontType);
139 	if (hasKeyFocus && !readOnly)
140 	{
141 		const auto now = std::chrono::steady_clock::now();
142 
143 		if (now - lastCursorBlinkTime > cursorVisibleTime)
144 		{
145 			showCursor = !showCursor;
146 			lastCursorBlinkTime = now;
147 		}
148 
149 		if (showCursor) font->showText (textPosition.x() + cursorXOffset + font->getTextWide (text.substr (startOffset, cursorPos - startOffset), fontType), textPosition.y(), "|", fontType);
150 	}
151 
152 	cClickableWidget::draw (destination, clipRect);
153 }
154 
155 //------------------------------------------------------------------------------
handleGetKeyFocus(cApplication & application)156 bool cLineEdit::handleGetKeyFocus (cApplication& application)
157 {
158 	if (readOnly) return false;
159 
160 	const auto hadKeyFocus = hasKeyFocus;
161 
162 	hasKeyFocus = true;
163 
164 	showCursor = true;
165 	lastCursorBlinkTime = std::chrono::steady_clock::now();
166 
167 	if (!hadKeyFocus)
168 	{
169 		cursorPos = (int)text.length();
170 		while (cursorPos > endOffset) doPosIncrease (endOffset, endOffset);
171 		while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosIncrease (startOffset, startOffset);
172 	}
173 
174 	SDL_StartTextInput();
175 
176 	return true;
177 }
178 
179 //------------------------------------------------------------------------------
handleLooseKeyFocus(cApplication & application)180 void cLineEdit::handleLooseKeyFocus (cApplication& application)
181 {
182 	if (hasKeyFocus)
183 	{
184 		hasKeyFocus = false;
185 		finishEditingInternal();
186 	}
187 }
188 
189 //------------------------------------------------------------------------------
handleClicked(cApplication & application,cMouse & mouse,eMouseButtonType button)190 bool cLineEdit::handleClicked (cApplication& application, cMouse& mouse, eMouseButtonType button)
191 {
192 	if (readOnly) return false;
193 
194 	int x = mouse.getPosition().x() - (getPosition().x() + getTextDrawOffset().x());
195 	int cursor = startOffset;
196 	while (font->getTextWide (text.substr (startOffset, cursor - startOffset), fontType) < x)
197 	{
198 		doPosIncrease (cursor, cursor);
199 		if (cursor >= endOffset)
200 		{
201 			cursor = endOffset;
202 			break;
203 		}
204 	}
205 	cursorPos = cursor;
206 
207 	return true;
208 }
209 
210 //------------------------------------------------------------------------------
createBackground()211 void cLineEdit::createBackground()
212 {
213 	if (frameType == eLineEditFrameType::Box)
214 	{
215 		//surface = SDL_CreateRGBSurface (0, getSize ().x (), getSize ().y (), Video.getColDepth (), 0, 0, 0, 0);
216 
217 		//SDL_FillRect (surface, nullptr, 0xFF00FF);
218 		//SDL_SetColorKey (surface, SDL_TRUE, 0xFF00FF);
219 	}
220 	else
221 	{
222 		surface = nullptr;
223 	}
224 }
225 
226 //------------------------------------------------------------------------------
getTextDrawOffset() const227 cPosition cLineEdit::getTextDrawOffset() const
228 {
229 	switch (frameType)
230 	{
231 		default:
232 		case eLineEditFrameType::None:
233 			return cPosition (0, 0);
234 		case eLineEditFrameType::Box:
235 			return cPosition (6, 3);
236 	}
237 }
238 
239 //------------------------------------------------------------------------------
getBorderSize() const240 int cLineEdit::getBorderSize() const
241 {
242 	switch (frameType)
243 	{
244 		default:
245 		case eLineEditFrameType::None:
246 			return 0;
247 		case eLineEditFrameType::Box:
248 			return 12;
249 	}
250 }
251 
252 //------------------------------------------------------------------------------
resetTextPosition()253 void cLineEdit::resetTextPosition()
254 {
255 	startOffset = 0;
256 	endOffset = (int)text.length();
257 	cursorPos = endOffset;
258 	while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosDecrease (endOffset);
259 }
260 
261 //------------------------------------------------------------------------------
doPosIncrease(int & value,int pos)262 void cLineEdit::doPosIncrease (int& value, int pos)
263 {
264 	if (pos < (int)text.length())
265 	{
266 		unsigned char c = text[pos];
267 		if ((c & 0xE0) == 0xE0) value += 3;
268 		else if ((c & 0xC0) == 0xC0) value += 2;
269 		else value += 1;
270 	}
271 
272 	if (value > (int)text.length())
273 	{
274 		value = (int)text.length();
275 		Log.write ("Invalid UTF-8 string in line edit: '" + text + "'", cLog::eLOG_TYPE_WARNING);
276 	}
277 }
278 
279 //------------------------------------------------------------------------------
doPosDecrease(int & pos)280 void cLineEdit::doPosDecrease (int& pos)
281 {
282 	if (pos > 0)
283 	{
284 		unsigned char c = text[pos - 1];
285 		while (((c & 0xE0) != 0xE0) && ((c & 0xC0) != 0xC0) && ((c & 0x80) == 0x80))
286 		{
287 			if (pos <= 1)
288 			{
289 				Log.write ("Invalid UTF-8 string in line edit: '" + text + "'", cLog::eLOG_TYPE_WARNING);
290 				break;
291 			}
292 
293 			pos--;
294 			c = text[pos - 1];
295 		}
296 		pos--;
297 	}
298 }
299 
300 //------------------------------------------------------------------------------
scrollLeft(bool changeCursor)301 void cLineEdit::scrollLeft (bool changeCursor)
302 {
303 	// makes the cursor go left
304 	if (changeCursor && cursorPos > 0) doPosDecrease (cursorPos);
305 
306 	if (cursorPos > 0) while (cursorPos - 1 < startOffset) doPosDecrease (startOffset);
307 	else while (cursorPos < startOffset) doPosDecrease (startOffset);
308 
309 	if (font->getTextWide (text.substr (startOffset, text.length() - startOffset), fontType) > getSize().x() - getBorderSize())
310 	{
311 		endOffset = (int)text.length();
312 		while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosDecrease (endOffset);
313 	}
314 }
315 
316 //------------------------------------------------------------------------------
scrollRight()317 void cLineEdit::scrollRight()
318 {
319 	// makes the cursor go right
320 	if (cursorPos < (int)text.length()) doPosIncrease (cursorPos, cursorPos);
321 	assert (cursorPos <= (int)text.length());
322 	while (cursorPos > endOffset) doPosIncrease (endOffset, endOffset);
323 	while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosIncrease (startOffset, startOffset);
324 }
325 
326 //------------------------------------------------------------------------------
deleteLeft()327 void cLineEdit::deleteLeft()
328 {
329 	// deletes the first character left from the cursor
330 	if (cursorPos > 0)
331 	{
332 		unsigned char c = text[cursorPos - 1];
333 		while (((c & 0xE0) != 0xE0) && ((c & 0xC0) != 0xC0) && ((c & 0x80) == 0x80))
334 		{
335 			if (cursorPos <= 1)
336 			{
337 				Log.write ("Invalid UTF-8 string in line edit: '" + text + "'", cLog::eLOG_TYPE_WARNING);
338 				break;
339 			}
340 
341 			text.erase (cursorPos - 1, 1);
342 			cursorPos--;
343 			c = text[cursorPos - 1];
344 		}
345 		text.erase (cursorPos - 1, 1);
346 		cursorPos--;
347 		endOffset = std::min<int> (text.length(), endOffset);
348 		scrollLeft (false);
349 	}
350 }
351 
352 //------------------------------------------------------------------------------
deleteRight()353 void cLineEdit::deleteRight()
354 {
355 	// deletes the first character right from the cursor
356 	if (cursorPos < (int)text.length())
357 	{
358 		unsigned char c = text[cursorPos];
359 		if ((c & 0xE0) == 0xE0) text.erase (cursorPos, 3);
360 		else if ((c & 0xC0) == 0xC0) text.erase (cursorPos, 2);
361 		else text.erase (cursorPos, 1);
362 		endOffset = std::min<int> (text.length(), endOffset);
363 	}
364 }
365 
366 //------------------------------------------------------------------------------
handleKeyPressed(cApplication & application,cKeyboard & keyboard,SDL_Keycode key)367 bool cLineEdit::handleKeyPressed (cApplication& application, cKeyboard& keyboard, SDL_Keycode key)
368 {
369 	if (readOnly || !hasKeyFocus) return false;
370 
371 	switch (key)
372 	{
373 		case SDLK_ESCAPE:
374 			escapePressed();
375 			application.releaseKeyFocus (*this);
376 			break;
377 		case SDLK_KP_ENTER: // fall through
378 		case SDLK_RETURN:
379 			returnPressed();
380 			break;
381 		case SDLK_LEFT:
382 			scrollLeft();
383 			break;
384 		case SDLK_RIGHT:
385 			scrollRight();
386 			break;
387 		case SDLK_HOME:
388 			cursorPos = 0;
389 			startOffset = 0;
390 			endOffset = (int)text.length();
391 			while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosDecrease (endOffset);
392 			break;
393 		case SDLK_END:
394 			cursorPos = (int)text.length();
395 			startOffset = 0;
396 			endOffset = (int)text.length();
397 			while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosIncrease (startOffset, startOffset);
398 			break;
399 		case SDLK_BACKSPACE:
400 			deleteLeft();
401 			break;
402 		case SDLK_DELETE:
403 			deleteRight();
404 			break;
405 		case SDLK_c:
406 			if (keyboard.getCurrentModifiers() & (toEnumFlag (eKeyModifierType::CtrlLeft) | eKeyModifierType::CtrlRight))
407 			{
408 				SDL_SetClipboardText (text.c_str());
409 			}
410 			break;
411 		case SDLK_x:
412 			if (keyboard.getCurrentModifiers() & (toEnumFlag (eKeyModifierType::CtrlLeft) | eKeyModifierType::CtrlRight))
413 			{
414 				SDL_SetClipboardText (text.c_str());
415 				text.clear();
416 				resetTextPosition();
417 			}
418 			break;
419 		case SDLK_v:
420 			if (keyboard.getCurrentModifiers() & (toEnumFlag (eKeyModifierType::CtrlLeft) | eKeyModifierType::CtrlRight) &&
421 				SDL_HasClipboardText())
422 			{
423 				const auto clipboardText = SDL_GetClipboardText();
424 
425 				if (clipboardText == nullptr) break;
426 
427 				const auto clipboardFree = makeScopedOperation ([clipboardText]() { SDL_free (clipboardText); });
428 
429 				std::string insertText (clipboardText);
430 
431 				if (validator)
432 				{
433 					const auto state = validator->validate (insertText);
434 					if (state == eValidatorState::Invalid) break;
435 				}
436 
437 				text.insert (cursorPos, insertText);
438 
439 				cursorPos += insertText.size();
440 
441 				endOffset = cursorPos;
442 
443 				while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosIncrease (startOffset, startOffset);
444 			}
445 			break;
446 		default: // normal characters are handled as textInput:
447 			break;
448 	}
449 	return true;
450 }
451 
452 //------------------------------------------------------------------------------
handleTextEntered(cApplication & application,cKeyboard & keyboard,const char * inputText)453 void cLineEdit::handleTextEntered (cApplication& application, cKeyboard& keyboard, const char* inputText)
454 {
455 	if (readOnly || !hasKeyFocus) return;
456 
457 	text.insert (cursorPos, inputText);
458 
459 	if (validator)
460 	{
461 		const auto state = validator->validate (text);
462 		if (state == eValidatorState::Invalid)
463 		{
464 			validator->fixup (text);
465 			resetTextPosition();
466 		}
467 	}
468 	if (cursorPos < (int)text.length()) doPosIncrease (cursorPos, cursorPos);
469 	if (cursorPos >= endOffset)
470 	{
471 		doPosIncrease (endOffset, endOffset);
472 		while (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosIncrease (startOffset, startOffset);
473 	}
474 	else
475 	{
476 		if (font->getTextWide (text.substr (startOffset, endOffset - startOffset), fontType) > getSize().x() - getBorderSize()) doPosDecrease (endOffset);
477 		else doPosIncrease (endOffset, cursorPos);
478 	}
479 }
480