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
import _
, translation
7 from zeroinstall
.support
import tasks
, pretty_size
8 from zeroinstall
.injector
import model
, reader
10 from zeroinstall
.gtkui
.icon
import load_icon
11 from zeroinstall
import support
12 from logging
import warn
, info
15 ngettext
= translation
.ngettext
19 if impl
.user_stability
is None:
20 return _(str(impl
.upstream_stability
))
21 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
22 % {'implementation_user_stability': _(str(impl
.user_stability
)),
23 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
26 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
28 def get_tooltip_text(mainwindow
, interface
, main_feed
, model_column
):
30 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
31 return _("Full name: %s") % interface
.uri
32 elif model_column
== InterfaceBrowser
.SUMMARY
:
33 if main_feed
is None or not main_feed
.description
:
34 return _("(no description available)")
35 first_para
= main_feed
.description
.split('\n\n', 1)[0]
36 return first_para
.replace('\n', ' ')
37 elif model_column
is None:
38 return _("Click here for more options...")
40 impl
= mainwindow
.driver
.solver
.selections
.get(interface
, None)
42 return _("No suitable version was found. Double-click "
43 "here to find out why.")
45 if model_column
== InterfaceBrowser
.VERSION
:
46 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
47 {'version': impl
.get_version(), 'stability': _stability(impl
)}
48 old_impl
= mainwindow
.original_implementation
.get(interface
, None)
49 if old_impl
is not None and old_impl
is not impl
:
50 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
51 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
54 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
56 if impl
.is_available(mainwindow
.driver
.config
.stores
):
57 return _("This version is already stored on your computer.")
59 src
= mainwindow
.driver
.config
.fetcher
.get_best_source(impl
)
61 return _("No downloads available!")
62 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
63 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
66 angle_right
= math
.pi
/ 2
67 class MenuIconRenderer(gtk
.GenericCellRenderer
):
69 gtk
.GenericCellRenderer
.__init
__(self
)
70 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
72 def do_set_property(self
, prop
, value
):
73 setattr(self
, prop
.name
, value
)
75 def do_get_size(self
, widget
, cell_area
, layout
= None):
77 on_get_size
= do_get_size
# GTK 2
79 if gtk
.pygtk_version
>= (2, 90):
80 # note: if you get "TypeError: Couldn't find conversion for foreign struct 'cairo.Context'", you need "python3-gi-cairo"
81 def do_render(self
, cr
, widget
, background_area
, cell_area
, flags
): # GTK 3
82 context
= widget
.get_style_context()
83 gtk
.render_arrow(context
, cr
, angle_right
,
84 cell_area
.x
+ 5, cell_area
.y
+ 5, max(cell_area
.width
, cell_area
.height
) - 10)
86 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
): # GTK 2
87 if flags
& gtk
.CELL_RENDERER_PRELIT
:
88 state
= gtk
.STATE_PRELIGHT
90 state
= gtk
.STATE_NORMAL
92 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
93 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
94 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
95 gtk
.ARROW_RIGHT
, True,
96 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
98 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
100 "image": (gobject
.TYPE_PYOBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
101 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
104 def do_set_property(self
, prop
, value
):
105 setattr(self
, prop
.name
, value
)
107 def do_get_size(self
, widget
, cell_area
, layout
= None):
109 layout
= widget
.create_pango_layout(self
.text
)
110 a
, rect
= layout
.get_pixel_extents()
112 pixmap_height
= self
.image
.get_height()
114 if not isinstance(rect
, tuple):
115 rect
= (rect
.x
, rect
.y
, rect
.width
, rect
.height
) # GTK 3
117 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
120 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
122 on_get_size
= do_get_size
# GTK 2
124 if gtk
.pygtk_version
>= (2, 90):
125 def do_render(self
, cr
, widget
, background_area
, cell_area
, flags
): # GTK 3
126 layout
= widget
.create_pango_layout(self
.text
)
127 a
, rect
= layout
.get_pixel_extents()
128 context
= widget
.get_style_context()
130 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
131 gtk
.render_icon(context
, cr
, self
.image
, cell_area
.x
, cell_area
.y
)
133 text_y
= int(0.5 * (cell_area
.height
- (rect
.y
+ rect
.height
)))
135 gtk
.render_layout(context
, cr
,
136 cell_area
.x
+ CELL_TEXT_INDENT
,
137 cell_area
.y
+ text_y
,
140 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
): # GTK 2
141 layout
= widget
.create_pango_layout(self
.text
)
142 a
, rect
= layout
.get_pixel_extents()
144 if flags
& gtk
.CELL_RENDERER_SELECTED
:
145 state
= gtk
.STATE_SELECTED
146 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
147 state
= gtk
.STATE_PRELIGHT
149 state
= gtk
.STATE_NORMAL
151 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
152 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
154 cell_area
.y
+ image_y
)
156 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
158 widget
.style
.paint_layout(window
, state
, True,
159 expose_area
, widget
, "cellrenderertext",
160 cell_area
.x
+ CELL_TEXT_INDENT
,
161 cell_area
.y
+ text_y
,
164 if gtk
.pygtk_version
< (2, 8, 0):
165 # Note sure exactly which versions need this.
166 # 2.8.0 gives a warning if you include it, though.
167 gobject
.type_register(IconAndTextRenderer
)
168 gobject
.type_register(MenuIconRenderer
)
173 for x
in walk(model
, model
.iter_children(it
)): yield x
174 it
= model
.iter_next(it
)
176 class InterfaceBrowser
:
182 original_implementation
= None
194 columns
= [(_('Component'), INTERFACE_NAME
),
195 (_('Version'), VERSION
),
196 (_('Fetch'), DOWNLOAD_SIZE
),
197 (_('Description'), SUMMARY
),
200 def __init__(self
, driver
, widgets
):
202 self
.config
= driver
.config
204 tree_view
= widgets
.get_widget('components')
205 tree_view
.set_property('has-tooltip', True)
206 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
207 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
208 pos
= tree_view
.get_path_at_pos(x
, y
)
210 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
213 col_index
= column_objects
.index(pos
[1])
217 col
= self
.columns
[col_index
][1]
218 row
= self
.model
[path
]
219 iface
= row
[InterfaceBrowser
.INTERFACE
]
220 main_feed
= self
.config
.iface_cache
.get_feed(iface
.uri
)
221 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
225 tree_view
.connect('query-tooltip', callback
)
227 self
.cached_icon
= {} # URI -> GdkPixbuf
228 self
.default_icon
= tree_view
.get_style().lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.get_style(),
229 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
231 self
.model
= gtk
.TreeStore(object, str, str, str, str, gobject
.TYPE_PYOBJECT
, str, bool)
232 self
.tree_view
= tree_view
233 tree_view
.set_model(self
.model
)
237 text
= gtk
.CellRendererText()
238 coloured_text
= gtk
.CellRendererText()
240 for name
, model_column
in self
.columns
:
241 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
242 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
244 image
= InterfaceBrowser
.ICON
)
245 elif model_column
== None:
246 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
248 if model_column
== InterfaceBrowser
.SUMMARY
:
249 text_ellip
= gtk
.CellRendererText()
251 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
254 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
255 column
.set_expand(True)
256 elif model_column
== InterfaceBrowser
.VERSION
:
257 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
258 background
= InterfaceBrowser
.BACKGROUND
)
260 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
261 tree_view
.append_column(column
)
262 column_objects
.append(column
)
264 tree_view
.set_enable_search(True)
266 selection
= tree_view
.get_selection()
268 def button_press(tree_view
, bev
):
269 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
272 path
, col
, x
, y
= pos
274 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
275 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
276 selection
.select_path(path
)
277 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
278 self
.show_popup_menu(iface
, bev
)
280 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
282 properties
.edit(driver
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
283 tree_view
.connect('button-press-event', button_press
)
285 tree_view
.connect('destroy', lambda s
: driver
.watchers
.remove(self
.build_tree
))
286 driver
.watchers
.append(self
.build_tree
)
288 def set_root(self
, root
):
289 assert isinstance(root
, model
.Interface
)
292 def set_update_icons(self
, update_icons
):
294 # Clear icons cache to make sure they're really updated
295 self
.cached_icon
= {}
296 self
.update_icons
= update_icons
298 def get_icon(self
, iface
):
299 """Get an icon for this interface. If the icon is in the cache, use that.
300 If not, start a download. If we already started a download (successful or
301 not) do nothing. Returns None if no icon is currently available."""
303 # Try the in-memory cache
304 return self
.cached_icon
[iface
.uri
]
306 # Try the on-disk cache
307 iconpath
= self
.config
.iface_cache
.get_icon_path(iface
)
310 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
311 # (if icon is None, cache the fact that we can't load it)
312 self
.cached_icon
[iface
.uri
] = icon
316 # Download a new icon if we don't have one, or if the
317 # user did a 'Refresh'
318 if iconpath
is None or self
.update_icons
:
319 if self
.config
.network_use
== model
.network_offline
:
322 fetcher
= self
.config
.fetcher
.download_icon(iface
)
324 if iface
.uri
not in self
.cached_icon
:
325 self
.cached_icon
[iface
.uri
] = None # Only try once
328 def update_display():
332 # Try to insert new icon into the cache
333 # If it fails, we'll be left with None in the cached_icon so
334 # we don't try again.
335 iconpath
= self
.config
.iface_cache
.get_icon_path(iface
)
337 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
340 warn("Failed to download icon for '%s'", iface
)
341 except Exception as ex
:
343 traceback
.print_exc()
344 self
.config
.handler
.report_error(ex
)
346 # elif fetcher is None: don't store anything in cached_icon
348 # Note: if no icon is available for downloading,
349 # more attempts are made later.
350 # It can happen that no icon is yet available because
351 # the interface was not downloaded yet, in which case
352 # it's desireable to try again once the interface is available
357 def build_tree(self
):
358 iface_cache
= self
.config
.iface_cache
360 if self
.original_implementation
is None:
361 self
.set_original_implementations()
363 done
= {} # Detect cycles
365 sels
= self
.driver
.solver
.selections
368 def add_node(parent
, iface
, commands
, essential
):
373 main_feed
= iface_cache
.get_feed(iface
.uri
)
375 name
= main_feed
.get_name()
376 summary
= main_feed
.summary
378 name
= iface
.get_name()
381 iter = self
.model
.append(parent
)
382 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
383 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
384 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
385 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
386 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = False
388 sel
= sels
.selections
.get(iface
.uri
, None)
391 old_impl
= self
.original_implementation
.get(iface
, None)
392 version_str
= impl
.get_version()
393 if old_impl
is not None and old_impl
.id != impl
.id:
394 version_str
+= _(' (was %s)') % old_impl
.get_version()
395 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
397 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
399 deps
= sel
.dependencies
401 deps
+= sel
.get_command(c
).requires
403 if isinstance(child
, model
.InterfaceDependency
):
405 iface_cache
.get_interface(child
.interface
),
406 child
.get_required_commands(),
407 child
.importance
== model
.Dependency
.Essential
)
409 child_iter
= self
.model
.append(parent
)
410 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
411 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
412 _('Unknown dependency type : %s') % child
413 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
415 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = essential
416 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)') if essential
else _('(none)')
418 add_node(None, self
.root
, [sels
.command
], essential
= True)
420 add_node(None, self
.root
, [], essential
= True)
421 self
.tree_view
.expand_all()
423 def show_popup_menu(self
, iface
, bev
):
426 have_source
= properties
.have_source_for(self
.config
, iface
)
428 global menu
# Fix GC problem in PyGObject
430 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.driver
, iface
, self
.compile)),
431 (_('Show Versions'), lambda: properties
.edit(self
.driver
, iface
, self
.compile, show_versions
= True)),
432 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.driver
, iface
))]:
433 item
= gtk
.MenuItem()
434 item
.set_label(label
)
436 item
.connect('activate', lambda item
, cb
=cb
: cb())
438 item
.set_sensitive(False)
442 item
= gtk
.MenuItem()
443 item
.set_label(_('Compile'))
447 compile_menu
= gtk
.Menu()
448 item
.set_submenu(compile_menu
)
450 item
= gtk
.MenuItem()
451 item
.set_label(_('Automatic'))
452 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
454 compile_menu
.append(item
)
456 item
= gtk
.MenuItem()
457 item
.set_label(_('Manual...'))
458 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
460 compile_menu
.append(item
)
462 item
.set_sensitive(False)
464 if gtk
.pygtk_version
>= (2, 90):
465 menu
.popup(None, None, None, None, bev
.button
, bev
.time
)
467 menu
.popup(None, None, None, bev
.button
, bev
.time
)
469 def compile(self
, interface
, autocompile
= True):
472 # A new local feed may have been registered, so reload it from the disk cache
473 info(_("0compile command completed successfully. Reloading interface details."))
474 reader
.update_from_cache(interface
)
475 for feed
in interface
.extra_feeds
:
476 self
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
479 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
481 def set_original_implementations(self
):
482 assert self
.original_implementation
is None
483 self
.original_implementation
= self
.driver
.solver
.selections
.copy()
485 def update_download_status(self
, only_update_visible
= False):
486 """Called at regular intervals while there are downloads in progress,
487 and once at the end. Also called when things are added to the store.
488 Update the TreeView with the interfaces."""
490 # A download may be for a feed, an interface or an implementation.
491 # Create the reverse mapping (item -> download)
493 for dl
in self
.config
.handler
.monitored_downloads
:
495 if dl
.hint
not in hints
:
497 hints
[dl
.hint
].append(dl
)
499 selections
= self
.driver
.solver
.selections
501 # Only update currently visible rows
502 if only_update_visible
and self
.tree_view
.get_visible_range() != None:
503 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
504 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
506 # (or should we just wait until the TreeView has settled enough to tell
507 # us what is visible?)
508 firstVisibleIter
= self
.model
.get_iter_root()
509 lastVisiblePath
= None
511 solver
= self
.driver
.solver
512 requirements
= self
.driver
.requirements
513 iface_cache
= self
.config
.iface_cache
515 for it
in walk(self
.model
, firstVisibleIter
):
517 iface
= row
[InterfaceBrowser
.INTERFACE
]
519 # Is this interface the download's hint?
520 downloads
= hints
.get(iface
, []) # The interface itself
521 downloads
+= hints
.get(iface
.uri
, []) # The main feed
523 arch
= solver
.get_arch_for(requirements
, iface
)
524 for feed
in iface_cache
.usable_feeds(iface
, arch
):
525 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
526 impl
= selections
.get(iface
, None)
528 downloads
+= hints
.get(impl
, []) # The chosen implementation
535 expected
= (expected
or 0) + dl
.expected_size
536 so_far
+= dl
.get_bytes_downloaded_so_far()
538 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
539 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
541 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
543 summary
= ngettext("(downloading %(downloaded)s/unknown)",
544 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
546 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
547 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
549 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
550 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
552 if self
.model
.get_path(it
) == lastVisiblePath
:
555 def highlight_problems(self
):
556 """Called when the solve finishes. Highlight any missing implementations."""
557 for it
in walk(self
.model
, self
.model
.get_iter_root()):
559 iface
= row
[InterfaceBrowser
.INTERFACE
]
560 sel
= self
.driver
.solver
.selections
.selections
.get(iface
.uri
, None)
562 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
563 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'