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