Replaced "Interface Properties" button with a menu button on each row of the table.
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / 0launch-gui / iface_browser.py
blob06ceeab1460caceec5d41bcf5d22d8098b75c892
1 import gtk, gobject, pango
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', ' ')
36 elif model_column is None:
37 return _("Click here for more options...")
39 impl = self.mainwindow.policy.implementation.get(interface, None)
40 if not impl:
41 return _("No suitable implementation was found. Check the "
42 "interface properties to find out why.")
44 if model_column == InterfaceBrowser.VERSION:
45 text = _("Currently preferred version: %s (%s)") % \
46 (impl.get_version(), _stability(impl))
47 old_impl = self.mainwindow.original_implementation.get(interface, None)
48 if old_impl is not None and old_impl is not impl:
49 text += _('\nPreviously preferred version: %s (%s)') % \
50 (old_impl.get_version(), _stability(old_impl))
51 return text
53 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
55 if self.mainwindow.policy.get_cached(impl):
56 return _("This version is already stored on your computer.")
57 else:
58 src = self.mainwindow.policy.fetcher.get_best_source(impl)
59 if not src:
60 return _("No downloads available!")
61 return _("Need to download %s (%s bytes)") % \
62 (support.pretty_size(src.size), src.size)
64 class MenuIconRenderer(gtk.GenericCellRenderer):
65 def __init__(self):
66 gtk.GenericCellRenderer.__init__(self)
67 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
69 def do_set_property(self, prop, value):
70 setattr(self, prop.name, value)
72 def on_get_size(self, widget, cell_area, layout = None):
73 return (0, 0, 20, 20)
75 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
76 if flags & gtk.CELL_RENDERER_PRELIT:
77 state = gtk.STATE_PRELIGHT
78 else:
79 state = gtk.STATE_NORMAL
81 widget.style.paint_box(window, state, gtk.SHADOW_OUT, expose_area, widget, None,
82 cell_area.x, cell_area.y, cell_area.width, cell_area.height)
83 widget.style.paint_arrow(window, state, gtk.SHADOW_NONE, expose_area, widget, None,
84 gtk.ARROW_RIGHT, True,
85 cell_area.x + 5, cell_area.y + 5, cell_area.width - 10, cell_area.height - 10)
87 class IconAndTextRenderer(gtk.GenericCellRenderer):
88 __gproperties__ = {
89 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
90 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
93 def do_set_property(self, prop, value):
94 setattr(self, prop.name, value)
96 def on_get_size(self, widget, cell_area, layout = None):
97 if not layout:
98 layout = widget.create_pango_layout(self.text)
99 a, rect = layout.get_pixel_extents()
101 pixmap_height = self.image.get_height()
103 both_height = max(rect[1] + rect[3], pixmap_height)
105 return (0, 0,
106 rect[0] + rect[2] + CELL_TEXT_INDENT,
107 both_height)
109 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
110 layout = widget.create_pango_layout(self.text)
111 a, rect = layout.get_pixel_extents()
113 if flags & gtk.CELL_RENDERER_SELECTED:
114 state = gtk.STATE_SELECTED
115 elif flags & gtk.CELL_RENDERER_PRELIT:
116 state = gtk.STATE_PRELIGHT
117 else:
118 state = gtk.STATE_NORMAL
120 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
121 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
122 cell_area.x,
123 cell_area.y + image_y)
125 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
127 widget.style.paint_layout(window, state, True,
128 expose_area, widget, "cellrenderertext",
129 cell_area.x + CELL_TEXT_INDENT,
130 cell_area.y + text_y,
131 layout)
133 if gtk.pygtk_version < (2, 8, 0):
134 # Note sure exactly which versions need this.
135 # 2.8.0 gives a warning if you include it, though.
136 gobject.type_register(IconAndTextRenderer)
137 gobject.type_register(MenuIconRenderer)
139 class InterfaceBrowser:
140 model = None
141 root = None
142 cached_icon = None
143 policy = None
144 original_implementation = None
146 INTERFACE = 0
147 INTERFACE_NAME = 1
148 VERSION = 2
149 SUMMARY = 3
150 DOWNLOAD_SIZE = 4
151 ICON = 5
153 columns = [(_('Interface'), INTERFACE_NAME),
154 (_('Version'), VERSION),
155 (_('Fetch'), DOWNLOAD_SIZE),
156 (_('Description'), SUMMARY),
157 ('', None)]
159 def __init__(self, policy, widgets):
160 tips = InterfaceTips(self)
162 tree_view = widgets.get_widget('components')
164 self.policy = policy
165 self.cached_icon = {} # URI -> GdkPixbuf
166 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
167 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
169 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
170 self.tree_view = tree_view
171 tree_view.set_model(self.model)
173 column_objects = []
175 text = gtk.CellRendererText()
177 for name, model_column in self.columns:
178 if model_column == InterfaceBrowser.INTERFACE_NAME:
179 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
180 text = model_column,
181 image = InterfaceBrowser.ICON)
182 elif model_column == None:
183 menu_column = column = gtk.TreeViewColumn('', MenuIconRenderer())
184 else:
185 if model_column == InterfaceBrowser.SUMMARY:
186 text_ellip = gtk.CellRendererText()
187 try:
188 text_ellip.set_property('ellipsize', pango.ELLIPSIZE_END)
189 except:
190 pass
191 column = gtk.TreeViewColumn(name, text_ellip, text = model_column)
192 column.set_expand(True)
193 else:
194 column = gtk.TreeViewColumn(name, text, text = model_column)
195 tree_view.append_column(column)
196 column_objects.append(column)
198 tree_view.set_enable_search(True)
200 selection = tree_view.get_selection()
202 def motion(tree_view, ev):
203 if ev.window is not tree_view.get_bin_window():
204 return False
205 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
206 if pos:
207 path = pos[0]
208 try:
209 col_index = column_objects.index(pos[1])
210 except ValueError:
211 tips.hide()
212 else:
213 col = self.columns[col_index][1]
214 row = self.model[path]
215 item = (row[InterfaceBrowser.INTERFACE], col)
216 if item != tips.item:
217 tips.prime(tree_view, item)
218 else:
219 tips.hide()
221 tree_view.connect('motion-notify-event', motion)
222 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
224 def button_press(tree_view, bev):
225 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
226 if not pos:
227 return False
228 path, col, x, y = pos
230 if (bev.button == 3 or (bev.button < 4 and col is menu_column)) \
231 and bev.type == gtk.gdk.BUTTON_PRESS:
232 selection.select_path(path)
233 iface = self.model[path][InterfaceBrowser.INTERFACE]
234 self.show_popup_menu(iface, bev)
235 return True
236 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
237 return False
238 properties.edit(policy, self.model[path][InterfaceBrowser.INTERFACE])
239 tree_view.connect('button-press-event', button_press)
241 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
242 policy.watchers.append(self.build_tree)
244 def set_root(self, root):
245 assert isinstance(root, model.Interface)
246 self.root = root
248 def get_icon(self, iface):
249 """Get an icon for this interface. If the icon is in the cache, use that.
250 If not, start a download. If we already started a download (successful or
251 not) do nothing. Returns None if no icon is currently available."""
252 try:
253 return self.cached_icon[iface.uri]
254 except KeyError:
255 path = iface_cache.get_icon_path(iface)
256 if path:
257 try:
258 loader = gtk.gdk.PixbufLoader('png')
259 try:
260 loader.write(file(path).read())
261 finally:
262 loader.close()
263 icon = loader.get_pixbuf()
264 assert icon, "Failed to load cached PNG icon data"
265 except Exception, ex:
266 warn("Failed to load cached PNG icon: %s", ex)
267 return None
268 w = icon.get_width()
269 h = icon.get_height()
270 scale = max(w, h, 1) / ICON_SIZE
271 icon = icon.scale_simple(int(w / scale),
272 int(h / scale),
273 gtk.gdk.INTERP_BILINEAR)
274 self.cached_icon[iface.uri] = icon
275 return icon
276 else:
277 # Try to download the icon
278 fetcher = self.policy.download_icon(iface)
279 if fetcher:
280 @tasks.async
281 def update_display():
282 yield fetcher
283 try:
284 tasks.check(fetcher)
285 self.build_tree()
286 except Exception, ex:
287 import traceback
288 traceback.print_exc()
289 self.policy.handler.report_error(ex)
290 update_display()
292 return None
294 def build_tree(self):
295 if self.original_implementation is None:
296 self.set_original_implementations()
298 done = {} # Detect cycles
300 self.model.clear()
301 parent = None
302 def add_node(parent, iface):
303 if iface in done:
304 return
305 done[iface] = True
307 iter = self.model.append(parent)
308 self.model[iter][InterfaceBrowser.INTERFACE] = iface
309 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
310 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
311 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
313 impl = self.policy.implementation.get(iface, None)
314 if impl:
315 old_impl = self.original_implementation.get(iface, None)
316 version_str = impl.get_version()
317 if old_impl is not None and old_impl is not impl:
318 version_str += " (was " + old_impl.get_version() + ")"
319 self.model[iter][InterfaceBrowser.VERSION] = version_str
321 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = self._get_fetch_info(impl)
322 if hasattr(impl, 'requires'):
323 children = impl.requires
324 else:
325 children = impl.dependencies
327 for child in children:
328 if isinstance(child, model.InterfaceDependency):
329 add_node(iter, iface_cache.get_interface(child.interface))
330 else:
331 child_iter = self.model.append(parent)
332 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
333 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
334 'Unknown dependency type : %s' % child
335 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
336 else:
337 self.model[iter][InterfaceBrowser.VERSION] = '(choose)'
338 add_node(None, self.root)
339 self.tree_view.expand_all()
341 def _get_fetch_info(self, impl):
342 """Get the text for the Fetch column."""
343 if impl is None:
344 return ""
345 elif self.policy.get_cached(impl):
346 if impl.id.startswith('/'):
347 return '(local)'
348 elif impl.id.startswith('package:'):
349 return '(package)'
350 else:
351 return '(cached)'
352 else:
353 src = self.policy.fetcher.get_best_source(impl)
354 if src:
355 return support.pretty_size(src.size)
356 else:
357 return '(unavailable)'
359 def show_popup_menu(self, iface, bev):
360 import bugs
362 if properties.have_source_for(self.policy, iface):
363 def compile_cb():
364 import compile
365 compile.compile(self.policy, iface)
366 else:
367 compile_cb = None
369 menu = gtk.Menu()
370 for label, cb in [(_('Show Feeds'), lambda: properties.edit(self.policy, iface)),
371 (_('Show Versions'), lambda: properties.edit(self.policy, iface, show_versions = True)),
372 (_('Report a Bug...'), lambda: bugs.report_bug(self.policy, iface)),
373 (_('Compile...'), compile_cb)]:
374 item = gtk.MenuItem(label)
375 if cb:
376 item.connect('activate', lambda item, cb=cb: cb())
377 else:
378 item.set_sensitive(False)
379 item.show()
380 menu.append(item)
381 menu.popup(None, None, None, bev.button, bev.time)
383 def set_original_implementations(self):
384 assert self.original_implementation is None
385 self.original_implementation = self.policy.implementation.copy()
387 def update_download_status(self):
388 """Called at regular intervals while there are downloads in progress,
389 and once at the end. Also called when things are added to the store.
390 Update the TreeView with the interfaces."""
391 hints = {}
392 for dl in self.policy.handler.monitored_downloads.values():
393 if dl.hint:
394 if dl.hint not in hints:
395 hints[dl.hint] = []
396 hints[dl.hint].append(dl)
398 selections = self.policy.solver.selections
400 def walk(it):
401 while it:
402 yield self.model[it]
403 for x in walk(self.model.iter_children(it)): yield x
404 it = self.model.iter_next(it)
406 for row in walk(self.model.get_iter_root()):
407 iface = row[InterfaceBrowser.INTERFACE]
409 # Is this interface the download's hint?
410 downloads = hints.get(iface, []) # The interface itself
411 downloads += hints.get(iface.uri, []) # The main feed
412 for feed in iface.feeds:
413 downloads += hints.get(feed.uri, []) # Other feeds
414 impl = selections.get(iface, None)
415 if impl:
416 downloads += hints.get(impl, []) # The chosen implementation
418 if downloads:
419 so_far = 0
420 expected = None
421 for dl in downloads:
422 if dl.expected_size:
423 expected = (expected or 0) + dl.expected_size
424 so_far += dl.get_bytes_downloaded_so_far()
425 if expected:
426 fraction = "%s [%.2f%%]" % (pretty_size(expected), 100 * so_far / float(expected))
427 else:
428 fraction = "unknown"
429 if len(downloads) > 1:
430 fraction += " in %d downloads" % len(downloads)
431 row[InterfaceBrowser.SUMMARY] = "(downloading %s/%s)" % (pretty_size(so_far), fraction)
432 else:
433 row[InterfaceBrowser.DOWNLOAD_SIZE] = self._get_fetch_info(impl)
434 row[InterfaceBrowser.SUMMARY] = iface.summary