1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 from zeroinstall
import gobject
7 from zeroinstall
import _
, translation
8 from zeroinstall
.support
import tasks
, pretty_size
9 from zeroinstall
.injector
import model
, reader
11 from zeroinstall
.gtkui
.icon
import load_icon
12 from zeroinstall
import support
13 from logging
import warn
, info
16 ngettext
= translation
.ngettext
20 if impl
.user_stability
is None:
21 return _(str(impl
.upstream_stability
))
22 return _("%(implementation_user_stability)s (was %(implementation_upstream_stability)s)") \
23 % {'implementation_user_stability': _(str(impl
.user_stability
)),
24 'implementation_upstream_stability': _(str(impl
.upstream_stability
))}
27 CELL_TEXT_INDENT
= int(ICON_SIZE
) + 4
29 def get_tooltip_text(mainwindow
, interface
, main_feed
, model_column
):
31 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
32 return _("Full name: %s") % interface
.uri
33 elif model_column
== InterfaceBrowser
.SUMMARY
:
34 if main_feed
is None or not main_feed
.description
:
35 return _("(no description available)")
36 first_para
= main_feed
.description
.split('\n\n', 1)[0]
37 return first_para
.replace('\n', ' ')
38 elif model_column
is None:
39 return _("Click here for more options...")
41 impl
= mainwindow
.driver
.solver
.selections
.get(interface
, None)
43 return _("No suitable version was found. Double-click "
44 "here to find out why.")
46 if model_column
== InterfaceBrowser
.VERSION
:
47 text
= _("Currently preferred version: %(version)s (%(stability)s)") % \
48 {'version': impl
.get_version(), 'stability': _stability(impl
)}
49 old_impl
= mainwindow
.original_implementation
.get(interface
, None)
50 if old_impl
is not None and old_impl
is not impl
:
51 text
+= '\n' + _('Previously preferred version: %(version)s (%(stability)s)') % \
52 {'version': old_impl
.get_version(), 'stability': _stability(old_impl
)}
55 assert model_column
== InterfaceBrowser
.DOWNLOAD_SIZE
57 if impl
.is_available(mainwindow
.driver
.config
.stores
):
58 return _("This version is already stored on your computer.")
60 src
= mainwindow
.driver
.config
.fetcher
.get_best_source(impl
)
62 return _("No downloads available!")
63 return _("Need to download %(pretty_size)s (%(size)s bytes)") % \
64 {'pretty_size': support
.pretty_size(src
.size
), 'size': src
.size
}
67 angle_right
= math
.pi
/ 2
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 do_get_size(self
, widget
, cell_area
, layout
= None):
78 on_get_size
= do_get_size
# GTK 2
80 if gtk
.pygtk_version
>= (2, 90):
81 # note: if you get "TypeError: Couldn't find conversion for foreign struct 'cairo.Context'", you need "python3-gi-cairo"
82 def do_render(self
, cr
, widget
, background_area
, cell_area
, flags
): # GTK 3
83 context
= widget
.get_style_context()
84 gtk
.render_arrow(context
, cr
, angle_right
,
85 cell_area
.x
+ 5, cell_area
.y
+ 5, max(cell_area
.width
, cell_area
.height
) - 10)
87 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
): # GTK 2
88 if flags
& gtk
.CELL_RENDERER_PRELIT
:
89 state
= gtk
.STATE_PRELIGHT
91 state
= gtk
.STATE_NORMAL
93 widget
.style
.paint_box(window
, state
, gtk
.SHADOW_OUT
, expose_area
, widget
, None,
94 cell_area
.x
, cell_area
.y
, cell_area
.width
, cell_area
.height
)
95 widget
.style
.paint_arrow(window
, state
, gtk
.SHADOW_NONE
, expose_area
, widget
, None,
96 gtk
.ARROW_RIGHT
, True,
97 cell_area
.x
+ 5, cell_area
.y
+ 5, cell_area
.width
- 10, cell_area
.height
- 10)
99 class IconAndTextRenderer(gtk
.GenericCellRenderer
):
101 "image": (gobject
.TYPE_PYOBJECT
, "Image", "Image", gobject
.PARAM_READWRITE
),
102 "text": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
105 def do_set_property(self
, prop
, value
):
106 setattr(self
, prop
.name
, value
)
108 def do_get_size(self
, widget
, cell_area
, layout
= None):
110 layout
= widget
.create_pango_layout(self
.text
)
111 a
, rect
= layout
.get_pixel_extents()
114 pixmap_height
= self
.image
.get_height()
118 if not isinstance(rect
, tuple):
119 rect
= (rect
.x
, rect
.y
, rect
.width
, rect
.height
) # GTK 3
121 both_height
= max(rect
[1] + rect
[3], pixmap_height
)
124 rect
[0] + rect
[2] + CELL_TEXT_INDENT
,
126 on_get_size
= do_get_size
# GTK 2
128 if gtk
.pygtk_version
>= (2, 90):
129 def do_render(self
, cr
, widget
, background_area
, cell_area
, flags
): # GTK 3
130 layout
= widget
.create_pango_layout(self
.text
)
131 a
, rect
= layout
.get_pixel_extents()
132 context
= widget
.get_style_context()
134 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
135 gtk
.render_icon(context
, cr
, self
.image
, cell_area
.x
, cell_area
.y
)
137 text_y
= int(0.5 * (cell_area
.height
- (rect
.y
+ rect
.height
)))
139 gtk
.render_layout(context
, cr
,
140 cell_area
.x
+ CELL_TEXT_INDENT
,
141 cell_area
.y
+ text_y
,
144 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
): # GTK 2
145 layout
= widget
.create_pango_layout(self
.text
)
146 a
, rect
= layout
.get_pixel_extents()
148 if flags
& gtk
.CELL_RENDERER_SELECTED
:
149 state
= gtk
.STATE_SELECTED
150 elif flags
& gtk
.CELL_RENDERER_PRELIT
:
151 state
= gtk
.STATE_PRELIGHT
153 state
= gtk
.STATE_NORMAL
155 image_y
= int(0.5 * (cell_area
.height
- self
.image
.get_height()))
156 window
.draw_pixbuf(widget
.style
.white_gc
, self
.image
, 0, 0,
158 cell_area
.y
+ image_y
)
160 text_y
= int(0.5 * (cell_area
.height
- (rect
[1] + rect
[3])))
162 widget
.style
.paint_layout(window
, state
, True,
163 expose_area
, widget
, "cellrenderertext",
164 cell_area
.x
+ CELL_TEXT_INDENT
,
165 cell_area
.y
+ text_y
,
168 if gtk
.pygtk_version
< (2, 8, 0):
169 # Note sure exactly which versions need this.
170 # 2.8.0 gives a warning if you include it, though.
171 gobject
.type_register(IconAndTextRenderer
)
172 gobject
.type_register(MenuIconRenderer
)
177 for x
in walk(model
, model
.iter_children(it
)): yield x
178 it
= model
.iter_next(it
)
180 class InterfaceBrowser
:
186 original_implementation
= None
198 columns
= [(_('Component'), INTERFACE_NAME
),
199 (_('Version'), VERSION
),
200 (_('Fetch'), DOWNLOAD_SIZE
),
201 (_('Description'), SUMMARY
),
204 def __init__(self
, driver
, widgets
):
206 self
.config
= driver
.config
208 tree_view
= widgets
.get_widget('components')
209 tree_view
.set_property('has-tooltip', True)
210 def callback(widget
, x
, y
, keyboard_mode
, tooltip
):
211 x
, y
= tree_view
.convert_widget_to_bin_window_coords(x
, y
)
212 pos
= tree_view
.get_path_at_pos(x
, y
)
214 tree_view
.set_tooltip_cell(tooltip
, pos
[0], pos
[1], None)
217 col_index
= column_objects
.index(pos
[1])
221 col
= self
.columns
[col_index
][1]
222 row
= self
.model
[path
]
223 iface
= row
[InterfaceBrowser
.INTERFACE
]
224 main_feed
= self
.config
.iface_cache
.get_feed(iface
.uri
)
225 tooltip
.set_text(get_tooltip_text(self
, iface
, main_feed
, col
))
229 tree_view
.connect('query-tooltip', callback
)
231 self
.cached_icon
= {} # URI -> GdkPixbuf
232 self
.default_icon
= tree_view
.get_style().lookup_icon_set(gtk
.STOCK_EXECUTE
).render_icon(tree_view
.get_style(),
233 gtk
.TEXT_DIR_NONE
, gtk
.STATE_NORMAL
, gtk
.ICON_SIZE_SMALL_TOOLBAR
, tree_view
, None)
235 self
.model
= gtk
.TreeStore(object, str, str, str, str, gobject
.TYPE_PYOBJECT
, str, bool)
236 self
.tree_view
= tree_view
237 tree_view
.set_model(self
.model
)
241 text
= gtk
.CellRendererText()
242 coloured_text
= gtk
.CellRendererText()
244 for name
, model_column
in self
.columns
:
245 if model_column
== InterfaceBrowser
.INTERFACE_NAME
:
246 column
= gtk
.TreeViewColumn(name
, IconAndTextRenderer(),
248 image
= InterfaceBrowser
.ICON
)
249 elif model_column
== None:
250 menu_column
= column
= gtk
.TreeViewColumn('', MenuIconRenderer())
252 if model_column
== InterfaceBrowser
.SUMMARY
:
253 text_ellip
= gtk
.CellRendererText()
255 text_ellip
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
258 column
= gtk
.TreeViewColumn(name
, text_ellip
, text
= model_column
)
259 column
.set_expand(True)
260 elif model_column
== InterfaceBrowser
.VERSION
:
261 column
= gtk
.TreeViewColumn(name
, coloured_text
, text
= model_column
,
262 background
= InterfaceBrowser
.BACKGROUND
)
264 column
= gtk
.TreeViewColumn(name
, text
, text
= model_column
)
265 tree_view
.append_column(column
)
266 column_objects
.append(column
)
268 tree_view
.set_enable_search(True)
270 selection
= tree_view
.get_selection()
272 def button_press(tree_view
, bev
):
273 pos
= tree_view
.get_path_at_pos(int(bev
.x
), int(bev
.y
))
276 path
, col
, x
, y
= pos
278 if (bev
.button
== 3 or (bev
.button
< 4 and col
is menu_column
)) \
279 and bev
.type == gtk
.gdk
.BUTTON_PRESS
:
280 selection
.select_path(path
)
281 iface
= self
.model
[path
][InterfaceBrowser
.INTERFACE
]
282 self
.show_popup_menu(iface
, bev
)
284 if bev
.button
!= 1 or bev
.type != gtk
.gdk
._2BUTTON
_PRESS
:
286 properties
.edit(driver
, self
.model
[path
][InterfaceBrowser
.INTERFACE
], self
.compile, show_versions
= True)
287 tree_view
.connect('button-press-event', button_press
)
289 tree_view
.connect('destroy', lambda s
: driver
.watchers
.remove(self
.build_tree
))
290 driver
.watchers
.append(self
.build_tree
)
292 def set_root(self
, root
):
293 assert isinstance(root
, model
.Interface
)
296 def set_update_icons(self
, update_icons
):
298 # Clear icons cache to make sure they're really updated
299 self
.cached_icon
= {}
300 self
.update_icons
= update_icons
302 def get_icon(self
, iface
):
303 """Get an icon for this interface. If the icon is in the cache, use that.
304 If not, start a download. If we already started a download (successful or
305 not) do nothing. Returns None if no icon is currently available."""
307 # Try the in-memory cache
308 return self
.cached_icon
[iface
.uri
]
310 # Try the on-disk cache
311 iconpath
= self
.config
.iface_cache
.get_icon_path(iface
)
314 icon
= load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
315 # (if icon is None, cache the fact that we can't load it)
316 self
.cached_icon
[iface
.uri
] = icon
320 # Download a new icon if we don't have one, or if the
321 # user did a 'Refresh'
322 if iconpath
is None or self
.update_icons
:
323 if self
.config
.network_use
== model
.network_offline
:
326 fetcher
= self
.config
.fetcher
.download_icon(iface
)
328 if iface
.uri
not in self
.cached_icon
:
329 self
.cached_icon
[iface
.uri
] = None # Only try once
332 def update_display():
336 # Try to insert new icon into the cache
337 # If it fails, we'll be left with None in the cached_icon so
338 # we don't try again.
339 iconpath
= self
.config
.iface_cache
.get_icon_path(iface
)
341 self
.cached_icon
[iface
.uri
] = load_icon(iconpath
, ICON_SIZE
, ICON_SIZE
)
344 warn("Failed to download icon for '%s'", iface
)
345 except Exception as ex
:
347 traceback
.print_exc()
348 self
.config
.handler
.report_error(ex
)
350 # elif fetcher is None: don't store anything in cached_icon
352 # Note: if no icon is available for downloading,
353 # more attempts are made later.
354 # It can happen that no icon is yet available because
355 # the interface was not downloaded yet, in which case
356 # it's desireable to try again once the interface is available
361 def build_tree(self
):
362 iface_cache
= self
.config
.iface_cache
364 if self
.original_implementation
is None:
365 self
.set_original_implementations()
367 done
= {} # Detect cycles
369 sels
= self
.driver
.solver
.selections
372 def add_node(parent
, iface
, commands
, essential
):
377 main_feed
= iface_cache
.get_feed(iface
.uri
)
379 name
= main_feed
.get_name()
380 summary
= main_feed
.summary
382 name
= iface
.get_name()
385 iter = self
.model
.append(parent
)
386 self
.model
[iter][InterfaceBrowser
.INTERFACE
] = iface
387 self
.model
[iter][InterfaceBrowser
.INTERFACE_NAME
] = name
388 self
.model
[iter][InterfaceBrowser
.SUMMARY
] = summary
or ''
389 self
.model
[iter][InterfaceBrowser
.ICON
] = self
.get_icon(iface
) or self
.default_icon
390 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = False
392 sel
= sels
.selections
.get(iface
.uri
, None)
395 old_impl
= self
.original_implementation
.get(iface
, None)
396 version_str
= impl
.get_version()
397 if old_impl
is not None and old_impl
.id != impl
.id:
398 version_str
+= _(' (was %s)') % old_impl
.get_version()
399 self
.model
[iter][InterfaceBrowser
.VERSION
] = version_str
401 self
.model
[iter][InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
403 deps
= sel
.dependencies
405 deps
+= sel
.get_command(c
).requires
407 if isinstance(child
, model
.InterfaceDependency
):
409 iface_cache
.get_interface(child
.interface
),
410 child
.get_required_commands(),
411 child
.importance
== model
.Dependency
.Essential
)
412 elif not isinstance(child
, model
.InterfaceRestriction
):
413 child_iter
= self
.model
.append(parent
)
414 self
.model
[child_iter
][InterfaceBrowser
.INTERFACE_NAME
] = '?'
415 self
.model
[child_iter
][InterfaceBrowser
.SUMMARY
] = \
416 _('Unknown dependency type : %s') % child
417 self
.model
[child_iter
][InterfaceBrowser
.ICON
] = self
.default_icon
419 self
.model
[iter][InterfaceBrowser
.PROBLEM
] = essential
420 self
.model
[iter][InterfaceBrowser
.VERSION
] = _('(problem)') if essential
else _('(none)')
423 add_node(None, self
.root
, [sels
.command
], essential
= True)
425 add_node(None, self
.root
, [], essential
= True)
426 self
.tree_view
.expand_all()
427 except Exception as ex
:
428 warn("Failed to build tree: %s", ex
, exc_info
= ex
)
431 def show_popup_menu(self
, iface
, bev
):
434 have_source
= properties
.have_source_for(self
.config
, iface
)
436 global menu
# Fix GC problem in PyGObject
438 for label
, cb
in [(_('Show Feeds'), lambda: properties
.edit(self
.driver
, iface
, self
.compile)),
439 (_('Show Versions'), lambda: properties
.edit(self
.driver
, iface
, self
.compile, show_versions
= True)),
440 (_('Report a Bug...'), lambda: bugs
.report_bug(self
.driver
, iface
))]:
441 item
= gtk
.MenuItem()
442 item
.set_label(label
)
444 item
.connect('activate', lambda item
, cb
=cb
: cb())
446 item
.set_sensitive(False)
450 item
= gtk
.MenuItem()
451 item
.set_label(_('Compile'))
455 compile_menu
= gtk
.Menu()
456 item
.set_submenu(compile_menu
)
458 item
= gtk
.MenuItem()
459 item
.set_label(_('Automatic'))
460 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= True))
462 compile_menu
.append(item
)
464 item
= gtk
.MenuItem()
465 item
.set_label(_('Manual...'))
466 item
.connect('activate', lambda item
: self
.compile(iface
, autocompile
= False))
468 compile_menu
.append(item
)
470 item
.set_sensitive(False)
472 if gtk
.pygtk_version
>= (2, 90):
473 menu
.popup(None, None, None, None, bev
.button
, bev
.time
)
475 menu
.popup(None, None, None, bev
.button
, bev
.time
)
477 def compile(self
, interface
, autocompile
= True):
480 # A new local feed may have been registered, so reload it from the disk cache
481 info(_("0compile command completed successfully. Reloading interface details."))
482 reader
.update_from_cache(interface
)
483 for feed
in interface
.extra_feeds
:
484 self
.config
.iface_cache
.get_feed(feed
.uri
, force
= True)
487 compile.compile(on_success
, interface
.uri
, autocompile
= autocompile
)
489 def set_original_implementations(self
):
490 assert self
.original_implementation
is None
491 self
.original_implementation
= self
.driver
.solver
.selections
.copy()
493 def update_download_status(self
, only_update_visible
= False):
494 """Called at regular intervals while there are downloads in progress,
495 and once at the end. Also called when things are added to the store.
496 Update the TreeView with the interfaces."""
498 # A download may be for a feed, an interface or an implementation.
499 # Create the reverse mapping (item -> download)
501 for dl
in self
.config
.handler
.monitored_downloads
:
503 if dl
.hint
not in hints
:
505 hints
[dl
.hint
].append(dl
)
507 selections
= self
.driver
.solver
.selections
509 # Only update currently visible rows
510 if only_update_visible
and self
.tree_view
.get_visible_range() != None:
511 firstVisiblePath
, lastVisiblePath
= self
.tree_view
.get_visible_range()
512 firstVisibleIter
= self
.model
.get_iter(firstVisiblePath
)
514 # (or should we just wait until the TreeView has settled enough to tell
515 # us what is visible?)
516 firstVisibleIter
= self
.model
.get_iter_root()
517 lastVisiblePath
= None
519 solver
= self
.driver
.solver
520 requirements
= self
.driver
.requirements
521 iface_cache
= self
.config
.iface_cache
523 for it
in walk(self
.model
, firstVisibleIter
):
525 iface
= row
[InterfaceBrowser
.INTERFACE
]
527 # Is this interface the download's hint?
528 downloads
= hints
.get(iface
, []) # The interface itself
529 downloads
+= hints
.get(iface
.uri
, []) # The main feed
531 arch
= solver
.get_arch_for(requirements
, iface
)
532 for feed
in iface_cache
.usable_feeds(iface
, arch
):
533 downloads
+= hints
.get(feed
.uri
, []) # Other feeds
534 impl
= selections
.get(iface
, None)
536 downloads
+= hints
.get(impl
, []) # The chosen implementation
543 expected
= (expected
or 0) + dl
.expected_size
544 so_far
+= dl
.get_bytes_downloaded_so_far()
546 summary
= ngettext("(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%])",
547 "(downloading %(downloaded)s/%(expected)s [%(percentage).2f%%] in %(number)d downloads)",
549 values_dict
= {'downloaded': pretty_size(so_far
), 'expected': pretty_size(expected
), 'percentage': 100 * so_far
/ float(expected
), 'number': len(downloads
)}
551 summary
= ngettext("(downloading %(downloaded)s/unknown)",
552 "(downloading %(downloaded)s/unknown in %(number)d downloads)",
554 values_dict
= {'downloaded': pretty_size(so_far
), 'number': len(downloads
)}
555 row
[InterfaceBrowser
.SUMMARY
] = summary
% values_dict
557 row
[InterfaceBrowser
.DOWNLOAD_SIZE
] = utils
.get_fetch_info(self
.config
, impl
)
558 row
[InterfaceBrowser
.SUMMARY
] = iface
.summary
560 if self
.model
.get_path(it
) == lastVisiblePath
:
563 def highlight_problems(self
):
564 """Called when the solve finishes. Highlight any missing implementations."""
565 for it
in walk(self
.model
, self
.model
.get_iter_root()):
567 iface
= row
[InterfaceBrowser
.INTERFACE
]
568 sel
= self
.driver
.solver
.selections
.selections
.get(iface
.uri
, None)
570 if sel
is None and row
[InterfaceBrowser
.PROBLEM
]:
571 row
[InterfaceBrowser
.BACKGROUND
] = '#f88'