Removed legacy code
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blob5ffa15e4dc8953ee007b0430b18c087fc8518669
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
9 import properties
10 from zeroinstall.gtkui.treetips import TreeTips
11 from zeroinstall import support
12 from logging import warn
13 import utils
15 def _stability(impl):
16 assert impl
17 if impl.user_stability is None:
18 return impl.upstream_stability
19 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
20 % {'implementation_user_stability': impl.user_stability, 'implementation_upstream_stability': impl.upstream_stability}
22 ICON_SIZE = 20.0
23 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
25 class InterfaceTips(TreeTips):
26 mainwindow = None
28 def __init__(self, mainwindow):
29 self.mainwindow = mainwindow
31 def get_tooltip_text(self):
32 interface, model_column = self.item
33 assert interface
34 if model_column == InterfaceBrowser.INTERFACE_NAME:
35 return _("Full name: %s") % interface.uri
36 elif model_column == InterfaceBrowser.SUMMARY:
37 if not interface.description:
38 return None
39 first_para = interface.description.split('\n\n', 1)[0]
40 return first_para.replace('\n', ' ')
41 elif model_column is None:
42 return _("Click here for more options...")
44 impl = self.mainwindow.policy.implementation.get(interface, None)
45 if not impl:
46 return _("No suitable implementation was found. Check the "
47 "interface properties to find out why.")
49 if model_column == InterfaceBrowser.VERSION:
50 text = _("Currently preferred version: %(version)s (%(stability)s)") % \
51 {'version': impl.get_version(), 'stability': _stability(impl)}
52 old_impl = self.mainwindow.original_implementation.get(interface, None)
53 if old_impl is not None and old_impl is not impl:
54 text += '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
55 {'version': old_impl.get_version(), 'stability': _stability(old_impl)}
56 return text
58 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
60 if self.mainwindow.policy.get_cached(impl):
61 return _("This version is already stored on your computer.")
62 else:
63 src = self.mainwindow.policy.fetcher.get_best_source(impl)
64 if not src:
65 return _("No downloads available!")
66 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
67 {'pretty_size': support.pretty_size(src.size), 'size': src.size}
69 class MenuIconRenderer(gtk.GenericCellRenderer):
70 def __init__(self):
71 gtk.GenericCellRenderer.__init__(self)
72 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
74 def do_set_property(self, prop, value):
75 setattr(self, prop.name, value)
77 def on_get_size(self, widget, cell_area, layout = None):
78 return (0, 0, 20, 20)
80 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
81 if flags & gtk.CELL_RENDERER_PRELIT:
82 state = gtk.STATE_PRELIGHT
83 else:
84 state = gtk.STATE_NORMAL
86 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
87 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
88 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
89 gtk.ARROW_RIGHT, True,
90 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
92 class IconAndTextRenderer(gtk.GenericCellRenderer):
93 __gproperties__ = {
94 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
95 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
98 def do_set_property(self, prop, value):
99 setattr(self, prop.name, value)
101 def on_get_size(self, widget, cell_area, layout = None):
102 if not layout:
103 layout = widget.create_pango_layout(self.text)
104 a, rect = layout.get_pixel_extents()
106 pixmap_height = self.image.get_height()
108 both_height = max(rect[1] + rect[3], pixmap_height)
110 return (0, 0,
111 rect[0] + rect[2] + CELL_TEXT_INDENT,
112 both_height)
114 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
115 layout = widget.create_pango_layout(self.text)
116 a, rect = layout.get_pixel_extents()
118 if flags & gtk.CELL_RENDERER_SELECTED:
119 state = gtk.STATE_SELECTED
120 elif flags & gtk.CELL_RENDERER_PRELIT:
121 state = gtk.STATE_PRELIGHT
122 else:
123 state = gtk.STATE_NORMAL
125 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
126 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
127 cell_area.x,
128 cell_area.y + image_y)
130 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
132 widget.style.paint_layout(window, state, True,
133 expose_area, widget, "cellrenderertext",
134 cell_area.x + CELL_TEXT_INDENT,
135 cell_area.y + text_y,
136 layout)
138 if gtk.pygtk_version < (2, 8, 0):
139 # Note sure exactly which versions need this.
140 # 2.8.0 gives a warning if you include it, though.
141 gobject.type_register(IconAndTextRenderer)
142 gobject.type_register(MenuIconRenderer)
144 class InterfaceBrowser:
145 model = None
146 root = None
147 cached_icon = None
148 policy = None
149 original_implementation = None
150 update_icons = False
152 INTERFACE = 0
153 INTERFACE_NAME = 1
154 VERSION = 2
155 SUMMARY = 3
156 DOWNLOAD_SIZE = 4
157 ICON = 5
159 columns = [(_('Component'), INTERFACE_NAME),
160 (_('Version'), VERSION),
161 (_('Fetch'), DOWNLOAD_SIZE),
162 (_('Description'), SUMMARY),
163 ('', None)]
165 def __init__(self, policy, widgets):
166 tips = InterfaceTips(self)
168 tree_view = widgets.get_widget('components')
170 self.policy = policy
171 self.cached_icon = {} # URI -> GdkPixbuf
172 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
173 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
175 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
176 self.tree_view = tree_view
177 tree_view.set_model(self.model)
179 column_objects = []
181 text = gtk.CellRendererText()
183 for name, model_column in self.columns:
184 if model_column == InterfaceBrowser.INTERFACE_NAME:
185 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
186 text = model_column,
187 image = InterfaceBrowser.ICON)
188 elif model_column == None:
189 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
190 else:
191 if model_column == InterfaceBrowser.SUMMARY:
192 text_ellip = gtk.CellRendererText()
193 try:
194 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
195 except:
196 pass
197 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
198 column.set_expand(True)
199 else:
200 column = gtk.TreeViewColumn(name, text, text = model_column)
201 tree_view.append_column(column)
202 column_objects.append(column)
204 tree_view.set_enable_search(True)
206 selection = tree_view.get_selection()
208 def motion(tree_view, ev):
209 if ev.window is not tree_view.get_bin_window():
210 return False
211 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
212 if pos:
213 path = pos[0]
214 try:
215 col_index = column_objects.index(pos[1])
216 except ValueError:
217 tips.hide()
218 else:
219 col = self.columns[col_index][1]
220 row = self.model[path]
221 item = (row[InterfaceBrowser.INTERFACE], col)
222 if item != tips.item:
223 tips.prime(tree_view, item)
224 else:
225 tips.hide()
227 tree_view.connect('motion-notify-event', motion)
228 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
230 def button_press(tree_view, bev):
231 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
232 if not pos:
233 return False
234 path, col, x, y = pos
236 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
237 and bev.type == gtk.gdk.BUTTON_PRESS:
238 selection.select_path(path)
239 iface = self.model[path][InterfaceBrowser.INTERFACE]
240 self.show_popup_menu(iface, bev)
241 return True
242 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
243 return False
244 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
245 tree_view.connect('button-press-event', button_press)
247 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
248 policy.watchers.append(self.build_tree)
250 def set_root(self, root):
251 assert isinstance(root, model.Interface)
252 self.root = root
254 def set_update_icons(self, update_icons):
255 if update_icons:
256 # Clear icons cache to make sure they're really updated
257 self.cached_icon = {}
258 self.update_icons = update_icons
260 def _load_icon(self, path):
261 assert path
262 try:
263 loader = gtk.gdk.PixbufLoader('png')
264 try:
265 loader.write(file(path).read())
266 finally:
267 loader.close()
268 icon = loader.get_pixbuf()
269 assert icon, "Failed to load cached PNG icon data"
270 except Exception, ex:
271 warn(_("Failed to load cached PNG icon: %s"), ex)
272 return None
273 w = icon.get_width()
274 h = icon.get_height()
275 scale = max(w, h, 1) / ICON_SIZE
276 icon = icon.scale_simple(int(w / scale),
277 int(h / scale),
278 gtk.gdk.INTERP_BILINEAR)
279 return icon
281 def get_icon(self, iface):
282 """Get an icon for this interface. If the icon is in the cache, use that.
283 If not, start a download. If we already started a download (successful or
284 not) do nothing. Returns None if no icon is currently available."""
285 try:
286 # Try the in-memory cache
287 return self.cached_icon[iface.uri]
288 except KeyError:
289 # Try the on-disk cache
290 iconpath = iface_cache.get_icon_path(iface)
292 if iconpath:
293 icon = self._load_icon(iconpath)
294 # (if icon is None, cache the fact that we can't load it)
295 self.cached_icon[iface.uri] = icon
296 else:
297 icon = None
299 # Download a new icon if we don't have one, or if the
300 # user did a 'Refresh'
301 if iconpath is None or self.update_icons:
302 fetcher = self.policy.download_icon(iface)
303 if fetcher:
304 if iface.uri not in self.cached_icon:
305 self.cached_icon[iface.uri] = None # Only try once
307 @tasks.async
308 def update_display():
309 yield fetcher
310 try:
311 tasks.check(fetcher)
312 # Try to insert new icon into the cache
313 # If it fails, we'll be left with None in the cached_icon so
314 # we don't try again.
315 iconpath = iface_cache.get_icon_path(iface)
316 if iconpath:
317 self.cached_icon[iface.uri] = self._load_icon(iconpath)
318 self.build_tree()
319 else:
320 warn("Failed to download icon for '%s'", iface)
321 except Exception, ex:
322 import traceback
323 traceback.print_exc()
324 self.policy.handler.report_error(ex)
325 update_display()
326 # elif fetcher is None: don't store anything in cached_icon
328 # Note: if no icon is available for downloading,
329 # more attempts are made later.
330 # It can happen that no icon is yet available because
331 # the interface was not downloaded yet, in which case
332 # it's desireable to try again once the interface is available
333 return icon
335 return None
337 def build_tree(self):
338 if self.original_implementation is None:
339 self.set_original_implementations()
341 done = {} # Detect cycles
343 self.model.clear()
344 parent = None
345 def add_node(parent, iface):
346 if iface in done:
347 return
348 done[iface] = True
350 iter = self.model.append(parent)
351 self.model[iter][InterfaceBrowser.INTERFACE] = iface
352 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
353 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
354 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
356 impl = self.policy.implementation.get(iface, None)
357 if impl:
358 old_impl = self.original_implementation.get(iface, None)
359 version_str = impl.get_version()
360 if old_impl is not None and old_impl is not impl:
361 version_str += _(' (was %s)') % old_impl.get_version()
362 self.model[iter][InterfaceBrowser.VERSION] = version_str
364 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
365 children = self.policy.solver.requires[iface]
367 for child in children:
368 if isinstance(child, model.InterfaceDependency):
369 add_node(iter, iface_cache.get_interface(child.interface))
370 else:
371 child_iter = self.model.append(parent)
372 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
373 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
374 _('Unknown dependency type : %s') % child
375 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
376 else:
377 self.model[iter][InterfaceBrowser.VERSION] = _('(choose)')
378 add_node(None, self.root)
379 self.tree_view.expand_all()
381 def show_popup_menu(self, iface, bev):
382 import bugs
383 import compile
385 have_source = properties.have_source_for(self.policy, iface)
387 menu = gtk.Menu()
388 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
389 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
390 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
391 item = gtk.MenuItem(label)
392 if cb:
393 item.connect('activate', lambda item, cb=cb: cb())
394 else:
395 item.set_sensitive(False)
396 item.show()
397 menu.append(item)
399 item = gtk.MenuItem(_('Compile'))
400 item.show()
401 menu.append(item)
402 if have_source:
403 compile_menu = gtk.Menu()
404 item.set_submenu(compile_menu)
406 item = gtk.MenuItem(_('Automatic'))
407 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = True))
408 item.show()
409 compile_menu.append(item)
411 item = gtk.MenuItem(_('Manual...'))
412 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = False))
413 item.show()
414 compile_menu.append(item)
415 else:
416 item.set_sensitive(False)
418 menu.popup(None, None, None, bev.button, bev.time)
420 def set_original_implementations(self):
421 assert self.original_implementation is None
422 self.original_implementation = self.policy.implementation.copy()
424 def update_download_status(self):
425 """Called at regular intervals while there are downloads in progress,
426 and once at the end. Also called when things are added to the store.
427 Update the TreeView with the interfaces."""
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 iface.feeds:
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