pyflakes
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blob226ff0cf43763980be746ac40b2dd87f1564b47b
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 import model, reader
8 import properties
9 from zeroinstall.gtkui.icon import load_icon
10 from zeroinstall import support
11 from logging import warn, info
12 import utils
14 def _stability(impl):
15 assert impl
16 if impl.user_stability is None:
17 return _(str(impl.upstream_stability))
18 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
19 % {'implementation_user_stability': _(str(impl.user_stability)),
20 'implementation_upstream_stability': _(str(impl.upstream_stability))}
22 ICON_SIZE = 20.0
23 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
25 def get_tooltip_text(mainwindow, interface, model_column):
26 assert interface
27 if model_column == InterfaceBrowser.INTERFACE_NAME:
28 return _("Full name: %s") % interface.uri
29 elif model_column == InterfaceBrowser.SUMMARY:
30 if not interface.description:
31 return _("(no description available)")
32 first_para = interface.description.split('\n\n', 1)[0]
33 return first_para.replace('\n', ' ')
34 elif model_column is None:
35 return _("Click here for more options...")
37 impl = mainwindow.policy.implementation.get(interface, None)
38 if not impl:
39 return _("No suitable version was found. Double-click "
40 "here to find out why.")
42 if model_column == InterfaceBrowser.VERSION:
43 text = _("Currently preferred version: %(version)s (%(stability)s)") % \
44 {'version': impl.get_version(), 'stability': _stability(impl)}
45 old_impl = mainwindow.original_implementation.get(interface, None)
46 if old_impl is not None and old_impl is not impl:
47 text += '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
48 {'version': old_impl.get_version(), 'stability': _stability(old_impl)}
49 return text
51 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
53 if mainwindow.policy.get_cached(impl):
54 return _("This version is already stored on your computer.")
55 else:
56 src = mainwindow.policy.fetcher.get_best_source(impl)
57 if not src:
58 return _("No downloads available!")
59 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
60 {'pretty_size': support.pretty_size(src.size), 'size': src.size}
62 class MenuIconRenderer(gtk.GenericCellRenderer):
63 def __init__(self):
64 gtk.GenericCellRenderer.__init__(self)
65 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
67 def do_set_property(self, prop, value):
68 setattr(self, prop.name, value)
70 def on_get_size(self, widget, cell_area, layout = None):
71 return (0, 0, 20, 20)
73 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
74 if flags & gtk.CELL_RENDERER_PRELIT:
75 state = gtk.STATE_PRELIGHT
76 else:
77 state = gtk.STATE_NORMAL
79 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
80 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
81 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
82 gtk.ARROW_RIGHT, True,
83 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
85 class IconAndTextRenderer(gtk.GenericCellRenderer):
86 __gproperties__ = {
87 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
88 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
91 def do_set_property(self, prop, value):
92 setattr(self, prop.name, value)
94 def on_get_size(self, widget, cell_area, layout = None):
95 if not layout:
96 layout = widget.create_pango_layout(self.text)
97 a, rect = layout.get_pixel_extents()
99 pixmap_height = self.image.get_height()
101 both_height = max(rect[1] + rect[3], pixmap_height)
103 return (0, 0,
104 rect[0] + rect[2] + CELL_TEXT_INDENT,
105 both_height)
107 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
108 layout = widget.create_pango_layout(self.text)
109 a, rect = layout.get_pixel_extents()
111 if flags & gtk.CELL_RENDERER_SELECTED:
112 state = gtk.STATE_SELECTED
113 elif flags & gtk.CELL_RENDERER_PRELIT:
114 state = gtk.STATE_PRELIGHT
115 else:
116 state = gtk.STATE_NORMAL
118 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
119 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
120 cell_area.x,
121 cell_area.y + image_y)
123 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
125 widget.style.paint_layout(window, state, True,
126 expose_area, widget, "cellrenderertext",
127 cell_area.x + CELL_TEXT_INDENT,
128 cell_area.y + text_y,
129 layout)
131 if gtk.pygtk_version < (2, 8, 0):
132 # Note sure exactly which versions need this.
133 # 2.8.0 gives a warning if you include it, though.
134 gobject.type_register(IconAndTextRenderer)
135 gobject.type_register(MenuIconRenderer)
137 class InterfaceBrowser:
138 model = None
139 root = None
140 cached_icon = None
141 policy = None
142 original_implementation = None
143 update_icons = False
145 INTERFACE = 0
146 INTERFACE_NAME = 1
147 VERSION = 2
148 SUMMARY = 3
149 DOWNLOAD_SIZE = 4
150 ICON = 5
151 BACKGROUND = 6
153 columns = [(_('Component'), INTERFACE_NAME),
154 (_('Version'), VERSION),
155 (_('Fetch'), DOWNLOAD_SIZE),
156 (_('Description'), SUMMARY),
157 ('', None)]
159 def __init__(self, policy, widgets):
160 tree_view = widgets.get_widget('components')
161 tree_view.set_property('has-tooltip', True)
162 def callback(widget, x, y, keyboard_mode, tooltip):
163 x, y = tree_view.convert_widget_to_bin_window_coords(x, y)
164 pos = tree_view.get_path_at_pos(x, y)
165 if pos:
166 tree_view.set_tooltip_cell(tooltip, pos[0], pos[1], None)
167 path = pos[0]
168 try:
169 col_index = column_objects.index(pos[1])
170 except ValueError:
171 return False
172 else:
173 col = self.columns[col_index][1]
174 row = self.model[path]
175 tooltip.set_text(get_tooltip_text(self, row[InterfaceBrowser.INTERFACE], col))
176 return True
177 else:
178 return False
179 tree_view.connect('query-tooltip', callback)
181 self.policy = policy
182 self.cached_icon = {} # URI -> GdkPixbuf
183 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
184 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
186 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf, str)
187 self.tree_view = tree_view
188 tree_view.set_model(self.model)
190 column_objects = []
192 text = gtk.CellRendererText()
193 coloured_text = gtk.CellRendererText()
195 for name, model_column in self.columns:
196 if model_column == InterfaceBrowser.INTERFACE_NAME:
197 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
198 text = model_column,
199 image = InterfaceBrowser.ICON)
200 elif model_column == None:
201 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
202 else:
203 if model_column == InterfaceBrowser.SUMMARY:
204 text_ellip = gtk.CellRendererText()
205 try:
206 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
207 except:
208 pass
209 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
210 column.set_expand(True)
211 elif model_column == InterfaceBrowser.VERSION:
212 column = gtk.TreeViewColumn(name, coloured_text, text = model_column,
213 background = InterfaceBrowser.BACKGROUND)
214 else:
215 column = gtk.TreeViewColumn(name, text, text = model_column)
216 tree_view.append_column(column)
217 column_objects.append(column)
219 tree_view.set_enable_search(True)
221 selection = tree_view.get_selection()
223 def button_press(tree_view, bev):
224 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
225 if not pos:
226 return False
227 path, col, x, y = pos
229 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
230 and bev.type == gtk.gdk.BUTTON_PRESS:
231 selection.select_path(path)
232 iface = self.model[path][InterfaceBrowser.INTERFACE]
233 self.show_popup_menu(iface, bev)
234 return True
235 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
236 return False
237 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE], self.compile, show_versions = True)
238 tree_view.connect('button-press-event', button_press)
240 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
241 policy.watchers.append(self.build_tree)
243 def set_root(self, root):
244 assert isinstance(root, model.Interface)
245 self.root = root
247 def set_update_icons(self, update_icons):
248 if update_icons:
249 # Clear icons cache to make sure they're really updated
250 self.cached_icon = {}
251 self.update_icons = update_icons
253 def get_icon(self, iface):
254 """Get an icon for this interface. If the icon is in the cache, use that.
255 If not, start a download. If we already started a download (successful or
256 not) do nothing. Returns None if no icon is currently available."""
257 try:
258 # Try the in-memory cache
259 return self.cached_icon[iface.uri]
260 except KeyError:
261 # Try the on-disk cache
262 iconpath = self.policy.config.iface_cache.get_icon_path(iface)
264 if iconpath:
265 icon = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
266 # (if icon is None, cache the fact that we can't load it)
267 self.cached_icon[iface.uri] = icon
268 else:
269 icon = None
271 # Download a new icon if we don't have one, or if the
272 # user did a 'Refresh'
273 if iconpath is None or self.update_icons:
274 fetcher = self.policy.download_icon(iface)
275 if fetcher:
276 if iface.uri not in self.cached_icon:
277 self.cached_icon[iface.uri] = None # Only try once
279 @tasks.async
280 def update_display():
281 yield fetcher
282 try:
283 tasks.check(fetcher)
284 # Try to insert new icon into the cache
285 # If it fails, we'll be left with None in the cached_icon so
286 # we don't try again.
287 iconpath = self.policy.config.iface_cache.get_icon_path(iface)
288 if iconpath:
289 self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
290 self.build_tree()
291 else:
292 warn("Failed to download icon for '%s'", iface)
293 except Exception, ex:
294 import traceback
295 traceback.print_exc()
296 self.policy.handler.report_error(ex)
297 update_display()
298 # elif fetcher is None: don't store anything in cached_icon
300 # Note: if no icon is available for downloading,
301 # more attempts are made later.
302 # It can happen that no icon is yet available because
303 # the interface was not downloaded yet, in which case
304 # it's desireable to try again once the interface is available
305 return icon
307 return None
309 def build_tree(self):
310 iface_cache = self.policy.config.iface_cache
312 if self.original_implementation is None:
313 self.set_original_implementations()
315 done = {} # Detect cycles
317 self.model.clear()
318 commands = self.policy.solver.selections.commands
319 def add_node(parent, iface, command):
320 # (command is the index into commands, if any)
321 if iface in done:
322 return
323 done[iface] = True
325 iter = self.model.append(parent)
326 self.model[iter][InterfaceBrowser.INTERFACE] = iface
327 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
328 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
329 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
331 sel = self.policy.solver.selections.selections.get(iface.uri, None)
332 if sel:
333 impl = sel.impl
334 old_impl = self.original_implementation.get(iface, None)
335 version_str = impl.get_version()
336 if old_impl is not None and old_impl.id != impl.id:
337 version_str += _(' (was %s)') % old_impl.get_version()
338 self.model[iter][InterfaceBrowser.VERSION] = version_str
340 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
342 deps = sel.dependencies
343 if command is not None:
344 deps += commands[command].requires
345 for child in deps:
346 if isinstance(child, model.InterfaceDependency):
347 if child.qdom.name == 'runner':
348 child_command = command + 1
349 else:
350 child_command = None
351 add_node(iter, iface_cache.get_interface(child.interface), child_command)
352 else:
353 child_iter = self.model.append(parent)
354 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
355 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
356 _('Unknown dependency type : %s') % child
357 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
358 else:
359 self.model[iter][InterfaceBrowser.VERSION] = _('(problem)')
360 self.model[iter][InterfaceBrowser.BACKGROUND] = '#f88'
361 if commands:
362 add_node(None, self.root, 0)
363 else:
364 # Nothing could be selected, or no command requested
365 add_node(None, self.root, None)
366 self.tree_view.expand_all()
368 def show_popup_menu(self, iface, bev):
369 import bugs
371 have_source = properties.have_source_for(self.policy, iface)
373 menu = gtk.Menu()
374 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface, self.compile)),
375 (_('Show Versions'), lambda: properties.edit(self.policy, iface, self.compile, show_versions = True)),
376 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface))]:
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)
385 item = gtk.MenuItem(_('Compile'))
386 item.show()
387 menu.append(item)
388 if have_source:
389 compile_menu = gtk.Menu()
390 item.set_submenu(compile_menu)
392 item = gtk.MenuItem(_('Automatic'))
393 item.connect('activate', lambda item: self.compile(iface, autocompile = True))
394 item.show()
395 compile_menu.append(item)
397 item = gtk.MenuItem(_('Manual...'))
398 item.connect('activate', lambda item: self.compile(iface, autocompile = False))
399 item.show()
400 compile_menu.append(item)
401 else:
402 item.set_sensitive(False)
404 menu.popup(None, None, None, bev.button, bev.time)
406 def compile(self, interface, autocompile = False):
407 import compile
408 def on_success():
409 # A new local feed may have been registered, so reload it from the disk cache
410 info(_("0compile command completed successfully. Reloading interface details."))
411 reader.update_from_cache(interface)
412 for feed in interface.extra_feeds:
413 self.policy.config.iface_cache.get_feed(feed.uri, force = True)
414 self.policy.recalculate()
415 compile.compile(on_success, interface.uri, autocompile = autocompile)
417 def set_original_implementations(self):
418 assert self.original_implementation is None
419 self.original_implementation = self.policy.implementation.copy()
421 def update_download_status(self):
422 """Called at regular intervals while there are downloads in progress,
423 and once at the end. Also called when things are added to the store.
424 Update the TreeView with the interfaces."""
426 # A download may be for a feed, an interface or an implementation.
427 # Create the reverse mapping (item -> download)
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 self.policy.usable_feeds(iface):
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