1 from zeroinstall
.injector
.model
import *
2 from zeroinstall
.injector
.iface_cache
import iface_cache
3 from zeroinstall
.injector
import writer
, namespaces
, gpg
5 import sets
# Note: for Python 2.3; frozenset is only in Python 2.4
8 from dialog
import Dialog
10 from impl_list
import ImplementationList
15 _dialogs
= {} # Interface -> Properties
28 def format_para(para
):
29 lines
= [l
.strip() for l
in para
.split('\n')]
30 return ' '.join(lines
)
32 def open_in_browser(link
):
33 browser
= os
.environ
.get('BROWSER', 'firefox')
38 os
.spawnlp(os
.P_NOWAIT
, browser
, browser
, link
)
41 print >>sys
.stderr
, "Error", ex
45 def have_source_for(interface
):
46 # Note: we don't want to actually fetch the source interfaces at
47 # this point, so we check whether:
48 # - We have a feed of type 'src' (not fetched), or
49 # - We have a source implementation in a regular feed
51 for f
in interface
.feeds
:
52 if f
.machine
== 'src':
54 # Don't have any src feeds. Do we have a source implementation
55 # as part of a regular feed?
56 impls
= interface
.implementations
.values()
57 for f
in policy
.usable_feeds(interface
):
59 feed_iface
= iface_cache
.get_interface(f
.uri
)
60 if feed_iface
.implementations
:
61 impls
.extend(feed_iface
.implementations
.values())
63 pass # OK, will get called again later
65 warn("Failed to load feed '%s': %s", f
.uri
, str(ex
))
67 if x
.machine
== 'src':
71 class Description(gtk
.ScrolledWindow
):
73 gtk
.ScrolledWindow
.__init
__(self
, None, None)
74 self
.set_shadow_type(gtk
.SHADOW_IN
)
75 self
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_ALWAYS
)
76 description
= gtk
.TextView()
77 description
.set_left_margin(4)
78 description
.set_right_margin(4)
79 description
.set_wrap_mode(gtk
.WRAP_WORD
)
80 description
.set_editable(False)
81 description
.set_cursor_visible(False)
82 description
.connect('button-press-event', self
.button_press
)
85 self
.buffer = description
.get_buffer()
86 self
.heading_style
= self
.buffer.create_tag(underline
= True, scale
= 1.2)
87 self
.link_style
= self
.buffer.create_tag(underline
= True, foreground
= 'blue')
88 description
.set_size_request(-1, 100)
90 def button_press(self
, tv
, bev
):
91 if bev
.type == gtk
.gdk
.BUTTON_PRESS
and bev
.button
== 1:
92 x
, y
= tv
.window_to_buffer_coords(tv
.get_window_type(bev
.window
),
93 int(bev
.x
), int(bev
.y
))
94 itr
= tv
.get_iter_at_location(x
, y
)
95 if itr
and self
.link_style
in itr
.get_tags():
96 if not itr
.begins_tag(self
.link_style
):
97 itr
.backward_to_tag_toggle(self
.link_style
)
99 end
.forward_to_tag_toggle(self
.link_style
)
100 target
= itr
.get_text(end
).strip()
101 open_in_browser(target
)
103 def set_details(self
, interface
):
105 heading_style
= self
.heading_style
107 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
109 iter = buffer.get_start_iter()
111 buffer.insert_with_tags(iter,
112 '%s ' % interface
.get_name(), heading_style
)
113 buffer.insert(iter, '(%s)' % interface
.summary
)
115 buffer.insert(iter, '\n%s\n' % interface
.uri
)
117 # (converts to local time)
118 if interface
.last_modified
:
119 buffer.insert(iter, '\nLast upstream change: %s' % time
.ctime(interface
.last_modified
))
121 if interface
.last_checked
:
122 buffer.insert(iter, '\nLast checked: %s' % time
.ctime(interface
.last_checked
))
124 if interface
.last_check_attempt
:
125 if interface
.last_checked
and interface
.last_checked
>= interface
.last_check_attempt
:
126 pass # Don't bother reporting successful attempts
128 buffer.insert(iter, '\nLast check attempt: %s (failed or in progress)' %
129 time
.ctime(interface
.last_check_attempt
))
131 buffer.insert_with_tags(iter, '\n\nDescription\n', heading_style
)
133 paragraphs
= [format_para(p
) for p
in (interface
.description
or "-").split('\n\n')]
135 buffer.insert(iter, '\n\n'.join(paragraphs
))
136 buffer.insert(iter, '\n')
138 if hasattr(interface
, 'get_metadata'):
140 for x
in interface
.get_metadata(namespaces
.XMLNS_IFACE
, 'homepage'):
142 buffer.insert(iter, '\n')
144 buffer.insert(iter, 'Homepage: ')
145 buffer.insert_with_tags(iter, '%s\n' % x
.content
, self
.link_style
)
147 if hasattr(iface_cache
, 'get_cached_signatures'):
148 buffer.insert_with_tags(iter, '\nSignatures\n', heading_style
)
149 sigs
= iface_cache
.get_cached_signatures(interface
.uri
)
152 if isinstance(sig
, gpg
.ValidSig
):
154 if hasattr(sig
, 'get_details'):
155 details
= sig
.get_details()
157 if item
[0] in ('pub', 'uid') and len(item
) > 9:
160 buffer.insert_with_tags(iter, 'Valid signature by "%s"\n- Dated: %s\n- Fingerprint: %s\n' %
161 (name
, time
.ctime(sig
.get_timestamp()), sig
.fingerprint
))
162 if not sig
.is_trusted():
163 if interface
.uri
.startswith('/'):
164 buffer.insert_with_tags(iter, 'WARNING: This key is not in the trusted list\n')
166 buffer.insert_with_tags(iter, 'WARNING: This key is not in the trusted list (either you removed it, or '
167 'you trust one of the other signatures)\n')
169 buffer.insert_with_tags(iter, '%s\n' % sig
)
171 buffer.insert_with_tags(iter, 'No signature information (old style interface or out-of-date cache)\n')
174 class Feeds(gtk
.VPaned
):
179 def __init__(self
, interface
):
180 gtk
.VPaned
.__init
__(self
)
181 self
.set_border_width(4)
182 self
.interface
= interface
184 hbox
= gtk
.HBox(False, 4)
185 self
.pack1(hbox
, False, False)
187 self
.model
= gtk
.ListStore(str, str, bool)
189 self
.lines
= self
.build_model()
190 for line
in self
.lines
:
191 self
.model
.append(line
)
193 self
.swin
= gtk
.ScrolledWindow()
194 self
.swin
.set_shadow_type(gtk
.SHADOW_IN
)
195 self
.swin
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_AUTOMATIC
)
196 hbox
.pack_start(self
.swin
, True, True, 0)
198 buttons_vbox
= gtk
.VButtonBox()
199 buttons_vbox
.set_layout(gtk
.BUTTONBOX_START
)
200 buttons_vbox
.set_spacing(4)
202 add_remote_feed_button
= dialog
.MixedButton(_('Add Remote Feed...'), gtk
.STOCK_ADD
, 0.0)
203 add_remote_feed_button
.connect('clicked',
204 lambda b
: add_remote_feed(self
.get_toplevel(), interface
))
205 buttons_vbox
.add(add_remote_feed_button
)
207 add_local_feed_button
= dialog
.MixedButton(_('Add Local Feed...'), gtk
.STOCK_ADD
, 0.0)
208 add_local_feed_button
.connect('clicked', lambda b
: add_local_feed(interface
))
209 tips
.set_tip(add_local_feed_button
,
210 _('If you have another implementation of this interface (e.g., a '
211 'CVS checkout), you can add it to the list by registering the XML '
212 'feed file that came with it.'))
213 buttons_vbox
.add(add_local_feed_button
)
215 self
.remove_feed_button
= dialog
.MixedButton(_('Remove Feed'), gtk
.STOCK_REMOVE
, 0.0)
216 def remove_feed(button
):
217 model
, iter = self
.tv
.get_selection().get_selected()
218 feed_uri
= model
[iter][Feeds
.URI
]
219 for x
in interface
.feeds
:
220 if x
.uri
== feed_uri
:
222 interface
.feeds
.remove(x
)
223 writer
.save_interface(interface
)
227 dialog
.alert(self
.get_toplevel(),
228 _("Can't remove '%s' as you didn't add it.") % feed_uri
)
230 raise Exception("Missing feed '%s'!" % feed_uri
)
231 self
.remove_feed_button
.connect('clicked', remove_feed
)
232 buttons_vbox
.add(self
.remove_feed_button
)
234 hbox
.pack_start(buttons_vbox
, False, True, 0)
236 self
.tv
= gtk
.TreeView(self
.model
)
237 text
= gtk
.CellRendererText()
238 self
.tv
.append_column(gtk
.TreeViewColumn('Source', text
, text
= Feeds
.URI
, sensitive
= Feeds
.USED
))
239 self
.tv
.append_column(gtk
.TreeViewColumn('Arch', text
, text
= Feeds
.ARCH
, sensitive
= Feeds
.USED
))
240 self
.swin
.add(self
.tv
)
242 self
.description
= Description()
243 self
.add2(self
.description
)
245 sel
= self
.tv
.get_selection()
246 sel
.set_mode(gtk
.SELECTION_BROWSE
)
247 sel
.connect('changed', self
.sel_changed
)
248 sel
.select_path((0,))
250 def build_model(self
):
251 usable_feeds
= sets
.ImmutableSet(policy
.usable_feeds(self
.interface
))
252 unusable_feeds
= sets
.ImmutableSet(self
.interface
.feeds
) - usable_feeds
254 out
= [[self
.interface
.uri
, None, True]]
256 if self
.interface
.feeds
:
257 for feed
in usable_feeds
:
258 out
.append([feed
.uri
, feed
.arch
, True])
259 for feed
in unusable_feeds
:
260 out
.append([feed
.uri
, feed
.arch
, False])
263 def sel_changed(self
, sel
):
264 model
, iter = sel
.get_selected()
265 if not iter: return # build in progress
266 iface
= model
[iter][Feeds
.URI
]
267 self
.remove_feed_button
.set_sensitive(iface
!= self
.interface
.uri
)
268 self
.description
.set_details(iface_cache
.get_interface(iface
))
271 new_lines
= self
.build_model()
272 if new_lines
!= self
.lines
:
273 self
.lines
= new_lines
275 for line
in self
.lines
:
276 self
.model
.append(line
)
277 self
.tv
.get_selection().select_path((0,))
279 self
.sel_changed(self
.tv
.get_selection())
281 class Properties(Dialog
):
285 def __init__(self
, interface
, show_versions
= False):
286 Dialog
.__init
__(self
)
287 self
.interface
= interface
288 self
.set_title('Interface ' + interface
.get_name())
289 self
.set_default_size(-1,
290 gtk
.gdk
.screen_height() / 3)
292 self
.add_button(gtk
.STOCK_HELP
, gtk
.RESPONSE_HELP
)
293 self
.compile_button
= self
.add_mixed_button(_('Compile'),
294 gtk
.STOCK_CONVERT
, COMPILE
)
295 self
.compile_button
.connect('clicked', lambda b
: compile.compile(interface
))
296 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_CANCEL
)
297 self
.set_default_response(gtk
.RESPONSE_CANCEL
)
299 def response(dialog
, resp
):
300 if resp
== gtk
.RESPONSE_CANCEL
:
303 # policy.begin_iface_download(interface, True)
304 elif resp
== gtk
.RESPONSE_HELP
:
305 properties_help
.display()
306 self
.connect('response', response
)
308 notebook
= gtk
.Notebook()
309 self
.vbox
.pack_start(notebook
, True, True, 0)
311 feeds
= Feeds(interface
)
312 notebook
.append_page(feeds
, gtk
.Label(_('Feeds')))
313 notebook
.append_page(self
.build_versions_column(interface
), gtk
.Label(_('Versions')))
318 feeds
.tv
.grab_focus()
324 self
.connect('destroy', lambda s
: policy
.watchers
.remove(updated
))
325 policy
.watchers
.append(updated
)
331 def shade_compile(self
):
332 self
.compile_button
.set_sensitive(have_source_for(self
.interface
))
334 def update_list(self
):
335 impls
= policy
.get_ranked_implementations(self
.interface
)
336 self
.use_list
.set_items(impls
)
338 def build_versions_column(self
, interface
):
339 assert self
.use_list
is None
341 vbox
= gtk
.VBox(False, 2)
342 vbox
.set_border_width(4)
344 hbox
= gtk
.HBox(False, 2)
345 vbox
.pack_start(hbox
, False, True, 2)
348 stability
= gtk
.combo_box_new_text()
350 stability
.append_text('Use default setting')
351 stability
.set_active(0)
352 for i
, x
in enumerate((stable
, testing
, developer
)):
353 stability
.append_text(str(x
).capitalize())
354 if x
is interface
.stability_policy
:
355 stability
.set_active(i
+ 1)
356 hbox
.pack_start(gtk
.Label('Preferred stability:'), False, True, 2)
357 hbox
.pack_start(eb
, False, True, 0)
358 def set_stability_policy(combo
):
359 i
= stability
.get_active()
363 name
= stability
.get_model()[i
][0].lower()
364 new_stability
= stability_levels
[name
]
365 interface
.set_stability_policy(new_stability
)
366 writer
.save_interface(interface
)
368 stability
.connect('changed', set_stability_policy
)
369 tips
.set_tip(eb
, _('Implementations at this stability level or higher '
370 'will be used in preference to others. You can use this '
371 'to override the global "Help test new versions" setting '
372 'just for this interface.'))
374 self
.use_list
= ImplementationList(interface
)
375 vbox
.pack_start(self
.use_list
, True, True, 2)
379 def add_remote_feed(parent
, interface
):
380 d
= gtk
.MessageDialog(parent
, 0, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_CANCEL
,
381 _('Enter the URL of the new source of implementations of this interface:'))
382 d
.add_button(gtk
.STOCK_ADD
, gtk
.RESPONSE_OK
)
383 d
.set_default_response(gtk
.RESPONSE_OK
)
386 align
= gtk
.VBox(False, 0)
387 align
.set_border_width(4)
389 d
.vbox
.pack_start(align
)
390 entry
.set_activates_default(True)
396 error_label
= gtk
.Label('')
397 error_label
.set_padding(4, 4)
398 align
.pack_start(error_label
)
402 error_label
.set_text(message
)
407 def download_done(iface
):
408 d
.set_sensitive(True)
410 error('Failed to read interface')
412 if not iface
.feed_for
:
413 error("Interface '%s' is not a feed." % iface
.get_name())
414 elif interface
.uri
not in iface
.feed_for
:
415 error("Interface is not a feed for '%s'.\nOnly for:\n%s" %
416 (interface
.uri
, '\n'.join(iface
.feed_for
)))
417 elif iface
.uri
in [f
.uri
for f
in interface
.feeds
]:
418 error("Feed from '%s' has already been added!" % iface
.uri
)
420 interface
.feeds
.append(Feed(iface
.uri
, arch
= None, user_override
= True))
421 writer
.save_interface(interface
)
425 def response(d
, resp
):
427 if resp
== gtk
.RESPONSE_OK
:
429 url
= entry
.get_text()
431 raise SafeException(_('Enter a URL'))
432 iface
= iface_cache
.get_interface(url
)
433 policy
.begin_iface_download(iface
) # Force a refresh
434 d
.set_sensitive(False)
435 policy
.add_dl_callback(url
, lambda: download_done(iface
))
436 except SafeException
, ex
:
441 d
.connect('response', response
)
444 def add_local_feed(interface
):
445 sel
= gtk
.FileSelection(_('Select XML feed file'))
446 sel
.set_has_separator(False)
448 from xml
.dom
import minidom
449 from zeroinstall
.injector
import reader
450 feed
= sel
.get_filename()
452 if hasattr(policy
, 'get_feed_targets'):
453 feed_targets
= policy
.get_feed_targets(feed
)
454 if interface
not in feed_targets
:
455 raise Exception("Not a valid feed for '%s'; this is a feed for:\n%s" %
457 '\n'.join([f
.uri
for f
in feed_targets
])))
458 if interface
.get_feed(feed
):
459 dialog
.alert(None, 'This feed is already registered.')
461 interface
.feeds
.append(Feed(feed
, user_override
= True, arch
= None))
463 doc
= minidom
.parse(feed
)
464 uri
= doc
.documentElement
.getAttribute('uri')
466 raise Exception("Missing uri attribute in interface file '%s'" % feed
)
467 if uri
!= interface
.uri
:
468 raise Exception("Feed is for interface '%s', not '%s'" %
469 (uri
, interface
.uri
))
470 if feed
in interface
.feeds
:
471 raise Exception("Feed is already registered")
472 interface
.feeds
.append(feed
)
473 writer
.save_interface(interface
)
475 reader
.update_from_cache(interface
)
477 except Exception, ex
:
478 dialog
.alert(None, "Error in feed file '%s':\n\n%s" % (feed
, str(ex
)))
480 sel
.ok_button
.connect('clicked', ok
)
481 sel
.cancel_button
.connect('clicked', lambda b
: sel
.destroy())
484 def edit(interface
, show_versions
= False):
485 assert isinstance(interface
, Interface
)
486 if interface
in _dialogs
:
487 _dialogs
[interface
].destroy()
488 _dialogs
[interface
] = Properties(interface
, show_versions
)
489 _dialogs
[interface
].show()
491 properties_help
= help_box
.HelpBox("Injector Properties Help",
492 ('Interface properties', """
493 This window displays information about an interface. There are two tabs at the top: \
494 Feeds shows the places where the injector looks for implementations of the interface, while \
495 Versions shows the list of implementations found (from all feeds) in order of preference."""),
497 ('The Feeds tab', """
498 At the top is a list of feeds. By default, the injector uses the full name of the interface \
499 as the default feed location (so if you ask it to run the program "http://foo/bar.xml" then it will \
500 by default get the list of versions by downloading "http://foo/bar.xml".
502 You can add and remove feeds using the buttons on the right. The main feed may also add \
503 some extra feeds itself. If you've checked out a developer version of a program, you can use \
504 the 'Add Local Feed...' button to let the injector know about it, for example.
506 Below the list of feeds is a box describing the selected one:
508 - At the top is its short name.
509 - Below that is the address (a URL or filename).
510 - 'Last upstream change' shows the version of the cached copy of the interface file.
511 - 'Last checked' is the last time a fresh copy of the upstream interface file was \
513 - Then there is a longer description of the interface."""),
515 ('The Versions tab', """
516 This tab shows a list of all known implementations of the interface, from all the feeds. \
517 The columns have the following meanings:
519 Version gives the version number. High-numbered versions are considered to be \
520 better than low-numbered ones.
522 Released gives the date this entry was added to the feed.
524 Stability is 'stable' if the implementation is believed to be stable, 'buggy' if \
525 it is known to contain serious bugs, and 'testing' if its stability is not yet \
526 known. This information is normally supplied and updated by the author of the \
527 software, but you can override their rating (overridden values are shown in upper-case). \
528 You can also use the special level 'preferred'.
530 C(ached) indicates whether the implementation is already stored on your computer. \
531 In off-line mode, only cached implementations are considered for use.
533 Arch indicates what kind of computer system the implementation is for, or 'any' \
534 if it works with all types of system.
537 The implementations are listed in the injector's currently preferred order (the one \
538 at the top will actually be used). Usable implementations all come before unusable \
541 Unusable ones are those for incompatible \
542 architectures, those marked as 'buggy', versions explicitly marked as incompatible with \
543 another interface you are using and, in off-line mode, uncached implementations. Unusable \
544 implementations are shown crossed out.
546 For the usable implementations, the order is as follows:
548 - Preferred implementations come first.
550 - Then, if network use is set to 'Minimal', cached implementations come before \
553 - Then, implementations at or above the selected stability level come before all others.
555 - Then, higher-numbered versions come before low-numbered ones.
557 - Then cached come before non-cached (for 'Full' network use mode).
561 If there is no binary available for your system then you may be able to compile one from \
562 source by clicking on the Compile button. If no source is available, the Compile button will \