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_render.h"
32 #include "../ui_sprite.h"
33 #include "ui_node_text.h"
34 #include "ui_node_messagelist.h"
35 #include "ui_node_abstractnode.h"
36 
37 #include "../../client.h"
38 #include "../../../shared/parse.h"
39 
40 #define EXTRADATA(node) UI_EXTRADATA(node, abstractScrollableExtraData_t)
41 #define EXTRADATACONST(node) UI_EXTRADATACONST(node, abstractScrollableExtraData_t)
42 
43 /** @todo use the font height? */
44 static const int LINEHEIGHT	= 20;
45 
46 static const int DATETIME_COLUUI_SIZE = 180;
47 
48 /* Used for drag&drop-like scrolling */
49 static int mouseScrollX;
50 static int mouseScrollY;
51 
52 /* Russian timestamp (with UTF-8) is 23 bytes long */
53 #define TIMESTAMP_TEXT 24
54 typedef struct uiMessageListNodeMessage_s {
55 	char title[MAX_VAR];
56 	char timestamp[TIMESTAMP_TEXT];
57 	char* text;
58 	date_t date;
59 	const char* iconName;
60 	int lineUsed;		/**< used by the node to cache the number of lines need (often =1) */
61 	struct uiMessageListNodeMessage_s* next;
62 } uiMessageListNodeMessage_t;
63 
64 /** @todo implement this on a per-node basis */
65 static uiMessageListNodeMessage_t* messageStack;
66 
UI_MessageGetStack(void)67 struct uiMessageListNodeMessage_s* UI_MessageGetStack (void)
68 {
69 	return messageStack;
70 }
71 
UI_MessageResetStack(void)72 void UI_MessageResetStack (void)
73 {
74 	messageStack = nullptr;
75 }
76 
UI_MessageAddStack(struct uiMessageListNodeMessage_s * message)77 void UI_MessageAddStack (struct uiMessageListNodeMessage_s* message)
78 {
79 	message->next = messageStack;
80 	messageStack = message;
81 }
82 
83 /**
84  * @return Number of lines need to display this message
85  */
UI_MessageGetLines(const uiNode_t * node,uiMessageListNodeMessage_t * message,const char * fontID,int width)86 static int UI_MessageGetLines (const uiNode_t* node, uiMessageListNodeMessage_t* message, const char* fontID, int width)
87 {
88 	const int column1 = DATETIME_COLUUI_SIZE;
89 	const int column2 = width - DATETIME_COLUUI_SIZE - node->padding;
90 	int lines1;
91 	int lines2;
92 	R_FontTextSize(fontID, message->timestamp, column1, LONGLINES_WRAP, nullptr, nullptr, &lines1, nullptr);
93 	R_FontTextSize(fontID, message->text, column2, LONGLINES_WRAP, nullptr, nullptr, &lines2, nullptr);
94 	return std::max(lines1, lines2);
95 }
96 
97 static char* lastDate;
98 
99 /**
100  * @todo do not hard code icons
101  * @todo cache icon result
102  */
UI_MessageGetIcon(const uiMessageListNodeMessage_t * message)103 static uiSprite_t* UI_MessageGetIcon (const uiMessageListNodeMessage_t* message)
104 {
105 	const char* iconName = message->iconName;
106 	if (Q_strnull(iconName))
107 		iconName = "icons/message_info";
108 
109 	return UI_GetSpriteByName(message->iconName);
110 }
111 
UI_MessageDraw(const uiNode_t * node,uiMessageListNodeMessage_t * message,const char * fontID,int x,int y,int width,int * screenLines)112 static void UI_MessageDraw (const uiNode_t* node, uiMessageListNodeMessage_t* message, const char* fontID, int x, int y, int width, int* screenLines)
113 {
114 	const int column1 = DATETIME_COLUUI_SIZE;
115 	const int column2 = width - DATETIME_COLUUI_SIZE - node->padding;
116 	int lines1 = *screenLines;
117 	int lines2 = *screenLines;
118 
119 	/* also display the first date on wraped message we only see the end */
120 	if (lines1 < 0)
121 		lines1 = 0;
122 
123 	/* display the date */
124 	if (lastDate == nullptr || !Q_streq(lastDate, message->timestamp)) {
125 		R_Color(node->color);
126 		UI_DrawString(fontID, ALIGN_UL, x, y, x, column1, LINEHEIGHT, message->timestamp, EXTRADATACONST(node).scrollY.viewSize, 0, &lines1, true, LONGLINES_WRAP);
127 		R_Color(nullptr);
128 	}
129 
130 	x += DATETIME_COLUUI_SIZE + node->padding;
131 
132 	/* identify the begin of a message with a mark */
133 	if (lines2 >= 0) {
134 		const uiSprite_t* icon = UI_MessageGetIcon(message);
135 		R_Color(nullptr);
136 		UI_DrawSpriteInBox(false, icon, SPRITE_STATUS_NORMAL, x - 25, y + LINEHEIGHT * lines2 - 1, 19, 19);
137 	}
138 
139 	/* draw the message */
140 	R_Color(node->color);
141 	UI_DrawString(fontID, ALIGN_UL, x, y, x, column2, LINEHEIGHT, message->text, EXTRADATACONST(node).scrollY.viewSize, 0, &lines2, true, LONGLINES_WRAP);
142 	R_Color(nullptr);
143 	*screenLines = std::max(lines1, lines2);
144 	lastDate = message->timestamp;
145 }
146 
147 /**
148  * @brief Draws the messagesystem node
149  * @param[in] node The context node
150  */
draw(uiNode_t * node)151 void uiMessageListNode::draw (uiNode_t* node)
152 {
153 	uiMessageListNodeMessage_t* message;
154 	int screenLines;
155 	const char* font = UI_GetFontFromNode(node);
156 	vec2_t pos;
157 	int x, y, width;
158 	int defaultHeight;
159 	int lineNumber = 0;
160 	int posY;
161 
162 /* #define AUTOSCROLL */		/**< if newer messages are on top, autoscroll is not need */
163 #ifdef AUTOSCROLL
164 	bool autoscroll;
165 #endif
166 	UI_GetNodeAbsPos(node, pos);
167 
168 	defaultHeight = LINEHEIGHT;
169 
170 #ifdef AUTOSCROLL
171 	autoscroll = (EXTRADATA(node).scrollY.viewPos + EXTRADATA(node).scrollY.viewSize == EXTRADATA(node).scrollY.fullSize)
172 		|| (EXTRADATA(node).scrollY.fullSize < EXTRADATA(node).scrollY.viewSize);
173 #endif
174 
175 	/* text box */
176 	x = pos[0] + node->padding;
177 	y = pos[1] + node->padding;
178 	width = node->box.size[0] - node->padding - node->padding;
179 
180 	/* update message cache */
181 	if (isSizeChange(node)) {
182 		/* recompute all line size */
183 		message = messageStack;
184 		while (message) {
185 			message->lineUsed = UI_MessageGetLines(node, message, font, width);
186 			lineNumber += message->lineUsed;
187 			message = message->next;
188 		}
189 	} else {
190 		/* only check unvalidated messages */
191 		message = messageStack;
192 		while (message) {
193 			if (message->lineUsed == 0)
194 				message->lineUsed = UI_MessageGetLines(node, message, font, width);
195 			lineNumber += message->lineUsed;
196 			message = message->next;
197 		}
198 	}
199 
200 	/* update scroll status */
201 #ifdef AUTOSCROLL
202 	if (autoscroll)
203 		setScrollY(node, lineNumber, node->box.size[1] / defaultHeight, lineNumber);
204 	else
205 		setScrollY(node, -1, node->box.size[1] / defaultHeight, lineNumber);
206 #else
207 	setScrollY(node, -1, node->box.size[1] / defaultHeight, lineNumber);
208 #endif
209 
210 	/* found the first message we must display */
211 	message = messageStack;
212 	posY = EXTRADATA(node).scrollY.viewPos;
213 	while (message && posY > 0) {
214 		posY -= message->lineUsed;
215 		if (posY < 0)
216 			break;
217 		message = message->next;
218 	}
219 
220 	/* draw */
221 	/** @note posY can be negative (if we must display last line of the first message) */
222 	lastDate = nullptr;
223 	screenLines = posY;
224 	while (message) {
225 		UI_MessageDraw(node, message, font, x, y, width, &screenLines);
226 		if (screenLines >= EXTRADATA(node).scrollY.viewSize)
227 			break;
228 		message = message->next;
229 	}
230 }
231 
onScroll(uiNode_t * node,int deltaX,int deltaY)232 bool uiMessageListNode::onScroll (uiNode_t* node, int deltaX, int deltaY)
233 {
234 	bool down = deltaY > 0;
235 	bool updated;
236 	if (deltaY == 0)
237 		return false;
238 	updated = scrollY(node, (down ? 1 : -1));
239 	/* @todo use super behaviour */
240 	if (node->onWheelUp && !down) {
241 		UI_ExecuteEventActions(node, node->onWheelUp);
242 		updated = true;
243 	}
244 	if (node->onWheelDown && down) {
245 		UI_ExecuteEventActions(node, node->onWheelDown);
246 		updated = true;
247 	}
248 	if (node->onWheel) {
249 		UI_ExecuteEventActions(node, node->onWheel);
250 		updated = true;
251 	}
252 	return updated;
253 }
254 
onLoading(uiNode_t * node)255 void uiMessageListNode::onLoading (uiNode_t* node)
256 {
257 	Vector4Set(node->color, 1.0, 1.0, 1.0, 1.0);
258 }
259 
260 /**
261  * @brief Track mouse down/up events to implement drag&drop-like scrolling, for touchscreen devices
262  * @sa UI_TextNodeMouseUp, UI_TextNodeCapturedMouseMove
263 */
onMouseDown(uiNode_t * node,int x,int y,int button)264 void uiMessageListNode::onMouseDown (uiNode_t* node, int x, int y, int button)
265 {
266 	if (!UI_GetMouseCapture() && button == K_MOUSE1 &&
267 		EXTRADATA(node).scrollY.fullSize > EXTRADATA(node).scrollY.viewSize) {
268 		UI_SetMouseCapture(node);
269 		mouseScrollX = x;
270 		mouseScrollY = y;
271 	}
272 }
273 
onMouseUp(uiNode_t * node,int x,int y,int button)274 void uiMessageListNode::onMouseUp (uiNode_t* node, int x, int y, int button)
275 {
276 	if (UI_GetMouseCapture() == node)  /* More checks can never hurt */
277 		UI_MouseRelease();
278 }
279 
onCapturedMouseMove(uiNode_t * node,int x,int y)280 void uiMessageListNode::onCapturedMouseMove (uiNode_t* node, int x, int y)
281 {
282 	const int lineHeight = getCellHeight(node);
283 	const int deltaY = (mouseScrollY - y) / lineHeight;
284 	/* We're doing only vertical scroll, that's enough for the most instances */
285 	if (abs(mouseScrollY - y) >= lineHeight) {
286 		scrollY(node, deltaY);
287 		/* @todo not accurate */
288 		mouseScrollX = x;
289 		mouseScrollY = y;
290 	}
291 	onMouseMove(node, x, y);
292 }
293 
294 /**
295  * @brief Return size of the cell, which is the size (in virtual "pixel") which represent 1 in the scroll values.
296  * Here we guess the widget can scroll pixel per pixel.
297  * @return Size in pixel.
298  */
getCellHeight(uiNode_t * node)299 int uiMessageListNode::getCellHeight (uiNode_t* node)
300 {
301 	return LINEHEIGHT;
302 }
303 
UI_RegisterMessageListNode(uiBehaviour_t * behaviour)304 void UI_RegisterMessageListNode (uiBehaviour_t* behaviour)
305 {
306 	behaviour->name = "messagelist";
307 	behaviour->extends = "abstractscrollable";
308 	behaviour->manager = UINodePtr(new uiMessageListNode());
309 }
310