When we start checking for updates on a feed, record the time. In the GUI, if this...
[zeroinstall.git] / zeroinstall / 0launch-gui / properties.py
blob0726c09f7e25f40b4cabd70b91b8db339f9c3f38
1 from zeroinstall.injector.model import *
2 from zeroinstall.injector.iface_cache import iface_cache
3 from zeroinstall.injector import writer, namespaces, gpg
4 import gtk, sys, os
5 import sets # Note: for Python 2.3; frozenset is only in Python 2.4
7 import help_box
8 from dialog import Dialog
9 from gui import policy
10 from impl_list import ImplementationList
11 import time
12 import dialog
13 import compile
15 _dialogs = {} # Interface -> Properties
17 tips = gtk.Tooltips()
19 # Response codes
20 COMPILE = 2
22 def enumerate(items):
23 x = 0
24 for i in items:
25 yield x, i
26 x += 1
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')
34 child = os.fork()
35 if child == 0:
36 # We are the child
37 try:
38 os.spawnlp(os.P_NOWAIT, browser, browser, link)
39 os._exit(0)
40 except Exception, ex:
41 print >>sys.stderr, "Error", ex
42 os._exit(1)
43 os.waitpid(child, 0)
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
50 have_src = False
51 for f in interface.feeds:
52 if f.machine == 'src':
53 return True
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):
58 try:
59 feed_iface = iface_cache.get_interface(f.uri)
60 if feed_iface.implementations:
61 impls.extend(feed_iface.implementations.values())
62 except NeedDownload:
63 pass # OK, will get called again later
64 except Exception, ex:
65 warn("Failed to load feed '%s': %s", f.uri, str(ex))
66 for x in impls:
67 if x.machine == 'src':
68 return True
69 return False
71 class Description(gtk.ScrolledWindow):
72 def __init__(self):
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)
83 self.add(description)
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)
98 end = itr.copy()
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):
104 buffer = self.buffer
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
127 else:
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'):
139 need_gap = True
140 for x in interface.get_metadata(namespaces.XMLNS_IFACE, 'homepage'):
141 if need_gap:
142 buffer.insert(iter, '\n')
143 need_gap = False
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)
150 if sigs:
151 for sig in sigs:
152 if isinstance(sig, gpg.ValidSig):
153 name = '<unknown>'
154 if hasattr(sig, 'get_details'):
155 details = sig.get_details()
156 for item in details:
157 if item[0] in ('pub', 'uid') and len(item) > 9:
158 name = item[9]
159 break
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')
165 else:
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')
168 else:
169 buffer.insert_with_tags(iter, '%s\n' % sig)
170 else:
171 buffer.insert_with_tags(iter, 'No signature information (old style interface or out-of-date cache)\n')
174 class Feeds(gtk.VPaned):
175 URI = 0
176 ARCH = 1
177 USED = 2
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:
221 if x.user_override:
222 interface.feeds.remove(x)
223 writer.save_interface(interface)
224 policy.recalculate()
225 return
226 else:
227 dialog.alert(self.get_toplevel(),
228 _("Can't remove '%s' as you didn't add it.") % feed_uri)
229 return
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])
261 return out
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))
270 def updated(self):
271 new_lines = self.build_model()
272 if new_lines != self.lines:
273 self.lines = new_lines
274 self.model.clear()
275 for line in self.lines:
276 self.model.append(line)
277 self.tv.get_selection().select_path((0,))
278 else:
279 self.sel_changed(self.tv.get_selection())
281 class Properties(Dialog):
282 interface = None
283 use_list = None
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:
301 self.destroy()
302 #elif resp == 1:
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')))
315 self.update_list()
316 notebook.show_all()
318 feeds.tv.grab_focus()
320 def updated():
321 self.update_list()
322 feeds.updated()
323 self.shade_compile()
324 self.connect('destroy', lambda s: policy.watchers.remove(updated))
325 policy.watchers.append(updated)
326 self.shade_compile()
328 if show_versions:
329 notebook.next_page()
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)
347 eb = gtk.EventBox()
348 stability = gtk.combo_box_new_text()
349 eb.add(stability)
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()
360 if i == 0:
361 new_stability = None
362 else:
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)
367 policy.recalculate()
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)
377 return vbox
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)
384 entry = gtk.Entry()
386 align = gtk.VBox(False, 0)
387 align.set_border_width(4)
388 align.add(entry)
389 d.vbox.pack_start(align)
390 entry.set_activates_default(True)
392 entry.set_text('')
394 d.vbox.show_all()
396 error_label = gtk.Label('')
397 error_label.set_padding(4, 4)
398 align.pack_start(error_label)
400 def error(message):
401 if message:
402 error_label.set_text(message)
403 error_label.show()
404 else:
405 error_label.hide()
407 def download_done(iface):
408 d.set_sensitive(True)
409 if not iface.name:
410 error('Failed to read interface')
411 return
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)
419 else:
420 interface.feeds.append(Feed(iface.uri, arch = None, user_override = True))
421 writer.save_interface(interface)
422 d.destroy()
423 policy.recalculate()
425 def response(d, resp):
426 error(None)
427 if resp == gtk.RESPONSE_OK:
428 try:
429 url = entry.get_text()
430 if not url:
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:
437 error(str(ex))
438 else:
439 d.destroy()
440 return
441 d.connect('response', response)
442 d.show()
444 def add_local_feed(interface):
445 sel = gtk.FileSelection(_('Select XML feed file'))
446 sel.set_has_separator(False)
447 def ok(b):
448 from xml.dom import minidom
449 from zeroinstall.injector import reader
450 feed = sel.get_filename()
451 try:
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" %
456 (interface.uri,
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.')
460 else:
461 interface.feeds.append(Feed(feed, user_override = True, arch = None))
462 else:
463 doc = minidom.parse(feed)
464 uri = doc.documentElement.getAttribute('uri')
465 if not 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)
474 sel.destroy()
475 reader.update_from_cache(interface)
476 policy.recalculate()
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())
482 sel.show()
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 \
512 downloaded.
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.
535 """),
536 ('Sort order', """
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 \
539 ones.
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 \
551 non-cached.
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).
558 """),
560 ('Compiling', """
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 \
563 be shown shaded.
564 """))