Use iface_cache.get_interface() rather than policy.get_interface() in several
[zeroinstall.git] / zeroinstall / 0launch-gui / iface_browser.py
blob464d4241ffe7aedbaef47808b1618d7851d1f4cf
1 import gtk, gobject
3 from zeroinstall.injector import basedir
4 from zeroinstall.injector.iface_cache import iface_cache
5 from zeroinstall.injector.model import Interface, escape
6 import properties
7 from treetips import TreeTips
8 from gui import policy, pretty_size
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 def get_tooltip_text(self, item):
22 interface, model_column = item
23 assert interface
24 if model_column == InterfaceBrowser.INTERFACE_NAME:
25 return _("Full name: %s") % interface.uri
26 elif model_column == InterfaceBrowser.SUMMARY:
27 if not interface.description:
28 return None
29 first_para = interface.description.split('\n\n', 1)[0]
30 return first_para.replace('\n', ' ')
32 impl = policy.implementation.get(interface, None)
33 if not impl:
34 return _("No suitable implementation was found. Check the "
35 "interface properties to find out why.")
37 if model_column == InterfaceBrowser.VERSION:
38 text = _("Currently preferred version: %s (%s)") % \
39 (impl.get_version(), _stability(impl))
40 old_impl = policy.original_implementation.get(interface, None)
41 if old_impl is not None and old_impl is not impl:
42 text += _('\nPreviously preferred version: %s (%s)') % \
43 (old_impl.get_version(), _stability(old_impl))
44 return text
46 assert model_column == InterfaceBrowser.DOWNLOAD_SIZE
48 if policy.get_cached(impl):
49 return _("This version is already stored on your computer.")
50 else:
51 src = policy.get_best_source(impl)
52 if not src:
53 return _("No downloads available!")
54 return _("Need to download %s (%s bytes)") % \
55 (pretty_size(src.size), src.size)
57 tips = InterfaceTips()
59 class IconAndTextRenderer(gtk.GenericCellRenderer):
60 __gproperties__ = {
61 "image": (gobject.TYPE_OBJECT, "Image", "Image", gobject.PARAM_READWRITE),
62 "text": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
65 def do_set_property(self, prop, value):
66 setattr(self, prop.name, value)
68 def on_get_size(self, widget, cell_area, layout = None):
69 if not layout:
70 layout = widget.create_pango_layout(self.text)
71 a, rect = layout.get_pixel_extents()
73 pixmap_height = self.image.get_height()
75 both_height = max(rect[1] + rect[3], pixmap_height)
77 return (0, 0,
78 rect[0] + rect[2] + CELL_TEXT_INDENT,
79 both_height)
81 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
82 layout = widget.create_pango_layout(self.text)
83 a, rect = layout.get_pixel_extents()
85 if flags & gtk.CELL_RENDERER_SELECTED:
86 state = gtk.STATE_SELECTED
87 elif flags & gtk.CELL_RENDERER_PRELIT:
88 state = gtk.STATE_PRELIGHT
89 else:
90 state = gtk.STATE_NORMAL
92 image_y = int(0.5 * (cell_area.height - self.image.get_height()))
93 window.draw_pixbuf(widget.style.white_gc, self.image, 0, 0,
94 cell_area.x,
95 cell_area.y + image_y)
97 text_y = int(0.5 * (cell_area.height - (rect[1] + rect[3])))
99 widget.style.paint_layout(window, state, True,
100 expose_area, widget, "cellrenderertext",
101 cell_area.x + CELL_TEXT_INDENT,
102 cell_area.y + text_y,
103 layout)
105 if gtk.pygtk_version < (2, 8, 0):
106 # Note sure exactly which versions need this.
107 # 2.8.0 gives a warning if you include it, though.
108 gobject.type_register(IconAndTextRenderer)
110 class InterfaceBrowser(gtk.ScrolledWindow):
111 model = None
112 root = None
113 edit_properties = None
114 cached_icon = None
116 INTERFACE = 0
117 INTERFACE_NAME = 1
118 VERSION = 2
119 SUMMARY = 3
120 DOWNLOAD_SIZE = 4
121 ICON = 5
123 columns = [(_('Interface'), INTERFACE_NAME),
124 (_('Version'), VERSION),
125 (_('Fetch'), DOWNLOAD_SIZE),
126 (_('Description'), SUMMARY)]
128 def __init__(self):
129 gtk.ScrolledWindow.__init__(self)
131 self.cached_icon = {} # URI -> GdkPixbuf
132 self.default_icon = self.style.lookup_icon_set(gtk.STOCK_EXECUTE).render_icon(self.style,
133 gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_SMALL_TOOLBAR, self, None)
135 self.edit_properties = gtk.Action('edit_properties',
136 'Interface Properties...',
137 'Set which implementation of this interface to use.',
138 gtk.STOCK_PROPERTIES)
139 self.edit_properties.set_property('sensitive', False)
141 self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
142 self.set_shadow_type(gtk.SHADOW_IN)
144 self.model = gtk.TreeStore(object, str, str, str, str, gtk.gdk.Pixbuf)
145 self.tree_view = tree_view = gtk.TreeView(self.model)
147 column_objects = []
149 text = gtk.CellRendererText()
151 for name, model_column in self.columns:
152 if model_column == InterfaceBrowser.INTERFACE_NAME:
153 column = gtk.TreeViewColumn(name, IconAndTextRenderer(),
154 text = model_column,
155 image = InterfaceBrowser.ICON)
156 else:
157 column = gtk.TreeViewColumn(name, text, text = model_column)
158 tree_view.append_column(column)
159 column_objects.append(column)
161 self.add(tree_view)
162 tree_view.show()
164 tree_view.set_enable_search(True)
166 selection = tree_view.get_selection()
168 def motion(tree_view, ev):
169 if ev.window is not tree_view.get_bin_window():
170 return False
171 pos = tree_view.get_path_at_pos(int(ev.x), int(ev.y))
172 if pos:
173 path = pos[0]
174 try:
175 col_index = column_objects.index(pos[1])
176 except ValueError:
177 tips.hide()
178 else:
179 col = self.columns[col_index][1]
180 row = self.model[path]
181 item = (row[InterfaceBrowser.INTERFACE], col)
182 if item != tips.item:
183 tips.prime(tree_view, item)
184 else:
185 tips.hide()
187 tree_view.connect('motion-notify-event', motion)
188 tree_view.connect('leave-notify-event', lambda tv, ev: tips.hide())
190 def sel_changed(sel):
191 store, iter = sel.get_selected()
192 self.edit_properties.set_property('sensitive', iter != None)
193 selection.connect('changed', sel_changed)
195 def button_press(tree_view, bev):
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('activate', edit_selected)
211 self.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, Interface)
216 self.root = root
217 policy.recalculate() # Calls build_tree
219 def get_icon(self, iface):
220 """Get an icon for this interface. If the icon is in the cache, use that.
221 If not, start a download. If we already started a download (successful or
222 not) do nothing. Returns None if no icon is currently available."""
223 try:
224 return self.cached_icon[iface.uri]
225 except KeyError:
226 if not hasattr(policy, 'get_icon_path'):
227 return None # injector < 0.19
228 path = policy.get_icon_path(iface)
229 if path:
230 try:
231 loader = gtk.gdk.PixbufLoader('png')
232 try:
233 loader.write(file(path).read())
234 finally:
235 loader.close()
236 icon = loader.get_pixbuf()
237 except Exception, ex:
238 warn("Failed to load cached PNG icon: %s", ex)
239 return None
240 w = icon.get_width()
241 h = icon.get_height()
242 scale = max(w, h, 1) / ICON_SIZE
243 icon = icon.scale_simple(int(w / scale),
244 int(h / scale),
245 gtk.gdk.INTERP_BILINEAR)
246 self.cached_icon[iface.uri] = icon
247 return icon
249 return None
251 def build_tree(self):
252 if policy.original_implementation is None:
253 policy.set_original_implementations()
255 done = {} # Detect cycles
257 self.model.clear()
258 parent = None
259 def add_node(parent, iface):
260 if iface in done:
261 return
262 done[iface] = True
264 iter = self.model.append(parent)
265 self.model[iter][InterfaceBrowser.INTERFACE] = iface
266 self.model[iter][InterfaceBrowser.INTERFACE_NAME] = iface.get_name()
267 self.model[iter][InterfaceBrowser.SUMMARY] = iface.summary
268 self.model[iter][InterfaceBrowser.ICON] = self.get_icon(iface) or self.default_icon
270 impl = policy.implementation.get(iface, None)
271 if impl:
272 old_impl = policy.original_implementation.get(iface, None)
273 version_str = impl.get_version()
274 if old_impl is not None and old_impl is not impl:
275 version_str += " (was " + old_impl.get_version() + ")"
276 self.model[iter][InterfaceBrowser.VERSION] = version_str
278 if policy.get_cached(impl):
279 if impl.id.startswith('/'):
280 fetch = '(local)'
281 else:
282 fetch = '(cached)'
283 else:
284 src = policy.get_best_source(impl)
285 if src:
286 fetch = pretty_size(src.size)
287 else:
288 fetch = '(unavailable)'
289 self.model[iter][InterfaceBrowser.DOWNLOAD_SIZE] = fetch
290 for child in impl.dependencies.values():
291 add_node(iter, iface_cache.get_interface(child.interface))
292 else:
293 self.model[iter][InterfaceBrowser.VERSION] = '(choose)'
294 add_node(None, self.root)
295 self.tree_view.expand_all()