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
, reader
10 from zeroinstall
.gtkui
.icon
import load_icon
11 from zeroinstall
import support
12 from logging
import warn
, info
17 if impl
.user_stability
is None:
18 return _(str(impl
.upstream_stability
))
19 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
20 % {'implementation_user_stability': _(str(impl
.user_stability
)),
21 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
24 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
26 def get_tooltip_text(mainwindow
, interface
, model_column
):
28 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
29 return _("Full name: %s") % interface
.uri
30 elif model_column
== InterfaceBrowser
.SUMMARY
:
31 if not interface
.description
:
32 return _("(no description available)")
33 first_para
= interface
.description
.split('\n\n', 1)[0]
34 return first_para
.replace('\n', ' ')
35 elif model_column
is None:
36 return _("Click here for more options...")
38 impl
= mainwindow
.policy
.implementation
.get(interface
, None)
40 return _("No suitable version was found. Double-click "
41 "here to find out why.")
43 if model_column
== InterfaceBrowser
.VERSION
:
44 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
45 {'version': impl
.get_version(), 'stability': _stability(impl
)}
46 old_impl
= mainwindow
.original_implementation
.get(interface
, None)
47 if old_impl
is not None and old_impl
is not impl
:
48 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
49 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
52 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
54 if mainwindow
.policy
.get_cached(impl
):
55 return _("This version is already stored on your computer.")
57 src
= mainwindow
.policy
.fetcher
.get_best_source(impl
)
59 return _("No downloads available!")
60 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
61 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
63 class MenuIconRenderer(gtk
.GenericCellRenderer
):
65 gtk
.GenericCellRenderer
.__init
__(self
)
66 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
68 def do_set_property(self
, prop
, value
):
69 setattr(self
, prop
.name
, value
)
71 def on_get_size(self
, widget
, cell_area
, layout
= None):
74 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
75 if flags
& gtk
.CELL_RENDERER_PRELIT
:
76 state
= gtk
.STATE_PRELIGHT
78 state
= gtk
.STATE_NORMAL
80 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
81 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
82 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
83 gtk
.ARROW_RIGHT
, True,
84 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
86 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
88 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
89 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
92 def do_set_property(self
, prop
, value
):
93 setattr(self
, prop
.name
, value
)
95 def on_get_size(self
, widget
, cell_area
, layout
= None):
97 layout
= widget
.create_pango_layout(self
.text
)
98 a
, rect
= layout
.get_pixel_extents()
100 pixmap_height
= self
.image
.get_height()
102 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
105 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
108 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
109 layout
= widget
.create_pango_layout(self
.text
)
110 a
, rect
= layout
.get_pixel_extents()
112 if flags
& gtk
.CELL_RENDERER_SELECTED
:
113 state
= gtk
.STATE_SELECTED
114 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
115 state
= gtk
.STATE_PRELIGHT
117 state
= gtk
.STATE_NORMAL
119 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
120 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
122 cell_area
.y
+ image_y
)
124 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
126 widget
.style
.paint_layout(window
, state
, True,
127 expose_area
, widget
, "cellrenderertext",
128 cell_area
.x
+ CELL_TEXT_INDENT
,
129 cell_area
.y
+ text_y
,
132 if gtk
.pygtk_version
< (2, 8, 0):
133 # Note sure exactly which versions need this.
134 # 2.8.0 gives a warning if you include it, though.
135 gobject
.type_register(IconAndTextRenderer
)
136 gobject
.type_register(MenuIconRenderer
)
138 class InterfaceBrowser
:
143 original_implementation
= None
154 columns
= [(_('Component'), INTERFACE_NAME
),
155 (_('Version'), VERSION
),
156 (_('Fetch'), DOWNLOAD_SIZE
),
157 (_('Description'), SUMMARY
),
160 def __init__(self
, policy
, widgets
):
161 tree_view
= widgets
.get_widget('components')
162 tree_view
.set_property('has-tooltip', True)
163 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
164 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
165 pos
= tree_view
.get_path_at_pos(x
, y
)
167 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
170 col_index
= column_objects
.index(pos
[1])
174 col
= self
.columns
[col_index
][1]
175 row
= self
.model
[path
]
176 tooltip
.set_text(get_tooltip_text(self
, row
[InterfaceBrowser
.INTERFACE
], col
))
180 tree_view
.connect('query-tooltip', callback
)
183 self
.cached_icon
= {} # URI -> GdkPixbuf
184 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
185 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
187 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str)
188 self
.tree_view
= tree_view
189 tree_view
.set_model(self
.model
)
193 text
= gtk
.CellRendererText()
194 coloured_text
= gtk
.CellRendererText()
196 for name
, model_column
in self
.columns
:
197 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
198 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
200 image
= InterfaceBrowser
.ICON
)
201 elif model_column
== None:
202 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
204 if model_column
== InterfaceBrowser
.SUMMARY
:
205 text_ellip
= gtk
.CellRendererText()
207 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
210 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
211 column
.set_expand(True)
212 elif model_column
== InterfaceBrowser
.VERSION
:
213 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
214 background
= InterfaceBrowser
.BACKGROUND
)
216 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
217 tree_view
.append_column(column
)
218 column_objects
.append(column
)
220 tree_view
.set_enable_search(True)
222 selection
= tree_view
.get_selection()
224 def button_press(tree_view
, bev
):
225 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
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
)
236 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
238 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
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
)
248 def set_update_icons(self
, update_icons
):
250 # Clear icons cache to make sure they're really updated
251 self
.cached_icon
= {}
252 self
.update_icons
= update_icons
254 def get_icon(self
, iface
):
255 """Get an icon for this interface. If the icon is in the cache, use that.
256 If not, start a download. If we already started a download (successful or
257 not) do nothing. Returns None if no icon is currently available."""
259 # Try the in-memory cache
260 return self
.cached_icon
[iface
.uri
]
262 # Try the on-disk cache
263 iconpath
= iface_cache
.get_icon_path(iface
)
266 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
267 # (if icon is None, cache the fact that we can't load it)
268 self
.cached_icon
[iface
.uri
] = icon
272 # Download a new icon if we don't have one, or if the
273 # user did a 'Refresh'
274 if iconpath
is None or self
.update_icons
:
275 fetcher
= self
.policy
.download_icon(iface
)
277 if iface
.uri
not in self
.cached_icon
:
278 self
.cached_icon
[iface
.uri
] = None # Only try once
281 def update_display():
285 # Try to insert new icon into the cache
286 # If it fails, we'll be left with None in the cached_icon so
287 # we don't try again.
288 iconpath
= iface_cache
.get_icon_path(iface
)
290 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
293 warn("Failed to download icon for '%s'", iface
)
294 except Exception, ex
:
296 traceback
.print_exc()
297 self
.policy
.handler
.report_error(ex
)
299 # elif fetcher is None: don't store anything in cached_icon
301 # Note: if no icon is available for downloading,
302 # more attempts are made later.
303 # It can happen that no icon is yet available because
304 # the interface was not downloaded yet, in which case
305 # it's desireable to try again once the interface is available
310 def build_tree(self
):
311 if self
.original_implementation
is None:
312 self
.set_original_implementations()
314 done
= {} # Detect cycles
318 commands
= self
.policy
.solver
.selections
.commands
319 def add_node(parent
, iface
, command
):
320 # (command is the index into commands, if any)
325 iter = self
.model
.append(parent
)
326 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
327 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
328 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
329 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
331 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
334 old_impl
= self
.original_implementation
.get(iface
, None)
335 version_str
= impl
.get_version()
336 if old_impl
is not None and old_impl
.id != impl
.id:
337 version_str
+= _(' (was %s)') % old_impl
.get_version()
338 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
340 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
342 deps
= sel
.dependencies
343 if command
is not None:
344 deps
+= commands
[command
].requires
346 if isinstance(child
, model
.InterfaceDependency
):
347 if child
.qdom
.name
== 'runner':
348 child_command
= command
+ 1
351 add_node(iter, iface_cache
.get_interface(child
.interface
), child_command
)
353 child_iter
= self
.model
.append(parent
)
354 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
355 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
356 _('Unknown dependency type : %s') % child
357 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
359 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)')
360 self
.model
[iter][InterfaceBrowser
.BACKGROUND
] = '#f88'
362 add_node(None, self
.root
, 0)
364 # Nothing could be selected, or no command requested
365 add_node(None, self
.root
, None)
366 self
.tree_view
.expand_all()
368 def show_popup_menu(self
, iface
, bev
):
371 have_source
= properties
.have_source_for(self
.policy
, iface
)
374 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
375 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
376 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
377 item
= gtk
.MenuItem(label
)
379 item
.connect('activate', lambda item
, cb
=cb
: cb())
381 item
.set_sensitive(False)
385 item
= gtk
.MenuItem(_('Compile'))
389 compile_menu
= gtk
.Menu()
390 item
.set_submenu(compile_menu
)
392 item
= gtk
.MenuItem(_('Automatic'))
393 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
395 compile_menu
.append(item
)
397 item
= gtk
.MenuItem(_('Manual...'))
398 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
400 compile_menu
.append(item
)
402 item
.set_sensitive(False)
404 menu
.popup(None, None, None, bev
.button
, bev
.time
)
406 def compile(self
, interface
, autocompile
= False):
409 # A new local feed may have been registered, so reload it from the disk cache
410 info(_("0compile command completed successfully. Reloading interface details."))
411 reader
.update_from_cache(interface
)
412 for feed
in interface
.extra_feeds
:
413 iface_cache
.get_feed(feed
.uri
, force
= True)
414 self
.policy
.recalculate()
415 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
417 def set_original_implementations(self
):
418 assert self
.original_implementation
is None
419 self
.original_implementation
= self
.policy
.implementation
.copy()
421 def update_download_status(self
):
422 """Called at regular intervals while there are downloads in progress,
423 and once at the end. Also called when things are added to the store.
424 Update the TreeView with the interfaces."""
426 # A download may be for a feed, an interface or an implementation.
427 # Create the reverse mapping (item -> download)
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 self
.policy
.usable_feeds(iface
):
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