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
, main_feed
, model_column
):
27 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
28 return _("Full name: %s") % interface
.uri
29 elif model_column
== InterfaceBrowser
.SUMMARY
:
30 if main_feed
is None or not main_feed
.description
:
31 return _("(no description available)")
32 first_para
= main_feed
.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 iface
= row
[InterfaceBrowser
.INTERFACE
]
176 main_feed
= self
.policy
.config
.iface_cache
.get_feed(iface
.uri
)
177 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
181 tree_view
.connect('query-tooltip', callback
)
184 self
.cached_icon
= {} # URI -> GdkPixbuf
185 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
186 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
188 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str)
189 self
.tree_view
= tree_view
190 tree_view
.set_model(self
.model
)
194 text
= gtk
.CellRendererText()
195 coloured_text
= gtk
.CellRendererText()
197 for name
, model_column
in self
.columns
:
198 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
199 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
201 image
= InterfaceBrowser
.ICON
)
202 elif model_column
== None:
203 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
205 if model_column
== InterfaceBrowser
.SUMMARY
:
206 text_ellip
= gtk
.CellRendererText()
208 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
211 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
212 column
.set_expand(True)
213 elif model_column
== InterfaceBrowser
.VERSION
:
214 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
215 background
= InterfaceBrowser
.BACKGROUND
)
217 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
218 tree_view
.append_column(column
)
219 column_objects
.append(column
)
221 tree_view
.set_enable_search(True)
223 selection
= tree_view
.get_selection()
225 def button_press(tree_view
, bev
):
226 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
229 path
, col
, x
, y
= pos
231 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
232 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
233 selection
.select_path(path
)
234 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
235 self
.show_popup_menu(iface
, bev
)
237 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
239 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
240 tree_view
.connect('button-press-event', button_press
)
242 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
243 policy
.watchers
.append(self
.build_tree
)
245 def set_root(self
, root
):
246 assert isinstance(root
, model
.Interface
)
249 def set_update_icons(self
, update_icons
):
251 # Clear icons cache to make sure they're really updated
252 self
.cached_icon
= {}
253 self
.update_icons
= update_icons
255 def get_icon(self
, iface
):
256 """Get an icon for this interface. If the icon is in the cache, use that.
257 If not, start a download. If we already started a download (successful or
258 not) do nothing. Returns None if no icon is currently available."""
260 # Try the in-memory cache
261 return self
.cached_icon
[iface
.uri
]
263 # Try the on-disk cache
264 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
267 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
268 # (if icon is None, cache the fact that we can't load it)
269 self
.cached_icon
[iface
.uri
] = icon
273 # Download a new icon if we don't have one, or if the
274 # user did a 'Refresh'
275 if iconpath
is None or self
.update_icons
:
276 fetcher
= self
.policy
.download_icon(iface
)
278 if iface
.uri
not in self
.cached_icon
:
279 self
.cached_icon
[iface
.uri
] = None # Only try once
282 def update_display():
286 # Try to insert new icon into the cache
287 # If it fails, we'll be left with None in the cached_icon so
288 # we don't try again.
289 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
291 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
294 warn("Failed to download icon for '%s'", iface
)
295 except Exception, ex
:
297 traceback
.print_exc()
298 self
.policy
.handler
.report_error(ex
)
300 # elif fetcher is None: don't store anything in cached_icon
302 # Note: if no icon is available for downloading,
303 # more attempts are made later.
304 # It can happen that no icon is yet available because
305 # the interface was not downloaded yet, in which case
306 # it's desireable to try again once the interface is available
311 def build_tree(self
):
312 iface_cache
= self
.policy
.config
.iface_cache
314 if self
.original_implementation
is None:
315 self
.set_original_implementations()
317 done
= {} # Detect cycles
320 commands
= self
.policy
.solver
.selections
.commands
321 def add_node(parent
, iface
, command
):
322 # (command is the index into commands, if any)
327 main_feed
= iface_cache
.get_feed(iface
.uri
)
329 name
= main_feed
.get_name()
330 summary
= main_feed
.summary
332 name
= iface
.get_name()
335 iter = self
.model
.append(parent
)
336 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
337 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
338 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
339 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
341 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
344 old_impl
= self
.original_implementation
.get(iface
, None)
345 version_str
= impl
.get_version()
346 if old_impl
is not None and old_impl
.id != impl
.id:
347 version_str
+= _(' (was %s)') % old_impl
.get_version()
348 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
350 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
352 deps
= sel
.dependencies
353 if command
is not None:
354 deps
+= commands
[command
].requires
356 if isinstance(child
, model
.InterfaceDependency
):
357 if child
.qdom
.name
== 'runner':
358 child_command
= command
+ 1
361 add_node(iter, iface_cache
.get_interface(child
.interface
), child_command
)
363 child_iter
= self
.model
.append(parent
)
364 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
365 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
366 _('Unknown dependency type : %s') % child
367 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
369 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)')
370 self
.model
[iter][InterfaceBrowser
.BACKGROUND
] = '#f88'
372 add_node(None, self
.root
, 0)
374 # Nothing could be selected, or no command requested
375 add_node(None, self
.root
, None)
376 self
.tree_view
.expand_all()
378 def show_popup_menu(self
, iface
, bev
):
381 have_source
= properties
.have_source_for(self
.policy
, iface
)
384 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
385 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
386 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
387 item
= gtk
.MenuItem(label
)
389 item
.connect('activate', lambda item
, cb
=cb
: cb())
391 item
.set_sensitive(False)
395 item
= gtk
.MenuItem(_('Compile'))
399 compile_menu
= gtk
.Menu()
400 item
.set_submenu(compile_menu
)
402 item
= gtk
.MenuItem(_('Automatic'))
403 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
405 compile_menu
.append(item
)
407 item
= gtk
.MenuItem(_('Manual...'))
408 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
410 compile_menu
.append(item
)
412 item
.set_sensitive(False)
414 menu
.popup(None, None, None, bev
.button
, bev
.time
)
416 def compile(self
, interface
, autocompile
= False):
419 # A new local feed may have been registered, so reload it from the disk cache
420 info(_("0compile command completed successfully. Reloading interface details."))
421 reader
.update_from_cache(interface
)
422 for feed
in interface
.extra_feeds
:
423 self
.policy
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
424 self
.policy
.recalculate()
425 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
427 def set_original_implementations(self
):
428 assert self
.original_implementation
is None
429 self
.original_implementation
= self
.policy
.implementation
.copy()
431 def update_download_status(self
):
432 """Called at regular intervals while there are downloads in progress,
433 and once at the end. Also called when things are added to the store.
434 Update the TreeView with the interfaces."""
436 # A download may be for a feed, an interface or an implementation.
437 # Create the reverse mapping (item -> download)
439 for dl
in self
.policy
.handler
.monitored_downloads
.values():
441 if dl
.hint
not in hints
:
443 hints
[dl
.hint
].append(dl
)
445 selections
= self
.policy
.solver
.selections
450 for x
in walk(self
.model
.iter_children(it
)): yield x
451 it
= self
.model
.iter_next(it
)
453 for row
in walk(self
.model
.get_iter_root()):
454 iface
= row
[InterfaceBrowser
.INTERFACE
]
456 # Is this interface the download's hint?
457 downloads
= hints
.get(iface
, []) # The interface itself
458 downloads
+= hints
.get(iface
.uri
, []) # The main feed
459 for feed
in self
.policy
.usable_feeds(iface
):
460 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
461 impl
= selections
.get(iface
, None)
463 downloads
+= hints
.get(impl
, []) # The chosen implementation
470 expected
= (expected
or 0) + dl
.expected_size
471 so_far
+= dl
.get_bytes_downloaded_so_far()
473 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
474 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
476 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
478 summary
= ngettext("(downloading %(downloaded)s/unknown)",
479 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
481 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
482 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
484 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
485 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary