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