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
.support
import tasks
, pretty_size
7 from zeroinstall
.injector
.iface_cache
import iface_cache
8 from zeroinstall
.injector
import model
10 from zeroinstall
.gtkui
.treetips
import TreeTips
11 from zeroinstall
import support
12 from logging
import warn
17 if impl
.user_stability
is None:
18 return impl
.upstream_stability
19 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
20 % {'implementation_user_stability': impl
.user_stability
, 'implementation_upstream_stability': impl
.upstream_stability
}
23 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
25 class InterfaceTips(TreeTips
):
28 def __init__(self
, mainwindow
):
29 self
.mainwindow
= mainwindow
31 def get_tooltip_text(self
):
32 interface
, model_column
= self
.item
34 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
35 return _("Full name: %s") % interface
.uri
36 elif model_column
== InterfaceBrowser
.SUMMARY
:
37 if not interface
.description
:
39 first_para
= interface
.description
.split('\n\n', 1)[0]
40 return first_para
.replace('\n', ' ')
41 elif model_column
is None:
42 return _("Click here for more options...")
44 impl
= self
.mainwindow
.policy
.implementation
.get(interface
, None)
46 return _("No suitable implementation was found. Check the "
47 "interface properties to find out why.")
49 if model_column
== InterfaceBrowser
.VERSION
:
50 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
51 {'version': impl
.get_version(), 'stability': _stability(impl
)}
52 old_impl
= self
.mainwindow
.original_implementation
.get(interface
, None)
53 if old_impl
is not None and old_impl
is not impl
:
54 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
55 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
58 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
60 if self
.mainwindow
.policy
.get_cached(impl
):
61 return _("This version is already stored on your computer.")
63 src
= self
.mainwindow
.policy
.fetcher
.get_best_source(impl
)
65 return _("No downloads available!")
66 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
67 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
69 class MenuIconRenderer(gtk
.GenericCellRenderer
):
71 gtk
.GenericCellRenderer
.__init
__(self
)
72 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
74 def do_set_property(self
, prop
, value
):
75 setattr(self
, prop
.name
, value
)
77 def on_get_size(self
, widget
, cell_area
, layout
= None):
80 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
81 if flags
& gtk
.CELL_RENDERER_PRELIT
:
82 state
= gtk
.STATE_PRELIGHT
84 state
= gtk
.STATE_NORMAL
86 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
87 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
88 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
89 gtk
.ARROW_RIGHT
, True,
90 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
92 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
94 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
95 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
98 def do_set_property(self
, prop
, value
):
99 setattr(self
, prop
.name
, value
)
101 def on_get_size(self
, widget
, cell_area
, layout
= None):
103 layout
= widget
.create_pango_layout(self
.text
)
104 a
, rect
= layout
.get_pixel_extents()
106 pixmap_height
= self
.image
.get_height()
108 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
111 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
114 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
115 layout
= widget
.create_pango_layout(self
.text
)
116 a
, rect
= layout
.get_pixel_extents()
118 if flags
& gtk
.CELL_RENDERER_SELECTED
:
119 state
= gtk
.STATE_SELECTED
120 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
121 state
= gtk
.STATE_PRELIGHT
123 state
= gtk
.STATE_NORMAL
125 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
126 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
128 cell_area
.y
+ image_y
)
130 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
132 widget
.style
.paint_layout(window
, state
, True,
133 expose_area
, widget
, "cellrenderertext",
134 cell_area
.x
+ CELL_TEXT_INDENT
,
135 cell_area
.y
+ text_y
,
138 if gtk
.pygtk_version
< (2, 8, 0):
139 # Note sure exactly which versions need this.
140 # 2.8.0 gives a warning if you include it, though.
141 gobject
.type_register(IconAndTextRenderer
)
142 gobject
.type_register(MenuIconRenderer
)
144 class InterfaceBrowser
:
149 original_implementation
= None
159 columns
= [(_('Component'), INTERFACE_NAME
),
160 (_('Version'), VERSION
),
161 (_('Fetch'), DOWNLOAD_SIZE
),
162 (_('Description'), SUMMARY
),
165 def __init__(self
, policy
, widgets
):
166 tips
= InterfaceTips(self
)
168 tree_view
= widgets
.get_widget('components')
171 self
.cached_icon
= {} # URI -> GdkPixbuf
172 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
173 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
175 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
)
176 self
.tree_view
= tree_view
177 tree_view
.set_model(self
.model
)
181 text
= gtk
.CellRendererText()
183 for name
, model_column
in self
.columns
:
184 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
185 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
187 image
= InterfaceBrowser
.ICON
)
188 elif model_column
== None:
189 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
191 if model_column
== InterfaceBrowser
.SUMMARY
:
192 text_ellip
= gtk
.CellRendererText()
194 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
197 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
198 column
.set_expand(True)
200 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
201 tree_view
.append_column(column
)
202 column_objects
.append(column
)
204 tree_view
.set_enable_search(True)
206 selection
= tree_view
.get_selection()
208 def motion(tree_view
, ev
):
209 if ev
.window
is not tree_view
.get_bin_window():
211 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
215 col_index
= column_objects
.index(pos
[1])
219 col
= self
.columns
[col_index
][1]
220 row
= self
.model
[path
]
221 item
= (row
[InterfaceBrowser
.INTERFACE
], col
)
222 if item
!= tips
.item
:
223 tips
.prime(tree_view
, item
)
227 tree_view
.connect('motion-notify-event', motion
)
228 tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
230 def button_press(tree_view
, bev
):
231 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
234 path
, col
, x
, y
= pos
236 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
237 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
238 selection
.select_path(path
)
239 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
240 self
.show_popup_menu(iface
, bev
)
242 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
244 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
])
245 tree_view
.connect('button-press-event', button_press
)
247 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
248 policy
.watchers
.append(self
.build_tree
)
250 def set_root(self
, root
):
251 assert isinstance(root
, model
.Interface
)
254 def set_update_icons(self
, update_icons
):
256 # Clear icons cache to make sure they're really updated
257 self
.cached_icon
= {}
258 self
.update_icons
= update_icons
260 def _load_icon(self
, path
):
263 loader
= gtk
.gdk
.PixbufLoader('png')
265 loader
.write(file(path
).read())
268 icon
= loader
.get_pixbuf()
269 assert icon
, "Failed to load cached PNG icon data"
270 except Exception, ex
:
271 warn(_("Failed to load cached PNG icon: %s"), ex
)
274 h
= icon
.get_height()
275 scale
= max(w
, h
, 1) / ICON_SIZE
276 icon
= icon
.scale_simple(int(w
/ scale
),
278 gtk
.gdk
.INTERP_BILINEAR
)
281 def get_icon(self
, iface
):
282 """Get an icon for this interface. If the icon is in the cache, use that.
283 If not, start a download. If we already started a download (successful or
284 not) do nothing. Returns None if no icon is currently available."""
286 # Try the in-memory cache
287 return self
.cached_icon
[iface
.uri
]
289 # Try the on-disk cache
290 iconpath
= iface_cache
.get_icon_path(iface
)
293 icon
= self
._load
_icon
(iconpath
)
294 # (if icon is None, cache the fact that we can't load it)
295 self
.cached_icon
[iface
.uri
] = icon
299 # Download a new icon if we don't have one, or if the
300 # user did a 'Refresh'
301 if iconpath
is None or self
.update_icons
:
302 fetcher
= self
.policy
.download_icon(iface
)
304 if iface
.uri
not in self
.cached_icon
:
305 self
.cached_icon
[iface
.uri
] = None # Only try once
308 def update_display():
312 # Try to insert new icon into the cache
313 # If it fails, we'll be left with None in the cached_icon so
314 # we don't try again.
315 iconpath
= iface_cache
.get_icon_path(iface
)
317 self
.cached_icon
[iface
.uri
] = self
._load
_icon
(iconpath
)
320 warn("Failed to download icon for '%s'", iface
)
321 except Exception, ex
:
323 traceback
.print_exc()
324 self
.policy
.handler
.report_error(ex
)
326 # elif fetcher is None: don't store anything in cached_icon
328 # Note: if no icon is available for downloading,
329 # more attempts are made later.
330 # It can happen that no icon is yet available because
331 # the interface was not downloaded yet, in which case
332 # it's desireable to try again once the interface is available
337 def build_tree(self
):
338 if self
.original_implementation
is None:
339 self
.set_original_implementations()
341 done
= {} # Detect cycles
345 def add_node(parent
, iface
):
350 iter = self
.model
.append(parent
)
351 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
352 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
353 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
354 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
356 impl
= self
.policy
.implementation
.get(iface
, None)
358 old_impl
= self
.original_implementation
.get(iface
, None)
359 version_str
= impl
.get_version()
360 if old_impl
is not None and old_impl
is not impl
:
361 version_str
+= _(' (was %s)') % old_impl
.get_version()
362 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
364 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
365 children
= self
.policy
.solver
.requires
[iface
]
367 for child
in children
:
368 if isinstance(child
, model
.InterfaceDependency
):
369 add_node(iter, iface_cache
.get_interface(child
.interface
))
371 child_iter
= self
.model
.append(parent
)
372 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
373 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
374 _('Unknown dependency type : %s') % child
375 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
377 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(choose)')
378 add_node(None, self
.root
)
379 self
.tree_view
.expand_all()
381 def show_popup_menu(self
, iface
, bev
):
385 have_source
= properties
.have_source_for(self
.policy
, iface
)
388 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
389 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
390 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
391 item
= gtk
.MenuItem(label
)
393 item
.connect('activate', lambda item
, cb
=cb
: cb())
395 item
.set_sensitive(False)
399 item
= gtk
.MenuItem(_('Compile'))
403 compile_menu
= gtk
.Menu()
404 item
.set_submenu(compile_menu
)
406 item
= gtk
.MenuItem(_('Automatic'))
407 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= True))
409 compile_menu
.append(item
)
411 item
= gtk
.MenuItem(_('Manual...'))
412 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= False))
414 compile_menu
.append(item
)
416 item
.set_sensitive(False)
418 menu
.popup(None, None, None, bev
.button
, bev
.time
)
420 def set_original_implementations(self
):
421 assert self
.original_implementation
is None
422 self
.original_implementation
= self
.policy
.implementation
.copy()
424 def update_download_status(self
):
425 """Called at regular intervals while there are downloads in progress,
426 and once at the end. Also called when things are added to the store.
427 Update the TreeView with the interfaces."""
429 for dl
in self
.policy
.handler
.monitored_downloads
.values():
431 if dl
.hint
not in hints
:
433 hints
[dl
.hint
].append(dl
)
435 selections
= self
.policy
.solver
.selections
440 for x
in walk(self
.model
.iter_children(it
)): yield x
441 it
= self
.model
.iter_next(it
)
443 for row
in walk(self
.model
.get_iter_root()):
444 iface
= row
[InterfaceBrowser
.INTERFACE
]
446 # Is this interface the download's hint?
447 downloads
= hints
.get(iface
, []) # The interface itself
448 downloads
+= hints
.get(iface
.uri
, []) # The main feed
449 for feed
in iface
.feeds
:
450 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
451 impl
= selections
.get(iface
, None)
453 downloads
+= hints
.get(impl
, []) # The chosen implementation
460 expected
= (expected
or 0) + dl
.expected_size
461 so_far
+= dl
.get_bytes_downloaded_so_far()
463 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
464 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
466 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
468 summary
= ngettext("(downloading %(downloaded)s/unknown)",
469 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
471 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
472 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
474 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
475 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary