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
.driver
.solver
.selections
.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 impl
.is_available(mainwindow
.driver
.config
.stores
):
57 return _("This version is already stored on your computer.")
59 src
= mainwindow
.driver
.config
.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_PYOBJECT
, "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
:
152 original_implementation
= None
164 columns
= [(_('Component'), INTERFACE_NAME
),
165 (_('Version'), VERSION
),
166 (_('Fetch'), DOWNLOAD_SIZE
),
167 (_('Description'), SUMMARY
),
170 def __init__(self
, driver
, widgets
):
172 self
.config
= driver
.config
174 tree_view
= widgets
.get_widget('components')
175 tree_view
.set_property('has-tooltip', True)
176 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
177 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
178 pos
= tree_view
.get_path_at_pos(x
, y
)
180 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
183 col_index
= column_objects
.index(pos
[1])
187 col
= self
.columns
[col_index
][1]
188 row
= self
.model
[path
]
189 iface
= row
[InterfaceBrowser
.INTERFACE
]
190 main_feed
= self
.config
.iface_cache
.get_feed(iface
.uri
)
191 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
195 tree_view
.connect('query-tooltip', callback
)
197 self
.cached_icon
= {} # URI -> GdkPixbuf
198 self
.default_icon
= tree_view
.get_style().lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.get_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, gobject
.TYPE_PYOBJECT
, 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(driver
, 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
: driver
.watchers
.remove(self
.build_tree
))
256 driver
.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
.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 if self
.config
.network_use
== model
.network_offline
:
292 fetcher
= self
.config
.fetcher
.download_icon(iface
)
294 if iface
.uri
not in self
.cached_icon
:
295 self
.cached_icon
[iface
.uri
] = None # Only try once
298 def update_display():
302 # Try to insert new icon into the cache
303 # If it fails, we'll be left with None in the cached_icon so
304 # we don't try again.
305 iconpath
= self
.config
.iface_cache
.get_icon_path(iface
)
307 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
310 warn("Failed to download icon for '%s'", iface
)
311 except Exception as ex
:
313 traceback
.print_exc()
314 self
.config
.handler
.report_error(ex
)
316 # elif fetcher is None: don't store anything in cached_icon
318 # Note: if no icon is available for downloading,
319 # more attempts are made later.
320 # It can happen that no icon is yet available because
321 # the interface was not downloaded yet, in which case
322 # it's desireable to try again once the interface is available
327 def build_tree(self
):
328 iface_cache
= self
.config
.iface_cache
330 if self
.original_implementation
is None:
331 self
.set_original_implementations()
333 done
= {} # Detect cycles
335 sels
= self
.driver
.solver
.selections
338 def add_node(parent
, iface
, commands
, essential
):
343 main_feed
= iface_cache
.get_feed(iface
.uri
)
345 name
= main_feed
.get_name()
346 summary
= main_feed
.summary
348 name
= iface
.get_name()
351 iter = self
.model
.append(parent
)
352 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
353 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
354 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
355 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
356 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = False
358 sel
= sels
.selections
.get(iface
.uri
, None)
361 old_impl
= self
.original_implementation
.get(iface
, None)
362 version_str
= impl
.get_version()
363 if old_impl
is not None and old_impl
.id != impl
.id:
364 version_str
+= _(' (was %s)') % old_impl
.get_version()
365 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
367 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
369 deps
= sel
.dependencies
371 deps
+= sel
.get_command(c
).requires
373 if isinstance(child
, model
.InterfaceDependency
):
375 iface_cache
.get_interface(child
.interface
),
376 child
.get_required_commands(),
377 child
.importance
== model
.Dependency
.Essential
)
379 child_iter
= self
.model
.append(parent
)
380 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
381 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
382 _('Unknown dependency type : %s') % child
383 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
385 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = essential
386 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)') if essential
else _('(none)')
388 add_node(None, self
.root
, [sels
.command
], essential
= True)
390 add_node(None, self
.root
, [], essential
= True)
391 self
.tree_view
.expand_all()
393 def show_popup_menu(self
, iface
, bev
):
396 have_source
= properties
.have_source_for(self
.config
, iface
)
399 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.driver
, iface
, self
.compile)),
400 (_('Show Versions'), lambda: properties
.edit(self
.driver
, iface
, self
.compile, show_versions
= True)),
401 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.driver
, iface
))]:
402 item
= gtk
.MenuItem()
403 item
.set_label(label
)
405 item
.connect('activate', lambda item
, cb
=cb
: cb())
407 item
.set_sensitive(False)
411 item
= gtk
.MenuItem()
412 item
.set_label(_('Compile'))
416 compile_menu
= gtk
.Menu()
417 item
.set_submenu(compile_menu
)
419 item
= gtk
.MenuItem()
420 item
.set_label(_('Automatic'))
421 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
423 compile_menu
.append(item
)
425 item
= gtk
.MenuItem()
426 item
.set_label(_('Manual...'))
427 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
429 compile_menu
.append(item
)
431 item
.set_sensitive(False)
433 menu
.popup(None, None, None, bev
.button
, bev
.time
)
435 def compile(self
, interface
, autocompile
= True):
438 # A new local feed may have been registered, so reload it from the disk cache
439 info(_("0compile command completed successfully. Reloading interface details."))
440 reader
.update_from_cache(interface
)
441 for feed
in interface
.extra_feeds
:
442 self
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
445 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
447 def set_original_implementations(self
):
448 assert self
.original_implementation
is None
449 self
.original_implementation
= self
.driver
.solver
.selections
.copy()
451 def update_download_status(self
, only_update_visible
= False):
452 """Called at regular intervals while there are downloads in progress,
453 and once at the end. Also called when things are added to the store.
454 Update the TreeView with the interfaces."""
456 # A download may be for a feed, an interface or an implementation.
457 # Create the reverse mapping (item -> download)
459 for dl
in self
.config
.handler
.monitored_downloads
:
461 if dl
.hint
not in hints
:
463 hints
[dl
.hint
].append(dl
)
465 selections
= self
.driver
.solver
.selections
467 # Only update currently visible rows
468 if only_update_visible
and self
.tree_view
.get_visible_range() != None:
469 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
470 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
472 # (or should we just wait until the TreeView has settled enough to tell
473 # us what is visible?)
474 firstVisibleIter
= self
.model
.get_iter_root()
475 lastVisiblePath
= None
477 solver
= self
.driver
.solver
478 requirements
= self
.driver
.requirements
479 iface_cache
= self
.config
.iface_cache
481 for it
in walk(self
.model
, firstVisibleIter
):
483 iface
= row
[InterfaceBrowser
.INTERFACE
]
485 # Is this interface the download's hint?
486 downloads
= hints
.get(iface
, []) # The interface itself
487 downloads
+= hints
.get(iface
.uri
, []) # The main feed
489 arch
= solver
.get_arch_for(requirements
, iface
)
490 for feed
in iface_cache
.usable_feeds(iface
, arch
):
491 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
492 impl
= selections
.get(iface
, None)
494 downloads
+= hints
.get(impl
, []) # The chosen implementation
501 expected
= (expected
or 0) + dl
.expected_size
502 so_far
+= dl
.get_bytes_downloaded_so_far()
504 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
505 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
507 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
509 summary
= ngettext("(downloading %(downloaded)s/unknown)",
510 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
512 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
513 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
515 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
516 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
518 if self
.model
.get_path(it
) == lastVisiblePath
:
521 def highlight_problems(self
):
522 """Called when the solve finishes. Highlight any missing implementations."""
523 for it
in walk(self
.model
, self
.model
.get_iter_root()):
525 iface
= row
[InterfaceBrowser
.INTERFACE
]
526 sel
= self
.driver
.solver
.selections
.selections
.get(iface
.uri
, None)
528 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
529 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'