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