1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
6 from zeroinstall
import _
7 from zeroinstall
.support
import tasks
, unicode
8 from zeroinstall
.injector
.model
import Interface
, Feed
, stable
, testing
, developer
, stability_levels
9 from zeroinstall
.injector
import writer
, namespaces
, gpg
10 from zeroinstall
.gtkui
import help_box
13 from logging
import warn
15 from dialog
import DialogResponse
, Template
16 from impl_list
import ImplementationList
20 _dialogs
= {} # Interface -> Properties
28 def format_para(para
):
29 lines
= [l
.strip() for l
in para
.split('\n')]
30 return ' '.join(lines
)
32 def have_source_for(config
, interface
):
33 iface_cache
= config
.iface_cache
34 # Note: we don't want to actually fetch the source interfaces at
35 # this point, so we check whether:
36 # - We have a feed of type 'src' (not fetched), or
37 # - We have a source implementation in a regular feed
38 for f
in iface_cache
.get_feed_imports(interface
):
39 if f
.machine
== 'src':
41 # Don't have any src feeds. Do we have a source implementation
42 # as part of a regular feed?
43 for x
in iface_cache
.get_implementations(interface
):
44 if x
.machine
== 'src':
49 def __init__(self
, widgets
):
50 description
= widgets
.get_widget('description')
51 description
.connect('button-press-event', self
.button_press
)
53 self
.buffer = description
.get_buffer()
54 self
.heading_style
= self
.buffer.create_tag(underline
= True, scale
= 1.2)
55 self
.link_style
= self
.buffer.create_tag(underline
= True, foreground
= 'blue')
56 description
.set_size_request(-1, 100)
58 def button_press(self
, tv
, bev
):
59 if bev
.type == gtk
.gdk
.BUTTON_PRESS
and bev
.button
== 1:
60 x
, y
= tv
.window_to_buffer_coords(tv
.get_window_type(bev
.window
),
61 int(bev
.x
), int(bev
.y
))
62 itr
= tv
.get_iter_at_location(x
, y
)
63 if itr
and self
.link_style
in itr
.get_tags():
64 if not itr
.begins_tag(self
.link_style
):
65 itr
.backward_to_tag_toggle(self
.link_style
)
67 end
.forward_to_tag_toggle(self
.link_style
)
68 target
= itr
.get_text(end
).strip()
70 browser
.open_in_browser(target
)
72 def strtime(self
, secs
):
74 from locale
import nl_langinfo
, D_T_FMT
75 return time
.strftime(nl_langinfo(D_T_FMT
), time
.localtime(secs
))
76 except (ImportError, ValueError):
77 return time
.ctime(secs
)
79 def set_details(self
, iface_cache
, feed
):
81 heading_style
= self
.heading_style
83 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
85 iter = buffer.get_start_iter()
88 buffer.insert(iter, 'Not yet downloaded.')
91 if isinstance(feed
, Exception):
92 buffer.insert(iter, unicode(feed
))
95 buffer.insert_with_tags(iter,
96 '%s ' % feed
.get_name(), heading_style
)
97 buffer.insert(iter, '(%s)' % feed
.summary
)
99 buffer.insert(iter, '\n%s\n' % feed
.url
)
101 # (converts to local time)
102 if feed
.last_modified
:
103 buffer.insert(iter, '\n' + _('Last upstream change: %s') % self
.strtime(feed
.last_modified
))
105 if feed
.last_checked
:
106 buffer.insert(iter, '\n' + _('Last checked: %s') % self
.strtime(feed
.last_checked
))
108 last_check_attempt
= iface_cache
.get_last_check_attempt(feed
.url
)
109 if last_check_attempt
:
110 if feed
.last_checked
and feed
.last_checked
>= last_check_attempt
:
111 pass # Don't bother reporting successful attempts
113 buffer.insert(iter, '\n' + _('Last check attempt: %s (failed or in progress)') %
114 self
.strtime(last_check_attempt
))
116 buffer.insert_with_tags(iter, '\n\n' + _('Description') + '\n', heading_style
)
118 paragraphs
= [format_para(p
) for p
in (feed
.description
or "-").split('\n\n')]
120 buffer.insert(iter, '\n\n'.join(paragraphs
))
121 buffer.insert(iter, '\n')
124 for x
in feed
.get_metadata(namespaces
.XMLNS_IFACE
, 'homepage'):
126 buffer.insert(iter, '\n')
128 buffer.insert(iter, _('Homepage: '))
129 buffer.insert_with_tags(iter, '%s\n' % x
.content
, self
.link_style
)
131 if feed
.local_path
is None:
132 buffer.insert_with_tags(iter, '\n' + _('Signatures') + '\n', heading_style
)
133 sigs
= iface_cache
.get_cached_signatures(feed
.url
)
136 if isinstance(sig
, gpg
.ValidSig
):
137 name
= _('<unknown>')
138 details
= sig
.get_details()
140 if item
[0] == 'uid' and len(item
) > 9:
143 buffer.insert_with_tags(iter, _('Valid signature by "%(name)s"\n- Dated: %(sig_date)s\n- Fingerprint: %(sig_fingerprint)s\n') %
144 {'name': name
, 'sig_date': time
.strftime('%c', time
.localtime(sig
.get_timestamp())), 'sig_fingerprint': sig
.fingerprint
})
145 if not sig
.is_trusted():
146 if os
.path
.isabs(feed
.url
):
147 buffer.insert_with_tags(iter, _('WARNING: This key is not in the trusted list') + '\n')
149 buffer.insert_with_tags(iter, _('WARNING: This key is not in the trusted list (either you removed it, or '
150 'you trust one of the other signatures)') + '\n')
152 buffer.insert_with_tags(iter, '%s\n' % sig
)
154 buffer.insert_with_tags(iter, _('No signature information (old style feed or out-of-date cache)') + '\n')
161 def __init__(self
, config
, arch
, interface
, widgets
):
164 self
.interface
= interface
166 self
.model
= gtk
.ListStore(str, str, bool)
168 self
.description
= Description(widgets
)
170 self
.lines
= self
.build_model()
171 for line
in self
.lines
:
172 self
.model
.append(line
)
174 add_remote_feed_button
= widgets
.get_widget('add_remote_feed')
175 add_remote_feed_button
.connect('clicked', lambda b
: add_remote_feed(config
, widgets
.get_widget(), interface
))
177 add_local_feed_button
= widgets
.get_widget('add_local_feed')
178 add_local_feed_button
.connect('clicked', lambda b
: add_local_feed(config
, interface
))
180 self
.remove_feed_button
= widgets
.get_widget('remove_feed')
181 def remove_feed(button
):
182 model
, iter = self
.tv
.get_selection().get_selected()
183 feed_uri
= model
[iter][Feeds
.URI
]
184 for x
in interface
.extra_feeds
:
185 if x
.uri
== feed_uri
:
187 interface
.extra_feeds
.remove(x
)
188 writer
.save_interface(interface
)
193 dialog
.alert(self
.remove_feed_button
.get_toplevel(),
194 _("Can't remove '%s' as you didn't add it.") % feed_uri
)
196 raise Exception(_("Missing feed '%s'!") % feed_uri
)
197 self
.remove_feed_button
.connect('clicked', remove_feed
)
199 self
.tv
= widgets
.get_widget('feeds_list')
200 self
.tv
.set_model(self
.model
)
201 text
= gtk
.CellRendererText()
202 self
.tv
.append_column(gtk
.TreeViewColumn(_('Source'), text
, text
= Feeds
.URI
, sensitive
= Feeds
.USED
))
203 self
.tv
.append_column(gtk
.TreeViewColumn(_('Arch'), text
, text
= Feeds
.ARCH
, sensitive
= Feeds
.USED
))
205 sel
= self
.tv
.get_selection()
206 sel
.set_mode(gtk
.SELECTION_BROWSE
)
207 sel
.connect('changed', self
.sel_changed
)
208 sel
.select_path((0,))
210 def build_model(self
):
211 iface_cache
= self
.config
.iface_cache
213 usable_feeds
= frozenset(self
.config
.iface_cache
.usable_feeds(self
.interface
, self
.arch
))
214 unusable_feeds
= frozenset(iface_cache
.get_feed_imports(self
.interface
)) - usable_feeds
216 out
= [[self
.interface
.uri
, None, True]]
218 for feed
in usable_feeds
:
219 out
.append([feed
.uri
, feed
.arch
, True])
220 for feed
in unusable_feeds
:
221 out
.append([feed
.uri
, feed
.arch
, False])
224 def sel_changed(self
, sel
):
225 iface_cache
= self
.config
.iface_cache
227 model
, miter
= sel
.get_selected()
228 if not miter
: return # build in progress
229 feed_url
= model
[miter
][Feeds
.URI
]
230 # Only enable removing user_override feeds
231 enable_remove
= False
232 for x
in self
.interface
.extra_feeds
:
233 if x
.uri
== feed_url
:
234 if x
.user_override
and not x
.site_package
:
237 self
.remove_feed_button
.set_sensitive(enable_remove
)
239 self
.description
.set_details(iface_cache
, iface_cache
.get_feed(feed_url
))
240 except zeroinstall
.SafeException
as ex
:
241 self
.description
.set_details(iface_cache
, ex
)
244 new_lines
= self
.build_model()
245 if new_lines
!= self
.lines
:
246 self
.lines
= new_lines
248 for line
in self
.lines
:
249 self
.model
.append(line
)
250 self
.tv
.get_selection().select_path((0,))
252 self
.sel_changed(self
.tv
.get_selection())
260 def __init__(self
, driver
, interface
, compile, show_versions
= False):
263 widgets
= Template('interface_properties')
265 self
.interface
= interface
267 window
= widgets
.get_widget('interface_properties')
269 window
.set_title(_('Properties for %s') % interface
.get_name())
270 window
.set_default_size(-1, gtk
.gdk
.screen_height() / 3)
272 self
.compile_button
= widgets
.get_widget('compile')
273 self
.compile_button
.connect('clicked', lambda b
: compile(interface
))
274 window
.set_default_response(gtk
.RESPONSE_CANCEL
)
276 def response(dialog
, resp
):
277 if resp
== gtk
.RESPONSE_CANCEL
:
279 elif resp
== gtk
.RESPONSE_HELP
:
280 properties_help
.display()
281 window
.connect('response', response
)
283 notebook
= widgets
.get_widget('interface_notebook')
286 target_arch
= self
.driver
.solver
.get_arch_for(driver
.requirements
, interface
= interface
)
287 feeds
= Feeds(driver
.config
, target_arch
, interface
, widgets
)
289 stability
= widgets
.get_widget('preferred_stability')
290 stability
.set_active(0)
291 if interface
.stability_policy
:
292 i
= [stable
, testing
, developer
].index(interface
.stability_policy
)
295 warn(_("Unknown stability policy %s"), interface
.stability_policy
)
298 stability
.set_active(i
)
300 def set_stability_policy(combo
, stability
= stability
): # (pygtk bug?)
301 i
= stability
.get_active()
305 name
= ['stable', 'testing', 'developer'][i
-1]
306 new_stability
= stability_levels
[name
]
307 interface
.set_stability_policy(new_stability
)
308 writer
.save_interface(interface
)
311 stability
.connect('changed', set_stability_policy
)
313 self
.use_list
= ImplementationList(driver
, interface
, widgets
)
317 feeds
.tv
.grab_focus()
323 window
.connect('destroy', lambda s
: driver
.watchers
.remove(updated
))
324 driver
.watchers
.append(updated
)
334 self
.window
.destroy()
336 def shade_compile(self
):
337 self
.compile_button
.set_sensitive(have_source_for(self
.driver
.config
, self
.interface
))
339 def update_list(self
):
340 ranked_items
= self
.driver
.solver
.details
.get(self
.interface
, None)
341 if ranked_items
is None:
342 # The Solver didn't get this far, but we should still display them!
343 ranked_items
= [(impl
, _("(solve aborted before here)"))
344 for impl
in self
.interface
.implementations
.values()]
345 # Always sort by version
347 self
.use_list
.set_items(ranked_items
)
350 def add_remote_feed(config
, parent
, interface
):
352 iface_cache
= config
.iface_cache
354 d
= gtk
.MessageDialog(parent
, 0, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_CANCEL
,
355 _('Enter the URL of the new source of implementations of this interface:'))
356 d
.add_button(gtk
.STOCK_ADD
, gtk
.RESPONSE_OK
)
357 d
.set_default_response(gtk
.RESPONSE_OK
)
360 align
= gtk
.VBox(False, 0)
361 align
.set_border_width(4)
363 d
.vbox
.pack_start(align
)
364 entry
.set_activates_default(True)
370 error_label
= gtk
.Label('')
371 error_label
.set_padding(4, 4)
372 align
.pack_start(error_label
)
378 error_label
.set_text(message
)
384 got_response
= DialogResponse(d
)
386 tasks
.check(got_response
)
387 resp
= got_response
.response
390 if resp
== gtk
.RESPONSE_OK
:
392 url
= entry
.get_text()
394 raise zeroinstall
.SafeException(_('Enter a URL'))
395 fetch
= config
.fetcher
.download_and_import_feed(url
, iface_cache
)
397 d
.set_sensitive(False)
399 d
.set_sensitive(True)
402 iface
= iface_cache
.get_interface(url
)
404 d
.set_sensitive(True)
406 error(_('Failed to read interface'))
408 if not iface
.feed_for
:
409 error(_("Feed '%(feed)s' is not a feed for '%(feed_for)s'.") % {'feed': iface
.get_name(), 'feed_for': interface
.get_name()})
410 elif interface
.uri
not in iface
.feed_for
:
411 error(_("This is not a feed for '%(uri)s'.\nOnly for:\n%(feed_for)s") %
412 {'uri': interface
.uri
, 'feed_for': '\n'.join(iface
.feed_for
)})
413 elif iface
.uri
in [f
.uri
for f
in interface
.extra_feeds
]:
414 error(_("Feed from '%s' has already been added!") % iface
.uri
)
416 interface
.extra_feeds
.append(Feed(iface
.uri
, arch
= None, user_override
= True))
417 writer
.save_interface(interface
)
421 except zeroinstall
.SafeException
as ex
:
426 except Exception as ex
:
428 traceback
.print_exc()
429 config
.handler
.report_error(ex
)
431 def add_local_feed(config
, interface
):
432 chooser
= gtk
.FileChooserDialog(_('Select XML feed file'), action
=gtk
.FILE_CHOOSER_ACTION_OPEN
, buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
, gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
))
434 from zeroinstall
.injector
import reader
436 feed_targets
= config
.iface_cache
.get_feed_targets(feed
)
437 if interface
not in feed_targets
:
438 raise Exception(_("Not a valid feed for '%(uri)s'; this is a feed for:\n%(feed_for)s") %
439 {'uri': interface
.uri
,
440 'feed_for': '\n'.join([f
.uri
for f
in feed_targets
])})
441 if feed
in [f
.uri
for f
in interface
.extra_feeds
]:
442 dialog
.alert(None, _('This feed is already registered.'))
444 interface
.extra_feeds
.append(Feed(feed
, user_override
= True, arch
= None))
446 writer
.save_interface(interface
)
448 reader
.update_from_cache(interface
)
451 except Exception as ex
:
452 dialog
.alert(None, _("Error in feed file '%(feed)s':\n\n%(exception)s") % {'feed': feed
, 'exception': str(ex
)})
454 def check_response(widget
, response
):
455 if response
== gtk
.RESPONSE_OK
:
456 ok(widget
.get_filename())
457 elif response
== gtk
.RESPONSE_CANCEL
:
460 chooser
.connect('response', check_response
)
463 def edit(driver
, interface
, compile, show_versions
= False):
464 assert isinstance(interface
, Interface
)
465 if interface
in _dialogs
:
466 _dialogs
[interface
].destroy()
467 _dialogs
[interface
] = Properties(driver
, interface
, compile, show_versions
= show_versions
)
468 _dialogs
[interface
].show()
470 properties_help
= help_box
.HelpBox(_("Injector Properties Help"),
471 (_('Interface properties'), '\n' +
472 _("""This window displays information about an interface. There are two tabs at the top: \
473 Feeds shows the places where the injector looks for implementations of the interface, while \
474 Versions shows the list of implementations found (from all feeds) in order of preference.""")),
476 (_('The Feeds tab'), '\n' +
477 _("""At the top is a list of feeds. By default, the injector uses the full name of the interface \
478 as the default feed location (so if you ask it to run the program "http://foo/bar.xml" then it will \
479 by default get the list of versions by downloading "http://foo/bar.xml".
481 You can add and remove feeds using the buttons on the right. The main feed may also add \
482 some extra feeds itself. If you've checked out a developer version of a program, you can use \
483 the 'Add Local Feed...' button to let the injector know about it, for example.
485 Below the list of feeds is a box describing the selected one:
487 - At the top is its short name.
488 - Below that is the address (a URL or filename).
489 - 'Last upstream change' shows the version of the cached copy of the interface file.
490 - 'Last checked' is the last time a fresh copy of the upstream interface file was \
492 - Then there is a longer description of the interface.""")),
494 (_('The Versions tab'), '\n' +
495 _("""This tab shows a list of all known implementations of the interface, from all the feeds. \
496 The columns have the following meanings:
498 Version gives the version number. High-numbered versions are considered to be \
499 better than low-numbered ones.
501 Released gives the date this entry was added to the feed.
503 Stability is 'stable' if the implementation is believed to be stable, 'buggy' if \
504 it is known to contain serious bugs, and 'testing' if its stability is not yet \
505 known. This information is normally supplied and updated by the author of the \
506 software, but you can override their rating by right-clicking here (overridden \
507 values are shown in upper-case). You can also use the special level 'preferred'.
509 Fetch indicates how much data needs to be downloaded to get this version if you don't \
510 have it. If the implementation has already been downloaded to your computer, \
511 it will say (cached). (local) means that you installed this version manually and \
512 told Zero Install about it by adding a feed. (package) means that this version \
513 is provided by your distribution's package manager, not by Zero Install. \
514 In off-line mode, only cached implementations are considered for use.
516 Arch indicates what kind of computer system the implementation is for, or 'any' \
517 if it works with all types of system.
519 If you want to know why a particular version wasn't chosen, right-click over it \
520 and choose "Explain this decision" from the popup menu.
522 (_('Sort order'), '\n' +
523 _("""The implementations are ordered by version number (highest first), with the \
524 currently selected one in bold. This is the "best" usable version.
526 Unusable ones are those for incompatible \
527 architectures, those marked as 'buggy' or 'insecure', versions explicitly marked as incompatible with \
528 another interface you are using and, in off-line mode, uncached implementations. Unusable \
529 implementations are shown crossed out.
531 For the usable implementations, the order is as follows:
533 - Preferred implementations come first.
535 - Then, if network use is set to 'Minimal', cached implementations come before \
538 - Then, implementations at or above the selected stability level come before all others.
540 - Then, higher-numbered versions come before low-numbered ones.
542 - Then cached come before non-cached (for 'Full' network use mode).""") + '\n'),
544 (_('Compiling'), '\n' +
545 _("""If there is no binary available for your system then you may be able to compile one from \
546 source by clicking on the Compile button. If no source is available, the Compile button will \
547 be shown shaded.""") + '\n'))