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