Added @tasks.async decorator to simplify common code.
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blobb8ed6e673cdf757f6dcaed85d195484bd666f748
1 import gtk, gobject
3 from zeroinstall.support import basedir, tasks
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 gui import policy
9 from zeroinstall import support
10 from logging import warn
12 def _stability(impl):
13 assert impl
14 if impl.user_stability is None:
15 return impl.upstream_stability
16 return _("%s (was %s)") % (impl.user_stability, impl.upstream_stability)
18 ICON_SIZE = 20.0
19 CELL_TEXT_INDENT = int(ICON_SIZE) + 4
21 class InterfaceTips(TreeTips):
22 def get_tooltip_text(self, item):
23 interface, model_column = item
24 assert interface
25 if model_column == InterfaceBrowser.INTERFACE_NAME:
26 return _("Full name: %s") % interface.uri
27 elif model_column == InterfaceBrowser.SUMMARY:
28 if not interface.description:
29 return None
30 first_para = interface.description.split('\n\n', 1)[0]
31 return first_para.replace('\n', ' ')
33 impl = policy.implementation.get(interface, None)
34 if not impl:
35 return _("No suitable implementation was found. Check the "
36 "interface properties to find out why.")
38 if model_column == InterfaceBrowser.VERSION:
39 text = _("Currently preferred version: %s (%s)") % \
40 (impl.get_version(), _stability(impl))
41 old_impl = policy.original_implementation.get(interface, None)
42 if old_impl is not None and old_impl is not impl:
43 text += _('\nPreviously preferred version: %s (%s)') % \
44 (old_impl.get_version(), _stability(old_impl))
45 return text
47 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
49 if policy.get_cached(impl):
50 return _("This version is already stored on your computer.")
51 else:
52 src = policy.get_best_source(impl)
53 if not src:
54 return _("No downloads available!")
55 return _("Need to download %s (%s bytes)") % \
56 (support.pretty_size(src.size), src.size)
58 tips = InterfaceTips()
60 class IconAndTextRenderer(gtk.GenericCellRenderer):
61 __gproperties__ = {
62 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
63 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
66 def do_set_property(self, prop, value):
67 setattr(self, prop.name, value)
69 def on_get_size(self, widget, cell_area, layout = None):
70 if not layout:
71 layout = widget.create_pango_layout(self.text)
72 a, rect = layout.get_pixel_extents()
74 pixmap_height = self.image.get_height()
76 both_height = max(rect[1] + rect[3], pixmap_height)
78 return (0, 0,
79 rect[0] + rect[2] + CELL_TEXT_INDENT,
80 both_height)
82 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
83 layout = widget.create_pango_layout(self.text)
84 a, rect = layout.get_pixel_extents()
86 if flags & gtk.CELL_RENDERER_SELECTED:
87 state = gtk.STATE_SELECTED
88 elif flags & gtk.CELL_RENDERER_PRELIT:
89 state = gtk.STATE_PRELIGHT
90 else:
91 state = gtk.STATE_NORMAL
93 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
94 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
95 cell_area.x,
96 cell_area.y + image_y)
98 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
100 widget.style.paint_layout(window, state, True,
101 expose_area, widget, "cellrenderertext",
102 cell_area.x + CELL_TEXT_INDENT,
103 cell_area.y + text_y,
104 layout)
106 if gtk.pygtk_version < (2, 8, 0):
107 # Note sure exactly which versions need this.
108 # 2.8.0 gives a warning if you include it, though.
109 gobject.type_register(IconAndTextRenderer)
111 class InterfaceBrowser:
112 model = None
113 root = None
114 edit_properties = None
115 cached_icon = None
117 INTERFACE = 0
118 INTERFACE_NAME = 1
119 VERSION = 2
120 SUMMARY = 3
121 DOWNLOAD_SIZE = 4
122 ICON = 5
124 columns = [(_('Interface'), INTERFACE_NAME),
125 (_('Version'), VERSION),
126 (_('Fetch'), DOWNLOAD_SIZE),
127 (_('Description'), SUMMARY)]
129 def __init__(self, tree_view):
130 self.cached_icon = {} # URI -> GdkPixbuf
131 self.default_icon = tree_view.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(tree_view.style,
132 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, tree_view, None)
134 self.edit_properties = policy.widgets.get_widget('properties')
135 self.edit_properties.set_property('sensitive', False)
137 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
138 self.tree_view = tree_view
139 tree_view.set_model(self.model)
141 column_objects = []
143 text = gtk.CellRendererText()
145 for name, model_column in self.columns:
146 if model_column == InterfaceBrowser.INTERFACE_NAME:
147 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
148 text = model_column,
149 image = InterfaceBrowser.ICON)
150 else:
151 column = gtk.TreeViewColumn(name, text, text = model_column)
152 tree_view.append_column(column)
153 column_objects.append(column)
155 tree_view.set_enable_search(True)
157 selection = tree_view.get_selection()
159 def motion(tree_view, ev):
160 if ev.window is not tree_view.get_bin_window():
161 return False
162 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
163 if pos:
164 path = pos[0]
165 try:
166 col_index = column_objects.index(pos[1])
167 except ValueError:
168 tips.hide()
169 else:
170 col = self.columns[col_index][1]
171 row = self.model[path]
172 item = (row[InterfaceBrowser.INTERFACE], col)
173 if item != tips.item:
174 tips.prime(tree_view, item)
175 else:
176 tips.hide()
178 tree_view.connect('motion-notify-event', motion)
179 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
181 def sel_changed(sel):
182 store, iter = sel.get_selected()
183 self.edit_properties.set_property('sensitive', iter != None)
184 selection.connect('changed', sel_changed)
186 def button_press(tree_view, bev):
187 if bev.button == 3 and bev.type == gtk.gdk.BUTTON_PRESS:
188 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
189 if not pos:
190 return False
191 path, col, x, y = pos
192 selection.select_path(path)
193 iface = self.model[path][InterfaceBrowser.INTERFACE]
194 self.show_popup_menu(iface, bev)
195 return True
196 if bev.button != 1 or bev.type != gtk.gdk._2BUTTON_PRESS:
197 return False
198 pos = tree_view.get_path_at_pos(int(bev.x), int(bev.y))
199 if not pos:
200 return False
201 path, col, x, y = pos
202 properties.edit(self.model[path][InterfaceBrowser.INTERFACE])
203 tree_view.connect('button-press-event', button_press)
205 def edit_selected(action):
206 store, iter = selection.get_selected()
207 assert iter
208 properties.edit(self.model[iter][InterfaceBrowser.INTERFACE])
209 self.edit_properties.connect('clicked', edit_selected)
211 tree_view.connect('destroy', lambda s: policy.watchers.remove(self.build_tree))
212 policy.watchers.append(self.build_tree)
214 def set_root(self, root):
215 assert isinstance(root, model.Interface)
216 self.root = root
218 def get_icon(self, iface):
219 """Get an icon for this interface. If the icon is in the cache, use that.
220 If not, start a download. If we already started a download (successful or
221 not) do nothing. Returns None if no icon is currently available."""
222 try:
223 return self.cached_icon[iface.uri]
224 except KeyError:
225 path = iface_cache.get_icon_path(iface)
226 if path:
227 try:
228 loader = gtk.gdk.PixbufLoader('png')
229 try:
230 loader.write(file(path).read())
231 finally:
232 loader.close()
233 icon = loader.get_pixbuf()
234 assert icon, "Failed to load cached PNG icon data"
235 except Exception, ex:
236 warn("Failed to load cached PNG icon: %s", ex)
237 return None
238 w = icon.get_width()
239 h = icon.get_height()
240 scale = max(w, h, 1) / ICON_SIZE
241 icon = icon.scale_simple(int(w / scale),
242 int(h / scale),
243 gtk.gdk.INTERP_BILINEAR)
244 self.cached_icon[iface.uri] = icon
245 return icon
246 else:
247 # Try to download the icon
248 fetcher = policy.download_icon(iface)
249 if fetcher:
250 @tasks.async
251 def update_display():
252 yield fetcher
253 try:
254 tasks.check(fetcher)
255 self.build_tree()
256 except Exception, ex:
257 import traceback
258 traceback.print_exc()
259 policy.handler.report_error(ex)
260 update_display()
262 return None
264 def build_tree(self):
265 if policy.original_implementation is None:
266 policy.set_original_implementations()
268 done = {} # Detect cycles
270 self.model.clear()
271 parent = None
272 def add_node(parent, iface):
273 if iface in done:
274 return
275 done[iface] = True
277 iter = self.model.append(parent)
278 self.model[iter][InterfaceBrowser.INTERFACE] = iface
279 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
280 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
281 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
283 impl = policy.implementation.get(iface, None)
284 if impl:
285 old_impl = policy.original_implementation.get(iface, None)
286 version_str = impl.get_version()
287 if old_impl is not None and old_impl is not impl:
288 version_str += " (was " + old_impl.get_version() + ")"
289 self.model[iter][InterfaceBrowser.VERSION] = version_str
291 if policy.get_cached(impl):
292 if impl.id.startswith('/'):
293 fetch = '(local)'
294 elif impl.id.startswith('package:'):
295 fetch = '(package)'
296 else:
297 fetch = '(cached)'
298 else:
299 src = policy.get_best_source(impl)
300 if src:
301 fetch = support.pretty_size(src.size)
302 else:
303 fetch = '(unavailable)'
304 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = fetch
305 if hasattr(impl, 'requires'):
306 children = impl.requires
307 else:
308 children = impl.dependencies
310 for child in children:
311 if isinstance(child, model.InterfaceDependency):
312 add_node(iter, iface_cache.get_interface(child.interface))
313 else:
314 child_iter = self.model.append(parent)
315 self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?'
316 self.model[child_iter][InterfaceBrowser.SUMMARY] = \
317 'Unknown dependency type : %s' % child
318 self.model[child_iter][InterfaceBrowser.ICON] = self.default_icon
319 else:
320 self.model[iter][InterfaceBrowser.VERSION] = '(choose)'
321 add_node(None, self.root)
322 self.tree_view.expand_all()
324 def show_popup_menu(self, iface, bev):
325 import bugs
327 if properties.have_source_for(iface):
328 def compile_cb():
329 import compile
330 compile.compile(iface)
331 else:
332 compile_cb = None
334 menu = gtk.Menu()
335 for label, cb in [(_('Show Feeds'), lambda: properties.edit(iface)),
336 (_('Show Versions'), lambda: properties.edit(iface, show_versions = True)),
337 (_('Report a Bug...'), lambda: bugs.report_bug(policy, iface)),
338 (_('Compile...'), compile_cb)]:
339 item = gtk.MenuItem(label)
340 if cb:
341 item.connect('activate', lambda item, cb=cb: cb())
342 else:
343 item.set_sensitive(False)
344 item.show()
345 menu.append(item)
346 menu.popup(None, None, None, bev.button, bev.time)