/* $Id$ Copyright (C) 2004-2011 tooar This file is part of emelFM2. emelFM2 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 3, or (at your option) any later version. emelFM2 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 emelFM2; see the file GPL. If not, see http://www.gnu.org/licenses. */ /** @file src/utils/e2_menu.c @brief Menu utility functions This file contains utility and helper functions for menus. */ #include #include #include "emelfm2.h" #include "e2_menu.h" #include "e2_dialog.h" #include "e2_plugins.h" #include "e2_task.h" #ifdef E2_FS_MOUNTABLE # include "e2_filelist.h" #endif extern pthread_mutex_t task_mutex; /** @brief clear data for each item in bookmarks menu @param data data to be cleared @param closure UNUSED @return */ void e2_menu_mark_clear (gpointer data, GClosure *closure) { E2_MarkData *mdata = data; printd (DEBUG, "e2_menu_mark_clear() for %s", mdata->mark_path); g_free (mdata->mark_path); if (mdata->tpath != NULL) gtk_tree_path_free (mdata->tpath); DEALLOCATE (E2_MarkData, data); } /*******************/ /**** callbacks ****/ /*******************/ /** @brief Timer callback to execute action corresponding to item selected from a destroyable menu We do this outside the menu-item "activated" callback to avoid disrupting gtk's menu-activation process. @a art is cleared here. @param art pointer to action runtime data @return FALSE to remove the source */ static gboolean _e2_menu_action_deferred_cb (E2_ActionRuntime *art) { printd (DEBUG, "timer callback: _e2_menu_action_deferred_cb"); gdk_threads_enter (); e2_action_run (app.main_window, art); //FIXME get better widget gdk_threads_leave (); e2_action_free_runtime (art); e2_utils_fake_event (); //CHECKME does this actually help? return FALSE; } /** @brief setup to execute action corresponding to item selected from a destroyable menu This is the "activate" signal callback for action-initiators in a menu that's to be destroyed after usage. To avoid disrupting gtk's menu-activation process, we do not run the action here. Note: depending on the menu-destruction process and whether the action is synchronous, menu_item may not persist until the action is finished running i.e. possible race. Downstream clears @a data when finished with it @param menuitem the selected menu item widget @param data pointer to action runtime data @return */ void e2_menu_action_activated_cb (GtkMenuItem *menu_item, gpointer data) { printd (DEBUG, "e2_menu_action_activated_cb"); //don't want this action-data to be destroyed with the menu E2_ActionRuntime *art = (E2_ActionRuntime *)g_object_steal_data (G_OBJECT (menu_item), "e2-free-actruntime"); //an idle-callback is no good !? g_timeout_add (100, (GSourceFunc)_e2_menu_action_deferred_cb, art); } /** @brief like _e2_menu_action_deferred_cb, but for menus which are not destroyed @param art pointer to action runtime data @return FALSE to remove the source */ static gboolean _e2_menu_action_deferred_cb2 (E2_ActionRuntime *art) { printd (DEBUG, "timer callback: _e2_menu_action_deferred_cb2"); gdk_threads_enter (); e2_action_run (app.main_window, art); //FIXME get better widget gdk_threads_leave (); e2_utils_fake_event (); //CHECKME does this actually help? return FALSE; } /** @brief like e2_menu_action_activated_cb, but for menus which are not destroyed. Thus, @a data is _not_ cleared downstream @param menuitem the selected menu item widget @param data pointer to action runtime data */ void e2_menu_action_activated_cb2 (GtkMenuItem *menu_item, gpointer data) { printd (DEBUG, "e2_menu_action_activated_cb2"); //an idle-callback is no good !? g_timeout_add (100, (GSourceFunc)_e2_menu_action_deferred_cb2, (E2_ActionRuntime *)data); } /** @brief timer callback to cleanup a redundant menu widget Assumes menu is hidden so that BGL is irrelevant @param menu the widget to be cleared @return FALSE to stop the timer */ gboolean e2_menu_destroy (GtkWidget *menu) { printd (DEBUG, "timer callback: e2_menu_destroy, menu: %x", menu); gtk_widget_destroy (menu); return FALSE; } /** @brief "selection-done" signal callback for @a menu, initiates its destruction @param menu the menu widget @param data UNUSED data specified when the callback was connected @return */ void e2_menu_selection_done_cb (GtkWidget *menu, gpointer data) { printd (DEBUG, "e2_menu_selection_done_cb, menu: %x", menu); //must not destroy the menu immediately, gtk upstream has not necessarily finished with it #ifdef USE_GLIB2_14 g_timeout_add_seconds (10, #else g_timeout_add (10000, #endif (GSourceFunc)e2_menu_destroy, menu); } /** @brief callback to @param menu_item the activated menu-item widget @param widget widget specified when the callback was connected @return */ void e2_menu_control_cb (GtkMenuItem *menu_item, gpointer widget) { e2_option_connect (widget, gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (menu_item))); } /** @brief update set value when tied check menu item is activated @param menu_item the item that was activated @param set pointer to data for optionset tied to @a menu_item @return */ static void _e2_menu_check_value_changed_cb (GtkWidget *menu_item, E2_OptionSet *set) { GtkWidget *controller = g_object_get_data (G_OBJECT (menu_item), "e2-controller-widget"); if (!GPOINTER_TO_INT (g_object_get_data (G_OBJECT (controller), "e2-controller-blocked"))) e2_option_bool_set_direct (set, gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (menu_item))); } /** @brief set @a menu_item state if it's not blocked and not already at the desired state This is a hook-function callack upon change of set data associated with @a menu_item @param state pointerised TRUE/FALSE, the value to apply to @a menu_item @param menu_item the widget to change @return TRUE always */ static gboolean _e2_menu_check_change_value_hook (gpointer state, GtkWidget *menu_item) { GtkWidget *controller = g_object_get_data (G_OBJECT (menu_item), "e2-controller-widget"); if (!GPOINTER_TO_INT (g_object_get_data (G_OBJECT (controller), "e2-controller-blocked"))) { gboolean value = GPOINTER_TO_INT (state); gboolean current = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (menu_item)); if (value != current) gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), value); } return TRUE; } /** @brief add tied check item to @a menu @param menu menu widget @param set pointer to data for set whose value is tied to the check button @param controller widget which may have "e2-controller-blocked" data to prevent set value being updated when item is activated, can be NULL @param func callback function for handling menu item toggle, or NULL @param data pointer to data to send to the callback @return the menu item widget */ static GtkWidget *_e2_menu_add_tied_check (GtkWidget *menu, E2_OptionSet *set, GtkWidget *controller, void (*func)(GtkCheckMenuItem*,gpointer), gpointer data) { GtkWidget *menu_item = e2_menu_add_check (menu, set->desc, e2_option_bool_get_direct (set), func, data); g_object_set_data (G_OBJECT (menu_item), "e2-controller-widget", controller); //this will update the set value when the check menu item is activated g_signal_connect (G_OBJECT (menu_item), "toggled", G_CALLBACK (_e2_menu_check_value_changed_cb), set); //this will update the check menu item when the config data value changes e2_option_attach_value_changed_simple (set, menu_item, (HookFunc)_e2_menu_check_change_value_hook, menu_item); e2_widget_set_safetip (menu_item, set->tip); //conform to dependent set if any e2_widget_handle_depends (menu_item, set); return menu_item; } /****************/ /**** public ****/ /****************/ /** @brief add item to @a menu Args of @a func should be (GtkMenuItem*,some pointer) @param menu menu widget @param label menu item text, optionally with mnemonic @param icon custom iconfile path, or stock-icon identifier, or NULL for no icon @param tip tooltip string, or NULL @param activate_cb callback function for handling menu item activation, or NULL @param data pointer to data to send to @a activate_cb @return the menu item widget */ GtkWidget *e2_menu_add (GtkWidget *menu, gchar *labeltext, gchar *icon, gchar *tip, void (*activate_cb)(), gpointer data) { GtkWidget *menu_item; gint choice; if (icon == NULL) choice = 2; //no icon else choice = e2_option_sel_get ("menu-show-icons"); if (choice == 0) //theme { //ensure that the "gtk-menu-images" property is registered static GtkWidget *dummy = NULL; if (dummy == NULL) { dummy = gtk_image_menu_item_new (); gtk_widget_destroy (dummy); } GtkSettings* defs = gtk_settings_get_default (); //assume this always exists gboolean show; g_object_get (G_OBJECT (defs), "gtk-menu-images", &show, NULL); if (show) choice = 1; } if (choice == 1) //show icon { GtkWidget *image = e2_widget_get_icon (icon, GTK_ICON_SIZE_MENU); //e2_option_int_get ("menu-isize") + 1); if (image != NULL) { menu_item = gtk_image_menu_item_new_with_mnemonic (labeltext != NULL ? labeltext : ""); gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), image); } else menu_item = gtk_menu_item_new_with_mnemonic (labeltext != NULL ? labeltext : _("Missing _image")); } else menu_item = gtk_menu_item_new_with_mnemonic (labeltext); //turn on markup for the label GList *member, *children = gtk_container_get_children (GTK_CONTAINER (menu_item)); for (member = children; member != NULL; member = member->next) { GtkWidget *child = (GtkWidget *)member->data; if (GTK_IS_LABEL (child)) { gtk_label_set_use_markup (GTK_LABEL (child), TRUE); break; } } g_list_free (children); if (activate_cb != G_CALLBACK(NULL)) g_signal_connect (G_OBJECT (menu_item), "activate", G_CALLBACK (activate_cb), data); gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item); gtk_widget_show_all (menu_item); if (tip != NULL && *tip != '\0') e2_widget_set_safetip (menu_item, tip); return menu_item; } /** @brief add action item to destroyable @a menu @param menu menu widget @param label menu item text, optionally with mnemonic @param icon custom iconfile path, or stock-icon identifier @param tip tooltip string @param action_name identifier of the form "_A(n)._A(m)", or external command @param arg argument(s) string for the action, or NULL @return the menu item widget, or NULL */ GtkWidget *e2_menu_add_action (GtkWidget *menu, gchar *label, gchar *icon, gchar *tip, gchar *action_name, gchar *arg) { gchar *real_arg; E2_Action *action = e2_action_get_with_custom (action_name, arg, &real_arg); E2_ActionRuntime *art = e2_action_pack_runtime (action, real_arg, g_free); if (art != NULL) { //note: callback relates to destroyable menu GtkWidget *item = e2_menu_add (menu, label, icon, tip, e2_menu_action_activated_cb, art); g_object_set_data_full (G_OBJECT(item), "e2-free-actruntime", art, (GDestroyNotify)e2_action_free_runtime); return item; } return NULL; } /** @brief add check item to @a menu Args of @a func should be (GtkCheckMenuItem*,some pointer) @param menu menu widget @param label menu item text, optionally with mnemonic @param state initial state of the created item, T/F @param func void* callback function for handling menu item selection, or NULL @param data pointer to data to send to the callback @return the menu item widget */ GtkWidget *e2_menu_add_check (GtkWidget *menu, gchar *label, gboolean state, void (*func)(), gpointer data) { GtkWidget *check = gtk_check_menu_item_new_with_mnemonic (label); if (menu != NULL) gtk_menu_shell_append (GTK_MENU_SHELL (menu), check); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (check), state); if (func != NULL) g_signal_connect (G_OBJECT (check), "toggled", G_CALLBACK (func), data); gtk_widget_show (check); return check; } /** @brief add radio item to @a menu Args of @a func should be (GtkMenuItem*,some pointer) @param menu menu widget, or NULL @param group pointer to gslist of radio group @param label menu item text, optionally with mnemonic @param state initial state of the created item, T/F @param func callback function for handling menu-item activation, or NULL @param data pointer to data to send to the callback @return the menu item widget */ GtkWidget *e2_menu_add_radio (GtkWidget *menu, GSList **group, gchar *label, gboolean state, void (*func)(), gpointer data) { GtkWidget *radio = gtk_radio_menu_item_new_with_mnemonic (*group, label); *group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (radio)); if (menu != NULL) gtk_menu_shell_append (GTK_MENU_SHELL (menu), radio); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (radio), state); if (func != NULL) g_signal_connect (G_OBJECT (radio), "activate", G_CALLBACK (func), data); gtk_widget_show (radio); return radio; } /** @brief add separator to @a menu @param menu menu widget, or NULL @return the menu item widget */ GtkWidget *e2_menu_add_separator (GtkWidget *menu) { GtkWidget *sep = gtk_separator_menu_item_new (); if (menu != NULL) gtk_menu_shell_append (GTK_MENU_SHELL (menu), sep); gtk_widget_show (sep); return sep; } /** @brief add tear-off to @a menu @param menu menu widget @return the menu item widget */ GtkWidget *e2_menu_add_tear_off (GtkWidget *menu) { GtkWidget *menu_item = gtk_tearoff_menu_item_new (); gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item); gtk_widget_show (menu_item); return menu_item; } /** @brief add sub-menu to @a menu Note - no use to add a tooltip for item, it does not show @param menu menu widget @param label string with the name to appear in the menu @param icon icon-file name string @return the sub-menu item widget */ GtkWidget *e2_menu_add_submenu (GtkWidget *menu, gchar *label, gchar *icon) { GtkWidget *menu_item = e2_menu_add (menu, label, icon, NULL, NULL, NULL); GtkWidget *submenu = gtk_menu_new (); gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), submenu); gtk_widget_show (submenu); return submenu; } /** @brief create toggle menu item and add it to destroyable @a menu This expects to be called twice in succession for each toggle-item. None of the strings may be NULL @param menu the menu to which the item will be added @param label menu label string @param onetime TRUE if @a menu will be destroyed after use @param icon menu icon name @param tip menu tip string @param type string "toggle.on" or "toggle.off" @param cmd toggle action, for creating a hash key @return after the first of the pair, NULL: after the second, the menu item or NULL upon error */ GtkWidget *e2_menu_add_toggle (GtkWidget *menu, gboolean onetime, gchar *label, gchar *icon, gchar *tip, gchar *type, gchar *cmd) { static gboolean first = TRUE; static gboolean firststate; static gchar *firstlabel; static gchar *firsticon; static gchar *firsttip; static gchar *firstcmd; //NEVER FREE THIS if (label == NULL) label = ""; if (icon == NULL) icon = ""; if (tip == NULL) tip = ""; if (cmd == NULL) cmd = ""; if (first) { //process 1st of a pair first = FALSE; //park until we get the paired name, so we can make hash key firststate = g_str_has_suffix (type, _A(116)); //_(on firstlabel = g_strdup (label); firsticon = g_strdup (icon); firsttip = g_strdup (tip); firstcmd = g_strdup (cmd); return NULL; } //process 2nd of the pair first = TRUE; //hash table key is the joined action strings gchar *hashkey = g_strconcat (firstcmd, ".", cmd, NULL); //get toggle data, if any E2_ToggleData *data = g_hash_table_lookup (toggles_hash, hashkey); if (data == NULL) { data = ALLOCATE (E2_ToggleData); //deallocation when # cleared CHECKALLOCATEDFATAL (data); g_hash_table_insert (toggles_hash, g_strdup (hashkey), data); data->current_state = firststate; data->boxes = NULL; if (firststate) { data->true_action = firstcmd; //action command string data->false_action = g_strdup (cmd); //ditto } else { data->false_action = firstcmd; data->true_action = g_strdup (cmd); } } gchar *uselabel, *useicon, *usetip; if (data->current_state == firststate) { uselabel = firstlabel; useicon = firsticon; usetip = firsttip; } else { uselabel = label; useicon = icon; usetip = tip; } gchar *realarg; E2_Action *action = e2_action_get_with_custom (type, cmd, &realarg); E2_ActionRuntime *art = e2_action_pack_runtime (action, hashkey, g_free); //CHECKME realarg instead ? GtkWidget *item; if (art != NULL) { item = e2_menu_add (menu, uselabel, useicon, usetip, (onetime) ? e2_menu_action_activated_cb:e2_menu_action_activated_cb2, art); if (onetime) g_object_set_data (G_OBJECT (item), "e2-free-actruntime", art); //art cleared in callback else g_object_set_data_full (G_OBJECT (item), "e2-free-actruntime", art, (GDestroyNotify) e2_action_free_runtime); } else item = NULL; g_free (firstlabel); g_free (firsticon); g_free (firsttip); g_free (realarg); return item; } /** @brief popup detroyable menu @a menu @param menu menu widget @param button event button @param time event time @return */ void e2_menu_popup (GtkWidget *menu, gint button, guint32 time) { g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, button, time); } /* * @brief @param menu menu widget @param x UNUSED @param y UNUSED @param button event button @param time event time @return */ /* UNUSED void _e2_menu_popup (GtkWidget *menu, gint x, gint y, gint button, guint32 time) { g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, button, time); } */ /** @brief add items for a series of config options to @a menu CHECKME possible that something downstream needs BGL closed @param controller "parent" widget for the created menu @param menu menu widget to populate, or NULL to create a new one @param set first of a NULL-terminated series of option data structs @return @a menu or a new menu widget */ GtkWidget *e2_menu_create_options_menu (GtkWidget *controller, GtkWidget *menu, E2_OptionSet *set, ...) { if (menu == NULL) menu = gtk_menu_new (); va_list args; va_start (args, set); while (set != NULL) { void (*func)(GtkCheckMenuItem*,gpointer) = va_arg (args, gpointer); gpointer data = va_arg (args, gpointer); switch (set->type) { case E2_OPTION_TYPE_BOOL: _e2_menu_add_tied_check (menu, set, controller, func, data); break; case E2_OPTION_TYPE_SEL: e2_option_sel_add_menu_widget (controller, menu, set, func, data); break; default: break; } set = va_arg (args, E2_OptionSet *); } va_end (args); return menu; } /** @brief add items to bookmarks menu, recursively if children exist @param menu (sub)menu widget to which the items will be added @param top_menu top-level bookmarks-menu widget, maybe same as @a menu @param action pointer to data for the pseudo-action being processed @param markflags flags indicating how mark is to be processed, E2_MARKFLAG_ACTIONS etc @param panenum the pane whose path will be used when adding a mark (E2PANECUR, E2PANE1 or E2PANE2) @param mdl pointer to config data treemodel for bookmarks @param iter pointer to iter to be used for interrogating @a mdl @return */ void e2_menu_add_bookmark_items (GtkWidget *menu, GtkWidget *top_menu, E2_Action *action, guint32 markflags, gint32 panenum, GtkTreeModel *mdl, GtkTreeIter *iter) { do { GtkWidget *item; gchar *label, *icon, *tip, *markpath; E2_MarkData *mdata = ALLOCATE (E2_MarkData); CHECKALLOCATEDWARN (mdata, return;) mdata->pane_num = panenum; mdata->mark_flags = markflags; mdata->tpath = gtk_tree_model_get_path (mdl, iter); mdata->top_menu = top_menu; gtk_tree_model_get (mdl, iter, 0, &label, 1, &icon, 2, &tip, 3, &markpath, -1); if (gtk_tree_model_iter_has_child (mdl, iter)) { GtkTreeIter iter2; mdata->mark_path = NULL; //no path clean gtk_tree_model_iter_children (mdl, &iter2, iter); //items with sub-menu can't show tip etc item = e2_menu_add (menu, label, icon, NULL, NULL, NULL); GtkWidget *submenu = gtk_menu_new (); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); e2_menu_add_bookmark_items (submenu, top_menu, action, markflags, panenum, mdl, &iter2); //recurse gtk_widget_show (submenu); } else { void (*activatecb)(); mdata->mark_path = e2_utils_replace_vars (markpath, TRUE); //don't support variables in label gchar *_tip = e2_utils_replace_vars (tip, FALSE); item = e2_menu_add (menu, label, icon, _tip, NULL, NULL); g_free (_tip); E2_ActionRuntime *art = e2_action_pack_runtime (action, g_strdup_printf ("%d %s", panenum, mdata->mark_path), g_free); //data for add/open if (markflags & E2_MARKFLAG_ONETIME) { activatecb = e2_menu_action_activated_cb; g_object_set_data (G_OBJECT (item), "e2-free-actruntime", art); //art cleared after use } else { activatecb = e2_menu_action_activated_cb2; g_object_set_data_full (G_OBJECT (item), "e2-free-actruntime", art, (GDestroyNotify) e2_action_free_runtime); } //must connect_after to the activate signal, or else the //button-press-event callback will never occur g_signal_connect_after (G_OBJECT (item), "activate", G_CALLBACK (activatecb), art); } g_signal_connect_data (G_OBJECT (item), "button-press-event", G_CALLBACK (e2_bookmark_button_press_cb), mdata, (GClosureNotify) e2_menu_mark_clear, 0); g_free (label); g_free (icon); g_free (tip); g_free (markpath); } while (gtk_tree_model_iter_next (mdl, iter)); } /** @brief add items for all relevant menu-enabled plugins, to destroyable @a menu Assumes app.plugins is in menu-order, for "non-child" plugins at least, and for any "parent" plugin, its embedded list of children is in sub-menu order @param menu widget to which items are added @param onetime TRUE if @a menu is destroyed after usage @param is_selection TRUE if one or more items are selected in active pane @return */ void e2_menu_create_plugins_menu (GtkWidget *menu, gboolean onetime, gboolean is_selection) { gchar *prefix = (is_selection) ? NULL : g_strconcat (_A(6), ".", NULL); GList *tmp; for (tmp = app.plugins; tmp != NULL; tmp = tmp->next) { Plugin *p = tmp->data; if (p->show_in_menu && (is_selection || !g_str_has_prefix (p->action->name, prefix))) { E2_ActionRuntime *art; GtkWidget *item; //FIXME plugin action runtime data from ? ATM plugins expect no data, // tho' action->data may be relevant // gpointer pdata = NULL; // gpointer pclean = NULL; void (*activatecb)() = (onetime) ? e2_menu_action_activated_cb:e2_menu_action_activated_cb2; if (p->child_list != NULL) { //create item and child menu, if relevant GtkWidget *menu2 = gtk_menu_new (); GList *member; for (member = p->child_list; member != NULL; member = member->next) { Plugin *pc = (Plugin *)member->data; if (pc->show_in_menu) { art = e2_action_pack_runtime (pc->action, NULL, NULL); if (art != NULL) { item = e2_menu_add (menu2, (gchar *)pc->menu_name, (gchar *)pc->icon, (gchar *)pc->description, activatecb, art); if (onetime) g_object_set_data (G_OBJECT (item), "e2-free-actruntime", art); //art cleared in callback else g_object_set_data_full (G_OBJECT (item), "e2-free-actruntime", art, (GDestroyNotify) e2_action_free_runtime); } } } member = gtk_container_get_children (GTK_CONTAINER(menu2)); if (member) { g_list_free (member); GtkWidget *menu_item = e2_menu_add (menu, (gchar *)p->menu_name, (gchar *)p->icon, NULL, NULL, NULL); gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), menu2); gtk_widget_show (menu2); } else gtk_widget_destroy (menu2); } else //just 1 action in the plugin, or it's a child if (p->module != NULL //it's not a child && p->show_in_menu) { art = e2_action_pack_runtime (p->action, NULL, NULL); if (art != NULL) { item = e2_menu_add (menu, (gchar *)p->menu_name, (gchar *)p->icon, (gchar *)p->description, activatecb, art); if (onetime) g_object_set_data (G_OBJECT (item), "e2-free-actruntime", art); //art cleared in callback else g_object_set_data_full (G_OBJECT (item), "e2-free-actruntime", art, (GDestroyNotify) e2_action_free_runtime); } } } } if (!is_selection) g_free (prefix); } /** @brief process custom-menu item This is the callback for 'activated' signal for the menu item @param item the activated menu item @param data UNUSED data specified when callback was connected @return */ static void _e2_menu_custom_cb (GtkMenuItem *item, gpointer data) { //command may be slow, get this out of the way /*/ gtk_widget_hide ( #ifdef USE_GTK2_14 get_widget_get_parent (item) #else item->parent #endif ); */ gchar *cmd = (gchar *) g_object_get_data (G_OBJECT (item), "custom-command"); e2_command_run (cmd, E2_COMMAND_RANGE_DEFAULT, item #ifdef E2_COMMANDQ , FALSE #endif ); //FIXME some sort of termination process ? } //static void _e2_menu_custom_finished_cb (GtkWidget *menu, gpointer data) //{ //} /** @brief add items to a menu of custom commands @param model config option tree model for custom menus @param iter tree iter to use for interrogating @a model @param menu the menu to which items are to be added @return */ static void _e2_menu_add_custom_items (GtkTreeModel *model, GtkTreeIter *iter, GtkWidget *menu) { GtkTreeIter iter2; gchar *icon, *label, *tip, *command; do { gtk_tree_model_get (model, iter, 1, &icon, 2, &label, 3, &tip, 4, &command, -1); if (gtk_tree_model_iter_children (model, &iter2, iter)) { GtkWidget *submenu = e2_menu_add_submenu (menu, label, icon); _e2_menu_add_custom_items (model, &iter2, submenu); //recurse g_free (command); } else { if (strcmp (command, _A(21)) != 0) { GtkWidget *menu_item = e2_menu_add (menu, label, icon, tip, _e2_menu_custom_cb, NULL); g_object_set_data_full (G_OBJECT (menu_item), "custom-command", command, (GDestroyNotify) g_free); } else { e2_menu_add_separator (menu); g_free (command); } } //cleanup except command g_free (icon); g_free (label); g_free (tip); } while (gtk_tree_model_iter_next (model, iter)); } /** @brief construct a menu of custom commands @param name UTF8 string, name of custom menu to use @return the menu item widget, or NULL */ GtkWidget *e2_menu_create_custom_menu (gchar *name) { GtkTreeIter iter, iter2; E2_OptionSet *set = e2_option_get ("custom-menus"); if (e2_tree_find_iter_from_str (set->ex.tree.model, 0, name, &iter, TRUE) && gtk_tree_model_iter_children (set->ex.tree.model, &iter2, &iter)) { GtkWidget *menu = gtk_menu_new (); _e2_menu_add_custom_items (set->ex.tree.model, &iter2, menu); return menu; } return NULL; } /** @brief construct but not pop up a destroyable menu of running child processes, with items which upon activation callback to @a func Args for @a func should be (GtkMenuItem*, some pointer) This func is usable for button-menu and output context menu Uses check-items (no mnemonics) to indicate which procesess are active @param type enumerator for which processes to gather @param activate_cb callback function for handling menu item activation The arguments for @a activate_cb must be (GtkMenuItem*,E2_TaskRuntime*). @return the menu widget, or NULL */ #define CHILDMENUWIDTH 30 GtkWidget *e2_menu_create_child_menu (E2_ChildMenuType type, void (*activate_cb)()) { gchar *label, *s; gboolean active, empty = TRUE; GtkWidget *item, *menu = gtk_menu_new (); pthread_mutex_lock (&task_mutex); GList *member = app.taskhistory; pthread_mutex_unlock (&task_mutex); while (member != NULL) { E2_TaskRuntime *rt = member->data; if (type == E2_CHILD_ALL || (type == E2_CHILD_ACTIVETASKS && (rt->status == E2_TASK_RUNNING || rt->status == E2_TASK_PAUSED) ) || (!rt->action && //for these menus, we're not interested in actions ( (type == E2_CHILD_ACTIVE && (rt->status == E2_TASK_RUNNING || rt->status == E2_TASK_PAUSED)) || (type == E2_CHILD_OUTPUT && gtk_text_buffer_get_mark (app.tab.buffer, rt->pidstr) != NULL) ) ) ) { if (type == E2_CHILD_ACTIVETASKS && rt->action) { E2_ActionTaskData *atask = &rt->ex.action; gchar *srcdir = F_FILENAME_FROM_LOCALE (atask->currdir); gchar *shortdir = e2_utils_str_shorten (srcdir, 20, E2_DOTS_START); const gchar *subject; if (g_str_has_prefix (atask->action->name, _A(6))) { //this was a "file-action" subject = (atask->rt_data == NULL || *((gchar *)atask->rt_data) == '\0') ? _("") : (gchar *)atask->rt_data; } else subject = ""; //results in a redundant trailing space in the label label = g_strdup_printf ("%s %s %s", atask->action->name, shortdir, subject); F_FREE (srcdir, atask->currdir); g_free (shortdir); } else if (!rt->action) { //ellipsize the middle of longish commands s = e2_utils_str_shorten (rt->ex.command.command, CHILDMENUWIDTH - 7, E2_DOTS_MIDDLE); label = g_strconcat (rt->pidstr, ": ", s, NULL); g_free (s); } else label = NULL; if (label != NULL) { empty = FALSE; if (type == E2_CHILD_ALL) { item = gtk_check_menu_item_new_with_label (label); active = (rt != NULL && (rt->status == E2_TASK_RUNNING || rt->status == E2_TASK_PAUSED)); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), active); } else { item = gtk_menu_item_new_with_label (label); } if (activate_cb != G_CALLBACK (NULL)) g_signal_connect (G_OBJECT (item), "activate", G_CALLBACK (activate_cb), rt); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); g_free (label); } } pthread_mutex_lock (&task_mutex); member = member->next; pthread_mutex_unlock (&task_mutex); } if (empty) { if (type == E2_CHILD_OUTPUT) { gtk_widget_destroy (menu); menu = NULL; } else { e2_menu_add (menu, _("no children"), NULL, NULL, activate_cb, NULL); } } if (menu != NULL) { gtk_widget_show_all (menu); g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); } return menu; } /** @brief create but not pop up a destroyable filters-menu for pane to which @a view belongs @param view data struct for view to be changed @return the menu widget */ GtkWidget *e2_menu_create_filter_menu (ViewInfo *view) { GtkWidget *menu = gtk_menu_new (); view->check_name = e2_menu_add_check (menu, _("_Name filter"), view->name_filter.active, e2_name_filter_dialog_create_cb, view); view->check_size = e2_menu_add_check (menu, _("_Size filter"), view->size_filter.active, e2_size_filter_dialog_create_cb, view); view->check_date = e2_menu_add_check (menu, _("_Date filter"), view->date_filter.active, e2_date_filter_dialog_create_cb, view); e2_menu_add_separator (menu); view->check_dirs = e2_menu_add_check (menu, _("_Directories too"), view->filter_directories, e2_fileview_filter_dirs_cb, view); if (view->name_filter.active || view->size_filter.active || view->date_filter.active) { e2_menu_add_separator (menu); e2_menu_add (menu, _("_Remove all filters"), NULL, NULL, e2_fileview_remove_filters_cb, view); } g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); return menu; } #ifdef E2_FS_MOUNTABLE /** @brief handle a selection from a mountpoints menu The state of @a item when it arrives here is opposite to that shown in the menu, when clicked @param item the selected menu item @param from the widget where the mounts menu was initiated @return */ static void _e2_menu_mount_cb (GtkCheckMenuItem *item, gpointer from) { gboolean newstate = gtk_check_menu_item_get_active (item); // gint result; gchar *elsewhere = NULL; gchar *mpoint; //#ifdef USE_GTK2_16 // point = gtk_menu_item_get_label (GTK_MENU_ITEM (item)); //#else mpoint = g_object_get_data (G_OBJECT (item), "_mountpoint_"); //#endif #if defined (E2_HAL) || defined (E2_DEVKIT) gboolean remove = e2_fs_mount_is_ejectable (mpoint); //check this before unmount #endif //CHECKME synchronous umount command to mimimize risk of CWD error //tag E2_BADQUOTES gchar *qp = e2_utils_quote_string (mpoint); gchar *cmd = g_strconcat ((newstate) ? E2_MOUNTCOMMAND : "|"E2_UNMOUNTCOMMAND, " ", qp, NULL); if (!newstate //doing an unmount #ifdef E2_VFSTMP //FIXME dir when not mounted local #else && g_str_has_prefix (curr_view->dir, mpoint)) #endif { //if possible, change CWD out of mountpoint so we don't ourself block the unmount gchar *s = strrchr (mpoint, G_DIR_SEPARATOR); //assumes no trailer on mountpoint string if (s > mpoint+1) //not doing root dir elsewhere = g_strndup (mpoint, s - mpoint); else //can't cd elsewhere = g_strdup (G_DIR_SEPARATOR_S); e2_fs_get_valid_path (&elsewhere, TRUE E2_ERR_NONE()); //if ( e2_fs_chdir (elsewhere E2_ERR_NONE()); //) /* #ifdef USE_GLIB2_22 if (!e2_fs_mount_gio_unmount (mpoint)) //try for removable, first #endif */ // result = e2_command_run_at (cmd, elsewhere, E2_COMMAND_RANGE_DEFAULT, from //has temporary change CWD #ifdef E2_COMMANDQ , FALSE #endif ); //else //FIXME warn user about blockage } else { /* #ifdef USE_GLIB2_22 if ((newstate && !e2_fs_mount_gio_mount (mpoint)) //mount && not removable || (!newstate && !e2_fs_mount_gio_unmount (mpoint))) //unmount && not removable //#endif */ // result = e2_command_run (cmd, E2_COMMAND_RANGE_DEFAULT, from #ifdef E2_COMMANDQ , FALSE #endif ); } g_free (cmd); if (newstate) //doing a mount { #ifdef E2_FAM e2_filelist_request_refresh (curr_view->dir, FALSE); e2_filelist_request_refresh (other_view->dir, TRUE); #else e2_filelist_check_dirty (GINT_TO_POINTER (1)); #endif } else //doing an unmount { #if defined (E2_HAL) || defined (E2_DEVKIT) if (remove) { cmd = g_strdup_printf ("eject %s", qp); e2_command_run_at (cmd, elsewhere, E2_COMMAND_RANGE_DEFAULT, from # ifdef E2_COMMANDQ , FALSE # endif ); g_free (cmd); } #endif if (elsewhere != NULL) { //E2_VFSTMP may change space too // e2_pane_change_dir (curr_pane, point); //not needed, with inofify at least g_free (elsewhere); } } g_free (qp); /* if (!(newstate || result == 0)) { //change back to where we started e2_pane_change_dir (curr_pane, olddir); g_free (olddir); } */ } /** @brief add check item to @a menu Can't use e2_menu_add_check() because we don't want mnemonics @param menu menu widget @param label menu item text @param state initial state of the created item, T/F @param func void* callback function for handling menu item selection, or NULL @param data data for callback @return the menu item widget */ static GtkWidget *_e2_menu_mount_add_check (GtkWidget *menu, gchar *label, gboolean state, void (*func)(GtkCheckMenuItem*,gpointer), gpointer data) { GtkWidget *check = gtk_check_menu_item_new_with_label (label); gtk_menu_shell_append (GTK_MENU_SHELL (menu), check); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (check), state); if (func != NULL) g_signal_connect (G_OBJECT (check), "toggled", G_CALLBACK (func), data); return check; } static GtkWidget *selectedmount = NULL; /** @brief "select" signal callback to log which item in mounts-menu is current @param item the slected menu-item @param user_data UNUSED @return */ void _e2_menu_mount_select_cb ( #ifdef USE_GTK3_0 GtkMenuItem *item, #else GtkItem *item, #endif gpointer user_data) { selectedmount = GTK_WIDGET(item); } /** @brief key press signal callback to insert into the commandline the path of the selected item in @a menu @param menu the focused menu-widget when the key was pressed @param event pointer to event data @param user_data UNUSED @return */ static gboolean _e2_menu_mount_keypress_cb (GtkWidget *menu, GdkEventKey *event, gpointer user_data) { printd (DEBUG, "_e2_menu_mount_keypress_cb"); if (event->keyval == GDK_Insert) { const gchar *path; //#ifdef USE_GTK2_16 // path = gtk_menu_item_get_label (GTK_MENU_ITEM (selectedmount)); //#else path = g_object_get_data (G_OBJECT(selectedmount), "_mountpoint_"); //#endif e2_command_line_insert (path); return TRUE; } return FALSE; } /** @brief create and pop up a destroyable mountpoints menu @param from the button that was clicked to popup the menu @param art action runtime data @return TRUE if the action succeeded */ gboolean e2_menu_create_mounts_menu (gpointer from, E2_ActionRuntime *art) { gchar *point; GList *mounts, *mountables, *member, *node; GtkWidget *item; if (GTK_IS_BUTTON (from)) { art->state &= ~GDK_MOD2_MASK; if (!ACTION_CLICK(art)) return FALSE; } GtkWidget *menu = gtk_menu_new (); mounts = e2_fs_mount_get_mounts_list (); for (member = mounts ; member != NULL; member = member->next) { item = _e2_menu_mount_add_check (menu, (gchar *) member->data, TRUE, _e2_menu_mount_cb, from); g_signal_connect (G_OBJECT (item), "select", G_CALLBACK (_e2_menu_mount_select_cb), NULL); g_object_set_data_full (G_OBJECT (item), "_mountpoint_", member->data, g_free); } mountables = e2_fs_mount_get_mountable_list (); for (member = mountables ; member != NULL; member = member->next) { point = (gchar *) member->data; gboolean mounted = FALSE; for (node = mounts; node != NULL; node = node->next) { if (!strcmp (point, (gchar *) node->data)) { mounted = TRUE; break; } } if (mounted) g_free (member->data); else { item = _e2_menu_mount_add_check (menu, (gchar *) member->data, FALSE, _e2_menu_mount_cb, from); g_signal_connect (G_OBJECT (item), "select", G_CALLBACK (_e2_menu_mount_select_cb), NULL); g_object_set_data_full (G_OBJECT (item), "_mountpoint_", member->data, g_free); } } g_list_free (mounts); //its data are cleared with the menu g_list_free (mountables); //ditto g_signal_connect (G_OBJECT (menu), "key-press-event", G_CALLBACK(_e2_menu_mount_keypress_cb), NULL); g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); gtk_widget_show_all (menu); if (GTK_IS_BUTTON (from)) gtk_menu_popup (GTK_MENU (menu), NULL, NULL, (GtkMenuPositionFunc) e2_toolbar_set_menu_position, from, 1, 0); else gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 1, 0); return TRUE; } #endif //def E2_FS_MOUNTABLE /** @brief install default tree options for custom menus This function is called only if the default is missing from the config file This default set data is for example purposes only, the user must provide real data @param set pointer to set data @return */ static void _e2_menu_tree_defaults (E2_OptionSet *set) { e2_option_tree_setup_defaults (set, g_strdup("custom-menus=<"), //internal name g_strdup("fusemounts||||"), //menu lookup name //these are examples only, no translation g_strconcat ("\t|vfs_on"E2ICONTB"|","_Mount","|","Mount tip","|","FTPDIR=/whatever;mount-command $FTPDIR",NULL), g_strconcat ("\t|vfs_off"E2ICONTB"|","_Unmount choices","||",NULL), g_strconcat ("\t\t||","_Unmount","|","Unount tip","|","fusermount -u $FTPDIR",NULL), g_strdup(">"), NULL); } /** @brief setup tree option for custom menus @return */ void e2_menu_custom_option_register (void) { gchar *group_name = g_strconcat(_C(20) ,".",_C(9),NULL); //_("interface.custom menus"; E2_OptionSet *set = e2_option_tree_register ("custom-menus", group_name, _C(9), //no translation NULL, NULL, NULL, E2_OPTION_TREE_UP_DOWN | E2_OPTION_TREE_ADD_DEL, E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDBARS); e2_option_tree_add_column (set, _("Menu"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, NULL, NULL); //since this a a new addition to config files, put the icon before the label e2_option_tree_add_column (set, _("Icon"), E2_OPTION_TREE_TYPE_ICON, 0, "", 0, NULL, NULL); e2_option_tree_add_column (set, _("Label"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, NULL, NULL); e2_option_tree_add_column (set, _("Tooltip"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, NULL, NULL); e2_option_tree_add_column (set, _("Command"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, NULL, GINT_TO_POINTER (E2_ACTION_EXCLUDE_TOGGLE)); e2_option_tree_create_store (set); e2_option_tree_prepare_defaults (set, _e2_menu_tree_defaults); }