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