1 /**
2 * @file
3 * @todo add getter/setter to cleanup access to extradata from cl_*.c files (check "u.text.")
4 */
5
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17
18 See the GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23
24 */
25
26 #include "../ui_main.h"
27 #include "../ui_internal.h"
28 #include "../ui_font.h"
29 #include "../ui_actions.h"
30 #include "../ui_parse.h"
31 #include "../ui_behaviour.h"
32 #include "../ui_render.h"
33 #include "ui_node_text.h"
34 #include "ui_node_abstractnode.h"
35
36 #include "../../client.h"
37 #include "../../cl_language.h"
38 #include "../../../shared/parse.h"
39
40 #define EXTRADATA_TYPE textExtraData_t
41 #define EXTRADATA(node) UI_EXTRADATA(node, EXTRADATA_TYPE)
42 #define EXTRADATACONST(node) UI_EXTRADATACONST(node, EXTRADATA_TYPE)
43
44 /* Used for drag&drop-like scrolling */
45 static int mouseScrollX;
46 static int mouseScrollY;
47
validateCache(uiNode_t * node)48 void uiTextNode::validateCache (uiNode_t* node)
49 {
50 int v;
51 if (EXTRADATA(node).dataID == TEXT_NULL || node->text != nullptr)
52 return;
53
54 v = UI_GetDataVersion(EXTRADATA(node).dataID);
55 if (v != EXTRADATA(node).versionId) {
56 updateCache(node);
57 }
58 }
59
UI_TextNodeGetSelectedText(uiNode_t * node,int num)60 const char* UI_TextNodeGetSelectedText (uiNode_t* node, int num)
61 {
62 const char* text = UI_GetTextFromList(EXTRADATA(node).dataID, num);
63 if (text == nullptr)
64 return "";
65 return text;
66 }
67
68 /**
69 * @brief Change the selected line
70 */
UI_TextNodeSelectLine(uiNode_t * node,int num)71 void UI_TextNodeSelectLine (uiNode_t* node, int num)
72 {
73 if (EXTRADATA(node).textLineSelected == num)
74 return;
75 EXTRADATA(node).textLineSelected = num;
76 EXTRADATA(node).textSelected = UI_TextNodeGetSelectedText(node, num);
77 if (node->onChange)
78 UI_ExecuteEventActions(node, node->onChange);
79 }
80
81 /**
82 * @brief Scroll to the bottom
83 * @param[in] nodePath absolute path
84 */
UI_TextScrollEnd(const char * nodePath)85 void UI_TextScrollEnd (const char* nodePath)
86 {
87 uiNode_t* node = UI_GetNodeByPath(nodePath);
88 if (!node) {
89 Com_DPrintf(DEBUG_CLIENT, "Node '%s' could not be found\n", nodePath);
90 return;
91 }
92
93 if (!UI_NodeInstanceOf(node, "text")) {
94 Com_Printf("UI_TextScrollBottom: '%s' node is not an 'text'.\n", Cmd_Argv(1));
95 return;
96 }
97
98 uiTextNode *b = dynamic_cast<uiTextNode*>(node->behaviour->manager.get());
99 b->validateCache(node);
100
101 if (EXTRADATA(node).super.scrollY.fullSize > EXTRADATA(node).super.scrollY.viewSize) {
102 EXTRADATA(node).super.scrollY.viewPos = EXTRADATA(node).super.scrollY.fullSize - EXTRADATA(node).super.scrollY.viewSize;
103 UI_ExecuteEventActions(node, EXTRADATA(node).super.onViewChange);
104 }
105 }
106
107 /**
108 * @brief Get the line number under an absolute position
109 * @param[in] node a text node
110 * @param[in] x position x on the screen
111 * @param[in] y position y on the screen
112 * @return The line number under the position (0 = first line)
113 */
UI_TextNodeGetLine(const uiNode_t * node,int x,int y)114 static int UI_TextNodeGetLine (const uiNode_t* node, int x, int y)
115 {
116 int lineHeight;
117 int line;
118 assert(UI_NodeInstanceOf(node, "text"));
119
120 lineHeight = EXTRADATACONST(node).lineHeight;
121 if (lineHeight == 0) {
122 const char* font = UI_GetFontFromNode(node);
123 lineHeight = UI_FontGetHeight(font);
124 }
125
126 UI_NodeAbsoluteToRelativePos(node, &x, &y);
127 y -= node->padding;
128
129 /* skip position over the first line */
130 if (y < 0)
131 return -1;
132 line = (int) (y / lineHeight) + EXTRADATACONST(node).super.scrollY.viewPos;
133
134 /* skip position under the last line */
135 if (line >= EXTRADATACONST(node).super.scrollY.fullSize)
136 return -1;
137
138 return line;
139 }
140
onMouseMove(uiNode_t * node,int x,int y)141 void uiTextNode::onMouseMove (uiNode_t* node, int x, int y)
142 {
143 EXTRADATA(node).lineUnderMouse = UI_TextNodeGetLine(node, x, y);
144 }
145
146 #define UI_TEXTNODE_BUFFERSIZE 32768
147
148 /**
149 * @brief Handles line breaks and drawing for shared data id
150 * @param[in] node The context node
151 * @param[in] text The test to draw else nullptr
152 * @param[in] list The test to draw else nullptr
153 * @param[in] noDraw If true, calling of this function only update the cache (real number of lines)
154 * @note text or list but be used, not both
155 */
drawText(uiNode_t * node,const char * text,const linkedList_t * list,bool noDraw)156 void uiTextNode::drawText (uiNode_t* node, const char* text, const linkedList_t* list, bool noDraw)
157 {
158 static char textCopy[UI_TEXTNODE_BUFFERSIZE];
159 char newFont[MAX_VAR];
160 const char* oldFont = nullptr;
161 vec4_t colorHover;
162 vec4_t colorSelectedHover;
163 char* cur, *tab, *end;
164 int fullSizeY;
165 const char* font = UI_GetFontFromNode(node);
166 vec2_t pos;
167 int x, y, width;
168 int viewSizeY;
169
170 UI_GetNodeAbsPos(node, pos);
171
172 if (isSizeChange(node)) {
173 int lineHeight = EXTRADATA(node).lineHeight;
174 if (lineHeight == 0) {
175 const char* font = UI_GetFontFromNode(node);
176 lineHeight = UI_FontGetHeight(font);
177 }
178 viewSizeY = node->box.size[1] / lineHeight;
179 } else {
180 viewSizeY = EXTRADATA(node).super.scrollY.viewSize;
181 }
182
183 /* text box */
184 x = pos[0] + node->padding;
185 y = pos[1] + node->padding;
186 width = node->box.size[0] - node->padding - node->padding;
187
188 if (text) {
189 Q_strncpyz(textCopy, text, sizeof(textCopy));
190 } else if (list) {
191 Q_strncpyz(textCopy, CL_Translate((const char*)list->data), sizeof(textCopy));
192 } else
193 return; /**< Nothing to draw */
194
195 cur = textCopy;
196
197 /* Hover darkening effect for normal text lines. */
198 VectorScale(node->color, 0.8, colorHover);
199 colorHover[3] = node->color[3];
200
201 /* Hover darkening effect for selected text lines. */
202 VectorScale(node->selectedColor, 0.8, colorSelectedHover);
203 colorSelectedHover[3] = node->selectedColor[3];
204
205 /* fix position of the start of the draw according to the align */
206 switch (node->contentAlign % 3) {
207 case 0: /* left */
208 break;
209 case 1: /* middle */
210 x += width / 2;
211 break;
212 case 2: /* right */
213 x += width;
214 break;
215 }
216
217 R_Color(node->color);
218
219 fullSizeY = 0;
220 do {
221 bool haveTab;
222 int x1; /* variable x position */
223 /* new line starts from node x position */
224 x1 = x;
225 if (oldFont) {
226 font = oldFont;
227 oldFont = nullptr;
228 }
229
230 /* text styles and inline images */
231 if (cur[0] == '^') {
232 switch (toupper(cur[1])) {
233 case 'B':
234 Com_sprintf(newFont, sizeof(newFont), "%s_bold", font);
235 oldFont = font;
236 font = newFont;
237 cur += 2; /* don't print the format string */
238 break;
239 }
240 }
241
242 /* get the position of the next newline - otherwise end will be null */
243 end = strchr(cur, '\n');
244 if (end)
245 /* set the \n to \0 to draw only this part (before the \n) with our font renderer */
246 /* let end point to the next char after the \n (or \0 now) */
247 *end++ = '\0';
248
249 /* highlighting */
250 if (fullSizeY == EXTRADATA(node).textLineSelected && EXTRADATA(node).textLineSelected >= 0) {
251 /* Draw current line in "selected" color (if the linenumber is stored). */
252 R_Color(node->selectedColor);
253 } else {
254 R_Color(node->color);
255 }
256
257 if (node->state && EXTRADATA(node).mousefx && fullSizeY == EXTRADATA(node).lineUnderMouse) {
258 /* Highlight line if mousefx is true. */
259 /** @todo what about multiline text that should be highlighted completely? */
260 if (fullSizeY == EXTRADATA(node).textLineSelected && EXTRADATA(node).textLineSelected >= 0) {
261 R_Color(colorSelectedHover);
262 } else {
263 R_Color(colorHover);
264 }
265 }
266
267 /* tabulation, we assume all the tabs fit on a single line */
268 haveTab = strchr(cur, '\t') != nullptr;
269 if (haveTab) {
270 while (cur && *cur) {
271 int tabwidth;
272
273 tab = strchr(cur, '\t');
274
275 /* use tab stop as given via property definition
276 * or use 1/3 of the node size (width) */
277 if (!EXTRADATA(node).tabWidth)
278 tabwidth = width / 3;
279 else
280 tabwidth = EXTRADATA(node).tabWidth;
281
282 if (tab) {
283 int numtabs = strspn(tab, "\t");
284 tabwidth *= numtabs;
285 while (*tab == '\t')
286 *tab++ = '\0';
287 } else {
288 /* maximize width for the last element */
289 tabwidth = width - (x1 - x);
290 if (tabwidth < 0)
291 tabwidth = 0;
292 }
293
294 /* minimize width for element outside node */
295 if ((x1 - x) + tabwidth > width)
296 tabwidth = width - (x1 - x);
297
298 /* make sure it is positive */
299 if (tabwidth < 0)
300 tabwidth = 0;
301
302 if (tabwidth != 0)
303 UI_DrawString(font, (align_t)node->contentAlign, x1, y, x1, tabwidth - 1, EXTRADATA(node).lineHeight, cur, viewSizeY, EXTRADATA(node).super.scrollY.viewPos, &fullSizeY, false, LONGLINES_PRETTYCHOP);
304
305 /* next */
306 x1 += tabwidth;
307 cur = tab;
308 }
309 fullSizeY++;
310 }
311
312 /*Com_Printf("until newline - lines: %i\n", lines);*/
313 /* the conditional expression at the end is a hack to draw "/n/n" as a blank line */
314 /* prevent line from being drawn if there is nothing that should be drawn after it */
315 if (cur && (cur[0] || end || list)) {
316 /* is it a white line? */
317 if (!cur) {
318 fullSizeY++;
319 } else {
320 if (noDraw) {
321 int lines = 0;
322 R_FontTextSize(font, cur, width, (longlines_t)EXTRADATA(node).longlines, nullptr, nullptr, &lines, nullptr);
323 fullSizeY += lines;
324 } else
325 UI_DrawString(font, (align_t)node->contentAlign, x1, y, x, width, EXTRADATA(node).lineHeight, cur, viewSizeY, EXTRADATA(node).super.scrollY.viewPos, &fullSizeY, true, (longlines_t)EXTRADATA(node).longlines);
326 }
327 }
328
329 if (EXTRADATA(node).mousefx)
330 R_Color(node->color); /* restore original color */
331
332 /* now set cur to the next char after the \n (see above) */
333 cur = end;
334 if (!cur && list) {
335 list = list->next;
336 if (list) {
337 Q_strncpyz(textCopy, CL_Translate((const char*)list->data), sizeof(textCopy));
338 cur = textCopy;
339 }
340 }
341 } while (cur);
342
343 /* update scroll status */
344 setScrollY(node, -1, viewSizeY, fullSizeY);
345
346 R_Color(nullptr);
347 }
348
updateCache(uiNode_t * node)349 void uiTextNode::updateCache (uiNode_t* node)
350 {
351 const uiSharedData_t* shared;
352
353 if (EXTRADATA(node).dataID == TEXT_NULL && node->text != nullptr)
354 return;
355
356 shared = &ui_global.sharedData[EXTRADATA(node).dataID];
357
358 switch (shared->type) {
359 case UI_SHARED_TEXT:
360 {
361 const char* t = CL_Translate(shared->data.text);
362 drawText(node, t, nullptr, true);
363 }
364 break;
365 case UI_SHARED_LINKEDLISTTEXT:
366 drawText(node, nullptr, shared->data.linkedListText, true);
367 break;
368 default:
369 break;
370 }
371
372 EXTRADATA(node).versionId = shared->versionId;
373 }
374
375 /**
376 * @brief Draw a text node
377 */
draw(uiNode_t * node)378 void uiTextNode::draw (uiNode_t* node)
379 {
380 const uiSharedData_t* shared;
381
382 if (EXTRADATA(node).dataID == TEXT_NULL && node->text != nullptr) {
383 const char* t = CL_Translate(UI_GetReferenceString(node, node->text));
384 drawText(node, t, nullptr, false);
385 return;
386 }
387
388 shared = &ui_global.sharedData[EXTRADATA(node).dataID];
389
390 switch (shared->type) {
391 case UI_SHARED_TEXT:
392 {
393 const char* t = CL_Translate(shared->data.text);
394 drawText(node, t, nullptr, false);
395 break;
396 }
397 case UI_SHARED_LINKEDLISTTEXT:
398 drawText(node, nullptr, shared->data.linkedListText, false);
399 break;
400 default:
401 break;
402 }
403
404 EXTRADATA(node).versionId = shared->versionId;
405 }
406
407 /**
408 * @brief Calls the script command for a text node that is clickable
409 * @sa UI_TextNodeRightClick
410 */
onLeftClick(uiNode_t * node,int x,int y)411 void uiTextNode::onLeftClick (uiNode_t* node, int x, int y)
412 {
413 int line = UI_TextNodeGetLine(node, x, y);
414
415 if (line < 0 || line >= EXTRADATA(node).super.scrollY.fullSize)
416 return;
417
418 UI_TextNodeSelectLine(node, line);
419
420 if (node->onClick)
421 UI_ExecuteEventActions(node, node->onClick);
422 }
423
424 /**
425 * @brief Calls the script command for a text node that is clickable via right mouse button
426 * @sa UI_TextNodeClick
427 */
onRightClick(uiNode_t * node,int x,int y)428 void uiTextNode::onRightClick (uiNode_t* node, int x, int y)
429 {
430 int line = UI_TextNodeGetLine(node, x, y);
431
432 if (line < 0 || line >= EXTRADATA(node).super.scrollY.fullSize)
433 return;
434
435 UI_TextNodeSelectLine(node, line);
436
437 if (node->onRightClick)
438 UI_ExecuteEventActions(node, node->onRightClick);
439 }
440
441 /**
442 */
onScroll(uiNode_t * node,int deltaX,int deltaY)443 bool uiTextNode::onScroll (uiNode_t* node, int deltaX, int deltaY)
444 {
445 bool updated;
446 bool down = deltaY > 0;
447 if (deltaY == 0)
448 return false;
449 updated = scrollY(node, (down ? 1 : -1));
450
451 /* @todo use super behaviour */
452 if (node->onWheelUp && !down) {
453 UI_ExecuteEventActions(node, node->onWheelUp);
454 updated = true;
455 }
456 if (node->onWheelDown && down) {
457 UI_ExecuteEventActions(node, node->onWheelDown);
458 updated = true;
459 }
460 if (node->onWheel) {
461 UI_ExecuteEventActions(node, node->onWheel);
462 updated = true;
463 }
464 return updated;
465 }
466
onLoading(uiNode_t * node)467 void uiTextNode::onLoading (uiNode_t* node)
468 {
469 EXTRADATA(node).textLineSelected = -1; /**< Invalid/no line selected per default. */
470 EXTRADATA(node).textSelected = "";
471 Vector4Set(node->selectedColor, 1.0, 1.0, 1.0, 1.0);
472 Vector4Set(node->color, 1.0, 1.0, 1.0, 1.0);
473 }
474
onLoaded(uiNode_t * node)475 void uiTextNode::onLoaded (uiNode_t* node)
476 {
477 int lineheight = EXTRADATA(node).lineHeight;
478 /* auto compute lineheight */
479 /* we don't overwrite EXTRADATA(node).lineHeight, because "0" is dynamically replaced by font height on draw function */
480 if (lineheight == 0) {
481 /* the font is used */
482 const char* font = UI_GetFontFromNode(node);
483 lineheight = UI_FontGetHeight(font);
484 }
485
486 /* auto compute rows (super.viewSizeY) */
487 if (EXTRADATA(node).super.scrollY.viewSize == 0) {
488 if (node->box.size[1] != 0 && lineheight != 0) {
489 EXTRADATA(node).super.scrollY.viewSize = node->box.size[1] / lineheight;
490 } else {
491 EXTRADATA(node).super.scrollY.viewSize = 1;
492 Com_Printf("UI_TextNodeLoaded: node '%s' has no rows value\n", UI_GetPath(node));
493 }
494 }
495
496 /* auto compute height */
497 if (node->box.size[1] == 0) {
498 node->box.size[1] = EXTRADATA(node).super.scrollY.viewSize * lineheight;
499 }
500
501 /* is text slot exists */
502 if (EXTRADATA(node).dataID >= UI_MAX_DATAID)
503 Com_Error(ERR_DROP, "Error in node %s - max shared data id num exceeded (num: %i, max: %i)", UI_GetPath(node), EXTRADATA(node).dataID, UI_MAX_DATAID);
504
505 #ifdef DEBUG
506 if (EXTRADATA(node).super.scrollY.viewSize != (int)(node->box.size[1] / lineheight)) {
507 Com_Printf("UI_TextNodeLoaded: rows value (%i) of node '%s' differs from size (%.0f) and format (%i) values\n",
508 EXTRADATA(node).super.scrollY.viewSize, UI_GetPath(node), node->box.size[1], lineheight);
509 }
510 #endif
511
512 if (node->text == nullptr && EXTRADATA(node).dataID == TEXT_NULL)
513 Com_Printf("UI_TextNodeLoaded: 'textid' property of node '%s' is not set\n", UI_GetPath(node));
514 }
515
516 /**
517 * @brief Track mouse down/up events to implement drag&drop-like scrolling, for touchscreen devices
518 * @sa UI_TextNodeMouseUp, UI_TextNodeCapturedMouseMove
519 */
onMouseDown(uiNode_t * node,int x,int y,int button)520 void uiTextNode::onMouseDown (uiNode_t* node, int x, int y, int button)
521 {
522 if (button == K_MOUSE1 && !UI_GetMouseCapture() &&
523 EXTRADATA(node).super.scrollY.fullSize > EXTRADATA(node).super.scrollY.viewSize) {
524 UI_SetMouseCapture(node);
525 mouseScrollX = x;
526 mouseScrollY = y;
527 }
528 }
529
onMouseUp(uiNode_t * node,int x,int y,int button)530 void uiTextNode::onMouseUp (uiNode_t* node, int x, int y, int button)
531 {
532 if (UI_GetMouseCapture() == node) /* More checks can never hurt */
533 UI_MouseRelease();
534 }
535
onCapturedMouseMove(uiNode_t * node,int x,int y)536 void uiTextNode::onCapturedMouseMove (uiNode_t* node, int x, int y)
537 {
538 const int lineHeight = getCellHeight(node);
539 const int deltaY = (mouseScrollY - y) / lineHeight;
540 /* We're doing only vertical scroll, that's enough for the most instances */
541 if (abs(mouseScrollY - y) >= lineHeight) {
542 scrollY(node, deltaY);
543 /* @todo not accurate */
544 mouseScrollX = x;
545 mouseScrollY = y;
546 }
547 onMouseMove(node, x, y);
548 }
549
550 /**
551 * @brief Return size of the cell, which is the size (in virtual "pixel") which represent 1 in the scroll values.
552 * Here we guess the widget can scroll pixel per pixel.
553 * @return Size in pixel.
554 */
getCellHeight(uiNode_t * node)555 int uiTextNode::getCellHeight (uiNode_t* node)
556 {
557 int lineHeight = EXTRADATA(node).lineHeight;
558 if (lineHeight == 0)
559 lineHeight = UI_FontGetHeight(UI_GetFontFromNode(node));
560 return lineHeight;
561 }
562
UI_RegisterTextNode(uiBehaviour_t * behaviour)563 void UI_RegisterTextNode (uiBehaviour_t* behaviour)
564 {
565 behaviour->name = "text";
566 behaviour->extends = "abstractscrollable";
567 behaviour->manager = UINodePtr(new uiTextNode());
568 behaviour->extraDataSize = sizeof(EXTRADATA_TYPE);
569
570 /* Current selected line */
571 UI_RegisterExtradataNodeProperty(behaviour, "lineselected", V_INT, textExtraData_t, textLineSelected);
572
573 /* Text of the current selected line */
574 UI_RegisterExtradataNodeProperty(behaviour, "textselected", V_CVAR_OR_STRING, textExtraData_t, textSelected);
575
576 /* One of the list TEXT_STANDARD, TEXT_LIST, TEXT_UFOPEDIA, TEXT_BUILDINGS,
577 * TEXT_BUILDING_INFO, TEXT_RESEARCH, TEXT_RESEARCH_INFO, TEXT_POPUP,
578 * TEXT_POPUP_INFO, TEXT_AIRCRAFT_LIST, TEXT_AIRCRAFT, TEXT_AIRCRAFT_INFO,
579 * TEXT_MESSAGESYSTEM, TEXT_CAMPAIGN_LIST, TEXT_MULTISELECTION.
580 * There are more IDs in use - see ui_data.h for an up-to-date list.
581 * Display a shared content registered by the client code.
582 */
583 UI_RegisterExtradataNodeProperty(behaviour, "dataid", V_UI_DATAID, textExtraData_t, dataID);
584 /* Size between two lines. Default value is 0, in this case it use a line height according to the font size. */
585 UI_RegisterExtradataNodeProperty(behaviour, "lineheight", V_INT, textExtraData_t, lineHeight);
586 /* Bigger size of the width replacing a tab character. */
587 UI_RegisterExtradataNodeProperty(behaviour, "tabwidth", V_INT, textExtraData_t, tabWidth);
588 /* What to do with text lines longer than node width. Default is to wordwrap them to make multiple lines.
589 * It can be LONGLINES_WRAP, LONGLINES_CHOP, LONGLINES_PRETTYCHOP
590 */
591 UI_RegisterExtradataNodeProperty(behaviour, "longlines", V_INT, textExtraData_t, longlines);
592
593 /* Number of visible line we can display into the node height.
594 * Currently, it translate the scrollable property <code>viewSize</code>
595 * @todo For a smooth scroll we should split that
596 */
597 UI_RegisterExtradataNodeProperty(behaviour, "rows", V_INT, textExtraData_t, super.scrollY.viewSize);
598 /* Number of lines contained into the node.
599 * Currently, it translate the scrollable property <code>fullSize</code>
600 * @todo For a smooth scroll we should split that
601 */
602 UI_RegisterExtradataNodeProperty(behaviour, "lines", V_INT, textExtraData_t, super.scrollY.fullSize);
603
604 /** Highlight each node elements when the mouse move over the node.
605 * @todo delete it when its possible (need to create a textlist...)
606 */
607 UI_RegisterExtradataNodeProperty(behaviour, "mousefx", V_BOOL, textExtraData_t, mousefx);
608
609 Com_RegisterConstInt("LONGLINES_WRAP", LONGLINES_WRAP);
610 Com_RegisterConstInt("LONGLINES_CHOP", LONGLINES_CHOP);
611 Com_RegisterConstInt("LONGLINES_PRETTYCHOP", LONGLINES_PRETTYCHOP);
612 }
613