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