+ gunichar shortcut = 0;
+
+ *position = 0;
+ *always_show = FALSE;
+
+ g_assert(strippedlabel != NULL);
+
+ if (label == NULL) {
+ *strippedlabel = NULL;
+ } else {
+ gchar *i;
+ gboolean escape;
+
+ *strippedlabel = g_strdup(label);
+
+ /* if allow_shortcut is false, then you can't use the '_', instead you
+ have to just use the first valid character
+ */
+
+ /* allow __ to escape an underscore */
+ i = *strippedlabel;
+ do {
+ escape = FALSE;
+ i = strchr(i, '_');
+ if (i && *(i+1) == '_') {
+ gchar *j;
+
+ /* remove the escape '_' from the string */
+ for (j = i; *j != '\0'; ++j)
+ *j = *(j+1);
+
+ ++i;
+ escape = TRUE;
+ }
+ } while (escape);
+
+ if (allow_shortcut && i != NULL) {
+ /* there is an underscore in the string */
+
+ /* you have to use a printable ascii character for shortcuts
+ don't allow space either, so you can have like "a _ b"
+ */
+ if (VALID_SHORTCUT(*(i+1))) {
+ shortcut = g_unichar_tolower(g_utf8_get_char(i+1));
+ *position = i - *strippedlabel;
+ *always_show = TRUE;
+
+ /* remove the '_' from the string */
+ for (; *i != '\0'; ++i)
+ *i = *(i+1);
+ } else if (*(i+1) == '\0') {
+ /* no default shortcut if the '_' is the last character
+ (eg. "Exit_") for menu entries that you don't want
+ to be executed by mistake
+ */
+ *i = '\0';
+ }
+ } else {
+ /* there is no underscore, so find the first valid character to use
+ instead */
+
+ for (i = *strippedlabel; *i != '\0'; ++i)
+ if (VALID_SHORTCUT(*i)) {
+ *position = i - *strippedlabel;
+ shortcut = g_unichar_tolower(g_utf8_get_char(i));
+ break;
+ }
+ }
+ }
+ return shortcut;
+}
+
+static void parse_menu_item(xmlNodePtr node, gpointer data)
+{
+ ObMenuParseState *state = data;
+ gchar *label;
+ gchar *icon;
+ ObMenuEntry *e;
+
+ if (state->parent) {
+ /* Don't try to extract "icon" attribute if icons in user-defined
+ menus are not enabled. */
+
+ if (obt_xml_attr_string_unstripped(node, "label", &label)) {
+ xmlNodePtr c;
+ GSList *acts = NULL;
+
+ c = obt_xml_find_node(node->children, "action");
+ while (c) {
+ ObActionsAct *action = actions_parse(c);
+ if (action)
+ acts = g_slist_append(acts, action);
+ c = obt_xml_find_node(c->next, "action");
+ }
+ e = menu_add_normal(state->parent, -1, label, acts, TRUE);
+
+ if (config_menu_show_icons &&
+ obt_xml_attr_string(node, "icon", &icon))
+ {
+ e->data.normal.icon = RrImageNewFromName(ob_rr_icons, icon);
+
+ if (e->data.normal.icon)
+ e->data.normal.icon_alpha = 0xff;
+
+ g_free(icon);
+ }
+ g_free(label);
+ }
+ }
+}
+
+static void parse_menu_separator(xmlNodePtr node, gpointer data)
+{
+ ObMenuParseState *state = data;
+
+ if (state->parent) {
+ gchar *label;
+
+ if (!obt_xml_attr_string_unstripped(node, "label", &label))
+ label = NULL;
+
+ menu_add_separator(state->parent, -1, label);
+ g_free(label);
+ }
+}
+
+static void parse_menu(xmlNodePtr node, gpointer data)
+{
+ ObMenuParseState *state = data;
+ gchar *name = NULL, *title = NULL, *script = NULL;
+ ObMenu *menu;
+ ObMenuEntry *e;
+ gchar *icon;
+
+ if (!obt_xml_attr_string(node, "id", &name))
+ goto parse_menu_fail;
+
+ if (!g_hash_table_lookup(menu_hash, name)) {
+ if (!obt_xml_attr_string_unstripped(node, "label", &title))
+ goto parse_menu_fail;
+
+ if ((menu = menu_new(name, title, TRUE, NULL))) {
+ menu->pipe_creator = state->pipe_creator;
+ if (obt_xml_attr_string(node, "execute", &script)) {
+ menu->execute = obt_paths_expand_tilde(script);
+ } else {
+ ObMenu *old;
+
+ old = state->parent;
+ state->parent = menu;
+ obt_xml_tree(menu_parse_inst, node->children);
+ state->parent = old;
+ }
+ }
+ }
+
+ if (state->parent) {
+ e = menu_add_submenu(state->parent, -1, name);
+
+ if (config_menu_show_icons &&
+ obt_xml_attr_string(node, "icon", &icon))
+ {
+ e->data.submenu.icon = RrImageNewFromName(ob_rr_icons, icon);
+
+ if (e->data.submenu.icon)
+ e->data.submenu.icon_alpha = 0xff;
+
+ g_free(icon);
+ }
+ }
+
+parse_menu_fail:
+ g_free(name);
+ g_free(title);
+ g_free(script);
+}
+
+ObMenu* menu_new(const gchar *name, const gchar *title,
+ gboolean allow_shortcut_selection, gpointer data)
+{
+ ObMenu *self;
+
+ self = g_slice_new0(ObMenu);
+ self->name = g_strdup(name);
+ self->data = data;
+
+ self->shortcut = parse_shortcut(title, allow_shortcut_selection,
+ &self->title, &self->shortcut_position,
+ &self->shortcut_always_show);
+ self->collate_key = g_utf8_collate_key(self->title, -1);
+
+ g_hash_table_replace(menu_hash, self->name, self);
+
+ /* Each menu has a single more_menu. When the menu spills past what
+ can fit on the screen, a new menu frame entry is created from this
+ more_menu, and a new menu frame for the submenu is created for this
+ menu, also pointing to the more_menu.
+
+ This can be done multiple times using the same more_menu.
+
+ more_menu->more_menu will always be NULL, since there is only 1 for
+ each menu. */
+ self->more_menu = g_slice_new0(ObMenu);
+ self->more_menu->name = _("More...");
+ self->more_menu->title = _("More...");
+ self->more_menu->collate_key = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";
+ self->more_menu->data = data;
+ self->more_menu->shortcut = g_unichar_tolower(g_utf8_get_char("M"));
+
+ return self;
+}
+
+static void menu_destroy_hash_value(ObMenu *self)
+{
+ /* make sure its not visible */
+ {
+ GList *it;
+ ObMenuFrame *f;
+
+ for (it = menu_frame_visible; it; it = g_list_next(it)) {
+ f = it->data;
+ if (f->menu == self)
+ menu_frame_hide_all();
+ }
+ }
+
+ if (self->destroy_func)
+ self->destroy_func(self, self->data);
+
+ menu_clear_entries(self);
+ g_free(self->name);
+ g_free(self->title);
+ g_free(self->collate_key);
+ g_free(self->execute);
+ g_slice_free(ObMenu, self->more_menu);
+
+ g_slice_free(ObMenu, self);
+}
+
+void menu_free(ObMenu *menu)
+{
+ if (menu)
+ g_hash_table_remove(menu_hash, menu->name);
+}
+
+static gboolean menu_hide_delay_func(gpointer data)
+{
+ menu_can_hide = TRUE;
+ menu_timeout_id = 0;
+ return FALSE; /* no repeat */
+}
+
+void menu_show(gchar *name, gint x, gint y, gboolean mouse, ObClient *client)
+{
+ ObMenu *self;
+ ObMenuFrame *frame;
+
+ if (!(self = menu_from_name(name)) ||
+ grab_on_keyboard() || grab_on_pointer()) return;
+
+ /* if the requested menu is already the top visible menu, then don't
+ bother */
+ if (menu_frame_visible) {
+ frame = menu_frame_visible->data;
+ if (frame->menu == self)
+ return;
+ }
+
+ menu_frame_hide_all();
+
+ /* clear the pipe menus when showing a new menu */
+ menu_clear_pipe_caches();
+
+ frame = menu_frame_new(self, 0, client);
+ if (!menu_frame_show_topmenu(frame, x, y, mouse))
+ menu_frame_free(frame);
+ else {
+ if (!mouse) {
+ /* select the first entry if it's not a submenu and we opened
+ * the menu with the keyboard, and skip all headers */
+ GList *it = frame->entries;
+ while (it) {
+ ObMenuEntryFrame *e = it->data;
+ if (e->entry->type == OB_MENU_ENTRY_TYPE_NORMAL) {
+ menu_frame_select(frame, e, FALSE);
+ break;
+ } else if (e->entry->type == OB_MENU_ENTRY_TYPE_SEPARATOR)
+ it = g_list_next(it);
+ else
+ break;
+ }
+ }
+
+ /* reset the hide timer */
+ if (!mouse)
+ menu_can_hide = TRUE;
+ else {
+ menu_can_hide = FALSE;
+ if (menu_timeout_id) g_source_remove(menu_timeout_id);
+ menu_timeout_id = g_timeout_add_full(G_PRIORITY_DEFAULT,
+ config_menu_hide_delay,
+ menu_hide_delay_func,
+ NULL, NULL);
+ }
+ }
+}
+
+gboolean menu_hide_delay_reached(void)
+{
+ return menu_can_hide;
+}
+
+static ObMenuEntry* menu_entry_new(ObMenu *menu, ObMenuEntryType type, gint id)
+{
+ ObMenuEntry *self;
+
+ g_assert(menu);
+
+ self = g_slice_new0(ObMenuEntry);
+ self->ref = 1;
+ self->type = type;
+ self->menu = menu;
+ self->id = id;
+
+ switch (type) {
+ case OB_MENU_ENTRY_TYPE_NORMAL:
+ self->data.normal.enabled = TRUE;
+ break;
+ case OB_MENU_ENTRY_TYPE_SUBMENU:
+ case OB_MENU_ENTRY_TYPE_SEPARATOR:
+ break;
+ }
+
+ return self;
+}
+
+void menu_entry_ref(ObMenuEntry *self)
+{
+ ++self->ref;
+}
+
+void menu_entry_unref(ObMenuEntry *self)
+{
+ if (self && --self->ref == 0) {
+ switch (self->type) {
+ case OB_MENU_ENTRY_TYPE_NORMAL:
+ RrImageUnref(self->data.normal.icon);
+ g_free(self->data.normal.label);
+ g_free(self->data.normal.collate_key);
+ while (self->data.normal.actions) {
+ actions_act_unref(self->data.normal.actions->data);
+ self->data.normal.actions =
+ g_slist_delete_link(self->data.normal.actions,
+ self->data.normal.actions);
+ }
+ break;
+ case OB_MENU_ENTRY_TYPE_SUBMENU:
+ RrImageUnref(self->data.submenu.icon);
+ g_free(self->data.submenu.name);
+ break;
+ case OB_MENU_ENTRY_TYPE_SEPARATOR:
+ g_free(self->data.separator.label);
+ break;
+ }
+
+ g_slice_free(ObMenuEntry, self);
+ }
+}
+
+void menu_clear_entries(ObMenu *self)
+{
+#ifdef DEBUG
+ /* assert that the menu isn't visible */
+ {
+ GList *it;
+ ObMenuFrame *f;
+
+ for (it = menu_frame_visible; it; it = g_list_next(it)) {
+ f = it->data;
+ g_assert(f->menu != self);
+ }
+ }
+#endif
+
+ while (self->entries) {
+ menu_entry_unref(self->entries->data);
+ self->entries = g_list_delete_link(self->entries, self->entries);
+ }
+ self->more_menu->entries = self->entries; /* keep it in sync */
+}
+
+void menu_entry_remove(ObMenuEntry *self)
+{
+ self->menu->entries = g_list_remove(self->menu->entries, self);
+ menu_entry_unref(self);
+}
+
+ObMenuEntry* menu_add_normal(ObMenu *self, gint id, const gchar *label,
+ GSList *actions, gboolean allow_shortcut)
+{
+ ObMenuEntry *e;
+
+ e = menu_entry_new(self, OB_MENU_ENTRY_TYPE_NORMAL, id);
+ e->data.normal.actions = actions;
+
+ menu_entry_set_label(e, label, allow_shortcut);
+
+ self->entries = g_list_append(self->entries, e);
+ self->more_menu->entries = self->entries; /* keep it in sync */
+ return e;