Parent Directory | Revision Log
File selection fremantleized
1 | /* |
2 | * Copyright (C) 2008 Till Harbaum <till@harbaum.org>. |
3 | * |
4 | * This file is part of GPXView. |
5 | * |
6 | * GPXView is free software: you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation, either version 3 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * GPXView is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License |
17 | * along with GPXView. If not, see <http://www.gnu.org/licenses/>. |
18 | */ |
19 | |
20 | #include "gpxview.h" |
21 | |
22 | // #undef PANNABLE_HTML |
23 | |
24 | #ifdef FREMANTLE |
25 | #include <hildon/hildon-banner.h> |
26 | #include <hildon/hildon-note.h> |
27 | #endif |
28 | |
29 | typedef struct load_context { |
30 | int active; |
31 | GMutex *mutex; |
32 | GtkWidget *view; |
33 | char *url, *path; |
34 | GtkHTMLStream *stream; |
35 | struct load_context *next; |
36 | } load_context_t; |
37 | |
38 | typedef struct { |
39 | appdata_t *appdata; |
40 | cache_t *cache; |
41 | GtkWidget *view; |
42 | load_context_t *load_context; |
43 | } http_context_t; |
44 | |
45 | static unsigned long name_hash(const char *name) { |
46 | unsigned long val = 0; |
47 | |
48 | while(*name) { |
49 | val = (val<<8) ^ (val >> 24) ^ (*name & 0xff); |
50 | name++; |
51 | } |
52 | |
53 | return val; |
54 | } |
55 | |
56 | void release_load_context(GThread *self, load_context_t *context) { |
57 | /* this must be atomar, so acquire a lock */ |
58 | |
59 | printf("%p: freeing context at %p\n", self, context); |
60 | |
61 | g_mutex_lock(context->mutex); |
62 | |
63 | /* don't do much if the other thread still uses this */ |
64 | if(context->active) { |
65 | printf(" still active -> just close link\n"); |
66 | |
67 | /* close link to view as the thread is either done or */ |
68 | /* main wants to close the view */ |
69 | gtk_html_end(GTK_HTML(context->view), context->stream, |
70 | GTK_HTML_STREAM_OK); |
71 | |
72 | context->active = FALSE; |
73 | g_mutex_unlock(context->mutex); |
74 | return; |
75 | } |
76 | |
77 | /* ok, other thread is also gone -> we can destroy everything */ |
78 | printf(" not active -> destroy\n"); |
79 | |
80 | /* just free everything */ |
81 | g_mutex_unlock(context->mutex); |
82 | free(context->url); |
83 | free(context->path); |
84 | free(context); |
85 | } |
86 | |
87 | gpointer loader_thread(gpointer data) { |
88 | GThread *self = g_thread_self(); |
89 | |
90 | GnomeVFSResult result; |
91 | GnomeVFSHandle *handle; |
92 | char buffer[4096]; |
93 | GnomeVFSFileSize bytes_read; |
94 | |
95 | load_context_t *context = (load_context_t*)data; |
96 | |
97 | printf("%p: loader thread for %s running\n", self, context->url); |
98 | |
99 | result = gnome_vfs_open(&handle, context->url, GNOME_VFS_OPEN_READ); |
100 | if(result != GNOME_VFS_OK) { |
101 | g_print("%p: open error: %s\n", self, gnome_vfs_result_to_string(result)); |
102 | |
103 | release_load_context(self, context); |
104 | return NULL; |
105 | } |
106 | |
107 | /* try to open file for writing */ |
108 | FILE *f = fopen(context->path, "wb"); |
109 | int running = TRUE; |
110 | |
111 | do { |
112 | result = gnome_vfs_read(handle, buffer, sizeof(buffer)-1, &bytes_read); |
113 | if((result == GNOME_VFS_OK) && (bytes_read > 0)) { |
114 | |
115 | /* update local "running" variable from shared variable */ |
116 | if(running) { |
117 | g_mutex_lock(context->mutex); |
118 | running = context->active; |
119 | g_mutex_unlock(context->mutex); |
120 | } |
121 | |
122 | if(running) |
123 | gtk_html_write(GTK_HTML(context->view), |
124 | context->stream, buffer, bytes_read); |
125 | |
126 | /* and also write local file */ |
127 | if(f) fwrite(buffer, 1l, bytes_read, f); |
128 | } |
129 | } while((result == GNOME_VFS_OK) && (bytes_read > 0) && running); |
130 | |
131 | if(f) fclose(f); |
132 | |
133 | gnome_vfs_close(handle); |
134 | |
135 | /* this file is likely incomplete, remove it */ |
136 | if(!running) { |
137 | printf("%p: thread killed, removing imcomplete cached image\n", self); |
138 | remove(context->path); |
139 | release_load_context(self, context); |
140 | return NULL; |
141 | } |
142 | |
143 | printf("%p: loader thread successfully finished\n", self); |
144 | release_load_context(self, context); |
145 | return NULL; |
146 | } |
147 | |
148 | #ifdef ENABLE_BROWSER_INTERFACE |
149 | static void on_link_clicked(GtkHTML *html, const gchar *url, |
150 | gpointer data) { |
151 | |
152 | appdata_t *appdata = (appdata_t*)data; |
153 | |
154 | #if !defined(USE_MAEMO) || (MAEMO_VERSION_MAJOR < 5) |
155 | GtkWidget *dialog = gtk_message_dialog_new( |
156 | GTK_WINDOW(appdata->window), |
157 | GTK_DIALOG_DESTROY_WITH_PARENT, |
158 | GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, |
159 | _("Open link on external browser?")); |
160 | gboolean yes = (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES); |
161 | #else |
162 | GtkWidget *dialog = |
163 | hildon_note_new_confirmation(GTK_WINDOW(appdata->window), |
164 | _("Open link in external browser?")); |
165 | gboolean yes = (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK); |
166 | #endif |
167 | |
168 | gtk_widget_destroy(dialog); |
169 | |
170 | if(yes) |
171 | browser_url(appdata, (char*)url); |
172 | } |
173 | #endif |
174 | |
175 | static void on_request_url(GtkHTML *html, const gchar *url, |
176 | GtkHTMLStream *stream, gpointer data) { |
177 | char buffer[4096]; |
178 | GnomeVFSFileSize bytes_read; |
179 | |
180 | http_context_t *context = (http_context_t*)data; |
181 | |
182 | if(context->cache) { |
183 | /* try to build local path */ |
184 | char *path = malloc(strlen(context->appdata->image_path)+ |
185 | strlen(context->cache->id)+ |
186 | 14); /* strlen("/xxxxxxxx.xxx\0") == 14 */ |
187 | |
188 | strcpy(path, context->appdata->image_path); |
189 | strcat(path, context->cache->id); |
190 | sprintf(path+strlen(path), "/%lx", name_hash(url)); |
191 | |
192 | /* only append extension if there's a useful one up to three chars ... */ |
193 | char *dot = strrchr(url, '.'); |
194 | if(dot) |
195 | if(strlen(dot) <= 4) |
196 | strcat(path, dot); |
197 | |
198 | printf("cache name = %s\n", path); |
199 | if(g_file_test(path, G_FILE_TEST_EXISTS)) { |
200 | printf("image file exists!\n"); |
201 | |
202 | FILE *f = fopen(path, "rb"); |
203 | |
204 | while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) != 0) { |
205 | |
206 | gtk_html_write(GTK_HTML(context->view), |
207 | stream, buffer, bytes_read); |
208 | |
209 | while (gtk_events_pending ()) |
210 | gtk_main_iteration (); |
211 | } |
212 | fclose(f); |
213 | |
214 | } else { |
215 | printf("image file doesn't exist, starting extra thread!\n"); |
216 | |
217 | checkdir(path); |
218 | |
219 | /* walk to end of list */ |
220 | load_context_t **load_context = &(context->load_context); |
221 | while(*load_context) |
222 | load_context = &(*load_context)->next; |
223 | |
224 | *load_context = g_new0(load_context_t, 1); |
225 | |
226 | (*load_context)->url = strdup(url); |
227 | (*load_context)->path = strdup(path); |
228 | (*load_context)->view = context->view; |
229 | (*load_context)->stream = stream; |
230 | (*load_context)->next = NULL; |
231 | (*load_context)->active = TRUE; |
232 | (*load_context)->mutex = g_mutex_new(); |
233 | |
234 | g_thread_create(loader_thread, *load_context, TRUE, NULL); |
235 | return; |
236 | } |
237 | } else { |
238 | /* not a cache, maybe help, so load images from icon directory */ |
239 | |
240 | /* try to build local path */ |
241 | char *path = malloc(strlen(ICONPATH)+strlen(url)+1); |
242 | |
243 | strcpy(path, ICONPATH); |
244 | strcat(path, url); |
245 | if(!g_file_test(path, G_FILE_TEST_EXISTS)) { |
246 | strcpy(path, "./icons/"); |
247 | strcat(path, url); |
248 | } |
249 | |
250 | if(g_file_test(path, G_FILE_TEST_EXISTS)) { |
251 | FILE *f = fopen(path, "rb"); |
252 | |
253 | while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) != 0) { |
254 | gtk_html_write(GTK_HTML(context->view), |
255 | stream, buffer, bytes_read); |
256 | |
257 | while (gtk_events_pending ()) |
258 | gtk_main_iteration (); |
259 | } |
260 | fclose(f); |
261 | } |
262 | } |
263 | |
264 | gtk_html_end(GTK_HTML(context->view), stream, GTK_HTML_STREAM_OK); |
265 | } |
266 | |
267 | /* notify all open html views of zoom event */ |
268 | void html_zoom(appdata_t *appdata, gboolean in) { |
269 | struct html_view *html_view = appdata->html_view; |
270 | |
271 | while(html_view) { |
272 | printf("zoom notify view %p\n", html_view->view); |
273 | |
274 | if(in) gtk_html_zoom_in(GTK_HTML(html_view->view)); |
275 | else gtk_html_zoom_out(GTK_HTML(html_view->view)); |
276 | |
277 | html_view = html_view->next; |
278 | } |
279 | } |
280 | |
281 | #ifndef NO_COPY_N_PASTE |
282 | static void on_destroy_textview(GtkWidget *widget, gpointer data) { |
283 | appdata_t *appdata = (appdata_t*)data; |
284 | int destroy_active = FALSE; |
285 | |
286 | /* only do this if main windows hasn't already been destroyed */ |
287 | if(!appdata->window) { |
288 | printf("detroy_textview: main window is gone\n"); |
289 | return; |
290 | } |
291 | |
292 | if(!appdata->active_buffer) |
293 | printf("Destroy, but there was no active buffer!\n"); |
294 | else { |
295 | if(GTK_WIDGET_TYPE(widget) == GTK_TYPE_TEXT_VIEW) { |
296 | printf("destroying textview\n"); |
297 | |
298 | if(appdata->active_buffer == |
299 | gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget))) { |
300 | printf("This was the active buffer\n"); |
301 | destroy_active = TRUE; |
302 | } |
303 | } else { |
304 | if(widget == (GtkWidget*)appdata->active_buffer) { |
305 | printf("This was the active html view\n"); |
306 | destroy_active = TRUE; |
307 | } |
308 | } |
309 | } |
310 | |
311 | if(destroy_active) { |
312 | appdata->active_buffer = NULL; |
313 | |
314 | gtk_widget_set_sensitive(appdata->menu_cut, FALSE); |
315 | gtk_widget_set_sensitive(appdata->menu_copy, FALSE); |
316 | gtk_widget_set_sensitive(appdata->menu_paste, FALSE); |
317 | } |
318 | } |
319 | #endif |
320 | |
321 | static void on_destroy_htmlview(GtkWidget *widget, gpointer data) { |
322 | http_context_t *context = (http_context_t*)data; |
323 | |
324 | printf("destroying html context\n"); |
325 | // printf("associated view = %p\n", context->view); |
326 | |
327 | struct html_view **h = &(context->appdata->html_view); |
328 | |
329 | while(*h && (*h)->view != context->view) |
330 | h = &((*h)->next); |
331 | |
332 | g_assert(h); |
333 | |
334 | /* remove entry from chain */ |
335 | void *tmp = *h; |
336 | *h = (*h)->next; |
337 | free(tmp); |
338 | |
339 | load_context_t *load_context = context->load_context; |
340 | while(load_context) { |
341 | printf("found an associated thread\n"); |
342 | |
343 | load_context_t *tmp_context = load_context->next; |
344 | release_load_context(NULL, load_context); |
345 | load_context = tmp_context; |
346 | } |
347 | |
348 | #ifndef NO_COPY_N_PASTE |
349 | on_destroy_textview(widget, context->appdata); |
350 | #endif |
351 | |
352 | /* destroy context */ |
353 | free(data); |
354 | } |
355 | |
356 | #ifndef NO_COPY_N_PASTE |
357 | static gboolean focus_in(GtkWidget *widget, GdkEventFocus *event, |
358 | gpointer data) { |
359 | appdata_t *appdata = (appdata_t*)data; |
360 | |
361 | printf("focus in!\n"); |
362 | |
363 | /* these buffers are read-only, thus only "copy" is enabled */ |
364 | gtk_widget_set_sensitive(appdata->menu_cut, FALSE); |
365 | gtk_widget_set_sensitive(appdata->menu_copy, TRUE); |
366 | gtk_widget_set_sensitive(appdata->menu_paste, FALSE); |
367 | |
368 | if(GTK_WIDGET_TYPE(widget) == GTK_TYPE_TEXT_VIEW) { |
369 | appdata->active_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); |
370 | } else if(GTK_WIDGET_TYPE(widget) == gtk_html_get_type()) { |
371 | appdata->active_buffer = (GtkTextBuffer*)widget; |
372 | } else { |
373 | printf("not a text nor a html view\n"); |
374 | } |
375 | |
376 | return FALSE; |
377 | } |
378 | |
379 | void html_copy_to_clipboard(appdata_t *appdata) { |
380 | gtk_html_copy(GTK_HTML(appdata->active_buffer)); |
381 | } |
382 | #endif |
383 | |
384 | #ifdef FREMANTLE |
385 | static void |
386 | tap_and_hold_cb (GtkWidget *widget, gpointer user_data) { |
387 | appdata_t *appdata = (appdata_t*)user_data; |
388 | |
389 | /* the HildonTextView becomes semi-copy'n pasteable if we enable */ |
390 | /* the cursor */ |
391 | if(GTK_WIDGET_TYPE(widget) == HILDON_TYPE_TEXT_VIEW) { |
392 | gboolean state = gtk_text_view_get_cursor_visible(GTK_TEXT_VIEW(widget)); |
393 | gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(widget), !state); |
394 | return; |
395 | } |
396 | |
397 | #if 0 |
398 | GtkWidget *parent = widget->parent; |
399 | |
400 | /* check if the parent actually is a hildonpannablearea */ |
401 | if(GTK_WIDGET_TYPE(parent) != HILDON_TYPE_PANNABLE_AREA) { |
402 | printf("parent is not pannable area, ignoring event\n"); |
403 | return; |
404 | } |
405 | |
406 | gboolean enabled = FALSE; |
407 | g_object_get(parent, "enabled", &enabled, NULL); |
408 | enabled = !enabled; |
409 | g_object_set(parent, "enabled", enabled, NULL); |
410 | |
411 | hildon_banner_show_information(GTK_WIDGET(appdata->window), |
412 | NULL, enabled?"enabled":"disabled"); |
413 | #endif |
414 | } |
415 | #endif |
416 | |
417 | /* the cache descriptions are not valid html and need some surrounding stuff */ |
418 | static const char *html_start = "<html><head>" |
419 | "<meta http-equiv=content-type content=\"text/html; charset=UTF-8\">" |
420 | "</head></body>"; |
421 | static const char *html_end = "</body></html>"; |
422 | |
423 | GtkWidget *html_view(appdata_t *appdata, char *text, |
424 | html_mode_t mode, gboolean scrollwin, |
425 | cache_t *cache, char *anchor) { |
426 | GtkWidget *view; |
427 | |
428 | if(mode == HTML_HTML) { |
429 | http_context_t *context = g_new0(http_context_t, 1); |
430 | context->appdata = appdata; |
431 | context->cache = cache; |
432 | |
433 | context->view = view = gtk_html_new(); |
434 | |
435 | #ifndef PANNABLE_HTML |
436 | // gtk_html_set_auto_panning(GTK_HTML(view), TRUE); |
437 | #else |
438 | gtk_html_set_auto_panning(GTK_HTML(view), FALSE); |
439 | #endif |
440 | |
441 | #ifdef FREMANTLE |
442 | /* allow selection does not allow anything, but disables auto panning ... */ |
443 | /* even worse: frequently causes the device to reboot */ |
444 | // gtk_html_allow_selection(GTK_HTML(view), TRUE); |
445 | #endif |
446 | |
447 | /* create a callback to load images only if a cache has been given */ |
448 | /* so that images can be cached/stored appropriately */ |
449 | g_signal_connect(G_OBJECT(view), "url_requested", |
450 | G_CALLBACK(on_request_url), context); |
451 | |
452 | #ifdef ENABLE_BROWSER_INTERFACE |
453 | g_signal_connect(G_OBJECT(view), "link_clicked", |
454 | G_CALLBACK(on_link_clicked), context->appdata); |
455 | #endif |
456 | |
457 | GtkHTMLStream *stream = gtk_html_begin(GTK_HTML(view)); |
458 | |
459 | gtk_html_write(GTK_HTML(view), stream, html_start, strlen(html_start)); |
460 | gtk_html_write(GTK_HTML(view), stream, text, strlen(text)); |
461 | gtk_html_write(GTK_HTML(view), stream, html_end, strlen(html_end)); |
462 | gtk_html_end(GTK_HTML(view), stream, GTK_HTML_STREAM_OK); |
463 | |
464 | if(anchor) { |
465 | while(gtk_events_pending()) |
466 | gtk_main_iteration (); |
467 | |
468 | gtk_html_jump_to_anchor(GTK_HTML(context->view), anchor); |
469 | } |
470 | |
471 | /* register this html view */ |
472 | struct html_view **h = &(appdata->html_view); |
473 | while(*h) h = &((*h)->next); |
474 | *h = g_new0(struct html_view, 1); |
475 | (*h)->view = view; |
476 | |
477 | #ifdef FREMANTLE |
478 | gtk_widget_tap_and_hold_setup(GTK_WIDGET(view), NULL, NULL, 0); |
479 | g_signal_connect(G_OBJECT(view), "tap-and-hold", |
480 | G_CALLBACK(tap_and_hold_cb), appdata); |
481 | #endif |
482 | |
483 | g_signal_connect(G_OBJECT(view), "destroy", |
484 | G_CALLBACK(on_destroy_htmlview), context); |
485 | } else { |
486 | GtkTextBuffer *buffer = gtk_text_buffer_new(NULL); |
487 | gtk_text_buffer_set_text(buffer, text, strlen(text)); |
488 | |
489 | #ifndef USE_HILDON_TEXT_VIEW |
490 | view = gtk_text_view_new_with_buffer(buffer); |
491 | #else |
492 | view = hildon_text_view_new(); |
493 | hildon_text_view_set_buffer(HILDON_TEXT_VIEW(view), buffer); |
494 | |
495 | gtk_widget_tap_and_hold_setup(GTK_WIDGET(view), NULL, NULL, 0); |
496 | g_signal_connect(G_OBJECT(view), "tap-and-hold", |
497 | G_CALLBACK(tap_and_hold_cb), appdata); |
498 | #endif |
499 | |
500 | gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD); |
501 | gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE); |
502 | gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE); |
503 | |
504 | /* make this look nicer in fremantle and not just black-on-white */ |
505 | /* just use the default style */ |
506 | #ifdef USE_STACKABLE_WINDOW |
507 | /* in fremantle this is really tricky and we need to inherit the */ |
508 | /* style from the topmost window in the stack */ |
509 | HildonWindowStack *stack = hildon_window_stack_get_default(); |
510 | GList *list = hildon_window_stack_get_windows(stack); |
511 | gtk_widget_set_style(view, GTK_WIDGET(list->data)->style); |
512 | g_list_free(list); |
513 | #else |
514 | gtk_widget_set_style(view, GTK_WIDGET(appdata->window)->style); |
515 | #endif |
516 | |
517 | #ifndef NO_COPY_N_PASTE |
518 | g_signal_connect(G_OBJECT(view), "destroy", |
519 | G_CALLBACK(on_destroy_textview), appdata); |
520 | #endif |
521 | } |
522 | |
523 | #ifndef NO_COPY_N_PASTE |
524 | g_signal_connect(G_OBJECT(view), "focus-in-event", |
525 | G_CALLBACK(focus_in), appdata); |
526 | #endif |
527 | |
528 | if(scrollwin) { |
529 | #ifndef USE_PANNABLE_AREA |
530 | GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); |
531 | gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), |
532 | GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); |
533 | gtk_container_add(GTK_CONTAINER(scrolled_window), view); |
534 | |
535 | return scrolled_window; |
536 | #else |
537 | #ifndef PANNABLE_HTML |
538 | if(mode == HTML_HTML) { |
539 | GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); |
540 | gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), |
541 | GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); |
542 | gtk_container_add(GTK_CONTAINER(scrolled_window), view); |
543 | return scrolled_window; |
544 | } else |
545 | #endif |
546 | { |
547 | GtkWidget *pannable_area = hildon_pannable_area_new(); |
548 | gtk_container_add(GTK_CONTAINER(pannable_area), view); |
549 | return pannable_area; |
550 | } |
551 | #endif |
552 | } |
553 | |
554 | return view; |
555 | } |
556 |