2 * Claws Mail -- A GTK+ based, lightweight, and fast e-mail client
3 * Copyright(C) 2019 the Claws Mail Team
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.
20 #include "claws-features.h"
24 #include <glib/gi18n.h>
25 #include <glib/gstdio.h>
27 #include <sys/types.h>
29 #include <curl/curl.h>
34 #include "litehtml/litehtml.h"
37 #include "lh_widget.h"
38 #include "lh_widget_wrapped.h"
41 const gchar *prefs_common_get_uri_cmd(void);
48 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
50 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
52 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
54 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
56 static void open_link_cb(GtkMenuItem *item, gpointer user_data);
57 static void copy_link_cb(GtkMenuItem *item, gpointer user_data);
59 lh_widget::lh_widget()
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);
69 GtkScrolledWindow *scw = GTK_SCROLLED_WINDOW(m_scrolled_window);
70 m_viewport = gtk_viewport_new(
71 gtk_scrolled_window_get_hadjustment(scw),
72 gtk_scrolled_window_get_vadjustment(scw));
73 gtk_container_add(GTK_CONTAINER(m_scrolled_window), m_viewport);
76 m_drawing_area = gtk_drawing_area_new();
77 gtk_container_add(GTK_CONTAINER(m_viewport), m_drawing_area);
78 g_signal_connect(m_drawing_area, "expose-event",
79 G_CALLBACK(expose_event_cb), this);
80 g_signal_connect(m_drawing_area, "motion_notify_event",
81 G_CALLBACK(motion_notify_event), this);
82 g_signal_connect(m_drawing_area, "button_press_event",
83 G_CALLBACK(button_press_event), this);
84 g_signal_connect(m_drawing_area, "button_release_event",
85 G_CALLBACK(button_release_event), this);
87 gtk_widget_show_all(m_scrolled_window);
90 m_context_menu = gtk_menu_new();
92 item = gtk_menu_item_new_with_label(_("Open Link"));
93 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(open_link_cb), this);
94 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
96 item = gtk_menu_item_new_with_label(_("Copy Link Location"));
97 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(copy_link_cb), this);
98 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
101 m_rendered_width = 0;
102 m_context.load_master_stylesheet(master_css);
109 m_showing_url = FALSE;
111 gtk_widget_set_events(m_drawing_area,
112 GDK_BUTTON_RELEASE_MASK
113 | GDK_BUTTON_PRESS_MASK
114 | GDK_POINTER_MOTION_MASK);
117 lh_widget::~lh_widget()
119 g_object_unref(m_drawing_area);
120 m_drawing_area = NULL;
121 g_object_unref(m_scrolled_window);
122 m_scrolled_window = NULL;
126 GtkWidget *lh_widget::get_widget() const
128 return m_scrolled_window;
131 void lh_widget::set_caption(const litehtml::tchar_t* caption)
133 debug_print("lh_widget set_caption\n");
137 void lh_widget::set_base_url(const litehtml::tchar_t* base_url)
139 debug_print("lh_widget set_base_url '%s'\n",
140 (base_url ? base_url : "(null)"));
141 m_base_url = base_url;
145 void lh_widget::on_anchor_click(const litehtml::tchar_t* url, const litehtml::element::ptr& el)
147 debug_print("lh_widget on_anchor_click. url -> %s\n", url);
149 m_clicked_url = fullurl(url);
153 void lh_widget::import_css(litehtml::tstring& text, const litehtml::tstring& url, litehtml::tstring& baseurl)
155 debug_print("lh_widget import_css\n");
156 baseurl = master_css;
159 void lh_widget::get_client_rect(litehtml::position& client) const
161 if (m_drawing_area == NULL)
164 client.width = m_rendered_width;
165 client.height = m_height;
169 // debug_print("lh_widget::get_client_rect: %dx%d\n",
170 // client.width, client.height);
173 void lh_widget::open_html(const gchar *contents)
175 gint num = clear_images(lh_prefs_get()->image_cache_size * 1024 * 1000);
178 debug_print("LH: cleared %d images from image cache\n", num);
182 lh_widget_statusbar_push("Loading HTML part ...");
183 m_html = litehtml::document::createFromString(contents, this, &m_context);
184 m_rendered_width = 0;
185 if (m_html != NULL) {
186 debug_print("lh_widget::open_html created document\n");
187 adj = gtk_scrolled_window_get_hadjustment(
188 GTK_SCROLLED_WINDOW(m_scrolled_window));
189 gtk_adjustment_set_value(adj, 0.0);
190 adj = gtk_scrolled_window_get_vadjustment(
191 GTK_SCROLLED_WINDOW(m_scrolled_window));
192 gtk_adjustment_set_value(adj, 0.0);
195 lh_widget_statusbar_pop();
198 void lh_widget::draw(cairo_t *cr)
200 double x1, x2, y1, y2;
201 double width, height;
206 cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
211 litehtml::position pos;
212 pos.width = (int)width;
213 pos.height = (int)height;
217 m_html->draw((litehtml::uint_ptr)cr, 0, 0, &pos);
220 void lh_widget::redraw(gboolean force_render)
232 /* Get width of the viewport. */
233 gdkwin = gtk_viewport_get_view_window(GTK_VIEWPORT(m_viewport));
234 width = gdk_window_get_width(gdkwin);
235 m_height = gdk_window_get_height(gdkwin);
237 /* If the available width has changed, rerender the HTML content. */
238 if (m_rendered_width != width || force_render) {
239 debug_print("lh_widget::redraw: width changed: %d != %d\n",
240 m_rendered_width, width);
242 /* Update our internally stored width, mainly so that
243 * lh_widget::get_client_rect() gives correct width during the
245 m_rendered_width = width;
247 /* Re-render HTML for this width. */
248 m_html->media_changed();
249 m_html->render(m_rendered_width);
250 debug_print("render is %dx%d\n", m_html->width(), m_html->height());
252 /* Change drawing area's size to match what was rendered. */
253 gtk_widget_set_size_request(m_drawing_area,
254 m_html->width(), m_html->height());
257 /* Paint the rendered HTML. */
258 gdkwin = gtk_widget_get_window(m_drawing_area);
259 if (gdkwin == NULL) {
260 g_warning("lh_widget::redraw: No GdkWindow to draw on!");
263 cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
269 void lh_widget::paint_white()
271 GdkWindow *gdkwin = gtk_widget_get_window(m_drawing_area);
272 if (gdkwin == NULL) {
273 g_warning("lh_widget::clear: No GdkWindow to draw on!");
276 cairo_t *cr = gdk_cairo_create(GDK_DRAWABLE(gdkwin));
278 /* Paint white background. */
280 gdk_drawable_get_size(gdkwin, &width, &height);
281 cairo_rectangle(cr, 0, 0, width, height);
282 cairo_set_source_rgb(cr, 255, 255, 255);
287 void lh_widget::clear()
291 m_rendered_width = 0;
293 m_clicked_url.clear();
296 void lh_widget::set_cursor(const litehtml::tchar_t* cursor)
298 litehtml::element::ptr over_el;
300 GdkWindow *w = gdk_display_get_window_at_pointer(gdk_display_get_default(),
303 if (w != gtk_widget_get_window(m_drawing_area))
306 over_el = m_html->root()->get_element_by_point(x, y, x, y);
309 m_over_element = NULL;
313 if (cursor && over_el) {
314 if (over_el != m_over_element) {
315 m_over_element = over_el;
316 update_cursor(cursor);
321 void lh_widget::update_cursor(const litehtml::tchar_t* cursor)
323 const litehtml::tchar_t *href;
324 GdkCursorType cursType = GDK_ARROW;
326 if (cursor == _t("pointer")) {
327 cursType = GDK_HAND2;
330 if (cursType == GDK_ARROW) {
331 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), NULL);
333 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), gdk_cursor_new(cursType));
336 /* If it's an anchor, show its "href" attribute in statusbar,
337 * otherwise clear statusbar. */
339 lh_widget_statusbar_pop();
340 m_showing_url = FALSE;
343 if ((href = get_href_at(m_over_element)) != NULL) {
344 lh_widget_statusbar_push(fullurl(href).c_str());
345 m_showing_url = TRUE;
349 const litehtml::tchar_t *lh_widget::get_href_at(litehtml::element::ptr element) const
351 litehtml::element::ptr el;
356 /* If it's not an anchor, check if it has a parent anchor
357 * (e.g. it's an image within an anchor) and grab a pointer
359 if (strcmp(element->get_tagName(), "a") && element->parent()) {
360 el = element->parent();
361 while (el && el != m_html->root() && strcmp(el->get_tagName(), "a")) {
365 if (!el || el == m_html->root())
371 /* At this point, over_el is pointing at an anchor tag, so let's
372 * grab its href attribute. */
373 return el->get_attr(_t("href"));
376 const litehtml::tchar_t *lh_widget::get_href_at(const gint x, const gint y) const
378 litehtml::element::ptr over_el, el;
383 over_el = m_html->root()->get_element_by_point(x, y, x, y);
387 return get_href_at(over_el);
390 void lh_widget::print()
392 debug_print("lh_widget print\n");
393 gtk_widget_realize(GTK_WIDGET(m_drawing_area));
396 void lh_widget::popup_context_menu(const litehtml::tchar_t *url,
397 GdkEventButton *event)
399 cm_return_if_fail(url != NULL);
400 cm_return_if_fail(event != NULL);
402 debug_print("lh_widget showing context menu for '%s'\n", url);
405 gtk_widget_show_all(m_context_menu);
406 gtk_menu_popup(GTK_MENU(m_context_menu), NULL, NULL, NULL, NULL,
407 event->button, event->time);
410 void lh_widget::update_font()
412 PangoFontDescription *pd =
413 pango_font_description_from_string(lh_prefs_get()->default_font);
414 gboolean absolute = pango_font_description_get_size_is_absolute(pd);
417 m_font_name = g_strdup(pango_font_description_get_family(pd));
418 m_font_size = pango_font_description_get_size(pd);
420 pango_font_description_free(pd);
423 m_font_size /= PANGO_SCALE;
425 debug_print("Font set to '%s', size %d\n", m_font_name, m_font_size);
428 const litehtml::tstring lh_widget::fullurl(const litehtml::tchar_t *url) const
430 if (*url == '#' && !m_base_url.empty())
431 return m_base_url + url;
436 void lh_widget::set_partinfo(MimeInfo *partinfo)
438 m_partinfo = partinfo;
441 GdkPixbuf *lh_widget::get_local_image(const litehtml::tstring url) const
445 MimeInfo *p = m_partinfo;
447 if (strncmp(url.c_str(), "cid:", 4) != 0) {
448 debug_print("lh_widget::get_local_image: '%s' is not a local URI, ignoring\n", url.c_str());
452 name = url.c_str() + 4;
453 debug_print("getting message part '%s'\n", name);
455 while ((p = procmime_mimeinfo_next(p)) != NULL) {
456 size_t len = strlen(name);
458 /* p->id is in format "<partname>" */
459 if (!strncasecmp(name, p->id + 1, len) &&
460 strlen(p->id) >= len + 2 &&
461 *(p->id + len + 1) == '>') {
462 GInputStream *stream;
463 GError *error = NULL;
465 stream = procmime_get_part_as_inputstream(p, &error);
467 g_warning("Couldn't get image MIME part: %s\n", error->message);
472 pixbuf = gdk_pixbuf_new_from_stream(stream, NULL, &error);
473 g_object_unref(stream);
475 g_warning("Couldn't load image: %s\n", error->message);
484 /* MIME part with requested name was not found */
488 ////////////////////////////////////////////////
489 static gboolean expose_event_cb(GtkWidget *widget, GdkEvent *event,
492 lh_widget *w = (lh_widget *)user_data;
497 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
500 litehtml::position::vector redraw_boxes;
501 lh_widget *w = (lh_widget *)user_data;
503 if (w->m_html == NULL)
506 //debug_print("lh_widget on_button_press_event\n");
508 if (event->type == GDK_2BUTTON_PRESS ||
509 event->type == GDK_3BUTTON_PRESS)
513 if (event->button == 3) {
514 const litehtml::tchar_t *url = w->get_href_at((gint)event->x, (gint)event->y);
517 w->popup_context_menu(url, event);
522 if(w->m_html->on_lbutton_down((int) event->x, (int) event->y,
523 (int) event->x, (int) event->y, redraw_boxes)) {
524 for(auto& pos : redraw_boxes) {
525 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
526 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
533 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
536 litehtml::position::vector redraw_boxes;
537 lh_widget *w = (lh_widget *)user_data;
539 //debug_print("lh_widget on_motion_notify_event\n");
543 if(w->m_html->on_mouse_over((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
545 for (auto& pos : redraw_boxes)
547 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
548 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
556 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
559 litehtml::position::vector redraw_boxes;
560 lh_widget *w = (lh_widget *)user_data;
561 GError* error = NULL;
563 if (w->m_html == NULL)
566 //debug_print("lh_widget on_button_release_event\n");
568 if (event->type == GDK_2BUTTON_PRESS ||
569 event->type == GDK_3BUTTON_PRESS)
573 if (event->button == 3)
576 w->m_clicked_url.clear();
578 if(w->m_html->on_lbutton_up((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
580 for (auto& pos : redraw_boxes)
582 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
583 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
587 if (!w->m_clicked_url.empty())
589 debug_print("Open in browser: %s\n", w->m_clicked_url.c_str());
590 open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
596 static void open_link_cb(GtkMenuItem *item, gpointer user_data)
598 lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
600 open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
603 static void copy_link_cb(GtkMenuItem *item, gpointer user_data)
605 lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
607 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
608 w->m_clicked_url.c_str(), -1);
609 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD),
610 w->m_clicked_url.c_str(), -1);
613 ///////////////////////////////////////////////////////////
616 lh_widget_wrapped *lh_widget_new()
618 return new lh_widget;
621 GtkWidget *lh_widget_get_widget(lh_widget_wrapped *w)
623 return w->get_widget();
626 void lh_widget_open_html(lh_widget_wrapped *w, const gchar *path)
631 void lh_widget_clear(lh_widget_wrapped *w)
636 void lh_widget_destroy(lh_widget_wrapped *w)
641 void lh_widget_print(lh_widget_wrapped *w) {
645 void lh_widget_set_partinfo(lh_widget_wrapped *w, MimeInfo *partinfo)
647 w->set_partinfo(partinfo);