1 /*
2 Uses the Context Menu system to implement user configured command menus.
3 
4 Files with a .menu name extension found in the users config directory are
5 considered user menu files. Each .menu file defines a new menu displayed
6 in a standard EL window container. The window is always on top and can be
7 moved around by the optional title bar. A standard right-click context menu
8 controls window options. These options include a run-time reload function so you
9 can update the menus without restarting the client. Left clicking a particular
10 menu name opens the menu.  The window options and position are saved
11 between sessions in the cfg file.  The use of user menus is enabled/disabled
12 by an option in the config window.
13 
14 The .menu files are flat text files so they are easy to create and edit outside
15 the client. The first line in a .menu file is used as the menu name in the
16 container window. Each of the remaining lines are new lines for the menu. Lines
17 for the menus have an associated set of commands that are executed when
18 that line is selected. Valid commands are anything you can enter at the user
19 command prompt; #commands, text for chat channels & PM, %setting etc.  In
20 addition, commands can be URLs which will be directly opened in your browser.
21 
22 Each menu line in a .menu file has two or more fields, the field separator is
23 "||". The first field is the text for the menu line, the remaining field or
24 fields are the associated commands.
25 
26 Commands can prompt for user input. If the command contains text inside "<>",
27 e.g. "some text <more text> some other text" then the text inside the "<>" will
28 be used as a prompt and the "<...>" replaced by the text entered by the user.
29 You can include as many input prompts as you wish.
30 
31 
32 Author bluap/pjbroad May 2009
33 */
34 
35 // 	An example:
36 /*
37 Example
38 Show Server Stats||#stats
39 Show Invasion Monsters Count||#il
40 Show Day, Date & Time||#day||#date||#time
41 Show Day, Date & Time with 5 second delay||#user_menu_wait_time_ms 5000||#day||#date||#time||#user_menu_wait_time_ms
42 ||
43 Say local hi||hi
44 Stats On||%show_stats_in_hud=1
45 Stats Off||%show_stats_in_hud=0
46 Channel Hi||@hi
47 ||
48 Open Readonly Storage||#sto
49 Guild Info...||#guild_info <Guild Name>
50 ||
51 Player Info...||#open_url http://game.eternal-lands.com/view_user.php?user=<Player Name>
52 BBC News||#open_url http://news.bbc.co.uk/
53 */
54 
55 
56 /* To Do:
57 	- tidy up how the container window creates its context menu
58 	- restore context help text
59 	- split Container class, possibly into window/container classes
60 	- virtical/horizontal option
61 	- option auto hide down to icon
62 	- option hide/show from clicking an icon - handles at ends
63 	- make the menu prettier
64 	- use a subtle text fade once the mouse is no longer over
65 	- parameters should be separate (static ?) class or vars so do not create container usless needed
66 	- tear off windows - sounds a lot of work.....
67 
68 */
69 
70 
71 #include <iostream>
72 #include <fstream>
73 #include <string>
74 #include <vector>
75 #include <queue>
76 #ifdef WINDOWS
77 #include "io.h"
78 #else
79 #include <glob.h>
80 #endif
81 
82 #include <SDL.h>
83 #include <SDL_thread.h>
84 
85 #include "chat.h"
86 #include "command_queue.hpp"
87 #include "context_menu.h"
88 #include "elconfig.h"
89 #include "elwindows.h"
90 #include "errors.h"
91 #include "font.h"
92 #include "gamewin.h"
93 #include "gl_init.h"
94 #include "hud.h"
95 #include "hud_indicators.h"
96 #include "icon_window.h"
97 #include "init.h"
98 #include "io/elpathwrapper.h"
99 #ifdef JSON_FILES
100 #include "json_io.h"
101 #endif
102 #include "translate.h"
103 #include "user_menus.h"
104 
105 namespace UserMenus
106 {
107 	//
108 	//	A single user menu, constructed from a menu file.  Contains
109 	//	one or more Line objects.
110 	//
111 	class Menu
112 	{
113 		public:
114 			Menu(const std::string &file_name);
115 			~Menu(void);
get_name(void) const116 			const std::string & get_name(void) const { return menu_name; }
get_name_width(void) const117 			int get_name_width(void) const { return menu_name_width; }
recalc_name_width(float zoom)118 			int recalc_name_width(float zoom)
119 			{
120 				menu_name_width = eternal_lands::FontManager::get_instance()
121 					.line_width(UI_FONT, reinterpret_cast<const unsigned char*>(menu_name.c_str()),
122 						menu_name.length(), zoom);
123 				return menu_name_width;
124 			}
get_cm_id(void) const125 			size_t get_cm_id(void) const { return cm_menu_id; }
action(int option,CommandQueue::Queue & cq) const126 			void action(int option, CommandQueue::Queue &cq) const { lines[option]->action(cq); };
127 		private:
128 			size_t cm_menu_id;
129 			int menu_name_width;
130 			std::string menu_name;
131 			std::vector<CommandQueue::Line *> lines;
132 	};
133 
134 
135 	//
136 	//	A singleton class for the user menu window, contains the
137 	//	menu objects and implements the window callbacks.
138 	//
139 	class Container
140 	{
141 		public:
142 			void destroy(void);
143 			void open_window(void);
close_window(void)144 			void close_window(void) { command_queue.clear(); if (win_id >= 0) hide_window(win_id); }
145 			void set_options(int win_x, int win_y, int options);
146 			void get_options(int *win_x, int *win_y, int *options) const;
147 #ifdef JSON_FILES
148 			void read_options(const char *dict_name);
149 			void write_options(const char *dict_name) const;
150 #endif
151 			static Container * get_instance(void);
action_handler(window_info * win,int widget_id,int mx,int my,int option)152 			static int action_handler(window_info *win, int widget_id, int mx, int my, int option) { return get_instance()->action(widget_id, option); }
pre_show_handler(window_info * win,int widget_id,int mx,int my,window_info * cm_win)153 			static void pre_show_handler(window_info *win, int widget_id, int mx, int my, window_info *cm_win) { get_instance()->pre_show(win, widget_id, mx, my, cm_win); }
154 
155 		private:
156 			Container(void);
157 			int win_id;
158 			int win_width;
159 			size_t current_mouseover_menu;
160 			bool mouse_over_window;
161 			bool reload_menus;
162 			size_t context_id;
163 			bool window_used;
164 			int title_on;
165 			int background_on;
166 			int border_on;
167 			int use_small_font;
168 			int include_datadir;
169 			bool auto_include_datadir;
170 			int just_echo;
171 			int win_x_pos;
172 			int win_y_pos;
173 			std::vector<Menu *> menus;
174 			CommandQueue::Queue command_queue;
175 			int name_sep;
176 			int window_x_pad, window_y_pad;
177 			int standard_window_position;
178 
179 			void update_standard_window_position(window_info *win);
set_title_state(window_info * win,bool title_state)180 			void set_title_state(window_info *win, bool title_state) { title_on = (title_state) ?1:0; set_win_flag(&win->flags, ELW_TITLE_BAR, title_on); }
181 			void reload(window_info *win);
182 			void recalc_win_width(window_info *win);
183 			int display(window_info *win);
184 			int action(size_t active_menu, int option);
185 			void pre_show(window_info *win, int widget_id, int mx, int my, window_info *cm_win);
186 			int click(window_info *win, int mx, Uint32 flags);
187 			int ui_scale_changed(window_info *win);
188 			int font_changed(window_info *win, eternal_lands::FontManager::Category cat);
189 			size_t get_mouse_over_menu(window_info *win, int mx);
190 			void delete_menus(void);
191 			int context(window_info *win, int widget_id, int mx, int my, int option);
192 			void set_win_flag(Uint32 *flags, Uint32 flag, int state);
mouseover(window_info * win,int mx)193 			void mouseover(window_info *win, int mx) { mouse_over_window = true; current_mouseover_menu = get_mouse_over_menu(win, mx); }
get_height(window_info * win) const194 			int get_height(window_info *win) const { return ((use_small_font) ?win->small_font_len_y :win->default_font_len_y) + 2 * window_y_pad; }
calc_actual_width(window_info * win,int width) const195 			int calc_actual_width(window_info *win, int width) const
196 				{ return (int)(0.5 + (use_small_font ? win->current_scale_small : win->current_scale) * width); }
197 
display_handler(window_info * win)198 			static int display_handler(window_info *win) { return get_instance()->display(win); }
mouseover_handler(window_info * win,int mx,int my)199 			static int mouseover_handler(window_info *win, int mx, int my) { get_instance()->mouseover(win, mx); return 0; }
click_handler(window_info * win,int mx,int my,Uint32 flags)200 			static int click_handler(window_info *win, int mx, int my, Uint32 flags) { return get_instance()->click(win, mx, flags); }
ui_scale_handler(window_info * win)201 			static int ui_scale_handler(window_info *win) { return get_instance()->ui_scale_changed(win); }
font_change_handler(window_info * win,font_cat cat)202 			static int font_change_handler(window_info *win, font_cat cat) { return get_instance()->font_changed(win, cat); }
context_handler(window_info * win,int widget_id,int mx,int my,int option)203 			static int context_handler(window_info *win, int widget_id, int mx, int my, int option){ return get_instance()->context(win, widget_id, mx, my, option); }
204 
205 			enum { STND_POS_NONE = 0, STND_POS_TOP_LEFT, STND_POS_TOP_CENTRE, STND_POS_TOP_RIGHT, STND_POS_BOTTOM_LEFT, STND_POS_BOTTOM_CENTRE, STND_POS_BOTTON_RIGHT, STND_POS_LAST };
206 			enum { CM_MOVEWIN=1, CM_FIXPOS, CM_CHANGEPOS, CM_BACKGND, CM_BORDER, CM_FONT, CM_STANDMENU, CM_SEP1, CM_SHOWCMD, CM_SEP2, CM_RELOAD, CM_DISABLE };
207 	};
208 
209 
210 	//
211 	//	Construct the Menu given a filepath and name.
212 	//
Menu(const std::string & file_name)213 	Menu::Menu(const std::string &file_name)
214 		: cm_menu_id(CM_INIT_VALUE), menu_name_width(0)
215 	{
216 		std::ifstream in(file_name.c_str());
217 		if (!in)
218 		{
219 			LOG_ERROR("%s: Failed to open [%s]\n", __FILE__, file_name.c_str() );
220 			return;
221 		}
222 
223 		// the first line is the menu name, a menu without a name is not a menu
224 		getline(in, menu_name);
225 		if (menu_name.empty())
226 		{
227 			LOG_ERROR("%s: Failed while reading [%s]\n", __FILE__, file_name.c_str() );
228 			in.close();
229 			return;
230 		}
231 		menu_name_width = get_string_width_zoom((const unsigned char*)menu_name.c_str(),
232 			UI_FONT, 1.0);
233 
234 		// read each line after the menu name line, creating a menu Line object for each
235 		std::string line;
236 		while (getline(in, line))
237 		{
238 			// lines starting with ## are ignored - like a comment
239 			if ((!line.empty()) && (line.substr(0,2) != "##"))
240 				lines.push_back(new CommandQueue::Line(line));
241 		}
242 		in.close();
243 
244 		// no lines, no menu
245 		if (lines.empty())
246 			return;
247 
248 		// create a context menu for the menu
249 		std::string menu_text;
250 		for (size_t i=0; i<lines.size(); i++)
251 			menu_text += lines[i]->get_text() + "\n";
252 		cm_menu_id = cm_create(menu_text.c_str(), Container::action_handler);
253 		cm_set_pre_show_handler(cm_menu_id, Container::pre_show_handler);
254 
255 	} // end Menu()
256 
257 
258 	//
259 	// destruct a Menu, insuring the lines are deleted
260 	//
~Menu(void)261 	Menu::~Menu(void)
262 	{
263 		if (cm_valid(cm_menu_id))
264 			cm_destroy(cm_menu_id);
265 		for (size_t i=0; i<lines.size(); i++)
266 			delete lines[i];
267 	}
268 
269 
270 	//
271 	//	constructor for Container, just initialises attributes
272 	//
Container(void)273 	Container::Container(void) : win_id(-1), win_width(0), current_mouseover_menu(0), mouse_over_window(false),
274 		reload_menus(false), context_id(CM_INIT_VALUE), window_used(false), title_on(1), background_on(1),
275 		border_on(1), use_small_font(0), include_datadir(1), auto_include_datadir(true), just_echo(0), win_x_pos(100),
276 		win_y_pos(100), name_sep(10), window_x_pad(8), window_y_pad(2), standard_window_position(STND_POS_NONE)
277 	{
278 	}
279 
280 
281 	//
282 	//	destroy the window, menus and lines
283 	//
destroy(void)284 	void Container::destroy(void)
285 	{
286 		delete_menus();
287 		if (cm_valid(context_id))
288 			cm_destroy(context_id);
289 		context_id = CM_INIT_VALUE;
290 		destroy_window(win_id);
291 	}
292 
293 
294 	//
295 	//	create the window for the user menus, or display it if already created
296 	//
open_window(void)297 	void Container::open_window(void)
298 	{
299 		if (win_id >= 0)
300 		{
301 			show_window(win_id);
302 			select_window(win_id);
303 			return;
304 		}
305 
306 		Uint32 win_flags = ELW_USE_UISCALE|ELW_SHOW_LAST|ELW_DRAGGABLE|ELW_SHOW|ELW_TITLE_NAME|ELW_ALPHA_BORDER|ELW_SWITCHABLE_OPAQUE;
307 
308 		set_win_flag(&win_flags, ELW_TITLE_BAR, title_on);
309 		set_win_flag(&win_flags, ELW_USE_BACKGROUND, background_on);
310 		set_win_flag(&win_flags, ELW_USE_BORDER, border_on);
311 		window_used = true;
312 
313 		win_id = create_window(um_window_title_str, -1, 0, win_x_pos, win_y_pos, 0, 0, win_flags);
314 		if (win_id < 0  || win_id >= windows_list.num_windows)
315 			return;
316 
317 		set_window_handler(win_id, ELW_HANDLER_DISPLAY, (int (*)())&display_handler );
318 		set_window_handler(win_id, ELW_HANDLER_MOUSEOVER, (int (*)())&mouseover_handler );
319 		set_window_handler(win_id, ELW_HANDLER_CLICK, (int (*)())&click_handler );
320 		set_window_handler(win_id, ELW_HANDLER_UI_SCALE, (int (*)())&ui_scale_handler );
321 		set_window_handler(win_id, ELW_HANDLER_FONT_CHANGE, (int (*)())&font_change_handler);
322 
323 		// the build-in context menu is only available if we have a title so always recreate
324 
325 		if (cm_valid(windows_list.window[win_id].cm_id))
326 		{
327 			cm_destroy(windows_list.window[win_id].cm_id);
328 			windows_list.window[win_id].cm_id = CM_INIT_VALUE;
329 		}
330 
331 		context_id = cm_create(cm_title_menu_str, NULL);
332 		cm_bool_line(context_id, 1, &windows_list.window[win_id].opaque, NULL);
333 		cm_bool_line(context_id, 2, &windows_on_top, "windows_on_top");
334 		cm_add(context_id, cm_user_menu_str, context_handler);
335 		cm_add_window(context_id, win_id);
336 
337 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_MOVEWIN, &title_on, NULL);
338 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_FIXPOS, &standard_window_position, NULL);
339 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_BACKGND, &background_on, NULL);
340 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_BORDER, &border_on, NULL);
341 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_FONT, &use_small_font, NULL);
342 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_STANDMENU, &include_datadir, NULL);
343 		cm_bool_line(context_id, ELW_CM_MENU_LEN+CM_SHOWCMD, &just_echo, NULL);
344 
345 		ui_scale_changed(&windows_list.window[win_id]);
346 		reload(&windows_list.window[win_id]);
347 
348 	} // Container::open_window()
349 
350 
351 	//
352 	//	return current window position and option values, normally for saving in the cfg file
353 	//
get_options(int * win_x,int * win_y,int * options) const354 	void Container::get_options(int *win_x, int *win_y, int *options) const
355 	{
356 		if(win_id >= 0)
357 		{
358 			*win_x = windows_list.window[win_id].cur_x;
359 			*win_y = windows_list.window[win_id].cur_y;
360 		}
361 		else
362 		{
363 			*win_x = win_x_pos;
364 			*win_y = win_y_pos;
365 		}
366 		*options = (window_used) ?1: 0;
367 		*options |= title_on << 1;
368 		*options |= border_on << 2;
369 		*options |= use_small_font << 3;
370 		*options |= include_datadir << 4;
371 		*options |= (background_on ^ 1) << 5; // added later so will not get set for existing users but we want it on by default
372 		*options |= standard_window_position << 6; // three bits
373 	}
374 
375 
376 	//
377 	//	set window position and option, normally from the cfg file
378 	//
set_options(int win_x,int win_y,int options)379 	void Container::set_options(int win_x, int win_y, int options)
380 	{
381 		if ((window_used = options & 1))
382 		{
383 			title_on = (options >> 1) & 1;
384 			border_on = (options >> 2) & 1;
385 			use_small_font = (options >> 3) & 1;
386 			include_datadir = (options >> 4) & 1;
387 			background_on = ((options >> 5) & 1) ^ 1;
388 			standard_window_position = (options >> 6) & 7; // three bits
389 			win_x_pos = win_x;
390 			win_y_pos = win_y;
391 		}
392 	}
393 
394 
395 #ifdef JSON_FILES
396 	//
397 	//	Write window position and option values, normally for saving in the cfg file
398 	//
write_options(const char * dict_name) const399 	void Container::write_options(const char *dict_name) const
400 	{
401 		if(win_id >= 0)
402 		{
403 			json_cstate_set_int(dict_name, "pos_x", windows_list.window[win_id].cur_x);
404 			json_cstate_set_int(dict_name, "pos_y", windows_list.window[win_id].cur_y);
405 		}
406 		else
407 		{
408 			json_cstate_set_int(dict_name, "pos_x", win_x_pos);
409 			json_cstate_set_int(dict_name, "pos_y", win_y_pos);
410 		}
411 		json_cstate_set_bool(dict_name, "window_used", window_used);
412 		json_cstate_set_bool(dict_name, "title_on", title_on);
413 		json_cstate_set_bool(dict_name, "border_on", border_on);
414 		json_cstate_set_bool(dict_name, "use_small_font", use_small_font);
415 		json_cstate_set_bool(dict_name, "include_datadir", include_datadir);
416 		json_cstate_set_bool(dict_name, "background_off", background_on ^ 1);
417 		json_cstate_set_int(dict_name, "standard_window_position", standard_window_position);
418 	}
419 
420 
421 	//
422 	//	Read window position and option, normally from the cfg file
423 	//
read_options(const char * dict_name)424 	void Container::read_options(const char *dict_name)
425 	{
426 		if ((window_used = json_cstate_get_bool(dict_name, "window_used", 0)))
427 		{
428 			title_on = json_cstate_get_bool(dict_name, "title_on", 0);
429 			border_on = json_cstate_get_bool(dict_name, "border_on", 0);
430 			use_small_font = json_cstate_get_bool(dict_name, "use_small_font", 0);
431 			include_datadir = json_cstate_get_bool(dict_name, "include_datadir", 0);
432 			background_on = json_cstate_get_bool(dict_name, "background_off", 0) ^ 1;
433 			standard_window_position = json_cstate_get_int(dict_name, "standard_window_position", 0);
434 			win_x_pos = json_cstate_get_int(dict_name, "pos_x", 0);
435 			win_y_pos = json_cstate_get_int(dict_name, "pos_y", 0);
436 		}
437 	}
438 #endif
439 
440 
441 	//
442 	// the window display callback
443 	//
display(window_info * win)444 	int Container::display(window_info *win)
445 	{
446 		// if the menus need reloading, try it now
447 		if (reload_menus)
448 			reload(win);
449 
450 		// update the command queue
451 		command_queue.process(just_echo);
452 
453 		// a reload may have changed the window size
454 		if ((win_width != win->len_x) || (get_height(win) != win->len_y))
455 		{
456 			recalc_win_width(win);
457 			resize_window (win->window_id, win_width, get_height(win));
458 		}
459 
460 		update_standard_window_position(win);
461 
462 		// enable the title bar if the window ends up off screen - resolution change perhaps
463 		if ((standard_window_position == STND_POS_NONE) && ((win->cur_x + 20 > window_width) || (win->cur_y + 10 > window_height)))
464 			set_title_state(win, true);
465 
466 		int curr_x = window_x_pad;
467 
468 		// if there are no menus, fill the window with a suitable message
469 		if (menus.empty())
470 		{
471 			if (mouse_over_window)
472 				glColor3fv(gui_color);
473 			else
474 				glColor3fv(gui_dull_color);
475 			if (use_small_font)
476 				draw_string_small_zoomed(curr_x, window_y_pad, (const unsigned char *)um_no_menus_str, 1, win->current_scale);
477 			else
478 				draw_string_zoomed(curr_x, window_y_pad, (const unsigned char *)um_no_menus_str, 1, win->current_scale );
479 			mouse_over_window = false;
480 			return 1;
481 		}
482 
483 		// find the menu index of an already open menu window
484 		size_t open_menu = menus.size();
485 		size_t open_cm = cm_window_shown();
486 		if (open_cm != CM_INIT_VALUE)
487 		{
488 			for (size_t i=0; i<menus.size(); i++)
489 				if (menus[i]->get_cm_id() == open_cm)
490 					open_menu = i;
491 		}
492 
493 		// draw the menu name text, hightlighting if it is open or if the mouse is over it
494 		for (size_t i=0; i<menus.size(); i++)
495 		{
496 			if ((current_mouseover_menu == i) || (open_menu == i))
497 				glColor3f(1.0f,1.0f,1.0f);
498 			else if (mouse_over_window)
499 				glColor3fv(gui_color);
500 			else
501 				glColor3fv(gui_dull_color);
502 			if (use_small_font)
503 				draw_string_small_zoomed(curr_x, window_y_pad, (const unsigned char *)menus[i]->get_name().c_str(), 1, win->current_scale);
504 			else
505 				draw_string_zoomed(curr_x, window_y_pad, (const unsigned char *)menus[i]->get_name().c_str(), 1, win->current_scale);
506 			curr_x += calc_actual_width(win, menus[i]->get_name_width()) + name_sep;
507 		}
508 
509 		// make sure next time we will not highlight a menu name if the mouse is not in the window
510 		current_mouseover_menu = menus.size();
511 		mouse_over_window = false;
512 
513 		return 1;
514 
515 	} // end Container::display()
516 
517 
518 	//
519 	// open the menu if the name is clicked
520 	//
click(window_info * win,int mx,Uint32 flags)521 	int Container::click(window_info *win, int mx, Uint32 flags)
522 	{
523 		if ((flags & ELW_LEFT_MOUSE) &&
524 			((current_mouseover_menu = get_mouse_over_menu(win, mx)) < menus.size()) &&
525 			cm_valid(menus[current_mouseover_menu]->get_cm_id()))
526 		{
527 			cm_show_direct(menus[current_mouseover_menu]->get_cm_id(), win_id, current_mouseover_menu);
528 			return 1;
529 		}
530 		return 0;
531 	}
532 
533 
534 	//
535 	// Called when the UI is scaled
536 	//
ui_scale_changed(window_info * win)537 	int Container::ui_scale_changed(window_info *win)
538 	{
539 		window_y_pad = static_cast<int>(0.5 + 2 * win->current_scale);
540 		window_x_pad = 4 * window_y_pad;
541 		name_sep = static_cast<int>(0.5 + 10 * win->current_scale);
542 		recalc_win_width(win);
543 		resize_window(win->window_id, win_width, get_height(win));
544 		return 1;
545 	}
546 
547 
548 	//
549 	// Called when font settings are changed
550 	//
font_changed(window_info * win,eternal_lands::FontManager::Category cat)551 	int Container::font_changed(window_info *win, eternal_lands::FontManager::Category cat)
552 	{
553 		if (cat != win->font_category)
554 			return 0;
555 		ui_scale_changed(win);
556 		return 1;
557 	}
558 
559 
560 	//
561 	// common callback fuction for context menu, line selection
562 	//
action(size_t active_menu,int option)563 	int Container::action(size_t active_menu, int option)
564 	{
565 		if (active_menu < menus.size())
566 			menus[active_menu]->action(option, command_queue);
567 		return 1;
568 	}
569 
570 
571 	//
572 	//	adjust the menu window position to be more menu like
573 	//
pre_show(window_info * win,int widget_id,int mx,int my,window_info * cm_win)574 	void Container::pre_show(window_info *win, int widget_id, int mx, int my, window_info *cm_win)
575 	{
576 		size_t curr_menu = (size_t)widget_id;
577 		if (win == NULL || cm_win == NULL || !(curr_menu<menus.size()))
578 		{
579 			std::cerr << __FUNCTION__ << ": Problem with input win=" << win << " widget=" << widget_id
580 			  << " mx=" << mx << " my=" << my << " cmwin=" << cm_win << std::endl;
581 			return;
582 		}
583 
584 		// get the menu name x offset in the menu window
585 		int x_offset = window_x_pad;
586 		for (size_t i=0; i<curr_menu && i<menus.size(); i++)
587 			x_offset += calc_actual_width(win, menus[i]->get_name_width()) + name_sep;
588 
589 		// see what fits x: position under the menu name, or hard at the right or hard at the left
590 		int new_x_pos = win->cur_x + x_offset;
591 		if (new_x_pos + cm_win->len_x > window_width)
592 			new_x_pos = window_width - cm_win->len_x;
593 		if (new_x_pos < 0)
594 			new_x_pos = 0;
595 
596 		// see what fits y: position below the window menu, or position above the window menu
597 		int new_y_pos = win->cur_y;
598 		if (new_y_pos + win->len_y + cm_win->len_y > window_height)
599 			new_y_pos -= cm_win->len_y;
600 		else
601 			 new_y_pos += win->len_y;
602 
603    		move_window(cm_win->window_id, -1, 0, new_x_pos, new_y_pos);
604 	}
605 
606 
607 	//
608 	// the "evil" singleton mechanism
609 	//
get_instance(void)610 	Container * Container::get_instance(void)
611 	{
612 		static Container um;
613 		static SDL_threadID creation_thread = SDL_ThreadID();
614 		if (SDL_ThreadID() != creation_thread)
615 			std::cerr << __FUNCTION__ << ": Danger W.R.! User menus call by non-creator thread." << std::endl;
616 		return &um;
617 	}
618 
619 
620 	//
621 	// load, or reload, the menu files
622 	//
reload(window_info * win)623 	void Container::reload(window_info *win)
624 	{
625 		// if a context menu is currently showing, do not reload yet
626 		if (cm_window_shown() != CM_INIT_VALUE)
627 		{
628 			reload_menus = true;
629 			return;
630 		}
631 		reload_menus = false;
632 
633 		delete_menus();
634 
635 		std::vector<std::string> filelist;
636 
637 		std::vector<std::string> search_paths;
638 		if (include_datadir)
639 			search_paths.push_back(std::string(datadir));
640 		search_paths.push_back(std::string(get_path_config()));
641 
642 		for (size_t i=0; i<search_paths.size(); i++)
643 		{
644 			std::string glob_path = search_paths[i] + "*.menu";
645 			// find all the menu files and build a list of path+filenames for later
646 #ifdef WINDOWS
647 			struct _finddata_t c_file;
648 			intptr_t hFile;
649 			if ((hFile = _findfirst(glob_path.c_str(), &c_file)) != static_cast<intptr_t>(-1))
650 			{
651 				do
652 					filelist.push_back(search_paths[i] + std::string(c_file.name));
653 				while (_findnext(hFile, &c_file) == 0);
654 				_findclose(hFile);
655 			}
656 #else 		// phew! it's a real operating system
657 			glob_t glob_res;
658 			if (glob(glob_path.c_str(), 0, NULL, &glob_res)==0)
659 			{
660 				for (size_t i=0; i<glob_res.gl_pathc; i++)
661 					filelist.push_back(std::string(glob_res.gl_pathv[i]));
662 				globfree(&glob_res);
663 			}
664 #endif
665 		}
666 
667 		// process all the menu files, creating menu objects for each valid file
668 		for (size_t i=0; i<filelist.size(); i++)
669 		{
670 			Menu *umb = new Menu(filelist[i]);
671 			if (umb->get_name().empty())
672 				delete umb;
673 			else
674 				menus.push_back(umb);
675 		}
676 
677 		current_mouseover_menu = menus.size();
678 		recalc_win_width(win);
679 
680 		// if there are no user menus but we're not trying standard menus...
681 		// unless already controlled this session, enable standard menus and reload
682 		if (menus.empty() && !include_datadir && auto_include_datadir)
683 		{
684 			include_datadir = 1;
685 			reload(win);
686 		}
687 
688 	} // end Container::reload()
689 
690 
691 	//
692 	//	Calculates the window width, which changes with the zoom
693 	//
recalc_win_width(window_info * win)694 	void Container::recalc_win_width(window_info *win)
695 	{
696 		// if there are no menus, use the size of the message for the window width
697 		if (menus.empty())
698 		{
699 			win_width = 2 * window_x_pad + calc_actual_width(win, get_string_width_zoom((const unsigned char*)um_no_menus_str, win->font_category, 1.0));
700 			return;
701 		}
702 
703 		// otherwise, calculate the width from the widths of all the menus names
704 		win_width = 2 * window_x_pad + name_sep * (menus.size() - 1);
705 		for (size_t i=0; i<menus.size(); i++)
706 			win_width += calc_actual_width(win, menus[i]->recalc_name_width(1.0));
707 	}
708 
709 
710 	//
711 	//	If the mouse is over a menu name and there are no context windows
712 	//	open then return the menu index, otherwise return the number of menus.
713 	//
714 	//	Side effects:
715 	//	If one of the menus is open and the mouse is over a different menu name,
716 	//	the current menu is closed and the one with the mouse over, opened.
717 	//
get_mouse_over_menu(window_info * win,int mx)718 	size_t Container::get_mouse_over_menu(window_info *win, int mx)
719 	{
720 		// if the mouse is over a menu name, get the menus[] index
721 		size_t mouse_over = menus.size();
722 		int name_end_x = window_x_pad;
723 		for (size_t i=0; i<menus.size(); i++)
724 		{
725 			name_end_x += calc_actual_width(win, menus[i]->get_name_width()) + name_sep;
726 			if (mx < name_end_x-name_sep/2)
727 			{
728 				mouse_over = i;
729 				break;
730 			}
731 		}
732 
733 		// if not over a menu name
734 		if (mouse_over == menus.size())
735 			return menus.size();
736 
737 		// get the id of any open context menu, return with result of mouse index if none
738 		size_t open_cm = cm_window_shown();
739 		if (open_cm == CM_INIT_VALUE)
740 			return mouse_over;
741 
742 		// a context menu is open, if it is one of our menus and the mouse is over another
743 		// close the current menu and open the one the mouse is over
744 		for (size_t i=0; i<menus.size(); i++)
745 			if (menus[i]->get_cm_id() == open_cm)
746 			{
747 				if (i != mouse_over)
748 				{
749 					cm_post_show_check(1);
750 					cm_show_direct(menus[mouse_over]->get_cm_id(), win_id, mouse_over);
751 				}
752 				break;
753 			}
754 
755 		return menus.size();
756 	}
757 
758 
759 	//
760 	//  prepare for a menu reload or exit
761 	//
delete_menus(void)762 	void Container::delete_menus(void)
763 	{
764 		for (size_t i=0; i<menus.size(); i++)
765 			delete menus[i];
766 		menus.clear();
767 	}
768 
769 
770 	//
771 	//	change a window property bit flag
772 	//
set_win_flag(Uint32 * flags,Uint32 flag,int state)773 	void Container::set_win_flag(Uint32 *flags, Uint32 flag, int state)
774 	{
775 		if (state)
776 			*flags |= flag;
777 		else
778 			*flags &= ~flag;
779 	}
780 
781 
782 	//
783 	//	Handle possitioning the window in one of the fixed locations.
784 	//
update_standard_window_position(window_info * win)785 	void Container::update_standard_window_position(window_info *win)
786 	{
787 		if (title_on)
788 		{
789 			if (standard_window_position != STND_POS_NONE)
790 				standard_window_position = STND_POS_NONE;
791 			return;
792 		}
793 
794 		int wanted_x = 0, wanted_y = 0, tmp_end = 0;
795 
796 		switch (standard_window_position)
797 		{
798 			case STND_POS_TOP_LEFT:
799 				wanted_x = get_tabbed_chat_end_x() + window_x_pad;
800 				wanted_y = 0;
801 				break;
802 			case STND_POS_TOP_CENTRE:
803 				tmp_end = get_tabbed_chat_end_x();
804 				wanted_x = tmp_end + ( window_width - hud_x - get_fps_default_width() - tmp_end - win->len_x) / 2;
805 				wanted_y = 0;
806 				break;
807 			case STND_POS_TOP_RIGHT:
808 				wanted_x = window_width - hud_x - get_fps_default_width() - win->len_x - window_x_pad;
809 				wanted_y = 0;
810 				break;
811 			case STND_POS_BOTTOM_LEFT:
812 				wanted_x = window_x_pad + get_icons_win_active_len();
813 				wanted_y = window_height - win->len_y;
814 				break;
815 			case STND_POS_BOTTOM_CENTRE:
816 				tmp_end = get_icons_win_active_len();
817 				wanted_x = tmp_end + (window_width - hud_x - get_hud_indicators_default_width() - tmp_end - win->len_x) / 2;
818 				wanted_y = window_height - win->len_y;
819 				break;
820 			case STND_POS_BOTTON_RIGHT:
821 				wanted_x = window_width - hud_x - get_hud_indicators_default_width() - win->len_x - window_x_pad;
822 				wanted_y = window_height - win->len_y;
823 				break;
824 			default:
825 				return;
826 		}
827 
828 		if ((win->cur_x != wanted_x) || (win->cur_y != wanted_y))
829 		{
830 			move_window(win->window_id, -1, 0, wanted_x, wanted_y);
831 			if ((win->cur_x != wanted_x) || (win->cur_y != wanted_y))
832 			{
833 				set_title_state(win, true);
834 				standard_window_position = STND_POS_NONE;
835 			}
836 		}
837 	}
838 
839 
840 	//
841 	//	handler window content menu options
842 	//
context(window_info * win,int widget_id,int mx,int my,int option)843 	int Container::context(window_info *win, int widget_id, int mx, int my, int option)
844 	{
845 		if (option<ELW_CM_MENU_LEN)
846 			return cm_title_handler(win, widget_id, mx, my, option);
847 
848 		switch (option)
849 		{
850 			case ELW_CM_MENU_LEN+CM_MOVEWIN:
851 			{
852 				set_win_flag(&win->flags, ELW_TITLE_BAR, title_on);
853 				if (win->cur_y == win->title_height)
854 					move_window(win->window_id, -1, 0, win->cur_x, 0);
855 				else if (win->cur_y == 0)
856 					move_window(win->window_id, -1, 0, win->cur_x, win->title_height);
857 				break;
858 			}
859 			case ELW_CM_MENU_LEN+CM_FIXPOS:
860 				if (standard_window_position != STND_POS_NONE)
861 				{
862 					set_title_state(win, false);
863 					update_standard_window_position(win);
864 				}
865 				break;
866 			case ELW_CM_MENU_LEN+CM_CHANGEPOS:
867 				set_title_state(win, false);
868 				if (++standard_window_position >= STND_POS_LAST)
869 					standard_window_position = STND_POS_TOP_LEFT;
870 				break;
871 			case ELW_CM_MENU_LEN+CM_BACKGND: set_win_flag(&win->flags, ELW_USE_BACKGROUND, background_on); break;
872 			case ELW_CM_MENU_LEN+CM_BORDER: set_win_flag(&win->flags, ELW_USE_BORDER, border_on); break;
873 			case ELW_CM_MENU_LEN+CM_FONT: recalc_win_width(win); break;
874 			case ELW_CM_MENU_LEN+CM_STANDMENU: auto_include_datadir = false; reload(win); break;
875 			case ELW_CM_MENU_LEN+CM_RELOAD: reload(win); break;
876 			case ELW_CM_MENU_LEN+CM_DISABLE: toggle_user_menus(&enable_user_menus); break;
877 		}
878 
879 		return 1;
880 	}
881 
882 } // end UserMenus namespace
883 
884 
885 //
886 //	External Interface functions
887 //
888 extern "C"
889 {
890 	int enable_user_menus = 0;
891 	int ready_for_user_menus = 0;
892 
set_options_user_menus(int win_x,int win_y,int options)893 	void set_options_user_menus(int win_x, int win_y, int options)
894 	{
895 		UserMenus::Container::get_instance()->set_options(win_x, win_y, options);
896 	}
897 
get_options_user_menus(int * win_x,int * win_y,int * options)898 	void get_options_user_menus(int *win_x, int *win_y, int *options)
899 	{
900 		UserMenus::Container::get_instance()->get_options(win_x, win_y, options);
901 	}
902 
903 #ifdef JSON_FILES
read_options_user_menus(const char * dict_name)904 	void read_options_user_menus(const char *dict_name)
905 	{
906 		UserMenus::Container::get_instance()->read_options(dict_name);
907 	}
908 
write_options_user_menus(const char * dict_name)909 	void write_options_user_menus(const char *dict_name)
910 	{
911 		UserMenus::Container::get_instance()->write_options(dict_name);
912 	}
913 #endif
914 
toggle_user_menus(int * enable)915 	void toggle_user_menus(int *enable)
916 	{
917 		*enable = !*enable;
918 		set_var_unsaved("enable_user_menus", INI_FILE_VAR);
919 		if (!ready_for_user_menus)
920 			return;
921 		if (*enable)
922 			UserMenus::Container::get_instance()->open_window();
923 		else
924 			UserMenus::Container::get_instance()->close_window();
925 	}
926 
display_user_menus(void)927 	void display_user_menus(void)
928 	{
929 		if (ready_for_user_menus)
930 			UserMenus::Container::get_instance()->open_window();
931 	}
932 
destroy_user_menus(void)933 	void destroy_user_menus(void) { UserMenus::Container::get_instance()->destroy(); }
934 }
935