1 /*
2 * Crossfire -- cooperative multi-player graphical RPG and adventure game
3 *
4 * Copyright (c) 1999-2013 Mark Wedel and the Crossfire Development Team
5 * Copyright (c) 1992 Frank Tore Johansen
6 *
7 * Crossfire is free software and comes with ABSOLUTELY NO WARRANTY. You are
8 * welcome to redistribute it under certain conditions. For details, please
9 * see COPYING and LICENSE.
10 *
11 * The authors can be reached via e-mail at <crossfire@metalforge.org>.
12 */
13
14 /**
15 * @file
16 * Implement client configuration dialog
17 */
18
19 #include "client.h"
20
21 #include <ctype.h>
22 #include <gtk/gtk.h>
23
24 #include "image.h"
25 #include "main.h"
26 #include "mapdata.h"
27 #include "gtk2proto.h"
28
29 static GKeyFile *config;
30 static GString *config_path;
31
32 GtkWidget *config_dialog, *config_button_echo, *config_button_fasttcp,
33 *config_button_timestamp, *config_button_grad_color,
34 *config_button_foodbeep, *config_button_sound, *config_button_cache,
35 *config_button_download, *config_button_fog, *config_button_smoothing;
36
37 GtkFileChooser *ui_filechooser, *theme_filechooser;
38 GtkComboBoxText *config_combobox_faceset;
39 GtkComboBox *config_combobox_displaymode, *config_combobox_lighting;
40
41 #define THEME_DEFAULT CF_DATADIR "/themes/Standard"
42
43 /* Configuration variables initialized to NULL, set by config_load() */
44 static char *theme;
45 char* last_server;
46
47 int predict_alpha;
48
49 static void on_config_close(GtkButton *button, gpointer user_data);
50
51 /**
52 * Return the basename of the current UI file.
53 */
ui_name()54 static char *ui_name() {
55 return g_path_get_basename(window_xml_file);
56 }
57
58 /**
59 * Sets up player-specific client and layout rc files and handles loading of a
60 * client theme if one is selected. First, the player-specific rc files are
61 * added to the GTK rc default files list. ${HOME}/.crossfire/gtkrc is added
62 * first. All client sessions are affected by this rc file if it exists.
63 * Next, ${HOME}/.crossfire/[layout].gtkrc is added, where [layout] is the
64 * name of the layout file that is loaded. IE. If gtk-v2.ui is loaded,
65 * [layout] is "gtk-v2". This sets up the possibility for a player to make a
66 * layout-specific rc file. Finally, if the client theme is not "None", the
67 * client theme file is added. In most cases, the player-specific files are
68 * probably not going to exist, so the theme system will continue to work the
69 * way it always has. The player will have to "do something" to get the extra
70 * functionality. At some point, conceptually the client itself could be
71 * enhanced to allow it to save some basic settings to either or both of the
72 * player-specific rc files.
73 *
74 * @param reload
75 * If true, user has changed theme after initial startup. In this mode, we
76 * need to call the routines that store away private theme data. When program
77 * is starting up, this is false, because all the widgets haven't been realized
78 * yet, and the initialize routines will get the theme data at that time.
79 */
80 static char **default_files = NULL;
init_theme()81 void init_theme() {
82 char path[MAX_BUF];
83 char **tmp;
84 int i;
85
86 /*
87 * The GTK man page says copy of this data should be made, so do that.
88 */
89 tmp = gtk_rc_get_default_files();
90 i = 0;
91 while (tmp && tmp[i]) {
92 i++;
93 }
94 /*
95 * Add two more GTK rc files that may be used by a player to customize
96 * the client appearance in general, or to customize the appearance
97 * of a specific layout. Allocate pointers to the local copy
98 * of the entire list.
99 */
100 i += 2;
101 default_files = g_malloc(sizeof(char *) * (i + 1));
102 /*
103 * Copy in GTK's default list which probably contains system paths
104 * like <SYSCONFDIR>/gtk-2.0/gtkrc and user-specific files like
105 * ${HOME}/.gtkrc, or even LANGuage-specific ones like
106 * ${HOME}/.gtkrc.en, etc.
107 */
108 i = 0;
109 while (tmp && tmp[i]) {
110 default_files[i] = g_strdup(tmp[i]);
111 i++;
112 }
113 /*
114 * Add a player-specific gtkrc to the list of default rc files. This
115 * file is probably reserved for player use, though in all liklihood
116 * will not get used that much. Still, it makes it easy for someone
117 * to make their own theme without having to have access to the
118 * system-wide theme folder. This is the lowest priority client rc
119 * file as either a <layout>.gtkrc file or a client-configured theme
120 * settings can over-ride it.
121 */
122 snprintf(path, sizeof(path), "%s/gtkrc", config_dir);
123 default_files[i] = g_strdup(path);
124 i++;
125 /*
126 * Add a UI layout-specific rc file to the list of default list. It
127 * seems reasonable to allow client code to have access to this file
128 * to make some basic changes to fonts, via a graphical interface.
129 * Truncate window_xml_file to remove a .extension if one exists, so
130 * that the window positions file can be created with a .gtkrc suffix.
131 * This is a mid-priority client rc file as its settings supersede the
132 * client gtkrc file, but are overridden by a client-configured theme.
133 */
134 snprintf(path, sizeof(path), "%s/%s.gtkrc", config_dir, ui_name());
135 default_files[i] = g_strdup(path);
136 i++;
137 /*
138 * Mark the end of the list of default rc files.
139 */
140 default_files[i] = NULL;
141 }
142
load_theme(int reload)143 void load_theme(int reload) {
144 /*
145 * Whether or not this is default and initial run, we want to register
146 * the modified rc search path list, so GTK needs to get the changes.
147 * It is necessary to reset the the list each time through here each
148 * theme change grows the list. Only one theme should be in the list
149 * at a time.
150 */
151 gtk_rc_set_default_files(default_files);
152
153 /*
154 * If a client-configured theme has been selected (something other than
155 * "None"), then add it to the list of GTK rc files to process. Since
156 * this file is added last, it takes priority over both the gtkrc and
157 * <layout>.gtkrc files. Remember, strcmp returns zero on a match, and
158 * a theme file should not be registered if "None" is selected.
159 */
160 g_assert(theme != NULL); // ensured by config_load()
161 {
162 /*
163 * Check for existence of the client theme file. Unfortunately, at
164 * initial run time, the window may not be realized yet, so the
165 * message cannot be sent to the user directly. It doesn't hurt to
166 * add the path even if the file isn't there, but the player might
167 * still want to know something is wrong since they picked a theme.
168 */
169 if (access(theme, R_OK) == -1) {
170 LOG(LOG_ERROR, "load_theme", "Unable to find theme file %s", theme);
171 g_free(theme);
172 theme = g_strdup(THEME_DEFAULT);
173 }
174 gtk_rc_add_default_file(theme);
175 }
176
177 /*
178 * Require GTK to reparse and rebind all the widget data.
179 */
180 gtk_rc_reparse_all_for_settings(
181 gtk_settings_get_for_screen(gdk_screen_get_default()), TRUE);
182 gtk_rc_reset_styles(
183 gtk_settings_get_for_screen(gdk_screen_get_default()));
184 /*
185 * Call client functions to reparse the custom widgets it controls.
186 */
187 info_get_styles();
188 inventory_get_styles();
189 stats_get_styles();
190 spell_get_styles();
191 update_spell_information();
192 /*
193 * Set inv_updated to force a redraw - otherwise it will not
194 * necessarily bind the lists with the new widgets.
195 */
196 cpl.below->inv_updated = 1;
197 cpl.ob->inv_updated = 1;
198 draw_lists();
199 draw_stats(TRUE);
200 draw_message_window(TRUE);
201 }
202
203 /**
204 * Load settings from the legacy file format.
205 */
config_load_legacy()206 static void config_load_legacy() {
207 char path[MAX_BUF], inbuf[MAX_BUF], *cp;
208 FILE *fp;
209 int i, val;
210
211 LOG(LOG_INFO, "config_load_legacy",
212 "Configuration not found; trying old configuration files.");
213 LOG(LOG_INFO, "config_load_legacy",
214 "You will need to move your keybindings to the new location.");
215
216 snprintf(path, sizeof(path), "%s/.crossfire/gdefaults2", g_getenv("HOME"));
217 if ((fp = fopen(path, "r")) == NULL) {
218 return;
219 }
220 while (fgets(inbuf, MAX_BUF - 1, fp)) {
221 inbuf[MAX_BUF - 1] = '\0';
222 inbuf[strlen(inbuf) - 1] = '\0'; /* kill newline */
223
224 if (inbuf[0] == '#') {
225 continue;
226 }
227 /* Skip any setting line that does not contain a colon character */
228 if (!(cp = strchr(inbuf, ':'))) {
229 continue;
230 }
231 *cp = '\0';
232 cp += 2; /* colon, space, then value */
233
234 val = -1;
235 if (isdigit(*cp)) {
236 val = atoi(cp);
237 } else if (!strcmp(cp, "True")) {
238 val = TRUE;
239 } else if (!strcmp(cp, "False")) {
240 val = FALSE;
241 }
242
243 for (i = 1; i < CONFIG_NUMS; i++) {
244 if (!strcmp(config_names[i], inbuf)) {
245 if (val == -1) {
246 LOG(LOG_WARNING, "config.c::load_defaults",
247 "Invalid value/line: %s: %s", inbuf, cp);
248 } else {
249 want_config[i] = val;
250 }
251 break; /* Found a match - won't find another */
252 }
253 }
254 /* We found a match in the loop above, so do not do anything more */
255 if (i < CONFIG_NUMS) {
256 continue;
257 }
258
259 /*
260 * Legacy - now use the map_width and map_height values Don't do sanity
261 * checking - that will be done below
262 */
263 if (!strcmp(inbuf, "mapsize")) {
264 if (sscanf(cp, "%hdx%hd", &want_config[CONFIG_MAPWIDTH],
265 &want_config[CONFIG_MAPHEIGHT]) != 2) {
266 LOG(LOG_WARNING, "config.c::load_defaults",
267 "Malformed mapsize option in gdefaults2. Ignoring");
268 }
269 } else if (!strcmp(inbuf, "theme")) {
270 if (theme != NULL) {
271 g_free(theme);
272 }
273 theme = g_strdup(cp);
274 continue;
275 } else if (!strcmp(inbuf, "window_layout")) {
276 strncpy(window_xml_file, cp, MAX_BUF - 1);
277 continue;
278 } else if (!strcmp(inbuf, "nopopups")) {
279 /* Changed name from nopopups to popups, so inverse value */
280 want_config[CONFIG_POPUPS] = !val;
281 continue;
282 } else if (!strcmp(inbuf, "nosplash")) {
283 want_config[CONFIG_SPLASH] = !val;
284 continue;
285 } else if (!strcmp(inbuf, "splash")) {
286 want_config[CONFIG_SPLASH] = val;
287 continue;
288 } else if (!strcmp(inbuf, "faceset")) {
289 face_info.want_faceset = g_strdup(cp); /* memory leak ! */
290 continue;
291 }
292 /* legacy, as this is now just saved as 'lighting' */
293 else if (!strcmp(inbuf, "per_tile_lighting")) {
294 if (val) {
295 want_config[CONFIG_LIGHTING] = CFG_LT_TILE;
296 }
297 } else if (!strcmp(inbuf, "per_pixel_lighting")) {
298 if (val) {
299 want_config[CONFIG_LIGHTING] = CFG_LT_PIXEL;
300 }
301 } else if (!strcmp(inbuf, "resists")) {
302 if (val) {
303 want_config[CONFIG_RESISTS] = val;
304 }
305 } else if (!strcmp(inbuf, "sdl")) {
306 if (val) {
307 want_config[CONFIG_DISPLAYMODE] = CFG_DM_SDL;
308 }
309 } else LOG(LOG_WARNING, "config.c::load_defaults",
310 "Unknown line in gdefaults2: %s %s", inbuf, cp);
311 }
312 fclose(fp);
313 }
314
315 /**
316 * Sanity check values set in want_config and copy them over to use_config
317 * when all of them are acceptable.
318 *
319 * This function should be called after config_load() and parse_args().
320 */
config_check()321 void config_check() {
322 if (want_config[CONFIG_ICONSCALE] < 25 ||
323 want_config[CONFIG_ICONSCALE] > 200) {
324 LOG(LOG_WARNING, "config_check",
325 "Ignoring invalid 'iconscale' value '%d'; "
326 "must be between 25 and 200.\n",
327 want_config[CONFIG_ICONSCALE]);
328 want_config[CONFIG_ICONSCALE] = use_config[CONFIG_ICONSCALE];
329 }
330
331 if (want_config[CONFIG_MAPSCALE] < 25 ||
332 want_config[CONFIG_MAPSCALE] > 200) {
333 LOG(LOG_WARNING, "config_check",
334 "Ignoring invalid 'mapscale' value '%d'; "
335 "must be between 25 and 200.\n",
336 want_config[CONFIG_MAPSCALE]);
337 want_config[CONFIG_MAPSCALE] = use_config[CONFIG_MAPSCALE];
338 }
339
340 if (!want_config[CONFIG_LIGHTING]) {
341 LOG(LOG_WARNING, "config_check",
342 "No lighting mechanism selected - will not use darkness code");
343 want_config[CONFIG_DARKNESS] = FALSE;
344 }
345
346 if (want_config[CONFIG_RESISTS] > 2) {
347 LOG(LOG_WARNING, "config_check",
348 "Ignoring invalid 'resists' value '%d'; "
349 "must be either 0, 1, or 2.\n",
350 want_config[CONFIG_RESISTS]);
351 want_config[CONFIG_RESISTS] = 0;
352 }
353
354 /* Make sure the map size os OK */
355 if (want_config[CONFIG_MAPWIDTH] < 9 ||
356 want_config[CONFIG_MAPWIDTH] > MAP_MAX_SIZE) {
357 LOG(LOG_WARNING, "config_check", "Invalid map width (%d) "
358 "option in gdefaults2. Valid range is 9 to %d",
359 want_config[CONFIG_MAPWIDTH], MAP_MAX_SIZE);
360 want_config[CONFIG_MAPWIDTH] = use_config[CONFIG_MAPWIDTH];
361 }
362
363 if (want_config[CONFIG_MAPHEIGHT] < 9 ||
364 want_config[CONFIG_MAPHEIGHT] > MAP_MAX_SIZE) {
365 LOG(LOG_WARNING, "config_check", "Invalid map height (%d) "
366 "option in gdefaults2. Valid range is 9 to %d",
367 want_config[CONFIG_MAPHEIGHT], MAP_MAX_SIZE);
368 want_config[CONFIG_MAPHEIGHT] = use_config[CONFIG_MAPHEIGHT];
369 }
370
371 #if !defined(HAVE_OPENGL)
372 if (want_config[CONFIG_DISPLAYMODE] == CFG_DM_OPENGL) {
373 want_config[CONFIG_DISPLAYMODE] = CFG_DM_PIXMAP;
374 LOG(LOG_ERROR, "config_check",
375 "Display mode is set to OpenGL, but client "
376 "is not compiled with OpenGL support. Reverting to pixmap mode.");
377 }
378 #endif
379
380 #if !defined(HAVE_SDL)
381 if (want_config[CONFIG_DISPLAYMODE] == CFG_DM_SDL) {
382 want_config[CONFIG_DISPLAYMODE] = CFG_DM_PIXMAP;
383 LOG(LOG_ERROR, "config_check",
384 "Display mode is set to SDL, but client "
385 "is not compiled with SDL support. Reverting to pixmap mode.");
386 }
387 #endif
388
389 /* Copy sanitized user settings to current settings. */
390 memcpy(use_config, want_config, sizeof(use_config));
391
392 image_size = DEFAULT_IMAGE_SIZE * use_config[CONFIG_ICONSCALE] / 100;
393 map_image_size = DEFAULT_IMAGE_SIZE * use_config[CONFIG_MAPSCALE] / 100;
394 map_image_half_size = DEFAULT_IMAGE_SIZE * use_config[CONFIG_MAPSCALE] / 200;
395 if (!use_config[CONFIG_CACHE]) {
396 use_config[CONFIG_DOWNLOAD] = FALSE;
397 }
398 }
399
400 /**
401 * Load settings from the user's configuration file into want_config.
402 */
config_load()403 void config_load() {
404 GError *error = NULL;
405
406 /* Copy initial desired settings from current settings. */
407 memcpy(want_config, use_config, sizeof(want_config));
408
409 g_assert(g_file_test(config_dir, G_FILE_TEST_IS_DIR) == TRUE);
410
411 /* Load existing or create new configuration file. */
412 config = g_key_file_new();
413 config_path = g_string_new(config_dir);
414 g_string_append(config_path, "/client.ini");
415
416 g_key_file_load_from_file(config, config_path->str, G_KEY_FILE_NONE, &error);
417
418 /* Load configuration values into settings array. */
419 if (error == NULL) {
420 for (int i = 1; i < CONFIG_NUMS; i++) {
421 want_config[i] = g_key_file_get_integer(config, "Client",
422 config_names[i], NULL);
423 }
424
425 /* Load additional settings. */
426 if (theme != NULL) {
427 g_free(theme);
428 }
429 theme = g_key_file_get_string(config, "GTKv2", "theme", NULL);
430
431 if (face_info.want_faceset != NULL) {
432 g_free(face_info.want_faceset);
433 }
434 face_info.want_faceset = g_key_file_get_string(config, "GTKv2", "faceset", NULL);
435
436 predict_alpha = g_key_file_get_integer(config, "GTKv2", "predict_alpha", NULL);
437
438 if (last_server != NULL) {
439 g_free(last_server);
440 }
441 last_server = g_key_file_get_string(config, "GTKv2", "last_server", NULL);
442
443 char *layout = g_key_file_get_string(config, "GTKv2", "window_layout", NULL);
444 g_strlcpy(window_xml_file, layout, sizeof(window_xml_file));
445 free(layout);
446 } else {
447 g_error_free(error);
448
449 /* Load legacy configuration file. */
450 config_load_legacy();
451 }
452
453 if (theme == NULL) {
454 theme = g_strdup(THEME_DEFAULT);
455 }
456
457 if (face_info.want_faceset == NULL) {
458 face_info.want_faceset = g_strdup("");
459 }
460
461 if (last_server == NULL) {
462 last_server = g_strdup("");
463 }
464 }
465
466 /**
467 * This function saves user settings chosen using the configuration popup
468 * dialog.
469 */
save_defaults()470 void save_defaults() {
471 GError *error = NULL;
472
473 /* Save GTKv2 specific client settings. */
474 g_key_file_set_string(config, "GTKv2", "theme", theme);
475 g_key_file_set_string(config, "GTKv2", "faceset", face_info.want_faceset);
476 g_key_file_set_string(config, "GTKv2", "last_server", last_server);
477 g_key_file_set_integer(config, "GTKv2", "predict_alpha", predict_alpha);
478 g_key_file_set_string(config, "GTKv2", "window_layout", window_xml_file);
479
480 /* Save the rest of the client settings. */
481 for (int i = 1; i < CONFIG_NUMS; i++) {
482 g_key_file_set_integer(config, "Client", config_names[i], want_config[i]);
483 }
484
485 g_file_set_contents(config_path->str,
486 g_key_file_to_data(config, NULL, NULL), -1, &error);
487
488 if (error != NULL) {
489 draw_ext_info(NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_CONFIG,
490 "Could not save settings!");
491 g_warning("Could not save settings: %s", error->message);
492 g_error_free(error);
493 }
494 }
495
config_init(GtkWidget * window_root)496 void config_init(GtkWidget *window_root) {
497 config_dialog =
498 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_dialog"));
499
500 // Initialize file choosers and set filename filters.
501 ui_filechooser =
502 GTK_FILE_CHOOSER(gtk_builder_get_object(dialog_xml, "ui_filechooser"));
503 theme_filechooser = GTK_FILE_CHOOSER(
504 gtk_builder_get_object(dialog_xml, "theme_filechooser"));
505
506 GtkFileFilter *ui_filter = gtk_file_filter_new();
507 gtk_file_filter_add_pattern(ui_filter, "*.ui");
508 gtk_file_chooser_set_filter(ui_filechooser, ui_filter);
509
510 config_button_echo =
511 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_echo"));
512 config_button_fasttcp =
513 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_fasttcp"));
514 config_button_timestamp =
515 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_timestamp"));
516 config_button_grad_color =
517 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_grad_color"));
518 config_button_foodbeep =
519 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_foodbeep"));
520 config_button_sound =
521 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_sound"));
522 config_button_cache =
523 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_cache"));
524 config_button_download =
525 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_download"));
526 config_button_fog =
527 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_fog"));
528 config_button_smoothing =
529 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_smoothing"));
530
531 config_combobox_displaymode = GTK_COMBO_BOX(
532 gtk_builder_get_object(dialog_xml, "config_combobox_displaymode"));
533 config_combobox_faceset = GTK_COMBO_BOX_TEXT(
534 gtk_builder_get_object(dialog_xml, "config_combobox_faceset"));
535 config_combobox_lighting = GTK_COMBO_BOX(
536 gtk_builder_get_object(dialog_xml, "config_combobox_lighting"));
537
538 GtkWidget *config_button_close =
539 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "config_button_close"));
540 g_signal_connect(config_button_close, "clicked",
541 G_CALLBACK(on_config_close), NULL);
542 g_signal_connect(config_dialog, "delete_event", G_CALLBACK(on_config_close),
543 NULL);
544
545 // Initialize available rendering modes.
546 GtkListStore *display_list =
547 GTK_LIST_STORE(gtk_combo_box_get_model(config_combobox_displaymode));
548 GtkTreeIter iter;
549 #ifdef HAVE_OPENGL
550 gtk_list_store_append(display_list, &iter);
551 gtk_list_store_set(display_list, &iter, 0, "OpenGL", 1, CFG_DM_OPENGL, -1);
552 #endif
553 #ifdef HAVE_SDL
554 gtk_list_store_append(display_list, &iter);
555 gtk_list_store_set(display_list, &iter, 0, "SDL", 1, CFG_DM_SDL, -1);
556 #endif
557 gtk_list_store_append(display_list, &iter);
558 gtk_list_store_set(display_list, &iter, 0, "Pixmap", 1, CFG_DM_PIXMAP, -1);
559 }
560
561 /**
562 * Removes all the text entries from the combo box. This function is not
563 * available in GTK+2, so implement it ourselves.
564 */
combo_box_text_remove_all(GtkComboBoxText * combo_box)565 static void combo_box_text_remove_all(GtkComboBoxText *combo_box) {
566 int count = gtk_tree_model_iter_n_children(
567 gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box)), NULL);
568 for (int i = 0; i < count; i++) {
569 gtk_combo_box_text_remove(combo_box, 0);
570 }
571 }
572
573 /*
574 * Setup config_dialog sets the buttons, combos, etc, to the state that matches
575 * the want_config[] values.
576 */
setup_config_dialog()577 static void setup_config_dialog() {
578 GtkTreeIter iter;
579
580 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_echo),
581 want_config[CONFIG_ECHO]);
582 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_fasttcp),
583 want_config[CONFIG_FASTTCP]);
584 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_timestamp),
585 want_config[CONFIG_TIMESTAMP]);
586 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_grad_color),
587 want_config[CONFIG_GRAD_COLOR]);
588 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_foodbeep),
589 want_config[CONFIG_FOODBEEP]);
590 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_sound),
591 want_config[CONFIG_SOUND]);
592 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_cache),
593 want_config[CONFIG_CACHE]);
594 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_download),
595 want_config[CONFIG_DOWNLOAD]);
596 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_fog),
597 want_config[CONFIG_FOGWAR]);
598 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(config_button_smoothing),
599 want_config[CONFIG_SMOOTH]);
600
601 // Fill face set combo box with available face sets from the server.
602 combo_box_text_remove_all(config_combobox_faceset);
603 if (face_info.have_faceset_info) {
604 for (int i = 0; i < MAX_FACE_SETS; i++) {
605 const char *name = face_info.facesets[i].fullname;
606 if (name != NULL) {
607 gtk_combo_box_text_append_text(config_combobox_faceset, name);
608 // g_ascii_strcasecmp expects both arguments to be non-null.
609 // It appears to return 0 when one is null, confounding the result with
610 // that of an actual match.
611 if (face_info.want_faceset && !g_ascii_strcasecmp(face_info.want_faceset, name)) {
612 gtk_combo_box_set_active(GTK_COMBO_BOX(config_combobox_faceset), i);
613 }
614 } else {
615 break;
616 }
617 }
618 }
619
620 // Set current display mode.
621 GtkTreeModel *model;
622 model = gtk_combo_box_get_model(config_combobox_displaymode);
623 bool next = gtk_tree_model_get_iter_first(model, &iter);
624 while (next) {
625 int current;
626 gtk_tree_model_get(model, &iter, 1, ¤t, -1);
627 if (current == want_config[CONFIG_DISPLAYMODE]) {
628 gtk_combo_box_set_active_iter(config_combobox_displaymode, &iter);
629 break;
630 }
631 next = gtk_tree_model_iter_next(model, &iter);
632 }
633
634 // Lighting option indexes never change, so set option using index.
635 gtk_combo_box_set_active(config_combobox_lighting,
636 want_config[CONFIG_LIGHTING]);
637
638 gtk_file_chooser_set_filename(ui_filechooser, window_xml_file);
639 gtk_file_chooser_set_filename(theme_filechooser, theme);
640 }
641
642 #define IS_DIFFERENT(TYPE) (want_config[TYPE] != use_config[TYPE])
643
644 /**
645 * Get an integer value from 'column' of the active field in 'combobox'.
646 */
combobox_get_value(GtkComboBox * combobox,int column)647 static int combobox_get_value(GtkComboBox *combobox, int column) {
648 GtkTreeModel *model = gtk_combo_box_get_model(combobox);
649 GtkTreeIter iter;
650 int result;
651
652 gtk_combo_box_get_active_iter(combobox, &iter);
653 gtk_tree_model_get(model, &iter, column, &result, -1);
654 return result;
655 }
656
657 /**
658 * This is basically the opposite of setup_config_dialog() above - instead of
659 * setting the display state appropriately, we read the display state and
660 * update the want_config values.
661 */
read_config_dialog(void)662 static void read_config_dialog(void) {
663 want_config[CONFIG_ECHO] =
664 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_echo));
665 want_config[CONFIG_FASTTCP] =
666 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_fasttcp));
667 want_config[CONFIG_TIMESTAMP] = gtk_toggle_button_get_active(
668 GTK_TOGGLE_BUTTON(config_button_timestamp));
669 want_config[CONFIG_GRAD_COLOR] = gtk_toggle_button_get_active(
670 GTK_TOGGLE_BUTTON(config_button_grad_color));
671 want_config[CONFIG_FOODBEEP] =
672 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_foodbeep));
673 want_config[CONFIG_SOUND] =
674 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_sound));
675 want_config[CONFIG_CACHE] =
676 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_cache));
677 want_config[CONFIG_DOWNLOAD] =
678 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_download));
679 want_config[CONFIG_FOGWAR] =
680 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(config_button_fog));
681 want_config[CONFIG_SMOOTH] = gtk_toggle_button_get_active(
682 GTK_TOGGLE_BUTTON(config_button_smoothing));
683
684 gchar *buf = 0;
685 GtkTreeIter iter;
686 /**
687 * Since the combo box does not have the "has-entry" property set to TRUE, we cannot use
688 * gtk_combo_box_text_get_active_text to get the currently selected option.
689 * Since we really have no good reason to turn that on and open up the box for
690 * arbitrary faceset strings, we can treat it more like a regular combo box.
691 * We need to use an iterator retrieval and gtk_tree_model_get to fetch the text,
692 * which is significantly more of a pain in the posterior.
693 *
694 * Daniel Hawkins -- 2020-11-21
695 */
696 if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(config_combobox_faceset), &iter)) {
697 // We have an active selection in our iterator. Now we get the string from the tree model.
698 GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(config_combobox_faceset));
699 gtk_tree_model_get(model, &iter, 0, &buf, -1);
700 if (buf) {
701 free(face_info.want_faceset);
702 face_info.want_faceset = g_strdup(buf);
703 g_free(buf);
704 }
705 else {
706 LOG(LOG_ERROR, "read_config_dialog", "Failed to get face set string from GTK Widget.");
707 }
708 }
709
710 want_config[CONFIG_DISPLAYMODE] =
711 combobox_get_value(config_combobox_displaymode, 1);
712
713 // Lighting option indexes never change, so get option using index.
714 want_config[CONFIG_LIGHTING] =
715 gtk_combo_box_get_active(config_combobox_lighting);
716
717 // Enable darkness if lighting is not 'None'.
718 if (want_config[CONFIG_LIGHTING] != CFG_LT_NONE) {
719 want_config[CONFIG_DARKNESS] = 1;
720 use_config[CONFIG_DARKNESS] = 1;
721 }
722
723 // Set UI file.
724 buf = gtk_file_chooser_get_filename(ui_filechooser);
725 if (buf != NULL) {
726 g_strlcpy(window_xml_file, buf, sizeof(window_xml_file));
727 g_free(buf);
728 }
729
730 // Set and load theme file.
731 buf = gtk_file_chooser_get_filename(theme_filechooser);
732 if (buf != NULL && g_ascii_strcasecmp(buf, theme) != 0) {
733 g_free(theme);
734 theme = buf;
735 load_theme(TRUE);
736 }
737
738 /*
739 * Some values can take effect right now, others not. Code below handles
740 * these cases - largely grabbed from gtk/config.c
741 */
742 if (IS_DIFFERENT(CONFIG_SOUND)) {
743 int tmp;
744 if (want_config[CONFIG_SOUND]) {
745 tmp = init_sounds();
746 if (csocket.fd) {
747 cs_print_string(csocket.fd, "setup sound %d", tmp >= 0);
748 }
749 } else {
750 if (csocket.fd) {
751 cs_print_string(csocket.fd, "setup sound 0");
752 }
753 }
754 use_config[CONFIG_SOUND] = want_config[CONFIG_SOUND];
755 }
756 if (IS_DIFFERENT(CONFIG_FASTTCP)) {
757 #ifdef TCP_NODELAY
758 #ifndef WIN32
759 // TODO: Merge with setsockopt code from client.c
760 int q = want_config[CONFIG_FASTTCP];
761
762 if (csocket.fd &&
763 setsockopt(csocket.fd, SOL_TCP, TCP_NODELAY, &q, sizeof(q)) == -1) {
764 perror("TCP_NODELAY");
765 }
766 #endif
767 #endif
768 use_config[CONFIG_FASTTCP] = want_config[CONFIG_FASTTCP];
769 }
770
771 if (IS_DIFFERENT(CONFIG_LIGHTING)) {
772 #ifdef HAVE_SDL
773 if (use_config[CONFIG_DISPLAYMODE] == CFG_DM_SDL)
774 /* This is done to make the 'lightmap' in the proper format */
775 {
776 init_SDL(NULL, 1);
777 }
778 #endif
779 }
780 /*
781 * Nothing to do, but we can switch immediately without problems. do force
782 * a redraw
783 */
784 if (IS_DIFFERENT(CONFIG_GRAD_COLOR)) {
785 use_config[CONFIG_GRAD_COLOR] = want_config[CONFIG_GRAD_COLOR];
786 draw_stats(TRUE);
787 }
788 }
789
on_configure_activate(GtkWidget * menuitem,gpointer user_data)790 void on_configure_activate(GtkWidget *menuitem, gpointer user_data) {
791 gtk_widget_show(config_dialog);
792 setup_config_dialog();
793 }
794
on_config_close(GtkButton * button,gpointer user_data)795 static void on_config_close(GtkButton *button, gpointer user_data) {
796 read_config_dialog();
797 save_defaults();
798 gtk_widget_hide(config_dialog);
799 }
800
801 /**
802 * Save client window positions to a file unique to each layout.
803 */
save_winpos()804 void save_winpos() {
805 GSList *pane_list, *list_loop;
806 int x, y, w, h, wx, wy;
807
808 /* Save window position and size. */
809 get_window_coord(window_root, &x, &y, &wx, &wy, &w, &h);
810
811 GString *window_root_info = g_string_new(NULL);
812 g_string_printf(window_root_info, "+%d+%dx%dx%d", wx, wy, w, h);
813
814 g_key_file_set_string(config, ui_name(),
815 "window_root", window_root_info->str);
816 g_string_free(window_root_info, TRUE);
817
818 /* Save the positions of all the HPANEDs and VPANEDs. */
819 pane_list = gtk_builder_get_objects(window_xml);
820
821 for (list_loop = pane_list; list_loop != NULL; list_loop = list_loop->next) {
822 GType type = G_OBJECT_TYPE(list_loop->data);
823
824 if (type == GTK_TYPE_HPANED || type == GTK_TYPE_VPANED) {
825 g_key_file_set_integer(config, ui_name(),
826 gtk_buildable_get_name(list_loop->data),
827 gtk_paned_get_position(GTK_PANED(list_loop->data)));
828 }
829 }
830
831 g_slist_free(pane_list);
832 save_defaults();
833
834 draw_ext_info(NDI_BLUE, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_CONFIG,
835 "Window positions saved!");
836 }
837
838 /**
839 * Handles saving of the window positions when the Client | Save Window
840 * Position menu item is activated.
841 *
842 * @param menuitem
843 * @param user_data
844 */
on_save_window_position_activate(GtkMenuItem * menuitem,gpointer user_data)845 void on_save_window_position_activate(GtkMenuItem *menuitem,
846 gpointer user_data) {
847 save_winpos();
848 /*
849 * The following prevents multiple saves per menu activation.
850 */
851 g_signal_stop_emission_by_name(GTK_WIDGET(menuitem), "activate");
852 }
853
854 /**
855 * Resize the client window and its panels using saved window positions.
856 *
857 * @param window_root The client's main window.
858 */
load_window_positions(GtkWidget * window_root)859 void load_window_positions(GtkWidget *window_root) {
860 GSList *pane_list, *list;
861 pane_list = gtk_builder_get_objects(window_xml);
862
863 // Load and set main window dimensions.
864 gchar *root_size = g_key_file_get_string(config, ui_name(),
865 "window_root", NULL);
866
867 if (root_size != NULL) {
868 int w, h;
869
870 if (sscanf(root_size, "+%*d+%*dx%dx%d", &w, &h) == 2) {
871 gtk_window_set_default_size(GTK_WINDOW(window_root), w, h);
872 }
873
874 g_free(root_size);
875 }
876
877 // Load and set panel positions.
878 for (list = pane_list; list != NULL; list = list->next) {
879 GType type = G_OBJECT_TYPE(list->data);
880
881 if (type == GTK_TYPE_HPANED || type == GTK_TYPE_VPANED) {
882 int position = g_key_file_get_integer(config, ui_name(),
883 gtk_buildable_get_name(list->data), NULL);
884
885 if (position != 0) {
886 gtk_paned_set_position(GTK_PANED(list->data), position);
887 }
888 }
889 }
890
891 g_slist_free(pane_list);
892 }
893