1 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
2 /* Balsa E-Mail Client
3 * Copyright (C) 1997-2013 Stuart Parmenter and others
4 * Written by (C) Albrecht Dre� <albrecht.dress@arcor.de> 2007
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2, or (at your option)
9 * any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19 * 02111-1307, USA.
20 */
21
22 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
23 # include "config.h"
24 #endif /* HAVE_CONFIG_H */
25 #include "balsa-print-object-text.h"
26
27 #include <gtk/gtk.h>
28 #include <string.h>
29 #include <glib/gi18n.h>
30 #include "libbalsa.h"
31 #include "rfc2445.h"
32 #include "balsa-icons.h"
33 #include "balsa-print-object.h"
34 #include "balsa-print-object-decor.h"
35 #include "balsa-print-object-default.h"
36
37
38 typedef enum {
39 PHRASE_BF = 0,
40 PHRASE_EM,
41 PHRASE_UL,
42 PHRASE_TYPE_COUNT
43 } PhraseType;
44
45 typedef struct {
46 PhraseType phrase_type;
47 guint start_index;
48 guint end_index;
49 } PhraseRegion;
50
51
52 /* object related functions */
53 static void balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass);
54 static void balsa_print_object_text_init(GTypeInstance * instance,
55 gpointer g_class);
56 static void balsa_print_object_text_destroy(GObject * self);
57
58 static void balsa_print_object_text_draw(BalsaPrintObject * self,
59 GtkPrintContext * context,
60 cairo_t * cairo_ctx);
61
62 static GList * collect_attrs(GList * all_attr, guint offset, guint len);
63 static PangoAttrList * phrase_list_to_pango(GList * phrase_list);
64 static GList * phrase_highlight(const gchar * buffer, gunichar tag_char,
65 PhraseType tag_type, GList * phrase_list);
66
67
68 static BalsaPrintObjectClass *parent_class = NULL;
69
70
71 GType
balsa_print_object_text_get_type()72 balsa_print_object_text_get_type()
73 {
74 static GType balsa_print_object_text_type = 0;
75
76 if (!balsa_print_object_text_type) {
77 static const GTypeInfo balsa_print_object_text_info = {
78 sizeof(BalsaPrintObjectTextClass),
79 NULL, /* base_init */
80 NULL, /* base_finalize */
81 (GClassInitFunc) balsa_print_object_text_class_init,
82 NULL, /* class_finalize */
83 NULL, /* class_data */
84 sizeof(BalsaPrintObjectText),
85 0, /* n_preallocs */
86 (GInstanceInitFunc) balsa_print_object_text_init
87 };
88
89 balsa_print_object_text_type =
90 g_type_register_static(BALSA_TYPE_PRINT_OBJECT,
91 "BalsaPrintObjectText",
92 &balsa_print_object_text_info, 0);
93 }
94
95 return balsa_print_object_text_type;
96 }
97
98
99 static void
balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass)100 balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass)
101 {
102 parent_class = g_type_class_ref(BALSA_TYPE_PRINT_OBJECT);
103 BALSA_PRINT_OBJECT_CLASS(klass)->draw =
104 balsa_print_object_text_draw;
105 G_OBJECT_CLASS(klass)->finalize = balsa_print_object_text_destroy;
106 }
107
108
109 static void
balsa_print_object_text_init(GTypeInstance * instance,gpointer g_class)110 balsa_print_object_text_init(GTypeInstance * instance, gpointer g_class)
111 {
112 BalsaPrintObjectText *po = BALSA_PRINT_OBJECT_TEXT(instance);
113
114 po->text = NULL;
115 po->attributes = NULL;
116 }
117
118
119 static void
balsa_print_object_text_destroy(GObject * self)120 balsa_print_object_text_destroy(GObject * self)
121 {
122 BalsaPrintObjectText *po = BALSA_PRINT_OBJECT_TEXT(self);
123
124 g_list_foreach(po->attributes, (GFunc) g_free, NULL);
125 g_list_free(po->attributes);
126 g_free(po->text);
127
128 G_OBJECT_CLASS(parent_class)->finalize(self);
129 }
130
131 /* prepare a text/plain part, which gets
132 * - citation bars and colourisation of cited text (prefs dependant)
133 * - syntax highlighting (prefs dependant)
134 * - RFC 3676 "flowed" processing */
135 GList *
balsa_print_object_text_plain(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)136 balsa_print_object_text_plain(GList *list, GtkPrintContext * context,
137 LibBalsaMessageBody * body,
138 BalsaPrintSetup * psetup)
139 {
140 #if USE_GREGEX
141 GRegex *rex;
142 #else /* USE_GREGEX */
143 regex_t rex;
144 #endif /* USE_GREGEX */
145 gchar *textbuf;
146 PangoFontDescription *font;
147 gdouble c_at_x;
148 gdouble c_use_width;
149 guint first_page;
150 gchar * par_start;
151 gchar * eol;
152 gint par_len;
153
154 /* set up the regular expression for qouted text */
155 #if USE_GREGEX
156 if (!(rex = balsa_quote_regex_new()))
157 #else /* USE_GREGEX */
158 if (regcomp(&rex, balsa_app.quote_regex, REG_EXTENDED) != 0)
159 #endif /* USE_GREGEX */
160 return balsa_print_object_default(list, context, body, psetup);
161
162 /* start on new page if less than 2 lines can be printed */
163 if (psetup->c_y_pos + 2 * P_TO_C(psetup->p_body_font_height) >
164 psetup->c_height) {
165 psetup->c_y_pos = 0;
166 psetup->page_count++;
167 }
168 c_at_x = psetup->c_x0 + psetup->curr_depth * C_LABEL_SEP;
169 c_use_width = psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
170
171 /* copy the text body to a buffer */
172 if (body->buffer)
173 textbuf = g_strdup(body->buffer);
174 else
175 libbalsa_message_body_get_content(body, &textbuf, NULL);
176
177 /* fake an empty buffer if textbuf is NULL */
178 if (!textbuf)
179 textbuf = g_strdup("");
180
181 /* be sure the we have correct utf-8 stuff here... */
182 libbalsa_utf8_sanitize(&textbuf, balsa_app.convert_unknown_8bit, NULL);
183
184 /* apply flowed if requested */
185 if (libbalsa_message_body_is_flowed(body)) {
186 GString *flowed;
187
188 flowed =
189 libbalsa_process_text_rfc2646(textbuf, G_MAXINT, FALSE, FALSE,
190 FALSE,
191 libbalsa_message_body_is_delsp
192 (body));
193 g_free(textbuf);
194 textbuf = flowed->str;
195 g_string_free(flowed, FALSE);
196 }
197
198 /* get the font */
199 font = pango_font_description_from_string(balsa_app.print_body_font);
200
201 /* loop over paragraphs */
202 par_start = textbuf;
203 eol = strchr(par_start, '\n');
204 par_len = eol ? eol - par_start : (gint) strlen(par_start);
205 while (*par_start) {
206 GString *level_buf;
207 guint curr_level;
208 guint cite_level;
209 GList *par_parts;
210 GList *this_par_part;
211 GList *attr_list;
212 PangoLayout *layout;
213 PangoAttrList *pango_attr_list;
214 GArray *attr_offs;
215 gdouble c_at_y;
216
217 level_buf = NULL;
218 curr_level = 0; /* just to make the compiler happy */
219 do {
220 gchar *thispar;
221 guint cite_idx;
222
223 thispar = g_strndup(par_start, par_len);
224
225 /* get the cite level and strip off the prefix */
226 #if USE_GREGEX
227 if (libbalsa_match_regex
228 (thispar, rex, &cite_level, &cite_idx))
229 #else /* USE_GREGEX */
230 if (libbalsa_match_regex
231 (thispar, &rex, &cite_level, &cite_idx))
232 #endif /* USE_GREGEX */
233 {
234 gchar *new;
235
236 new = thispar + cite_idx;
237 if (g_unichar_isspace(g_utf8_get_char(new)))
238 new = g_utf8_next_char(new);
239 new = g_strdup(new);
240 g_free(thispar);
241 thispar = new;
242 }
243
244 /* glue paragraphs with the same cite level together */
245 if (!level_buf || (curr_level == cite_level)) {
246 if (!level_buf) {
247 level_buf = g_string_new(thispar);
248 curr_level = cite_level;
249 } else {
250 level_buf = g_string_append_c(level_buf, '\n');
251 level_buf = g_string_append(level_buf, thispar);
252 }
253
254 par_start = eol ? eol + 1 : par_start + par_len;
255 eol = strchr(par_start, '\n');
256 par_len = eol ? eol - par_start : (gint) strlen(par_start);
257 }
258
259 g_free(thispar);
260 } while (*par_start && (curr_level == cite_level));
261
262 /* configure the layout so we can use Pango to split the text into pages */
263 layout = gtk_print_context_create_pango_layout(context);
264 pango_layout_set_font_description(layout, font);
265 pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
266 pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
267
268 /* leave place for the citation bars */
269 pango_layout_set_width(layout,
270 C_TO_P(c_use_width - curr_level * C_LABEL_SEP));
271
272 /* highlight structured phrases if requested */
273 if (balsa_app.print_highlight_phrases) {
274 attr_list =
275 phrase_highlight(level_buf->str, '*', PHRASE_BF, NULL);
276 attr_list =
277 phrase_highlight(level_buf->str, '/', PHRASE_EM, attr_list);
278 attr_list =
279 phrase_highlight(level_buf->str, '_', PHRASE_UL, attr_list);
280 } else
281 attr_list = NULL;
282
283 /* start on new page if less than one line can be printed */
284 if (psetup->c_y_pos + P_TO_C(psetup->p_body_font_height) >
285 psetup->c_height) {
286 psetup->c_y_pos = 0;
287 psetup->page_count++;
288 }
289
290 /* split paragraph if necessary */
291 pango_attr_list = phrase_list_to_pango(attr_list);
292 first_page = psetup->page_count - 1;
293 c_at_y = psetup->c_y_pos;
294 par_parts =
295 split_for_layout(layout, level_buf->str, pango_attr_list,
296 psetup, FALSE, &attr_offs);
297 if (pango_attr_list)
298 pango_attr_list_unref(pango_attr_list);
299 g_object_unref(G_OBJECT(layout));
300 g_string_free(level_buf, TRUE);
301
302 /* each part is a new text object */
303 this_par_part = par_parts;
304 while (this_par_part) {
305 BalsaPrintObjectText *pot;
306
307 pot = g_object_new(BALSA_TYPE_PRINT_OBJECT_TEXT, NULL);
308 g_assert(pot != NULL);
309 BALSA_PRINT_OBJECT(pot)->on_page = first_page++;
310 BALSA_PRINT_OBJECT(pot)->c_at_x = c_at_x;
311 BALSA_PRINT_OBJECT(pot)->c_at_y = psetup->c_y0 + c_at_y;
312 BALSA_PRINT_OBJECT(pot)->depth = psetup->curr_depth;
313 c_at_y = 0.0;
314 BALSA_PRINT_OBJECT(pot)->c_width = c_use_width;
315 /* note: height is calculated when the object is drawn */
316 pot->text = (gchar *) this_par_part->data;
317 pot->cite_level = curr_level;
318 pot->attributes =
319 collect_attrs(attr_list,
320 g_array_index(attr_offs, guint, 0),
321 strlen(pot->text));
322
323 list = g_list_append(list, pot);
324 g_array_remove_index(attr_offs, 0);
325 this_par_part = g_list_next(this_par_part);
326 }
327 if (attr_list) {
328 g_list_foreach(attr_list, (GFunc) g_free, NULL);
329 g_list_free(attr_list);
330 }
331 g_list_free(par_parts);
332 g_array_free(attr_offs, TRUE);
333 }
334
335 /* clean up */
336 pango_font_description_free(font);
337 g_free(textbuf);
338 #if USE_GREGEX
339 g_regex_unref(rex);
340 #endif /* USE_GREGEX */
341 return list;
342 }
343
344
345 /* prepare a text part which is simply printed "as is" without all the bells
346 * and whistles of text/plain (see above) */
347 GList *
balsa_print_object_text(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)348 balsa_print_object_text(GList *list, GtkPrintContext * context,
349 LibBalsaMessageBody * body,
350 BalsaPrintSetup * psetup)
351 {
352 gchar *textbuf;
353 PangoFontDescription *font;
354 gdouble c_at_x;
355 gdouble c_use_width;
356 guint first_page;
357 GList *par_parts;
358 GList *this_par_part;
359 PangoLayout *layout;
360 gdouble c_at_y;
361
362 /* start on new page if less than 2 lines can be printed */
363 if (psetup->c_y_pos + 2 * P_TO_C(psetup->p_body_font_height) >
364 psetup->c_height) {
365 psetup->c_y_pos = 0;
366 psetup->page_count++;
367 }
368 c_at_x = psetup->c_x0 + psetup->curr_depth * C_LABEL_SEP;
369 c_use_width = psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
370
371 /* copy the text body to a buffer */
372 if (body->buffer)
373 textbuf = g_strdup(body->buffer);
374 else
375 libbalsa_message_body_get_content(body, &textbuf, NULL);
376
377 /* fake an empty buffer if textbuf is NULL */
378 if (!textbuf)
379 textbuf = g_strdup("");
380
381 /* be sure the we have correct utf-8 stuff here... */
382 libbalsa_utf8_sanitize(&textbuf, balsa_app.convert_unknown_8bit, NULL);
383
384 /* get the font */
385 font = pango_font_description_from_string(balsa_app.print_body_font);
386
387 /* configure the layout so we can use Pango to split the text into pages */
388 layout = gtk_print_context_create_pango_layout(context);
389 pango_layout_set_font_description(layout, font);
390 pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
391 pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
392 pango_layout_set_width(layout, C_TO_P(c_use_width));
393
394 /* split paragraph if necessary */
395 first_page = psetup->page_count - 1;
396 c_at_y = psetup->c_y_pos;
397 par_parts = split_for_layout(layout, textbuf, NULL, psetup, FALSE, NULL);
398 g_object_unref(G_OBJECT(layout));
399 pango_font_description_free(font);
400 g_free(textbuf);
401
402 /* each part is a new text object */
403 this_par_part = par_parts;
404 while (this_par_part) {
405 BalsaPrintObjectText *pot;
406
407 pot = g_object_new(BALSA_TYPE_PRINT_OBJECT_TEXT, NULL);
408 g_assert(pot != NULL);
409 BALSA_PRINT_OBJECT(pot)->on_page = first_page++;
410 BALSA_PRINT_OBJECT(pot)->c_at_x = c_at_x;
411 BALSA_PRINT_OBJECT(pot)->c_at_y = psetup->c_y0 + c_at_y;
412 BALSA_PRINT_OBJECT(pot)->depth = psetup->curr_depth;
413 c_at_y = 0.0;
414 BALSA_PRINT_OBJECT(pot)->c_width = c_use_width;
415 /* note: height is calculated when the object is drawn */
416 pot->text = (gchar *) this_par_part->data;
417 pot->cite_level = 0;
418 pot->attributes = NULL;
419
420 list = g_list_append(list, pot);
421 this_par_part = g_list_next(this_par_part);
422 }
423 g_list_free(par_parts);
424
425 return list;
426 }
427
428
429 /* note: a vcard is an icon plus a series of labels/text, so this function actually
430 * returns a BalsaPrintObjectDefault... */
431
432 #define ADD_VCARD_FIELD(buf, labwidth, layout, field, descr) \
433 do { \
434 if (field) { \
435 gint label_width = p_string_width_from_layout(layout, descr); \
436 if (label_width > labwidth) \
437 labwidth = label_width; \
438 if ((buf)->len > 0) \
439 g_string_append_c(buf, '\n'); \
440 g_string_append_printf(buf, "%s\t%s", descr, field); \
441 } \
442 } while(0)
443
444 GList *
balsa_print_object_text_vcard(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)445 balsa_print_object_text_vcard(GList * list,
446 GtkPrintContext * context,
447 LibBalsaMessageBody * body,
448 BalsaPrintSetup * psetup)
449 {
450 BalsaPrintObjectDefault *pod;
451 BalsaPrintObject *po;
452 PangoFontDescription *header_font;
453 PangoLayout *test_layout;
454 PangoTabArray *tabs;
455 GString *desc_buf;
456 gdouble c_max_height;
457 LibBalsaAddress * addr = NULL;
458 gchar *textbuf;
459
460 /* check if we can create an address from the body and fall back to default if
461 * this fails */
462 if (body->buffer)
463 textbuf = g_strdup(body->buffer);
464 else
465 libbalsa_message_body_get_content(body, &textbuf, NULL);
466 if (textbuf)
467 addr = libbalsa_address_new_from_vcard(textbuf, body->charset);
468 if (!addr) {
469 g_free(textbuf);
470 return balsa_print_object_text(list, context, body, psetup);
471 }
472
473 /* proceed with the address information */
474 pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
475 g_assert(pod != NULL);
476 po = BALSA_PRINT_OBJECT(pod);
477
478 /* create the part */
479 po->depth = psetup->curr_depth;
480 po->c_width =
481 psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
482
483 /* get the stock contacts icon or the mime type icon on fail */
484 pod->pixbuf =
485 gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
486 BALSA_PIXMAP_IDENTITY, 48,
487 GTK_ICON_LOOKUP_USE_BUILTIN, NULL);
488 if (!pod->pixbuf) {
489 gchar *conttype = libbalsa_message_body_get_mime_type(body);
490
491 pod->pixbuf = libbalsa_icon_finder(NULL, conttype, NULL, NULL,
492 GTK_ICON_SIZE_DND);
493 }
494 pod->c_image_width = gdk_pixbuf_get_width(pod->pixbuf);
495 pod->c_image_height = gdk_pixbuf_get_height(pod->pixbuf);
496
497
498 /* create a layout for calculating the maximum label width */
499 header_font =
500 pango_font_description_from_string(balsa_app.print_header_font);
501 test_layout = gtk_print_context_create_pango_layout(context);
502 pango_layout_set_font_description(test_layout, header_font);
503 pango_font_description_free(header_font);
504
505 /* add fields from the address */
506 desc_buf = g_string_new("");
507 pod->p_label_width = 0;
508 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
509 addr->full_name, _("Full Name"));
510 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
511 addr->nick_name, _("Nick Name"));
512 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
513 addr->first_name, _("First Name"));
514 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
515 addr->last_name, _("Last Name"));
516 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
517 addr->organization, _("Organization"));
518 if (addr->address_list)
519 ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
520 (const gchar *) addr->address_list->data, _("Email Address"));
521 g_object_unref(addr);
522
523 /* add a small space between label and value */
524 pod->p_label_width += C_TO_P(C_LABEL_SEP);
525
526 /* configure the layout so we can calculate the text height */
527 pango_layout_set_indent(test_layout, -pod->p_label_width);
528 tabs =
529 pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT,
530 pod->p_label_width);
531 pango_layout_set_tabs(test_layout, tabs);
532 pango_tab_array_free(tabs);
533 pango_layout_set_width(test_layout,
534 C_TO_P(po->c_width -
535 4 * C_LABEL_SEP - pod->c_image_width));
536 pango_layout_set_alignment(test_layout, PANGO_ALIGN_LEFT);
537 pod->c_text_height =
538 P_TO_C(p_string_height_from_layout(test_layout, desc_buf->str));
539 pod->description = g_string_free(desc_buf, FALSE);
540
541 /* check if we should move to the next page */
542 c_max_height = MAX(pod->c_text_height, pod->c_image_height);
543 if (psetup->c_y_pos + c_max_height > psetup->c_height) {
544 psetup->c_y_pos = 0;
545 psetup->page_count++;
546 }
547
548 /* remember the extent */
549 po->on_page = psetup->page_count - 1;
550 po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
551 po->c_at_y = psetup->c_y0 + psetup->c_y_pos;
552 po->c_width = psetup->c_width - 2 * po->depth * C_LABEL_SEP;
553 po->c_height = c_max_height;
554
555 /* adjust the y position */
556 psetup->c_y_pos += c_max_height;
557
558 return g_list_append(list, po);
559 }
560
561
562 /* add a text/calendar object */
563
564 #define ADD_VCAL_FIELD(buf, labwidth, layout, field, descr) \
565 do { \
566 if (field) { \
567 gint label_width = p_string_width_from_layout(layout, descr); \
568 if (label_width > labwidth) \
569 labwidth = label_width; \
570 if ((buf)->len > 0) \
571 g_string_append_c(buf, '\n'); \
572 g_string_append_printf(buf, "%s\t%s", descr, field); \
573 } \
574 } while(0)
575
576 #define ADD_VCAL_DATE(buf, labwidth, layout, date, descr) \
577 do { \
578 if (date != (time_t) -1) { \
579 gchar * _dstr = \
580 libbalsa_date_to_utf8(&date, balsa_app.date_string); \
581 ADD_VCAL_FIELD(buf, labwidth, layout, _dstr, descr); \
582 g_free(_dstr); \
583 } \
584 } while (0)
585
586 #define ADD_VCAL_ADDRESS(buf, labwidth, layout, addr, descr) \
587 do { \
588 if (addr) { \
589 gchar * _astr = libbalsa_vcal_attendee_to_str(addr); \
590 ADD_VCAL_FIELD(buf, labwidth, layout, _astr, descr); \
591 g_free(_astr); \
592 } \
593 } while (0)
594
595
596 GList *
balsa_print_object_text_calendar(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)597 balsa_print_object_text_calendar(GList * list,
598 GtkPrintContext * context,
599 LibBalsaMessageBody * body,
600 BalsaPrintSetup * psetup)
601 {
602 BalsaPrintObjectDefault *pod;
603 BalsaPrintObject *po;
604 PangoFontDescription *header_font;
605 PangoLayout *test_layout;
606 PangoTabArray *tabs;
607 GString *desc_buf;
608 LibBalsaVCal * vcal_obj;
609 GList * this_ev;
610 guint first_page;
611 GList *par_parts;
612 GList *this_par_part;
613 gdouble c_at_y;
614
615 /* check if we can evaluate the body as calendar object and fall back
616 * to text if not */
617 if (!(vcal_obj = libbalsa_vcal_new_from_body(body)))
618 return balsa_print_object_text(list, context, body, psetup);
619
620 /* proceed with the address information */
621 pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
622 g_assert(pod != NULL);
623 po = BALSA_PRINT_OBJECT(pod);
624
625 /* create the part */
626 po->depth = psetup->curr_depth;
627 po->c_width =
628 psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
629
630 /* get the stock calendar icon or the mime type icon on fail */
631 pod->pixbuf =
632 gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
633 "x-office-calendar", 48,
634 GTK_ICON_LOOKUP_USE_BUILTIN, NULL);
635 if (!pod->pixbuf) {
636 gchar *conttype = libbalsa_message_body_get_mime_type(body);
637
638 pod->pixbuf = libbalsa_icon_finder(NULL, conttype, NULL, NULL,
639 GTK_ICON_SIZE_DND);
640 }
641 pod->c_image_width = gdk_pixbuf_get_width(pod->pixbuf);
642 pod->c_image_height = gdk_pixbuf_get_height(pod->pixbuf);
643
644 /* move to the next page if the icon doesn't fit */
645 if (psetup->c_y_pos + pod->c_image_height > psetup->c_height) {
646 psetup->c_y_pos = 0;
647 psetup->page_count++;
648 }
649
650 /* create a layout for calculating the maximum label width and for splitting
651 * the body (if necessary) */
652 header_font =
653 pango_font_description_from_string(balsa_app.print_header_font);
654 test_layout = gtk_print_context_create_pango_layout(context);
655 pango_layout_set_font_description(test_layout, header_font);
656 pango_font_description_free(header_font);
657
658 /* add fields from the events*/
659 desc_buf = g_string_new("");
660 pod->p_label_width = 0;
661 for (this_ev = vcal_obj->vevent; this_ev; this_ev = g_list_next(this_ev)) {
662 LibBalsaVEvent * event = (LibBalsaVEvent *) this_ev->data;
663
664 if (desc_buf->len > 0)
665 g_string_append_c(desc_buf, '\n');
666 ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
667 event->summary, _("Summary"));
668 ADD_VCAL_ADDRESS(desc_buf, pod->p_label_width, test_layout,
669 event->organizer, _("Organizer"));
670 ADD_VCAL_DATE(desc_buf, pod->p_label_width, test_layout,
671 event->start, _("Start"));
672 ADD_VCAL_DATE(desc_buf, pod->p_label_width, test_layout,
673 event->end, _("End"));
674 ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
675 event->location, _("Location"));
676 if (event->attendee) {
677 GList * att = event->attendee;
678 gchar * this_att;
679
680 this_att =
681 libbalsa_vcal_attendee_to_str(LIBBALSA_ADDRESS(att->data));
682 att = g_list_next(att);
683 ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
684 this_att, att ? _("Attendees") : _("Attendee"));
685 g_free(this_att);
686 for (; att; att = g_list_next(att)) {
687 this_att =
688 libbalsa_vcal_attendee_to_str(LIBBALSA_ADDRESS(att->data));
689 g_string_append_printf(desc_buf, "\n\t%s", this_att);
690 g_free(this_att);
691 }
692 }
693 if (event->description) {
694 gchar ** desc_lines = g_strsplit(event->description, "\n", -1);
695 gint i;
696
697 ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
698 desc_lines[0], _("Description"));
699 for (i = 1; desc_lines[i]; i++)
700 g_string_append_printf(desc_buf, "\n\t%s", desc_lines[i]);
701 g_strfreev(desc_lines);
702 }
703 }
704 g_object_unref(vcal_obj);
705
706 /* add a small space between label and value */
707 pod->p_label_width += C_TO_P(C_LABEL_SEP);
708
709 /* configure the layout so we can split the text */
710 pango_layout_set_indent(test_layout, -pod->p_label_width);
711 tabs =
712 pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT,
713 pod->p_label_width);
714 pango_layout_set_tabs(test_layout, tabs);
715 pango_tab_array_free(tabs);
716 pango_layout_set_width(test_layout,
717 C_TO_P(po->c_width -
718 4 * C_LABEL_SEP - pod->c_image_width));
719 pango_layout_set_alignment(test_layout, PANGO_ALIGN_LEFT);
720
721 /* split paragraph if necessary */
722 first_page = psetup->page_count - 1;
723 c_at_y = psetup->c_y_pos;
724 par_parts =
725 split_for_layout(test_layout, desc_buf->str, NULL, psetup, TRUE, NULL);
726 g_string_free(desc_buf, TRUE);
727
728 /* set the parameters of the first part */
729 pod->description = (gchar *) par_parts->data;
730 pod->c_text_height =
731 P_TO_C(p_string_height_from_layout(test_layout, pod->description));
732 po->on_page = first_page++;
733 po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
734 po->c_at_y = psetup->c_y0 + c_at_y;
735 po->c_height = MAX(pod->c_image_height, pod->c_text_height);
736 list = g_list_append(list, pod);
737
738 /* add more parts */
739 for (this_par_part = g_list_next(par_parts); this_par_part;
740 this_par_part = g_list_next(this_par_part)) {
741 BalsaPrintObjectDefault * new_pod;
742 BalsaPrintObject *new_po;
743
744 /* create a new object */
745 new_pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
746 g_assert(new_pod != NULL);
747 new_po = BALSA_PRINT_OBJECT(new_pod);
748
749 /* fill data */
750 new_pod->p_label_width = pod->p_label_width;
751 new_pod->c_image_width = pod->c_image_width;
752 new_pod->description = (gchar *) this_par_part->data;
753 new_pod->c_text_height =
754 P_TO_C(p_string_height_from_layout(test_layout, new_pod->description));
755 new_po->on_page = first_page++;
756 new_po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
757 new_po->c_at_y = psetup->c_y0;
758 new_po->c_height = new_pod->c_text_height;
759 new_po->depth = psetup->curr_depth;
760 new_po->c_width =
761 psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
762
763 /* append */
764 list = g_list_append(list, new_pod);
765 }
766 g_list_free(par_parts);
767 g_object_unref(G_OBJECT(test_layout));
768
769 return list;
770 }
771
772
773 static void
balsa_print_object_text_draw(BalsaPrintObject * self,GtkPrintContext * context,cairo_t * cairo_ctx)774 balsa_print_object_text_draw(BalsaPrintObject * self,
775 GtkPrintContext * context,
776 cairo_t * cairo_ctx)
777 {
778 BalsaPrintObjectText *po;
779 PangoFontDescription *font;
780 gint p_height;
781 PangoLayout *layout;
782 PangoAttrList *attr_list;
783
784 po = BALSA_PRINT_OBJECT_TEXT(self);
785 g_assert(po != NULL);
786
787 /* prepare */
788 font = pango_font_description_from_string(balsa_app.print_body_font);
789 layout = gtk_print_context_create_pango_layout(context);
790 pango_layout_set_font_description(layout, font);
791 pango_font_description_free(font);
792 pango_layout_set_width(layout,
793 C_TO_P(self->c_width - po->cite_level * C_LABEL_SEP));
794 pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
795 pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
796 pango_layout_set_text(layout, po->text, -1);
797 if ((attr_list = phrase_list_to_pango(po->attributes))) {
798 pango_layout_set_attributes(layout, attr_list);
799 pango_attr_list_unref(attr_list);
800 }
801 pango_layout_get_size(layout, NULL, &p_height);
802 if (po->cite_level > 0) {
803 cairo_save(cairo_ctx);
804 if (balsa_app.print_highlight_cited) {
805 gint k = (po->cite_level - 1) % MAX_QUOTED_COLOR;
806
807 cairo_set_source_rgb(cairo_ctx,
808 balsa_app.quoted_color[k].red,
809 balsa_app.quoted_color[k].green,
810 balsa_app.quoted_color[k].blue);
811 }
812 }
813 cairo_move_to(cairo_ctx, self->c_at_x + po->cite_level * C_LABEL_SEP,
814 self->c_at_y);
815 pango_cairo_show_layout(cairo_ctx, layout);
816 g_object_unref(G_OBJECT(layout));
817 if (po->cite_level > 0) {
818 guint n;
819
820 cairo_new_path(cairo_ctx);
821 cairo_set_line_width(cairo_ctx, 1.0);
822 for (n = 0; n < po->cite_level; n++) {
823 gdouble c_xpos = self->c_at_x + 0.5 + n * C_LABEL_SEP;
824
825 cairo_move_to(cairo_ctx, c_xpos, self->c_at_y);
826 cairo_line_to(cairo_ctx, c_xpos, self->c_at_y + P_TO_C(p_height));
827 }
828 cairo_stroke(cairo_ctx);
829 cairo_restore(cairo_ctx);
830 }
831
832 self->c_height = P_TO_C(p_height); /* needed to properly print borders */
833 }
834
835
836 #define UNICHAR_PREV(p) g_utf8_get_char(g_utf8_prev_char(p))
837
838 static GList *
phrase_highlight(const gchar * buffer,gunichar tag_char,PhraseType tag_type,GList * phrase_list)839 phrase_highlight(const gchar * buffer, gunichar tag_char,
840 PhraseType tag_type, GList * phrase_list)
841 {
842 gchar *utf_start;
843
844 /* find the tag char in the text and scan the buffer for
845 <buffer start or whitespace><tag char><alnum><any text><alnum><tagchar>
846 <whitespace, punctuation or buffer end> */
847 utf_start = g_utf8_strchr(buffer, -1, tag_char);
848 while (utf_start) {
849 gchar *s_next = g_utf8_next_char(utf_start);
850
851 if ((utf_start == buffer
852 || g_unichar_isspace(UNICHAR_PREV(utf_start)))
853 && *s_next != '\0'
854 && g_unichar_isalnum(g_utf8_get_char(s_next))) {
855 gchar *utf_end;
856 gchar *line_end;
857 gchar *e_next;
858
859 /* found a proper start sequence - find the end or eject */
860 if (!(utf_end = g_utf8_strchr(s_next, -1, tag_char)))
861 return phrase_list;
862 line_end = g_utf8_strchr(s_next, -1, '\n');
863 e_next = g_utf8_next_char(utf_end);
864 while (!g_unichar_isalnum(UNICHAR_PREV(utf_end)) ||
865 !(*e_next == '\0' ||
866 g_unichar_isspace(g_utf8_get_char(e_next)) ||
867 g_unichar_ispunct(g_utf8_get_char(e_next)))) {
868 if (!(utf_end = g_utf8_strchr(e_next, -1, tag_char)))
869 return phrase_list;
870 e_next = g_utf8_next_char(utf_end);
871 }
872
873 /* append the attribute if there is no line break */
874 if (!line_end || line_end >= e_next) {
875 PhraseRegion *new_region = g_new0(PhraseRegion, 1);
876
877 new_region->phrase_type = tag_type;
878 new_region->start_index = utf_start - buffer;
879 new_region->end_index = e_next - buffer;
880 phrase_list = g_list_prepend(phrase_list, new_region);
881
882 /* set the next start properly */
883 utf_start =
884 *e_next ? g_utf8_strchr(e_next, -1, tag_char) : NULL;
885 } else
886 utf_start =
887 *s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
888 } else
889 /* no start sequence, find the next start tag char */
890 utf_start =
891 *s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
892 }
893
894 return phrase_list;
895 }
896
897
898 static PangoAttrList *
phrase_list_to_pango(GList * phrase_list)899 phrase_list_to_pango(GList * phrase_list)
900 {
901 PangoAttrList *attr_list;
902 PangoAttribute *ph_attr[PHRASE_TYPE_COUNT];
903 gint n;
904
905 if (!phrase_list)
906 return NULL;
907
908 attr_list = pango_attr_list_new();
909 ph_attr[PHRASE_BF] = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
910 ph_attr[PHRASE_EM] = pango_attr_style_new(PANGO_STYLE_ITALIC);
911 ph_attr[PHRASE_UL] = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
912
913 while (phrase_list) {
914 PhraseRegion *region = (PhraseRegion *) phrase_list->data;
915 PangoAttribute *new_attr;
916
917 new_attr = pango_attribute_copy(ph_attr[region->phrase_type]);
918 new_attr->start_index = region->start_index;
919 new_attr->end_index = region->end_index;
920 pango_attr_list_insert(attr_list, new_attr);
921
922 phrase_list = g_list_next(phrase_list);
923 }
924
925 for (n = 0; n < PHRASE_TYPE_COUNT; n++)
926 pango_attribute_destroy(ph_attr[n]);
927
928 return attr_list;
929 }
930
931
932 static GList *
collect_attrs(GList * all_attr,guint offset,guint len)933 collect_attrs(GList * all_attr, guint offset, guint len)
934 {
935 GList *attr = NULL;
936
937 while (all_attr) {
938 PhraseRegion *region = (PhraseRegion *) all_attr->data;
939
940 if ((region->start_index >= offset
941 && region->start_index <= offset + len)
942 || (region->end_index >= offset
943 && region->end_index <= offset + len)) {
944 PhraseRegion *this_reg =
945 g_memdup(region, sizeof(PhraseRegion));
946
947 if (this_reg->start_index < offset)
948 this_reg->start_index = 0;
949 else
950 this_reg->start_index -= offset;
951 if (this_reg->end_index > offset + len)
952 this_reg->end_index = len;
953 else
954 this_reg->end_index -= offset;
955 attr = g_list_prepend(attr, this_reg);
956 }
957 all_attr = g_list_next(all_attr);
958 }
959
960 return attr;
961 }
962