API changes: use Feed rather than Interface in many places
[zeroinstall/zeroinstall-afb.git] / zeroinstall / 0launch-gui / iface_browser.py
blobcd4d0854d15c2618fdcaed3ccb30e02d88478d03
1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import gtk, gobject, pango
6 from zeroinstall.support import tasks, pretty_size
7 from zeroinstall.injector.iface_cache import iface_cache
8 from zeroinstall.injector import model
9 import properties
10 from zeroinstall.gtkui.treetips import TreeTips
11 from zeroinstall.gtkui.icon import load_icon
12 from zeroinstall import support
13 from logging import warn
14 import utils
16 def _stability(impl):
17 assert impl
18 if impl.user_stability is None:
19 return impl.upstream_stability
20 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
21 % {'implementation_user_stability': impl.user_stability, 'implementation_upstream_stability': impl.upstream_stability}
23 ICON_SIZE = 20.0
24 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
26 class InterfaceTips(TreeTips):
27 mainwindow = None
29 def __init__(self, mainwindow):
30 self.mainwindow = mainwindow
32 def get_tooltip_text(self):
33 interface, model_column = self.item
34 assert interface
35 if model_column == InterfaceBrowser.INTERFACE_NAME:
36 return _("Full name: %s") % interface.uri
37 elif model_column == InterfaceBrowser.SUMMARY:
38 if not interface.description:
39 return None
40 first_para = interface.description.split('\n\n', 1)[0]
41 return first_para.replace('\n', ' ')
42 elif model_column is None:
43 return _("Click here for more options...")
45 impl = self.mainwindow.policy.implementation.get(interface, None)
46 if not impl:
47 return _("No suitable implementation was found. Check the "
48 "interface properties to find out why.")
50 if model_column == InterfaceBrowser.VERSION:
51 text = _("Currently preferred version: %(version)s (%(stability)s)") % \
52 {'version': impl.get_version(), 'stability': _stability(impl)}
53 old_impl = self.mainwindow.original_implementation.get(interface, None)
54 if old_impl is not None and old_impl is not impl:
55 text += '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
56 {'version': old_impl.get_version(), 'stability': _stability(old_impl)}
57 return text
59 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
61 if self.mainwindow.policy.get_cached(impl):
62 return _("This version is already stored on your computer.")
63 else:
64 src = self.mainwindow.policy.fetcher.get_best_source(impl)
65 if not src:
66 return _("No downloads available!")
67 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
68 {'pretty_size': support.pretty_size(src.size), 'size': src.size}
70 class MenuIconRenderer(gtk.GenericCellRenderer):
71 def __init__(self):
72 gtk.GenericCellRenderer.__init__(self)
73 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
75 def do_set_property(self, prop, value):
76 setattr(self, prop.name, value)
78 def on_get_size(self, widget, cell_area, layout = None):
79 return (0, 0, 20, 20)
81 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
82 if flags & gtk.CELL_RENDERER_PRELIT:
83 state = gtk.STATE_PRELIGHT
84 else:
85 state = gtk.STATE_NORMAL
87 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
88 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
89 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
90 gtk.ARROW_RIGHT, True,
91 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
93 class IconAndTextRenderer(gtk.GenericCellRenderer):
94 __gproperties__ = {
95 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
96 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
99 def do_set_property(self, prop, value):
100 setattr(self, prop.name, value)
102 def on_get_size(self, widget, cell_area, layout = None):
103 if not layout:
104 layout = widget.create_pango_layout(self.text)
105 a, rect = layout.get_pixel_extents()
107 pixmap_height = self.image.get_height()
109 both_height = max(rect[1] + rect[3], pixmap_height)
111 return (0, 0,
112 rect[0] + rect[2] + CELL_TEXT_INDENT,
113 both_height)
115 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
116 layout = widget.create_pango_layout(self.text)
117 a, rect = layout.get_pixel_extents()
119 if flags & gtk.CELL_RENDERER_SELECTED:
120 state = gtk.STATE_SELECTED
121 elif flags & gtk.CELL_RENDERER_PRELIT:
122 state = gtk.STATE_PRELIGHT
123 else:
124 state = gtk.STATE_NORMAL
126 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
127 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
128 cell_area.x,
129 cell_area.y + image_y)
131 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
133 widget.style.paint_layout(window, state, True,
134 expose_area, widget, "cellrenderertext",
135 cell_area.x + CELL_TEXT_INDENT,
136 cell_area.y + text_y,
137 layout)
139 if gtk.pygtk_version < (2, 8, 0):
140 # Note sure exactly which versions need this.
141 # 2.8.0 gives a warning if you include it, though.
142 gobject.type_register(IconAndTextRenderer)
143 gobject.type_register(MenuIconRenderer)
145 class InterfaceBrowser:
146 model = None
147 root = None
148 cached_icon = None
149 policy = None
150 original_implementation = None
151 update_icons = False
153 INTERFACE = 0
154 INTERFACE_NAME = 1
155 VERSION = 2
156 SUMMARY = 3
157 DOWNLOAD_SIZE = 4
158 ICON = 5
160 columns = [(_('Component'), INTERFACE_NAME),
161 (_('Version'), VERSION),
162 (_('Fetch'), DOWNLOAD_SIZE),
163 (_('Description'), SUMMARY),
164 ('', None)]
166 def __init__(self, policy, widgets):
167 tips = InterfaceTips(self)
169 tree_view = widgets.get_widget('components')
171 self.policy = policy
172 self.cached_icon = {} # URI -> GdkPixbuf
173 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
174 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
176 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
177 self.tree_view = tree_view
178 tree_view.set_model(self.model)
180 column_objects = []
182 text = gtk.CellRendererText()
184 for name, model_column in self.columns:
185 if model_column == InterfaceBrowser.INTERFACE_NAME:
186 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
187 text = model_column,
188 image = InterfaceBrowser.ICON)
189 elif model_column == None:
190 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
191 else:
192 if model_column == InterfaceBrowser.SUMMARY:
193 text_ellip = gtk.CellRendererText()
194 try:
195 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
196 except:
197 pass
198 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
199 column.set_expand(True)
200 else:
201 column = gtk.TreeViewColumn(name, text, text = model_column)
202 tree_view.append_column(column)
203 column_objects.append(column)
205 tree_view.set_enable_search(True)
207 selection = tree_view.get_selection()
209 def motion(tree_view, ev):
210 if ev.window is not tree_view.get_bin_window():
211 return False
212 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
213 if pos:
214 path = pos[0]
215 try:
216 col_index = column_objects.index(pos[1])
217 except ValueError:
218 tips.hide()
219 else:
220 col = self.columns[col_index][1]
221 row = self.model[path]
222 item = (row[InterfaceBrowser.INTERFACE], col)
223 if item != tips.item:
224 tips.prime(tree_view, item)
225 else:
226 tips.hide()
228 tree_view.connect('motion-notify-event', motion)
229 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
231 def button_press(tree_view, bev):
232 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
233 if not pos:
234 return False
235 path, col, x, y = pos
237 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
238 and bev.type == gtk.gdk.BUTTON_PRESS:
239 selection.select_path(path)
240 iface = self.model[path][InterfaceBrowser.INTERFACE]
241 self.show_popup_menu(iface, bev)
242 return True
243 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
244 return False
245 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
246 tree_view.connect('button-press-event', button_press)
248 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
249 policy.watchers.append(self.build_tree)
251 def set_root(self, root):
252 assert isinstance(root, model.Interface)
253 self.root = root
255 def set_update_icons(self, update_icons):
256 if update_icons:
257 # Clear icons cache to make sure they're really updated
258 self.cached_icon = {}
259 self.update_icons = update_icons
261 def get_icon(self, iface):
262 """Get an icon for this interface. If the icon is in the cache, use that.
263 If not, start a download. If we already started a download (successful or
264 not) do nothing. Returns None if no icon is currently available."""
265 try:
266 # Try the in-memory cache
267 return self.cached_icon[iface.uri]
268 except KeyError:
269 # Try the on-disk cache
270 iconpath = iface_cache.get_icon_path(iface)
272 if iconpath:
273 icon = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
274 # (if icon is None, cache the fact that we can't load it)
275 self.cached_icon[iface.uri] = icon
276 else:
277 icon = None
279 # Download a new icon if we don't have one, or if the
280 # user did a 'Refresh'
281 if iconpath is None or self.update_icons:
282 fetcher = self.policy.download_icon(iface)
283 if fetcher:
284 if iface.uri not in self.cached_icon:
285 self.cached_icon[iface.uri] = None # Only try once
287 @tasks.async
288 def update_display():
289 yield fetcher
290 try:
291 tasks.check(fetcher)
292 # Try to insert new icon into the cache
293 # If it fails, we'll be left with None in the cached_icon so
294 # we don't try again.
295 iconpath = iface_cache.get_icon_path(iface)
296 if iconpath:
297 self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
298 self.build_tree()
299 else:
300 warn("Failed to download icon for '%s'", iface)
301 except Exception, ex:
302 import traceback
303 traceback.print_exc()
304 self.policy.handler.report_error(ex)
305 update_display()
306 # elif fetcher is None: don't store anything in cached_icon
308 # Note: if no icon is available for downloading,
309 # more attempts are made later.
310 # It can happen that no icon is yet available because
311 # the interface was not downloaded yet, in which case
312 # it's desireable to try again once the interface is available
313 return icon
315 return None
317 def build_tree(self):
318 if self.original_implementation is None:
319 self.set_original_implementations()
321 done = {} # Detect cycles
323 self.model.clear()
324 parent = None
325 def add_node(parent, iface):
326 if iface in done:
327 return
328 done[iface] = True
330 iter = self.model.append(parent)
331 self.model[iter][InterfaceBrowser.INTERFACE] = iface
332 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
333 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
334 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
336 impl = self.policy.implementation.get(iface, None)
337 if impl:
338 old_impl = self.original_implementation.get(iface, None)
339 version_str = impl.get_version()
340 if old_impl is not None and old_impl.id != impl.id:
341 version_str += _(' (was %s)') % old_impl.get_version()
342 self.model[iter][InterfaceBrowser.VERSION] = version_str
344 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
345 children = self.policy.solver.requires[iface]
347 for child in children:
348 if isinstance(child, model.InterfaceDependency):
349 add_node(iter, iface_cache.get_interface(child.interface))
350 else:
351 child_iter = self.model.append(parent)
352 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
353 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
354 _('Unknown dependency type : %s') % child
355 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
356 else:
357 self.model[iter][InterfaceBrowser.VERSION] = _('(choose)')
358 add_node(None, self.root)
359 self.tree_view.expand_all()
361 def show_popup_menu(self, iface, bev):
362 import bugs
363 import compile
365 have_source = properties.have_source_for(self.policy, iface)
367 menu = gtk.Menu()
368 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
369 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
370 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
371 item = gtk.MenuItem(label)
372 if cb:
373 item.connect('activate', lambda item, cb=cb: cb())
374 else:
375 item.set_sensitive(False)
376 item.show()
377 menu.append(item)
379 item = gtk.MenuItem(_('Compile'))
380 item.show()
381 menu.append(item)
382 if have_source:
383 compile_menu = gtk.Menu()
384 item.set_submenu(compile_menu)
386 item = gtk.MenuItem(_('Automatic'))
387 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = True))
388 item.show()
389 compile_menu.append(item)
391 item = gtk.MenuItem(_('Manual...'))
392 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = False))
393 item.show()
394 compile_menu.append(item)
395 else:
396 item.set_sensitive(False)
398 menu.popup(None, None, None, bev.button, bev.time)
400 def set_original_implementations(self):
401 assert self.original_implementation is None
402 self.original_implementation = self.policy.implementation.copy()
404 def update_download_status(self):
405 """Called at regular intervals while there are downloads in progress,
406 and once at the end. Also called when things are added to the store.
407 Update the TreeView with the interfaces."""
408 hints = {}
409 for dl in self.policy.handler.monitored_downloads.values():
410 if dl.hint:
411 if dl.hint not in hints:
412 hints[dl.hint] = []
413 hints[dl.hint].append(dl)
415 selections = self.policy.solver.selections
417 def walk(it):
418 while it:
419 yield self.model[it]
420 for x in walk(self.model.iter_children(it)): yield x
421 it = self.model.iter_next(it)
423 for row in walk(self.model.get_iter_root()):
424 iface = row[InterfaceBrowser.INTERFACE]
426 # Is this interface the download's hint?
427 downloads = hints.get(iface, []) # The interface itself
428 downloads += hints.get(iface.uri, []) # The main feed
429 for feed in self.policy.usable_feeds(iface):
430 downloads += hints.get(feed.uri, []) # Other feeds
431 impl = selections.get(iface, None)
432 if impl:
433 downloads += hints.get(impl, []) # The chosen implementation
435 if downloads:
436 so_far = 0
437 expected = None
438 for dl in downloads:
439 if dl.expected_size:
440 expected = (expected or 0) + dl.expected_size
441 so_far += dl.get_bytes_downloaded_so_far()
442 if expected:
443 summary = ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
444 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
445 downloads)
446 values_dict = {'downloaded': pretty_size(so_far), 'expected': pretty_size(expected), 'percentage': 100 * so_far / float(expected), 'number': len(downloads)}
447 else:
448 summary = ngettext("(downloading %(downloaded)s/unknown)",
449 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
450 downloads)
451 values_dict = {'downloaded': pretty_size(so_far), 'number': len(downloads)}
452 row[InterfaceBrowser.SUMMARY] = summary % values_dict
453 else:
454 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
455 row[InterfaceBrowser.SUMMARY] = iface.summary