Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / 0launch-gui / iface_browser.py
blob8cfe646661063a5ef145101e59aa1f0a93396acb
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
150 INTERFACE = 0
151 INTERFACE_NAME = 1
152 VERSION = 2
153 SUMMARY = 3
154 DOWNLOAD_SIZE = 4
155 ICON = 5
157 columns = [(_('Component'), INTERFACE_NAME),
158 (_('Version'), VERSION),
159 (_('Fetch'), DOWNLOAD_SIZE),
160 (_('Description'), SUMMARY),
161 ('', None)]
163 def __init__(self, policy, widgets):
164 tips = InterfaceTips(self)
166 tree_view = widgets.get_widget('components')
168 self.policy = policy
169 self.cached_icon = {} # URI -> GdkPixbuf
170 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
171 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
173 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
174 self.tree_view = tree_view
175 tree_view.set_model(self.model)
177 column_objects = []
179 text = gtk.CellRendererText()
181 for name, model_column in self.columns:
182 if model_column == InterfaceBrowser.INTERFACE_NAME:
183 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
184 text = model_column,
185 image = InterfaceBrowser.ICON)
186 elif model_column == None:
187 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
188 else:
189 if model_column == InterfaceBrowser.SUMMARY:
190 text_ellip = gtk.CellRendererText()
191 try:
192 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
193 except:
194 pass
195 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
196 column.set_expand(True)
197 else:
198 column = gtk.TreeViewColumn(name, text, text = model_column)
199 tree_view.append_column(column)
200 column_objects.append(column)
202 tree_view.set_enable_search(True)
204 selection = tree_view.get_selection()
206 def motion(tree_view, ev):
207 if ev.window is not tree_view.get_bin_window():
208 return False
209 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
210 if pos:
211 path = pos[0]
212 try:
213 col_index = column_objects.index(pos[1])
214 except ValueError:
215 tips.hide()
216 else:
217 col = self.columns[col_index][1]
218 row = self.model[path]
219 item = (row[InterfaceBrowser.INTERFACE], col)
220 if item != tips.item:
221 tips.prime(tree_view, item)
222 else:
223 tips.hide()
225 tree_view.connect('motion-notify-event', motion)
226 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
228 def button_press(tree_view, bev):
229 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
230 if not pos:
231 return False
232 path, col, x, y = pos
234 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
235 and bev.type == gtk.gdk.BUTTON_PRESS:
236 selection.select_path(path)
237 iface = self.model[path][InterfaceBrowser.INTERFACE]
238 self.show_popup_menu(iface, bev)
239 return True
240 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
241 return False
242 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
243 tree_view.connect('button-press-event', button_press)
245 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
246 policy.watchers.append(self.build_tree)
248 def set_root(self, root):
249 assert isinstance(root, model.Interface)
250 self.root = root
252 def _get_icon_from_cache(self, iface):
253 path = iface_cache.get_icon_path(iface)
254 if path:
255 try:
256 loader = gtk.gdk.PixbufLoader('png')
257 try:
258 loader.write(file(path).read())
259 finally:
260 loader.close()
261 icon = loader.get_pixbuf()
262 assert icon, "Failed to load cached PNG icon data"
263 except Exception, ex:
264 warn("Failed to load cached PNG icon: %s", ex)
265 return None
266 w = icon.get_width()
267 h = icon.get_height()
268 scale = max(w, h, 1) / ICON_SIZE
269 icon = icon.scale_simple(int(w / scale),
270 int(h / scale),
271 gtk.gdk.INTERP_BILINEAR)
272 self.cached_icon[iface.uri] = icon
273 return icon
274 else:
275 return None
277 def get_icon(self, iface):
278 """Get an icon for this interface. If the icon is in the cache, use that.
279 If not, start a download. If we already started a download (successful or
280 not) do nothing. Returns None if no icon is currently available."""
281 try:
282 return self.cached_icon[iface.uri]
283 except KeyError:
284 icon = self._get_icon_from_cache(iface)
285 if icon:
286 return icon
287 else:
288 # Try to download the icon
289 fetcher = self.policy.download_icon(iface)
290 if fetcher:
291 self.cached_icon[iface.uri] = None # Only try once
292 @tasks.async
293 def update_display():
294 yield fetcher
295 try:
296 tasks.check(fetcher)
297 # Try to insert new icon into the cache
298 # If it fails, we'll be left with None in the cached_icon so
299 # we don't try again.
300 self._get_icon_from_cache(iface)
301 self.build_tree()
302 except Exception, ex:
303 import traceback
304 traceback.print_exc()
305 self.policy.handler.report_error(ex)
306 update_display()
307 # Note: if no icon is available for downloading,
308 # more attempts are made later.
309 # It can happen that no icon is yet available because
310 # the interface was not downloaded yet, in which case
311 # it's desireable to try again once the interface is available
313 return None
315 def build_tree(self):
316 if self.original_implementation is None:
317 self.set_original_implementations()
319 done = {} # Detect cycles
321 self.model.clear()
322 parent = None
323 def add_node(parent, iface):
324 if iface in done:
325 return
326 done[iface] = True
328 iter = self.model.append(parent)
329 self.model[iter][InterfaceBrowser.INTERFACE] = iface
330 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
331 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
332 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
334 impl = self.policy.implementation.get(iface, None)
335 if impl:
336 old_impl = self.original_implementation.get(iface, None)
337 version_str = impl.get_version()
338 if old_impl is not None and old_impl is not impl:
339 version_str += _(' (was %s)') % old_impl.get_version()
340 self.model[iter][InterfaceBrowser.VERSION] = version_str
342 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
343 if hasattr(impl, 'requires'):
344 children = impl.requires
345 else:
346 children = impl.dependencies
348 for child in children:
349 if isinstance(child, model.InterfaceDependency):
350 add_node(iter, iface_cache.get_interface(child.interface))
351 else:
352 child_iter = self.model.append(parent)
353 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
354 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
355 _('Unknown dependency type : %s') % child
356 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
357 else:
358 self.model[iter][InterfaceBrowser.VERSION] = _('(choose)')
359 add_node(None, self.root)
360 self.tree_view.expand_all()
362 def show_popup_menu(self, iface, bev):
363 import bugs
365 if properties.have_source_for(self.policy, iface):
366 def compile_cb():
367 import compile
368 compile.compile(self.policy, iface)
369 else:
370 compile_cb = None
372 menu = gtk.Menu()
373 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
374 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
375 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface)),
376 (_('Compile...'), compile_cb)]:
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)
384 menu.popup(None, None, None, bev.button, bev.time)
386 def set_original_implementations(self):
387 assert self.original_implementation is None
388 self.original_implementation = self.policy.implementation.copy()
390 def update_download_status(self):
391 """Called at regular intervals while there are downloads in progress,
392 and once at the end. Also called when things are added to the store.
393 Update the TreeView with the interfaces."""
394 hints = {}
395 for dl in self.policy.handler.monitored_downloads.values():
396 if dl.hint:
397 if dl.hint not in hints:
398 hints[dl.hint] = []
399 hints[dl.hint].append(dl)
401 selections = self.policy.solver.selections
403 def walk(it):
404 while it:
405 yield self.model[it]
406 for x in walk(self.model.iter_children(it)): yield x
407 it = self.model.iter_next(it)
409 for row in walk(self.model.get_iter_root()):
410 iface = row[InterfaceBrowser.INTERFACE]
412 # Is this interface the download's hint?
413 downloads = hints.get(iface, []) # The interface itself
414 downloads += hints.get(iface.uri, []) # The main feed
415 for feed in iface.feeds:
416 downloads += hints.get(feed.uri, []) # Other feeds
417 impl = selections.get(iface, None)
418 if impl:
419 downloads += hints.get(impl, []) # The chosen implementation
421 if downloads:
422 so_far = 0
423 expected = None
424 for dl in downloads:
425 if dl.expected_size:
426 expected = (expected or 0) + dl.expected_size
427 so_far += dl.get_bytes_downloaded_so_far()
428 if expected:
429 fraction = "%s [%.2f%%]" % (pretty_size(expected), 100 * so_far / float(expected))
430 else:
431 fraction = _("unknown")
432 if len(downloads) > 1:
433 fraction += _(" in %d downloads") % len(downloads)
434 row[InterfaceBrowser.SUMMARY] = _("(downloading %s/%s)") % (pretty_size(so_far), fraction)
435 else:
436 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
437 row[InterfaceBrowser.SUMMARY] = iface.summary