Fixed warning from PyGObject
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blob0bbd32112d600adcbc32f19e4b12307469d5bbe4
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.driver.solver.selections.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 impl.is_available(mainwindow.driver.config.stores):
57 return _("This version is already stored on your computer.")
58 else:
59 src = mainwindow.driver.config.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_PYOBJECT, "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):
141 while it:
142 yield it
143 for x in walk(model, model.iter_children(it)): yield x
144 it = model.iter_next(it)
146 class InterfaceBrowser:
147 model = None
148 root = None
149 cached_icon = None
150 driver = None
151 config = None
152 original_implementation = None
153 update_icons = False
155 INTERFACE = 0
156 INTERFACE_NAME = 1
157 VERSION = 2
158 SUMMARY = 3
159 DOWNLOAD_SIZE = 4
160 ICON = 5
161 BACKGROUND = 6
162 PROBLEM = 7
164 columns = [(_('Component'), INTERFACE_NAME),
165 (_('Version'), VERSION),
166 (_('Fetch'), DOWNLOAD_SIZE),
167 (_('Description'), SUMMARY),
168 ('', None)]
170 def __init__(self, driver, widgets):
171 self.driver = driver
172 self.config = driver.config
174 tree_view = widgets.get_widget('components')
175 tree_view.set_property('has-tooltip', True)
176 def callback(widget, x, y, keyboard_mode, tooltip):
177 x, y = tree_view.convert_widget_to_bin_window_coords(x, y)
178 pos = tree_view.get_path_at_pos(x, y)
179 if pos:
180 tree_view.set_tooltip_cell(tooltip, pos[0], pos[1], None)
181 path = pos[0]
182 try:
183 col_index = column_objects.index(pos[1])
184 except ValueError:
185 return False
186 else:
187 col = self.columns[col_index][1]
188 row = self.model[path]
189 iface = row[InterfaceBrowser.INTERFACE]
190 main_feed = self.config.iface_cache.get_feed(iface.uri)
191 tooltip.set_text(get_tooltip_text(self, iface, main_feed, col))
192 return True
193 else:
194 return False
195 tree_view.connect('query-tooltip', callback)
197 self.cached_icon = {} # URI -> GdkPixbuf
198 self.default_icon = tree_view.get_style().lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.get_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, gobject.TYPE_PYOBJECT, 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(driver, 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: driver.watchers.remove(self.build_tree))
256 driver.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.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 if self.config.network_use == model.network_offline:
290 fetcher = None
291 else:
292 fetcher = self.config.fetcher.download_icon(iface)
293 if fetcher:
294 if iface.uri not in self.cached_icon:
295 self.cached_icon[iface.uri] = None # Only try once
297 @tasks.async
298 def update_display():
299 yield fetcher
300 try:
301 tasks.check(fetcher)
302 # Try to insert new icon into the cache
303 # If it fails, we'll be left with None in the cached_icon so
304 # we don't try again.
305 iconpath = self.config.iface_cache.get_icon_path(iface)
306 if iconpath:
307 self.cached_icon[iface.uri] = load_icon(iconpath, ICON_SIZE, ICON_SIZE)
308 self.build_tree()
309 else:
310 warn("Failed to download icon for '%s'", iface)
311 except Exception as ex:
312 import traceback
313 traceback.print_exc()
314 self.config.handler.report_error(ex)
315 update_display()
316 # elif fetcher is None: don't store anything in cached_icon
318 # Note: if no icon is available for downloading,
319 # more attempts are made later.
320 # It can happen that no icon is yet available because
321 # the interface was not downloaded yet, in which case
322 # it's desireable to try again once the interface is available
323 return icon
325 return None
327 def build_tree(self):
328 iface_cache = self.config.iface_cache
330 if self.original_implementation is None:
331 self.set_original_implementations()
333 done = {} # Detect cycles
335 sels = self.driver.solver.selections
337 self.model.clear()
338 def add_node(parent, iface, commands, essential):
339 if iface in done:
340 return
341 done[iface] = True
343 main_feed = iface_cache.get_feed(iface.uri)
344 if main_feed:
345 name = main_feed.get_name()
346 summary = main_feed.summary
347 else:
348 name = iface.get_name()
349 summary = None
351 iter = self.model.append(parent)
352 self.model[iter][InterfaceBrowser.INTERFACE] = iface
353 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = name
354 self.model[iter][InterfaceBrowser.SUMMARY] = summary
355 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
356 self.model[iter][InterfaceBrowser.PROBLEM] = False
358 sel = sels.selections.get(iface.uri, None)
359 if sel:
360 impl = sel.impl
361 old_impl = self.original_implementation.get(iface, None)
362 version_str = impl.get_version()
363 if old_impl is not None and old_impl.id != impl.id:
364 version_str += _(' (was %s)') % old_impl.get_version()
365 self.model[iter][InterfaceBrowser.VERSION] = version_str
367 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.config, impl)
369 deps = sel.dependencies
370 for c in commands:
371 deps += sel.get_command(c).requires
372 for child in deps:
373 if isinstance(child, model.InterfaceDependency):
374 add_node(iter,
375 iface_cache.get_interface(child.interface),
376 child.get_required_commands(),
377 child.importance == model.Dependency.Essential)
378 else:
379 child_iter = self.model.append(parent)
380 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
381 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
382 _('Unknown dependency type : %s') % child
383 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
384 else:
385 self.model[iter][InterfaceBrowser.PROBLEM] = essential
386 self.model[iter][InterfaceBrowser.VERSION] = _('(problem)') if essential else _('(none)')
387 if sels.command:
388 add_node(None, self.root, [sels.command], essential = True)
389 else:
390 add_node(None, self.root, [], essential = True)
391 self.tree_view.expand_all()
393 def show_popup_menu(self, iface, bev):
394 import bugs
396 have_source = properties.have_source_for(self.config, iface)
398 menu = gtk.Menu()
399 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.driver, iface, self.compile)),
400 (_('Show Versions'), lambda: properties.edit(self.driver, iface, self.compile, show_versions = True)),
401 (_('Report a Bug...'), lambda: bugs.report_bug(self.driver, iface))]:
402 item = gtk.MenuItem()
403 item.set_label(label)
404 if cb:
405 item.connect('activate', lambda item, cb=cb: cb())
406 else:
407 item.set_sensitive(False)
408 item.show()
409 menu.append(item)
411 item = gtk.MenuItem()
412 item.set_label(_('Compile'))
413 item.show()
414 menu.append(item)
415 if have_source:
416 compile_menu = gtk.Menu()
417 item.set_submenu(compile_menu)
419 item = gtk.MenuItem()
420 item.set_label(_('Automatic'))
421 item.connect('activate', lambda item: self.compile(iface, autocompile = True))
422 item.show()
423 compile_menu.append(item)
425 item = gtk.MenuItem()
426 item.set_label(_('Manual...'))
427 item.connect('activate', lambda item: self.compile(iface, autocompile = False))
428 item.show()
429 compile_menu.append(item)
430 else:
431 item.set_sensitive(False)
433 menu.popup(None, None, None, bev.button, bev.time)
435 def compile(self, interface, autocompile = True):
436 import compile
437 def on_success():
438 # A new local feed may have been registered, so reload it from the disk cache
439 info(_("0compile command completed successfully. Reloading interface details."))
440 reader.update_from_cache(interface)
441 for feed in interface.extra_feeds:
442 self.config.iface_cache.get_feed(feed.uri, force = True)
443 import main
444 main.recalculate()
445 compile.compile(on_success, interface.uri, autocompile = autocompile)
447 def set_original_implementations(self):
448 assert self.original_implementation is None
449 self.original_implementation = self.driver.solver.selections.copy()
451 def update_download_status(self, only_update_visible = False):
452 """Called at regular intervals while there are downloads in progress,
453 and once at the end. Also called when things are added to the store.
454 Update the TreeView with the interfaces."""
456 # A download may be for a feed, an interface or an implementation.
457 # Create the reverse mapping (item -> download)
458 hints = {}
459 for dl in self.config.handler.monitored_downloads:
460 if dl.hint:
461 if dl.hint not in hints:
462 hints[dl.hint] = []
463 hints[dl.hint].append(dl)
465 selections = self.driver.solver.selections
467 # Only update currently visible rows
468 if only_update_visible and self.tree_view.get_visible_range() != None:
469 firstVisiblePath, lastVisiblePath = self.tree_view.get_visible_range()
470 firstVisibleIter = self.model.get_iter(firstVisiblePath)
471 else:
472 # (or should we just wait until the TreeView has settled enough to tell
473 # us what is visible?)
474 firstVisibleIter = self.model.get_iter_root()
475 lastVisiblePath = None
477 solver = self.driver.solver
478 requirements = self.driver.requirements
479 iface_cache = self.config.iface_cache
481 for it in walk(self.model, firstVisibleIter):
482 row = self.model[it]
483 iface = row[InterfaceBrowser.INTERFACE]
485 # Is this interface the download's hint?
486 downloads = hints.get(iface, []) # The interface itself
487 downloads += hints.get(iface.uri, []) # The main feed
489 arch = solver.get_arch_for(requirements, iface)
490 for feed in iface_cache.usable_feeds(iface, arch):
491 downloads += hints.get(feed.uri, []) # Other feeds
492 impl = selections.get(iface, None)
493 if impl:
494 downloads += hints.get(impl, []) # The chosen implementation
496 if downloads:
497 so_far = 0
498 expected = None
499 for dl in downloads:
500 if dl.expected_size:
501 expected = (expected or 0) + dl.expected_size
502 so_far += dl.get_bytes_downloaded_so_far()
503 if expected:
504 summary = ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
505 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
506 downloads)
507 values_dict = {'downloaded': pretty_size(so_far), 'expected': pretty_size(expected), 'percentage': 100 * so_far / float(expected), 'number': len(downloads)}
508 else:
509 summary = ngettext("(downloading %(downloaded)s/unknown)",
510 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
511 downloads)
512 values_dict = {'downloaded': pretty_size(so_far), 'number': len(downloads)}
513 row[InterfaceBrowser.SUMMARY] = summary % values_dict
514 else:
515 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.config, impl)
516 row[InterfaceBrowser.SUMMARY] = iface.summary
518 if self.model.get_path(it) == lastVisiblePath:
519 break
521 def highlight_problems(self):
522 """Called when the solve finishes. Highlight any missing implementations."""
523 for it in walk(self.model, self.model.get_iter_root()):
524 row = self.model[it]
525 iface = row[InterfaceBrowser.INTERFACE]
526 sel = self.driver.solver.selections.selections.get(iface.uri, None)
528 if sel is None and row[InterfaceBrowser.PROBLEM]:
529 row[InterfaceBrowser.BACKGROUND] = '#f88'