--- pup.py 26 Jul 2006 20:56:21 -0000 1.18 +++ pup.py 9 Aug 2006 14:46:06 -0000 1.19 @@ -4,6 +4,7 @@ # # Jeremy Katz # Paul Nasrat +# Luke Macken # # 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 @@ -25,6 +26,7 @@ import subprocess import time import rpm +import webbrowser import gtk import gtk.glade @@ -39,6 +41,7 @@ import urlgrabber, urlgrabber.progress import rpmUtils.miscutils from yum.packages import YumInstalledPackage +from yum.update_md import UpdateMetadata from rhpl.exception import installExceptionHandler from rhpl.translate import _, N_, textdomain @@ -67,14 +70,20 @@ self.mainwin = self.pupxml.get_widget("pupWindow") self.mainwin.set_icon_from_file(imgfn) + + self.vpaned = self.pupxml.get_widget("vpaned1") + self.details = self.pupxml.get_widget("updateDetails") + self.expander = self.pupxml.get_widget("detailsExpander") + + self.scratchBuffer = gtk.TextBuffer() + self.updateMetadata = UpdateMetadata() + self._connectSignals() self._createUpdateStore() self.mainwin.connect("delete_event", self._quit) # note that nothing which takes "time" should be called here! GraphicalYumBase.__init__(self, False) - self.localPackages = [] - self.updates = [] def _connectSignals(self): sigs = {"on_quitButton_clicked": self._quit, @@ -83,13 +92,24 @@ "on_refreshButton_clicked": self.doRefresh} self.pupxml.signal_autoconnect(sigs) + self.details.set_buffer(gtk.TextBuffer()) + self.details.connect("event-after", UpdateDetails.event_after) + self.expander.connect("activate", lambda x: self.vpaned.set_position(-1)) + + # FIXME: figure out why this event only gets called when your cursor + # enters and leaves the TextView (making it impossible to change the + # cursor when hovering over a link) + #self.details.connect("motion-notify-event", + # UpdateDetails.motion_notify_event) + def _createUpdateStore(self): # checkbox, display string, list of - # (updateFunc, printFunc, new, old) tuples + # (updateFunc, printFunc, new, old, notice) tuples self.store = gtk.TreeStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT, - gobject.TYPE_STRING) + gobject.TYPE_STRING, + gobject.TYPE_PYOBJECT) tree = self.pupxml.get_widget("updateList") tree.set_model(self.store) @@ -114,7 +134,7 @@ self.store.set_sort_column_id(1, gtk.SORT_ASCENDING) - self.pupxml.get_widget("updateDetails").set_buffer(gtk.TextBuffer()) + self.details.set_buffer(self.scratchBuffer) selection = tree.get_selection() selection.connect("changed", self._updateSelected) @@ -124,16 +144,28 @@ self.store.set_value(i, 0, not val) def _updateSelected(self, selection): - ud = self.pupxml.get_widget("updateDetails") - ud.get_buffer().set_text("") (model, i) = selection.get_selected() if not i: return lst = model.get_value(i, 2) + notice = model.get_value(i, 4) + if notice: + self.details.set_buffer(notice) + return + strs = [] for (updfunc, strfunc, new, old) in lst: + md = self.updateMetadata.get_notice((new.name,new.ver,new.rel)) + if md: # use the update metadata + details = UpdateDetails(md.get_metadata()) + self.details.set_buffer(details) + model.set_value(i, 4, details) + return + else: # use the predefined strfunc strs.append(strfunc(new, old)) - ud.get_buffer().set_text(string.join(strs, "\n")) + + self.scratchBuffer.set_text(string.join(strs, '\n')) + self.details.set_buffer(self.scratchBuffer) def _runGtkmain(self, *args): while gtk.events_pending(): @@ -234,6 +266,14 @@ for (new, old) in updates: updating = self.getPackageObject(new) updated = YumInstalledPackage(self.rpmdb.returnHeaderByTuple(old)[0]) + + # populate update metadata + repo = self.repos.getRepo(updating.repoid) + try: # attempt to grab the updateinfo.xml.gz from the repodata + self.updateMetadata.add(repo) + except: + pass + srpm = updating.returnSimple("sourcerpm") if upds.has_key(srpm): upds[srpm].append( (self._doUpdate, self._printUpdate, @@ -254,7 +294,7 @@ self.store.append(None, [True, _("Updated %s packages available") % (rpmUtils.miscutils.splitFilename(srpm)[0],), - lst, pix]) + lst, pix, None]) if len(upds) == 0: self.pupxml.get_widget("updateNotebook").set_current_page(1) @@ -369,6 +409,132 @@ self._cleanup() sys.exit(0) +hovering_over_link = False +hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) +regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM) + +class UpdateDetails(gtk.TextBuffer): + + def __init__(self, metadata): + gtk.TextBuffer.__init__(self) + self.md = metadata + self.iter = self.get_start_iter() + self._build_tags() + self._parse_references() + self._populate_details() + + def _build_tags(self): + self.bold_tag = self.create_tag(weight=pango.WEIGHT_BOLD) + self.title_tag = self.create_tag(font='DejaVu LGC Sans Mono Bold') + self.title_tag.set_property('foreground', 'white') + self.title_tag.set_property('background-gdk', + gtk.gdk.color_parse('#CCCCCC')) + + def _parse_references(self): + self.cves = [] + self.bzs = [] + for ref in self.md['references']: + type = ref['type'] + if type == 'cve': + self.cves.append((ref['id'], ref['href'])) + elif type == 'bugzilla': + self.bzs.append((ref['id'], ref['href'])) + + def _populate_details(self): + titlecol_width = 12 + margin = ''.zfill(titlecol_width + 1).replace('0', ' ') + + def _add_item(title, field=None): + title = title.zfill(titlecol_width).replace('0', ' ') + self.insert_with_tags(self.iter, '%s ' % title, self.title_tag) + if field: + self.insert_with_tags(self.iter, ' %s\n' % self.md[field]) + + _add_item('ID', 'update_id') + _add_item('Type', 'type') + _add_item('Status', 'status') + _add_item('Issued', 'issued') + _add_item('Updated', 'updated') + + # Append the references + for title, list, lmt in (('Bugs', self.bzs, 6), ('CVEs', self.cves, 4)): + if len(list) == 0: + continue + title = title.zfill(titlecol_width).replace('0', ' ') + self.insert_with_tags(self.iter, '%s ' % title, self.title_tag) + i = 0 + for id, url in list: + self._insert_link(id, url) + self.insert(self.iter, ' ') + i += 1 + if i % lmt == 0: # allow lmt references per line + self.insert(self.iter, '\n') + self.insert_with_tags(self.iter, margin, self.title_tag) + self.insert(self.iter, '\n') + + if self.md['description']: + desc = 'Description'.zfill(titlecol_width).replace('0', ' ') + self.insert_with_tags(self.iter, desc + ' ', self.title_tag) + lines = self.md['description'].split('\n') + self.insert(self.iter, ' %s' % lines[0]) + for line in lines[1:]: + self.insert(self.iter, '\n') + self.insert_with_tags(self.iter, margin, self.title_tag) + self.insert(self.iter, ' ' + line) + + def _insert_link(self, text, url): + tag = self.create_tag(underline=pango.UNDERLINE_SINGLE, + foreground='blue') + tag.set_data('page', url) + self.insert_with_tags(self.iter, ' ' + text, tag) + + @staticmethod + def event_after(view, event): + """ Callback to monitor mouse clicks and handle links. """ + if event.type != gtk.gdk.BUTTON_RELEASE or event.button != 1: + return False + + # don't follow a link if the user has selected something + bounds = view.get_buffer().get_selection_bounds() + if len(bounds) != 0: + return False + + x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + iter = view.get_iter_at_location(x, y) + + for tag in iter.get_tags(): + page = tag.get_data('page') + if page: + webbrowser.open(page, new=True) + + ## FIXME: figure out why this method is only getting called when the + ## cursor enters or leaves the TextView. + @staticmethod + def motion_notify_event(view, event): + """ Callback to monitor mouse motion and change the cursor if it is + hovering over a link. + """ + print "motion_notify_event" + global hovering_over_link + hovering = False + + x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + iter = view.get_iter_at_location(x, y) + for tag in iter.get_tags(): + page = tag.get_data('page') + if page: + print "Hovering == True" + hovering = True + break + if hovering != hovering_over_link: + print "Changing cursor" + hovering_over_link = hovering + win = view.get_window(gtk.TEXT_WINDOW_WIDGET) + win.set_cursor(hovering_over_link and hand_cursor or regular_cursor) + + def main(): textdomain("pirut") gtk.glade.bindtextdomain("pirut", "/usr/share/locale") @@ -381,5 +547,3 @@ if __name__ == "__main__": installExceptionHandler("Software Updater", "") main() - -