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