Only show two decimal places for download progress percentages.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / 0launch-gui / iface_browser.py
blob18bcee3e79847e1f6161f8888b2705bf976c9aa0
1 import gtk, gobject
3 from zeroinstall.support import basedir, tasks, pretty_size
4 from zeroinstall.injector.iface_cache import iface_cache
5 from zeroinstall.injector import model
6 import properties
7 from treetips import TreeTips
8 from zeroinstall import support
9 from logging import warn
11 def _stability(impl):
12 assert impl
13 if impl.user_stability is None:
14 return impl.upstream_stability
15 return _("%s (was %s)") % (impl.user_stability, impl.upstream_stability)
17 ICON_SIZE = 20.0
18 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
20 class InterfaceTips(TreeTips):
21 mainwindow = None
23 def __init__(self, mainwindow):
24 self.mainwindow = mainwindow
26 def get_tooltip_text(self, item):
27 interface, model_column = item
28 assert interface
29 if model_column == InterfaceBrowser.INTERFACE_NAME:
30 return _("Full name: %s") % interface.uri
31 elif model_column == InterfaceBrowser.SUMMARY:
32 if not interface.description:
33 return None
34 first_para = interface.description.split('\n\n', 1)[0]
35 return first_para.replace('\n', ' ')
37 impl = self.mainwindow.policy.implementation.get(interface, None)
38 if not impl:
39 return _("No suitable implementation was found. Check the "
40 "interface properties to find out why.")
42 if model_column == InterfaceBrowser.VERSION:
43 text = _("Currently preferred version: %s (%s)") % \
44 (impl.get_version(), _stability(impl))
45 old_impl = self.mainwindow.original_implementation.get(interface, None)
46 if old_impl is not None and old_impl is not impl:
47 text += _('\nPreviously preferred version: %s (%s)') % \
48 (old_impl.get_version(), _stability(old_impl))
49 return text
51 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
53 if self.mainwindow.policy.get_cached(impl):
54 return _("This version is already stored on your computer.")
55 else:
56 src = self.mainwindow.policy.fetcher.get_best_source(impl)
57 if not src:
58 return _("No downloads available!")
59 return _("Need to download %s (%s bytes)") % \
60 (support.pretty_size(src.size), src.size)
62 class IconAndTextRenderer(gtk.GenericCellRenderer):
63 __gproperties__ = {
64 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
65 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
68 def do_set_property(self, prop, value):
69 setattr(self, prop.name, value)
71 def on_get_size(self, widget, cell_area, layout = None):
72 if not layout:
73 layout = widget.create_pango_layout(self.text)
74 a, rect = layout.get_pixel_extents()
76 pixmap_height = self.image.get_height()
78 both_height = max(rect[1] + rect[3], pixmap_height)
80 return (0, 0,
81 rect[0] + rect[2] + CELL_TEXT_INDENT,
82 both_height)
84 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
85 layout = widget.create_pango_layout(self.text)
86 a, rect = layout.get_pixel_extents()
88 if flags & gtk.CELL_RENDERER_SELECTED:
89 state = gtk.STATE_SELECTED
90 elif flags & gtk.CELL_RENDERER_PRELIT:
91 state = gtk.STATE_PRELIGHT
92 else:
93 state = gtk.STATE_NORMAL
95 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
96 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
97 cell_area.x,
98 cell_area.y + image_y)
100 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
102 widget.style.paint_layout(window, state, True,
103 expose_area, widget, "cellrenderertext",
104 cell_area.x + CELL_TEXT_INDENT,
105 cell_area.y + text_y,
106 layout)
108 if gtk.pygtk_version < (2, 8, 0):
109 # Note sure exactly which versions need this.
110 # 2.8.0 gives a warning if you include it, though.
111 gobject.type_register(IconAndTextRenderer)
113 class InterfaceBrowser:
114 model = None
115 root = None
116 edit_properties = None
117 cached_icon = None
118 policy = None
119 original_implementation = None
121 INTERFACE = 0
122 INTERFACE_NAME = 1
123 VERSION = 2
124 SUMMARY = 3
125 DOWNLOAD_SIZE = 4
126 ICON = 5
128 columns = [(_('Interface'), INTERFACE_NAME),
129 (_('Version'), VERSION),
130 (_('Fetch'), DOWNLOAD_SIZE),
131 (_('Description'), SUMMARY)]
133 def __init__(self, policy, widgets):
134 tips = InterfaceTips(self)
136 tree_view = widgets.get_widget('components')
138 self.policy = policy
139 self.cached_icon = {} # URI -> GdkPixbuf
140 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
141 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
143 self.edit_properties = widgets.get_widget('properties')
144 self.edit_properties.set_property('sensitive', False)
146 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
147 self.tree_view = tree_view
148 tree_view.set_model(self.model)
150 column_objects = []
152 text = gtk.CellRendererText()
154 for name, model_column in self.columns:
155 if model_column == InterfaceBrowser.INTERFACE_NAME:
156 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
157 text = model_column,
158 image = InterfaceBrowser.ICON)
159 else:
160 column = gtk.TreeViewColumn(name, text, text = model_column)
161 tree_view.append_column(column)
162 column_objects.append(column)
164 tree_view.set_enable_search(True)
166 selection = tree_view.get_selection()
168 def motion(tree_view, ev):
169 if ev.window is not tree_view.get_bin_window():
170 return False
171 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
172 if pos:
173 path = pos[0]
174 try:
175 col_index = column_objects.index(pos[1])
176 except ValueError:
177 tips.hide()
178 else:
179 col = self.columns[col_index][1]
180 row = self.model[path]
181 item = (row[InterfaceBrowser.INTERFACE], col)
182 if item != tips.item:
183 tips.prime(tree_view, item)
184 else:
185 tips.hide()
187 tree_view.connect('motion-notify-event', motion)
188 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
190 def sel_changed(sel):
191 store, iter = sel.get_selected()
192 self.edit_properties.set_property('sensitive', iter != None)
193 selection.connect('changed', sel_changed)
195 def button_press(tree_view, bev):
196 if bev.button == 3 and bev.type == gtk.gdk.BUTTON_PRESS:
197 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
198 if not pos:
199 return False
200 path, col, x, y = pos
201 selection.select_path(path)
202 iface = self.model[path][InterfaceBrowser.INTERFACE]
203 self.show_popup_menu(iface, bev)
204 return True
205 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
206 return False
207 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
208 if not pos:
209 return False
210 path, col, x, y = pos
211 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
212 tree_view.connect('button-press-event', button_press)
214 def edit_selected(action):
215 store, iter = selection.get_selected()
216 assert iter
217 properties.edit(policy, self.model[iter][InterfaceBrowser.INTERFACE])
218 self.edit_properties.connect('clicked', edit_selected)
220 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
221 policy.watchers.append(self.build_tree)
223 def set_root(self, root):
224 assert isinstance(root, model.Interface)
225 self.root = root
227 def get_icon(self, iface):
228 """Get an icon for this interface. If the icon is in the cache, use that.
229 If not, start a download. If we already started a download (successful or
230 not) do nothing. Returns None if no icon is currently available."""
231 try:
232 return self.cached_icon[iface.uri]
233 except KeyError:
234 path = iface_cache.get_icon_path(iface)
235 if path:
236 try:
237 loader = gtk.gdk.PixbufLoader('png')
238 try:
239 loader.write(file(path).read())
240 finally:
241 loader.close()
242 icon = loader.get_pixbuf()
243 assert icon, "Failed to load cached PNG icon data"
244 except Exception, ex:
245 warn("Failed to load cached PNG icon: %s", ex)
246 return None
247 w = icon.get_width()
248 h = icon.get_height()
249 scale = max(w, h, 1) / ICON_SIZE
250 icon = icon.scale_simple(int(w / scale),
251 int(h / scale),
252 gtk.gdk.INTERP_BILINEAR)
253 self.cached_icon[iface.uri] = icon
254 return icon
255 else:
256 # Try to download the icon
257 fetcher = self.policy.download_icon(iface)
258 if fetcher:
259 @tasks.async
260 def update_display():
261 yield fetcher
262 try:
263 tasks.check(fetcher)
264 self.build_tree()
265 except Exception, ex:
266 import traceback
267 traceback.print_exc()
268 self.policy.handler.report_error(ex)
269 update_display()
271 return None
273 def build_tree(self):
274 if self.original_implementation is None:
275 self.set_original_implementations()
277 done = {} # Detect cycles
279 self.model.clear()
280 parent = None
281 def add_node(parent, iface):
282 if iface in done:
283 return
284 done[iface] = True
286 iter = self.model.append(parent)
287 self.model[iter][InterfaceBrowser.INTERFACE] = iface
288 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
289 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
290 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
292 impl = self.policy.implementation.get(iface, None)
293 if impl:
294 old_impl = self.original_implementation.get(iface, None)
295 version_str = impl.get_version()
296 if old_impl is not None and old_impl is not impl:
297 version_str += " (was " + old_impl.get_version() + ")"
298 self.model[iter][InterfaceBrowser.VERSION] = version_str
300 if self.policy.get_cached(impl):
301 if impl.id.startswith('/'):
302 fetch = '(local)'
303 elif impl.id.startswith('package:'):
304 fetch = '(package)'
305 else:
306 fetch = '(cached)'
307 else:
308 src = self.policy.fetcher.get_best_source(impl)
309 if src:
310 fetch = support.pretty_size(src.size)
311 else:
312 fetch = '(unavailable)'
313 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = fetch
314 if hasattr(impl, 'requires'):
315 children = impl.requires
316 else:
317 children = impl.dependencies
319 for child in children:
320 if isinstance(child, model.InterfaceDependency):
321 add_node(iter, iface_cache.get_interface(child.interface))
322 else:
323 child_iter = self.model.append(parent)
324 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
325 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
326 'Unknown dependency type : %s' % child
327 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
328 else:
329 self.model[iter][InterfaceBrowser.VERSION] = '(choose)'
330 add_node(None, self.root)
331 self.tree_view.expand_all()
333 def show_popup_menu(self, iface, bev):
334 import bugs
336 if properties.have_source_for(self.policy, iface):
337 def compile_cb():
338 import compile
339 compile.compile(self.policy, iface)
340 else:
341 compile_cb = None
343 menu = gtk.Menu()
344 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
345 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
346 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface)),
347 (_('Compile...'), compile_cb)]:
348 item = gtk.MenuItem(label)
349 if cb:
350 item.connect('activate', lambda item, cb=cb: cb())
351 else:
352 item.set_sensitive(False)
353 item.show()
354 menu.append(item)
355 menu.popup(None, None, None, bev.button, bev.time)
357 def set_original_implementations(self):
358 assert self.original_implementation is None
359 self.original_implementation = self.policy.implementation.copy()
361 def update_download_status(self):
362 """Called at regular intervals while there are downloads in progress,
363 and once at the end. Update the TreeView with the interfaces."""
364 hints = {}
365 for dl in self.policy.handler.monitored_downloads.values():
366 if dl.hint:
367 if dl.hint not in hints:
368 hints[dl.hint] = []
369 hints[dl.hint].append(dl)
371 selections = self.policy.solver.selections
373 def walk(it):
374 while it:
375 yield self.model[it]
376 for x in walk(self.model.iter_children(it)): yield x
377 it = self.model.iter_next(it)
379 for row in walk(self.model.get_iter_root()):
380 iface = row[InterfaceBrowser.INTERFACE]
382 # Is this interface the download's hint?
383 downloads = hints.get(iface, []) # The interface itself
384 downloads += hints.get(iface.uri, []) # The main feed
385 for feed in iface.feeds:
386 downloads += hints.get(feed.uri, []) # Other feeds
387 impl = selections.get(iface, None)
388 if impl:
389 downloads += hints.get(impl, []) # The chosen implementation
391 if downloads:
392 so_far = 0
393 expected = None
394 for dl in downloads:
395 if dl.expected_size:
396 expected = (expected or 0) + dl.expected_size
397 so_far += dl.get_bytes_downloaded_so_far()
398 if expected:
399 fraction = "%s [%.2f%%]" % (pretty_size(expected), 100 * so_far / float(expected))
400 else:
401 fraction = "unknown"
402 if len(downloads) > 1:
403 fraction += " in %d downloads" % len(downloads)
404 row[InterfaceBrowser.SUMMARY] = "(downloading %s/%s)" % (pretty_size(so_far), fraction)
405 else:
406 row[InterfaceBrowser.SUMMARY] = iface.summary