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
162 columns
= [(_('Component'), INTERFACE_NAME
),
163 (_('Version'), VERSION
),
164 (_('Fetch'), DOWNLOAD_SIZE
),
165 (_('Description'), SUMMARY
),
168 def __init__(self
, policy
, widgets
):
169 tree_view
= widgets
.get_widget('components')
170 tree_view
.set_property('has-tooltip', True)
171 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
172 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
173 pos
= tree_view
.get_path_at_pos(x
, y
)
175 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
178 col_index
= column_objects
.index(pos
[1])
182 col
= self
.columns
[col_index
][1]
183 row
= self
.model
[path
]
184 iface
= row
[InterfaceBrowser
.INTERFACE
]
185 main_feed
= self
.policy
.config
.iface_cache
.get_feed(iface
.uri
)
186 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
190 tree_view
.connect('query-tooltip', callback
)
193 self
.cached_icon
= {} # URI -> GdkPixbuf
194 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
195 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
197 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
, str)
198 self
.tree_view
= tree_view
199 tree_view
.set_model(self
.model
)
203 text
= gtk
.CellRendererText()
204 coloured_text
= gtk
.CellRendererText()
206 for name
, model_column
in self
.columns
:
207 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
208 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
210 image
= InterfaceBrowser
.ICON
)
211 elif model_column
== None:
212 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
214 if model_column
== InterfaceBrowser
.SUMMARY
:
215 text_ellip
= gtk
.CellRendererText()
217 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
220 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
221 column
.set_expand(True)
222 elif model_column
== InterfaceBrowser
.VERSION
:
223 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
224 background
= InterfaceBrowser
.BACKGROUND
)
226 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
227 tree_view
.append_column(column
)
228 column_objects
.append(column
)
230 tree_view
.set_enable_search(True)
232 selection
= tree_view
.get_selection()
234 def button_press(tree_view
, bev
):
235 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
238 path
, col
, x
, y
= pos
240 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
241 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
242 selection
.select_path(path
)
243 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
244 self
.show_popup_menu(iface
, bev
)
246 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
248 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
249 tree_view
.connect('button-press-event', button_press
)
251 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
252 policy
.watchers
.append(self
.build_tree
)
254 def set_root(self
, root
):
255 assert isinstance(root
, model
.Interface
)
258 def set_update_icons(self
, update_icons
):
260 # Clear icons cache to make sure they're really updated
261 self
.cached_icon
= {}
262 self
.update_icons
= update_icons
264 def get_icon(self
, iface
):
265 """Get an icon for this interface. If the icon is in the cache, use that.
266 If not, start a download. If we already started a download (successful or
267 not) do nothing. Returns None if no icon is currently available."""
269 # Try the in-memory cache
270 return self
.cached_icon
[iface
.uri
]
272 # Try the on-disk cache
273 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
276 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
277 # (if icon is None, cache the fact that we can't load it)
278 self
.cached_icon
[iface
.uri
] = icon
282 # Download a new icon if we don't have one, or if the
283 # user did a 'Refresh'
284 if iconpath
is None or self
.update_icons
:
285 fetcher
= self
.policy
.download_icon(iface
)
287 if iface
.uri
not in self
.cached_icon
:
288 self
.cached_icon
[iface
.uri
] = None # Only try once
291 def update_display():
295 # Try to insert new icon into the cache
296 # If it fails, we'll be left with None in the cached_icon so
297 # we don't try again.
298 iconpath
= self
.policy
.config
.iface_cache
.get_icon_path(iface
)
300 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
303 warn("Failed to download icon for '%s'", iface
)
304 except Exception, ex
:
306 traceback
.print_exc()
307 self
.policy
.handler
.report_error(ex
)
309 # elif fetcher is None: don't store anything in cached_icon
311 # Note: if no icon is available for downloading,
312 # more attempts are made later.
313 # It can happen that no icon is yet available because
314 # the interface was not downloaded yet, in which case
315 # it's desireable to try again once the interface is available
320 def build_tree(self
):
321 iface_cache
= self
.policy
.config
.iface_cache
323 if self
.original_implementation
is None:
324 self
.set_original_implementations()
326 done
= {} # Detect cycles
329 commands
= self
.policy
.solver
.selections
.commands
330 def add_node(parent
, iface
, command
):
331 # (command is the index into commands, if any)
336 main_feed
= iface_cache
.get_feed(iface
.uri
)
338 name
= main_feed
.get_name()
339 summary
= main_feed
.summary
341 name
= iface
.get_name()
344 iter = self
.model
.append(parent
)
345 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
346 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
347 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
348 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
350 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
353 old_impl
= self
.original_implementation
.get(iface
, None)
354 version_str
= impl
.get_version()
355 if old_impl
is not None and old_impl
.id != impl
.id:
356 version_str
+= _(' (was %s)') % old_impl
.get_version()
357 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
359 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
361 deps
= sel
.dependencies
362 if command
is not None:
363 deps
+= commands
[command
].requires
365 if isinstance(child
, model
.InterfaceDependency
):
366 if child
.qdom
.name
== 'runner':
367 child_command
= command
+ 1
370 add_node(iter, iface_cache
.get_interface(child
.interface
), child_command
)
372 child_iter
= self
.model
.append(parent
)
373 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
374 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
375 _('Unknown dependency type : %s') % child
376 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
378 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)')
380 add_node(None, self
.root
, 0)
382 # Nothing could be selected, or no command requested
383 add_node(None, self
.root
, None)
384 self
.tree_view
.expand_all()
386 def show_popup_menu(self
, iface
, bev
):
389 have_source
= properties
.have_source_for(self
.policy
, iface
)
392 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
, self
.compile)),
393 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, self
.compile, show_versions
= True)),
394 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
395 item
= gtk
.MenuItem(label
)
397 item
.connect('activate', lambda item
, cb
=cb
: cb())
399 item
.set_sensitive(False)
403 item
= gtk
.MenuItem(_('Compile'))
407 compile_menu
= gtk
.Menu()
408 item
.set_submenu(compile_menu
)
410 item
= gtk
.MenuItem(_('Automatic'))
411 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
413 compile_menu
.append(item
)
415 item
= gtk
.MenuItem(_('Manual...'))
416 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
418 compile_menu
.append(item
)
420 item
.set_sensitive(False)
422 menu
.popup(None, None, None, bev
.button
, bev
.time
)
424 def compile(self
, interface
, autocompile
= False):
427 # A new local feed may have been registered, so reload it from the disk cache
428 info(_("0compile command completed successfully. Reloading interface details."))
429 reader
.update_from_cache(interface
)
430 for feed
in interface
.extra_feeds
:
431 self
.policy
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
434 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
436 def set_original_implementations(self
):
437 assert self
.original_implementation
is None
438 self
.original_implementation
= self
.policy
.implementation
.copy()
440 def update_download_status(self
):
441 """Called at regular intervals while there are downloads in progress,
442 and once at the end. Also called when things are added to the store.
443 Update the TreeView with the interfaces."""
445 # A download may be for a feed, an interface or an implementation.
446 # Create the reverse mapping (item -> download)
448 for dl
in self
.policy
.handler
.monitored_downloads
.values():
450 if dl
.hint
not in hints
:
452 hints
[dl
.hint
].append(dl
)
454 selections
= self
.policy
.solver
.selections
456 for row
in walk(self
.model
, self
.model
.get_iter_root()):
457 iface
= row
[InterfaceBrowser
.INTERFACE
]
459 # Is this interface the download's hint?
460 downloads
= hints
.get(iface
, []) # The interface itself
461 downloads
+= hints
.get(iface
.uri
, []) # The main feed
462 for feed
in self
.policy
.usable_feeds(iface
):
463 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
464 impl
= selections
.get(iface
, None)
466 downloads
+= hints
.get(impl
, []) # The chosen implementation
473 expected
= (expected
or 0) + dl
.expected_size
474 so_far
+= dl
.get_bytes_downloaded_so_far()
476 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
477 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
479 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
481 summary
= ngettext("(downloading %(downloaded)s/unknown)",
482 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
484 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
485 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
487 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
488 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
490 def highlight_problems(self
):
491 """Called when the solve finishes. Highlight any missing implementations."""
492 for row
in walk(self
.model
, self
.model
.get_iter_root()):
493 iface
= row
[InterfaceBrowser
.INTERFACE
]
494 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
497 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'