Release 1.4
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blobc913f69aff522360ff6b2c28c6d0815ba3627f5d
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 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
161 PROBLEM = 7
163 columns = [(_('Component'), INTERFACE_NAME),
164 (_('Version'), VERSION),
165 (_('Fetch'), DOWNLOAD_SIZE),
166 (_('Description'), SUMMARY),
167 ('', None)]
169 def __init__(self, policy, widgets):
170 tree_view = widgets.get_widget('components')
171 tree_view.set_property('has-tooltip', True)
172 def callback(widget, x, y, keyboard_mode, tooltip):
173 x, y = tree_view.convert_widget_to_bin_window_coords(x, y)
174 pos = tree_view.get_path_at_pos(x, y)
175 if pos:
176 tree_view.set_tooltip_cell(tooltip, pos[0], pos[1], None)
177 path = pos[0]
178 try:
179 col_index = column_objects.index(pos[1])
180 except ValueError:
181 return False
182 else:
183 col = self.columns[col_index][1]
184 row = self.model[path]
185 iface = row[InterfaceBrowser.INTERFACE]
186 main_feed = self.policy.config.iface_cache.get_feed(iface.uri)
187 tooltip.set_text(get_tooltip_text(self, iface, main_feed, col))
188 return True
189 else:
190 return False
191 tree_view.connect('query-tooltip', callback)
193 self.policy = policy
194 self.cached_icon = {} # URI -> GdkPixbuf
195 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
196 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
198 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf, str, bool)
199 self.tree_view = tree_view
200 tree_view.set_model(self.model)
202 column_objects = []
204 text = gtk.CellRendererText()
205 coloured_text = gtk.CellRendererText()
207 for name, model_column in self.columns:
208 if model_column == InterfaceBrowser.INTERFACE_NAME:
209 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
210 text = model_column,
211 image = InterfaceBrowser.ICON)
212 elif model_column == None:
213 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
214 else:
215 if model_column == InterfaceBrowser.SUMMARY:
216 text_ellip = gtk.CellRendererText()
217 try:
218 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
219 except:
220 pass
221 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
222 column.set_expand(True)
223 elif model_column == InterfaceBrowser.VERSION:
224 column = gtk.TreeViewColumn(name, coloured_text, text = model_column,
225 background = InterfaceBrowser.BACKGROUND)
226 else:
227 column = gtk.TreeViewColumn(name, text, text = model_column)
228 tree_view.append_column(column)
229 column_objects.append(column)
231 tree_view.set_enable_search(True)
233 selection = tree_view.get_selection()
235 def button_press(tree_view, bev):
236 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
237 if not pos:
238 return False
239 path, col, x, y = pos
241 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
242 and bev.type == gtk.gdk.BUTTON_PRESS:
243 selection.select_path(path)
244 iface = self.model[path][InterfaceBrowser.INTERFACE]
245 self.show_popup_menu(iface, bev)
246 return True
247 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
248 return False
249 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE], self.compile, show_versions = True)
250 tree_view.connect('button-press-event', button_press)
252 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
253 policy.watchers.append(self.build_tree)
255 def set_root(self, root):
256 assert isinstance(root, model.Interface)
257 self.root = root
259 def set_update_icons(self, update_icons):
260 if update_icons:
261 # Clear icons cache to make sure they're really updated
262 self.cached_icon = {}
263 self.update_icons = update_icons
265 def get_icon(self, iface):
266 """Get an icon for this interface. If the icon is in the cache, use that.
267 If not, start a download. If we already started a download (successful or
268 not) do nothing. Returns None if no icon is currently available."""
269 try:
270 # Try the in-memory cache
271 return self.cached_icon[iface.uri]
272 except KeyError:
273 # Try the on-disk cache
274 iconpath = self.policy.config.iface_cache.get_icon_path(iface)
276 if iconpath:
277 icon = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
278 # (if icon is None, cache the fact that we can't load it)
279 self.cached_icon[iface.uri] = icon
280 else:
281 icon = None
283 # Download a new icon if we don't have one, or if the
284 # user did a 'Refresh'
285 if iconpath is None or self.update_icons:
286 fetcher = self.policy.download_icon(iface)
287 if fetcher:
288 if iface.uri not in self.cached_icon:
289 self.cached_icon[iface.uri] = None # Only try once
291 @tasks.async
292 def update_display():
293 yield fetcher
294 try:
295 tasks.check(fetcher)
296 # Try to insert new icon into the cache
297 # If it fails, we'll be left with None in the cached_icon so
298 # we don't try again.
299 iconpath = self.policy.config.iface_cache.get_icon_path(iface)
300 if iconpath:
301 self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
302 self.build_tree()
303 else:
304 warn("Failed to download icon for '%s'", iface)
305 except Exception as ex:
306 import traceback
307 traceback.print_exc()
308 self.policy.handler.report_error(ex)
309 update_display()
310 # elif fetcher is None: don't store anything in cached_icon
312 # Note: if no icon is available for downloading,
313 # more attempts are made later.
314 # It can happen that no icon is yet available because
315 # the interface was not downloaded yet, in which case
316 # it's desireable to try again once the interface is available
317 return icon
319 return None
321 def build_tree(self):
322 iface_cache = self.policy.config.iface_cache
324 if self.original_implementation is None:
325 self.set_original_implementations()
327 done = {} # Detect cycles
329 sels = self.policy.solver.selections
331 self.model.clear()
332 def add_node(parent, iface, commands, essential):
333 if iface in done:
334 return
335 done[iface] = True
337 main_feed = iface_cache.get_feed(iface.uri)
338 if main_feed:
339 name = main_feed.get_name()
340 summary = main_feed.summary
341 else:
342 name = iface.get_name()
343 summary = None
345 iter = self.model.append(parent)
346 self.model[iter][InterfaceBrowser.INTERFACE] = iface
347 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = name
348 self.model[iter][InterfaceBrowser.SUMMARY] = summary
349 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
350 self.model[iter][InterfaceBrowser.PROBLEM] = False
352 sel = sels.selections.get(iface.uri, None)
353 if sel:
354 impl = sel.impl
355 old_impl = self.original_implementation.get(iface, None)
356 version_str = impl.get_version()
357 if old_impl is not None and old_impl.id != impl.id:
358 version_str += _(' (was %s)') % old_impl.get_version()
359 self.model[iter][InterfaceBrowser.VERSION] = version_str
361 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
363 deps = sel.dependencies
364 for c in commands:
365 deps += sel.get_command(c).requires
366 for child in deps:
367 if isinstance(child, model.InterfaceDependency):
368 add_node(iter,
369 iface_cache.get_interface(child.interface),
370 child.get_required_commands(),
371 child.importance == model.Dependency.Essential)
372 else:
373 child_iter = self.model.append(parent)
374 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
375 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
376 _('Unknown dependency type : %s') % child
377 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
378 else:
379 self.model[iter][InterfaceBrowser.PROBLEM] = essential
380 self.model[iter][InterfaceBrowser.VERSION] = _('(problem)') if essential else _('(none)')
381 if sels.command:
382 add_node(None, self.root, [sels.command], essential = True)
383 else:
384 add_node(None, self.root, [], essential = True)
385 self.tree_view.expand_all()
387 def show_popup_menu(self, iface, bev):
388 import bugs
390 have_source = properties.have_source_for(self.policy, iface)
392 menu = gtk.Menu()
393 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface, self.compile)),
394 (_('Show Versions'), lambda: properties.edit(self.policy, iface, self.compile, show_versions = True)),
395 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
396 item = gtk.MenuItem(label)
397 if cb:
398 item.connect('activate', lambda item, cb=cb: cb())
399 else:
400 item.set_sensitive(False)
401 item.show()
402 menu.append(item)
404 item = gtk.MenuItem(_('Compile'))
405 item.show()
406 menu.append(item)
407 if have_source:
408 compile_menu = gtk.Menu()
409 item.set_submenu(compile_menu)
411 item = gtk.MenuItem(_('Automatic'))
412 item.connect('activate', lambda item: self.compile(iface, autocompile = True))
413 item.show()
414 compile_menu.append(item)
416 item = gtk.MenuItem(_('Manual...'))
417 item.connect('activate', lambda item: self.compile(iface, autocompile = False))
418 item.show()
419 compile_menu.append(item)
420 else:
421 item.set_sensitive(False)
423 menu.popup(None, None, None, bev.button, bev.time)
425 def compile(self, interface, autocompile = True):
426 import compile
427 def on_success():
428 # A new local feed may have been registered, so reload it from the disk cache
429 info(_("0compile command completed successfully. Reloading interface details."))
430 reader.update_from_cache(interface)
431 for feed in interface.extra_feeds:
432 self.policy.config.iface_cache.get_feed(feed.uri, force = True)
433 import main
434 main.recalculate()
435 compile.compile(on_success, interface.uri, autocompile = autocompile)
437 def set_original_implementations(self):
438 assert self.original_implementation is None
439 self.original_implementation = self.policy.implementation.copy()
441 def update_download_status(self, only_update_visible = False):
442 """Called at regular intervals while there are downloads in progress,
443 and once at the end. Also called when things are added to the store.
444 Update the TreeView with the interfaces."""
446 # A download may be for a feed, an interface or an implementation.
447 # Create the reverse mapping (item -> download)
448 hints = {}
449 for dl in self.policy.handler.monitored_downloads.values():
450 if dl.hint:
451 if dl.hint not in hints:
452 hints[dl.hint] = []
453 hints[dl.hint].append(dl)
455 selections = self.policy.solver.selections
457 # Only update currently visible rows
458 if only_update_visible and self.tree_view.get_visible_range() != None:
459 firstVisiblePath, lastVisiblePath = self.tree_view.get_visible_range()
460 firstVisibleIter = self.model.get_iter(firstVisiblePath)
461 else:
462 # (or should we just wait until the TreeView has settled enough to tell
463 # us what is visible?)
464 firstVisibleIter = self.model.get_iter_root()
465 lastVisiblePath = None
467 for it in walk(self.model, firstVisibleIter):
468 row = self.model[it]
469 iface = row[InterfaceBrowser.INTERFACE]
471 # Is this interface the download's hint?
472 downloads = hints.get(iface, []) # The interface itself
473 downloads += hints.get(iface.uri, []) # The main feed
474 for feed in self.policy.usable_feeds(iface):
475 downloads += hints.get(feed.uri, []) # Other feeds
476 impl = selections.get(iface, None)
477 if impl:
478 downloads += hints.get(impl, []) # The chosen implementation
480 if downloads:
481 so_far = 0
482 expected = None
483 for dl in downloads:
484 if dl.expected_size:
485 expected = (expected or 0) + dl.expected_size
486 so_far += dl.get_bytes_downloaded_so_far()
487 if expected:
488 summary = ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
489 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
490 downloads)
491 values_dict = {'downloaded': pretty_size(so_far), 'expected': pretty_size(expected), 'percentage': 100 * so_far / float(expected), 'number': len(downloads)}
492 else:
493 summary = ngettext("(downloading %(downloaded)s/unknown)",
494 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
495 downloads)
496 values_dict = {'downloaded': pretty_size(so_far), 'number': len(downloads)}
497 row[InterfaceBrowser.SUMMARY] = summary % values_dict
498 else:
499 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
500 row[InterfaceBrowser.SUMMARY] = iface.summary
502 if self.model.get_path(it) == lastVisiblePath:
503 break
505 def highlight_problems(self):
506 """Called when the solve finishes. Highlight any missing implementations."""
507 for it in walk(self.model, self.model.get_iter_root()):
508 row = self.model[it]
509 iface = row[InterfaceBrowser.INTERFACE]
510 sel = self.policy.solver.selections.selections.get(iface.uri, None)
512 if sel is None and row[InterfaceBrowser.PROBLEM]:
513 row[InterfaceBrowser.BACKGROUND] = '#f88'