Parent Directory | Revision Log
Custom markup renderer prepared
1 | harbaum | 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 | typedef struct load_context { | ||
23 | int active; | ||
24 | GMutex *mutex; | ||
25 | GtkWidget *view; | ||
26 | char *url, *path; | ||
27 | GtkHTMLStream *stream; | ||
28 | struct load_context *next; | ||
29 | } load_context_t; | ||
30 | |||
31 | typedef struct { | ||
32 | appdata_t *appdata; | ||
33 | cache_t *cache; | ||
34 | GtkWidget *view; | ||
35 | load_context_t *load_context; | ||
36 | } http_context_t; | ||
37 | |||
38 | static unsigned long name_hash(const char *name) { | ||
39 | unsigned long val = 0; | ||
40 | |||
41 | while(*name) { | ||
42 | val = (val<<8) ^ (val >> 24) ^ (*name & 0xff); | ||
43 | name++; | ||
44 | } | ||
45 | |||
46 | return val; | ||
47 | } | ||
48 | |||
49 | void release_load_context(GThread *self, load_context_t *context) { | ||
50 | /* this must be atomar, so acquire a lock */ | ||
51 | |||
52 | printf("%p: freeing context at %p\n", self, context); | ||
53 | |||
54 | g_mutex_lock(context->mutex); | ||
55 | |||
56 | /* don't do much if the other thread still uses this */ | ||
57 | if(context->active) { | ||
58 | printf(" still active -> just close link\n"); | ||
59 | |||
60 | /* close link to view as the thread is either done or */ | ||
61 | /* main wants to close the view */ | ||
62 | gtk_html_end(GTK_HTML(context->view), context->stream, | ||
63 | GTK_HTML_STREAM_OK); | ||
64 | |||
65 | context->active = FALSE; | ||
66 | g_mutex_unlock(context->mutex); | ||
67 | return; | ||
68 | } | ||
69 | |||
70 | /* ok, other thread is also gone -> we can destroy everything */ | ||
71 | printf(" not active -> destroy\n"); | ||
72 | |||
73 | /* just free everything */ | ||
74 | g_mutex_unlock(context->mutex); | ||
75 | free(context->url); | ||
76 | free(context->path); | ||
77 | free(context); | ||
78 | } | ||
79 | |||
80 | gpointer loader_thread(gpointer data) { | ||
81 | GThread *self = g_thread_self(); | ||
82 | |||
83 | GnomeVFSResult result; | ||
84 | GnomeVFSHandle *handle; | ||
85 | char buffer[4096]; | ||
86 | GnomeVFSFileSize bytes_read; | ||
87 | |||
88 | load_context_t *context = (load_context_t*)data; | ||
89 | |||
90 | printf("%p: loader thread for %s running\n", self, context->url); | ||
91 | |||
92 | result = gnome_vfs_open(&handle, context->url, GNOME_VFS_OPEN_READ); | ||
93 | if(result != GNOME_VFS_OK) { | ||
94 | g_print("%p: open error: %s\n", self, gnome_vfs_result_to_string(result)); | ||
95 | |||
96 | release_load_context(self, context); | ||
97 | return NULL; | ||
98 | } | ||
99 | |||
100 | /* try to open file for writing */ | ||
101 | FILE *f = fopen(context->path, "wb"); | ||
102 | int running = TRUE; | ||
103 | |||
104 | do { | ||
105 | result = gnome_vfs_read(handle, buffer, sizeof(buffer)-1, &bytes_read); | ||
106 | if((result == GNOME_VFS_OK) && (bytes_read > 0)) { | ||
107 | |||
108 | /* update local "running" variable from shared variable */ | ||
109 | if(running) { | ||
110 | g_mutex_lock(context->mutex); | ||
111 | running = context->active; | ||
112 | g_mutex_unlock(context->mutex); | ||
113 | } | ||
114 | |||
115 | if(running) | ||
116 | gtk_html_write(GTK_HTML(context->view), | ||
117 | context->stream, buffer, bytes_read); | ||
118 | |||
119 | /* and also write local file */ | ||
120 | if(f) fwrite(buffer, 1l, bytes_read, f); | ||
121 | } | ||
122 | } while((result == GNOME_VFS_OK) && (bytes_read > 0) && running); | ||
123 | |||
124 | if(f) fclose(f); | ||
125 | |||
126 | gnome_vfs_close(handle); | ||
127 | |||
128 | /* this file is likely incomplete, remove it */ | ||
129 | if(!running) { | ||
130 | printf("%p: thread killed, removing imcomplete cached image\n", self); | ||
131 | remove(context->path); | ||
132 | release_load_context(self, context); | ||
133 | return NULL; | ||
134 | } | ||
135 | |||
136 | printf("%p: loader thread successfully finished\n", self); | ||
137 | release_load_context(self, context); | ||
138 | return NULL; | ||
139 | } | ||
140 | |||
141 | static void on_request_url(GtkHTML *html, const gchar *url, | ||
142 | GtkHTMLStream *stream, gpointer data) { | ||
143 | char buffer[4096]; | ||
144 | GnomeVFSFileSize bytes_read; | ||
145 | |||
146 | http_context_t *context = (http_context_t*)data; | ||
147 | |||
148 | if(context->cache) { | ||
149 | /* try to build local path */ | ||
150 | char *path = malloc(strlen(context->appdata->image_path)+ | ||
151 | strlen(context->cache->id)+ | ||
152 | 14); /* strlen("/xxxxxxxx.xxx\0") == 14 */ | ||
153 | |||
154 | strcpy(path, context->appdata->image_path); | ||
155 | strcat(path, context->cache->id); | ||
156 | sprintf(path+strlen(path), "/%lx", name_hash(url)); | ||
157 | |||
158 | /* only append extension if there's a useful one up to three chars ... */ | ||
159 | char *dot = strrchr(url, '.'); | ||
160 | if(dot) | ||
161 | if(strlen(dot) <= 4) | ||
162 | strcat(path, dot); | ||
163 | |||
164 | printf("cache name = %s\n", path); | ||
165 | if(g_file_test(path, G_FILE_TEST_EXISTS)) { | ||
166 | printf("image file exists!\n"); | ||
167 | |||
168 | FILE *f = fopen(path, "rb"); | ||
169 | |||
170 | while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) != 0) { | ||
171 | |||
172 | gtk_html_write(GTK_HTML(context->view), | ||
173 | stream, buffer, bytes_read); | ||
174 | |||
175 | while (gtk_events_pending ()) | ||
176 | gtk_main_iteration (); | ||
177 | } | ||
178 | fclose(f); | ||
179 | |||
180 | } else { | ||
181 | harbaum | 12 | printf("image file doesn't exist, starting extra thread!\n"); |
182 | harbaum | 1 | |
183 | harbaum | 12 | checkdir(path); |
184 | |||
185 | /* walk to end of list */ | ||
186 | load_context_t **load_context = &(context->load_context); | ||
187 | while(*load_context) | ||
188 | load_context = &(*load_context)->next; | ||
189 | |||
190 | *load_context = g_new0(load_context_t, 1); | ||
191 | |||
192 | (*load_context)->url = strdup(url); | ||
193 | (*load_context)->path = strdup(path); | ||
194 | (*load_context)->view = context->view; | ||
195 | (*load_context)->stream = stream; | ||
196 | (*load_context)->next = NULL; | ||
197 | (*load_context)->active = TRUE; | ||
198 | (*load_context)->mutex = g_mutex_new(); | ||
199 | |||
200 | g_thread_create(loader_thread, *load_context, TRUE, NULL); | ||
201 | return; | ||
202 | harbaum | 1 | } |
203 | } else { | ||
204 | /* not a cache, maybe help, so load images from icon directory */ | ||
205 | |||
206 | /* try to build local path */ | ||
207 | char *path = malloc(strlen(ICONPATH)+strlen(url)+1); | ||
208 | |||
209 | strcpy(path, ICONPATH); | ||
210 | strcat(path, url); | ||
211 | if(!g_file_test(path, G_FILE_TEST_EXISTS)) { | ||
212 | strcpy(path, "./icons/"); | ||
213 | strcat(path, url); | ||
214 | } | ||
215 | |||
216 | if(g_file_test(path, G_FILE_TEST_EXISTS)) { | ||
217 | FILE *f = fopen(path, "rb"); | ||
218 | |||
219 | while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) != 0) { | ||
220 | gtk_html_write(GTK_HTML(context->view), | ||
221 | stream, buffer, bytes_read); | ||
222 | |||
223 | while (gtk_events_pending ()) | ||
224 | gtk_main_iteration (); | ||
225 | } | ||
226 | fclose(f); | ||
227 | } | ||
228 | } | ||
229 | |||
230 | gtk_html_end(GTK_HTML(context->view), stream, GTK_HTML_STREAM_OK); | ||
231 | } | ||
232 | |||
233 | /* notify all open html views of zoom event */ | ||
234 | void html_zoom(appdata_t *appdata, gboolean in) { | ||
235 | struct html_view *html_view = appdata->html_view; | ||
236 | |||
237 | while(html_view) { | ||
238 | printf("zoom notify view %p\n", html_view->view); | ||
239 | |||
240 | if(in) gtk_html_zoom_in(GTK_HTML(html_view->view)); | ||
241 | else gtk_html_zoom_out(GTK_HTML(html_view->view)); | ||
242 | |||
243 | html_view = html_view->next; | ||
244 | } | ||
245 | } | ||
246 | |||
247 | harbaum | 2 | #ifndef NO_COPY_N_PASTE |
248 | harbaum | 1 | static void on_destroy_textview(GtkWidget *widget, gpointer data) { |
249 | appdata_t *appdata = (appdata_t*)data; | ||
250 | int destroy_active = FALSE; | ||
251 | |||
252 | /* only do this if main windows hasn't already been destroyed */ | ||
253 | if(!appdata->window) { | ||
254 | printf("detroy_textview: main window is gone\n"); | ||
255 | return; | ||
256 | } | ||
257 | |||
258 | if(!appdata->active_buffer) | ||
259 | printf("Destroy, but there was no active buffer!\n"); | ||
260 | else { | ||
261 | if(GTK_WIDGET_TYPE(widget) == GTK_TYPE_TEXT_VIEW) { | ||
262 | printf("destroying textview\n"); | ||
263 | |||
264 | if(appdata->active_buffer == | ||
265 | gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget))) { | ||
266 | printf("This was the active buffer\n"); | ||
267 | destroy_active = TRUE; | ||
268 | } | ||
269 | } else { | ||
270 | if(widget == (GtkWidget*)appdata->active_buffer) { | ||
271 | printf("This was the active html view\n"); | ||
272 | destroy_active = TRUE; | ||
273 | } | ||
274 | } | ||
275 | } | ||
276 | |||
277 | if(destroy_active) { | ||
278 | appdata->active_buffer = NULL; | ||
279 | |||
280 | gtk_widget_set_sensitive(appdata->menu_cut, FALSE); | ||
281 | gtk_widget_set_sensitive(appdata->menu_copy, FALSE); | ||
282 | gtk_widget_set_sensitive(appdata->menu_paste, FALSE); | ||
283 | } | ||
284 | } | ||
285 | harbaum | 2 | #endif |
286 | harbaum | 1 | |
287 | static void on_destroy_htmlview(GtkWidget *widget, gpointer data) { | ||
288 | http_context_t *context = (http_context_t*)data; | ||
289 | |||
290 | printf("destroying html context\n"); | ||
291 | // printf("associated view = %p\n", context->view); | ||
292 | |||
293 | struct html_view **h = &(context->appdata->html_view); | ||
294 | |||
295 | while(*h && (*h)->view != context->view) | ||
296 | h = &((*h)->next); | ||
297 | |||
298 | g_assert(h); | ||
299 | |||
300 | /* remove entry from chain */ | ||
301 | void *tmp = *h; | ||
302 | *h = (*h)->next; | ||
303 | free(tmp); | ||
304 | |||
305 | load_context_t *load_context = context->load_context; | ||
306 | while(load_context) { | ||
307 | printf("found an associated thread\n"); | ||
308 | |||
309 | load_context_t *tmp_context = load_context->next; | ||
310 | release_load_context(NULL, load_context); | ||
311 | load_context = tmp_context; | ||
312 | } | ||
313 | |||
314 | harbaum | 2 | #ifndef NO_COPY_N_PASTE |
315 | harbaum | 1 | on_destroy_textview(widget, context->appdata); |
316 | harbaum | 2 | #endif |
317 | harbaum | 1 | |
318 | /* destroy context */ | ||
319 | free(data); | ||
320 | } | ||
321 | |||
322 | harbaum | 2 | #ifndef NO_COPY_N_PASTE |
323 | harbaum | 1 | static gboolean focus_in(GtkWidget *widget, GdkEventFocus *event, |
324 | gpointer data) { | ||
325 | appdata_t *appdata = (appdata_t*)data; | ||
326 | |||
327 | printf("focus in!\n"); | ||
328 | |||
329 | /* these buffers are read-only, thus only "copy" is enabled */ | ||
330 | gtk_widget_set_sensitive(appdata->menu_cut, FALSE); | ||
331 | gtk_widget_set_sensitive(appdata->menu_copy, TRUE); | ||
332 | gtk_widget_set_sensitive(appdata->menu_paste, FALSE); | ||
333 | |||
334 | if(GTK_WIDGET_TYPE(widget) == GTK_TYPE_TEXT_VIEW) { | ||
335 | appdata->active_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); | ||
336 | } else if(GTK_WIDGET_TYPE(widget) == gtk_html_get_type()) { | ||
337 | appdata->active_buffer = (GtkTextBuffer*)widget; | ||
338 | } else { | ||
339 | printf("not a text nor a html view\n"); | ||
340 | } | ||
341 | |||
342 | return FALSE; | ||
343 | } | ||
344 | |||
345 | void html_copy_to_clipboard(appdata_t *appdata) { | ||
346 | gtk_html_copy(GTK_HTML(appdata->active_buffer)); | ||
347 | } | ||
348 | harbaum | 2 | #endif |
349 | harbaum | 1 | |
350 | #ifdef PANNABLE_HTML | ||
351 | /* eat the button events */ | ||
352 | static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, | ||
353 | gpointer user_data) { | ||
354 | return TRUE; | ||
355 | } | ||
356 | #endif | ||
357 | |||
358 | /* the cache descriptions are not valid html and need some surrounding stuff */ | ||
359 | static const char *html_start = "<html><head>" | ||
360 | "<meta http-equiv=content-type content=\"text/html; charset=UTF-8\">" | ||
361 | "</head></body>"; | ||
362 | static const char *html_end = "</body></html>"; | ||
363 | |||
364 | GtkWidget *html_view(appdata_t *appdata, char *text, | ||
365 | harbaum | 140 | html_mode_t mode, gboolean scrollwin, |
366 | harbaum | 1 | cache_t *cache, char *anchor) { |
367 | GtkWidget *view; | ||
368 | |||
369 | harbaum | 140 | if(mode == HTML_HTML) { |
370 | harbaum | 1 | http_context_t *context = g_new0(http_context_t, 1); |
371 | context->appdata = appdata; | ||
372 | context->cache = cache; | ||
373 | |||
374 | context->view = view = gtk_html_new(); | ||
375 | |||
376 | /* create a callback to load images only if a cache has been given */ | ||
377 | /* so that images can be cached/stored appropriately */ | ||
378 | g_signal_connect(G_OBJECT(view), "url_requested", | ||
379 | G_CALLBACK(on_request_url), context); | ||
380 | |||
381 | GtkHTMLStream *stream = gtk_html_begin(GTK_HTML(view)); | ||
382 | |||
383 | gtk_html_write(GTK_HTML(view), stream, html_start, strlen(html_start)); | ||
384 | gtk_html_write(GTK_HTML(view), stream, text, strlen(text)); | ||
385 | gtk_html_write(GTK_HTML(view), stream, html_end, strlen(html_end)); | ||
386 | gtk_html_end(GTK_HTML(view), stream, GTK_HTML_STREAM_OK); | ||
387 | |||
388 | if(anchor) { | ||
389 | while(gtk_events_pending()) | ||
390 | gtk_main_iteration (); | ||
391 | |||
392 | gtk_html_jump_to_anchor(GTK_HTML(context->view), anchor); | ||
393 | } | ||
394 | |||
395 | /* register this html view */ | ||
396 | struct html_view **h = &(appdata->html_view); | ||
397 | while(*h) h = &((*h)->next); | ||
398 | *h = g_new0(struct html_view, 1); | ||
399 | (*h)->view = view; | ||
400 | |||
401 | #ifdef PANNABLE_HTML | ||
402 | /* this causes finger scrolling to work nicely but also prevents */ | ||
403 | /* copy'n paste from working correctly */ | ||
404 | gtk_widget_set_sensitive(GTK_WIDGET(view), FALSE); | ||
405 | |||
406 | g_signal_connect(G_OBJECT(view), "button-press-event", | ||
407 | G_CALLBACK(on_button_press), NULL); | ||
408 | #endif | ||
409 | |||
410 | g_signal_connect(G_OBJECT(view), "destroy", | ||
411 | G_CALLBACK(on_destroy_htmlview), context); | ||
412 | } else { | ||
413 | GtkTextBuffer *buffer = gtk_text_buffer_new(NULL); | ||
414 | gtk_text_buffer_set_text(buffer, text, strlen(text)); | ||
415 | |||
416 | harbaum | 8 | #ifndef USE_HILDON_TEXT_VIEW |
417 | harbaum | 1 | view = gtk_text_view_new_with_buffer(buffer); |
418 | #else | ||
419 | view = hildon_text_view_new(); | ||
420 | hildon_text_view_set_buffer(HILDON_TEXT_VIEW(view), buffer); | ||
421 | #endif | ||
422 | |||
423 | gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD); | ||
424 | gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE); | ||
425 | gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE); | ||
426 | |||
427 | harbaum | 2 | #ifndef NO_COPY_N_PASTE |
428 | harbaum | 1 | g_signal_connect(G_OBJECT(view), "destroy", |
429 | G_CALLBACK(on_destroy_textview), appdata); | ||
430 | harbaum | 2 | #endif |
431 | harbaum | 1 | } |
432 | |||
433 | harbaum | 2 | #ifndef NO_COPY_N_PASTE |
434 | harbaum | 1 | g_signal_connect(G_OBJECT(view), "focus-in-event", |
435 | G_CALLBACK(focus_in), appdata); | ||
436 | harbaum | 2 | #endif |
437 | harbaum | 1 | |
438 | if(scrollwin) { | ||
439 | #ifndef USE_PANNABLE_AREA | ||
440 | GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); | ||
441 | gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), | ||
442 | GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); | ||
443 | gtk_container_add(GTK_CONTAINER(scrolled_window), view); | ||
444 | |||
445 | #if 1 | ||
446 | return scrolled_window; | ||
447 | #else | ||
448 | GtkWidget *fixed = gtk_fixed_new(); | ||
449 | gtk_fixed_put(GTK_FIXED(fixed), scrolled_window, 0, 0); | ||
450 | GtkWidget *tbutton = gtk_toggle_button_new_with_label("sel"); | ||
451 | gtk_fixed_put(GTK_FIXED(fixed), tbutton, 0, 0); | ||
452 | return fixed; | ||
453 | #endif | ||
454 | #else | ||
455 | #ifndef PANNABLE_HTML | ||
456 | harbaum | 140 | if(mode == HTML_HTML) { |
457 | harbaum | 1 | GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); |
458 | gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), | ||
459 | GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); | ||
460 | gtk_container_add(GTK_CONTAINER(scrolled_window), view); | ||
461 | return scrolled_window; | ||
462 | } else | ||
463 | #endif | ||
464 | { | ||
465 | GtkWidget *pannable_area = hildon_pannable_area_new(); | ||
466 | gtk_container_add(GTK_CONTAINER(pannable_area), view); | ||
467 | return pannable_area; | ||
468 | } | ||
469 | #endif | ||
470 | } | ||
471 | |||
472 | return view; | ||
473 | } | ||
474 |