Added support for local image attachments to the Litehtml plugin
[claws.git] / src / plugins / litehtml_viewer / lh_widget.cpp
1 /*
2  * Claws Mail -- A GTK+ based, lightweight, and fast e-mail client
3  * Copyright(C) 2019 the Claws Mail Team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  * You should have received a copy of the GNU General Public License
14  * along with this program; if not, write tothe Free Software
15  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  */
17
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #include "claws-features.h"
21 #endif
22
23 #include <glib.h>
24 #include <glib/gi18n.h>
25 #include <glib/gstdio.h>
26 #include <fcntl.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include <curl/curl.h>
30 #include <gdk/gdk.h>
31
32 #include "utils.h"
33
34 #include "litehtml/litehtml.h"
35
36 #include "lh_prefs.h"
37 #include "lh_widget.h"
38 #include "lh_widget_wrapped.h"
39
40 extern "C" {
41 const gchar *prefs_common_get_uri_cmd(void);
42 }
43
44 char master_css[] = {
45 #include "css.inc"
46 };
47
48 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
49                 gpointer user_data);
50 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
51                 gpointer user_data);
52 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
53         gpointer user_data);
54 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
55         gpointer user_data);
56 static void open_link_cb(GtkMenuItem *item, gpointer user_data);
57 static void copy_link_cb(GtkMenuItem *item, gpointer user_data);
58
59 lh_widget::lh_widget()
60 {
61         GtkWidget *item;
62
63         /* scrolled window */
64         m_scrolled_window = gtk_scrolled_window_new(NULL, NULL);
65         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_scrolled_window),
66                         GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
67
68         /* viewport */
69         GtkScrolledWindow *scw = GTK_SCROLLED_WINDOW(m_scrolled_window);
70         m_viewport = gtk_viewport_new(
71                         gtk_scrolled_window_get_hadjustment(scw),
72                         gtk_scrolled_window_get_vadjustment(scw));
73         gtk_container_add(GTK_CONTAINER(m_scrolled_window), m_viewport);
74
75         /* drawing area */
76         m_drawing_area = gtk_drawing_area_new();
77         gtk_container_add(GTK_CONTAINER(m_viewport), m_drawing_area);
78         g_signal_connect(m_drawing_area, "expose-event",
79                         G_CALLBACK(expose_event_cb), this);
80         g_signal_connect(m_drawing_area, "motion_notify_event",
81                         G_CALLBACK(motion_notify_event), this);
82         g_signal_connect(m_drawing_area, "button_press_event",
83                         G_CALLBACK(button_press_event), this);
84         g_signal_connect(m_drawing_area, "button_release_event",
85                         G_CALLBACK(button_release_event), this);
86
87         gtk_widget_show_all(m_scrolled_window);
88
89         /* context menu */
90         m_context_menu = gtk_menu_new();
91
92         item = gtk_menu_item_new_with_label(_("Open Link"));
93         g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(open_link_cb), this);
94         gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
95
96         item = gtk_menu_item_new_with_label(_("Copy Link Location"));
97         g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(copy_link_cb), this);
98         gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
99
100         m_html = NULL;
101         m_rendered_width = 0;
102         m_context.load_master_stylesheet(master_css);
103
104         m_font_name = NULL;
105         m_font_size = 0;
106
107         m_partinfo = NULL;
108
109         gtk_widget_set_events(m_drawing_area,
110                                 GDK_BUTTON_RELEASE_MASK
111                               | GDK_BUTTON_PRESS_MASK
112                               | GDK_POINTER_MOTION_MASK);
113 }
114
115 lh_widget::~lh_widget()
116 {
117         g_object_unref(m_drawing_area);
118         m_drawing_area = NULL;
119         g_object_unref(m_scrolled_window);
120         m_scrolled_window = NULL;
121         m_html = NULL;
122 }
123
124 GtkWidget *lh_widget::get_widget() const
125 {
126         return m_scrolled_window;
127 }
128
129 void lh_widget::set_caption(const litehtml::tchar_t* caption)
130 {
131         debug_print("lh_widget set_caption\n");
132         return;
133 }
134
135 void lh_widget::set_base_url(const litehtml::tchar_t* base_url)
136 {
137         debug_print("lh_widget set_base_url '%s'\n",
138                         (base_url ? base_url : "(null)"));
139         m_base_url = base_url;
140         return;
141 }
142
143 void lh_widget::on_anchor_click(const litehtml::tchar_t* url, const litehtml::element::ptr& el)
144 {
145         debug_print("lh_widget on_anchor_click. url -> %s\n", url);
146
147         m_clicked_url = fullurl(url);
148         return;
149 }
150
151 void lh_widget::import_css(litehtml::tstring& text, const litehtml::tstring& url, litehtml::tstring& baseurl)
152 {
153         debug_print("lh_widget import_css\n");
154         baseurl = master_css;
155 }
156
157 void lh_widget::get_client_rect(litehtml::position& client) const
158 {
159         if (m_drawing_area == NULL)
160                 return;
161
162         client.width = m_rendered_width;
163         client.height = m_height;
164         client.x = 0;
165         client.y = 0;
166
167 //      debug_print("lh_widget::get_client_rect: %dx%d\n",
168 //                      client.width, client.height);
169 }
170
171 void lh_widget::open_html(const gchar *contents)
172 {
173         gint num = clear_images(lh_prefs_get()->image_cache_size * 1024 * 1000);
174         GtkAdjustment *adj;
175
176         debug_print("LH: cleared %d images from image cache\n", num);
177
178         update_font();
179
180         lh_widget_statusbar_push("Loading HTML part ...");
181         m_html = litehtml::document::createFromString(contents, this, &m_context);
182         m_rendered_width = 0;
183         if (m_html != NULL) {
184                 debug_print("lh_widget::open_html created document\n");
185                 adj = gtk_scrolled_window_get_hadjustment(
186                                 GTK_SCROLLED_WINDOW(m_scrolled_window));
187                 gtk_adjustment_set_value(adj, 0.0);
188                 adj = gtk_scrolled_window_get_vadjustment(
189                                 GTK_SCROLLED_WINDOW(m_scrolled_window));
190                 gtk_adjustment_set_value(adj, 0.0);
191                 redraw(false);
192         }
193         lh_widget_statusbar_pop();
194 }
195
196 void lh_widget::draw(cairo_t *cr)
197 {
198         double x1, x2, y1, y2;
199         double width, height;
200
201         if (m_html == NULL)
202                 return;
203
204         cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
205
206         width = x2 - x1;
207         height = y2 - y1;
208
209         litehtml::position pos;
210         pos.width = (int)width;
211         pos.height = (int)height;
212         pos.x = (int)x1;
213         pos.y = (int)y1;
214
215         m_html->draw((litehtml::uint_ptr)cr, 0, 0, &pos);
216 }
217
218 void lh_widget::redraw(gboolean force_render)
219 {
220         GtkAllocation rect;
221         gint width;
222         GdkWindow *gdkwin;
223         cairo_t *cr;
224
225         paint_white();
226
227         if (m_html == NULL)
228                 return;
229
230         /* Get width of the viewport. */
231         gdkwin = gtk_viewport_get_view_window(GTK_VIEWPORT(m_viewport));
232         width = gdk_window_get_width(gdkwin);
233         m_height = gdk_window_get_height(gdkwin);
234
235         /* If the available width has changed, rerender the HTML content. */
236         if (m_rendered_width != width || force_render) {
237                 debug_print("lh_widget::redraw: width changed: %d != %d\n",
238                                 m_rendered_width, width);
239
240                 /* Update our internally stored width, mainly so that
241                  * lh_widget::get_client_rect() gives correct width during the
242                  * render. */
243                 m_rendered_width = width;
244
245                 /* Re-render HTML for this width. */
246                 m_html->media_changed();
247                 m_html->render(m_rendered_width);
248                 debug_print("render is %dx%d\n", m_html->width(), m_html->height());
249
250                 /* Change drawing area's size to match what was rendered. */
251                 gtk_widget_set_size_request(m_drawing_area,
252                                 m_html->width(), m_html->height());
253         }
254
255         /* Paint the rendered HTML. */
256         gdkwin = gtk_widget_get_window(m_drawing_area);
257         if (gdkwin == NULL) {
258                 g_warning("lh_widget::redraw: No GdkWindow to draw on!");
259                 return;
260         }
261         cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
262         draw(cr);
263
264         cairo_destroy(cr);
265 }
266
267 void lh_widget::paint_white()
268 {
269         GdkWindow *gdkwin = gtk_widget_get_window(m_drawing_area);
270         if (gdkwin == NULL) {
271                 g_warning("lh_widget::clear: No GdkWindow to draw on!");
272                 return;
273         }
274         cairo_t *cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
275
276         /* Paint white background. */
277         gint width, height;
278         gdk_drawable_get_size(gdkwin, &width, &height);
279         cairo_rectangle(cr, 0, 0, width, height);
280         cairo_set_source_rgb(cr, 255, 255, 255);
281         cairo_fill(cr);
282
283         cairo_destroy(cr);
284 }
285 void lh_widget::clear()
286 {
287         m_html = nullptr;
288         paint_white();
289         m_rendered_width = 0;
290         m_base_url.clear();
291         m_clicked_url.clear();
292 }
293
294 void lh_widget::set_cursor(const litehtml::tchar_t* cursor)
295 {
296         if (cursor) {
297                 if (m_cursor != cursor) {
298                         m_cursor = cursor;
299                         update_cursor();
300                 }
301         }
302 }
303
304 void lh_widget::update_cursor()
305 {
306         gint x, y;
307         const litehtml::tchar_t *href;
308         GdkWindow *w = gdk_display_get_window_at_pointer(gdk_display_get_default(),
309                         &x, &y);
310         GdkCursorType cursType = GDK_ARROW;
311
312         if (m_cursor == _t("pointer")) {
313                 cursType = GDK_HAND2;
314         }
315
316         if (cursType == GDK_ARROW) {
317                 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), NULL);
318         } else {
319                 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), gdk_cursor_new(cursType));
320         }
321
322         if (w != gtk_widget_get_window(m_drawing_area))
323                 return;
324
325         /* If it's an anchor, show its "href" attribute in statusbar,
326          * otherwise clear statusbar. */
327         if ((href = get_href_at(x, y)) != NULL) {
328                 lh_widget_statusbar_push(fullurl(href).c_str());
329         } else {
330                 lh_widget_statusbar_pop();
331         }
332 }
333
334 const litehtml::tchar_t *lh_widget::get_href_at(const gint x, const gint y) const
335 {
336         litehtml::element::ptr over_el, el;
337
338         if (m_html == NULL)
339                 return NULL;
340
341         over_el = m_html->root()->get_element_by_point(x, y, x, y);
342         if (over_el == NULL)
343                 return NULL;
344
345         /* If it's not an anchor, check if it has a parent anchor
346          * (e.g. it's an image within an anchor) and grab a pointer
347          * to that. */
348         if (strcmp(over_el->get_tagName(), "a") && over_el->parent()) {
349                 el = over_el->parent();
350                 while (el && el != m_html->root() && strcmp(el->get_tagName(), "a")) {
351                         el = el->parent();
352                 }
353
354                 if (el && el != m_html->root())
355                         over_el = el;
356                 else
357                         return NULL;
358         }
359
360         /* At this point, over_el is pointing at an anchor tag, so let's
361          * grab its href attribute. */
362         return over_el->get_attr(_t("href"));
363 }
364
365 void lh_widget::print()
366 {
367     debug_print("lh_widget print\n");
368     gtk_widget_realize(GTK_WIDGET(m_drawing_area));
369 }
370
371 void lh_widget::popup_context_menu(const litehtml::tchar_t *url,
372                 GdkEventButton *event)
373 {
374         cm_return_if_fail(url != NULL);
375         cm_return_if_fail(event != NULL);
376
377         debug_print("lh_widget showing context menu for '%s'\n", url);
378
379         m_clicked_url = url;
380         gtk_widget_show_all(m_context_menu);
381         gtk_menu_popup(GTK_MENU(m_context_menu), NULL, NULL, NULL, NULL,
382                         event->button, event->time);
383 }
384
385 void lh_widget::update_font()
386 {
387         PangoFontDescription *pd =
388                 pango_font_description_from_string(lh_prefs_get()->default_font);
389         gboolean absolute = pango_font_description_get_size_is_absolute(pd);
390
391         g_free(m_font_name);
392         m_font_name = g_strdup(pango_font_description_get_family(pd));
393         m_font_size = pango_font_description_get_size(pd);
394
395         pango_font_description_free(pd);
396
397         if (!absolute)
398                 m_font_size /= PANGO_SCALE;
399
400         debug_print("Font set to '%s', size %d\n", m_font_name, m_font_size);
401 }
402
403 const litehtml::tstring lh_widget::fullurl(const litehtml::tchar_t *url) const
404 {
405         if (*url == '#' && !m_base_url.empty())
406                 return m_base_url + url;
407
408         return _t(url);
409 }
410
411 void lh_widget::set_partinfo(MimeInfo *partinfo)
412 {
413         m_partinfo = partinfo;
414 }
415
416 GdkPixbuf *lh_widget::get_local_image(const litehtml::tstring url) const
417 {
418         GdkPixbuf *pixbuf;
419         const gchar *name;
420         MimeInfo *p = m_partinfo;
421
422         if (strncmp(url.c_str(), "cid:", 4) != 0) {
423                 debug_print("lh_widget::get_local_image: '%s' is not a local URI, ignoring\n", url.c_str());
424                 return NULL;
425         }
426
427         name = url.c_str() + 4;
428         debug_print("getting message part '%s'\n", name);
429
430         while ((p = procmime_mimeinfo_next(p)) != NULL) {
431                 size_t len = strlen(name);
432
433                 /* p->id is in format "<partname>" */
434                 if (!strncasecmp(name, p->id + 1, len) &&
435                                 strlen(p->id) >= len + 2 &&
436                                 *(p->id + len + 1) == '>') {
437                         GInputStream *stream;
438                         GError *error = NULL;
439
440                         stream = procmime_get_part_as_inputstream(p, &error);
441                         if (error != NULL) {
442                                 g_warning("Couldn't get image MIME part: %s\n", error->message);
443                                 g_error_free(error);
444                                 return NULL;
445                         }
446
447                         pixbuf = gdk_pixbuf_new_from_stream(stream, NULL, &error);
448                         g_object_unref(stream);
449                         if (error != NULL) {
450                                 g_warning("Couldn't load image: %s\n", error->message);
451                                 g_error_free(error);
452                                 return NULL;
453                         }
454
455                         return pixbuf;
456                 }
457         }
458
459         /* MIME part with requested name was not found */
460         return NULL;
461 }
462
463 ////////////////////////////////////////////////
464 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
465                 gpointer user_data)
466 {
467         lh_widget *w = (lh_widget *)user_data;
468         w->redraw(false);
469         return FALSE;
470 }
471
472 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
473                 gpointer user_data)
474 {
475         litehtml::position::vector redraw_boxes;
476         lh_widget *w = (lh_widget *)user_data;
477
478         if (w->m_html == NULL)
479                 return false;
480
481         //debug_print("lh_widget on_button_press_event\n");
482
483         if (event->type == GDK_2BUTTON_PRESS ||
484                         event->type == GDK_3BUTTON_PRESS)
485                 return true;
486
487         /* Right-click */
488         if (event->button == 3) {
489                 const litehtml::tchar_t *url = w->get_href_at((gint)event->x, (gint)event->y);
490
491                 if (url != NULL)
492                         w->popup_context_menu(url, event);
493
494                 return true;
495         }
496
497         if(w->m_html->on_lbutton_down((int) event->x, (int) event->y,
498                                 (int) event->x, (int) event->y, redraw_boxes)) {
499                 for(auto& pos : redraw_boxes) {
500                         debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
501                         gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
502                 }
503         }
504         
505         return true;
506 }
507
508 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
509         gpointer user_data)
510 {
511     litehtml::position::vector redraw_boxes;
512     lh_widget *w = (lh_widget *)user_data;
513     
514     //debug_print("lh_widget on_motion_notify_event\n");
515
516     if(w->m_html)
517     {    
518         if(w->m_html->on_mouse_over((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
519         {
520             for (auto& pos : redraw_boxes)
521             {
522                 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
523                 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
524             }
525         }
526         }
527         
528         return true;
529 }
530
531 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
532         gpointer user_data)
533 {
534     litehtml::position::vector redraw_boxes;
535     lh_widget *w = (lh_widget *)user_data;
536     GError* error = NULL;
537
538         if (w->m_html == NULL)
539                 return false;
540
541         //debug_print("lh_widget on_button_release_event\n");
542
543         if (event->type == GDK_2BUTTON_PRESS ||
544                         event->type == GDK_3BUTTON_PRESS)
545                 return true;
546
547         /* Right-click */
548         if (event->button == 3)
549                 return true;
550
551         w->m_clicked_url.clear();
552
553     if(w->m_html->on_lbutton_up((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
554     {
555         for (auto& pos : redraw_boxes)
556         {
557             debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
558             gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
559         }
560     }
561
562     if (!w->m_clicked_url.empty())
563     {
564             debug_print("Open in browser: %s\n", w->m_clicked_url.c_str());
565             open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
566     }
567
568         return true;
569 }
570
571 static void open_link_cb(GtkMenuItem *item, gpointer user_data)
572 {
573         lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
574
575         open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
576 }
577
578 static void copy_link_cb(GtkMenuItem *item, gpointer user_data)
579 {
580         lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
581
582         gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
583                         w->m_clicked_url.c_str(), -1);
584         gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD),
585                         w->m_clicked_url.c_str(), -1);
586 }
587
588 ///////////////////////////////////////////////////////////
589 extern "C" {
590
591 lh_widget_wrapped *lh_widget_new()
592 {
593         return new lh_widget;
594 }
595
596 GtkWidget *lh_widget_get_widget(lh_widget_wrapped *w)
597 {
598         return w->get_widget();
599 }
600
601 void lh_widget_open_html(lh_widget_wrapped *w, const gchar *path)
602 {
603         w->open_html(path);
604 }
605
606 void lh_widget_clear(lh_widget_wrapped *w)
607 {
608         w->clear();
609 }
610
611 void lh_widget_destroy(lh_widget_wrapped *w)
612 {
613         delete w;
614 }
615
616 void lh_widget_print(lh_widget_wrapped *w) {
617         w->print();
618 }
619
620 void lh_widget_set_partinfo(lh_widget_wrapped *w, MimeInfo *partinfo)
621 {
622         w->set_partinfo(partinfo);
623 }
624
625 } /* extern "C" */