Make more strings translatable
[zeroinstall/solver.git] / zeroinstall / 0launch-gui / iface_browser.py
blob720d59d65a3abaf34a8eb3e3f531f130ab5af2e6
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 _("%s (was %s)") % (impl.user_stability, impl.upstream_stability)
21 ICON_SIZE = 20.0
22 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
24 class InterfaceTips(TreeTips):
25 mainwindow = None
27 def __init__(self, mainwindow):
28 self.mainwindow = mainwindow
30 def get_tooltip_text(self):
31 interface, model_column = self.item
32 assert interface
33 if model_column == InterfaceBrowser.INTERFACE_NAME:
34 return _("Full name: %s") % interface.uri
35 elif model_column == InterfaceBrowser.SUMMARY:
36 if not interface.description:
37 return None
38 first_para = interface.description.split('\n\n', 1)[0]
39 return first_para.replace('\n', ' ')
40 elif model_column is None:
41 return _("Click here for more options...")
43 impl = self.mainwindow.policy.implementation.get(interface, None)
44 if not impl:
45 return _("No suitable implementation was found. Check the "
46 "interface properties to find out why.")
48 if model_column == InterfaceBrowser.VERSION:
49 text = _("Currently preferred version: %s (%s)") % \
50 (impl.get_version(), _stability(impl))
51 old_impl = self.mainwindow.original_implementation.get(interface, None)
52 if old_impl is not None and old_impl is not impl:
53 text += _('\nPreviously preferred version: %s (%s)') % \
54 (old_impl.get_version(), _stability(old_impl))
55 return text
57 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
59 if self.mainwindow.policy.get_cached(impl):
60 return _("This version is already stored on your computer.")
61 else:
62 src = self.mainwindow.policy.fetcher.get_best_source(impl)
63 if not src:
64 return _("No downloads available!")
65 return _("Need to download %s (%s bytes)") % \
66 (support.pretty_size(src.size), src.size)
68 class MenuIconRenderer(gtk.GenericCellRenderer):
69 def __init__(self):
70 gtk.GenericCellRenderer.__init__(self)
71 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
73 def do_set_property(self, prop, value):
74 setattr(self, prop.name, value)
76 def on_get_size(self, widget, cell_area, layout = None):
77 return (0, 0, 20, 20)
79 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
80 if flags & gtk.CELL_RENDERER_PRELIT:
81 state = gtk.STATE_PRELIGHT
82 else:
83 state = gtk.STATE_NORMAL
85 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
86 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
87 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
88 gtk.ARROW_RIGHT, True,
89 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
91 class IconAndTextRenderer(gtk.GenericCellRenderer):
92 __gproperties__ = {
93 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
94 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
97 def do_set_property(self, prop, value):
98 setattr(self, prop.name, value)
100 def on_get_size(self, widget, cell_area, layout = None):
101 if not layout:
102 layout = widget.create_pango_layout(self.text)
103 a, rect = layout.get_pixel_extents()
105 pixmap_height = self.image.get_height()
107 both_height = max(rect[1] + rect[3], pixmap_height)
109 return (0, 0,
110 rect[0] + rect[2] + CELL_TEXT_INDENT,
111 both_height)
113 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
114 layout = widget.create_pango_layout(self.text)
115 a, rect = layout.get_pixel_extents()
117 if flags & gtk.CELL_RENDERER_SELECTED:
118 state = gtk.STATE_SELECTED
119 elif flags & gtk.CELL_RENDERER_PRELIT:
120 state = gtk.STATE_PRELIGHT
121 else:
122 state = gtk.STATE_NORMAL
124 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
125 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
126 cell_area.x,
127 cell_area.y + image_y)
129 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
131 widget.style.paint_layout(window, state, True,
132 expose_area, widget, "cellrenderertext",
133 cell_area.x + CELL_TEXT_INDENT,
134 cell_area.y + text_y,
135 layout)
137 if gtk.pygtk_version < (2, 8, 0):
138 # Note sure exactly which versions need this.
139 # 2.8.0 gives a warning if you include it, though.
140 gobject.type_register(IconAndTextRenderer)
141 gobject.type_register(MenuIconRenderer)
143 class InterfaceBrowser:
144 model = None
145 root = None
146 cached_icon = None
147 policy = None
148 original_implementation = None
149 update_icons = False
151 INTERFACE = 0
152 INTERFACE_NAME = 1
153 VERSION = 2
154 SUMMARY = 3
155 DOWNLOAD_SIZE = 4
156 ICON = 5
158 columns = [(_('Component'), INTERFACE_NAME),
159 (_('Version'), VERSION),
160 (_('Fetch'), DOWNLOAD_SIZE),
161 (_('Description'), SUMMARY),
162 ('', None)]
164 def __init__(self, policy, widgets):
165 tips = InterfaceTips(self)
167 tree_view = widgets.get_widget('components')
169 self.policy = policy
170 self.cached_icon = {} # URI -> GdkPixbuf
171 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
172 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
174 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
175 self.tree_view = tree_view
176 tree_view.set_model(self.model)
178 column_objects = []
180 text = gtk.CellRendererText()
182 for name, model_column in self.columns:
183 if model_column == InterfaceBrowser.INTERFACE_NAME:
184 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
185 text = model_column,
186 image = InterfaceBrowser.ICON)
187 elif model_column == None:
188 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
189 else:
190 if model_column == InterfaceBrowser.SUMMARY:
191 text_ellip = gtk.CellRendererText()
192 try:
193 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
194 except:
195 pass
196 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
197 column.set_expand(True)
198 else:
199 column = gtk.TreeViewColumn(name, text, text = model_column)
200 tree_view.append_column(column)
201 column_objects.append(column)
203 tree_view.set_enable_search(True)
205 selection = tree_view.get_selection()
207 def motion(tree_view, ev):
208 if ev.window is not tree_view.get_bin_window():
209 return False
210 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
211 if pos:
212 path = pos[0]
213 try:
214 col_index = column_objects.index(pos[1])
215 except ValueError:
216 tips.hide()
217 else:
218 col = self.columns[col_index][1]
219 row = self.model[path]
220 item = (row[InterfaceBrowser.INTERFACE], col)
221 if item != tips.item:
222 tips.prime(tree_view, item)
223 else:
224 tips.hide()
226 tree_view.connect('motion-notify-event', motion)
227 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
229 def button_press(tree_view, bev):
230 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
231 if not pos:
232 return False
233 path, col, x, y = pos
235 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
236 and bev.type == gtk.gdk.BUTTON_PRESS:
237 selection.select_path(path)
238 iface = self.model[path][InterfaceBrowser.INTERFACE]
239 self.show_popup_menu(iface, bev)
240 return True
241 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
242 return False
243 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
244 tree_view.connect('button-press-event', button_press)
246 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
247 policy.watchers.append(self.build_tree)
249 def set_root(self, root):
250 assert isinstance(root, model.Interface)
251 self.root = root
253 def set_update_icons(self, update_icons):
254 if update_icons:
255 # Clear icons cache to make sure they're really updated
256 self.cached_icon = {}
257 self.update_icons = update_icons
259 def _load_icon(self, path):
260 assert path
261 try:
262 loader = gtk.gdk.PixbufLoader('png')
263 try:
264 loader.write(file(path).read())
265 finally:
266 loader.close()
267 icon = loader.get_pixbuf()
268 assert icon, "Failed to load cached PNG icon data"
269 except Exception, ex:
270 warn("Failed to load cached PNG icon: %s", ex)
271 return None
272 w = icon.get_width()
273 h = icon.get_height()
274 scale = max(w, h, 1) / ICON_SIZE
275 icon = icon.scale_simple(int(w / scale),
276 int(h / scale),
277 gtk.gdk.INTERP_BILINEAR)
278 return icon
280 def get_icon(self, iface):
281 """Get an icon for this interface. If the icon is in the cache, use that.
282 If not, start a download. If we already started a download (successful or
283 not) do nothing. Returns None if no icon is currently available."""
284 try:
285 # Try the in-memory cache
286 return self.cached_icon[iface.uri]
287 except KeyError:
288 # Try the on-disk cache
289 iconpath = iface_cache.get_icon_path(iface)
291 if iconpath:
292 icon = self._load_icon(iconpath)
293 # (if icon is None, cache the fact that we can't load it)
294 self.cached_icon[iface.uri] = icon
295 else:
296 icon = None
298 # Download a new icon if we don't have one, or if the
299 # user did a 'Refresh'
300 if iconpath is None or self.update_icons:
301 fetcher = self.policy.download_icon(iface)
302 if fetcher:
303 if iface.uri not in self.cached_icon:
304 self.cached_icon[iface.uri] = None # Only try once
306 @tasks.async
307 def update_display():
308 yield fetcher
309 try:
310 tasks.check(fetcher)
311 # Try to insert new icon into the cache
312 # If it fails, we'll be left with None in the cached_icon so
313 # we don't try again.
314 iconpath = iface_cache.get_icon_path(iface)
315 if iconpath:
316 self.cached_icon[iface.uri] = self._load_icon(iconpath)
317 self.build_tree()
318 else:
319 warn("Failed to download icon for '%s'", iface)
320 except Exception, ex:
321 import traceback
322 traceback.print_exc()
323 self.policy.handler.report_error(ex)
324 update_display()
325 # elif fetcher is None: don't store anything in cached_icon
327 # Note: if no icon is available for downloading,
328 # more attempts are made later.
329 # It can happen that no icon is yet available because
330 # the interface was not downloaded yet, in which case
331 # it's desireable to try again once the interface is available
332 return icon
334 return None
336 def build_tree(self):
337 if self.original_implementation is None:
338 self.set_original_implementations()
340 done = {} # Detect cycles
342 self.model.clear()
343 parent = None
344 def add_node(parent, iface):
345 if iface in done:
346 return
347 done[iface] = True
349 iter = self.model.append(parent)
350 self.model[iter][InterfaceBrowser.INTERFACE] = iface
351 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
352 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
353 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
355 impl = self.policy.implementation.get(iface, None)
356 if impl:
357 old_impl = self.original_implementation.get(iface, None)
358 version_str = impl.get_version()
359 if old_impl is not None and old_impl is not impl:
360 version_str += _(' (was %s)') % old_impl.get_version()
361 self.model[iter][InterfaceBrowser.VERSION] = version_str
363 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
364 if hasattr(impl, 'requires'):
365 children = impl.requires
366 else:
367 children = impl.dependencies
369 for child in children:
370 if isinstance(child, model.InterfaceDependency):
371 add_node(iter, iface_cache.get_interface(child.interface))
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.VERSION] = _('(choose)')
380 add_node(None, self.root)
381 self.tree_view.expand_all()
383 def show_popup_menu(self, iface, bev):
384 import bugs
385 import compile
387 have_source = properties.have_source_for(self.policy, iface)
389 menu = gtk.Menu()
390 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
391 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
392 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
393 item = gtk.MenuItem(label)
394 if cb:
395 item.connect('activate', lambda item, cb=cb: cb())
396 else:
397 item.set_sensitive(False)
398 item.show()
399 menu.append(item)
401 item = gtk.MenuItem(_('Compile'))
402 item.show()
403 menu.append(item)
404 if have_source:
405 compile_menu = gtk.Menu()
406 item.set_submenu(compile_menu)
408 item = gtk.MenuItem(_('Automatic'))
409 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = True))
410 item.show()
411 compile_menu.append(item)
413 item = gtk.MenuItem(_('Manual...'))
414 item.connect('activate', lambda item: compile.compile(self.policy, iface, autocompile = False))
415 item.show()
416 compile_menu.append(item)
417 else:
418 item.set_sensitive(False)
420 menu.popup(None, None, None, bev.button, bev.time)
422 def set_original_implementations(self):
423 assert self.original_implementation is None
424 self.original_implementation = self.policy.implementation.copy()
426 def update_download_status(self):
427 """Called at regular intervals while there are downloads in progress,
428 and once at the end. Also called when things are added to the store.
429 Update the TreeView with the interfaces."""
430 hints = {}
431 for dl in self.policy.handler.monitored_downloads.values():
432 if dl.hint:
433 if dl.hint not in hints:
434 hints[dl.hint] = []
435 hints[dl.hint].append(dl)
437 selections = self.policy.solver.selections
439 def walk(it):
440 while it:
441 yield self.model[it]
442 for x in walk(self.model.iter_children(it)): yield x
443 it = self.model.iter_next(it)
445 for row in walk(self.model.get_iter_root()):
446 iface = row[InterfaceBrowser.INTERFACE]
448 # Is this interface the download's hint?
449 downloads = hints.get(iface, []) # The interface itself
450 downloads += hints.get(iface.uri, []) # The main feed
451 for feed in iface.feeds:
452 downloads += hints.get(feed.uri, []) # Other feeds
453 impl = selections.get(iface, None)
454 if impl:
455 downloads += hints.get(impl, []) # The chosen implementation
457 if downloads:
458 so_far = 0
459 expected = None
460 for dl in downloads:
461 if dl.expected_size:
462 expected = (expected or 0) + dl.expected_size
463 so_far += dl.get_bytes_downloaded_so_far()
464 if expected:
465 fraction = "%s [%.2f%%]" % (pretty_size(expected), 100 * so_far / float(expected))
466 else:
467 fraction = _("unknown")
468 if len(downloads) > 1:
469 fraction += _(" in %d downloads") % len(downloads)
470 row[InterfaceBrowser.SUMMARY] = _("(downloading %s/%s)") % (pretty_size(so_far), fraction)
471 else:
472 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
473 row[InterfaceBrowser.SUMMARY] = iface.summary