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