1 /**
2  * @file
3  * @brief This node allow to edit a cvar text with the keyboard. When we
4  * click on the node, we active the edition, we can validate it with the ''RETURN'' key,
5  * or abort it with ''ESCAPE'' key. A validation fire a scriptable callback event.
6  * We can custom the mouse behaviour when we click outside the node in edition mode.
7  * It can validate or abort the edition.
8  * @todo allow to edit text without any cvar
9  * @todo add a custom max size
10  */
11 
12 /*
13 Copyright (C) 2002-2013 UFO: Alien Invasion.
14 
15 This program is free software; you can redistribute it and/or
16 modify it under the terms of the GNU General Public License
17 as published by the Free Software Foundation; either version 2
18 of the License, or (at your option) any later version.
19 
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
23 
24 See the GNU General Public License for more details.
25 
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, write to the Free Software
28 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
29 
30 */
31 
32 #include "../ui_main.h"
33 #include "../ui_nodes.h"
34 #include "../ui_font.h"
35 #include "../ui_parse.h"
36 #include "../ui_behaviour.h"
37 #include "../ui_input.h"
38 #include "../ui_actions.h"
39 #include "../ui_render.h"
40 #include "../ui_sprite.h"
41 #include "ui_node_textentry.h"
42 #include "ui_node_abstractnode.h"
43 #include "ui_node_panel.h"
44 
45 #include "../../client.h"
46 #include "../../../shared/utf8.h"
47 
48 #if SDL_VERSION_ATLEAST(2,0,0)
49 #include <SDL.h>
50 #else
51 #ifdef ANDROID
52 #include <SDL/SDL_screenkeyboard.h>
53 #endif
54 #endif
55 
56 #define EXTRADATA_TYPE textEntryExtraData_t
57 #define EXTRADATA(node) UI_EXTRADATA(node, EXTRADATA_TYPE)
58 
59 static const char CURSOR_ON = '|';		/**< Use as the cursor when we edit the text - visible */
60 static const char CURSOR_OFF = ' ';		/**< Use as the cursor when we edit the text - invisible*/
61 static const char HIDECHAR = '*';	/**< use as a mask for password */
62 
63 /* limit the input for cvar editing (base name, save slots and so on) */
64 #define MAX_CVAR_EDITING_LENGTH 256 /* MAXCMDLINE */
65 
66 /* global data */
67 static char cvarValueBackup[MAX_CVAR_EDITING_LENGTH];
68 static cvar_t* editedCvar = nullptr;
69 static bool isAborted = false;
70 
71 /**
72  * @brief callback from the keyboard
73  */
UI_TextEntryNodeValidateEdition(uiNode_t * node)74 static void UI_TextEntryNodeValidateEdition (uiNode_t* node)
75 {
76 	/* invalidate cache */
77 	editedCvar = nullptr;
78 	cvarValueBackup[0] = '\0';
79 
80 	/* fire change event */
81 	if (node->onChange) {
82 		UI_ExecuteEventActions(node, node->onChange);
83 	}
84 }
85 
86 /**
87  * @brief callback from the keyboard
88  */
UI_TextEntryNodeAbortEdition(uiNode_t * node)89 static void UI_TextEntryNodeAbortEdition (uiNode_t* node)
90 {
91 	assert(editedCvar);
92 
93 	/* set the old cvar value */
94 	Cvar_ForceSet(editedCvar->name, cvarValueBackup);
95 
96 	/* invalidate cache */
97 	editedCvar = nullptr;
98 	cvarValueBackup[0] = '\0';
99 
100 	/* fire abort event */
101 	if (EXTRADATA(node).onAbort) {
102 		UI_ExecuteEventActions(node, EXTRADATA(node).onAbort);
103 	}
104 }
105 
106 /**
107  * @brief force edition of a textentry node
108  * @note the textentry must be on the active window
109  */
UI_TextEntryNodeFocus(uiNode_t * node,const uiCallContext_t * context)110 static void UI_TextEntryNodeFocus (uiNode_t* node, const uiCallContext_t* context)
111 {
112 	/* remove the focus to show changes */
113 	if (!UI_HasFocus(node)) {
114 		UI_RequestFocus(node);
115 	}
116 }
117 
118 /**
119  * @brief Called when the user click with the right mouse button
120  */
onLeftClick(uiNode_t * node,int x,int y)121 void uiTextEntryNode::onLeftClick (uiNode_t* node, int x, int y)
122 {
123 	if (node->disabled)
124 		return;
125 
126 	/* no cvar */
127 	if (!node->text)
128 		return;
129 	if (!Q_strstart(node->text, "*cvar:"))
130 		return;
131 
132 	if (!UI_HasFocus(node)) {
133 		if (node->onClick) {
134 			UI_ExecuteEventActions(node, node->onClick);
135 		}
136 		UI_RequestFocus(node);
137 	}
138 }
139 
140 /**
141  * @brief Called when the node got the focus
142  */
onFocusGained(uiNode_t * node)143 void uiTextEntryNode::onFocusGained (uiNode_t* node)
144 {
145 	assert(editedCvar == nullptr);
146 	/* skip '*cvar ' */
147 	const char* cvarRef = "*cvar:";
148 	editedCvar = Cvar_Get(&((const char*)node->text)[strlen(cvarRef)]);
149 	assert(editedCvar);
150 	Q_strncpyz(cvarValueBackup, editedCvar->string, sizeof(cvarValueBackup));
151 	isAborted = false;
152 	EXTRADATA(node).cursorPosition = UTF8_strlen(editedCvar->string);
153 
154 #if SDL_VERSION_ATLEAST(2,0,0)
155 	SDL_StartTextInput();
156 	vec2_t pos;
157 	UI_GetNodeAbsPos(node, pos);
158 	SDL_Rect r = {static_cast<int>(pos[0]), static_cast<int>(pos[1]), static_cast<int>(node->box.size[0]), static_cast<int>(node->box.size[1])};
159 	SDL_SetTextInputRect(&r);
160 #else
161 #ifdef ANDROID
162 	char buf[MAX_CVAR_EDITING_LENGTH];
163 	Q_strncpyz(buf, editedCvar->string, sizeof(buf));
164 	SDL_ANDROID_GetScreenKeyboardTextInput(buf, sizeof(buf));
165 	Cvar_ForceSet(editedCvar->name, buf);
166 	UI_TextEntryNodeValidateEdition(node);
167 	UI_RemoveFocus();
168 #endif
169 #endif
170 }
171 
172 /**
173  * @brief Called when the node lost the focus
174  */
onFocusLost(uiNode_t * node)175 void uiTextEntryNode::onFocusLost (uiNode_t* node)
176 {
177 	/* already aborted/changed with the keyboard */
178 	if (editedCvar == nullptr)
179 		return;
180 
181 	/* release the keyboard */
182 	if (isAborted || EXTRADATA(node).clickOutAbort) {
183 		UI_TextEntryNodeAbortEdition(node);
184 	} else {
185 		UI_TextEntryNodeValidateEdition(node);
186 	}
187 #if SDL_VERSION_ATLEAST(2,0,0)
188 	SDL_StopTextInput();
189 #endif
190 }
191 
192 /**
193  * @brief edit the current cvar with a char
194  */
UI_TextEntryNodeEdit(uiNode_t * node,unsigned int unicode)195 static void UI_TextEntryNodeEdit (uiNode_t* node, unsigned int unicode)
196 {
197 	char buffer[MAX_CVAR_EDITING_LENGTH];
198 
199 	/* copy the cvar */
200 	Q_strncpyz(buffer, editedCvar->string, sizeof(buffer));
201 
202 	/* compute result */
203 	if (unicode == K_BACKSPACE) {
204 		if (EXTRADATA(node).cursorPosition > 0){
205 			UTF8_delete_char_at(buffer, EXTRADATA(node).cursorPosition - 1);
206 			EXTRADATA(node).cursorPosition--;
207 		}
208 	} else if (unicode == K_DEL) {
209 		if (EXTRADATA(node).cursorPosition < UTF8_strlen(editedCvar->string)){
210 			UTF8_delete_char_at(buffer, EXTRADATA(node).cursorPosition);
211 		}
212 	} else {
213 		int length = strlen(buffer);
214 		int charLength = UTF8_encoded_len(unicode);
215 
216 		/* is buffer full? */
217 		if (length + charLength >= sizeof(buffer))
218 			return;
219 
220 		int insertedLength = UTF8_insert_char_at(buffer, sizeof(buffer), EXTRADATA(node).cursorPosition, unicode);
221 		if (insertedLength > 0)
222 			EXTRADATA(node).cursorPosition++;
223 	}
224 
225 	/* update the cvar */
226 	Cvar_ForceSet(editedCvar->name, buffer);
227 }
228 
229 /**
230  * @brief Called when we press a key when the node got the focus
231  * @return True, if we use the event
232  */
onKeyPressed(uiNode_t * node,unsigned int key,unsigned short unicode)233 bool uiTextEntryNode::onKeyPressed (uiNode_t* node, unsigned int key, unsigned short unicode)
234 {
235 	switch (key) {
236 	/* remove the last char. */
237 	case K_BACKSPACE:
238 		UI_TextEntryNodeEdit(node, K_BACKSPACE);
239 		return true;
240 	/* cancel the edition */
241 	case K_ESCAPE:
242 		isAborted = true;
243 		UI_RemoveFocus();
244 		return true;
245 	/* validate the edition */
246 	case K_ENTER:
247 	case K_KP_ENTER:
248 		UI_TextEntryNodeValidateEdition(node);
249 		UI_RemoveFocus();
250 		return true;
251 	case K_LEFTARROW:
252 	case K_KP_LEFTARROW:
253 		if (EXTRADATA(node).cursorPosition > 0)
254 			EXTRADATA(node).cursorPosition--;
255 		return true;
256 	case K_RIGHTARROW:
257 	case K_KP_RIGHTARROW:
258 		if (EXTRADATA(node).cursorPosition < UTF8_strlen(editedCvar->string))
259 			EXTRADATA(node).cursorPosition++;
260 		return true;
261 	case K_HOME:
262 	case K_KP_HOME:
263 		EXTRADATA(node).cursorPosition = 0;
264 		return true;
265 	case K_END:
266 	case K_KP_END:
267 		EXTRADATA(node).cursorPosition = UTF8_strlen(editedCvar->string);
268 		return true;
269 	case K_DEL:
270 	case K_KP_DEL:
271 		UI_TextEntryNodeEdit(node, K_DEL);
272 		return true;
273 	}
274 
275 	/* non printable */
276 	if (unicode < 32 || (unicode >= 127 && unicode < 192))
277 		return false;
278 
279 	/* add a char. */
280 	UI_TextEntryNodeEdit(node, unicode);
281 	return true;
282 }
283 
draw(uiNode_t * node)284 void uiTextEntryNode::draw (uiNode_t* node)
285 {
286 	const float* textColor;
287 	vec2_t pos;
288 	static vec4_t disabledColor = {0.5, 0.5, 0.5, 1.0};
289 	const char* font = UI_GetFontFromNode(node);
290 	uiSpriteStatus_t iconStatus = SPRITE_STATUS_NORMAL;
291 
292 	if (node->disabled) {
293 		/** @todo need custom color when node is disabled */
294 		textColor = disabledColor;
295 		iconStatus = SPRITE_STATUS_DISABLED;
296 	} else if (node->state) {
297 		textColor = node->color;
298 		iconStatus = SPRITE_STATUS_HOVER;
299 	} else {
300 		textColor = node->color;
301 	}
302 	if (UI_HasFocus(node)) {
303 		textColor = node->selectedColor;
304 	}
305 
306 	UI_GetNodeAbsPos(node, pos);
307 
308 	if (EXTRADATA(node).background) {
309 		UI_DrawSpriteInBox(false, EXTRADATA(node).background, iconStatus, pos[0], pos[1], node->box.size[0], node->box.size[1]);
310 	}
311 
312 	if (char const* const text = UI_GetReferenceString(node, node->text)) {
313 		char  buf[MAX_VAR];
314 		if (EXTRADATA(node).isPassword) {
315 			size_t size = UTF8_strlen(text);
316 
317 			if (size > MAX_VAR - 2)
318 				size = MAX_VAR - 2;
319 
320 			memset(buf, HIDECHAR, size);
321 			buf[size] = '\0';
322 		} else {
323 			/* leave one byte empty for the text-based cursor */
324 			UTF8_strncpyz(buf, text, sizeof(buf) - 1);
325 		}
326 
327 		/** @todo Make the cursor into a real graphical object instead of using a text character. */
328 		if (UI_HasFocus(node)) {
329 			if (CL_Milliseconds() % 1000 < 500) {
330 				UTF8_insert_char_at(buf, sizeof(buf), EXTRADATA(node).cursorPosition, (int)CURSOR_ON);
331 			} else {
332 				UTF8_insert_char_at(buf, sizeof(buf), EXTRADATA(node).cursorPosition, (int)CURSOR_OFF);
333 			}
334 		}
335 
336 		if (*buf != '\0') {
337 			R_Color(textColor);
338 			UI_DrawStringInBox(font, (align_t)node->contentAlign,
339 				pos[0] + node->padding, pos[1] + node->padding,
340 				node->box.size[0] - node->padding - node->padding, node->box.size[1] - node->padding - node->padding,
341 				buf);
342 			R_Color(nullptr);
343 		}
344 	}
345 }
346 
347 /**
348  * @brief Call before the script initialization of the node
349  */
onLoading(uiNode_t * node)350 void uiTextEntryNode::onLoading (uiNode_t* node)
351 {
352 	node->padding = 8;
353 	node->contentAlign = ALIGN_CL;
354 	Vector4Set(node->color, 1, 1, 1, 1);
355 	Vector4Set(node->selectedColor, 1, 1, 1, 1);
356 }
357 
UI_RegisterTextEntryNode(uiBehaviour_t * behaviour)358 void UI_RegisterTextEntryNode (uiBehaviour_t* behaviour)
359 {
360 	behaviour->name = "textentry";
361 	behaviour->manager = UINodePtr(new uiTextEntryNode());
362 	behaviour->extraDataSize = sizeof(EXTRADATA_TYPE);
363 
364 	/* Call back event called when we click on the node. If the click select the node,
365 	 * it called before we start the cvar edition.
366 	 */
367 	UI_RegisterOveridedNodeProperty(behaviour, "onClick");
368 
369 	/* Call back event (like click...) fired when the text is changed, after
370 	 * validation. An abort of the edition dont fire this event.
371 	 */
372 	UI_RegisterOveridedNodeProperty(behaviour, "onChange");
373 
374 	/* Custom the draw behaviour by hiding each character of the text with a star (''*''). */
375 	UI_RegisterExtradataNodeProperty(behaviour, "isPassword", V_BOOL, textEntryExtraData_t, isPassword);
376 	/* ustom the mouse event behaviour. When we are editing the text, if we click out of the node, the edition is aborted. Changes on
377 	 * the text are canceled, and no change event are fired.
378 	 */
379 	UI_RegisterExtradataNodeProperty(behaviour, "clickOutAbort", V_BOOL, textEntryExtraData_t, clickOutAbort);
380 	/* Cursor position (offset of next UTF-8 char to the right) */
381 	UI_RegisterExtradataNodeProperty(behaviour, "cursorPosition", V_INT, textEntryExtraData_t, cursorPosition);
382 	/* Call it when we abort the edition */
383 	UI_RegisterExtradataNodeProperty(behaviour, "onAbort", V_UI_ACTION, textEntryExtraData_t, onAbort);
384 	/* Call it to force node edition */
385 	UI_RegisterNodeMethod(behaviour, "edit", UI_TextEntryNodeFocus);
386 	/* Sprite used to display the background */
387 	UI_RegisterExtradataNodeProperty(behaviour, "background", V_UI_SPRITEREF, EXTRADATA_TYPE, background);
388 }
389