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