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, see the
9 * 'LICENSE' and 'COPYING' files.
10 *
11 * The authors can be reached via e-mail to crossfire-devel@real-time.com
12 */
13
14 /**
15 * @file
16 * Text-drawing functions for the message window
17 */
18
19 #include "client.h"
20
21 #include <gtk/gtk.h>
22
23 #include "image.h"
24 #include "main.h"
25 #include "gtk2proto.h"
26
27 /** Color names set by the user in the gtkrc file. */
28 static const char *const usercolorname[NUM_COLORS] = {
29 "black", /* 0 */
30 "white", /* 1 */
31 "darkblue", /* 2 */
32 "red", /* 3 */
33 "orange", /* 4 */
34 "lightblue", /* 5 */
35 "darkorange", /* 6 */
36 "green", /* 7 */
37 "darkgreen", /* 8 *//* Used for window background color */
38 "grey", /* 9 */
39 "brown", /* 10 */
40 "yellow", /* 11 */
41 "tan" /* 12 */
42 };
43
44 /**
45 * A mapping of font numbers to style based on the rcfile content.
46 */
47 static const char *font_style_names[NUM_FONTS] = {
48 "info_font_normal",
49 "info_font_arcane",
50 "info_font_strange",
51 "info_font_fixed",
52 "info_font_hand"
53 };
54 /**
55 * @} EndOf GTK V2 Font Style Definitions.
56 */
57
58 /**
59 * The number of supported message panes (normal + critical). This define is
60 * meant to support anything that iterates over all the information panels.
61 * It does nothing to help remove or document hardcoded panel numbers
62 * throughout the code.
63 *
64 * @todo Create defines for each panel and replace panel numbers with the
65 * defines describing the panel. This integer declaration is to that
66 * account.c knows how many are being used here, and can add appropriately.
67 */
68 #define NUM_TEXT_VIEWS 2
69
70 extern const char * const usercolorname[NUM_COLORS];
71
72 Info_Pane info_pane[NUM_TEXT_VIEWS];
73
74 extern const char * const colorname[NUM_COLORS];
75
76 /*
77 * The idea behind the msg_type_names is to provide meaningful names that the
78 * client can use to load/save these values, in particular, the GTK-V2 client
79 * uses these to find styles on how to draw the different msg types. We could
80 * set this up as a two dimension array instead, but that probably is not as
81 * efficient when the number of subtypes varies so wildly. The 0 subtypes are
82 * used for general cases (describe the entire class of those message types).
83 * Note also that the names here are verbose - the actual code that uses these
84 * will expand it further. In practice, there should never be entries with
85 * both the same type/subtype (each subtype should be unique) - if so, the
86 * results are probably unpredictable on which one the code would use.
87 */
88 #include "msgtypes.h"
89
90 static int max_subtype=0, has_style=0;
91
92 /**
93 * @{
94 * @name GTK V2 Message Control System.
95 * Supports a client-side implementation of what used to be provided by the
96 * server output-count and output-sync commands. These defines presently
97 * control the way the system works. The hardcoded values here are temporary
98 * and shall give way to client commands and/or a GUI method of configuring
99 * the system. Turn off the output count/sync by setting MESSAGE_COUNT_MAX
100 * to 1. It should be safe to experiment with most values as long as none of
101 * them are set less than 1, and as long as the two buffer sizes are set to
102 * reasonable values (buffer sizes include the terminating null character).
103 */
104
105 static void
106 message_callback(int orig_color, int type, int subtype, char *message);
107
108 GtkWidget *msgctrl_window; /**< The message control dialog
109 * where routing and buffer
110 * configuration is set up.
111 */
112 GtkWidget *msgctrl_table; /**< The message control table
113 * where routing and buffer
114 * configuration is set up.
115 */
116 #define MESSAGE_BUFFER_COUNT 10 /**< The maximum number of messages
117 * to concurrently monitor for
118 * duplicate occurances.
119 */
120 #define MESSAGE_BUFFER_SIZE 56 /**< The maximum allowable size of
121 * messages that are checked for
122 * duplicate reduction.
123 */
124 #define COUNT_BUFFER_SIZE 16 /**< The maximum size of the tag
125 * that indicates the number of
126 * times a message occured while
127 * buffered. Example: "4 times "
128 */
129 #define MESSAGE_COUNT_MAX 16 /**< The maximum number of times a
130 * buffered message may repeat
131 * before it is sent to a client
132 * panel for for display.
133 */
134 #define MESSAGE_AGE_MAX 16 /**< The maximum time in client
135 * ticks, that a message resides in
136 * a buffer before it is sent to a
137 * client panel for display. 8
138 * ticks is roughly 1 second.
139 */
140 /** @struct info_buffer_t
141 * @brief A buffer record that supports suppression of duplicate messages.
142 * This buffer holds data for messages that are monitored for suppression of
143 * duplicates. The buffer holds all data passed to message_callback(),
144 * including type, subtype, suggested color, and the text. Age and count
145 * fields are provided to track the time a message is in the buffer, and how
146 * many times it occured during the time it is buffered.
147 */
148 struct info_buffer_t {
149 int age; /**< The length of time a message
150 * spends in the buffer, measured in
151 * client ticks.
152 */
153 int count; /**< The number of times a buffered
154 * message is detected while it is
155 * buffered. A count of -1
156 * indicates the buffer is empty.
157 */
158 int orig_color; /**< Message data: The suggested
159 * color to display the text in.
160 */
161 int type; /**< Message data: Classification
162 * of the buffered message.
163 */
164 int subtype; /**< Message data: Sub-class of
165 * the buffered message.
166 */
167 char message[MESSAGE_BUFFER_SIZE]; /**< Message data: Message text.
168 */
169 } info_buffer[MESSAGE_BUFFER_COUNT]; /**< Several buffers that support
170 * suppression of duplicates even
171 * even when the duplicates are
172 * alternate with other messages.
173 */
174 /** @struct buffer_parameter_t
175 * @brief A container for a single buffer control parameter like output count
176 * or time. The structure holds a widget pointer, a state variable to track
177 * the widget value, and a default value.
178 */
179 typedef struct {
180 GtkWidget* ptr; /**< Spinbutton widget pointer.
181 */
182 guint state; /**< The state of the spinbutton.
183 */
184 const guint default_state; /**< The state of the spinbutton.
185 */
186 } buffer_parameter_t;
187
188 /** @struct buffer_control_t
189 * @brief A container for all of the buffer control parameters like output
190 * count and time. The structure holds widget pointers, a state variables to
191 * track the parameter values, and the client built-in defaults. Only the
192 * final initializer for output_count and output_time is used as a default.
193 */
194 struct buffer_control_t {
195 buffer_parameter_t count; /**< Output count control & default */
196 buffer_parameter_t timer; /**< Output time control & default */
197 } buffer_control = {
198 /*
199 * { uninitialized_pointer, uninitialized_state, default_value },
200 */
201 { NULL, 0, MESSAGE_COUNT_MAX },
202 { NULL, 0, MESSAGE_AGE_MAX }
203 };
204 /** @struct boolean_widget_t
205 * @brief A container that holds the pointer and state of a checkbox control.
206 * Each Message Control dialog checkbox is tracked in one of these structs.
207 */
208 typedef struct {
209 GtkWidget* ptr; /**< Checkbox widget pointer.
210 */
211 gboolean state; /**< The state of the checkbox.
212 */
213 } boolean_widget_t;
214
215 /** @struct message_control_t
216 * @brief A container for all of the checkboxes associated with a single
217 * message type.
218 */
219 typedef struct {
220 boolean_widget_t buffer; /**< Checkbox widget and state for a
221 * single message type.
222 */
223 boolean_widget_t pane[NUM_TEXT_VIEWS];/**< Checkbox widgets and state for
224 * each client-supported message
225 * panel.
226 */
227 } message_control_t;
228
229 message_control_t
230 msgctrl_widgets[MSG_TYPE_LAST-1]; /**< All of the checkbox widgets for
231 * the entire message control
232 * dialog.
233 */
234 /** @struct msgctrl_data_t
235 * @brief Descriptive message type names with pane routing and buffer enable.
236 * A single struct defines a hard-coded, player-friendly, descriptive name to
237 * use for a single message type. All other fields in the structure define
238 * routing of messages to either or both client message panels, and whether
239 * or not messages of this type are passed through the duplicate suppression
240 * buffer system. This struct is intended to be used as the base type of an
241 * array that contains one struct per message type defined in newclient.h.
242 * The hard-coding of the descriptive name for the message type here is not
243 * ideal as it would be nicer to have it alongside the MSG_TYPE_* defines.
244 */
245 struct msgctrl_data_t {
246 const char * description; /**< A descriptive name to give to
247 * a message type when displaying it
248 * for a player. These values
249 * should be kept in sync with the
250 * MSG_TYPE_* declarations in
251 * ../../common/shared/newclient.h
252 */
253 const gboolean buffer; /**< Whether or not to consider the
254 * message type for output-count
255 * buffering. 0/1 == disable/enable
256 * duplicate suppression
257 * (output-count).
258 */
259 const gboolean pane[NUM_TEXT_VIEWS]; /**< The routing instructions for a
260 * single message type. For each
261 * pane, 0/1 == disable/enable
262 * display of the message type in
263 * the associated client message
264 * pane.
265 */
266 } msgctrl_defaults[MSG_TYPE_LAST-1] = /**< A data structure to track how
267 * to handle each message type in
268 * with respect to panel routing and
269 * output count.
270 */
271
272 {
273 /*
274 * { "description", buffer, { pane[0], pane[1] } },
275 */
276 { "Books", FALSE, { TRUE, FALSE } },
277 { "Cards", FALSE, { TRUE, FALSE } },
278 { "Paper", FALSE, { TRUE, FALSE } },
279 { "Signs & Magic Mouths", FALSE, { TRUE, FALSE } },
280 { "Monuments", FALSE, { TRUE, FALSE } },
281 { "Dialogs (Altar/NPC/Magic Ear)" , FALSE, { TRUE, FALSE } },
282 { "Message of the day", FALSE, { TRUE, FALSE } },
283 { "Administrative", FALSE, { TRUE, FALSE } },
284 { "Shops", TRUE, { TRUE, FALSE } },
285 { "Command responses", FALSE, { TRUE, FALSE } },
286 { "Changes to attributes", TRUE, { TRUE, TRUE } },
287 { "Skill-related messages", TRUE, { TRUE, FALSE } },
288 { "Apply results", TRUE, { TRUE, FALSE } },
289 { "Attack results", TRUE, { TRUE, FALSE } },
290 { "Player communication", FALSE, { TRUE, TRUE } },
291 { "Spell results", TRUE, { TRUE, FALSE } },
292 { "Item information", TRUE, { TRUE, FALSE } },
293 { "Miscellaneous", TRUE, { TRUE, FALSE } },
294 { "Victim notification", FALSE, { TRUE, TRUE } },
295 { "Client-generated messages", FALSE, { TRUE, FALSE } }
296 };
297 /**
298 * @} EndOf GTK V2 Message Control System.
299 */
300
301 /**
302 * Sets attributes in the text tag from a style. Best I can gather, there is
303 * no way to take all of the attributes from a style and apply them directly to
304 * a text tag, hence this function to do the work. GtkTextTags also know what
305 * attributes are set and which are not set - thus, you can apply multiple tags
306 * to the same text, and get all of the effects. For styles, that isn't the
307 * case - a style contains all of the information. So this function also
308 * compares the loaded style from the base style, and only sets the attributes
309 * that are different.
310 *
311 * @param tag Text tag to set values on.
312 * @param style Style name to get values from.
313 * @param base_style Base style for the widget to compare against.
314 */
set_text_tag_from_style(GtkTextTag * tag,GtkStyle * style,const GtkStyle * const base_style)315 void set_text_tag_from_style(GtkTextTag *tag, GtkStyle *style, const GtkStyle * const base_style)
316 {
317 g_object_set(tag, "foreground-set", FALSE, NULL);
318 g_object_set(tag, "background-set", FALSE, NULL);
319 g_object_set(tag, "font-desc", NULL, NULL);
320
321 if (memcmp(
322 &style->fg[GTK_STATE_NORMAL],
323 &base_style->fg[GTK_STATE_NORMAL],
324 sizeof(GdkColor)))
325
326 {
327 g_object_set(tag, "foreground-gdk", &style->fg[GTK_STATE_NORMAL], NULL);
328 }
329
330 if (memcmp(
331 &style->bg[GTK_STATE_NORMAL],
332 &base_style->bg[GTK_STATE_NORMAL],
333 sizeof(GdkColor)))
334
335 {
336 g_object_set(tag, "background-gdk", &style->bg[GTK_STATE_NORMAL], NULL);
337 }
338
339 if (style->font_desc != base_style->font_desc) {
340 g_object_set(tag, "font-desc", style->font_desc, NULL);
341 }
342 }
343
344 /**
345 * Adds the various tags to the next buffer. If textbuf is non-null, then it
346 * also sets the text buffer for that pane to textbuf. This is called right
347 * now by info_get_styles() below and from within the account code.
348 *
349 * @param pane Message panel number to add buffer to.
350 * @param textbuf Text buffer to apply tags to. It is allowed to be null if
351 * info_pane[pane].textbuffer has already been set.
352 */
add_tags_to_textbuffer(Info_Pane * pane,GtkTextBuffer * textbuf)353 void add_tags_to_textbuffer(Info_Pane *pane, GtkTextBuffer *textbuf)
354 {
355 int i;
356
357 if (textbuf) {
358 pane->textbuffer = textbuf;
359 }
360
361 for (i = 0; i < MSG_TYPE_LAST; i++)
362 pane->msg_type_tags[i] =
363 calloc(max_subtype + 1, sizeof(GtkTextTag*));
364
365 for (i = 0; i < NUM_FONTS; i++) {
366 pane->font_tags[i] = NULL;
367 }
368
369 for (i = 0; i < NUM_COLORS; i++) {
370 pane->color_tags[i] = NULL;
371 }
372 /*
373 * These tag definitions never change - we don't get them from the
374 * settings file (maybe we should), so we only need to allocate them once.
375 */
376 pane->bold_tag =
377 gtk_text_buffer_create_tag(pane->textbuffer,
378 "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
379
380 pane->italic_tag =
381 gtk_text_buffer_create_tag(pane->textbuffer,
382 "italic", "style", PANGO_STYLE_ITALIC, NULL);
383
384 pane->underline_tag =
385 gtk_text_buffer_create_tag(pane->textbuffer,
386 "underline", "underline", PANGO_UNDERLINE_SINGLE, NULL);
387 /*
388 * This is really a convenience - so multiple tags may be passed when
389 * drawing text, but once a NULL tag is found, that signifies no more
390 * tags. Rather than having to set up an array to pass in, instead, an
391 * empty tag is passed in so that calling semantics remain consistent,
392 * just differing in what tags are passed in.
393 */
394 if (!pane->default_tag)
395 pane->default_tag =
396 gtk_text_buffer_create_tag(pane->textbuffer, "default", NULL);
397 }
398
399 /**
400 * This is like add_tags_to_textbuffer above, but styles can be changed during
401 * the run of the client. So this has to be separate to note it it might be a
402 * reload.
403 *
404 * @param pane Message panel number to update.
405 * @param base_style Base style if retrieved - may be null.
406 */
add_style_to_textbuffer(Info_Pane * pane,GtkStyle * base_style)407 void add_style_to_textbuffer(Info_Pane *pane, GtkStyle *base_style)
408 {
409 int i;
410 char style_name[MAX_BUF];
411 GtkStyle *tmp_style;
412
413 if (base_style) {
414 /*
415 * Old message/color support.
416 */
417 for (i = 0; i < NUM_COLORS; i++) {
418 snprintf(style_name, MAX_BUF, "info_%s", usercolorname[i]);
419
420 tmp_style =
421 gtk_rc_get_style_by_paths(
422 gtk_settings_get_default(), NULL, style_name, G_TYPE_NONE);
423
424 if (tmp_style) {
425 if (!pane->color_tags[i]) {
426 pane->color_tags[i] =
427 gtk_text_buffer_create_tag(
428 pane->textbuffer, NULL, NULL);
429 }
430 set_text_tag_from_style(
431 pane->color_tags[i], tmp_style, base_style);
432 } else {
433 if (pane->color_tags[i]) {
434 gtk_text_tag_table_remove(
435 gtk_text_buffer_get_tag_table(
436 pane->textbuffer), pane->color_tags[i]);
437 pane->color_tags[i] = NULL;
438 }
439 }
440 }
441
442 /* Font type support */
443 for (i = 0; i < NUM_FONTS; i++) {
444 tmp_style =
445 gtk_rc_get_style_by_paths(
446 gtk_settings_get_default(),
447 NULL, font_style_names[i], G_TYPE_NONE);
448
449 if (tmp_style) {
450 if (!pane->font_tags[i]) {
451 pane->font_tags[i] =
452 gtk_text_buffer_create_tag(
453 pane->textbuffer, NULL, NULL);
454 }
455 set_text_tag_from_style(
456 pane->font_tags[i], tmp_style, base_style);
457 } else {
458 if (pane->font_tags[i]) {
459 gtk_text_tag_table_remove(
460 gtk_text_buffer_get_tag_table(pane->textbuffer),
461 pane->font_tags[i]);
462 pane->font_tags[i] = NULL;
463 }
464 }
465 }
466 } else {
467
468 for (i = 0; i < NUM_COLORS; i++) {
469 if (pane->color_tags[i]) {
470 gtk_text_tag_table_remove(
471 gtk_text_buffer_get_tag_table(
472 pane->textbuffer), pane->color_tags[i]);
473 pane->color_tags[i] = NULL;
474 }
475 }
476 /* Font type support */
477 for (i = 0; i < NUM_FONTS; i++) {
478 if (pane->font_tags[i]) {
479 gtk_text_tag_table_remove(
480 gtk_text_buffer_get_tag_table(
481 pane->textbuffer), pane->font_tags[i]);
482 pane->font_tags[i] = NULL;
483 }
484 }
485 }
486 }
487
488 /**
489 * Loads up values from the style file. Note that the actual name of the
490 * style file is set elsewhere.
491 *
492 * This function is designed so that it should be possible to call it multiple
493 * times - it will release old style data and load up new values. In this
494 * way, a user should be able to change styles on the fly and have things
495 * work.
496 */
info_get_styles(void)497 void info_get_styles(void)
498 {
499 unsigned int i, j;
500 static int has_init=0;
501 GtkStyle *tmp_style, *base_style;
502
503 if (!has_init) {
504 /*
505 * We want to set up a 2 dimensional array of msg_type_tags to
506 * correspond to all the types/subtypes, so looking up any value is
507 * really easy. We know the size of the types, but don't know the
508 * number of subtypes - no single declared value. So we just parse
509 * the msg_type_names to find that, then know how big to make the
510 * other dimension. We could allocate different number of entries for
511 * each type, but that makes processing a bit harder (no single value
512 * on the number of subtypes), and this extra memory usage shouldn't
513 * really be at all significant.
514 */
515 for (i = 0; i < sizeof(msg_type_names) / sizeof(Msg_Type_Names); i++) {
516 if (msg_type_names[i].subtype > max_subtype) {
517 max_subtype = msg_type_names[i].subtype;
518 }
519 }
520 for (j = 0; j < NUM_TEXT_VIEWS; j++) {
521 add_tags_to_textbuffer(&info_pane[j], NULL);
522
523 }
524 has_init = 1;
525 }
526 base_style = gtk_rc_get_style_by_paths(gtk_settings_get_default(), NULL,
527 "info_default", G_TYPE_NONE);
528 if (!base_style) {
529 LOG(LOG_INFO, "info.c::info_get_styles",
530 "Unable to find base style info_default"
531 " - will not process most info tag styles!");
532 }
533
534 has_style = 0;
535
536 /*
537 * If we don't have a base style tag, we can't process these other tags,
538 * as we need to be able to do a difference, and doing a difference from
539 * nothing (meaning, taking everything in style) still doesn't work really
540 * well.
541 */
542 if (base_style) {
543 /*
544 * This processes the type/subtype styles. We look up the names in
545 * the array to find what name goes to what number.
546 */
547 for (i = 0; i < sizeof(msg_type_names) / sizeof(Msg_Type_Names); i++) {
548 int type, subtype;
549
550 char style_name[MAX_BUF];
551 snprintf(style_name, sizeof(style_name),
552 "msg_%s", msg_type_names[i].style_name);
553 type = msg_type_names[i].type;
554 subtype = msg_type_names[i].subtype;
555
556 tmp_style =
557 gtk_rc_get_style_by_paths(
558 gtk_settings_get_default(), NULL, style_name, G_TYPE_NONE);
559
560 for (j = 0; j < NUM_TEXT_VIEWS; j++) {
561 /*
562 * If we have a style for this, update the tag that goes along
563 * with this. If we don't have a tag for this style, create
564 * it.
565 */
566 if (tmp_style) {
567 if (!info_pane[j].msg_type_tags[type][subtype]) {
568 info_pane[j].msg_type_tags[type][subtype] =
569 gtk_text_buffer_create_tag(
570 info_pane[j].textbuffer, NULL, NULL);
571 }
572 set_text_tag_from_style(
573 info_pane[j].msg_type_tags[type][subtype],
574 tmp_style, base_style);
575 has_style = 1;
576 } else {
577 /*
578 * No setting for this type/subtype, so remove tag if
579 * there is one.
580 */
581 if (info_pane[j].msg_type_tags[type][subtype]) {
582 gtk_text_tag_table_remove(
583 gtk_text_buffer_get_tag_table(
584 info_pane[j].textbuffer),
585 info_pane[j].msg_type_tags[type][subtype]);
586 info_pane[j].msg_type_tags[type][subtype] = NULL;
587 }
588 }
589 }
590 }
591 for (j = 0; j < NUM_TEXT_VIEWS; j++) {
592 add_style_to_textbuffer(&info_pane[j], base_style);
593 }
594 } else {
595 /*
596 * There is no base style - this should not normally be the case with
597 * any real setting files, but certainly can be the case if the user
598 * selected the 'None' setting. So in this case, we just free all the
599 * text tags.
600 */
601 has_style = 0;
602 for (i = 0; i < sizeof(msg_type_names) / sizeof(Msg_Type_Names); i++) {
603 int type, subtype;
604
605 type = msg_type_names[i].type;
606 subtype = msg_type_names[i].subtype;
607
608 for (j = 0; j < NUM_TEXT_VIEWS; j++) {
609 if (info_pane[j].msg_type_tags[type][subtype]) {
610 gtk_text_tag_table_remove(
611 gtk_text_buffer_get_tag_table(
612 info_pane[j].textbuffer),
613 info_pane[j].msg_type_tags[type][subtype]);
614 info_pane[j].msg_type_tags[type][subtype] = NULL;
615 }
616 }
617 }
618 for (j = 0; j < NUM_TEXT_VIEWS; j++) {
619 add_style_to_textbuffer(&info_pane[j], NULL);
620 }
621 }
622 }
623
624 /**
625 * Initialize the information panels in the client. These panels are the
626 * client areas where text is drawn.
627 *
628 * @param window_root Pointer to the parent (root) application window.
629 */
info_init(GtkWidget * window_root)630 void info_init(GtkWidget *window_root)
631 {
632 int i;
633 GtkTextIter end;
634 char widget_name[MAX_BUF];
635
636 for (i = 0; i < NUM_TEXT_VIEWS; i++) {
637 snprintf(widget_name, MAX_BUF, "textview_info%d", i+1);
638 info_pane[i].textview =
639 GTK_WIDGET(gtk_builder_get_object(window_xml, widget_name));
640
641 snprintf(widget_name, MAX_BUF, "scrolledwindow_textview%d", i+1);
642
643 info_pane[i].scrolled_window =
644 GTK_WIDGET(gtk_builder_get_object(window_xml, widget_name));
645
646 gtk_text_view_set_wrap_mode(
647 GTK_TEXT_VIEW(info_pane[i].textview), GTK_WRAP_WORD_CHAR);
648
649 info_pane[i].textbuffer =
650 gtk_text_view_get_buffer(GTK_TEXT_VIEW(info_pane[i].textview));
651
652 info_pane[i].adjustment =
653 gtk_scrolled_window_get_vadjustment(
654 GTK_SCROLLED_WINDOW(info_pane[i].scrolled_window));
655
656 gtk_text_buffer_get_end_iter(info_pane[i].textbuffer, &end);
657
658 info_pane[i].textmark =
659 gtk_text_buffer_create_mark(
660 info_pane[i].textbuffer, NULL, &end, FALSE);
661
662 gtk_widget_realize(info_pane[i].textview);
663 }
664
665 /*info_get_styles();*/
666 info_buffer_init();
667
668 /* Register callbacks for all message types */
669 for (i = 0; i < MSG_TYPE_LAST; i++) {
670 setTextManager(i, message_callback);
671 }
672 }
673
674 /**
675 * Adds some data to the text buffer of the specified information panel using
676 * the appropriate tags to provide the desired formatting. Note that the
677 * style within the users theme determines how a particular type/subtype is
678 * drawn.
679 *
680 * @param pane The client message panel to write a message to.
681 * @param message A pointer to some text to show in a client message window.
682 * @param type The message type - see the MSG_TYPE values in newclient.h.
683 * @param subtype Message subtype - see MSG_TYPE_*_* values in newclient.h.
684 * @param bold If true, should be in bold text.
685 * @param italic If true, should be in italic text
686 * @param font Which font to use - resolved to an actual font style based
687 * on the user's theme file.
688 * @param color String name of the color.
689 * @param underline If true, draw underlined text.
690 */
add_to_textbuf(Info_Pane * pane,const char * message,int type,int subtype,int bold,int italic,int font,const char * color,int underline)691 static void add_to_textbuf(Info_Pane *pane, const char *message,
692 int type, int subtype,
693 int bold, int italic,
694 int font, const char *color, int underline)
695 {
696 GtkTextIter end;
697 GdkRectangle rect;
698 int scroll_to_end=0, color_num;
699 GtkTextTag *color_tag=NULL, *type_tag=NULL;
700
701 /*
702 * Lets see if the defined color matches any of our defined colors. If we
703 * get a match, set color_tag. If color_tag is null, we either don't have
704 * a match, we don't have a defined tag for the color, or we don't have a
705 * color, use the default tag. It would be nice to know if color is a sub
706 * value set with [color] tag, or is part of the message itself - if we're
707 * just being passed NDI_RED in the draw_ext_info from the server, we
708 * really don't care about that - the type/subtype styles should really be
709 * what determines what color to use.
710 */
711 if (color) {
712 for (color_num = 0; color_num < NUM_COLORS; color_num++)
713 if (!g_ascii_strcasecmp(usercolorname[color_num], color)) {
714 break;
715 }
716 if (color_num < NUM_COLORS) {
717 color_tag = pane->color_tags[color_num];
718 }
719 }
720
721 if (!color_tag) {
722 color_tag = pane->default_tag;
723 }
724
725 /*
726 * Following block of code deals with the type/subtype. First, we check
727 * and make sure the passed in values are legal. If so, first see if
728 * there is a particular style for the type/subtype combo, if not, fall
729 * back to one just for the type.
730 */
731 type_tag = pane->default_tag;
732
733 /* Clear subtype on MSG_TYPE_CLIENT if max_subtype is not set
734 * Errors are generated during initialization, before max_subtype
735 * has been set, so we can not route to a specific pane.
736 * We also want to make sure we do not hit the pane->msg_type_tags
737 * code, as that is not initialzed yet.
738 */
739 if (type == MSG_TYPE_CLIENT && !max_subtype) {
740 /* We would set subtype = 0, but that'd be a dead assignment. */
741 } else if (type >= MSG_TYPE_LAST
742 || subtype >= max_subtype
743 || type < 0 || subtype < 0 ) {
744 LOG(LOG_ERROR, "info.c::add_to_textbuf",
745 "type (%d) >= MSG_TYPE_LAST (%d) or "
746 "subtype (%d) >= max_subtype (%d)\n",
747 type, MSG_TYPE_LAST, subtype, max_subtype);
748 } else {
749 if (pane->msg_type_tags[type][subtype]) {
750 type_tag = pane->msg_type_tags[type][subtype];
751 } else if (pane->msg_type_tags[type][0]) {
752 type_tag = pane->msg_type_tags[type][0];
753 }
754 }
755
756 gtk_text_view_get_visible_rect(
757 GTK_TEXT_VIEW(pane->textview), &rect);
758
759 /* Simple panes (like those of the login windows) don't have adjustments
760 * set (and if they did, we wouldn't want to scroll to end in any case),
761 * so check here on what to do.
762 */
763 if (pane->adjustment &&
764 (gtk_adjustment_get_value(pane->adjustment) + rect.height) >=
765 gtk_adjustment_get_upper(pane->adjustment)) {
766 scroll_to_end = 1;
767 }
768
769 gtk_text_buffer_get_end_iter(pane->textbuffer, &end);
770
771 gtk_text_buffer_insert_with_tags(
772 pane->textbuffer, &end, message, strlen(message),
773 bold ? pane->bold_tag : pane->default_tag,
774 italic ? pane->italic_tag : pane->default_tag,
775 underline ? pane->underline_tag : pane->default_tag,
776 pane->font_tags[font] ?
777 pane->font_tags[font] : pane->default_tag,
778 color_tag, type_tag, NULL);
779
780 if (scroll_to_end)
781 gtk_text_view_scroll_mark_onscreen(
782 GTK_TEXT_VIEW(pane->textview), pane->textmark);
783 }
784
785 /**
786 * This just does the work of taking text (which may have markup) and putting
787 * it into the target pane. This is a lower level than the draw_ext_info()
788 * below, as it does not do message routing. This is called from
789 * draw_ext_info() below, as well as account.c to update news/motd/rules.
790 *
791 * @param pane Pointer to the pane info to draw info.
792 * @param message Message that is parsed and displayed.
793 * @param type Type of the message - for default coloring information.
794 * @param subtype Subtype of message - used for default coloring information.
795 * @param orig_color Legacy color hint not based on type that is used when a
796 * theme does not define a style for the message.
797 * @note Note both type and subtype are the values passed to draw_ext_info().
798 */
799
add_marked_text_to_pane(Info_Pane * pane,const char * message,int type,int subtype,int orig_color)800 void add_marked_text_to_pane(Info_Pane *pane, const char *message, int type, int subtype, int orig_color)
801 {
802 char *marker, *current, *original;
803 int bold=0, italic=0, font=0, underline=0;
804 const char *color=NULL; /**< Only if we get a [color] tag should we care,
805 * otherwise, the type/subtype should dictate
806 * color (unless no style set!)
807 */
808
809 current = g_strdup(message);
810 original = current; /* Just so we know what to free */
811
812 /*
813 * If there is no style information, or if a specific style has not been
814 * set for the type/subtype of this message, allow orig_color to set the
815 * color of the text. The orig_color handling here adds compatibility
816 * with former draw_info() calls that gave a color hint. The color hint
817 * still works now in the event that the theme has not set a style for the
818 * message type.
819 */
820 if (! has_style || pane->msg_type_tags[type][subtype] == 0) {
821 if (orig_color <0 || orig_color>NUM_COLORS) {
822 LOG(LOG_ERROR, "info.c::draw_ext_info",
823 "Passed invalid color from server: %d, max allowed is %d\n",
824 orig_color, NUM_COLORS);
825 } else {
826 /*
827 * Not efficient - we have a number, but convert it to a string,
828 * at which point add_to_textbuf() converts it back to a number.
829 */
830 color = usercolorname[orig_color];
831 }
832 }
833
834 while ((marker = strchr(current, '[')) != NULL) {
835 *marker = 0;
836
837 if (strlen(current) > 0)
838 add_to_textbuf(pane, current, type, subtype,
839 bold, italic, font, color, underline);
840
841 current = marker + 1;
842
843 if ((marker = strchr(current, ']')) == NULL) {
844 free(original);
845 return;
846 }
847
848 *marker = 0;
849 if (!strcmp(current, "b")) {
850 bold = TRUE;
851 } else if (!strcmp(current, "/b")) {
852 bold = FALSE;
853 } else if (!strcmp(current, "i")) {
854 italic = TRUE;
855 } else if (!strcmp(current, "/i")) {
856 italic = FALSE;
857 } else if (!strcmp(current, "ul")) {
858 underline = TRUE;
859 } else if (!strcmp(current, "/ul")) {
860 underline = FALSE;
861 } else if (!strcmp(current, "fixed")) {
862 font = FONT_FIXED;
863 } else if (!strcmp(current, "arcane")) {
864 font = FONT_ARCANE;
865 } else if (!strcmp(current, "hand")) {
866 font = FONT_HAND;
867 } else if (!strcmp(current, "strange")) {
868 font = FONT_STRANGE;
869 } else if (!strcmp(current, "print")) {
870 font = FONT_NORMAL;
871 } else if (!strcmp(current, "/color")) {
872 color = NULL;
873 } else if (!strncmp(current, "color=", 6)) {
874 color = current + 6;
875 } else
876 LOG(LOG_INFO, "info.c::message_callback",
877 "unrecognized tag: [%s]\n", current);
878
879 current = marker + 1;
880 }
881
882 add_to_textbuf(
883 pane, current, type, subtype,
884 bold, italic, font, color, underline);
885
886 add_to_textbuf(
887 pane, "\n", type, subtype,
888 bold, italic, font, color, underline);
889
890 free(original);
891
892 }
893
894 /**
895 * A message processor that accepts messages along with meta information color
896 * and type. The message type and subtype are analyzed to select font and
897 * other text attributes. All gtk-v2 client messages pass through this
898 * processor before being output. Before addition of the output buffering
899 * feature, this was the message callback function. It is a separate function
900 * so that it can be called both by the callback, and but buffer maintenance
901 * functions.
902 *
903 * Client-sourced messages generally should be passed directly to this handler
904 * instead of to the callback. This will save some overhead as the callback
905 * implements a system that coalesces duplicate messages - a feature that is
906 * not really applicable to most messages that do not come from the server.
907 *
908 * @param orig_color Suggested text color that type/subtype can over-ride.
909 * @param type Message type. See MSG_TYPE definitions in newclient.h.
910 * @param subtype Message subtype. See MSG_TYPE_*_* values in newclient.h.
911 * @param message The message text to display.
912 */
draw_ext_info(int orig_color,int type,int subtype,const char * message)913 void draw_ext_info(int orig_color, int type, int subtype, const char *message)
914 {
915 int type_err=0; /**< When 0, the type is valid and may be used to pick
916 * the panel routing, otherwise the message can only
917 * go to the main message pane.
918 */
919 int pane;
920 char *stamp = NULL;
921 const char *draw = NULL;
922
923 if (want_config[CONFIG_TIMESTAMP]) {
924 time_t curtime;
925 struct tm *ltime;
926 stamp = calloc(1, strlen(message) + 7);
927 curtime = time(NULL);
928 ltime = localtime(&curtime);
929 strftime(stamp, 6, "%I:%M", ltime);
930 strcat(stamp, " ");
931 strcat(stamp, message);
932 draw = stamp;
933 } else {
934 draw = message;
935 }
936
937 /*
938 * A valid message type is required to index into the msgctrl_widgets
939 * array. If an invalid type is identified, log an error as any message
940 * without a valid type should be hunted down and assigned an appropriate
941 * type.
942 */
943 if ((type < 1) || (type >= MSG_TYPE_LAST)) {
944 LOG(LOG_ERROR, "info.c::draw_ext_info",
945 "Invalid message type: %d", type);
946 type_err = 1;
947 }
948 /*
949 * Route messages to any one of the client information panels based on the
950 * type of the message text. If a message with an invalid type comes in,
951 * it goes to the main message panel (0). Messages can actually be sent
952 * to more than one panel if the player so desires.
953 */
954 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
955 /*
956 * If the message type is invalid, then the message must go to pane 0,
957 * otherwise the msgctrl_widgets[].pane[pane].state setting determines
958 * whether to send the message to a particular pane or not. The type
959 * is one-based, so must be decremented when referencing
960 * msgctrl_widgets[];
961 */
962 if (type_err != 0) {
963 if (pane != 0) {
964 break;
965 }
966 } else {
967 if (msgctrl_widgets[type - 1].pane[pane].state == FALSE) {
968 continue;
969 }
970 }
971 add_marked_text_to_pane(&info_pane[pane], draw, type, subtype, orig_color);
972 }
973
974 if (want_config[CONFIG_TIMESTAMP]) {
975 free(stamp);
976 }
977 }
978
979 /**
980 * @defgroup GTKv2OutputCountSync GTK V2 client output count/sync functions.
981 * @{
982 */
983
984 /**
985 * Output count/sync message buffer initialization to set all buffers empty.
986 * Called only once at client start from info_init(), the function initializes
987 * all message buffers to the empty state (count == -1). At a minimum, age,
988 * count, and message should be initialized. Type, subtype, and orig_color
989 * are also set just for an extra measure of safety.
990 */
info_buffer_init(void)991 void info_buffer_init(void)
992 {
993 int loop;
994
995 for (loop = 0; loop < MESSAGE_BUFFER_COUNT; loop += 1) {
996 info_buffer[loop].count = -1;
997 info_buffer[loop].age = 0;
998 info_buffer[loop].type = 0;
999 info_buffer[loop].subtype = 0;
1000 info_buffer[loop].orig_color = 0;
1001 info_buffer[loop].message[0] = '\0';
1002 };
1003 }
1004
1005 /**
1006 * Handles message buffer flushes, and, as needed, displays the text. Flushed
1007 * buffers have their count set to -1. On flush, the message text is output
1008 * only when the message count is greater than zero. If the message text is
1009 * displayed, and if the count is greater than one, it is prepended to the
1010 * message in the form "N * times ". This function is called whenever a
1011 * message must be ejected from the output count/sync system buffers. Note
1012 * that the message details are preserved when the buffer is flushed. This
1013 * allows the buffer contents to be re-used if another message with the same
1014 * text comes in before the buffer is re-used for a different message.
1015 *
1016 * @param id The message control buffer to flush (0 - MESSAGE_BUFFER_COUNT).
1017 */
info_buffer_flush(const int id)1018 void info_buffer_flush(const int id)
1019 {
1020 char output_buffer[MESSAGE_BUFFER_SIZE /* Buffer for output big enough */
1021 +COUNT_BUFFER_SIZE]; /* to hold both count and text. */
1022 /*
1023 * Messages are output with no output-count at the time they are first
1024 * placed in a buffer, so do not bother displaying it again if another
1025 * instance of the message was not seen after the initial buffering.
1026 */
1027 if (info_buffer[id].count > 0) {
1028 /*
1029 * Report the number of times the message was seen only if it was seen
1030 * after having been initially buffered.
1031 */
1032 if (info_buffer[id].count > 1) {
1033 snprintf(output_buffer, sizeof(output_buffer), "%u times %s",
1034 info_buffer[id].count, info_buffer[id].message);
1035 /*
1036 * Output the message count and message text.
1037 */
1038 draw_ext_info(
1039 info_buffer[id].orig_color,
1040 info_buffer[id].type,
1041 info_buffer[id].subtype,
1042 output_buffer);
1043 } else
1044 /*
1045 * Output only the message text.
1046 */
1047 draw_ext_info(
1048 info_buffer[id].orig_color,
1049 info_buffer[id].type,
1050 info_buffer[id].subtype,
1051 info_buffer[id].message);
1052 };
1053 /*
1054 * Mark the buffer newly emptied.
1055 */
1056 info_buffer[id].count = -1;
1057 }
1058
1059 /**
1060 * Output count/sync buffer maintainer adds buffer time and output messages.
1061 * For every tick, age active messages so it eventually gets displayed. If
1062 * the data in an buffer reaches the maximum permissible age or message
1063 * occurance count, it is ejected and displayed. Inactive buffers are also
1064 * aged so that the oldest empty buffer is used first when a new message
1065 * comes in.
1066 */
info_buffer_tick(void)1067 void info_buffer_tick(void)
1068 {
1069 int loop;
1070
1071 for (loop = 0; loop < MESSAGE_BUFFER_COUNT; loop += 1) {
1072 if (info_buffer[loop].count > -1) {
1073 if ((info_buffer[loop].age < (int) buffer_control.timer.state)
1074 && (info_buffer[loop].count < (int) buffer_control.count.state)) {
1075 /*
1076 * The buffer has data in it, and has not reached maximum age,
1077 * so bump the age up a notch.
1078 */
1079 info_buffer[loop].age += 1;
1080 } else {
1081 /*
1082 * The data has been in the buffer too long, so either display
1083 * it (and report how many times it was seen while in the
1084 * buffer) or simply expire the buffer content if duplicates
1085 * did not occur.
1086 */
1087 info_buffer_flush(loop);
1088 }
1089 } else {
1090 /*
1091 * Overflow-protected aging of empty or inactive buffers. Aging
1092 * of inactive buffers is the reason overflow must be handled.
1093 */
1094 if (info_buffer[loop].age < info_buffer[loop].age + 1) {
1095 info_buffer[loop].age += 1;
1096 }
1097 }
1098 }
1099 }
1100
1101 /**
1102 * A callback to accept messages along with meta information color and type.
1103 * Unlike the GTK V1 client, we don't do anything tricky like popups with
1104 * different message types, but the output-count/sync features do consider
1105 * message type, etc. To allow user-defined buffering rules all messages
1106 * need to pass through a common processor. This callback is the interface
1107 * for the output buffering. Even if output buffering could be bypassed, it
1108 * is still necessary to pass messages through a common interface to handle
1109 * style, theme, and display panel configuration. This callback routes all
1110 * messages to the appropriate handler for pre-display processing
1111 * (draw_ext_info()).
1112 *
1113 * It is recommended that client-sourced messages be passed directly to
1114 * draw_ext_info() instead of through the callback to avoid unnecessary
1115 * processing. MSG_TYPE_CLIENT messages are deliberately not buffered here
1116 * because they are generally unique, adminstrative messages that should not
1117 * be delayed.
1118 *
1119 * @param orig_color Suggested text color that type/subtype can over-ride.
1120 * @param type Message type. See MSG_TYPE definitions in newclient.h.
1121 * @param subtype Message subtype. See MSG_TYPE_*_* values in newclient.h.
1122 * @param message The message text to display.
1123 */
1124 static void
message_callback(int orig_color,int type,int subtype,char * message)1125 message_callback(int orig_color, int type, int subtype, char *message)
1126 {
1127 int search; /* Loop for searching the buffers. */
1128 int found; /* Which buffer a message is in. */
1129 int empty; /* The ID of an empty buffer. */
1130 int oldest; /* Oldest non-empty buffer found. */
1131 int empty_age; /* Age of oldest empty buffer. */
1132 int oldest_age; /* Age of oldest non-empty buffer. */
1133
1134 /*
1135 * Any message that has an invalid type cannot be buffered. An error is
1136 * not logged here as draw_ext_info() is where all messages pass through.
1137 *
1138 * A legacy switch to prevent message folding is to set the color of the
1139 * message to NDI_UNIQUE. This over-rides the player preferences.
1140 *
1141 * Usually msgctrl_widgets[] is used to determine whether or not messages
1142 * are buffered as it is where the player sets buffering preferences. The
1143 * type must be decremented when used to index into msgctrl_widgets[].
1144 *
1145 * The system also declines to buffer messages over a set length as most
1146 * messages that need coalescing are short. Most messages that are long
1147 * are usually unique and should not be delayed. >= allows for the null
1148 * at the end of the string in the buffer. IE. If the buffer size is 40,
1149 * only 39 chars can be put into it to ensure room for a null character.
1150 */
1151 if ((type < 1)
1152 || (type >= MSG_TYPE_LAST)
1153 || (orig_color == NDI_UNIQUE)
1154 || (msgctrl_widgets[type - 1].buffer.state == FALSE)
1155 || (strlen(message) >= MESSAGE_BUFFER_SIZE)) {
1156 /*
1157 * If the message buffering feature is off, simply pass the message on
1158 * to the parser that will determine the panel routing and style.
1159 */
1160 draw_ext_info(orig_color, type, subtype, message);
1161 } else {
1162 empty = -1; /* Default: Buffers are empty until proven full */
1163 found = -1; /* Default: Incoming message is not in a buffer */
1164 oldest = -1; /* Default: Oldest active buffer ID is unknown */
1165 empty_age= -1; /* Default: Oldest empty buffer age is unknown */
1166 oldest_age= -1; /* Default: Oldest active buffer age is unknown */
1167
1168 for (search = 0; search < MESSAGE_BUFFER_COUNT; search += 1) {
1169 /*
1170 * 1) Find the oldest empty or inactive buffer, if one exists.
1171 * 2) Find the oldest non-empty/active buffer in case we need to
1172 * eject a message to make room for a new message.
1173 * 3) Find a buffer that matches the incoming message, whether the
1174 * buffer is active or not.
1175 */
1176 if (info_buffer[search].count < 0) {
1177 /*
1178 * We want to find the oldest empty buffer. If a new message
1179 * that is not already buffered comes in, this is the ideal
1180 * place to put it.
1181 */
1182 if ((info_buffer[search].age > empty_age)) {
1183 empty_age = info_buffer[search].age;
1184 empty = search;
1185 }
1186 } else {
1187 /*
1188 * The buffer is not empty, so process it to find the oldest
1189 * buffered message. If a new message comes in that is not
1190 * already buffered, and if there are no empty buffers
1191 * available, the oldest message will be pushed out to make
1192 * room for the new one.
1193 */
1194 if (info_buffer[search].age > oldest_age) {
1195 oldest_age = (info_buffer[search].age);
1196 oldest = search;
1197 }
1198 }
1199 /*
1200 * Check all buffers, inactive and active, to see if the incoming
1201 * message matches an existing buffer. Because empty buffers are
1202 * re-used if they match, it should not be possible for more than
1203 * one buffer to match, so do not bother searching after the first
1204 * match is found.
1205 */
1206 if (found < 0) {
1207 if (! strcmp(message, info_buffer[search].message)) {
1208 found = search;
1209 }
1210 }
1211 }
1212
1213 #if 0
1214 LOG(LOG_DEBUG, "info.c::message_callback", "\n "
1215 "type: %d-%d empty: %d found: %d oldest: %d oldest_age: %d",
1216 type, subtype, empty, found, oldest, oldest_age);
1217 #endif
1218
1219 /*
1220 * If the incoming message is already buffered, then increment the
1221 * message count and exit, otherwise add the message to the buffer.
1222 */
1223 if (found > -1) {
1224 /*
1225 * If the found buffer was inactive, this automatically activates
1226 * it, and sets the count to one to ensure printing of the message
1227 * occurance as messages are pre-printed only when they are
1228 * inserted into a buffer after not being found.
1229 */
1230 if (info_buffer[found].count == -1) {
1231 info_buffer[found].count += 1;
1232 info_buffer[found].age = 0;
1233 }
1234 info_buffer[found].count += 1;
1235 } else {
1236 /*
1237 * The message was not found in a buffer, so check if there is an
1238 * available buffer. If not, dump the oldest buffer to make room,
1239 * then mark it empty.
1240 */
1241 if (empty == -1) {
1242 if (oldest > -1) {
1243 /*
1244 * The oldest message is getting kicked out of the buffer
1245 * to make room for a new message coming in.
1246 */
1247 info_buffer_flush(oldest);
1248 } else {
1249 LOG(LOG_ERROR, "info.c::message_callback",
1250 "Buffer full; oldest unknown", strlen(message));
1251 }
1252 }
1253 /*
1254 * To avoid delaying player notification in cases where multiple
1255 * messages might not occur, or especially if a message is really
1256 * important to get right away, go ahead an output the message
1257 * without a count at the time it is first put into a buffer. As
1258 * this message has already been output, the buffer count is set
1259 * zero, so that info_buffer_flush() will not re-display it if a
1260 * duplicate does not occur while this message is in the buffer.
1261 */
1262 draw_ext_info(orig_color, type, subtype, message);
1263 /*
1264 * There should always be an empty buffer at this point, but just
1265 * in case, recheck before putting the new message in the buffer.
1266 * Do not log another error as one was just logged, but instead
1267 * just output the message that came in without passing it through
1268 * the buffer system.
1269 */
1270 if (empty > -1) {
1271 /*
1272 * Copy the incoming message to the empty buffer.
1273 */
1274 info_buffer[empty].age = 0;
1275 info_buffer[empty].count = 0;
1276 info_buffer[empty].orig_color = orig_color;
1277 info_buffer[empty].type = type;
1278 info_buffer[empty].subtype = subtype;
1279 strcpy(info_buffer[empty].message, message);
1280 }
1281 }
1282 }
1283 }
1284
1285 /**
1286 * @} */ /* EndOf GTKv2OutputCountSync
1287 */
1288
1289 /**
1290 * Clears all the message panels. It is not clear why someone would use it,
1291 * but is called from the common area, and so is supported here.
1292 */
menu_clear(void)1293 void menu_clear(void)
1294 {
1295 int i;
1296
1297 for (i=0; i < NUM_TEXT_VIEWS; i++) {
1298 gtk_text_buffer_set_text(info_pane[i].textbuffer, "", 0);
1299 }
1300 }
1301
1302 /**
1303 * Initialize the message control panel by populating it with descriptions of
1304 * each message type along with checkboxes that are used to configure the
1305 * routing and duplicate suppression system. If previously saved settings are
1306 * found on disk, they are loaded and applied, otherwise the built in client
1307 * defaults are loaded and applied. This initialization must occur after the
1308 * info_init() function runs.
1309 *
1310 * @param window_root The client main window
1311 */
msgctrl_init(GtkWidget * window_root)1312 void msgctrl_init(GtkWidget *window_root)
1313 {
1314 GtkTableChild* child; /* Used to get number of title rows */
1315 GtkWidget* widget; /* Used to connect widgets */
1316 GtkTable* table; /* The table of checkbox controls */
1317 GList* list; /* Iterator: table children */
1318 guint pane; /* Iterator: client message panes */
1319 guint type; /* Iterator: message types */
1320 guint row; /* Attachement for current widget */
1321 gint title_rows = -1; /* Title rows in msgctrl_table as
1322 * defined in layout designer. -1
1323 * means there are no title rows.
1324 */
1325 /*
1326 * Get the window pointer and a pointer to the tree of widgets it contains
1327 */
1328 msgctrl_window = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_window"));
1329
1330 g_signal_connect((gpointer) msgctrl_window, "delete_event",
1331 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
1332 /*
1333 * Initialize the spinbutton pointers.
1334 */
1335 buffer_control.count.ptr =
1336 GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_spinbutton_count"));
1337
1338 buffer_control.timer.ptr =
1339 GTK_WIDGET(gtk_builder_get_object(dialog_xml,"msgctrl_spinbutton_timer"));
1340
1341 /*
1342 * Locate the table widget to fill with controls and its structure.
1343 */
1344 msgctrl_table = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_table"));
1345 table = GTK_TABLE(msgctrl_table);
1346 /*
1347 * How many title rows were set up in the table? The title rows are the
1348 * non-empty rows. Row numbers are zero-based. IMPORTANT: It is assumed
1349 * any row with at least one widget has widgets in all columns. WARNING:
1350 * This assumption is unwise if client layouts begin to be implemented to
1351 * have fewer message panes than the code supports!
1352 */
1353 for (list = gtk_container_get_children(GTK_CONTAINER(table)); list; list = list->next) {
1354 child = list->data;
1355 if ((child->widget != 0) && (child->top_attach > title_rows)) {
1356 title_rows = child->top_attach;
1357 }
1358 }
1359
1360 /*
1361 * The table is defined in the dialog created with the design tool, but
1362 * the dimensions of the table are not known at design time, so it must be
1363 * resized and built up at run-time.
1364 *
1365 * The table columns are: message type description, message buffer
1366 * enable, and one enable per message pane supported by the client code.
1367 * The client layout might not support all of the panes, but all of them
1368 * will be put into the table.
1369 *
1370 * The table rows are: the header rows + the number of message types that
1371 * the client and server support. We assume the XML file designer did
1372 * properly set up the header rows. Since MSG_TYPE_LAST is 1 more than
1373 * the actual number of types, and since title_rows is one less than the
1374 * actual number of header rows, they balance out when added together.
1375 */
1376 gtk_table_resize(table,
1377 (guint)(MSG_TYPE_LAST + title_rows), (guint)(1 + 1 + NUM_TEXT_VIEWS));
1378 /*
1379 * Now we need to put labels and checkboxes in each of the empty rows and
1380 * initialize the state of the checkboxes to match the default settings.
1381 * It helps if we change title_rows to a one-based number. Walk through
1382 * each message type and set the corresponding row of the table it needs
1383 * to go with. type is one-based. The msgctrl_defaults and _widget
1384 * arrays are zero based.
1385 */
1386 title_rows += 1;
1387 for (type = 0; type < MSG_TYPE_LAST - 1; type += 1) {
1388 row = type + title_rows;
1389 /*
1390 * The message type description. Just put the the message type name
1391 * in a label, left-justified with some padding to keep it away from
1392 * the dialog frame and perhaps the neighboring checkbox.
1393 */
1394 widget = gtk_label_new(msgctrl_defaults[type].description);
1395 gtk_misc_set_alignment(GTK_MISC(widget), 0.0f, 0.5f);
1396 gtk_misc_set_padding(GTK_MISC(widget), 2, 0);
1397 gtk_table_attach_defaults(table, widget, 0, 1, row, row + 1);
1398 gtk_widget_show(widget);
1399 /*
1400 * The buffer enable/disable. Display a check box that is preset to
1401 * the built-in default setting.
1402 */
1403 msgctrl_widgets[type].buffer.ptr = gtk_check_button_new();
1404 gtk_table_attach_defaults(
1405 table, msgctrl_widgets[type].buffer.ptr, 1, 2, row, row + 1);
1406 gtk_widget_show(msgctrl_widgets[type].buffer.ptr);
1407 /*
1408 * The message pane routings. Display a check box that is preset to
1409 * the built in defaults.
1410 */
1411 /**
1412 * @todo Panes that are unsupported in the current layout should
1413 * always have their routing disabled, and should disallow user
1414 * interaction with the control but this logic is not yet implemented.
1415 */
1416 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1417 msgctrl_widgets[type].pane[pane].ptr = gtk_check_button_new();
1418 gtk_table_attach_defaults(
1419 table, msgctrl_widgets[type].pane[pane].ptr,
1420 pane + 2, pane + 3, row, row + 1);
1421 gtk_widget_show(msgctrl_widgets[type].pane[pane].ptr);
1422 }
1423 }
1424 /*
1425 * Initialize the state variables for the checkbox and spinbutton controls
1426 * on the message control dialog and then set all the widgets to match the
1427 * client defautl settings.
1428 */
1429 default_msgctrl_configuration();
1430 load_msgctrl_configuration();
1431
1432 /*
1433 * Connect the control's buttons to the appropriate handlers.
1434 */
1435 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_button_save"));
1436 g_signal_connect((gpointer) widget, "clicked",
1437 G_CALLBACK(on_msgctrl_button_save_clicked), NULL);
1438
1439 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_button_load"));
1440 g_signal_connect((gpointer) widget, "clicked",
1441 G_CALLBACK(on_msgctrl_button_load_clicked), NULL);
1442
1443 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_button_defaults"));
1444 g_signal_connect((gpointer) widget, "clicked",
1445 G_CALLBACK(on_msgctrl_button_defaults_clicked), NULL);
1446
1447 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_button_apply"));
1448 g_signal_connect((gpointer) widget, "clicked",
1449 G_CALLBACK(on_msgctrl_button_apply_clicked), NULL);
1450
1451 widget = GTK_WIDGET(gtk_builder_get_object(dialog_xml, "msgctrl_button_close"));
1452 g_signal_connect((gpointer) widget, "clicked",
1453 G_CALLBACK(on_msgctrl_button_close_clicked), NULL);
1454 }
1455
1456 /**
1457 * Update the state of the message control dialog so the configuration matches
1458 * the currently selected settings. Do not call this before msgctrl_widgets[]
1459 * is initialized. It also really only makes sense to call it if changes have
1460 * been made to msgctrl_widgets[].
1461 */
update_msgctrl_configuration(void)1462 void update_msgctrl_configuration(void)
1463 {
1464 guint pane; /* Client-supported message pane */
1465 guint type; /* Message type */
1466
1467 gtk_spin_button_set_value(
1468 GTK_SPIN_BUTTON(buffer_control.count.ptr),
1469 (gdouble) buffer_control.count.state);
1470
1471 gtk_spin_button_set_value(
1472 GTK_SPIN_BUTTON(buffer_control.timer.ptr),
1473 (gdouble) buffer_control.timer.state);
1474
1475 for (type = 0; type < MSG_TYPE_LAST - 1; type += 1) {
1476 gtk_toggle_button_set_active(
1477 GTK_TOGGLE_BUTTON(msgctrl_widgets[type].buffer.ptr),
1478 msgctrl_widgets[type].buffer.state);
1479 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1480 gtk_toggle_button_set_active(
1481 GTK_TOGGLE_BUTTON(msgctrl_widgets[type].pane[pane].ptr),
1482 msgctrl_widgets[type].pane[pane].state);
1483 }
1484 }
1485 }
1486
1487 /**
1488 * Applies the current state of the checkboxes to the msgctrl_widgets state
1489 * variables and saves the settings to disk so the configuration persists
1490 * across client sessions.
1491 */
save_msgctrl_configuration(void)1492 void save_msgctrl_configuration(void)
1493 {
1494 char pathbuf[MAX_BUF]; /* Buffer for a save file path name */
1495 char textbuf[MAX_BUF]; /* Buffer for output to save file */
1496 FILE* fptr; /* Message Control savefile pointer */
1497 guint pane; /* Client-supported message pane */
1498 guint type; /* Message type */
1499
1500 read_msgctrl_configuration(); /* Apply the displayed settings 1st */
1501
1502 snprintf(pathbuf, sizeof(pathbuf), "%s/msgs", config_dir);
1503
1504 if (make_path_to_file(pathbuf) == -1) {
1505 LOG(LOG_WARNING,
1506 "gtk-v2::save_msgctrl_configuration","Error creating %s",pathbuf);
1507 snprintf(textbuf, sizeof(textbuf),
1508 "Error creating %s, Message Control settings not saved.",pathbuf);
1509 draw_ext_info(
1510 NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_ERROR, textbuf);
1511 return;
1512 }
1513 if ((fptr = fopen(pathbuf, "w")) == NULL) {
1514 snprintf(textbuf, sizeof(textbuf),
1515 "Error opening %s, Message Control settings not saved.", pathbuf);
1516 draw_ext_info(
1517 NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_ERROR, textbuf);
1518 return;
1519 }
1520
1521 /*
1522 * It might be best to check the status of all writes, but it is not done.
1523 */
1524 fprintf(fptr, "# Message Control System Configuration\n");
1525 fprintf(fptr, "#\n");
1526 fprintf(fptr, "# Count: 1-96\n");
1527 fprintf(fptr, "#\n");
1528 fprintf(fptr, "C %u\n", buffer_control.count.state);
1529 fprintf(fptr, "#\n");
1530 fprintf(fptr, "# Timer: 1-96 (8 ~= one second)\n");
1531 fprintf(fptr, "#\n");
1532 fprintf(fptr, "T %u\n", buffer_control.timer.state);
1533 fprintf(fptr, "#\n");
1534 fprintf(fptr, "# type, buffer, pane[0], pane[1]...\n");
1535 fprintf(fptr, "# Do not edit the 'type' field.\n");
1536 fprintf(fptr, "# 0 == disable; 1 == enable.\n");
1537 fprintf(fptr, "#\n");
1538 for (type = 0; type < MSG_TYPE_LAST - 1; type += 1) {
1539 fprintf(
1540 fptr, "M %02d %d ", type+1, msgctrl_widgets[type].buffer.state);
1541 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1542 fprintf(fptr, "%d ", msgctrl_widgets[type].pane[pane].state);
1543 }
1544 fprintf(fptr, "\n");
1545 }
1546 fprintf(fptr, "#\n# End of Message Control System Configuration\n");
1547 fclose(fptr);
1548
1549 LOG(LOG_DEBUG, "gtk-v2::save_msgctrl_configuration",
1550 "Message control settings saved to '%s'", pathbuf);
1551
1552 snprintf(textbuf, sizeof(textbuf), "Message control settings saved!");
1553 draw_ext_info(NDI_BLUE, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_CONFIG, textbuf);
1554 }
1555
1556 /**
1557 * Setup the state of the message control dialog so the configuration matches
1558 * a previously saved configuration.
1559 */
load_msgctrl_configuration(void)1560 void load_msgctrl_configuration(void)
1561 {
1562 char pathbuf[MAX_BUF]; /* Buffer for a save file path name */
1563 char textbuf[MAX_BUF]; /* Buffer for input from save file */
1564 char recordtype; /* Savefile data entry type found */
1565 char* cptr; /* Pointer used when reading data */
1566 FILE* fptr; /* Message Control savefile pointer */
1567 guint pane; /* Client-supported message pane */
1568 guint type; /* Message type */
1569 guint error; /* Savefile parsing status */
1570 message_control_t statebuf; /* Holding area for savefile values */
1571 buffer_parameter_t countbuf; /* Holding area for savefile values */
1572 buffer_parameter_t timerbuf; /* Holding area for savefile values */
1573 guint cvalid, tvalid, mvalid; /* Counts the valid entries found */
1574
1575 snprintf(pathbuf, sizeof(pathbuf), "%s/msgs", config_dir);
1576
1577 if ((fptr = fopen(pathbuf, "r")) == NULL) {
1578 snprintf(textbuf, sizeof(textbuf),
1579 "Error opening %s, Message Control settings not loaded.",pathbuf);
1580 draw_ext_info(
1581 NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_ERROR, textbuf);
1582 return;
1583 }
1584 /*
1585 * When we parse the file we buffer each entire record before any values
1586 * are applied to the client message control configuration. If any
1587 * problems are found at all, the entire record is skipped and the file
1588 * is reported as corrupt. Even if individual records are corrupt, the
1589 * rest of the file is processed.
1590 *
1591 * If more than one record for the same error type exists the last one is
1592 * used, but if too many records are found the file is reported as corrupt
1593 * even though it accepts all the data.
1594 */
1595 error = 0;
1596 cvalid = 0;
1597 tvalid = 0;
1598 mvalid = 0;
1599 while(fgets(textbuf, MAX_BUF-1, fptr) != NULL) {
1600 if (textbuf[0] == '#' || textbuf[0] == '\n') {
1601 continue;
1602 }
1603
1604 /*
1605 * Identify the savefile record type found.
1606 */
1607 cptr = strtok(textbuf, "\t ");
1608 if ((cptr == NULL)
1609 || ((*cptr != 'C') && (*cptr != 'T') && (*cptr != 'M'))) {
1610 error += 1;
1611 continue;
1612 }
1613 recordtype = *cptr;
1614
1615 /*
1616 * Process the following fields by record type
1617 */
1618 if (recordtype == 'C') {
1619 cptr = strtok(NULL, "\n");
1620 if ((cptr == NULL)
1621 || (sscanf(cptr, "%u", &countbuf.state) != 1)
1622 || (countbuf.state < 1)
1623 || (countbuf.state > 96)) {
1624 error += 1;
1625 continue;
1626 }
1627 }
1628 if (recordtype == 'T') {
1629 cptr = strtok(NULL, "\n");
1630 if ((cptr == NULL)
1631 || (sscanf(cptr, "%u", &timerbuf.state) != 1)
1632 || (timerbuf.state < 1)
1633 || (timerbuf.state > 96)) {
1634 error += 1;
1635 continue;
1636 }
1637 }
1638 if (recordtype == 'M') {
1639 cptr = strtok(NULL, "\t ");
1640 if ((cptr == NULL)
1641 || (sscanf(cptr, "%d", &type) != 1)
1642 || (type < 1)
1643 || (type >= MSG_TYPE_LAST)) {
1644 error += 1;
1645 continue;
1646 }
1647 cptr = strtok(NULL, "\t ");
1648 if ((cptr == NULL)
1649 || (sscanf(cptr, "%d", &statebuf.buffer.state) != 1)
1650 || (statebuf.buffer.state < 0)
1651 || (statebuf.buffer.state > 1)) {
1652 error += 1;
1653 continue;
1654 }
1655 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1656 cptr = strtok(NULL, "\t ");
1657 if ((cptr == NULL)
1658 || (sscanf(cptr, "%d", &statebuf.pane[pane].state) != 1)
1659 || (statebuf.pane[pane].state < 0)
1660 || (statebuf.pane[pane].state > 1)) {
1661 error += 1;
1662 continue;
1663 }
1664 }
1665 /*
1666 * Ignore the record if it has too many fields. This might be a
1667 * bit strict, but it does help enforce the file integrity in the
1668 * event that the the number of supported panels increases in the
1669 * future.
1670 */
1671 cptr = strtok(NULL, "\n");
1672 if (cptr != NULL) {
1673 error += 1;
1674 continue;
1675 }
1676 }
1677
1678 /*
1679 * Remember, type is one-based, but the index into an array is zero-
1680 * based, so adjust type. Also, since the record parsed out fine,
1681 * increment the number of valid records found. Apply all the values
1682 * read to the buffer_control structure and msgctrl_widgets[] array so
1683 * the dialog can be updated when all data has been read.
1684 */
1685 if (recordtype == 'C') {
1686 buffer_control.count.state = countbuf.state;
1687 cvalid += 1;
1688 }
1689 if (recordtype == 'T') {
1690 buffer_control.timer.state = timerbuf.state;
1691 tvalid += 1;
1692 }
1693 if (recordtype == 'M') {
1694 type -= 1;
1695 msgctrl_widgets[type].buffer.state = statebuf.buffer.state;
1696 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1697 msgctrl_widgets[type].pane[pane].state =
1698 statebuf.pane[pane].state;
1699 }
1700 mvalid += 1;
1701 }
1702 }
1703 fclose(fptr);
1704 /*
1705 * If there was any oddity with the data file, report it as corrupted even
1706 * if some of the values were used. A corrupted file can be uncorrupted
1707 * by loading it and saving it again. A found value is needed for count,
1708 * timer, and each message type.
1709 */
1710 if ((error > 0)
1711 || (cvalid != 1)
1712 || (tvalid != 1)
1713 || (mvalid != MSG_TYPE_LAST - 1)) {
1714 snprintf(textbuf, sizeof(textbuf),
1715 "Corrupted Message Control settings in %s.", pathbuf);
1716 draw_ext_info(
1717 NDI_RED, MSG_TYPE_CLIENT, MSG_TYPE_CLIENT_ERROR, textbuf);
1718 LOG(LOG_ERROR, "gtk-v2::load_msgctrl_configuration",
1719 "Error loading %s. %s\n", pathbuf, textbuf);
1720 }
1721 /*
1722 * If any data was accepted from the save file, report that settings were
1723 * loaded. Apply the loaded values to the Message Control dialog checkbox
1724 * widgets. so they reflect the states that were previously saved.
1725 */
1726 if ((cvalid + tvalid + mvalid) > 0) {
1727 LOG(LOG_DEBUG, "gtk-v2::load_msgctrl_configuration",
1728 "Message control settings loaded from '%s'", pathbuf);
1729 update_msgctrl_configuration(); /* Update checkboxes w/ loaded data */
1730 }
1731 }
1732
1733 /**
1734 * Setup the state of the message control dialog so the configuration matches
1735 * the default settings built in to the client.
1736 *
1737 * Iterate through each message type. For each, copy the built-in client
1738 * default to the Message Control dialog state variables. All supported
1739 * defaults are copied, not just the ones supported by the layout.
1740 */
default_msgctrl_configuration(void)1741 void default_msgctrl_configuration(void)
1742 {
1743 guint pane; /* Client-supported message pane */
1744 guint type; /* Message type */
1745
1746 buffer_control.count.state = (guint) buffer_control.count.default_state;
1747 buffer_control.timer.state = (guint) buffer_control.timer.default_state;
1748 for (type = 0; type < MSG_TYPE_LAST - 1; type += 1) {
1749 msgctrl_widgets[type].buffer.state = msgctrl_defaults[type].buffer;
1750 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1751 msgctrl_widgets[type].pane[pane].state =
1752 msgctrl_defaults[type].pane[pane];
1753 }
1754 }
1755 update_msgctrl_configuration();
1756 }
1757
1758 /**
1759 * Reads the state of the message control dialog and applies the settings to
1760 * the msgctrl_widgets[] state variables that control the message routing
1761 * and duplicate suppression system.
1762 */
read_msgctrl_configuration(void)1763 void read_msgctrl_configuration(void)
1764 {
1765 guint pane; /* Client-supported message pane */
1766 guint type; /* Message type */
1767
1768 buffer_control.count.state =
1769 gtk_spin_button_get_value_as_int(
1770 GTK_SPIN_BUTTON(buffer_control.count.ptr));
1771 buffer_control.timer.state =
1772 gtk_spin_button_get_value_as_int(
1773 GTK_SPIN_BUTTON(buffer_control.timer.ptr));
1774 /*
1775 * Iterate through each message type. For each, record the value of the
1776 * message duplicate suppression checkbox, and also obtain the routing
1777 * settings for all client supported panels (even if the layout does not
1778 * support them all.
1779 */
1780 for (type = 0; type < MSG_TYPE_LAST - 1; type += 1) {
1781 msgctrl_widgets[type].buffer.state =
1782 gtk_toggle_button_get_active(
1783 GTK_TOGGLE_BUTTON(msgctrl_widgets[type].buffer.ptr));
1784 for (pane = 0; pane < NUM_TEXT_VIEWS; pane += 1) {
1785 msgctrl_widgets[type].pane[pane].state =
1786 gtk_toggle_button_get_active(
1787 GTK_TOGGLE_BUTTON(msgctrl_widgets[type].pane[pane].ptr));
1788 }
1789 }
1790 }
1791
1792 /**
1793 * When the message control dialog save button is pressed, the currently shown
1794 * settings are applied for immediate use and they are saved to disk so the
1795 * settings persist across client sessions. Saved settings automatically
1796 * load and apply when the client is started.
1797 *
1798 * @param button
1799 * @param user_data
1800 */
1801 void
on_msgctrl_button_save_clicked(GtkButton * button,gpointer user_data)1802 on_msgctrl_button_save_clicked (GtkButton *button,
1803 gpointer user_data)
1804 {
1805 read_msgctrl_configuration();
1806 save_msgctrl_configuration();
1807 }
1808
1809 /**
1810 * When the message control dialog load button is pressed, the settings last
1811 * saved are restored and applied. It may be used to "undo" both applied and
1812 * unapplied setting changes.
1813 *
1814 * @param button
1815 * @param user_data
1816 */
1817 void
on_msgctrl_button_load_clicked(GtkButton * button,gpointer user_data)1818 on_msgctrl_button_load_clicked (GtkButton *button,
1819 gpointer user_data)
1820 {
1821 load_msgctrl_configuration();
1822 }
1823
1824 /**
1825 * When the message control dialog defaults button is pressed, the default
1826 * settings built into the client are restored and applied.
1827 *
1828 * @param button
1829 * @param user_data
1830 */
1831 void
on_msgctrl_button_defaults_clicked(GtkButton * button,gpointer user_data)1832 on_msgctrl_button_defaults_clicked (GtkButton *button,
1833 gpointer user_data)
1834 {
1835 default_msgctrl_configuration();
1836 }
1837
1838 /**
1839 * When the message control dialog apply button is pressed, the currently
1840 * displayed settings are applied. The dialog is not dismissed, but remains
1841 * open for further adjustments to be made.
1842 *
1843 * @param button
1844 * @param user_data
1845 */
1846 void
on_msgctrl_button_apply_clicked(GtkButton * button,gpointer user_data)1847 on_msgctrl_button_apply_clicked (GtkButton *button,
1848 gpointer user_data)
1849 {
1850 read_msgctrl_configuration();
1851 }
1852
1853 /**
1854 * When the message control dialog close button is pressed, the currently
1855 * displayed settings are applied and the dialog is dismissed.
1856 *
1857 * @param button
1858 * @param user_data
1859 */
1860 void
on_msgctrl_button_close_clicked(GtkButton * button,gpointer user_data)1861 on_msgctrl_button_close_clicked (GtkButton *button,
1862 gpointer user_data)
1863 {
1864 read_msgctrl_configuration();
1865 gtk_widget_hide(msgctrl_window);
1866 }
1867
1868 /**
1869 * Shows the message control dialog when the menu item is activated. The
1870 * settings shown on the dialog when it is activated are the settings
1871 * currently in use.
1872 *
1873 * @param menuitem
1874 * @param user_data
1875 */
1876 void
on_msgctrl_activate(GtkMenuItem * menuitem,gpointer user_data)1877 on_msgctrl_activate (GtkMenuItem *menuitem,
1878 gpointer user_data)
1879 {
1880 gtk_widget_show(msgctrl_window);
1881 }
1882