1 # Copyright (C) 2008, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import gtk
, gobject
, pango
6 from zeroinstall
.support
import basedir
, tasks
, pretty_size
7 from zeroinstall
.injector
.iface_cache
import iface_cache
8 from zeroinstall
.injector
import model
10 from 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
, item
):
31 interface
, model_column
= 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(self
, iface
):
253 """Get an icon for this interface. If the icon is in the cache, use that.
254 If not, start a download. If we already started a download (successful or
255 not) do nothing. Returns None if no icon is currently available."""
257 return self
.cached_icon
[iface
.uri
]
259 path
= iface_cache
.get_icon_path(iface
)
262 loader
= gtk
.gdk
.PixbufLoader('png')
264 loader
.write(file(path
).read())
267 icon
= loader
.get_pixbuf()
268 assert icon
, "Failed to load cached PNG icon data"
269 except Exception, ex
:
270 warn("Failed to load cached PNG icon: %s", ex
)
273 h
= icon
.get_height()
274 scale
= max(w
, h
, 1) / ICON_SIZE
275 icon
= icon
.scale_simple(int(w
/ scale
),
277 gtk
.gdk
.INTERP_BILINEAR
)
278 self
.cached_icon
[iface
.uri
] = icon
281 # Try to download the icon
282 fetcher
= self
.policy
.download_icon(iface
)
285 def update_display():
290 except Exception, ex
:
292 traceback
.print_exc()
293 self
.policy
.handler
.report_error(ex
)
298 def build_tree(self
):
299 if self
.original_implementation
is None:
300 self
.set_original_implementations()
302 done
= {} # Detect cycles
306 def add_node(parent
, iface
):
311 iter = self
.model
.append(parent
)
312 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
313 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = iface
.get_name()
314 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = iface
.summary
315 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
317 impl
= self
.policy
.implementation
.get(iface
, None)
319 old_impl
= self
.original_implementation
.get(iface
, None)
320 version_str
= impl
.get_version()
321 if old_impl
is not None and old_impl
is not impl
:
322 version_str
+= " (was " + old_impl
.get_version() + ")"
323 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
325 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
326 if hasattr(impl
, 'requires'):
327 children
= impl
.requires
329 children
= impl
.dependencies
331 for child
in children
:
332 if isinstance(child
, model
.InterfaceDependency
):
333 add_node(iter, iface_cache
.get_interface(child
.interface
))
335 child_iter
= self
.model
.append(parent
)
336 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
337 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
338 'Unknown dependency type : %s' % child
339 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
341 self
.model
[iter][InterfaceBrowser
.VERSION
] = '(choose)'
342 add_node(None, self
.root
)
343 self
.tree_view
.expand_all()
345 def show_popup_menu(self
, iface
, bev
):
348 if properties
.have_source_for(self
.policy
, iface
):
351 compile.compile(self
.policy
, iface
)
356 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.policy
, iface
)),
357 (_('Show Versions'), lambda: properties
.edit(self
.policy
, iface
, show_versions
= True)),
358 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.policy
, iface
)),
359 (_('Compile...'), compile_cb
)]:
360 item
= gtk
.MenuItem(label
)
362 item
.connect('activate', lambda item
, cb
=cb
: cb())
364 item
.set_sensitive(False)
367 menu
.popup(None, None, None, bev
.button
, bev
.time
)
369 def set_original_implementations(self
):
370 assert self
.original_implementation
is None
371 self
.original_implementation
= self
.policy
.implementation
.copy()
373 def update_download_status(self
):
374 """Called at regular intervals while there are downloads in progress,
375 and once at the end. Also called when things are added to the store.
376 Update the TreeView with the interfaces."""
378 for dl
in self
.policy
.handler
.monitored_downloads
.values():
380 if dl
.hint
not in hints
:
382 hints
[dl
.hint
].append(dl
)
384 selections
= self
.policy
.solver
.selections
389 for x
in walk(self
.model
.iter_children(it
)): yield x
390 it
= self
.model
.iter_next(it
)
392 for row
in walk(self
.model
.get_iter_root()):
393 iface
= row
[InterfaceBrowser
.INTERFACE
]
395 # Is this interface the download's hint?
396 downloads
= hints
.get(iface
, []) # The interface itself
397 downloads
+= hints
.get(iface
.uri
, []) # The main feed
398 for feed
in iface
.feeds
:
399 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
400 impl
= selections
.get(iface
, None)
402 downloads
+= hints
.get(impl
, []) # The chosen implementation
409 expected
= (expected
or 0) + dl
.expected_size
410 so_far
+= dl
.get_bytes_downloaded_so_far()
412 fraction
= "%s [%.2f%%]" % (pretty_size(expected
), 100 * so_far
/ float(expected
))
415 if len(downloads
) > 1:
416 fraction
+= " in %d downloads" % len(downloads
)
417 row
[InterfaceBrowser
.SUMMARY
] = "(downloading %s/%s)" % (pretty_size(so_far
), fraction
)
419 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.policy
, impl
)
420 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary