| |
1 /* |
| |
2 * @file gtkwebview.c GTK+ WebKitWebView wrapper class. |
| |
3 * @ingroup pidgin |
| |
4 */ |
| |
5 |
| |
6 /* pidgin |
| |
7 * |
| |
8 * Pidgin is the legal property of its developers, whose names are too numerous |
| |
9 * to list here. Please refer to the COPYRIGHT file distributed with this |
| |
10 * source distribution. |
| |
11 * |
| |
12 * This program is free software; you can redistribute it and/or modify |
| |
13 * it under the terms of the GNU General Public License as published by |
| |
14 * the Free Software Foundation; either version 2 of the License, or |
| |
15 * (at your option) any later version. |
| |
16 * |
| |
17 * This program is distributed in the hope that it will be useful, |
| |
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| |
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| |
20 * GNU General Public License for more details. |
| |
21 * |
| |
22 * You should have received a copy of the GNU General Public License |
| |
23 * along with this program; if not, write to the Free Software |
| |
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA |
| |
25 * |
| |
26 */ |
| |
27 |
| |
28 #ifdef HAVE_CONFIG_H |
| |
29 #include <config.h> |
| |
30 #endif |
| |
31 |
| |
32 #include <ctype.h> |
| |
33 #include <string.h> |
| |
34 #include <glib.h> |
| |
35 #include <glib/gstdio.h> |
| |
36 #include <JavaScriptCore/JavaScript.h> |
| |
37 |
| |
38 #include "util.h" |
| |
39 #include "gtkwebview.h" |
| |
40 #include "imgstore.h" |
| |
41 |
| |
42 static WebKitWebViewClass *parent_class = NULL; |
| |
43 |
| |
44 struct GtkWebViewPriv { |
| |
45 GHashTable *images; /**< a map from id to temporary file for the image */ |
| |
46 gboolean empty; /**< whether anything has been appended **/ |
| |
47 |
| |
48 /* JS execute queue */ |
| |
49 GQueue *js_queue; |
| |
50 gboolean is_loading; |
| |
51 GtkAdjustment *vadj; |
| |
52 guint scroll_src; |
| |
53 GTimer *scroll_time; |
| |
54 }; |
| |
55 |
| |
56 GtkWidget * |
| |
57 gtk_webview_new(void) |
| |
58 { |
| |
59 GtkWebView* ret = GTK_WEBVIEW(g_object_new(gtk_webview_get_type(), NULL)); |
| |
60 return GTK_WIDGET(ret); |
| |
61 } |
| |
62 |
| |
63 static char * |
| |
64 get_image_filename_from_id(GtkWebView* view, int id) |
| |
65 { |
| |
66 char *filename = NULL; |
| |
67 FILE *file; |
| |
68 PurpleStoredImage* img; |
| |
69 |
| |
70 if (!view->priv->images) |
| |
71 view->priv->images = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); |
| |
72 |
| |
73 filename = (char *)g_hash_table_lookup(view->priv->images, GINT_TO_POINTER(id)); |
| |
74 if (filename) |
| |
75 return filename; |
| |
76 |
| |
77 /* else get from img store */ |
| |
78 file = purple_mkstemp(&filename, TRUE); |
| |
79 |
| |
80 img = purple_imgstore_find_by_id(id); |
| |
81 |
| |
82 fwrite(purple_imgstore_get_data(img), purple_imgstore_get_size(img), 1, file); |
| |
83 g_hash_table_insert(view->priv->images, GINT_TO_POINTER(id), filename); |
| |
84 fclose(file); |
| |
85 return filename; |
| |
86 } |
| |
87 |
| |
88 static void |
| |
89 clear_single_image(gpointer key, gpointer value, gpointer userdata) |
| |
90 { |
| |
91 g_unlink((char *)value); |
| |
92 } |
| |
93 |
| |
94 static void |
| |
95 clear_images(GtkWebView *view) |
| |
96 { |
| |
97 if (!view->priv->images) |
| |
98 return; |
| |
99 g_hash_table_foreach(view->priv->images, clear_single_image, NULL); |
| |
100 g_hash_table_unref(view->priv->images); |
| |
101 } |
| |
102 |
| |
103 /* |
| |
104 * Replace all <img id=""> tags with <img src="">. I hoped to never |
| |
105 * write any HTML parsing code, but I'm forced to do this, until |
| |
106 * purple changes the way it works. |
| |
107 */ |
| |
108 static char * |
| |
109 replace_img_id_with_src(GtkWebView *view, const char *html) |
| |
110 { |
| |
111 GString *buffer = g_string_sized_new(strlen(html)); |
| |
112 const char* cur = html; |
| |
113 char *id; |
| |
114 int nid; |
| |
115 |
| |
116 while (*cur) { |
| |
117 const char *img = strstr(cur, "<img"); |
| |
118 if (!img) { |
| |
119 g_string_append(buffer, cur); |
| |
120 break; |
| |
121 } else |
| |
122 g_string_append_len(buffer, cur, img - cur); |
| |
123 |
| |
124 cur = strstr(img, "/>"); |
| |
125 if (!cur) |
| |
126 cur = strstr(img, ">"); |
| |
127 |
| |
128 if (!cur) { /* invalid html? */ |
| |
129 g_string_printf(buffer, "%s", html); |
| |
130 break; |
| |
131 } |
| |
132 |
| |
133 if (strstr(img, "src=") || !strstr(img, "id=")) { |
| |
134 g_string_printf(buffer, "%s", html); |
| |
135 break; |
| |
136 } |
| |
137 |
| |
138 /* |
| |
139 * if this is valid HTML, then I can be sure that it |
| |
140 * has an id= and does not have an src=, since |
| |
141 * '=' cannot appear in parameters. |
| |
142 */ |
| |
143 |
| |
144 id = strstr(img, "id=") + 3; |
| |
145 |
| |
146 /* *id can't be \0, since a ">" appears after this */ |
| |
147 if (isdigit(*id)) |
| |
148 nid = atoi(id); |
| |
149 else |
| |
150 nid = atoi(id + 1); |
| |
151 |
| |
152 /* let's dump this, tag and then dump the src information */ |
| |
153 g_string_append_len(buffer, img, cur - img); |
| |
154 |
| |
155 g_string_append_printf(buffer, " src='file://%s' ", get_image_filename_from_id(view, nid)); |
| |
156 } |
| |
157 |
| |
158 return g_string_free(buffer, FALSE); |
| |
159 } |
| |
160 |
| |
161 static void |
| |
162 gtk_webview_finalize(GObject *view) |
| |
163 { |
| |
164 gpointer temp; |
| |
165 |
| |
166 while ((temp = g_queue_pop_head(GTK_WEBVIEW(view)->priv->js_queue))) |
| |
167 g_free(temp); |
| |
168 g_queue_free(GTK_WEBVIEW(view)->priv->js_queue); |
| |
169 |
| |
170 clear_images(GTK_WEBVIEW(view)); |
| |
171 g_free(GTK_WEBVIEW(view)->priv); |
| |
172 G_OBJECT_CLASS(parent_class)->finalize(G_OBJECT(view)); |
| |
173 } |
| |
174 |
| |
175 static void |
| |
176 gtk_webview_class_init(GtkWebViewClass *klass, gpointer userdata) |
| |
177 { |
| |
178 parent_class = g_type_class_ref(webkit_web_view_get_type()); |
| |
179 G_OBJECT_CLASS(klass)->finalize = gtk_webview_finalize; |
| |
180 } |
| |
181 |
| |
182 static gboolean |
| |
183 webview_link_clicked(WebKitWebView *view, |
| |
184 WebKitWebFrame *frame, |
| |
185 WebKitNetworkRequest *request, |
| |
186 WebKitWebNavigationAction *navigation_action, |
| |
187 WebKitWebPolicyDecision *policy_decision) |
| |
188 { |
| |
189 const gchar *uri; |
| |
190 WebKitWebNavigationReason reason; |
| |
191 |
| |
192 uri = webkit_network_request_get_uri(request); |
| |
193 reason = webkit_web_navigation_action_get_reason(navigation_action); |
| |
194 |
| |
195 if (reason == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) { |
| |
196 /* the gtk imhtml way was to create an idle cb, not sure |
| |
197 * why, so right now just using purple_notify_uri directly */ |
| |
198 purple_notify_uri(NULL, uri); |
| |
199 } else |
| |
200 webkit_web_policy_decision_use(policy_decision); |
| |
201 |
| |
202 return TRUE; |
| |
203 } |
| |
204 |
| |
205 static gboolean |
| |
206 process_js_script_queue(GtkWebView *view) |
| |
207 { |
| |
208 char *script; |
| |
209 if (view->priv->is_loading) |
| |
210 return FALSE; /* we will be called when loaded */ |
| |
211 if (!view->priv->js_queue || g_queue_is_empty(view->priv->js_queue)) |
| |
212 return FALSE; /* nothing to do! */ |
| |
213 |
| |
214 script = g_queue_pop_head(view->priv->js_queue); |
| |
215 webkit_web_view_execute_script(WEBKIT_WEB_VIEW(view), script); |
| |
216 g_free(script); |
| |
217 |
| |
218 return TRUE; /* there may be more for now */ |
| |
219 } |
| |
220 |
| |
221 static void |
| |
222 webview_load_started(WebKitWebView *view, |
| |
223 WebKitWebFrame *frame, |
| |
224 gpointer userdata) |
| |
225 { |
| |
226 /* is there a better way to test for is_loading? */ |
| |
227 GTK_WEBVIEW(view)->priv->is_loading = TRUE; |
| |
228 } |
| |
229 |
| |
230 static void |
| |
231 webview_load_finished(WebKitWebView *view, |
| |
232 WebKitWebFrame *frame, |
| |
233 gpointer userdata) |
| |
234 { |
| |
235 GTK_WEBVIEW(view)->priv->is_loading = FALSE; |
| |
236 g_idle_add((GSourceFunc)process_js_script_queue, view); |
| |
237 } |
| |
238 |
| |
239 void |
| |
240 gtk_webview_safe_execute_script(GtkWebView *view, const char *script) |
| |
241 { |
| |
242 g_queue_push_tail(view->priv->js_queue, g_strdup(script)); |
| |
243 g_idle_add((GSourceFunc)process_js_script_queue, view); |
| |
244 } |
| |
245 |
| |
246 static void |
| |
247 gtk_webview_init(GtkWebView *view, gpointer userdata) |
| |
248 { |
| |
249 view->priv = g_new0(struct GtkWebViewPriv, 1); |
| |
250 g_signal_connect(view, "navigation-policy-decision-requested", |
| |
251 G_CALLBACK(webview_link_clicked), |
| |
252 view); |
| |
253 |
| |
254 g_signal_connect(view, "load-started", |
| |
255 G_CALLBACK(webview_load_started), |
| |
256 view); |
| |
257 |
| |
258 g_signal_connect(view, "load-finished", |
| |
259 G_CALLBACK(webview_load_finished), |
| |
260 view); |
| |
261 |
| |
262 view->priv->empty = TRUE; |
| |
263 view->priv->js_queue = g_queue_new(); |
| |
264 } |
| |
265 |
| |
266 |
| |
267 void |
| |
268 gtk_webview_load_html_string_with_imgstore(GtkWebView *view, const char *html) |
| |
269 { |
| |
270 char *html_imged; |
| |
271 |
| |
272 clear_images(view); |
| |
273 html_imged = replace_img_id_with_src(view, html); |
| |
274 webkit_web_view_load_html_string(WEBKIT_WEB_VIEW(view), html_imged, "file:///"); |
| |
275 g_free(html_imged); |
| |
276 } |
| |
277 |
| |
278 char * |
| |
279 gtk_webview_quote_js_string(const char *text) |
| |
280 { |
| |
281 GString *str = g_string_new("\""); |
| |
282 const char *cur = text; |
| |
283 |
| |
284 while (cur && *cur) { |
| |
285 switch (*cur) { |
| |
286 case '\\': |
| |
287 g_string_append(str, "\\\\"); |
| |
288 break; |
| |
289 case '\"': |
| |
290 g_string_append(str, "\\\""); |
| |
291 break; |
| |
292 case '\r': |
| |
293 g_string_append(str, "<br/>"); |
| |
294 break; |
| |
295 case '\n': |
| |
296 break; |
| |
297 default: |
| |
298 g_string_append_c(str, *cur); |
| |
299 } |
| |
300 cur++; |
| |
301 } |
| |
302 g_string_append_c(str, '"'); |
| |
303 return g_string_free(str, FALSE); |
| |
304 } |
| |
305 |
| |
306 void |
| |
307 gtk_webview_set_vadjustment(GtkWebView *webview, GtkAdjustment *vadj) |
| |
308 { |
| |
309 webview->priv->vadj = vadj; |
| |
310 } |
| |
311 |
| |
312 /* this is a "hack", my plan is to eventually handle this |
| |
313 * correctly using a signals and a plugin: the plugin will have |
| |
314 * the information as to what javascript function to call. It seems |
| |
315 * wrong to hardcode that here. |
| |
316 */ |
| |
317 void |
| |
318 gtk_webview_append_html(GtkWebView *view, const char *html) |
| |
319 { |
| |
320 char *escaped = gtk_webview_quote_js_string(html); |
| |
321 char *script = g_strdup_printf("document.write(%s)", escaped); |
| |
322 webkit_web_view_execute_script(WEBKIT_WEB_VIEW(view), script); |
| |
323 view->priv->empty = FALSE; |
| |
324 gtk_webview_scroll_to_end(view, TRUE); |
| |
325 g_free(script); |
| |
326 g_free(escaped); |
| |
327 } |
| |
328 |
| |
329 gboolean |
| |
330 gtk_webview_is_empty(GtkWebView *view) |
| |
331 { |
| |
332 return view->priv->empty; |
| |
333 } |
| |
334 |
| |
335 #define MAX_SCROLL_TIME 0.4 /* seconds */ |
| |
336 #define SCROLL_DELAY 33 /* milliseconds */ |
| |
337 |
| |
338 /* |
| |
339 * Smoothly scroll a WebView. |
| |
340 * |
| |
341 * @return TRUE if the window needs to be scrolled further, FALSE if we're at the bottom. |
| |
342 */ |
| |
343 static gboolean |
| |
344 smooth_scroll_cb(gpointer data) |
| |
345 { |
| |
346 struct GtkWebViewPriv *priv = data; |
| |
347 GtkAdjustment *adj = priv->vadj; |
| |
348 gdouble max_val = adj->upper - adj->page_size; |
| |
349 gdouble scroll_val = gtk_adjustment_get_value(adj) + ((max_val - gtk_adjustment_get_value(adj)) / 3); |
| |
350 |
| |
351 g_return_val_if_fail(priv->scroll_time != NULL, FALSE); |
| |
352 |
| |
353 if (g_timer_elapsed(priv->scroll_time, NULL) > MAX_SCROLL_TIME || scroll_val >= max_val) { |
| |
354 /* time's up. jump to the end and kill the timer */ |
| |
355 gtk_adjustment_set_value(adj, max_val); |
| |
356 g_timer_destroy(priv->scroll_time); |
| |
357 priv->scroll_time = NULL; |
| |
358 g_source_remove(priv->scroll_src); |
| |
359 priv->scroll_src = 0; |
| |
360 return FALSE; |
| |
361 } |
| |
362 |
| |
363 /* scroll by 1/3rd the remaining distance */ |
| |
364 gtk_adjustment_set_value(adj, scroll_val); |
| |
365 return TRUE; |
| |
366 } |
| |
367 |
| |
368 static gboolean |
| |
369 scroll_idle_cb(gpointer data) |
| |
370 { |
| |
371 struct GtkWebViewPriv *priv = data; |
| |
372 GtkAdjustment *adj = priv->vadj; |
| |
373 if (adj) { |
| |
374 gtk_adjustment_set_value(adj, adj->upper - adj->page_size); |
| |
375 } |
| |
376 priv->scroll_src = 0; |
| |
377 return FALSE; |
| |
378 } |
| |
379 |
| |
380 void |
| |
381 gtk_webview_scroll_to_end(GtkWebView *webview, gboolean smooth) |
| |
382 { |
| |
383 struct GtkWebViewPriv *priv = webview->priv; |
| |
384 if (priv->scroll_time) |
| |
385 g_timer_destroy(priv->scroll_time); |
| |
386 if (priv->scroll_src) |
| |
387 g_source_remove(priv->scroll_src); |
| |
388 if(smooth) { |
| |
389 priv->scroll_time = g_timer_new(); |
| |
390 priv->scroll_src = g_timeout_add_full(G_PRIORITY_LOW, SCROLL_DELAY, smooth_scroll_cb, priv, NULL); |
| |
391 } else { |
| |
392 priv->scroll_time = NULL; |
| |
393 priv->scroll_src = g_idle_add_full(G_PRIORITY_LOW, scroll_idle_cb, priv, NULL); |
| |
394 } |
| |
395 } |
| |
396 |
| |
397 GType |
| |
398 gtk_webview_get_type(void) |
| |
399 { |
| |
400 static GType mview_type = 0; |
| |
401 if (G_UNLIKELY(mview_type == 0)) { |
| |
402 static const GTypeInfo mview_info = { |
| |
403 sizeof(GtkWebViewClass), |
| |
404 NULL, |
| |
405 NULL, |
| |
406 (GClassInitFunc) gtk_webview_class_init, |
| |
407 NULL, |
| |
408 NULL, |
| |
409 sizeof(GtkWebView), |
| |
410 0, |
| |
411 (GInstanceInitFunc) gtk_webview_init, |
| |
412 NULL |
| |
413 }; |
| |
414 mview_type = g_type_register_static(webkit_web_view_get_type(), |
| |
415 "GtkWebView", &mview_info, 0); |
| |
416 } |
| |
417 return mview_type; |
| |
418 } |
| |
419 |