summaryrefslogtreecommitdiffstats
path: root/libreport/src/gui-wizard-gtk/wizard.c
diff options
context:
space:
mode:
Diffstat (limited to 'libreport/src/gui-wizard-gtk/wizard.c')
-rw-r--r--libreport/src/gui-wizard-gtk/wizard.c1782
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);
+}