#include #include #include #include "gm-world-text-view.h" #include "gm-color-table.h" #include "gm-marshal.h" #include "gm-ansi.h" #include "gm-debug.h" #include "gm-support.h" static gboolean gm_world_text_view_button_press_event(GtkWidget *widget, GdkEventButton *event); static gboolean gm_world_text_view_button_release_event(GtkWidget *widget, GdkEventButton *event); static gboolean gm_world_text_view_leave_event(GtkWidget *widget, GdkEventCrossing *event); static void gm_world_text_view_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, guint info, guint time); static void gm_world_text_view_drag_end(GtkWidget *widget, GdkDragContext *context); static void gm_world_text_view_style_set(GtkWidget *widget, GtkStyle *previous_style); static void gm_world_text_view_populate_popup(GtkTextView *text_view, GtkMenu *menu); /* Callback definitions */ // Popup menu actions callbacks static void on_gm_world_text_view_copy_address(GtkMenuItem *item, GmWorldTextView *view); static void on_gm_world_text_view_open_address(GtkMenuItem *item, GmWorldTextView *view); static gboolean on_gm_world_text_view_url_event(GtkTextTag *tag, GObject *object, GdkEvent *event, GtkTextIter *iter, GmWorldTextView *view); static gboolean on_gm_world_text_view_event(GmWorldTextView *view, GdkEventMotion *event, GtkTextTag *tag); /*void on_gm_world_text_view_color_table_bold_toggled(GmColorTable *table, gboolean bold, GmWorldTextView *view);*/ static void on_gm_world_text_view_color_table_color_changed(GmColorTable *table, gchar *name, GmWorldTextView *view); static void on_gm_world_text_view_color_table_font_changed(GmColorTable *table, gchar *font_description, GmWorldTextView *view); static void gm_world_text_view_create_tags(GmWorldTextView *view); static void gm_world_text_view_init_tags(GmWorldTextView *view); static void gm_world_text_view_update_font(GmWorldTextView *view); #define GM_WORLD_TEXT_VIEW_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE( \ (object), GM_TYPE_WORLD_TEXT_VIEW, GmWorldTextViewPrivate)) #define GM_WORLD_TEXT_VIEW_BUFFER(view) gtk_text_view_get_buffer( \ GTK_TEXT_VIEW(view)) typedef struct _GmWorldTextViewInsertInfo GmWorldTextViewInsertInfo; typedef struct _BlinkInfo { guint blink; GtkTextBuffer *text; gchar *fill; guint timeout; GmWorldTextView *view; gint offset; } BlinkInfo; static void blink_info_free(BlinkInfo *info); struct _GmWorldTextViewInsertInfo { GList *tags; gboolean bold; gboolean inverse; guint blink; gchar *text; }; struct _GmWorldTextViewPrivate { GSList *blinkers; guint blink_state; GtkTextTag *tag_blink; GtkTextTag *tag_blink_fast; guint blink_timeout_id; guint character_width; guint character_height; gint max_lines; GmWorldTextViewInsertInfo last_info; GmColorTable *color_table; gboolean drag_url; gchar *drag_url_text; guint drag_x; guint drag_y; GtkTargetList *source_target_list; gboolean is_hand; }; /* Signals */ enum { URL_ACTIVATE, CHARACTER_SIZE_CHANGED, NUM_SIGNALS }; static guint world_text_view_signals[NUM_SIGNALS] = {0}; static GtkTextViewClass *parent_class = NULL; G_DEFINE_TYPE(GmWorldTextView, gm_world_text_view, GTK_TYPE_TEXT_VIEW) /* Class object functions etc */ static void gm_world_text_view_finalize(GObject *object) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(object); GSList *item; BlinkInfo *info; gm_world_text_view_set_color_table(view, NULL); if (view->priv->blink_timeout_id) { g_source_remove(view->priv->blink_timeout_id); } for (item = view->priv->blinkers; item; item = item->next) { info = (BlinkInfo *)(item->data); g_source_remove(info->timeout); blink_info_free(info); } g_slist_free(view->priv->blinkers); g_list_free(view->priv->last_info.tags); g_free(view->priv->last_info.text); gtk_target_list_unref(view->priv->source_target_list); G_OBJECT_CLASS(gm_world_text_view_parent_class)->finalize(object); } static void gm_world_text_view_class_init(GmWorldTextViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); GtkTextViewClass *text_view_class = GTK_TEXT_VIEW_CLASS(klass); parent_class = GTK_TEXT_VIEW_CLASS(g_type_class_peek_parent(klass)); text_view_class->populate_popup = gm_world_text_view_populate_popup; object_class->finalize = gm_world_text_view_finalize; widget_class->button_press_event = gm_world_text_view_button_press_event; widget_class->button_release_event = gm_world_text_view_button_release_event; widget_class->leave_notify_event = gm_world_text_view_leave_event; widget_class->drag_data_get = gm_world_text_view_drag_data_get; widget_class->drag_end = gm_world_text_view_drag_end; widget_class->style_set = gm_world_text_view_style_set; world_text_view_signals[URL_ACTIVATE] = g_signal_new("url_activate", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmWorldTextViewClass, url_activate), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); world_text_view_signals[CHARACTER_SIZE_CHANGED] = g_signal_new("character_size_changed", G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GmWorldTextViewClass, character_size_changed), NULL, NULL, gm_marshal_VOID__UINT_UINT, G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); g_type_class_add_private(object_class, sizeof(GmWorldTextViewPrivate)); } enum { DRAG_URL, DRAG_URI_LIST, DRAG_TEXT, NUM_TARGETS }; static const GtkTargetEntry drag_targets[] = { {"_NETSCAPE_URL", 0, DRAG_URL}, {"text/uri-list", 0, DRAG_URI_LIST}, {"text/plain", 0, DRAG_TEXT}, }; static void gm_world_text_view_init(GmWorldTextView *view) { view->priv = GM_WORLD_TEXT_VIEW_GET_PRIVATE(view); // This view can't focus GTK_WIDGET_UNSET_FLAGS(GTK_WIDGET(view), GTK_CAN_FOCUS); // Therefore we need the active state to be the selected state gtk_widget_modify_base(GTK_WIDGET(view), GTK_STATE_ACTIVE, &(GTK_WIDGET(view)->style->base[GTK_STATE_SELECTED])); gtk_widget_modify_text(GTK_WIDGET(view), GTK_STATE_ACTIVE, &(GTK_WIDGET(view)->style->text[GTK_STATE_SELECTED])); gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE); // Margins gtk_text_view_set_left_margin(GTK_TEXT_VIEW(view), 3); gtk_text_view_set_right_margin(GTK_TEXT_VIEW(view), 3); // Set default wrapping mode gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD_CHAR); view->priv->color_table = NULL; view->priv->max_lines = 2000; view->priv->last_info.bold = FALSE; view->priv->last_info.inverse = FALSE; view->priv->last_info.tags = NULL; view->priv->last_info.text = NULL; view->priv->source_target_list = gtk_target_list_new(drag_targets, NUM_TARGETS); } /* Private functions */ static void blink_info_free(BlinkInfo *info) { g_object_unref(info->text); g_free(info->fill); g_free(info); } static gboolean gm_world_text_view_button_press_event(GtkWidget *widget, GdkEventButton *event) { GtkTextView *text_view = GTK_TEXT_VIEW(widget); GmWorldTextView *view = GM_WORLD_TEXT_VIEW(widget); GtkTextIter start, end; GtkTextBuffer *buffer = gtk_text_view_get_buffer(text_view); GtkTextTag *tag; gint x, y; if (event->button == 1 && !(event->state & GDK_MOD1_MASK) && !(event->state & GDK_SHIFT_MASK) && event->window == gtk_text_view_get_window(text_view, GTK_TEXT_WINDOW_TEXT)) { // Are we at a link gtk_text_view_window_to_buffer_coords(text_view, GTK_TEXT_WINDOW_TEXT, event->x, event->y, &x, &y); gtk_text_view_get_iter_at_location(text_view, &start, x, y); tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), "url"); if (gtk_text_iter_has_tag(&start, tag)) { end = start; if (!gtk_text_iter_begins_tag(&start, tag)) { gtk_text_iter_backward_to_tag_toggle(&start, tag); } if (!gtk_text_iter_ends_tag(&end, tag)) { gtk_text_iter_forward_to_tag_toggle(&end, tag); } view->priv->drag_url_text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); view->priv->drag_x = event->x; view->priv->drag_y = event->y; view->priv->drag_url = TRUE; return TRUE; } } if (GTK_WIDGET_CLASS(parent_class)->button_press_event) { return GTK_WIDGET_CLASS(parent_class)->button_press_event(widget, event); } return FALSE; } static void gm_world_text_view_no_selection(GmWorldTextView *view) { GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextIter iter; gtk_text_buffer_get_iter_at_mark(buffer, &iter, gtk_text_buffer_get_insert(buffer)); gtk_text_buffer_move_mark(buffer, gtk_text_buffer_get_selection_bound(buffer), &iter); } static gboolean gm_world_text_view_button_release_event(GtkWidget *widget, GdkEventButton *event) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(widget); if (event->button == 1 && view->priv->drag_url) { gm_world_text_view_no_selection(view); view->priv->drag_url = FALSE; g_free(view->priv->drag_url_text); view->priv->drag_url_text = NULL; } if (GTK_WIDGET_CLASS(parent_class)->button_release_event) { return GTK_WIDGET_CLASS(parent_class)->button_release_event(widget, event); } return FALSE; } static gboolean gm_world_text_view_leave_event(GtkWidget *widget, GdkEventCrossing *event) { GM_WORLD_TEXT_VIEW(widget)->priv->drag_url = FALSE; if (GTK_WIDGET_CLASS(parent_class)->leave_notify_event) { return GTK_WIDGET_CLASS(parent_class)->leave_notify_event(widget, event); } return FALSE; } static void gm_world_text_view_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, guint info, guint time) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(widget); GdkAtom target = data->target; gchar *str = NULL; if (view->priv->drag_url_text) { if (target == gdk_atom_intern("_NETSCAPE_URL", FALSE) || target == gdk_atom_intern("text/plain", FALSE)) { str = g_strdup(view->priv->drag_url_text); } else if (target == gdk_atom_intern("text/uri-list", FALSE)) { str = g_strconcat(view->priv->drag_url_text, "\r\n", NULL); } else { g_assert_not_reached(); } } else if (GTK_WIDGET_CLASS(parent_class)->drag_data_get) { GTK_WIDGET_CLASS(parent_class)->drag_data_get(widget, context, data, info, time); return; } else { g_assert_not_reached(); } gtk_selection_data_set(data, target, 8, (const guchar *)str, strlen(str)); g_free(str); } static void gm_world_text_view_drag_end(GtkWidget *widget, GdkDragContext *context) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(widget); if (view->priv->drag_url_text) { g_free(view->priv->drag_url_text); view->priv->drag_url_text = NULL; } else if (GTK_WIDGET_CLASS(parent_class)->drag_end) { GTK_WIDGET_CLASS(parent_class)->drag_end(widget, context); } } static void gm_world_text_view_style_set(GtkWidget *widget, GtkStyle *previous_style) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(widget); GtkRcStyle *style; PangoContext *pc; PangoLayout *pl; gint cwidth, cheight; if (GTK_WIDGET_CLASS(parent_class)->style_set) { GTK_WIDGET_CLASS(parent_class)->style_set(widget, previous_style); } style = gtk_widget_get_modifier_style(widget); pc = gtk_widget_create_pango_context(widget); if (style->font_desc != NULL) { pango_context_set_font_description(pc, style->font_desc); pl = pango_layout_new(pc); pango_layout_set_text(pl, "G", 1); pango_layout_get_pixel_size(pl, &(cwidth), &(cheight)); if (view->priv->character_width != (guint)cwidth || view->priv->character_height != (guint)cheight) { view->priv->character_width = (guint)cwidth; view->priv->character_height = (guint)cheight; g_signal_emit(view, world_text_view_signals[CHARACTER_SIZE_CHANGED], 0, view->priv->character_width, view->priv->character_height); } g_object_unref(pl); } g_object_unref(pc); } static void gm_world_text_view_populate_popup(GtkTextView *text_view, GtkMenu *menu) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(text_view); GtkTextTagTable *table; GtkTextTag *tag; gint x, y; GtkTextIter iter, start, end; GtkWidget *item; gchar *str = NULL; if (parent_class->populate_popup) { parent_class->populate_popup(text_view, menu); } table = gtk_text_buffer_get_tag_table(GM_WORLD_TEXT_VIEW_BUFFER(view)); tag = gtk_text_tag_table_lookup(table, "url"); gtk_widget_get_pointer(GTK_WIDGET(view), &x, &y); gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(view), GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y); gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(view), &iter, x, y); start = end = iter; if (gtk_text_iter_backward_to_tag_toggle(&start, tag) && gtk_text_iter_forward_to_tag_toggle(&end, tag)) { str = gtk_text_buffer_get_text(GM_WORLD_TEXT_VIEW_BUFFER(view), &start, &end, FALSE); } if (str == NULL || *str == '\0') { return; } item = gtk_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); gtk_widget_show(item); /* Set data just to get the string freed when not needed. */ g_object_set_data_full(G_OBJECT(menu), "url", str, (GDestroyNotify)g_free); item = gtk_menu_item_new_with_mnemonic(_("_Copy Link Address")); g_signal_connect(item, "activate", G_CALLBACK(on_gm_world_text_view_copy_address), view); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); gtk_widget_show(item); item = gtk_menu_item_new_with_mnemonic(_("_Open Link")); g_signal_connect(item, "activate", G_CALLBACK(on_gm_world_text_view_open_address), view); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); gtk_widget_show(item); } static void gm_world_text_view_update_color_tag(GmWorldTextView *view, const gchar *name, GtkTextTag *tag) { GtkTextBuffer *buf = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buf); GdkColor col; if (tag == NULL) { tag = gtk_text_tag_table_lookup(table, name); } if (view->priv->color_table != NULL) { gm_color_table_get(view->priv->color_table, name, &col); } if (name[0] == 'f') { if (view->priv->color_table == NULL) { gdk_color_parse("#ffffff", &col); } g_object_set(tag, "foreground-gdk", &col, NULL); if (strcmp(name, "fg_default") == 0) { // Update inverse_bg tag = gtk_text_tag_table_lookup(table, "inverse_bg"); g_object_set(tag, "background-gdk", &col, NULL); } } else { if (view->priv->color_table == NULL) { gdk_color_parse("#000000", &col); } g_object_set(tag, "background-gdk", &col, NULL); if (strcmp(name, "bg_default")) { // Update inverse_fg tag = gtk_text_tag_table_lookup(table, "inverse_fg"); g_object_set(tag, "foreground-gdk", &col, NULL); } } } static void gm_world_text_view_check_buffer_size(GmWorldTextView *view) { GtkTextBuffer *buf = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextIter start, end; int d = gtk_text_buffer_get_line_count(buf) - view->priv->max_lines; if (d > 0) { gtk_text_buffer_get_iter_at_line(buf, &start, 0); gtk_text_buffer_get_iter_at_line(buf, &end, d); gtk_text_buffer_delete(buf, &start, &end); } } static void gm_world_text_view_create_tags(GmWorldTextView *view) { /* Create all the tags */ int i; GtkTextTag *tag; GtkTextBuffer *buf = GM_WORLD_TEXT_VIEW_BUFFER(view); GdkColor col; /* Url tag */ tag = gtk_text_buffer_create_tag(buf, "url", "foreground", "steelblue", "underline", PANGO_UNDERLINE_SINGLE, NULL); g_signal_connect(tag, "event", G_CALLBACK(on_gm_world_text_view_url_event), view); g_signal_connect(view, "event", G_CALLBACK(on_gm_world_text_view_event), tag); view->priv->tag_blink = gtk_text_buffer_create_tag(buf, "blink", NULL); view->priv->tag_blink_fast = gtk_text_buffer_create_tag(buf, "blink-fast", NULL); gtk_text_buffer_create_tag(buf, "blink-after", "weight", PANGO_WEIGHT_ULTRABOLD, NULL); tag = gtk_text_buffer_create_tag(buf, "inverse_bg", NULL); if (view->priv->color_table != NULL) { gm_color_table_get(view->priv->color_table, "fg_default", &col); g_object_set(tag, "background-gdk", &col, NULL); } tag = gtk_text_buffer_create_tag(buf, "inverse_fg", NULL); if (view->priv->color_table != NULL) { gm_color_table_get(view->priv->color_table, "bg_default", &col); g_object_set(tag, "background-gdk", &col, NULL); } for (i = 0; i < (int)(sizeof(ansi_colors) / sizeof(ansinamepair)); i++) { tag = gtk_text_buffer_create_tag(buf, ansi_colors[i].name, NULL); gm_world_text_view_update_color_tag(view, ansi_colors[i].name, tag); } for (i = 0; i < (int)(sizeof(ansi_styles) / sizeof(ansinamepair)); i++) { tag = gtk_text_buffer_create_tag(buf, ansi_styles[i].name, NULL); switch (ansi_styles[i].code) { case A_BOLD: g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_ULTRABOLD, NULL); break; case A_BOLD_OFF: g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_NORMAL, NULL); break; case A_FAINT: g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_ULTRALIGHT, NULL); break; case A_UNDERLINE: g_object_set(G_OBJECT(tag), "underline", PANGO_UNDERLINE_SINGLE, NULL); break; case A_DOUBLE_UNDERLINE: g_object_set(G_OBJECT(tag), "underline", PANGO_UNDERLINE_DOUBLE, NULL); break; case A_UNDERLINE_OFF: g_object_set(G_OBJECT(tag), "underline", PANGO_UNDERLINE_NONE, NULL); break; case A_CROSSOUT: g_object_set(G_OBJECT(tag), "strikethrough", TRUE, NULL); break; case A_CROSSOUT_OFF: g_object_set(G_OBJECT(tag), "strikethrough", FALSE, NULL); break; case A_ITALIC: g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL); break; case A_ITALIC_OFF: g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL); break; case A_INVISIBLE: g_object_set(G_OBJECT(tag), "invisible", TRUE, NULL); break; case A_INVISIBLE_OFF: g_object_set(G_OBJECT(tag), "invisible", FALSE, NULL); break; case A_NOWRAP: g_object_set(G_OBJECT(tag), "wrap-mode", GTK_WRAP_NONE, NULL); break; default: gtk_text_tag_table_remove(gtk_text_buffer_get_tag_table(buf), tag); break; } } } static void gm_world_text_view_update_tags(GmWorldTextView *view) { gint i; for (i = 0; i < (int)(sizeof(ansi_colors) / sizeof(ansinamepair)); i++) { gm_world_text_view_update_color_tag(view, ansi_colors[i].name, NULL); } /* tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table( GM_WORLD_TEXT_VIEW_BUFFER(view)), "bold"); if (gm_color_table_bold(view->priv->color_table)) { g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_NORMAL, NULL); } else { g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_ULTRABOLD, NULL); }*/ } static void gm_world_text_view_init_tags(GmWorldTextView *view) { GdkColor col; gm_color_table_get(view->priv->color_table, "fg_default", &col); gtk_widget_modify_text(GTK_WIDGET(view), GTK_STATE_NORMAL, &col); gm_color_table_get(view->priv->color_table, "bg_default", &col); gtk_widget_modify_base(GTK_WIDGET(view), GTK_STATE_NORMAL, &col); gm_world_text_view_update_tags(view); } static void gm_world_text_view_update_font(GmWorldTextView *view) { PangoFontDescription *f = pango_font_description_from_string( gm_color_table_font_description(view->priv->color_table)); if (f != NULL) { gtk_widget_modify_font(GTK_WIDGET(view), f); pango_font_description_free(f); } } static gboolean g_utf8_toint(gchar *str, guint *result) { gunichar c; *result = 0; while ((c = g_utf8_get_char(str)) != '\0') { if (g_unichar_isdigit(c)) { *result = (*result * 10) + g_unichar_digit_value(c); } else { return FALSE; } str = g_utf8_next_char(str); } return TRUE; } static void gm_world_text_view_blinker_iters(BlinkInfo *info, GtkTextIter *start, GtkTextIter *end, GtkTextTag **tag_blink) { GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(info->view); GtkTextTag *tag; if (info->blink == 1) { tag = info->view->priv->tag_blink; } else { tag = info->view->priv->tag_blink_fast; } gtk_text_buffer_get_iter_at_offset(buffer, start, info->offset); if (end != NULL) { *end = *start; gtk_text_iter_forward_chars(end, g_utf8_strlen(info->fill, -1)); } if (tag_blink != NULL) { *tag_blink = tag; } } static void gm_world_text_view_blinker_set_visible(BlinkInfo *info, gboolean visible) { GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(info->view); GtkTextIter start, end, s, e; GtkTextTag *tag; gm_world_text_view_blinker_iters(info, &start, &end, &tag); gtk_text_buffer_delete(buffer, &start, &end); gm_world_text_view_blinker_iters(info, &start, NULL, NULL); if (visible) { gtk_text_buffer_get_bounds(info->text, &s, &e); gtk_text_buffer_insert_range(buffer, &start, &s, &e); } else { gtk_text_buffer_insert_with_tags(buffer, &start, info->fill, -1, tag, NULL); } } static gboolean gm_world_text_view_blinker_timeout(BlinkInfo *info) { GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(info->view); GtkTextTag *tag; GtkTextIter start, end; gm_world_text_view_blinker_set_visible(info, TRUE); gm_world_text_view_blinker_iters(info, &start, &end, &tag); gtk_text_buffer_remove_tag(buffer, tag, &start, &end); gtk_text_buffer_apply_tag_by_name(buffer, "blink-after", &start, &end); info->view->priv->blinkers = g_slist_remove(info->view->priv->blinkers, info); blink_info_free(info); return FALSE; } static gboolean gm_world_text_view_blink_timeout(GmWorldTextView *view) { guint blink_state = view->priv->blink_state; GSList *blinker; BlinkInfo *info; if (!view->priv->blinkers) { view->priv->blink_state = 0; view->priv->blink_timeout_id = 0; return FALSE; } for (blinker = view->priv->blinkers; blinker; blinker = blinker->next) { info = (BlinkInfo *)(blinker->data); if (info->blink == 1 && (blink_state == 0 || blink_state == 2)) { gm_world_text_view_blinker_set_visible(info, blink_state == 2); } else if (info->blink == 2) { gm_world_text_view_blinker_set_visible(info, blink_state == 1 || blink_state == 3); } } if (blink_state == 3) { view->priv->blink_state = 0; } else { view->priv->blink_state = blink_state + 1; } return TRUE; } static const gchar * gm_world_text_view_tagname_from_code(guint code) { gint i, len; len = (sizeof(ansi_colors) / sizeof(ansinamepair)); for (i = 0; i < len; i++) { if (ansi_colors[i].code == code) { return ansi_colors[i].name; } } len = sizeof(ansi_styles) / sizeof(ansinamepair); for (i = 0; i < len; i++) { if (ansi_styles[i].code == code) { return ansi_styles[i].name; } } return NULL; } static GList * gm_world_text_view_tags_apply_bold(GmWorldTextView *view, GList *tags) { GList *t, *item; GtkTextTag *tag, *htag; gboolean fg_set; gchar *name, *hname; gboolean added = FALSE; GtkTextTagTable *tag_table = gtk_text_buffer_get_tag_table( GM_WORLD_TEXT_VIEW_BUFFER(view)); t = g_list_copy(tags); for (item = t; item; item = item->next) { tag = GTK_TEXT_TAG(item->data); g_object_get(G_OBJECT(tag), "foreground-set", &fg_set, NULL); g_object_get(G_OBJECT(tag), "name", &name, NULL); if (fg_set) { hname = g_strconcat(name, "_h", NULL); htag = gtk_text_tag_table_lookup(tag_table, hname); if (htag) { if (!g_list_find(tags, htag)) { tags = g_list_append(tags, htag); } added = TRUE; } g_free(hname); } g_free(name); } /* If there is no high color added than this means we have a default color */ if (!added) { tags = g_list_append(tags, gtk_text_tag_table_lookup(tag_table, "fg_default_h")); } g_list_free(t); return tags; } static GList * gm_world_text_view_tags_fix_for_inverse(GmWorldTextView *view, GList *tags) { gboolean bold = view->priv->last_info.bold; gboolean fg_changed = FALSE, bg_changed = FALSE; GList *new_tags = NULL, *item; GtkTextTag *tag; gchar *name, *base, *tagname; int i; GtkTextTagTable *tab = gtk_text_buffer_get_tag_table( GM_WORLD_TEXT_VIEW_BUFFER(view)); for (item = tags; item; item = item->next) { tag = GTK_TEXT_TAG(item->data); g_object_get(G_OBJECT(tag), "name", &name, NULL); if (strncmp(name, "fg_", 3) == 0) { base = name + 3; bg_changed = TRUE; i = 0; while (base[i] != '_' && base[i] != '\0') i++; base[i] = '\0'; tagname = g_strconcat("bg_", base, NULL); new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, tagname)); g_free(tagname); } else if (strncmp(name, "bg_", 3) == 0) { base = name + 3; fg_changed = TRUE; if (bold) { tagname = g_strconcat("fg_", base, "_h", NULL); } else { tagname = g_strconcat("fg_", base, NULL); } new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, tagname)); g_free(tagname); } else if (strcmp(name, "inverse_bg") == 0) { fg_changed = TRUE; new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, "bg_default")); } else if (strcmp(name, "inverse_fg") == 0) { bg_changed = TRUE; new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, "fg_default")); } else { new_tags = g_list_append(new_tags, tag); } g_free(name); } if (!bg_changed) { new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, "inverse_bg")); } if (!fg_changed) { new_tags = g_list_append(new_tags, gtk_text_tag_table_lookup(tab, "inverse_fg")); } g_list_free(tags); return new_tags; } static const gchar * tag_checks[] = { "foreground-set", "background-set", "weight-set", "underline-set", "style-set", "wrap-mode-set", "invisible-set", NULL }; static gboolean gm_world_text_view_tags_overlap(GtkTextTag *t1, GtkTextTag *t2) { int i = 0; gboolean val1, val2; while (tag_checks[i]) { g_object_get(G_OBJECT(t1), tag_checks[i], &val1, NULL); g_object_get(G_OBJECT(t2), tag_checks[i], &val2, NULL); if (val1 && val2) { return TRUE; } ++i; } return FALSE; } static GList * gm_world_text_view_tags_remove_obsolete(GmWorldTextView *view, GList *tags, GtkTextTag *tag) { GList *t = g_list_copy(tags), *item; GtkTextTag *tag2; for (item = t; item; item = item->next) { tag2 = GTK_TEXT_TAG(item->data); if (gm_world_text_view_tags_overlap(tag, tag2)) { tags = g_list_remove(tags, tag2); } } g_list_free(t); return tags; } static GList * gm_world_text_view_tags_remove_bold(GmWorldTextView *view, GList *tags) { GList *t = g_list_copy(tags), *item; gchar *name; for (item = t; item; item = item->next) { g_object_get(G_OBJECT(item->data), "name", &name, NULL); if (strncmp(name, "fg_", 3) == 0 && name[strlen(name) - 1] == 'h') { tags = g_list_remove(tags, item->data); } g_free(name); } g_list_free(t); return tags; } static void gm_world_text_view_insert_text(GmWorldTextView *view, const gchar *text, GmWorldTextViewInsertInfo *insert_info) { GtkTextIter end_iter, start_iter, ins; GtkTextBuffer *tb = GM_WORLD_TEXT_VIEW_BUFFER(view); gint start_offset; BlinkInfo *info; GList *tags; if (insert_info->bold) { // If it needs to be bold then apply high colors for every color insert_info->tags = gm_world_text_view_tags_apply_bold(view, insert_info->tags); } else { // Remove all possible high colors insert_info->tags = gm_world_text_view_tags_remove_bold(view, insert_info->tags); } gtk_text_buffer_get_end_iter(tb, &end_iter); start_offset = gtk_text_iter_get_offset(&end_iter); gtk_text_buffer_insert(tb, &end_iter, text, -1); gtk_text_buffer_get_iter_at_offset(tb, &start_iter, start_offset); gtk_text_buffer_get_end_iter(tb, &end_iter); for (tags = insert_info->tags; tags; tags = tags->next) { gtk_text_buffer_apply_tag(tb, GTK_TEXT_TAG(tags->data), &start_iter, &end_iter); } if (insert_info->blink) { info = g_new0(BlinkInfo, 1); info->blink = insert_info->blink; info->offset = gtk_text_iter_get_offset(&start_iter); info->view = view; if (insert_info->blink == 1) { gtk_text_buffer_apply_tag_by_name(tb, "blink", &start_iter, &end_iter); } else { gtk_text_buffer_apply_tag_by_name(tb, "blink-fast", &start_iter, &end_iter); } info->text = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(tb)); gtk_text_buffer_get_iter_at_offset(info->text, &ins, 0); gtk_text_buffer_insert_range(info->text, &ins, &start_iter, &end_iter); info->fill = g_strnfill(g_utf8_strlen(text, -1), ' '); if (!view->priv->blink_timeout_id) { view->priv->blink_timeout_id = g_timeout_add(250, (GSourceFunc)gm_world_text_view_blink_timeout, view); } /* Let them blink for about... 10 seconds */ info->timeout = g_timeout_add(10000, (GSourceFunc)gm_world_text_view_blinker_timeout, info); view->priv->blinkers = g_slist_append(view->priv->blinkers, info); } } static GList * gm_world_text_view_tags_add(GmWorldTextView *view, GList *tags, GtkTextTag *tag) { tags = gm_world_text_view_tags_remove_obsolete(view, tags, tag); return g_list_append(tags, tag); } static void gm_world_text_view_tag_urls(GmWorldTextView *view, gchar *text, GtkTextIter *started, GtkTextIter *ended) { gint num_matches, i; GArray *start, *end; gint s = 0, e = 0; GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextTag *url_tag = gtk_text_tag_table_lookup( gtk_text_buffer_get_tag_table(buffer), "url"); GtkTextIter urlstart, urlend; start = g_array_new(FALSE, FALSE, sizeof(gint)); end = g_array_new(FALSE, FALSE, sizeof(gint)); num_matches = gm_url_regex_match(text, strlen(text), start, end); for (i = 0; i < num_matches; i++) { urlstart = *started; urlend = *started; s = g_array_index(start, gint, i); e = g_array_index(end, gint, i); gtk_text_iter_forward_cursor_positions(&urlstart, g_utf8_pointer_to_offset(text, text + s)); gtk_text_iter_forward_cursor_positions(&urlend, g_utf8_pointer_to_offset(text, text + e)); gtk_text_buffer_apply_tag(buffer, url_tag, &urlstart, &urlend); } g_array_free(start, TRUE); g_array_free(end, TRUE); } /* Public/exported functions */ GtkWidget * gm_world_text_view_new() { GtkWidget *result; GmColorTable *table = gm_color_table_new(); result = gm_world_text_view_new_with_color_table(table); g_object_unref(table); return result; } GtkWidget * gm_world_text_view_new_with_color_table(GmColorTable *color_table) { GmWorldTextView *view = GM_WORLD_TEXT_VIEW(g_object_new( GM_TYPE_WORLD_TEXT_VIEW, NULL)); gm_world_text_view_create_tags(view); gm_world_text_view_set_color_table(view, color_table); return GTK_WIDGET(view); } void gm_world_text_view_set_color_table(GmWorldTextView *view, GmColorTable *color_table) { if (view->priv->color_table != NULL) { g_signal_handlers_disconnect_by_func(view->priv->color_table, on_gm_world_text_view_color_table_color_changed, view); g_signal_handlers_disconnect_by_func(view->priv->color_table, on_gm_world_text_view_color_table_font_changed, view); g_object_unref(view->priv->color_table); } if (color_table != NULL) { view->priv->color_table = g_object_ref(color_table); g_signal_connect(view->priv->color_table, "color_changed", G_CALLBACK(on_gm_world_text_view_color_table_color_changed), view); g_signal_connect(view->priv->color_table, "font_changed", G_CALLBACK(on_gm_world_text_view_color_table_font_changed), view); gm_world_text_view_init_tags(view); gm_world_text_view_update_font(view); } else { view->priv->color_table = NULL; } } GmColorTable * gm_world_text_view_color_table(GmWorldTextView *view) { return view->priv->color_table; } gchar * gm_world_text_view_insert(GmWorldTextView *view, const gchar *text) { gchar *ptr, *ansi_start, *ansi_stop, **ansis; gchar *name; int i; guint seq; GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextTagTable *tab = gtk_text_buffer_get_tag_table(buffer); GmWorldTextViewInsertInfo *info = &(view->priv->last_info); GString *new_line = NULL; GtkTextIter started, ended; GtkTextMark *start_mark; GtkTextTag *tag; gboolean skip; g_return_val_if_fail(g_utf8_validate(text, -1, NULL), NULL); if (info->text != NULL) { new_line = g_string_new(info->text); g_free(info->text); info->text = NULL; } else { new_line = g_string_new(""); } g_string_append(new_line, text); if ((ptr = g_utf8_strchr(new_line->str, -1, '\x07'))) { gdk_beep(); while (ptr) { g_string_erase(new_line, g_utf8_pointer_to_offset(new_line->str, ptr), 1); ptr = g_utf8_strchr(new_line->str, -1, '\x07'); } } gm_debug_msg(DEBUG_DEFAULT, "GmWorldTextView.Insert: %s", new_line->str); gtk_text_buffer_get_end_iter(buffer, &started); start_mark = gtk_text_buffer_create_mark(buffer, "last-insertion", &started, TRUE); while (new_line != NULL && (ansi_start = strstr(new_line->str, "\x1B["))) { i = g_utf8_pointer_to_offset(new_line->str, ansi_start); if (i != 0) { ptr = g_strndup(new_line->str, ansi_start - new_line->str); gm_world_text_view_insert_text(view, ptr, info); g_free(ptr); } if ((ansi_stop = g_utf8_strchr(ansi_start, -1, 'm'))) { /* Advance to the ansi sequence */ ansi_start = ansi_start + 2; ptr = g_strndup(ansi_start, ansi_stop - ansi_start); ansis = g_strsplit(ptr, ";", -1); g_free(ptr); for (i = 0; ansis[i] != NULL; i++) { if (ansis[i][0] != '\0' && g_utf8_toint(ansis[i], &seq)) { skip = FALSE; switch (seq) { case A_FG_DEFAULT: case A_FG_BLACK: case A_FG_RED: case A_FG_GREEN: case A_FG_YELLOW: case A_FG_BLUE: case A_FG_PURPLE: case A_FG_CYAN: case A_FG_WHITE: if (info->inverse) { seq += 10; } break; case A_BG_DEFAULT: case A_BG_BLACK: case A_BG_RED: case A_BG_GREEN: case A_BG_YELLOW: case A_BG_BLUE: case A_BG_PURPLE: case A_BG_CYAN: case A_BG_WHITE: if (info->inverse) { seq -= 10; } break; case A_BOLD: info->bold = TRUE; if (info->inverse) skip = TRUE; break; case A_FAINT: info->bold = TRUE; if (info->inverse) skip = TRUE; break; case A_BOLD_OFF: info->bold = FALSE; if (info->inverse) skip = TRUE; break; case A_INVERSE: info->inverse = !(info->inverse); info->tags = gm_world_text_view_tags_fix_for_inverse( view, info->tags); break; case A_INVERSE_OFF: if (info->inverse) { info->tags = gm_world_text_view_tags_fix_for_inverse( view, info->tags); info->inverse = FALSE; } break; case A_BLINK: info->blink = 1; break; case A_BLINK_OFF: info->blink = 0; break; case A_BLINK_FAST: info->blink = 2; break; case A_BLINK_FAST_OFF: info->blink = 0; break; case A_DEFAULT: g_list_free(info->tags); info->tags = NULL; info->bold = FALSE; info->inverse = FALSE; info->blink = 0; break; } name = (gchar *)gm_world_text_view_tagname_from_code(seq); if (name != NULL && !skip) { tag = gtk_text_tag_table_lookup(tab, name); if (tag != NULL) { info->tags = gm_world_text_view_tags_add(view, info->tags, tag); } } } } g_strfreev(ansis); g_string_erase(new_line, 0, (ansi_stop - new_line->str) + 1); } else { info->text = g_strdup(ansi_start); g_string_free(new_line, TRUE); new_line = NULL; } } if (new_line && new_line->len != 0) { gm_world_text_view_insert_text(view, new_line->str, info); } if (new_line) { g_string_free(new_line, TRUE); } gm_world_text_view_check_buffer_size(view); gtk_text_buffer_get_iter_at_mark(buffer, &started, start_mark); gtk_text_buffer_get_end_iter(buffer, &ended); gtk_text_buffer_delete_mark(buffer, start_mark); ptr = gtk_text_buffer_get_text(buffer, &started, &ended, FALSE); gm_world_text_view_tag_urls(view, ptr, &started, &ended); return ptr; } void gm_world_text_view_get_metrics(GmWorldTextView *view, guint *width, guint *height) { *width = view->priv->character_width; *height = view->priv->character_height; } /* Callbacks */ static gboolean on_gm_world_text_view_url_event(GtkTextTag *tag, GObject *object, GdkEvent *event, GtkTextIter *iter, GmWorldTextView *view) { GtkTextIter start, end; gchar *str = NULL; GtkTextBuffer *buffer = GM_WORLD_TEXT_VIEW_BUFFER(view); if (event->type == GDK_BUTTON_RELEASE && event->button.button == 1 && !(event->button.state & GDK_CONTROL_MASK) && !(event->button.state & GDK_SHIFT_MASK) && !(event->button.state & GDK_MOD1_MASK)) { start = end = *iter; if (gtk_text_iter_backward_to_tag_toggle(&start, tag) && gtk_text_iter_forward_to_tag_toggle(&end, tag)) { str = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); } if (str == NULL) { return FALSE; } /* If the link is being selected, don't do anything. */ gtk_text_buffer_get_selection_bounds(buffer, &start, &end); if (!gtk_text_iter_equal(&start, &end)) { g_free(str); return FALSE; } g_signal_emit(view, world_text_view_signals[URL_ACTIVATE], 0, str); g_free(str); } return FALSE; } gboolean on_gm_world_text_view_event(GmWorldTextView *view, GdkEventMotion *event, GtkTextTag *tag) { static GdkCursor *hand = NULL; GtkTextWindowType type; GtkTextIter iter; GdkWindow *win; gint x, y, buf_x, buf_y; gboolean has_tag; type = gtk_text_view_get_window_type(GTK_TEXT_VIEW(view), event->window); if (type != GTK_TEXT_WINDOW_TEXT) { return FALSE; } if (event->type != GDK_MOTION_NOTIFY) { return FALSE; } /* Get where the pointer really is. */ win = gtk_text_view_get_window(GTK_TEXT_VIEW(view), type); gdk_window_get_pointer(win, &x, &y, NULL); /* Get the iter where the cursor is at */ gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(view), type, x, y, &buf_x, &buf_y); gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(view), &iter, buf_x, buf_y); if (!hand) { hand = gdk_cursor_new(GDK_HAND1); } has_tag = gtk_text_iter_has_tag(&iter, tag); if (has_tag && !view->priv->is_hand) { view->priv->is_hand = TRUE; gdk_window_set_cursor(win, hand); } else if (!has_tag && view->priv->is_hand) { view->priv->is_hand = FALSE; gdk_window_set_cursor(win, NULL); } if (view->priv->drag_url && gtk_drag_check_threshold(GTK_WIDGET(view), view->priv->drag_x, view->priv->drag_y, event->x, event->y)) { view->priv->drag_url = FALSE; gtk_drag_begin(GTK_WIDGET(view), view->priv->source_target_list, GDK_ACTION_COPY | GDK_ACTION_LINK | GDK_ACTION_ASK, 1, (GdkEvent *)event); } return FALSE; } static void on_gm_world_text_view_copy_address(GtkMenuItem *item, GmWorldTextView *view) { GtkClipboard *clipboard; gchar *url = (gchar *)(g_object_get_data(G_OBJECT(gtk_widget_get_parent( GTK_WIDGET(item))), "url")); clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_text(clipboard, url, -1); } static void on_gm_world_text_view_open_address(GtkMenuItem *item, GmWorldTextView *view) { gchar *url = (gchar *)(g_object_get_data(G_OBJECT(gtk_widget_get_parent( GTK_WIDGET(item))), "url")); g_signal_emit(view, world_text_view_signals[URL_ACTIVATE], 0, url); } /* void gm_world_text_view_color_table_fix_high_color(GtkTextTag *tag, gpointer data) { gboolean bold = (gboolean)(GPOINTER_TO_INT(data)); gchar *name; GdkColor *col; g_object_get(G_OBJECT(tag), "name", &name, NULL); if (strncmp(name, "fg_", 3) == 0 && name[strlen(name) - 1] == 'h') { g_object_get(G_OBJECT(tag), "foreground-gdk", &col, NULL); if (bold) { g_object_set(G_OBJECT(tag), "foreground-gdk", col, NULL); } else { g_object_set(G_OBJECT(tag), "foreground-set", FALSE, NULL); } } g_free(name); } void on_gm_world_text_view_color_table_bold_toggled(GmColorTable *table, gboolean bold, GmWorldTextView *view) { GtkTextTag *tag; GtkTextBuffer *buf = GM_WORLD_TEXT_VIEW_BUFFER(view); GtkTextTagTable *tb = gtk_text_buffer_get_tag_table(buf); // Fix bold tag tag = gtk_text_tag_table_lookup(tb, "bold"); if (tag != NULL) { if (bold) { g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_NORMAL, NULL); } else { g_object_set(G_OBJECT(tag), "weight", PANGO_WEIGHT_ULTRABOLD, NULL); } // Fix high color tags gtk_text_tag_table_foreach(tb, gm_world_text_view_color_table_fix_high_color, GINT_TO_POINTER(bold)); } }*/ static void on_gm_world_text_view_color_table_color_changed(GmColorTable *table, gchar *name, GmWorldTextView *view) { GdkColor col; gm_world_text_view_update_color_tag(view, name, NULL); if (strcmp(name, "fg_default") == 0) { gm_color_table_get(view->priv->color_table, "fg_default", &col); gtk_widget_modify_text(GTK_WIDGET(view), GTK_STATE_NORMAL, &col); } else if (strcmp(name, "bg_default") == 0) { gm_color_table_get(view->priv->color_table, "bg_default", &col); gtk_widget_modify_base(GTK_WIDGET(view), GTK_STATE_NORMAL, &col); } } static void on_gm_world_text_view_color_table_font_changed(GmColorTable *table, gchar *font_description, GmWorldTextView *view) { gm_world_text_view_update_font(view); }