]> icculus.org git repositories - dana/openbox.git/blob - openbox/apps_menu.c
wip: Add config_parser.c which will provide a nice means to specify config variables...
[dana/openbox.git] / openbox / apps_menu.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    apps_menu.c for the Openbox window manager
4    Copyright (c) 2011        Dana Jansens
5
6    This program 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 2 of the License, or
9    (at your option) any later version.
10
11    This program 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    See the COPYING file for a copy of the GNU General Public License.
17 */
18
19 #include "openbox.h"
20 #include "menu.h"
21 #include "menuframe.h"
22 #include "screen.h"
23 #include "apps_menu.h"
24 #include "config.h"
25 #include "gettext.h"
26 #include "obt/bsearch.h"
27 #include "obt/linkbase.h"
28 #include "obt/link.h"
29 #include "obt/paths.h"
30
31 #include <glib.h>
32
33 #define MENU_NAME "apps-menu"
34
35 typedef struct _ObAppsMenuCategory ObAppsMenuCategory;
36
37 struct _ObAppsMenuCategory {
38     /*! The quark for the internal name of the category, eg. AudioVideo. These
39       come from the XDG menu specification. */
40     GQuark q;
41     /*! The user-visible (translated) name for the category. */
42     const gchar *friendly;
43     /*! The string identifier of the menu */
44     gchar *menu_name;
45     /*! The submenu for this category.  Its label will be "apps-menu-<category>
46       where <category> is the internal name of the category referred to by q.
47     */
48     ObMenu *menu;
49     /*! The menu entry in the apps menu for this. */
50     ObMenuEntry *entry;
51     /*! */
52     GPtrArray *links;
53 };
54
55 static ObMenu *apps_menu;
56 static ObtLinkBase *linkbase;
57 static gboolean dirty;
58
59 static ObAppsMenuCategory *categories;
60 /*! An array of pointers to the categories array, sorted by their friendly
61   names. */
62 static ObAppsMenuCategory **sorted_categories;
63 static guint n_categories;
64
65 static void self_destroy(ObMenu *menu, gpointer data)
66 {
67     menu_clear_entries(menu);
68 }
69
70 static gboolean self_update(ObMenuFrame *frame, gpointer data)
71 {
72     guint i;
73     gboolean sort;
74
75     if (!dirty) return TRUE;  /* nothing has changed to be updated */
76
77     sort = FALSE;
78     for (i = 0; i < n_categories; ++i) {
79         ObAppsMenuCategory *cat = &categories[i];
80         GList *it, *entries;
81         guint j;
82         gboolean sort_submenu;
83
84         sort_submenu = FALSE; /* sort the category's submenu? */
85
86         /* add/remove categories from the main apps menu if they were/are
87            empty. */
88         if (cat->links->len == 0) {
89             if (cat->entry) {
90                 menu_entry_remove(cat->entry);
91                 cat->entry = NULL;
92             }
93         }
94         else if (!cat->entry) {
95             cat->entry = menu_add_submenu(apps_menu, 0, cat->menu_name);
96             sort = TRUE;
97         }
98
99         /* add/remove links from this category. */
100         entries = g_list_copy(cat->menu->entries);
101         it = entries; /* iterates through the entries list */
102         j = 0; /* iterates through the valid ObtLinks list */
103         while (j < cat->links->len) {
104             ObtLink *m, *l;
105             int r;
106
107             /* get the next link from the menu */
108             if (it)
109                 m = ((ObMenuEntry*)it->data)->data.normal.data;
110             else
111                 m = NULL;
112             /* get the next link from our list */
113             l = g_ptr_array_index(cat->links, j);
114
115             if (it && m == NULL) {
116                 GList *next = it->next;
117
118                 /* the entry was removed from the linkbase and nulled here, so
119                    drop it from the menu */
120                 menu_entry_remove(it->data);
121                 r = 0;
122
123                 /* don't move forward in the category list, as we're removing
124                    something from the menu here, but do continue moving
125                    in the menu in a safe way. */
126                 it = next;
127             }
128             else {
129                 if (it)
130                     r = obt_link_cmp_by_name(&m, &l);
131                 else
132                     /* we reached the end of the menu, but there's more in
133                        the list, so just add it */
134                     r = 1;
135
136                 if (r > 0) {
137                     /* the menu progressed faster than the list, the list has
138                        something it doesn't, so add that */
139                     ObMenuEntry *e;
140                     e = menu_add_normal(
141                         cat->menu, 0, obt_link_name(l), NULL, FALSE);
142                     e->data.normal.data = l; /* save the link in the entry */
143                     sort_submenu = TRUE;
144                 }
145                 else
146                     /* if r < 0 then we didn't null something that was removed
147                        from the list */
148                     g_assert(r == 0);
149
150                 if (r == 0)
151                     /* if we did not add anything ot the menu, then move
152                        forward in the menu. otherwise, stay put as we inserted
153                        something just before this item, and want to compare
154                        it again. */
155                     it = g_list_next(it);
156                 /* the category link was either found in the menu or added to
157                    it, so move to the next */
158                 ++j;
159             }
160         }
161
162         if (sort_submenu)
163             menu_sort_entries(cat->menu);
164     }
165
166     if (sort) {
167         menu_sort_entries(apps_menu);
168     }
169
170     dirty = FALSE;
171     return TRUE; /* always show the menu */
172 }
173
174 static gboolean add_link_to_category(ObtLink *link, ObAppsMenuCategory *cat)
175 {
176     guint i;
177     gboolean add;
178
179     /* check for link in our existing list */
180     add = TRUE;
181     for (i = 0; i < cat->links->len; ++i) {
182         ObtLink *l = g_ptr_array_index(cat->links, i);
183         if (l == link) {
184             add = FALSE;
185             break;
186         }
187     }
188
189     if (add) { /* wasn't found in our list already */
190         g_ptr_array_add(cat->links, link);
191         obt_link_ref(link);
192
193         dirty = TRUE; /* never set dirty to FALSE here.. */
194     }
195     return add;
196 }
197
198 static gboolean remove_link_from_category(ObtLink *link,
199                                           ObAppsMenuCategory *cat)
200 {
201     gboolean rm;
202
203     rm = g_ptr_array_remove(cat->links, link);
204
205     if (rm) {
206         GList *it;
207
208         dirty = TRUE; /* never set dirty to FALSE here.. */
209
210         /* set the data inside the category's menu for this link to NULL
211            since it stops existing now */
212         if (cat->menu) {
213             for (it = cat->menu->entries; it; it = g_list_next(it)) {
214                 ObMenuEntry *e = it->data;
215                 /* our submenus only contain normal entries */
216                 if (e->data.normal.data == link) {
217                     /* this menu entry points to this link, but the
218                        link is going away, so point it at NULL. */
219                     e->data.normal.data = NULL;
220                     break;
221                 }
222             }
223         }
224     }
225     return rm;
226 }
227
228 static void linkbase_update(ObtLinkBase *lb, ObtLink *removed,
229                             ObtLink *added, gpointer data)
230 {
231     const GQuark *cats;
232     gulong n_cats, i;
233
234     /* For each category in the link, search our list of categories and
235        if we are showing that category in the menu, add the link to it (or
236        remove the link from it). */
237
238 #define CAT_TO_INT(c) (c.q)
239     if (removed) {
240         cats = obt_link_app_categories(removed, &n_cats);
241         for (i = 0; i < n_cats; ++i) {
242             BSEARCH_SETUP();
243             BSEARCH_CMP(ObAppsMenuCategory, categories, 0, n_categories,
244                     cats[i], CAT_TO_INT);
245             if (BSEARCH_FOUND())
246                 remove_link_from_category(removed, &categories[BSEARCH_AT()]);
247         }
248     }
249
250     if (added) {
251         cats = obt_link_app_categories(added, &n_cats);
252         for (i = 0; i < n_cats; ++i) {
253             BSEARCH_SETUP();
254             BSEARCH_CMP(ObAppsMenuCategory, categories, 0, n_categories,
255                         cats[i], CAT_TO_INT);
256             if (BSEARCH_FOUND()) {
257                 add_link_to_category(added, &categories[BSEARCH_AT()]);
258                 g_ptr_array_sort(categories[BSEARCH_AT()].links,
259                                  obt_link_cmp_by_name);
260             }
261         }
262     }
263 #undef  CAT_TO_INT
264 }
265
266 static int cat_cmp(const void *a, const void *b)
267 {
268     const ObAppsMenuCategory *ca = a, *cb = b;
269     return ca->q - cb->q;
270 }
271
272 static int cat_friendly_cmp(const void *a, const void *b)
273 {
274     ObAppsMenuCategory *const *ca = a, *const *cb = b;
275     return strcmp((*ca)->friendly, (*cb)->friendly);
276 }
277
278 void apps_menu_startup(gboolean reconfig)
279 {
280     /* From http://standards.freedesktop.org/menu-spec/latest/apa.html */
281     struct {
282         const gchar *name;
283         const gchar *friendly;
284     } const cats[] = {
285         { "AudioVideo", "Sound & Video" },
286         { "Development", "Programming" },
287         { "Education", "Education" },
288         { "Game", "Games" },
289         { "Graphics", "Graphics" },
290         { "Network", "Internet" },
291         { "Office", "Office" },
292         { "Settings", "Settings" },
293         { "System", "System" },
294         { "Utility", "Accessories" },
295         { NULL, NULL }
296     };
297     guint i;
298
299     if (reconfig) {
300         /* Force a re-read of the applications available in case we are not
301            getting notifications about changes. */
302         obt_linkbase_refresh(linkbase);
303     }
304     else {
305         ObtPaths *paths;
306
307         paths = obt_paths_new();
308         /* XXX allow more environments, like GNOME or KDE, to be included */
309         linkbase = obt_linkbase_new(paths, ob_locale_msg,
310                                     OBT_LINK_ENV_OPENBOX|OBT_LINK_ENV_GNOME);
311         obt_paths_unref(paths);
312         obt_linkbase_set_update_func(linkbase, linkbase_update, NULL);
313
314         dirty = FALSE;
315
316         for (i = 0; cats[i].name; ++i); /* count them */
317         n_categories = i;
318
319         categories = g_new0(ObAppsMenuCategory, n_categories);
320         sorted_categories = g_new(ObAppsMenuCategory*, n_categories);
321
322         for (i = 0; cats[i].name; ++i) {
323             categories[i].q = g_quark_from_static_string(cats[i].name);
324             categories[i].friendly = _(cats[i].friendly);
325             categories[i].menu_name = g_strdup_printf(
326                 "%s-%s", MENU_NAME, cats[i].name);
327             categories[i].links = g_ptr_array_new_with_free_func(
328                 (GDestroyNotify)obt_link_unref);
329         }
330
331         /* Sort the categories by their quark values so we can binary search */
332         qsort(categories, n_categories, sizeof(ObAppsMenuCategory), cat_cmp);
333
334         for (i = 0; i < n_categories; ++i)
335             sorted_categories[i] = &categories[i];
336         qsort(sorted_categories, n_categories, sizeof(void*),
337               cat_friendly_cmp);
338
339         /* add all the existing links */
340         for (i = 0; i < n_categories; ++i) {
341             GSList *links, *it;
342
343             links = obt_linkbase_category(linkbase, categories[i].q);
344             for (it = links; it; it = g_slist_next(it))
345                 add_link_to_category(it->data, &categories[i]);
346
347             g_ptr_array_sort(categories[i].links, obt_link_cmp_by_name);
348         }
349     }
350
351     /* recreate menus every time since reconfiguring destroys them */
352
353     apps_menu = menu_new(MENU_NAME, _("Applications"), FALSE, NULL);
354     menu_set_update_func(apps_menu, self_update);
355     menu_set_destroy_func(apps_menu, self_destroy);
356
357     for (i = 0; cats[i].name; ++i)
358         categories[i].menu = menu_new(categories[i].menu_name,
359                                       categories[i].friendly, FALSE,
360                                       &categories[i]);
361
362 }
363
364 void apps_menu_shutdown(gboolean reconfig)
365 {
366     guint i;
367
368     /* the menu structures get freed automatically by the menu_hash table
369        so we must not keep any pointers around to them, and want to update
370        the menu next time we display it, so we mark it dirty. */
371     for (i = 0; i < n_categories; ++i) {
372         categories[i].menu = NULL;
373         categories[i].entry = NULL;
374     }
375     dirty = TRUE;
376
377     if (!reconfig) {
378         obt_linkbase_unref(linkbase);
379         linkbase = NULL;
380
381         for (i = 0; i < n_categories; ++i) {
382             g_free(categories[i].menu_name);
383             g_ptr_array_unref(categories[i].links);
384         }
385         g_free(categories);
386         categories = NULL;
387         g_free(sorted_categories);
388         sorted_categories = NULL;
389     }
390
391     apps_menu = NULL;
392 }