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)')
382 add_node(None, self
.root
, [sels
.command
], essential
= True)
384 add_node(None, self
.root
, [], essential
= True)
385 self
.tree_view
.expand_all()
387 def show_popup_menu(self
, iface
, bev
):
390 have_source
= properties
.have_source_for(self
.policy
, iface
)
393 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
394 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
395 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
396 item
= gtk
.MenuItem(label
)
398 item
.connect('activate', lambda item
, cb
=cb
: cb())
400 item
.set_sensitive(False)
404 item
= gtk
.MenuItem(_('Compile'))
408 compile_menu
= gtk
.Menu()
409 item
.set_submenu(compile_menu
)
411 item
= gtk
.MenuItem(_('Automatic'))
412 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
414 compile_menu
.append(item
)
416 item
= gtk
.MenuItem(_('Manual...'))
417 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
419 compile_menu
.append(item
)
421 item
.set_sensitive(False)
423 menu
.popup(None, None, None, bev
.button
, bev
.time
)
425 def compile(self
, interface
, autocompile
= True):
428 # A new local feed may have been registered, so reload it from the disk cache
429 info(_("0compile command completed successfully. Reloading interface details."))
430 reader
.update_from_cache(interface
)
431 for feed
in interface
.extra_feeds
:
432 self
.policy
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
435 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
437 def set_original_implementations(self
):
438 assert self
.original_implementation
is None
439 self
.original_implementation
= self
.policy
.implementation
.copy()
441 def update_download_status(self
, only_update_visible
= False):
442 """Called at regular intervals while there are downloads in progress,
443 and once at the end. Also called when things are added to the store.
444 Update the TreeView with the interfaces."""
446 # A download may be for a feed, an interface or an implementation.
447 # Create the reverse mapping (item -> download)
449 for dl
in self
.policy
.handler
.monitored_downloads
.values():
451 if dl
.hint
not in hints
:
453 hints
[dl
.hint
].append(dl
)
455 selections
= self
.policy
.solver
.selections
457 # Only update currently visible rows
458 if only_update_visible
and self
.tree_view
.get_visible_range() != None:
459 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
460 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
462 # (or should we just wait until the TreeView has settled enough to tell
463 # us what is visible?)
464 firstVisibleIter
= self
.model
.get_iter_root()
465 lastVisiblePath
= None
467 for it
in walk(self
.model
, firstVisibleIter
):
469 iface
= row
[InterfaceBrowser
.INTERFACE
]
471 # Is this interface the download's hint?
472 downloads
= hints
.get(iface
, []) # The interface itself
473 downloads
+= hints
.get(iface
.uri
, []) # The main feed
474 for feed
in self
.policy
.usable_feeds(iface
):
475 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
476 impl
= selections
.get(iface
, None)
478 downloads
+= hints
.get(impl
, []) # The chosen implementation
485 expected
= (expected
or 0) + dl
.expected_size
486 so_far
+= dl
.get_bytes_downloaded_so_far()
488 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
489 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
491 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
493 summary
= ngettext("(downloading %(downloaded)s/unknown)",
494 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
496 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
497 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
499 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
500 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
502 if self
.model
.get_path(it
) == lastVisiblePath
:
505 def highlight_problems(self
):
506 """Called when the solve finishes. Highlight any missing implementations."""
507 for it
in walk(self
.model
, self
.model
.get_iter_root()):
509 iface
= row
[InterfaceBrowser
.INTERFACE
]
510 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
512 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
513 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'