Use conventional style for empty string check
[pidgin-git.git] / pidgin / gtkimhtml.c
blob777db0bef44d76a4fd0919ff54a1f0548bd65812
1 /*
2 * @file gtkimhtml.c GTK+ IMHtml
3 * @ingroup pidgin
4 */
6 /* pidgin
8 * Pidgin is the legal property of its developers, whose names are too numerous
9 * to list here. Please refer to the COPYRIGHT file distributed with this
10 * source distribution.
12 * This program is free software; you can redistribute it and/or modify
13 * under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
27 #define _PIDGIN_GTKIMHTML_C_
29 #ifdef HAVE_CONFIG_H
30 #include <config.h>
31 #endif
33 #include "internal.h"
34 #include "pidgin.h"
35 #include "pidginstock.h"
36 #include "gtkutils.h"
37 #include "smiley.h"
38 #include "imgstore.h"
40 #include "debug.h"
41 #include "util.h"
42 #include "gtkimhtml.h"
43 #include "gtksmiley.h"
44 #include "gtksourceiter.h"
45 #include "gtksourceundomanager.h"
46 #include "gtksourceview-marshal.h"
47 #include <gtk/gtk.h>
48 #include <glib.h>
49 #include <gdk/gdkkeysyms.h>
50 #include <string.h>
51 #include <ctype.h>
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <math.h>
55 #ifdef HAVE_LANGINFO_CODESET
56 #include <langinfo.h>
57 #include <locale.h>
58 #endif
59 #ifdef _WIN32
60 #include <gdk/gdkwin32.h>
61 #include <windows.h>
62 #endif
64 #include <pango/pango-font.h>
66 #define TOOLTIP_TIMEOUT 500
68 static GtkTextViewClass *parent_class = NULL;
70 struct scalable_data {
71 GtkIMHtmlScalable *scalable;
72 GtkTextMark *mark;
75 typedef struct {
76 GtkIMHtmlScalable *image;
77 gpointer data;
78 gsize datasize;
79 } GtkIMHtmlImageSave;
81 struct im_image_data {
82 int id;
83 GtkTextMark *mark;
86 struct _GtkIMHtmlLink
88 GtkIMHtml *imhtml;
89 gchar *url;
90 GtkTextTag *tag;
93 typedef struct _GtkIMHtmlProtocol
95 char *name;
96 int length;
98 gboolean (*activate)(GtkIMHtml *imhtml, GtkIMHtmlLink *link);
99 gboolean (*context_menu)(GtkIMHtml *imhtml, GtkIMHtmlLink *link, GtkWidget *menu);
100 } GtkIMHtmlProtocol;
102 typedef struct _GtkIMHtmlFontDetail {
103 gushort size;
104 gchar *face;
105 gchar *fore;
106 gchar *back;
107 gchar *bg;
108 gchar *sml;
109 gboolean underline;
110 gboolean strike;
111 gshort bold;
112 } GtkIMHtmlFontDetail;
114 static gboolean
115 gtk_text_view_drag_motion (GtkWidget *widget,
116 GdkDragContext *context,
117 gint x,
118 gint y,
119 guint time);
121 static void preinsert_cb(GtkTextBuffer *buffer, GtkTextIter *iter, gchar *text, gint len, GtkIMHtml *imhtml);
122 static void insert_cb(GtkTextBuffer *buffer, GtkTextIter *iter, gchar *text, gint len, GtkIMHtml *imhtml);
123 static void delete_cb(GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextIter *end, GtkIMHtml *imhtml);
124 static void insert_ca_cb(GtkTextBuffer *buffer, GtkTextIter *arg1, GtkTextChildAnchor *arg2, gpointer user_data);
125 static void gtk_imhtml_apply_tags_on_insert(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *end);
126 void gtk_imhtml_close_tags(GtkIMHtml *imhtml, GtkTextIter *iter);
127 static void gtk_imhtml_link_drop_cb(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data);
128 static void gtk_imhtml_link_drag_rcv_cb(GtkWidget *widget, GdkDragContext *dc, guint x, guint y, GtkSelectionData *sd, guint info, guint t, GtkIMHtml *imhtml);
129 static void mark_set_cb(GtkTextBuffer *buffer, GtkTextIter *arg1, GtkTextMark *mark, GtkIMHtml *imhtml);
130 static void hijack_menu_cb(GtkIMHtml *imhtml, GtkMenu *menu, gpointer data);
131 static void paste_received_cb (GtkClipboard *clipboard, GtkSelectionData *selection_data, gpointer data);
132 static void paste_plaintext_received_cb (GtkClipboard *clipboard, const gchar *text, gpointer data);
133 static void imhtml_paste_insert(GtkIMHtml *imhtml, const char *text, gboolean plaintext);
134 static void imhtml_toggle_bold(GtkIMHtml *imhtml);
135 static void imhtml_toggle_italic(GtkIMHtml *imhtml);
136 static void imhtml_toggle_strike(GtkIMHtml *imhtml);
137 static void imhtml_toggle_underline(GtkIMHtml *imhtml);
138 static void imhtml_font_grow(GtkIMHtml *imhtml);
139 static void imhtml_font_shrink(GtkIMHtml *imhtml);
140 static void imhtml_clear_formatting(GtkIMHtml *imhtml);
141 static int gtk_imhtml_is_protocol(const char *text);
142 static void gtk_imhtml_activate_tag(GtkIMHtml *imhtml, GtkTextTag *tag);
143 static void gtk_imhtml_link_destroy(GtkIMHtmlLink *link);
145 /* POINT_SIZE converts from AIM font sizes to a point size scale factor. */
146 #define MAX_FONT_SIZE 7
147 #define POINT_SIZE(x) (_point_sizes [MIN ((x > 0 ? x : 1), MAX_FONT_SIZE) - 1])
148 static const gdouble _point_sizes [] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736};
150 enum {
151 TARGET_HTML,
152 TARGET_UTF8_STRING,
153 TARGET_COMPOUND_TEXT,
154 TARGET_STRING,
155 TARGET_TEXT
158 enum {
159 URL_CLICKED,
160 BUTTONS_UPDATE,
161 TOGGLE_FORMAT,
162 CLEAR_FORMAT,
163 UPDATE_FORMAT,
164 MESSAGE_SEND,
165 UNDO,
166 REDO,
167 PASTE,
168 LAST_SIGNAL
170 static guint signals [LAST_SIGNAL] = { 0 };
172 static char *html_clipboard = NULL;
173 static char *text_clipboard = NULL;
174 static GtkClipboard *clipboard_selection = NULL;
176 static const GtkTargetEntry selection_targets[] = {
177 #ifndef _WIN32
178 { "text/html", 0, TARGET_HTML },
179 #else
180 { "HTML Format", 0, TARGET_HTML },
181 #endif
182 { "UTF8_STRING", 0, TARGET_UTF8_STRING },
183 { "COMPOUND_TEXT", 0, TARGET_COMPOUND_TEXT },
184 { "STRING", 0, TARGET_STRING },
185 { "TEXT", 0, TARGET_TEXT}};
187 static const GtkTargetEntry link_drag_drop_targets[] = {
188 GTK_IMHTML_DND_TARGETS
191 #ifdef _WIN32
192 static gchar *
193 clipboard_win32_to_html(char *clipboard) {
194 const char *header;
195 const char *begin, *end;
196 gint start = 0;
197 gint finish = 0;
198 gchar *html;
199 gchar **split;
200 int clipboard_length = 0;
202 #if 0 /* Debugging for Windows clipboard */
203 FILE *fd;
205 purple_debug_info("imhtml clipboard", "from clipboard: %s\n", clipboard);
207 fd = g_fopen("c:\\purplecb.txt", "wb");
208 fprintf(fd, "%s", clipboard);
209 fclose(fd);
210 #endif
212 clipboard_length = strlen(clipboard);
214 if (!(header = strstr(clipboard, "StartFragment:")) || (header - clipboard) >= clipboard_length)
215 return NULL;
216 sscanf(header, "StartFragment:%d", &start);
218 if (!(header = strstr(clipboard, "EndFragment:")) || (header - clipboard) >= clipboard_length)
219 return NULL;
220 sscanf(header, "EndFragment:%d", &finish);
222 if (finish > clipboard_length)
223 finish = clipboard_length;
225 if (start > finish)
226 start = finish;
228 begin = clipboard + start;
230 end = clipboard + finish;
232 html = g_strndup(begin, end - begin);
234 /* any newlines in the string will now be \r\n, so we need to strip out the \r */
235 split = g_strsplit(html, "\r\n", 0);
236 g_free(html);
237 html = g_strjoinv("\n", split);
238 g_strfreev(split);
240 #if 0 /* Debugging for Windows clipboard */
241 purple_debug_info("imhtml clipboard", "HTML fragment: '%s'\n", html);
242 #endif
244 return html;
247 static gchar *
248 clipboard_html_to_win32(char *html) {
249 int length;
250 GString *clipboard;
252 if (html == NULL)
253 return NULL;
255 length = strlen(html);
256 clipboard = g_string_new ("Version:1.0\r\n");
257 g_string_append(clipboard, "StartHTML:0000000105\r\n");
258 g_string_append_printf(clipboard, "EndHTML:%010d\r\n", 147 + length);
259 g_string_append(clipboard, "StartFragment:0000000127\r\n");
260 g_string_append_printf(clipboard, "EndFragment:%010d\r\n", 127 + length);
261 g_string_append(clipboard, "<!--StartFragment-->\r\n");
262 g_string_append(clipboard, html);
263 g_string_append(clipboard, "\r\n<!--EndFragment-->");
265 return g_string_free(clipboard, FALSE);
268 static gboolean clipboard_paste_html_win32(GtkIMHtml *imhtml) {
269 gboolean pasted = FALSE;
271 /* Win32 clipboard format value, and functions to convert back and
272 * forth between HTML and the clipboard format.
274 static UINT win_html_fmt = 0;
276 /* Register HTML Format as desired clipboard format */
277 if (!win_html_fmt)
278 win_html_fmt = RegisterClipboardFormat("HTML Format");
280 if (gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml))
281 && IsClipboardFormatAvailable(win_html_fmt)) {
282 gboolean error_reading_clipboard = FALSE;
283 HWND hwnd = GDK_WINDOW_HWND(GTK_WIDGET(imhtml)->window);
285 if (OpenClipboard(hwnd)) {
286 HGLOBAL hdata = GetClipboardData(win_html_fmt);
287 if (hdata == NULL) {
288 if (GetLastError() != ERROR_SUCCESS)
289 error_reading_clipboard = TRUE;
290 } else {
291 char *buffer = GlobalLock(hdata);
292 if (buffer == NULL) {
293 error_reading_clipboard = TRUE;
294 } else {
295 char *text = clipboard_win32_to_html(
296 buffer);
297 imhtml_paste_insert(imhtml, text,
298 FALSE);
299 g_free(text);
300 pasted = TRUE;
302 GlobalUnlock(hdata);
305 CloseClipboard();
306 } else {
307 error_reading_clipboard = TRUE;
310 if (error_reading_clipboard) {
311 gchar *err_msg = g_win32_error_message(GetLastError());
312 purple_debug_info("html clipboard",
313 "Unable to read clipboard data: %s\n",
314 err_msg ? err_msg : "Unknown Error");
315 g_free(err_msg);
319 return pasted;
321 #endif
323 static GtkSmileyTree*
324 gtk_smiley_tree_new (void)
326 return g_new0 (GtkSmileyTree, 1);
329 static void
330 gtk_smiley_tree_insert (GtkSmileyTree *tree,
331 GtkIMHtmlSmiley *smiley)
333 GtkSmileyTree *t = tree;
334 const gchar *x = smiley->smile;
336 if (!(*x))
337 return;
339 do {
340 gchar *pos;
341 gint index;
343 if (!t->values)
344 t->values = g_string_new ("");
346 pos = strchr (t->values->str, *x);
347 if (!pos) {
348 t->values = g_string_append_c (t->values, *x);
349 index = t->values->len - 1;
350 t->children = g_realloc (t->children, t->values->len * sizeof (GtkSmileyTree *));
351 t->children [index] = g_new0 (GtkSmileyTree, 1);
352 } else
353 index = GPOINTER_TO_INT(pos) - GPOINTER_TO_INT(t->values->str);
355 t = t->children [index];
357 x++;
358 } while (*x);
360 t->image = smiley;
364 static void
365 gtk_smiley_tree_destroy (GtkSmileyTree *tree)
367 GSList *list = g_slist_prepend (NULL, tree);
369 while (list) {
370 GtkSmileyTree *t = list->data;
371 gsize i;
372 list = g_slist_remove(list, t);
373 if (t && t->values) {
374 for (i = 0; i < t->values->len; i++)
375 list = g_slist_prepend (list, t->children [i]);
376 g_string_free (t->values, TRUE);
377 g_free (t->children);
380 g_free (t);
384 static void (*parent_size_allocate)(GtkWidget *widget, GtkAllocation *alloc);
386 static void gtk_imhtml_size_allocate(GtkWidget *widget, GtkAllocation *alloc)
388 GtkIMHtml *imhtml = GTK_IMHTML(widget);
389 GdkRectangle rect;
390 int xminus;
391 int height = 0, y = 0;
392 GtkTextIter iter;
393 gboolean scroll = TRUE;
395 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &iter);
397 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(widget), &rect);
398 gtk_text_view_get_line_yrange(GTK_TEXT_VIEW(imhtml), &iter, &y, &height);
400 if (((y + height) - (rect.y + rect.height)) > height &&
401 gtk_text_buffer_get_char_count(imhtml->text_buffer)) {
402 scroll = FALSE;
405 if(imhtml->old_rect.width != rect.width || imhtml->old_rect.height != rect.height) {
406 GList *iter = GTK_IMHTML(widget)->scalables;
408 xminus = gtk_text_view_get_left_margin(GTK_TEXT_VIEW(widget)) +
409 gtk_text_view_get_right_margin(GTK_TEXT_VIEW(widget));
411 while(iter){
412 struct scalable_data *sd = iter->data;
413 GtkIMHtmlScalable *scale = GTK_IMHTML_SCALABLE(sd->scalable);
414 scale->scale(scale, rect.width - xminus, rect.height);
416 iter = iter->next;
420 imhtml->old_rect = rect;
421 parent_size_allocate(widget, alloc);
423 /* Don't scroll here if we're in the middle of a smooth scroll */
424 if (scroll && imhtml->scroll_time == NULL &&
425 GTK_WIDGET_REALIZED(imhtml))
426 gtk_imhtml_scroll_to_end(imhtml, FALSE);
429 #define DEFAULT_SEND_COLOR "#204a87"
430 #define DEFAULT_RECV_COLOR "#cc0000"
431 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
432 #define DEFAULT_ACTION_COLOR "#062585"
433 #define DEFAULT_WHISPER_ACTION_COLOR "#6C2585"
434 #define DEFAULT_WHISPER_COLOR "#00FF00"
436 static void (*parent_style_set)(GtkWidget *widget, GtkStyle *prev_style);
438 static void
439 gtk_imhtml_style_set(GtkWidget *widget, GtkStyle *prev_style)
441 int i;
442 struct {
443 const char *tag;
444 const char *color;
445 const char *def;
446 } styles[] = {
447 {"send-name", "send-name-color", DEFAULT_SEND_COLOR},
448 {"receive-name", "receive-name-color", DEFAULT_RECV_COLOR},
449 {"highlight-name", "highlight-name-color", DEFAULT_HIGHLIGHT_COLOR},
450 {"action-name", "action-name-color", DEFAULT_ACTION_COLOR},
451 {"whisper-action-name", "whisper-action-name-color", DEFAULT_WHISPER_ACTION_COLOR},
452 {"whisper-name", "whisper-name-color", DEFAULT_WHISPER_COLOR},
453 {NULL, NULL, NULL}
455 GtkIMHtml *imhtml = GTK_IMHTML(widget);
456 GtkTextTagTable *table = gtk_text_buffer_get_tag_table(imhtml->text_buffer);
458 for (i = 0; styles[i].tag; i++) {
459 GdkColor *color = NULL;
460 GtkTextTag *tag = gtk_text_tag_table_lookup(table, styles[i].tag);
461 if (!tag) {
462 purple_debug_warning("gtkimhtml", "Cannot find tag '%s'. This should never happen. Please file a bug.\n", styles[i].tag);
463 continue;
465 gtk_widget_style_get(widget, styles[i].color, &color, NULL);
466 if (color) {
467 g_object_set(tag, "foreground-gdk", color, NULL);
468 gdk_color_free(color);
469 } else {
470 GdkColor defcolor;
471 gdk_color_parse(styles[i].def, &defcolor);
472 g_object_set(tag, "foreground-gdk", &defcolor, NULL);
475 parent_style_set(widget, prev_style);
478 static gboolean
479 imhtml_get_iter_bounds(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *end)
481 if (imhtml->wbfo) {
482 gtk_text_buffer_get_bounds(imhtml->text_buffer, start, end);
483 return TRUE;
484 } else if (imhtml->editable) {
485 if (!gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, start, end)) {
486 GtkTextMark *mark = gtk_text_buffer_get_insert(imhtml->text_buffer);
487 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, start, mark);
488 *end = *start;
490 return TRUE;
493 return FALSE;
496 static void
497 gtk_imhtml_set_link_color(GtkIMHtml *imhtml, GtkTextTag *tag)
499 GdkColor *color = NULL;
500 gboolean visited = !!g_object_get_data(G_OBJECT(tag), "visited");
501 gtk_widget_style_get(GTK_WIDGET(imhtml), visited ? "hyperlink-visited-color" : "hyperlink-color", &color, NULL);
502 if (color) {
503 g_object_set(G_OBJECT(tag), "foreground-gdk", color, NULL);
504 gdk_color_free(color);
505 } else {
506 g_object_set(G_OBJECT(tag), "foreground", visited ? "#800000" : "blue", NULL);
510 static gint
511 gtk_imhtml_tip_paint (GtkIMHtml *imhtml)
513 PangoLayout *layout;
515 g_return_val_if_fail(GTK_IS_IMHTML(imhtml), FALSE);
517 /* We set the text in a separate function call so we can specify a
518 max length. This is important so the tooltip isn't too wide for
519 the screen, and also because some X library function exits the
520 process when it can't allocate enough memory for a super wide
521 tooltip. */
522 layout = gtk_widget_create_pango_layout(imhtml->tip_window, NULL);
523 pango_layout_set_text(layout, imhtml->tip, 200);
525 gtk_paint_flat_box (imhtml->tip_window->style, imhtml->tip_window->window,
526 GTK_STATE_NORMAL, GTK_SHADOW_OUT, NULL, imhtml->tip_window,
527 "tooltip", 0, 0, -1, -1);
529 gtk_paint_layout (imhtml->tip_window->style, imhtml->tip_window->window, GTK_STATE_NORMAL,
530 FALSE, NULL, imhtml->tip_window, NULL, 4, 4, layout);
532 g_object_unref(layout);
533 return FALSE;
536 static gint
537 gtk_imhtml_tip (gpointer data)
539 GtkIMHtml *imhtml = data;
540 PangoFontMetrics *font_metrics;
541 PangoLayout *layout;
542 PangoFont *font;
544 gint gap, x, y, h, w, scr_w, baseline_skip;
546 g_return_val_if_fail(GTK_IS_IMHTML(imhtml), FALSE);
548 if (!imhtml->tip || !GTK_WIDGET_DRAWABLE (GTK_WIDGET(imhtml))) {
549 imhtml->tip_timer = 0;
550 return FALSE;
553 if (imhtml->tip_window){
554 gtk_widget_destroy (imhtml->tip_window);
555 imhtml->tip_window = NULL;
558 imhtml->tip_timer = 0;
559 imhtml->tip_window = gtk_window_new (GTK_WINDOW_POPUP);
560 gtk_widget_set_app_paintable (imhtml->tip_window, TRUE);
561 gtk_window_set_title(GTK_WINDOW(imhtml->tip_window), "GtkIMHtml");
562 gtk_window_set_resizable (GTK_WINDOW (imhtml->tip_window), FALSE);
563 gtk_widget_set_name (imhtml->tip_window, "gtk-tooltips");
564 gtk_window_set_type_hint (GTK_WINDOW (imhtml->tip_window),
565 GDK_WINDOW_TYPE_HINT_TOOLTIP);
566 g_signal_connect_swapped (G_OBJECT (imhtml->tip_window), "expose_event",
567 G_CALLBACK (gtk_imhtml_tip_paint), imhtml);
569 gtk_widget_ensure_style (imhtml->tip_window);
571 /* We set the text in a separate function call so we can specify a
572 max length. This is important so the tooltip isn't too wide for
573 the screen, and also because some X library function exits the
574 process when it can't allocate enough memory for a super wide
575 tooltip. */
576 layout = gtk_widget_create_pango_layout(imhtml->tip_window, NULL);
577 pango_layout_set_text(layout, imhtml->tip, 200);
579 font = pango_context_load_font(pango_layout_get_context(layout),
580 imhtml->tip_window->style->font_desc);
582 if (font == NULL) {
583 char *tmp = pango_font_description_to_string(
584 imhtml->tip_window->style->font_desc);
586 purple_debug(PURPLE_DEBUG_ERROR, "gtk_imhtml_tip",
587 "pango_context_load_font() couldn't load font: '%s'\n",
588 tmp);
589 g_free(tmp);
591 g_object_unref(layout);
592 return FALSE;
595 font_metrics = pango_font_get_metrics(font, NULL);
597 pango_layout_get_pixel_size(layout, &scr_w, NULL);
598 gap = PANGO_PIXELS((pango_font_metrics_get_ascent(font_metrics) +
599 pango_font_metrics_get_descent(font_metrics))/ 4);
601 if (gap < 2)
602 gap = 2;
603 baseline_skip = PANGO_PIXELS(pango_font_metrics_get_ascent(font_metrics) +
604 pango_font_metrics_get_descent(font_metrics));
605 w = 8 + scr_w;
606 h = 8 + baseline_skip;
608 gdk_window_get_pointer (NULL, &x, &y, NULL);
609 if (GTK_WIDGET_NO_WINDOW (GTK_WIDGET(imhtml)))
610 y += GTK_WIDGET(imhtml)->allocation.y;
612 scr_w = gdk_screen_width();
614 x -= ((w >> 1) + 4);
616 if ((x + w) > scr_w)
617 x -= (x + w) - scr_w;
618 else if (x < 0)
619 x = 0;
621 y = y + PANGO_PIXELS(pango_font_metrics_get_ascent(font_metrics) +
622 pango_font_metrics_get_descent(font_metrics));
624 gtk_widget_set_size_request (imhtml->tip_window, w, h);
625 gtk_window_move (GTK_WINDOW(imhtml->tip_window), x, y);
626 gtk_widget_show (imhtml->tip_window);
628 pango_font_metrics_unref(font_metrics);
629 g_object_unref(font);
630 g_object_unref(layout);
632 return FALSE;
635 static gboolean
636 gtk_motion_event_notify(GtkWidget *imhtml, GdkEventMotion *event, gpointer data)
638 GtkTextIter iter;
639 GdkWindow *win = event->window;
640 int x, y;
641 char *tip = NULL;
642 GSList *tags = NULL, *templist = NULL;
643 GtkTextTag *tag = NULL, *oldprelit_tag;
644 GtkTextChildAnchor* anchor;
645 gboolean hand = TRUE;
646 GdkCursor *cursor = NULL;
648 oldprelit_tag = GTK_IMHTML(imhtml)->prelit_tag;
650 gdk_window_get_pointer(GTK_WIDGET(imhtml)->window, NULL, NULL, NULL);
651 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(imhtml), GTK_TEXT_WINDOW_WIDGET,
652 event->x, event->y, &x, &y);
653 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(imhtml), &iter, x, y);
654 tags = gtk_text_iter_get_tags(&iter);
656 templist = tags;
657 while (templist) {
658 tag = templist->data;
659 tip = g_object_get_data(G_OBJECT(tag), "link_url");
660 if (tip)
661 break;
662 templist = templist->next;
665 if (tip && (!tag || !g_object_get_data(G_OBJECT(tag), "visited"))) {
666 GTK_IMHTML(imhtml)->prelit_tag = tag;
667 if (tag != oldprelit_tag) {
668 GdkColor *pre = NULL;
669 gtk_widget_style_get(GTK_WIDGET(imhtml), "hyperlink-prelight-color", &pre, NULL);
670 if (pre) {
671 g_object_set(G_OBJECT(tag), "foreground-gdk", pre, NULL);
672 gdk_color_free(pre);
673 } else
674 g_object_set(G_OBJECT(tag), "foreground", "#70a0ff", NULL);
676 } else {
677 GTK_IMHTML(imhtml)->prelit_tag = NULL;
680 if ((oldprelit_tag != NULL) && (GTK_IMHTML(imhtml)->prelit_tag != oldprelit_tag)) {
681 gtk_imhtml_set_link_color(GTK_IMHTML(imhtml), oldprelit_tag);
684 if (GTK_IMHTML(imhtml)->tip) {
685 if (tip == GTK_IMHTML(imhtml)->tip) {
686 g_slist_free(tags);
687 return FALSE;
689 /* We've left the cell. Remove the timeout and create a new one below */
690 if (GTK_IMHTML(imhtml)->tip_window) {
691 gtk_widget_destroy(GTK_IMHTML(imhtml)->tip_window);
692 GTK_IMHTML(imhtml)->tip_window = NULL;
694 if (GTK_IMHTML(imhtml)->editable)
695 cursor = GTK_IMHTML(imhtml)->text_cursor;
696 else
697 cursor = GTK_IMHTML(imhtml)->arrow_cursor;
698 if (GTK_IMHTML(imhtml)->tip_timer)
699 g_source_remove(GTK_IMHTML(imhtml)->tip_timer);
700 GTK_IMHTML(imhtml)->tip_timer = 0;
703 /* If we don't have a tip from a URL, let's see if we have a tip from a smiley */
704 anchor = gtk_text_iter_get_child_anchor(&iter);
705 if (anchor) {
706 tip = g_object_get_data(G_OBJECT(anchor), "gtkimhtml_tiptext");
707 hand = FALSE;
710 if (tip && *tip) {
711 GTK_IMHTML(imhtml)->tip_timer = g_timeout_add (TOOLTIP_TIMEOUT,
712 gtk_imhtml_tip, imhtml);
713 } else if (!tip) {
714 hand = FALSE;
715 for (templist = tags; templist; templist = templist->next) {
716 tag = templist->data;
717 if ((tip = g_object_get_data(G_OBJECT(tag), "cursor"))) {
718 hand = TRUE;
719 break;
724 if (hand && !(GTK_IMHTML(imhtml)->editable))
725 cursor = GTK_IMHTML(imhtml)->hand_cursor;
727 if (cursor)
728 gdk_window_set_cursor(win, cursor);
730 GTK_IMHTML(imhtml)->tip = tip;
731 g_slist_free(tags);
732 return FALSE;
735 static gboolean
736 gtk_enter_event_notify(GtkWidget *imhtml, GdkEventCrossing *event, gpointer data)
738 if (GTK_IMHTML(imhtml)->editable)
739 gdk_window_set_cursor(
740 gtk_text_view_get_window(GTK_TEXT_VIEW(imhtml),
741 GTK_TEXT_WINDOW_TEXT),
742 GTK_IMHTML(imhtml)->text_cursor);
743 else
744 gdk_window_set_cursor(
745 gtk_text_view_get_window(GTK_TEXT_VIEW(imhtml),
746 GTK_TEXT_WINDOW_TEXT),
747 GTK_IMHTML(imhtml)->arrow_cursor);
749 /* propagate the event normally */
750 return FALSE;
753 static gboolean
754 gtk_leave_event_notify(GtkWidget *imhtml, GdkEventCrossing *event, gpointer data)
756 /* when leaving the widget, clear any current & pending tooltips and restore the cursor */
757 if (GTK_IMHTML(imhtml)->prelit_tag) {
758 gtk_imhtml_set_link_color(GTK_IMHTML(imhtml), GTK_IMHTML(imhtml)->prelit_tag);
759 GTK_IMHTML(imhtml)->prelit_tag = NULL;
762 if (GTK_IMHTML(imhtml)->tip_window) {
763 gtk_widget_destroy(GTK_IMHTML(imhtml)->tip_window);
764 GTK_IMHTML(imhtml)->tip_window = NULL;
766 if (GTK_IMHTML(imhtml)->tip_timer) {
767 g_source_remove(GTK_IMHTML(imhtml)->tip_timer);
768 GTK_IMHTML(imhtml)->tip_timer = 0;
770 gdk_window_set_cursor(
771 gtk_text_view_get_window(GTK_TEXT_VIEW(imhtml),
772 GTK_TEXT_WINDOW_TEXT), NULL);
774 /* propagate the event normally */
775 return FALSE;
778 static gint
779 gtk_imhtml_expose_event (GtkWidget *widget,
780 GdkEventExpose *event)
782 GtkTextIter start, end, cur;
783 int buf_x, buf_y;
784 GdkRectangle visible_rect;
785 cairo_t *cr = gdk_cairo_create(GDK_DRAWABLE(event->window));
786 GdkColor gcolor;
788 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(widget), &visible_rect);
789 gtk_text_view_buffer_to_window_coords(GTK_TEXT_VIEW(widget),
790 GTK_TEXT_WINDOW_TEXT,
791 visible_rect.x,
792 visible_rect.y,
793 &visible_rect.x,
794 &visible_rect.y);
796 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(widget), GTK_TEXT_WINDOW_TEXT,
797 event->area.x, event->area.y, &buf_x, &buf_y);
799 if (GTK_IMHTML(widget)->editable || GTK_IMHTML(widget)->wbfo) {
801 if (GTK_IMHTML(widget)->edit.background) {
802 gdk_color_parse(GTK_IMHTML(widget)->edit.background, &gcolor);
803 gdk_cairo_set_source_color(cr, &gcolor);
804 } else {
805 gdk_cairo_set_source_color(cr, &(widget->style->base[GTK_WIDGET_STATE(widget)]));
808 cairo_rectangle(cr,
809 visible_rect.x, visible_rect.y,
810 visible_rect.width, visible_rect.height);
811 cairo_fill(cr);
812 cairo_destroy(cr);
814 if (GTK_WIDGET_CLASS (parent_class)->expose_event)
815 return (* GTK_WIDGET_CLASS (parent_class)->expose_event)
816 (widget, event);
817 return FALSE;
821 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &start, buf_x, buf_y);
822 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &end,
823 buf_x + event->area.width, buf_y + event->area.height);
825 gtk_text_iter_order(&start, &end);
827 cur = start;
829 while (gtk_text_iter_in_range(&cur, &start, &end)) {
830 GSList *tags = gtk_text_iter_get_tags(&cur);
831 GSList *l;
833 for (l = tags; l; l = l->next) {
834 GtkTextTag *tag = l->data;
835 GdkRectangle rect;
836 GdkRectangle tag_area;
837 const char *color;
839 if (strncmp(tag->name, "BACKGROUND ", 11))
840 continue;
842 if (gtk_text_iter_ends_tag(&cur, tag))
843 continue;
845 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(widget), &cur, &tag_area);
846 gtk_text_view_buffer_to_window_coords(GTK_TEXT_VIEW(widget),
847 GTK_TEXT_WINDOW_TEXT,
848 tag_area.x,
849 tag_area.y,
850 &tag_area.x,
851 &tag_area.y);
852 rect.x = visible_rect.x;
853 rect.y = tag_area.y;
854 rect.width = visible_rect.width;
857 gtk_text_iter_forward_to_tag_toggle(&cur, tag);
858 while (!gtk_text_iter_is_end(&cur) && gtk_text_iter_begins_tag(&cur, tag));
860 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(widget), &cur, &tag_area);
861 gtk_text_view_buffer_to_window_coords(GTK_TEXT_VIEW(widget),
862 GTK_TEXT_WINDOW_TEXT,
863 tag_area.x,
864 tag_area.y,
865 &tag_area.x,
866 &tag_area.y);
869 rect.height = tag_area.y + tag_area.height - rect.y
870 + gtk_text_view_get_pixels_above_lines(GTK_TEXT_VIEW(widget))
871 + gtk_text_view_get_pixels_below_lines(GTK_TEXT_VIEW(widget));
873 color = tag->name + 11;
875 if (!gdk_color_parse(color, &gcolor)) {
876 gchar tmp[8];
877 tmp[0] = '#';
878 strncpy(&tmp[1], color, 7);
879 tmp[7] = '\0';
880 if (!gdk_color_parse(tmp, &gcolor))
881 gdk_color_parse("white", &gcolor);
883 gdk_cairo_set_source_color(cr, &gcolor);
885 cairo_rectangle(cr,
886 rect.x, rect.y,
887 rect.width, rect.height);
888 cairo_fill(cr);
889 gtk_text_iter_backward_char(&cur); /* go back one, in case the end is the begining is the end
890 * note that above, we always moved cur ahead by at least
891 * one character */
892 break;
895 g_slist_free(tags);
897 /* loop until another tag begins, or no tag begins */
898 while (gtk_text_iter_forward_to_tag_toggle(&cur, NULL) &&
899 !gtk_text_iter_is_end(&cur) &&
900 !gtk_text_iter_begins_tag(&cur, NULL));
903 cairo_destroy(cr);
905 if (GTK_WIDGET_CLASS (parent_class)->expose_event)
906 return (* GTK_WIDGET_CLASS (parent_class)->expose_event)
907 (widget, event);
909 return FALSE;
913 static void paste_unformatted_cb(GtkMenuItem *menu, GtkIMHtml *imhtml)
915 GtkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD);
917 gtk_clipboard_request_text(clipboard, paste_plaintext_received_cb, imhtml);
921 static void clear_formatting_cb(GtkMenuItem *menu, GtkIMHtml *imhtml)
923 gtk_imhtml_clear_formatting(imhtml);
926 static void disable_smiley_selected(GtkMenuItem *item, GtkIMHtml *imhtml)
928 GtkTextIter start, end;
929 GtkTextMark *mark;
930 char *text;
932 if (!gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end))
933 return;
935 text = gtk_imhtml_get_markup_range(imhtml, &start, &end);
937 mark = gtk_text_buffer_get_selection_bound(imhtml->text_buffer);
938 gtk_text_buffer_delete_selection(imhtml->text_buffer, FALSE, FALSE);
940 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &start, mark);
941 gtk_imhtml_insert_html_at_iter(imhtml, text, GTK_IMHTML_NO_NEWLINE | GTK_IMHTML_NO_SMILEY, &start);
943 g_free(text);
946 static void hijack_menu_cb(GtkIMHtml *imhtml, GtkMenu *menu, gpointer data)
948 GtkWidget *menuitem;
949 GtkTextIter start, end;
951 menuitem = gtk_menu_item_new_with_mnemonic(_("Paste as Plain _Text"));
952 gtk_widget_show(menuitem);
954 * TODO: gtk_clipboard_wait_is_text_available() iterates the glib
955 * mainloop, which tends to be a source of bugs. It would
956 * be good to audit this or change it to not wait.
958 gtk_widget_set_sensitive(menuitem,
959 (imhtml->editable &&
960 gtk_clipboard_wait_is_text_available(
961 gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD))));
962 /* put it after "Paste" */
963 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 3);
965 g_signal_connect(G_OBJECT(menuitem), "activate",
966 G_CALLBACK(paste_unformatted_cb), imhtml);
968 menuitem = gtk_menu_item_new_with_mnemonic(_("_Reset formatting"));
969 gtk_widget_show(menuitem);
970 gtk_widget_set_sensitive(menuitem, imhtml->editable);
971 /* put it after Delete */
972 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 5);
974 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(clear_formatting_cb), imhtml);
976 menuitem = gtk_menu_item_new_with_mnemonic(_("Disable _smileys in selected text"));
977 gtk_widget_show(menuitem);
978 if (gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) {
979 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(disable_smiley_selected), imhtml);
980 } else {
981 gtk_widget_set_sensitive(menuitem, FALSE);
983 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 6);
986 static char *
987 ucs2_order(gboolean swap)
989 gboolean be;
991 be = G_BYTE_ORDER == G_BIG_ENDIAN;
992 be = swap ? be : !be;
994 if (be)
995 return "UTF-16BE";
996 else
997 return "UTF-16LE";
1001 /* Convert from UTF-16LE to UTF-8, stripping the BOM if one is present.*/
1002 static gchar *
1003 utf16_to_utf8_with_bom_check(gchar *data, guint len) {
1004 char *fromcode = NULL;
1005 GError *error = NULL;
1006 guint16 c;
1007 gchar *utf8_ret;
1010 * Unicode Techinical Report 20
1011 * ( http://www.unicode.org/unicode/reports/tr20/ ) says to treat an
1012 * initial 0xfeff (ZWNBSP) as a byte order indicator so that is
1013 * what we do. If there is no indicator assume it is in the default
1014 * order
1017 memcpy(&c, data, 2);
1018 switch (c) {
1019 case 0xfeff:
1020 case 0xfffe:
1021 fromcode = ucs2_order(c == 0xfeff);
1022 data += 2;
1023 len -= 2;
1024 break;
1025 default:
1026 fromcode = "UTF-16";
1027 break;
1030 utf8_ret = g_convert(data, len, "UTF-8", fromcode, NULL, NULL, &error);
1032 if (error) {
1033 purple_debug_warning("gtkimhtml", "g_convert error: %s\n", error->message);
1034 g_error_free(error);
1036 return utf8_ret;
1040 static void gtk_imhtml_clipboard_get(GtkClipboard *clipboard, GtkSelectionData *selection_data, guint info, GtkIMHtml *imhtml) {
1041 char *text = NULL;
1042 gboolean primary = (clipboard != clipboard_selection);
1043 GtkTextIter start, end;
1045 if (primary) {
1046 GtkTextMark *sel = NULL, *ins = NULL;
1048 g_return_if_fail(imhtml != NULL);
1050 ins = gtk_text_buffer_get_insert(imhtml->text_buffer);
1051 sel = gtk_text_buffer_get_selection_bound(imhtml->text_buffer);
1052 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &start, sel);
1053 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &end, ins);
1056 if (info == TARGET_HTML) {
1057 char *selection;
1058 #ifndef _WIN32
1059 gsize len;
1060 if (primary) {
1061 text = gtk_imhtml_get_markup_range(imhtml, &start, &end);
1062 } else
1063 text = html_clipboard;
1065 /* Mozilla asks that we start our text/html with the Unicode byte order mark */
1066 selection = g_convert(text, -1, "UTF-16", "UTF-8", NULL, &len, NULL);
1067 gtk_selection_data_set(selection_data, gdk_atom_intern("text/html", FALSE), 16, (const guchar *)selection, len);
1068 #else
1069 selection = clipboard_html_to_win32(html_clipboard);
1070 gtk_selection_data_set(selection_data, gdk_atom_intern("HTML Format", FALSE), 8, (const guchar *)selection, strlen(selection));
1071 #endif
1072 g_free(selection);
1073 } else {
1074 if (primary) {
1075 text = gtk_imhtml_get_text(imhtml, &start, &end);
1076 } else
1077 text = text_clipboard;
1078 gtk_selection_data_set_text(selection_data, text, strlen(text));
1080 if (primary) /* This was allocated here */
1081 g_free(text);
1084 static void gtk_imhtml_primary_clipboard_clear(GtkClipboard *clipboard, GtkIMHtml *imhtml)
1086 GtkTextIter insert;
1087 GtkTextIter selection_bound;
1089 gtk_text_buffer_get_iter_at_mark (imhtml->text_buffer, &insert,
1090 gtk_text_buffer_get_mark (imhtml->text_buffer, "insert"));
1091 gtk_text_buffer_get_iter_at_mark (imhtml->text_buffer, &selection_bound,
1092 gtk_text_buffer_get_mark (imhtml->text_buffer, "selection_bound"));
1094 if (!gtk_text_iter_equal (&insert, &selection_bound))
1095 gtk_text_buffer_move_mark (imhtml->text_buffer,
1096 gtk_text_buffer_get_mark (imhtml->text_buffer, "selection_bound"),
1097 &insert);
1100 static void gtk_imhtml_clipboard_clear (GtkClipboard *clipboard, GtkSelectionData *sel_data,
1101 guint info, gpointer user_data_or_owner)
1105 static void copy_clipboard_cb(GtkIMHtml *imhtml, gpointer unused)
1107 GtkTextIter start, end;
1108 if (gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) {
1109 if (!clipboard_selection)
1110 clipboard_selection = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD);
1111 gtk_clipboard_set_with_data(clipboard_selection,
1112 selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry),
1113 (GtkClipboardGetFunc)gtk_imhtml_clipboard_get,
1114 (GtkClipboardClearFunc)gtk_imhtml_clipboard_clear, NULL);
1116 g_free(html_clipboard);
1117 g_free(text_clipboard);
1119 html_clipboard = gtk_imhtml_get_markup_range(imhtml, &start, &end);
1120 text_clipboard = gtk_imhtml_get_text(imhtml, &start, &end);
1123 g_signal_stop_emission_by_name(imhtml, "copy-clipboard");
1126 static void cut_clipboard_cb(GtkIMHtml *imhtml, gpointer unused)
1128 GtkTextIter start, end;
1129 if (gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) {
1130 if (!clipboard_selection)
1131 clipboard_selection = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD);
1132 gtk_clipboard_set_with_data(clipboard_selection,
1133 selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry),
1134 (GtkClipboardGetFunc)gtk_imhtml_clipboard_get,
1135 (GtkClipboardClearFunc)gtk_imhtml_clipboard_clear, NULL);
1137 g_free(html_clipboard);
1138 g_free(text_clipboard);
1140 html_clipboard = gtk_imhtml_get_markup_range(imhtml, &start, &end);
1141 text_clipboard = gtk_imhtml_get_text(imhtml, &start, &end);
1143 if (imhtml->editable)
1144 gtk_text_buffer_delete_selection(imhtml->text_buffer, FALSE, FALSE);
1147 g_signal_stop_emission_by_name(imhtml, "cut-clipboard");
1150 static void imhtml_paste_insert(GtkIMHtml *imhtml, const char *text, gboolean plaintext)
1152 GtkTextIter iter;
1153 GtkIMHtmlOptions flags = plaintext ? GTK_IMHTML_NO_SMILEY : (GTK_IMHTML_NO_NEWLINE | GTK_IMHTML_NO_COMMENTS);
1155 /* Delete any currently selected text */
1156 gtk_text_buffer_delete_selection(imhtml->text_buffer, TRUE, TRUE);
1158 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, gtk_text_buffer_get_insert(imhtml->text_buffer));
1159 if (!imhtml->wbfo && !plaintext)
1160 gtk_imhtml_close_tags(imhtml, &iter);
1162 gtk_imhtml_insert_html_at_iter(imhtml, text, flags, &iter);
1163 gtk_text_buffer_move_mark_by_name(imhtml->text_buffer, "insert", &iter);
1164 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(imhtml), gtk_text_buffer_get_insert(imhtml->text_buffer),
1165 0, FALSE, 0.0, 0.0);
1166 if (!imhtml->wbfo && !plaintext)
1167 gtk_imhtml_close_tags(imhtml, &iter);
1171 static void paste_plaintext_received_cb (GtkClipboard *clipboard, const gchar *text, gpointer data)
1173 char *tmp;
1175 if (text == NULL || !(*text))
1176 return;
1178 tmp = g_markup_escape_text(text, -1);
1179 imhtml_paste_insert(data, tmp, TRUE);
1180 g_free(tmp);
1183 static void paste_received_cb (GtkClipboard *clipboard, GtkSelectionData *selection_data, gpointer data)
1185 char *text;
1186 GtkIMHtml *imhtml = data;
1188 if (!gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml)))
1189 return;
1191 if (imhtml->wbfo || selection_data->length <= 0) {
1192 gtk_clipboard_request_text(clipboard, paste_plaintext_received_cb, imhtml);
1193 return;
1194 } else {
1195 #if 0
1196 /* Here's some debug code, for figuring out what sent to us over the clipboard. */
1198 int i;
1200 purple_debug_misc("gtkimhtml", "In paste_received_cb():\n\tformat = %d, length = %d\n\t",
1201 selection_data->format, selection_data->length);
1203 for (i = 0; i < (/*(selection_data->format / 8) **/ selection_data->length); i++) {
1204 if ((i % 70) == 0)
1205 printf("\n\t");
1206 if (selection_data->data[i] == '\0')
1207 printf(".");
1208 else
1209 printf("%c", selection_data->data[i]);
1211 printf("\n");
1213 #endif
1215 text = g_malloc(selection_data->length + 1);
1216 memcpy(text, selection_data->data, selection_data->length);
1217 /* Make sure the paste data is null-terminated. Given that
1218 * we're passed length (but assume later that it is
1219 * null-terminated), this seems sensible to me.
1221 text[selection_data->length] = '\0';
1224 #ifdef _WIN32
1225 if (gtk_selection_data_get_data_type(selection_data) == gdk_atom_intern("HTML Format", FALSE)) {
1226 char *tmp = clipboard_win32_to_html(text);
1227 g_free(text);
1228 text = tmp;
1230 #endif
1232 if (selection_data->length >= 2 &&
1233 (*(guint16 *)text == 0xfeff || *(guint16 *)text == 0xfffe)) {
1234 /* This is UTF-16 */
1235 char *utf8 = utf16_to_utf8_with_bom_check(text, selection_data->length);
1236 g_free(text);
1237 text = utf8;
1238 if (!text) {
1239 purple_debug_warning("gtkimhtml", "g_convert from UTF-16 failed in paste_received_cb\n");
1240 return;
1244 if (!(*text) || !g_utf8_validate(text, -1, NULL)) {
1245 purple_debug_warning("gtkimhtml", "empty string or invalid UTF-8 in paste_received_cb\n");
1246 g_free(text);
1247 return;
1250 imhtml_paste_insert(imhtml, text, FALSE);
1251 g_free(text);
1255 static void smart_backspace_cb(GtkIMHtml *imhtml, gpointer blah)
1257 GtkTextIter iter;
1258 GtkTextChildAnchor* anchor;
1259 char * text;
1260 gint offset;
1262 if (!imhtml->editable)
1263 return;
1265 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, gtk_text_buffer_get_insert(imhtml->text_buffer));
1267 /* Get the character before the insertion point */
1268 offset = gtk_text_iter_get_offset(&iter);
1269 if (offset <= 0)
1270 return;
1272 gtk_text_iter_backward_char(&iter);
1273 anchor = gtk_text_iter_get_child_anchor(&iter);
1275 if (!anchor)
1276 return; /* No smiley here */
1278 text = g_object_get_data(G_OBJECT(anchor), "gtkimhtml_plaintext");
1279 if (!text)
1280 return;
1282 /* ok, then we need to insert the image buffer text before the anchor */
1283 gtk_text_buffer_insert(imhtml->text_buffer, &iter, text, -1);
1286 static void paste_clipboard_cb(GtkIMHtml *imhtml, gpointer blah)
1288 #ifdef _WIN32
1289 /* If we're on windows, let's see if we can get data from the HTML Format
1290 clipboard before we try to paste from the GTK buffer */
1291 if (!clipboard_paste_html_win32(imhtml) && gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml))) {
1292 GtkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD);
1293 gtk_clipboard_request_text(clipboard, paste_plaintext_received_cb, imhtml);
1296 #else
1297 GtkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD);
1298 gtk_clipboard_request_contents(clipboard, gdk_atom_intern("text/html", FALSE),
1299 paste_received_cb, imhtml);
1300 #endif
1301 g_signal_stop_emission_by_name(imhtml, "paste-clipboard");
1304 static void imhtml_realized_remove_primary(GtkIMHtml *imhtml, gpointer unused)
1306 gtk_text_buffer_remove_selection_clipboard(GTK_IMHTML(imhtml)->text_buffer,
1307 gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_PRIMARY));
1311 static void imhtml_destroy_add_primary(GtkIMHtml *imhtml, gpointer unused)
1313 gtk_text_buffer_add_selection_clipboard(GTK_IMHTML(imhtml)->text_buffer,
1314 gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_PRIMARY));
1317 static void mark_set_so_update_selection_cb(GtkTextBuffer *buffer, GtkTextIter *arg1, GtkTextMark *mark, GtkIMHtml *imhtml)
1319 if (gtk_text_buffer_get_selection_bounds(buffer, NULL, NULL)) {
1320 gtk_clipboard_set_with_owner(gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_PRIMARY),
1321 selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry),
1322 (GtkClipboardGetFunc)gtk_imhtml_clipboard_get,
1323 (GtkClipboardClearFunc)gtk_imhtml_primary_clipboard_clear, G_OBJECT(imhtml));
1327 static gboolean gtk_imhtml_button_press_event(GtkIMHtml *imhtml, GdkEventButton *event, gpointer unused)
1329 if (event->button == 2) {
1330 int x, y;
1331 GtkTextIter iter;
1332 GtkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_PRIMARY);
1334 if (!imhtml->editable)
1335 return FALSE;
1337 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(imhtml),
1338 GTK_TEXT_WINDOW_TEXT,
1339 event->x,
1340 event->y,
1342 &y);
1343 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(imhtml), &iter, x, y);
1344 gtk_text_buffer_place_cursor(imhtml->text_buffer, &iter);
1346 gtk_clipboard_request_contents(clipboard, gdk_atom_intern("text/html", FALSE),
1347 paste_received_cb, imhtml);
1349 return TRUE;
1352 return FALSE;
1355 static void
1356 gtk_imhtml_undo(GtkIMHtml *imhtml)
1358 g_return_if_fail(GTK_IS_IMHTML(imhtml));
1359 if (imhtml->editable &&
1360 gtk_source_undo_manager_can_undo(imhtml->undo_manager))
1361 gtk_source_undo_manager_undo(imhtml->undo_manager);
1364 static void
1365 gtk_imhtml_redo(GtkIMHtml *imhtml)
1367 g_return_if_fail(GTK_IS_IMHTML(imhtml));
1368 if (imhtml->editable &&
1369 gtk_source_undo_manager_can_redo(imhtml->undo_manager))
1370 gtk_source_undo_manager_redo(imhtml->undo_manager);
1374 static gboolean imhtml_message_send(GtkIMHtml *imhtml)
1376 return FALSE;
1379 static void
1380 imhtml_paste_cb(GtkIMHtml *imhtml, const char *str)
1382 if (!gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml)))
1383 return;
1385 if (!str || !*str || purple_strequal(str, "html"))
1386 g_signal_emit_by_name(imhtml, "paste_clipboard");
1387 else if (purple_strequal(str, "text"))
1388 paste_unformatted_cb(NULL, imhtml);
1391 static void imhtml_toggle_format(GtkIMHtml *imhtml, GtkIMHtmlButtons buttons)
1393 /* since this function is the handler for the formatting keystrokes,
1394 we need to check here that the formatting attempted is permitted */
1395 buttons &= imhtml->format_functions;
1397 switch (buttons) {
1398 case GTK_IMHTML_BOLD:
1399 imhtml_toggle_bold(imhtml);
1400 break;
1401 case GTK_IMHTML_ITALIC:
1402 imhtml_toggle_italic(imhtml);
1403 break;
1404 case GTK_IMHTML_UNDERLINE:
1405 imhtml_toggle_underline(imhtml);
1406 break;
1407 case GTK_IMHTML_STRIKE:
1408 imhtml_toggle_strike(imhtml);
1409 break;
1410 case GTK_IMHTML_SHRINK:
1411 imhtml_font_shrink(imhtml);
1412 break;
1413 case GTK_IMHTML_GROW:
1414 imhtml_font_grow(imhtml);
1415 break;
1416 default:
1417 break;
1421 static void
1422 gtk_imhtml_finalize (GObject *object)
1424 GtkIMHtml *imhtml = GTK_IMHTML(object);
1425 GList *scalables;
1426 GSList *l;
1428 if (imhtml->scroll_src)
1429 g_source_remove(imhtml->scroll_src);
1430 if (imhtml->scroll_time)
1431 g_timer_destroy(imhtml->scroll_time);
1433 g_hash_table_destroy(imhtml->smiley_data);
1434 gtk_smiley_tree_destroy(imhtml->default_smilies);
1435 gdk_cursor_unref(imhtml->hand_cursor);
1436 gdk_cursor_unref(imhtml->arrow_cursor);
1437 gdk_cursor_unref(imhtml->text_cursor);
1439 if(imhtml->tip_window){
1440 gtk_widget_destroy(imhtml->tip_window);
1442 if(imhtml->tip_timer)
1443 g_source_remove(imhtml->tip_timer);
1445 for(scalables = imhtml->scalables; scalables; scalables = scalables->next) {
1446 struct scalable_data *sd = scalables->data;
1447 GtkIMHtmlScalable *scale = GTK_IMHTML_SCALABLE(sd->scalable);
1448 scale->free(scale);
1449 g_free(sd);
1452 for (l = imhtml->im_images; l; l = l->next) {
1453 struct im_image_data *img_data = l->data;
1454 if (imhtml->funcs->image_unref)
1455 imhtml->funcs->image_unref(img_data->id);
1456 g_free(img_data);
1459 g_list_free(imhtml->scalables);
1460 g_slist_free(imhtml->im_images);
1461 g_queue_free(imhtml->animations);
1462 g_free(imhtml->protocol_name);
1463 g_free(imhtml->search_string);
1464 g_object_unref(imhtml->undo_manager);
1465 G_OBJECT_CLASS(parent_class)->finalize (object);
1469 static GtkIMHtmlProtocol *
1470 imhtml_find_protocol(const char *url, gboolean reverse)
1472 GtkIMHtmlClass *klass;
1473 GList *iter;
1474 GtkIMHtmlProtocol *proto = NULL;
1475 int length = reverse ? strlen(url) : 0;
1477 klass = g_type_class_ref(GTK_TYPE_IMHTML);
1478 for (iter = klass->protocols; iter; iter = iter->next) {
1479 proto = iter->data;
1480 if (g_ascii_strncasecmp(url, proto->name, reverse ? MIN(length, proto->length) : proto->length) == 0) {
1481 return proto;
1484 return NULL;
1487 static void
1488 imhtml_url_clicked(GtkIMHtml *imhtml, const char *url)
1490 GtkIMHtmlProtocol *proto = imhtml_find_protocol(url, FALSE);
1491 GtkIMHtmlLink *link;
1492 if (!proto)
1493 return;
1494 link = g_new0(GtkIMHtmlLink, 1);
1495 link->imhtml = g_object_ref(imhtml);
1496 link->url = g_strdup(url);
1497 proto->activate(imhtml, link); /* XXX: Do something with the return value? */
1498 gtk_imhtml_link_destroy(link);
1501 /* Boring GTK+ stuff */
1502 static void gtk_imhtml_class_init (GtkIMHtmlClass *klass)
1504 GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
1505 GtkBindingSet *binding_set;
1506 GObjectClass *gobject_class;
1507 gobject_class = (GObjectClass*) klass;
1508 parent_class = g_type_class_ref(GTK_TYPE_TEXT_VIEW);
1509 signals[URL_CLICKED] = g_signal_new("url_clicked",
1510 G_TYPE_FROM_CLASS(gobject_class),
1511 G_SIGNAL_RUN_FIRST,
1512 G_STRUCT_OFFSET(GtkIMHtmlClass, url_clicked),
1513 NULL,
1515 g_cclosure_marshal_VOID__POINTER,
1516 G_TYPE_NONE, 1,
1517 G_TYPE_POINTER);
1518 signals[BUTTONS_UPDATE] = g_signal_new("format_buttons_update",
1519 G_TYPE_FROM_CLASS(gobject_class),
1520 G_SIGNAL_RUN_FIRST,
1521 G_STRUCT_OFFSET(GtkIMHtmlClass, buttons_update),
1522 NULL,
1524 g_cclosure_marshal_VOID__INT,
1525 G_TYPE_NONE, 1,
1526 G_TYPE_INT);
1527 signals[TOGGLE_FORMAT] = g_signal_new("format_function_toggle",
1528 G_TYPE_FROM_CLASS(gobject_class),
1529 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1530 G_STRUCT_OFFSET(GtkIMHtmlClass, toggle_format),
1531 NULL,
1533 g_cclosure_marshal_VOID__INT,
1534 G_TYPE_NONE, 1,
1535 G_TYPE_INT);
1536 signals[CLEAR_FORMAT] = g_signal_new("format_function_clear",
1537 G_TYPE_FROM_CLASS(gobject_class),
1538 G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
1539 G_STRUCT_OFFSET(GtkIMHtmlClass, clear_format),
1540 NULL,
1542 g_cclosure_marshal_VOID__VOID,
1543 G_TYPE_NONE, 0);
1544 signals[UPDATE_FORMAT] = g_signal_new("format_function_update",
1545 G_TYPE_FROM_CLASS(gobject_class),
1546 G_SIGNAL_RUN_FIRST,
1547 G_STRUCT_OFFSET(GtkIMHtmlClass, update_format),
1548 NULL,
1550 g_cclosure_marshal_VOID__VOID,
1551 G_TYPE_NONE, 0);
1552 signals[MESSAGE_SEND] = g_signal_new("message_send",
1553 G_TYPE_FROM_CLASS(gobject_class),
1554 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1555 G_STRUCT_OFFSET(GtkIMHtmlClass, message_send),
1556 NULL,
1557 0, g_cclosure_marshal_VOID__VOID,
1558 G_TYPE_NONE, 0);
1559 signals[PASTE] = g_signal_new("paste",
1560 G_TYPE_FROM_CLASS(gobject_class),
1561 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1563 NULL,
1564 0, g_cclosure_marshal_VOID__STRING,
1565 G_TYPE_NONE, 1, G_TYPE_STRING);
1566 signals [UNDO] = g_signal_new ("undo",
1567 G_TYPE_FROM_CLASS (klass),
1568 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1569 G_STRUCT_OFFSET (GtkIMHtmlClass, undo),
1570 NULL,
1571 NULL,
1572 gtksourceview_marshal_VOID__VOID,
1573 G_TYPE_NONE,
1575 signals [REDO] = g_signal_new ("redo",
1576 G_TYPE_FROM_CLASS (klass),
1577 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
1578 G_STRUCT_OFFSET (GtkIMHtmlClass, redo),
1579 NULL,
1580 NULL,
1581 gtksourceview_marshal_VOID__VOID,
1582 G_TYPE_NONE,
1587 klass->toggle_format = imhtml_toggle_format;
1588 klass->message_send = imhtml_message_send;
1589 klass->clear_format = imhtml_clear_formatting;
1590 klass->url_clicked = imhtml_url_clicked;
1591 klass->undo = gtk_imhtml_undo;
1592 klass->redo = gtk_imhtml_redo;
1594 gobject_class->finalize = gtk_imhtml_finalize;
1595 widget_class->drag_motion = gtk_text_view_drag_motion;
1596 widget_class->expose_event = gtk_imhtml_expose_event;
1597 parent_size_allocate = widget_class->size_allocate;
1598 widget_class->size_allocate = gtk_imhtml_size_allocate;
1599 parent_style_set = widget_class->style_set;
1600 widget_class->style_set = gtk_imhtml_style_set;
1602 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("hyperlink-color",
1603 _("Hyperlink color"),
1604 _("Color to draw hyperlinks."),
1605 GDK_TYPE_COLOR, G_PARAM_READABLE));
1606 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("hyperlink-visited-color",
1607 _("Hyperlink visited color"),
1608 _("Color to draw hyperlink after it has been visited (or activated)."),
1609 GDK_TYPE_COLOR, G_PARAM_READABLE));
1610 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("hyperlink-prelight-color",
1611 _("Hyperlink prelight color"),
1612 _("Color to draw hyperlinks when mouse is over them."),
1613 GDK_TYPE_COLOR, G_PARAM_READABLE));
1614 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("send-name-color",
1615 _("Sent Message Name Color"),
1616 _("Color to draw the name of a message you sent."),
1617 GDK_TYPE_COLOR, G_PARAM_READABLE));
1618 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("receive-name-color",
1619 _("Received Message Name Color"),
1620 _("Color to draw the name of a message you received."),
1621 GDK_TYPE_COLOR, G_PARAM_READABLE));
1622 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("highlight-name-color",
1623 _("\"Attention\" Name Color"),
1624 _("Color to draw the name of a message you received containing your name."),
1625 GDK_TYPE_COLOR, G_PARAM_READABLE));
1626 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("action-name-color",
1627 _("Action Message Name Color"),
1628 _("Color to draw the name of an action message."),
1629 GDK_TYPE_COLOR, G_PARAM_READABLE));
1630 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("whisper-action-name-color",
1631 _("Action Message Name Color for Whispered Message"),
1632 _("Color to draw the name of a whispered action message."),
1633 GDK_TYPE_COLOR, G_PARAM_READABLE));
1634 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("whisper-name-color",
1635 _("Whisper Message Name Color"),
1636 _("Color to draw the name of a whispered message."),
1637 GDK_TYPE_COLOR, G_PARAM_READABLE));
1639 /* Customizable typing notification ... sort of. Example:
1640 * GtkIMHtml::typing-notification-font = "monospace italic light 8.0"
1641 * GtkIMHtml::typing-notification-color = "#ff0000"
1642 * GtkIMHtml::typing-notification-enable = 1
1644 gtk_widget_class_install_style_property(widget_class, g_param_spec_boxed("typing-notification-color",
1645 _("Typing notification color"),
1646 _("The color to use for the typing notification"),
1647 GDK_TYPE_COLOR, G_PARAM_READABLE));
1648 gtk_widget_class_install_style_property(widget_class, g_param_spec_string("typing-notification-font",
1649 _("Typing notification font"),
1650 _("The font to use for the typing notification"),
1651 "light 8.0", G_PARAM_READABLE));
1652 gtk_widget_class_install_style_property(widget_class, g_param_spec_boolean("typing-notification-enable",
1653 _("Enable typing notification"),
1654 _("Enable typing notification"),
1655 TRUE, G_PARAM_READABLE));
1657 binding_set = gtk_binding_set_by_class (parent_class);
1658 gtk_binding_entry_add_signal (binding_set, GDK_b, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_BOLD);
1659 gtk_binding_entry_add_signal (binding_set, GDK_i, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_ITALIC);
1660 gtk_binding_entry_add_signal (binding_set, GDK_u, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_UNDERLINE);
1661 gtk_binding_entry_add_signal (binding_set, GDK_plus, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_GROW);
1662 gtk_binding_entry_add_signal (binding_set, GDK_equal, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_GROW);
1663 gtk_binding_entry_add_signal (binding_set, GDK_minus, GDK_CONTROL_MASK, "format_function_toggle", 1, G_TYPE_INT, GTK_IMHTML_SHRINK);
1664 binding_set = gtk_binding_set_by_class(klass);
1665 gtk_binding_entry_add_signal (binding_set, GDK_r, GDK_CONTROL_MASK, "format_function_clear", 0);
1666 gtk_binding_entry_add_signal (binding_set, GDK_KP_Enter, 0, "message_send", 0);
1667 gtk_binding_entry_add_signal (binding_set, GDK_Return, 0, "message_send", 0);
1668 gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK, "undo", 0);
1669 gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "redo", 0);
1670 gtk_binding_entry_add_signal (binding_set, GDK_F14, 0, "undo", 0);
1671 gtk_binding_entry_add_signal(binding_set, GDK_v, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "paste", 1, G_TYPE_STRING, "text");
1674 static void gtk_imhtml_init (GtkIMHtml *imhtml)
1676 imhtml->text_buffer = gtk_text_buffer_new(NULL);
1677 imhtml->undo_manager = gtk_source_undo_manager_new(imhtml->text_buffer);
1678 gtk_text_view_set_buffer(GTK_TEXT_VIEW(imhtml), imhtml->text_buffer);
1679 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(imhtml), GTK_WRAP_WORD_CHAR);
1680 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(imhtml), 2);
1681 gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(imhtml), 3);
1682 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(imhtml), 2);
1683 gtk_text_view_set_right_margin(GTK_TEXT_VIEW(imhtml), 2);
1684 /*gtk_text_view_set_indent(GTK_TEXT_VIEW(imhtml), -15);*/
1685 /*gtk_text_view_set_justification(GTK_TEXT_VIEW(imhtml), GTK_JUSTIFY_FILL);*/
1687 /* These tags will be used often and can be reused--we create them on init and then apply them by name
1688 * other tags (color, size, face, etc.) will have to be created and applied dynamically
1689 * Note that even though we created SUB, SUP, and PRE tags here, we don't really
1690 * apply them anywhere yet. */
1691 gtk_text_buffer_create_tag(imhtml->text_buffer, "BOLD", "weight", PANGO_WEIGHT_BOLD, NULL);
1692 gtk_text_buffer_create_tag(imhtml->text_buffer, "ITALICS", "style", PANGO_STYLE_ITALIC, NULL);
1693 gtk_text_buffer_create_tag(imhtml->text_buffer, "UNDERLINE", "underline", PANGO_UNDERLINE_SINGLE, NULL);
1694 gtk_text_buffer_create_tag(imhtml->text_buffer, "STRIKE", "strikethrough", TRUE, NULL);
1695 gtk_text_buffer_create_tag(imhtml->text_buffer, "SUB", "rise", -5000, NULL);
1696 gtk_text_buffer_create_tag(imhtml->text_buffer, "SUP", "rise", 5000, NULL);
1697 gtk_text_buffer_create_tag(imhtml->text_buffer, "PRE", "family", "Monospace", NULL);
1698 gtk_text_buffer_create_tag(imhtml->text_buffer, "search", "background", "#22ff00", "weight", "bold", NULL);
1699 gtk_text_buffer_create_tag(imhtml->text_buffer, "comment", "weight", PANGO_WEIGHT_NORMAL,
1700 #if FALSE && GTK_CHECK_VERSION(2,10,10)
1701 "invisible", FALSE,
1702 #endif
1703 NULL);
1705 gtk_text_buffer_create_tag(imhtml->text_buffer, "send-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1706 gtk_text_buffer_create_tag(imhtml->text_buffer, "receive-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1707 gtk_text_buffer_create_tag(imhtml->text_buffer, "highlight-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1708 gtk_text_buffer_create_tag(imhtml->text_buffer, "action-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1709 gtk_text_buffer_create_tag(imhtml->text_buffer, "whisper-action-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1710 gtk_text_buffer_create_tag(imhtml->text_buffer, "whisper-name", "weight", PANGO_WEIGHT_BOLD, NULL);
1712 /* When hovering over a link, we show the hand cursor--elsewhere we show the plain ol' pointer cursor */
1713 imhtml->hand_cursor = gdk_cursor_new (GDK_HAND2);
1714 imhtml->arrow_cursor = gdk_cursor_new (GDK_LEFT_PTR);
1715 imhtml->text_cursor = gdk_cursor_new (GDK_XTERM);
1717 imhtml->show_comments = TRUE;
1719 imhtml->smiley_data = g_hash_table_new_full(g_str_hash, g_str_equal,
1720 g_free, (GDestroyNotify)gtk_smiley_tree_destroy);
1721 imhtml->default_smilies = gtk_smiley_tree_new();
1723 g_signal_connect(G_OBJECT(imhtml), "motion-notify-event", G_CALLBACK(gtk_motion_event_notify), NULL);
1724 g_signal_connect(G_OBJECT(imhtml), "leave-notify-event", G_CALLBACK(gtk_leave_event_notify), NULL);
1725 g_signal_connect(G_OBJECT(imhtml), "enter-notify-event", G_CALLBACK(gtk_enter_event_notify), NULL);
1726 g_signal_connect(G_OBJECT(imhtml), "button_press_event", G_CALLBACK(gtk_imhtml_button_press_event), NULL);
1727 g_signal_connect(G_OBJECT(imhtml->text_buffer), "insert-text", G_CALLBACK(preinsert_cb), imhtml);
1728 g_signal_connect(G_OBJECT(imhtml->text_buffer), "delete_range", G_CALLBACK(delete_cb), imhtml);
1729 g_signal_connect_after(G_OBJECT(imhtml->text_buffer), "insert-text", G_CALLBACK(insert_cb), imhtml);
1730 g_signal_connect_after(G_OBJECT(imhtml->text_buffer), "insert-child-anchor", G_CALLBACK(insert_ca_cb), imhtml);
1731 gtk_drag_dest_set(GTK_WIDGET(imhtml), 0,
1732 link_drag_drop_targets, sizeof(link_drag_drop_targets) / sizeof(GtkTargetEntry),
1733 GDK_ACTION_COPY);
1734 g_signal_connect(G_OBJECT(imhtml), "drag_data_received", G_CALLBACK(gtk_imhtml_link_drag_rcv_cb), imhtml);
1735 g_signal_connect(G_OBJECT(imhtml), "drag_drop", G_CALLBACK(gtk_imhtml_link_drop_cb), imhtml);
1737 g_signal_connect(G_OBJECT(imhtml), "copy-clipboard", G_CALLBACK(copy_clipboard_cb), NULL);
1738 g_signal_connect(G_OBJECT(imhtml), "cut-clipboard", G_CALLBACK(cut_clipboard_cb), NULL);
1739 g_signal_connect(G_OBJECT(imhtml), "paste-clipboard", G_CALLBACK(paste_clipboard_cb), NULL);
1740 g_signal_connect_after(G_OBJECT(imhtml), "realize", G_CALLBACK(imhtml_realized_remove_primary), NULL);
1741 g_signal_connect(G_OBJECT(imhtml), "unrealize", G_CALLBACK(imhtml_destroy_add_primary), NULL);
1742 g_signal_connect(G_OBJECT(imhtml), "paste", G_CALLBACK(imhtml_paste_cb), NULL);
1744 #ifndef _WIN32
1745 g_signal_connect_after(G_OBJECT(GTK_IMHTML(imhtml)->text_buffer), "mark-set",
1746 G_CALLBACK(mark_set_so_update_selection_cb), imhtml);
1747 #endif
1749 gtk_widget_add_events(GTK_WIDGET(imhtml),
1750 GDK_LEAVE_NOTIFY_MASK | GDK_ENTER_NOTIFY_MASK);
1752 imhtml->tip = NULL;
1753 imhtml->tip_timer = 0;
1754 imhtml->tip_window = NULL;
1756 imhtml->edit.bold = FALSE;
1757 imhtml->edit.italic = FALSE;
1758 imhtml->edit.underline = FALSE;
1759 imhtml->edit.forecolor = NULL;
1760 imhtml->edit.backcolor = NULL;
1761 imhtml->edit.fontface = NULL;
1762 imhtml->edit.fontsize = 0;
1763 imhtml->edit.link = NULL;
1766 imhtml->scalables = NULL;
1767 imhtml->animations = g_queue_new();
1768 gtk_imhtml_set_editable(imhtml, FALSE);
1769 g_signal_connect(G_OBJECT(imhtml), "populate-popup",
1770 G_CALLBACK(hijack_menu_cb), NULL);
1773 GtkWidget *gtk_imhtml_new(void *a, void *b)
1775 return GTK_WIDGET(g_object_new(gtk_imhtml_get_type(), NULL));
1778 GType gtk_imhtml_get_type()
1780 static GType imhtml_type = 0;
1782 if (!imhtml_type) {
1783 static const GTypeInfo imhtml_info = {
1784 sizeof(GtkIMHtmlClass),
1785 NULL,
1786 NULL,
1787 (GClassInitFunc) gtk_imhtml_class_init,
1788 NULL,
1789 NULL,
1790 sizeof (GtkIMHtml),
1792 (GInstanceInitFunc) gtk_imhtml_init,
1793 NULL
1796 imhtml_type = g_type_register_static(gtk_text_view_get_type(),
1797 "GtkIMHtml", &imhtml_info, 0);
1800 return imhtml_type;
1803 static void gtk_imhtml_link_destroy(GtkIMHtmlLink *link)
1805 if (link->imhtml)
1806 g_object_unref(link->imhtml);
1807 if (link->tag)
1808 g_object_unref(link->tag);
1809 g_free(link->url);
1810 g_free(link);
1813 /* The callback for an event on a link tag. */
1814 static gboolean tag_event(GtkTextTag *tag, GObject *imhtml, GdkEvent *event, GtkTextIter *arg2, gpointer unused)
1816 GdkEventButton *event_button = (GdkEventButton *) event;
1817 if (GTK_IMHTML(imhtml)->editable)
1818 return FALSE;
1819 if (event->type == GDK_BUTTON_RELEASE) {
1820 if ((event_button->button == 1) || (event_button->button == 2)) {
1821 GtkTextIter start, end;
1822 /* we shouldn't open a URL if the user has selected something: */
1823 if (gtk_text_buffer_get_selection_bounds(
1824 gtk_text_iter_get_buffer(arg2), &start, &end))
1825 return FALSE;
1826 gtk_imhtml_activate_tag(GTK_IMHTML(imhtml), tag);
1827 return FALSE;
1828 } else if(event_button->button == 3) {
1829 GList *children;
1830 GtkWidget *menu;
1831 GtkIMHtmlProtocol *proto;
1832 GtkIMHtmlLink *link = g_new(GtkIMHtmlLink, 1);
1833 link->imhtml = g_object_ref(imhtml);
1834 link->url = g_strdup(g_object_get_data(G_OBJECT(tag), "link_url"));
1835 link->tag = g_object_ref(tag);
1837 /* Don't want the tooltip around if user right-clicked on link */
1838 if (GTK_IMHTML(imhtml)->tip_window) {
1839 gtk_widget_destroy(GTK_IMHTML(imhtml)->tip_window);
1840 GTK_IMHTML(imhtml)->tip_window = NULL;
1842 if (GTK_IMHTML(imhtml)->tip_timer) {
1843 g_source_remove(GTK_IMHTML(imhtml)->tip_timer);
1844 GTK_IMHTML(imhtml)->tip_timer = 0;
1846 if (GTK_IMHTML(imhtml)->editable)
1847 gdk_window_set_cursor(event_button->window, GTK_IMHTML(imhtml)->text_cursor);
1848 else
1849 gdk_window_set_cursor(event_button->window, GTK_IMHTML(imhtml)->arrow_cursor);
1850 menu = gtk_menu_new();
1851 g_object_set_data_full(G_OBJECT(menu), "x-imhtml-url-data", link,
1852 (GDestroyNotify)gtk_imhtml_link_destroy);
1854 proto = imhtml_find_protocol(link->url, FALSE);
1856 if (proto && proto->context_menu) {
1857 proto->context_menu(GTK_IMHTML(link->imhtml), link, menu);
1860 children = gtk_container_get_children(GTK_CONTAINER(menu));
1861 if (!children) {
1862 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
1863 gtk_widget_show(item);
1864 gtk_widget_set_sensitive(item, FALSE);
1865 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
1866 } else {
1867 g_list_free(children);
1871 gtk_widget_show_all(menu);
1872 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
1873 event_button->button, event_button->time);
1875 return TRUE;
1878 if(event->type == GDK_BUTTON_PRESS && event_button->button == 3)
1879 return TRUE; /* Clicking the right mouse button on a link shouldn't
1880 be caught by the regular GtkTextView menu */
1881 else
1882 return FALSE; /* Let clicks go through if we didn't catch anything */
1885 static gboolean
1886 gtk_text_view_drag_motion (GtkWidget *widget,
1887 GdkDragContext *context,
1888 gint x,
1889 gint y,
1890 guint time)
1892 GdkDragAction suggested_action = 0;
1894 if (gtk_drag_dest_find_target (widget, context, NULL) == GDK_NONE) {
1895 /* can't accept any of the offered targets */
1896 } else {
1897 GtkWidget *source_widget;
1898 suggested_action = context->suggested_action;
1899 source_widget = gtk_drag_get_source_widget (context);
1900 if (source_widget == widget) {
1901 /* Default to MOVE, unless the user has
1902 * pressed ctrl or alt to affect available actions
1904 if ((context->actions & GDK_ACTION_MOVE) != 0)
1905 suggested_action = GDK_ACTION_MOVE;
1909 gdk_drag_status (context, suggested_action, time);
1911 /* TRUE return means don't propagate the drag motion to parent
1912 * widgets that may also be drop sites.
1914 return TRUE;
1917 static void
1918 gtk_imhtml_link_drop_cb(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data)
1920 GdkAtom target = gtk_drag_dest_find_target (widget, context, NULL);
1922 if (target != GDK_NONE)
1923 gtk_drag_get_data (widget, context, target, time);
1924 else
1925 gtk_drag_finish (context, FALSE, FALSE, time);
1927 return;
1930 static void
1931 gtk_imhtml_link_drag_rcv_cb(GtkWidget *widget, GdkDragContext *dc, guint x, guint y,
1932 GtkSelectionData *sd, guint info, guint t, GtkIMHtml *imhtml)
1934 gchar **links;
1935 gchar *link;
1936 char *text = (char *)sd->data;
1937 GtkTextMark *mark = gtk_text_buffer_get_insert(imhtml->text_buffer);
1938 GtkTextIter iter;
1939 gint i = 0;
1941 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, mark);
1943 if(gtk_imhtml_get_editable(imhtml) && sd->data){
1944 switch (info) {
1945 case GTK_IMHTML_DRAG_URL:
1946 /* TODO: Is it really ok to change sd->data...? */
1947 purple_str_strip_char((char *)sd->data, '\r');
1949 links = g_strsplit((char *)sd->data, "\n", 0);
1950 while((link = links[i]) != NULL){
1951 if (gtk_imhtml_is_protocol(link)) {
1952 gchar *label;
1954 if(links[i + 1])
1955 i++;
1957 label = links[i];
1959 gtk_imhtml_insert_link(imhtml, mark, link, label);
1960 } else if (*link == '\0') {
1961 /* Ignore blank lines */
1962 } else {
1963 /* Special reasons, aka images being put in via other tag, etc. */
1964 /* ... don't pretend we handled it if we didn't */
1965 gtk_drag_finish(dc, FALSE, FALSE, t);
1966 g_strfreev(links);
1967 return;
1970 i++;
1972 g_strfreev(links);
1973 break;
1974 case GTK_IMHTML_DRAG_HTML:
1976 char *utf8 = NULL;
1977 /* Ewww. This is all because mozilla thinks that text/html is 'for internal use only.'
1978 * as explained by this comment in gtkhtml:
1980 * FIXME This hack decides the charset of the selection. It seems that
1981 * mozilla/netscape alway use ucs2 for text/html
1982 * and openoffice.org seems to always use utf8 so we try to validate
1983 * the string as utf8 and if that fails we assume it is ucs2
1985 * See also the comment on text/html here:
1986 * http://mail.gnome.org/archives/gtk-devel-list/2001-September/msg00114.html
1988 if (sd->length >= 2 && !g_utf8_validate(text, sd->length - 1, NULL)) {
1989 utf8 = utf16_to_utf8_with_bom_check(text, sd->length);
1991 if (!utf8) {
1992 purple_debug_warning("gtkimhtml", "g_convert from UTF-16 failed in drag_rcv_cb\n");
1993 return;
1995 } else if (!(*text) || !g_utf8_validate(text, -1, NULL)) {
1996 purple_debug_warning("gtkimhtml", "empty string or invalid UTF-8 in drag_rcv_cb\n");
1997 return;
2000 gtk_imhtml_insert_html_at_iter(imhtml, utf8 ? utf8 : text, 0, &iter);
2001 g_free(utf8);
2002 break;
2004 case GTK_IMHTML_DRAG_TEXT:
2005 if (!(*text) || !g_utf8_validate(text, -1, NULL)) {
2006 purple_debug_warning("gtkimhtml", "empty string or invalid UTF-8 in drag_rcv_cb\n");
2007 return;
2008 } else {
2009 char *tmp = g_markup_escape_text(text, -1);
2010 gtk_imhtml_insert_html_at_iter(imhtml, tmp, 0, &iter);
2011 g_free(tmp);
2013 break;
2014 default:
2015 gtk_drag_finish(dc, FALSE, FALSE, t);
2016 return;
2018 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
2019 } else {
2020 gtk_drag_finish(dc, FALSE, FALSE, t);
2024 static void gtk_smiley_tree_remove (GtkSmileyTree *tree,
2025 GtkIMHtmlSmiley *smiley)
2027 GtkSmileyTree *t = tree;
2028 const gchar *x = smiley->smile;
2029 gint len = 0;
2031 while (*x) {
2032 gchar *pos;
2034 if (!t->values)
2035 return;
2037 pos = strchr (t->values->str, *x);
2038 if (pos)
2039 t = t->children [pos - t->values->str];
2040 else
2041 return;
2043 x++; len++;
2046 if (t->image) {
2047 t->image = NULL;
2051 static gint
2052 gtk_smiley_tree_lookup (GtkSmileyTree *tree,
2053 const gchar *text)
2055 GtkSmileyTree *t = tree;
2056 const gchar *x = text;
2057 gint len = 0;
2058 const gchar *amp;
2059 gint alen;
2061 while (*x) {
2062 gchar *pos;
2064 if (!t->values)
2065 break;
2067 if(*x == '&' && (amp = purple_markup_unescape_entity(x, &alen))) {
2068 gboolean matched = TRUE;
2069 /* Make sure all chars of the unescaped value match */
2070 while (*(amp + 1)) {
2071 pos = strchr (t->values->str, *amp);
2072 if (pos)
2073 t = t->children [GPOINTER_TO_INT(pos) - GPOINTER_TO_INT(t->values->str)];
2074 else {
2075 matched = FALSE;
2076 break;
2078 amp++;
2080 if (!matched)
2081 break;
2083 pos = strchr (t->values->str, *amp);
2085 else if (*x == '<') /* Because we're all WYSIWYG now, a '<'
2086 * char should only appear as the start of a tag. Perhaps a safer (but costlier)
2087 * check would be to call gtk_imhtml_is_tag on it */
2088 break;
2089 else {
2090 alen = 1;
2091 pos = strchr (t->values->str, *x);
2094 if (pos)
2095 t = t->children [GPOINTER_TO_INT(pos) - GPOINTER_TO_INT(t->values->str)];
2096 else
2097 break;
2099 x += alen;
2100 len += alen;
2103 if (t->image)
2104 return len;
2106 return 0;
2109 static void
2110 gtk_imhtml_disassociate_smiley_foreach(gpointer key, gpointer value,
2111 gpointer user_data)
2113 GtkSmileyTree *tree = (GtkSmileyTree *) value;
2114 GtkIMHtmlSmiley *smiley = (GtkIMHtmlSmiley *) user_data;
2115 gtk_smiley_tree_remove(tree, smiley);
2118 static void
2119 gtk_imhtml_disconnect_smiley(GtkIMHtml *imhtml, GtkIMHtmlSmiley *smiley)
2121 smiley->imhtml = NULL;
2122 g_signal_handlers_disconnect_matched(imhtml, G_SIGNAL_MATCH_DATA, 0, 0,
2123 NULL, NULL, smiley);
2126 static void
2127 gtk_imhtml_disassociate_smiley(GtkIMHtmlSmiley *smiley)
2129 if (smiley->imhtml) {
2130 gtk_smiley_tree_remove(smiley->imhtml->default_smilies, smiley);
2131 g_hash_table_foreach(smiley->imhtml->smiley_data,
2132 gtk_imhtml_disassociate_smiley_foreach, smiley);
2133 g_signal_handlers_disconnect_matched(smiley->imhtml, G_SIGNAL_MATCH_DATA,
2134 0, 0, NULL, NULL, smiley);
2135 smiley->imhtml = NULL;
2139 void
2140 gtk_imhtml_associate_smiley (GtkIMHtml *imhtml,
2141 const gchar *sml,
2142 GtkIMHtmlSmiley *smiley)
2144 GtkSmileyTree *tree;
2145 g_return_if_fail (imhtml != NULL);
2146 g_return_if_fail (GTK_IS_IMHTML (imhtml));
2148 if (sml == NULL)
2149 tree = imhtml->default_smilies;
2150 else if (!(tree = g_hash_table_lookup(imhtml->smiley_data, sml))) {
2151 tree = gtk_smiley_tree_new();
2152 g_hash_table_insert(imhtml->smiley_data, g_strdup(sml), tree);
2155 /* need to disconnect old imhtml, if there is one */
2156 if (smiley->imhtml) {
2157 g_signal_handlers_disconnect_matched(smiley->imhtml, G_SIGNAL_MATCH_DATA,
2158 0, 0, NULL, NULL, smiley);
2161 smiley->imhtml = imhtml;
2163 gtk_smiley_tree_insert (tree, smiley);
2165 /* connect destroy signal for the imhtml */
2166 g_signal_connect(imhtml, "destroy", G_CALLBACK(gtk_imhtml_disconnect_smiley),
2167 smiley);
2170 static gboolean
2171 gtk_imhtml_is_smiley (GtkIMHtml *imhtml,
2172 GSList *fonts,
2173 const gchar *text,
2174 gint *len)
2176 GtkSmileyTree *tree;
2177 GtkIMHtmlFontDetail *font;
2178 char *sml = NULL;
2180 if (fonts) {
2181 font = fonts->data;
2182 sml = font->sml;
2185 if (!sml)
2186 sml = imhtml->protocol_name;
2188 if (!sml || !(tree = g_hash_table_lookup(imhtml->smiley_data, sml)))
2189 tree = imhtml->default_smilies;
2191 if (tree == NULL)
2192 return FALSE;
2194 *len = gtk_smiley_tree_lookup (tree, text);
2195 return (*len > 0);
2198 static GtkIMHtmlSmiley *gtk_imhtml_smiley_get_from_tree(GtkSmileyTree *t, const gchar *text)
2200 const gchar *x = text;
2201 gchar *pos;
2203 if (t == NULL)
2204 return NULL;
2206 while (*x) {
2207 if (!t->values)
2208 return NULL;
2210 pos = strchr(t->values->str, *x);
2211 if (!pos)
2212 return NULL;
2214 t = t->children[GPOINTER_TO_INT(pos) - GPOINTER_TO_INT(t->values->str)];
2215 x++;
2218 return t->image;
2221 GtkIMHtmlSmiley *
2222 gtk_imhtml_smiley_get(GtkIMHtml *imhtml, const gchar *sml, const gchar *text)
2224 GtkIMHtmlSmiley *ret;
2226 /* Look for custom smileys first */
2227 if (sml != NULL) {
2228 ret = gtk_imhtml_smiley_get_from_tree(g_hash_table_lookup(imhtml->smiley_data, sml), text);
2229 if (ret != NULL)
2230 return ret;
2233 /* Fall back to check for default smileys */
2234 return gtk_imhtml_smiley_get_from_tree(imhtml->default_smilies, text);
2237 static GdkPixbufAnimation *
2238 gtk_smiley_get_image(GtkIMHtmlSmiley *smiley)
2240 if (!smiley->icon) {
2241 if (smiley->file) {
2242 smiley->icon = gdk_pixbuf_animation_new_from_file(smiley->file, NULL);
2243 } else if (smiley->loader) {
2244 smiley->icon = gdk_pixbuf_loader_get_animation(smiley->loader);
2245 if (smiley->icon)
2246 g_object_ref(G_OBJECT(smiley->icon));
2250 return smiley->icon;
2253 #define VALID_TAG(x) do { \
2254 if (!g_ascii_strncasecmp (string, x ">", strlen (x ">"))) { \
2255 if (tag) *tag = g_strndup (string, strlen (x)); \
2256 if (len) *len = strlen (x) + 1; \
2257 return TRUE; \
2259 if (type) (*type)++; \
2260 } while (0)
2262 #define VALID_OPT_TAG(x) do { \
2263 if (!g_ascii_strncasecmp (string, x " ", strlen (x " "))) { \
2264 const gchar *c = string + strlen (x " "); \
2265 gchar e = '"'; \
2266 gboolean quote = FALSE; \
2267 while (*c) { \
2268 if (*c == '"' || *c == '\'') { \
2269 if (quote && (*c == e)) \
2270 quote = !quote; \
2271 else if (!quote) { \
2272 quote = !quote; \
2273 e = *c; \
2275 } else if (!quote && (*c == '>')) \
2276 break; \
2277 c++; \
2279 if (*c) { \
2280 if (tag) *tag = g_strndup (string, c - string); \
2281 if (len) *len = c - string + 1; \
2282 return TRUE; \
2285 if (type) (*type)++; \
2286 } while (0)
2289 static gboolean
2290 gtk_imhtml_is_tag (const gchar *string,
2291 gchar **tag,
2292 gint *len,
2293 gint *type)
2295 char *close;
2296 if (type)
2297 *type = 1;
2299 if (!(close = strchr (string, '>')))
2300 return FALSE;
2302 VALID_TAG ("B");
2303 VALID_TAG ("BOLD");
2304 VALID_TAG ("/B");
2305 VALID_TAG ("/BOLD");
2306 VALID_TAG ("I");
2307 VALID_TAG ("ITALIC");
2308 VALID_TAG ("/I");
2309 VALID_TAG ("/ITALIC");
2310 VALID_TAG ("U");
2311 VALID_TAG ("UNDERLINE");
2312 VALID_TAG ("/U");
2313 VALID_TAG ("/UNDERLINE");
2314 VALID_TAG ("S");
2315 VALID_TAG ("STRIKE");
2316 VALID_TAG ("/S");
2317 VALID_TAG ("/STRIKE");
2318 VALID_TAG ("SUB");
2319 VALID_TAG ("/SUB");
2320 VALID_TAG ("SUP");
2321 VALID_TAG ("/SUP");
2322 VALID_TAG ("PRE");
2323 VALID_TAG ("/PRE");
2324 VALID_TAG ("TITLE");
2325 VALID_TAG ("/TITLE");
2326 VALID_TAG ("BR");
2327 VALID_TAG ("HR");
2328 VALID_TAG ("/FONT");
2329 VALID_TAG ("/A");
2330 VALID_TAG ("P");
2331 VALID_TAG ("/P");
2332 VALID_TAG ("H3");
2333 VALID_TAG ("/H3");
2334 VALID_TAG ("HTML");
2335 VALID_TAG ("/HTML");
2336 VALID_TAG ("BODY");
2337 VALID_TAG ("/BODY");
2338 VALID_TAG ("FONT");
2339 VALID_TAG ("HEAD");
2340 VALID_TAG ("/HEAD");
2341 VALID_TAG ("BINARY");
2342 VALID_TAG ("/BINARY");
2344 VALID_OPT_TAG ("HR");
2345 VALID_OPT_TAG ("FONT");
2346 VALID_OPT_TAG ("BODY");
2347 VALID_OPT_TAG ("A");
2348 VALID_OPT_TAG ("IMG");
2349 VALID_OPT_TAG ("P");
2350 VALID_OPT_TAG ("H3");
2351 VALID_OPT_TAG ("HTML");
2353 VALID_TAG ("CITE");
2354 VALID_TAG ("/CITE");
2355 VALID_TAG ("EM");
2356 VALID_TAG ("/EM");
2357 VALID_TAG ("STRONG");
2358 VALID_TAG ("/STRONG");
2360 VALID_OPT_TAG ("SPAN");
2361 VALID_TAG ("/SPAN");
2362 VALID_TAG ("BR/"); /* hack until gtkimhtml handles things better */
2363 VALID_TAG ("IMG");
2364 VALID_TAG("SPAN");
2365 VALID_OPT_TAG("BR");
2367 if (!g_ascii_strncasecmp(string, "!--", strlen ("!--"))) {
2368 gchar *e = strstr (string + strlen("!--"), "-->");
2369 if (e) {
2370 if (len) {
2371 *len = e - string + strlen ("-->");
2372 if (tag)
2373 *tag = g_strndup (string + strlen ("!--"), *len - strlen ("!---->"));
2375 return TRUE;
2379 if (type)
2380 *type = -1;
2381 if (len)
2382 *len = close - string + 1;
2383 if (tag)
2384 *tag = g_strndup(string, close - string);
2385 return TRUE;
2388 static gchar*
2389 gtk_imhtml_get_html_opt (gchar *tag,
2390 const gchar *opt)
2392 gchar *t = tag;
2393 gchar *e, *a;
2394 gchar *val;
2395 gint len;
2396 const gchar *c;
2397 GString *ret;
2399 while (g_ascii_strncasecmp (t, opt, strlen (opt))) {
2400 gboolean quote = FALSE;
2401 if (*t == '\0') break;
2402 while (*t && !((*t == ' ') && !quote)) {
2403 if (*t == '\"')
2404 quote = ! quote;
2405 t++;
2407 while (*t && (*t == ' ')) t++;
2410 if (!g_ascii_strncasecmp (t, opt, strlen (opt))) {
2411 t += strlen (opt);
2412 } else {
2413 return NULL;
2416 if ((*t == '\"') || (*t == '\'')) {
2417 e = a = ++t;
2418 while (*e && (*e != *(t - 1))) e++;
2419 if (*e == '\0') {
2420 return NULL;
2421 } else
2422 val = g_strndup(a, e - a);
2423 } else {
2424 e = a = t;
2425 while (*e && !isspace ((gint) *e)) e++;
2426 val = g_strndup(a, e - a);
2429 ret = g_string_new("");
2430 e = val;
2431 while(*e) {
2432 if((c = purple_markup_unescape_entity(e, &len))) {
2433 ret = g_string_append(ret, c);
2434 e += len;
2435 } else {
2436 gunichar uni = g_utf8_get_char(e);
2437 ret = g_string_append_unichar(ret, uni);
2438 e = g_utf8_next_char(e);
2442 g_free(val);
2444 return g_string_free(ret, FALSE);
2447 /* returns if the beginning of the text is a protocol. If it is the protocol, returns the length so
2448 the caller knows how long the protocol string is. */
2449 static int gtk_imhtml_is_protocol(const char *text)
2451 GtkIMHtmlProtocol *proto = imhtml_find_protocol(text, FALSE);
2452 return proto ? proto->length : 0;
2455 static gboolean smooth_scroll_cb(gpointer data);
2458 <KingAnt> marv: The two IM image functions in oscar are purple_odc_send_im and purple_odc_incoming
2461 [19:58] <Robot101> marv: images go into the imgstore, a refcounted... well.. hash. :)
2462 [19:59] <KingAnt> marv: I think the image tag used by the core is something like <img id="#"/>
2463 [19:59] Ro0tSiEgE robert42 RobFlynn Robot101 ross22 roz
2464 [20:00] <KingAnt> marv: Where the ID is the what is returned when you add the image to the imgstore using purple_imgstore_add
2465 [20:00] <marv> Robot101: so how does the image get passed to serv_got_im() and serv_send_im()? just as the <img id="#" and then the prpl looks it up from the store?
2466 [20:00] <KingAnt> marv: Right
2467 [20:00] <marv> alright
2469 Here's my plan with IMImages. make gtk_imhtml_[append|insert]_text_with_images instead just
2470 gtkimhtml_[append|insert]_text (hrm maybe it should be called html instead of text), add a
2471 function for purple to register for look up images, i.e. gtk_imhtml_set_get_img_fnc, so that
2472 images can be looked up like that, instead of passing a GSList of them.
2475 void gtk_imhtml_append_text_with_images (GtkIMHtml *imhtml,
2476 const gchar *text,
2477 GtkIMHtmlOptions options,
2478 GSList *unused)
2480 GtkTextIter iter, ins, sel;
2481 int ins_offset = 0, sel_offset = 0;
2482 gboolean fixins = FALSE, fixsel = FALSE;
2484 g_return_if_fail (imhtml != NULL);
2485 g_return_if_fail (GTK_IS_IMHTML (imhtml));
2486 g_return_if_fail (text != NULL);
2489 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &iter);
2490 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &ins, gtk_text_buffer_get_insert(imhtml->text_buffer));
2491 if (gtk_text_iter_equal(&iter, &ins) && gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, NULL, NULL)) {
2492 fixins = TRUE;
2493 ins_offset = gtk_text_iter_get_offset(&ins);
2496 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &sel, gtk_text_buffer_get_selection_bound(imhtml->text_buffer));
2497 if (gtk_text_iter_equal(&iter, &sel) && gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, NULL, NULL)) {
2498 fixsel = TRUE;
2499 sel_offset = gtk_text_iter_get_offset(&sel);
2502 if (!(options & GTK_IMHTML_NO_SCROLL)) {
2503 GdkRectangle rect;
2504 int y, height;
2506 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(imhtml), &rect);
2507 gtk_text_view_get_line_yrange(GTK_TEXT_VIEW(imhtml), &iter, &y, &height);
2509 if (((y + height) - (rect.y + rect.height)) > height &&
2510 gtk_text_buffer_get_char_count(imhtml->text_buffer)) {
2511 /* If we are in the middle of smooth-scrolling, then take a scroll step.
2512 * If we are not in the middle of smooth-scrolling, that means we were
2513 * not looking at the end of the buffer before the new text was added,
2514 * so do not scroll. */
2515 if (imhtml->scroll_time)
2516 smooth_scroll_cb(imhtml);
2517 else
2518 options |= GTK_IMHTML_NO_SCROLL;
2522 gtk_imhtml_insert_html_at_iter(imhtml, text, options, &iter);
2524 if (fixins) {
2525 gtk_text_buffer_get_iter_at_offset(imhtml->text_buffer, &ins, ins_offset);
2526 gtk_text_buffer_move_mark(imhtml->text_buffer, gtk_text_buffer_get_insert(imhtml->text_buffer), &ins);
2529 if (fixsel) {
2530 gtk_text_buffer_get_iter_at_offset(imhtml->text_buffer, &sel, sel_offset);
2531 gtk_text_buffer_move_mark(imhtml->text_buffer, gtk_text_buffer_get_selection_bound(imhtml->text_buffer), &sel);
2534 if (!(options & GTK_IMHTML_NO_SCROLL)) {
2535 gtk_imhtml_scroll_to_end(imhtml, (options & GTK_IMHTML_USE_SMOOTHSCROLLING));
2539 #define MAX_SCROLL_TIME 0.4 /* seconds */
2540 #define SCROLL_DELAY 33 /* milliseconds */
2543 * Smoothly scroll a GtkIMHtml.
2545 * @return TRUE if the window needs to be scrolled further, FALSE if we're at the bottom.
2547 static gboolean smooth_scroll_cb(gpointer data)
2549 GtkIMHtml *imhtml = data;
2550 GtkAdjustment *adj = GTK_TEXT_VIEW(imhtml)->vadjustment;
2551 gdouble max_val = adj->upper - adj->page_size;
2552 gdouble scroll_val = gtk_adjustment_get_value(adj) + ((max_val - gtk_adjustment_get_value(adj)) / 3);
2554 g_return_val_if_fail(imhtml->scroll_time != NULL, FALSE);
2556 if (g_timer_elapsed(imhtml->scroll_time, NULL) > MAX_SCROLL_TIME || scroll_val >= max_val) {
2557 /* time's up. jump to the end and kill the timer */
2558 gtk_adjustment_set_value(adj, max_val);
2559 g_timer_destroy(imhtml->scroll_time);
2560 imhtml->scroll_time = NULL;
2561 g_source_remove(imhtml->scroll_src);
2562 imhtml->scroll_src = 0;
2563 return FALSE;
2566 /* scroll by 1/3rd the remaining distance */
2567 gtk_adjustment_set_value(adj, scroll_val);
2568 return TRUE;
2571 static gboolean scroll_idle_cb(gpointer data)
2573 GtkIMHtml *imhtml = data;
2574 GtkAdjustment *adj = GTK_TEXT_VIEW(imhtml)->vadjustment;
2575 if(adj) {
2576 gtk_adjustment_set_value(adj, adj->upper - adj->page_size);
2578 imhtml->scroll_src = 0;
2579 return FALSE;
2582 void gtk_imhtml_scroll_to_end(GtkIMHtml *imhtml, gboolean smooth)
2584 if (imhtml->scroll_time)
2585 g_timer_destroy(imhtml->scroll_time);
2586 if (imhtml->scroll_src)
2587 g_source_remove(imhtml->scroll_src);
2588 if(smooth) {
2589 imhtml->scroll_time = g_timer_new();
2590 imhtml->scroll_src = g_timeout_add_full(G_PRIORITY_LOW, SCROLL_DELAY, smooth_scroll_cb, imhtml, NULL);
2591 } else {
2592 imhtml->scroll_time = NULL;
2593 imhtml->scroll_src = g_idle_add_full(G_PRIORITY_LOW, scroll_idle_cb, imhtml, NULL);
2597 /* CSS colors are either rgb (x,y,z) or #hex
2598 * we need to convert to hex if it is RGB */
2599 static gchar*
2600 parse_css_color(gchar *in_color)
2602 char *tmp = in_color;
2604 if (*tmp == 'r' && *(++tmp) == 'g' && *(++tmp) == 'b' && *(++tmp)) {
2605 int rgbval[] = {0, 0, 0};
2606 int count = 0;
2607 const char *v_start;
2609 while (*tmp && g_ascii_isspace(*tmp))
2610 tmp++;
2611 if (*tmp != '(') {
2612 /* We don't support rgba() */
2613 purple_debug_warning("gtkimhtml", "Invalid rgb CSS color in '%s'!\n", in_color);
2614 return in_color;
2616 tmp++;
2618 while (count < 3) {
2619 /* Skip any leading spaces */
2620 while (*tmp && g_ascii_isspace(*tmp))
2621 tmp++;
2623 /* Find the subsequent contiguous digits */
2624 v_start = tmp;
2625 if (*v_start == '-')
2626 tmp++;
2627 while (*tmp && g_ascii_isdigit(*tmp))
2628 tmp++;
2630 if (tmp != v_start) {
2631 char prev = *tmp;
2632 *tmp = '\0';
2633 rgbval[count] = atoi(v_start);
2634 *tmp = prev;
2636 /* deal with % */
2637 while (*tmp && g_ascii_isspace(*tmp))
2638 tmp++;
2639 if (*tmp == '%') {
2640 rgbval[count] = (rgbval[count] / 100.0) * 255;
2641 tmp++;
2643 } else {
2644 purple_debug_warning("gtkimhtml", "Invalid rgb CSS color in '%s'!\n", in_color);
2645 return in_color;
2648 if (rgbval[count] > 255) {
2649 rgbval[count] = 255;
2650 } else if (rgbval[count] < 0) {
2651 rgbval[count] = 0;
2654 while (*tmp && g_ascii_isspace(*tmp))
2655 tmp++;
2656 if (*tmp == ',')
2657 tmp++;
2659 count++;
2662 g_free(in_color);
2663 return g_strdup_printf("#%02X%02X%02X", rgbval[0], rgbval[1], rgbval[2]);
2666 return in_color;
2669 void gtk_imhtml_insert_html_at_iter(GtkIMHtml *imhtml,
2670 const gchar *text,
2671 GtkIMHtmlOptions options,
2672 GtkTextIter *iter)
2674 GdkRectangle rect;
2675 gint pos = 0;
2676 gchar *ws;
2677 gchar *tag;
2678 gchar *bg = NULL;
2679 gint len;
2680 gint tlen, smilelen, wpos=0;
2681 gint type;
2682 const gchar *c;
2683 const gchar *amp;
2684 gint len_protocol;
2686 guint bold = 0,
2687 italics = 0,
2688 underline = 0,
2689 strike = 0,
2690 sub = 0,
2691 sup = 0,
2692 title = 0,
2693 pre = 0;
2695 gboolean br = FALSE;
2696 gboolean align_right = FALSE;
2697 gboolean rtl_direction = FALSE;
2698 gint align_line = 0;
2700 GSList *fonts = NULL;
2701 GObject *object;
2702 GtkIMHtmlScalable *scalable = NULL;
2704 g_return_if_fail (imhtml != NULL);
2705 g_return_if_fail (GTK_IS_IMHTML (imhtml));
2706 g_return_if_fail (text != NULL);
2707 c = text;
2708 len = strlen(text);
2709 ws = g_malloc(len + 1);
2710 ws[0] = '\0';
2712 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_thismsg", GINT_TO_POINTER(0));
2714 gtk_text_buffer_begin_user_action(imhtml->text_buffer);
2715 while (pos < len) {
2716 if (*c == '<' && gtk_imhtml_is_tag (c + 1, &tag, &tlen, &type)) {
2717 c++;
2718 pos++;
2719 ws[wpos] = '\0';
2720 br = FALSE;
2721 switch (type)
2723 case 1: /* B */
2724 case 2: /* BOLD */
2725 case 54: /* STRONG */
2726 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2727 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2729 if ((bold == 0) && (imhtml->format_functions & GTK_IMHTML_BOLD))
2730 gtk_imhtml_toggle_bold(imhtml);
2731 bold++;
2732 ws[0] = '\0'; wpos = 0;
2734 break;
2735 case 3: /* /B */
2736 case 4: /* /BOLD */
2737 case 55: /* /STRONG */
2738 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2739 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2740 ws[0] = '\0'; wpos = 0;
2742 if (bold) {
2743 bold--;
2744 if ((bold == 0) && (imhtml->format_functions & GTK_IMHTML_BOLD) && !imhtml->wbfo)
2745 gtk_imhtml_toggle_bold(imhtml);
2748 break;
2749 case 5: /* I */
2750 case 6: /* ITALIC */
2751 case 52: /* EM */
2752 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2753 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2754 ws[0] = '\0'; wpos = 0;
2755 if ((italics == 0) && (imhtml->format_functions & GTK_IMHTML_ITALIC))
2756 gtk_imhtml_toggle_italic(imhtml);
2757 italics++;
2759 break;
2760 case 7: /* /I */
2761 case 8: /* /ITALIC */
2762 case 53: /* /EM */
2763 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2764 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2765 ws[0] = '\0'; wpos = 0;
2766 if (italics) {
2767 italics--;
2768 if ((italics == 0) && (imhtml->format_functions & GTK_IMHTML_ITALIC) && !imhtml->wbfo)
2769 gtk_imhtml_toggle_italic(imhtml);
2772 break;
2773 case 9: /* U */
2774 case 10: /* UNDERLINE */
2775 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2776 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2777 ws[0] = '\0'; wpos = 0;
2778 if ((underline == 0) && (imhtml->format_functions & GTK_IMHTML_UNDERLINE))
2779 gtk_imhtml_toggle_underline(imhtml);
2780 underline++;
2782 break;
2783 case 11: /* /U */
2784 case 12: /* /UNDERLINE */
2785 if (!(options & GTK_IMHTML_NO_FORMATTING)) {
2786 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2787 ws[0] = '\0'; wpos = 0;
2788 if (underline) {
2789 underline--;
2790 if ((underline == 0) && (imhtml->format_functions & GTK_IMHTML_UNDERLINE) && !imhtml->wbfo)
2791 gtk_imhtml_toggle_underline(imhtml);
2794 break;
2795 case 13: /* S */
2796 case 14: /* STRIKE */
2797 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2798 ws[0] = '\0'; wpos = 0;
2799 if ((strike == 0) && (imhtml->format_functions & GTK_IMHTML_STRIKE))
2800 gtk_imhtml_toggle_strike(imhtml);
2801 strike++;
2802 break;
2803 case 15: /* /S */
2804 case 16: /* /STRIKE */
2805 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2806 ws[0] = '\0'; wpos = 0;
2807 if (strike)
2808 strike--;
2809 if ((strike == 0) && (imhtml->format_functions & GTK_IMHTML_STRIKE) && !imhtml->wbfo)
2810 gtk_imhtml_toggle_strike(imhtml);
2811 break;
2812 case 17: /* SUB */
2813 /* FIXME: reimpliment this */
2814 sub++;
2815 break;
2816 case 18: /* /SUB */
2817 /* FIXME: reimpliment this */
2818 if (sub)
2819 sub--;
2820 break;
2821 case 19: /* SUP */
2822 /* FIXME: reimplement this */
2823 sup++;
2824 break;
2825 case 20: /* /SUP */
2826 /* FIXME: reimplement this */
2827 if (sup)
2828 sup--;
2829 break;
2830 case 21: /* PRE */
2831 /* FIXME: reimplement this */
2832 pre++;
2833 break;
2834 case 22: /* /PRE */
2835 /* FIXME: reimplement this */
2836 if (pre)
2837 pre--;
2838 break;
2839 case 23: /* TITLE */
2840 /* FIXME: what was this supposed to do anyway? */
2841 title++;
2842 break;
2843 case 24: /* /TITLE */
2844 /* FIXME: make this undo whatever 23 was supposed to do */
2845 if (title) {
2846 if (options & GTK_IMHTML_NO_TITLE) {
2847 wpos = 0;
2848 ws [wpos] = '\0';
2850 title--;
2852 break;
2853 case 25: /* BR */
2854 case 58: /* BR/ */
2855 case 61: /* BR (opt) */
2856 ws[wpos] = '\n';
2857 wpos++;
2858 br = TRUE;
2859 break;
2860 case 26: /* HR */
2861 case 42: /* HR (opt) */
2863 int minus;
2864 struct scalable_data *sd = g_new(struct scalable_data, 1);
2866 ws[wpos++] = '\n';
2867 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2869 sd->scalable = scalable = gtk_imhtml_hr_new();
2870 sd->mark = gtk_text_buffer_create_mark(imhtml->text_buffer, NULL, iter, TRUE);
2871 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(imhtml), &rect);
2872 scalable->add_to(scalable, imhtml, iter);
2873 minus = gtk_text_view_get_left_margin(GTK_TEXT_VIEW(imhtml)) +
2874 gtk_text_view_get_right_margin(GTK_TEXT_VIEW(imhtml));
2875 scalable->scale(scalable, rect.width - minus, rect.height);
2876 imhtml->scalables = g_list_append(imhtml->scalables, sd);
2877 ws[0] = '\0'; wpos = 0;
2878 ws[wpos++] = '\n';
2880 break;
2882 case 27: /* /FONT */
2883 if (fonts && !imhtml->wbfo) {
2884 GtkIMHtmlFontDetail *font = fonts->data;
2885 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2886 ws[0] = '\0'; wpos = 0;
2887 /* NEW_BIT (NEW_TEXT_BIT); */
2889 if (font->face && (imhtml->format_functions & GTK_IMHTML_FACE)) {
2890 gtk_imhtml_toggle_fontface(imhtml, NULL);
2892 g_free (font->face);
2893 if (font->fore && (imhtml->format_functions & GTK_IMHTML_FORECOLOR)) {
2894 gtk_imhtml_toggle_forecolor(imhtml, NULL);
2896 g_free (font->fore);
2897 if (font->back && (imhtml->format_functions & GTK_IMHTML_BACKCOLOR)) {
2898 gtk_imhtml_toggle_backcolor(imhtml, NULL);
2900 g_free (font->back);
2901 g_free (font->sml);
2903 if ((font->size != 3) && (imhtml->format_functions & (GTK_IMHTML_GROW|GTK_IMHTML_SHRINK)))
2904 gtk_imhtml_font_set_size(imhtml, 3);
2906 fonts = g_slist_remove (fonts, font);
2907 g_free(font);
2909 if (fonts) {
2910 GtkIMHtmlFontDetail *font = fonts->data;
2912 if (font->face && (imhtml->format_functions & GTK_IMHTML_FACE))
2913 gtk_imhtml_toggle_fontface(imhtml, font->face);
2914 if (font->fore && (imhtml->format_functions & GTK_IMHTML_FORECOLOR))
2915 gtk_imhtml_toggle_forecolor(imhtml, font->fore);
2916 if (font->back && (imhtml->format_functions & GTK_IMHTML_BACKCOLOR))
2917 gtk_imhtml_toggle_backcolor(imhtml, font->back);
2918 if ((font->size != 3) && (imhtml->format_functions & (GTK_IMHTML_GROW|GTK_IMHTML_SHRINK)))
2919 gtk_imhtml_font_set_size(imhtml, font->size);
2922 break;
2923 case 28: /* /A */
2924 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2925 gtk_imhtml_toggle_link(imhtml, NULL);
2926 ws[0] = '\0'; wpos = 0;
2927 break;
2929 case 29: /* P */
2930 case 30: /* /P */
2931 case 31: /* H3 */
2932 case 32: /* /H3 */
2933 case 33: /* HTML */
2934 case 34: /* /HTML */
2935 case 35: /* BODY */
2936 break;
2937 case 36: /* /BODY */
2938 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2939 ws[0] = '\0'; wpos = 0;
2940 gtk_imhtml_toggle_background(imhtml, NULL);
2941 break;
2942 case 37: /* FONT */
2943 case 38: /* HEAD */
2944 case 39: /* /HEAD */
2945 case 40: /* BINARY */
2946 case 41: /* /BINARY */
2947 break;
2948 case 43: /* FONT (opt) */
2950 gchar *color, *back, *face, *size, *sml;
2951 GtkIMHtmlFontDetail *font, *oldfont = NULL;
2952 color = gtk_imhtml_get_html_opt (tag, "COLOR=");
2953 back = gtk_imhtml_get_html_opt (tag, "BACK=");
2954 face = gtk_imhtml_get_html_opt (tag, "FACE=");
2955 size = gtk_imhtml_get_html_opt (tag, "SIZE=");
2956 sml = gtk_imhtml_get_html_opt (tag, "SML=");
2957 if (!(color || back || face || size || sml))
2958 break;
2960 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
2961 ws[0] = '\0'; wpos = 0;
2963 font = g_new0 (GtkIMHtmlFontDetail, 1);
2964 if (fonts)
2965 oldfont = fonts->data;
2967 if (color && !(options & GTK_IMHTML_NO_COLOURS) && (imhtml->format_functions & GTK_IMHTML_FORECOLOR)) {
2968 font->fore = color;
2969 gtk_imhtml_toggle_forecolor(imhtml, font->fore);
2970 } else
2971 g_free(color);
2973 if (back && !(options & GTK_IMHTML_NO_COLOURS) && (imhtml->format_functions & GTK_IMHTML_BACKCOLOR)) {
2974 font->back = back;
2975 gtk_imhtml_toggle_backcolor(imhtml, font->back);
2976 } else
2977 g_free(back);
2979 if (face && !(options & GTK_IMHTML_NO_FONTS) && (imhtml->format_functions & GTK_IMHTML_FACE)) {
2980 font->face = face;
2981 gtk_imhtml_toggle_fontface(imhtml, font->face);
2982 } else
2983 g_free(face);
2985 if (sml)
2986 font->sml = sml;
2987 else {
2988 g_free(sml);
2989 if (oldfont && oldfont->sml)
2990 font->sml = g_strdup(oldfont->sml);
2993 if (size && !(options & GTK_IMHTML_NO_SIZES) && (imhtml->format_functions & (GTK_IMHTML_GROW|GTK_IMHTML_SHRINK))) {
2994 if (*size == '+') {
2995 sscanf (size + 1, "%hd", &font->size);
2996 font->size += 3;
2997 } else if (*size == '-') {
2998 sscanf (size + 1, "%hd", &font->size);
2999 font->size = MAX (0, 3 - font->size);
3000 } else if (isdigit (*size)) {
3001 sscanf (size, "%hd", &font->size);
3003 if (font->size > 100)
3004 font->size = 100;
3005 } else if (oldfont)
3006 font->size = oldfont->size;
3007 else
3008 font->size = 3;
3009 if ((imhtml->format_functions & (GTK_IMHTML_GROW|GTK_IMHTML_SHRINK)) && (font->size != 3 || (oldfont && oldfont->size == 3)))
3010 gtk_imhtml_font_set_size(imhtml, font->size);
3011 g_free(size);
3012 fonts = g_slist_prepend (fonts, font);
3014 break;
3015 case 44: /* BODY (opt) */
3016 if (!(options & GTK_IMHTML_NO_COLOURS)) {
3017 char *bgcolor = gtk_imhtml_get_html_opt (tag, "BGCOLOR=");
3018 if (bgcolor && (imhtml->format_functions & GTK_IMHTML_BACKCOLOR)) {
3019 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3020 ws[0] = '\0'; wpos = 0;
3021 /* NEW_BIT(NEW_TEXT_BIT); */
3022 g_free(bg);
3023 bg = bgcolor;
3024 gtk_imhtml_toggle_background(imhtml, bg);
3025 } else
3026 g_free(bgcolor);
3028 break;
3029 case 45: /* A (opt) */
3031 gchar *href = gtk_imhtml_get_html_opt (tag, "HREF=");
3032 if (href && (imhtml->format_functions & GTK_IMHTML_LINK)) {
3033 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3034 ws[0] = '\0'; wpos = 0;
3035 gtk_imhtml_toggle_link(imhtml, href);
3037 g_free(href);
3039 break;
3040 case 46: /* IMG (opt) */
3041 case 59: /* IMG */
3043 char *id;
3045 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3046 ws[0] = '\0'; wpos = 0;
3048 if (!(imhtml->format_functions & GTK_IMHTML_IMAGE))
3049 break;
3051 id = gtk_imhtml_get_html_opt(tag, "ID=");
3052 if (id) {
3053 gtk_imhtml_insert_image_at_iter(imhtml, atoi(id), iter);
3054 g_free(id);
3055 } else {
3056 char *src, *alt;
3057 src = gtk_imhtml_get_html_opt(tag, "SRC=");
3058 alt = gtk_imhtml_get_html_opt(tag, "ALT=");
3059 if (src) {
3060 gtk_imhtml_toggle_link(imhtml, src);
3061 gtk_text_buffer_insert(imhtml->text_buffer, iter, alt ? alt : src, -1);
3062 gtk_imhtml_toggle_link(imhtml, NULL);
3064 g_free (src);
3065 g_free (alt);
3067 break;
3069 case 47: /* P (opt) */
3070 case 48: /* H3 (opt) */
3071 case 49: /* HTML (opt) */
3072 case 50: /* CITE */
3073 case 51: /* /CITE */
3074 case 56: /* SPAN (opt) */
3075 /* Inline CSS Support - Douglas Thrift
3077 * color
3078 * background
3079 * font-family
3080 * font-size
3081 * text-decoration: underline
3082 * font-weight: bold
3083 * direction: rtl
3084 * text-align: right
3086 * TODO:
3087 * background-color
3088 * font-style
3091 gchar *style, *color, *background, *family, *size, *direction, *alignment;
3092 gchar *textdec, *weight;
3093 GtkIMHtmlFontDetail *font, *oldfont = NULL;
3094 style = gtk_imhtml_get_html_opt (tag, "style=");
3096 if (!style) break;
3098 color = purple_markup_get_css_property (style, "color");
3099 background = purple_markup_get_css_property (style, "background");
3100 family = purple_markup_get_css_property (style, "font-family");
3101 size = purple_markup_get_css_property (style, "font-size");
3102 textdec = purple_markup_get_css_property (style, "text-decoration");
3103 weight = purple_markup_get_css_property (style, "font-weight");
3104 direction = purple_markup_get_css_property (style, "direction");
3105 alignment = purple_markup_get_css_property (style, "text-align");
3108 if (!(color || family || size || background || textdec || weight || direction || alignment)) {
3109 g_free(style);
3110 break;
3114 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3115 ws[0] = '\0'; wpos = 0;
3116 /* NEW_BIT (NEW_TEXT_BIT); */
3118 /* Bi-Directional text support */
3119 if (direction && (!g_ascii_strncasecmp(direction, "RTL", 3))) {
3120 rtl_direction = TRUE;
3121 /* insert RLE character to set direction */
3122 ws[wpos++] = 0xE2;
3123 ws[wpos++] = 0x80;
3124 ws[wpos++] = 0xAB;
3125 ws[wpos] = '\0';
3126 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3127 ws[0] = '\0'; wpos = 0;
3129 g_free(direction);
3131 if (alignment && (!g_ascii_strncasecmp(alignment, "RIGHT", 5))) {
3132 align_right = TRUE;
3133 align_line = gtk_text_iter_get_line(iter);
3135 g_free(alignment);
3137 font = g_new0 (GtkIMHtmlFontDetail, 1);
3138 if (fonts)
3139 oldfont = fonts->data;
3141 if (color && !(options & GTK_IMHTML_NO_COLOURS) && (imhtml->format_functions & GTK_IMHTML_FORECOLOR)) {
3142 font->fore = parse_css_color(color);
3143 gtk_imhtml_toggle_forecolor(imhtml, font->fore);
3144 } else {
3145 if (oldfont && oldfont->fore)
3146 font->fore = g_strdup(oldfont->fore);
3147 g_free(color);
3150 if (background && !(options & GTK_IMHTML_NO_COLOURS) && (imhtml->format_functions & GTK_IMHTML_BACKCOLOR)) {
3151 font->back = parse_css_color(background);
3152 gtk_imhtml_toggle_backcolor(imhtml, font->back);
3153 } else {
3154 if (oldfont && oldfont->back)
3155 font->back = g_strdup(oldfont->back);
3156 g_free(background);
3159 if (family && !(options & GTK_IMHTML_NO_FONTS) && (imhtml->format_functions & GTK_IMHTML_FACE)) {
3160 font->face = family;
3161 gtk_imhtml_toggle_fontface(imhtml, font->face);
3162 } else {
3163 if (oldfont && oldfont->face)
3164 font->face = g_strdup(oldfont->face);
3165 g_free(family);
3167 if (font->face && (atoi(font->face) > 100)) {
3168 /* WTF is this? */
3169 /* Maybe it sets a max size on the font face? I seem to
3170 * remember bad things happening if the font size was
3171 * 2 billion */
3172 g_free(font->face);
3173 font->face = g_strdup("100");
3176 if (oldfont && oldfont->sml)
3177 font->sml = g_strdup(oldfont->sml);
3179 if (size && !(options & GTK_IMHTML_NO_SIZES) && (imhtml->format_functions & (GTK_IMHTML_SHRINK|GTK_IMHTML_GROW))) {
3180 if (g_ascii_strcasecmp(size, "xx-small") == 0)
3181 font->size = 1;
3182 else if (g_ascii_strcasecmp(size, "smaller") == 0
3183 || g_ascii_strcasecmp(size, "x-small") == 0)
3184 font->size = 2;
3185 else if (g_ascii_strcasecmp(size, "medium") == 0)
3186 font->size = 3;
3187 else if (g_ascii_strcasecmp(size, "large") == 0
3188 || g_ascii_strcasecmp(size, "larger") == 0)
3189 font->size = 4;
3190 else if (g_ascii_strcasecmp(size, "x-large") == 0)
3191 font->size = 5;
3192 else if (g_ascii_strcasecmp(size, "xx-large") == 0)
3193 font->size = 6;
3196 * TODO: Handle other values, like percentages, or
3197 * lengths specified as em, ex, px, in, cm, mm, pt
3198 * or pc. Or even better, use an actual HTML
3199 * renderer like webkit.
3201 if (font->size > 0)
3202 gtk_imhtml_font_set_size(imhtml, font->size);
3204 else if (oldfont)
3206 font->size = oldfont->size;
3209 if (oldfont)
3211 font->underline = oldfont->underline;
3213 if (textdec && font->underline != 1
3214 && g_ascii_strcasecmp(textdec, "underline") == 0
3215 && (imhtml->format_functions & GTK_IMHTML_UNDERLINE)
3216 && !(options & GTK_IMHTML_NO_FORMATTING))
3218 gtk_imhtml_toggle_underline(imhtml);
3219 font->underline = 1;
3222 if (oldfont)
3224 font->strike = oldfont->strike;
3226 if (textdec && font->strike != 1
3227 && g_ascii_strcasecmp(textdec, "line-through") == 0
3228 && (imhtml->format_functions & GTK_IMHTML_STRIKE)
3229 && !(options & GTK_IMHTML_NO_FORMATTING))
3231 gtk_imhtml_toggle_strike(imhtml);
3232 font->strike = 1;
3234 g_free(textdec);
3236 if (oldfont)
3238 font->bold = oldfont->bold;
3240 if (weight)
3242 if(!g_ascii_strcasecmp(weight, "normal")) {
3243 font->bold = 0;
3244 } else if(!g_ascii_strcasecmp(weight, "bold")) {
3245 font->bold = 1;
3246 } else if(!g_ascii_strcasecmp(weight, "bolder")) {
3247 font->bold++;
3248 } else if(!g_ascii_strcasecmp(weight, "lighter")) {
3249 if(font->bold > 0)
3250 font->bold--;
3251 } else {
3252 int num = atoi(weight);
3253 if(num >= 700)
3254 font->bold = 1;
3255 else
3256 font->bold = 0;
3258 if (((font->bold && oldfont && !oldfont->bold) || (oldfont && oldfont->bold && !font->bold) || (font->bold && !oldfont)) && !(options & GTK_IMHTML_NO_FORMATTING))
3260 gtk_imhtml_toggle_bold(imhtml);
3262 g_free(weight);
3265 g_free(style);
3266 g_free(size);
3267 fonts = g_slist_prepend (fonts, font);
3269 break;
3270 case 57: /* /SPAN */
3271 /* Inline CSS Support - Douglas Thrift */
3272 if (fonts && !imhtml->wbfo) {
3273 GtkIMHtmlFontDetail *oldfont = NULL;
3274 GtkIMHtmlFontDetail *font = fonts->data;
3275 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3276 ws[0] = '\0'; wpos = 0;
3277 /* NEW_BIT (NEW_TEXT_BIT); */
3278 fonts = g_slist_remove (fonts, font);
3279 if (fonts)
3280 oldfont = fonts->data;
3282 if (!oldfont) {
3283 gtk_imhtml_font_set_size(imhtml, 3);
3284 if (font->underline && !(options & GTK_IMHTML_NO_FORMATTING))
3285 gtk_imhtml_toggle_underline(imhtml);
3286 if (font->strike && !(options & GTK_IMHTML_NO_FORMATTING))
3287 gtk_imhtml_toggle_strike(imhtml);
3288 if (font->bold && !(options & GTK_IMHTML_NO_FORMATTING))
3289 gtk_imhtml_toggle_bold(imhtml);
3290 if (!(options & GTK_IMHTML_NO_FONTS))
3291 gtk_imhtml_toggle_fontface(imhtml, NULL);
3292 if (!(options & GTK_IMHTML_NO_COLOURS))
3293 gtk_imhtml_toggle_forecolor(imhtml, NULL);
3294 if (!(options & GTK_IMHTML_NO_COLOURS))
3295 gtk_imhtml_toggle_backcolor(imhtml, NULL);
3297 else
3300 if ((font->size != oldfont->size) && !(options & GTK_IMHTML_NO_SIZES))
3301 gtk_imhtml_font_set_size(imhtml, oldfont->size);
3303 if ((font->underline != oldfont->underline) && !(options & GTK_IMHTML_NO_FORMATTING))
3304 gtk_imhtml_toggle_underline(imhtml);
3306 if ((font->strike != oldfont->strike) && !(options & GTK_IMHTML_NO_FORMATTING))
3307 gtk_imhtml_toggle_strike(imhtml);
3309 if (((font->bold && !oldfont->bold) || (oldfont->bold && !font->bold)) && !(options & GTK_IMHTML_NO_FORMATTING))
3310 gtk_imhtml_toggle_bold(imhtml);
3312 if (font->face && !purple_strequal(font->face, oldfont->face) && !(options & GTK_IMHTML_NO_FONTS))
3313 gtk_imhtml_toggle_fontface(imhtml, oldfont->face);
3315 if (font->fore && !purple_strequal(font->fore, oldfont->fore) && !(options & GTK_IMHTML_NO_COLOURS))
3316 gtk_imhtml_toggle_forecolor(imhtml, oldfont->fore);
3318 if (font->back && !purple_strequal(font->back, oldfont->back) && !(options & GTK_IMHTML_NO_COLOURS))
3319 gtk_imhtml_toggle_backcolor(imhtml, oldfont->back);
3322 g_free (font->face);
3323 g_free (font->fore);
3324 g_free (font->back);
3325 g_free (font->sml);
3327 g_free (font);
3329 break;
3330 case 60: /* SPAN */
3331 break;
3332 case 62: /* comment */
3333 /* NEW_BIT (NEW_TEXT_BIT); */
3334 ws[wpos] = '\0';
3336 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3338 #if FALSE && GTK_CHECK_VERSION(2,10,10)
3339 wpos = g_snprintf (ws, len, "%s", tag);
3340 gtk_text_buffer_insert_with_tags_by_name(imhtml->text_buffer, iter, ws, wpos, "comment", NULL);
3341 #else
3342 if (imhtml->show_comments && !(options & GTK_IMHTML_NO_COMMENTS)) {
3343 wpos = g_snprintf (ws, len, "%s", tag);
3344 gtk_text_buffer_insert_with_tags_by_name(imhtml->text_buffer, iter, ws, wpos, "comment", NULL);
3346 #endif
3347 ws[0] = '\0'; wpos = 0;
3349 /* NEW_BIT (NEW_COMMENT_BIT); */
3350 break;
3351 default:
3352 break;
3354 c += tlen;
3355 pos += tlen;
3356 g_free(tag); /* This was allocated back in VALID_TAG() */
3357 } else if (imhtml->edit.link == NULL &&
3358 !(options & GTK_IMHTML_NO_SMILEY) &&
3359 gtk_imhtml_is_smiley(imhtml, fonts, c, &smilelen)) {
3360 GtkIMHtmlFontDetail *fd;
3361 gchar *sml = NULL;
3363 br = FALSE;
3365 if (fonts) {
3366 fd = fonts->data;
3367 sml = fd->sml;
3369 if (!sml)
3370 sml = imhtml->protocol_name;
3372 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3373 wpos = g_snprintf (ws, smilelen + 1, "%s", c);
3375 gtk_imhtml_insert_smiley_at_iter(imhtml, sml, ws, iter);
3377 c += smilelen;
3378 pos += smilelen;
3379 wpos = 0;
3380 ws[0] = 0;
3381 } else if (*c == '&' && (amp = purple_markup_unescape_entity(c, &tlen))) {
3382 br = FALSE;
3383 while(*amp) {
3384 ws [wpos++] = *amp++;
3386 c += tlen;
3387 pos += tlen;
3388 } else if (*c == '\n') {
3389 if (!(options & GTK_IMHTML_NO_NEWLINE)) {
3390 ws[wpos] = '\n';
3391 wpos++;
3392 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3393 ws[0] = '\0'; wpos = 0;
3394 /* NEW_BIT (NEW_TEXT_BIT); */
3395 } else if (!br) { /* Don't insert a space immediately after an HTML break */
3396 /* A newline is defined by HTML as whitespace, which means we have to replace it with a word boundary.
3397 * word breaks vary depending on the language used, so the correct thing to do is to use Pango to determine
3398 * what language this is, determine the proper word boundary to use, and insert that. I'm just going to insert
3399 * a space instead. What are the non-English speakers going to do? Complain in a language I'll understand?
3400 * Bu-wahaha! */
3401 ws[wpos] = ' ';
3402 wpos++;
3403 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3404 ws[0] = '\0'; wpos = 0;
3406 c++;
3407 pos++;
3408 } else if ((pos == 0 || wpos == 0 || isspace(*(c - 1))) &&
3409 (len_protocol = gtk_imhtml_is_protocol(c)) > 0 &&
3410 c[len_protocol] && !isspace(c[len_protocol]) &&
3411 (c[len_protocol] != '<' || !gtk_imhtml_is_tag(c + 1, NULL, NULL, NULL))) {
3412 br = FALSE;
3413 if (wpos > 0) {
3414 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3415 ws[0] = '\0'; wpos = 0;
3417 while (len_protocol--) {
3418 /* Skip the next len_protocol characters, but
3419 * make sure they're copied into the ws array.
3421 ws [wpos++] = *c++;
3422 pos++;
3424 if (!imhtml->edit.link && (imhtml->format_functions & GTK_IMHTML_LINK)) {
3425 while (*c && !isspace((int)*c) &&
3426 (*c != '<' || !gtk_imhtml_is_tag(c + 1, NULL, NULL, NULL))) {
3427 if (*c == '&' && (amp = purple_markup_unescape_entity(c, &tlen))) {
3428 while (*amp)
3429 ws[wpos++] = *amp++;
3430 c += tlen;
3431 pos += tlen;
3432 } else {
3433 ws [wpos++] = *c++;
3434 pos++;
3437 ws[wpos] = '\0';
3438 gtk_imhtml_toggle_link(imhtml, ws);
3439 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3440 ws[0] = '\0'; wpos = 0;
3441 gtk_imhtml_toggle_link(imhtml, NULL);
3443 } else if (*c) {
3444 br = FALSE;
3445 ws [wpos++] = *c++;
3446 pos++;
3447 } else {
3448 break;
3451 gtk_text_buffer_insert(imhtml->text_buffer, iter, ws, wpos);
3452 ws[0] = '\0'; wpos = 0;
3454 /* NEW_BIT(NEW_TEXT_BIT); */
3456 if(align_right) {
3457 /* insert RLM+LRM at beginning of the line to set alignment */
3458 GtkTextIter line_iter;
3459 line_iter = *iter;
3460 gtk_text_iter_set_line(&line_iter, align_line);
3461 /* insert RLM character to set alignment */
3462 ws[wpos++] = 0xE2;
3463 ws[wpos++] = 0x80;
3464 ws[wpos++] = 0x8F;
3466 if (!rtl_direction)
3468 /* insert LRM character to set direction */
3469 /* (alignment=right and direction=LTR) */
3470 ws[wpos++] = 0xE2;
3471 ws[wpos++] = 0x80;
3472 ws[wpos++] = 0x8E;
3475 ws[wpos] = '\0';
3476 gtk_text_buffer_insert(imhtml->text_buffer, &line_iter, ws, wpos);
3477 gtk_text_buffer_get_end_iter(gtk_text_iter_get_buffer(&line_iter), iter);
3478 ws[0] = '\0'; wpos = 0;
3481 while (fonts) {
3482 GtkIMHtmlFontDetail *font = fonts->data;
3483 fonts = g_slist_remove (fonts, font);
3484 g_free (font->face);
3485 g_free (font->fore);
3486 g_free (font->back);
3487 g_free (font->sml);
3488 g_free (font);
3491 g_free(ws);
3492 g_free(bg);
3494 if (!imhtml->wbfo)
3495 gtk_imhtml_close_tags(imhtml, iter);
3497 object = g_object_ref(G_OBJECT(imhtml));
3498 g_signal_emit(object, signals[UPDATE_FORMAT], 0);
3499 g_object_unref(object);
3501 gtk_text_buffer_end_user_action(imhtml->text_buffer);
3504 void gtk_imhtml_remove_smileys(GtkIMHtml *imhtml)
3506 g_hash_table_destroy(imhtml->smiley_data);
3507 gtk_smiley_tree_destroy(imhtml->default_smilies);
3508 imhtml->smiley_data = g_hash_table_new_full(g_str_hash, g_str_equal,
3509 g_free, (GDestroyNotify)gtk_smiley_tree_destroy);
3510 imhtml->default_smilies = gtk_smiley_tree_new();
3513 void gtk_imhtml_show_comments (GtkIMHtml *imhtml,
3514 gboolean show)
3516 #if FALSE && GTK_CHECK_VERSION(2,10,10)
3517 GtkTextTag *tag;
3518 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), "comment");
3519 if (tag)
3520 g_object_set(G_OBJECT(tag), "invisible", !show, NULL);
3521 #endif
3522 imhtml->show_comments = show;
3525 const char *
3526 gtk_imhtml_get_protocol_name(GtkIMHtml *imhtml) {
3527 return imhtml->protocol_name;
3530 void
3531 gtk_imhtml_set_protocol_name(GtkIMHtml *imhtml, const gchar *protocol_name) {
3532 g_free(imhtml->protocol_name);
3533 imhtml->protocol_name = g_strdup(protocol_name);
3536 void
3537 gtk_imhtml_delete(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *end) {
3538 GList *l;
3539 GSList *sl;
3540 GtkTextIter i, i_s, i_e;
3541 GObject *object = g_object_ref(G_OBJECT(imhtml));
3543 if (start == NULL) {
3544 gtk_text_buffer_get_start_iter(imhtml->text_buffer, &i_s);
3545 start = &i_s;
3548 if (end == NULL) {
3549 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &i_e);
3550 end = &i_e;
3553 l = imhtml->scalables;
3554 while (l) {
3555 GList *next = l->next;
3556 struct scalable_data *sd = l->data;
3557 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer,
3558 &i, sd->mark);
3559 if (gtk_text_iter_in_range(&i, start, end)) {
3560 GtkIMHtmlScalable *scale = GTK_IMHTML_SCALABLE(sd->scalable);
3561 scale->free(scale);
3562 g_free(sd);
3563 imhtml->scalables = g_list_delete_link(imhtml->scalables, l);
3565 l = next;
3568 sl = imhtml->im_images;
3569 while (sl) {
3570 GSList *next = sl->next;
3571 struct im_image_data *img_data = sl->data;
3572 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer,
3573 &i, img_data->mark);
3574 if (gtk_text_iter_in_range(&i, start, end)) {
3575 if (imhtml->funcs->image_unref)
3576 imhtml->funcs->image_unref(img_data->id);
3577 imhtml->im_images = g_slist_delete_link(imhtml->im_images, sl);
3578 g_free(img_data);
3580 sl = next;
3582 gtk_text_buffer_delete(imhtml->text_buffer, start, end);
3584 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_total", GINT_TO_POINTER(0));
3586 g_object_unref(object);
3589 void gtk_imhtml_page_up (GtkIMHtml *imhtml)
3591 GdkRectangle rect;
3592 GtkTextIter iter;
3594 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(imhtml), &rect);
3595 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(imhtml), &iter, rect.x,
3596 rect.y - rect.height);
3597 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(imhtml), &iter, 0, TRUE, 0, 0);
3600 void gtk_imhtml_page_down (GtkIMHtml *imhtml)
3602 GdkRectangle rect;
3603 GtkTextIter iter;
3605 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(imhtml), &rect);
3606 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(imhtml), &iter, rect.x,
3607 rect.y + rect.height);
3608 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(imhtml), &iter, 0, TRUE, 0, 0);
3611 /* GtkIMHtmlScalable, gtk_imhtml_image, gtk_imhtml_hr */
3612 GtkIMHtmlScalable *gtk_imhtml_image_new(GdkPixbuf *img, const gchar *filename, int id)
3614 GtkIMHtmlImage *im_image = g_malloc(sizeof(GtkIMHtmlImage));
3616 GTK_IMHTML_SCALABLE(im_image)->scale = gtk_imhtml_image_scale;
3617 GTK_IMHTML_SCALABLE(im_image)->add_to = gtk_imhtml_image_add_to;
3618 GTK_IMHTML_SCALABLE(im_image)->free = gtk_imhtml_image_free;
3620 im_image->pixbuf = img;
3621 im_image->image = GTK_IMAGE(gtk_image_new_from_pixbuf(im_image->pixbuf));
3622 im_image->width = gdk_pixbuf_get_width(img);
3623 im_image->height = gdk_pixbuf_get_height(img);
3624 im_image->mark = NULL;
3625 im_image->filename = g_strdup(filename);
3626 im_image->id = id;
3627 im_image->filesel = NULL;
3629 g_object_ref(img);
3630 return GTK_IMHTML_SCALABLE(im_image);
3633 static gboolean
3634 animate_image_cb(gpointer data)
3636 GtkIMHtmlImage *im_image;
3637 int width, height;
3638 int delay;
3640 im_image = data;
3642 /* Update the pointer to this GdkPixbuf frame of the animation */
3643 if (gdk_pixbuf_animation_iter_advance(GTK_IMHTML_ANIMATION(im_image)->iter, NULL)) {
3644 GdkPixbuf *pb = gdk_pixbuf_animation_iter_get_pixbuf(GTK_IMHTML_ANIMATION(im_image)->iter);
3645 g_object_unref(G_OBJECT(im_image->pixbuf));
3646 im_image->pixbuf = gdk_pixbuf_copy(pb);
3648 /* Update the displayed GtkImage */
3649 width = gdk_pixbuf_get_width(gtk_image_get_pixbuf(im_image->image));
3650 height = gdk_pixbuf_get_height(gtk_image_get_pixbuf(im_image->image));
3651 if (width > 0 && height > 0)
3653 /* Need to scale the new frame to the same size as the old frame */
3654 GdkPixbuf *tmp;
3655 tmp = gdk_pixbuf_scale_simple(im_image->pixbuf, width, height, GDK_INTERP_BILINEAR);
3656 gtk_image_set_from_pixbuf(im_image->image, tmp);
3657 g_object_unref(G_OBJECT(tmp));
3658 } else {
3659 /* Display at full-size */
3660 gtk_image_set_from_pixbuf(im_image->image, im_image->pixbuf);
3664 delay = MIN(gdk_pixbuf_animation_iter_get_delay_time(GTK_IMHTML_ANIMATION(im_image)->iter), 100);
3665 GTK_IMHTML_ANIMATION(im_image)->timer = g_timeout_add(delay, animate_image_cb, im_image);
3667 return FALSE;
3670 GtkIMHtmlScalable *gtk_imhtml_animation_new(GdkPixbufAnimation *anim, const gchar *filename, int id)
3672 GtkIMHtmlImage *im_image = (GtkIMHtmlImage *) g_new0(GtkIMHtmlAnimation, 1);
3674 GTK_IMHTML_SCALABLE(im_image)->scale = gtk_imhtml_image_scale;
3675 GTK_IMHTML_SCALABLE(im_image)->add_to = gtk_imhtml_image_add_to;
3676 GTK_IMHTML_SCALABLE(im_image)->free = gtk_imhtml_animation_free;
3678 GTK_IMHTML_ANIMATION(im_image)->anim = anim;
3679 if (gdk_pixbuf_animation_is_static_image(anim)) {
3680 im_image->pixbuf = gdk_pixbuf_animation_get_static_image(anim);
3681 g_object_ref(im_image->pixbuf);
3682 } else {
3683 int delay;
3684 GdkPixbuf *pb;
3685 GTK_IMHTML_ANIMATION(im_image)->iter = gdk_pixbuf_animation_get_iter(anim, NULL);
3686 pb = gdk_pixbuf_animation_iter_get_pixbuf(GTK_IMHTML_ANIMATION(im_image)->iter);
3687 im_image->pixbuf = gdk_pixbuf_copy(pb);
3688 delay = MIN(gdk_pixbuf_animation_iter_get_delay_time(GTK_IMHTML_ANIMATION(im_image)->iter), 100);
3689 GTK_IMHTML_ANIMATION(im_image)->timer = g_timeout_add(delay, animate_image_cb, im_image);
3691 im_image->image = GTK_IMAGE(gtk_image_new_from_pixbuf(im_image->pixbuf));
3692 im_image->width = gdk_pixbuf_animation_get_width(anim);
3693 im_image->height = gdk_pixbuf_animation_get_height(anim);
3694 im_image->filename = g_strdup(filename);
3695 im_image->id = id;
3697 g_object_ref(anim);
3699 return GTK_IMHTML_SCALABLE(im_image);
3702 void gtk_imhtml_image_scale(GtkIMHtmlScalable *scale, int width, int height)
3704 GtkIMHtmlImage *im_image = (GtkIMHtmlImage *)scale;
3706 if (im_image->width > width || im_image->height > height) {
3707 double ratio_w, ratio_h, ratio;
3708 int new_h, new_w;
3709 GdkPixbuf *new_image = NULL;
3711 ratio_w = ((double)width - 2) / im_image->width;
3712 ratio_h = ((double)height - 2) / im_image->height;
3714 ratio = (ratio_w < ratio_h) ? ratio_w : ratio_h;
3716 new_w = (int)(im_image->width * ratio);
3717 new_h = (int)(im_image->height * ratio);
3719 new_image = gdk_pixbuf_scale_simple(im_image->pixbuf, new_w, new_h, GDK_INTERP_BILINEAR);
3720 gtk_image_set_from_pixbuf(im_image->image, new_image);
3721 g_object_unref(G_OBJECT(new_image));
3722 } else if (gdk_pixbuf_get_width(gtk_image_get_pixbuf(im_image->image)) != im_image->width) {
3723 /* Enough space to show the full-size of the image. */
3724 GdkPixbuf *new_image;
3726 new_image = gdk_pixbuf_scale_simple(im_image->pixbuf, im_image->width, im_image->height, GDK_INTERP_BILINEAR);
3727 gtk_image_set_from_pixbuf(im_image->image, new_image);
3728 g_object_unref(G_OBJECT(new_image));
3732 static void
3733 image_save_yes_cb(GtkIMHtmlImageSave *save, const char *filename)
3735 GError *error = NULL;
3736 GtkIMHtmlImage *image = (GtkIMHtmlImage *)save->image;
3738 gtk_widget_destroy(image->filesel);
3739 image->filesel = NULL;
3741 if (save->data && save->datasize) {
3742 g_file_set_contents(filename, save->data, save->datasize, &error);
3743 } else {
3744 gchar *type = NULL;
3745 GSList *formats = gdk_pixbuf_get_formats();
3746 char *newfilename;
3748 while (formats) {
3749 GdkPixbufFormat *format = formats->data;
3750 gchar **extensions = gdk_pixbuf_format_get_extensions(format);
3751 gpointer p = extensions;
3753 while(gdk_pixbuf_format_is_writable(format) && extensions && extensions[0]){
3754 gchar *fmt_ext = extensions[0];
3755 const gchar* file_ext = filename + strlen(filename) - strlen(fmt_ext);
3757 if(!g_ascii_strcasecmp(fmt_ext, file_ext)){
3758 type = gdk_pixbuf_format_get_name(format);
3759 break;
3762 extensions++;
3765 g_strfreev(p);
3767 if (type)
3768 break;
3770 formats = formats->next;
3773 g_slist_free(formats);
3775 /* If I can't find a valid type, I will just tell the user about it and then assume
3776 it's a png */
3777 if (!type){
3778 char *basename, *tmp;
3779 char *dirname;
3780 GtkWidget *dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
3781 _("<span size='larger' weight='bold'>Unrecognized file type</span>\n\nDefaulting to PNG."));
3783 g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
3784 gtk_widget_show(dialog);
3786 type = g_strdup("png");
3787 dirname = g_path_get_dirname(filename);
3788 basename = g_path_get_basename(filename);
3789 tmp = strrchr(basename, '.');
3790 if (tmp != NULL)
3791 tmp[0] = '\0';
3792 newfilename = g_strdup_printf("%s" G_DIR_SEPARATOR_S "%s.png", dirname, basename);
3793 g_free(dirname);
3794 g_free(basename);
3795 } else {
3797 * We're able to save the file in it's original format, so we
3798 * can use the original file name.
3800 newfilename = g_strdup(filename);
3803 gdk_pixbuf_save(image->pixbuf, newfilename, type, &error, NULL);
3805 g_free(newfilename);
3806 g_free(type);
3809 if (error){
3810 GtkWidget *dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
3811 _("<span size='larger' weight='bold'>Error saving image</span>\n\n%s"), error->message);
3812 g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
3813 gtk_widget_show(dialog);
3814 g_error_free(error);
3818 static void
3819 image_save_check_if_exists_cb(GtkWidget *widget, gint response, GtkIMHtmlImageSave *save)
3821 gchar *filename;
3822 GtkIMHtmlImage *image = (GtkIMHtmlImage *)save->image;
3824 if (response != GTK_RESPONSE_ACCEPT) {
3825 gtk_widget_destroy(widget);
3826 image->filesel = NULL;
3827 return;
3830 filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
3833 * XXX - We should probably prompt the user to determine if they really
3834 * want to overwrite the file or not. However, I don't feel like doing
3835 * that, so we're just always going to overwrite if the file exists.
3838 if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
3839 } else
3840 image_save_yes_cb(image, filename);
3843 image_save_yes_cb(save, filename);
3845 g_free(filename);
3848 static void
3849 gtk_imhtml_image_save(GtkWidget *w, GtkIMHtmlImageSave *save)
3851 GtkIMHtmlImage *image = (GtkIMHtmlImage *)save->image;
3853 if (image->filesel != NULL) {
3854 gtk_window_present(GTK_WINDOW(image->filesel));
3855 return;
3858 image->filesel = gtk_file_chooser_dialog_new(_("Save Image"),
3859 NULL,
3860 GTK_FILE_CHOOSER_ACTION_SAVE,
3861 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
3862 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
3863 NULL);
3864 gtk_dialog_set_default_response(GTK_DIALOG(image->filesel), GTK_RESPONSE_ACCEPT);
3865 if (image->filename != NULL)
3866 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(image->filesel), image->filename);
3867 g_signal_connect(G_OBJECT(GTK_FILE_CHOOSER(image->filesel)), "response",
3868 G_CALLBACK(image_save_check_if_exists_cb), save);
3870 gtk_widget_show(image->filesel);
3873 static void
3874 gtk_imhtml_custom_smiley_save(GtkWidget *w, GtkIMHtmlImageSave *save)
3876 GtkIMHtmlImage *image = (GtkIMHtmlImage *)save->image;
3878 /* Create an add dialog */
3879 PidginSmiley *editor = pidgin_smiley_edit(NULL, NULL);
3880 pidgin_smiley_editor_set_shortcut(editor, image->filename);
3881 pidgin_smiley_editor_set_image(editor, image->pixbuf);
3882 pidgin_smiley_editor_set_data(editor, save->data, save->datasize);
3886 * So, um, AIM Direct IM lets you send any file, not just images. You can
3887 * just insert a sound or a file or whatever in a conversation. It's
3888 * basically like file transfer, except there is an icon to open the file
3889 * embedded in the conversation. Someone should make the Purple core handle
3890 * all of that.
3892 static gboolean gtk_imhtml_image_clicked(GtkWidget *w, GdkEvent *event, GtkIMHtmlImageSave *save)
3894 GdkEventButton *event_button = (GdkEventButton *) event;
3895 GtkIMHtmlImage *image = (GtkIMHtmlImage *)save->image;
3897 if (event->type == GDK_BUTTON_RELEASE) {
3898 if(event_button->button == 3) {
3899 GtkWidget *img, *item, *menu;
3900 menu = gtk_menu_new();
3902 /* buttons and such */
3903 img = gtk_image_new_from_stock(GTK_STOCK_SAVE, GTK_ICON_SIZE_MENU);
3904 item = gtk_image_menu_item_new_with_mnemonic(_("_Save Image..."));
3905 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), img);
3906 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(gtk_imhtml_image_save), save);
3907 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3909 /* Add menu item for adding custom smiley to local smileys */
3910 /* we only add the menu if the image is of "custom smiley size"
3911 <= 96x96 pixels */
3912 if (image->width <= 96 && image->height <= 96) {
3913 img = gtk_image_new_from_stock(GTK_STOCK_ADD, GTK_ICON_SIZE_MENU);
3914 item = gtk_image_menu_item_new_with_mnemonic(_("_Add Custom Smiley..."));
3915 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), img);
3916 g_signal_connect(G_OBJECT(item), "activate",
3917 G_CALLBACK(gtk_imhtml_custom_smiley_save), save);
3918 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3921 gtk_widget_show_all(menu);
3922 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
3923 event_button->button, event_button->time);
3925 return TRUE;
3928 if(event->type == GDK_BUTTON_PRESS && event_button->button == 3)
3929 return TRUE; /* Clicking the right mouse button on a link shouldn't
3930 be caught by the regular GtkTextView menu */
3931 else
3932 return FALSE; /* Let clicks go through if we didn't catch anything */
3936 static gboolean gtk_imhtml_smiley_clicked(GtkWidget *w, GdkEvent *event, GtkIMHtmlSmiley *smiley)
3938 GdkPixbufAnimation *anim = NULL;
3939 GtkIMHtmlImageSave *save = NULL;
3940 gboolean ret;
3942 if (event->type != GDK_BUTTON_RELEASE || ((GdkEventButton*)event)->button != 3)
3943 return FALSE;
3945 anim = gtk_smiley_get_image(smiley);
3946 if (!anim)
3947 return FALSE;
3949 save = g_new0(GtkIMHtmlImageSave, 1);
3950 save->image = (GtkIMHtmlScalable *)gtk_imhtml_animation_new(anim, smiley->smile, 0);
3951 save->data = smiley->data; /* Do not need to memdup here, since the smiley is not
3952 destroyed before this GtkIMHtmlImageSave */
3953 save->datasize = smiley->datasize;
3954 ret = gtk_imhtml_image_clicked(w, event, save);
3955 g_object_set_data_full(G_OBJECT(w), "image-data", save->image, (GDestroyNotify)gtk_imhtml_animation_free);
3956 g_object_set_data_full(G_OBJECT(w), "image-save-data", save, (GDestroyNotify)g_free);
3957 return ret;
3960 void gtk_imhtml_image_free(GtkIMHtmlScalable *scale)
3962 GtkIMHtmlImage *image = (GtkIMHtmlImage *)scale;
3964 g_object_unref(image->pixbuf);
3965 g_free(image->filename);
3966 if (image->filesel)
3967 gtk_widget_destroy(image->filesel);
3968 g_free(scale);
3971 void gtk_imhtml_animation_free(GtkIMHtmlScalable *scale)
3973 GtkIMHtmlAnimation *animation = (GtkIMHtmlAnimation *)scale;
3975 if (animation->timer > 0)
3976 g_source_remove(animation->timer);
3977 if (animation->iter != NULL)
3978 g_object_unref(animation->iter);
3979 g_object_unref(animation->anim);
3981 gtk_imhtml_image_free(scale);
3984 void gtk_imhtml_image_add_to(GtkIMHtmlScalable *scale, GtkIMHtml *imhtml, GtkTextIter *iter)
3986 GtkIMHtmlImage *image = (GtkIMHtmlImage *)scale;
3987 GtkWidget *box = gtk_event_box_new();
3988 char *tag;
3989 GtkTextChildAnchor *anchor = gtk_text_buffer_create_child_anchor(imhtml->text_buffer, iter);
3990 GtkIMHtmlImageSave *save;
3992 gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(image->image));
3994 if(!gtk_check_version(2, 4, 0))
3995 g_object_set(G_OBJECT(box), "visible-window", FALSE, NULL);
3997 gtk_widget_show(GTK_WIDGET(image->image));
3998 gtk_widget_show(box);
4000 tag = g_strdup_printf("<IMG ID=\"%d\">", image->id);
4001 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_htmltext", tag, g_free);
4002 g_object_set_data(G_OBJECT(anchor), "gtkimhtml_plaintext", "[Image]");
4004 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(imhtml), box, anchor);
4006 save = g_new0(GtkIMHtmlImageSave, 1);
4007 save->image = scale;
4008 g_signal_connect(G_OBJECT(box), "event", G_CALLBACK(gtk_imhtml_image_clicked), save);
4009 g_object_set_data_full(G_OBJECT(box), "image-save-data", save, (GDestroyNotify)g_free);
4012 GtkIMHtmlScalable *gtk_imhtml_hr_new()
4014 GtkIMHtmlHr *hr = g_malloc(sizeof(GtkIMHtmlHr));
4016 GTK_IMHTML_SCALABLE(hr)->scale = gtk_imhtml_hr_scale;
4017 GTK_IMHTML_SCALABLE(hr)->add_to = gtk_imhtml_hr_add_to;
4018 GTK_IMHTML_SCALABLE(hr)->free = gtk_imhtml_hr_free;
4020 hr->sep = gtk_hseparator_new();
4021 gtk_widget_set_size_request(hr->sep, 5000, 2);
4022 gtk_widget_show(hr->sep);
4024 return GTK_IMHTML_SCALABLE(hr);
4027 void gtk_imhtml_hr_scale(GtkIMHtmlScalable *scale, int width, int height)
4029 gtk_widget_set_size_request(((GtkIMHtmlHr *)scale)->sep, width - 2, 2);
4032 void gtk_imhtml_hr_add_to(GtkIMHtmlScalable *scale, GtkIMHtml *imhtml, GtkTextIter *iter)
4034 GtkIMHtmlHr *hr = (GtkIMHtmlHr *)scale;
4035 GtkTextChildAnchor *anchor = gtk_text_buffer_create_child_anchor(imhtml->text_buffer, iter);
4036 g_object_set_data(G_OBJECT(anchor), "gtkimhtml_htmltext", "<hr>");
4037 g_object_set_data(G_OBJECT(anchor), "gtkimhtml_plaintext", "\n---\n");
4038 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(imhtml), hr->sep, anchor);
4041 void gtk_imhtml_hr_free(GtkIMHtmlScalable *scale)
4043 g_free(scale);
4046 gboolean gtk_imhtml_search_find(GtkIMHtml *imhtml, const gchar *text)
4048 GtkTextIter iter, start, end;
4049 gboolean new_search = TRUE;
4050 GtkTextMark *start_mark;
4052 g_return_val_if_fail(imhtml != NULL, FALSE);
4053 g_return_val_if_fail(text != NULL, FALSE);
4055 start_mark = gtk_text_buffer_get_mark(imhtml->text_buffer, "search");
4057 if (start_mark && imhtml->search_string && purple_strequal(text, imhtml->search_string))
4058 new_search = FALSE;
4060 if (new_search) {
4061 gtk_imhtml_search_clear(imhtml);
4062 g_free(imhtml->search_string);
4063 imhtml->search_string = g_strdup(text);
4064 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &iter);
4065 } else {
4066 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter,
4067 start_mark);
4070 if (gtk_source_iter_backward_search(&iter, imhtml->search_string,
4071 GTK_SOURCE_SEARCH_VISIBLE_ONLY | GTK_SOURCE_SEARCH_CASE_INSENSITIVE,
4072 &start, &end, NULL))
4074 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(imhtml), &start, 0, TRUE, 0, 0);
4075 gtk_text_buffer_create_mark(imhtml->text_buffer, "search", &start, FALSE);
4076 if (new_search)
4078 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "search", &iter, &end);
4080 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "search", &start, &end);
4081 while (gtk_source_iter_backward_search(&start, imhtml->search_string,
4082 GTK_SOURCE_SEARCH_VISIBLE_ONLY |
4083 GTK_SOURCE_SEARCH_CASE_INSENSITIVE,
4084 &start, &end, NULL));
4086 return TRUE;
4088 else if (!new_search)
4090 /* We hit the end, so start at the beginning again. */
4091 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &iter);
4093 if (gtk_source_iter_backward_search(&iter, imhtml->search_string,
4094 GTK_SOURCE_SEARCH_VISIBLE_ONLY | GTK_SOURCE_SEARCH_CASE_INSENSITIVE,
4095 &start, &end, NULL))
4097 gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(imhtml), &start, 0, TRUE, 0, 0);
4098 gtk_text_buffer_create_mark(imhtml->text_buffer, "search", &start, FALSE);
4100 return TRUE;
4105 return FALSE;
4108 void gtk_imhtml_search_clear(GtkIMHtml *imhtml)
4110 GtkTextIter start, end;
4112 g_return_if_fail(imhtml != NULL);
4114 gtk_text_buffer_get_start_iter(imhtml->text_buffer, &start);
4115 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &end);
4117 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "search", &start, &end);
4118 g_free(imhtml->search_string);
4119 imhtml->search_string = NULL;
4122 static GtkTextTag *find_font_forecolor_tag(GtkIMHtml *imhtml, gchar *color)
4124 gchar str[18];
4125 GtkTextTag *tag;
4127 g_snprintf(str, sizeof(str), "FORECOLOR %s", color);
4129 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), str);
4130 if (!tag) {
4131 GdkColor gcolor;
4132 if (!gdk_color_parse(color, &gcolor)) {
4133 gchar tmp[8];
4134 tmp[0] = '#';
4135 strncpy(&tmp[1], color, 7);
4136 tmp[7] = '\0';
4137 if (!gdk_color_parse(tmp, &gcolor))
4138 gdk_color_parse("black", &gcolor);
4140 tag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "foreground-gdk", &gcolor, NULL);
4143 return tag;
4146 static GtkTextTag *find_font_backcolor_tag(GtkIMHtml *imhtml, gchar *color)
4148 gchar str[18];
4149 GtkTextTag *tag;
4151 g_snprintf(str, sizeof(str), "BACKCOLOR %s", color);
4153 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), str);
4154 if (!tag) {
4155 GdkColor gcolor;
4156 if (!gdk_color_parse(color, &gcolor)) {
4157 gchar tmp[8];
4158 tmp[0] = '#';
4159 strncpy(&tmp[1], color, 7);
4160 tmp[7] = '\0';
4161 if (!gdk_color_parse(tmp, &gcolor))
4162 gdk_color_parse("white", &gcolor);
4164 tag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "background-gdk", &gcolor, NULL);
4167 return tag;
4170 static GtkTextTag *find_font_background_tag(GtkIMHtml *imhtml, gchar *color)
4172 gchar str[19];
4173 GtkTextTag *tag;
4175 g_snprintf(str, sizeof(str), "BACKGROUND %s", color);
4177 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), str);
4178 if (!tag)
4179 tag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, NULL);
4181 return tag;
4184 static GtkTextTag *find_font_face_tag(GtkIMHtml *imhtml, gchar *face)
4186 gchar str[256];
4187 GtkTextTag *tag;
4189 g_snprintf(str, sizeof(str), "FONT FACE %s", face);
4190 str[255] = '\0';
4192 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), str);
4193 if (!tag)
4194 tag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "family", face, NULL);
4196 return tag;
4199 static GtkTextTag *find_font_size_tag(GtkIMHtml *imhtml, int size)
4201 gchar str[24];
4202 GtkTextTag *tag;
4204 g_snprintf(str, sizeof(str), "FONT SIZE %d", size);
4205 str[23] = '\0';
4207 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(imhtml->text_buffer), str);
4208 if (!tag) {
4209 /* For reasons I don't understand, setting "scale" here scaled
4210 * based on some default size other than my theme's default
4211 * size. Our size 4 was actually smaller than our size 3 for
4212 * me. So this works around that oddity.
4214 GtkTextAttributes *attr = gtk_text_view_get_default_attributes(GTK_TEXT_VIEW(imhtml));
4215 tag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "size",
4216 (gint) (pango_font_description_get_size(attr->font) *
4217 (double) POINT_SIZE(size)), NULL);
4218 gtk_text_attributes_unref(attr);
4221 return tag;
4224 static void remove_tag_by_prefix(GtkIMHtml *imhtml, const GtkTextIter *i, const GtkTextIter *e,
4225 const char *prefix, guint len, gboolean homo)
4227 GSList *tags, *l;
4228 GtkTextIter iter;
4230 tags = gtk_text_iter_get_tags(i);
4232 for (l = tags; l; l = l->next) {
4233 GtkTextTag *tag = l->data;
4235 if (tag->name && !strncmp(tag->name, prefix, len))
4236 gtk_text_buffer_remove_tag(imhtml->text_buffer, tag, i, e);
4239 g_slist_free(tags);
4241 if (homo)
4242 return;
4244 iter = *i;
4246 while (gtk_text_iter_forward_char(&iter) && !gtk_text_iter_equal(&iter, e)) {
4247 if (gtk_text_iter_begins_tag(&iter, NULL)) {
4248 tags = gtk_text_iter_get_toggled_tags(&iter, TRUE);
4250 for (l = tags; l; l = l->next) {
4251 GtkTextTag *tag = l->data;
4253 if (tag->name && !strncmp(tag->name, prefix, len))
4254 gtk_text_buffer_remove_tag(imhtml->text_buffer, tag, &iter, e);
4257 g_slist_free(tags);
4262 static void remove_font_size(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4264 remove_tag_by_prefix(imhtml, i, e, "FONT SIZE ", 10, homo);
4267 static void remove_font_face(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4269 remove_tag_by_prefix(imhtml, i, e, "FONT FACE ", 10, homo);
4272 static void remove_font_forecolor(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4274 remove_tag_by_prefix(imhtml, i, e, "FORECOLOR ", 10, homo);
4277 static void remove_font_backcolor(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4279 remove_tag_by_prefix(imhtml, i, e, "BACKCOLOR ", 10, homo);
4282 static void remove_font_background(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4284 remove_tag_by_prefix(imhtml, i, e, "BACKGROUND ", 10, homo);
4287 static void remove_font_link(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo)
4289 remove_tag_by_prefix(imhtml, i, e, "LINK ", 5, homo);
4292 static void
4293 imhtml_clear_formatting(GtkIMHtml *imhtml)
4295 GtkTextIter start, end;
4297 if (!imhtml->editable)
4298 return;
4300 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4301 return;
4303 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "BOLD", &start, &end);
4304 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "ITALICS", &start, &end);
4305 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "UNDERLINE", &start, &end);
4306 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "STRIKE", &start, &end);
4307 remove_font_size(imhtml, &start, &end, FALSE);
4308 remove_font_face(imhtml, &start, &end, FALSE);
4309 remove_font_forecolor(imhtml, &start, &end, FALSE);
4310 remove_font_backcolor(imhtml, &start, &end, FALSE);
4311 remove_font_background(imhtml, &start, &end, FALSE);
4312 remove_font_link(imhtml, &start, &end, FALSE);
4314 imhtml->edit.bold = 0;
4315 imhtml->edit.italic = 0;
4316 imhtml->edit.underline = 0;
4317 imhtml->edit.strike = 0;
4318 imhtml->edit.fontsize = 0;
4320 g_free(imhtml->edit.fontface);
4321 imhtml->edit.fontface = NULL;
4323 g_free(imhtml->edit.forecolor);
4324 imhtml->edit.forecolor = NULL;
4326 g_free(imhtml->edit.backcolor);
4327 imhtml->edit.backcolor = NULL;
4329 g_free(imhtml->edit.background);
4330 imhtml->edit.background = NULL;
4333 /* Editable stuff */
4334 static void preinsert_cb(GtkTextBuffer *buffer, GtkTextIter *iter, gchar *text, gint len, GtkIMHtml *imhtml)
4336 imhtml->insert_offset = gtk_text_iter_get_offset(iter);
4339 static void insert_ca_cb(GtkTextBuffer *buffer, GtkTextIter *arg1, GtkTextChildAnchor *arg2, gpointer user_data)
4341 GtkTextIter start;
4343 start = *arg1;
4344 gtk_text_iter_backward_char(&start);
4346 gtk_imhtml_apply_tags_on_insert(user_data, &start, arg1);
4349 static void insert_cb(GtkTextBuffer *buffer, GtkTextIter *end, gchar *text, gint len, GtkIMHtml *imhtml)
4351 GtkTextIter start;
4353 if (!len)
4354 return;
4356 start = *end;
4357 gtk_text_iter_set_offset(&start, imhtml->insert_offset);
4359 gtk_imhtml_apply_tags_on_insert(imhtml, &start, end);
4362 static void delete_cb(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end, GtkIMHtml *imhtml)
4364 GSList *tags, *l;
4366 tags = gtk_text_iter_get_tags(start);
4367 for (l = tags; l != NULL; l = l->next) {
4368 GtkTextTag *tag = GTK_TEXT_TAG(l->data);
4370 if (tag && /* Remove the formatting only if */
4371 gtk_text_iter_starts_word(start) && /* beginning of a word */
4372 gtk_text_iter_begins_tag(start, tag) && /* the tag starts with the selection */
4373 (!gtk_text_iter_has_tag(end, tag) || /* the tag ends within the selection */
4374 gtk_text_iter_ends_tag(end, tag))) {
4375 gtk_text_buffer_remove_tag(imhtml->text_buffer, tag, start, end);
4376 if (tag->name &&
4377 strncmp(tag->name, "LINK ", 5) == 0 && imhtml->edit.link) {
4378 gtk_imhtml_toggle_link(imhtml, NULL);
4382 g_slist_free(tags);
4385 static void gtk_imhtml_apply_tags_on_insert(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *end)
4387 if (imhtml->edit.bold)
4388 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "BOLD", start, end);
4389 else
4390 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "BOLD", start, end);
4392 if (imhtml->edit.italic)
4393 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "ITALICS", start, end);
4394 else
4395 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "ITALICS", start, end);
4397 if (imhtml->edit.underline)
4398 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "UNDERLINE", start, end);
4399 else
4400 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "UNDERLINE", start, end);
4402 if (imhtml->edit.strike)
4403 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "STRIKE", start, end);
4404 else
4405 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "STRIKE", start, end);
4407 if (imhtml->edit.forecolor) {
4408 remove_font_forecolor(imhtml, start, end, TRUE);
4409 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4410 find_font_forecolor_tag(imhtml, imhtml->edit.forecolor),
4411 start, end);
4414 if (imhtml->edit.backcolor) {
4415 remove_font_backcolor(imhtml, start, end, TRUE);
4416 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4417 find_font_backcolor_tag(imhtml, imhtml->edit.backcolor),
4418 start, end);
4421 if (imhtml->edit.background) {
4422 remove_font_background(imhtml, start, end, TRUE);
4423 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4424 find_font_background_tag(imhtml, imhtml->edit.background),
4425 start, end);
4427 if (imhtml->edit.fontface) {
4428 remove_font_face(imhtml, start, end, TRUE);
4429 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4430 find_font_face_tag(imhtml, imhtml->edit.fontface),
4431 start, end);
4434 if (imhtml->edit.fontsize) {
4435 remove_font_size(imhtml, start, end, TRUE);
4436 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4437 find_font_size_tag(imhtml, imhtml->edit.fontsize),
4438 start, end);
4441 if (imhtml->edit.link) {
4442 remove_font_link(imhtml, start, end, TRUE);
4443 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4444 imhtml->edit.link,
4445 start, end);
4449 void gtk_imhtml_set_editable(GtkIMHtml *imhtml, gboolean editable)
4451 gtk_text_view_set_editable(GTK_TEXT_VIEW(imhtml), editable);
4453 * We need a visible caret for accessibility, so mouseless
4454 * people can highlight stuff.
4456 /* gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(imhtml), editable); */
4457 if (editable && !imhtml->editable) {
4458 g_signal_connect_after(G_OBJECT(GTK_IMHTML(imhtml)->text_buffer), "mark-set",
4459 G_CALLBACK(mark_set_cb), imhtml);
4460 g_signal_connect(G_OBJECT(imhtml), "backspace", G_CALLBACK(smart_backspace_cb), NULL);
4461 } else if (!editable && imhtml->editable) {
4462 g_signal_handlers_disconnect_by_func(G_OBJECT(GTK_IMHTML(imhtml)->text_buffer),
4463 mark_set_cb, imhtml);
4464 g_signal_handlers_disconnect_by_func(G_OBJECT(imhtml),
4465 smart_backspace_cb, NULL);
4468 imhtml->editable = editable;
4469 imhtml->format_functions = GTK_IMHTML_ALL;
4472 void gtk_imhtml_set_whole_buffer_formatting_only(GtkIMHtml *imhtml, gboolean wbfo)
4474 g_return_if_fail(imhtml != NULL);
4476 imhtml->wbfo = wbfo;
4479 void gtk_imhtml_set_format_functions(GtkIMHtml *imhtml, GtkIMHtmlButtons buttons)
4481 GObject *object = g_object_ref(G_OBJECT(imhtml));
4482 imhtml->format_functions = buttons;
4483 g_signal_emit(object, signals[BUTTONS_UPDATE], 0, buttons);
4484 g_object_unref(object);
4487 GtkIMHtmlButtons gtk_imhtml_get_format_functions(GtkIMHtml *imhtml)
4489 return imhtml->format_functions;
4492 void gtk_imhtml_get_current_format(GtkIMHtml *imhtml, gboolean *bold,
4493 gboolean *italic, gboolean *underline)
4495 if (bold != NULL)
4496 (*bold) = imhtml->edit.bold;
4497 if (italic != NULL)
4498 (*italic) = imhtml->edit.italic;
4499 if (underline != NULL)
4500 (*underline) = imhtml->edit.underline;
4503 char *
4504 gtk_imhtml_get_current_fontface(GtkIMHtml *imhtml)
4506 return g_strdup(imhtml->edit.fontface);
4509 char *
4510 gtk_imhtml_get_current_forecolor(GtkIMHtml *imhtml)
4512 return g_strdup(imhtml->edit.forecolor);
4515 char *
4516 gtk_imhtml_get_current_backcolor(GtkIMHtml *imhtml)
4518 return g_strdup(imhtml->edit.backcolor);
4521 char *
4522 gtk_imhtml_get_current_background(GtkIMHtml *imhtml)
4524 return g_strdup(imhtml->edit.background);
4527 gint
4528 gtk_imhtml_get_current_fontsize(GtkIMHtml *imhtml)
4530 return imhtml->edit.fontsize;
4533 gboolean gtk_imhtml_get_editable(GtkIMHtml *imhtml)
4535 return imhtml->editable;
4538 void
4539 gtk_imhtml_clear_formatting(GtkIMHtml *imhtml)
4541 GObject *object;
4543 object = g_object_ref(G_OBJECT(imhtml));
4544 g_signal_emit(object, signals[CLEAR_FORMAT], 0);
4546 gtk_widget_grab_focus(GTK_WIDGET(imhtml));
4548 g_object_unref(object);
4552 * I had this crazy idea about changing the text cursor color to reflex the foreground color
4553 * of the text about to be entered. This is the place you'd do it, along with the place where
4554 * we actually set a new foreground color.
4555 * I may not do this, because people will bitch about Purple overriding their gtk theme's cursor
4556 * colors.
4558 * Just in case I do do this, I asked about what to set the secondary text cursor to.
4560 * (12:45:27) ?? ???: secondary_cursor_color = (rgb(background) + rgb(primary_cursor_color) ) / 2
4561 * (12:45:55) ?? ???: understand?
4562 * (12:46:14) Tim: yeah. i didn't know there was an exact formula
4563 * (12:46:56) ?? ???: u might need to extract separate each color from RGB
4566 static void mark_set_cb(GtkTextBuffer *buffer, GtkTextIter *arg1, GtkTextMark *mark,
4567 GtkIMHtml *imhtml)
4569 GSList *tags, *l;
4570 GtkTextIter iter;
4572 if (mark != gtk_text_buffer_get_insert(buffer))
4573 return;
4575 if (!gtk_text_buffer_get_char_count(buffer))
4576 return;
4578 imhtml->edit.bold = imhtml->edit.italic = imhtml->edit.underline = imhtml->edit.strike = FALSE;
4579 g_free(imhtml->edit.forecolor);
4580 imhtml->edit.forecolor = NULL;
4582 g_free(imhtml->edit.backcolor);
4583 imhtml->edit.backcolor = NULL;
4585 g_free(imhtml->edit.fontface);
4586 imhtml->edit.fontface = NULL;
4588 imhtml->edit.fontsize = 0;
4589 imhtml->edit.link = NULL;
4591 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, mark);
4594 if (gtk_text_iter_is_end(&iter))
4595 tags = gtk_text_iter_get_toggled_tags(&iter, FALSE);
4596 else
4597 tags = gtk_text_iter_get_tags(&iter);
4599 for (l = tags; l != NULL; l = l->next) {
4600 GtkTextTag *tag = GTK_TEXT_TAG(l->data);
4602 if (tag->name) {
4603 if (purple_strequal(tag->name, "BOLD"))
4604 imhtml->edit.bold = TRUE;
4605 else if (purple_strequal(tag->name, "ITALICS"))
4606 imhtml->edit.italic = TRUE;
4607 else if (purple_strequal(tag->name, "UNDERLINE"))
4608 imhtml->edit.underline = TRUE;
4609 else if (purple_strequal(tag->name, "STRIKE"))
4610 imhtml->edit.strike = TRUE;
4611 else if (strncmp(tag->name, "FORECOLOR ", 10) == 0)
4612 imhtml->edit.forecolor = g_strdup(&(tag->name)[10]);
4613 else if (strncmp(tag->name, "BACKCOLOR ", 10) == 0)
4614 imhtml->edit.backcolor = g_strdup(&(tag->name)[10]);
4615 else if (strncmp(tag->name, "FONT FACE ", 10) == 0)
4616 imhtml->edit.fontface = g_strdup(&(tag->name)[10]);
4617 else if (strncmp(tag->name, "FONT SIZE ", 10) == 0)
4618 imhtml->edit.fontsize = strtol(&(tag->name)[10], NULL, 10);
4619 else if ((strncmp(tag->name, "LINK ", 5) == 0) && !gtk_text_iter_is_end(&iter))
4620 imhtml->edit.link = tag;
4624 g_slist_free(tags);
4627 static void imhtml_emit_signal_for_format(GtkIMHtml *imhtml, GtkIMHtmlButtons button)
4629 GObject *object;
4631 g_return_if_fail(imhtml != NULL);
4633 object = g_object_ref(G_OBJECT(imhtml));
4634 g_signal_emit(object, signals[TOGGLE_FORMAT], 0, button);
4635 g_object_unref(object);
4638 static void imhtml_toggle_bold(GtkIMHtml *imhtml)
4640 GtkTextIter start, end;
4642 imhtml->edit.bold = !imhtml->edit.bold;
4644 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4645 return;
4647 if (imhtml->edit.bold)
4648 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "BOLD", &start, &end);
4649 else
4650 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "BOLD", &start, &end);
4653 void gtk_imhtml_toggle_bold(GtkIMHtml *imhtml)
4655 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_BOLD);
4658 static void imhtml_toggle_italic(GtkIMHtml *imhtml)
4660 GtkTextIter start, end;
4662 imhtml->edit.italic = !imhtml->edit.italic;
4664 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4665 return;
4667 if (imhtml->edit.italic)
4668 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "ITALICS", &start, &end);
4669 else
4670 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "ITALICS", &start, &end);
4673 void gtk_imhtml_toggle_italic(GtkIMHtml *imhtml)
4675 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_ITALIC);
4678 static void imhtml_toggle_underline(GtkIMHtml *imhtml)
4680 GtkTextIter start, end;
4682 imhtml->edit.underline = !imhtml->edit.underline;
4684 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4685 return;
4687 if (imhtml->edit.underline)
4688 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "UNDERLINE", &start, &end);
4689 else
4690 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "UNDERLINE", &start, &end);
4693 void gtk_imhtml_toggle_underline(GtkIMHtml *imhtml)
4695 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_UNDERLINE);
4698 static void imhtml_toggle_strike(GtkIMHtml *imhtml)
4700 GtkTextIter start, end;
4702 imhtml->edit.strike = !imhtml->edit.strike;
4704 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4705 return;
4707 if (imhtml->edit.strike)
4708 gtk_text_buffer_apply_tag_by_name(imhtml->text_buffer, "STRIKE", &start, &end);
4709 else
4710 gtk_text_buffer_remove_tag_by_name(imhtml->text_buffer, "STRIKE", &start, &end);
4713 void gtk_imhtml_toggle_strike(GtkIMHtml *imhtml)
4715 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_STRIKE);
4718 void gtk_imhtml_font_set_size(GtkIMHtml *imhtml, gint size)
4720 GObject *object;
4721 GtkTextIter start, end;
4723 imhtml->edit.fontsize = size;
4725 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4726 return;
4728 remove_font_size(imhtml, &start, &end, imhtml->wbfo);
4729 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4730 find_font_size_tag(imhtml, imhtml->edit.fontsize), &start, &end);
4732 object = g_object_ref(G_OBJECT(imhtml));
4733 g_signal_emit(object, signals[TOGGLE_FORMAT], 0, GTK_IMHTML_SHRINK | GTK_IMHTML_GROW);
4734 g_object_unref(object);
4737 static void imhtml_font_shrink(GtkIMHtml *imhtml)
4739 GtkTextIter start, end;
4741 if (imhtml->edit.fontsize == 1)
4742 return;
4744 if (!imhtml->edit.fontsize)
4745 imhtml->edit.fontsize = 2;
4746 else
4747 imhtml->edit.fontsize--;
4749 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4750 return;
4751 remove_font_size(imhtml, &start, &end, imhtml->wbfo);
4752 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4753 find_font_size_tag(imhtml, imhtml->edit.fontsize), &start, &end);
4756 void gtk_imhtml_font_shrink(GtkIMHtml *imhtml)
4758 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_SHRINK);
4761 static void imhtml_font_grow(GtkIMHtml *imhtml)
4763 GtkTextIter start, end;
4765 if (imhtml->edit.fontsize == MAX_FONT_SIZE)
4766 return;
4768 if (!imhtml->edit.fontsize)
4769 imhtml->edit.fontsize = 4;
4770 else
4771 imhtml->edit.fontsize++;
4773 if (!imhtml_get_iter_bounds(imhtml, &start, &end))
4774 return;
4775 remove_font_size(imhtml, &start, &end, imhtml->wbfo);
4776 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4777 find_font_size_tag(imhtml, imhtml->edit.fontsize), &start, &end);
4780 void gtk_imhtml_font_grow(GtkIMHtml *imhtml)
4782 imhtml_emit_signal_for_format(imhtml, GTK_IMHTML_GROW);
4785 static gboolean gtk_imhtml_toggle_str_tag(GtkIMHtml *imhtml, const char *value, char **edit_field,
4786 void (*remove_func)(GtkIMHtml *imhtml, GtkTextIter *i, GtkTextIter *e, gboolean homo),
4787 GtkTextTag *(find_func)(GtkIMHtml *imhtml, gchar *color), GtkIMHtmlButtons button)
4789 GObject *object;
4790 GtkTextIter start;
4791 GtkTextIter end;
4793 g_free(*edit_field);
4794 *edit_field = NULL;
4796 if (value && *value)
4798 *edit_field = g_strdup(value);
4800 if (imhtml_get_iter_bounds(imhtml, &start, &end)) {
4801 remove_func(imhtml, &start, &end, imhtml->wbfo);
4802 gtk_text_buffer_apply_tag(imhtml->text_buffer,
4803 find_func(imhtml, *edit_field), &start, &end);
4806 else
4808 if (imhtml_get_iter_bounds(imhtml, &start, &end))
4809 remove_func(imhtml, &start, &end, TRUE); /* 'TRUE' or 'imhtml->wbfo'? */
4812 object = g_object_ref(G_OBJECT(imhtml));
4813 g_signal_emit(object, signals[TOGGLE_FORMAT], 0, button);
4814 g_object_unref(object);
4816 return *edit_field != NULL;
4819 gboolean gtk_imhtml_toggle_forecolor(GtkIMHtml *imhtml, const char *color)
4821 return gtk_imhtml_toggle_str_tag(imhtml, color, &imhtml->edit.forecolor,
4822 remove_font_forecolor, find_font_forecolor_tag,
4823 GTK_IMHTML_FORECOLOR);
4826 gboolean gtk_imhtml_toggle_backcolor(GtkIMHtml *imhtml, const char *color)
4828 return gtk_imhtml_toggle_str_tag(imhtml, color, &imhtml->edit.backcolor,
4829 remove_font_backcolor, find_font_backcolor_tag,
4830 GTK_IMHTML_BACKCOLOR);
4833 gboolean gtk_imhtml_toggle_background(GtkIMHtml *imhtml, const char *color)
4835 return gtk_imhtml_toggle_str_tag(imhtml, color, &imhtml->edit.background,
4836 remove_font_background, find_font_background_tag,
4837 GTK_IMHTML_BACKGROUND);
4840 gboolean gtk_imhtml_toggle_fontface(GtkIMHtml *imhtml, const char *face)
4842 return gtk_imhtml_toggle_str_tag(imhtml, face, &imhtml->edit.fontface,
4843 remove_font_face, find_font_face_tag,
4844 GTK_IMHTML_FACE);
4847 void gtk_imhtml_toggle_link(GtkIMHtml *imhtml, const char *url)
4849 GObject *object;
4850 GtkTextIter start, end;
4851 GtkTextTag *linktag;
4852 static guint linkno = 0;
4853 gchar str[48];
4854 GdkColor *color = NULL;
4856 imhtml->edit.link = NULL;
4858 if (url) {
4859 g_snprintf(str, sizeof(str), "LINK %d", linkno++);
4860 str[47] = '\0';
4862 gtk_widget_style_get(GTK_WIDGET(imhtml), "hyperlink-color", &color, NULL);
4863 if (color) {
4864 imhtml->edit.link = linktag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "foreground-gdk", color, "underline", PANGO_UNDERLINE_SINGLE, NULL);
4865 gdk_color_free(color);
4866 } else {
4867 imhtml->edit.link = linktag = gtk_text_buffer_create_tag(imhtml->text_buffer, str, "foreground", "blue", "underline", PANGO_UNDERLINE_SINGLE, NULL);
4869 g_object_set_data_full(G_OBJECT(linktag), "link_url", g_strdup(url), g_free);
4870 g_signal_connect(G_OBJECT(linktag), "event", G_CALLBACK(tag_event), NULL);
4872 if (imhtml->editable && gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) {
4873 remove_font_link(imhtml, &start, &end, FALSE);
4874 gtk_text_buffer_apply_tag(imhtml->text_buffer, linktag, &start, &end);
4878 object = g_object_ref(G_OBJECT(imhtml));
4879 g_signal_emit(object, signals[TOGGLE_FORMAT], 0, GTK_IMHTML_LINK);
4880 g_object_unref(object);
4883 void gtk_imhtml_insert_link(GtkIMHtml *imhtml, GtkTextMark *mark, const char *url, const char *text)
4885 GtkTextIter iter;
4887 /* Delete any currently selected text */
4888 gtk_text_buffer_delete_selection(imhtml->text_buffer, TRUE, TRUE);
4890 gtk_imhtml_toggle_link(imhtml, url);
4891 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, mark);
4892 gtk_text_buffer_insert(imhtml->text_buffer, &iter, text, -1);
4893 gtk_imhtml_toggle_link(imhtml, NULL);
4896 void gtk_imhtml_insert_smiley(GtkIMHtml *imhtml, const char *sml, char *smiley)
4898 GtkTextMark *mark;
4899 GtkTextIter iter;
4901 /* Delete any currently selected text */
4902 gtk_text_buffer_delete_selection(imhtml->text_buffer, TRUE, TRUE);
4904 mark = gtk_text_buffer_get_insert(imhtml->text_buffer);
4906 gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &iter, mark);
4907 gtk_text_buffer_begin_user_action(imhtml->text_buffer);
4908 gtk_imhtml_insert_smiley_at_iter(imhtml, sml, smiley, &iter);
4909 gtk_text_buffer_end_user_action(imhtml->text_buffer);
4912 static gboolean
4913 image_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data)
4915 GTK_WIDGET_CLASS(GTK_WIDGET_GET_CLASS(widget))->expose_event(widget, event);
4917 return TRUE;
4920 /* In case the smiley gets removed from the imhtml before it gets removed from the queue */
4921 static void animated_smiley_destroy_cb(GtkObject *widget, GtkIMHtml *imhtml)
4923 GList *l = imhtml->animations->head;
4924 while (l) {
4925 GList *next = l->next;
4926 if (l->data == widget) {
4927 if (l == imhtml->animations->tail)
4928 imhtml->animations->tail = imhtml->animations->tail->prev;
4929 imhtml->animations->head = g_list_delete_link(imhtml->animations->head, l);
4930 imhtml->num_animations--;
4932 l = next;
4936 void gtk_imhtml_insert_smiley_at_iter(GtkIMHtml *imhtml, const char *sml, char *smiley, GtkTextIter *iter)
4938 GdkPixbuf *pixbuf = NULL;
4939 GdkPixbufAnimation *annipixbuf = NULL;
4940 GtkWidget *icon = NULL;
4941 GtkTextChildAnchor *anchor = NULL;
4942 char *unescaped;
4943 GtkIMHtmlSmiley *imhtml_smiley;
4944 GtkWidget *ebox = NULL;
4945 int numsmileys_thismsg, numsmileys_total;
4948 * This GtkIMHtml has the maximum number of smileys allowed, so don't
4949 * add any more. We do this for performance reasons, because smileys
4950 * are apparently pretty inefficient. Hopefully we can remove this
4951 * restriction when we're using a better HTML widget.
4953 unescaped = purple_unescape_html(smiley);
4954 numsmileys_thismsg = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_thismsg"));
4955 if (numsmileys_thismsg >= 30) {
4956 gtk_text_buffer_insert(imhtml->text_buffer, iter, unescaped, -1);
4957 g_free(unescaped);
4958 return;
4960 numsmileys_total = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_total"));
4961 if (numsmileys_total >= 300) {
4962 gtk_text_buffer_insert(imhtml->text_buffer, iter, unescaped, -1);
4963 g_free(unescaped);
4964 return;
4967 imhtml_smiley = gtk_imhtml_smiley_get(imhtml, sml, unescaped);
4969 if (imhtml->format_functions & GTK_IMHTML_SMILEY) {
4970 annipixbuf = imhtml_smiley ? gtk_smiley_get_image(imhtml_smiley) : NULL;
4971 if (annipixbuf) {
4972 if (gdk_pixbuf_animation_is_static_image(annipixbuf)) {
4973 pixbuf = gdk_pixbuf_animation_get_static_image(annipixbuf);
4974 if (pixbuf)
4975 icon = gtk_image_new_from_pixbuf(pixbuf);
4976 } else {
4977 icon = gtk_image_new_from_animation(annipixbuf);
4978 if (imhtml->num_animations == 20) {
4979 GtkImage *image = GTK_IMAGE(g_queue_pop_head(imhtml->animations));
4980 GdkPixbufAnimation *anim = gtk_image_get_animation(image);
4981 g_signal_handlers_disconnect_matched(G_OBJECT(image), G_SIGNAL_MATCH_FUNC,
4982 0, 0, NULL, G_CALLBACK(animated_smiley_destroy_cb), NULL);
4983 if (anim) {
4984 GdkPixbuf *pb = gdk_pixbuf_animation_get_static_image(anim);
4985 if (pb != NULL) {
4986 GdkPixbuf *copy = gdk_pixbuf_copy(pb);
4987 gtk_image_set_from_pixbuf(image, copy);
4988 g_object_unref(G_OBJECT(copy));
4991 } else {
4992 imhtml->num_animations++;
4994 g_signal_connect(G_OBJECT(icon), "destroy", G_CALLBACK(animated_smiley_destroy_cb), imhtml);
4995 g_queue_push_tail(imhtml->animations, icon);
5000 if (imhtml_smiley && imhtml_smiley->flags & GTK_IMHTML_SMILEY_CUSTOM) {
5001 ebox = gtk_event_box_new();
5002 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
5003 gtk_widget_show(ebox);
5006 if (icon) {
5007 anchor = gtk_text_buffer_create_child_anchor(imhtml->text_buffer, iter);
5008 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_plaintext", g_strdup(unescaped), g_free);
5009 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_tiptext", g_strdup(unescaped), g_free);
5010 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_htmltext", g_strdup(smiley), g_free);
5012 /* This catches the expose events generated by animated
5013 * images, and ensures that they are handled by the image
5014 * itself, without propagating to the textview and causing
5015 * a complete refresh */
5016 g_signal_connect(G_OBJECT(icon), "expose-event", G_CALLBACK(image_expose), NULL);
5018 gtk_widget_show(icon);
5019 if (ebox)
5020 gtk_container_add(GTK_CONTAINER(ebox), icon);
5021 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(imhtml), ebox ? ebox : icon, anchor);
5023 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_thismsg", GINT_TO_POINTER(numsmileys_thismsg + 1));
5024 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_total", GINT_TO_POINTER(numsmileys_total + 1));
5025 } else if (imhtml_smiley != NULL && (imhtml->format_functions & GTK_IMHTML_SMILEY)) {
5026 anchor = gtk_text_buffer_create_child_anchor(imhtml->text_buffer, iter);
5027 imhtml_smiley->anchors = g_slist_append(imhtml_smiley->anchors, g_object_ref(anchor));
5028 if (ebox) {
5029 GtkWidget *img = gtk_image_new_from_stock(GTK_STOCK_MISSING_IMAGE, GTK_ICON_SIZE_MENU);
5030 gtk_container_add(GTK_CONTAINER(ebox), img);
5031 gtk_widget_show(img);
5032 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_plaintext", g_strdup(unescaped), g_free);
5033 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_tiptext", g_strdup(unescaped), g_free);
5034 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_htmltext", g_strdup(smiley), g_free);
5035 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(imhtml), ebox, anchor);
5038 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_thismsg", GINT_TO_POINTER(numsmileys_thismsg + 1));
5039 g_object_set_data(G_OBJECT(imhtml), "gtkimhtml_numsmileys_total", GINT_TO_POINTER(numsmileys_total + 1));
5040 } else {
5041 gtk_text_buffer_insert(imhtml->text_buffer, iter, unescaped, -1);
5044 if (ebox) {
5045 g_signal_connect(G_OBJECT(ebox), "event", G_CALLBACK(gtk_imhtml_smiley_clicked), imhtml_smiley);
5048 g_free(unescaped);
5051 void gtk_imhtml_insert_image_at_iter(GtkIMHtml *imhtml, int id, GtkTextIter *iter)
5053 GdkPixbufAnimation *anim = NULL;
5054 const char *filename = NULL;
5055 gpointer image;
5056 GdkRectangle rect;
5057 GtkIMHtmlScalable *scalable = NULL;
5058 struct scalable_data *sd;
5059 int minus;
5061 if (!imhtml->funcs || !imhtml->funcs->image_get ||
5062 !imhtml->funcs->image_get_size || !imhtml->funcs->image_get_data ||
5063 !imhtml->funcs->image_get_filename || !imhtml->funcs->image_ref ||
5064 !imhtml->funcs->image_unref)
5065 return;
5067 image = imhtml->funcs->image_get(id);
5069 if (image) {
5070 gpointer data;
5071 size_t len;
5073 data = imhtml->funcs->image_get_data(image);
5074 len = imhtml->funcs->image_get_size(image);
5075 if (data && len)
5076 anim = pidgin_pixbuf_anim_from_data(data, len);
5080 if (anim) {
5081 struct im_image_data *t = g_new(struct im_image_data, 1);
5082 filename = imhtml->funcs->image_get_filename(image);
5083 imhtml->funcs->image_ref(id);
5084 t->id = id;
5085 t->mark = gtk_text_buffer_create_mark(imhtml->text_buffer, NULL, iter, TRUE);
5086 imhtml->im_images = g_slist_prepend(imhtml->im_images, t);
5087 scalable = gtk_imhtml_animation_new(anim, filename, id);
5088 g_object_unref(G_OBJECT(anim));
5089 } else {
5090 GdkPixbuf *pixbuf;
5091 pixbuf = gtk_widget_render_icon(GTK_WIDGET(imhtml), GTK_STOCK_MISSING_IMAGE,
5092 GTK_ICON_SIZE_BUTTON, "gtkimhtml-missing-image");
5093 scalable = gtk_imhtml_image_new(pixbuf, filename, id);
5094 g_object_unref(G_OBJECT(pixbuf));
5097 sd = g_new(struct scalable_data, 1);
5098 sd->scalable = scalable;
5099 sd->mark = gtk_text_buffer_create_mark(imhtml->text_buffer, NULL, iter, TRUE);
5100 gtk_text_view_get_visible_rect(GTK_TEXT_VIEW(imhtml), &rect);
5101 scalable->add_to(scalable, imhtml, iter);
5102 minus = gtk_text_view_get_left_margin(GTK_TEXT_VIEW(imhtml)) +
5103 gtk_text_view_get_right_margin(GTK_TEXT_VIEW(imhtml));
5104 scalable->scale(scalable, rect.width - minus, rect.height);
5105 imhtml->scalables = g_list_append(imhtml->scalables, sd);
5108 static const gchar *tag_to_html_start(GtkTextTag *tag)
5110 const gchar *name;
5111 static gchar buf[16384];
5113 name = tag->name;
5114 g_return_val_if_fail(name != NULL, "");
5116 if (purple_strequal(name, "BOLD")) {
5117 return "<b>";
5118 } else if (purple_strequal(name, "ITALICS")) {
5119 return "<i>";
5120 } else if (purple_strequal(name, "UNDERLINE")) {
5121 return "<u>";
5122 } else if (purple_strequal(name, "STRIKE")) {
5123 return "<s>";
5124 } else if (strncmp(name, "LINK ", 5) == 0) {
5125 char *tmp = g_object_get_data(G_OBJECT(tag), "link_url");
5126 if (tmp) {
5127 gchar *escaped = purple_markup_escape_text(tmp, -1);
5128 g_snprintf(buf, sizeof(buf), "<a href=\"%s\">", escaped);
5129 buf[sizeof(buf)-1] = '\0';
5130 g_free(escaped);
5131 return buf;
5132 } else {
5133 return "";
5135 } else if (strncmp(name, "FORECOLOR ", 10) == 0) {
5136 g_snprintf(buf, sizeof(buf), "<font color=\"%s\">", &name[10]);
5137 return buf;
5138 } else if (strncmp(name, "BACKCOLOR ", 10) == 0) {
5139 g_snprintf(buf, sizeof(buf), "<font back=\"%s\">", &name[10]);
5140 return buf;
5141 } else if (strncmp(name, "BACKGROUND ", 10) == 0) {
5142 g_snprintf(buf, sizeof(buf), "<body bgcolor=\"%s\">", &name[11]);
5143 return buf;
5144 } else if (strncmp(name, "FONT FACE ", 10) == 0) {
5145 g_snprintf(buf, sizeof(buf), "<font face=\"%s\">", &name[10]);
5146 return buf;
5147 } else if (strncmp(name, "FONT SIZE ", 10) == 0) {
5148 g_snprintf(buf, sizeof(buf), "<font size=\"%s\">", &name[10]);
5149 return buf;
5150 } else {
5151 char *str = buf;
5152 gboolean isset;
5153 int ivalue = 0;
5154 GdkColor *color = NULL;
5155 GObject *obj = G_OBJECT(tag);
5156 gboolean empty = TRUE;
5158 str += g_snprintf(str, sizeof(buf) - (str - buf), "<span style='");
5160 /* Weight */
5161 g_object_get(obj, "weight-set", &isset, "weight", &ivalue, NULL);
5162 if (isset) {
5163 const char *weight = "";
5164 if (ivalue >= PANGO_WEIGHT_ULTRABOLD)
5165 weight = "bolder";
5166 else if (ivalue >= PANGO_WEIGHT_BOLD)
5167 weight = "bold";
5168 else if (ivalue >= PANGO_WEIGHT_NORMAL)
5169 weight = "normal";
5170 else
5171 weight = "lighter";
5173 str += g_snprintf(str, sizeof(buf) - (str - buf), "font-weight: %s;", weight);
5174 empty = FALSE;
5177 /* Foreground color */
5178 g_object_get(obj, "foreground-set", &isset, "foreground-gdk", &color, NULL);
5179 if (isset && color) {
5180 str += g_snprintf(str, sizeof(buf) - (str - buf),
5181 "color: #%02x%02x%02x;",
5182 color->red >> 8, color->green >> 8, color->blue >> 8);
5183 empty = FALSE;
5185 gdk_color_free(color);
5187 /* Background color */
5188 g_object_get(obj, "background-set", &isset, "background-gdk", &color, NULL);
5189 if (isset && color) {
5190 str += g_snprintf(str, sizeof(buf) - (str - buf),
5191 "background: #%02x%02x%02x;",
5192 color->red >> 8, color->green >> 8, color->blue >> 8);
5193 empty = FALSE;
5195 gdk_color_free(color);
5197 /* Underline */
5198 g_object_get(obj, "underline-set", &isset, "underline", &ivalue, NULL);
5199 if (isset) {
5200 switch (ivalue) {
5201 case PANGO_UNDERLINE_NONE:
5202 case PANGO_UNDERLINE_ERROR:
5203 break;
5204 default:
5205 str += g_snprintf(str, sizeof(buf) - (str - buf), "text-decoration: underline;");
5206 empty = FALSE;
5210 g_snprintf(str, sizeof(buf) - (str - buf), "'>");
5212 return (empty ? "" : buf);
5216 static const gchar *tag_to_html_end(GtkTextTag *tag)
5218 const gchar *name;
5220 name = tag->name;
5221 g_return_val_if_fail(name != NULL, "");
5223 if (purple_strequal(name, "BOLD")) {
5224 return "</b>";
5225 } else if (purple_strequal(name, "ITALICS")) {
5226 return "</i>";
5227 } else if (purple_strequal(name, "UNDERLINE")) {
5228 return "</u>";
5229 } else if (purple_strequal(name, "STRIKE")) {
5230 return "</s>";
5231 } else if (strncmp(name, "LINK ", 5) == 0) {
5232 return "</a>";
5233 } else if (strncmp(name, "FORECOLOR ", 10) == 0) {
5234 return "</font>";
5235 } else if (strncmp(name, "BACKCOLOR ", 10) == 0) {
5236 return "</font>";
5237 } else if (strncmp(name, "BACKGROUND ", 10) == 0) {
5238 return "</body>";
5239 } else if (strncmp(name, "FONT FACE ", 10) == 0) {
5240 return "</font>";
5241 } else if (strncmp(name, "FONT SIZE ", 10) == 0) {
5242 return "</font>";
5243 } else {
5244 const char *props[] = {"weight-set", "foreground-set", "background-set",
5245 "size-set", "underline-set", NULL};
5246 int i;
5247 for (i = 0; props[i]; i++) {
5248 gboolean set = FALSE;
5249 g_object_get(G_OBJECT(tag), props[i], &set, NULL);
5250 if (set)
5251 return "</span>";
5254 return "";
5258 typedef struct {
5259 GtkTextTag *tag;
5260 char *end;
5261 char *start;
5262 } PidginTextTagData;
5264 static PidginTextTagData *text_tag_data_new(GtkTextTag *tag)
5266 const char *start, *end;
5267 PidginTextTagData *ret = NULL;
5269 start = tag_to_html_start(tag);
5270 if (!start || !*start)
5271 return NULL;
5272 end = tag_to_html_end(tag);
5273 if (!end || !*end)
5274 return NULL;
5276 ret = g_new0(PidginTextTagData, 1);
5277 ret->start = g_strdup(start);
5278 ret->end = g_strdup(end);
5279 ret->tag = tag;
5280 return ret;
5283 static void text_tag_data_destroy(PidginTextTagData *data)
5285 g_free(data->start);
5286 g_free(data->end);
5287 g_free(data);
5290 static gboolean tag_ends_here(GtkTextTag *tag, GtkTextIter *iter, GtkTextIter *niter)
5292 return ((gtk_text_iter_has_tag(iter, GTK_TEXT_TAG(tag)) &&
5293 !gtk_text_iter_has_tag(niter, GTK_TEXT_TAG(tag))) ||
5294 gtk_text_iter_is_end(niter));
5297 /* Basic notion here: traverse through the text buffer one-by-one, non-character elements, such
5298 * as smileys and IM images are represented by the Unicode "unknown" character. Handle them. Else
5299 * check for tags that are toggled on, insert their html form, and push them on the queue. Then insert
5300 * the actual text. Then check for tags that are toggled off and insert them, after checking the queue.
5301 * Finally, replace <, >, &, and " with their HTML equivalent.
5303 char *gtk_imhtml_get_markup_range(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *end)
5305 gunichar c;
5306 GtkTextIter iter, next_iter, non_neutral_iter;
5307 gboolean is_rtl_message = FALSE;
5308 GString *str = g_string_new("");
5309 GSList *tags, *sl;
5310 GQueue *q;
5311 GtkTextTag *tag;
5312 PidginTextTagData *tagdata;
5314 q = g_queue_new();
5316 gtk_text_iter_order(start, end);
5317 non_neutral_iter = next_iter = iter = *start;
5318 gtk_text_iter_forward_char(&next_iter);
5320 /* Bi-directional text support */
5321 /* Get to the first non-neutral character */
5322 #ifdef HAVE_PANGO14
5323 while ((PANGO_DIRECTION_NEUTRAL == pango_unichar_direction(gtk_text_iter_get_char(&non_neutral_iter)))
5324 && gtk_text_iter_forward_char(&non_neutral_iter));
5325 if (PANGO_DIRECTION_RTL == pango_unichar_direction(gtk_text_iter_get_char(&non_neutral_iter))) {
5326 is_rtl_message = TRUE;
5327 g_string_append(str, "<SPAN style=\"direction:rtl;text-align:right;\">");
5329 #endif
5331 /* First add the tags that are already in progress (we don't care about non-printing tags)*/
5332 tags = gtk_text_iter_get_tags(start);
5334 for (sl = tags; sl; sl = sl->next) {
5335 tag = sl->data;
5336 if (!gtk_text_iter_toggles_tag(start, GTK_TEXT_TAG(tag))) {
5337 PidginTextTagData *data = text_tag_data_new(tag);
5338 if (data) {
5339 g_string_append(str, data->start);
5340 g_queue_push_tail(q, data);
5344 g_slist_free(tags);
5346 while ((c = gtk_text_iter_get_char(&iter)) != 0 && !gtk_text_iter_equal(&iter, end)) {
5348 tags = gtk_text_iter_get_tags(&iter);
5350 for (sl = tags; sl; sl = sl->next) {
5351 tag = sl->data;
5352 if (gtk_text_iter_begins_tag(&iter, GTK_TEXT_TAG(tag))) {
5353 PidginTextTagData *data = text_tag_data_new(tag);
5354 if (data) {
5355 g_string_append(str, data->start);
5356 g_queue_push_tail(q, data);
5361 if (c == 0xFFFC) {
5362 GtkTextChildAnchor* anchor = gtk_text_iter_get_child_anchor(&iter);
5363 if (anchor) {
5364 char *text = g_object_get_data(G_OBJECT(anchor), "gtkimhtml_htmltext");
5365 if (text)
5366 str = g_string_append(str, text);
5368 } else if (c == '<') {
5369 str = g_string_append(str, "&lt;");
5370 } else if (c == '>') {
5371 str = g_string_append(str, "&gt;");
5372 } else if (c == '&') {
5373 str = g_string_append(str, "&amp;");
5374 } else if (c == '"') {
5375 str = g_string_append(str, "&quot;");
5376 } else if (c == '\n') {
5377 str = g_string_append(str, "<br>");
5378 } else {
5379 str = g_string_append_unichar(str, c);
5382 tags = g_slist_reverse(tags);
5383 for (sl = tags; sl; sl = sl->next) {
5384 tag = sl->data;
5385 /** don't worry about non-printing tags ending */
5386 if (tag_ends_here(tag, &iter, &next_iter) &&
5387 strlen(tag_to_html_end(tag)) > 0 &&
5388 strlen(tag_to_html_start(tag)) > 0) {
5390 PidginTextTagData *tmp;
5391 GQueue *r = g_queue_new();
5393 while ((tmp = g_queue_pop_tail(q)) && tmp->tag != tag) {
5394 g_string_append(str, tmp->end);
5395 if (!tag_ends_here(tmp->tag, &iter, &next_iter))
5396 g_queue_push_tail(r, tmp);
5397 else
5398 text_tag_data_destroy(tmp);
5401 if (tmp != NULL) {
5402 g_string_append(str, tmp->end);
5403 text_tag_data_destroy(tmp);
5405 #if 0 /* This can't be allowed to happen because it causes the iters to be invalidated in the debug window imhtml during text copying */
5406 else
5407 purple_debug_warning("gtkimhtml", "empty queue, more closing tags than open tags!\n");
5408 #endif
5410 while ((tmp = g_queue_pop_head(r))) {
5411 g_string_append(str, tmp->start);
5412 g_queue_push_tail(q, tmp);
5414 g_queue_free(r);
5418 g_slist_free(tags);
5419 gtk_text_iter_forward_char(&iter);
5420 gtk_text_iter_forward_char(&next_iter);
5423 while ((tagdata = g_queue_pop_tail(q))) {
5424 g_string_append(str, tagdata->end);
5425 text_tag_data_destroy(tagdata);
5428 /* Bi-directional text support - close tags */
5429 if (is_rtl_message)
5430 g_string_append(str, "</SPAN>");
5432 g_queue_free(q);
5433 return g_string_free(str, FALSE);
5436 void gtk_imhtml_close_tags(GtkIMHtml *imhtml, GtkTextIter *iter)
5438 if (imhtml->edit.bold)
5439 gtk_imhtml_toggle_bold(imhtml);
5441 if (imhtml->edit.italic)
5442 gtk_imhtml_toggle_italic(imhtml);
5444 if (imhtml->edit.underline)
5445 gtk_imhtml_toggle_underline(imhtml);
5447 if (imhtml->edit.strike)
5448 gtk_imhtml_toggle_strike(imhtml);
5450 if (imhtml->edit.forecolor)
5451 gtk_imhtml_toggle_forecolor(imhtml, NULL);
5453 if (imhtml->edit.backcolor)
5454 gtk_imhtml_toggle_backcolor(imhtml, NULL);
5456 if (imhtml->edit.fontface)
5457 gtk_imhtml_toggle_fontface(imhtml, NULL);
5459 imhtml->edit.fontsize = 0;
5461 if (imhtml->edit.link)
5462 gtk_imhtml_toggle_link(imhtml, NULL);
5465 char *gtk_imhtml_get_markup(GtkIMHtml *imhtml)
5467 GtkTextIter start, end;
5469 gtk_text_buffer_get_start_iter(imhtml->text_buffer, &start);
5470 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &end);
5471 return gtk_imhtml_get_markup_range(imhtml, &start, &end);
5474 char **gtk_imhtml_get_markup_lines(GtkIMHtml *imhtml)
5476 int i, j, lines;
5477 GtkTextIter start, end;
5478 char **ret;
5480 lines = gtk_text_buffer_get_line_count(imhtml->text_buffer);
5481 ret = g_new0(char *, lines + 1);
5482 gtk_text_buffer_get_start_iter(imhtml->text_buffer, &start);
5483 end = start;
5484 gtk_text_iter_forward_to_line_end(&end);
5486 for (i = 0, j = 0; i < lines; i++) {
5487 if (gtk_text_iter_get_char(&start) != '\n') {
5488 ret[j] = gtk_imhtml_get_markup_range(imhtml, &start, &end);
5489 if (ret[j] != NULL)
5490 j++;
5493 gtk_text_iter_forward_line(&start);
5494 end = start;
5495 gtk_text_iter_forward_to_line_end(&end);
5498 return ret;
5501 char *gtk_imhtml_get_text(GtkIMHtml *imhtml, GtkTextIter *start, GtkTextIter *stop)
5503 GString *str = g_string_new("");
5504 GtkTextIter iter, end;
5505 gunichar c;
5507 if (start == NULL)
5508 gtk_text_buffer_get_start_iter(imhtml->text_buffer, &iter);
5509 else
5510 iter = *start;
5512 if (stop == NULL)
5513 gtk_text_buffer_get_end_iter(imhtml->text_buffer, &end);
5514 else
5515 end = *stop;
5517 gtk_text_iter_order(&iter, &end);
5519 while ((c = gtk_text_iter_get_char(&iter)) != 0 && !gtk_text_iter_equal(&iter, &end)) {
5520 if (c == 0xFFFC) {
5521 GtkTextChildAnchor* anchor;
5522 char *text = NULL;
5524 anchor = gtk_text_iter_get_child_anchor(&iter);
5525 if (anchor)
5526 text = g_object_get_data(G_OBJECT(anchor), "gtkimhtml_plaintext");
5527 if (text)
5528 str = g_string_append(str, text);
5529 } else {
5530 g_string_append_unichar(str, c);
5532 gtk_text_iter_forward_char(&iter);
5535 return g_string_free(str, FALSE);
5538 void gtk_imhtml_set_funcs(GtkIMHtml *imhtml, GtkIMHtmlFuncs *f)
5540 g_return_if_fail(imhtml != NULL);
5541 imhtml->funcs = f;
5544 void gtk_imhtml_setup_entry(GtkIMHtml *imhtml, PurpleConnectionFlags flags)
5546 GtkIMHtmlButtons buttons;
5548 if (flags & PURPLE_CONNECTION_HTML) {
5549 char color[8];
5550 GdkColor fg_color, bg_color;
5552 buttons = GTK_IMHTML_ALL;
5554 if (flags & PURPLE_CONNECTION_NO_BGCOLOR)
5555 buttons &= ~GTK_IMHTML_BACKCOLOR;
5556 if (flags & PURPLE_CONNECTION_NO_FONTSIZE)
5558 buttons &= ~GTK_IMHTML_GROW;
5559 buttons &= ~GTK_IMHTML_SHRINK;
5561 if (flags & PURPLE_CONNECTION_NO_URLDESC)
5562 buttons &= ~GTK_IMHTML_LINKDESC;
5564 gtk_imhtml_set_format_functions(imhtml, GTK_IMHTML_ALL);
5565 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold") != imhtml->edit.bold)
5566 gtk_imhtml_toggle_bold(imhtml);
5568 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic") != imhtml->edit.italic)
5569 gtk_imhtml_toggle_italic(imhtml);
5571 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline") != imhtml->edit.underline)
5572 gtk_imhtml_toggle_underline(imhtml);
5574 gtk_imhtml_toggle_fontface(imhtml,
5575 purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/font_face"));
5577 if (!(flags & PURPLE_CONNECTION_NO_FONTSIZE))
5579 int size = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/font_size");
5581 /* 3 is the default. */
5582 if (size != 3)
5583 gtk_imhtml_font_set_size(imhtml, size);
5586 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor"), ""))
5588 gdk_color_parse(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor"),
5589 &fg_color);
5590 g_snprintf(color, sizeof(color), "#%02x%02x%02x",
5591 fg_color.red / 256,
5592 fg_color.green / 256,
5593 fg_color.blue / 256);
5594 } else
5595 strcpy(color, "");
5597 gtk_imhtml_toggle_forecolor(imhtml, color);
5599 if(!(flags & PURPLE_CONNECTION_NO_BGCOLOR) &&
5600 !purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor"), ""))
5602 gdk_color_parse(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor"),
5603 &bg_color);
5604 g_snprintf(color, sizeof(color), "#%02x%02x%02x",
5605 bg_color.red / 256,
5606 bg_color.green / 256,
5607 bg_color.blue / 256);
5608 } else
5609 strcpy(color, "");
5611 gtk_imhtml_toggle_background(imhtml, color);
5613 if (flags & PURPLE_CONNECTION_FORMATTING_WBFO)
5614 gtk_imhtml_set_whole_buffer_formatting_only(imhtml, TRUE);
5615 else
5616 gtk_imhtml_set_whole_buffer_formatting_only(imhtml, FALSE);
5617 } else {
5618 buttons = GTK_IMHTML_SMILEY | GTK_IMHTML_IMAGE;
5619 imhtml_clear_formatting(imhtml);
5622 if (flags & PURPLE_CONNECTION_NO_IMAGES)
5623 buttons &= ~GTK_IMHTML_IMAGE;
5625 if (flags & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY)
5626 buttons |= GTK_IMHTML_CUSTOM_SMILEY;
5627 else
5628 buttons &= ~GTK_IMHTML_CUSTOM_SMILEY;
5630 gtk_imhtml_set_format_functions(imhtml, buttons);
5633 /*******
5634 * GtkIMHtmlSmiley functions
5635 *******/
5636 static void gtk_custom_smiley_allocated(GdkPixbufLoader *loader, gpointer user_data)
5638 GtkIMHtmlSmiley *smiley;
5640 smiley = (GtkIMHtmlSmiley *)user_data;
5641 smiley->icon = gdk_pixbuf_loader_get_animation(loader);
5643 if (smiley->icon)
5644 g_object_ref(G_OBJECT(smiley->icon));
5645 #ifdef DEBUG_CUSTOM_SMILEY
5646 purple_debug_info("custom-smiley", "gtk_custom_smiley_allocated(): got GdkPixbufAnimation %p for smiley '%s'\n", smiley->icon, smiley->smile);
5647 #endif
5650 static void gtk_custom_smiley_closed(GdkPixbufLoader *loader, gpointer user_data)
5652 GtkIMHtmlSmiley *smiley;
5653 GtkWidget *icon = NULL;
5654 GtkTextChildAnchor *anchor = NULL;
5655 GSList *current = NULL;
5657 smiley = (GtkIMHtmlSmiley *)user_data;
5658 if (!smiley->imhtml) {
5659 #ifdef DEBUG_CUSTOM_SMILEY
5660 purple_debug_error("custom-smiley", "gtk_custom_smiley_closed(): orphan smiley found: %p\n", smiley);
5661 #endif
5662 g_object_unref(G_OBJECT(loader));
5663 smiley->loader = NULL;
5664 return;
5667 for (current = smiley->anchors; current; current = g_slist_next(current)) {
5668 anchor = GTK_TEXT_CHILD_ANCHOR(current->data);
5669 if (gtk_text_child_anchor_get_deleted(anchor))
5670 icon = NULL;
5671 else
5672 icon = gtk_image_new_from_animation(smiley->icon);
5674 #ifdef DEBUG_CUSTOM_SMILEY
5675 purple_debug_info("custom-smiley", "gtk_custom_smiley_closed(): got GtkImage %p from GtkPixbufAnimation %p for smiley '%s'\n",
5676 icon, smiley->icon, smiley->smile);
5677 #endif
5678 if (icon) {
5679 GList *wids;
5680 gtk_widget_show(icon);
5682 wids = gtk_text_child_anchor_get_widgets(anchor);
5684 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_plaintext", purple_unescape_html(smiley->smile), g_free);
5685 g_object_set_data_full(G_OBJECT(anchor), "gtkimhtml_htmltext", g_strdup(smiley->smile), g_free);
5687 if (smiley->imhtml) {
5688 if (wids) {
5689 GList *children = gtk_container_get_children(GTK_CONTAINER(wids->data));
5690 g_list_foreach(children, (GFunc)gtk_widget_destroy, NULL);
5691 g_list_free(children);
5692 gtk_container_add(GTK_CONTAINER(wids->data), icon);
5693 } else
5694 gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(smiley->imhtml), icon, anchor);
5696 g_list_free(wids);
5698 g_object_unref(anchor);
5701 g_slist_free(smiley->anchors);
5702 smiley->anchors = NULL;
5704 g_object_unref(G_OBJECT(loader));
5705 smiley->loader = NULL;
5708 static void
5709 gtk_custom_smiley_size_prepared(GdkPixbufLoader *loader, gint width, gint height, gpointer data)
5711 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys")) {
5712 int custom_smileys_size = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size");
5713 if (width <= custom_smileys_size && height <= custom_smileys_size)
5714 return;
5716 if (width >= height) {
5717 height = height * custom_smileys_size / width;
5718 width = custom_smileys_size;
5719 } else {
5720 width = width * custom_smileys_size / height;
5721 height = custom_smileys_size;
5724 gdk_pixbuf_loader_set_size(loader, width, height);
5727 void
5728 gtk_imhtml_smiley_reload(GtkIMHtmlSmiley *smiley)
5730 if (smiley->icon)
5731 g_object_unref(smiley->icon);
5732 if (smiley->loader)
5733 g_object_unref(smiley->loader); /* XXX: does this crash? */
5735 smiley->icon = NULL;
5736 smiley->loader = NULL;
5738 if (smiley->file) {
5739 /* We do not use the pixbuf loader for a smiley that can be loaded
5740 * from a file. (e.g., local custom smileys)
5742 return;
5745 smiley->loader = gdk_pixbuf_loader_new();
5747 g_signal_connect(smiley->loader, "area_prepared", G_CALLBACK(gtk_custom_smiley_allocated), smiley);
5748 g_signal_connect(smiley->loader, "closed", G_CALLBACK(gtk_custom_smiley_closed), smiley);
5749 g_signal_connect(smiley->loader, "size_prepared", G_CALLBACK(gtk_custom_smiley_size_prepared), smiley);
5752 GtkIMHtmlSmiley *gtk_imhtml_smiley_create(const char *file, const char *shortcut, gboolean hide,
5753 GtkIMHtmlSmileyFlags flags)
5755 GtkIMHtmlSmiley *smiley = g_new0(GtkIMHtmlSmiley, 1);
5756 smiley->file = g_strdup(file);
5757 smiley->smile = g_strdup(shortcut);
5758 smiley->hidden = hide;
5759 smiley->flags = flags;
5760 smiley->imhtml = NULL;
5761 gtk_imhtml_smiley_reload(smiley);
5762 return smiley;
5765 void gtk_imhtml_smiley_destroy(GtkIMHtmlSmiley *smiley)
5767 gtk_imhtml_disassociate_smiley(smiley);
5768 g_free(smiley->smile);
5769 g_free(smiley->file);
5770 if (smiley->icon)
5771 g_object_unref(smiley->icon);
5772 if (smiley->loader)
5773 g_object_unref(smiley->loader);
5774 g_free(smiley->data);
5775 g_free(smiley);
5778 gboolean gtk_imhtml_class_register_protocol(const char *name,
5779 gboolean (*activate)(GtkIMHtml *imhtml, GtkIMHtmlLink *link),
5780 gboolean (*context_menu)(GtkIMHtml *imhtml, GtkIMHtmlLink *link, GtkWidget *menu))
5782 GtkIMHtmlClass *klass;
5783 GtkIMHtmlProtocol *proto;
5785 g_return_val_if_fail(name, FALSE);
5787 klass = g_type_class_ref(GTK_TYPE_IMHTML);
5788 g_return_val_if_fail(klass, FALSE);
5790 if ((proto = imhtml_find_protocol(name, TRUE))) {
5791 if (activate) {
5792 return FALSE;
5794 klass->protocols = g_list_remove(klass->protocols, proto);
5795 g_free(proto->name);
5796 g_free(proto);
5797 return TRUE;
5798 } else if (!activate) {
5799 return FALSE;
5802 proto = g_new0(GtkIMHtmlProtocol, 1);
5803 proto->name = g_strdup(name);
5804 proto->length = strlen(name);
5805 proto->activate = activate;
5806 proto->context_menu = context_menu;
5807 klass->protocols = g_list_prepend(klass->protocols, proto);
5809 return TRUE;
5812 static void
5813 gtk_imhtml_activate_tag(GtkIMHtml *imhtml, GtkTextTag *tag)
5815 /* A link was clicked--we emit the "url_clicked" signal
5816 * with the URL as the argument */
5817 g_object_ref(G_OBJECT(tag));
5818 g_signal_emit(imhtml, signals[URL_CLICKED], 0, g_object_get_data(G_OBJECT(tag), "link_url"));
5819 g_object_unref(G_OBJECT(tag));
5820 g_object_set_data(G_OBJECT(tag), "visited", GINT_TO_POINTER(TRUE));
5821 gtk_imhtml_set_link_color(GTK_IMHTML(imhtml), tag);
5824 gboolean gtk_imhtml_link_activate(GtkIMHtmlLink *link)
5826 g_return_val_if_fail(link, FALSE);
5828 if (link->tag) {
5829 gtk_imhtml_activate_tag(link->imhtml, link->tag);
5830 } else if (link->url) {
5831 g_signal_emit(link->imhtml, signals[URL_CLICKED], 0, link->url);
5832 } else
5833 return FALSE;
5834 return TRUE;
5837 const char *gtk_imhtml_link_get_url(GtkIMHtmlLink *link)
5839 return link->url;
5842 const GtkTextTag * gtk_imhtml_link_get_text_tag(GtkIMHtmlLink *link)
5844 return link->tag;
5847 static gboolean return_add_newline_cb(GtkWidget *widget, gpointer data)
5849 GtkTextBuffer *buffer;
5850 GtkTextMark *mark;
5851 GtkTextIter iter;
5853 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
5855 /* Delete any currently selected text */
5856 gtk_text_buffer_delete_selection(buffer, TRUE, TRUE);
5858 /* Insert a newline at the current cursor position */
5859 mark = gtk_text_buffer_get_insert(buffer);
5860 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
5861 gtk_imhtml_insert_html_at_iter(GTK_IMHTML(widget), "\n", 0, &iter);
5864 * If we just newlined ourselves past the end of the visible area
5865 * then scroll down so the cursor is in view.
5867 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(widget),
5868 gtk_text_buffer_get_insert(buffer),
5869 0, FALSE, 0.0, 0.0);
5871 return TRUE;
5875 * It's kind of a pain that we need this function and the above just
5876 * to reinstate the default GtkTextView behavior. It might be better
5877 * if GtkIMHtml didn't intercept the enter key and just required the
5878 * application to deal with it--it's really not much more work than it
5879 * is to connect to the current "message_send" signal.
5881 void gtk_imhtml_set_return_inserts_newline(GtkIMHtml *imhtml)
5883 g_signal_connect(G_OBJECT(imhtml), "message_send",
5884 G_CALLBACK(return_add_newline_cb), NULL);
5887 void gtk_imhtml_set_populate_primary_clipboard(GtkIMHtml *imhtml, gboolean populate)
5889 gulong signal_id;
5890 signal_id = g_signal_handler_find(imhtml->text_buffer,
5891 G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_UNBLOCKED, 0, 0, NULL,
5892 mark_set_so_update_selection_cb, NULL);
5893 if (populate) {
5894 if (!signal_id) {
5895 /* We didn't find an unblocked signal handler, which means there
5896 is a blocked handler. Now unblock it.
5897 This is necessary to avoid a mutex-lock when the debug message
5898 saying 'no handler is blocked' is printed in the debug window.
5899 -- sad
5901 g_signal_handlers_unblock_matched(imhtml->text_buffer,
5902 G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
5903 mark_set_so_update_selection_cb, NULL);
5905 } else {
5906 /* Block only if we found an unblocked handler */
5907 if (signal_id)
5908 g_signal_handler_block(imhtml->text_buffer, signal_id);