Make Litehtml plugin open links in configured way
[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/gstdio.h>
27 #include <fcntl.h>
28 #include <sys/types.h>
29 #include <sys/stat.h>
30 #include <curl/curl.h>
31 #include <gdk/gdk.h>
32
33 #include "utils.h"
34
35 #include "litehtml/litehtml.h"
36
37 #include "lh_prefs.h"
38 #include "lh_widget.h"
39 #include "lh_widget_wrapped.h"
40 #include "http.h"
41
42 extern "C" {
43 const gchar *prefs_common_get_uri_cmd(void);
44 }
45
46 char master_css[] = {
47 #include "css.inc"
48 };
49
50 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
51                 gpointer user_data);
52 static void size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation,
53                 gpointer user_data);
54 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
55                 gpointer user_data);
56 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
57         gpointer user_data);
58 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
59         gpointer user_data);
60
61 lh_widget::lh_widget()
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         g_signal_connect(m_scrolled_window, "size-allocate",
68                         G_CALLBACK(size_allocate_cb), this);
69
70         /* viewport */
71         GtkScrolledWindow *scw = GTK_SCROLLED_WINDOW(m_scrolled_window);
72         m_viewport = gtk_viewport_new(
73                         gtk_scrolled_window_get_hadjustment(scw),
74                         gtk_scrolled_window_get_vadjustment(scw));
75         gtk_container_add(GTK_CONTAINER(m_scrolled_window), m_viewport);
76
77         /* drawing area */
78         m_drawing_area = gtk_drawing_area_new();
79         gtk_container_add(GTK_CONTAINER(m_viewport), m_drawing_area);
80         g_signal_connect(m_drawing_area, "expose-event",
81                         G_CALLBACK(expose_event_cb), this);
82         g_signal_connect(m_drawing_area, "motion_notify_event",
83                         G_CALLBACK(motion_notify_event), this);
84         g_signal_connect(m_drawing_area, "button_press_event",
85                         G_CALLBACK(button_press_event), this);
86         g_signal_connect(m_drawing_area, "button_release_event",
87                         G_CALLBACK(button_release_event), this);
88
89         gtk_widget_show_all(m_scrolled_window);
90
91         m_html = NULL;
92         m_rendered_width = 0;
93         m_context.load_master_stylesheet(master_css);
94
95         gtk_widget_set_events(m_drawing_area,
96                                 GDK_BUTTON_RELEASE_MASK
97                               | GDK_BUTTON_PRESS_MASK
98                               | GDK_POINTER_MOTION_MASK);
99 }
100
101 lh_widget::~lh_widget()
102 {
103         g_object_unref(m_drawing_area);
104         m_drawing_area = NULL;
105         g_object_unref(m_scrolled_window);
106         m_scrolled_window = NULL;
107         m_html = NULL;
108 }
109
110 GtkWidget *lh_widget::get_widget() const
111 {
112         return m_scrolled_window;
113 }
114
115 void lh_widget::set_caption(const litehtml::tchar_t* caption)
116 {
117         debug_print("lh_widget set_caption\n");
118         return;
119 }
120
121 void lh_widget::set_base_url(const litehtml::tchar_t* base_url)
122 {
123         debug_print("lh_widget set_base_url\n");
124         return;
125 }
126
127 void lh_widget::on_anchor_click(const litehtml::tchar_t* url, const litehtml::element::ptr& el)
128 {
129         debug_print("lh_widget on_anchor_click. url -> %s\n", url);
130         m_clicked_url = url;
131         
132         return;
133 }
134
135 void lh_widget::import_css(litehtml::tstring& text, const litehtml::tstring& url, litehtml::tstring& baseurl)
136 {
137         debug_print("lh_widget import_css\n");
138         baseurl = master_css;
139 }
140
141 void lh_widget::get_client_rect(litehtml::position& client) const
142 {
143         if (m_drawing_area == NULL)
144                 return;
145
146         client.width = m_rendered_width;
147         client.height = m_height;
148         client.x = 0;
149         client.y = 0;
150
151 //      debug_print("lh_widget::get_client_rect: %dx%d\n",
152 //                      client.width, client.height);
153 }
154
155 GdkPixbuf *lh_widget::get_image(const litehtml::tchar_t* url, bool redraw_on_ready)
156 {
157         GError *error = NULL;
158         GdkPixbuf *pixbuf = NULL;
159         http* http_loader = NULL;
160
161         if (!lh_prefs_get()->enable_remote_content) {
162                 debug_print("blocking download of image from '%s'\n", url);
163                 return NULL;
164         }
165
166         debug_print("Loading... %s\n", url);
167         gchar *msg = g_strdup_printf("Loading %s ...", url);
168         lh_widget_statusbar_push(msg);
169         g_free(msg);
170         
171         http_loader = new http();
172         GInputStream *image = http_loader->load_url(url, &error);
173     
174         if (error || !image) {
175             if (error) {
176                 g_warning("lh_widget::get_image: Could not create pixbuf %s", error->message);
177                 g_clear_error(&error);
178             }
179             goto statusbar_pop;
180         }
181
182         pixbuf = gdk_pixbuf_new_from_stream(image, NULL, &error);
183         if (error) {
184             g_warning("lh_widget::get_image: Could not create pixbuf %s", error->message);
185             pixbuf = NULL;
186             g_clear_error(&error);
187         }
188
189 /*      if (redraw_on_ready) {
190                 redraw();
191         }*/
192
193 statusbar_pop:
194         lh_widget_statusbar_pop();
195         if (http_loader) {
196                 delete http_loader;
197         }
198         
199         return pixbuf;
200 }
201
202 void lh_widget::open_html(const gchar *contents)
203 {
204         lh_widget_statusbar_push("Loading HTML part ...");
205         m_html = litehtml::document::createFromString(contents, this, &m_context);
206         m_rendered_width = 0;
207         if (m_html != NULL) {
208                 debug_print("lh_widget::open_html created document\n");
209                 redraw();
210         }
211         lh_widget_statusbar_pop();
212 }
213
214 void lh_widget::draw(cairo_t *cr)
215 {
216         double x1, x2, y1, y2;
217         double width, height;
218
219         if (m_html == NULL)
220                 return;
221
222         cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
223
224         width = x2 - x1;
225         height = y2 - y1;
226
227         litehtml::position pos;
228         pos.width = (int)width;
229         pos.height = (int)height;
230         pos.x = (int)x1;
231         pos.y = (int)y1;
232
233         m_html->draw((litehtml::uint_ptr)cr, 0, 0, &pos);
234 }
235
236 void lh_widget::redraw()
237 {
238         GtkAllocation rect;
239         gint width, height;
240         GdkWindow *gdkwin;
241         cairo_t *cr;
242
243         if (m_html == NULL) {
244                 g_warning("lh_widget::redraw: No document!");
245                 return;
246         }
247
248         /* Get width of the viewport. */
249         gdkwin = gtk_viewport_get_view_window(GTK_VIEWPORT(m_viewport));
250         gdk_drawable_get_size(gdkwin, &width, NULL);
251
252         /* If the available width has changed, rerender the HTML content. */
253         if (m_rendered_width != width) {
254                 debug_print("lh_widget::redraw: width changed: %d != %d\n",
255                                 m_rendered_width, width);
256
257                 /* Update our internally stored width, mainly so that
258                  * lh_widget::get_client_rect() gives correct width during the
259                  * render. */
260                 m_rendered_width = width;
261
262                 /* Re-render HTML for this width. */
263                 m_html->media_changed();
264                 m_html->render(m_rendered_width);
265                 debug_print("render is %dx%d\n", m_html->width(), m_html->height());
266
267                 /* Change drawing area's size to match what was rendered. */
268                 gtk_widget_set_size_request(m_drawing_area,
269                                 m_html->width(), m_html->height());
270         }
271
272         paint_white();
273
274         /* Paint the rendered HTML. */
275         gdkwin = gtk_widget_get_window(m_drawing_area);
276         if (gdkwin == NULL) {
277                 g_warning("lh_widget::redraw: No GdkWindow to draw on!");
278                 return;
279         }
280         cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
281         draw(cr);
282
283         cairo_destroy(cr);
284 }
285
286 void lh_widget::paint_white()
287 {
288         GdkWindow *gdkwin = gtk_widget_get_window(m_drawing_area);
289         if (gdkwin == NULL) {
290                 g_warning("lh_widget::clear: No GdkWindow to draw on!");
291                 return;
292         }
293         cairo_t *cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
294
295         /* Paint white background. */
296         gint width, height;
297         gdk_drawable_get_size(gdkwin, &width, &height);
298         cairo_rectangle(cr, 0, 0, width, height);
299         cairo_set_source_rgb(cr, 255, 255, 255);
300         cairo_fill(cr);
301
302         cairo_destroy(cr);
303 }
304 void lh_widget::clear()
305 {
306         paint_white();
307         m_rendered_width = 0;
308 }
309
310 void lh_widget::set_cursor(const litehtml::tchar_t* cursor)
311 {
312         if (cursor) {
313                 if (m_cursor != cursor) {
314                         m_cursor = cursor;
315                         update_cursor();
316                 }
317         }
318 }
319
320 void lh_widget::update_cursor()
321 {
322         gint x, y;
323         litehtml::element::ptr root_el, over_el, el;
324         GdkWindow *w = gdk_display_get_window_at_pointer(gdk_display_get_default(),
325                         &x, &y);
326         GdkCursorType cursType = GDK_ARROW;
327
328         if (m_cursor == _t("pointer")) {
329                 cursType = GDK_HAND2;
330         }
331
332         if (cursType == GDK_ARROW) {
333                 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), NULL);
334         } else {
335                 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), gdk_cursor_new(cursType));
336         }
337
338         if (w != gtk_widget_get_window(m_drawing_area))
339                 return;
340
341         /* Find the element we are hovering over */
342         root_el = m_html->root();
343         g_return_if_fail(root_el != NULL);
344         over_el = root_el->get_element_by_point(x, y, x, y);
345         g_return_if_fail(over_el != 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(over_el->get_tagName(), "a") && over_el->parent()) {
351                 el = over_el->parent();
352                 while (el && el != root_el && strcmp(el->get_tagName(), "a")) {
353                         el = el->parent();
354                 }
355
356                 if (el && el != root_el)
357                         over_el = el;
358         }
359
360         /* If it's an anchor, show its "href" attribute in statusbar,
361          * otherwise clear statusbar. */
362         if (!strcmp(over_el->get_tagName(), "a")) {
363                 lh_widget_statusbar_push(over_el->get_attr(_t("href")));
364         } else {
365                 lh_widget_statusbar_pop();
366         }
367 }
368
369 void lh_widget::print()
370 {
371     debug_print("lh_widget print\n");
372     gtk_widget_realize(GTK_WIDGET(m_drawing_area));
373 }
374
375 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
376                 gpointer user_data)
377 {
378         lh_widget *w = (lh_widget *)user_data;
379         w->redraw();
380         return FALSE;
381 }
382
383 static void size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation,
384                 gpointer user_data)
385 {
386         lh_widget *w = (lh_widget *)user_data;
387
388         debug_print("size_allocate_cb: %dx%d\n",
389                         allocation->width, allocation->height);
390
391         w->setHeight(allocation->height);
392         w->redraw();
393 }
394
395 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
396                 gpointer user_data)
397 {
398     litehtml::position::vector redraw_boxes;
399     lh_widget *w = (lh_widget *)user_data;
400     
401     debug_print("lh_widget on_button_press_event\n");
402
403     if(w->m_html)
404     {    
405         if(w->m_html->on_lbutton_down((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
406         {
407             for(auto& pos : redraw_boxes)
408             {
409                 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
410                 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
411             }
412         }
413         }
414         
415         return true;
416 }
417
418 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
419         gpointer user_data)
420 {
421     litehtml::position::vector redraw_boxes;
422     lh_widget *w = (lh_widget *)user_data;
423     
424     //debug_print("lh_widget on_motion_notify_event\n");
425
426     if(w->m_html)
427     {    
428         //if(m_cursor == _t("pointer"))
429         if(w->m_html->on_mouse_over((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
430         {
431             for (auto& pos : redraw_boxes)
432             {
433                 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
434                 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
435             }
436         }
437         }
438         
439         return true;
440 }
441
442 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
443         gpointer user_data)
444 {
445     litehtml::position::vector redraw_boxes;
446     lh_widget *w = (lh_widget *)user_data;
447     GError* error = NULL;
448
449         debug_print("lh_widget on_button_release_event\n");
450         
451         if(w->m_html)
452         {
453             w->m_clicked_url.clear();
454             if(w->m_html->on_lbutton_up((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
455         {
456             for (auto& pos : redraw_boxes)
457             {
458                 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
459                 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
460             }
461         }
462         
463         if (!w->m_clicked_url.empty())
464         {
465                 debug_print("Open in browser: %s\n", w->m_clicked_url.c_str());
466                 open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
467         }
468     }
469
470         return true;
471 }
472
473 ///////////////////////////////////////////////////////////
474 extern "C" {
475
476 lh_widget_wrapped *lh_widget_new()
477 {
478         return new lh_widget;
479 }
480
481 GtkWidget *lh_widget_get_widget(lh_widget_wrapped *w)
482 {
483         return w->get_widget();
484 }
485
486 void lh_widget_open_html(lh_widget_wrapped *w, const gchar *path)
487 {
488         w->open_html(path);
489 }
490
491 void lh_widget_clear(lh_widget_wrapped *w)
492 {
493         w->clear();
494 }
495
496 void lh_widget_destroy(lh_widget_wrapped *w)
497 {
498         delete w;
499 }
500
501 void lh_widget_print(lh_widget_wrapped *w) {
502         w->print();
503 }
504
505 } /* extern "C" */