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
draw_cb(GtkWidget
*widget
, cairo_t
*cr
,
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()
63 m_force_render
= false;
67 m_scrolled_window
= gtk_scrolled_window_new(NULL
, NULL
);
68 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_scrolled_window
),
69 GTK_POLICY_AUTOMATIC
, GTK_POLICY_ALWAYS
);
72 GtkScrolledWindow
*scw
= GTK_SCROLLED_WINDOW(m_scrolled_window
);
73 m_viewport
= gtk_viewport_new(
74 gtk_scrolled_window_get_hadjustment(scw
),
75 gtk_scrolled_window_get_vadjustment(scw
));
76 gtk_container_add(GTK_CONTAINER(m_scrolled_window
), m_viewport
);
79 m_drawing_area
= gtk_drawing_area_new();
80 gtk_container_add(GTK_CONTAINER(m_viewport
), m_drawing_area
);
81 g_signal_connect(m_drawing_area
, "draw",
82 G_CALLBACK(draw_cb
), this);
83 g_signal_connect(m_drawing_area
, "motion_notify_event",
84 G_CALLBACK(motion_notify_event
), this);
85 g_signal_connect(m_drawing_area
, "button_press_event",
86 G_CALLBACK(button_press_event
), this);
87 g_signal_connect(m_drawing_area
, "button_release_event",
88 G_CALLBACK(button_release_event
), this);
90 gtk_widget_show_all(m_scrolled_window
);
93 m_context_menu
= gtk_menu_new();
95 item
= gtk_menu_item_new_with_label(_("Open Link"));
96 g_signal_connect(G_OBJECT(item
), "activate", G_CALLBACK(open_link_cb
), this);
97 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu
), item
);
99 item
= gtk_menu_item_new_with_label(_("Copy Link Location"));
100 g_signal_connect(G_OBJECT(item
), "activate", G_CALLBACK(copy_link_cb
), this);
101 gtk_menu_shell_append(GTK_MENU_SHELL(m_context_menu
), item
);
104 m_rendered_width
= 0;
105 m_context
.load_master_stylesheet(master_css
);
112 m_showing_url
= FALSE
;
114 m_cairo_context
= NULL
;
116 gtk_widget_set_events(m_drawing_area
,
117 GDK_BUTTON_RELEASE_MASK
118 | GDK_BUTTON_PRESS_MASK
119 | GDK_POINTER_MOTION_MASK
);
122 lh_widget::~lh_widget()
124 g_object_unref(m_drawing_area
);
125 m_drawing_area
= NULL
;
126 g_object_unref(m_scrolled_window
);
127 m_scrolled_window
= NULL
;
132 GtkWidget
*lh_widget::get_widget() const
134 return m_scrolled_window
;
137 void lh_widget::set_caption(const litehtml::tchar_t
* caption
)
139 debug_print("lh_widget set_caption\n");
143 void lh_widget::set_base_url(const litehtml::tchar_t
* base_url
)
145 debug_print("lh_widget set_base_url '%s'\n",
146 (base_url
? base_url
: "(null)"));
147 m_base_url
= base_url
;
151 void lh_widget::on_anchor_click(const litehtml::tchar_t
* url
, const litehtml::element::ptr
& el
)
153 debug_print("lh_widget on_anchor_click. url -> %s\n", url
);
155 m_clicked_url
= fullurl(url
);
159 void lh_widget::import_css(litehtml::tstring
& text
, const litehtml::tstring
& url
, litehtml::tstring
& baseurl
)
161 debug_print("lh_widget import_css\n");
162 baseurl
= master_css
;
165 void lh_widget::get_client_rect(litehtml::position
& client
) const
167 if (m_drawing_area
== NULL
)
170 client
.width
= m_rendered_width
;
171 client
.height
= m_height
;
175 // debug_print("lh_widget::get_client_rect: %dx%d\n",
176 // client.width, client.height);
179 void lh_widget::open_html(const gchar
*contents
)
181 gint num
= clear_images(lh_prefs_get()->image_cache_size
* 1024 * 1000);
184 debug_print("LH: cleared %d images from image cache\n", num
);
188 lh_widget_statusbar_push("Loading HTML part ...");
189 m_html
= litehtml::document::createFromString(contents
, this, &m_context
);
190 m_rendered_width
= 0;
191 if (m_html
!= NULL
) {
192 debug_print("lh_widget::open_html created document\n");
193 adj
= gtk_scrolled_window_get_hadjustment(
194 GTK_SCROLLED_WINDOW(m_scrolled_window
));
195 gtk_adjustment_set_value(adj
, 0.0);
196 adj
= gtk_scrolled_window_get_vadjustment(
197 GTK_SCROLLED_WINDOW(m_scrolled_window
));
198 gtk_adjustment_set_value(adj
, 0.0);
201 lh_widget_statusbar_pop();
204 void lh_widget::rerender()
206 m_force_render
= true;
207 gtk_widget_queue_draw(m_drawing_area
);
210 void lh_widget::draw(cairo_t
*cr
)
212 double x1
, x2
, y1
, y2
;
213 double width
, height
;
218 cairo_clip_extents(cr
, &x1
, &y1
, &x2
, &y2
);
223 litehtml::position pos
;
224 pos
.width
= (int)width
;
225 pos
.height
= (int)height
;
229 m_html
->draw((litehtml::uint_ptr
)cr
, 0, 0, &pos
);
232 void lh_widget::redraw()
238 cairo_region_t
*creg
;
239 GdkDrawingContext
*gdkctx
;
240 gboolean destroy
= FALSE
;
245 /* Get width of the viewport. */
246 gtk_widget_get_allocation(GTK_WIDGET(m_viewport
), &rect
);
248 m_height
= rect
.height
;
250 /* If the available width has changed, rerender the HTML content. */
251 if (m_rendered_width
!= width
|| std::atomic_exchange(&m_force_render
, false)) {
252 debug_print("lh_widget::redraw: width changed: %d != %d\n",
253 m_rendered_width
, width
);
255 /* Update our internally stored width, mainly so that
256 * lh_widget::get_client_rect() gives correct width during the
258 m_rendered_width
= width
;
260 /* Re-render HTML for this width. */
261 m_html
->media_changed();
262 m_html
->render(m_rendered_width
);
263 debug_print("render is %dx%d\n", m_html
->width(), m_html
->height());
265 /* Change drawing area's size to match what was rendered. */
266 gtk_widget_set_size_request(m_drawing_area
,
267 m_html
->width(), m_html
->height());
270 /* Use provided cairo context, if any. Otherwise create our own. */
271 if (m_cairo_context
!= NULL
) {
272 cr
= m_cairo_context
;
274 gdkwin
= gtk_widget_get_window(m_drawing_area
);
275 if (gdkwin
== NULL
) {
276 g_warning("lh_widget::redraw: No GdkWindow to draw on!");
279 creg
= cairo_region_create_rectangle(&rect
);
280 gdkctx
= gdk_window_begin_draw_frame(gdkwin
, creg
);
281 cr
= gdk_drawing_context_get_cairo_context(gdkctx
);
285 if(!std::atomic_exchange(&m_blank
, false)) {
288 cairo_rectangle(cr
, rect
.x
, rect
.y
, rect
.width
, rect
.height
);
289 cairo_set_source_rgb(cr
, 255, 255, 255);
293 /* Only destroy the used cairo context if we created it earlier. */
295 gdk_window_end_draw_frame(gdkwin
, gdkctx
);
296 cairo_region_destroy(creg
);
300 void lh_widget::clear()
304 m_rendered_width
= 0;
306 m_clicked_url
.clear();
309 void lh_widget::set_cursor(const litehtml::tchar_t
* cursor
)
311 litehtml::element::ptr over_el
= m_html
->over_element();
314 (over_el
== NULL
|| over_el
!= m_over_element
)) {
315 lh_widget_statusbar_pop();
316 m_showing_url
= FALSE
;
319 if (over_el
!= m_over_element
) {
320 m_over_element
= over_el
;
321 update_cursor(cursor
);
325 void lh_widget::update_cursor(const litehtml::tchar_t
* cursor
)
327 GdkCursorType cursType
= GDK_ARROW
;
328 const litehtml::tchar_t
*href
= get_href_at(m_over_element
);
330 /* If there is a href, and litehtml is okay with showing a pointer
331 * cursor ("pointer" or "auto"), set it, otherwise keep the
332 * default arrow cursor */
333 if ((!strcmp(cursor
, "pointer") || !strcmp(cursor
, "auto")) &&
335 cursType
= GDK_HAND2
;
338 if (cursType
== GDK_ARROW
) {
339 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area
), NULL
);
341 gdk_window_set_cursor(gtk_widget_get_window(m_drawing_area
),
342 gdk_cursor_new_for_display(gtk_widget_get_display(m_drawing_area
),
346 /* If there is a href, show it in statusbar */
348 lh_widget_statusbar_push(fullurl(href
).c_str());
349 m_showing_url
= TRUE
;
353 const litehtml::tchar_t
*lh_widget::get_href_at(litehtml::element::ptr element
) const
355 litehtml::element::ptr el
;
360 /* If it's not an anchor, check if it has a parent anchor
361 * (e.g. it's an image within an anchor) and grab a pointer
363 if (strcmp(element
->get_tagName(), "a") && element
->parent()) {
364 el
= element
->parent();
365 while (el
&& el
!= m_html
->root() && strcmp(el
->get_tagName(), "a")) {
369 if (!el
|| el
== m_html
->root())
375 /* At this point, over_el is pointing at an anchor tag, so let's
376 * grab its href attribute. */
377 return el
->get_attr(_t("href"));
380 const litehtml::tchar_t
*lh_widget::get_href_at(const gint x
, const gint y
) const
382 litehtml::element::ptr over_el
, el
;
387 over_el
= m_html
->root()->get_element_by_point(x
, y
, x
, y
);
391 return get_href_at(over_el
);
394 void lh_widget::print()
396 debug_print("lh_widget print\n");
397 gtk_widget_realize(GTK_WIDGET(m_drawing_area
));
400 void lh_widget::popup_context_menu(const litehtml::tchar_t
*url
,
401 GdkEventButton
*event
)
403 cm_return_if_fail(url
!= NULL
);
404 cm_return_if_fail(event
!= NULL
);
406 debug_print("lh_widget showing context menu for '%s'\n", url
);
409 gtk_widget_show_all(m_context_menu
);
410 gtk_menu_popup_at_pointer(GTK_MENU(m_context_menu
), (GdkEvent
*)event
);
413 void lh_widget::update_font()
415 PangoFontDescription
*pd
=
416 pango_font_description_from_string(lh_prefs_get()->default_font
);
417 gboolean absolute
= pango_font_description_get_size_is_absolute(pd
);
420 m_font_name
= g_strdup(pango_font_description_get_family(pd
));
421 m_font_size
= pango_font_description_get_size(pd
);
423 pango_font_description_free(pd
);
426 m_font_size
/= PANGO_SCALE
;
428 debug_print("Font set to '%s', size %d\n", m_font_name
, m_font_size
);
431 const litehtml::tstring
lh_widget::fullurl(const litehtml::tchar_t
*url
) const
433 if (*url
== '#' && !m_base_url
.empty())
434 return m_base_url
+ url
;
439 void lh_widget::set_partinfo(MimeInfo
*partinfo
)
441 m_partinfo
= partinfo
;
444 GdkPixbuf
*lh_widget::get_local_image(const litehtml::tstring url
) const
448 MimeInfo
*p
= m_partinfo
;
450 if (strncmp(url
.c_str(), "cid:", 4) != 0) {
451 debug_print("lh_widget::get_local_image: '%s' is not a local URI, ignoring\n", url
.c_str());
455 name
= url
.c_str() + 4;
456 debug_print("getting message part '%s'\n", name
);
458 while ((p
= procmime_mimeinfo_next(p
)) != NULL
) {
459 size_t len
= strlen(name
);
461 /* p->id is in format "<partname>" */
463 strlen(p
->id
) >= len
+ 2 &&
464 !strncasecmp(name
, p
->id
+ 1, len
) &&
465 *(p
->id
+ len
+ 1) == '>') {
466 GError
*error
= NULL
;
468 pixbuf
= procmime_get_part_as_pixbuf(p
, &error
);
470 g_warning("couldn't load image: %s", error
->message
);
479 /* MIME part with requested name was not found */
483 void lh_widget::set_cairo_context(cairo_t
*cr
)
485 m_cairo_context
= cr
;
489 ////////////////////////////////////////////////
490 static gboolean
draw_cb(GtkWidget
*widget
, cairo_t
*cr
,
493 lh_widget
*w
= (lh_widget
*)user_data
;
494 w
->set_cairo_context(cr
);
496 w
->set_cairo_context(NULL
);
500 static gboolean
button_press_event(GtkWidget
*widget
, GdkEventButton
*event
,
503 litehtml::position::vector redraw_boxes
;
504 lh_widget
*w
= (lh_widget
*)user_data
;
506 if (w
->m_html
== NULL
)
509 //debug_print("lh_widget on_button_press_event\n");
511 if (event
->type
== GDK_2BUTTON_PRESS
||
512 event
->type
== GDK_3BUTTON_PRESS
)
516 if (event
->button
== 3) {
517 const litehtml::tchar_t
*url
= w
->get_href_at((gint
)event
->x
, (gint
)event
->y
);
520 w
->popup_context_menu(url
, event
);
525 if(w
->m_html
->on_lbutton_down((int) event
->x
, (int) event
->y
,
526 (int) event
->x
, (int) event
->y
, redraw_boxes
)) {
527 for(auto& pos
: redraw_boxes
) {
528 debug_print("x: %d y:%d w: %d h: %d\n", pos
.x
, pos
.y
, pos
.width
, pos
.height
);
529 gtk_widget_queue_draw_area(widget
, pos
.x
, pos
.y
, pos
.width
, pos
.height
);
536 static gboolean
motion_notify_event(GtkWidget
*widget
, GdkEventButton
*event
,
539 litehtml::position::vector redraw_boxes
;
540 lh_widget
*w
= (lh_widget
*)user_data
;
542 //debug_print("lh_widget on_motion_notify_event\n");
546 if(w
->m_html
->on_mouse_over((int) event
->x
, (int) event
->y
, (int) event
->x
, (int) event
->y
, redraw_boxes
))
548 for (auto& pos
: redraw_boxes
)
550 debug_print("x: %d y:%d w: %d h: %d\n", pos
.x
, pos
.y
, pos
.width
, pos
.height
);
551 gtk_widget_queue_draw_area(widget
, pos
.x
, pos
.y
, pos
.width
, pos
.height
);
559 static gboolean
button_release_event(GtkWidget
*widget
, GdkEventButton
*event
,
562 litehtml::position::vector redraw_boxes
;
563 lh_widget
*w
= (lh_widget
*)user_data
;
565 if (w
->m_html
== NULL
)
568 //debug_print("lh_widget on_button_release_event\n");
570 if (event
->type
== GDK_2BUTTON_PRESS
||
571 event
->type
== GDK_3BUTTON_PRESS
)
575 if (event
->button
== 3)
578 w
->m_clicked_url
.clear();
580 if(w
->m_html
->on_lbutton_up((int) event
->x
, (int) event
->y
, (int) event
->x
, (int) event
->y
, redraw_boxes
))
582 for (auto& pos
: redraw_boxes
)
584 debug_print("x: %d y:%d w: %d h: %d\n", pos
.x
, pos
.y
, pos
.width
, pos
.height
);
585 gtk_widget_queue_draw_area(widget
, pos
.x
, pos
.y
, pos
.width
, pos
.height
);
589 if (!w
->m_clicked_url
.empty())
591 debug_print("Open in browser: %s\n", w
->m_clicked_url
.c_str());
592 open_uri(w
->m_clicked_url
.c_str(), prefs_common_get_uri_cmd());
598 static void open_link_cb(GtkMenuItem
*item
, gpointer user_data
)
600 lh_widget_wrapped
*w
= (lh_widget_wrapped
*)user_data
;
602 open_uri(w
->m_clicked_url
.c_str(), prefs_common_get_uri_cmd());
605 static void copy_link_cb(GtkMenuItem
*item
, gpointer user_data
)
607 lh_widget_wrapped
*w
= (lh_widget_wrapped
*)user_data
;
609 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY
),
610 w
->m_clicked_url
.c_str(), -1);
611 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD
),
612 w
->m_clicked_url
.c_str(), -1);
615 ///////////////////////////////////////////////////////////
618 lh_widget_wrapped
*lh_widget_new()
620 return new lh_widget
;
623 GtkWidget
*lh_widget_get_widget(lh_widget_wrapped
*w
)
625 return w
->get_widget();
628 void lh_widget_open_html(lh_widget_wrapped
*w
, const gchar
*path
)
633 void lh_widget_clear(lh_widget_wrapped
*w
)
638 void lh_widget_destroy(lh_widget_wrapped
*w
)
643 void lh_widget_print(lh_widget_wrapped
*w
) {
647 void lh_widget_set_partinfo(lh_widget_wrapped
*w
, MimeInfo
*partinfo
)
649 w
->set_partinfo(partinfo
);