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 /* gui-gtk-2.0 */
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_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_options.gui_gtk2_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_b:
353       inputline_make_tag(GTK_ENTRY(w), TTT_BOLD);
354       return TRUE;
355 
356     case GDK_c:
357       inputline_make_tag(GTK_ENTRY(w), TTT_COLOR);
358       return TRUE;
359 
360     case GDK_i:
361       inputline_make_tag(GTK_ENTRY(w), TTT_ITALIC);
362       return TRUE;
363 
364     case GDK_s:
365       inputline_make_tag(GTK_ENTRY(w), TTT_STRIKE);
366       return TRUE;
367 
368     case GDK_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_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_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_Tab:
402       if (gui_options.gui_gtk2_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 
424   if (!gtk_editable_get_selection_bounds(editable, &start_pos, &end_pos)) {
425     /* Let's say the selection starts and ends at the current position. */
426     start_pos = end_pos = gtk_editable_get_position(editable);
427   }
428 
429   selection = gtk_editable_get_chars(editable, start_pos, end_pos);
430 
431   if (type == TTT_COLOR) {
432     /* Get the color arguments. */
433     char fg_color_text[32], bg_color_text[32];
434     GdkColor *fg_color = g_object_get_data(G_OBJECT(entry), "fg_color");
435     GdkColor *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     color_to_string(fg_color, fg_color_text, sizeof(fg_color_text));
442     color_to_string(bg_color, bg_color_text, sizeof(bg_color_text));
443 
444     if (0 == featured_text_apply_tag(selection, buf, sizeof(buf),
445                                      TTT_COLOR, 0, FT_OFFSET_UNSET,
446                                      ft_color_construct(fg_color_text,
447                                                         bg_color_text))) {
448       goto CLEAN_UP;
449     }
450   } else if (0 == featured_text_apply_tag(selection, buf, sizeof(buf),
451                                           type, 0, FT_OFFSET_UNSET)) {
452     goto CLEAN_UP;
453   }
454 
455   /* Replace the selection. */
456   gtk_editable_delete_text(editable, start_pos, end_pos);
457   end_pos = start_pos;
458   gtk_editable_insert_text(editable, buf, -1, &end_pos);
459   gtk_editable_select_region(editable, start_pos, end_pos);
460 
461 CLEAN_UP:
462   g_free(selection);
463 }
464 
465 /**************************************************************************
466   Make a chat link at the current position or make the current selection
467   clickable.
468 **************************************************************************/
inputline_make_chat_link(struct tile * ptile,bool unit)469 void inputline_make_chat_link(struct tile *ptile, bool unit)
470 {
471   char buf[MAX_LEN_MSG];
472   GtkWidget *entry = toolkit.entry;
473   GtkEditable *editable = GTK_EDITABLE(entry);
474   gint start_pos, end_pos;
475   gchar *chars;
476   struct unit *punit;
477 
478   /* Get the target. */
479   if (unit) {
480     punit = find_visible_unit(ptile);
481     if (!punit) {
482       output_window_append(ftc_client, _("No visible unit on this tile."));
483       return;
484     }
485   } else {
486     punit = NULL;
487   }
488 
489   if (gtk_editable_get_selection_bounds(editable, &start_pos, &end_pos)) {
490     /* There is a selection, make it clickable. */
491     gpointer target;
492     enum text_link_type type;
493 
494     chars = gtk_editable_get_chars(editable, start_pos, end_pos);
495     if (punit) {
496       type = TLT_UNIT;
497       target = punit;
498     } else if (tile_city(ptile)) {
499       type = TLT_CITY;
500       target = tile_city(ptile);
501     } else {
502       type = TLT_TILE;
503       target = ptile;
504     }
505 
506     if (0 != featured_text_apply_tag(chars, buf, sizeof(buf), TTT_LINK,
507                                      0, FT_OFFSET_UNSET, type, target)) {
508       /* Replace the selection. */
509       gtk_editable_delete_text(editable, start_pos, end_pos);
510       end_pos = start_pos;
511       gtk_editable_insert_text(editable, buf, -1, &end_pos);
512       gtk_widget_grab_focus(entry);
513       gtk_editable_select_region(editable, start_pos, end_pos);
514     }
515   } else {
516     /* Just insert the link at the current position. */
517     start_pos = gtk_editable_get_position(editable);
518     end_pos = start_pos;
519     chars = gtk_editable_get_chars(editable, MAX(start_pos - 1, 0),
520                                    start_pos + 1);
521     if (punit) {
522       sz_strlcpy(buf, unit_link(punit));
523     } else if (tile_city(ptile)) {
524       sz_strlcpy(buf, city_link(tile_city(ptile)));
525     } else {
526       sz_strlcpy(buf, tile_link(ptile));
527     }
528 
529     if (start_pos > 0 && strlen(chars) > 0 && chars[0] != ' ') {
530       /* Maybe insert an extra space. */
531       gtk_editable_insert_text(editable, " ", 1, &end_pos);
532     }
533     gtk_editable_insert_text(editable, buf, -1, &end_pos);
534     if (chars[start_pos > 0 ? 1 : 0] != '\0'
535         && chars[start_pos > 0 ? 1 : 0] != ' ') {
536       /* Maybe insert an extra space. */
537       gtk_editable_insert_text(editable, " ", 1, &end_pos);
538     }
539     gtk_widget_grab_focus(entry);
540     gtk_editable_set_position(editable, end_pos);
541   }
542 
543   g_free(chars);
544 }
545 
546 /**************************************************************************
547   Scroll a textview so that the given mark is visible, but only if the
548   scroll window containing the textview is very close to the bottom. The
549   text mark 'scroll_target' should probably be the first character of the
550   last line in the text buffer.
551 **************************************************************************/
scroll_if_necessary(GtkTextView * textview,GtkTextMark * scroll_target)552 void scroll_if_necessary(GtkTextView *textview, GtkTextMark *scroll_target)
553 {
554   GtkWidget *sw;
555   GtkAdjustment *vadj;
556   gdouble val, max, upper, page_size;
557 
558   fc_assert_ret(textview != NULL);
559   fc_assert_ret(scroll_target != NULL);
560 
561   sw = gtk_widget_get_parent(GTK_WIDGET(textview));
562   fc_assert_ret(sw != NULL);
563   fc_assert_ret(GTK_IS_SCROLLED_WINDOW(sw));
564 
565   vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
566   val = gtk_adjustment_get_value(GTK_ADJUSTMENT(vadj));
567   g_object_get(G_OBJECT(vadj), "upper", &upper,
568                "page-size", &page_size, NULL);
569   max = upper - page_size;
570   if (max - val < 10.0) {
571     gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(textview), scroll_target,
572                                  0.0, TRUE, 1.0, 0.0);
573   }
574 }
575 
576 /**************************************************************************
577   Click a link.
578 **************************************************************************/
event_after(GtkWidget * text_view,GdkEventButton * event)579 static gboolean event_after(GtkWidget *text_view, GdkEventButton *event)
580 {
581   GtkTextIter start, end, iter;
582   GtkTextBuffer *buffer;
583   GSList *tags, *tagp;
584   gint x, y;
585   struct tile *ptile = NULL;
586 
587   if (event->type != GDK_BUTTON_RELEASE || event->button != 1) {
588     return FALSE;
589   }
590 
591   buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
592 
593   /* We shouldn't follow a link if the user has selected something. */
594   gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
595   if (gtk_text_iter_get_offset(&start) != gtk_text_iter_get_offset(&end)) {
596     return FALSE;
597   }
598 
599   gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW (text_view),
600                                         GTK_TEXT_WINDOW_WIDGET,
601                                         event->x, event->y, &x, &y);
602 
603   gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(text_view), &iter, x, y);
604 
605   if ((tags = gtk_text_iter_get_tags(&iter))) {
606     for (tagp = tags; tagp; tagp = tagp->next) {
607       GtkTextTag *tag = tagp->data;
608       enum text_link_type type =
609         GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tag), "type"));
610 
611       if (type != 0) {
612         /* This is a link. */
613         int id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tag), "id"));
614         ptile = NULL;
615 
616         /* Real type is type - 1.
617          * See comment in apply_text_tag() for g_object_set_data(). */
618         type--;
619 
620         switch (type) {
621         case TLT_CITY:
622           {
623             struct city *pcity = game_city_by_number(id);
624 
625             if (pcity) {
626               ptile = client_city_tile(pcity);
627             } else {
628               output_window_append(ftc_client, _("This city isn't known!"));
629             }
630           }
631           break;
632         case TLT_TILE:
633           ptile = index_to_tile(id);
634 
635           if (!ptile) {
636             output_window_append(ftc_client,
637                                  _("This tile doesn't exist in this game!"));
638           }
639           break;
640         case TLT_UNIT:
641           {
642             struct unit *punit = game_unit_by_number(id);
643 
644             if (punit) {
645               ptile = unit_tile(punit);
646             } else {
647               output_window_append(ftc_client, _("This unit isn't known!"));
648             }
649           }
650           break;
651         }
652 
653         if (ptile) {
654           center_tile_mapcanvas(ptile);
655           link_mark_restore(type, id);
656           gtk_widget_grab_focus(GTK_WIDGET(map_canvas));
657         }
658       }
659     }
660     g_slist_free(tags);
661   }
662 
663   return FALSE;
664 }
665 
666 /**************************************************************************
667   Set the "hand" cursor when moving over a link.
668 **************************************************************************/
set_cursor_if_appropriate(GtkTextView * text_view,gint x,gint y)669 static void set_cursor_if_appropriate(GtkTextView *text_view, gint x, gint y)
670 {
671   static gboolean hovering_over_link = FALSE;
672   static GdkCursor *hand_cursor = NULL;
673   static GdkCursor *regular_cursor = NULL;
674   GSList *tags, *tagp;
675   GtkTextIter iter;
676   gboolean hovering = FALSE;
677 
678   /* Initialize the cursors. */
679   if (!hand_cursor) {
680     hand_cursor = gdk_cursor_new_for_display(
681         gdk_screen_get_display(gdk_screen_get_default()), GDK_HAND2);
682   }
683   if (!regular_cursor) {
684     regular_cursor = gdk_cursor_new_for_display(
685         gdk_screen_get_display(gdk_screen_get_default()), GDK_XTERM);
686   }
687 
688   gtk_text_view_get_iter_at_location(text_view, &iter, x, y);
689 
690   tags = gtk_text_iter_get_tags(&iter);
691   for (tagp = tags; tagp; tagp = tagp->next) {
692     enum text_link_type type =
693       GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tagp->data), "type"));
694 
695     if (type != 0) {
696       hovering = TRUE;
697       break;
698     }
699   }
700 
701   if (hovering != hovering_over_link) {
702     hovering_over_link = hovering;
703 
704     if (hovering_over_link) {
705       gdk_window_set_cursor(gtk_text_view_get_window(text_view,
706                                                      GTK_TEXT_WINDOW_TEXT),
707                             hand_cursor);
708     } else {
709       gdk_window_set_cursor(gtk_text_view_get_window(text_view,
710                                                      GTK_TEXT_WINDOW_TEXT),
711                             regular_cursor);
712     }
713   }
714 
715   if (tags) {
716     g_slist_free(tags);
717   }
718 }
719 
720 /**************************************************************************
721   Maybe are the mouse is moving over a link.
722 **************************************************************************/
motion_notify_event(GtkWidget * text_view,GdkEventMotion * event)723 static gboolean motion_notify_event(GtkWidget *text_view,
724                                     GdkEventMotion *event)
725 {
726   gint x, y;
727 
728   gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(text_view),
729                                         GTK_TEXT_WINDOW_WIDGET,
730                                         event->x, event->y, &x, &y);
731   set_cursor_if_appropriate(GTK_TEXT_VIEW(text_view), x, y);
732   gdk_window_get_pointer(text_view->window, NULL, NULL, NULL);
733 
734   return FALSE;
735 }
736 
737 /**************************************************************************
738   Maybe are the mouse is moving over a link.
739 **************************************************************************/
visibility_notify_event(GtkWidget * text_view,GdkEventVisibility * event)740 static gboolean visibility_notify_event(GtkWidget *text_view,
741                                         GdkEventVisibility *event)
742 {
743   gint wx, wy, bx, by;
744 
745   gdk_window_get_pointer(text_view->window, &wx, &wy, NULL);
746   gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW (text_view),
747                                         GTK_TEXT_WINDOW_WIDGET,
748                                         wx, wy, &bx, &by);
749   set_cursor_if_appropriate(GTK_TEXT_VIEW(text_view), bx, by);
750 
751   return FALSE;
752 }
753 
754 /**************************************************************************
755   Set the appropriate callbacks for the message buffer.
756 **************************************************************************/
set_message_buffer_view_link_handlers(GtkWidget * view)757 void set_message_buffer_view_link_handlers(GtkWidget *view)
758 {
759   g_signal_connect(view, "event-after",
760 		   G_CALLBACK(event_after), NULL);
761   g_signal_connect(view, "motion-notify-event",
762 		   G_CALLBACK(motion_notify_event), NULL);
763   g_signal_connect(view, "visibility-notify-event",
764 		   G_CALLBACK(visibility_notify_event), NULL);
765 
766 }
767 
768 /**************************************************************************
769   Convert a struct text_tag to a GtkTextTag.
770 **************************************************************************/
apply_text_tag(const struct text_tag * ptag,GtkTextBuffer * buf,ft_offset_t text_start_offset,const char * text)771 void apply_text_tag(const struct text_tag *ptag, GtkTextBuffer *buf,
772                     ft_offset_t text_start_offset, const char *text)
773 {
774   static bool initalized = FALSE;
775   GtkTextIter start, stop;
776 
777   if (!initalized) {
778     gtk_text_buffer_create_tag(buf, "bold",
779                                "weight", PANGO_WEIGHT_BOLD, NULL);
780     gtk_text_buffer_create_tag(buf, "italic",
781                                "style", PANGO_STYLE_ITALIC, NULL);
782     gtk_text_buffer_create_tag(buf, "strike",
783                                "strikethrough", TRUE, NULL);
784     gtk_text_buffer_create_tag(buf, "underline",
785                                "underline", PANGO_UNDERLINE_SINGLE, NULL);
786     initalized = TRUE;
787   }
788 
789   /* Get the position. */
790   /*
791    * N.B.: text_tag_*_offset() value is in bytes, so we need to convert it
792    * to utf8 character offset.
793    */
794   gtk_text_buffer_get_iter_at_offset(buf, &start, text_start_offset
795                                      + g_utf8_pointer_to_offset(text,
796                                      text + text_tag_start_offset(ptag)));
797   if (text_tag_stop_offset(ptag) == FT_OFFSET_UNSET) {
798     gtk_text_buffer_get_end_iter(buf, &stop);
799   } else {
800     gtk_text_buffer_get_iter_at_offset(buf, &stop, text_start_offset
801                                        + g_utf8_pointer_to_offset(text,
802                                        text + text_tag_stop_offset(ptag)));
803   }
804 
805   switch (text_tag_type(ptag)) {
806   case TTT_BOLD:
807     gtk_text_buffer_apply_tag_by_name(buf, "bold", &start, &stop);
808     break;
809   case TTT_ITALIC:
810     gtk_text_buffer_apply_tag_by_name(buf, "italic", &start, &stop);
811     break;
812   case TTT_STRIKE:
813     gtk_text_buffer_apply_tag_by_name(buf, "strike", &start, &stop);
814     break;
815   case TTT_UNDERLINE:
816     gtk_text_buffer_apply_tag_by_name(buf, "underline", &start, &stop);
817     break;
818   case TTT_COLOR:
819     {
820       /* We have to make a new tag every time. */
821       GtkTextTag *tag = NULL;
822       GdkColor foreground;
823       GdkColor background;
824 
825       if (gdk_color_parse(text_tag_color_foreground(ptag), &foreground)) {
826         if (gdk_color_parse(text_tag_color_background(ptag),
827                             &background)) {
828           tag = gtk_text_buffer_create_tag(buf, NULL,
829                                            "foreground-gdk", &foreground,
830                                            "background-gdk", &background,
831                                            NULL);
832         } else {
833           tag = gtk_text_buffer_create_tag(buf, NULL,
834                                            "foreground-gdk", &foreground,
835                                            NULL);
836         }
837       } else if (gdk_color_parse(text_tag_color_background(ptag),
838                                  &background)) {
839         tag = gtk_text_buffer_create_tag(buf, NULL,
840                                          "background-gdk", &background,
841                                          NULL);
842       }
843 
844       if (!tag) {
845         break; /* No color. */
846       }
847       gtk_text_buffer_apply_tag(buf, tag, &start, &stop);
848     }
849     break;
850   case TTT_LINK:
851     {
852       struct color *pcolor = NULL;
853       GtkTextTag *tag;
854 
855       switch (text_tag_link_type(ptag)) {
856       case TLT_CITY:
857         pcolor = get_color(tileset, COLOR_MAPVIEW_CITY_LINK);
858         break;
859       case TLT_TILE:
860         pcolor = get_color(tileset, COLOR_MAPVIEW_TILE_LINK);
861         break;
862       case TLT_UNIT:
863         pcolor = get_color(tileset, COLOR_MAPVIEW_UNIT_LINK);
864         break;
865       }
866 
867       if (!pcolor) {
868         break; /* Not a valid link type case. */
869       }
870 
871       tag = gtk_text_buffer_create_tag(buf, NULL,
872                                        "foreground-gdk", &pcolor->color,
873                                        "underline", PANGO_UNDERLINE_SINGLE,
874                                        NULL);
875 
876       /* Type 0 is reserved for non-link tags.  So, add 1 to the
877        * type value. */
878       g_object_set_data(G_OBJECT(tag), "type",
879                         GINT_TO_POINTER(text_tag_link_type(ptag) + 1));
880       g_object_set_data(G_OBJECT(tag), "id",
881                         GINT_TO_POINTER(text_tag_link_id(ptag)));
882       gtk_text_buffer_apply_tag(buf, tag, &start, &stop);
883       break;
884     }
885   }
886 }
887 
888 /**************************************************************************
889   Appends the string to the chat output window.  The string should be
890   inserted on its own line, although it will have no newline.
891 **************************************************************************/
real_output_window_append(const char * astring,const struct text_tag_list * tags,int conn_id)892 void real_output_window_append(const char *astring,
893                                const struct text_tag_list *tags,
894                                int conn_id)
895 {
896   GtkTextBuffer *buf;
897   GtkTextIter iter;
898   GtkTextMark *mark;
899   ft_offset_t text_start_offset;
900 
901   buf = message_buffer;
902 
903   if (buf == NULL) {
904     log_error("Output when no message buffer: %s", astring);
905 
906     return;
907   }
908 
909   gtk_text_buffer_get_end_iter(buf, &iter);
910   gtk_text_buffer_insert(buf, &iter, "\n", -1);
911   mark = gtk_text_buffer_create_mark(buf, NULL, &iter, TRUE);
912 
913   if (gui_options.gui_gtk2_show_chat_message_time) {
914     char timebuf[64];
915     time_t now;
916     struct tm *now_tm;
917 
918     now = time(NULL);
919     now_tm = localtime(&now);
920     strftime(timebuf, sizeof(timebuf), "[%H:%M:%S] ", now_tm);
921     gtk_text_buffer_insert(buf, &iter, timebuf, -1);
922   }
923 
924   text_start_offset = gtk_text_iter_get_offset(&iter);
925   gtk_text_buffer_insert(buf, &iter, astring, -1);
926   text_tag_list_iterate(tags, ptag) {
927     apply_text_tag(ptag, buf, text_start_offset, astring);
928   } text_tag_list_iterate_end;
929 
930   if (main_message_area) {
931     scroll_if_necessary(GTK_TEXT_VIEW(main_message_area), mark);
932   }
933   if (start_message_area) {
934     scroll_if_necessary(GTK_TEXT_VIEW(start_message_area), mark);
935   }
936   gtk_text_buffer_delete_mark(buf, mark);
937 
938   append_network_statusbar(astring, FALSE);
939 }
940 
941 /**************************************************************************
942  I have no idea what module this belongs in -- Syela
943  I've decided to put output_window routines in chatline.c, because
944  the are somewhat related and output_window_* is already here.  --dwp
945 **************************************************************************/
log_output_window(void)946 void log_output_window(void)
947 {
948   GtkTextIter start, end;
949   gchar *txt;
950 
951   gtk_text_buffer_get_bounds(message_buffer, &start, &end);
952   txt = gtk_text_buffer_get_text(message_buffer, &start, &end, TRUE);
953 
954   write_chatline_content(txt);
955   g_free(txt);
956 }
957 
958 /**************************************************************************
959   Clear output window. This does *not* destroy it, or free its resources
960 **************************************************************************/
clear_output_window(void)961 void clear_output_window(void)
962 {
963   set_output_window_text(_("Cleared output window."));
964 }
965 
966 /**************************************************************************
967   Set given text to output window
968 **************************************************************************/
set_output_window_text(const char * text)969 void set_output_window_text(const char *text)
970 {
971   gtk_text_buffer_set_text(message_buffer, text, -1);
972 }
973 
974 /**************************************************************************
975   Returns whether the chatline is scrolled to the bottom.
976 **************************************************************************/
chatline_is_scrolled_to_bottom(void)977 bool chatline_is_scrolled_to_bottom(void)
978 {
979   GtkWidget *sw, *w;
980   GtkAdjustment *vadj;
981   gdouble val, max, upper, page_size;
982 
983   if (get_client_page() == PAGE_GAME) {
984     w = GTK_WIDGET(main_message_area);
985   } else {
986     w = GTK_WIDGET(start_message_area);
987   }
988 
989   if (w == NULL) {
990     return TRUE;
991   }
992 
993   sw = gtk_widget_get_parent(w);
994   vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
995   val = gtk_adjustment_get_value(GTK_ADJUSTMENT(vadj));
996   g_object_get(G_OBJECT(vadj), "upper", &upper,
997                "page-size", &page_size, NULL);
998   max = upper - page_size;
999 
1000   /* Approximation. */
1001   return max - val < 0.00000001;
1002 }
1003 
1004 /**************************************************************************
1005   Scrolls the pregame and in-game chat windows all the way to the bottom.
1006 
1007   Why do we do it in such a convuluted fasion rather than calling
1008   chatline_scroll_to_bottom directly from toplevel_configure?
1009   Because the widget is not at its final size yet when the configure
1010   event occurs.
1011 **************************************************************************/
chatline_scroll_callback(gpointer data)1012 static gboolean chatline_scroll_callback(gpointer data)
1013 {
1014   chatline_scroll_to_bottom(FALSE);     /* Not delayed this time! */
1015 
1016   *((guint *) data) = 0;
1017   return FALSE;         /* Remove this idle function. */
1018 }
1019 
1020 /**************************************************************************
1021   Scrolls the pregame and in-game chat windows all the way to the bottom.
1022   If delayed is TRUE, it will be done in a idle_callback.
1023 **************************************************************************/
chatline_scroll_to_bottom(bool delayed)1024 void chatline_scroll_to_bottom(bool delayed)
1025 {
1026   static guint callback_id = 0;
1027 
1028   if (delayed) {
1029     if (callback_id == 0) {
1030       callback_id = g_idle_add(chatline_scroll_callback, &callback_id);
1031     }
1032   } else if (message_buffer) {
1033     GtkTextIter end;
1034 
1035     gtk_text_buffer_get_end_iter(message_buffer, &end);
1036 
1037     if (main_message_area) {
1038       gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(main_message_area),
1039                                    &end, 0.0, TRUE, 1.0, 0.0);
1040     }
1041     if (start_message_area) {
1042       gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(start_message_area),
1043                                    &end, 0.0, TRUE, 1.0, 0.0);
1044     }
1045   }
1046 }
1047 
1048 /**************************************************************************
1049   Tool button clicked.
1050 **************************************************************************/
make_tag_callback(GtkToolButton * button,gpointer data)1051 static void make_tag_callback(GtkToolButton *button, gpointer data)
1052 {
1053   inputline_make_tag(GTK_ENTRY(data),
1054                      GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button),
1055                                                        "text_tag_type")));
1056 }
1057 
1058 /**************************************************************************
1059   Set the color for an object.  Update the button if not NULL.
1060 **************************************************************************/
color_set(GObject * object,const gchar * color_target,GdkColor * color,GtkToolButton * button)1061 static void color_set(GObject *object, const gchar *color_target,
1062                       GdkColor *color, GtkToolButton *button)
1063 {
1064   GdkColor *current_color = g_object_get_data(object, color_target);
1065   GdkColormap *colormap = gdk_colormap_get_system();
1066 
1067   if (NULL == color) {
1068     /* Clears the current color. */
1069     if (NULL != current_color) {
1070       gdk_colormap_free_colors(colormap, current_color, 1);
1071       gdk_color_free(current_color);
1072       g_object_set_data(object, color_target, NULL);
1073       if (NULL != button) {
1074         gtk_tool_button_set_icon_widget(button, NULL);
1075       }
1076     }
1077   } else {
1078     /* Apply the new color. */
1079     if (NULL != current_color) {
1080       /* We already have a GdkColor pointer. */
1081       gdk_colormap_free_colors(colormap, current_color, 1);
1082       *current_color = *color;
1083     } else {
1084       /* We need to make a GdkColor pointer. */
1085       current_color = gdk_color_copy(color);
1086       g_object_set_data(object, color_target, current_color);
1087     }
1088 
1089     gdk_colormap_alloc_color(colormap, current_color, TRUE, TRUE);
1090     if (NULL != button) {
1091       /* Update the button. */
1092       GdkPixmap *pixmap;
1093       GtkWidget *image;
1094 
1095       pixmap = gdk_pixmap_new(root_window, 16, 16, -1);
1096       gdk_gc_set_foreground(fill_bg_gc, current_color);
1097       gdk_draw_rectangle(pixmap, fill_bg_gc, TRUE, 0, 0, 16, 16);
1098       image = gtk_image_new_from_pixmap(pixmap, NULL);
1099       gtk_tool_button_set_icon_widget(button, image);
1100       gtk_widget_show(image);
1101       g_object_unref(G_OBJECT(pixmap));
1102     }
1103   }
1104 }
1105 
1106 /**************************************************************************
1107   Color selection dialog response.
1108 **************************************************************************/
color_selected(GtkDialog * dialog,gint res,gpointer data)1109 static void color_selected(GtkDialog *dialog, gint res, gpointer data)
1110 {
1111   GtkToolButton *button =
1112     GTK_TOOL_BUTTON(g_object_get_data(G_OBJECT(dialog), "button"));
1113   const gchar *color_target =
1114     g_object_get_data(G_OBJECT(button), "color_target");
1115 
1116   if (res == GTK_RESPONSE_REJECT) {
1117     /* Clears the current color. */
1118     color_set(G_OBJECT(data), color_target, NULL, button);
1119   } else if (res == GTK_RESPONSE_OK) {
1120     /* Apply the new color. */
1121     GtkColorSelection *selection =
1122       GTK_COLOR_SELECTION(g_object_get_data(G_OBJECT(dialog), "selection"));
1123     GdkColor new_color;
1124 
1125     gtk_color_selection_get_current_color(selection, &new_color);
1126     color_set(G_OBJECT(data), color_target, &new_color, button);
1127   }
1128 
1129   gtk_widget_destroy(GTK_WIDGET(dialog));
1130 }
1131 
1132 /**************************************************************************
1133   Color selection tool button clicked.
1134 **************************************************************************/
select_color_callback(GtkToolButton * button,gpointer data)1135 static void select_color_callback(GtkToolButton *button, gpointer data)
1136 {
1137   GtkWidget *dialog, *selection;
1138   /* "fg_color" or "bg_color". */
1139   const gchar *color_target = g_object_get_data(G_OBJECT(button),
1140                                                 "color_target");
1141   GdkColor *current_color = g_object_get_data(G_OBJECT(data), color_target);
1142 
1143   /* TRANS: "text" or "background". */
1144   gchar *buf = g_strdup_printf(_("Select the %s color"),
1145               (const char *) g_object_get_data(G_OBJECT(button),
1146                                                "color_info"));
1147   dialog = gtk_dialog_new_with_buttons(buf, NULL, GTK_DIALOG_MODAL,
1148                                        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
1149                                        GTK_STOCK_CLEAR, GTK_RESPONSE_REJECT,
1150                                        GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
1151   setup_dialog(dialog, toplevel);
1152   g_object_set_data(G_OBJECT(dialog), "button", button);
1153   g_signal_connect(dialog, "response", G_CALLBACK(color_selected), data);
1154 
1155   selection = gtk_color_selection_new();
1156   gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), selection,
1157                      FALSE, FALSE, 0);
1158   g_object_set_data(G_OBJECT(dialog), "selection", selection);
1159   if (current_color) {
1160     gtk_color_selection_set_current_color(GTK_COLOR_SELECTION(selection),
1161                                           current_color);
1162   }
1163 
1164   gtk_widget_show_all(dialog);
1165   g_free(buf);
1166 }
1167 
1168 /**************************************************************************
1169   Moves the tool kit to the toolkit view.
1170 **************************************************************************/
move_toolkit(GtkWidget * toolkit_view,GdkEventExpose * event,gpointer data)1171 static gboolean move_toolkit(GtkWidget *toolkit_view, GdkEventExpose *event,
1172                              gpointer data)
1173 {
1174   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1175   GtkWidget *parent = gtk_widget_get_parent(ptoolkit->main_widget);
1176   GtkWidget *button_box = GTK_WIDGET(g_object_get_data(G_OBJECT(toolkit_view),
1177                                                        "button_box"));
1178   GList *list, *iter;
1179 
1180   if (parent) {
1181     if (parent == toolkit_view) {
1182       return FALSE;     /* Already owned. */
1183     }
1184 
1185     /* N.B.: We need to hide/show the toolbar to reset the sensitivity
1186      * of the tool buttons. */
1187     if (ptoolkit->toolbar_displayed) {
1188       gtk_widget_hide(ptoolkit->toolbar);
1189     }
1190     gtk_widget_reparent(ptoolkit->main_widget, toolkit_view);
1191     if (ptoolkit->toolbar_displayed) {
1192       gtk_widget_show(ptoolkit->toolbar);
1193     }
1194 
1195     if (!gtk_widget_get_parent(button_box)) {
1196       /* Attach to the toolkit button_box. */
1197       gtk_box_pack_end(GTK_BOX(ptoolkit->button_box), button_box,
1198                        FALSE, FALSE, 0);
1199     }
1200     gtk_widget_show_all(ptoolkit->main_widget);
1201 
1202     /* Hide all other buttons boxes. */
1203     list = gtk_container_get_children(GTK_CONTAINER(ptoolkit->button_box));
1204     for (iter = list; iter != NULL; iter = g_list_next(iter)) {
1205       GtkWidget *widget = GTK_WIDGET(iter->data);
1206 
1207       if (widget != button_box) {
1208         gtk_widget_hide(widget);
1209       }
1210     }
1211     g_list_free(list);
1212 
1213   } else {
1214     /* First time attached to a parent. */
1215     gtk_box_pack_start(GTK_BOX(toolkit_view), ptoolkit->main_widget,
1216                        TRUE, TRUE, 0);
1217     gtk_box_pack_end(GTK_BOX(ptoolkit->button_box), button_box,
1218                      FALSE, FALSE, 0);
1219     gtk_widget_show_all(ptoolkit->main_widget);
1220   }
1221 
1222   return FALSE;
1223 }
1224 
1225 /**************************************************************************
1226   Show/Hide the toolbar.
1227 **************************************************************************/
set_toolbar_visibility(GtkWidget * w,GdkEventExpose * event,gpointer data)1228 static gboolean set_toolbar_visibility(GtkWidget *w,
1229                                        GdkEventExpose *event,
1230                                        gpointer data)
1231 {
1232   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1233   GtkToggleButton *button = GTK_TOGGLE_BUTTON(toolkit.toggle_button);
1234 
1235   if (ptoolkit->toolbar_displayed) {
1236     if (!gtk_toggle_button_get_active(button)) {
1237       /* button_toggled() will be called and the toolbar shown. */
1238       gtk_toggle_button_set_active(button, TRUE);
1239     } else {
1240       /* Unsure the widget is visible. */
1241       gtk_widget_show(ptoolkit->toolbar);
1242     }
1243   } else {
1244     if (gtk_toggle_button_get_active(button)) {
1245       /* button_toggled() will be called and the toolbar hiden. */
1246       gtk_toggle_button_set_active(button, FALSE);
1247     } else {
1248       /* Unsure the widget is visible. */
1249       gtk_widget_hide(ptoolkit->toolbar);
1250     }
1251   }
1252 
1253   return FALSE;
1254 }
1255 
1256 /**************************************************************************
1257   Show/Hide the toolbar.
1258 **************************************************************************/
button_toggled(GtkToggleButton * button,gpointer data)1259 static void button_toggled(GtkToggleButton *button, gpointer data)
1260 {
1261   struct inputline_toolkit *ptoolkit = (struct inputline_toolkit *) data;
1262 
1263   if (gtk_toggle_button_get_active(button)) {
1264     gtk_widget_show(ptoolkit->toolbar);
1265     ptoolkit->toolbar_displayed = TRUE;
1266     if (chatline_is_scrolled_to_bottom()) {
1267       /* Make sure to be still at the end. */
1268       chatline_scroll_to_bottom(TRUE);
1269     }
1270   } else {
1271     gtk_widget_hide(ptoolkit->toolbar);
1272     ptoolkit->toolbar_displayed = FALSE;
1273   }
1274 }
1275 
1276 /**************************************************************************
1277   Returns a new inputline toolkit view widget that can contain the
1278   inputline.
1279 
1280   This widget has the following datas:
1281   "button_box": pointer to the GtkHBox where to append buttons.
1282 **************************************************************************/
inputline_toolkit_view_new(void)1283 GtkWidget *inputline_toolkit_view_new(void)
1284 {
1285   GtkWidget *toolkit_view, *bbox;
1286 
1287   /* Main widget. */
1288   toolkit_view = gtk_vbox_new(FALSE, 0);
1289   g_signal_connect(toolkit_view, "expose-event",
1290                    G_CALLBACK(move_toolkit), &toolkit);
1291 
1292   /* Button box. */
1293   bbox = gtk_hbox_new(FALSE, 12);
1294   g_object_set_data(G_OBJECT(toolkit_view), "button_box", bbox);
1295 
1296   return toolkit_view;
1297 }
1298 
1299 /**************************************************************************
1300   Appends a button to the inputline toolkit view widget.
1301 **************************************************************************/
inputline_toolkit_view_append_button(GtkWidget * toolkit_view,GtkWidget * button)1302 void inputline_toolkit_view_append_button(GtkWidget *toolkit_view,
1303                                           GtkWidget *button)
1304 {
1305   gtk_box_pack_start(GTK_BOX(g_object_get_data(G_OBJECT(toolkit_view),
1306                      "button_box")), button, FALSE, FALSE, 0);
1307 }
1308 
1309 /**************************************************************************
1310   Initializes the chatline stuff.
1311 **************************************************************************/
chatline_init(void)1312 void chatline_init(void)
1313 {
1314   GtkWidget *vbox, *toolbar, *hbox, *button, *entry, *bbox;
1315   GtkToolItem *item;
1316   GdkColor color;
1317 
1318   /* Chatline history. */
1319   if (!history_list) {
1320     history_list = genlist_new();
1321     history_pos = -1;
1322   }
1323 
1324   /* Inputline toolkit. */
1325   memset(&toolkit, 0, sizeof(toolkit));
1326 
1327   vbox = gtk_vbox_new(FALSE, 2);
1328   toolkit.main_widget = vbox;
1329   g_signal_connect(vbox, "expose-event",
1330                    G_CALLBACK(set_toolbar_visibility), &toolkit);
1331 
1332   entry = gtk_entry_new();
1333   toolkit.entry = entry;
1334 
1335   /* First line: toolbar */
1336   toolbar = gtk_toolbar_new();
1337   gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
1338   gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
1339   gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar), FALSE);
1340   gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);
1341   gtk_toolbar_set_orientation(GTK_TOOLBAR(toolbar),
1342                               GTK_ORIENTATION_HORIZONTAL);
1343   toolkit.toolbar = toolbar;
1344 
1345   /* Bold button. */
1346   item = gtk_tool_button_new_from_stock(GTK_STOCK_BOLD);
1347   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1348   g_object_set_data(G_OBJECT(item), "text_tag_type",
1349                     GINT_TO_POINTER(TTT_BOLD));
1350   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1351   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Bold (Ctrl-B)"));
1352 
1353   /* Italic button. */
1354   item = gtk_tool_button_new_from_stock(GTK_STOCK_ITALIC);
1355   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1356   g_object_set_data(G_OBJECT(item), "text_tag_type",
1357                     GINT_TO_POINTER(TTT_ITALIC));
1358   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1359   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Italic (Ctrl-I)"));
1360 
1361   /* Strike button. */
1362   item = gtk_tool_button_new_from_stock(GTK_STOCK_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_from_stock(GTK_STOCK_UNDERLINE);
1371   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1372   g_object_set_data(G_OBJECT(item), "text_tag_type",
1373                     GINT_TO_POINTER(TTT_UNDERLINE));
1374   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1375   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Underline (Ctrl-U)"));
1376 
1377   /* Color button. */
1378   item = gtk_tool_button_new_from_stock(GTK_STOCK_SELECT_COLOR);
1379   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1380   g_object_set_data(G_OBJECT(item), "text_tag_type",
1381                     GINT_TO_POINTER(TTT_COLOR));
1382   g_signal_connect(item, "clicked", G_CALLBACK(make_tag_callback), entry);
1383   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Color (Ctrl-C)"));
1384 
1385   gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
1386                      gtk_separator_tool_item_new(), -1);
1387 
1388   /* Foreground selector. */
1389   item = gtk_tool_button_new(NULL, "");
1390   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1391   g_object_set_data(G_OBJECT(item), "color_target", fc_strdup("fg_color"));
1392   g_object_set_data(G_OBJECT(item), "color_info",
1393                     fc_strdup(_("foreground")));
1394   g_signal_connect(item, "clicked",
1395                    G_CALLBACK(select_color_callback), entry);
1396   gtk_widget_set_tooltip_text(GTK_WIDGET(item), _("Select the text color"));
1397   if (gdk_color_parse("#000000", &color)) {
1398     /* Set default foreground color. */
1399     color_set(G_OBJECT(entry), "fg_color", &color, GTK_TOOL_BUTTON(item));
1400   } else {
1401     log_error("Failed to set the default foreground color.");
1402   }
1403 
1404   /* Background selector. */
1405   item = gtk_tool_button_new(NULL, "");
1406   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1407   g_object_set_data(G_OBJECT(item), "color_target", fc_strdup("bg_color"));
1408   g_object_set_data(G_OBJECT(item), "color_info",
1409                     fc_strdup(_("background")));
1410   g_signal_connect(item, "clicked",
1411                    G_CALLBACK(select_color_callback), entry);
1412   gtk_widget_set_tooltip_text(GTK_WIDGET(item),
1413                               _("Select the background color"));
1414   if (gdk_color_parse("#ffffff", &color)) {
1415     /* Set default background color. */
1416     color_set(G_OBJECT(entry), "bg_color", &color, GTK_TOOL_BUTTON(item));
1417   } else {
1418     log_error("Failed to set the default background color.");
1419   }
1420 
1421   gtk_toolbar_insert(GTK_TOOLBAR(toolbar),
1422                      gtk_separator_tool_item_new(), -1);
1423 
1424   /* Return button. */
1425   item = gtk_tool_button_new_from_stock(GTK_STOCK_OK);
1426   gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);
1427   g_signal_connect_swapped(item, "clicked",
1428                            G_CALLBACK(inputline_return), entry);
1429   gtk_widget_set_tooltip_text(GTK_WIDGET(item),
1430                               /* TRANS: "Return" means the return key. */
1431                               _("Send the chat (Return)"));
1432 
1433   /* Second line */
1434   hbox = gtk_hbox_new(FALSE, 4);
1435   gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
1436 
1437   /* Toggle button. */
1438   button = gtk_toggle_button_new();
1439   gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 2);
1440   gtk_container_add(GTK_CONTAINER(button),
1441 #ifdef GTK_STOCK_EDIT
1442                     gtk_image_new_from_stock(GTK_STOCK_EDIT,
1443 #else
1444                     gtk_image_new_from_stock(GTK_STOCK_PREFERENCES,
1445 #endif
1446                                              GTK_ICON_SIZE_MENU));
1447   g_signal_connect(button, "toggled", G_CALLBACK(button_toggled), &toolkit);
1448   gtk_widget_set_tooltip_text(GTK_WIDGET(button), _("Chat tools"));
1449   toolkit.toggle_button = button;
1450 
1451   /* Entry. */
1452   gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 2);
1453   g_signal_connect(entry, "activate", G_CALLBACK(inputline_return), NULL);
1454   g_signal_connect(entry, "key_press_event",
1455                    G_CALLBACK(inputline_handler), NULL);
1456 
1457   /* Button box. */
1458   bbox = gtk_hbox_new(FALSE, 0);
1459   gtk_box_pack_end(GTK_BOX(hbox), bbox, FALSE, FALSE, 0);
1460   toolkit.button_box = bbox;
1461 }
1462 
1463 /**************************************************************************
1464   Main thread side callback to print version message
1465 **************************************************************************/
version_message_main_thread(gpointer user_data)1466 static gboolean version_message_main_thread(gpointer user_data)
1467 {
1468   char *vertext = (char *)user_data;
1469 
1470   output_window_append(ftc_client, vertext);
1471 
1472   FC_FREE(vertext);
1473 
1474   return FALSE;
1475 }
1476 
1477 /**************************************************************************
1478   Got version message from metaserver thread.
1479 **************************************************************************/
version_message(const char * vertext)1480 void version_message(const char *vertext)
1481 {
1482   int len = strlen(vertext) + 1;
1483   char *persistent = fc_malloc(len);
1484 
1485   strncpy(persistent, vertext, len);
1486 
1487   gdk_threads_add_idle(version_message_main_thread, persistent);
1488 }
1489