diff options
author | Perberos <[email protected]> | 2011-12-01 21:51:44 -0300 |
---|---|---|
committer | Perberos <[email protected]> | 2011-12-01 21:51:44 -0300 |
commit | 0b0e6bc987da4fd88a7854ebb12bde705e92c428 (patch) | |
tree | 47d329edd31c67eaa36b2147780e37e197e901b5 /libslab/app-shell.c | |
download | mate-control-center-0b0e6bc987da4fd88a7854ebb12bde705e92c428.tar.bz2 mate-control-center-0b0e6bc987da4fd88a7854ebb12bde705e92c428.tar.xz |
moving from https://github.com/perberos/mate-desktop-environment
Diffstat (limited to 'libslab/app-shell.c')
-rw-r--r-- | libslab/app-shell.c | 1413 |
1 files changed, 1413 insertions, 0 deletions
diff --git a/libslab/app-shell.c b/libslab/app-shell.c new file mode 100644 index 00000000..0944f404 --- /dev/null +++ b/libslab/app-shell.c @@ -0,0 +1,1413 @@ +/* + * This file is part of libslab. + * + * Copyright (c) 2006 Novell, Inc. + * + * Libslab is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * Libslab 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 Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libslab; if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libmate/mate-desktop-item.h> +#include <gio/gio.h> +#include <gdk/gdkkeysyms.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <string.h> +#include <stdlib.h> + +#include <glib/gi18n-lib.h> + +#include "app-shell.h" +#include "shell-window.h" +#include "app-resizer.h" +#include "slab-section.h" +#include "slab-mate-util.h" +#include "search-bar.h" + +#include "application-tile.h" +#include "themed-icon.h" + +#define TILE_EXEC_NAME "Tile_desktop_exec_name" +#define SECONDS_IN_DAY 86400 +#define EXIT_SHELL_ON_ACTION_START "exit_shell_on_action_start" +#define EXIT_SHELL_ON_ACTION_HELP "exit_shell_on_action_help" +#define EXIT_SHELL_ON_ACTION_ADD_REMOVE "exit_shell_on_action_add_remove" +#define EXIT_SHELL_ON_ACTION_UPGRADE_UNINSTALL "exit_shell_on_action_upgrade_uninstall" +#define NEW_APPS_FILE_KEY "new_apps_file_key" + +static void create_application_category_sections (AppShellData * app_data); +static GtkWidget *create_filter_section (AppShellData * app_data, const gchar * title); +static GtkWidget *create_groups_section (AppShellData * app_data, const gchar * title); +static GtkWidget *create_actions_section (AppShellData * app_data, const gchar * title, + void (*actions_handler) (Tile *, TileEvent *, gpointer)); + +static void generate_category (const char * category, MateMenuTreeDirectory * root_dir, AppShellData * app_data, gboolean recursive); +static void generate_launchers (MateMenuTreeDirectory * root_dir, AppShellData * app_data, + CategoryData * cat_data, gboolean recursive); +static void generate_new_apps (AppShellData * app_data); +static void insert_launcher_into_category (CategoryData * cat_data, MateDesktopItem * desktop_item, + AppShellData * app_data); + +static gboolean main_keypress_callback (GtkWidget * widget, GdkEventKey * event, + AppShellData * app_data); +static gboolean main_delete_callback (GtkWidget * widget, GdkEvent * event, + AppShellData * app_data); +static void application_launcher_clear_search_bar (AppShellData * app_data); +static void launch_selected_app (AppShellData * app_data); +static void generate_potential_apps (gpointer catdata, gpointer user_data); + +static void relayout_shell (AppShellData * app_data); +static gboolean handle_filter_changed (NldSearchBar * search_bar, int context, const char *text, + gpointer user_data); +static void handle_group_clicked (Tile * tile, TileEvent * event, gpointer user_data); +static void set_state (AppShellData * app_data, GtkWidget * widget); +static void populate_groups_section (AppShellData * app_data); +static void generate_filtered_lists (gpointer catdata, gpointer user_data); +static void show_no_results_message (AppShellData * app_data, GtkWidget * containing_vbox); +static void populate_application_category_sections (AppShellData * app_data, + GtkWidget * containing_vbox); +static void populate_application_category_section (AppShellData * app_data, SlabSection * section, + GList * launcher_list); +static void tile_activated_cb (Tile * tile, TileEvent * event, gpointer user_data); +static void handle_launcher_single_clicked (Tile * launcher, gpointer data); +static void handle_menu_action_performed (Tile * launcher, TileEvent * event, TileAction * action, + gpointer data); +static gint application_launcher_compare (gconstpointer a, gconstpointer b); +static void matemenu_tree_changed_callback (MateMenuTree * tree, gpointer user_data); +gboolean regenerate_categories (AppShellData * app_data); + +void +hide_shell (AppShellData * app_data) +{ + gtk_window_get_position (GTK_WINDOW (app_data->main_app), + &app_data->main_app_window_x, &app_data->main_app_window_y); + /* printf("x:%d, y:%d\n", app_data->main_app_window_x, app_data->main_app_window_y); */ + /* clear the search bar now so reshowing is fast and flicker free - BNC#283186 */ + application_launcher_clear_search_bar (app_data); + gtk_widget_hide (app_data->main_app); +} + +void +show_shell (AppShellData * app_data) +{ + gtk_widget_show_all (app_data->main_app); + if (!app_data->static_actions) + gtk_widget_hide_all (app_data->actions_section); /* don't show unless a launcher is selected */ + + if (app_data->main_app_window_shown_once) + gtk_window_move (GTK_WINDOW (app_data->main_app), + app_data->main_app_window_x, app_data->main_app_window_y); + + /* if this is the first time shown, need to clear this handler */ + else + shell_window_clear_resize_handler (SHELL_WINDOW (app_data->shell)); + app_data->main_app_window_shown_once = TRUE; +} + +gboolean +create_main_window (AppShellData * app_data, const gchar * app_name, const gchar * title, + const gchar * window_icon, gint width, gint height, gboolean hidden) +{ + GtkWidget *main_app = gtk_window_new (GTK_WINDOW_TOPLEVEL); + app_data->main_app = main_app; + gtk_widget_set_name (main_app, app_name); + gtk_window_set_title (GTK_WINDOW (main_app), title); + /* gtk_window_set_default_size(GTK_WINDOW(main_app), width, height); */ + gtk_window_set_icon_name (GTK_WINDOW (main_app), window_icon); + gtk_container_add (GTK_CONTAINER (main_app), app_data->shell); + + g_signal_connect (main_app, "delete-event", G_CALLBACK (main_delete_callback), app_data); + g_signal_connect (main_app, "key-press-event", G_CALLBACK (main_keypress_callback), + app_data); + + gtk_window_set_position (GTK_WINDOW (app_data->main_app), GTK_WIN_POS_CENTER); + if (!hidden) + show_shell (app_data); + + return TRUE; +} + +static void +generate_potential_apps (gpointer catdata, gpointer user_data) +{ + GHashTable *app_hash = (GHashTable *) user_data; + CategoryData *data = (CategoryData *) catdata; + gchar *uri; + + GList *launcher_list = data->filtered_launcher_list; + + while (launcher_list) + { + g_object_get (launcher_list->data, "tile-uri", &uri, NULL); + /* eliminate dups of same app in multiple categories */ + if (!g_hash_table_lookup (app_hash, uri)) + g_hash_table_insert (app_hash, uri, launcher_list->data); + else + g_free (uri); + launcher_list = g_list_next (launcher_list); + } +} + +static gboolean +return_first_entry (gpointer key, gpointer value, gpointer unused) +{ + return TRUE; /*better way to pull an entry out ? */ +} + +static void +launch_selected_app (AppShellData * app_data) +{ + GHashTable *app_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + guint num_apps; + + g_list_foreach (app_data->categories_list, generate_potential_apps, app_hash); + num_apps = g_hash_table_size (app_hash); + if (num_apps == 1) + { + ApplicationTile *launcher = + APPLICATION_TILE (g_hash_table_find (app_hash, return_first_entry, NULL)); + g_hash_table_destroy (app_hash); + handle_launcher_single_clicked (TILE (launcher), app_data); + return; + } + + g_hash_table_destroy (app_hash); +} + +static gboolean +main_keypress_callback (GtkWidget * widget, GdkEventKey * event, AppShellData * app_data) +{ + if (event->keyval == GDK_Return) + { + SlabSection *section = SLAB_SECTION (app_data->filter_section); + NldSearchBar *search_bar; + + /* Make sure our implementation has not changed */ + g_assert (NLD_IS_SEARCH_BAR (section->contents)); + search_bar = NLD_SEARCH_BAR (section->contents); + if (nld_search_bar_has_focus (search_bar)) + { + launch_selected_app (app_data); + return TRUE; + } + } + + /* quit on ESC or Ctl-W or Ctl-Q */ + if (event->keyval == GDK_Escape || + ((event->keyval == GDK_w || event->keyval == GDK_W) && (event->state & GDK_CONTROL_MASK)) || + ((event->keyval == GDK_q || event->keyval == GDK_Q) && (event->state & GDK_CONTROL_MASK))) + { + if (app_data->exit_on_close) + gtk_main_quit (); + else + hide_shell (app_data); + return TRUE; + } + return FALSE; +} + +static gboolean +main_delete_callback (GtkWidget * widget, GdkEvent * event, AppShellData * app_data) +{ + if (app_data->exit_on_close) + { + gtk_main_quit (); + return FALSE; + } + + hide_shell (app_data); + return TRUE; /* stop the processing of this event */ +} + +void +layout_shell (AppShellData * app_data, const gchar * filter_title, const gchar * groups_title, + const gchar * actions_title, GSList * actions, + void (*actions_handler) (Tile *, TileEvent *, gpointer)) +{ + GtkWidget *filter_section; + GtkWidget *groups_section; + GtkWidget *actions_section; + + GtkWidget *left_vbox; + GtkWidget *right_vbox; + gint num_cols; + + GtkWidget *sw; + GtkAdjustment *adjustment; + + app_data->shell = shell_window_new (app_data); + app_data->static_actions = actions; + + right_vbox = gtk_vbox_new (FALSE, CATEGORY_SPACING); + + num_cols = SIZING_SCREEN_WIDTH_LARGE_NUMCOLS; + if (gdk_screen_width () <= SIZING_SCREEN_WIDTH_LARGE) + { + if (gdk_screen_width () <= SIZING_SCREEN_WIDTH_MEDIUM) + num_cols = SIZING_SCREEN_WIDTH_SMALL_NUMCOLS; + else + num_cols = SIZING_SCREEN_WIDTH_MEDIUM_NUMCOLS; + } + app_data->category_layout = + app_resizer_new (GTK_VBOX (right_vbox), num_cols, TRUE, app_data); + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (sw), app_data->category_layout); + adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (sw)); + g_object_set (adjustment, "step-increment", (double) 20, NULL); + + create_application_category_sections (app_data); + populate_application_category_sections (app_data, right_vbox); + app_resizer_set_table_cache (APP_RESIZER (app_data->category_layout), + app_data->cached_tables_list); + + gtk_container_set_focus_vadjustment (GTK_CONTAINER (right_vbox), + gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (sw))); + + left_vbox = gtk_vbox_new (FALSE, 15); + + filter_section = create_filter_section (app_data, filter_title); + app_data->filter_section = filter_section; + gtk_box_pack_start (GTK_BOX (left_vbox), filter_section, FALSE, FALSE, 0); + + groups_section = create_groups_section (app_data, groups_title); + app_data->groups_section = groups_section; + populate_groups_section (app_data); + gtk_box_pack_start (GTK_BOX (left_vbox), groups_section, FALSE, FALSE, 0); + + actions_section = create_actions_section (app_data, actions_title, actions_handler); + app_data->actions_section = actions_section; + gtk_box_pack_start (GTK_BOX (left_vbox), actions_section, FALSE, FALSE, 0); + + shell_window_set_contents (SHELL_WINDOW (app_data->shell), left_vbox, sw); +} + +static gboolean +relayout_shell_partial (gpointer user_data) +{ + AppShellData *app_data = (AppShellData *) user_data; + GtkVBox *vbox = APP_RESIZER (app_data->category_layout)->child; + CategoryData *data; + + if (app_data->stop_incremental_relayout) + return FALSE; + + if (app_data->incremental_relayout_cat_list != NULL) + { + /* There are still categories to layout */ + data = (CategoryData *) app_data->incremental_relayout_cat_list->data; + if (data->filtered_launcher_list != NULL) + { + populate_application_category_section (app_data, data->section, + data->filtered_launcher_list); + gtk_box_pack_start (GTK_BOX (vbox), GTK_WIDGET (data->section), TRUE, TRUE, + 0); + app_data->filtered_out_everything = FALSE; + } + + app_data->incremental_relayout_cat_list = + g_list_next (app_data->incremental_relayout_cat_list); + return TRUE; + } + + /* We're done laying out the categories; finish up */ + if (app_data->filtered_out_everything) + show_no_results_message (app_data, GTK_WIDGET (vbox)); + + app_resizer_set_table_cache (APP_RESIZER (app_data->category_layout), + app_data->cached_tables_list); + populate_groups_section (app_data); + + gtk_widget_show_all (app_data->category_layout); + gdk_window_set_cursor (app_data->shell->window, NULL); + + app_data->stop_incremental_relayout = TRUE; + return FALSE; +} + +static void +relayout_shell_incremental (AppShellData * app_data) +{ + GtkVBox *vbox = APP_RESIZER (app_data->category_layout)->child; + + app_data->stop_incremental_relayout = FALSE; + app_data->filtered_out_everything = TRUE; + app_data->incremental_relayout_cat_list = app_data->categories_list; + + if (app_data->cached_tables_list) + g_list_free (app_data->cached_tables_list); + app_data->cached_tables_list = NULL; + + remove_container_entries (GTK_CONTAINER (vbox)); + + g_idle_add ((GSourceFunc) relayout_shell_partial, app_data); +} + +static void +relayout_shell (AppShellData * app_data) +{ + GtkWidget *shell = app_data->shell; + GtkVBox *vbox = APP_RESIZER (app_data->category_layout)->child; + + populate_application_category_sections (app_data, GTK_WIDGET (vbox)); + app_resizer_set_table_cache (APP_RESIZER (app_data->category_layout), + app_data->cached_tables_list); + populate_groups_section (app_data); + + gtk_widget_show_all (shell); + if (!app_data->static_actions && !app_data->last_clicked_launcher) + gtk_widget_hide_all (app_data->actions_section); /* don't show unless a launcher is selected */ +} + +static GtkWidget * +create_actions_section (AppShellData * app_data, const gchar * title, + void (*actions_handler) (Tile *, TileEvent *, gpointer)) +{ + GtkWidget *section, *launcher; + GtkWidget *vbox; + GSList *actions; + AppAction *action; + AtkObject *a11y_cat; + + g_assert (app_data != NULL); + + section = slab_section_new (title, Style1); + g_object_ref (section); + + vbox = gtk_vbox_new (FALSE, 0); + slab_section_set_contents (SLAB_SECTION (section), vbox); + + if (app_data->static_actions) + { + for (actions = app_data->static_actions; actions; actions = actions->next) + { + GtkWidget *header; + + action = (AppAction *) actions->data; + header = gtk_label_new (action->name); + gtk_misc_set_alignment (GTK_MISC (header), 0, 0.5); + launcher = nameplate_tile_new (NULL, NULL, header, NULL); + + g_object_set_data (G_OBJECT (launcher), APP_ACTION_KEY, action->item); + g_signal_connect (launcher, "tile-activated", G_CALLBACK (actions_handler), + app_data); + gtk_box_pack_start (GTK_BOX (vbox), launcher, FALSE, FALSE, 0); + + a11y_cat = gtk_widget_get_accessible (GTK_WIDGET (launcher)); + atk_object_set_name (a11y_cat, action->name); + } + } + + return section; +} + +static GtkWidget * +create_groups_section (AppShellData * app_data, const gchar * title) +{ + GtkWidget *section; + GtkWidget *vbox; + + g_assert (app_data != NULL); + + section = slab_section_new (title, Style1); + g_object_ref (section); + + vbox = gtk_vbox_new (FALSE, 0); + slab_section_set_contents (SLAB_SECTION (section), vbox); + + return section; +} + +static void +populate_groups_section (AppShellData * app_data) +{ + SlabSection *section = SLAB_SECTION (app_data->groups_section); + GtkVBox *vbox; + GList *cat_list; + + /* Make sure our implementation has not changed and it's still a GtkVBox */ + g_assert (GTK_IS_VBOX (section->contents)); + + vbox = GTK_VBOX (section->contents); + remove_container_entries (GTK_CONTAINER (vbox)); + + cat_list = app_data->categories_list; + do + { + CategoryData *data = (CategoryData *) cat_list->data; + if (NULL != data->filtered_launcher_list) + { + gtk_widget_set_state (GTK_WIDGET (data->group_launcher), GTK_STATE_NORMAL); + gtk_box_pack_start (GTK_BOX (vbox), GTK_WIDGET (data->group_launcher), + FALSE, FALSE, 0); + } + } + while (NULL != (cat_list = g_list_next (cat_list))); +} + +static void +handle_group_clicked (Tile * tile, TileEvent * event, gpointer user_data) +{ + AppShellData *app_data = (AppShellData *) user_data; + GtkWidget *section = NULL; + + gint clicked_pos = + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tile), GROUP_POSITION_NUMBER_KEY)); + + GList *cat_list = app_data->categories_list; + + gint total = 0; + do + { + CategoryData *cat_data = (CategoryData *) cat_list->data; + gint pos = + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cat_data->group_launcher), + GROUP_POSITION_NUMBER_KEY)); + if (pos == clicked_pos) + { + section = GTK_WIDGET (cat_data->section); + break; + } + + if (NULL != cat_data->filtered_launcher_list) + { + total += GTK_WIDGET (cat_data->section)->allocation.height + + CATEGORY_SPACING; + } + } + while (NULL != (cat_list = g_list_next (cat_list))); + + g_assert (section != NULL); + set_state (app_data, section); + + app_resizer_set_vadjustment_value (app_data->category_layout, total); +} + +static void +set_state (AppShellData * app_data, GtkWidget * widget) +{ + if (app_data->selected_group) + { + slab_section_set_selected (app_data->selected_group, FALSE); + app_data->selected_group = NULL; + } + + if (widget) + { + app_data->selected_group = SLAB_SECTION (widget); + slab_section_set_selected (SLAB_SECTION (widget), TRUE); + } + gtk_widget_queue_draw (app_data->shell); +} + +static GtkWidget * +create_filter_section (AppShellData * app_data, const gchar * title) +{ + GtkWidget *section; + + GtkWidget *search_bar; + + section = slab_section_new (title, Style1); + g_object_ref (section); + + search_bar = nld_search_bar_new (); + nld_search_bar_set_search_timeout (NLD_SEARCH_BAR (search_bar), 0); + slab_section_set_contents (SLAB_SECTION (section), search_bar); + + g_signal_connect (G_OBJECT (search_bar), "search", G_CALLBACK (handle_filter_changed), + app_data); + + return section; +} + +static gboolean +handle_filter_changed_delayed (gpointer user_data) +{ + AppShellData *app_data = (AppShellData *) user_data; + + g_list_foreach (app_data->categories_list, generate_filtered_lists, + (gpointer) app_data->filter_string); + app_data->last_clicked_launcher = NULL; + + /* showing the updates incremtally is very visually distracting. Much worse than just blanking until + the incremental work is done and then doing one show. It would be nice to optimize this though + somehow and not even show any change but the cursor change until all the work is done. But since + we do the work incrementally in an idle loop I don't know how else besides hiding to not show + incremental updates + */ + /* gdk_window_freeze_updates(app_data->category_layout->window); */ + gtk_widget_hide (app_data->category_layout); + app_data->busy_cursor = + gdk_cursor_new_for_display (gtk_widget_get_display (app_data->shell), GDK_WATCH); + gdk_window_set_cursor (app_data->shell->window, app_data->busy_cursor); + gdk_cursor_unref (app_data->busy_cursor); + + set_state (app_data, NULL); + app_resizer_set_vadjustment_value (app_data->category_layout, 0); + + relayout_shell_incremental (app_data); + + app_data->filter_changed_timeout = 0; + return FALSE; +} + +static gboolean +handle_filter_changed (NldSearchBar * search_bar, int context, const char *text, gpointer data) +{ + AppShellData *app_data; + + app_data = (AppShellData *) data; + + if (app_data->filter_string) + g_free (app_data->filter_string); + app_data->filter_string = g_strdup (text); + + if (app_data->filter_changed_timeout) + g_source_remove (app_data->filter_changed_timeout); + + app_data->filter_changed_timeout = + g_timeout_add (75, handle_filter_changed_delayed, app_data); + app_data->stop_incremental_relayout = TRUE; + + return FALSE; +} + +static void +generate_filtered_lists (gpointer catdata, gpointer user_data) +{ + CategoryData *data = (CategoryData *) catdata; + + /* Fixme - everywhere you use ascii you need to fix up for multibyte */ + gchar *filter_string = g_ascii_strdown (user_data, -1); + gchar *temp1, *temp2; + GList *launcher_list = data->launcher_list; + + g_list_free (data->filtered_launcher_list); + data->filtered_launcher_list = NULL; + + do + { + ApplicationTile *launcher = APPLICATION_TILE (launcher_list->data); + const gchar *filename; + + temp1 = NULL; + temp2 = NULL; + + /* Since the filter may remove this entry from the + container it will not get a mouse out event */ + gtk_widget_set_state (GTK_WIDGET (launcher), GTK_STATE_NORMAL); + filename = g_object_get_data (G_OBJECT (launcher), TILE_EXEC_NAME); /* do I need to free this */ + + temp1 = g_ascii_strdown (launcher->name, -1); + if (launcher->description) + temp2 = g_ascii_strdown (launcher->description, -1); + if (g_strrstr (temp1, filter_string) || (launcher->description + && g_strrstr (temp2, filter_string)) + || g_strrstr (filename, filter_string)) + { + data->filtered_launcher_list = + g_list_append (data->filtered_launcher_list, launcher); + } + if (temp1) + g_free (temp1); + if (temp2) + g_free (temp2); + } + while (NULL != (launcher_list = g_list_next (launcher_list))); + g_free (filter_string); +} + +static void +delete_old_data (AppShellData * app_data) +{ + GList *temp; + GList *cat_list; + + g_assert (app_data != NULL); + g_assert (app_data->categories_list != NULL); + + cat_list = app_data->categories_list; + + do + { + CategoryData *data = (CategoryData *) cat_list->data; + gtk_widget_destroy (GTK_WIDGET (data->section)); + gtk_widget_destroy (GTK_WIDGET (data->group_launcher)); + g_object_unref (data->section); + g_object_unref (data->group_launcher); + g_free (data->category); + + for (temp = data->launcher_list; temp; temp = g_list_next (temp)) + { + g_free (g_object_get_data (G_OBJECT (temp->data), TILE_EXEC_NAME)); + g_object_unref (temp->data); + } + + g_list_free (data->launcher_list); + g_list_free (data->filtered_launcher_list); + g_free (data); + } + while (NULL != (cat_list = g_list_next (cat_list))); + + g_list_free (app_data->categories_list); + app_data->categories_list = NULL; + app_data->selected_group = NULL; +} + +static void +create_application_category_sections (AppShellData * app_data) +{ + GList *cat_list; + AtkObject *a11y_cat; + gint pos = 0; + + g_assert (app_data != NULL); + g_assert (app_data->categories_list != NULL); /* Fixme - pop up a dialog box and then close */ + + cat_list = app_data->categories_list; + + do + { + CategoryData *data = (CategoryData *) cat_list->data; + GtkWidget *header = gtk_label_new (data->category); + gchar *markup; + GtkWidget *hbox; + GtkWidget *table; + + gtk_misc_set_alignment (GTK_MISC (header), 0, 0.5); + data->group_launcher = TILE (nameplate_tile_new (NULL, NULL, header, NULL)); + g_object_ref (data->group_launcher); + + g_object_set_data (G_OBJECT (data->group_launcher), GROUP_POSITION_NUMBER_KEY, + GINT_TO_POINTER (pos)); + pos++; + g_signal_connect (data->group_launcher, "tile-activated", + G_CALLBACK (handle_group_clicked), app_data); + a11y_cat = gtk_widget_get_accessible (GTK_WIDGET (data->group_launcher)); + atk_object_set_name (a11y_cat, data->category); + + markup = g_markup_printf_escaped ("<span size=\"x-large\" weight=\"bold\">%s</span>", + data->category); + data->section = SLAB_SECTION (slab_section_new_with_markup (markup, Style2)); + + /* as we filter these will be added/removed from parent container and we dont want them destroyed */ + g_object_ref (data->section); + g_free (markup); + + hbox = gtk_hbox_new (FALSE, 0); + table = gtk_table_new (0, 0, TRUE); + gtk_table_set_col_spacings (GTK_TABLE (table), 5); + gtk_table_set_row_spacings (GTK_TABLE (table), 5); + gtk_box_pack_start (GTK_BOX (hbox), table, FALSE, FALSE, 15); + slab_section_set_contents (SLAB_SECTION (data->section), hbox); + } + while (NULL != (cat_list = g_list_next (cat_list))); +} + +static void +show_no_results_message (AppShellData * app_data, GtkWidget * containing_vbox) +{ + gchar *markup; + gchar *str1; + gchar *str2; + + if (!app_data->filtered_out_everything_widget) + { + GtkWidget *hbox; + GtkWidget *image; + GtkWidget *label; + + app_data->filtered_out_everything_widget = gtk_alignment_new (0.5, 0.5, 0.0, 0.0); + g_object_ref (app_data->filtered_out_everything_widget); + + hbox = gtk_hbox_new (FALSE, 0); + image = themed_icon_new ("face-surprise", GTK_ICON_SIZE_DIALOG); + gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0); + + label = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 15); + app_data->filtered_out_everything_widget_label = GTK_LABEL (label); + + gtk_container_add (GTK_CONTAINER (app_data->filtered_out_everything_widget), hbox); + } + + str1 = g_markup_printf_escaped ("<b>%s</b>", app_data->filter_string); + str2 = g_strdup_printf (_("Your filter \"%s\" does not match any items."), str1); + markup = g_strdup_printf ("<span size=\"large\"><b>%s</b></span>\n\n%s", + _("No matches found."), str2); + gtk_label_set_text (app_data->filtered_out_everything_widget_label, markup); + gtk_label_set_use_markup (app_data->filtered_out_everything_widget_label, TRUE); + gtk_box_pack_start (GTK_BOX (containing_vbox), app_data->filtered_out_everything_widget, + TRUE, TRUE, 0); + g_free (str1); + g_free (str2); + g_free (markup); +} + +static void +populate_application_category_sections (AppShellData * app_data, GtkWidget * containing_vbox) +{ + GList *cat_list = app_data->categories_list; + gboolean filtered_out_everything = TRUE; + if (app_data->cached_tables_list) + g_list_free (app_data->cached_tables_list); + app_data->cached_tables_list = NULL; + + remove_container_entries (GTK_CONTAINER (containing_vbox)); + do + { + CategoryData *data = (CategoryData *) cat_list->data; + if (NULL != data->filtered_launcher_list) + { + populate_application_category_section (app_data, data->section, + data->filtered_launcher_list); + gtk_box_pack_start (GTK_BOX (containing_vbox), GTK_WIDGET (data->section), + TRUE, TRUE, 0); + filtered_out_everything = FALSE; + } + } + while (NULL != (cat_list = g_list_next (cat_list))); + + if (TRUE == filtered_out_everything) + show_no_results_message (app_data, containing_vbox); +} + +static void +populate_application_category_section (AppShellData * app_data, SlabSection * section, + GList * launcher_list) +{ + GtkWidget *hbox; + GtkTable *table; + GList *children; + + g_assert (GTK_IS_HBOX (section->contents)); + hbox = GTK_WIDGET (section->contents); + + children = gtk_container_get_children (GTK_CONTAINER (hbox)); + table = children->data; + g_list_free (children); + + /* Make sure our implementation has not changed and it's still a GtkTable */ + g_assert (GTK_IS_TABLE (table)); + + app_data->cached_tables_list = g_list_append (app_data->cached_tables_list, table); + + app_resizer_layout_table_default (APP_RESIZER (app_data->category_layout), table, + launcher_list); + +} + +gboolean +regenerate_categories (AppShellData * app_data) +{ + delete_old_data (app_data); + generate_categories (app_data); + create_application_category_sections (app_data); + relayout_shell (app_data); + + return FALSE; /* remove this function from the list */ +} + +static void +matemenu_tree_changed_callback (MateMenuTree * old_tree, gpointer user_data) +{ + /* + This method only gets called on the first change (matemenu appears to ignore subsequent) until + we reget the root dir which we can't do in this method because if we do for some reason this + method then gets called multiple times for one actual change. This actually is okay because + it's probably a good idea to wait a couple seconds to regenerate the categories in case there + are multiple quick changes being made, no sense regenerating multiple times. + */ + g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE, 3000, (GSourceFunc) regenerate_categories, + user_data, NULL); +} + +AppShellData * +appshelldata_new (const gchar * menu_name, NewAppConfig * new_apps, const gchar * mateconf_keys_prefix, + GtkIconSize icon_size, gboolean show_tile_generic_name, gboolean exit_on_close) +{ + AppShellData *app_data = g_new0 (AppShellData, 1); + app_data->mateconf_prefix = mateconf_keys_prefix; + app_data->new_apps = new_apps; + app_data->menu_name = menu_name; + app_data->icon_size = icon_size; + app_data->stop_incremental_relayout = TRUE; + app_data->show_tile_generic_name = show_tile_generic_name; + app_data->exit_on_close = exit_on_close; + return app_data; +} + +void +generate_categories (AppShellData * app_data) +{ + MateMenuTreeDirectory *root_dir; + GSList *contents, *l; + gboolean need_misc = FALSE; + + if (!app_data->tree) + { + app_data->tree = matemenu_tree_lookup (app_data->menu_name, MATEMENU_TREE_FLAGS_NONE); + matemenu_tree_add_monitor (app_data->tree, matemenu_tree_changed_callback, app_data); + } + root_dir = matemenu_tree_get_root_directory (app_data->tree); + if (root_dir) + contents = matemenu_tree_directory_get_contents (root_dir); + else + contents = NULL; + if (!root_dir || !contents) + { + GtkWidget *dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failure loading - %s", + app_data->menu_name); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + exit (1); /* Fixme - is there a MATE/GTK way to do this. */ + } + + for (l = contents; l; l = l->next) + { + const char *category; + MateMenuTreeItem *item = l->data; + + switch (matemenu_tree_item_get_type (item)) + { + case MATEMENU_TREE_ITEM_DIRECTORY: + category = matemenu_tree_directory_get_name ((MateMenuTreeDirectory*)item); + generate_category(category, (MateMenuTreeDirectory*)item, app_data, TRUE); + break; + case MATEMENU_TREE_ITEM_ENTRY: + need_misc = TRUE; + break; + default: + break; + } + + matemenu_tree_item_unref (item); + } + g_slist_free (contents); + + if (need_misc) + generate_category (_("Other"), root_dir, app_data, FALSE); + + if (app_data->hash) + { + g_hash_table_destroy (app_data->hash); + app_data->hash = NULL; + } + + matemenu_tree_item_unref (root_dir); + + if (app_data->new_apps && (app_data->new_apps->max_items > 0)) + generate_new_apps (app_data); +} + +static void +generate_category (const char * category, MateMenuTreeDirectory * root_dir, AppShellData * app_data, gboolean recursive) +{ + CategoryData *data; + /* This is not needed. MateMenu already returns an ordered, non duplicate list + GList *list_entry; + list_entry = + g_list_find_custom (app_data->categories_list, category, + category_name_compare); + if (!list_entry) + { + */ + data = g_new0 (CategoryData, 1); + data->category = g_strdup (category); + app_data->categories_list = + /* use the matemenu order instead of alphabetical */ + g_list_append (app_data->categories_list, data); + /* g_list_insert_sorted (app_data->categories_list, data, category_data_compare); */ + /* + } + else + { + data = list_entry->data; + } + */ + + if (app_data->hash) /* used to eliminate dups on a per category basis. */ + g_hash_table_destroy (app_data->hash); + app_data->hash = g_hash_table_new (g_str_hash, g_str_equal); + generate_launchers (root_dir, app_data, data, recursive); +} + +static gboolean +check_specific_apps_hack (MateDesktopItem * item) +{ + static const gchar *COMMAND_LINE_LOCKDOWN_MATECONF_KEY = + "/desktop/mate/lockdown/disable_command_line"; + static const gchar *COMMAND_LINE_LOCKDOWN_DESKTOP_CATEGORY = "TerminalEmulator"; + static gboolean got_lockdown_value = FALSE; + static gboolean command_line_lockdown; + + gchar *path; + const char *exec; + + if (!got_lockdown_value) + { + got_lockdown_value = TRUE; + command_line_lockdown = get_slab_mateconf_bool (COMMAND_LINE_LOCKDOWN_MATECONF_KEY); + } + + /* This seems like an ugly hack but it's the way it's currently done in the old control center */ + exec = mate_desktop_item_get_string (item, MATE_DESKTOP_ITEM_EXEC); + + /* discard xscreensaver if mate-screensaver is installed */ + if ((exec && !strcmp (exec, "xscreensaver-demo")) + && (path = g_find_program_in_path ("mate-screensaver-preferences"))) + { + g_free (path); + return TRUE; + } + + /* discard mate-keyring-manager if CASA is installed */ + if ((exec && !strcmp (exec, "mate-keyring-manager")) + && (path = g_find_program_in_path ("CASAManager.sh"))) + { + g_free (path); + return TRUE; + } + + /* discard terminals if lockdown key is set */ + if (command_line_lockdown) + { + const gchar *categories = + mate_desktop_item_get_string (item, MATE_DESKTOP_ITEM_CATEGORIES); + if (g_strrstr (categories, COMMAND_LINE_LOCKDOWN_DESKTOP_CATEGORY)) + { + /* printf ("eliminating %s\n", mate_desktop_item_get_location (item)); */ + return TRUE; + } + } + + return FALSE; +} + +static void +generate_launchers (MateMenuTreeDirectory * root_dir, AppShellData * app_data, CategoryData * cat_data, gboolean recursive) +{ + MateDesktopItem *desktop_item; + const gchar *desktop_file; + GSList *contents, *l; + + contents = matemenu_tree_directory_get_contents (root_dir); + for (l = contents; l; l = l->next) + { + switch (matemenu_tree_item_get_type (l->data)) + { + case MATEMENU_TREE_ITEM_DIRECTORY: + /* g_message ("Found sub-category %s", matemenu_tree_directory_get_name (l->data)); */ + if (recursive) + generate_launchers (l->data, app_data, cat_data, TRUE); + break; + case MATEMENU_TREE_ITEM_ENTRY: + /* g_message ("Found item name is:%s", matemenu_tree_entry_get_name (l->data)); */ + desktop_file = matemenu_tree_entry_get_desktop_file_path (l->data); + if (desktop_file) + { + if (g_hash_table_lookup (app_data->hash, desktop_file)) + { + break; /* duplicate */ + } + /* Fixme - make sure it's safe to store this without duping it. As far as I can tell it is + safe as long as I don't hang on to this anylonger than I hang on to the MateMenuTreeEntry* + which brings up another point - am I supposed to free these or does freeing the top level recurse + */ + g_hash_table_insert (app_data->hash, (gpointer) desktop_file, + (gpointer) desktop_file); + } + desktop_item = mate_desktop_item_new_from_file (desktop_file, 0, NULL); + if (!desktop_item) + { + g_critical ("Failure - mate_desktop_item_new_from_file(%s)", + desktop_file); + break; + } + if (!check_specific_apps_hack (desktop_item)) + insert_launcher_into_category (cat_data, desktop_item, app_data); + mate_desktop_item_unref (desktop_item); + break; + default: + break; + } + + matemenu_tree_item_unref (l->data); + } + g_slist_free (contents); +} + +static void +generate_new_apps (AppShellData * app_data) +{ + GHashTable *all_apps_cache = NULL; + gchar *all_apps; + GError *error = NULL; + gchar *separator = "\n"; + gchar *mateconf_key; + + gchar *basename; + gchar *all_apps_file_name; + gchar **all_apps_split; + gint x; + gboolean got_new_apps; + CategoryData *new_apps_category = NULL; + GList *categories, *launchers; + GHashTable *new_apps_dups; + + mateconf_key = g_strdup_printf ("%s%s", app_data->mateconf_prefix, NEW_APPS_FILE_KEY); + basename = get_slab_mateconf_string (mateconf_key); + g_free (mateconf_key); + if (!basename) + { + g_warning ("Failure getting mateconf key NEW_APPS_FILE_KEY"); + return; + } + + all_apps_file_name = g_build_filename (g_get_home_dir (), basename, NULL); + g_free (basename); + + if (!g_file_get_contents (all_apps_file_name, &all_apps, NULL, &error)) + { + /* If file does not exist, this is the first time this user has run this, create the baseline file */ + GList *categories, *launchers; + GString *gstr; + gchar *dirname; + + g_error_free (error); + error = NULL; + + /* best initial size determined by running on a couple different platforms */ + gstr = g_string_sized_new (10000); + + for (categories = app_data->categories_list; categories; categories = categories->next) + { + CategoryData *data = categories->data; + for (launchers = data->launcher_list; launchers; launchers = launchers->next) + { + Tile *tile = TILE (launchers->data); + MateDesktopItem *item = + application_tile_get_desktop_item (APPLICATION_TILE (tile)); + const gchar *uri = mate_desktop_item_get_location (item); + g_string_append (gstr, uri); + g_string_append (gstr, separator); + } + } + + dirname = g_path_get_dirname (all_apps_file_name); + g_mkdir_with_parents (dirname, 0700); /* creates if does not exist */ + g_free (dirname); + + if (!g_file_set_contents (all_apps_file_name, gstr->str, -1, &error)) + g_warning ("Error setting all apps file:%s\n", error->message); + + g_string_free (gstr, TRUE); + g_free (all_apps_file_name); + return; + } + + all_apps_cache = g_hash_table_new (g_str_hash, g_str_equal); + all_apps_split = g_strsplit (all_apps, separator, -1); + for (x = 0; all_apps_split[x]; x++) + { + g_hash_table_insert (all_apps_cache, all_apps_split[x], all_apps_split[x]); + } + + got_new_apps = FALSE; + new_apps_dups = g_hash_table_new (g_str_hash, g_str_equal); + for (categories = app_data->categories_list; categories; categories = categories->next) + { + CategoryData *cat_data = categories->data; + for (launchers = cat_data->launcher_list; launchers; launchers = launchers->next) + { + Tile *tile = TILE (launchers->data); + MateDesktopItem *item = + application_tile_get_desktop_item (APPLICATION_TILE (tile)); + const gchar *uri = mate_desktop_item_get_location (item); + if (!g_hash_table_lookup (all_apps_cache, uri)) + { + GFile *file; + GFileInfo *info; + long filetime; + + if (g_hash_table_lookup (new_apps_dups, uri)) + { + /* if a desktop file is in 2 or more top level categories, only show it once */ + /* printf("Discarding Newapp duplicate:%s\n", uri); */ + break; + } + g_hash_table_insert (new_apps_dups, (gpointer) uri, (gpointer) uri); + + if (!got_new_apps) + { + new_apps_category = g_new0 (CategoryData, 1); + new_apps_category->category = + g_strdup (app_data->new_apps->name); + app_data->new_apps->garray = + g_array_sized_new (FALSE, TRUE, + sizeof (NewAppData *), + app_data->new_apps->max_items); + + /* should not need this, but a bug in glib does not actually clear the elements until you call this method */ + g_array_set_size (app_data->new_apps->garray, app_data->new_apps->max_items); + got_new_apps = TRUE; + } + + file = g_file_new_for_uri (uri); + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + 0, NULL, NULL); + + if (!info) + { + g_object_unref (file); + g_warning ("Cant get vfs info for %s\n", uri); + return; + } + filetime = (long) g_file_info_get_attribute_uint64 (info, + G_FILE_ATTRIBUTE_TIME_MODIFIED); + g_object_unref (info); + g_object_unref (file); + + for (x = 0; x < app_data->new_apps->max_items; x++) + { + NewAppData *temp_data = (NewAppData *) + g_array_index (app_data->new_apps->garray, NewAppData *, x); + if (!temp_data || filetime > temp_data->time) /* if this slot is empty or we are newer than this slot */ + { + NewAppData *temp = g_new0 (NewAppData, 1); + temp->time = filetime; + temp->item = item; + g_array_insert_val (app_data->new_apps->garray, x, + temp); + break; + } + } + } + } + } + g_hash_table_destroy (new_apps_dups); + g_hash_table_destroy (all_apps_cache); + + if (got_new_apps) + { + for (x = 0; x < app_data->new_apps->max_items; x++) + { + NewAppData *data = + (NewAppData *) g_array_index (app_data->new_apps->garray, + NewAppData *, x); + if (data) + { + insert_launcher_into_category (new_apps_category, data->item, + app_data); + g_free (data); + } + else + break; + } + app_data->categories_list = + g_list_prepend (app_data->categories_list, new_apps_category); + + g_array_free (app_data->new_apps->garray, TRUE); + } + g_free (all_apps); + g_free (all_apps_file_name); + g_strfreev (all_apps_split); +} + +static void +insert_launcher_into_category (CategoryData * cat_data, MateDesktopItem * desktop_item, + AppShellData * app_data) +{ + GtkWidget *launcher; + static GtkSizeGroup *icon_group = NULL; + + gchar *filepath; + gchar *filename; + GtkWidget *tile_icon; + + if (!icon_group) + icon_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + launcher = + application_tile_new_full (mate_desktop_item_get_location (desktop_item), + app_data->icon_size, app_data->show_tile_generic_name, app_data->mateconf_prefix); + gtk_widget_set_size_request (launcher, SIZING_TILE_WIDTH, -1); + + filepath = + g_strdup (mate_desktop_item_get_string (desktop_item, MATE_DESKTOP_ITEM_EXEC)); + g_strdelimit (filepath, " ", '\0'); /* just want the file name - no args or replacements */ + filename = g_strrstr (filepath, "/"); + if (filename) + g_stpcpy (filepath, filename + 1); + filename = g_ascii_strdown (filepath, -1); + g_free (filepath); + g_object_set_data (G_OBJECT (launcher), TILE_EXEC_NAME, filename); + + tile_icon = NAMEPLATE_TILE (launcher)->image; + gtk_size_group_add_widget (icon_group, tile_icon); + + g_signal_connect (launcher, "tile-activated", G_CALLBACK (tile_activated_cb), app_data); + + /* Note that this will handle the case of the action being launched via the side panel as + well as directly from the context menu of an individual launcher, because they both + funnel through tile_button_action_activate. + */ + g_signal_connect (launcher, "tile-action-triggered", + G_CALLBACK (handle_menu_action_performed), app_data); + + /* These will be inserted/removed from tables as the filter changes and we dont want them */ + /* destroyed when they are removed */ + g_object_ref (launcher); + + /* use alphabetical order instead of the matemenu order. We group all sub items in each top level + category together, ignoring sub menus, so we also ignore sub menu layout hints */ + cat_data->launcher_list = + /* g_list_insert (cat_data->launcher_list, launcher, -1); */ + g_list_insert_sorted (cat_data->launcher_list, launcher, application_launcher_compare); + cat_data->filtered_launcher_list = + /* g_list_insert (cat_data->filtered_launcher_list, launcher, -1); */ + g_list_insert_sorted (cat_data->filtered_launcher_list, launcher, application_launcher_compare); +} + +static gint +application_launcher_compare (gconstpointer a, gconstpointer b) +{ + ApplicationTile *launcher1 = APPLICATION_TILE (a); + ApplicationTile *launcher2 = APPLICATION_TILE (b); + + gchar *val1 = launcher1->name; + gchar *val2 = launcher2->name; + + if (val1 == NULL || val2 == NULL) + { + g_assert_not_reached (); + } + return g_ascii_strcasecmp (val1, val2); +} + +static void +application_launcher_clear_search_bar (AppShellData * app_data) +{ + SlabSection *section = SLAB_SECTION (app_data->filter_section); + NldSearchBar *search_bar; + g_assert (NLD_IS_SEARCH_BAR (section->contents)); + search_bar = NLD_SEARCH_BAR (section->contents); + nld_search_bar_set_text (search_bar, "", TRUE); +} + +/* +static gint +category_name_compare (gconstpointer a, gconstpointer b) +{ + CategoryData *data = (CategoryData *) a; + const gchar *category = b; + + if (category == NULL || data->category == NULL) + { + g_assert_not_reached (); + } + return g_ascii_strcasecmp (category, data->category); +} +*/ + +static void +tile_activated_cb (Tile * tile, TileEvent * event, gpointer user_data) +{ + switch (event->type) + { + case TILE_EVENT_ACTIVATED_SINGLE_CLICK: + case TILE_EVENT_ACTIVATED_KEYBOARD: + handle_launcher_single_clicked (tile, user_data); + break; + default: + break; + } + +} + +static void +handle_launcher_single_clicked (Tile * launcher, gpointer data) +{ + AppShellData *app_data = (AppShellData *) data; + gchar *mateconf_key; + + tile_trigger_action (launcher, launcher->actions[APPLICATION_TILE_ACTION_START]); + + mateconf_key = g_strdup_printf ("%s%s", app_data->mateconf_prefix, EXIT_SHELL_ON_ACTION_START); + if (get_slab_mateconf_bool (mateconf_key)) + { + if (app_data->exit_on_close) + gtk_main_quit (); + else + hide_shell (app_data); + } + g_free (mateconf_key); +} + +static void +handle_menu_action_performed (Tile * launcher, TileEvent * event, TileAction * action, + gpointer data) +{ + AppShellData *app_data = (AppShellData *) data; + gchar *temp; + + temp = NULL; + if (action == launcher->actions[APPLICATION_TILE_ACTION_START]) + { + temp = g_strdup_printf ("%s%s", app_data->mateconf_prefix, EXIT_SHELL_ON_ACTION_START); + } + + else if (action == launcher->actions[APPLICATION_TILE_ACTION_HELP]) + { + temp = g_strdup_printf ("%s%s", app_data->mateconf_prefix, EXIT_SHELL_ON_ACTION_HELP); + } + + else if (action == launcher->actions[APPLICATION_TILE_ACTION_UPDATE_MAIN_MENU] + || action == launcher->actions[APPLICATION_TILE_ACTION_UPDATE_STARTUP]) + { + temp = g_strdup_printf ("%s%s", app_data->mateconf_prefix, + EXIT_SHELL_ON_ACTION_ADD_REMOVE); + } + + else if (action == launcher->actions[APPLICATION_TILE_ACTION_UPGRADE_PACKAGE] + || action == launcher->actions[APPLICATION_TILE_ACTION_UNINSTALL_PACKAGE]) + { + temp = g_strdup_printf ("%s%s", app_data->mateconf_prefix, + EXIT_SHELL_ON_ACTION_UPGRADE_UNINSTALL); + } + + if (temp) + { + if (get_slab_mateconf_bool (temp)) + { + if (app_data->exit_on_close) + gtk_main_quit (); + else + hide_shell (app_data); + } + g_free (temp); + } + else + g_warning ("Unknown Action"); +} |