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