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