Include cache.glade when installing with setup.py.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / 0launch-gui / iface_browser.py
blob9269a2c312e86da5f4081df071666ad4ca6d3260
1 # Copyright (C) 2008, 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(self, iface):
253 """Get an icon for this interface. If the icon is in the cache, use that.
254 If not, start a download. If we already started a download (successful or
255 not) do nothing. Returns None if no icon is currently available."""
256 try:
257 return self.cached_icon[iface.uri]
258 except KeyError:
259 path = iface_cache.get_icon_path(iface)
260 if path:
261 try:
262 loader = gtk.gdk.PixbufLoader('png')
263 try:
264 loader.write(file(path).read())
265 finally:
266 loader.close()
267 icon = loader.get_pixbuf()
268 assert icon, "Failed to load cached PNG icon data"
269 except Exception, ex:
270 warn("Failed to load cached PNG icon: %s", ex)
271 return None
272 w = icon.get_width()
273 h = icon.get_height()
274 scale = max(w, h, 1) / ICON_SIZE
275 icon = icon.scale_simple(int(w / scale),
276 int(h / scale),
277 gtk.gdk.INTERP_BILINEAR)
278 self.cached_icon[iface.uri] = icon
279 return icon
280 else:
281 # Try to download the icon
282 self.cached_icon[iface.uri] = None # Only try once
283 fetcher = self.policy.download_icon(iface)
284 if fetcher:
285 @tasks.async
286 def update_display():
287 yield fetcher
288 try:
289 tasks.check(fetcher)
290 self.build_tree()
291 except Exception, ex:
292 import traceback
293 traceback.print_exc()
294 self.policy.handler.report_error(ex)
295 update_display()
297 return None
299 def build_tree(self):
300 if self.original_implementation is None:
301 self.set_original_implementations()
303 done = {} # Detect cycles
305 self.model.clear()
306 parent = None
307 def add_node(parent, iface):
308 if iface in done:
309 return
310 done[iface] = True
312 iter = self.model.append(parent)
313 self.model[iter][InterfaceBrowser.INTERFACE] = iface
314 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
315 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
316 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
318 impl = self.policy.implementation.get(iface, None)
319 if impl:
320 old_impl = self.original_implementation.get(iface, None)
321 version_str = impl.get_version()
322 if old_impl is not None and old_impl is not impl:
323 version_str += " (was " + old_impl.get_version() + ")"
324 self.model[iter][InterfaceBrowser.VERSION] = version_str
326 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
327 if hasattr(impl, 'requires'):
328 children = impl.requires
329 else:
330 children = impl.dependencies
332 for child in children:
333 if isinstance(child, model.InterfaceDependency):
334 add_node(iter, iface_cache.get_interface(child.interface))
335 else:
336 child_iter = self.model.append(parent)
337 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
338 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
339 'Unknown dependency type : %s' % child
340 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
341 else:
342 self.model[iter][InterfaceBrowser.VERSION] = '(choose)'
343 add_node(None, self.root)
344 self.tree_view.expand_all()
346 def show_popup_menu(self, iface, bev):
347 import bugs
349 if properties.have_source_for(self.policy, iface):
350 def compile_cb():
351 import compile
352 compile.compile(self.policy, iface)
353 else:
354 compile_cb = None
356 menu = gtk.Menu()
357 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
358 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
359 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface)),
360 (_('Compile...'), compile_cb)]:
361 item = gtk.MenuItem(label)
362 if cb:
363 item.connect('activate', lambda item, cb=cb: cb())
364 else:
365 item.set_sensitive(False)
366 item.show()
367 menu.append(item)
368 menu.popup(None, None, None, bev.button, bev.time)
370 def set_original_implementations(self):
371 assert self.original_implementation is None
372 self.original_implementation = self.policy.implementation.copy()
374 def update_download_status(self):
375 """Called at regular intervals while there are downloads in progress,
376 and once at the end. Also called when things are added to the store.
377 Update the TreeView with the interfaces."""
378 hints = {}
379 for dl in self.policy.handler.monitored_downloads.values():
380 if dl.hint:
381 if dl.hint not in hints:
382 hints[dl.hint] = []
383 hints[dl.hint].append(dl)
385 selections = self.policy.solver.selections
387 def walk(it):
388 while it:
389 yield self.model[it]
390 for x in walk(self.model.iter_children(it)): yield x
391 it = self.model.iter_next(it)
393 for row in walk(self.model.get_iter_root()):
394 iface = row[InterfaceBrowser.INTERFACE]
396 # Is this interface the download's hint?
397 downloads = hints.get(iface, []) # The interface itself
398 downloads += hints.get(iface.uri, []) # The main feed
399 for feed in iface.feeds:
400 downloads += hints.get(feed.uri, []) # Other feeds
401 impl = selections.get(iface, None)
402 if impl:
403 downloads += hints.get(impl, []) # The chosen implementation
405 if downloads:
406 so_far = 0
407 expected = None
408 for dl in downloads:
409 if dl.expected_size:
410 expected = (expected or 0) + dl.expected_size
411 so_far += dl.get_bytes_downloaded_so_far()
412 if expected:
413 fraction = "%s [%.2f%%]" % (pretty_size(expected), 100 * so_far / float(expected))
414 else:
415 fraction = "unknown"
416 if len(downloads) > 1:
417 fraction += " in %d downloads" % len(downloads)
418 row[InterfaceBrowser.SUMMARY] = "(downloading %s/%s)" % (pretty_size(so_far), fraction)
419 else:
420 row[InterfaceBrowser.DOWNLOAD_SIZE] = utils.get_fetch_info(self.policy, impl)
421 row[InterfaceBrowser.SUMMARY] = iface.summary