Fixed GUI to display recursive runners
[zeroinstall/zeroinstall-limyreth.git] / zeroinstall / 0launch-gui / iface_browser.py
blobb23db9e8fd9c5ca2a7d03a2311053b606ca98095
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, reader
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, info
14 import utils
16 def _stability(impl):
17 assert impl
18 if impl.user_stability is None:
19 return _(str(impl.upstream_stability))
20 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
21 % {'implementation_user_stability': _(str(impl.user_stability)),
22 'implementation_upstream_stability': _(str(impl.upstream_stability))}
24 ICON_SIZE = 20.0
25 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
27 class InterfaceTips(TreeTips):
28 mainwindow = None
30 def __init__(self, mainwindow):
31 self.mainwindow = mainwindow
33 def get_tooltip_text(self):
34 interface, model_column = self.item
35 assert interface
36 if model_column == InterfaceBrowser.INTERFACE_NAME:
37 return _("Full name: %s") % interface.uri
38 elif model_column == InterfaceBrowser.SUMMARY:
39 if not interface.description:
40 return None
41 first_para = interface.description.split('\n\n', 1)[0]
42 return first_para.replace('\n', ' ')
43 elif model_column is None:
44 return _("Click here for more options...")
46 impl = self.mainwindow.policy.implementation.get(interface, None)
47 if not impl:
48 return _("No suitable implementation was found. Check the "
49 "interface properties to find out why.")
51 if model_column == InterfaceBrowser.VERSION:
52 text = _("Currently preferred version: %(version)s (%(stability)s)") % \
53 {'version': impl.get_version(), 'stability': _stability(impl)}
54 old_impl = self.mainwindow.original_implementation.get(interface, None)
55 if old_impl is not None and old_impl is not impl:
56 text += '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
57 {'version': old_impl.get_version(), 'stability': _stability(old_impl)}
58 return text
60 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
62 if self.mainwindow.policy.get_cached(impl):
63 return _("This version is already stored on your computer.")
64 else:
65 src = self.mainwindow.policy.fetcher.get_best_source(impl)
66 if not src:
67 return _("No downloads available!")
68 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
69 {'pretty_size': support.pretty_size(src.size), 'size': src.size}
71 class MenuIconRenderer(gtk.GenericCellRenderer):
72 def __init__(self):
73 gtk.GenericCellRenderer.__init__(self)
74 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
76 def do_set_property(self, prop, value):
77 setattr(self, prop.name, value)
79 def on_get_size(self, widget, cell_area, layout = None):
80 return (0, 0, 20, 20)
82 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
83 if flags & gtk.CELL_RENDERER_PRELIT:
84 state = gtk.STATE_PRELIGHT
85 else:
86 state = gtk.STATE_NORMAL
88 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
89 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
90 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
91 gtk.ARROW_RIGHT, True,
92 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
94 class IconAndTextRenderer(gtk.GenericCellRenderer):
95 __gproperties__ = {
96 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
97 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
100 def do_set_property(self, prop, value):
101 setattr(self, prop.name, value)
103 def on_get_size(self, widget, cell_area, layout = None):
104 if not layout:
105 layout = widget.create_pango_layout(self.text)
106 a, rect = layout.get_pixel_extents()
108 pixmap_height = self.image.get_height()
110 both_height = max(rect[1] + rect[3], pixmap_height)
112 return (0, 0,
113 rect[0] + rect[2] + CELL_TEXT_INDENT,
114 both_height)
116 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
117 layout = widget.create_pango_layout(self.text)
118 a, rect = layout.get_pixel_extents()
120 if flags & gtk.CELL_RENDERER_SELECTED:
121 state = gtk.STATE_SELECTED
122 elif flags & gtk.CELL_RENDERER_PRELIT:
123 state = gtk.STATE_PRELIGHT
124 else:
125 state = gtk.STATE_NORMAL
127 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
128 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
129 cell_area.x,
130 cell_area.y + image_y)
132 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
134 widget.style.paint_layout(window, state, True,
135 expose_area, widget, "cellrenderertext",
136 cell_area.x + CELL_TEXT_INDENT,
137 cell_area.y + text_y,
138 layout)
140 if gtk.pygtk_version < (2, 8, 0):
141 # Note sure exactly which versions need this.
142 # 2.8.0 gives a warning if you include it, though.
143 gobject.type_register(IconAndTextRenderer)
144 gobject.type_register(MenuIconRenderer)
146 class InterfaceBrowser:
147 model = None
148 root = None
149 cached_icon = None
150 policy = None
151 original_implementation = None
152 update_icons = False
154 INTERFACE = 0
155 INTERFACE_NAME = 1
156 VERSION = 2
157 SUMMARY = 3
158 DOWNLOAD_SIZE = 4
159 ICON = 5
161 columns = [(_('Component'), INTERFACE_NAME),
162 (_('Version'), VERSION),
163 (_('Fetch'), DOWNLOAD_SIZE),
164 (_('Description'), SUMMARY),
165 ('', None)]
167 def __init__(self, policy, widgets):
168 tips = InterfaceTips(self)
170 tree_view = widgets.get_widget('components')
172 self.policy = policy
173 self.cached_icon = {} # URI -> GdkPixbuf
174 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
175 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
177 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
178 self.tree_view = tree_view
179 tree_view.set_model(self.model)
181 column_objects = []
183 text = gtk.CellRendererText()
185 for name, model_column in self.columns:
186 if model_column == InterfaceBrowser.INTERFACE_NAME:
187 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
188 text = model_column,
189 image = InterfaceBrowser.ICON)
190 elif model_column == None:
191 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
192 else:
193 if model_column == InterfaceBrowser.SUMMARY:
194 text_ellip = gtk.CellRendererText()
195 try:
196 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
197 except:
198 pass
199 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
200 column.set_expand(True)
201 else:
202 column = gtk.TreeViewColumn(name, text, text = model_column)
203 tree_view.append_column(column)
204 column_objects.append(column)
206 tree_view.set_enable_search(True)
208 selection = tree_view.get_selection()
210 def motion(tree_view, ev):
211 if ev.window is not tree_view.get_bin_window():
212 return False
213 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
214 if pos:
215 path = pos[0]
216 try:
217 col_index = column_objects.index(pos[1])
218 except ValueError:
219 tips.hide()
220 else:
221 col = self.columns[col_index][1]
222 row = self.model[path]
223 item = (row[InterfaceBrowser.INTERFACE], col)
224 if item != tips.item:
225 tips.prime(tree_view, item)
226 else:
227 tips.hide()
229 tree_view.connect('motion-notify-event', motion)
230 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
232 def button_press(tree_view, bev):
233 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
234 if not pos:
235 return False
236 path, col, x, y = pos
238 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
239 and bev.type == gtk.gdk.BUTTON_PRESS:
240 selection.select_path(path)
241 iface = self.model[path][InterfaceBrowser.INTERFACE]
242 self.show_popup_menu(iface, bev)
243 return True
244 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
245 return False
246 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE], self.compile)
247 tree_view.connect('button-press-event', button_press)
249 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
250 policy.watchers.append(self.build_tree)
252 def set_root(self, root):
253 assert isinstance(root, model.Interface)
254 self.root = root
256 def set_update_icons(self, update_icons):
257 if update_icons:
258 # Clear icons cache to make sure they're really updated
259 self.cached_icon = {}
260 self.update_icons = update_icons
262 def get_icon(self, iface):
263 """Get an icon for this interface. If the icon is in the cache, use that.
264 If not, start a download. If we already started a download (successful or
265 not) do nothing. Returns None if no icon is currently available."""
266 try:
267 # Try the in-memory cache
268 return self.cached_icon[iface.uri]
269 except KeyError:
270 # Try the on-disk cache
271 iconpath = iface_cache.get_icon_path(iface)
273 if iconpath:
274 icon = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
275 # (if icon is None, cache the fact that we can't load it)
276 self.cached_icon[iface.uri] = icon
277 else:
278 icon = None
280 # Download a new icon if we don't have one, or if the
281 # user did a 'Refresh'
282 if iconpath is None or self.update_icons:
283 fetcher = self.policy.download_icon(iface)
284 if fetcher:
285 if iface.uri not in self.cached_icon:
286 self.cached_icon[iface.uri] = None # Only try once
288 @tasks.async
289 def update_display():
290 yield fetcher
291 try:
292 tasks.check(fetcher)
293 # Try to insert new icon into the cache
294 # If it fails, we'll be left with None in the cached_icon so
295 # we don't try again.
296 iconpath = iface_cache.get_icon_path(iface)
297 if iconpath:
298 self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
299 self.build_tree()
300 else:
301 warn("Failed to download icon for '%s'", iface)
302 except Exception, ex:
303 import traceback
304 traceback.print_exc()
305 self.policy.handler.report_error(ex)
306 update_display()
307 # elif fetcher is None: don't store anything in cached_icon
309 # Note: if no icon is available for downloading,
310 # more attempts are made later.
311 # It can happen that no icon is yet available because
312 # the interface was not downloaded yet, in which case
313 # it's desireable to try again once the interface is available
314 return icon
316 return None
318 def build_tree(self):
319 if self.original_implementation is None:
320 self.set_original_implementations()
322 done = {} # Detect cycles
324 self.model.clear()
325 parent = None
326 commands = self.policy.solver.selections.commands
327 def add_node(parent, iface, command):
328 # (command is the index into commands, if any)
329 if iface in done:
330 return
331 done[iface] = True
333 iter = self.model.append(parent)
334 self.model[iter][InterfaceBrowser.INTERFACE] = iface
335 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
336 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
337 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
339 sel = self.policy.solver.selections.selections.get(iface.uri, None)
340 if sel:
341 impl = sel.impl
342 old_impl = self.original_implementation.get(iface, None)
343 version_str = impl.get_version()
344 if old_impl is not None and old_impl.id != impl.id:
345 version_str += _(' (was %s)') % old_impl.get_version()
346 self.model[iter][InterfaceBrowser.VERSION] = version_str
348 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
350 deps = sel.dependencies
351 if command is not None:
352 deps += commands[command].requires
353 for child in deps:
354 if isinstance(child, model.InterfaceDependency):
355 if child.qdom.name == 'runner':
356 child_command = command + 1
357 else:
358 child_command = None
359 add_node(iter, iface_cache.get_interface(child.interface), child_command)
360 else:
361 child_iter = self.model.append(parent)
362 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
363 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
364 _('Unknown dependency type : %s') % child
365 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
366 else:
367 self.model[iter][InterfaceBrowser.VERSION] = _('(choose)')
368 if commands:
369 add_node(None, self.root, 0)
370 else:
371 # Nothing could be selected, or no command requested
372 add_node(None, self.root, None)
373 self.tree_view.expand_all()
375 def show_popup_menu(self, iface, bev):
376 import bugs
378 have_source = properties.have_source_for(self.policy, iface)
380 menu = gtk.Menu()
381 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface, self.compile)),
382 (_('Show Versions'), lambda: properties.edit(self.policy, iface, self.compile, show_versions = True)),
383 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
384 item = gtk.MenuItem(label)
385 if cb:
386 item.connect('activate', lambda item, cb=cb: cb())
387 else:
388 item.set_sensitive(False)
389 item.show()
390 menu.append(item)
392 item = gtk.MenuItem(_('Compile'))
393 item.show()
394 menu.append(item)
395 if have_source:
396 compile_menu = gtk.Menu()
397 item.set_submenu(compile_menu)
399 item = gtk.MenuItem(_('Automatic'))
400 item.connect('activate', lambda item: self.compile(iface, autocompile = True))
401 item.show()
402 compile_menu.append(item)
404 item = gtk.MenuItem(_('Manual...'))
405 item.connect('activate', lambda item: self.compile(iface, autocompile = False))
406 item.show()
407 compile_menu.append(item)
408 else:
409 item.set_sensitive(False)
411 menu.popup(None, None, None, bev.button, bev.time)
413 def compile(self, interface, autocompile = False):
414 import compile
415 def on_success():
416 # A new local feed may have been registered, so reload it from the disk cache
417 info(_("0compile command completed successfully. Reloading interface details."))
418 reader.update_from_cache(interface)
419 for feed in interface.extra_feeds:
420 iface_cache.get_feed(feed.uri, force = True)
421 self.policy.recalculate()
422 compile.compile(on_success, interface.uri, autocompile = autocompile)
424 def set_original_implementations(self):
425 assert self.original_implementation is None
426 self.original_implementation = self.policy.implementation.copy()
428 def update_download_status(self):
429 """Called at regular intervals while there are downloads in progress,
430 and once at the end. Also called when things are added to the store.
431 Update the TreeView with the interfaces."""
433 # A download may be for a feed, an interface or an implementation.
434 # Create the reverse mapping (item -> download)
435 hints = {}
436 for dl in self.policy.handler.monitored_downloads.values():
437 if dl.hint:
438 if dl.hint not in hints:
439 hints[dl.hint] = []
440 hints[dl.hint].append(dl)
442 selections = self.policy.solver.selections
444 def walk(it):
445 while it:
446 yield self.model[it]
447 for x in walk(self.model.iter_children(it)): yield x
448 it = self.model.iter_next(it)
450 for row in walk(self.model.get_iter_root()):
451 iface = row[InterfaceBrowser.INTERFACE]
453 # Is this interface the download's hint?
454 downloads = hints.get(iface, []) # The interface itself
455 downloads += hints.get(iface.uri, []) # The main feed
456 for feed in self.policy.usable_feeds(iface):
457 downloads += hints.get(feed.uri, []) # Other feeds
458 impl = selections.get(iface, None)
459 if impl:
460 downloads += hints.get(impl, []) # The chosen implementation
462 if downloads:
463 so_far = 0
464 expected = None
465 for dl in downloads:
466 if dl.expected_size:
467 expected = (expected or 0) + dl.expected_size
468 so_far += dl.get_bytes_downloaded_so_far()
469 if expected:
470 summary = ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
471 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
472 downloads)
473 values_dict = {'downloaded': pretty_size(so_far), 'expected': pretty_size(expected), 'percentage': 100 * so_far / float(expected), 'number': len(downloads)}
474 else:
475 summary = ngettext("(downloading %(downloaded)s/unknown)",
476 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
477 downloads)
478 values_dict = {'downloaded': pretty_size(so_far), 'number': len(downloads)}
479 row[InterfaceBrowser.SUMMARY] = summary % values_dict
480 else:
481 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
482 row[InterfaceBrowser.SUMMARY] = iface.summary