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)
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 ***********************************************************************/
15 #include <fc_config.h>
23 #include <gdk/gdkkeysyms.h>
34 #include "featured_text.h"
39 #include "client_main.h"
42 #include "mapview_common.h"
47 #include "gui_stuff.h"
52 #define MAX_CHATLINE_HISTORY 20
54 static struct genlist
*history_list
= NULL
;
55 static int history_pos
= -1;
57 static struct inputline_toolkit
{
58 GtkWidget
*main_widget
;
60 GtkWidget
*button_box
;
62 GtkWidget
*toggle_button
;
63 bool toolbar_displayed
;
64 } toolkit
; /* Singleton. */
66 static void inputline_make_tag(GtkEntry
*entry
, enum text_tag_type type
);
68 /**************************************************************************
69 Returns TRUE iff the input line has focus.
70 **************************************************************************/
71 bool inputline_has_focus(void)
73 return GTK_WIDGET_HAS_FOCUS(toolkit
.entry
);
76 /**************************************************************************
77 Gives the focus to the intput line.
78 **************************************************************************/
79 void inputline_grab_focus(void)
81 gtk_widget_grab_focus(toolkit
.entry
);
84 /**************************************************************************
85 Returns TRUE iff the input line is currently visible.
86 **************************************************************************/
87 bool inputline_is_visible(void)
89 return GTK_WIDGET_MAPPED(toolkit
.entry
);
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 **************************************************************************/
97 static bool is_plain_public_message(const char *s
)
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
) {
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]);
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
)) {
128 } else if (*p
== CHAT_DIRECT_PREFIX
) {
137 /**************************************************************************
138 Called when the return key is pressed.
139 **************************************************************************/
140 static void inputline_return(GtkEntry
*w
, gpointer data
)
142 const char *theinput
;
144 theinput
= gtk_entry_get_text(w
);
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
];
152 fc_snprintf(buf
, sizeof(buf
), ". %s", theinput
);
158 if (genlist_size(history_list
) >= MAX_CHATLINE_HISTORY
) {
161 history_data
= genlist_get(history_list
, -1);
162 genlist_remove(history_list
, history_data
);
166 genlist_prepend(history_list
, fc_strdup(theinput
));
170 gtk_entry_set_text(w
, "");
173 /**************************************************************************
174 Returns the name of player or user, set in the same list.
175 **************************************************************************/
176 static const char *get_player_or_user_name(int id
)
178 size_t size
= conn_list_size(game
.all_connections
);
181 return conn_list_get(game
.all_connections
, id
)->username
;
183 struct player
*pplayer
= player_by_number(id
- size
);
185 return pplayer
->name
;
187 /* Empty slot. Relies on being used with comparison function
188 * which can cope with NULL. */
194 /**************************************************************************
195 Find a player or a user by 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.
202 Returns the number of the matches names.
203 **************************************************************************/
204 static int check_player_or_user_name(const char *prefix
,
205 const char **matches
,
206 const int max_matches
)
208 int matches_id
[max_matches
* 2], ind
, num
;
210 switch (match_prefix_full(get_player_or_user_name
,
212 + conn_list_size(game
.all_connections
),
213 MAX_LEN_NAME
, fc_strncasecmp
, strlen
,
214 prefix
, &ind
, matches_id
,
215 max_matches
* 2, &num
)) {
218 matches
[0] = get_player_or_user_name(ind
);
220 case M_PRE_AMBIGUOUS
:
222 /* Remove duplications playername/username. */
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
)) {
249 /**************************************************************************
250 Find the larger common prefix.
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.
257 Returns the length of the common prefix (in characters).
258 **************************************************************************/
259 static size_t get_common_prefix(const char *const *prefixes
,
261 char *buf
, size_t buf_len
)
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
))) {
279 return g_utf8_strlen(buf
, -1);
282 /**************************************************************************
283 Autocompletes the input line with a player or user name.
284 Returns FALSE if there is no string to complete.
285 **************************************************************************/
286 static bool chatline_autocomplete(GtkEditable
*editable
)
288 #define MAX_MATCHES 10
289 const char *name
[MAX_MATCHES
];
290 char buf
[MAX_LEN_NAME
* MAX_MATCHES
];
292 gchar
*chars
, *p
, *prev
;
296 /* Part 1: get the string to complete. */
297 pos
= gtk_editable_get_position(editable
);
298 chars
= gtk_editable_get_chars(editable
, 0, pos
);
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
))) {
307 /* p points to the start of the last word, or the start of the string. */
309 prefix_len
= g_utf8_strlen(p
, -1);
310 if (0 == prefix_len
) {
311 /* Empty: nothing to complete, propagate the event. */
316 /* Part 2: compare with player and user names. */
317 num
= check_player_or_user_name(p
, name
, MAX_MATCHES
);
319 gtk_editable_delete_text(editable
, pos
- prefix_len
, pos
);
321 gtk_editable_insert_text(editable
, name
[0], strlen(name
[0]), &pos
);
322 gtk_editable_set_position(editable
, pos
);
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
);
329 gtk_editable_insert_text(editable
, buf
, strlen(buf
), &pos
);
330 gtk_editable_set_position(editable
, pos
);
332 sz_strlcpy(buf
, name
[0]);
333 for (i
= 1; i
< num
; i
++) {
334 cat_snprintf(buf
, sizeof(buf
), ", %s", name
[i
]);
336 /* TRANS: comma-separated list of player/user names for completion */
337 output_window_printf(ftc_client
, _("Suggestions: %s."), buf
);
344 /**************************************************************************
345 Called when a key is pressed.
346 **************************************************************************/
347 static gboolean
inputline_handler(GtkWidget
*w
, GdkEventKey
*ev
)
349 if ((ev
->state
& GDK_CONTROL_MASK
)) {
350 /* Chatline featured text support. */
351 switch (ev
->keyval
) {
353 inputline_make_tag(GTK_ENTRY(w
), TTT_BOLD
);
357 inputline_make_tag(GTK_ENTRY(w
), TTT_COLOR
);
361 inputline_make_tag(GTK_ENTRY(w
), TTT_ITALIC
);
365 inputline_make_tag(GTK_ENTRY(w
), TTT_STRIKE
);
369 inputline_make_tag(GTK_ENTRY(w
), TTT_UNDERLINE
);
377 /* Chatline history controls. */
378 switch (ev
->keyval
) {
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);
388 if (history_pos
>= 0) {
392 if (history_pos
>= 0) {
393 gtk_entry_set_text(GTK_ENTRY(w
),
394 genlist_get(history_list
, history_pos
));
396 gtk_entry_set_text(GTK_ENTRY(w
), "");
398 gtk_editable_set_position(GTK_EDITABLE(w
), -1);
402 if (gui_options
.gui_gtk2_chatline_autocompletion
) {
403 return chatline_autocomplete(GTK_EDITABLE(w
));
414 /**************************************************************************
415 Make a text tag for the selected text.
416 **************************************************************************/
417 void inputline_make_tag(GtkEntry
*entry
, enum text_tag_type type
)
419 char buf
[MAX_LEN_MSG
];
420 GtkEditable
*editable
= GTK_EDITABLE(entry
);
421 gint start_pos
, end_pos
;
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
);
429 selection
= gtk_editable_get_chars(editable
, start_pos
, end_pos
);
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");
437 if (!fg_color
&& !bg_color
) {
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
));
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
,
450 } else if (0 == featured_text_apply_tag(selection
, buf
, sizeof(buf
),
451 type
, 0, FT_OFFSET_UNSET
)) {
455 /* Replace the selection. */
456 gtk_editable_delete_text(editable
, start_pos
, end_pos
);
458 gtk_editable_insert_text(editable
, buf
, -1, &end_pos
);
459 gtk_editable_select_region(editable
, start_pos
, end_pos
);
465 /**************************************************************************
466 Make a chat link at the current position or make the current selection
468 **************************************************************************/
469 void inputline_make_chat_link(struct tile
*ptile
, bool unit
)
471 char buf
[MAX_LEN_MSG
];
472 GtkWidget
*entry
= toolkit
.entry
;
473 GtkEditable
*editable
= GTK_EDITABLE(entry
);
474 gint start_pos
, end_pos
;
478 /* Get the target. */
480 punit
= find_visible_unit(ptile
);
482 output_window_append(ftc_client
, _("No visible unit on this tile."));
489 if (gtk_editable_get_selection_bounds(editable
, &start_pos
, &end_pos
)) {
490 /* There is a selection, make it clickable. */
492 enum text_link_type type
;
494 chars
= gtk_editable_get_chars(editable
, start_pos
, end_pos
);
498 } else if (tile_city(ptile
)) {
500 target
= tile_city(ptile
);
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
);
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
);
516 /* Just insert the link at the current position. */
517 start_pos
= gtk_editable_get_position(editable
);
519 chars
= gtk_editable_get_chars(editable
, MAX(start_pos
- 1, 0),
522 sz_strlcpy(buf
, unit_link(punit
));
523 } else if (tile_city(ptile
)) {
524 sz_strlcpy(buf
, city_link(tile_city(ptile
)));
526 sz_strlcpy(buf
, tile_link(ptile
));
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
);
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
);
539 gtk_widget_grab_focus(entry
);
540 gtk_editable_set_position(editable
, end_pos
);
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 **************************************************************************/
552 void scroll_if_necessary(GtkTextView
*textview
, GtkTextMark
*scroll_target
)
556 gdouble val
, max
, upper
, page_size
;
558 fc_assert_ret(textview
!= NULL
);
559 fc_assert_ret(scroll_target
!= NULL
);
561 sw
= gtk_widget_get_parent(GTK_WIDGET(textview
));
562 fc_assert_ret(sw
!= NULL
);
563 fc_assert_ret(GTK_IS_SCROLLED_WINDOW(sw
));
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);
576 /**************************************************************************
578 **************************************************************************/
579 static gboolean
event_after(GtkWidget
*text_view
, GdkEventButton
*event
)
581 GtkTextIter start
, end
, iter
;
582 GtkTextBuffer
*buffer
;
585 struct tile
*ptile
= NULL
;
587 if (event
->type
!= GDK_BUTTON_RELEASE
|| event
->button
!= 1) {
591 buffer
= gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view
));
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
)) {
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
);
603 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(text_view
), &iter
, x
, y
);
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"));
612 /* This is a link. */
613 int id
= GPOINTER_TO_INT(g_object_get_data(G_OBJECT(tag
), "id"));
616 /* Real type is type - 1.
617 * See comment in apply_text_tag() for g_object_set_data(). */
623 struct city
*pcity
= game_city_by_number(id
);
626 ptile
= client_city_tile(pcity
);
628 output_window_append(ftc_client
, _("This city isn't known!"));
633 ptile
= index_to_tile(id
);
636 output_window_append(ftc_client
,
637 _("This tile doesn't exist in this game!"));
642 struct unit
*punit
= game_unit_by_number(id
);
645 ptile
= unit_tile(punit
);
647 output_window_append(ftc_client
, _("This unit isn't known!"));
654 center_tile_mapcanvas(ptile
);
655 link_mark_restore(type
, id
);
656 gtk_widget_grab_focus(GTK_WIDGET(map_canvas
));
666 /**************************************************************************
667 Set the "hand" cursor when moving over a link.
668 **************************************************************************/
669 static void set_cursor_if_appropriate(GtkTextView
*text_view
, gint x
, gint y
)
671 static gboolean hovering_over_link
= FALSE
;
672 static GdkCursor
*hand_cursor
= NULL
;
673 static GdkCursor
*regular_cursor
= NULL
;
676 gboolean hovering
= FALSE
;
678 /* Initialize the cursors. */
680 hand_cursor
= gdk_cursor_new_for_display(
681 gdk_screen_get_display(gdk_screen_get_default()), GDK_HAND2
);
683 if (!regular_cursor
) {
684 regular_cursor
= gdk_cursor_new_for_display(
685 gdk_screen_get_display(gdk_screen_get_default()), GDK_XTERM
);
688 gtk_text_view_get_iter_at_location(text_view
, &iter
, x
, y
);
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"));
701 if (hovering
!= hovering_over_link
) {
702 hovering_over_link
= hovering
;
704 if (hovering_over_link
) {
705 gdk_window_set_cursor(gtk_text_view_get_window(text_view
,
706 GTK_TEXT_WINDOW_TEXT
),
709 gdk_window_set_cursor(gtk_text_view_get_window(text_view
,
710 GTK_TEXT_WINDOW_TEXT
),
720 /**************************************************************************
721 Maybe are the mouse is moving over a link.
722 **************************************************************************/
723 static gboolean
motion_notify_event(GtkWidget
*text_view
,
724 GdkEventMotion
*event
)
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
);
737 /**************************************************************************
738 Maybe are the mouse is moving over a link.
739 **************************************************************************/
740 static gboolean
visibility_notify_event(GtkWidget
*text_view
,
741 GdkEventVisibility
*event
)
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
,
749 set_cursor_if_appropriate(GTK_TEXT_VIEW(text_view
), bx
, by
);
754 /**************************************************************************
755 Set the appropriate callbacks for the message buffer.
756 **************************************************************************/
757 void set_message_buffer_view_link_handlers(GtkWidget
*view
)
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
);
768 /**************************************************************************
769 Convert a struct text_tag to a GtkTextTag.
770 **************************************************************************/
771 void apply_text_tag(const struct text_tag
*ptag
, GtkTextBuffer
*buf
,
772 ft_offset_t text_start_offset
, const char *text
)
774 static bool initalized
= FALSE
;
775 GtkTextIter start
, stop
;
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
);
789 /* Get the position. */
791 * N.B.: text_tag_*_offset() value is in bytes, so we need to convert it
792 * to utf8 character offset.
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
);
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
)));
805 switch (text_tag_type(ptag
)) {
807 gtk_text_buffer_apply_tag_by_name(buf
, "bold", &start
, &stop
);
810 gtk_text_buffer_apply_tag_by_name(buf
, "italic", &start
, &stop
);
813 gtk_text_buffer_apply_tag_by_name(buf
, "strike", &start
, &stop
);
816 gtk_text_buffer_apply_tag_by_name(buf
, "underline", &start
, &stop
);
820 /* We have to make a new tag every time. */
821 GtkTextTag
*tag
= NULL
;
825 if (gdk_color_parse(text_tag_color_foreground(ptag
), &foreground
)) {
826 if (gdk_color_parse(text_tag_color_background(ptag
),
828 tag
= gtk_text_buffer_create_tag(buf
, NULL
,
829 "foreground-gdk", &foreground
,
830 "background-gdk", &background
,
833 tag
= gtk_text_buffer_create_tag(buf
, NULL
,
834 "foreground-gdk", &foreground
,
837 } else if (gdk_color_parse(text_tag_color_background(ptag
),
839 tag
= gtk_text_buffer_create_tag(buf
, NULL
,
840 "background-gdk", &background
,
845 break; /* No color. */
847 gtk_text_buffer_apply_tag(buf
, tag
, &start
, &stop
);
848 g_object_unref(G_OBJECT(tag
));
853 struct color
*pcolor
= NULL
;
856 switch (text_tag_link_type(ptag
)) {
858 pcolor
= get_color(tileset
, COLOR_MAPVIEW_CITY_LINK
);
861 pcolor
= get_color(tileset
, COLOR_MAPVIEW_TILE_LINK
);
864 pcolor
= get_color(tileset
, COLOR_MAPVIEW_UNIT_LINK
);
869 break; /* Not a valid link type case. */
872 tag
= gtk_text_buffer_create_tag(buf
, NULL
,
873 "foreground-gdk", &pcolor
->color
,
874 "underline", PANGO_UNDERLINE_SINGLE
,
877 /* Type 0 is reserved for non-link tags. So, add 1 to the
879 g_object_set_data(G_OBJECT(tag
), "type",
880 GINT_TO_POINTER(text_tag_link_type(ptag
) + 1));
881 g_object_set_data(G_OBJECT(tag
), "id",
882 GINT_TO_POINTER(text_tag_link_id(ptag
)));
883 gtk_text_buffer_apply_tag(buf
, tag
, &start
, &stop
);
884 g_object_unref(G_OBJECT(tag
));
890 /**************************************************************************
891 Appends the string to the chat output window. The string should be
892 inserted on its own line, although it will have no newline.
893 **************************************************************************/
894 void real_output_window_append(const char *astring
,
895 const struct text_tag_list
*tags
,
901 ft_offset_t text_start_offset
;
903 buf
= message_buffer
;
906 log_error("Output when no message buffer: %s", astring
);
911 gtk_text_buffer_get_end_iter(buf
, &iter
);
912 gtk_text_buffer_insert(buf
, &iter
, "\n", -1);
913 mark
= gtk_text_buffer_create_mark(buf
, NULL
, &iter
, TRUE
);
915 if (gui_options
.gui_gtk2_show_chat_message_time
) {
921 now_tm
= localtime(&now
);
922 strftime(timebuf
, sizeof(timebuf
), "[%H:%M:%S] ", now_tm
);
923 gtk_text_buffer_insert(buf
, &iter
, timebuf
, -1);
926 text_start_offset
= gtk_text_iter_get_offset(&iter
);
927 gtk_text_buffer_insert(buf
, &iter
, astring
, -1);
928 text_tag_list_iterate(tags
, ptag
) {
929 apply_text_tag(ptag
, buf
, text_start_offset
, astring
);
930 } text_tag_list_iterate_end
;
932 if (main_message_area
) {
933 scroll_if_necessary(GTK_TEXT_VIEW(main_message_area
), mark
);
935 if (start_message_area
) {
936 scroll_if_necessary(GTK_TEXT_VIEW(start_message_area
), mark
);
938 gtk_text_buffer_delete_mark(buf
, mark
);
940 append_network_statusbar(astring
, FALSE
);
943 /**************************************************************************
944 I have no idea what module this belongs in -- Syela
945 I've decided to put output_window routines in chatline.c, because
946 the are somewhat related and output_window_* is already here. --dwp
947 **************************************************************************/
948 void log_output_window(void)
950 GtkTextIter start
, end
;
953 gtk_text_buffer_get_bounds(message_buffer
, &start
, &end
);
954 txt
= gtk_text_buffer_get_text(message_buffer
, &start
, &end
, TRUE
);
956 write_chatline_content(txt
);
960 /**************************************************************************
961 Clear output window. This does *not* destroy it, or free its resources
962 **************************************************************************/
963 void clear_output_window(void)
965 set_output_window_text(_("Cleared output window."));
968 /**************************************************************************
969 Set given text to output window
970 **************************************************************************/
971 void set_output_window_text(const char *text
)
973 gtk_text_buffer_set_text(message_buffer
, text
, -1);
976 /**************************************************************************
977 Returns whether the chatline is scrolled to the bottom.
978 **************************************************************************/
979 bool chatline_is_scrolled_to_bottom(void)
983 gdouble val
, max
, upper
, page_size
;
985 if (get_client_page() == PAGE_GAME
) {
986 w
= GTK_WIDGET(main_message_area
);
988 w
= GTK_WIDGET(start_message_area
);
995 sw
= gtk_widget_get_parent(w
);
996 vadj
= gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw
));
997 val
= gtk_adjustment_get_value(GTK_ADJUSTMENT(vadj
));
998 g_object_get(G_OBJECT(vadj
), "upper", &upper
,
999 "page-size", &page_size
, NULL
);
1000 max
= upper
- page_size
;
1002 /* Approximation. */
1003 return max
- val
< 0.00000001;
1006 /**************************************************************************
1007 Scrolls the pregame and in-game chat windows all the way to the bottom.
1009 Why do we do it in such a convuluted fasion rather than calling
1010 chatline_scroll_to_bottom directly from toplevel_configure?
1011 Because the widget is not at its final size yet when the configure
1013 **************************************************************************/
1014 static gboolean
chatline_scroll_callback(gpointer data
)
1016 chatline_scroll_to_bottom(FALSE
); /* Not delayed this time! */
1018 *((guint
*) data
) = 0;
1019 return FALSE
; /* Remove this idle function. */
1022 /**************************************************************************
1023 Scrolls the pregame and in-game chat windows all the way to the bottom.
1024 If delayed is TRUE, it will be done in a idle_callback.
1025 **************************************************************************/
1026 void chatline_scroll_to_bottom(bool delayed
)
1028 static guint callback_id
= 0;
1031 if (callback_id
== 0) {
1032 callback_id
= g_idle_add(chatline_scroll_callback
, &callback_id
);
1034 } else if (message_buffer
) {
1037 gtk_text_buffer_get_end_iter(message_buffer
, &end
);
1039 if (main_message_area
) {
1040 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(main_message_area
),
1041 &end
, 0.0, TRUE
, 1.0, 0.0);
1043 if (start_message_area
) {
1044 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(start_message_area
),
1045 &end
, 0.0, TRUE
, 1.0, 0.0);
1050 /**************************************************************************
1051 Tool button clicked.
1052 **************************************************************************/
1053 static void make_tag_callback(GtkToolButton
*button
, gpointer data
)
1055 inputline_make_tag(GTK_ENTRY(data
),
1056 GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button
),
1060 /**************************************************************************
1061 Set the color for an object. Update the button if not NULL.
1062 **************************************************************************/
1063 static void color_set(GObject
*object
, const gchar
*color_target
,
1064 GdkColor
*color
, GtkToolButton
*button
)
1066 GdkColor
*current_color
= g_object_get_data(object
, color_target
);
1067 GdkColormap
*colormap
= gdk_colormap_get_system();
1069 if (NULL
== color
) {
1070 /* Clears the current color. */
1071 if (NULL
!= current_color
) {
1072 gdk_colormap_free_colors(colormap
, current_color
, 1);
1073 gdk_color_free(current_color
);
1074 g_object_set_data(object
, color_target
, NULL
);
1075 if (NULL
!= button
) {
1076 gtk_tool_button_set_icon_widget(button
, NULL
);
1080 /* Apply the new color. */
1081 if (NULL
!= current_color
) {
1082 /* We already have a GdkColor pointer. */
1083 gdk_colormap_free_colors(colormap
, current_color
, 1);
1084 *current_color
= *color
;
1086 /* We need to make a GdkColor pointer. */
1087 current_color
= gdk_color_copy(color
);
1088 g_object_set_data(object
, color_target
, current_color
);
1091 gdk_colormap_alloc_color(colormap
, current_color
, TRUE
, TRUE
);
1092 if (NULL
!= button
) {
1093 /* Update the button. */
1097 pixmap
= gdk_pixmap_new(root_window
, 16, 16, -1);
1098 gdk_gc_set_foreground(fill_bg_gc
, current_color
);
1099 gdk_draw_rectangle(pixmap
, fill_bg_gc
, TRUE
, 0, 0, 16, 16);
1100 image
= gtk_image_new_from_pixmap(pixmap
, NULL
);
1101 gtk_tool_button_set_icon_widget(button
, image
);
1102 gtk_widget_show(image
);
1103 g_object_unref(G_OBJECT(pixmap
));
1108 /**************************************************************************
1109 Color selection dialog response.
1110 **************************************************************************/
1111 static void color_selected(GtkDialog
*dialog
, gint res
, gpointer data
)
1113 GtkToolButton
*button
=
1114 GTK_TOOL_BUTTON(g_object_get_data(G_OBJECT(dialog
), "button"));
1115 const gchar
*color_target
=
1116 g_object_get_data(G_OBJECT(button
), "color_target");
1118 if (res
== GTK_RESPONSE_REJECT
) {
1119 /* Clears the current color. */
1120 color_set(G_OBJECT(data
), color_target
, NULL
, button
);
1121 } else if (res
== GTK_RESPONSE_OK
) {
1122 /* Apply the new color. */
1123 GtkColorSelection
*selection
=
1124 GTK_COLOR_SELECTION(g_object_get_data(G_OBJECT(dialog
), "selection"));
1127 gtk_color_selection_get_current_color(selection
, &new_color
);
1128 color_set(G_OBJECT(data
), color_target
, &new_color
, button
);
1131 gtk_widget_destroy(GTK_WIDGET(dialog
));
1134 /**************************************************************************
1135 Color selection tool button clicked.
1136 **************************************************************************/
1137 static void select_color_callback(GtkToolButton
*button
, gpointer data
)
1139 GtkWidget
*dialog
, *selection
;
1140 /* "fg_color" or "bg_color". */
1141 const gchar
*color_target
= g_object_get_data(G_OBJECT(button
),
1143 GdkColor
*current_color
= g_object_get_data(G_OBJECT(data
), color_target
);
1145 /* TRANS: "text" or "background". */
1146 gchar
*buf
= g_strdup_printf(_("Select the %s color"),
1147 (const char *) g_object_get_data(G_OBJECT(button
),
1149 dialog
= gtk_dialog_new_with_buttons(buf
, NULL
, GTK_DIALOG_MODAL
,
1150 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
1151 GTK_STOCK_CLEAR
, GTK_RESPONSE_REJECT
,
1152 GTK_STOCK_OK
, GTK_RESPONSE_OK
, NULL
);
1153 setup_dialog(dialog
, toplevel
);
1154 g_object_set_data(G_OBJECT(dialog
), "button", button
);
1155 g_signal_connect(dialog
, "response", G_CALLBACK(color_selected
), data
);
1157 selection
= gtk_color_selection_new();
1158 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog
)->vbox
), selection
,
1160 g_object_set_data(G_OBJECT(dialog
), "selection", selection
);
1161 if (current_color
) {
1162 gtk_color_selection_set_current_color(GTK_COLOR_SELECTION(selection
),
1166 gtk_widget_show_all(dialog
);
1170 /**************************************************************************
1171 Moves the tool kit to the toolkit view.
1172 **************************************************************************/
1173 static gboolean
move_toolkit(GtkWidget
*toolkit_view
, GdkEventExpose
*event
,
1176 struct inputline_toolkit
*ptoolkit
= (struct inputline_toolkit
*) data
;
1177 GtkWidget
*parent
= gtk_widget_get_parent(ptoolkit
->main_widget
);
1178 GtkWidget
*button_box
= GTK_WIDGET(g_object_get_data(G_OBJECT(toolkit_view
),
1183 if (parent
== toolkit_view
) {
1184 return FALSE
; /* Already owned. */
1187 /* N.B.: We need to hide/show the toolbar to reset the sensitivity
1188 * of the tool buttons. */
1189 if (ptoolkit
->toolbar_displayed
) {
1190 gtk_widget_hide(ptoolkit
->toolbar
);
1192 gtk_widget_reparent(ptoolkit
->main_widget
, toolkit_view
);
1193 if (ptoolkit
->toolbar_displayed
) {
1194 gtk_widget_show(ptoolkit
->toolbar
);
1197 if (!gtk_widget_get_parent(button_box
)) {
1198 /* Attach to the toolkit button_box. */
1199 gtk_box_pack_end(GTK_BOX(ptoolkit
->button_box
), button_box
,
1202 gtk_widget_show_all(ptoolkit
->main_widget
);
1204 /* Hide all other buttons boxes. */
1205 list
= gtk_container_get_children(GTK_CONTAINER(ptoolkit
->button_box
));
1206 for (iter
= list
; iter
!= NULL
; iter
= g_list_next(iter
)) {
1207 GtkWidget
*widget
= GTK_WIDGET(iter
->data
);
1209 if (widget
!= button_box
) {
1210 gtk_widget_hide(widget
);
1216 /* First time attached to a parent. */
1217 gtk_box_pack_start(GTK_BOX(toolkit_view
), ptoolkit
->main_widget
,
1219 gtk_box_pack_end(GTK_BOX(ptoolkit
->button_box
), button_box
,
1221 gtk_widget_show_all(ptoolkit
->main_widget
);
1227 /**************************************************************************
1228 Show/Hide the toolbar.
1229 **************************************************************************/
1230 static gboolean
set_toolbar_visibility(GtkWidget
*w
,
1231 GdkEventExpose
*event
,
1234 struct inputline_toolkit
*ptoolkit
= (struct inputline_toolkit
*) data
;
1235 GtkToggleButton
*button
= GTK_TOGGLE_BUTTON(toolkit
.toggle_button
);
1237 if (ptoolkit
->toolbar_displayed
) {
1238 if (!gtk_toggle_button_get_active(button
)) {
1239 /* button_toggled() will be called and the toolbar shown. */
1240 gtk_toggle_button_set_active(button
, TRUE
);
1242 /* Unsure the widget is visible. */
1243 gtk_widget_show(ptoolkit
->toolbar
);
1246 if (gtk_toggle_button_get_active(button
)) {
1247 /* button_toggled() will be called and the toolbar hiden. */
1248 gtk_toggle_button_set_active(button
, FALSE
);
1250 /* Unsure the widget is visible. */
1251 gtk_widget_hide(ptoolkit
->toolbar
);
1258 /**************************************************************************
1259 Show/Hide the toolbar.
1260 **************************************************************************/
1261 static void button_toggled(GtkToggleButton
*button
, gpointer data
)
1263 struct inputline_toolkit
*ptoolkit
= (struct inputline_toolkit
*) data
;
1265 if (gtk_toggle_button_get_active(button
)) {
1266 gtk_widget_show(ptoolkit
->toolbar
);
1267 ptoolkit
->toolbar_displayed
= TRUE
;
1268 if (chatline_is_scrolled_to_bottom()) {
1269 /* Make sure to be still at the end. */
1270 chatline_scroll_to_bottom(TRUE
);
1273 gtk_widget_hide(ptoolkit
->toolbar
);
1274 ptoolkit
->toolbar_displayed
= FALSE
;
1278 /**************************************************************************
1279 Returns a new inputline toolkit view widget that can contain the
1282 This widget has the following datas:
1283 "button_box": pointer to the GtkHBox where to append buttons.
1284 **************************************************************************/
1285 GtkWidget
*inputline_toolkit_view_new(void)
1287 GtkWidget
*toolkit_view
, *bbox
;
1290 toolkit_view
= gtk_vbox_new(FALSE
, 0);
1291 g_signal_connect(toolkit_view
, "expose-event",
1292 G_CALLBACK(move_toolkit
), &toolkit
);
1295 bbox
= gtk_hbox_new(FALSE
, 12);
1296 g_object_set_data(G_OBJECT(toolkit_view
), "button_box", bbox
);
1298 return toolkit_view
;
1301 /**************************************************************************
1302 Appends a button to the inputline toolkit view widget.
1303 **************************************************************************/
1304 void inputline_toolkit_view_append_button(GtkWidget
*toolkit_view
,
1307 gtk_box_pack_start(GTK_BOX(g_object_get_data(G_OBJECT(toolkit_view
),
1308 "button_box")), button
, FALSE
, FALSE
, 0);
1311 /**************************************************************************
1312 Initializes the chatline stuff.
1313 **************************************************************************/
1314 void chatline_init(void)
1316 GtkWidget
*vbox
, *toolbar
, *hbox
, *button
, *entry
, *bbox
;
1320 /* Chatline history. */
1321 if (!history_list
) {
1322 history_list
= genlist_new();
1326 /* Inputline toolkit. */
1327 memset(&toolkit
, 0, sizeof(toolkit
));
1329 vbox
= gtk_vbox_new(FALSE
, 2);
1330 toolkit
.main_widget
= vbox
;
1331 g_signal_connect(vbox
, "expose-event",
1332 G_CALLBACK(set_toolbar_visibility
), &toolkit
);
1334 entry
= gtk_entry_new();
1335 toolkit
.entry
= entry
;
1337 /* First line: toolbar */
1338 toolbar
= gtk_toolbar_new();
1339 gtk_box_pack_start(GTK_BOX(vbox
), toolbar
, FALSE
, FALSE
, 0);
1340 gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar
), GTK_ICON_SIZE_MENU
);
1341 gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar
), FALSE
);
1342 gtk_toolbar_set_style(GTK_TOOLBAR(toolbar
), GTK_TOOLBAR_BOTH_HORIZ
);
1343 gtk_toolbar_set_orientation(GTK_TOOLBAR(toolbar
),
1344 GTK_ORIENTATION_HORIZONTAL
);
1345 toolkit
.toolbar
= toolbar
;
1348 item
= gtk_tool_button_new_from_stock(GTK_STOCK_BOLD
);
1349 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1350 g_object_set_data(G_OBJECT(item
), "text_tag_type",
1351 GINT_TO_POINTER(TTT_BOLD
));
1352 g_signal_connect(item
, "clicked", G_CALLBACK(make_tag_callback
), entry
);
1353 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Bold (Ctrl-B)"));
1355 /* Italic button. */
1356 item
= gtk_tool_button_new_from_stock(GTK_STOCK_ITALIC
);
1357 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1358 g_object_set_data(G_OBJECT(item
), "text_tag_type",
1359 GINT_TO_POINTER(TTT_ITALIC
));
1360 g_signal_connect(item
, "clicked", G_CALLBACK(make_tag_callback
), entry
);
1361 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Italic (Ctrl-I)"));
1363 /* Strike button. */
1364 item
= gtk_tool_button_new_from_stock(GTK_STOCK_STRIKETHROUGH
);
1365 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1366 g_object_set_data(G_OBJECT(item
), "text_tag_type",
1367 GINT_TO_POINTER(TTT_STRIKE
));
1368 g_signal_connect(item
, "clicked", G_CALLBACK(make_tag_callback
), entry
);
1369 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Strikethrough (Ctrl-S)"));
1371 /* Underline button. */
1372 item
= gtk_tool_button_new_from_stock(GTK_STOCK_UNDERLINE
);
1373 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1374 g_object_set_data(G_OBJECT(item
), "text_tag_type",
1375 GINT_TO_POINTER(TTT_UNDERLINE
));
1376 g_signal_connect(item
, "clicked", G_CALLBACK(make_tag_callback
), entry
);
1377 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Underline (Ctrl-U)"));
1380 item
= gtk_tool_button_new_from_stock(GTK_STOCK_SELECT_COLOR
);
1381 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1382 g_object_set_data(G_OBJECT(item
), "text_tag_type",
1383 GINT_TO_POINTER(TTT_COLOR
));
1384 g_signal_connect(item
, "clicked", G_CALLBACK(make_tag_callback
), entry
);
1385 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Color (Ctrl-C)"));
1387 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
),
1388 gtk_separator_tool_item_new(), -1);
1390 /* Foreground selector. */
1391 item
= gtk_tool_button_new(NULL
, "");
1392 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1393 g_object_set_data(G_OBJECT(item
), "color_target", fc_strdup("fg_color"));
1394 g_object_set_data(G_OBJECT(item
), "color_info",
1395 fc_strdup(_("foreground")));
1396 g_signal_connect(item
, "clicked",
1397 G_CALLBACK(select_color_callback
), entry
);
1398 gtk_widget_set_tooltip_text(GTK_WIDGET(item
), _("Select the text color"));
1399 if (gdk_color_parse("#000000", &color
)) {
1400 /* Set default foreground color. */
1401 color_set(G_OBJECT(entry
), "fg_color", &color
, GTK_TOOL_BUTTON(item
));
1403 log_error("Failed to set the default foreground color.");
1406 /* Background selector. */
1407 item
= gtk_tool_button_new(NULL
, "");
1408 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1409 g_object_set_data(G_OBJECT(item
), "color_target", fc_strdup("bg_color"));
1410 g_object_set_data(G_OBJECT(item
), "color_info",
1411 fc_strdup(_("background")));
1412 g_signal_connect(item
, "clicked",
1413 G_CALLBACK(select_color_callback
), entry
);
1414 gtk_widget_set_tooltip_text(GTK_WIDGET(item
),
1415 _("Select the background color"));
1416 if (gdk_color_parse("#ffffff", &color
)) {
1417 /* Set default background color. */
1418 color_set(G_OBJECT(entry
), "bg_color", &color
, GTK_TOOL_BUTTON(item
));
1420 log_error("Failed to set the default background color.");
1423 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
),
1424 gtk_separator_tool_item_new(), -1);
1426 /* Return button. */
1427 item
= gtk_tool_button_new_from_stock(GTK_STOCK_OK
);
1428 gtk_toolbar_insert(GTK_TOOLBAR(toolbar
), item
, -1);
1429 g_signal_connect_swapped(item
, "clicked",
1430 G_CALLBACK(inputline_return
), entry
);
1431 gtk_widget_set_tooltip_text(GTK_WIDGET(item
),
1432 /* TRANS: "Return" means the return key. */
1433 _("Send the chat (Return)"));
1436 hbox
= gtk_hbox_new(FALSE
, 4);
1437 gtk_box_pack_start(GTK_BOX(vbox
), hbox
, FALSE
, FALSE
, 0);
1439 /* Toggle button. */
1440 button
= gtk_toggle_button_new();
1441 gtk_box_pack_start(GTK_BOX(hbox
), button
, FALSE
, FALSE
, 2);
1442 gtk_container_add(GTK_CONTAINER(button
),
1443 #ifdef GTK_STOCK_EDIT
1444 gtk_image_new_from_stock(GTK_STOCK_EDIT
,
1446 gtk_image_new_from_stock(GTK_STOCK_PREFERENCES
,
1448 GTK_ICON_SIZE_MENU
));
1449 g_signal_connect(button
, "toggled", G_CALLBACK(button_toggled
), &toolkit
);
1450 gtk_widget_set_tooltip_text(GTK_WIDGET(button
), _("Chat tools"));
1451 toolkit
.toggle_button
= button
;
1454 gtk_box_pack_start(GTK_BOX(hbox
), entry
, TRUE
, TRUE
, 2);
1455 g_signal_connect(entry
, "activate", G_CALLBACK(inputline_return
), NULL
);
1456 g_signal_connect(entry
, "key_press_event",
1457 G_CALLBACK(inputline_handler
), NULL
);
1460 bbox
= gtk_hbox_new(FALSE
, 0);
1461 gtk_box_pack_end(GTK_BOX(hbox
), bbox
, FALSE
, FALSE
, 0);
1462 toolkit
.button_box
= bbox
;
1465 /**************************************************************************
1466 Main thread side callback to print version message
1467 **************************************************************************/
1468 static gboolean
version_message_main_thread(gpointer user_data
)
1470 char *vertext
= (char *)user_data
;
1472 output_window_append(ftc_client
, vertext
);
1479 /**************************************************************************
1480 Got version message from metaserver thread.
1481 **************************************************************************/
1482 void version_message(const char *vertext
)
1484 int len
= strlen(vertext
) + 1;
1485 char *persistent
= fc_malloc(len
);
1487 strncpy(persistent
, vertext
, len
);
1489 gdk_threads_add_idle(version_message_main_thread
, persistent
);