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