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 impl
.upstream_stability
20 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
21 % {'implementation_user_stability': impl
.user_stability
, 'implementation_upstream_stability': impl
.upstream_stability
}
24 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
26 class InterfaceTips(TreeTips
):
29 def __init__(self
, mainwindow
):
30 self
.mainwindow
= mainwindow
32 def get_tooltip_text(self
):
33 interface
, model_column
= self
.item
35 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
36 return _("Full name: %s") % interface
.uri
37 elif model_column
== InterfaceBrowser
.SUMMARY
:
38 if not interface
.description
:
40 first_para
= interface
.description
.split('\n\n', 1)[0]
41 return first_para
.replace('\n', ' ')
42 elif model_column
is None:
43 return _("Click here for more options...")
45 impl
= self
.mainwindow
.policy
.implementation
.get(interface
, None)
47 return _("No suitable implementation was found. Check the "
48 "interface properties to find out why.")
50 if model_column
== InterfaceBrowser
.VERSION
:
51 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
52 {'version': impl
.get_version(), 'stability': _stability(impl
)}
53 old_impl
= self
.mainwindow
.original_implementation
.get(interface
, None)
54 if old_impl
is not None and old_impl
is not impl
:
55 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
56 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
59 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
61 if self
.mainwindow
.policy
.get_cached(impl
):
62 return _("This version is already stored on your computer.")
64 src
= self
.mainwindow
.policy
.fetcher
.get_best_source(impl
)
66 return _("No downloads available!")
67 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
68 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
70 class MenuIconRenderer(gtk
.GenericCellRenderer
):
72 gtk
.GenericCellRenderer
.__init
__(self
)
73 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
75 def do_set_property(self
, prop
, value
):
76 setattr(self
, prop
.name
, value
)
78 def on_get_size(self
, widget
, cell_area
, layout
= None):
81 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
82 if flags
& gtk
.CELL_RENDERER_PRELIT
:
83 state
= gtk
.STATE_PRELIGHT
85 state
= gtk
.STATE_NORMAL
87 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
88 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
89 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
90 gtk
.ARROW_RIGHT
, True,
91 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
93 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
95 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
96 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
99 def do_set_property(self
, prop
, value
):
100 setattr(self
, prop
.name
, value
)
102 def on_get_size(self
, widget
, cell_area
, layout
= None):
104 layout
= widget
.create_pango_layout(self
.text
)
105 a
, rect
= layout
.get_pixel_extents()
107 pixmap_height
= self
.image
.get_height()
109 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
112 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
115 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
116 layout
= widget
.create_pango_layout(self
.text
)
117 a
, rect
= layout
.get_pixel_extents()
119 if flags
& gtk
.CELL_RENDERER_SELECTED
:
120 state
= gtk
.STATE_SELECTED
121 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
122 state
= gtk
.STATE_PRELIGHT
124 state
= gtk
.STATE_NORMAL
126 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
127 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
129 cell_area
.y
+ image_y
)
131 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
133 widget
.style
.paint_layout(window
, state
, True,
134 expose_area
, widget
, "cellrenderertext",
135 cell_area
.x
+ CELL_TEXT_INDENT
,
136 cell_area
.y
+ text_y
,
139 if gtk
.pygtk_version
< (2, 8, 0):
140 # Note sure exactly which versions need this.
141 # 2.8.0 gives a warning if you include it, though.
142 gobject
.type_register(IconAndTextRenderer
)
143 gobject
.type_register(MenuIconRenderer
)
145 class InterfaceBrowser
:
150 original_implementation
= None
160 columns
= [(_('Component'), INTERFACE_NAME
),
161 (_('Version'), VERSION
),
162 (_('Fetch'), DOWNLOAD_SIZE
),
163 (_('Description'), SUMMARY
),
166 def __init__(self
, policy
, widgets
):
167 tips
= InterfaceTips(self
)
169 tree_view
= widgets
.get_widget('components')
172 self
.cached_icon
= {} # URI -> GdkPixbuf
173 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
174 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
176 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
)
177 self
.tree_view
= tree_view
178 tree_view
.set_model(self
.model
)
182 text
= gtk
.CellRendererText()
184 for name
, model_column
in self
.columns
:
185 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
186 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
188 image
= InterfaceBrowser
.ICON
)
189 elif model_column
== None:
190 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
192 if model_column
== InterfaceBrowser
.SUMMARY
:
193 text_ellip
= gtk
.CellRendererText()
195 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
198 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
199 column
.set_expand(True)
201 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
202 tree_view
.append_column(column
)
203 column_objects
.append(column
)
205 tree_view
.set_enable_search(True)
207 selection
= tree_view
.get_selection()
209 def motion(tree_view
, ev
):
210 if ev
.window
is not tree_view
.get_bin_window():
212 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
216 col_index
= column_objects
.index(pos
[1])
220 col
= self
.columns
[col_index
][1]
221 row
= self
.model
[path
]
222 item
= (row
[InterfaceBrowser
.INTERFACE
], col
)
223 if item
!= tips
.item
:
224 tips
.prime(tree_view
, item
)
228 tree_view
.connect('motion-notify-event', motion
)
229 tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
231 def button_press(tree_view
, bev
):
232 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
235 path
, col
, x
, y
= pos
237 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
238 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
239 selection
.select_path(path
)
240 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
241 self
.show_popup_menu(iface
, bev
)
243 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
245 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
])
246 tree_view
.connect('button-press-event', button_press
)
248 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
249 policy
.watchers
.append(self
.build_tree
)
251 def set_root(self
, root
):
252 assert isinstance(root
, model
.Interface
)
255 def set_update_icons(self
, update_icons
):
257 # Clear icons cache to make sure they're really updated
258 self
.cached_icon
= {}
259 self
.update_icons
= update_icons
261 def get_icon(self
, iface
):
262 """Get an icon for this interface. If the icon is in the cache, use that.
263 If not, start a download. If we already started a download (successful or
264 not) do nothing. Returns None if no icon is currently available."""
266 # Try the in-memory cache
267 return self
.cached_icon
[iface
.uri
]
269 # Try the on-disk cache
270 iconpath
= iface_cache
.get_icon_path(iface
)
273 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
274 # (if icon is None, cache the fact that we can't load it)
275 self
.cached_icon
[iface
.uri
] = icon
279 # Download a new icon if we don't have one, or if the
280 # user did a 'Refresh'
281 if iconpath
is None or self
.update_icons
:
282 fetcher
= self
.policy
.download_icon(iface
)
284 if iface
.uri
not in self
.cached_icon
:
285 self
.cached_icon
[iface
.uri
] = None # Only try once
288 def update_display():
292 # Try to insert new icon into the cache
293 # If it fails, we'll be left with None in the cached_icon so
294 # we don't try again.
295 iconpath
= iface_cache
.get_icon_path(iface
)
297 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
300 warn("Failed to download icon for '%s'", iface
)
301 except Exception, ex
:
303 traceback
.print_exc()
304 self
.policy
.handler
.report_error(ex
)
306 # elif fetcher is None: don't store anything in cached_icon
308 # Note: if no icon is available for downloading,
309 # more attempts are made later.
310 # It can happen that no icon is yet available because
311 # the interface was not downloaded yet, in which case
312 # it's desireable to try again once the interface is available
317 def build_tree(self
):
318 if self
.original_implementation
is None:
319 self
.set_original_implementations()
321 done
= {} # Detect cycles
325 def add_node(parent
, iface
):
330 iter = self
.model
.append(parent
)
331 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
332 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
333 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
334 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
336 impl
= self
.policy
.implementation
.get(iface
, None)
338 old_impl
= self
.original_implementation
.get(iface
, None)
339 version_str
= impl
.get_version()
340 if old_impl
is not None and old_impl
.id != impl
.id:
341 version_str
+= _(' (was %s)') % old_impl
.get_version()
342 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
344 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
345 children
= self
.policy
.solver
.requires
[iface
]
347 for child
in children
:
348 if isinstance(child
, model
.InterfaceDependency
):
349 add_node(iter, iface_cache
.get_interface(child
.interface
))
351 child_iter
= self
.model
.append(parent
)
352 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
353 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
354 _('Unknown dependency type : %s') % child
355 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
357 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(choose)')
358 add_node(None, self
.root
)
359 self
.tree_view
.expand_all()
361 def show_popup_menu(self
, iface
, bev
):
365 have_source
= properties
.have_source_for(self
.policy
, iface
)
368 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
369 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
370 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
))]:
371 item
= gtk
.MenuItem(label
)
373 item
.connect('activate', lambda item
, cb
=cb
: cb())
375 item
.set_sensitive(False)
379 item
= gtk
.MenuItem(_('Compile'))
383 compile_menu
= gtk
.Menu()
384 item
.set_submenu(compile_menu
)
386 item
= gtk
.MenuItem(_('Automatic'))
387 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= True))
389 compile_menu
.append(item
)
391 item
= gtk
.MenuItem(_('Manual...'))
392 item
.connect('activate', lambda item
: compile.compile(self
.policy
, iface
, autocompile
= False))
394 compile_menu
.append(item
)
396 item
.set_sensitive(False)
398 menu
.popup(None, None, None, bev
.button
, bev
.time
)
400 def set_original_implementations(self
):
401 assert self
.original_implementation
is None
402 self
.original_implementation
= self
.policy
.implementation
.copy()
404 def update_download_status(self
):
405 """Called at regular intervals while there are downloads in progress,
406 and once at the end. Also called when things are added to the store.
407 Update the TreeView with the interfaces."""
409 for dl
in self
.policy
.handler
.monitored_downloads
.values():
411 if dl
.hint
not in hints
:
413 hints
[dl
.hint
].append(dl
)
415 selections
= self
.policy
.solver
.selections
420 for x
in walk(self
.model
.iter_children(it
)): yield x
421 it
= self
.model
.iter_next(it
)
423 for row
in walk(self
.model
.get_iter_root()):
424 iface
= row
[InterfaceBrowser
.INTERFACE
]
426 # Is this interface the download's hint?
427 downloads
= hints
.get(iface
, []) # The interface itself
428 downloads
+= hints
.get(iface
.uri
, []) # The main feed
429 for feed
in self
.policy
.usable_feeds(iface
):
430 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
431 impl
= selections
.get(iface
, None)
433 downloads
+= hints
.get(impl
, []) # The chosen implementation
440 expected
= (expected
or 0) + dl
.expected_size
441 so_far
+= dl
.get_bytes_downloaded_so_far()
443 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
444 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
446 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
448 summary
= ngettext("(downloading %(downloaded)s/unknown)",
449 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
451 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
452 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
454 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
455 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary