1 /***********************************************************************
2  Freeciv - Copyright (C) 1996 - A Kjeldberg, L Gregersen, P Unold
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published by
5    the Free Software Foundation; either version 2, or (at your option)
6    any later version.
7 
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY; without even the implied warranty of
10    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11    GNU General Public License for more details.
12 ***********************************************************************/
13 
14 #ifdef HAVE_CONFIG_H
15 #include <fc_config.h>
16 #endif
17 
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <time.h>
22 
23 #include <gdk/gdkkeysyms.h>
24 
25 /* utility */
26 #include "fcintl.h"
27 #include "genlist.h"
28 #include "log.h"
29 #include "mem.h"
30 #include "support.h"
31 
32 /* common */
33 #include "chat.h"
34 #include "featured_text.h"
35 #include "game.h"
36 #include "packets.h"
37 
38 /* client */
39 #include "client_main.h"
40 #include "climap.h"
41 #include "control.h"
42 #include "mapview_common.h"
43 
44 /* client/gui-gtk-3.22 */
45 #include "colors.h"
46 #include "gui_main.h"
47 #include "gui_stuff.h"
48 #include "pages.h"
49 
50 #include "chatline.h"
51 
52 #define	MAX_CHATLINE_HISTORY 20
53 
54 static struct genlist *history_list = NULL;
55 static int history_pos = -1;
56 
57 static struct inputline_toolkit {
58   GtkWidget *main_widget;
59   GtkWidget *entry;
60   GtkWidget *button_box;
61   GtkWidget *toolbar;
62   GtkWidget *toggle_button;
63   bool toolbar_displayed;
64 } toolkit;      /* Singleton. */
65 
66 static void inputline_make_tag(GtkEntry *entry, enum text_tag_type type);
67 
68 /**************************************************************************
69   Returns TRUE iff the input line has focus.
70 **************************************************************************/
inputline_has_focus(void)71 bool inputline_has_focus(void)
72 {
73   return gtk_widget_has_focus(toolkit.entry);
74 }
75 
76 /**************************************************************************
77   Gives the focus to the intput line.
78 **************************************************************************/
inputline_grab_focus(void)79 void inputline_grab_focus(void)
80 {
81   gtk_widget_grab_focus(toolkit.entry);
82 }
83 
84 /**************************************************************************
85   Returns TRUE iff the input line is currently visible.
86 **************************************************************************/
inputline_is_visible(void)87 bool inputline_is_visible(void)
88 {
89   return gtk_widget_get_mapped(toolkit.entry);
90 }
91 
92 /**************************************************************************
93   Helper function to determine if a given client input line is intended as
94   a "plain" public message. Note that messages prefixed with : are a
95   special case (explicit public messages), and will return FALSE.
96 **************************************************************************/
is_plain_public_message(const char * s)97 static bool is_plain_public_message(const char *s)
98 {
99   const char *p;
100 
101   /* If it is a server command or an explicit ally
102    * message, then it is not a public message. */
103   if (s[0] == SERVER_COMMAND_PREFIX || s[0] == CHAT_ALLIES_PREFIX) {
104     return FALSE;
105   }
106 
107   /* It might be a private message of the form
108    *   'player name with spaces':the message
109    * or with ". So skip past the player name part. */
110   if (s[0] == '\'' || s[0] == '"') {
111     p = strchr(s + 1, s[0]);
112   } else {
113     p = s;
114   }
115 
116   /* Now we just need to check that it is not a private
117    * message. If we encounter a space then the preceeding
118    * text could not have been a user/player name (the
119    * quote check above eliminated names with spaces) so
120    * it must be a public message. Otherwise if we encounter
121    * the message prefix : then the text parsed up until now
122    * was a player/user name and the line is intended as
123    * a private message (or explicit public message if the
124    * first character is :). */
125   while (p != NULL && *p != '\0') {
126     if (fc_isspace(*p)) {
127       return TRUE;
128     } else if (*p == CHAT_DIRECT_PREFIX) {
129       return FALSE;
130     }
131     p++;
132   }
133   return TRUE;
134 }
135 
136 
137 /**************************************************************************
138   Called when the return key is pressed.
139 **************************************************************************/
inputline_return(GtkEntry * w,gpointer data)140 static void inputline_return(GtkEntry *w, gpointer data)
141 {
142   const char *theinput;
143 
144   theinput = gtk_entry_get_text(w);
145 
146   if (*theinput) {
147     if (client_state() == C_S_RUNNING
148         && GUI_GTK_OPTION(allied_chat_only)
149         && is_plain_public_message(theinput)) {
150       char buf[MAX_LEN_MSG];
151 
152       fc_snprintf(buf, sizeof(buf), ". %s", theinput);
153       send_chat(buf);
154     } else {
155       send_chat(theinput);
156     }
157 
158     if (genlist_size(history_list) >= MAX_CHATLINE_HISTORY) {
159       void *history_data;
160 
161       history_data = genlist_get(history_list, -1);
162       genlist_remove(history_list, history_data);
163       free(history_data);
164     }
165 
166     genlist_prepend(history_list, fc_strdup(theinput));
167     history_pos=-1;
168   }
169 
170   gtk_entry_set_text(w, "");
171 }
172 
173 /**************************************************************************
174   Returns the name of player or user, set in the same list.
175 **************************************************************************/
get_player_or_user_name(int id)176 static const char *get_player_or_user_name(int id)
177 {
178   size_t size = conn_list_size(game.all_connections);
179 
180   if (id < size) {
181     return conn_list_get(game.all_connections, id)->username;
182   } else {
183     struct player *pplayer = player_by_number(id - size);
184     if (pplayer) {
185       return pplayer->name;
186     } else {
187       /* Empty slot. Relies on being used with comparison function
188        * which can cope with NULL. */
189       return NULL;
190     }
191   }
192 }
193 
194 /**************************************************************************
195   Find a player or a user by prefix.
196 
197   prefix - The prefix.
198   matches - A string array to set the matches result.
199   max_matches - The maximum of matches.
200   match_len - The length of the string used to returns matches.
201 
202   Returns the number of the matches names.
203 **************************************************************************/
check_player_or_user_name(const char * prefix,const char ** matches,const int max_matches)204 static int check_player_or_user_name(const char *prefix,
205                                      const char **matches,
206                                      const int max_matches)
207 {
208   int matches_id[max_matches * 2], ind, num;
209 
210   switch (match_prefix_full(get_player_or_user_name,
211                             player_slot_count()
212                             + conn_list_size(game.all_connections),
213                             MAX_LEN_NAME, fc_strncasecmp, strlen,
214                             prefix, &ind, matches_id,
215                             max_matches * 2, &num)) {
216   case M_PRE_EXACT:
217   case M_PRE_ONLY:
218     matches[0] = get_player_or_user_name(ind);
219     return 1;
220   case M_PRE_AMBIGUOUS:
221     {
222       /* Remove duplications playername/username. */
223       const char *name;
224       int i, j, c = 0;
225 
226       for (i = 0; i < num && c < max_matches; i++) {
227         name = get_player_or_user_name(matches_id[i]);
228         for (j = 0; j < c; j++) {
229           if (0 == fc_strncasecmp(name, matches[j], MAX_LEN_NAME)) {
230             break;
231           }
232         }
233         if (j >= c) {
234           matches[c++] = name;
235         }
236       }
237       return c;
238     }
239   case M_PRE_EMPTY:
240   case M_PRE_LONG:
241   case M_PRE_FAIL:
242   case M_PRE_LAST:
243     break;
244   }
245 
246   return 0;
247 }
248 
249 /**************************************************************************
250   Find the larger common prefix.
251 
252   prefixes - A list of prefixes.
253   num_prefixes - The number of prefixes.
254   buf - The buffer to set.
255   buf_len - The maximal size of the buffer.
256 
257   Returns the length of the common prefix (in characters).
258 **************************************************************************/
get_common_prefix(const char * const * prefixes,size_t num_prefixes,char * buf,size_t buf_len)259 static size_t get_common_prefix(const char *const *prefixes,
260                                 size_t num_prefixes,
261                                 char *buf, size_t buf_len)
262 {
263   const char *p;
264   char *q;
265   size_t i;
266 
267   fc_strlcpy(buf, prefixes[0], buf_len);
268   for (i = 1; i < num_prefixes; i++) {
269     for (p = prefixes[i], q = buf; *p != '\0' && *q != '\0';
270          p = g_utf8_next_char(p), q = g_utf8_next_char(q)) {
271       if (g_unichar_toupper(g_utf8_get_char(p))
272           != g_unichar_toupper(g_utf8_get_char(q))) {
273         *q = '\0';
274         break;
275       }
276     }
277   }
278 
279  return g_utf8_strlen(buf, -1);
280 }
281 
282 /**************************************************************************
283   Autocompletes the input line with a player or user name.
284   Returns FALSE if there is no string to complete.
285 **************************************************************************/
chatline_autocomplete(GtkEditable * editable)286 static bool chatline_autocomplete(GtkEditable *editable)
287 {
288 #define MAX_MATCHES 10
289   const char *name[MAX_MATCHES];
290   char buf[MAX_LEN_NAME * MAX_MATCHES];
291   gint pos;
292   gchar *chars, *p, *prev;
293   int num, i;
294   size_t prefix_len;
295 
296   /* Part 1: get the string to complete. */
297   pos = gtk_editable_get_position(editable);
298   chars = gtk_editable_get_chars(editable, 0, pos);
299 
300   p = chars + strlen(chars);
301   while ((prev = g_utf8_find_prev_char(chars, p))) {
302     if (!g_unichar_isalnum(g_utf8_get_char(prev))) {
303       break;
304     }
305     p = prev;
306   }
307   /* p points to the start of the last word, or the start of the string. */
308 
309   prefix_len = g_utf8_strlen(p, -1);
310   if (0 == prefix_len) {
311     /* Empty: nothing to complete, propagate the event. */
312     g_free(chars);
313     return FALSE;
314   }
315 
316   /* Part 2: compare with player and user names. */
317   num = check_player_or_user_name(p, name, MAX_MATCHES);
318   if (1 == num) {
319     gtk_editable_delete_text(editable, pos - prefix_len, pos);
320     pos -= prefix_len;
321     gtk_editable_insert_text(editable, name[0], strlen(name[0]), &pos);
322     gtk_editable_set_position(editable, pos);
323     g_free(chars);
324     return TRUE;
325   } else if (num > 1) {
326     if (get_common_prefix(name, num, buf, sizeof(buf)) > prefix_len) {
327       gtk_editable_delete_text(editable, pos - prefix_len, pos);
328       pos -= prefix_len;
329       gtk_editable_insert_text(editable, buf, strlen(buf), &pos);
330       gtk_editable_set_position(editable, pos);
331     }
332     sz_strlcpy(buf, name[0]);
333     for (i = 1; i < num; i++) {
334       cat_snprintf(buf, sizeof(buf), ", %s", name[i]);
335     }
336     /* TRANS: comma-separated list of player/user names for completion */
337     output_window_printf(ftc_client, _("Suggestions: %s."), buf);
338   }
339 
340   g_free(chars);
341   return TRUE;
342 }
343 
344 /**************************************************************************
345   Called when a key is pressed.
346 **************************************************************************/
inputline_handler(GtkWidget * w,GdkEventKey * ev)347 static gboolean inputline_handler(GtkWidget *w, GdkEventKey *ev)
348 {
349   if ((ev->state & GDK_CONTROL_MASK)) {
350     /* Chatline featured text support. */
351     switch (ev->keyval) {
352     case GDK_KEY_b:
353       inputline_make_tag(GTK_ENTRY(w), TTT_BOLD);
354       return TRUE;
355 
356     case GDK_KEY_c:
357       inputline_make_tag(GTK_ENTRY(w), TTT_COLOR);
358       return TRUE;
359 
360     case GDK_KEY_i:
361       inputline_make_tag(GTK_ENTRY(w), TTT_ITALIC);
362       return TRUE;
363 
364     case GDK_KEY_s:
365       inputline_make_tag(GTK_ENTRY(w), TTT_STRIKE);
366       return TRUE;
367 
368     case GDK_KEY_u:
369       inputline_make_tag(GTK_ENTRY(w), TTT_UNDERLINE);
370       return TRUE;
371 
372     default:
373       break;
374     }
375 
376   } else {
377     /* Chatline history controls. */
378     switch (ev->keyval) {
379     case GDK_KEY_Up:
380       if (history_pos < genlist_size(history_list) - 1) {
381         gtk_entry_set_text(GTK_ENTRY(w),
382                            genlist_get(history_list, ++history_pos));
383         gtk_editable_set_position(GTK_EDITABLE(w), -1);
384       }
385       return TRUE;
386 
387     case GDK_KEY_Down:
388       if (history_pos >= 0) {
389         history_pos--;
390       }
391 
392       if (history_pos >= 0) {
393         gtk_entry_set_text(GTK_ENTRY(w),
394                            genlist_get(history_list, history_pos));
395       } else {
396         gtk_entry_set_text(GTK_ENTRY(w), "");
397       }
398       gtk_editable_set_position(GTK_EDITABLE(w), -1);
399       return TRUE;
400 
401     case GDK_KEY_Tab:
402       if (GUI_GTK_OPTION(chatline_autocompletion)) {
403         return chatline_autocomplete(GTK_EDITABLE(w));
404       }
405 
406     default:
407       break;
408     }
409   }
410 
411   return FALSE;
412 }
413 
414 /**************************************************************************
415   Make a text tag for the selected text.
416 **************************************************************************/
inputline_make_tag(GtkEntry * entry,enum text_tag_type type)417 void inputline_make_tag(GtkEntry *entry, enum text_tag_type type)
418 {
419   char buf[MAX_LEN_MSG];
420   GtkEditable *editable = GTK_EDITABLE(entry);
421   gint start_pos, end_pos;
422   gchar *selection;
423   gchar *fg_color_text = NULL, *bg_color_text = NULL;
424 
425   if (!gtk_editable_get_selection_bounds(editable, &start_pos, &end_pos)) {
426     /* Let's say the selection starts and ends at the current position. */
427     start_pos = end_pos = gtk_editable_get_position(editable);
428   }
429 
430   selection = gtk_editable_get_chars(editable, start_pos, end_pos);
431 
432   if (type == TTT_COLOR) {
433     /* Get the color arguments. */
434     GdkRGBA *fg_color = g_object_get_data(G_OBJECT(entry), "fg_color");
435     GdkRGBA *bg_color = g_object_get_data(G_OBJECT(entry), "bg_color");
436 
437     if (!fg_color && !bg_color) {
438       goto CLEAN_UP;
439     }
440 
441     if (fg_color) {
442       fg_color_text = gdk_rgba_to_string(fg_color);
443     }
444     if (bg_color) {
445       bg_color_text = gdk_rgba_to_string(bg_color);
446     }
447 
448     if (0 == featured_text_apply_tag(selection, buf, sizeof(buf),
449                                      TTT_COLOR, 0, FT_OFFSET_UNSET,
450                                      ft_color_construct(fg_color_text,
451                                                         bg_color_text))) {
452       goto CLEAN_UP;
453     }
454   } else if (0 == featured_text_apply_tag(selection, buf, sizeof(buf),
455                                           type, 0, FT_OFFSET_UNSET)) {
456     goto CLEAN_UP;
457   }
458 
459   /* Replace the selection. */
460   gtk_editable_delete_text(editable, start_pos, end_pos);
461   end_pos = start_pos;
462   gtk_editable_insert_text(editable, buf, -1, &end_pos);
463   gtk_editable_select_region(editable, start_pos, end_pos);
464 
465 CLEAN_UP:
466   g_free(selection);
467   g_free(fg_color_text);
468   g_free(bg_color_text);
469 }
470 
471 /**************************************************************************
472   Make a chat link at the current position or make the current selection
473   clickable.
474 **************************************************************************/
inputline_make_chat_link(struct tile * ptile,bool unit)475 void inputline_make_chat_link(struct tile *ptile, bool unit)
476 {
477   char buf[MAX_LEN_MSG];
478   GtkWidget *entry = toolkit.entry;
479   GtkEditable *editable = GTK_EDITABLE(entry);
480   gint start_pos, end_pos;
481   gchar *chars;
482   struct unit *punit;
483 
484   /* Get the target. */
485   if (unit) {
486     punit = find_visible_unit(ptile);
487     if (!punit) {
488       output_window_append(ftc_client, _("No visible unit on this tile."));
489       return;
490     }
491   } else {
492     punit = NULL;
493   }
494 
495   if (gtk_editable_get_selection_bounds(editable, &start_pos, &end_pos)) {
496     /* There is a selection, make it clickable. */
497     gpointer target;
498     enum text_link_type type;
499 
500     chars = gtk_editable_get_chars(editable, start_pos, end_pos);
501     if (punit) {
502       type = TLT_UNIT;
503       target = punit;
504     } else if (tile_city(ptile)) {
505       type = TLT_CITY;
506       target = tile_city(ptile);
507     } else {
508       type = TLT_TILE;
509       target = ptile;
510     }
511 
512     if (0 != featured_text_apply_tag(chars, buf, sizeof(buf), TTT_LINK,
513                                      0, FT_OFFSET_UNSET, type, target)) {
514       /* Replace the selection. */
515       gtk_editable_delete_text(editable, start_pos, end_pos);
516       end_pos = start_pos;
517       gtk_editable_insert_text(editable, buf, -1, &end_pos);
518       gtk_widget_grab_focus(entry);
519       gtk_editable_select_region(editable, start_pos, end_pos);
520     }
521   } else {
522     /* Just insert the link at the current position. */
523     start_pos = gtk_editable_get_position(editable);
524     end_pos = start_pos;
525     chars = gtk_editable_get_chars(editable, MAX(start_pos - 1, 0),
526                                    start_pos + 1);
527     if (punit) {
528       sz_strlcpy(buf, unit_link(punit));
529     } else if (tile_city(ptile)) {
530       sz_strlcpy(buf, city_link(tile_city(ptile)));
531     } else {
532       sz_strlcpy(buf, tile_link(ptile));
533     }
534 
535     if (start_pos > 0 && strlen(chars) > 0 && chars[0] != ' ') {
536       /* Maybe insert an extra space. */
537       gtk_editable_insert_text(editable, " ", 1, &end_pos);
538     }
539     gtk_editable_insert_text(editable, buf, -1, &end_pos);
540     if (chars[start_pos > 0 ? 1 : 0] != '\0'
541         && chars[start_pos > 0 ? 1 : 0] != ' ') {
542       /* Maybe insert an extra space. */
543       gtk_editable_insert_text(editable, " ", 1, &end_pos);
544     }
545     gtk_widget_grab_focus(entry);
546     gtk_editable_set_position(editable, end_pos);
547   }
548 
549   g_free(chars);
550 }
551 
552 /**************************************************************************
553   Scroll a textview so that the given mark is visible, but only if the
554   scroll window containing the textview is very close to the bottom. The
555   text mark 'scroll_target' should probably be the first character of the
556   last line in the text buffer.
557 **************************************************************************/
scroll_if_necessary(GtkTextView * textview,GtkTextMark * scroll_target)558 void scroll_if_necessary(GtkTextView *textview, GtkTextMark *scroll_target)
559 {
560   GtkWidget *sw;
561   GtkAdjustment *vadj;
562   gdouble val, max, upper, page_size;
563 
564   fc_assert_ret(textview != NULL);
565   fc_assert_ret(scroll_target != NULL);
566 
567   sw = gtk_widget_get_parent(GTK_WIDGET(textview));
568   fc_assert_ret(sw != NULL);
569   fc_assert_ret(GTK_IS_SCROLLED_WINDOW(sw));
570 
571   vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
572   val = gtk_adjustment_get_value(vadj);
573   g_object_get(G_OBJECT(vadj), "upper", &upper,
574                "page-size", &page_size, NULL);
575   max = upper - page_size;
576   if (max - val < 10.0) {
577     gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(textview), scroll_target,
578                                  0.0, TRUE, 1.0, 0.0);
579   }
580 }
581 
582 /**************************************************************************
583   Click a link.
584 **************************************************************************/
event_after(GtkWidget * text_view,GdkEventButton * event)585 static gboolean event_after(GtkWidget *text_view, GdkEventButton *event)
586 {
587   GtkTextIter start, end, iter;
588   GtkTextBuffer *buffer;
589   GSList *tags, *tagp;
590   gint x, y;
591   struct tile *ptile = NULL;
592 
593   if (event->type != GDK_BUTTON_RELEASE || event->button != 1) {
594     return FALSE;
595   }
596 
597   buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
598 
599   /* We shouldn't follow a link if the user has selected something. */
600   gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
601   if (gtk_text_iter_get_offset(&start) != gtk_text_iter_get_offset(&end)) {
602     return FALSE;
603   }
604 
605   gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW (text_view),
606                                         GTK_TEXT_WINDOW_WIDGET,
607                                         event->x, event->y, &x, &y);
608 
609   gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(text_view), &iter, x, y);
610 
611   if ((tags = gtk_text_iter_get_tags(&iter))) {
612     for (tagp = tags; tagp; tagp = tagp->next) {
613       GtkTextTag *tag = tagp->data;
614       enum text_link_type type =
615         GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tag), "type"));
616 
617       if (type != 0) {
618         /* This is a link. */
619         int id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tag), "id"));
620         ptile = NULL;
621 
622         /* Real type is type - 1.
623          * See comment in apply_text_tag() for g_object_set_data(). */
624         type--;
625 
626         switch (type) {
627         case TLT_CITY:
628           {
629             struct city *pcity = game_city_by_number(id);
630 
631             if (pcity) {
632               ptile = client_city_tile(pcity);
633             } else {
634               output_window_append(ftc_client, _("This city isn't known!"));
635             }
636           }
637           break;
638         case TLT_TILE:
639           ptile = index_to_tile(id);
640 
641           if (!ptile) {
642             output_window_append(ftc_client,
643                                  _("This tile doesn't exist in this game!"));
644           }
645           break;
646         case TLT_UNIT:
647           {
648             struct unit *punit = game_unit_by_number(id);
649 
650             if (punit) {
651               ptile = unit_tile(punit);
652             } else {
653               output_window_append(ftc_client, _("This unit isn't known!"));
654             }
655           }
656           break;
657         }
658 
659         if (ptile) {
660           center_tile_mapcanvas(ptile);
661           link_mark_restore(type, id);
662           gtk_widget_grab_focus(GTK_WIDGET(map_canvas));
663         }
664       }
665     }
666     g_slist_free(tags);
667   }
668 
669   return FALSE;
670 }
671 
672 /**************************************************************************
673   Set the "hand" cursor when moving over a link.
674 **************************************************************************/
set_cursor_if_appropriate(GtkTextView * text_view,gint x,gint y)675 static void set_cursor_if_appropriate(GtkTextView *text_view, gint x, gint y)
676 {
677   static gboolean hovering_over_link = FALSE;
678   static GdkCursor *hand_cursor = NULL;
679   static GdkCursor *regular_cursor = NULL;
680   GSList *tags, *tagp;
681   GtkTextIter iter;
682   gboolean hovering = FALSE;
683 
684   /* Initialize the cursors. */
685   if (!hand_cursor) {
686     hand_cursor = gdk_cursor_new_for_display(
687         gtk_widget_get_display(GTK_WIDGET(text_view)), GDK_HAND2);
688   }
689   if (!regular_cursor) {
690     regular_cursor = gdk_cursor_new_for_display(
691         gtk_widget_get_display(GTK_WIDGET(text_view)), GDK_XTERM);
692   }
693 
694   gtk_text_view_get_iter_at_location(text_view, &iter, x, y);
695 
696   tags = gtk_text_iter_get_tags(&iter);
697   for (tagp = tags; tagp; tagp = tagp->next) {
698     enum text_link_type type =
699       GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tagp->data), "type"));
700 
701     if (type != 0) {
702       hovering = TRUE;
703       break;
704     }
705   }
706 
707   if (hovering != hovering_over_link) {
708     hovering_over_link = hovering;
709 
710     if (hovering_over_link) {
711       gdk_window_set_cursor(gtk_text_view_get_window(text_view,
712                                                      GTK_TEXT_WINDOW_TEXT),
713                             hand_cursor);
714     } else {
715       gdk_window_set_cursor(gtk_text_view_get_window(text_view,
716                                                      GTK_TEXT_WINDOW_TEXT),
717                             regular_cursor);
718     }
719   }
720 
721   if (tags) {
722     g_slist_free(tags);
723   }
724 }
725 
726 /**************************************************************************
727   Maybe are the mouse is moving over a link.
728 **************************************************************************/
motion_notify_event(GtkWidget * text_view,GdkEventMotion * event)729 static gboolean motion_notify_event(GtkWidget *text_view,
730                                     GdkEventMotion *event)
731 {
732   gint x, y;
733 
734   gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(text_view),
735                                         GTK_TEXT_WINDOW_WIDGET,
736                                         event->x, event->y, &x, &y);
737   set_cursor_if_appropriate(GTK_TEXT_VIEW(text_view), x, y);
738 
739   return FALSE;
740 }
741 
742 /**************************************************************************
743   Set the appropriate callbacks for the message buffer.
744 **************************************************************************/
set_message_buffer_view_link_handlers(GtkWidget * view)745 void set_message_buffer_view_link_handlers(GtkWidget *view)
746 {
747   g_signal_connect(view, "event-after",
748 		   G_CALLBACK(event_after), NULL);
749   g_signal_connect(view, "motion-notify-event",
750 		   G_CALLBACK(motion_notify_event), NULL);
751 }
752 
753 /**************************************************************************
754   Convert a struct text_tag to a GtkTextTag.
755 **************************************************************************/
apply_text_tag(const struct text_tag * ptag,GtkTextBuffer * buf,ft_offset_t text_start_offset,const char * text)756 void apply_text_tag(const struct text_tag *ptag, GtkTextBuffer *buf,
757                     ft_offset_t text_start_offset, const char *text)
758 {
759   static bool initalized = FALSE;
760   GtkTextIter start, stop;
761 
762   if (!initalized) {
763     gtk_text_buffer_create_tag(buf, "bold",
764                                "weight", PANGO_WEIGHT_BOLD, NULL);
765     gtk_text_buffer_create_tag(buf, "italic",
766                                "style", PANGO_STYLE_ITALIC, NULL);
767     gtk_text_buffer_create_tag(buf, "strike",
768                                "strikethrough", TRUE, NULL);
769     gtk_text_buffer_create_tag(buf, "underline",
770                                "underline", PANGO_UNDERLINE_SINGLE, NULL);
771     initalized = TRUE;
772   }
773 
774   /* Get the position. */
775   /*
776    * N.B.: text_tag_*_offset() value is in bytes, so we need to convert it
777    * to utf8 character offset.
778    */
779   gtk_text_buffer_get_iter_at_offset(buf, &start, text_start_offset
780                                      + g_utf8_pointer_to_offset(text,
781                                      text + text_tag_start_offset(ptag)));
782   if (text_tag_stop_offset(ptag) == FT_OFFSET_UNSET) {
783     gtk_text_buffer_get_end_iter(buf, &stop);
784   } else {
785     gtk_text_buffer_get_iter_at_offset(buf, &stop, text_start_offset
786                                        + g_utf8_pointer_to_offset(text,
787                                        text + text_tag_stop_offset(ptag)));
788   }
789 
790   switch (text_tag_type(ptag)) {
791   case TTT_BOLD:
792     gtk_text_buffer_apply_tag_by_name(buf, "bold", &start, &stop);
793     break;
794   case TTT_ITALIC:
795     gtk_text_buffer_apply_tag_by_name(buf, "italic", &start, &stop);
796     break;
797   case TTT_STRIKE:
798     gtk_text_buffer_apply_tag_by_name(buf, "strike", &start, &stop);
799     break;
800   case TTT_UNDERLINE:
801     gtk_text_buffer_apply_tag_by_name(buf, "underline", &start, &stop);
802     break;
803   case TTT_COLOR:
804     {
805       /* We have to make a new tag every time. */
806       GtkTextTag *tag = NULL;
807       const char *foreground = text_tag_color_foreground(ptag);
808       const char *background = text_tag_color_background(ptag);
809 
810       if (foreground && foreground[0]) {
811         if (background && background[0]) {
812           tag = gtk_text_buffer_create_tag(buf, NULL,
813                                            "foreground", foreground,
814                                            "background", background,
815                                            NULL);
816         } else {
817           tag = gtk_text_buffer_create_tag(buf, NULL,
818                                            "foreground", foreground,
819                                            NULL);
820         }
821       } else if (background && background[0]) {
822         tag = gtk_text_buffer_create_tag(buf, NULL,
823                                          "background", background,
824                                          NULL);
825       }
826 
827       if (!tag) {
828         break; /* No color. */
829       }
830       gtk_text_buffer_apply_tag(buf, tag, &start, &stop);
831     }
832     break;
833   case TTT_LINK:
834     {
835       struct color *pcolor = NULL;
836       GtkTextTag *tag;
837 
838       switch (text_tag_link_type(ptag)) {
839       case TLT_CITY:
840         pcolor = get_color(tileset, COLOR_MAPVIEW_CITY_LINK);
841         break;
842       case TLT_TILE:
843         pcolor = get_color(tileset, COLOR_MAPVIEW_TILE_LINK);
844         break;
845       case TLT_UNIT:
846         pcolor = get_color(tileset, COLOR_MAPVIEW_UNIT_LINK);
847         break;
848       }
849 
850       if (!pcolor) {
851         break; /* Not a valid link type case. */
852       }
853 
854       tag = gtk_text_buffer_create_tag(buf, NULL,
855                                        "foreground-rgba", &pcolor->color,
856                                        "underline", PANGO_UNDERLINE_SINGLE,
857                                        NULL);
858 
859       /* Type 0 is reserved for non-link tags.  So, add 1 to the
860        * type value. */
861       g_object_set_data(G_OBJECT(tag), "type",
862                         GINT_TO_POINTER(text_tag_link_type(ptag) + 1));
863       g_object_set_data(G_OBJECT(tag), "id",
864                         GINT_TO_POINTER(text_tag_link_id(ptag)));
865       gtk_text_buffer_apply_tag(buf, tag, &start, &stop);
866       break;
867     }
868   }
869 }
870 
871 /**************************************************************************
872   Appends the string to the chat output window.  The string should be
873   inserted on its own line, although it will have no newline.
874 **************************************************************************/
real_output_window_append(const char * astring,const struct text_tag_list * tags,int conn_id)875 void real_output_window_append(const char *astring,
876                                const struct text_tag_list *tags,
877                                int conn_id)
878 {
879   GtkTextBuffer *buf;
880   GtkTextIter iter;
881   GtkTextMark *mark;
882   ft_offset_t text_start_offset;
883 
884   buf = message_buffer;
885 
886   if (buf == NULL) {
887     log_error("Output when no message buffer: %s", astring);
888 
889     return;
890   }
891 
892   gtk_text_buffer_get_end_iter(buf, &iter);
893   gtk_text_buffer_insert(buf, &iter, "\n", -1);
894   mark = gtk_text_buffer_create_mark(buf, NULL, &iter, TRUE);
895 
896   if (GUI_GTK_OPTION(show_chat_message_time)) {
897     char timebuf[64];
898     time_t now;
899     struct tm *now_tm;
900 
901     now = time(NULL);
902     now_tm = localtime(&now);
903     strftime(timebuf, sizeof(timebuf), "[%H:%M:%S] ", now_tm);
904     gtk_text_buffer_insert(buf, &iter, timebuf, -1);
905   }
906 
907   text_start_offset = gtk_text_iter_get_offset(&iter);
908   gtk_text_buffer_insert(buf, &iter, astring, -1);
909   text_tag_list_iterate(tags, ptag) {
910     apply_text_tag(ptag, buf, text_start_offset, astring);
911   } text_tag_list_iterate_end;
912 
913   if (main_message_area) {
914     scroll_if_necessary(GTK_TEXT_VIEW(main_message_area), mark);
915   }
916   if (start_message_area) {
917     scroll_if_necessary(GTK_TEXT_VIEW(start_message_area), mark);
918   }
919   gtk_text_buffer_delete_mark(buf, mark);
920 
921   append_network_statusbar(astring, FALSE);
922 }
923 
924 /**************************************************************************
925  I have no idea what module this belongs in -- Syela
926  I've decided to put output_window routines in chatline.c, because
927  the are somewhat related and output_window_* is already here.  --dwp
928 **************************************************************************/
log_output_window(void)929 void log_output_window(void)
930 {
931   GtkTextIter start, end;
932   gchar *txt;
933 
934   gtk_text_buffer_get_bounds(message_buffer, &start, &end);
935   txt = gtk_text_buffer_get_text(message_buffer, &start, &end, TRUE);
936 
937   write_chatline_content(txt);
938   g_free(txt);
939 }
940 
941 /**************************************************************************
942   Clear output window. This does *not* destroy it, or free its resources
943 **************************************************************************/
clear_output_window(void)944 void clear_output_window(void)
945 {
946   set_output_window_text(_("Cleared output window."));
947 }
948 
949 /**************************************************************************
950   Set given text to output window
951 **************************************************************************/
set_output_window_text(const char * text)952 void set_output_window_text(const char *text)
953 {
954   gtk_text_buffer_set_text(message_buffer, text, -1);
955 }
956 
957 /**************************************************************************
958   Returns whether the chatline is scrolled to the bottom.
959 **************************************************************************/
chatline_is_scrolled_to_bottom(void)960 bool chatline_is_scrolled_to_bottom(void)
961 {
962   GtkWidget *sw, *w;
963   GtkAdjustment *vadj;
964   gdouble val, max, upper, page_size;
965 
966   if (get_client_page() == PAGE_GAME) {
967     w = GTK_WIDGET(main_message_area);
968   } else {
969     w = GTK_WIDGET(start_message_area);
970   }
971 
972   if (w == NULL) {
973     return TRUE;
974   }
975 
976   sw = gtk_widget_get_parent(w);
977   vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
978   val = gtk_adjustment_get_value(vadj);
979   g_object_get(G_OBJECT(vadj), "upper", &upper,
980                "page-size", &page_size, NULL);
981   max = upper - page_size;
982 
983   /* Approximation. */
984   return max - val < 0.00000001;
985 }
986 
987 /**************************************************************************
988   Scrolls the pregame and in-game chat windows all the way to the bottom.
989 
990   Why do we do it in such a convuluted fasion rather than calling
991   chatline_scroll_to_bottom directly from toplevel_configure?
992   Because the widget is not at its final size yet when the configure
993   event occurs.
994 **************************************************************************/
chatline_scroll_callback(gpointer data)995 static gboolean chatline_scroll_callback(gpointer data)
996 {
997   chatline_scroll_to_bottom(FALSE);     /* Not delayed this time! */
998 
999   *((guint *) data) = 0;
1000   return FALSE;         /* Remove this idle function. */
1001 }
1002 
1003 /**************************************************************************
1004   Scrolls the pregame and in-game chat windows all the way to the bottom.
1005   If delayed is TRUE, it will be done in a idle_callback.
1006 **************************************************************************/
chatline_scroll_to_bottom(bool delayed)1007 void chatline_scroll_to_bottom(bool delayed)
1008 {
1009   static guint callback_id = 0;
1010 
1011   if (delayed) {
1012     if (callback_id == 0) {
1013       callback_id = g_idle_add(chatline_scroll_callback, &callback_id);
1014     }
1015   } else if (message_buffer) {
1016     GtkTextIter end;
1017 
1018     gtk_text_buffer_get_end_iter(message_buffer, &end);
1019 
1020     if (main_message_area) {
1021       gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(main_message_area),
1022                                    &end, 0.0, TRUE, 1.0, 0.0);
1023     }
1024     if (start_message_area) {
1025       gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(start_message_area),
1026                                    &end, 0.0, TRUE, 1.0, 0.0);
1027     }
1028   }
1029 }
1030 
1031 /**************************************************************************
1032   Tool button clicked.
1033 **************************************************************************/
make_tag_callback(GtkToolButton * button,gpointer data)1034 static void make_tag_callback(GtkToolButton *button, gpointer data)
1035 {
1036   inputline_make_tag(GTK_ENTRY(data),
1037                      GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button),
1038                                                        "text_tag_type")));
1039 }
1040 
1041 /**************************************************************************
1042   Set the color for an object.  Update the button if not NULL.
1043 **************************************************************************/
color_set(GObject * object,const gchar * color_target,GdkRGBA * color,GtkToolButton * button)1044 static void color_set(GObject *object, const gchar *color_target,
1045                       GdkRGBA *color, GtkToolButton *button)
1046 {
1047   GdkRGBA *current_color = g_object_get_data(object, color_target);
1048 
1049   if (NULL == color) {
1050     /* Clears the current color. */
1051     if (NULL != current_color) {
1052       gdk_rgba_free(current_color);
1053       g_object_set_data(object, color_target, NULL);
1054       if (NULL != button) {
1055         gtk_tool_button_set_icon_widget(button, NULL);
1056       }
1057     }
1058   } else {
1059     /* Apply the new color. */
1060     if (NULL != current_color) {
1061       /* We already have a GdkRGBA pointer. */
1062       *current_color = *color;
1063     } else {
1064       /* We need to make a GdkRGBA pointer. */
1065       current_color = gdk_rgba_copy(color);
1066       g_object_set_data(object, color_target, current_color);
1067     }
1068 
1069     if (NULL != button) {
1070       /* Update the button. */
1071       GdkPixbuf *pixbuf;
1072       GtkWidget *image;
1073 
1074       {
1075         cairo_surface_t *surface = cairo_image_surface_create(
1076             CAIRO_FORMAT_RGB24, 16, 16);
1077         cairo_t *cr = cairo_create(surface);
1078         gdk_cairo_set_source_rgba(cr, current_color);
1079         cairo_paint(cr);
1080         cairo_destroy(cr);
1081         pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, 16, 16);
1082         cairo_surface_destroy(surface);
1083       }
1084       image = gtk_image_new_from_pixbuf(pixbuf);
1085       gtk_tool_button_set_icon_widget(button, image);
1086       gtk_widget_show(image);
1087       g_object_unref(G_OBJECT(pixbuf));
1088     }
1089   }
1090 }
1091 
1092 /**************************************************************************
1093   Color selection dialog response.
1094 **************************************************************************/
color_selected(GtkDialog * dialog,gint res,gpointer data)1095 static void color_selected(GtkDialog *dialog, gint res, gpointer data)
1096 {
1097   GtkToolButton *button =
1098     GTK_TOOL_BUTTON(g_object_get_data(G_OBJECT(dialog), "button"));
1099   const gchar *color_target =
1100     g_object_get_data(G_OBJECT(button), "color_target");
1101 
1102   if (res == GTK_RESPONSE_REJECT) {
1103     /* Clears the current color. */
1104     color_set(G_OBJECT(data), color_target, NULL, button);
1105   } else if (res == GTK_RESPONSE_OK) {
1106     /* Apply the new color. */
1107     GtkColorChooser *chooser =
1108       GTK_COLOR_CHOOSER(g_object_get_data(G_OBJECT(dialog), "chooser"));
1109     GdkRGBA new_color;
1110 
1111     gtk_color_chooser_get_rgba(chooser, &new_color);
1112     color_set(G_OBJECT(data), color_target, &new_color, button);
1113   }
1114 
1115   gtk_widget_destroy(GTK_WIDGET(dialog));
1116 }
1117 
1118 /**************************************************************************
1119   Color selection tool button clicked.
1120 **************************************************************************/
select_color_callback(GtkToolButton * button,gpointer data)1121 static void select_color_callback(GtkToolButton *button, gpointer data)
1122 {
1123   GtkWidget *dialog, *chooser;
1124   /* "fg_color" or "bg_color". */
1125   const gchar *color_target = g_object_get_data(G_OBJECT(button),
1126                                                 "color_target");
1127   GdkRGBA *current_color = g_object_get_data(G_OBJECT(data), color_target);
1128 
1129   /* TRANS: "text" or "background". */
1130   gchar *buf = g_strdup_printf(_("Select the %s color"),
1131               (const char *) g_object_get_data(G_OBJECT(button),
1132                                                "color_info"));
1133   dialog = gtk_dialog_new_with_buttons(buf, NULL, GTK_DIALOG_MODAL,
1134                                        _("_Cancel"), GTK_RESPONSE_CANCEL,
1135                                        _("C_lear"), GTK_RESPONSE_REJECT,
1136                                        _("_OK"), GTK_RESPONSE_OK, NULL);
1137   setup_dialog(dialog, toplevel);
1138   g_object_set_data(G_OBJECT(dialog), "button", button);
1139   g_signal_connect(dialog, "response", G_CALLBACK(color_selected), data);
1140 
1141   chooser = gtk_color_chooser_widget_new();
1142   gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
1143                      chooser, FALSE, FALSE, 0);
1144   g_object_set_data(G_OBJECT(dialog), "chooser", chooser);
1145   if (current_color) {
1146     gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(chooser), current_color);
1147   }
1148 
1149   gtk_widget_show_all(dialog);
1150   g_free(buf);
1151 }
1152 
1153 /**************************************************************************
1154   Moves the tool kit to the toolkit view.
1155 **************************************************************************/
move_toolkit(GtkWidget * toolkit_view,gpointer data)1156 static gboolean move_toolkit(GtkWidget *toolkit_view,
1157                              gpointer data)
1158 {
1159   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1160   GtkWidget *parent = gtk_widget_get_parent(ptoolkit->main_widget);
1161   GtkWidget *button_box = GTK_WIDGET(g_object_get_data(G_OBJECT(toolkit_view),
1162                                                        "button_box"));
1163   GList *list, *iter;
1164 
1165   if (parent) {
1166     if (parent == toolkit_view) {
1167       return FALSE;     /* Already owned. */
1168     }
1169 
1170     /* N.B.: We need to hide/show the toolbar to reset the sensitivity
1171      * of the tool buttons. */
1172     if (ptoolkit->toolbar_displayed) {
1173       gtk_widget_hide(ptoolkit->toolbar);
1174     }
1175     g_object_ref(ptoolkit->main_widget); /* Make sure reference count stays above 0
1176                                           * during the transition to new parent. */
1177     gtk_container_remove(GTK_CONTAINER(parent), ptoolkit->main_widget);
1178     gtk_container_add(GTK_CONTAINER(toolkit_view), ptoolkit->main_widget);
1179     g_object_unref(ptoolkit->main_widget);
1180     if (ptoolkit->toolbar_displayed) {
1181       gtk_widget_show(ptoolkit->toolbar);
1182     }
1183 
1184     if (!gtk_widget_get_parent(button_box)) {
1185       /* Attach to the toolkit button_box. */
1186       gtk_container_add(GTK_CONTAINER(ptoolkit->button_box), button_box);
1187     }
1188     gtk_widget_show_all(ptoolkit->main_widget);
1189     if (!ptoolkit->toolbar_displayed) {
1190       gtk_widget_hide(ptoolkit->toolbar);
1191     }
1192 
1193     /* Hide all other buttons boxes. */
1194     list = gtk_container_get_children(GTK_CONTAINER(ptoolkit->button_box));
1195     for (iter = list; iter != NULL; iter = g_list_next(iter)) {
1196       GtkWidget *widget = GTK_WIDGET(iter->data);
1197 
1198       if (widget != button_box) {
1199         gtk_widget_hide(widget);
1200       }
1201     }
1202     g_list_free(list);
1203 
1204   } else {
1205     /* First time attached to a parent. */
1206     gtk_container_add(GTK_CONTAINER(toolkit_view), ptoolkit->main_widget);
1207     gtk_container_add(GTK_CONTAINER(ptoolkit->button_box), button_box);
1208     gtk_widget_show_all(ptoolkit->main_widget);
1209   }
1210 
1211   return FALSE;
1212 }
1213 
1214 /**************************************************************************
1215   Show/Hide the toolbar.
1216 **************************************************************************/
set_toolbar_visibility(GtkWidget * w,gpointer data)1217 static gboolean set_toolbar_visibility(GtkWidget *w,
1218                                        gpointer data)
1219 {
1220   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1221   GtkToggleButton *button = GTK_TOGGLE_BUTTON(toolkit.toggle_button);
1222 
1223   if (ptoolkit->toolbar_displayed) {
1224     if (!gtk_toggle_button_get_active(button)) {
1225       /* button_toggled() will be called and the toolbar shown. */
1226       gtk_toggle_button_set_active(button, TRUE);
1227     } else {
1228       /* Unsure the widget is visible. */
1229       gtk_widget_show(ptoolkit->toolbar);
1230     }
1231   } else {
1232     if (gtk_toggle_button_get_active(button)) {
1233       /* button_toggled() will be called and the toolbar hiden. */
1234       gtk_toggle_button_set_active(button, FALSE);
1235     } else {
1236       /* Unsure the widget is visible. */
1237       gtk_widget_hide(ptoolkit->toolbar);
1238     }
1239   }
1240 
1241   return FALSE;
1242 }
1243 
1244 /**************************************************************************
1245   Show/Hide the toolbar.
1246 **************************************************************************/
button_toggled(GtkToggleButton * button,gpointer data)1247 static void button_toggled(GtkToggleButton *button, gpointer data)
1248 {
1249   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1250 
1251   if (gtk_toggle_button_get_active(button)) {
1252     gtk_widget_show(ptoolkit->toolbar);
1253     ptoolkit->toolbar_displayed = TRUE;
1254     if (chatline_is_scrolled_to_bottom()) {
1255       /* Make sure to be still at the end. */
1256       chatline_scroll_to_bottom(TRUE);
1257     }
1258   } else {
1259     gtk_widget_hide(ptoolkit->toolbar);
1260     ptoolkit->toolbar_displayed = FALSE;
1261   }
1262 }
1263 
1264 /**************************************************************************
1265   Returns a new inputline toolkit view widget that can contain the
1266   inputline.
1267 
1268   This widget has the following datas:
1269   "button_box": pointer to the GtkHBox where to append buttons.
1270 **************************************************************************/
inputline_toolkit_view_new(void)1271 GtkWidget *inputline_toolkit_view_new(void)
1272 {
1273   GtkWidget *toolkit_view, *bbox;
1274 
1275   /* Main widget. */
1276   toolkit_view = gtk_grid_new();
1277   gtk_orientable_set_orientation(GTK_ORIENTABLE(toolkit_view),
1278                                  GTK_ORIENTATION_VERTICAL);
1279   g_signal_connect_after(toolkit_view, "map",
1280                    G_CALLBACK(move_toolkit), &toolkit);
1281 
1282   /* Button box. */
1283   bbox = gtk_grid_new();
1284   gtk_grid_set_column_spacing(GTK_GRID(bbox), 12);
1285   g_object_set_data(G_OBJECT(toolkit_view), "button_box", bbox);
1286 
1287   return toolkit_view;
1288 }
1289 
1290 /**************************************************************************
1291   Appends a button to the inputline toolkit view widget.
1292 **************************************************************************/
inputline_toolkit_view_append_button(GtkWidget * toolkit_view,GtkWidget * button)1293 void inputline_toolkit_view_append_button(GtkWidget *toolkit_view,
1294                                           GtkWidget *button)
1295 {
1296   gtk_container_add(GTK_CONTAINER(g_object_get_data(G_OBJECT(toolkit_view),
1297                     "button_box")), button);
1298 }
1299 
1300 /**************************************************************************
1301   Initializes the chatline stuff.
1302 **************************************************************************/
chatline_init(void)1303 void chatline_init(void)
1304 {
1305   GtkWidget *vbox, *toolbar, *hbox, *button, *entry, *bbox;
1306   GtkToolItem *item;
1307   GdkRGBA color;
1308 
1309   /* Chatline history. */
1310   if (!history_list) {
1311     history_list = genlist_new();
1312     history_pos = -1;
1313   }
1314 
1315   /* Inputline toolkit. */
1316   memset(&toolkit, 0, sizeof(toolkit));
1317 
1318   vbox = gtk_grid_new();
1319   gtk_grid_set_row_spacing(GTK_GRID(vbox), 2);
1320   gtk_orientable_set_orientation(GTK_ORIENTABLE(vbox),
1321                                  GTK_ORIENTATION_VERTICAL);
1322   toolkit.main_widget = vbox;
1323   g_signal_connect_after(vbox, "map",
1324                    G_CALLBACK(set_toolbar_visibility), &toolkit);
1325 
1326   entry = gtk_entry_new();
1327   g_object_set(entry, "margin", 2, NULL);
1328   gtk_widget_set_hexpand(entry, TRUE);
1329   toolkit.entry = entry;
1330 
1331   /* First line: toolbar */
1332   toolbar = gtk_toolbar_new();
1333   gtk_container_add(GTK_CONTAINER(vbox), toolbar);
1334   gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
1335   gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar), FALSE);
1336   gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);
1337   gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar),
1338                                  GTK_ORIENTATION_HORIZONTAL);
1339   toolkit.toolbar = toolbar;
1340 
1341   /* Bold button. */
1342   item = gtk_tool_button_new(gtk_image_new_from_icon_name("format-text-bold", 0),
1343                              _("Bold"));
1344 
1345   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1346   g_object_set_data(G_OBJECT(item), "text_tag_type",
1347                     GINT_TO_POINTER(TTT_BOLD));
1348   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1349   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Bold (Ctrl-B)"));
1350 
1351   /* Italic button. */
1352   item = gtk_tool_button_new(gtk_image_new_from_icon_name("format-text-italic", 0),
1353                              _("Italic"));
1354   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1355   g_object_set_data(G_OBJECT(item), "text_tag_type",
1356                     GINT_TO_POINTER(TTT_ITALIC));
1357   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1358   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Italic (Ctrl-I)"));
1359 
1360   /* Strike button. */
1361   item = gtk_tool_button_new(gtk_image_new_from_icon_name("format-text-strikethrough", 0),
1362                              _("Strikethrough"));
1363   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1364   g_object_set_data(G_OBJECT(item), "text_tag_type",
1365                     GINT_TO_POINTER(TTT_STRIKE));
1366   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1367   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Strikethrough (Ctrl-S)"));
1368 
1369   /* Underline button. */
1370   item = gtk_tool_button_new(gtk_image_new_from_icon_name("format-text-underline", 0),
1371                              _("Underline"));
1372   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1373   g_object_set_data(G_OBJECT(item), "text_tag_type",
1374                     GINT_TO_POINTER(TTT_UNDERLINE));
1375   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1376   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Underline (Ctrl-U)"));
1377 
1378   /* Color button. */
1379   item = gtk_tool_button_new(NULL, _("Color"));
1380   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1381   g_object_set_data(G_OBJECT(item), "text_tag_type",
1382                     GINT_TO_POINTER(TTT_COLOR));
1383   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1384   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Color (Ctrl-C)"));
1385 
1386   gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
1387                      gtk_separator_tool_item_new(), -1);
1388 
1389   /* Foreground selector. */
1390   item = gtk_tool_button_new(NULL, "");
1391   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1392   g_object_set_data(G_OBJECT(item), "color_target", fc_strdup("fg_color"));
1393   g_object_set_data(G_OBJECT(item), "color_info",
1394                     fc_strdup(_("foreground")));
1395   g_signal_connect(item, "clicked",
1396                    G_CALLBACK(select_color_callback), entry);
1397   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Select the text color"));
1398   if (gdk_rgba_parse(&color, "#000000")) {
1399     /* Set default foreground color. */
1400     color_set(G_OBJECT(entry), "fg_color", &color, GTK_TOOL_BUTTON(item));
1401   } else {
1402     log_error("Failed to set the default foreground color.");
1403   }
1404 
1405   /* Background selector. */
1406   item = gtk_tool_button_new(NULL, "");
1407   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1408   g_object_set_data(G_OBJECT(item), "color_target", fc_strdup("bg_color"));
1409   g_object_set_data(G_OBJECT(item), "color_info",
1410                     fc_strdup(_("background")));
1411   g_signal_connect(item, "clicked",
1412                    G_CALLBACK(select_color_callback), entry);
1413   gtk_widget_set_tooltip_text(GTK_WIDGET(item),
1414                               _("Select the background color"));
1415   if (gdk_rgba_parse(&color, "#ffffff")) {
1416     /* Set default background color. */
1417     color_set(G_OBJECT(entry), "bg_color", &color, GTK_TOOL_BUTTON(item));
1418   } else {
1419     log_error("Failed to set the default background color.");
1420   }
1421 
1422   gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
1423                      gtk_separator_tool_item_new(), -1);
1424 
1425   /* Return button. */
1426   item = gtk_tool_button_new(NULL, _("OK"));
1427   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1428   g_signal_connect_swapped(item, "clicked",
1429                            G_CALLBACK(inputline_return), entry);
1430   gtk_widget_set_tooltip_text(GTK_WIDGET(item),
1431                               /* TRANS: "Return" means the return key. */
1432                               _("Send the chat (Return)"));
1433 
1434   /* Second line */
1435   hbox = gtk_grid_new();
1436   gtk_grid_set_column_spacing(GTK_GRID(hbox), 4);
1437   gtk_container_add(GTK_CONTAINER(vbox), hbox);
1438 
1439   /* Toggle button. */
1440   button = gtk_toggle_button_new();
1441   g_object_set(button, "margin", 2, NULL);
1442   gtk_container_add(GTK_CONTAINER(hbox), button);
1443   gtk_button_set_image(GTK_BUTTON(button),
1444                        gtk_image_new_from_icon_name("gtk-edit", 0));
1445   g_signal_connect(button, "toggled", G_CALLBACK(button_toggled), &toolkit);
1446   gtk_widget_set_tooltip_text(GTK_WIDGET(button), _("Chat tools"));
1447   toolkit.toggle_button = button;
1448 
1449   /* Entry. */
1450   gtk_container_add(GTK_CONTAINER(hbox), entry);
1451   g_signal_connect(entry, "activate", G_CALLBACK(inputline_return), NULL);
1452   g_signal_connect(entry, "key_press_event",
1453                    G_CALLBACK(inputline_handler), NULL);
1454 
1455   /* Button box. */
1456   bbox = gtk_grid_new();
1457   gtk_container_add(GTK_CONTAINER(hbox), bbox);
1458   toolkit.button_box = bbox;
1459 }
1460 
1461 /**************************************************************************
1462   Main thread side callback to print version message
1463 **************************************************************************/
version_message_main_thread(gpointer user_data)1464 static gboolean version_message_main_thread(gpointer user_data)
1465 {
1466   char *vertext = (char *)user_data;
1467 
1468   output_window_append(ftc_client, vertext);
1469 
1470   FC_FREE(vertext);
1471 
1472   return G_SOURCE_REMOVE;
1473 }
1474 
1475 /**************************************************************************
1476   Got version message from metaserver thread.
1477 **************************************************************************/
version_message(const char * vertext)1478 void version_message(const char *vertext)
1479 {
1480   int len = strlen(vertext) + 1;
1481   char *persistent = fc_malloc(len);
1482 
1483   strncpy(persistent, vertext, len);
1484 
1485   gdk_threads_add_idle(version_message_main_thread, persistent);
1486 }
1487