1 import gtk
, gobject
, pango
3 from zeroinstall
.support
import basedir
, tasks
, pretty_size
4 from zeroinstall
.injector
.iface_cache
import iface_cache
5 from zeroinstall
.injector
import model
7 from treetips
import TreeTips
8 from zeroinstall
import support
9 from logging
import warn
13 if impl
.user_stability
is None:
14 return impl
.upstream_stability
15 return _("%s (was %s)") % (impl
.user_stability
, impl
.upstream_stability
)
18 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
20 class InterfaceTips(TreeTips
):
23 def __init__(self
, mainwindow
):
24 self
.mainwindow
= mainwindow
26 def get_tooltip_text(self
, item
):
27 interface
, model_column
= item
29 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
30 return _("Full name: %s") % interface
.uri
31 elif model_column
== InterfaceBrowser
.SUMMARY
:
32 if not interface
.description
:
34 first_para
= interface
.description
.split('\n\n', 1)[0]
35 return first_para
.replace('\n', ' ')
36 elif model_column
is None:
37 return _("Click here for more options...")
39 impl
= self
.mainwindow
.policy
.implementation
.get(interface
, None)
41 return _("No suitable implementation was found. Check the "
42 "interface properties to find out why.")
44 if model_column
== InterfaceBrowser
.VERSION
:
45 text
= _("Currently preferred version: %s (%s)") % \
46 (impl
.get_version(), _stability(impl
))
47 old_impl
= self
.mainwindow
.original_implementation
.get(interface
, None)
48 if old_impl
is not None and old_impl
is not impl
:
49 text
+= _('\nPreviously preferred version: %s (%s)') % \
50 (old_impl
.get_version(), _stability(old_impl
))
53 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
55 if self
.mainwindow
.policy
.get_cached(impl
):
56 return _("This version is already stored on your computer.")
58 src
= self
.mainwindow
.policy
.fetcher
.get_best_source(impl
)
60 return _("No downloads available!")
61 return _("Need to download %s (%s bytes)") % \
62 (support
.pretty_size(src
.size
), src
.size
)
64 class MenuIconRenderer(gtk
.GenericCellRenderer
):
66 gtk
.GenericCellRenderer
.__init
__(self
)
67 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
69 def do_set_property(self
, prop
, value
):
70 setattr(self
, prop
.name
, value
)
72 def on_get_size(self
, widget
, cell_area
, layout
= None):
75 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
76 if flags
& gtk
.CELL_RENDERER_PRELIT
:
77 state
= gtk
.STATE_PRELIGHT
79 state
= gtk
.STATE_NORMAL
81 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
82 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
83 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
84 gtk
.ARROW_RIGHT
, True,
85 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
87 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
89 "image": (gobject
.TYPE_OBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
90 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
93 def do_set_property(self
, prop
, value
):
94 setattr(self
, prop
.name
, value
)
96 def on_get_size(self
, widget
, cell_area
, layout
= None):
98 layout
= widget
.create_pango_layout(self
.text
)
99 a
, rect
= layout
.get_pixel_extents()
101 pixmap_height
= self
.image
.get_height()
103 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
106 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
109 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
110 layout
= widget
.create_pango_layout(self
.text
)
111 a
, rect
= layout
.get_pixel_extents()
113 if flags
& gtk
.CELL_RENDERER_SELECTED
:
114 state
= gtk
.STATE_SELECTED
115 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
116 state
= gtk
.STATE_PRELIGHT
118 state
= gtk
.STATE_NORMAL
120 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
121 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
123 cell_area
.y
+ image_y
)
125 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
127 widget
.style
.paint_layout(window
, state
, True,
128 expose_area
, widget
, "cellrenderertext",
129 cell_area
.x
+ CELL_TEXT_INDENT
,
130 cell_area
.y
+ text_y
,
133 if gtk
.pygtk_version
< (2, 8, 0):
134 # Note sure exactly which versions need this.
135 # 2.8.0 gives a warning if you include it, though.
136 gobject
.type_register(IconAndTextRenderer
)
137 gobject
.type_register(MenuIconRenderer
)
139 class InterfaceBrowser
:
144 original_implementation
= None
153 columns
= [(_('Interface'), INTERFACE_NAME
),
154 (_('Version'), VERSION
),
155 (_('Fetch'), DOWNLOAD_SIZE
),
156 (_('Description'), SUMMARY
),
159 def __init__(self
, policy
, widgets
):
160 tips
= InterfaceTips(self
)
162 tree_view
= widgets
.get_widget('components')
165 self
.cached_icon
= {} # URI -> GdkPixbuf
166 self
.default_icon
= tree_view
.style
.lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.style
,
167 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
169 self
.model
= gtk
.TreeStore(object, str, str, str, str, gtk
.gdk
.Pixbuf
)
170 self
.tree_view
= tree_view
171 tree_view
.set_model(self
.model
)
175 text
= gtk
.CellRendererText()
177 for name
, model_column
in self
.columns
:
178 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
179 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
181 image
= InterfaceBrowser
.ICON
)
182 elif model_column
== None:
183 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
185 if model_column
== InterfaceBrowser
.SUMMARY
:
186 text_ellip
= gtk
.CellRendererText()
188 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
191 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
192 column
.set_expand(True)
194 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
195 tree_view
.append_column(column
)
196 column_objects
.append(column
)
198 tree_view
.set_enable_search(True)
200 selection
= tree_view
.get_selection()
202 def motion(tree_view
, ev
):
203 if ev
.window
is not tree_view
.get_bin_window():
205 pos
= tree_view
.get_path_at_pos(int(ev
.x
), int(ev
.y
))
209 col_index
= column_objects
.index(pos
[1])
213 col
= self
.columns
[col_index
][1]
214 row
= self
.model
[path
]
215 item
= (row
[InterfaceBrowser
.INTERFACE
], col
)
216 if item
!= tips
.item
:
217 tips
.prime(tree_view
, item
)
221 tree_view
.connect('motion-notify-event', motion
)
222 tree_view
.connect('leave-notify-event', lambda tv
, ev
: tips
.hide())
224 def button_press(tree_view
, bev
):
225 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
228 path
, col
, x
, y
= pos
230 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
231 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
232 selection
.select_path(path
)
233 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
234 self
.show_popup_menu(iface
, bev
)
236 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
238 properties
.edit(policy
, self
.model
[path
][InterfaceBrowser
.INTERFACE
])
239 tree_view
.connect('button-press-event', button_press
)
241 tree_view
.connect('destroy', lambda s
: policy
.watchers
.remove(self
.build_tree
))
242 policy
.watchers
.append(self
.build_tree
)
244 def set_root(self
, root
):
245 assert isinstance(root
, model
.Interface
)
248 def get_icon(self
, iface
):
249 """Get an icon for this interface. If the icon is in the cache, use that.
250 If not, start a download. If we already started a download (successful or
251 not) do nothing. Returns None if no icon is currently available."""
253 return self
.cached_icon
[iface
.uri
]
255 path
= iface_cache
.get_icon_path(iface
)
258 loader
= gtk
.gdk
.PixbufLoader('png')
260 loader
.write(file(path
).read())
263 icon
= loader
.get_pixbuf()
264 assert icon
, "Failed to load cached PNG icon data"
265 except Exception, ex
:
266 warn("Failed to load cached PNG icon: %s", ex
)
269 h
= icon
.get_height()
270 scale
= max(w
, h
, 1) / ICON_SIZE
271 icon
= icon
.scale_simple(int(w
/ scale
),
273 gtk
.gdk
.INTERP_BILINEAR
)
274 self
.cached_icon
[iface
.uri
] = icon
277 # Try to download the icon
278 fetcher
= self
.policy
.download_icon(iface
)
281 def update_display():
286 except Exception, ex
:
288 traceback
.print_exc()
289 self
.policy
.handler
.report_error(ex
)
294 def build_tree(self
):
295 if self
.original_implementation
is None:
296 self
.set_original_implementations()
298 done
= {} # Detect cycles
302 def add_node(parent
, iface
):
307 iter = self
.model
.append(parent
)
308 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
309 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
310 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
311 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
313 impl
= self
.policy
.implementation
.get(iface
, None)
315 old_impl
= self
.original_implementation
.get(iface
, None)
316 version_str
= impl
.get_version()
317 if old_impl
is not None and old_impl
is not impl
:
318 version_str
+= " (was " + old_impl
.get_version() + ")"
319 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
321 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = self
._get
_fetch
_info
(impl
)
322 if hasattr(impl
, 'requires'):
323 children
= impl
.requires
325 children
= impl
.dependencies
327 for child
in children
:
328 if isinstance(child
, model
.InterfaceDependency
):
329 add_node(iter, iface_cache
.get_interface(child
.interface
))
331 child_iter
= self
.model
.append(parent
)
332 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
333 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
334 'Unknown dependency type : %s' % child
335 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
337 self
.model
[iter][InterfaceBrowser
.VERSION
] = '(choose)'
338 add_node(None, self
.root
)
339 self
.tree_view
.expand_all()
341 def _get_fetch_info(self
, impl
):
342 """Get the text for the Fetch column."""
345 elif self
.policy
.get_cached(impl
):
346 if impl
.id.startswith('/'):
348 elif impl
.id.startswith('package:'):
353 src
= self
.policy
.fetcher
.get_best_source(impl
)
355 return support
.pretty_size(src
.size
)
357 return '(unavailable)'
359 def show_popup_menu(self
, iface
, bev
):
362 if properties
.have_source_for(self
.policy
, iface
):
365 compile.compile(self
.policy
, iface
)
370 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
371 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
372 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
)),
373 (_('Compile...'), compile_cb
)]:
374 item
= gtk
.MenuItem(label
)
376 item
.connect('activate', lambda item
, cb
=cb
: cb())
378 item
.set_sensitive(False)
381 menu
.popup(None, None, None, bev
.button
, bev
.time
)
383 def set_original_implementations(self
):
384 assert self
.original_implementation
is None
385 self
.original_implementation
= self
.policy
.implementation
.copy()
387 def update_download_status(self
):
388 """Called at regular intervals while there are downloads in progress,
389 and once at the end. Also called when things are added to the store.
390 Update the TreeView with the interfaces."""
392 for dl
in self
.policy
.handler
.monitored_downloads
.values():
394 if dl
.hint
not in hints
:
396 hints
[dl
.hint
].append(dl
)
398 selections
= self
.policy
.solver
.selections
403 for x
in walk(self
.model
.iter_children(it
)): yield x
404 it
= self
.model
.iter_next(it
)
406 for row
in walk(self
.model
.get_iter_root()):
407 iface
= row
[InterfaceBrowser
.INTERFACE
]
409 # Is this interface the download's hint?
410 downloads
= hints
.get(iface
, []) # The interface itself
411 downloads
+= hints
.get(iface
.uri
, []) # The main feed
412 for feed
in iface
.feeds
:
413 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
414 impl
= selections
.get(iface
, None)
416 downloads
+= hints
.get(impl
, []) # The chosen implementation
423 expected
= (expected
or 0) + dl
.expected_size
424 so_far
+= dl
.get_bytes_downloaded_so_far()
426 fraction
= "%s [%.2f%%]" % (pretty_size(expected
), 100 * so_far
/ float(expected
))
429 if len(downloads
) > 1:
430 fraction
+= " in %d downloads" % len(downloads
)
431 row
[InterfaceBrowser
.SUMMARY
] = "(downloading %s/%s)" % (pretty_size(so_far
), fraction
)
433 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = self
._get
_fetch
_info
(impl
)
434 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary