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 implementation was found. Check the "
41 "interface properties 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
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
)
187 self
.tree_view
= tree_view
188 tree_view
.set_model(self
.model
)
192 text
= gtk
.CellRendererText()
194 for name
, model_column
in self
.columns
:
195 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
196 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
198 image
= InterfaceBrowser
.ICON
)
199 elif model_column
== None:
200 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
202 if model_column
== InterfaceBrowser
.SUMMARY
:
203 text_ellip
= gtk
.CellRendererText()
205 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
208 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
209 column
.set_expand(True)
211 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
212 tree_view
.append_column(column
)
213 column_objects
.append(column
)
215 tree_view
.set_enable_search(True)
217 selection
= tree_view
.get_selection()
219 def button_press(tree_view
, bev
):
220 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
223 path
, col
, x
, y
= pos
225 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
226 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
227 selection
.select_path(path
)
228 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
229 self
.show_popup_menu(iface
, bev
)
231 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
233 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile)
234 tree_view
.connect('button-press-event', button_press
)
236 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
237 policy
.watchers
.append(self
.build_tree
)
239 def set_root(self
, root
):
240 assert isinstance(root
, model
.Interface
)
243 def set_update_icons(self
, update_icons
):
245 # Clear icons cache to make sure they're really updated
246 self
.cached_icon
= {}
247 self
.update_icons
= update_icons
249 def get_icon(self
, iface
):
250 """Get an icon for this interface. If the icon is in the cache, use that.
251 If not, start a download. If we already started a download (successful or
252 not) do nothing. Returns None if no icon is currently available."""
254 # Try the in-memory cache
255 return self
.cached_icon
[iface
.uri
]
257 # Try the on-disk cache
258 iconpath
= iface_cache
.get_icon_path(iface
)
261 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
262 # (if icon is None, cache the fact that we can't load it)
263 self
.cached_icon
[iface
.uri
] = icon
267 # Download a new icon if we don't have one, or if the
268 # user did a 'Refresh'
269 if iconpath
is None or self
.update_icons
:
270 fetcher
= self
.policy
.download_icon(iface
)
272 if iface
.uri
not in self
.cached_icon
:
273 self
.cached_icon
[iface
.uri
] = None # Only try once
276 def update_display():
280 # Try to insert new icon into the cache
281 # If it fails, we'll be left with None in the cached_icon so
282 # we don't try again.
283 iconpath
= iface_cache
.get_icon_path(iface
)
285 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
288 warn("Failed to download icon for '%s'", iface
)
289 except Exception, ex
:
291 traceback
.print_exc()
292 self
.policy
.handler
.report_error(ex
)
294 # elif fetcher is None: don't store anything in cached_icon
296 # Note: if no icon is available for downloading,
297 # more attempts are made later.
298 # It can happen that no icon is yet available because
299 # the interface was not downloaded yet, in which case
300 # it's desireable to try again once the interface is available
305 def build_tree(self
):
306 if self
.original_implementation
is None:
307 self
.set_original_implementations()
309 done
= {} # Detect cycles
313 commands
= self
.policy
.solver
.selections
.commands
314 def add_node(parent
, iface
, command
):
315 # (command is the index into commands, if any)
320 iter = self
.model
.append(parent
)
321 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
322 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
323 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
324 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
326 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
329 old_impl
= self
.original_implementation
.get(iface
, None)
330 version_str
= impl
.get_version()
331 if old_impl
is not None and old_impl
.id != impl
.id:
332 version_str
+= _(' (was %s)') % old_impl
.get_version()
333 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
335 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
337 deps
= sel
.dependencies
338 if command
is not None:
339 deps
+= commands
[command
].requires
341 if isinstance(child
, model
.InterfaceDependency
):
342 if child
.qdom
.name
== 'runner':
343 child_command
= command
+ 1
346 add_node(iter, iface_cache
.get_interface(child
.interface
), child_command
)
348 child_iter
= self
.model
.append(parent
)
349 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
350 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
351 _('Unknown dependency type : %s') % child
352 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
354 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(choose)')
356 add_node(None, self
.root
, 0)
358 # Nothing could be selected, or no command requested
359 add_node(None, self
.root
, None)
360 self
.tree_view
.expand_all()
362 def show_popup_menu(self
, iface
, bev
):
365 have_source
= properties
.have_source_for(self
.policy
, iface
)
368 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
369 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
370 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
371 item
= gtk
.MenuItem(label
)
373 item
.connect('activate', lambda item
, cb
=cb
: cb())
375 item
.set_sensitive(False)
379 item
= gtk
.MenuItem(_('Compile'))
383 compile_menu
= gtk
.Menu()
384 item
.set_submenu(compile_menu
)
386 item
= gtk
.MenuItem(_('Automatic'))
387 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
389 compile_menu
.append(item
)
391 item
= gtk
.MenuItem(_('Manual...'))
392 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
394 compile_menu
.append(item
)
396 item
.set_sensitive(False)
398 menu
.popup(None, None, None, bev
.button
, bev
.time
)
400 def compile(self
, interface
, autocompile
= False):
403 # A new local feed may have been registered, so reload it from the disk cache
404 info(_("0compile command completed successfully. Reloading interface details."))
405 reader
.update_from_cache(interface
)
406 for feed
in interface
.extra_feeds
:
407 iface_cache
.get_feed(feed
.uri
, force
= True)
408 self
.policy
.recalculate()
409 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
411 def set_original_implementations(self
):
412 assert self
.original_implementation
is None
413 self
.original_implementation
= self
.policy
.implementation
.copy()
415 def update_download_status(self
):
416 """Called at regular intervals while there are downloads in progress,
417 and once at the end. Also called when things are added to the store.
418 Update the TreeView with the interfaces."""
420 # A download may be for a feed, an interface or an implementation.
421 # Create the reverse mapping (item -> download)
423 for dl
in self
.policy
.handler
.monitored_downloads
.values():
425 if dl
.hint
not in hints
:
427 hints
[dl
.hint
].append(dl
)
429 selections
= self
.policy
.solver
.selections
434 for x
in walk(self
.model
.iter_children(it
)): yield x
435 it
= self
.model
.iter_next(it
)
437 for row
in walk(self
.model
.get_iter_root()):
438 iface
= row
[InterfaceBrowser
.INTERFACE
]
440 # Is this interface the download's hint?
441 downloads
= hints
.get(iface
, []) # The interface itself
442 downloads
+= hints
.get(iface
.uri
, []) # The main feed
443 for feed
in self
.policy
.usable_feeds(iface
):
444 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
445 impl
= selections
.get(iface
, None)
447 downloads
+= hints
.get(impl
, []) # The chosen implementation
454 expected
= (expected
or 0) + dl
.expected_size
455 so_far
+= dl
.get_bytes_downloaded_so_far()
457 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
458 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
460 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
462 summary
= ngettext("(downloading %(downloaded)s/unknown)",
463 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
465 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
466 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
468 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
469 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary