diff options
author | Jiri Moskovcak <jmoskovc@redhat.com> | 2011-06-08 17:27:42 +0200 |
---|---|---|
committer | Jiri Moskovcak <jmoskovc@redhat.com> | 2011-06-08 17:27:42 +0200 |
commit | 0e74f780545b1aa8f6a5277b3cfdc9887c37ed5e (patch) | |
tree | b1249b7b96c908931f2c29f48f016da8be450429 /libreport/src/gui-wizard-gtk/wizard.c | |
parent | 7ac32c68dea1fe0c6efa6ac9bb0e8ad8716d7f7d (diff) | |
download | abrt-0e74f780545b1aa8f6a5277b3cfdc9887c37ed5e.tar.gz abrt-0e74f780545b1aa8f6a5277b3cfdc9887c37ed5e.tar.xz abrt-0e74f780545b1aa8f6a5277b3cfdc9887c37ed5e.zip |
split libreport to a separate package
Diffstat (limited to 'libreport/src/gui-wizard-gtk/wizard.c')
-rw-r--r-- | libreport/src/gui-wizard-gtk/wizard.c | 1782 |
1 files changed, 1782 insertions, 0 deletions
diff --git a/libreport/src/gui-wizard-gtk/wizard.c b/libreport/src/gui-wizard-gtk/wizard.c new file mode 100644 index 00000000..430c8a93 --- /dev/null +++ b/libreport/src/gui-wizard-gtk/wizard.c @@ -0,0 +1,1782 @@ +/* + Copyright (C) 2011 ABRT Team + Copyright (C) 2011 RedHat inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ +#include <gtk/gtk.h> +#include "abrt_dbus.h" +#include "libreport-gtk.h" +#include "wizard.h" + +#define DEFAULT_WIDTH 800 +#define DEFAULT_HEIGHT 500 + +#if GTK_MAJOR_VERSION == 2 && GTK_MINOR_VERSION < 22 +# define gtk_assistant_commit(...) ((void)0) +#endif + +typedef struct event_gui_data_t +{ + char *event_name; + GtkToggleButton *toggle_button; +} event_gui_data_t; + + +static GtkAssistant *g_assistant; + +static char *g_analyze_event_selected; +static char *g_reporter_events_selected; +static unsigned g_black_event_count = 0; + +static GtkBox *g_box_analyzers; +/* List of event_gui_data's */ +static GList *g_list_analyzers; +static GtkLabel *g_lbl_analyze_log; +static GtkTextView *g_tv_analyze_log; + +static GtkBox *g_box_reporters; +/* List of event_gui_data's */ +static GList *g_list_reporters; +static GtkLabel *g_lbl_report_log; +static GtkTextView *g_tv_report_log; + +static GtkContainer *g_container_details1; +static GtkContainer *g_container_details2; + +static GtkLabel *g_lbl_cd_reason; +static GtkTextView *g_tv_backtrace; +static GtkTextView *g_tv_comment; +static GtkEventBox *g_eb_comment; +static GtkWidget *g_widget_warnings_area; +static GtkBox *g_box_warning_labels; +static GtkToggleButton *g_tb_approve_bt; +static GtkButton *g_btn_refresh; + +static GtkLabel *g_lbl_reporters; +static GtkLabel *g_lbl_size; + +static GtkTreeView *g_tv_details; +static GtkCellRenderer *g_tv_details_renderer_value; +static GtkTreeViewColumn *g_tv_details_col_checkbox; +//static GtkCellRenderer *g_tv_details_renderer_checkbox; +static GtkListStore *g_ls_details; +enum +{ + /* Note: need to update types in + * gtk_list_store_new(DETAIL_NUM_COLUMNS, TYPE1, TYPE2...) + * if you change these: + */ + DETAIL_COLUMN_CHECKBOX, + DETAIL_COLUMN_NAME, + DETAIL_COLUMN_VALUE, + DETAIL_NUM_COLUMNS, +}; + +/* Search in bt */ +static guint g_timeout = 0; +static GtkEntry *g_search_entry_bt; + +static GtkBuilder *builder; +static PangoFontDescription *monospace_font; + + +/* THE PAGE FLOW + * page_5: user comments + * page_1: analyze action selection + * page_2: analyze progress + * page_3: reporter selection + * page_4: backtrace editor + * page_6: summary + * page_7: reporting progress + */ +enum { + PAGENO_SUMMARY, + PAGENO_EDIT_COMMENT, + PAGENO_ANALYZE_SELECTOR, + PAGENO_ANALYZE_PROGRESS, + PAGENO_REPORTER_SELECTOR, + PAGENO_EDIT_BACKTRACE, + PAGENO_REVIEW_DATA, + PAGENO_REPORT_PROGRESS, + PAGENO_REPORT_DONE, + PAGENO_NOT_SHOWN, + NUM_PAGES +}; + +/* Use of arrays (instead of, say, #defines to C strings) + * allows cheaper page_obj_t->name == PAGE_FOO comparisons + * instead of strcmp. + */ +static const gchar PAGE_SUMMARY[] = "page_0"; +static const gchar PAGE_EDIT_COMMENT[] = "page_1"; +static const gchar PAGE_ANALYZE_SELECTOR[] = "page_2"; +static const gchar PAGE_ANALYZE_PROGRESS[] = "page_3"; +static const gchar PAGE_REPORTER_SELECTOR[] = "page_4_report"; +static const gchar PAGE_EDIT_BACKTRACE[] = "page_5"; +static const gchar PAGE_REVIEW_DATA[] = "page_6_report"; +static const gchar PAGE_REPORT_PROGRESS[] = "page_7_report"; +static const gchar PAGE_REPORT_DONE[] = "page_8_report"; +static const gchar PAGE_NOT_SHOWN[] = "page_9_report"; + +static const gchar *const page_names[] = +{ + PAGE_SUMMARY, + PAGE_EDIT_COMMENT, + PAGE_ANALYZE_SELECTOR, + PAGE_ANALYZE_PROGRESS, + PAGE_REPORTER_SELECTOR, + PAGE_EDIT_BACKTRACE, + PAGE_REVIEW_DATA, + PAGE_REPORT_PROGRESS, + PAGE_REPORT_DONE, + PAGE_NOT_SHOWN, + NULL +}; + +typedef struct +{ + const gchar *name; + const gchar *title; + GtkAssistantPageType type; + GtkWidget *page_widget; +} page_obj_t; + +static page_obj_t pages[] = +{ + /* Page types: + * CONTENT: normal page (has all btns: [Cancel] [Last] [Back] [Fwd]) + * INTRO: only [Fwd] button is shown + * (we use these where we want to suppress [Back]-navigation) + * CONFIRM: has [Apply] instead of [Fwd] and emits "apply" signal + * PROGRESS: skipped on [Back] navigation + * SUMMARY: has only [Close] button + * + * Note that we suppress [Cancel] everywhere once and for all + * using gtk_assistant_commit at init time. + */ + /* glade element name , on-screen text , type */ + { PAGE_SUMMARY , "Problem description" , GTK_ASSISTANT_PAGE_CONTENT }, + { PAGE_EDIT_COMMENT,"Provide additional information", GTK_ASSISTANT_PAGE_CONTENT }, + { PAGE_ANALYZE_SELECTOR , "Select analyzer" , GTK_ASSISTANT_PAGE_CONFIRM }, + { PAGE_ANALYZE_PROGRESS , "Analyzing" , GTK_ASSISTANT_PAGE_INTRO }, + /* Some reporters don't need backtrace, we can skip bt page for them. + * Therefore we want to know reporters _before_ we go to bt page + */ + { PAGE_REPORTER_SELECTOR , "Select reporter" , GTK_ASSISTANT_PAGE_CONTENT }, + { PAGE_EDIT_BACKTRACE , "Review the backtrace" , GTK_ASSISTANT_PAGE_CONTENT }, + { PAGE_REVIEW_DATA , "Confirm data to report", GTK_ASSISTANT_PAGE_CONFIRM }, + /* Was GTK_ASSISTANT_PAGE_PROGRESS, but we want to allow returning to it */ + { PAGE_REPORT_PROGRESS , "Reporting" , GTK_ASSISTANT_PAGE_INTRO }, + { PAGE_REPORT_DONE , "Reporting done" , GTK_ASSISTANT_PAGE_CONTENT }, + /* We prevent user from reaching this page, as SUMMARY can't be navigated away + * (must be always closed) and we don't want that + */ + { PAGE_NOT_SHOWN , "" , GTK_ASSISTANT_PAGE_SUMMARY }, + { NULL } +}; + +static page_obj_t *added_pages[NUM_PAGES]; + + +/* Utility functions */ + +static void remove_child_widget(GtkWidget *widget, gpointer unused) +{ + /* Destroy will safely remove it and free the memory + * if there are no refs left + */ + gtk_widget_destroy(widget); +} + +static void save_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) +{ + *(gint*)user_data = response_id; +} + +struct dump_dir *steal_if_needed(struct dump_dir *dd) +{ + if (!dd) + xfunc_die(); /* error msg was already logged */ + + if (dd->locked) + return dd; + + dd_close(dd); + + char *HOME = getenv("HOME"); + if (!HOME || !HOME[0]) + { + struct passwd *pw = getpwuid(getuid()); + HOME = pw ? pw->pw_dir : NULL; + } + if (HOME && HOME[0]) + HOME = concat_path_file(HOME, ".abrt/spool"); + else + HOME = xstrdup("/tmp"); + + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(g_assistant), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_OK_CANCEL, + _("Need writable directory, but '%s' is not writable." + " Move it to '%s' and operate on the moved copy?"), + g_dump_dir_name, HOME + ); + gint response = GTK_RESPONSE_CANCEL; + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(save_dialog_response), &response); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + if (response != GTK_RESPONSE_OK) + return NULL; + + dd = steal_directory(HOME, g_dump_dir_name); + if (!dd) + return NULL; /* Stealing failed. Error msg was already logged */ + + /* Delete old dir and switch to new one. + * Don't want to keep new dd open across deletion, + * therefore it's a bit more complicated. + */ + char *old_name = g_dump_dir_name; + g_dump_dir_name = xstrdup(dd->dd_dirname); + dd_close(dd); + + gtk_window_set_title(GTK_WINDOW(g_assistant), g_dump_dir_name); + delete_dump_dir_possibly_using_abrtd(old_name); //TODO: if (deletion_failed) error_msg("BAD")? + free(old_name); + + dd = dd_opendir(g_dump_dir_name, 0); + if (!dd) + xfunc_die(); /* error msg was already logged */ + + return dd; +} + +void show_error_as_msgbox(const char *msg) +{ + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(g_assistant), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_CLOSE, + "%s", msg + ); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static void load_text_to_text_view(GtkTextView *tv, const char *name) +{ + const char *str = g_cd ? get_problem_item_content_or_NULL(g_cd, name) : NULL; + gtk_text_buffer_set_text(gtk_text_view_get_buffer(tv), (str ? str : ""), -1); +} + +static gchar *get_malloced_string_from_text_view(GtkTextView *tv) +{ + GtkTextBuffer *buffer = gtk_text_view_get_buffer(tv); + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_start_iter(buffer, &start); + gtk_text_buffer_get_end_iter(buffer, &end); + return gtk_text_buffer_get_text(buffer, &start, &end, FALSE); +} + +static void save_text_if_changed(const char *name, const char *new_value) +{ + const char *old_value = g_cd ? get_problem_item_content_or_NULL(g_cd, name) : ""; + if (!old_value) + old_value = ""; + if (strcmp(new_value, old_value) != 0) + { + struct dump_dir *dd = dd_opendir(g_dump_dir_name, DD_OPEN_READONLY); + dd = steal_if_needed(dd); + if (dd && dd->locked) + { + dd_save_text(dd, name, new_value); + } +//FIXME: else: what to do with still-unsaved data in the widget?? + dd_close(dd); + reload_problem_data_from_dump_dir(); + update_gui_state_from_problem_data(); + } +} + +static void save_text_from_text_view(GtkTextView *tv, const char *name) +{ + gchar *new_str = get_malloced_string_from_text_view(tv); + save_text_if_changed(name, new_str); + free(new_str); +} + +static void append_to_textview(GtkTextView *tv, const char *str) +{ + GtkTextBuffer *tb = gtk_text_view_get_buffer(tv); + + /* Ensure we insert text at the end */ + GtkTextIter text_iter; + gtk_text_buffer_get_iter_at_offset(tb, &text_iter, -1); + gtk_text_buffer_place_cursor(tb, &text_iter); + + gtk_text_buffer_insert_at_cursor(tb, str, strlen(str)); + + /* Scroll so that the end of the log is visible */ + gtk_text_buffer_get_iter_at_offset(tb, &text_iter, -1); + gtk_text_view_scroll_to_iter(tv, &text_iter, + /*within_margin:*/ 0.0, /*use_align:*/ FALSE, /*xalign:*/ 0, /*yalign:*/ 0); +} + + +/* event_gui_data_t */ + +static event_gui_data_t *new_event_gui_data_t(void) +{ + return xzalloc(sizeof(event_gui_data_t)); +} + +static void free_event_gui_data_t(event_gui_data_t *evdata, void *unused) +{ + if (evdata) + { + free(evdata->event_name); + free(evdata); + } +} + + +/* tv_details handling */ + +static struct problem_item *get_current_problem_item_or_NULL(GtkTreeView *tree_view, gchar **pp_item_name) +{ + GtkTreeModel *model; + GtkTreeIter iter; + GtkTreeSelection* selection = gtk_tree_view_get_selection(tree_view); + if (!gtk_tree_selection_get_selected(selection, &model, &iter)) + return NULL; + + *pp_item_name = NULL; + gtk_tree_model_get(model, &iter, + DETAIL_COLUMN_NAME, pp_item_name, + -1); + if (!*pp_item_name) /* paranoia, should never happen */ + return NULL; + struct problem_item *item = get_problem_data_item_or_NULL(g_cd, *pp_item_name); + + return item; +} + +static void tv_details_row_activated( + GtkTreeView *tree_view, + GtkTreePath *tree_path_UNUSED, + GtkTreeViewColumn *column, + gpointer user_data) +{ + gchar *item_name; + struct problem_item *item = get_current_problem_item_or_NULL(tree_view, &item_name); + if (!item || !(item->flags & CD_FLAG_TXT)) + goto ret; + if (!strchr(item->content, '\n')) /* one line? */ + goto ret; /* yes */ + + gchar *arg[3]; + arg[0] = (char *) "xdg-open"; + arg[1] = concat_path_file(g_dump_dir_name, item_name); + arg[2] = NULL; + + g_spawn_sync(NULL, arg, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL, + NULL, NULL, NULL, NULL, NULL, NULL); + + free(arg[1]); + ret: + g_free(item_name); +} + +/* static gboolean tv_details_select_cursor_row( + GtkTreeView *tree_view, + gboolean arg1, + gpointer user_data) {...} */ + +static void tv_details_cursor_changed( + GtkTreeView *tree_view, + gpointer user_data_UNUSED) +{ + gchar *item_name; + struct problem_item *item = get_current_problem_item_or_NULL(tree_view, &item_name); + g_free(item_name); + + gboolean editable = (item && (item->flags & CD_FLAG_TXT) && !strchr(item->content, '\n')); + + /* Allow user to select the text with mouse. + * Has undesirable side-effect of allowing user to "edit" the text, + * but changes aren't saved (the old text reappears as soon as user + * leaves the field). Need to disable editing somehow. + */ + g_object_set(G_OBJECT(g_tv_details_renderer_value), + "editable", editable, + NULL); +} + +static void g_tv_details_checkbox_toggled( + GtkCellRendererToggle *cell_renderer_UNUSED, + gchar *tree_path, + gpointer user_data_UNUSED) +{ + //log("%s: path:'%s'", __func__, tree_path); + GtkTreeIter iter; + if (!gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(g_ls_details), &iter, tree_path)) + return; + + gchar *item_name = NULL; + gtk_tree_model_get(GTK_TREE_MODEL(g_ls_details), &iter, + DETAIL_COLUMN_NAME, &item_name, + -1); + if (!item_name) /* paranoia, should never happen */ + return; + struct problem_item *item = get_problem_data_item_or_NULL(g_cd, item_name); + g_free(item_name); + if (!item) /* paranoia */ + return; + + int cur_value; + if (item->selected_by_user == 0) + cur_value = item->default_by_reporter; + else + cur_value = !!(item->selected_by_user + 1); /* map -1,1 to 0,1 */ + //log("%s: allowed:%d reqd:%d def:%d user:%d cur:%d", __func__, + // item->allowed_by_reporter, + // item->required_by_reporter, + // item->default_by_reporter, + // item->selected_by_user, + // cur_value + //); + if (item->allowed_by_reporter && !item->required_by_reporter) + { + cur_value = !cur_value; + item->selected_by_user = cur_value * 2 - 1; /* map 0,1 to -1,1 */ + //log("%s: now ->selected_by_user=%d", __func__, item->selected_by_user); + gtk_list_store_set(g_ls_details, &iter, + DETAIL_COLUMN_CHECKBOX, cur_value, + -1); + } +} + + +/* update_gui_state_from_problem_data */ + +static gint find_by_button(gconstpointer a, gconstpointer button) +{ + const event_gui_data_t *evdata = a; + return (evdata->toggle_button != button); +} + +static void analyze_rb_was_toggled(GtkButton *button, gpointer user_data) +{ + free(g_analyze_event_selected); + g_analyze_event_selected = NULL; + GList *found = g_list_find_custom(g_list_analyzers, button, find_by_button); + if (found) + { + event_gui_data_t *evdata = found->data; + g_analyze_event_selected = xstrdup(evdata->event_name); + } +} + +static void report_tb_was_toggled(GtkButton *button_unused, gpointer user_data_unused) +{ + struct strbuf *reporters_string = strbuf_new(); + GList *li = g_list_reporters; + for (; li; li = li->next) + { + event_gui_data_t *event_gui_data = li->data; + if (gtk_toggle_button_get_active(event_gui_data->toggle_button) == TRUE) + { + strbuf_append_strf(reporters_string, + "%s%s", + (reporters_string->len != 0 ? ", " : ""), + event_gui_data->event_name + ); + g_validate_event(event_gui_data->event_name); + } + } + + gtk_assistant_set_page_complete(g_assistant, + pages[PAGENO_REPORTER_SELECTOR].page_widget, + reporters_string->len != 0 /* true if at least one checkbox is active */ + ); + + /* Update "list of reporters" label */ + free(g_reporter_events_selected); + g_reporter_events_selected = strbuf_free_nobuf(reporters_string); + gtk_label_set_text(g_lbl_reporters, g_reporter_events_selected); +} + +/* event_name contains "EVENT1\nEVENT2\nEVENT3\n". + * Add new {radio/check}buttons to GtkBox for each EVENTn (type depends on bool radio). + * Remember them in GList **p_event_list (list of event_gui_data_t's). + * Set "toggled" callback on each button to given GCallback if it's not NULL. + * Return active button (or NULL if none created). + */ +static event_gui_data_t *add_event_buttons(GtkBox *box, + GList **p_event_list, + char *event_name, + GCallback func, + bool radio) +{ + //VERB2 log("removing all buttons from box %p", box); + gtk_container_foreach(GTK_CONTAINER(box), &remove_child_widget, NULL); + g_list_foreach(*p_event_list, (GFunc)free_event_gui_data_t, NULL); + g_list_free(*p_event_list); + *p_event_list = NULL; + + if (radio) + g_black_event_count = 0; + + event_gui_data_t *first_button = NULL; + event_gui_data_t *active_button = NULL; + while (event_name[0]) + { + char *event_name_end = strchr(event_name, '\n'); + *event_name_end = '\0'; + + event_config_t *cfg = get_event_config(event_name); + + /* Form a pretty text representation of event */ + /* By default, use event name, just strip "foo_" prefix if it exists: */ + const char *event_screen_name = strchr(event_name, '_'); + if (event_screen_name) + event_screen_name++; + else + event_screen_name = event_name; + + const char *event_description = NULL; + char *tmp_description = NULL; + bool green_choice = false; + if (cfg) + { + /* .xml has (presumably) prettier description, use it: */ + if (cfg->screen_name) + event_screen_name = cfg->screen_name; + event_description = cfg->description; + if (cfg->ec_creates_items) + { + if (get_problem_data_item_or_NULL(g_cd, cfg->ec_creates_items)) + { + green_choice = true; + event_description = tmp_description = xasprintf(_("(not needed, '%s' already exists)"), cfg->ec_creates_items); + } + } + } + if (radio && !green_choice) + g_black_event_count++; + + //VERB2 log("adding button '%s' to box %p", event_name, box); + char *event_label = xasprintf("%s%s%s", + event_screen_name, + (event_description ? " - " : ""), + event_description ? event_description : "" + ); + free(tmp_description); + + GtkWidget *button = radio + ? gtk_radio_button_new_with_label_from_widget( + (first_button ? GTK_RADIO_BUTTON(first_button->toggle_button) : NULL), + event_label + ) + : gtk_check_button_new_with_label(event_label); + free(event_label); + + if (green_choice) + { + //static const GdkColor red = { .red = 0xffff }; + //gtk_widget_modify_text(button, GTK_STATE_NORMAL, &red); + GtkWidget *child = gtk_bin_get_child(GTK_BIN(button)); + if (child) + { + static const GdkColor green = { .green = 0x7fff }; + gtk_widget_modify_fg(child, GTK_STATE_NORMAL, &green); + gtk_widget_modify_fg(child, GTK_STATE_ACTIVE, &green); + gtk_widget_modify_fg(child, GTK_STATE_PRELIGHT, &green); + } + } + + if (func) + g_signal_connect(G_OBJECT(button), "toggled", func, NULL); + if (cfg && cfg->long_descr) + gtk_widget_set_tooltip_text(button, cfg->long_descr); + + event_gui_data_t *event_gui_data = new_event_gui_data_t(); + event_gui_data->event_name = xstrdup(event_name); + event_gui_data->toggle_button = GTK_TOGGLE_BUTTON(button); + *p_event_list = g_list_append(*p_event_list, event_gui_data); + + if (!first_button) + first_button = event_gui_data; + + if (radio && !green_choice && !active_button) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), true); + active_button = event_gui_data; + } + + *event_name_end = '\n'; + event_name = event_name_end + 1; + + gtk_box_pack_start(box, button, /*expand*/ false, /*fill*/ false, /*padding*/ 0); + } + + if (radio) + { + const char *msg_proceed_to_reporting = _("Go to reporting step"); + GtkWidget *button = radio + ? gtk_radio_button_new_with_label_from_widget( + (first_button ? GTK_RADIO_BUTTON(first_button->toggle_button) : NULL), + msg_proceed_to_reporting + ) + : gtk_check_button_new_with_label(msg_proceed_to_reporting); + if (func) + g_signal_connect(G_OBJECT(button), "toggled", func, NULL); + + event_gui_data_t *event_gui_data = new_event_gui_data_t(); + event_gui_data->event_name = xstrdup(""); + event_gui_data->toggle_button = GTK_TOGGLE_BUTTON(button); + *p_event_list = g_list_append(*p_event_list, event_gui_data); + + if (!active_button) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), true); + active_button = event_gui_data; + } + + gtk_box_pack_start(box, button, /*expand*/ false, /*fill*/ false, /*padding*/ 0); + } + + return active_button; +} + +struct cd_stats { + off_t filesize; + unsigned filecount; +}; + +static void append_item_to_ls_details(gpointer name, gpointer value, gpointer data) +{ + problem_item *item = (problem_item*)value; + struct cd_stats *stats = data; + GtkTreeIter iter; + + gtk_list_store_append(g_ls_details, &iter); + stats->filecount++; + + //FIXME: use the human-readable format_problem_item(item) instead of item->content. + if (item->flags & CD_FLAG_TXT) + { + stats->filesize += strlen(item->content); + /* If not multiline... */ + if (!strchr(item->content, '\n')) + { + gtk_list_store_set(g_ls_details, &iter, + DETAIL_COLUMN_NAME, (char *)name, + DETAIL_COLUMN_VALUE, item->content, + -1); + } + else + { + gtk_list_store_set(g_ls_details, &iter, + DETAIL_COLUMN_NAME, (char *)name, + DETAIL_COLUMN_VALUE, _("(click here to view/edit)"), + -1); + } + } + else if (item->flags & CD_FLAG_BIN) + { + struct stat statbuf; + statbuf.st_size = 0; + stat(item->content, &statbuf); + stats->filesize += statbuf.st_size; + char *msg = xasprintf(_("(binary file, %llu bytes)"), (long long)statbuf.st_size); + gtk_list_store_set(g_ls_details, &iter, + DETAIL_COLUMN_NAME, (char *)name, + DETAIL_COLUMN_VALUE, msg, + -1); + free(msg); + } +} + +void update_gui_state_from_problem_data(void) +{ + gtk_window_set_title(GTK_WINDOW(g_assistant), g_dump_dir_name); + + const char *reason = get_problem_item_content_or_NULL(g_cd, FILENAME_REASON); + gtk_label_set_text(g_lbl_cd_reason, reason ? reason : _("(no description)")); + + gtk_list_store_clear(g_ls_details); + struct cd_stats stats = { 0 }; + g_hash_table_foreach(g_cd, append_item_to_ls_details, &stats); + char *msg = xasprintf(_("%llu bytes, %u files"), (long long)stats.filesize, stats.filecount); + gtk_label_set_text(g_lbl_size, msg); + free(msg); + + load_text_to_text_view(g_tv_backtrace, FILENAME_BACKTRACE); + load_text_to_text_view(g_tv_comment, FILENAME_COMMENT); + + /* Update analyze radio buttons */ + event_gui_data_t *active_button = add_event_buttons(g_box_analyzers, &g_list_analyzers, + g_analyze_events, G_CALLBACK(analyze_rb_was_toggled), + /*radio:*/ true + ); + /* Update the value of currently selected analyzer */ + if (active_button) + { + free(g_analyze_event_selected); + g_analyze_event_selected = xstrdup(active_button->event_name); + VERB2 log("g_analyze_event_selected='%s'", g_analyze_event_selected); + } + + /* Update reporter checkboxes */ + /* Remember names of selected reporters */ + GList *old_reporters = NULL; + GList *li = g_list_reporters; + for (; li; li = li->next) + { + event_gui_data_t *event_gui_data = li->data; + if (gtk_toggle_button_get_active(event_gui_data->toggle_button) == TRUE) + { + /* order isn't important. prepend is faster */ + old_reporters = g_list_prepend(old_reporters, xstrdup(event_gui_data->event_name)); + } + } + /* Delete old checkboxes and create new ones */ + add_event_buttons(g_box_reporters, &g_list_reporters, + g_report_events, /*callback:*/ G_CALLBACK(report_tb_was_toggled), + /*radio:*/ false + ); + /* Re-select new reporters which were selected before we deleted them */ + GList *li_new = g_list_reporters; + for (; li_new; li_new = li_new->next) + { + event_gui_data_t *new_gui_data = li_new->data; + GList *li_old = old_reporters; + for (; li_old; li_old = li_old->next) + { + if (strcmp(new_gui_data->event_name, li_old->data) == 0) + { + gtk_toggle_button_set_active(new_gui_data->toggle_button, true); + break; + } + } + } + list_free_with_free(old_reporters); + + /* Update readiness state of reporter selector page and "list of reporters" label */ + report_tb_was_toggled(NULL, NULL); + + /* We can't just do gtk_widget_show_all once in main: + * We created new widgets (buttons). Need to make them visible. + */ + gtk_widget_show_all(GTK_WIDGET(g_assistant)); + + if (g_analyze_events[0]) + gtk_widget_show(GTK_WIDGET(g_btn_refresh)); + else + gtk_widget_hide(GTK_WIDGET(g_btn_refresh)); +} + + +/* start_event_run */ + +struct analyze_event_data +{ + struct run_event_state *run_state; + const char *event_name; + GList *more_events; + GList *env_list; + GtkWidget *page_widget; + GtkLabel *status_label; + GtkTextView *tv_log; + const char *end_msg; + GIOChannel *channel; + struct strbuf *event_log; + int event_log_state; + int fd; + /*guint event_source_id;*/ +}; +enum { + LOGSTATE_FIRSTLINE = 0, + LOGSTATE_BEGLINE, + LOGSTATE_ERRLINE, + LOGSTATE_MIDLINE, +}; + +static void set_excluded_envvar(void) +{ + struct strbuf *item_list = strbuf_new(); + const char *fmt = "%s"; + + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(g_ls_details), &iter)) + { + do { + gchar *item_name = NULL; + gboolean checked = 0; + gtk_tree_model_get(GTK_TREE_MODEL(g_ls_details), &iter, + DETAIL_COLUMN_NAME, &item_name, + DETAIL_COLUMN_CHECKBOX, &checked, + -1); + if (!item_name) /* paranoia, should never happen */ + continue; + if (!checked) + { + strbuf_append_strf(item_list, fmt, item_name); + fmt = ",%s"; + } + g_free(item_name); + } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(g_ls_details), &iter)); + } + char *var = strbuf_free_nobuf(item_list); + //log("EXCLUDE_FROM_REPORT='%s'", var); + if (var) + { + xsetenv("EXCLUDE_FROM_REPORT", var); + free(var); + } + else + unsetenv("EXCLUDE_FROM_REPORT"); +} + +static int spawn_next_command_in_evd(struct analyze_event_data *evd) +{ + evd->env_list = export_event_config(evd->event_name); + int r = spawn_next_command(evd->run_state, g_dump_dir_name, evd->event_name); + if (r < 0) + { + unexport_event_config(evd->env_list); + evd->env_list = NULL; + } + return r; +} + +static void save_to_event_log(struct analyze_event_data *evd, const char *str) +{ + static const char delim[] = { + [LOGSTATE_FIRSTLINE] = '>', + [LOGSTATE_BEGLINE] = ' ', + [LOGSTATE_ERRLINE] = '*', + }; + + while (str[0]) + { + char *end = strchrnul(str, '\n'); + char end_char = *end; + if (end_char == '\n') + end++; + switch (evd->event_log_state) + { + case LOGSTATE_FIRSTLINE: + case LOGSTATE_BEGLINE: + case LOGSTATE_ERRLINE: + /* skip empty lines */ + if (str[0] == '\n') + goto next; + strbuf_append_strf(evd->event_log, "%s%c %.*s", + iso_date_string(NULL), + delim[evd->event_log_state], + (int)(end - str), str + ); + break; + case LOGSTATE_MIDLINE: + strbuf_append_strf(evd->event_log, "%.*s", (int)(end - str), str); + break; + } + evd->event_log_state = LOGSTATE_MIDLINE; + if (end_char != '\n') + break; + evd->event_log_state = LOGSTATE_BEGLINE; + next: + str = end; + } +} + +static void update_event_log_on_disk(const char *str) +{ + /* Load existing log */ + struct dump_dir *dd = dd_opendir(g_dump_dir_name, 0); + if (!dd) + return; + char *event_log = dd_load_text_ext(dd, FILENAME_EVENT_LOG, DD_FAIL_QUIETLY_ENOENT); + + /* Append new log part to existing log */ + unsigned len = strlen(event_log); + if (len != 0 && event_log[len - 1] != '\n') + event_log = append_to_malloced_string(event_log, "\n"); + event_log = append_to_malloced_string(event_log, str); + + /* Trim log according to size watermarks */ + len = strlen(event_log); + char *new_log = event_log; + if (len > EVENT_LOG_HIGH_WATERMARK) + { + new_log += len - EVENT_LOG_LOW_WATERMARK; + new_log = strchrnul(new_log, '\n'); + if (new_log[0]) + new_log++; + } + + /* Save */ + dd_save_text(dd, FILENAME_EVENT_LOG, new_log); + free(event_log); + dd_close(dd); +} + +static gboolean consume_cmd_output(GIOChannel *source, GIOCondition condition, gpointer data) +{ + struct analyze_event_data *evd = data; + + /* Read and insert the output into the log pane */ + char buf[257]; /* usually we get one line, no need to have big buf */ + int r; + while ((r = read(evd->fd, buf, sizeof(buf)-1)) > 0) + { + buf[r] = '\0'; + append_to_textview(evd->tv_log, buf); + save_to_event_log(evd, buf); + } + + if (r < 0 && errno == EAGAIN) + /* We got all buffered data, but fd is still open. Done for now */ + return TRUE; /* "please don't remove this event (yet)" */ + + /* EOF/error */ + + unexport_event_config(evd->env_list); + evd->env_list = NULL; + + /* Wait for child to actually exit, collect status */ + int status; + waitpid(evd->run_state->command_pid, &status, 0); + int retval = WEXITSTATUS(status); + if (WIFSIGNALED(status)) + retval = WTERMSIG(status) + 128; + + /* Write a final message to the log */ + if (evd->event_log->len != 0 && evd->event_log->buf[evd->event_log->len - 1] != '\n') + save_to_event_log(evd, "\n"); + /* If program failed, or if it finished successfully without saying anything... */ + if (retval != 0 || evd->event_log_state == LOGSTATE_FIRSTLINE) + { + if (retval != 0) /* If program failed, emit error line */ + evd->event_log_state = LOGSTATE_ERRLINE; + char *msg; + if (WIFSIGNALED(status)) + msg = xasprintf("(killed by signal %u)\n", WTERMSIG(status)); + else + msg = xasprintf("(exited with %u)\n", retval); + append_to_textview(evd->tv_log, msg); + save_to_event_log(evd, msg); + free(msg); + } + + /* Append log to FILENAME_EVENT_LOG */ + update_event_log_on_disk(evd->event_log->buf); + strbuf_clear(evd->event_log); + evd->event_log_state = LOGSTATE_FIRSTLINE; + + if (geteuid() == 0) + { + /* Reset mode/uig/gid to correct values for all files created by event run */ + struct dump_dir *dd = dd_opendir(g_dump_dir_name, 0); + if (dd) + { + dd_sanitize_mode_and_owner(dd); + dd_close(dd); + } + } + + /* Stop if exit code is not 0, or no more commands */ + if (retval != 0 + || spawn_next_command_in_evd(evd) < 0 + ) { + VERB1 log("done running event on '%s': %d", g_dump_dir_name, retval); + append_to_textview(evd->tv_log, "\n"); + + for (;;) + { + if (!evd->more_events) + { + char *msg = xasprintf(evd->end_msg, retval); + gtk_label_set_text(evd->status_label, msg); + free(msg); + + /* Enable (un-gray out) navigation buttons */ + gtk_widget_set_sensitive(GTK_WIDGET(g_assistant), true); + + /*g_source_remove(evd->event_source_id);*/ + close(evd->fd); + free_run_event_state(evd->run_state); + strbuf_free(evd->event_log); + free(evd); + + reload_problem_data_from_dump_dir(); + update_gui_state_from_problem_data(); + + /* Inform abrt-gui that it is a good idea to rescan the directory */ + kill(getppid(), SIGCHLD); + + return FALSE; /* "please remove this event" */ + } + + evd->event_name = evd->more_events->data; + evd->more_events = g_list_remove(evd->more_events, evd->more_events->data); + + if (prepare_commands(evd->run_state, g_dump_dir_name, evd->event_name) != 0 + && spawn_next_command_in_evd(evd) >= 0 + ) { + VERB1 log("running event '%s' on '%s'", evd->event_name, g_dump_dir_name); + char *msg = xasprintf("--- Running %s ---\n", evd->event_name); + append_to_textview(evd->tv_log, msg); + free(msg); + break; + } + /* No commands needed?! (This is untypical) */ + } + } + + /* New command was started. Continue waiting for input */ + + /* Transplant cmd's output fd onto old one, so that main loop + * is none the wiser that fd it waits on has changed + */ + xmove_fd(evd->run_state->command_out_fd, evd->fd); + evd->run_state->command_out_fd = evd->fd; /* just to keep it consistent */ + ndelay_on(evd->fd); + + return TRUE; /* "please don't remove this event (yet)" */ +} + +static void start_event_run(const char *event_name, + GList *more_events, + GtkWidget *page, + GtkTextView *tv_log, + GtkLabel *status_label, + const char *start_msg, + const char *end_msg +) { + /* Start event asynchronously on the dump dir + * (synchronous run would freeze GUI until completion) + */ + struct run_event_state *state = new_run_event_state(); + + if (prepare_commands(state, g_dump_dir_name, event_name) == 0) + { + no_cmds: + /* No commands needed?! (This is untypical) */ + free_run_event_state(state); +//TODO: better msg? + char *msg = xasprintf(_("No processing for event '%s' is defined"), event_name); + gtk_label_set_text(status_label, msg); + free(msg); + return; + } + + struct dump_dir *dd = dd_opendir(g_dump_dir_name, DD_OPEN_READONLY); + dd = steal_if_needed(dd); + int locked = (dd && dd->locked); + dd_close(dd); + if (!locked) + return; /* user refused to steal, or write error, etc... */ + + set_excluded_envvar(); + GList *env_list = export_event_config(event_name); + + if (spawn_next_command(state, g_dump_dir_name, event_name) < 0) + { + unexport_event_config(env_list); + goto no_cmds; + } + + /* At least one command is needed, and we started first one. + * Hook its output fd to the main loop. + */ + struct analyze_event_data *evd = xzalloc(sizeof(*evd)); + evd->run_state = state; + evd->event_name = event_name; + evd->more_events = more_events; + evd->env_list = env_list; + evd->page_widget = page; + evd->status_label = status_label; + evd->tv_log = tv_log; + evd->end_msg = end_msg; + evd->event_log = strbuf_new(); + evd->fd = state->command_out_fd; + ndelay_on(evd->fd); + evd->channel = g_io_channel_unix_new(evd->fd); + /*evd->event_source_id = */ g_io_add_watch(evd->channel, + G_IO_IN | G_IO_ERR | G_IO_HUP, /* need HUP to detect EOF w/o any data */ + consume_cmd_output, + evd + ); + + gtk_label_set_text(status_label, start_msg); + + VERB1 log("running event '%s' on '%s'", event_name, g_dump_dir_name); +//TODO: save_to_event_log(evd, "message that we run event foo")? + char *msg = xasprintf("--- Running %s ---\n", event_name); + append_to_textview(evd->tv_log, msg); + free(msg); + + /* Disable (gray out) navigation buttons */ + gtk_widget_set_sensitive(GTK_WIDGET(g_assistant), false); +} + + +/* Backtrace checkbox handling */ + +static void add_warning(const char *warning) +{ + char *label_str = xasprintf("• %s", warning); + GtkWidget *warning_lbl = gtk_label_new(label_str); + /* should be safe to free it, gtk calls strdup() to copy it */ + free(label_str); + + gtk_misc_set_alignment(GTK_MISC(warning_lbl), 0.0, 0.0); + gtk_label_set_justify(GTK_LABEL(warning_lbl), GTK_JUSTIFY_LEFT); + gtk_box_pack_start(g_box_warning_labels, warning_lbl, false, false, 0); + gtk_widget_show(warning_lbl); +} + +static void check_bt_rating_and_allow_send(void) +{ + bool send = true; + bool warn = false; + + /* erase all warnings */ + gtk_widget_hide(g_widget_warnings_area); + gtk_container_foreach(GTK_CONTAINER(g_box_warning_labels), &remove_child_widget, NULL); + + /* + * FIXME: this should be bind to a reporter not to a compoment + * but so far only oopses don't have rating, so for now we + * skip the "kernel" manually + */ + const char *component = get_problem_item_content_or_NULL(g_cd, FILENAME_COMPONENT); +//FIXME: say "no" to special casing! + if (strcmp(component, "kernel") != 0) + { + const char *rating = get_problem_item_content_or_NULL(g_cd, FILENAME_RATING); +//COMPAT, remove after 2.1 release + if (!rating) rating= get_problem_item_content_or_NULL(g_cd, "rating"); + if (rating) switch (*rating) + { + case '4': /* bt is ok - no warning here */ + break; + case '3': /* bt is usable, but not complete, so show a warning */ + add_warning(_("The backtrace is incomplete, please make sure you provide the steps to reproduce.")); + warn = true; + break; + default: + //FIXME: see CreporterAssistant: 394 for ideas + add_warning(_("Reporting disabled because the backtrace is unusable.")); + send = false; + warn = true; + break; + } + } + + if (!gtk_toggle_button_get_active(g_tb_approve_bt)) + { + add_warning(_("You should check the backtrace for sensitive data.")); + add_warning(_("You must agree with sending the backtrace.")); + send = false; + warn = true; + } + + gtk_assistant_set_page_complete(g_assistant, + pages[PAGENO_EDIT_BACKTRACE].page_widget, + send); + if (warn) + gtk_widget_show(g_widget_warnings_area); +} + +static void on_bt_approve_toggle(GtkToggleButton *togglebutton, gpointer user_data) +{ + check_bt_rating_and_allow_send(); +} + +static void on_comment_changed(GtkTextBuffer *buffer, gpointer user_data) +{ + bool good = gtk_text_buffer_get_char_count(buffer) >= 10; + + /* The page doesn't exist with report-only option */ + if (pages[PAGENO_EDIT_COMMENT].page_widget == NULL) + return; + + /* Allow next page only when the comment has at least 10 chars */ + gtk_assistant_set_page_complete(g_assistant, pages[PAGENO_EDIT_COMMENT].page_widget, good); + + /* And show the eventbox with label */ + if (good) + gtk_widget_hide(GTK_WIDGET(g_eb_comment)); + else + gtk_widget_show(GTK_WIDGET(g_eb_comment)); +} + + +/* Refresh button handling */ + +static void on_btn_refresh_clicked(GtkButton *button) +{ + /* Save backtrace text if changed */ + save_text_from_text_view(g_tv_backtrace, FILENAME_BACKTRACE); + + /* Refresh GUI so that we see new analyze buttons */ + update_gui_state_from_problem_data(); + + /* Change page to analyzer selector - let user play with them */ + gtk_assistant_set_current_page(g_assistant, PAGENO_ANALYZE_SELECTOR); +} + + +/* Page navigation handlers */ + +static void next_page(GtkAssistant *assistant, gpointer user_data) +{ + /* page_no is actually the previous page, because this + * function is called before assistant goes to the next page + */ + int page_no = gtk_assistant_get_current_page(assistant); + VERB2 log("page_no:%d", page_no); + + if (added_pages[page_no]->name == PAGE_ANALYZE_SELECTOR) + { + VERB2 log("g_analyze_event_selected:'%s'", g_analyze_event_selected); + if (g_analyze_event_selected + && g_analyze_event_selected[0] + ) { + start_event_run(g_analyze_event_selected, + NULL, + pages[PAGENO_ANALYZE_PROGRESS].page_widget, + g_tv_analyze_log, + g_lbl_analyze_log, + _("Analyzing..."), + _("Analyzing finished with exit code %d") + ); + } + } + + if (added_pages[page_no]->name == PAGE_REVIEW_DATA) + { + GList *reporters = NULL; + GList *li = g_list_reporters; + for (; li; li = li->next) + { + event_gui_data_t *event_gui_data = li->data; + if (gtk_toggle_button_get_active(event_gui_data->toggle_button) == TRUE) + { + reporters = g_list_append(reporters, event_gui_data->event_name); + } + } + if (reporters) + { + char *first_event_name = reporters->data; + reporters = g_list_remove(reporters, reporters->data); + start_event_run(first_event_name, + reporters, + pages[PAGENO_REPORT_PROGRESS].page_widget, + g_tv_report_log, + g_lbl_report_log, + _("Reporting..."), + _("Reporting finished with exit code %d") + ); + } + } +} + +static void on_show_event_list_cb(GtkWidget *button, gpointer user_data) +{ + show_events_list_dialog(GTK_WINDOW(g_assistant)); +} + +#if 0 +static void log_ready_state() +{ + char buf[NUM_PAGES+1]; + for (int i = 0; i < NUM_PAGES; i++) + { + char ch = '_'; + if (pages[i].page_widget) + ch = gtk_assistant_get_page_complete(g_assistant, pages[i].page_widget) ? '+' : '-'; + buf[i] = ch; + } + buf[NUM_PAGES] = 0; + log("Completeness:[%s]", buf); +} +#endif + +static bool is_in_comma_separated_list(const char *value, const char *list) +{ + if (!list) + return false; + unsigned len = strlen(value); + while (*list) + { + const char *comma = strchrnul(list, ','); + if ((comma - list == len) && strncmp(value, list, len) == 0) + return true; + if (!*comma) + break; + list = comma + 1; + } + return false; +} + +static void on_page_prepare(GtkAssistant *assistant, GtkWidget *page, gpointer user_data) +{ + //int page_no = gtk_assistant_get_current_page(g_assistant); + //log_ready_state(); + + /* This suppresses [Last] button: assistant thinks that + * we never have this page ready unless we are on it + * -> therefore there is at least one non-ready page + * -> therefore it won't show [Last] + */ + // Doesn't work: if Completeness:[++++++-+++], + // then [Last] btn will still be shown. + //gtk_assistant_set_page_complete(g_assistant, + // pages[PAGENO_REVIEW_DATA].page_widget, + // pages[PAGENO_REVIEW_DATA].page_widget == page + //); + + if (pages[PAGENO_EDIT_BACKTRACE].page_widget == page) + { + check_bt_rating_and_allow_send(); + } + + /* Save text fields if changed */ + save_text_from_text_view(g_tv_backtrace, FILENAME_BACKTRACE); + save_text_from_text_view(g_tv_comment, FILENAME_COMMENT); + + if (pages[PAGENO_SUMMARY].page_widget == page + || pages[PAGENO_REVIEW_DATA].page_widget == page + ) { + GtkWidget *w = GTK_WIDGET(g_tv_details); + GtkContainer *c = GTK_CONTAINER(gtk_widget_get_parent(w)); + if (c) + gtk_container_remove(c, w); + gtk_container_add(pages[PAGENO_SUMMARY].page_widget == page ? + g_container_details1 : g_container_details2, + w + ); + /* Make checkbox column visible only on the last page */ + gtk_tree_view_column_set_visible(g_tv_details_col_checkbox, + (pages[PAGENO_REVIEW_DATA].page_widget == page) + ); + //gtk_cell_renderer_set_visible(g_tv_details_renderer_checkbox, + // (pages[PAGENO_REVIEW_DATA].page_widget == page) + //); + + if (pages[PAGENO_REVIEW_DATA].page_widget == page) + { + /* Based on selected reporter, update item checkboxes */ + event_config_t *cfg = get_event_config(g_reporter_events_selected ? g_reporter_events_selected : ""); + //log("%s: event:'%s', cfg:'%p'", __func__, g_reporter_events_selected, cfg); + if (cfg) + { + /* Default settings are... */ + int allowed_by_reporter = 1; + if (cfg->ec_exclude_items_always && strcmp(cfg->ec_exclude_items_always, "*") == 0) + allowed_by_reporter = 0; + int default_by_reporter = allowed_by_reporter; + if (cfg->ec_exclude_items_by_default && strcmp(cfg->ec_exclude_items_by_default, "*") == 0) + default_by_reporter = 0; + + GHashTableIter iter; + char *name; + struct problem_item *item; + g_hash_table_iter_init(&iter, g_cd); + while (g_hash_table_iter_next(&iter, (void**)&name, (void**)&item)) + { + /* Decide whether item is allowed, required, and what's the default */ + item->allowed_by_reporter = allowed_by_reporter; + if (is_in_comma_separated_list(name, cfg->ec_exclude_items_always)) + item->allowed_by_reporter = 0; + if ((item->flags & CD_FLAG_BIN) && cfg->ec_exclude_binary_items) + item->allowed_by_reporter = 0; + + item->default_by_reporter = item->allowed_by_reporter ? default_by_reporter : 0; + if (is_in_comma_separated_list(name, cfg->ec_exclude_items_by_default)) + item->default_by_reporter = 0; + if (is_in_comma_separated_list(name, cfg->ec_include_items_by_default)) + item->allowed_by_reporter = item->default_by_reporter = 1; + + item->required_by_reporter = 0; + if (is_in_comma_separated_list(name, cfg->ec_requires_items)) + item->default_by_reporter = item->allowed_by_reporter = item->required_by_reporter = 1; + + int cur_value; + if (item->selected_by_user == 0) + cur_value = item->default_by_reporter; + else + cur_value = !!(item->selected_by_user + 1); /* map -1,1 to 0,1 */ + + //log("%s: '%s' allowed:%d reqd:%d def:%d user:%d", __func__, name, + // item->allowed_by_reporter, + // item->required_by_reporter, + // item->default_by_reporter, + // item->selected_by_user + //); + + /* Find corresponding line and update checkbox */ + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(g_ls_details), &iter)) + { + do { + gchar *item_name = NULL; + gtk_tree_model_get(GTK_TREE_MODEL(g_ls_details), &iter, + DETAIL_COLUMN_NAME, &item_name, + -1); + if (!item_name) /* paranoia, should never happen */ + continue; + int differ = strcmp(name, item_name); + g_free(item_name); + if (differ) + continue; + gtk_list_store_set(g_ls_details, &iter, + DETAIL_COLUMN_CHECKBOX, cur_value, + -1); + //log("%s: changed gtk_list_store_set to %d", __func__, (item->allowed_by_reporter && item->selected_by_user >= 0)); + break; + } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(g_ls_details), &iter)); + } + } + } + } + } + + if (pages[PAGENO_EDIT_COMMENT].page_widget == page) + on_comment_changed(gtk_text_view_get_buffer(g_tv_comment), NULL); + //log_ready_state(); +} + +static gint select_next_page_no(gint current_page_no, gpointer data) +{ + /* we don't need any magic here if we're in only-report mode */ + if (g_report_only) + return current_page_no + 1; + + gint prev_page_no = current_page_no; + + again: + current_page_no++; + + switch (current_page_no) + { +#if 0 + case PAGENO_EDIT_COMMENT: + if (get_problem_item_content_or_NULL(g_cd, FILENAME_COMMENT)) + goto again; /* no comment, skip this page */ + break; +#endif + + case PAGENO_EDIT_BACKTRACE: + if (!get_problem_item_content_or_NULL(g_cd, FILENAME_BACKTRACE)) + goto again; /* no backtrace, skip this page */ + break; + + case PAGENO_ANALYZE_SELECTOR: + if (!g_analyze_events[0] || g_black_event_count == 0) + { + /* skip analyze selector page and analyze log page */ + current_page_no = PAGENO_REPORTER_SELECTOR-1; + goto again; + } + break; + + case PAGENO_ANALYZE_PROGRESS: + VERB2 log("%s: ANALYZE_PROGRESS: g_analyze_event_selected:'%s'", + __func__, g_analyze_event_selected); + if (!g_analyze_event_selected || !g_analyze_event_selected[0]) + goto again; /* skip this page */ + break; + + case PAGENO_REPORTER_SELECTOR: + VERB2 log("%s: REPORTER_SELECTOR: g_black_event_count:%d", + __func__, g_black_event_count); + /* if we _did_ run an event (didn't skip it) + * and still have analyzers which didn't run + */ + if (prev_page_no == PAGENO_ANALYZE_PROGRESS + && g_black_event_count != 0 + ) { + /* Go back to analyzer selectors */ + current_page_no = PAGENO_ANALYZE_SELECTOR-1; + goto again; + } + break; + case PAGENO_NOT_SHOWN: + /* No! this would SEGV (infinitely recurse into select_next_page_no) */ + /*gtk_assistant_commit(g_assistant);*/ + current_page_no = PAGENO_ANALYZE_SELECTOR-1; + goto again; + } + + VERB2 log("%s: selected page #%d", __func__, current_page_no); + return current_page_no; +} + + +static gboolean highlight_search(gpointer user_data) +{ + GtkEntry *entry = GTK_ENTRY(user_data); + GtkTextBuffer *buffer = gtk_text_view_get_buffer(g_tv_backtrace); + GtkTextIter start_find; + GtkTextIter end_find; + GtkTextIter start_match; + GtkTextIter end_match; + int offset = 0; + + gtk_text_buffer_get_start_iter(buffer, &start_find); + gtk_text_buffer_get_end_iter(buffer, &end_find); + gtk_text_buffer_remove_tag_by_name(buffer, "search_result_bg", &start_find, &end_find); + VERB1 log("searching: %s", gtk_entry_get_text(entry)); + while (gtk_text_iter_forward_search(&start_find, gtk_entry_get_text(entry), + GTK_TEXT_SEARCH_TEXT_ONLY, &start_match, + &end_match, NULL)) + { + gtk_text_buffer_apply_tag_by_name(buffer, "search_result_bg", + &start_match, &end_match); + offset = gtk_text_iter_get_offset(&end_match); + gtk_text_buffer_get_iter_at_offset(buffer, &start_find, offset); + } + + /* returning false will make glib to remove this event */ + return false; +} + +static void search_timeout(GtkEntry *entry) +{ + /* this little hack makes the search start searching after 500 milisec after + * user stops writing into entry box + * if this part is removed, then the search will be started on every + * change of the search entry + */ + if (g_timeout != 0) + g_source_remove(g_timeout); + g_timeout = g_timeout_add(500, &highlight_search, (gpointer)entry); +} + + +/* Initialization */ + +/* wizard.glade file as a string WIZARD_GLADE_CONTENTS: */ +#include "wizard_glade.c" + +static void add_pages() +{ + GError *error = NULL; + if (!g_glade_file) + { + /* Load UI from internal string */ + gtk_builder_add_objects_from_string(builder, + WIZARD_GLADE_CONTENTS, sizeof(WIZARD_GLADE_CONTENTS) - 1, + (gchar**)page_names, + &error); + if (error != NULL) + error_msg_and_die("Error loading glade data: %s", error->message); + } + else + { + /* -g FILE: load IU from it */ + gtk_builder_add_objects_from_file(builder, g_glade_file, (gchar**)page_names, &error); + if (error != NULL) + error_msg_and_die("Can't load %s: %s", g_glade_file, error->message); + } + + int i; + int page_no = 0; + for (i = 0; page_names[i] != NULL; i++) + { + char *delim = strrchr(page_names[i], '_'); + if (delim != NULL) + { + if (g_report_only && (strncmp(delim + 1, "report", strlen("report"))) != 0) + { + pages[i].page_widget = NULL; + continue; + } + } + GtkWidget *page = GTK_WIDGET(gtk_builder_get_object(builder, page_names[i])); + + pages[i].page_widget = page; + added_pages[page_no++] = &pages[i]; + + gtk_assistant_append_page(g_assistant, page); + /* If we set all pages to complete the wizard thinks there is nothing + * to do and shows the button "Last" which allows user to skip all pages + * so we need to set them all as incomplete and complete them one by one + * on proper place - on_page_prepare() ? + */ + gtk_assistant_set_page_complete(g_assistant, page, true); + + gtk_assistant_set_page_title(g_assistant, page, pages[i].title); + gtk_assistant_set_page_type(g_assistant, page, pages[i].type); + + VERB1 log("added page: %s", page_names[i]); + } + + /* Set pointers to objects we might need to work with */ + g_lbl_cd_reason = GTK_LABEL( gtk_builder_get_object(builder, "lbl_cd_reason")); + g_box_analyzers = GTK_BOX( gtk_builder_get_object(builder, "vb_analyzers")); + g_lbl_analyze_log = GTK_LABEL( gtk_builder_get_object(builder, "lbl_analyze_log")); + g_tv_analyze_log = GTK_TEXT_VIEW( gtk_builder_get_object(builder, "tv_analyze_log")); + g_box_reporters = GTK_BOX( gtk_builder_get_object(builder, "vb_reporters")); + g_lbl_report_log = GTK_LABEL( gtk_builder_get_object(builder, "lbl_report_log")); + g_tv_report_log = GTK_TEXT_VIEW( gtk_builder_get_object(builder, "tv_report_log")); + g_tv_backtrace = GTK_TEXT_VIEW( gtk_builder_get_object(builder, "tv_backtrace")); + g_tv_comment = GTK_TEXT_VIEW( gtk_builder_get_object(builder, "tv_comment")); + g_eb_comment = GTK_EVENT_BOX( gtk_builder_get_object(builder, "eb_comment")); + g_tv_details = GTK_TREE_VIEW( gtk_builder_get_object(builder, "tv_details")); + g_box_warning_labels = GTK_BOX( gtk_builder_get_object(builder, "box_warning_labels")); + g_tb_approve_bt = GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "cb_approve_bt")); + g_widget_warnings_area = GTK_WIDGET( gtk_builder_get_object(builder, "box_warning_area")); + g_btn_refresh = GTK_BUTTON( gtk_builder_get_object(builder, "btn_refresh")); + g_search_entry_bt = GTK_ENTRY( gtk_builder_get_object(builder, "entry_search_bt")); + g_container_details1 = GTK_CONTAINER( gtk_builder_get_object(builder, "container_details1")); + g_container_details2 = GTK_CONTAINER( gtk_builder_get_object(builder, "container_details2")); + g_lbl_reporters = GTK_LABEL( gtk_builder_get_object(builder, "lbl_reporters")); + g_lbl_size = GTK_LABEL( gtk_builder_get_object(builder, "lbl_size")); + + gtk_widget_modify_font(GTK_WIDGET(g_tv_analyze_log), monospace_font); + gtk_widget_modify_font(GTK_WIDGET(g_tv_report_log), monospace_font); + gtk_widget_modify_font(GTK_WIDGET(g_tv_backtrace), monospace_font); + fix_all_wrapped_labels(GTK_WIDGET(g_assistant)); + + if (pages[PAGENO_EDIT_BACKTRACE].page_widget != NULL) + gtk_assistant_set_page_complete(g_assistant, pages[PAGENO_EDIT_BACKTRACE].page_widget, + gtk_toggle_button_get_active(g_tb_approve_bt)); + + /* Configure btn on select analyzers page */ + GtkWidget *config_btn = GTK_WIDGET(gtk_builder_get_object(builder, "button_cfg1")); + if (config_btn) + g_signal_connect(G_OBJECT(config_btn), "clicked", G_CALLBACK(on_show_event_list_cb), NULL); + + /* Configure btn on select reporters page */ + config_btn = GTK_WIDGET(gtk_builder_get_object(builder, "button_cfg2")); + if (config_btn) + g_signal_connect(G_OBJECT(config_btn), "clicked", G_CALLBACK(on_show_event_list_cb), NULL); + + /* Add "Close" button */ + GtkWidget *w; + w = gtk_button_new_from_stock(GTK_STOCK_CLOSE); + g_signal_connect(w, "clicked", G_CALLBACK(gtk_main_quit), NULL); + gtk_widget_show(w); + gtk_assistant_add_action_widget(g_assistant, w); + /* and hide "Cancel" button - "Close" is a better name for what we want */ + gtk_assistant_commit(g_assistant); + + /* Set color of the comment evenbox */ + GdkColor color; + gdk_color_parse("#CC3333", &color); + gtk_widget_modify_bg(GTK_WIDGET(g_eb_comment), GTK_STATE_NORMAL, &color); +} + +static void create_details_treeview() +{ + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + + //g_tv_details_renderer_checkbox = + renderer = gtk_cell_renderer_toggle_new(); + g_tv_details_col_checkbox = column = gtk_tree_view_column_new_with_attributes( + _("Include"), renderer, + /* which "attr" of renderer to set from which COLUMN? (can be repeated) */ + "active", DETAIL_COLUMN_CHECKBOX, + NULL); + gtk_tree_view_append_column(g_tv_details, column); + /* This column has a handler */ + g_signal_connect(renderer, "toggled", G_CALLBACK(g_tv_details_checkbox_toggled), NULL); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes( + _("Name"), renderer, + "text", DETAIL_COLUMN_NAME, + NULL); + gtk_tree_view_append_column(g_tv_details, column); + + g_tv_details_renderer_value = renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes( + _("Value"), renderer, + "text", DETAIL_COLUMN_VALUE, + NULL); + gtk_tree_view_append_column(g_tv_details, column); + + /* + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes( + _("Path"), renderer, + "text", DETAIL_COLUMN_PATH, + NULL); + gtk_tree_view_append_column(g_tv_details, column); + */ + + gtk_tree_view_column_set_sort_column_id(column, DETAIL_COLUMN_NAME); + + g_ls_details = gtk_list_store_new(DETAIL_NUM_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_view_set_model(g_tv_details, GTK_TREE_MODEL(g_ls_details)); + + g_signal_connect(g_tv_details, "row-activated", G_CALLBACK(tv_details_row_activated), NULL); + g_signal_connect(g_tv_details, "cursor-changed", G_CALLBACK(tv_details_cursor_changed), NULL); + /* [Enter] on a row: + * g_signal_connect(g_tv_details, "select-cursor-row", G_CALLBACK(tv_details_select_cursor_row), NULL); + */ +} + +void create_assistant(void) +{ + monospace_font = pango_font_description_from_string("monospace"); + + builder = gtk_builder_new(); + + g_assistant = GTK_ASSISTANT(gtk_assistant_new()); + + gtk_assistant_set_forward_page_func(g_assistant, select_next_page_no, NULL, NULL); + + GtkWindow *wnd_assistant = GTK_WINDOW(g_assistant); + g_parent_window = wnd_assistant; + gtk_window_set_default_size(wnd_assistant, DEFAULT_WIDTH, DEFAULT_HEIGHT); + /* set_default sets icon for every windows used in this app, so we don't + * have to set the icon for those windows manually + */ + gtk_window_set_default_icon_name("abrt"); + + GObject *obj_assistant = G_OBJECT(g_assistant); + g_signal_connect(obj_assistant, "cancel", G_CALLBACK(gtk_main_quit), NULL); + g_signal_connect(obj_assistant, "close", G_CALLBACK(gtk_main_quit), NULL); + g_signal_connect(obj_assistant, "apply", G_CALLBACK(next_page), NULL); + g_signal_connect(obj_assistant, "prepare", G_CALLBACK(on_page_prepare), NULL); + + add_pages(); + + create_details_treeview(); + + g_signal_connect(g_tb_approve_bt, "toggled", G_CALLBACK(on_bt_approve_toggle), NULL); + g_signal_connect(g_btn_refresh, "clicked", G_CALLBACK(on_btn_refresh_clicked), NULL); + g_signal_connect(gtk_text_view_get_buffer(g_tv_comment), "changed", G_CALLBACK(on_comment_changed), NULL); + + /* init searching */ + GtkTextBuffer *backtrace_buf = gtk_text_view_get_buffer(g_tv_backtrace); + /* found items background */ + gtk_text_buffer_create_tag(backtrace_buf, "search_result_bg", "background", "red", NULL); + g_signal_connect(g_search_entry_bt, "changed", G_CALLBACK(search_timeout), NULL); +} |