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
)
140 def walk(model
, it
, last
):
143 for x
in walk(model
, model
.iter_children(it
), last
): yield x
144 if last
== None or model
.get_path(it
) != model
.get_path(last
):
145 it
= model
.iter_next(it
)
149 class InterfaceBrowser
:
154 original_implementation
= None
166 columns
= [(_('Component'), INTERFACE_NAME
),
167 (_('Version'), VERSION
),
168 (_('Fetch'), DOWNLOAD_SIZE
),
169 (_('Description'), SUMMARY
),
172 def __init__(self
, policy
, widgets
):
173 tree_view
= widgets
.get_widget('components')
174 tree_view
.set_property('has-tooltip', True)
175 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
176 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
177 pos
= tree_view
.get_path_at_pos(x
, y
)
179 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
182 col_index
= column_objects
.index(pos
[1])
186 col
= self
.columns
[col_index
][1]
187 row
= self
.model
[path
]
188 iface
= row
[InterfaceBrowser
.INTERFACE
]
189 main_feed
= self
.policy
.config
.iface_cache
.get_feed(iface
.uri
)
190 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
194 tree_view
.connect('query-tooltip', callback
)
197 self
.cached_icon
= {} # URI -> GdkPixbuf
198 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
199 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
201 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str, bool)
202 self
.tree_view
= tree_view
203 tree_view
.set_model(self
.model
)
207 text
= gtk
.CellRendererText()
208 coloured_text
= gtk
.CellRendererText()
210 for name
, model_column
in self
.columns
:
211 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
212 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
214 image
= InterfaceBrowser
.ICON
)
215 elif model_column
== None:
216 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
218 if model_column
== InterfaceBrowser
.SUMMARY
:
219 text_ellip
= gtk
.CellRendererText()
221 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
224 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
225 column
.set_expand(True)
226 elif model_column
== InterfaceBrowser
.VERSION
:
227 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
228 background
= InterfaceBrowser
.BACKGROUND
)
230 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
231 tree_view
.append_column(column
)
232 column_objects
.append(column
)
234 tree_view
.set_enable_search(True)
236 selection
= tree_view
.get_selection()
238 def button_press(tree_view
, bev
):
239 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
242 path
, col
, x
, y
= pos
244 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
245 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
246 selection
.select_path(path
)
247 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
248 self
.show_popup_menu(iface
, bev
)
250 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
252 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
253 tree_view
.connect('button-press-event', button_press
)
255 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
256 policy
.watchers
.append(self
.build_tree
)
258 def set_root(self
, root
):
259 assert isinstance(root
, model
.Interface
)
262 def set_update_icons(self
, update_icons
):
264 # Clear icons cache to make sure they're really updated
265 self
.cached_icon
= {}
266 self
.update_icons
= update_icons
268 def get_icon(self
, iface
):
269 """Get an icon for this interface. If the icon is in the cache, use that.
270 If not, start a download. If we already started a download (successful or
271 not) do nothing. Returns None if no icon is currently available."""
273 # Try the in-memory cache
274 return self
.cached_icon
[iface
.uri
]
276 # Try the on-disk cache
277 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
280 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
281 # (if icon is None, cache the fact that we can't load it)
282 self
.cached_icon
[iface
.uri
] = icon
286 # Download a new icon if we don't have one, or if the
287 # user did a 'Refresh'
288 if iconpath
is None or self
.update_icons
:
289 fetcher
= self
.policy
.download_icon(iface
)
291 if iface
.uri
not in self
.cached_icon
:
292 self
.cached_icon
[iface
.uri
] = None # Only try once
295 def update_display():
299 # Try to insert new icon into the cache
300 # If it fails, we'll be left with None in the cached_icon so
301 # we don't try again.
302 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
304 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
307 warn("Failed to download icon for '%s'", iface
)
308 except Exception as ex
:
310 traceback
.print_exc()
311 self
.policy
.handler
.report_error(ex
)
313 # elif fetcher is None: don't store anything in cached_icon
315 # Note: if no icon is available for downloading,
316 # more attempts are made later.
317 # It can happen that no icon is yet available because
318 # the interface was not downloaded yet, in which case
319 # it's desireable to try again once the interface is available
324 def build_tree(self
):
325 iface_cache
= self
.policy
.config
.iface_cache
327 if self
.original_implementation
is None:
328 self
.set_original_implementations()
330 done
= {} # Detect cycles
333 commands
= self
.policy
.solver
.selections
.commands
334 def add_node(parent
, iface
, command
, essential
):
335 # (command is the index into commands, if any)
340 main_feed
= iface_cache
.get_feed(iface
.uri
)
342 name
= main_feed
.get_name()
343 summary
= main_feed
.summary
345 name
= iface
.get_name()
348 iter = self
.model
.append(parent
)
349 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
350 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
351 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
352 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
353 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = False
355 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
358 old_impl
= self
.original_implementation
.get(iface
, None)
359 version_str
= impl
.get_version()
360 if old_impl
is not None and old_impl
.id != impl
.id:
361 version_str
+= _(' (was %s)') % old_impl
.get_version()
362 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
364 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
366 deps
= sel
.dependencies
367 if command
is not None:
368 deps
+= commands
[command
].requires
370 if isinstance(child
, model
.InterfaceDependency
):
371 if child
.qdom
.name
== 'runner':
372 child_command
= command
+ 1
375 add_node(iter, iface_cache
.get_interface(child
.interface
), child_command
, child
.importance
== model
.Dependency
.Essential
)
377 child_iter
= self
.model
.append(parent
)
378 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
379 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
380 _('Unknown dependency type : %s') % child
381 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
383 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = essential
384 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)') if essential
else _('(none)')
386 add_node(None, self
.root
, 0, essential
= True)
388 # Nothing could be selected, or no command requested
389 add_node(None, self
.root
, None, essential
= True)
390 self
.tree_view
.expand_all()
392 def show_popup_menu(self
, iface
, bev
):
395 have_source
= properties
.have_source_for(self
.policy
, iface
)
398 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
399 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
400 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
401 item
= gtk
.MenuItem(label
)
403 item
.connect('activate', lambda item
, cb
=cb
: cb())
405 item
.set_sensitive(False)
409 item
= gtk
.MenuItem(_('Compile'))
413 compile_menu
= gtk
.Menu()
414 item
.set_submenu(compile_menu
)
416 item
= gtk
.MenuItem(_('Automatic'))
417 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
419 compile_menu
.append(item
)
421 item
= gtk
.MenuItem(_('Manual...'))
422 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
424 compile_menu
.append(item
)
426 item
.set_sensitive(False)
428 menu
.popup(None, None, None, bev
.button
, bev
.time
)
430 def compile(self
, interface
, autocompile
= True):
433 # A new local feed may have been registered, so reload it from the disk cache
434 info(_("0compile command completed successfully. Reloading interface details."))
435 reader
.update_from_cache(interface
)
436 for feed
in interface
.extra_feeds
:
437 self
.policy
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
440 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
442 def set_original_implementations(self
):
443 assert self
.original_implementation
is None
444 self
.original_implementation
= self
.policy
.implementation
.copy()
446 def update_download_status(self
):
447 """Called at regular intervals while there are downloads in progress,
448 and once at the end. Also called when things are added to the store.
449 Update the TreeView with the interfaces."""
451 # A download may be for a feed, an interface or an implementation.
452 # Create the reverse mapping (item -> download)
454 for dl
in self
.policy
.handler
.monitored_downloads
.values():
456 if dl
.hint
not in hints
:
458 hints
[dl
.hint
].append(dl
)
460 selections
= self
.policy
.solver
.selections
462 # Only update currently visible rows
463 if self
.tree_view
.get_visible_range() != None:
464 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
465 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
466 lastVisibleIter
= self
.model
.get_iter(lastVisiblePath
)
468 firstVisibleIter
= self
.model
.get_iter_root()
469 lastVisibleIter
= None
471 for row
in walk(self
.model
, firstVisibleIter
, lastVisibleIter
):
472 iface
= row
[InterfaceBrowser
.INTERFACE
]
474 # Is this interface the download's hint?
475 downloads
= hints
.get(iface
, []) # The interface itself
476 downloads
+= hints
.get(iface
.uri
, []) # The main feed
477 for feed
in self
.policy
.usable_feeds(iface
):
478 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
479 impl
= selections
.get(iface
, None)
481 downloads
+= hints
.get(impl
, []) # The chosen implementation
488 expected
= (expected
or 0) + dl
.expected_size
489 so_far
+= dl
.get_bytes_downloaded_so_far()
491 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
492 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
494 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
496 summary
= ngettext("(downloading %(downloaded)s/unknown)",
497 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
499 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
500 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
502 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
503 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
505 def highlight_problems(self
):
506 """Called when the solve finishes. Highlight any missing implementations."""
507 for row
in walk(self
.model
, self
.model
.get_iter_root(), None):
508 iface
= row
[InterfaceBrowser
.INTERFACE
]
509 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
511 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
512 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'