From c0bd4813a42f3fe817d0de4dd6a1e9937a0c80b5 Mon Sep 17 00:00:00 2001 From: Jiri Moskovcak Date: Wed, 31 Mar 2010 12:07:15 +0200 Subject: GUI: total rewrite based on design from Mairin Duffy --- src/Gui/CCDump.py | 75 +++--- src/Gui/CCDumpList.py | 2 +- src/Gui/CCMainWindow.py | 187 ++++++++++---- src/Gui/CC_gui_functions.py | 12 +- src/Gui/PluginsSettingsDialog.py | 2 +- src/Gui/ccgui.glade | 527 ++++++++++++++++++++++++++++++++++----- 6 files changed, 653 insertions(+), 152 deletions(-) (limited to 'src') diff --git a/src/Gui/CCDump.py b/src/Gui/CCDump.py index fe903845..8a09b5b6 100644 --- a/src/Gui/CCDump.py +++ b/src/Gui/CCDump.py @@ -44,56 +44,73 @@ CD_MESSAGE = "Message" class Dump(): """Class for mapping the debug dump to python object""" + not_required_fields = ["comment", "Message"] def __init__(self): - self.UUID = None - self.uid = None - self.Count = None - self.executable = None - self.package = None - self.time = None - self.description = None - self.Message = None - self.Reported = None - self.analyzer = None + # we set all attrs dynamically, so no need to have it in init + for field in self.not_required_fields: + self.__dict__[field] = None + + def __setattr__(self, name, value): + if value != None: + if name == "time": + try: + self.__dict__["date"] = datetime.fromtimestamp(int(value[CD_CONTENT])).strftime("%c") + except Exception, ex: + self.__dict__["date"] = value[CD_CONTENT] + log2("can't convert timestamp to date: %s" % ex) + self.__dict__[name] = value[CD_CONTENT] + else: + self.__dict__[name] = value def getUUID(self): - return self.UUID[CD_CONTENT] + return self.UUID def getUID(self): - return self.uid[CD_CONTENT] + return self.uid def getCount(self): - return int(self.Count[CD_CONTENT]) + return int(self.Count) def getExecutable(self): - return self.executable[CD_CONTENT] + return self.executable def getPackage(self): - return self.package[CD_CONTENT] + return self.package def isReported(self): - return self.Reported[CD_CONTENT] == "1" + return self.Reported == "1" def getMessage(self): if not self.Message: return "" #[] #return self.Message[CD_CONTENT].split('\n') - return self.Message[CD_CONTENT] - - def getTime(self, fmt): - #print format - if fmt: - try: - return datetime.fromtimestamp(int(self.time[CD_CONTENT])).strftime(fmt) - except Exception, e: - print e - return int(self.time[CD_CONTENT]) + return self.Message + + def getTime(self, fmt=None): + if self.time: + if fmt: + try: + return datetime.fromtimestamp(int(self.time)).strftime(fmt) + except Exception, ex: + log1(ex) + return int(self.time) + return self.time def getPackageName(self): - return self.package[CD_CONTENT][:self.package[CD_CONTENT].find("-")] + name_delimiter_pos = self.package[:self.package.rfind("-")].rfind("-") + # fix for kerneloops + if name_delimiter_pos > 0: + return self.package[:name_delimiter_pos] + return self.package def getDescription(self): - return self.description[CD_CONTENT] + return self.description def getAnalyzerName(self): - return self.analyzer[CD_CONTENT] + return self.analyzer + + def get_reason(self): + return self.reason + + def get_comment(self): + return self.comment diff --git a/src/Gui/CCDumpList.py b/src/Gui/CCDumpList.py index c962f217..3c555d84 100644 --- a/src/Gui/CCDumpList.py +++ b/src/Gui/CCDumpList.py @@ -19,7 +19,7 @@ class DumpList(list): entry = Dump() for column in row: log2(" Dump.%s='%s'", column, row[column]) - entry.__dict__[column] = row[column] + entry.__setattr__(column, row[column]) self.append(entry) except Exception: # FIXME handle exception better diff --git a/src/Gui/CCMainWindow.py b/src/Gui/CCMainWindow.py index 781a8015..25b08460 100644 --- a/src/Gui/CCMainWindow.py +++ b/src/Gui/CCMainWindow.py @@ -41,54 +41,41 @@ class MainWindow(): #Get the Main Window, and connect the "destroy" event self.window = self.wTree.get_widget("main_window") if self.window: - self.window.set_default_size(700, 480) + self.window.set_default_size(600, 700) self.window.connect("delete_event", self.delete_event_cb) self.window.connect("destroy", self.destroy) self.window.connect("focus-in-event", self.focus_in_cb) - + self.wTree.get_widget("vp_details").modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse("#FFFFFF")) #init the dumps treeview self.dlist = self.wTree.get_widget("tvDumps") #rows of items with: - ICON_COL = 0 - PACKAGE_COL = 1 - APPLICATION_COL = 2 - TIME_STR_COL = 3 - CRASH_RATE_COL = 4 - USER_COL = 5 - IS_REPORTED_COL = 6 - UNIX_TIME_COL = 7 - DUMP_OBJECT_COL = 8 - #icon, package_name, application, date, crash_rate, user, is_reported, time_in_sec ?object? - self.dumpsListStore = gtk.ListStore(gtk.gdk.Pixbuf, str,str,str,int,str,bool, int, object) + STATUS_COL = 0 + APP_NAME_COL = 1 + TIME_STR_COL = 2 + UNIX_TIME_COL = 3 + DUMP_OBJECT_COL = 4 + #is_reported, application_name, date, time_in_sec ?object? + self.dumpsListStore = gtk.ListStore(str, str, str, int, object) self.dlist.set_model(self.dumpsListStore) # add pixbuff separatelly - icon_column = gtk.TreeViewColumn(_("Icon")) + icon_column = gtk.TreeViewColumn(_("Reported")) icon_column.cell = gtk.CellRendererPixbuf() - icon_column.cell.set_property('cell-background', "#C9C9C9") + #icon_column.cell.set_property('cell-background', "#C9C9C9") n = self.dlist.append_column(icon_column) - icon_column.pack_start(icon_column.cell, False) - icon_column.set_attributes(icon_column.cell, pixbuf=(n-1), cell_background_set=6) + icon_column.pack_start(icon_column.cell, True) + icon_column.set_attributes(icon_column.cell, stock_id=(n-1))# cell_background_set=6) # =============================================== columns = [] - columns.append(gtk.TreeViewColumn(_("Package"))) - columns[-1].set_sort_column_id(PACKAGE_COL) columns.append(gtk.TreeViewColumn(_("Application"))) - columns[-1].set_sort_column_id(APPLICATION_COL) - columns.append(gtk.TreeViewColumn(_("Date"))) + columns[-1].set_sort_column_id(APP_NAME_COL) + columns.append(gtk.TreeViewColumn(_("Latest Crash"))) columns[-1].set_sort_column_id(UNIX_TIME_COL) - columns.append(gtk.TreeViewColumn(_("Crash count"))) - columns[-1].set_sort_column_id(CRASH_RATE_COL) - columns.append(gtk.TreeViewColumn(_("User"))) - columns[-1].set_sort_column_id(USER_COL) - # create list + # add cells to colums and bind cells to the liststore values for column in columns: n = self.dlist.append_column(column) column.cell = gtk.CellRendererText() column.pack_start(column.cell, False) - #column.set_attributes(column.cell, ) - # FIXME: use some relative indexing - column.cell.set_property('cell-background', "#C9C9C9") - column.set_attributes(column.cell, text=(n-1), cell_background_set=6) + column.set_attributes(column.cell, text=(n-1)) column.set_resizable(True) #connect signals self.dlist.connect("cursor-changed", self.on_tvDumps_cursor_changed) @@ -96,6 +83,9 @@ class MainWindow(): self.dlist.connect("button-press-event", self.on_popupActivate) self.wTree.get_widget("bDelete").connect("clicked", self.on_bDelete_clicked, self.dlist) self.wTree.get_widget("bReport").connect("clicked", self.on_bReport_clicked) + self.wTree.get_widget("b_close").connect("clicked", self.on_bQuit_clicked) + self.wTree.get_widget("b_copy").connect("clicked", self.on_b_copy_clicked) + self.wTree.get_widget("b_help").connect("clicked", self.on_miAbout_clicked) self.wTree.get_widget("miQuit").connect("activate", self.on_bQuit_clicked) self.wTree.get_widget("miAbout").connect("activate", self.on_miAbout_clicked) self.wTree.get_widget("miPlugins").connect("activate", self.on_miPreferences_clicked) @@ -166,6 +156,19 @@ class MainWindow(): tvUpdates.set_buffer(buff) tvUpdates.scroll_mark_onscreen(end) + def get_username_from_uid(self, uid): + # if uid == None or "" return it back + if not uid: + return uid + user = "N/A" + if uid != "-1": # compat: only abrt <= 1.0.9 used UID = -1 + try: + user = pwd.getpwuid(int(uid))[0] + except Exception, ex: + user = "UID: %s" % uid + return user + + def hydrate(self): n = None self.dumpsListStore.clear() @@ -177,18 +180,11 @@ class MainWindow(): # so we shouldn't continue.. sys.exit() for entry in dumplist[::-1]: - try: - icon = get_icon_for_package(self.theme, entry.getPackageName()) - except: - icon = None - user = "N/A" - if entry.getUID() != "-1": # compat: only abrt <= 1.0.9 used UID = -1 - try: - user = pwd.getpwuid(int(entry.getUID()))[0] - except Exception, ex: - user = "UID: %s" % entry.getUID() - n = self.dumpsListStore.append([icon, entry.getPackage(), entry.getExecutable(), - entry.getTime("%c"), entry.getCount(), user, entry.isReported(), entry.getTime(""), entry]) + n = self.dumpsListStore.append([["gtk-no","gtk-yes"][entry.isReported()], + entry.getExecutable(), + entry.getTime("%c"), + entry.getTime(), + entry]) # activate the first row if any.. if n: # we can use (0,) as path for the first row, but what if API changes? @@ -198,21 +194,78 @@ class MainWindow(): # for later.. return True - def on_tvDumps_cursor_changed(self,treeview): + def on_tvDumps_cursor_changed(self, treeview): dumpsListStore, path = self.dlist.get_selection().get_selected_rows() if not path: self.wTree.get_widget("bDelete").set_sensitive(False) self.wTree.get_widget("bReport").set_sensitive(False) + self.wTree.get_widget("b_copy").set_sensitive(False) + # create an empty dump to fill the labels with empty strings + self.wTree.get_widget("sw_details").hide() return - self.wTree.get_widget("bDelete").set_sensitive(True) - self.wTree.get_widget("bReport").set_sensitive(True) - # this should work until we keep the row object in the last position - dump = dumpsListStore.get_value(dumpsListStore.get_iter(path[0]), dumpsListStore.get_n_columns()-1) + else: + self.wTree.get_widget("sw_details").show() + self.wTree.get_widget("bDelete").set_sensitive(True) + self.wTree.get_widget("bReport").set_sensitive(True) + self.wTree.get_widget("b_copy").set_sensitive(True) + # this should work until we keep the row object in the last position + dump = dumpsListStore.get_value(dumpsListStore.get_iter(path[0]), + dumpsListStore.get_n_columns()-1) + + try: + icon = get_icon_for_package(self.theme, dump.getPackageName()) + except: + icon = None + + i_package_icon = self.wTree.get_widget("i_package_icon") + if icon: + i_package_icon.set_from_pixbuf(icon) + else: + i_package_icon.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_DIALOG) + + l_heading = self.wTree.get_widget("l_detail_heading") + l_heading.set_markup(_("%s Crash\n%s") % (dump.getPackageName().title(),dump.getPackage())) + + # process the labels in sw_details + # hide the fields that are not filled by daemon - e.g. comments + # and how to reproduce + for field in dump.not_required_fields: + self.wTree.get_widget("l_%s" % field.lower()).hide() + self.wTree.get_widget("l_%s_heading" % field.lower()).hide() + + # fill the details + # read attributes from CCDump object and if a corresponding label is + # found, then the label text is set to the attribute's value + # field names in glade file: + # heading label: l__heading + # text label: l_ + for att in dump.__dict__: + label = self.wTree.get_widget("l_%s" % str(att).lower()) + if label: + label.show() + if att in dump.not_required_fields: + try: + lbl_heading = self.wTree.get_widget("l_%s_heading" % str(att).lower()) + lbl_heading.show() + except: + # we don't care if we fail to show the heading, it will + # break the gui a little, but it's better then exit + log2("failed to show the heading for >%s< : %s" % (att,e)) + pass + if dump.__dict__[att] != None: + label.set_text(dump.__dict__[att]) + else: + label.set_text("") + self.wTree.get_widget("l_date").set_text(dump.getTime("%c")) + self.wTree.get_widget("l_user").set_text(self.get_username_from_uid(dump.getUID())) + #move this to Dump class - lReported = self.wTree.get_widget("lReported") + hb_reports = self.wTree.get_widget("hb_reports") + lReported = self.wTree.get_widget("l_message") if dump.isReported(): - report_label_raw = _("This crash has been reported:\n") - report_label = _("This crash has been reported:\n") + hb_reports.show() + report_label_raw = "" + report_label = "" # plugin message follows, but at least in case of kerneloops, # it is not informative (no URL to the report) for message in dump.getMessage().split(';'): @@ -221,13 +274,13 @@ class MainWindow(): report_label += "%s\n" % report_message report_label_raw += "%s\n" % message log2("setting markup '%s'", report_label) - lReported.set_text(report_label_raw) # Sometimes (!) set_markup() fails with - # "GtkWarning: Failed to set text from markup due to error parsing markup: Unknown tag 'a'" - # If it does, then set_text() above acts as a fallback + # "GtkWarning: Failed to set text from markup due to error parsing + # markup: Unknown tag 'a'" If it does, then set_text() + # in "fill the details" above acts as a fallback lReported.set_markup(report_label) else: - lReported.set_markup(_("Not reported!")) + hb_reports.hide() def mark_last_selected_row(self, dump_list_store, path, iter, last_selected_uuid): # Get dump object from list (in our list it's in last col) @@ -257,6 +310,32 @@ class MainWindow(): except Exception, ex: print ex + def dumplist_get_selected(self): + dumpsListStore, path = self.dlist.get_selection().get_selected_rows() + if path and dumpsListStore: + return dumpsListStore.get_value(dumpsListStore.get_iter(path[0]), dumpsListStore.get_n_columns()-1) + return None + + def on_b_copy_clicked(self, button): + clipboard = gtk.clipboard_get() + dump = self.dumplist_get_selected() + if not dump: + gui_info_dialog(_("You have to select a crash to copy."), parent=self.window) + return + # dictionaries are not sorted, so we need this as a workaround + dumpinfo = [("Package:", dump.package), + ("Latest Crash:", dump.date), + ("Command:", dump.cmdline), + ("Reason:", dump.reason), + ("Comment:", dump.comment), + ("Bug Reports:", dump.Message), + ] + dumpinfo_text = "" + for line in dumpinfo: + dumpinfo_text += ("%-12s\t%s" % (line[0], line[1])).replace('\n','\n\t\t') + dumpinfo_text += '\n' + clipboard.set_text(dumpinfo_text) + def destroy(self, widget, data=None): gtk.main_quit() diff --git a/src/Gui/CC_gui_functions.py b/src/Gui/CC_gui_functions.py index f8f1c9ed..2365720a 100644 --- a/src/Gui/CC_gui_functions.py +++ b/src/Gui/CC_gui_functions.py @@ -109,11 +109,11 @@ def gui_report_dialog ( report_status_dict, parent_dialog, dialog.destroy() return ret -def gui_info_dialog ( message, parent_dialog, +def gui_info_dialog ( message, parent=None, message_type=gtk.MESSAGE_INFO, widget=None, page=0, broken_widget=None ): - dialog = gtk.MessageDialog( parent_dialog, + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, message_type, gtk.BUTTONS_OK, message ) @@ -128,9 +128,9 @@ def gui_info_dialog ( message, parent_dialog, if isinstance (broken_widget, gtk.Entry): broken_widget.select_region (0, -1) - if parent_dialog: + if parent: dialog.set_position (gtk.WIN_POS_CENTER_ON_PARENT) - dialog.set_transient_for(parent_dialog) + dialog.set_transient_for(parent) else: dialog.set_position (gtk.WIN_POS_CENTER) @@ -182,7 +182,7 @@ def gui_question_dialog ( message, parent_dialog=None, def get_icon_for_package(theme, package): log2("get_icon_for_package('%s')", package) try: - return theme.load_icon(package, 22, gtk.ICON_LOOKUP_USE_BUILTIN) + return theme.load_icon(package, 48, gtk.ICON_LOOKUP_USE_BUILTIN) except: # try to find icon filename by manually if not rpm: @@ -225,7 +225,7 @@ def get_icon_for_package(theme, package): break if icon_filename: log1("icon created from %s", icon_filename) - return gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, 22, 22) + return gtk.gdk.pixbuf_new_from_file_at_size(icon_filename, 48, 48) else: return None diff --git a/src/Gui/PluginsSettingsDialog.py b/src/Gui/PluginsSettingsDialog.py index be20b17a..05904300 100644 --- a/src/Gui/PluginsSettingsDialog.py +++ b/src/Gui/PluginsSettingsDialog.py @@ -145,7 +145,7 @@ class PluginsSettingsDialog: def on_bConfigurePlugin_clicked(self, button, pluginview): pluginsListStore, path = pluginview.get_selection().get_selected_rows() if not path: - gui_info_dialog(_("Please select a plugin from the list to edit it's options."), self.window) + gui_info_dialog(_("Please select a plugin from the list to edit it's options."), parent=self.window) return # this should work until we keep the row object in the last position pluginfo = pluginsListStore.get_value(pluginsListStore.get_iter(path[0]), pluginsListStore.get_n_columns()-1) diff --git a/src/Gui/ccgui.glade b/src/Gui/ccgui.glade index 73114684..cbb957a6 100644 --- a/src/Gui/ccgui.glade +++ b/src/Gui/ccgui.glade @@ -12,8 +12,7 @@ False ABRT @VER@ - (C) 2009 Red Hat, Inc. -(C) 2010 Red Hat, Inc. + (C) 2009, 2010 Red Hat, Inc. http://fedorahosted.org/abrt/ 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. @@ -27,7 +26,10 @@ Nikola Pajkovsky <npajkovs@redhat.com> Zdenek Prikryl <zprikryl@redhat.com> Denys Vlasenko <dvlasenk@redhat.com> translator-credits - Patrick Connelly <pcon@fedoraproject.org> + Patrick Connelly <pcon@fedoraproject.org> + +UI Design: +Máirín Duffy <duffy@redhat.com> abrt True @@ -118,6 +120,12 @@ Denys Vlasenko <dvlasenk@redhat.com> True + + + View log + True + + gtk-about @@ -137,49 +145,6 @@ Denys Vlasenko <dvlasenk@redhat.com> 0 - - - True - - - True - False - Delete - Delete - gtk-delete - - - False - True - - - - - True - Report - Report - gtk-go-up - - - False - True - - - - - True - - - False - True - - - - - False - 1 - - True @@ -197,6 +162,7 @@ Denys Vlasenko <dvlasenk@redhat.com> True True + True True 1 @@ -208,30 +174,322 @@ Denys Vlasenko <dvlasenk@redhat.com> - - True - True - automatic + + False + never automatic in - + True queue none - + True - 6 - 6 + vertical - + True - 0 - 0 - Not Reported - True + 10 + + + True + 5 + gtk-missing-image + 6 + + + False + False + 0 + + + + + True + 0 + True + + + 1 + + + + False + False + 10 + 0 + + + + + True + 5 + + + True + 0 + 0 + 5 + <b>Bug Reports:</b> + True + + + False + 0 + + + + + True + 0 + 0 + + + 1 + + + + + False + False + 1 + + + + + True + + + True + 5 + 2 + 5 + 5 + + + True + 0 + 0 + <b>Latest Crash:</b> + True + + + GTK_FILL + + + + + + True + 0 + 0 + <b>Command:</b> + True + + + 1 + 2 + GTK_FILL + GTK_FILL + + + + + True + 0 + 0 + <b>User:</b> + True + + + 2 + 3 + GTK_FILL + + + + + + True + 0 + 0 + <b>Crash Count:</b> + True + + + 3 + 4 + GTK_FILL + + + + + + True + 0 + 0 + True + 30 + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + 0 + True + 30 + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + 1.862645149230957e-09 + 0 + + + 1 + 2 + 2 + 3 + GTK_FILL + GTK_FILL + + + + + True + 0 + 0 + + + 1 + 2 + 3 + 4 + GTK_FILL + GTK_FILL + + + + + True + + + + + + 4 + 5 + + + + + True + + + + + + 1 + 2 + 4 + 5 + + + + + False + False + 5 + 0 + + + + + True + vertical + 5 + + + True + 0 + 0 + <b>Reason:</b> + True + + + False + False + 0 + + + + + True + 0 + 0 + True + 40 + + + 1 + + + + + True + 0 + 0 + <b>Comment:</b> + True + + + False + False + 2 + + + + + True + 0 + 0 + True + 40 + + + 3 + + + + + 1 + + + + + False + False + 2 + @@ -240,23 +498,170 @@ Denys Vlasenko <dvlasenk@redhat.com> False - True + False + + + + + 1 + + + + + True + 10 + 5 + True + + + True + + + + + + 0 + + + + + gtk-delete + True + False + True + True + True + + + 1 + + + + + Copy to Clipboard + True + False + True + True + + + 2 + + + + + Report + True + False + True + True + + + 3 + False + False 2 - + True + + + False + 10 3 + + + True + True + + + gtk-help + True + True + True + True + + + 10 + 0 + + + + + True + + + + + + 1 + + + + + True + + + + + + 2 + + + + + True + + + + + + 3 + + + + + gtk-close + True + True + True + True + + + 10 + 4 + + + + + False + 4 + + + + + True + + + + + + False + 10 + 5 + + -- cgit