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, &current, -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