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
import _
, translation
7 from zeroinstall
.support
import tasks
, pretty_size
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
15 ngettext
= translation
.ngettext
19 if impl
.user_stability
is None:
20 return _(str(impl
.upstream_stability
))
21 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
22 % {'implementation_user_stability': _(str(impl
.user_stability
)),
23 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
26 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
28 def get_tooltip_text(mainwindow
, interface
, main_feed
, model_column
):
30 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
31 return _("Full name: %s") % interface
.uri
32 elif model_column
== InterfaceBrowser
.SUMMARY
:
33 if main_feed
is None or not main_feed
.description
:
34 return _("(no description available)")
35 first_para
= main_feed
.description
.split('\n\n', 1)[0]
36 return first_para
.replace('\n', ' ')
37 elif model_column
is None:
38 return _("Click here for more options...")
40 impl
= mainwindow
.policy
.implementation
.get(interface
, None)
42 return _("No suitable version was found. Double-click "
43 "here to find out why.")
45 if model_column
== InterfaceBrowser
.VERSION
:
46 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
47 {'version': impl
.get_version(), 'stability': _stability(impl
)}
48 old_impl
= mainwindow
.original_implementation
.get(interface
, None)
49 if old_impl
is not None and old_impl
is not impl
:
50 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
51 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
54 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
56 if mainwindow
.policy
.get_cached(impl
):
57 return _("This version is already stored on your computer.")
59 src
= mainwindow
.policy
.fetcher
.get_best_source(impl
)
61 return _("No downloads available!")
62 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
63 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
65 class MenuIconRenderer(gtk
.GenericCellRenderer
):
67 gtk
.GenericCellRenderer
.__init
__(self
)
68 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
70 def do_set_property(self
, prop
, value
):
71 setattr(self
, prop
.name
, value
)
73 def on_get_size(self
, widget
, cell_area
, layout
= None):
76 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
77 if flags
& gtk
.CELL_RENDERER_PRELIT
:
78 state
= gtk
.STATE_PRELIGHT
80 state
= gtk
.STATE_NORMAL
82 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
83 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
84 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
85 gtk
.ARROW_RIGHT
, True,
86 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
88 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
90 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
91 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
94 def do_set_property(self
, prop
, value
):
95 setattr(self
, prop
.name
, value
)
97 def on_get_size(self
, widget
, cell_area
, layout
= None):
99 layout
= widget
.create_pango_layout(self
.text
)
100 a
, rect
= layout
.get_pixel_extents()
102 pixmap_height
= self
.image
.get_height()
104 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
107 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
110 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
111 layout
= widget
.create_pango_layout(self
.text
)
112 a
, rect
= layout
.get_pixel_extents()
114 if flags
& gtk
.CELL_RENDERER_SELECTED
:
115 state
= gtk
.STATE_SELECTED
116 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
117 state
= gtk
.STATE_PRELIGHT
119 state
= gtk
.STATE_NORMAL
121 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
122 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
124 cell_area
.y
+ image_y
)
126 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
128 widget
.style
.paint_layout(window
, state
, True,
129 expose_area
, widget
, "cellrenderertext",
130 cell_area
.x
+ CELL_TEXT_INDENT
,
131 cell_area
.y
+ text_y
,
134 if gtk
.pygtk_version
< (2, 8, 0):
135 # Note sure exactly which versions need this.
136 # 2.8.0 gives a warning if you include it, though.
137 gobject
.type_register(IconAndTextRenderer
)
138 gobject
.type_register(MenuIconRenderer
)
143 for x
in walk(model
, model
.iter_children(it
)): yield x
144 it
= model
.iter_next(it
)
146 class InterfaceBrowser
:
151 original_implementation
= None
163 columns
= [(_('Component'), INTERFACE_NAME
),
164 (_('Version'), VERSION
),
165 (_('Fetch'), DOWNLOAD_SIZE
),
166 (_('Description'), SUMMARY
),
169 def __init__(self
, policy
, widgets
):
170 tree_view
= widgets
.get_widget('components')
171 tree_view
.set_property('has-tooltip', True)
172 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
173 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
174 pos
= tree_view
.get_path_at_pos(x
, y
)
176 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
179 col_index
= column_objects
.index(pos
[1])
183 col
= self
.columns
[col_index
][1]
184 row
= self
.model
[path
]
185 iface
= row
[InterfaceBrowser
.INTERFACE
]
186 main_feed
= self
.policy
.config
.iface_cache
.get_feed(iface
.uri
)
187 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
191 tree_view
.connect('query-tooltip', callback
)
194 self
.cached_icon
= {} # URI -> GdkPixbuf
195 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
196 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
198 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str, bool)
199 self
.tree_view
= tree_view
200 tree_view
.set_model(self
.model
)
204 text
= gtk
.CellRendererText()
205 coloured_text
= gtk
.CellRendererText()
207 for name
, model_column
in self
.columns
:
208 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
209 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
211 image
= InterfaceBrowser
.ICON
)
212 elif model_column
== None:
213 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
215 if model_column
== InterfaceBrowser
.SUMMARY
:
216 text_ellip
= gtk
.CellRendererText()
218 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
221 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
222 column
.set_expand(True)
223 elif model_column
== InterfaceBrowser
.VERSION
:
224 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
225 background
= InterfaceBrowser
.BACKGROUND
)
227 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
228 tree_view
.append_column(column
)
229 column_objects
.append(column
)
231 tree_view
.set_enable_search(True)
233 selection
= tree_view
.get_selection()
235 def button_press(tree_view
, bev
):
236 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
239 path
, col
, x
, y
= pos
241 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
242 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
243 selection
.select_path(path
)
244 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
245 self
.show_popup_menu(iface
, bev
)
247 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
249 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
250 tree_view
.connect('button-press-event', button_press
)
252 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
253 policy
.watchers
.append(self
.build_tree
)
255 def set_root(self
, root
):
256 assert isinstance(root
, model
.Interface
)
259 def set_update_icons(self
, update_icons
):
261 # Clear icons cache to make sure they're really updated
262 self
.cached_icon
= {}
263 self
.update_icons
= update_icons
265 def get_icon(self
, iface
):
266 """Get an icon for this interface. If the icon is in the cache, use that.
267 If not, start a download. If we already started a download (successful or
268 not) do nothing. Returns None if no icon is currently available."""
270 # Try the in-memory cache
271 return self
.cached_icon
[iface
.uri
]
273 # Try the on-disk cache
274 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
277 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
278 # (if icon is None, cache the fact that we can't load it)
279 self
.cached_icon
[iface
.uri
] = icon
283 # Download a new icon if we don't have one, or if the
284 # user did a 'Refresh'
285 if iconpath
is None or self
.update_icons
:
286 fetcher
= self
.policy
.download_icon(iface
)
288 if iface
.uri
not in self
.cached_icon
:
289 self
.cached_icon
[iface
.uri
] = None # Only try once
292 def update_display():
296 # Try to insert new icon into the cache
297 # If it fails, we'll be left with None in the cached_icon so
298 # we don't try again.
299 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
301 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
304 warn("Failed to download icon for '%s'", iface
)
305 except Exception as ex
:
307 traceback
.print_exc()
308 self
.policy
.handler
.report_error(ex
)
310 # elif fetcher is None: don't store anything in cached_icon
312 # Note: if no icon is available for downloading,
313 # more attempts are made later.
314 # It can happen that no icon is yet available because
315 # the interface was not downloaded yet, in which case
316 # it's desireable to try again once the interface is available
321 def build_tree(self
):
322 iface_cache
= self
.policy
.config
.iface_cache
324 if self
.original_implementation
is None:
325 self
.set_original_implementations()
327 done
= {} # Detect cycles
329 sels
= self
.policy
.solver
.selections
332 def add_node(parent
, iface
, commands
, essential
):
337 main_feed
= iface_cache
.get_feed(iface
.uri
)
339 name
= main_feed
.get_name()
340 summary
= main_feed
.summary
342 name
= iface
.get_name()
345 iter = self
.model
.append(parent
)
346 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
347 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
348 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
349 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
350 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = False
352 sel
= sels
.selections
.get(iface
.uri
, None)
355 old_impl
= self
.original_implementation
.get(iface
, None)
356 version_str
= impl
.get_version()
357 if old_impl
is not None and old_impl
.id != impl
.id:
358 version_str
+= _(' (was %s)') % old_impl
.get_version()
359 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
361 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
363 deps
= sel
.dependencies
365 deps
+= sel
.get_command(c
).requires
367 if isinstance(child
, model
.InterfaceDependency
):
369 iface_cache
.get_interface(child
.interface
),
370 child
.get_required_commands(),
371 child
.importance
== model
.Dependency
.Essential
)
373 child_iter
= self
.model
.append(parent
)
374 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
375 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
376 _('Unknown dependency type : %s') % child
377 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
379 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = essential
380 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)') if essential
else _('(none)')
381 add_node(None, self
.root
, [sels
.command
], essential
= True)
382 self
.tree_view
.expand_all()
384 def show_popup_menu(self
, iface
, bev
):
387 have_source
= properties
.have_source_for(self
.policy
, iface
)
390 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
391 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
392 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
393 item
= gtk
.MenuItem(label
)
395 item
.connect('activate', lambda item
, cb
=cb
: cb())
397 item
.set_sensitive(False)
401 item
= gtk
.MenuItem(_('Compile'))
405 compile_menu
= gtk
.Menu()
406 item
.set_submenu(compile_menu
)
408 item
= gtk
.MenuItem(_('Automatic'))
409 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
411 compile_menu
.append(item
)
413 item
= gtk
.MenuItem(_('Manual...'))
414 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
416 compile_menu
.append(item
)
418 item
.set_sensitive(False)
420 menu
.popup(None, None, None, bev
.button
, bev
.time
)
422 def compile(self
, interface
, autocompile
= True):
425 # A new local feed may have been registered, so reload it from the disk cache
426 info(_("0compile command completed successfully. Reloading interface details."))
427 reader
.update_from_cache(interface
)
428 for feed
in interface
.extra_feeds
:
429 self
.policy
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
432 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
434 def set_original_implementations(self
):
435 assert self
.original_implementation
is None
436 self
.original_implementation
= self
.policy
.implementation
.copy()
438 def update_download_status(self
, only_update_visible
= False):
439 """Called at regular intervals while there are downloads in progress,
440 and once at the end. Also called when things are added to the store.
441 Update the TreeView with the interfaces."""
443 # A download may be for a feed, an interface or an implementation.
444 # Create the reverse mapping (item -> download)
446 for dl
in self
.policy
.handler
.monitored_downloads
.values():
448 if dl
.hint
not in hints
:
450 hints
[dl
.hint
].append(dl
)
452 selections
= self
.policy
.solver
.selections
454 # Only update currently visible rows
455 if only_update_visible
and self
.tree_view
.get_visible_range() != None:
456 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
457 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
459 # (or should we just wait until the TreeView has settled enough to tell
460 # us what is visible?)
461 firstVisibleIter
= self
.model
.get_iter_root()
462 lastVisiblePath
= None
464 for it
in walk(self
.model
, firstVisibleIter
):
466 iface
= row
[InterfaceBrowser
.INTERFACE
]
468 # Is this interface the download's hint?
469 downloads
= hints
.get(iface
, []) # The interface itself
470 downloads
+= hints
.get(iface
.uri
, []) # The main feed
471 for feed
in self
.policy
.usable_feeds(iface
):
472 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
473 impl
= selections
.get(iface
, None)
475 downloads
+= hints
.get(impl
, []) # The chosen implementation
482 expected
= (expected
or 0) + dl
.expected_size
483 so_far
+= dl
.get_bytes_downloaded_so_far()
485 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
486 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
488 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
490 summary
= ngettext("(downloading %(downloaded)s/unknown)",
491 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
493 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
494 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
496 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
497 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
499 if self
.model
.get_path(it
) == lastVisiblePath
:
502 def highlight_problems(self
):
503 """Called when the solve finishes. Highlight any missing implementations."""
504 for it
in walk(self
.model
, self
.model
.get_iter_root()):
506 iface
= row
[InterfaceBrowser
.INTERFACE
]
507 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
509 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
510 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'