GCC warning sweep and correct the C-style casting.
[lxde/lxpanel.git] / src / plugins / ptk-app-menu.c
1 /*
2 * ptk-app-menu.c
3 *
4 * Description: Generate menu from desktop files according to the spec on freedesktop.org
5 *
6 *
7 * Author: Hong Jen Yee (PCMan) <pcman.tw (AT) gmail.com>, (C) 2006
8 *
9 * Copyright: GNU Lesser General Public License Version 2
10 *
11 */
12
13 #include <gtk/gtk.h>
14 #include <glib/gi18n.h>
15 #include <stdio.h>
16 #include <sys/stat.h>
17 #include <string.h>
18 #include "ptk-app-menu.h"
19
20 /* Compatibility macros for older versions of glib */
21 #if ! GLIB_CHECK_VERSION(2, 10, 0)
22 /* older versions of glib don't provde g_slice API */
23 #define g_slice_alloc(size) g_malloc(size)
24 #define g_slice_alloc0(size) g_malloc0(size)
25 #define g_slice_new(type) g_new(type, 1)
26 #define g_slice_new0(type) g_new0(type, 1)
27 #define g_slice_free(type, mem) g_free(mem)
28 #define g_slice_free1(size, mem) g_free(mem)
29 #endif
30
31 #include "misc.h" /* Misc functions for lxpanel */
32
33 #define ICON_SIZE 24
34
35 GtkWidget* ptk_app_menu_new();
36
37 const char desktop_ent[] = "Desktop Entry";
38 const char app_dir_name[] = "applications";
39 static time_t* times = NULL;
40 static int n_ref = 0;
41
42 typedef struct _CatInfo
43 {
44 char* title;
45 char* icon;
46 const char** sub_cats;
47 }CatInfo;
48
49 typedef struct _PtkAppMenuItem
50 {
51 char* name;
52 char* icon;
53 char* exec;
54 }PtkAppMenuItem;
55
56 static guint data_id = 0;
57
58 const char* development_cats[]={
59 "Development",
60 "Translation",
61 "Building","Debugger",
62 "IDE",
63 "GUIDesigner",
64 "Profiling",
65 "RevisionControl",
66 "WebDevelopment",
67 NULL
68 };
69 const char* office_cats[] = {
70 "Office",
71 "Dictionary",
72 "Chart",
73 "Calendar",
74 "ContactManagement",
75 "Database",
76 NULL
77 };
78 const char* graphics_cats[] = {
79 "Graphics",
80 "2DGraphics",
81 "3DGraphics",
82 "VectorGraphics",
83 "RasterGraphics",
84 "Viewer",
85 NULL
86 };
87 const char* network_cats[] = {
88 "Network",
89 "Dialup",
90 "Email",
91 "WebBrowser",
92 "InstantMessaging",
93 "IRCClient",
94 "FileTransfer",
95 "News",
96 "P2P",
97 "RemoteAccess",
98 "Telephony",
99 NULL
100 };
101 const char* settings_cats[] = {
102 "Settings",
103 "DesktopSettings",
104 "HardwareSettings",
105 "Accessibility",
106 NULL
107 };
108 const char* system_cats[] = {
109 "System",
110 "Core",
111 "Security",
112 "PackageManager",
113 NULL
114 };
115 const char* audiovideo_cats[] ={
116 "AudioVideo",
117 "Audio",
118 "Video",
119 "Mixer",
120 "Sequencer",
121 "Tuner",
122 "TV",
123 "AudioVideoEditing",
124 "Player",
125 "Recorder",
126 "DiscBurning",
127 "Music",
128 NULL
129 };
130 const char* game_cats[] = {
131 "Game",
132 "Amusement",
133 NULL
134 };
135 const char* education_cats[] = {
136 "Education",
137 NULL
138 };
139 const char* utility_cats[] = {
140 "Utility",
141 NULL
142 };
143
144 const CatInfo known_cats[]=
145 {
146 {N_("Other"), "gnome-other", NULL},
147 {N_("Game"), "gnome-joystick", game_cats},
148 {N_("Education"), "gnome-amusements", education_cats},
149 {N_("Development"), "gnome-devel", development_cats},
150 {N_("Audio & Video"), "gnome-multimedia", audiovideo_cats},
151 {N_("Graphics"), "gnome-graphics", graphics_cats},
152 {N_("Settings"), "gnome-settings", settings_cats},
153 {N_("System Tools"), "gnome-system", system_cats},
154 {N_("Network"), "gnome-globe", network_cats},
155 {N_("Office"), "gnome-applications", office_cats},
156 {N_("Accessories"), "gnome-util", utility_cats}
157 };
158
159 int find_cat( char** cats )
160 {
161 char** cat;
162 for( cat = cats; *cat; ++cat )
163 {
164 int i;
165 /* Skip other */
166 for( i = 1; i < G_N_ELEMENTS(known_cats); ++i )
167 {
168 const char** sub_cats = known_cats[i].sub_cats;
169 while( *sub_cats )
170 {
171 if( 0 == strncmp(*cat, "X-", 2) ) /* Desktop specific*/
172 return -1;
173 if( 0 == strcmp( *sub_cats, *cat ) )
174 return i;
175 ++sub_cats;
176 }
177 }
178 }
179 return -1;
180 }
181
182 static void app_dirs_foreach( GFunc func, gpointer user_data );
183
184 static int compare_menu_item_titles( gpointer a, gpointer b )
185 {
186 const gchar *title_a, *title_b;
187 title_a = gtk_label_get_text( GTK_LABEL(gtk_bin_get_child(GTK_BIN(a))) );
188 title_b = gtk_label_get_text( GTK_LABEL(gtk_bin_get_child(GTK_BIN(b))) );
189 return g_ascii_strcasecmp(title_a, title_b);
190 }
191
192 static int find_menu_item_by_name( gpointer a, gpointer b )
193 {
194 PtkAppMenuItem* data = g_object_get_qdata( G_OBJECT(a), data_id );
195 const char* name = (char*)b;
196 return strcmp(data->name, name);
197 }
198
199 /* Moved to misc.c of lxpanel to be used in other plugins */
200 #if 0
201 static char* translate_exec( const char* exec, const char* icon,
202 const char* title, const char* fpath )
203 {
204 GString* cmd = g_string_sized_new( 256 );
205 for( ; *exec; ++exec )
206 {
207 if( G_UNLIKELY(*exec == '%') )
208 {
209 ++exec;
210 if( !*exec )
211 break;
212 switch( *exec )
213 {
214 case 'c':
215 g_string_append( cmd, title );
216 break;
217 case 'i':
218 if( icon )
219 {
220 g_string_append( cmd, "--icon " );
221 g_string_append( cmd, icon );
222 }
223 break;
224 case 'k':
225 {
226 char* uri = g_filename_to_uri( fpath, NULL, NULL );
227 g_string_append( cmd, uri );
228 g_free( uri );
229 break;
230 }
231 case '%':
232 g_string_append_c( cmd, '%' );
233 break;
234 }
235 }
236 else
237 g_string_append_c( cmd, *exec );
238 }
239 return g_string_free( cmd, FALSE );
240 }
241 #endif
242
243 void unload_old_icons( GtkWidget* menu )
244 {
245 GList* items = gtk_container_get_children( GTK_CONTAINER(menu) );
246 GList* l;
247 for( l = items; l; l = l->next )
248 {
249 GtkWidget* sub_menu = gtk_menu_item_get_submenu( GTK_MENU_ITEM(l->data) );
250 GtkWidget* img = gtk_image_menu_item_get_image( GTK_IMAGE_MENU_ITEM(l->data) );
251 if( ! g_object_get_qdata( G_OBJECT(l->data), data_id ) )
252 continue;
253 if( img )
254 gtk_widget_destroy( img );
255 if( sub_menu )
256 unload_old_icons( sub_menu );
257 }
258 g_list_free( items );
259 }
260
261 static void on_menu_item_size_request( GtkWidget* item,
262 GtkRequisition* req,
263 gpointer user_data )
264 {
265 if( req->height < ICON_SIZE )
266 req->height = ICON_SIZE;
267 if( req->width < ICON_SIZE )
268 req->width = ICON_SIZE;
269 }
270
271 static gboolean on_menu_item_expose( GtkWidget* item,
272 GdkEventExpose* evt,
273 gpointer user_data )
274 {
275 GtkWidget* img;
276 GdkPixbuf* pix;
277 PtkAppMenuItem* data = (PtkAppMenuItem*)user_data;
278 if( !data )
279 return FALSE;
280 img = GTK_WIDGET(gtk_image_menu_item_get_image((GtkImageMenuItem *) item));
281 if( img )
282 return FALSE;
283 if( G_UNLIKELY(!data) || G_UNLIKELY(!data->icon) )
284 return FALSE;
285 pix = NULL;
286 if( data->icon[0] == '/' )
287 {
288 pix = gdk_pixbuf_new_from_file_at_size(data->icon, ICON_SIZE, ICON_SIZE, NULL);
289 }
290 else
291 {
292 GtkIconInfo* inf;
293 inf = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), data->icon, ICON_SIZE, 0);
294 if( inf )
295 {
296 pix = gdk_pixbuf_new_from_file_at_size( gtk_icon_info_get_filename(inf), ICON_SIZE, ICON_SIZE, NULL);
297 gtk_icon_info_free ( inf );
298 }
299 }
300 if( G_LIKELY(pix) )
301 {
302 img = gtk_image_new_from_pixbuf( pix );
303 if( G_LIKELY(pix) )
304 g_object_unref( pix );
305 }
306 else
307 {
308 img = gtk_image_new();
309 gtk_image_set_pixel_size( GTK_IMAGE(img), ICON_SIZE );
310 }
311 gtk_image_menu_item_set_image( (GtkImageMenuItem *) item, img );
312 return FALSE;
313 }
314
315 static void on_app_menu_item_activate( GtkMenuItem* item, PtkAppMenuItem* data )
316 {
317 GError* err = NULL;
318 /* FIXME: support startup notification */
319 g_debug("run command: %s", data->exec);
320 if( !g_spawn_command_line_async( data->exec, &err ) )
321 {
322 /* FIXME: show error message */
323 g_error_free( err );
324 }
325 }
326
327 static void ptk_app_menu_item_free( PtkAppMenuItem* data )
328 {
329 g_free( data->name );
330 g_free( data->icon );
331 g_free( data->exec );
332 g_slice_free( PtkAppMenuItem, data );
333 }
334
335 static void do_load_dir( int prefix_len,
336 const char* path,
337 GList** sub_menus )
338 {
339 GDir* dir = g_dir_open( path, 0, NULL );
340 const char* name;
341 GKeyFile* file;
342
343 if( G_UNLIKELY( ! dir ) )
344 return;
345
346 file = g_key_file_new();
347
348 while( (name = g_dir_read_name( dir )) )
349 {
350 char* fpath;
351 char **cats;
352 char **only_show_in;
353
354 if( name[0] =='.' )
355 continue;
356 fpath = g_build_filename( path, name, NULL );
357 if( g_file_test(fpath, G_FILE_TEST_IS_DIR) )
358 {
359 do_load_dir( prefix_len, fpath, sub_menus );
360 g_free( fpath );
361 continue;
362 }
363 if( ! g_str_has_suffix( name, ".desktop" ) )
364 {
365 g_free( fpath );
366 continue;
367 }
368 if( ! g_key_file_load_from_file( file, fpath, 0, NULL ) )
369 {
370 g_free( fpath );
371 continue;
372 }
373 if( g_key_file_get_boolean( file, desktop_ent, "NoDisplay", NULL ) )
374 {
375 g_free( fpath );
376 continue;
377 }
378 only_show_in = g_key_file_get_string_list( file, desktop_ent, "OnlyShowIn", NULL, NULL );
379 if( only_show_in )
380 {
381 g_free( fpath );
382 g_strfreev( only_show_in );
383 continue;
384 }
385 cats = g_key_file_get_string_list( file, desktop_ent, "Categories", NULL, NULL );
386 if( cats )
387 {
388 int i = find_cat( cats );
389 if( i >= 0 )
390 {
391 GtkWidget* menu_item;
392 char *title, *exec, *icon;
393 /* FIXME: processing duplicated items */
394 exec = g_key_file_get_string( file, desktop_ent, "Exec", NULL);
395 if( exec )
396 {
397 title = g_key_file_get_locale_string( file, desktop_ent, "Name", NULL, NULL);
398 if( title )
399 {
400 PtkAppMenuItem* data;
401 GList* prev;
402 prev =g_list_find_custom( sub_menus[i], (fpath + prefix_len),
403 (GCompareFunc) find_menu_item_by_name );
404 if( ! prev )
405 {
406 menu_item = gtk_image_menu_item_new_with_label( title );
407 data = g_slice_new0(PtkAppMenuItem);
408 }
409 else
410 {
411 GtkLabel* label;
412 menu_item = GTK_WIDGET(prev->data);
413 label = GTK_LABEL(gtk_bin_get_child(GTK_BIN(menu_item)));
414 data = (PtkAppMenuItem*)g_object_get_qdata( G_OBJECT(menu_item), data_id );
415 gtk_label_set_text( label, title );
416 g_free( data->name );
417 g_free( data->exec );
418 g_free( data->icon );
419 }
420 data->name = g_strdup( fpath + prefix_len );
421 data->exec = exec ? translate_exec_to_cmd( exec, data->icon, title, fpath ) : NULL;
422 g_free( title );
423 g_signal_connect( menu_item, "expose-event",
424 G_CALLBACK(on_menu_item_expose), data );
425 g_signal_connect( menu_item, "size-request",
426 G_CALLBACK(on_menu_item_size_request), data );
427 icon = g_strdup( g_key_file_get_string( file, desktop_ent, "Icon", NULL) );
428 if( icon )
429 {
430 char* dot = strchr( icon, '.' );
431 if( icon[0] !='/' && dot )
432 *dot = '\0';
433 }
434 data->icon = icon;
435 if( !prev )
436 {
437 g_signal_connect( menu_item, "activate",
438 G_CALLBACK(on_app_menu_item_activate), data );
439 g_object_set_qdata_full( G_OBJECT(menu_item), data_id, data,
440 (GDestroyNotify) ptk_app_menu_item_free );
441 sub_menus[i] = g_list_insert_sorted( sub_menus[i],
442 (gpointer) menu_item,
443 (GCompareFunc) compare_menu_item_titles );
444 }
445 } /* if( title ) */
446 g_free( exec );
447 } /* if( exec ) */
448 }
449 g_strfreev(cats);
450 }
451 g_free( fpath );
452 }
453 g_key_file_free( file );
454 g_dir_close( dir );
455 }
456
457 static void load_dir( const char* path, GList** sub_menus )
458 {
459 do_load_dir( strlen( path ) + 1, path, sub_menus );
460 }
461
462 #if defined( PTK_APP_MENU_DEMO )
463 static GtkWidget* app_menu = NULL;
464 static void on_menu( GtkWidget* btn, gpointer user_data )
465 {
466 if( ptk_app_menu_need_reload() )
467 {
468 if( app_menu )
469 gtk_widget_destroy( app_menu );
470 app_menu = ptk_app_menu_new();
471 }
472 else if( !app_menu )
473 app_menu = ptk_app_menu_new();
474 gtk_menu_popup(GTK_MENU(app_menu), NULL, NULL, NULL, NULL, 0, 0 );
475 }
476 #endif
477
478 void on_app_menu_destroy( gpointer user_data, GObject* menu )
479 {
480 g_signal_handler_disconnect( gtk_icon_theme_get_default(),
481 GPOINTER_TO_INT(user_data) );
482 --n_ref;
483 if( n_ref == 0 )
484 {
485 g_free( times );
486 times = NULL;
487 }
488 }
489
490 gboolean ptk_app_menu_item_has_data( GtkMenuItem* item )
491 {
492 return (g_object_get_qdata( G_OBJECT(item), data_id ) != NULL);
493 }
494
495 /*
496 * Insert application menus into specified menu
497 * menu: The parent menu to which the items should be inserted
498 * pisition: Position to insert items.
499 Passing -1 in this parameter means append all items
500 at the end of menu.
501 */
502 void ptk_app_menu_insert_items( GtkMenu* menu, int position )
503 {
504 GList* sub_menus[ G_N_ELEMENTS(known_cats) ] = {0};
505 int i;
506 GList *sub_items, *l;
507 guint change_handler;
508
509 if( ! data_id )
510 data_id = g_quark_from_static_string("PtkAppMenuItem");
511 app_dirs_foreach( (GFunc) load_dir, sub_menus );
512 for( i = 0; i < G_N_ELEMENTS(known_cats); ++i )
513 {
514 GtkMenu* sub_menu;
515 GtkWidget* menu_item;
516 PtkAppMenuItem* data;
517 if( ! (sub_items = sub_menus[i]) )
518 continue;
519 sub_menu = GTK_MENU(gtk_menu_new());
520
521 for( l = sub_items; l; l = l->next )
522 gtk_menu_shell_append( GTK_MENU_SHELL(sub_menu), GTK_WIDGET(l->data) );
523 g_list_free( sub_items );
524 menu_item = gtk_image_menu_item_new_with_label( _(known_cats[i].title) );
525 data = g_slice_new0( PtkAppMenuItem );
526 data->icon = g_strdup(known_cats[i].icon);
527 g_object_set_qdata_full( G_OBJECT(menu_item), data_id, data, (GDestroyNotify) ptk_app_menu_item_free );
528 g_signal_connect( menu_item, "expose-event", G_CALLBACK(on_menu_item_expose), data );
529 g_signal_connect( menu_item, "size-request", G_CALLBACK(on_menu_item_size_request), data );
530 on_menu_item_expose( menu_item, NULL, data );
531 gtk_menu_item_set_submenu( GTK_MENU_ITEM(menu_item), GTK_WIDGET(sub_menu) );
532 if( position == -1 )
533 gtk_menu_shell_append( GTK_MENU_SHELL(menu), menu_item );
534 else
535 {
536 gtk_menu_shell_insert( GTK_MENU_SHELL(menu), menu_item, position );
537 ++position;
538 }
539 }
540 gtk_widget_show_all(GTK_WIDGET(menu));
541 change_handler = g_signal_connect_swapped( gtk_icon_theme_get_default(), "changed", G_CALLBACK(unload_old_icons), menu );
542 g_object_weak_ref( G_OBJECT(menu), on_app_menu_destroy, GINT_TO_POINTER(change_handler) );
543 ++n_ref;
544 }
545
546 GtkWidget* ptk_app_menu_new()
547 {
548 GtkWidget* menu;
549 menu = gtk_menu_new();
550 ptk_app_menu_insert_items( GTK_MENU(menu), -1 );
551 return menu;
552 }
553
554 void app_dirs_foreach( GFunc func, gpointer user_data )
555 {
556 const char** sys_dirs = (const char**)g_get_system_data_dirs();
557 char* path;
558 int i, len;
559 struct stat dir_stat;
560
561 len = g_strv_length((gchar **) sys_dirs);
562 if( !times )
563 times = g_new0( time_t, len + 2 );
564 for( i = 0; i < len; ++i )
565 {
566 path = g_build_filename( sys_dirs[i], app_dir_name, NULL );
567 if( stat( path, &dir_stat) == 0 )
568 {
569 times[i] = dir_stat.st_mtime;
570 func( path, user_data );
571 }
572 g_free( path );
573 }
574 path = g_build_filename( g_get_user_data_dir(), app_dir_name, NULL );
575 times[i] = dir_stat.st_mtime;
576 if( stat( path, &dir_stat) == 0 )
577 {
578 times[i] = dir_stat.st_mtime;
579 func( path, user_data );
580 }
581 g_free( path );
582 }
583
584 #if defined( PTK_APP_MENU_DEMO )
585 int main( int argc, char** argv )
586 {
587 gtk_init(&argc, &argv);
588 GtkWidget* window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
589 gtk_window_set_title(GTK_WINDOW( window ), "Show Applications" );
590 GtkWidget* button = gtk_button_new_with_label("Application Menu");
591 g_signal_connect(button, "clicked", G_CALLBACK(on_menu), NULL );
592 gtk_container_add( GTK_CONTAINER(window), button );
593 g_signal_connect(window, "delete-event", G_CALLBACK(gtk_main_quit), NULL );
594 gtk_widget_show_all(window);
595 if( app_menu )
596 gtk_widget_destroy( app_menu );
597 gtk_main();
598 return 0;
599 }
600 #endif
601
602 gboolean ptk_app_menu_need_reload()
603 {
604 const char** sys_dirs = (const char**)g_get_system_data_dirs();
605 char* path;
606 int i, len;
607 struct stat dir_stat;
608
609 if( !times )
610 return TRUE;
611 len = g_strv_length((gchar **) sys_dirs);
612 for( i = 0; i < len; ++i )
613 {
614 path = g_build_filename( sys_dirs[i], app_dir_name, NULL );
615 if( stat( path, &dir_stat) == 0 )
616 {
617 if( times[i] != dir_stat.st_mtime )
618 {
619 g_free( path );
620 return TRUE;
621 }
622 }
623 g_free( path );
624 }
625 path = g_build_filename( g_get_user_data_dir(), app_dir_name, NULL );
626 if( stat( path, &dir_stat) == 0 )
627 {
628 if( times[i] != dir_stat.st_mtime )
629 {
630 g_free( path );
631 return TRUE;
632 }
633 }
634 g_free( path );
635 return FALSE;
636 }
637