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
import model
, reader
9 from zeroinstall
.gtkui
.icon
import load_icon
10 from zeroinstall
import support
11 from logging
import warn
, info
16 if impl
.user_stability
is None:
17 return _(str(impl
.upstream_stability
))
18 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
19 % {'implementation_user_stability': _(str(impl
.user_stability
)),
20 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
23 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
25 def get_tooltip_text(mainwindow
, interface
, model_column
):
27 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
28 return _("Full name: %s") % interface
.uri
29 elif model_column
== InterfaceBrowser
.SUMMARY
:
30 if not interface
.description
:
31 return _("(no description available)")
32 first_para
= interface
.description
.split('\n\n', 1)[0]
33 return first_para
.replace('\n', ' ')
34 elif model_column
is None:
35 return _("Click here for more options...")
37 impl
= mainwindow
.policy
.implementation
.get(interface
, None)
39 return _("No suitable version was found. Double-click "
40 "here to find out why.")
42 if model_column
== InterfaceBrowser
.VERSION
:
43 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
44 {'version': impl
.get_version(), 'stability': _stability(impl
)}
45 old_impl
= mainwindow
.original_implementation
.get(interface
, None)
46 if old_impl
is not None and old_impl
is not impl
:
47 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
48 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
51 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
53 if mainwindow
.policy
.get_cached(impl
):
54 return _("This version is already stored on your computer.")
56 src
= mainwindow
.policy
.fetcher
.get_best_source(impl
)
58 return _("No downloads available!")
59 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
60 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
62 class MenuIconRenderer(gtk
.GenericCellRenderer
):
64 gtk
.GenericCellRenderer
.__init
__(self
)
65 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
67 def do_set_property(self
, prop
, value
):
68 setattr(self
, prop
.name
, value
)
70 def on_get_size(self
, widget
, cell_area
, layout
= None):
73 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
74 if flags
& gtk
.CELL_RENDERER_PRELIT
:
75 state
= gtk
.STATE_PRELIGHT
77 state
= gtk
.STATE_NORMAL
79 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
80 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
81 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
82 gtk
.ARROW_RIGHT
, True,
83 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
85 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
87 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
88 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
91 def do_set_property(self
, prop
, value
):
92 setattr(self
, prop
.name
, value
)
94 def on_get_size(self
, widget
, cell_area
, layout
= None):
96 layout
= widget
.create_pango_layout(self
.text
)
97 a
, rect
= layout
.get_pixel_extents()
99 pixmap_height
= self
.image
.get_height()
101 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
104 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
107 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
108 layout
= widget
.create_pango_layout(self
.text
)
109 a
, rect
= layout
.get_pixel_extents()
111 if flags
& gtk
.CELL_RENDERER_SELECTED
:
112 state
= gtk
.STATE_SELECTED
113 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
114 state
= gtk
.STATE_PRELIGHT
116 state
= gtk
.STATE_NORMAL
118 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
119 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
121 cell_area
.y
+ image_y
)
123 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
125 widget
.style
.paint_layout(window
, state
, True,
126 expose_area
, widget
, "cellrenderertext",
127 cell_area
.x
+ CELL_TEXT_INDENT
,
128 cell_area
.y
+ text_y
,
131 if gtk
.pygtk_version
< (2, 8, 0):
132 # Note sure exactly which versions need this.
133 # 2.8.0 gives a warning if you include it, though.
134 gobject
.type_register(IconAndTextRenderer
)
135 gobject
.type_register(MenuIconRenderer
)
137 class InterfaceBrowser
:
142 original_implementation
= None
153 columns
= [(_('Component'), INTERFACE_NAME
),
154 (_('Version'), VERSION
),
155 (_('Fetch'), DOWNLOAD_SIZE
),
156 (_('Description'), SUMMARY
),
159 def __init__(self
, policy
, widgets
):
160 tree_view
= widgets
.get_widget('components')
161 tree_view
.set_property('has-tooltip', True)
162 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
163 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
164 pos
= tree_view
.get_path_at_pos(x
, y
)
166 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
169 col_index
= column_objects
.index(pos
[1])
173 col
= self
.columns
[col_index
][1]
174 row
= self
.model
[path
]
175 tooltip
.set_text(get_tooltip_text(self
, row
[InterfaceBrowser
.INTERFACE
], col
))
179 tree_view
.connect('query-tooltip', callback
)
182 self
.cached_icon
= {} # URI -> GdkPixbuf
183 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
184 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
186 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str)
187 self
.tree_view
= tree_view
188 tree_view
.set_model(self
.model
)
192 text
= gtk
.CellRendererText()
193 coloured_text
= gtk
.CellRendererText()
195 for name
, model_column
in self
.columns
:
196 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
197 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
199 image
= InterfaceBrowser
.ICON
)
200 elif model_column
== None:
201 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
203 if model_column
== InterfaceBrowser
.SUMMARY
:
204 text_ellip
= gtk
.CellRendererText()
206 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
209 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
210 column
.set_expand(True)
211 elif model_column
== InterfaceBrowser
.VERSION
:
212 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
213 background
= InterfaceBrowser
.BACKGROUND
)
215 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
216 tree_view
.append_column(column
)
217 column_objects
.append(column
)
219 tree_view
.set_enable_search(True)
221 selection
= tree_view
.get_selection()
223 def button_press(tree_view
, bev
):
224 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
227 path
, col
, x
, y
= pos
229 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
230 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
231 selection
.select_path(path
)
232 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
233 self
.show_popup_menu(iface
, bev
)
235 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
237 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
238 tree_view
.connect('button-press-event', button_press
)
240 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
241 policy
.watchers
.append(self
.build_tree
)
243 def set_root(self
, root
):
244 assert isinstance(root
, model
.Interface
)
247 def set_update_icons(self
, update_icons
):
249 # Clear icons cache to make sure they're really updated
250 self
.cached_icon
= {}
251 self
.update_icons
= update_icons
253 def get_icon(self
, iface
):
254 """Get an icon for this interface. If the icon is in the cache, use that.
255 If not, start a download. If we already started a download (successful or
256 not) do nothing. Returns None if no icon is currently available."""
258 # Try the in-memory cache
259 return self
.cached_icon
[iface
.uri
]
261 # Try the on-disk cache
262 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
265 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
266 # (if icon is None, cache the fact that we can't load it)
267 self
.cached_icon
[iface
.uri
] = icon
271 # Download a new icon if we don't have one, or if the
272 # user did a 'Refresh'
273 if iconpath
is None or self
.update_icons
:
274 fetcher
= self
.policy
.download_icon(iface
)
276 if iface
.uri
not in self
.cached_icon
:
277 self
.cached_icon
[iface
.uri
] = None # Only try once
280 def update_display():
284 # Try to insert new icon into the cache
285 # If it fails, we'll be left with None in the cached_icon so
286 # we don't try again.
287 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
289 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
292 warn("Failed to download icon for '%s'", iface
)
293 except Exception, ex
:
295 traceback
.print_exc()
296 self
.policy
.handler
.report_error(ex
)
298 # elif fetcher is None: don't store anything in cached_icon
300 # Note: if no icon is available for downloading,
301 # more attempts are made later.
302 # It can happen that no icon is yet available because
303 # the interface was not downloaded yet, in which case
304 # it's desireable to try again once the interface is available
309 def build_tree(self
):
310 iface_cache
= self
.policy
.config
.iface_cache
312 if self
.original_implementation
is None:
313 self
.set_original_implementations()
315 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 self
.policy
.config
.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