ede/ede-panel/applets/start-menu/XdgMenuReader.cpp
2013-01-09 16:06:45 +00:00

1079 lines
27 KiB
C++

/*
* $Id$
*
* Copyright (C) 2012 Sanel Zukan
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <FL/Fl_Shared_Image.H>
#include <edelib/TiXml.h>
#include <edelib/Debug.h>
#include <edelib/StrUtil.h>
#include <edelib/Util.h>
#include <edelib/FileTest.h>
#include <edelib/Directory.h>
#include <edelib/DesktopFile.h>
#include <edelib/IconLoader.h>
#include <edelib/Nls.h>
#include <edelib/Run.h>
#include "DesktopEntry.h"
#include "MenuRules.h"
#include "XdgMenuReader.h"
EDELIB_NS_USING(DesktopFile)
EDELIB_NS_USING(IconLoader)
EDELIB_NS_USING(system_config_dirs)
EDELIB_NS_USING(system_data_dirs)
EDELIB_NS_USING(user_data_dir)
EDELIB_NS_USING(build_filename)
EDELIB_NS_USING(file_test)
EDELIB_NS_USING(str_ends)
EDELIB_NS_USING(run_async)
EDELIB_NS_USING(FILE_TEST_IS_DIR)
EDELIB_NS_USING(DESK_FILE_TYPE_DIRECTORY)
EDELIB_NS_USING(ICON_SIZE_SMALL)
#define DOT_OR_DOTDOT(base) (base[0] == '.' && (base[1] == '\0' || (base[1] == '.' && base[2] == '\0')))
#define ELEMENT_IS(elem, val) (strcmp(elem->Value(), val) == 0)
#define ELEMENT_GET_TEXT(elem) (elem->FirstChild() ? elem->FirstChild()->ToText() : NULL)
/* max. name size */
#define NAME_BUFSZ 128
/* do not allow empty menus to be shown */
#define NO_EMPTY_MENUS 1
/* in FLTK, a default size */
extern int FL_NORMAL_SIZE;
struct MenuParseContext;
struct MenuContext;
typedef list<MenuParseContext*> MenuParseList;
typedef list<MenuParseContext*>::iterator MenuParseListIt;
typedef list<MenuContext*> MenuContextList;
typedef list<MenuContext*>::iterator MenuContextListIt;
struct MenuParseContext {
/* for <Deleted> <NotDeled> tags */
bool deleted;
/* for <OnlyUnallocated> and <NotOnlyUnallocated> (default) */
bool only_unallocated;
/* menu tag content */
String *name;
/* a stack of .directory names; the top one is used */
StrList dir_files;
/* directories where to find .directory file */
StrList dir_dirs;
/* a list of .desktop files */
DesktopEntryList desk_files;
/* include rules */
MenuRulesList include_rules;
/* exclude rules */
MenuRulesList exclude_rules;
/* nested menus */
MenuParseList submenus;
};
struct MenuContext {
/* menu label */
String *name;
/* should this entry be displayed */
bool display_it;
/* menu icon */
String *icon;
/* a list of .desktop files; at the same time also items in menu list */
DesktopEntryList items;
/* nested menus */
MenuContextList submenus;
};
/* TODO: bug in edelib */
static bool menu_context_sorter(MenuContext* const& c1, MenuContext* const& c2) {
return *(c1->name) < *(c2->name);
}
static MenuParseContext *menu_parse_context_new(void) {
MenuParseContext *m = new MenuParseContext;
m->name = NULL;
m->deleted = false;
m->only_unallocated = false;
return m;
}
static void menu_parse_context_delete(MenuParseContext *m) {
E_RETURN_IF_FAIL(m != NULL);
delete m->name;
/* delete rules */
if(!m->include_rules.empty()) {
MenuRulesListIt it = m->include_rules.begin(), it_end = m->include_rules.end();
while(it != it_end) {
menu_rules_delete(*it);
it = m->include_rules.erase(it);
}
}
if(!m->exclude_rules.empty()) {
MenuRulesListIt it = m->exclude_rules.begin(), it_end = m->exclude_rules.end();
while(it != it_end) {
menu_rules_delete(*it);
it = m->exclude_rules.erase(it);
}
}
/* recurse for nested menus */
if(!m->submenus.empty()) {
MenuParseListIt it = m->submenus.begin(), it_end = m->submenus.end();
while(it != it_end) {
menu_parse_context_delete(*it);
it = m->submenus.erase(it);
}
}
delete m;
}
static void menu_parse_context_append_default_dir_dirs(MenuParseContext *ctx) {
StrList lst;
int ret = system_data_dirs(lst);
if(ret < 1)
return;
StrListIt it = lst.begin(), it_end = lst.end();
for(; it != it_end; ++it)
ctx->dir_dirs.push_back(build_filename((*it).c_str(), "desktop-directories"));
}
static void menu_parse_context_append_desktop_files(MenuParseContext *ctx, const char *dir, const char *basedir) {
DIR *ds = opendir(dir);
if(!ds)
return;
dirent *dp;
DesktopEntry *entry;
while((dp = readdir(ds)) != NULL) {
/* skip dots and (possibly) hidden files */
if(dp->d_name[0] == '.' || DOT_OR_DOTDOT(dp->d_name))
continue;
entry = new DesktopEntry;
entry->assign_path(dir, dp->d_name, basedir);
if(file_test(entry->get_path(), FILE_TEST_IS_DIR)) {
/* recurse if needed; the spec said we must */
menu_parse_context_append_desktop_files(ctx, entry->get_path(), basedir);
/* delete it */
delete entry;
continue;
}
/* files must ends with this extension */
if(str_ends(entry->get_path(), ".desktop")) {
ctx->desk_files.push_back(entry);
} else {
/* clear non .desktop items */
delete entry;
}
}
closedir(ds);
}
static void menu_parse_context_append_desktop_files_from_xdg_data_dirs(MenuParseContext *ctx) {
StrList lst;
xdg_menu_applications_location(lst);
StrListIt it = lst.begin(), it_end = lst.end();
for(; it != it_end; ++it)
menu_parse_context_append_desktop_files(ctx, it->c_str(), it->c_str());
}
static void scan_include_exclude_tag(TiXmlNode *elem, MenuRulesList &rules) {
E_RETURN_IF_FAIL(elem != NULL);
TiXmlNode *child;
TiXmlText *txt;
for(child = elem->FirstChildElement(); child; child = child->NextSibling()) {
/* assure we do not include/exclude insinde include/exclude */
if(ELEMENT_IS(child, "Include") || ELEMENT_IS(child, "Exclude")) {
E_WARNING(E_STRLOC ": Nesting <Include> and <Exclude> tags are not supported\n");
continue;
}
if(ELEMENT_IS(child, "Filename")) {
txt = ELEMENT_GET_TEXT(child);
menu_rules_append_rule(rules, MENU_RULES_OPERATOR_FILENAME, txt->Value());
continue;
}
if(ELEMENT_IS(child, "Category")) {
txt = ELEMENT_GET_TEXT(child);
menu_rules_append_rule(rules, MENU_RULES_OPERATOR_CATEGORY, txt->Value());
continue;
}
if(ELEMENT_IS(child, "All")) {
menu_rules_append_rule(rules, MENU_RULES_OPERATOR_ALL, NULL);
continue;
}
if(ELEMENT_IS(child, "And")) {
MenuRules *and_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_AND, NULL);
/* recurse */
scan_include_exclude_tag(child, and_rule->subrules);
continue;
}
if(ELEMENT_IS(child, "Or")) {
MenuRules *or_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_OR, NULL);
/* recurse */
scan_include_exclude_tag(child, or_rule->subrules);
continue;
}
if(ELEMENT_IS(child, "Not")) {
MenuRules *not_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_NOT, NULL);
/* recurse */
scan_include_exclude_tag(child, not_rule->subrules);
continue;
}
}
}
static void scan_menu_tag(TiXmlNode *elem, MenuParseList &parse_list) {
E_RETURN_IF_FAIL(elem != NULL);
TiXmlText *txt;
bool got_default_app_dirs = false, got_default_dir_dirs = false;
MenuParseContext *ctx = menu_parse_context_new();
for(elem = elem->FirstChildElement(); elem; elem = elem->NextSibling()) {
/* in case we got '<Menu>' as submenu, dive in it recursively and fill submenus */
if(ELEMENT_IS(elem, "Menu"))
scan_menu_tag(elem, ctx->submenus);
if(ELEMENT_IS(elem, "Name")) {
txt = ELEMENT_GET_TEXT(elem);
if(txt && !ctx->name)
ctx->name = new String(txt->Value());
continue;
}
if(ELEMENT_IS(elem, "Directory")) {
txt = ELEMENT_GET_TEXT(elem);
/* entries must ends with .directory */
if(txt && str_ends(txt->Value(), ".directory")) {
/* push it at the top */
ctx->dir_files.push_front(txt->Value());
}
continue;
}
if(ELEMENT_IS(elem, "AppDir")) {
txt = ELEMENT_GET_TEXT(elem);
if(txt)
menu_parse_context_append_desktop_files(ctx, txt->Value(), NULL);
continue;
}
if(ELEMENT_IS(elem, "DirectoryDir")) {
txt = ELEMENT_GET_TEXT(elem);
if(txt) {
/* push it at the top */
ctx->dir_dirs.push_front(txt->Value());
}
continue;
}
/* spec: '<DefaultAppDirs>' expands to $XDG_DATA_DIRS/applications */
if(ELEMENT_IS(elem, "DefaultAppDirs")) {
if(!got_default_app_dirs) {
menu_parse_context_append_desktop_files_from_xdg_data_dirs(ctx);
/* scan it only once */
got_default_app_dirs = true;
}
continue;
}
if(ELEMENT_IS(elem, "DefaultDirectoryDirs")) {
if(!got_default_dir_dirs) {
menu_parse_context_append_default_dir_dirs(ctx);
got_default_dir_dirs = true;
}
continue;
}
if(ELEMENT_IS(elem, "Include")) {
scan_include_exclude_tag(elem, ctx->include_rules);
continue;
}
if(ELEMENT_IS(elem, "Exclude")) {
scan_include_exclude_tag(elem, ctx->exclude_rules);
continue;
}
if(ELEMENT_IS(elem, "Deleted")) {
ctx->deleted = true;
continue;
}
if(ELEMENT_IS(elem, "NotDeleted")) {
ctx->deleted = false;
continue;
}
if(ELEMENT_IS(elem, "OnlyUnallocated")) {
ctx->only_unallocated = true;
continue;
}
if(ELEMENT_IS(elem, "NotOnlyUnallocated")) {
ctx->only_unallocated = false;
continue;
}
}
parse_list.push_back(ctx);
}
static bool menu_context_construct_name_and_get_icon(MenuParseContext *m,
MenuParseContext *top,
String **ret_name,
String **ret_icon,
bool *should_be_displayed)
{
E_RETURN_VAL_IF_FAIL(m != NULL, false);
*ret_name = *ret_icon = NULL;
*should_be_displayed = true;
if(!m->dir_files.empty()) {
/*
* We have two locations where are keeping directory list: node specific and top node
* specific, where often is put <DefaultDirectoryDirs/> tag. Here, first we will look in
* node specific directory list, then will go in top node <DefaultDirectoryDirs/>.
*/
StrListIt dir_it = m->dir_dirs.begin(), dir_it_end = m->dir_dirs.end();
/* this list has 'stack-ed' items; the last one is on top */
StrListIt file_it, file_it_end = m->dir_files.end();
DesktopFile df;
String tmp;
/* first try specific directory list */
for(; dir_it != dir_it_end; ++dir_it) {
for(file_it = m->dir_files.begin(); file_it != file_it_end; ++file_it) {
tmp = build_filename((*dir_it).c_str(), (*file_it).c_str());
//E_DEBUG("==> %s\n", tmp.c_str());
/* load it and see if it is real .desktop file */
df.load(tmp.c_str());
if(df && (df.type() == DESK_FILE_TYPE_DIRECTORY)) {
/* check if it can be displayed */
if(df.no_display())
*should_be_displayed = false;
char buf[NAME_BUFSZ];
/* try icon first */
if(!(*ret_icon) && df.icon(buf, NAME_BUFSZ))
*ret_icon = new String(buf);
/* then name, so we can quit nicely */
if(!(*ret_name) && df.name(buf, NAME_BUFSZ)) {
*ret_name = new String(buf);
return true;
}
}
}
}
/* now try default ones */
dir_it = top->dir_dirs.begin(), dir_it_end = top->dir_dirs.end();
for(; dir_it != dir_it_end; ++dir_it) {
for(file_it = m->dir_files.begin(); file_it != file_it_end; ++file_it) {
tmp = build_filename((*dir_it).c_str(), (*file_it).c_str());
//E_DEBUG("++> %s\n", tmp.c_str());
/* load it and see if it is real .desktop file */
df.load(tmp.c_str());
if(df && (df.type() == DESK_FILE_TYPE_DIRECTORY)) {
/* check if it can be displayed */
if(df.no_display())
*should_be_displayed = false;
char buf[NAME_BUFSZ];
/* try icon first */
if(!(*ret_icon) && df.icon(buf, NAME_BUFSZ))
*ret_icon = new String(buf);
/* then name, so we can quit nicely */
if(!(*ret_name) && df.name(buf, NAME_BUFSZ)) {
*ret_name = new String(buf);
return true;
}
}
}
}
}
E_RETURN_VAL_IF_FAIL(m->name != NULL, false);
/* if there are no files and can be displayed, use context name; let icon name be NULL */
*ret_name = new String(*(m->name));
return true;
}
static void apply_include_rules(MenuContext *ctx, DesktopEntryList &items, MenuRulesList &rules) {
if(items.empty() || rules.empty())
return;
MenuRulesListIt rit, rit_end = rules.end();
DesktopEntryListIt it = items.begin(), it_end = items.end();
DesktopEntry *entry;
bool eval_true;
for(; it != it_end; ++it) {
for(rit = rules.begin(); rit != rit_end; ++rit) {
entry = *it;
eval_true = menu_rules_eval(*rit, entry);
/* append entry if matches to the rule, and mark it as allocated */
if(eval_true) {
entry->mark_as_allocated();
ctx->items.push_back(entry);
/* do not scan rules any more; go to the next item */
break;
}
}
}
}
static void apply_exclude_rules(DesktopEntryList& items, MenuRulesList &rules) {
if(items.empty() || rules.empty())
return;
MenuRulesListIt rit, rit_end = rules.end();
DesktopEntryListIt it = items.begin(), it_end = items.end();
bool eval_true;
while(it != it_end) {
eval_true = false;
for(rit = rules.begin(); rit != rit_end; ++rit) {
eval_true = menu_rules_eval(*rit, *it);
if(eval_true) {
/* pop entry if matches */
it = items.erase(it);
break;
}
}
if(!eval_true)
++it;
}
}
#if NO_EMPTY_MENUS
/* forward decl */
static void menu_context_delete(MenuContext *c);
#endif
static MenuContext *menu_parse_context_to_menu_context(MenuParseContext *m,
MenuParseContext *top,
DesktopEntryList *all_unallocated)
{
E_RETURN_VAL_IF_FAIL(m != NULL, NULL);
/* make sure we are not processing only_unallocated node when not get all_unallocated */
if(m->only_unallocated && !all_unallocated)
return NULL;
/*
* figure out the name first; if returns false, either menu should not be displayed, or something
* went wrong
*/
String *n = NULL, *ic = NULL;
bool should_be_displayed = false;
if(!menu_context_construct_name_and_get_icon(m, top, &n, &ic, &should_be_displayed))
return NULL;
/*
* nodes marked as 'NoDisplay' (from .directory file) or '<Deleted>' (from applications.menu) must
* be processed as ordinary nodes, since this operation will correctly setup allocated
* (<OnlyUnallocated> and <NotOnlyUnallocated>) entries
*/
if(m->deleted)
should_be_displayed = false;
/* assure we got name here; icon can be NULL */
E_RETURN_VAL_IF_FAIL(n != NULL, NULL);
MenuContext *ctx = new MenuContext;
ctx->name = n;
ctx->icon = ic;
ctx->display_it = should_be_displayed;
//E_DEBUG("+ Menu: %s %i\n", ctx->name->c_str(), m->include_rules.size());
/* fill MenuContext items, depending on what list was passed */
if(all_unallocated) {
apply_include_rules(ctx, *all_unallocated, m->include_rules);
} else {
apply_include_rules(ctx, m->desk_files, m->include_rules);
/* check the rules for top list, but make sure we are not applying them on the same node again */
if(m != top)
apply_include_rules(ctx, top->desk_files, m->include_rules);
}
/* pop filled MenuContext items if match the rule */
apply_exclude_rules(ctx->items, m->exclude_rules);
//E_DEBUG("- Menu: %s %i\n", ctx->name->c_str(), ctx->items.size());
/* sort entries via their full names */
desktop_entry_list_sort(ctx->items);
/* process submenus */
if(!m->submenus.empty()) {
MenuParseListIt mit = m->submenus.begin(), mit_end = m->submenus.end();
MenuContext *sub_ctx;
for(; mit != mit_end; ++mit) {
sub_ctx = menu_parse_context_to_menu_context(*mit, top, all_unallocated);
if(sub_ctx)
ctx->submenus.push_back(sub_ctx);
}
}
#if NO_EMPTY_MENUS
/* do not allow empty menus */
if(ctx->items.empty() && ctx->submenus.empty()) {
menu_context_delete(ctx);
ctx = NULL;
}
#endif
return ctx;
}
static void menu_context_delete(MenuContext *c) {
E_RETURN_IF_FAIL(c != NULL);
if(!c->submenus.empty()) {
MenuContextListIt it = c->submenus.begin(), it_end = c->submenus.end();
for(; it != it_end; ++it)
menu_context_delete(*it);
}
c->items.clear();
delete c->name;
delete c->icon;
delete c;
}
static void menu_parse_context_list_get_all_unallocated_desk_files(MenuParseList &parse_list, DesktopEntryList &ret) {
if(parse_list.empty())
return;
MenuParseListIt it = parse_list.begin(), it_end = parse_list.end();
DesktopEntryListIt dit, dit_end;
MenuParseContext *parse_ctx;
for(; it != it_end; ++it) {
parse_ctx = *it;
dit = parse_ctx->desk_files.begin();
dit_end = parse_ctx->desk_files.end();
for(; dit != dit_end; ++dit) {
if((*dit)->is_allocated() == false)
ret.push_back(*dit);
}
/* recurse */
menu_parse_context_list_get_all_unallocated_desk_files(parse_ctx->submenus, ret);
}
}
static void menu_context_list_sort(MenuContextList &lst) {
if(lst.empty())
return;
lst.sort(menu_context_sorter);
MenuContextListIt it = lst.begin(), it_end = lst.end();
for(; it != it_end; ++it)
menu_context_list_sort((*it)->submenus);
}
static void menu_parse_context_list_to_menu_context_list(MenuParseList &parse_list,
MenuContextList &ret)
{
MenuParseListIt it = parse_list.begin(), it_end = parse_list.end();
MenuParseContext *parse_ctx;
MenuContext *ctx;
for(; it != it_end; ++it) {
parse_ctx = *it;
/* remove duplicate id's */
desktop_entry_list_remove_duplicates(parse_ctx->desk_files);
/* read all .desktop files from disk */
desktop_entry_list_load_all(parse_ctx->desk_files);
/* now convert it to usable menu node */
ctx = menu_parse_context_to_menu_context(parse_ctx, parse_ctx, NULL);
if(ctx)
ret.push_back(ctx);
}
/* now, pickup all unallocated items */
DesktopEntryList all_unallocated;
menu_parse_context_list_get_all_unallocated_desk_files(parse_list, all_unallocated);
/*
* second pass; process unallocated items, but put them in second list that will later be
* merged; this is to preserve the order got in the first list
*/
MenuContextList unallocated_list;
for(it = parse_list.begin(); it != it_end; ++it) {
parse_ctx = *it;
/* now convert it to usable menu node */
ctx = menu_parse_context_to_menu_context(parse_ctx, parse_ctx, &all_unallocated);
if(ctx)
unallocated_list.push_back(ctx);
}
/*
* both list have the same root node, so we skip the first node and merge below it; the first node is
* top level <Menu> and is often only menu name and description
*/
E_RETURN_IF_FAIL(ret.size() == 1);
E_RETURN_IF_FAIL(unallocated_list.size() == 1);
MenuContext *head = ret.front(), *unallocated_head = unallocated_list.front();
MenuContextListIt uit = unallocated_head->submenus.begin(), uit_end = unallocated_head->submenus.end();
for(; uit != uit_end; ++uit)
head->submenus.push_back(*uit);
/* sort everthing at the end */
menu_context_list_sort(ret);
}
/*
* Count the number of items in each submenu + submenu node itself; used to allocate
* array for edelib::MenuItem list.
*/
static unsigned int menu_context_list_count(MenuContextList &lst) {
if(lst.empty())
return 0;
unsigned int ret = lst.size();
MenuContextListIt it = lst.begin(), it_end = lst.end();
MenuContext *cc;
for(; it != it_end; ++it) {
cc = *it;
ret += cc->items.size();
ret += menu_context_list_count(cc->submenus);
/*
* a room for NULL to deduce submenus in edelib::MenuItem, no matter if submenus are empty
* in case empty menus are going to be displayed
*/
ret += 1;
}
return ret;
}
static void menu_all_parse_lists_clear(MenuParseList &parse_list, MenuContextList &ctx_list) {
MenuContextListIt cit = ctx_list.begin(), cit_end = ctx_list.end();
MenuParseListIt pit = parse_list.begin(), pit_end = parse_list.end();
MenuParseContext *cc;
while(cit != cit_end) {
menu_context_delete(*cit);
cit = ctx_list.erase(cit);
}
while(pit != pit_end) {
cc = *pit;
/*
* Desktop entries are shared among MenuContext and MenuParseContext, so they
* must be explicitly deleted. This sharing depends on 'Include' rules, so some MenuParseContext
* entries are on MenuContext list and all MenuContext entries are in MenuParseContext.
*/
DesktopEntryListIt it = cc->desk_files.begin(), it_end = cc->desk_files.end();
while(it != it_end) {
delete *it;
it = cc->desk_files.erase(it);
}
menu_parse_context_delete(cc);
pit = parse_list.erase(pit);
}
}
static TiXmlNode *load_menu_file(TiXmlDocument &doc) {
char *menu_prefix = getenv("XDG_MENU_PREFIX");
String menu_file;
if(menu_prefix) {
menu_file = menu_prefix;
menu_file += "applications.menu";
} else {
menu_file = "applications.menu";
}
StrList paths;
if(system_config_dirs(paths) < 1)
return NULL;
String tmp;
StrListIt it = paths.begin(), it_end = paths.end();
for(; it != it_end; ++it) {
tmp = build_filename((*it).c_str(), "menus", menu_file.c_str());
if(doc.LoadFile(tmp.c_str()))
goto done;
}
return NULL;
done:
return doc.FirstChild("Menu");
}
static void menu_context_list_dump(MenuContextList &lst) {
if(lst.empty())
return;
MenuContextListIt it = lst.begin(), it_end = lst.end();
DesktopEntryListIt ds, de;
for(; it != it_end; ++it) {
if((*it)->display_it == false)
continue;
ds = (*it)->items.begin();
de = (*it)->items.end();
/* print each desktop entry with menu name */
for(; ds != de; ++ds) {
printf("%s/\t%s\t%s\n", (*it)->name->c_str(),
(*ds)->get_id(),
(*ds)->get_path());
}
menu_context_list_dump((*it)->submenus);
}
}
static void menu_all_parse_lists_load(MenuParseList &parse_list, MenuContextList &content) {
/*
* TiXmlDocument object must be used externaly, so as long as this object is
* alive, the whole XML tree is alive too (see DOM reference).
*/
TiXmlDocument doc;
TiXmlNode *elem = load_menu_file(doc);
if(!elem)
return;
/* parse XML file */
scan_menu_tag(elem, parse_list);
/* convert it to our list */
menu_parse_context_list_to_menu_context_list(parse_list, content);
}
void xdg_menu_dump_for_test_suite(void) {
MenuParseList pl;
MenuContextList cl;
/* load everything */
menu_all_parse_lists_load(pl, cl);
menu_context_list_dump(cl);
/* clear everything */
menu_all_parse_lists_clear(pl, cl);
}
/* public API */
struct XdgMenuContent {
MenuItem *fltk_menu;
/*
* We are keeping them as fltk_menu references some objects, like text.
* This is since FLTK menu can't free label strings as MenuItem is plain struct.
*/
MenuParseList parse_list;
MenuContextList context_list;
};
static void item_cb(Fl_Widget*, void *en) {
DesktopEntry *entry = (DesktopEntry*)en;
run_async("ede-launch %s", entry->get_exec());
E_DEBUG(E_STRLOC ": ede-launch %s\n", entry->get_exec());
}
static void logout_cb(Fl_Widget*, void*) {
run_async("ede-shutdown");
}
static unsigned int construct_edelib_menu(MenuContextList &lst, MenuItem *mi, unsigned int pos) {
if(lst.empty())
return pos;
MenuContextListIt it = lst.begin(), it_end = lst.end();
MenuContext *cc;
DesktopEntryListIt ds, de;
unsigned long initial_pos = pos;
for(; it != it_end; ++it) {
cc = *it;
if(!cc->display_it)
continue;
mi[pos].text = cc->name->c_str();
/* every MenuContext is submenu for itself */
mi[pos].flags = FL_SUBMENU;
//E_DEBUG("{%s submenu}\n", mi[pos].text);
/* some default values that must be filled */
mi[pos].shortcut_ = 0;
mi[pos].callback_ = 0;
mi[pos].user_data_ = 0;
mi[pos].labeltype_ = FL_NORMAL_LABEL;
mi[pos].labelfont_ = FL_HELVETICA;
mi[pos].labelsize_ = FL_NORMAL_SIZE;
mi[pos].labelcolor_ = FL_BLACK;
MenuItem::init_extensions(&mi[pos]);
/* set image for menu */
if(cc->icon && IconLoader::inited()) {
Fl_Image *img = IconLoader::get(cc->icon->c_str(), ICON_SIZE_SMALL);
mi[pos].image(img);
}
/* a room for an item */
pos++;
/* try with nested submenus first, so submenus be before desktop entries in current menu node */
pos = construct_edelib_menu(cc->submenus, mi, pos);
/* now, add the real items if they exists*/
if(!cc->items.empty()) {
ds = cc->items.begin();
de = cc->items.end();
for(; ds != de; ++ds, ++pos) {
mi[pos].text = (*ds)->get_name();
mi[pos].flags = 0;
//E_DEBUG(" {%s item}\n", mi[pos].text);
/* some default values that must be filled */
mi[pos].shortcut_ = 0;
/* set callback and callback data to be current entry */
mi[pos].callback_ = item_cb;
mi[pos].user_data_ = *ds;
mi[pos].labeltype_ = FL_NORMAL_LABEL;
mi[pos].labelfont_ = FL_HELVETICA;
mi[pos].labelsize_ = FL_NORMAL_SIZE;
mi[pos].labelcolor_ = FL_BLACK;
MenuItem::init_extensions(&mi[pos]);
/* set image for menu item*/
if((*ds)->get_icon() && IconLoader::inited()) {
Fl_Image *img = IconLoader::get((*ds)->get_icon(), ICON_SIZE_SMALL);
mi[pos].image(img);
}
/* set tooltip if we have; it is actually a comment from .desktop file */
mi[pos].tooltip((*ds)->get_comment());
}
}
/* to inject Logout button */
if(initial_pos == 0) {
//E_DEBUG(" {Logout item}\n");
mi[pos].text = _("Logout");
if(pos)
mi[pos - 1].flags |= FL_MENU_DIVIDER;
mi[pos].flags = 0;
mi[pos].shortcut_ = 0;
mi[pos].labeltype_ = FL_NORMAL_LABEL;
mi[pos].labelfont_ = FL_HELVETICA;
mi[pos].labelsize_ = FL_NORMAL_SIZE;
mi[pos].labelcolor_ = FL_BLACK;
MenuItem::init_extensions(&mi[pos]);
/* set callback and callback data to be current entry */
mi[pos].callback_ = logout_cb;
mi[pos].user_data_ = 0;
if(IconLoader::inited()) {
Fl_Image *img = IconLoader::get("system-log-out", ICON_SIZE_SMALL);
mi[pos].image(img);
}
pos++;
}
/* end this menu */
mi[pos].text = NULL;
MenuItem::init_extensions(&mi[pos]);
//E_DEBUG("{0}\n");
/* make a room for the next item */
pos++;
}
/* return position to next item */
return pos;
}
void xdg_menu_applications_location(StrList &lst) {
lst.clear();
if(system_data_dirs(lst) < 1)
return;
StrListIt it = lst.begin(), it_end = lst.end();
for(; it != it_end; ++it)
*it = build_filename(it->c_str(), "applications");
/*
* Add user directory too; the spec is unclear about this, but official menu spec tests
* requires it. Also, users will be able to add menu items without superuser permissions.
*/
String user_dir = user_data_dir();
lst.push_back(build_filename(user_dir.c_str(), "applications"));
}
XdgMenuContent *xdg_menu_load(void) {
XdgMenuContent *content = new XdgMenuContent();
/* load everything */
menu_all_parse_lists_load(content->parse_list, content->context_list);
unsigned int sz = menu_context_list_count(content->context_list);
E_RETURN_VAL_IF_FAIL(sz > 0, NULL);
MenuItem *mi = new MenuItem[sz + 2]; /* plus logout + ending NULL */
unsigned int pos = construct_edelib_menu(content->context_list, mi, 0);
mi[pos].text = NULL;
/*
* MenuItem does not have constructor, so everywhere where we access MenuItem object, image
* member must be NULL-ed too
*/
MenuItem::init_extensions(&mi[pos]);
E_ASSERT(pos <= sz + 2);
content->fltk_menu = mi;
return content;
}
void xdg_menu_delete(XdgMenuContent *m) {
E_RETURN_IF_FAIL(m != NULL);
delete [] m->fltk_menu;
menu_all_parse_lists_clear(m->parse_list, m->context_list);
delete m;
}
MenuItem *xdg_menu_to_fltk_menu(XdgMenuContent *m) {
E_RETURN_VAL_IF_FAIL(m != NULL, NULL);
return m->fltk_menu;
}