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
.support
import tasks
, pretty_size
7 from zeroinstall
.injector
.iface_cache
import iface_cache
8 from zeroinstall
.injector
import model
10 from zeroinstall
.gtkui
.treetips
import TreeTips
11 from zeroinstall
.gtkui
.icon
import load_icon
12 from zeroinstall
import support
13 from logging
import warn
18 if impl
.user_stability
is None:
19 return _(str(impl
.upstream_stability
))
20 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
21 % {'implementation_user_stability': _(str(impl
.user_stability
)),
22 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
25 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
27 class InterfaceTips(TreeTips
):
30 def __init__(self
, mainwindow
):
31 self
.mainwindow
= mainwindow
33 def get_tooltip_text(self
):
34 interface
, model_column
= self
.item
36 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
37 return _("Full name: %s") % interface
.uri
38 elif model_column
== InterfaceBrowser
.SUMMARY
:
39 if not interface
.description
:
41 first_para
= interface
.description
.split('\n\n', 1)[0]
42 return first_para
.replace('\n', ' ')
43 elif model_column
is None:
44 return _("Click here for more options...")
46 impl
= self
.mainwindow
.policy
.implementation
.get(interface
, None)
48 return _("No suitable implementation was found. Check the "
49 "interface properties to find out why.")
51 if model_column
== InterfaceBrowser
.VERSION
:
52 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
53 {'version': impl
.get_version(), 'stability': _stability(impl
)}
54 old_impl
= self
.mainwindow
.original_implementation
.get(interface
, None)
55 if old_impl
is not None and old_impl
is not impl
:
56 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
57 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
60 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
62 if self
.mainwindow
.policy
.get_cached(impl
):
63 return _("This version is already stored on your computer.")
65 src
= self
.mainwindow
.policy
.fetcher
.get_best_source(impl
)
67 return _("No downloads available!")
68 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
69 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
71 class MenuIconRenderer(gtk
.GenericCellRenderer
):
73 gtk
.GenericCellRenderer
.__init
__(self
)
74 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
76 def do_set_property(self
, prop
, value
):
77 setattr(self
, prop
.name
, value
)
79 def on_get_size(self
, widget
, cell_area
, layout
= None):
82 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
83 if flags
& gtk
.CELL_RENDERER_PRELIT
:
84 state
= gtk
.STATE_PRELIGHT
86 state
= gtk
.STATE_NORMAL
88 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
89 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
90 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
91 gtk
.ARROW_RIGHT
, True,
92 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
94 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
96 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
97 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
100 def do_set_property(self
, prop
, value
):
101 setattr(self
, prop
.name
, value
)
103 def on_get_size(self
, widget
, cell_area
, layout
= None):
105 layout
= widget
.create_pango_layout(self
.text
)
106 a
, rect
= layout
.get_pixel_extents()
108 pixmap_height
= self
.image
.get_height()
110 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
113 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
116 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
117 layout
= widget
.create_pango_layout(self
.text
)
118 a
, rect
= layout
.get_pixel_extents()
120 if flags
& gtk
.CELL_RENDERER_SELECTED
:
121 state
= gtk
.STATE_SELECTED
122 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
123 state
= gtk
.STATE_PRELIGHT
125 state
= gtk
.STATE_NORMAL
127 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
128 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
130 cell_area
.y
+ image_y
)
132 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
134 widget
.style
.paint_layout(window
, state
, True,
135 expose_area
, widget
, "cellrenderertext",
136 cell_area
.x
+ CELL_TEXT_INDENT
,
137 cell_area
.y
+ text_y
,
140 if gtk
.pygtk_version
< (2, 8, 0):
141 # Note sure exactly which versions need this.
142 # 2.8.0 gives a warning if you include it, though.
143 gobject
.type_register(IconAndTextRenderer
)
144 gobject
.type_register(MenuIconRenderer
)
146 class InterfaceBrowser
:
151 original_implementation
= None
161 columns
= [(_('Component'), INTERFACE_NAME
),
162 (_('Version'), VERSION
),
163 (_('Fetch'), DOWNLOAD_SIZE
),
164 (_('Description'), SUMMARY
),
167 def __init__(self
, policy
, widgets
):
168 tips
= InterfaceTips(self
)
170 tree_view
= widgets
.get_widget('components')
173 self
.cached_icon
= {} # URI -> GdkPixbuf
174 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
175 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
177 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
)
178 self
.tree_view
= tree_view
179 tree_view
.set_model(self
.model
)
183 text
= gtk
.CellRendererText()
185 for name
, model_column
in self
.columns
:
186 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
187 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
189 image
= InterfaceBrowser
.ICON
)
190 elif model_column
== None:
191 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
193 if model_column
== InterfaceBrowser
.SUMMARY
:
194 text_ellip
= gtk
.CellRendererText()
196 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
199 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
200 column
.set_expand(True)
202 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
203 tree_view
.append_column(column
)
204 column_objects
.append(column
)
206 tree_view
.set_enable_search(True)
208 selection
= tree_view
.get_selection()
210 def motion(tree_view
, ev
):
211 if ev
.window
is not tree_view
.get_bin_window():
213 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
217 col_index
= column_objects
.index(pos
[1])
221 col
= self
.columns
[col_index
][1]
222 row
= self
.model
[path
]
223 item
= (row
[InterfaceBrowser
.INTERFACE
], col
)
224 if item
!= tips
.item
:
225 tips
.prime(tree_view
, item
)
229 tree_view
.connect('motion-notify-event', motion
)
230 tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
232 def button_press(tree_view
, bev
):
233 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
236 path
, col
, x
, y
= pos
238 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
239 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
240 selection
.select_path(path
)
241 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
242 self
.show_popup_menu(iface
, bev
)
244 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
246 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
])
247 tree_view
.connect('button-press-event', button_press
)
249 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
250 policy
.watchers
.append(self
.build_tree
)
252 def set_root(self
, root
):
253 assert isinstance(root
, model
.Interface
)
256 def set_update_icons(self
, update_icons
):
258 # Clear icons cache to make sure they're really updated
259 self
.cached_icon
= {}
260 self
.update_icons
= update_icons
262 def get_icon(self
, iface
):
263 """Get an icon for this interface. If the icon is in the cache, use that.
264 If not, start a download. If we already started a download (successful or
265 not) do nothing. Returns None if no icon is currently available."""
267 # Try the in-memory cache
268 return self
.cached_icon
[iface
.uri
]
270 # Try the on-disk cache
271 iconpath
= iface_cache
.get_icon_path(iface
)
274 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
275 # (if icon is None, cache the fact that we can't load it)
276 self
.cached_icon
[iface
.uri
] = icon
280 # Download a new icon if we don't have one, or if the
281 # user did a 'Refresh'
282 if iconpath
is None or self
.update_icons
:
283 fetcher
= self
.policy
.download_icon(iface
)
285 if iface
.uri
not in self
.cached_icon
:
286 self
.cached_icon
[iface
.uri
] = None # Only try once
289 def update_display():
293 # Try to insert new icon into the cache
294 # If it fails, we'll be left with None in the cached_icon so
295 # we don't try again.
296 iconpath
= iface_cache
.get_icon_path(iface
)
298 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
301 warn("Failed to download icon for '%s'", iface
)
302 except Exception, ex
:
304 traceback
.print_exc()
305 self
.policy
.handler
.report_error(ex
)
307 # elif fetcher is None: don't store anything in cached_icon
309 # Note: if no icon is available for downloading,
310 # more attempts are made later.
311 # It can happen that no icon is yet available because
312 # the interface was not downloaded yet, in which case
313 # it's desireable to try again once the interface is available
318 def build_tree(self
):
319 if self
.original_implementation
is None:
320 self
.set_original_implementations()
322 done
= {} # Detect cycles
326 def add_node(parent
, iface
, command
= None):
331 iter = self
.model
.append(parent
)
332 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
333 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
334 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
335 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
337 sel
= self
.policy
.solver
.selections
.selections
.get(iface
.uri
, None)
340 old_impl
= self
.original_implementation
.get(iface
, None)
341 version_str
= impl
.get_version()
342 if old_impl
is not None and old_impl
.id != impl
.id:
343 version_str
+= _(' (was %s)') % old_impl
.get_version()
344 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
346 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
348 deps
= sel
.dependencies
350 deps
+= command
.requires
352 if isinstance(child
, model
.InterfaceDependency
):
353 add_node(iter, iface_cache
.get_interface(child
.interface
))
355 child_iter
= self
.model
.append(parent
)
356 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
357 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
358 _('Unknown dependency type : %s') % child
359 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
361 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(choose)')
362 add_node(None, self
.root
, self
.policy
.solver
.selections
.command
)
363 self
.tree_view
.expand_all()
365 def show_popup_menu(self
, iface
, bev
):
369 have_source
= properties
.have_source_for(self
.policy
, iface
)
372 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
373 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
374 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
375 item
= gtk
.MenuItem(label
)
377 item
.connect('activate', lambda item
, cb
=cb
: cb())
379 item
.set_sensitive(False)
383 item
= gtk
.MenuItem(_('Compile'))
387 compile_menu
= gtk
.Menu()
388 item
.set_submenu(compile_menu
)
390 item
= gtk
.MenuItem(_('Automatic'))
391 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= True))
393 compile_menu
.append(item
)
395 item
= gtk
.MenuItem(_('Manual...'))
396 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= False))
398 compile_menu
.append(item
)
400 item
.set_sensitive(False)
402 menu
.popup(None, None, None, bev
.button
, bev
.time
)
404 def set_original_implementations(self
):
405 assert self
.original_implementation
is None
406 self
.original_implementation
= self
.policy
.implementation
.copy()
408 def update_download_status(self
):
409 """Called at regular intervals while there are downloads in progress,
410 and once at the end. Also called when things are added to the store.
411 Update the TreeView with the interfaces."""
413 # A download may be for a feed, an interface or an implementation.
414 # Create the reverse mapping (item -> download)
416 for dl
in self
.policy
.handler
.monitored_downloads
.values():
418 if dl
.hint
not in hints
:
420 hints
[dl
.hint
].append(dl
)
422 selections
= self
.policy
.solver
.selections
427 for x
in walk(self
.model
.iter_children(it
)): yield x
428 it
= self
.model
.iter_next(it
)
430 for row
in walk(self
.model
.get_iter_root()):
431 iface
= row
[InterfaceBrowser
.INTERFACE
]
433 # Is this interface the download's hint?
434 downloads
= hints
.get(iface
, []) # The interface itself
435 downloads
+= hints
.get(iface
.uri
, []) # The main feed
436 for feed
in self
.policy
.usable_feeds(iface
):
437 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
438 impl
= selections
.get(iface
, None)
440 downloads
+= hints
.get(impl
, []) # The chosen implementation
447 expected
= (expected
or 0) + dl
.expected_size
448 so_far
+= dl
.get_bytes_downloaded_so_far()
450 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
451 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
453 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
455 summary
= ngettext("(downloading %(downloaded)s/unknown)",
456 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
458 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
459 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
461 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
462 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary