libyui-gtk  2.49.0
ygtkrichtext.c
1 /********************************************************************
2  * YaST2-GTK - http://en.opensuse.org/YaST2-GTK *
3  ********************************************************************/
4 
5 /* YGtkRichText widget */
6 // check the header file for information about this widget
7 
8 #include <yui/Libyui_config.h>
9 #include "ygtkrichtext.h"
10 #include <gtk/gtk.h>
11 #include <string.h>
12 
13 #define IDENT_MARGIN 20
14 #define PARAGRAPH_SPACING 12
15 
16 // convert liberal html to xhtml, as we use a xhtml parser
17 extern gchar *ygutils_convert_to_xhtml (const char *instr);
18 
19 G_DEFINE_TYPE (YGtkRichText, ygtk_rich_text, YGTK_TYPE_TEXT_VIEW)
20 
21 static guint link_clicked_signal;
22 static GdkColor link_color = { 0, 0, 0, 0xeeee };
23 
24 // utilities
25 // Looks at all tags covering the position of iter in the text view,
26 // and returns the link the text points to, in case that text is a link.
27 static const char *get_link_at_iter (GtkTextView *text_view, GtkTextIter *iter)
28 {
29  char *link = NULL;
30  GSList *tags = gtk_text_iter_get_tags (iter), *tagp;
31  for (tagp = tags; tagp != NULL; tagp = tagp->next) {
32  GtkTextTag *tag = (GtkTextTag*) tagp->data;
33  link = (char*) g_object_get_data (G_OBJECT (tag), "link");
34  if (link)
35  break;
36  }
37 
38  if (tags)
39  g_slist_free (tags);
40  return link;
41 }
42 static const char *get_link (GtkTextView *text_view, gint win_x, gint win_y)
43 {
44  gint buffer_x, buffer_y;
45  gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view),
46  GTK_TEXT_WINDOW_WIDGET, win_x, win_y, &buffer_x, &buffer_y);
47  GtkTextIter iter;
48  gtk_text_view_get_iter_at_location (text_view, &iter, buffer_x, buffer_y);
49  return get_link_at_iter (text_view, &iter);
50 }
51 
52 // callbacks
53 // Links can also be activated by clicking.
54 static gboolean event_after (GtkWidget *text_view, GdkEvent *ev)
55 {
56  if (ev->type != GDK_BUTTON_RELEASE)
57  return FALSE;
58  GtkTextIter start, end;
59  GtkTextBuffer *buffer;
60  GdkEventButton *event = (GdkEventButton *) ev;
61  if (event->button != 1)
62  return FALSE;
63  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
64 
65  // We shouldn't follow a link if the user is selecting something.
66  gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
67  if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end))
68  return FALSE;
69 
70  const char *link = get_link (GTK_TEXT_VIEW (text_view), event->x, event->y);
71  if (link) {
72  if (*link == '#') {
73  GtkTextMark *mark = gtk_text_buffer_get_mark (buffer, link + 1);
74  if (mark)
75  gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (text_view), mark, 0.4, TRUE, 0, 0);
76  } else {
77  // report link
78  g_signal_emit (YGTK_RICH_TEXT (text_view), link_clicked_signal, 0, link);
79  }
80  }
81  return FALSE;
82 }
83 
84 #include <stdlib.h>
85 static int mystrcmp(void *a, void *b)
86 { return g_ascii_strcasecmp (*(char **)a, *(char **)b); }
87 
88 static gboolean isBlockTag (const char *tag)
89 {
90  static const char *Tags[] =
91  { "blockquote", "dd", "dl", "dt", "h1", "h2", "h3", "h4", "h5", "li", "p", "pre" };
92  void *ret;
93  ret = bsearch (&tag, Tags, sizeof (Tags)/sizeof(char*), sizeof(char *), (void*)mystrcmp);
94  return ret != 0;
95 }
96 static gboolean isIdentTag (const char *tag)
97 {
98  static const char *Tags[] =
99  { "blockquote", "dd", "ol", "ul" };
100  void *ret;
101  ret = bsearch (&tag, Tags, sizeof (Tags)/sizeof(char*), sizeof(char *), (void*)mystrcmp);
102  return ret != 0;
103 }
104 
105 void ygtk_rich_text_init (YGtkRichText *rtext)
106 {
107  GtkWidget *widget = GTK_WIDGET (rtext);
108  GtkTextView *tview = GTK_TEXT_VIEW (rtext);
109  gtk_text_view_set_editable (tview, FALSE);
110  gtk_text_view_set_wrap_mode (tview, GTK_WRAP_WORD_CHAR);
111  gtk_text_view_set_pixels_below_lines (tview, 4);
112  gtk_text_view_set_left_margin (tview, 4);
113 
114  // Init link support
115  GdkDisplay *display = gtk_widget_get_display (widget);
116  rtext->hand_cursor = gdk_cursor_new_for_display (display, GDK_HAND2);
117  g_object_ref (rtext->hand_cursor);
118 
119  gtk_widget_style_get (widget, "link_color", &link_color, NULL);
120  g_signal_connect (tview, "event-after",
121  G_CALLBACK (event_after), NULL);
122 
123  // Create a few tags like 'h3', 'b', 'i'. others need to be created as we parse
124  GtkTextBuffer *buffer = gtk_text_view_get_buffer (tview);
125 
126  gboolean reverse = gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL;
127  const char *left_margin = reverse ? "right-margin" : "left-margin";
128  const char *right_margin = reverse ? "left-margin" : "right-margin";
129 
130  gtk_text_buffer_create_tag (buffer, "body", NULL);
131  gtk_text_buffer_create_tag (buffer, "h1", "weight", PANGO_WEIGHT_HEAVY,
132  "scale", PANGO_SCALE_XX_LARGE, "pixels-below-lines", 16,
133  "foreground", "#5c5c5c", NULL);
134  gtk_text_buffer_create_tag (buffer, "h2", "weight", PANGO_WEIGHT_ULTRABOLD,
135  "scale", PANGO_SCALE_X_LARGE, "pixels-below-lines", 15,
136  "foreground", "#5c5c5c", NULL);
137  gtk_text_buffer_create_tag (buffer, "h3", "weight", PANGO_WEIGHT_BOLD,
138  "scale", PANGO_SCALE_LARGE, "pixels-below-lines", 14,
139  "foreground", "#5c5c5c", NULL);
140  gtk_text_buffer_create_tag (buffer, "h4", "weight", PANGO_WEIGHT_SEMIBOLD,
141  "scale", PANGO_SCALE_LARGE, "pixels-below-lines", 13,
142  "foreground", "#5c5c5c", NULL);
143  gtk_text_buffer_create_tag (buffer, "h5",
144  "scale", PANGO_SCALE_LARGE, "foreground", "#5c5c5c", NULL);
145  gtk_text_buffer_create_tag (buffer, "p", "pixels-below-lines", 12, NULL);
146  gtk_text_buffer_create_tag (buffer, "big", "scale", PANGO_SCALE_LARGE, NULL);
147  gtk_text_buffer_create_tag (buffer, "small", "scale", PANGO_SCALE_SMALL, NULL);
148  gtk_text_buffer_create_tag (buffer, "tt", "family", "monospace", NULL);
149  gtk_text_buffer_create_tag (buffer, "pre", "family", "monospace",
150  "paragraph-background", "#f0f0f0", left_margin, 16, right_margin, 20, NULL);
151  gtk_text_buffer_create_tag (buffer, "b", "weight", PANGO_WEIGHT_BOLD, NULL);
152  gtk_text_buffer_create_tag (buffer, "i", "style", PANGO_STYLE_ITALIC, NULL);
153  gtk_text_buffer_create_tag (buffer, "u", "underline", PANGO_UNDERLINE_SINGLE, NULL);
154  gtk_text_buffer_create_tag (buffer, "center", "justification", GTK_JUSTIFY_CENTER, NULL);
155  gtk_text_buffer_create_tag (buffer, "right", "justification", GTK_JUSTIFY_RIGHT, NULL);
156  // helpers
157  gtk_text_buffer_create_tag (buffer, "keyword", "background", "yellow",
158  "foreground", "#000000", NULL);
159 }
160 
161 static void ygtk_rich_text_destroy (GtkWidget *widget)
162 {
163  // destroy can be called multiple times, and we must ref only once
164  YGtkRichText *rtext = YGTK_RICH_TEXT (widget);
165  g_object_unref (rtext->hand_cursor);
166  ygtk_rich_text_set_background (rtext, NULL);
167  GTK_WIDGET_CLASS (ygtk_rich_text_parent_class)->destroy(widget);
168 }
169 
170 // Change the cursor to the "hands" cursor typically used by web browsers,
171 // if there is a link in the given position.
172 static void set_cursor_if_appropriate (GtkTextView *view, gint wx, gint wy)
173 {
174  if (wx == -1) {
175  GtkWidget *widget = GTK_WIDGET (view);
176  GtkAllocation alloc;
177  gtk_widget_get_allocation(widget, &alloc);
178 
179  GdkWindow *window = gtk_widget_get_window (widget);
180  GdkDisplay *display = gdk_window_get_display (window);
181 
182 # if GTK_CHECK_VERSION (3, 20, 0)
183  GdkSeat *seat = gdk_display_get_default_seat (display);
184  GdkDevice *pointer = gdk_seat_get_pointer (seat);
185 # else
186  GdkDeviceManager *device_manager = gdk_display_get_device_manager (display);
187  GdkDevice *pointer = gdk_device_manager_get_client_pointer (device_manager);
188 # endif
189 
190  gdk_window_get_device_position (window, pointer, &wx, &wy, NULL);
191 
192  if (wx < 0 || wy < 0 || wx >= alloc.width ||
193  wy >= alloc.height)
194  return;
195  }
196 
197  static gboolean hovering_over_link = FALSE;
198  gboolean hovering = get_link (view, wx, wy) != NULL;
199 
200  if (hovering != hovering_over_link) {
201  hovering_over_link = hovering;
202  YGtkRichText *rtext = YGTK_RICH_TEXT (view);
203  GdkWindow *window = gtk_text_view_get_window (view, GTK_TEXT_WINDOW_TEXT);
204  GdkCursor *cursor = hovering ? rtext->hand_cursor : NULL;
205  gdk_window_set_cursor (window, cursor);
206  }
207 }
208 
209 // Update the cursor image if the pointer moved.
210 static gboolean ygtk_rich_text_motion_notify_event (GtkWidget *widget,
211  GdkEventMotion *event)
212 {
213  set_cursor_if_appropriate (GTK_TEXT_VIEW (widget), event->x, event->y);
214  return TRUE;
215 }
216 
217 static gboolean ygtk_rich_text_draw (GtkWidget *widget, cairo_t *cr)
218 {
219  GtkTextView *text = GTK_TEXT_VIEW (widget);
220  YGtkRichText *rtext = YGTK_RICH_TEXT (widget);
221  GtkAllocation alloc;
222  gtk_widget_get_allocation(widget, &alloc);
223  if (rtext->background_pixbuf) {
224  GdkWindow *window = gtk_text_view_get_window (text, GTK_TEXT_WINDOW_TEXT);
225  if (gtk_cairo_should_draw_window(cr, window)) {
226  int x, y;
227  int width = gdk_pixbuf_get_width (rtext->background_pixbuf);
228  int height = gdk_pixbuf_get_height (rtext->background_pixbuf);
229  gtk_text_view_buffer_to_window_coords (text, GTK_TEXT_WINDOW_TEXT,
230  alloc.width-((2*width)/5), -height/3, &x, &y);
231 
232  gtk_cairo_transform_to_window(cr, widget, window);
233  gdk_cairo_set_source_pixbuf (cr, rtext->background_pixbuf, x, y);
234  cairo_paint (cr);
235  }
236  }
237 
238  gboolean ret;
239  ret = GTK_WIDGET_CLASS (ygtk_rich_text_parent_class)->draw (widget, cr);
240  set_cursor_if_appropriate (text, -1, -1);
241  return ret;
242 }
243 
244 /* Rich Text parsing methods. */
245 
246 typedef struct _HTMLList
247 {
248  gboolean ordered;
249  char enumeration;
250 } HTMLList;
251 
252 static void HTMLList_init (HTMLList *list, gboolean ordered)
253 {
254  list->ordered = ordered;
255  list->enumeration = 1;
256 }
257 
258 typedef struct GRTPTag {
259  GtkTextMark *mark;
260  GtkTextTag *tag;
261 } GRTPTag;
262 typedef struct GRTParseState {
263  GtkTextBuffer *buffer;
264  GtkTextTagTable *tags;
265  GList *htags; // of GRTPTag
266 
267  // Attributes for tags that affect their children
268  gboolean pre_mode;
269  gboolean default_color;
270  int left_margin;
271  GList *html_list; // of HTMLList
272  gboolean closed_block_tag;
273 } GRTParseState;
274 
275 static void GRTParseState_init (GRTParseState *state, GtkTextBuffer *buffer)
276 {
277  state->buffer = buffer;
278  state->pre_mode = FALSE;
279  state->default_color = TRUE;
280  state->left_margin = 0;
281  state->tags = gtk_text_buffer_get_tag_table (buffer);
282  state->html_list = NULL;
283  state->htags = NULL;
284  state->closed_block_tag = FALSE;
285 }
286 
287 static void free_list (GList *list)
288 {
289  GList *i;
290  for (i = g_list_first (list); i; i = i->next)
291  g_free (i->data);
292  g_list_free (list);
293 }
294 
295 static void GRTParseState_free (GRTParseState *state)
296 {
297  // NOTE: some elements might not have been freed because of bad html
298  free_list (state->html_list);
299  free_list (state->htags);
300 }
301 
302 static void insert_li_enumeration (GRTParseState *state, GtkTextIter *iter, gboolean start)
303 {
304  gboolean _start = gtk_widget_get_default_direction() != GTK_TEXT_DIR_RTL;
305  if (_start != start) return;
306 
307  HTMLList *front_list;
308  if (state->html_list && (front_list = g_list_first (state->html_list)->data) &&
309  (front_list->ordered)) {
310  const gchar *form = start ? "%d. " : " .%d";
311  gchar *str = g_strdup_printf (form, front_list->enumeration++);
312  gtk_text_buffer_insert (state->buffer, iter, str, -1);
313  g_free (str);
314  }
315  else { // \\u25cf for bigger bullets
316  const char *str = start ? "\u2022 " : " \u2022";
317  gtk_text_buffer_insert (state->buffer, iter, str, -1);
318  }
319 }
320 
321 // Tags to support: <p> and not </p>:
322 // either 'elide' \ns (turn off with <pre> I guess
323 static void
324 rt_start_element (GMarkupParseContext *context,
325  const gchar *element_name,
326  const gchar **attribute_names,
327  const gchar **attribute_values,
328  gpointer user_data,
329  GError **error)
330 { // Called for open tags <foo bar="baz">
331  GRTParseState *state = (GRTParseState*) user_data;
332  GRTPTag *tag = g_malloc (sizeof (GRTPTag));
333  GtkTextIter iter;
334  gtk_text_buffer_get_end_iter (state->buffer, &iter);
335  tag->mark = gtk_text_buffer_create_mark (state->buffer, NULL, &iter, TRUE);
336 
337  if (!g_ascii_strcasecmp (element_name, "pre"))
338  state->pre_mode = TRUE;
339 
340  // Check if this is a block tag
341  if (isBlockTag (element_name)) {
342  // make sure this opens a new paragraph
343  if (state->html_list && gtk_text_iter_get_line_offset (&iter) < 6)
344  ; // on a list, there is the "1. " in the buffer so we have to do this
345  else if (!gtk_text_iter_starts_line (&iter)) {
346  gtk_text_buffer_insert (state->buffer, &iter, "\n", -1);
347  gtk_text_buffer_get_end_iter (state->buffer, &iter);
348  }
349  }
350  state->closed_block_tag = FALSE;
351 
352  char *lower = g_ascii_strdown (element_name, -1);
353  tag->tag = gtk_text_tag_table_lookup (state->tags, lower);
354 
355  // Special tags that must be inserted manually
356  if (!tag->tag) {
357  if (!g_ascii_strcasecmp (element_name, "font")) {
358  int i;
359  for (i = 0; attribute_names[i]; i++) {
360  const char *attrb = attribute_names[i];
361  const char *value = attribute_values[i];
362  if (!g_ascii_strcasecmp (attrb, "color")) {
363  tag->tag = gtk_text_buffer_create_tag (state->buffer, NULL,
364  "foreground", value, NULL);
365  state->default_color = FALSE;
366  }
367  // not from html -- we use this internally
368  else if (!g_ascii_strcasecmp (attrb, "bgcolor")) {
369  tag->tag = gtk_text_buffer_create_tag (state->buffer, NULL,
370  "background", value, NULL);
371  }
372  else
373  g_warning ("Unknown font attribute: '%s'", attrb);
374  }
375  }
376  else if (!g_ascii_strcasecmp (element_name, "a")) {
377  if (attribute_names[0] &&
378  !g_ascii_strcasecmp (attribute_names[0], "href")) {
379  tag->tag = gtk_text_buffer_create_tag (state->buffer, NULL,
380  "underline", PANGO_UNDERLINE_SINGLE, NULL);
381  if (state->default_color)
382  g_object_set (tag->tag, "foreground-gdk", &link_color, NULL);
383  g_object_set_data (G_OBJECT (tag->tag), "link", g_strdup (attribute_values[0]));
384  }
385  else if (attribute_names[0] &&
386  !g_ascii_strcasecmp (attribute_names[0], "name")) {
387  gtk_text_buffer_create_mark (state->buffer, attribute_values[0], &iter, TRUE);
388  }
389  else
390  g_warning ("Unknown a attribute: '%s'", attribute_names[0]);
391  }
392  else if (!g_ascii_strcasecmp (element_name, "li"))
393  insert_li_enumeration (state, &iter, TRUE);
394  // Tags that affect the margin
395  else if (!g_ascii_strcasecmp (element_name, "ul") ||
396  !g_ascii_strcasecmp (element_name, "ol")) {
397  HTMLList *list = g_malloc (sizeof (HTMLList));
398  HTMLList_init (list, !g_ascii_strcasecmp (element_name, "ol"));
399  state->html_list = g_list_append (state->html_list, list);
400  }
401  else if (!g_ascii_strcasecmp (element_name, "img")) {
402  if (attribute_names[0] &&
403  !g_ascii_strcasecmp (attribute_names[0], "src")) {
404  const char *filename = attribute_values[0];
405  if (filename) {
406  GdkPixbuf *pixbuf;
407  if (filename[0] == '/')
408  pixbuf = gdk_pixbuf_new_from_file (filename, NULL);
409  else
410  pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default(),
411  filename, 64, 0, NULL);
412  if (pixbuf) {
413  gtk_text_buffer_insert_pixbuf (state->buffer, &iter, pixbuf);
414  g_object_unref (G_OBJECT (pixbuf));
415  }
416  }
417  }
418  else
419  g_warning ("Unknown img attribute: '%s'", attribute_names[0]);
420  }
421  // tags like <br/>, GMarkup will pass them through the end
422  // tag callback too, so we'll deal with them there
423  else if (!g_ascii_strcasecmp (element_name, "br")) ;
424  else if (!g_ascii_strcasecmp (element_name, "hr")) ;
425 
426  else
427  {
428  if (isBlockTag (element_name))
429  ;
430  else
431  g_warning ("Unknown tag '%s'", element_name);
432  }
433  }
434  else if (attribute_names[0]) { // tags that may have extra attributes
435  if (!g_ascii_strcasecmp (element_name, "p")) {
436  // not from html (basic html only supports background color in
437  // tables), but we use this internally
438  if (!g_ascii_strcasecmp (attribute_names[0], "bgcolor"))
439  tag->tag = gtk_text_buffer_create_tag (state->buffer, NULL,
440  "paragraph-background", attribute_values[0], NULL);
441  else
442  g_warning ("Unknown p attribute: '%s'", attribute_names[0]);
443  }
444  }
445 
446  if (!tag->tag && isIdentTag (element_name)) {
447  state->left_margin += IDENT_MARGIN;
448 
449  gboolean reverse = gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL;
450  const char *margin = reverse ? "right-margin" : "left-margin";
451  tag->tag = gtk_text_buffer_create_tag (state->buffer, NULL,
452  margin, state->left_margin, NULL);
453  }
454 
455  g_free (lower);
456  state->htags = g_list_append (state->htags, tag);
457 }
458 
459 #include "hr.xpm"
460 
461 static void
462 rt_end_element (GMarkupParseContext *context,
463  const gchar *element_name,
464  gpointer user_data,
465  GError **error)
466 { // Called for close tags </foo>
467  GRTParseState *state = (GRTParseState*) user_data;
468 
469  if (g_list_length (state->htags) == 0) {
470  g_warning ("Urgh - empty tag queue closing '%s'", element_name);
471  return;
472  }
473 
474  g_return_if_fail (state->htags != NULL);
475  GRTPTag *tag = g_list_last (state->htags)->data;
476  state->htags = g_list_remove (state->htags, tag);
477 
478  GtkTextIter start, end;
479  gtk_text_buffer_get_iter_at_mark (state->buffer, &start, tag->mark);
480  gtk_text_buffer_get_end_iter (state->buffer, &end);
481 
482  gint appendLines = 0;
483 
484  if (isIdentTag (element_name))
485  state->left_margin -= IDENT_MARGIN;
486 
487  if (!g_ascii_strcasecmp (element_name, "ul") ||
488  !g_ascii_strcasecmp (element_name, "ol")) {
489  HTMLList *last_list = g_list_last (state->html_list)->data;
490  state->html_list = g_list_remove (state->html_list, last_list);
491  g_free (last_list);
492  }
493  else if (!g_ascii_strcasecmp (element_name, "font"))
494  state->default_color = TRUE;
495 
496  else if (!g_ascii_strcasecmp (element_name, "pre"))
497  state->pre_mode = FALSE;
498 
499  else if (!g_ascii_strcasecmp (element_name, "hr")) {
500  GdkPixbuf *pixbuf = gdk_pixbuf_new_from_xpm_data (hr_xpm);
501  gtk_text_buffer_insert_pixbuf (state->buffer, &end, pixbuf);
502  g_object_unref (pixbuf);
503  gtk_text_buffer_get_iter_at_mark (state->buffer, &start, tag->mark);
504  gtk_text_buffer_get_end_iter (state->buffer, &end);
505  gtk_text_buffer_apply_tag_by_name (state->buffer, "center", &start, &end);
506  appendLines = 1;
507  }
508  else if (!g_ascii_strcasecmp (element_name, "li"))
509  insert_li_enumeration (state, &end, FALSE);
510 
511  if (isBlockTag (element_name) || !g_ascii_strcasecmp (element_name, "br")) {
512  appendLines = 1;
513  if (isBlockTag (element_name) && gtk_text_iter_starts_line (&end))
514  appendLines = 0;
515  state->closed_block_tag = TRUE;
516  }
517  else
518  state->closed_block_tag = FALSE;
519 
520  if (appendLines) {
521  gtk_text_buffer_insert (state->buffer, &end,
522  appendLines == 1 ? "\n" : "\n\n", -1);
523  gtk_text_buffer_get_iter_at_mark (state->buffer, &start, tag->mark);
524  gtk_text_buffer_get_end_iter (state->buffer, &end);
525  }
526 
527  if (tag->tag)
528  gtk_text_buffer_apply_tag (state->buffer, tag->tag, &start, &end);
529 
530  gtk_text_buffer_delete_mark (state->buffer, tag->mark);
531  g_free (tag);
532 }
533 
534 static void
535 rt_text (GMarkupParseContext *context,
536  const gchar *text,
537  gsize text_len,
538  gpointer user_data,
539  GError **error)
540 { // Called for character data, NB. text NOT nul-terminated
541  GRTParseState *state = (GRTParseState*) user_data;
542  GtkTextIter start, end;
543  gtk_text_buffer_get_end_iter (state->buffer, &start);
544  if (state->pre_mode)
545  gtk_text_buffer_insert_with_tags (state->buffer, &start,
546  text, text_len, NULL, NULL);
547  else {
548  gboolean rtl = gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL;
549 
550  int i = 0;
551  if (state->closed_block_tag) {
552  for (; i < text_len; i++)
553  if (!g_ascii_isspace (text[i]))
554  break;
555  }
556 
557  // hack: for right-to-left languages, change "Device:" to ":Device" (bug 581800)
558  if (rtl && text[text_len-1] == ':') {
559  gtk_text_buffer_insert (state->buffer, &start, ":", 1);
560  text_len--;
561  }
562 
563  gtk_text_buffer_insert (state->buffer, &start, text+i, text_len-i);
564  }
565  gtk_text_buffer_get_end_iter (state->buffer, &end);
566 }
567 
568 static void
569 rt_passthrough (GMarkupParseContext *context,
570  const gchar *passthrough_text,
571  gsize text_len,
572  gpointer user_data,
573  GError **error)
574 {
575  // ignore comments etc.
576 }
577 
578 static void
579 rt_error (GMarkupParseContext *context,
580  GError *error,
581  gpointer user_data)
582 {
583 }
584 
585 static GMarkupParser rt_parser = {
586  rt_start_element,
587  rt_end_element,
588  rt_text,
589  rt_passthrough,
590  rt_error
591 };
592 
593 GtkWidget *ygtk_rich_text_new (void)
594 { return g_object_new (YGTK_TYPE_RICH_TEXT, NULL); }
595 
596 static void ygtk_rich_text_set_rtl (YGtkRichText *rtext)
597 {
598  GtkTextView *view = GTK_TEXT_VIEW (rtext);
599  GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
600  GtkTextIter iter;
601  gtk_text_buffer_get_start_iter (buffer, &iter);
602  do {
603  GtkTextIter end = iter;
604  if (!gtk_text_iter_forward_line (&end))
605  gtk_text_buffer_get_end_iter (buffer, &end);
606 
607  gchar *text = gtk_text_iter_get_text (&iter, &end);
608  PangoDirection dir = pango_find_base_dir (text, -1);
609  if (dir == PANGO_DIRECTION_LTR)
610  gtk_text_buffer_apply_tag_by_name (buffer, "right", &iter, &end);
611 
612  iter = end;
613  } while (!gtk_text_iter_is_end (&iter));
614 }
615 
616 void ygtk_rich_text_set_plain_text (YGtkRichText* rtext, const gchar* text)
617 {
618  GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rtext));
619  gtk_text_buffer_set_text (buffer, text, -1);
620 }
621 
622 void ygtk_rich_text_set_text (YGtkRichText* rtext, const gchar* text)
623 {
624  GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rtext));
625  gtk_text_buffer_set_text (buffer, "", 0); // remove any existing text
626 
627  GRTParseState state;
628  GRTParseState_init (&state, buffer);
629 
630  GMarkupParseContext *ctx;
631  ctx = g_markup_parse_context_new (&rt_parser, (GMarkupParseFlags)0, &state, NULL);
632 
633  char *xml = ygutils_convert_to_xhtml (text);
634  GError *error = NULL;
635  if (!g_markup_parse_context_parse (ctx, xml, -1, &error)) {
636  g_warning ("Markup parse error '%s'", error ? error->message : "Unknown");
637  }
638  g_free (xml);
639 
640  g_markup_parse_context_free (ctx);
641  GRTParseState_free (&state);
642 
643  // remove last empty line, if any
644  GtkTextIter end_it, before_end_it;
645  gtk_text_buffer_get_end_iter (buffer, &end_it);
646  before_end_it = end_it;
647  if (gtk_text_iter_backward_char (&before_end_it) &&
648  gtk_text_iter_get_char (&before_end_it) == '\n')
649  gtk_text_buffer_delete (buffer, &before_end_it, &end_it);
650 
651  // GtkTextView does LTR and RTL depending on the paragraph; we want
652  // to change that behavior so it's RTL to the all thing for Arabic
653  if (gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL)
654  ygtk_rich_text_set_rtl (rtext);
655 }
656 
657 /* gtk_text_iter_forward_search() is case-sensitive so we roll our own.
658  The idea is to keep use get_text and strstr there, but to be more
659  efficient we check per line. */
660 static gboolean ygtk_rich_text_forward_search (const GtkTextIter *begin,
661  const GtkTextIter *end, const gchar *_key, GtkTextIter *match_start,
662  GtkTextIter *match_end)
663 {
664  if (*_key == 0)
665  return FALSE;
666 
667  /* gtk_text_iter_get_char() returns a gunichar (ucs4 coding), so we
668  convert the given string (which is utf-8, like anyhting in gtk+) */
669  gunichar *key = g_utf8_to_ucs4 (_key, -1, NULL, NULL, NULL);
670  if (!key) // conversion error -- should not happen
671  return FALSE;
672 
673  // convert key to lower case, to avoid work later
674  gunichar *k;
675  for (k = key; *k; k++)
676  *k = g_unichar_tolower (*k);
677 
678  GtkTextIter iter = *begin, iiter;
679  while (!gtk_text_iter_is_end (&iter) && gtk_text_iter_compare (&iter, end) <= 0) {
680  iiter = iter;
681  for (k = key; *k == g_unichar_tolower (gtk_text_iter_get_char (&iiter)) && (*k);
682  k++, gtk_text_iter_forward_char (&iiter))
683  ;
684  if (!*k) {
685  *match_start = iter;
686  *match_end = iiter;
687  return TRUE;
688  }
689  gtk_text_iter_forward_char (&iter);
690  }
691  return FALSE;
692 }
693 
694 gboolean ygtk_rich_text_mark_text (YGtkRichText *rtext, const gchar *text)
695 {
696  GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rtext));
697  GtkTextIter iter, end, match_start, match_end;
698 
699  gtk_text_buffer_get_bounds (buffer, &iter, &end);
700  gtk_text_buffer_remove_tag_by_name (buffer, "keyword", &iter, &end);
701 
702  gtk_text_buffer_select_range (buffer, &iter, &iter); // unselect text
703  if (!text || *text == '\0')
704  return TRUE;
705 
706  gboolean found = FALSE;
707  while (ygtk_rich_text_forward_search (&iter, &end, text,
708  &match_start, &match_end)) {
709  found = TRUE;
710  gtk_text_buffer_apply_tag_by_name (buffer, "keyword", &match_start, &match_end);
711  iter = match_end;
712  gtk_text_iter_forward_char (&iter);
713  }
714  return found;
715 }
716 
717 gboolean ygtk_rich_text_forward_mark (YGtkRichText *rtext, const gchar *text)
718 {
719  GtkTextIter start_iter, end_iter;
720  GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rtext));
721  gtk_text_buffer_get_iter_at_mark (buffer, &start_iter,
722  gtk_text_buffer_get_selection_bound (buffer));
723  gtk_text_buffer_get_end_iter (buffer, &end_iter);
724 
725  gboolean found;
726  found = ygtk_rich_text_forward_search (&start_iter, &end_iter, text,
727  &start_iter, &end_iter);
728  if (!found) {
729  gtk_text_buffer_get_start_iter (buffer, &start_iter);
730  found = ygtk_rich_text_forward_search (&start_iter, &end_iter, text,
731  &start_iter, &end_iter);
732  }
733 
734  if (found) {
735  gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (rtext), &start_iter, 0.10,
736  FALSE, 0, 0);
737  gtk_text_buffer_select_range (buffer, &start_iter, &end_iter);
738  return TRUE;
739  }
740  return FALSE;
741 }
742 
743 void ygtk_rich_text_set_background (YGtkRichText *rtext, GdkPixbuf *pixbuf)
744 {
745  if (rtext->background_pixbuf)
746  g_object_unref (G_OBJECT (rtext->background_pixbuf));
747  rtext->background_pixbuf = pixbuf;
748  if (pixbuf)
749  g_object_ref (G_OBJECT (pixbuf));
750 }
751 
752 static void ygtk_rich_text_class_init (YGtkRichTextClass *klass)
753 {
754  GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS (klass);
755  gtkwidget_class->motion_notify_event = ygtk_rich_text_motion_notify_event;
756  gtkwidget_class->draw = ygtk_rich_text_draw;
757  gtkwidget_class->destroy = ygtk_rich_text_destroy;
758 
759  link_clicked_signal = g_signal_new ("link-clicked",
760  G_TYPE_FROM_CLASS (G_OBJECT_CLASS (klass)), G_SIGNAL_RUN_LAST,
761  G_STRUCT_OFFSET (YGtkRichTextClass, link_clicked), NULL, NULL,
762  g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);
763 }
764