import gtk, gobject from zeroinstall.injector import basedir from zeroinstall.injector.iface_cache import iface_cache from zeroinstall.injector import model import properties from treetips import TreeTips from gui import policy from zeroinstall import support from logging import warn def _stability(impl): assert impl if impl.user_stability is None: return impl.upstream_stability return _("%s (was %s)") % (impl.user_stability, impl.upstream_stability) ICON_SIZE = 20.0 CELL_TEXT_INDENT = int(ICON_SIZE) + 4 class InterfaceTips(TreeTips): def get_tooltip_text(self, item): interface, model_column = item assert interface if model_column == InterfaceBrowser.INTERFACE_NAME: return _("Full name: %s") % interface.uri elif model_column == InterfaceBrowser.SUMMARY: if not interface.description: return None first_para = interface.description.split('\n\n', 1)[0] return first_para.replace('\n', ' ') impl = policy.implementation.get(interface, None) if not impl: return _("No suitable implementation was found. Check the " "interface properties to find out why.") if model_column == InterfaceBrowser.VERSION: text = _("Currently preferred version: %s (%s)") % \ (impl.get_version(), _stability(impl)) old_impl = policy.original_implementation.get(interface, None) if old_impl is not None and old_impl is not impl: text += _('\nPreviously preferred version: %s (%s)') % \ (old_impl.get_version(), _stability(old_impl)) return text assert model_column == InterfaceBrowser.DOWNLOAD_SIZE if policy.get_cached(impl): return _("This version is already stored on your computer.") else: src = policy.get_best_source(impl) if not src: return _("No downloads available!") return _("Need to download %s (%s bytes)") % \ (support.pretty_size(src.size), src.size) tips = InterfaceTips() class IconAndTextRenderer(gtk.GenericCellRenderer): __gproperties__ = { "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE), "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE), } def do_set_property(self, prop, value): setattr(self, prop.name, value) def on_get_size(self, widget, cell_area, layout = None): if not layout: layout = widget.create_pango_layout(self.text) a, rect = layout.get_pixel_extents() pixmap_height = self.image.get_height() both_height = max(rect[1] + rect[3], pixmap_height) return (0, 0, rect[0] + rect[2] + CELL_TEXT_INDENT, both_height) def on_render(self, window, widget, background_area, cell_area, expose_area, flags): layout = widget.create_pango_layout(self.text) a, rect = layout.get_pixel_extents() if flags & gtk.CELL_RENDERER_SELECTED: state = gtk.STATE_SELECTED elif flags & gtk.CELL_RENDERER_PRELIT: state = gtk.STATE_PRELIGHT else: state = gtk.STATE_NORMAL image_y = int(0.5 * (cell_area.height - self.image.get_height())) window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0, cell_area.x, cell_area.y + image_y) text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3]))) widget.style.paint_layout(window, state, True, expose_area, widget, "cellrenderertext", cell_area.x + CELL_TEXT_INDENT, cell_area.y + text_y, layout) if gtk.pygtk_version < (2, 8, 0): # Note sure exactly which versions need this. # 2.8.0 gives a warning if you include it, though. gobject.type_register(IconAndTextRenderer) class InterfaceBrowser(gtk.ScrolledWindow): model = None root = None edit_properties = None cached_icon = None INTERFACE = 0 INTERFACE_NAME = 1 VERSION = 2 SUMMARY = 3 DOWNLOAD_SIZE = 4 ICON = 5 columns = [(_('Interface'), INTERFACE_NAME), (_('Version'), VERSION), (_('Fetch'), DOWNLOAD_SIZE), (_('Description'), SUMMARY)] def __init__(self): gtk.ScrolledWindow.__init__(self) self.cached_icon = {} # URI -> GdkPixbuf self.default_icon = self.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(self.style, gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, self, None) self.edit_properties = gtk.Action('edit_properties', 'Interface Properties...', 'Set which implementation of this interface to use.', gtk.STOCK_PROPERTIES) self.edit_properties.set_property('sensitive', False) self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) self.set_shadow_type(gtk.SHADOW_IN) self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf) self.tree_view = tree_view = gtk.TreeView(self.model) column_objects = [] text = gtk.CellRendererText() for name, model_column in self.columns: if model_column == InterfaceBrowser.INTERFACE_NAME: column = gtk.TreeViewColumn(name, IconAndTextRenderer(), text = model_column, image = InterfaceBrowser.ICON) else: column = gtk.TreeViewColumn(name, text, text = model_column) tree_view.append_column(column) column_objects.append(column) self.add(tree_view) tree_view.show() tree_view.set_enable_search(True) selection = tree_view.get_selection() def motion(tree_view, ev): if ev.window is not tree_view.get_bin_window(): return False pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y)) if pos: path = pos[0] try: col_index = column_objects.index(pos[1]) except ValueError: tips.hide() else: col = self.columns[col_index][1] row = self.model[path] item = (row[InterfaceBrowser.INTERFACE], col) if item != tips.item: tips.prime(tree_view, item) else: tips.hide() tree_view.connect('motion-notify-event', motion) tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide()) def sel_changed(sel): store, iter = sel.get_selected() self.edit_properties.set_property('sensitive', iter != None) selection.connect('changed', sel_changed) def button_press(tree_view, bev): if bev.button == 3 and bev.type == gtk.gdk.BUTTON_PRESS: pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y)) if not pos: return False path, col, x, y = pos selection.select_path(path) iface = self.model[path][InterfaceBrowser.INTERFACE] self.show_popup_menu(iface, bev) return True if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS: return False pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y)) if not pos: return False path, col, x, y = pos properties.edit(self.model[path][InterfaceBrowser.INTERFACE]) tree_view.connect('button-press-event', button_press) def edit_selected(action): store, iter = selection.get_selected() assert iter properties.edit(self.model[iter][InterfaceBrowser.INTERFACE]) self.edit_properties.connect('activate', edit_selected) self.connect('destroy', lambda s: policy.watchers.remove(self.build_tree)) policy.watchers.append(self.build_tree) def set_root(self, root): assert isinstance(root, model.Interface) self.root = root policy.recalculate() # Calls build_tree def get_icon(self, iface): """Get an icon for this interface. If the icon is in the cache, use that. If not, start a download. If we already started a download (successful or not) do nothing. Returns None if no icon is currently available.""" try: return self.cached_icon[iface.uri] except KeyError: path = policy.get_icon_path(iface) if path: try: loader = gtk.gdk.PixbufLoader('png') try: loader.write(file(path).read()) finally: loader.close() icon = loader.get_pixbuf() except Exception, ex: warn("Failed to load cached PNG icon: %s", ex) return None w = icon.get_width() h = icon.get_height() scale = max(w, h, 1) / ICON_SIZE icon = icon.scale_simple(int(w / scale), int(h / scale), gtk.gdk.INTERP_BILINEAR) self.cached_icon[iface.uri] = icon return icon return None def build_tree(self): if policy.original_implementation is None: policy.set_original_implementations() done = {} # Detect cycles self.model.clear() parent = None def add_node(parent, iface): if iface in done: return done[iface] = True iter = self.model.append(parent) self.model[iter][InterfaceBrowser.INTERFACE] = iface self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name() self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon impl = policy.implementation.get(iface, None) if impl: old_impl = policy.original_implementation.get(iface, None) version_str = impl.get_version() if old_impl is not None and old_impl is not impl: version_str += " (was " + old_impl.get_version() + ")" self.model[iter][InterfaceBrowser.VERSION] = version_str if policy.get_cached(impl): if impl.id.startswith('/'): fetch = '(local)' elif impl.id.startswith('package:'): fetch = '(package)' else: fetch = '(cached)' else: src = policy.get_best_source(impl) if src: fetch = support.pretty_size(src.size) else: fetch = '(unavailable)' self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = fetch if hasattr(impl, 'requires'): children = impl.requires else: children = impl.dependencies for child in children: if isinstance(child, model.InterfaceDependency): add_node(iter, iface_cache.get_interface(child.interface)) else: child_iter = self.model.append(parent) self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?' self.model[child_iter][InterfaceBrowser.SUMMARY] = \ 'Unknown dependency type : %s' % child self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon else: self.model[iter][InterfaceBrowser.VERSION] = '(choose)' add_node(None, self.root) self.tree_view.expand_all() def show_popup_menu(self, iface, bev): import bugs if properties.have_source_for(iface): def compile_cb(): import compile compile.compile(iface) else: compile_cb = None menu = gtk.Menu() for label, cb in [(_('Show Feeds'), lambda: properties.edit(iface)), (_('Show Versions'), lambda: properties.edit(iface, show_versions = True)), (_('Report a Bug...'), lambda: bugs.report_bug(policy, iface)), (_('Compile...'), compile_cb)]: item = gtk.MenuItem(label) if cb: item.connect('activate', lambda item, cb=cb: cb()) else: item.set_sensitive(False) item.show() menu.append(item) menu.popup(None, None, None, bev.button, bev.time)