Sync with api changes
[claws.git] / src / plugins / litehtml_viewer / lh_widget.cpp
blobf06834d0e9a748b17297cf60e68d5576706186c7
1 /*
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.
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #include "claws-features.h"
21 #endif
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>
32 #include "utils.h"
34 #include "litehtml/litehtml.h"
36 #include "lh_prefs.h"
37 #include "lh_widget.h"
38 #include "lh_widget_wrapped.h"
40 extern "C" {
41 const gchar *prefs_common_get_uri_cmd(void);
44 static gboolean draw_cb(GtkWidget *widget, cairo_t *cr,
45 gpointer user_data);
46 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
47 gpointer user_data);
48 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
49 gpointer user_data);
50 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
51 gpointer user_data);
52 static void open_link_cb(GtkMenuItem *item, gpointer user_data);
53 static void copy_link_cb(GtkMenuItem *item, gpointer user_data);
55 lh_widget::lh_widget()
57 GtkWidget *item;
59 m_force_render = false;
60 m_blank = false;
62 /* scrolled window */
63 m_scrolled_window = gtk_scrolled_window_new(NULL, NULL);
64 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_scrolled_window),
65 GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
67 /* viewport */
68 GtkScrolledWindow *scw = GTK_SCROLLED_WINDOW(m_scrolled_window);
69 m_viewport = gtk_viewport_new(
70 gtk_scrolled_window_get_hadjustment(scw),
71 gtk_scrolled_window_get_vadjustment(scw));
72 gtk_container_add(GTK_CONTAINER(m_scrolled_window), m_viewport);
74 /* drawing area */
75 m_drawing_area = gtk_drawing_area_new();
76 gtk_container_add(GTK_CONTAINER(m_viewport), m_drawing_area);
77 g_signal_connect(m_drawing_area, "draw",
78 G_CALLBACK(draw_cb), this);
79 g_signal_connect(m_drawing_area, "motion_notify_event",
80 G_CALLBACK(motion_notify_event), this);
81 g_signal_connect(m_drawing_area, "button_press_event",
82 G_CALLBACK(button_press_event), this);
83 g_signal_connect(m_drawing_area, "button_release_event",
84 G_CALLBACK(button_release_event), this);
86 gtk_widget_show_all(m_scrolled_window);
88 /* context menu */
89 m_context_menu = gtk_menu_new();
91 item = gtk_menu_item_new_with_label(_("Open Link"));
92 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(open_link_cb), this);
93 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
95 item = gtk_menu_item_new_with_label(_("Copy Link Location"));
96 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(copy_link_cb), this);
97 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu), item);
99 m_html = NULL;
100 m_rendered_width = 0;
102 m_font_name = NULL;
103 m_font_size = 0;
105 m_partinfo = NULL;
107 m_showing_url = FALSE;
109 m_cairo_context = NULL;
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;
123 m_html = NULL;
124 g_free(m_font_name);
127 GtkWidget *lh_widget::get_widget() const
129 return m_scrolled_window;
132 void lh_widget::set_caption(const char *caption)
134 debug_print("lh_widget set_caption\n");
135 return;
138 void lh_widget::set_base_url(const char *base_url)
140 debug_print("lh_widget set_base_url '%s'\n",
141 (base_url ? base_url : "(null)"));
142 if (base_url)
143 m_base_url = base_url;
144 else
145 m_base_url.clear();
147 return;
150 void lh_widget::on_anchor_click(const char *url, const litehtml::element::ptr& el)
152 debug_print("lh_widget on_anchor_click. url -> %s\n", url);
154 m_clicked_url = fullurl(url);
155 return;
158 void lh_widget::import_css(litehtml::string& text, const litehtml::string& url, litehtml::string& baseurl)
160 debug_print("lh_widget import_css. url=\"%s\" baseurl=\"%s\"\n", url.c_str(), baseurl.c_str());
163 void lh_widget::get_client_rect(litehtml::position& client) const
165 if (m_drawing_area == NULL)
166 return;
168 client.width = m_rendered_width;
169 client.height = m_height;
170 client.x = 0;
171 client.y = 0;
173 // debug_print("lh_widget::get_client_rect: %dx%d\n",
174 // client.width, client.height);
177 void lh_widget::open_html(const gchar *contents)
179 gint num = clear_images(lh_prefs_get()->image_cache_size * 1024 * 1000);
180 GtkAdjustment *adj;
182 debug_print("LH: cleared %d images from image cache\n", num);
184 update_font();
186 lh_widget_statusbar_push("Loading HTML part ...");
187 m_html = litehtml::document::createFromString(contents, this);
188 m_rendered_width = 0;
189 if (m_html != NULL) {
190 debug_print("lh_widget::open_html created document\n");
191 adj = gtk_scrolled_window_get_hadjustment(
192 GTK_SCROLLED_WINDOW(m_scrolled_window));
193 gtk_adjustment_set_value(adj, 0.0);
194 adj = gtk_scrolled_window_get_vadjustment(
195 GTK_SCROLLED_WINDOW(m_scrolled_window));
196 gtk_adjustment_set_value(adj, 0.0);
197 m_blank = false;
199 lh_widget_statusbar_pop();
202 void lh_widget::rerender()
204 m_force_render = true;
205 gtk_widget_queue_draw(m_drawing_area);
208 void lh_widget::draw(cairo_t *cr)
210 double x1, x2, y1, y2;
211 double width, height;
213 if (m_html == NULL)
214 return;
216 cairo_clip_extents(cr, &x1, &y1, &x2, &y2);
218 width = x2 - x1;
219 height = y2 - y1;
221 litehtml::position pos;
222 pos.width = (int)width;
223 pos.height = (int)height;
224 pos.x = (int)x1;
225 pos.y = (int)y1;
227 m_html->draw((litehtml::uint_ptr)cr, 0, 0, &pos);
230 void lh_widget::redraw()
232 GtkAllocation rect;
233 gint width;
234 GdkWindow *gdkwin;
235 cairo_t *cr;
236 cairo_region_t *creg;
237 GdkDrawingContext *gdkctx;
238 gboolean destroy = FALSE;
240 if (m_html == NULL)
241 return;
243 /* Get width of the viewport. */
244 gtk_widget_get_allocation(GTK_WIDGET(m_viewport), &rect);
245 width = rect.width;
246 m_height = rect.height;
248 /* If the available width has changed, rerender the HTML content. */
249 if (m_rendered_width != width || std::atomic_exchange(&m_force_render, false)) {
250 debug_print("lh_widget::redraw: width changed: %d != %d\n",
251 m_rendered_width, width);
253 /* Update our internally stored width, mainly so that
254 * lh_widget::get_client_rect() gives correct width during the
255 * render. */
256 m_rendered_width = width;
258 /* Re-render HTML for this width. */
259 m_html->media_changed();
260 m_html->render(m_rendered_width);
261 debug_print("render is %dx%d\n", m_html->width(), m_html->height());
263 /* Change drawing area's size to match what was rendered. */
264 gtk_widget_set_size_request(m_drawing_area,
265 m_html->width(), m_html->height());
268 /* Use provided cairo context, if any. Otherwise create our own. */
269 if (m_cairo_context != NULL) {
270 cr = m_cairo_context;
271 } else {
272 gdkwin = gtk_widget_get_window(m_drawing_area);
273 if (gdkwin == NULL) {
274 g_warning("lh_widget::redraw: No GdkWindow to draw on!");
275 return;
277 creg = cairo_region_create_rectangle(&rect);
278 gdkctx = gdk_window_begin_draw_frame(gdkwin, creg);
279 cr = gdk_drawing_context_get_cairo_context(gdkctx);
280 destroy = TRUE;
283 if(!std::atomic_exchange(&m_blank, false)) {
284 draw(cr);
285 } else {
286 cairo_rectangle(cr, rect.x, rect.y, rect.width, rect.height);
287 cairo_set_source_rgb(cr, 255, 255, 255);
288 cairo_fill(cr);
291 /* Only destroy the used cairo context if we created it earlier. */
292 if (destroy) {
293 gdk_window_end_draw_frame(gdkwin, gdkctx);
294 cairo_region_destroy(creg);
298 void lh_widget::clear()
300 m_html = nullptr;
301 m_blank = true;
302 m_rendered_width = 0;
303 m_base_url.clear();
304 m_clicked_url.clear();
307 void lh_widget::set_cursor(const char *cursor)
309 litehtml::element::ptr over_el = m_html->over_element();
311 if (m_showing_url &&
312 (over_el == NULL || over_el != m_over_element)) {
313 lh_widget_statusbar_pop();
314 m_showing_url = FALSE;
317 if (over_el != m_over_element) {
318 m_over_element = over_el;
319 update_cursor(cursor);
323 void lh_widget::update_cursor(const char *cursor)
325 GdkCursorType cursType = GDK_ARROW;
326 const char *href = get_href_at(m_over_element);
328 /* If there is a href, and litehtml is okay with showing a pointer
329 * cursor ("pointer" or "auto"), set it, otherwise keep the
330 * default arrow cursor */
331 if ((!strcmp(cursor, "pointer") || !strcmp(cursor, "auto")) &&
332 href != NULL) {
333 cursType = GDK_HAND2;
336 if (cursType == GDK_ARROW) {
337 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area), NULL);
338 } else {
339 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area),
340 gdk_cursor_new_for_display(gtk_widget_get_display(m_drawing_area),
341 cursType));
344 /* If there is a href, show it in statusbar */
345 if (href != NULL) {
346 lh_widget_statusbar_push(fullurl(href).c_str());
347 m_showing_url = TRUE;
351 const char *lh_widget::get_href_at(litehtml::element::ptr element) const
353 litehtml::element::ptr el;
355 if (element == NULL)
356 return NULL;
358 /* If it's not an anchor, check if it has a parent anchor
359 * (e.g. it's an image within an anchor) and grab a pointer
360 * to that. */
361 if (strcmp(element->get_tagName(), "a") && element->parent()) {
362 el = element->parent();
363 while (el && el != m_html->root() && strcmp(el->get_tagName(), "a")) {
364 el = el->parent();
367 if (!el || el == m_html->root())
368 return NULL;
369 } else {
370 el = element;
373 /* At this point, over_el is pointing at an anchor tag, so let's
374 * grab its href attribute. */
375 return el->get_attr("href");
378 void lh_widget::print()
380 debug_print("lh_widget print\n");
381 gtk_widget_realize(GTK_WIDGET(m_drawing_area));
384 void lh_widget::popup_context_menu(const char *url,
385 GdkEventButton *event)
387 cm_return_if_fail(url != NULL);
388 cm_return_if_fail(event != NULL);
390 debug_print("lh_widget showing context menu for '%s'\n", url);
392 m_clicked_url = url;
393 gtk_widget_show_all(m_context_menu);
394 gtk_menu_popup_at_pointer(GTK_MENU(m_context_menu), (GdkEvent *)event);
397 void lh_widget::update_font()
399 PangoFontDescription *pd =
400 pango_font_description_from_string(lh_prefs_get()->default_font);
401 gboolean absolute = pango_font_description_get_size_is_absolute(pd);
403 g_free(m_font_name);
404 m_font_name = g_strdup(pango_font_description_get_family(pd));
405 m_font_size = pango_font_description_get_size(pd);
407 pango_font_description_free(pd);
409 if (!absolute)
410 m_font_size /= PANGO_SCALE;
412 debug_print("Font set to '%s', size %d\n", m_font_name, m_font_size);
415 const litehtml::string lh_widget::fullurl(const char *url) const
417 if (*url == '#' && !m_base_url.empty())
418 return m_base_url + url;
420 return url;
423 void lh_widget::set_partinfo(MimeInfo *partinfo)
425 m_partinfo = partinfo;
428 GdkPixbuf *lh_widget::get_local_image(const litehtml::string url) const
430 GdkPixbuf *pixbuf;
431 const gchar *name;
432 MimeInfo *p = m_partinfo;
434 if (strncmp(url.c_str(), "cid:", 4) != 0) {
435 debug_print("lh_widget::get_local_image: '%s' is not a local URI, ignoring\n", url.c_str());
436 return NULL;
439 name = url.c_str() + 4;
440 debug_print("getting message part '%s'\n", name);
442 while ((p = procmime_mimeinfo_next(p)) != NULL) {
443 size_t len = strlen(name);
445 /* p->id is in format "<partname>" */
446 if (p->id != NULL &&
447 strlen(p->id) >= len + 2 &&
448 !strncasecmp(name, p->id + 1, len) &&
449 *(p->id + len + 1) == '>') {
450 GError *error = NULL;
452 pixbuf = procmime_get_part_as_pixbuf(p, &error);
453 if (error != NULL) {
454 g_warning("couldn't load image: %s", error->message);
455 g_error_free(error);
456 return NULL;
459 return pixbuf;
463 /* MIME part with requested name was not found */
464 return NULL;
467 void lh_widget::set_cairo_context(cairo_t *cr)
469 m_cairo_context = cr;
473 ////////////////////////////////////////////////
474 static gboolean draw_cb(GtkWidget *widget, cairo_t *cr,
475 gpointer user_data)
477 lh_widget *w = (lh_widget *)user_data;
478 w->set_cairo_context(cr);
479 w->redraw();
480 w->set_cairo_context(NULL);
481 return FALSE;
484 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
485 gpointer user_data)
487 litehtml::position::vector redraw_boxes;
488 lh_widget *w = (lh_widget *)user_data;
490 if (w->m_html == NULL)
491 return FALSE;
493 //debug_print("lh_widget on_button_press_event\n");
495 if (event->type == GDK_2BUTTON_PRESS ||
496 event->type == GDK_3BUTTON_PRESS)
497 return TRUE;
499 /* Right-click */
500 if (event->button == 3) {
501 const char *url = w->get_href_at(w->m_html->over_element());
503 if (url != NULL)
504 w->popup_context_menu(url, event);
506 return TRUE;
509 if(w->m_html->on_lbutton_down((int) event->x, (int) event->y,
510 (int) event->x, (int) event->y, redraw_boxes)) {
511 for(auto& pos : redraw_boxes) {
512 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
513 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
517 return TRUE;
520 static gboolean motion_notify_event(GtkWidget *widget, GdkEventButton *event,
521 gpointer user_data)
523 litehtml::position::vector redraw_boxes;
524 lh_widget *w = (lh_widget *)user_data;
526 //debug_print("lh_widget on_motion_notify_event\n");
528 if(w->m_html)
530 if(w->m_html->on_mouse_over((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
532 for (auto& pos : redraw_boxes)
534 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
535 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
540 return TRUE;
543 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
544 gpointer user_data)
546 litehtml::position::vector redraw_boxes;
547 lh_widget *w = (lh_widget *)user_data;
549 if (w->m_html == NULL)
550 return FALSE;
552 //debug_print("lh_widget on_button_release_event\n");
554 if (event->type == GDK_2BUTTON_PRESS ||
555 event->type == GDK_3BUTTON_PRESS)
556 return TRUE;
558 /* Right-click */
559 if (event->button == 3)
560 return TRUE;
562 w->m_clicked_url.clear();
564 if(w->m_html->on_lbutton_up((int) event->x, (int) event->y, (int) event->x, (int) event->y, redraw_boxes))
566 for (auto& pos : redraw_boxes)
568 debug_print("x: %d y:%d w: %d h: %d\n", pos.x, pos.y, pos.width, pos.height);
569 gtk_widget_queue_draw_area(widget, pos.x, pos.y, pos.width, pos.height);
573 if (!w->m_clicked_url.empty())
575 debug_print("Open in browser: %s\n", w->m_clicked_url.c_str());
576 open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
579 return TRUE;
582 static void open_link_cb(GtkMenuItem *item, gpointer user_data)
584 lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
586 open_uri(w->m_clicked_url.c_str(), prefs_common_get_uri_cmd());
589 static void copy_link_cb(GtkMenuItem *item, gpointer user_data)
591 lh_widget_wrapped *w = (lh_widget_wrapped *)user_data;
593 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
594 w->m_clicked_url.c_str(), -1);
595 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD),
596 w->m_clicked_url.c_str(), -1);
599 ///////////////////////////////////////////////////////////
600 extern "C" {
602 lh_widget_wrapped *lh_widget_new()
604 return new lh_widget;
607 GtkWidget *lh_widget_get_widget(lh_widget_wrapped *w)
609 return w->get_widget();
612 void lh_widget_open_html(lh_widget_wrapped *w, const gchar *path)
614 w->open_html(path);
617 void lh_widget_clear(lh_widget_wrapped *w)
619 w->clear();
622 void lh_widget_destroy(lh_widget_wrapped *w)
624 delete w;
627 void lh_widget_print(lh_widget_wrapped *w) {
628 w->print();
631 void lh_widget_set_partinfo(lh_widget_wrapped *w, MimeInfo *partinfo)
633 w->set_partinfo(partinfo);
636 } /* extern "C" */