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