77f690f997d83c63592b124b7b33e6cb0a04a3a3
[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 char* title_a = gtk_label_get_text( gtk_bin_get_child(GTK_BIN(a)) );
187 char* title_b = gtk_label_get_text( gtk_bin_get_child(GTK_BIN(b)) );
188 return g_ascii_strcasecmp(title_a, title_b);
189 }
190
191 static int find_menu_item_by_name( gpointer a, gpointer b )
192 {
193 PtkAppMenuItem* data = g_object_get_qdata( G_OBJECT(a), data_id );
194 const char* name = (char*)b;
195 return strcmp(data->name, b);
196 }
197
198 /* Moved to misc.c of lxpanel to be used in other plugins */
199 #if 0
200 static char* translate_exec( const char* exec, const char* icon,
201 const char* title, const char* fpath )
202 {
203 GString* cmd = g_string_sized_new( 256 );
204 for( ; *exec; ++exec )
205 {
206 if( G_UNLIKELY(*exec == '%') )
207 {
208 ++exec;
209 if( !*exec )
210 break;
211 switch( *exec )
212 {
213 case 'c':
214 g_string_append( cmd, title );
215 break;
216 case 'i':
217 if( icon )
218 {
219 g_string_append( cmd, "--icon " );
220 g_string_append( cmd, icon );
221 }
222 break;
223 case 'k':
224 {
225 char* uri = g_filename_to_uri( fpath, NULL, NULL );
226 g_string_append( cmd, uri );
227 g_free( uri );
228 break;
229 }
230 case '%':
231 g_string_append_c( cmd, '%' );
232 break;
233 }
234 }
235 else
236 g_string_append_c( cmd, *exec );
237 }
238 return g_string_free( cmd, FALSE );
239 }
240 #endif
241
242 void unload_old_icons( GtkWidget* menu )
243 {
244 GList* items = gtk_container_get_children( GTK_CONTAINER(menu) );
245 GList* l;
246 for( l = items; l; l = l->next )
247 {
248 GtkWidget* sub_menu = gtk_menu_item_get_submenu( GTK_MENU_ITEM(l->data) );
249 GtkWidget* img = gtk_image_menu_item_get_image( GTK_IMAGE_MENU_ITEM(l->data) );
250 if( ! g_object_get_qdata( G_OBJECT(l->data), data_id ) )
251 continue;
252 if( img )
253 gtk_widget_destroy( img );
254 if( sub_menu )
255 unload_old_icons( sub_menu );
256 }
257 g_list_free( items );
258 }
259
260 static void on_menu_item_size_request( GtkWidget* item,
261 GtkRequisition* req,
262 gpointer user_data )
263 {
264 if( req->height < ICON_SIZE )
265 req->height = ICON_SIZE;
266 if( req->width < ICON_SIZE )
267 req->width = ICON_SIZE;
268 }
269
270 static gboolean on_menu_item_expose( GtkWidget* item,
271 GdkEventExpose* evt,
272 gpointer user_data )
273 {
274 GtkWidget* img;
275 GdkPixbuf* pix;
276 PtkAppMenuItem* data = (PtkAppMenuItem*)user_data;
277 if( !data )
278 return FALSE;
279 img = gtk_image_menu_item_get_image(item);
280 if( img )
281 return FALSE;
282 if( G_UNLIKELY(!data) || G_UNLIKELY(!data->icon) )
283 return FALSE;
284 pix = NULL;
285 if( data->icon[0] == '/' )
286 {
287 pix = gdk_pixbuf_new_from_file_at_size(data->icon, ICON_SIZE, ICON_SIZE, NULL);
288 }
289 else
290 {
291 GtkIconInfo* inf;
292 inf = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), data->icon, ICON_SIZE, 0);
293 if( inf )
294 {
295 pix = gdk_pixbuf_new_from_file_at_size( gtk_icon_info_get_filename(inf), ICON_SIZE, ICON_SIZE, NULL);
296 gtk_icon_info_free ( inf );
297 }
298 }
299 if( G_LIKELY(pix) )
300 {
301 img = gtk_image_new_from_pixbuf( pix );
302 if( G_LIKELY(pix) )
303 g_object_unref( pix );
304 }
305 else
306 {
307 img = gtk_image_new();
308 gtk_image_set_pixel_size( img, ICON_SIZE );
309 }
310 gtk_image_menu_item_set_image( item, img );
311 return FALSE;
312 }
313
314 static void on_app_menu_item_activate( GtkMenuItem* item, PtkAppMenuItem* data )
315 {
316 GError* err = NULL;
317 /* FIXME: support startup notification */
318 g_debug("run command: %s", data->exec);
319 if( !g_spawn_command_line_async( data->exec, &err ) )
320 {
321 /* FIXME: show error message */
322 g_error_free( err );
323 }
324 }
325
326 static void ptk_app_menu_item_free( PtkAppMenuItem* data )
327 {
328 g_free( data->name );
329 g_free( data->icon );
330 g_free( data->exec );
331 g_slice_free( PtkAppMenuItem, data );
332 }
333
334 static void do_load_dir( int prefix_len,
335 const char* path,
336 GList** sub_menus )
337 {
338 GDir* dir = g_dir_open( path, 0, NULL );
339 const char* name;
340 GKeyFile* file;
341
342 if( G_UNLIKELY( ! dir ) )
343 return;
344
345 file = g_key_file_new();
346
347 while( name = g_dir_read_name( dir ) )
348 {
349 char* fpath;
350 char **cats, **cat;
351 char **only_show_in;
352
353 if( name[0] =='.' )
354 continue;
355 fpath = g_build_filename( path, name, NULL );
356 if( g_file_test(fpath, G_FILE_TEST_IS_DIR) )
357 {
358 do_load_dir( prefix_len, fpath, sub_menus );
359 g_free( fpath );
360 continue;
361 }
362 if( ! g_str_has_suffix( name, ".desktop" ) )
363 {
364 g_free( fpath );
365 continue;
366 }
367 if( ! g_key_file_load_from_file( file, fpath, 0, NULL ) )
368 {
369 g_free( fpath );
370 continue;
371 }
372 if( g_key_file_get_boolean( file, desktop_ent, "NoDisplay", NULL ) )
373 {
374 g_free( fpath );
375 continue;
376 }
377 only_show_in = g_key_file_get_string_list( file, desktop_ent, "OnlyShowIn", NULL, NULL );
378 if( only_show_in )
379 {
380 g_free( fpath );
381 g_strfreev( only_show_in );
382 continue;
383 }
384 cats = g_key_file_get_string_list( file, desktop_ent, "Categories", NULL, NULL );
385 if( cats )
386 {
387 int i = find_cat( cats );
388 if( i >= 0 )
389 {
390 GtkWidget* menu_item;
391 char *title, *exec, *icon;
392 /* FIXME: processing duplicated items */
393 exec = g_key_file_get_string( file, desktop_ent, "Exec", NULL);
394 if( exec )
395 {
396 title = g_key_file_get_locale_string( file, desktop_ent, "Name", NULL, NULL);
397 if( title )
398 {
399 PtkAppMenuItem* data;
400 GList* prev;
401 prev =g_list_find_custom( sub_menus[i], (fpath + prefix_len),
402 find_menu_item_by_name );
403 if( ! prev )
404 {
405 menu_item = gtk_image_menu_item_new_with_label( title );
406 data = g_slice_new0(PtkAppMenuItem);
407 }
408 else
409 {
410 GtkLabel* label;
411 menu_item = GTK_WIDGET(prev->data);
412 label = GTK_LABEL(gtk_bin_get_child(GTK_BIN(menu_item)));
413 data = (PtkAppMenuItem*)g_object_get_qdata( menu_item, data_id );
414 gtk_label_set_text( label, title );
415 g_free( data->name );
416 g_free( data->exec );
417 g_free( data->icon );
418 }
419 data->name = g_strdup( fpath + prefix_len );
420 data->exec = exec ? translate_exec_to_cmd( exec, data->icon, title, fpath ) : NULL;
421 g_free( title );
422 g_signal_connect( menu_item, "expose-event", on_menu_item_expose, data );
423 g_signal_connect( menu_item, "size-request", on_menu_item_size_request, data );
424 icon = g_strdup( g_key_file_get_string( file, desktop_ent, "Icon", NULL) );
425 if( icon )
426 {
427 char* dot = strchr( icon, '.' );
428 if( icon[0] !='/' && dot )
429 *dot = '\0';
430 }
431 data->icon = icon;
432 if( !prev )
433 {
434 g_signal_connect( menu_item, "activate", on_app_menu_item_activate, data );
435 g_object_set_qdata_full( menu_item, data_id, data, ptk_app_menu_item_free );
436 sub_menus[i] = g_list_insert_sorted( sub_menus[i], menu_item,compare_menu_item_titles );
437 }
438 } /* if( title ) */
439 g_free( exec );
440 } /* if( exec ) */
441 }
442 g_strfreev(cats);
443 }
444 g_free( fpath );
445 }
446 g_key_file_free( file );
447 g_dir_close( dir );
448 }
449
450 static void load_dir( const char* path, GList** sub_menus )
451 {
452 do_load_dir( strlen( path ) + 1, path, sub_menus );
453 }
454
455 static GtkWidget* app_menu = NULL;
456 static void on_menu( GtkWidget* btn, gpointer user_data )
457 {
458 if( ptk_app_menu_need_reload() )
459 {
460 if( app_menu )
461 gtk_widget_destroy( app_menu );
462 app_menu = ptk_app_menu_new();
463 }
464 else if( !app_menu )
465 app_menu = ptk_app_menu_new();
466 gtk_menu_popup(GTK_MENU(app_menu), NULL, NULL, NULL, NULL, 0, 0 );
467 }
468
469 void on_app_menu_destroy( gpointer user_data, GObject* menu )
470 {
471 g_signal_handler_disconnect( gtk_icon_theme_get_default(),
472 GPOINTER_TO_INT(user_data) );
473 --n_ref;
474 if( n_ref == 0 )
475 {
476 g_free( times );
477 times = NULL;
478 }
479 }
480
481 gboolean ptk_app_menu_item_has_data( GtkMenuItem* item )
482 {
483 return (g_object_get_qdata( G_OBJECT(item), data_id ) != NULL);
484 }
485
486 /*
487 * Insert application menus into specified menu
488 * menu: The parent menu to which the items should be inserted
489 * pisition: Position to insert items.
490 Passing -1 in this parameter means append all items
491 at the end of menu.
492 */
493 void ptk_app_menu_insert_items( GtkMenu* menu, int position )
494 {
495 GList* sub_menus[ G_N_ELEMENTS(known_cats) ] = {0};
496 int i;
497 GList *sub_items, *l;
498 guint change_handler;
499
500 if( ! data_id )
501 data_id = g_quark_from_static_string("PtkAppMenuItem");
502 app_dirs_foreach( (GFunc) load_dir, sub_menus );
503 for( i = 0; i < G_N_ELEMENTS(known_cats); ++i )
504 {
505 GtkMenu* sub_menu;
506 GtkWidget* menu_item;
507 PtkAppMenuItem* data;
508 if( ! (sub_items = sub_menus[i]) )
509 continue;
510 sub_menu = gtk_menu_new();
511
512 for( l = sub_items; l; l = l->next )
513 gtk_menu_shell_append( GTK_MENU_SHELL(sub_menu), GTK_WIDGET(l->data) );
514 g_list_free( sub_items );
515 menu_item = gtk_image_menu_item_new_with_label( _(known_cats[i].title) );
516 data = g_slice_new0( PtkAppMenuItem );
517 data->icon = g_strdup(known_cats[i].icon);
518 g_object_set_qdata_full( G_OBJECT(menu_item), data_id, data, (GDestroyNotify) ptk_app_menu_item_free );
519 g_signal_connect( menu_item, "expose-event", G_CALLBACK(on_menu_item_expose), data );
520 g_signal_connect( menu_item, "size-request", G_CALLBACK(on_menu_item_size_request), data );
521 on_menu_item_expose( menu_item, NULL, data );
522 gtk_menu_item_set_submenu( GTK_MENU_ITEM(menu_item), GTK_WIDGET(sub_menu) );
523 if( position == -1 )
524 gtk_menu_shell_append( GTK_MENU_SHELL(menu), menu_item );
525 else
526 {
527 gtk_menu_shell_insert( GTK_MENU_SHELL(menu), menu_item, position );
528 ++position;
529 }
530 }
531 gtk_widget_show_all(GTK_WIDGET(menu));
532 change_handler = g_signal_connect_swapped( gtk_icon_theme_get_default(), "changed", G_CALLBACK(unload_old_icons), menu );
533 g_object_weak_ref( G_OBJECT(menu), on_app_menu_destroy, GINT_TO_POINTER(change_handler) );
534 ++n_ref;
535 }
536
537 GtkWidget* ptk_app_menu_new()
538 {
539 GtkWidget* menu;
540 menu = gtk_menu_new();
541 ptk_app_menu_insert_items( GTK_MENU(menu), -1 );
542 return menu;
543 }
544
545 void app_dirs_foreach( GFunc func, gpointer user_data )
546 {
547 const char** sys_dirs = (const char**)g_get_system_data_dirs();
548 char* path;
549 int i, len;
550 struct stat dir_stat;
551
552 len = g_strv_length((gchar **) sys_dirs);
553 if( !times )
554 times = g_new0( time_t, len + 2 );
555 for( i = 0; i < len; ++i )
556 {
557 path = g_build_filename( sys_dirs[i], app_dir_name, NULL );
558 if( stat( path, &dir_stat) == 0 )
559 {
560 times[i] = dir_stat.st_mtime;
561 func( path, user_data );
562 }
563 g_free( path );
564 }
565 path = g_build_filename( g_get_user_data_dir(), app_dir_name, NULL );
566 times[i] = dir_stat.st_mtime;
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
575 #if defined( PTK_APP_MENU_DEMO )
576 int main( int argc, char** argv )
577 {
578 gtk_init(&argc, &argv);
579 GtkWidget* window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
580 gtk_window_set_title(GTK_WINDOW( window ), "Show Applications" );
581 GtkWidget* button = gtk_button_new_with_label("Application Menu");
582 g_signal_connect(button, "clicked", G_CALLBACK(on_menu), NULL );
583 gtk_container_add( GTK_CONTAINER(window), button );
584 g_signal_connect(window, "delete-event", G_CALLBACK(gtk_main_quit), NULL );
585 gtk_widget_show_all(window);
586 if( app_menu )
587 gtk_widget_destroy( app_menu );
588 gtk_main();
589 return 0;
590 }
591 #endif
592
593 gboolean ptk_app_menu_need_reload()
594 {
595 const char** sys_dirs = (const char**)g_get_system_data_dirs();
596 char* path;
597 int i, len;
598 struct stat dir_stat;
599
600 if( !times )
601 return TRUE;
602 len = g_strv_length((gchar **) sys_dirs);
603 for( i = 0; i < len; ++i )
604 {
605 path = g_build_filename( sys_dirs[i], app_dir_name, NULL );
606 if( stat( path, &dir_stat) == 0 )
607 {
608 if( times[i] != dir_stat.st_mtime )
609 {
610 g_free( path );
611 return TRUE;
612 }
613 }
614 g_free( path );
615 }
616 path = g_build_filename( g_get_user_data_dir(), app_dir_name, NULL );
617 if( stat( path, &dir_stat) == 0 )
618 {
619 if( times[i] != dir_stat.st_mtime )
620 {
621 g_free( path );
622 return TRUE;
623 }
624 }
625 g_free( path );
626 return FALSE;
627 }
628