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
import support
12 from logging
import warn
17 if impl
.user_stability
is None:
18 return impl
.upstream_stability
19 return _("%s (was %s)") % (impl
.user_stability
, impl
.upstream_stability
)
22 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
24 class InterfaceTips(TreeTips
):
27 def __init__(self
, mainwindow
):
28 self
.mainwindow
= mainwindow
30 def get_tooltip_text(self
):
31 interface
, model_column
= self
.item
33 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
34 return _("Full name: %s") % interface
.uri
35 elif model_column
== InterfaceBrowser
.SUMMARY
:
36 if not interface
.description
:
38 first_para
= interface
.description
.split('\n\n', 1)[0]
39 return first_para
.replace('\n', ' ')
40 elif model_column
is None:
41 return _("Click here for more options...")
43 impl
= self
.mainwindow
.policy
.implementation
.get(interface
, None)
45 return _("No suitable implementation was found. Check the "
46 "interface properties to find out why.")
48 if model_column
== InterfaceBrowser
.VERSION
:
49 text
= _("Currently preferred version: %s (%s)") % \
50 (impl
.get_version(), _stability(impl
))
51 old_impl
= self
.mainwindow
.original_implementation
.get(interface
, None)
52 if old_impl
is not None and old_impl
is not impl
:
53 text
+= _('\nPreviously preferred version: %s (%s)') % \
54 (old_impl
.get_version(), _stability(old_impl
))
57 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
59 if self
.mainwindow
.policy
.get_cached(impl
):
60 return _("This version is already stored on your computer.")
62 src
= self
.mainwindow
.policy
.fetcher
.get_best_source(impl
)
64 return _("No downloads available!")
65 return _("Need to download %s (%s bytes)") % \
66 (support
.pretty_size(src
.size
), src
.size
)
68 class MenuIconRenderer(gtk
.GenericCellRenderer
):
70 gtk
.GenericCellRenderer
.__init
__(self
)
71 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
73 def do_set_property(self
, prop
, value
):
74 setattr(self
, prop
.name
, value
)
76 def on_get_size(self
, widget
, cell_area
, layout
= None):
79 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
80 if flags
& gtk
.CELL_RENDERER_PRELIT
:
81 state
= gtk
.STATE_PRELIGHT
83 state
= gtk
.STATE_NORMAL
85 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
86 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
87 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
88 gtk
.ARROW_RIGHT
, True,
89 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
91 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
93 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
94 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
97 def do_set_property(self
, prop
, value
):
98 setattr(self
, prop
.name
, value
)
100 def on_get_size(self
, widget
, cell_area
, layout
= None):
102 layout
= widget
.create_pango_layout(self
.text
)
103 a
, rect
= layout
.get_pixel_extents()
105 pixmap_height
= self
.image
.get_height()
107 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
110 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
113 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
114 layout
= widget
.create_pango_layout(self
.text
)
115 a
, rect
= layout
.get_pixel_extents()
117 if flags
& gtk
.CELL_RENDERER_SELECTED
:
118 state
= gtk
.STATE_SELECTED
119 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
120 state
= gtk
.STATE_PRELIGHT
122 state
= gtk
.STATE_NORMAL
124 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
125 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
127 cell_area
.y
+ image_y
)
129 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
131 widget
.style
.paint_layout(window
, state
, True,
132 expose_area
, widget
, "cellrenderertext",
133 cell_area
.x
+ CELL_TEXT_INDENT
,
134 cell_area
.y
+ text_y
,
137 if gtk
.pygtk_version
< (2, 8, 0):
138 # Note sure exactly which versions need this.
139 # 2.8.0 gives a warning if you include it, though.
140 gobject
.type_register(IconAndTextRenderer
)
141 gobject
.type_register(MenuIconRenderer
)
143 class InterfaceBrowser
:
148 original_implementation
= None
157 columns
= [(_('Component'), INTERFACE_NAME
),
158 (_('Version'), VERSION
),
159 (_('Fetch'), DOWNLOAD_SIZE
),
160 (_('Description'), SUMMARY
),
163 def __init__(self
, policy
, widgets
):
164 tips
= InterfaceTips(self
)
166 tree_view
= widgets
.get_widget('components')
169 self
.cached_icon
= {} # URI -> GdkPixbuf
170 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
171 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
173 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
)
174 self
.tree_view
= tree_view
175 tree_view
.set_model(self
.model
)
179 text
= gtk
.CellRendererText()
181 for name
, model_column
in self
.columns
:
182 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
183 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
185 image
= InterfaceBrowser
.ICON
)
186 elif model_column
== None:
187 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
189 if model_column
== InterfaceBrowser
.SUMMARY
:
190 text_ellip
= gtk
.CellRendererText()
192 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
195 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
196 column
.set_expand(True)
198 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
199 tree_view
.append_column(column
)
200 column_objects
.append(column
)
202 tree_view
.set_enable_search(True)
204 selection
= tree_view
.get_selection()
206 def motion(tree_view
, ev
):
207 if ev
.window
is not tree_view
.get_bin_window():
209 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
213 col_index
= column_objects
.index(pos
[1])
217 col
= self
.columns
[col_index
][1]
218 row
= self
.model
[path
]
219 item
= (row
[InterfaceBrowser
.INTERFACE
], col
)
220 if item
!= tips
.item
:
221 tips
.prime(tree_view
, item
)
225 tree_view
.connect('motion-notify-event', motion
)
226 tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
228 def button_press(tree_view
, bev
):
229 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
232 path
, col
, x
, y
= pos
234 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
235 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
236 selection
.select_path(path
)
237 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
238 self
.show_popup_menu(iface
, bev
)
240 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
242 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
])
243 tree_view
.connect('button-press-event', button_press
)
245 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
246 policy
.watchers
.append(self
.build_tree
)
248 def set_root(self
, root
):
249 assert isinstance(root
, model
.Interface
)
252 def _get_icon_from_cache(self
, iface
):
253 path
= iface_cache
.get_icon_path(iface
)
256 loader
= gtk
.gdk
.PixbufLoader('png')
258 loader
.write(file(path
).read())
261 icon
= loader
.get_pixbuf()
262 assert icon
, "Failed to load cached PNG icon data"
263 except Exception, ex
:
264 warn("Failed to load cached PNG icon: %s", ex
)
267 h
= icon
.get_height()
268 scale
= max(w
, h
, 1) / ICON_SIZE
269 icon
= icon
.scale_simple(int(w
/ scale
),
271 gtk
.gdk
.INTERP_BILINEAR
)
272 self
.cached_icon
[iface
.uri
] = icon
277 def get_icon(self
, iface
):
278 """Get an icon for this interface. If the icon is in the cache, use that.
279 If not, start a download. If we already started a download (successful or
280 not) do nothing. Returns None if no icon is currently available."""
282 return self
.cached_icon
[iface
.uri
]
284 icon
= self
._get
_icon
_from
_cache
(iface
)
288 # Try to download the icon
289 fetcher
= self
.policy
.download_icon(iface
)
291 self
.cached_icon
[iface
.uri
] = None # Only try once
293 def update_display():
297 # Try to insert new icon into the cache
298 # If it fails, we'll be left with None in the cached_icon so
299 # we don't try again.
300 self
._get
_icon
_from
_cache
(iface
)
302 except Exception, ex
:
304 traceback
.print_exc()
305 self
.policy
.handler
.report_error(ex
)
307 # Note: if no icon is available for downloading,
308 # more attempts are made later.
309 # It can happen that no icon is yet available because
310 # the interface was not downloaded yet, in which case
311 # it's desireable to try again once the interface is available
315 def build_tree(self
):
316 if self
.original_implementation
is None:
317 self
.set_original_implementations()
319 done
= {} # Detect cycles
323 def add_node(parent
, iface
):
328 iter = self
.model
.append(parent
)
329 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
330 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
331 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
332 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
334 impl
= self
.policy
.implementation
.get(iface
, None)
336 old_impl
= self
.original_implementation
.get(iface
, None)
337 version_str
= impl
.get_version()
338 if old_impl
is not None and old_impl
is not impl
:
339 version_str
+= _(' (was %s)') % old_impl
.get_version()
340 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
342 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
343 if hasattr(impl
, 'requires'):
344 children
= impl
.requires
346 children
= impl
.dependencies
348 for child
in children
:
349 if isinstance(child
, model
.InterfaceDependency
):
350 add_node(iter, iface_cache
.get_interface(child
.interface
))
352 child_iter
= self
.model
.append(parent
)
353 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
354 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
355 _('Unknown dependency type : %s') % child
356 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
358 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(choose)')
359 add_node(None, self
.root
)
360 self
.tree_view
.expand_all()
362 def show_popup_menu(self
, iface
, bev
):
365 if properties
.have_source_for(self
.policy
, iface
):
368 compile.compile(self
.policy
, iface
)
373 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
374 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
375 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
)),
376 (_('Compile...'), compile_cb
)]:
377 item
= gtk
.MenuItem(label
)
379 item
.connect('activate', lambda item
, cb
=cb
: cb())
381 item
.set_sensitive(False)
384 menu
.popup(None, None, None, bev
.button
, bev
.time
)
386 def set_original_implementations(self
):
387 assert self
.original_implementation
is None
388 self
.original_implementation
= self
.policy
.implementation
.copy()
390 def update_download_status(self
):
391 """Called at regular intervals while there are downloads in progress,
392 and once at the end. Also called when things are added to the store.
393 Update the TreeView with the interfaces."""
395 for dl
in self
.policy
.handler
.monitored_downloads
.values():
397 if dl
.hint
not in hints
:
399 hints
[dl
.hint
].append(dl
)
401 selections
= self
.policy
.solver
.selections
406 for x
in walk(self
.model
.iter_children(it
)): yield x
407 it
= self
.model
.iter_next(it
)
409 for row
in walk(self
.model
.get_iter_root()):
410 iface
= row
[InterfaceBrowser
.INTERFACE
]
412 # Is this interface the download's hint?
413 downloads
= hints
.get(iface
, []) # The interface itself
414 downloads
+= hints
.get(iface
.uri
, []) # The main feed
415 for feed
in iface
.feeds
:
416 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
417 impl
= selections
.get(iface
, None)
419 downloads
+= hints
.get(impl
, []) # The chosen implementation
426 expected
= (expected
or 0) + dl
.expected_size
427 so_far
+= dl
.get_bytes_downloaded_so_far()
429 fraction
= "%s [%.2f%%]" % (pretty_size(expected
), 100 * so_far
/ float(expected
))
431 fraction
= _("unknown")
432 if len(downloads
) > 1:
433 fraction
+= _(" in %d downloads") % len(downloads
)
434 row
[InterfaceBrowser
.SUMMARY
] = _("(downloading %s/%s)") % (pretty_size(so_far
), fraction
)
436 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
437 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary