Use localized timestamp display
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / 0launch-gui / properties.py
blobaeef6083aa60d4dc799c6ba64229479b6639d974
1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import zeroinstall
5 from zeroinstall.support import tasks
6 from zeroinstall.injector.model import Interface, Feed, stable, testing, developer, stability_levels
7 from zeroinstall.injector.iface_cache import iface_cache
8 from zeroinstall.injector import writer, namespaces, gpg
9 from zeroinstall.gtkui import help_box
11 import gtk
12 from logging import warn
14 from dialog import DialogResponse, Template
15 from impl_list import ImplementationList
16 import time
17 import dialog
18 import compile
20 _dialogs = {} # Interface -> Properties
22 tips = gtk.Tooltips()
24 # Response codes
25 COMPILE = 2
27 def enumerate(items):
28 x = 0
29 for i in items:
30 yield x, i
31 x += 1
33 def format_para(para):
34 lines = [l.strip() for l in para.split('\n')]
35 return ' '.join(lines)
37 def have_source_for(policy, interface):
38 # Note: we don't want to actually fetch the source interfaces at
39 # this point, so we check whether:
40 # - We have a feed of type 'src' (not fetched), or
41 # - We have a source implementation in a regular feed
42 have_src = False
43 for f in interface.feeds:
44 if f.machine == 'src':
45 return True
46 # Don't have any src feeds. Do we have a source implementation
47 # as part of a regular feed?
48 impls = interface.implementations.values()
49 for f in policy.usable_feeds(interface):
50 try:
51 feed_iface = iface_cache.get_interface(f.uri)
52 if feed_iface.implementations:
53 impls.extend(feed_iface.implementations.values())
54 except zeroinstall.NeedDownload:
55 pass # OK, will get called again later
56 except Exception, ex:
57 warn("Failed to load feed '%s': %s", f.uri, str(ex))
58 for x in impls:
59 if x.machine == 'src':
60 return True
61 return False
63 class Description:
64 def __init__(self, widgets):
65 description = widgets.get_widget('description')
66 description.connect('button-press-event', self.button_press)
68 self.buffer = description.get_buffer()
69 self.heading_style = self.buffer.create_tag(underline = True, scale = 1.2)
70 self.link_style = self.buffer.create_tag(underline = True, foreground = 'blue')
71 description.set_size_request(-1, 100)
73 def button_press(self, tv, bev):
74 if bev.type == gtk.gdk.BUTTON_PRESS and bev.button == 1:
75 x, y = tv.window_to_buffer_coords(tv.get_window_type(bev.window),
76 int(bev.x), int(bev.y))
77 itr = tv.get_iter_at_location(x, y)
78 if itr and self.link_style in itr.get_tags():
79 if not itr.begins_tag(self.link_style):
80 itr.backward_to_tag_toggle(self.link_style)
81 end = itr.copy()
82 end.forward_to_tag_toggle(self.link_style)
83 target = itr.get_text(end).strip()
84 import browser
85 browser.open_in_browser(target)
87 def strtime(self, secs):
88 try:
89 from locale import nl_langinfo, D_T_FMT
90 return time.strftime(nl_langinfo(D_T_FMT), time.localtime(secs))
91 except ImportError, ValueError:
92 return time.ctime(secs)
94 def set_details(self, interface):
95 buffer = self.buffer
96 heading_style = self.heading_style
98 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
100 iter = buffer.get_start_iter()
102 buffer.insert_with_tags(iter,
103 '%s ' % interface.get_name(), heading_style)
104 buffer.insert(iter, '(%s)' % interface.summary)
106 buffer.insert(iter, '\n%s\n' % interface.uri)
108 # (converts to local time)
109 if interface.last_modified:
110 buffer.insert(iter, '\n' + _('Last upstream change: %s') % self.strtime(interface.last_modified))
112 if interface.last_checked:
113 buffer.insert(iter, '\n' + _('Last checked: %s') % self.strtime(interface.last_checked))
115 last_check_attempt = iface_cache.get_last_check_attempt(interface.uri)
116 if last_check_attempt:
117 if interface.last_checked and interface.last_checked >= last_check_attempt:
118 pass # Don't bother reporting successful attempts
119 else:
120 buffer.insert(iter, '\n' + _('Last check attempt: %s (failed or in progress)') %
121 self.strtime(last_check_attempt))
123 buffer.insert_with_tags(iter, '\n\n' + _('Description') + '\n', heading_style)
125 paragraphs = [format_para(p) for p in (interface.description or "-").split('\n\n')]
127 buffer.insert(iter, '\n\n'.join(paragraphs))
128 buffer.insert(iter, '\n')
130 need_gap = True
131 for x in interface.get_metadata(namespaces.XMLNS_IFACE, 'homepage'):
132 if need_gap:
133 buffer.insert(iter, '\n')
134 need_gap = False
135 buffer.insert(iter, 'Homepage: ')
136 buffer.insert_with_tags(iter, '%s\n' % x.content, self.link_style)
138 buffer.insert_with_tags(iter, '\nSignatures\n', heading_style)
139 sigs = iface_cache.get_cached_signatures(interface.uri)
140 if sigs:
141 for sig in sigs:
142 if isinstance(sig, gpg.ValidSig):
143 name = '<unknown>'
144 details = sig.get_details()
145 for item in details:
146 if item[0] in ('pub', 'uid') and len(item) > 9:
147 name = item[9]
148 break
149 buffer.insert_with_tags(iter, _('Valid signature by "%s"\n- Dated: %s\n- Fingerprint: %s\n') %
150 (name, self.strtime(sig.get_timestamp()), sig.fingerprint))
151 if not sig.is_trusted():
152 if interface.uri.startswith('/'):
153 buffer.insert_with_tags(iter, 'WARNING: This key is not in the trusted list\n')
154 else:
155 buffer.insert_with_tags(iter, 'WARNING: This key is not in the trusted list (either you removed it, or '
156 'you trust one of the other signatures)\n')
157 else:
158 buffer.insert_with_tags(iter, '%s\n' % sig)
159 else:
160 buffer.insert_with_tags(iter, 'No signature information (old style interface or out-of-date cache)\n')
162 class Feeds:
163 URI = 0
164 ARCH = 1
165 USED = 2
167 def __init__(self, policy, interface, widgets):
168 self.policy = policy
169 self.interface = interface
171 self.model = gtk.ListStore(str, str, bool)
173 self.description = Description(widgets)
175 self.lines = self.build_model()
176 for line in self.lines:
177 self.model.append(line)
179 add_remote_feed_button = widgets.get_widget('add_remote_feed')
180 add_remote_feed_button.connect('clicked', lambda b: add_remote_feed(policy, widgets.get_widget(), interface))
182 add_local_feed_button = widgets.get_widget('add_local_feed')
183 add_local_feed_button.connect('clicked', lambda b: add_local_feed(policy, interface))
185 self.remove_feed_button = widgets.get_widget('remove_feed')
186 def remove_feed(button):
187 model, iter = self.tv.get_selection().get_selected()
188 feed_uri = model[iter][Feeds.URI]
189 for x in interface.feeds:
190 if x.uri == feed_uri:
191 if x.user_override:
192 interface.extra_feeds.remove(x)
193 writer.save_interface(interface)
194 policy.recalculate()
195 return
196 else:
197 dialog.alert(self.get_toplevel(),
198 _("Can't remove '%s' as you didn't add it.") % feed_uri)
199 return
200 raise Exception("Missing feed '%s'!" % feed_uri)
201 self.remove_feed_button.connect('clicked', remove_feed)
203 self.tv = widgets.get_widget('feeds_list')
204 self.tv.set_model(self.model)
205 text = gtk.CellRendererText()
206 self.tv.append_column(gtk.TreeViewColumn(_('Source'), text, text = Feeds.URI, sensitive = Feeds.USED))
207 self.tv.append_column(gtk.TreeViewColumn(_('Arch'), text, text = Feeds.ARCH, sensitive = Feeds.USED))
209 sel = self.tv.get_selection()
210 sel.set_mode(gtk.SELECTION_BROWSE)
211 sel.connect('changed', self.sel_changed)
212 sel.select_path((0,))
214 def build_model(self):
215 usable_feeds = frozenset(self.policy.usable_feeds(self.interface))
216 unusable_feeds = frozenset(self.interface.feeds) - usable_feeds
218 out = [[self.interface.uri, None, True]]
220 if self.interface.feeds:
221 for feed in usable_feeds:
222 out.append([feed.uri, feed.arch, True])
223 for feed in unusable_feeds:
224 out.append([feed.uri, feed.arch, False])
225 return out
227 def sel_changed(self, sel):
228 model, miter = sel.get_selected()
229 if not miter: return # build in progress
230 iface = model[miter][Feeds.URI]
231 # Only enable removing user_override feeds
232 enable_remove = False
233 for x in self.interface.feeds:
234 if x.uri == iface:
235 if x.user_override:
236 enable_remove = True
237 self.remove_feed_button.set_sensitive( enable_remove )
238 self.description.set_details(iface_cache.get_interface(iface))
240 def updated(self):
241 new_lines = self.build_model()
242 if new_lines != self.lines:
243 self.lines = new_lines
244 self.model.clear()
245 for line in self.lines:
246 self.model.append(line)
247 self.tv.get_selection().select_path((0,))
248 else:
249 self.sel_changed(self.tv.get_selection())
251 class Properties:
252 interface = None
253 use_list = None
254 window = None
255 policy = None
257 def __init__(self, policy, interface, show_versions = False):
258 self.policy = policy
260 widgets = Template('interface_properties')
262 self.interface = interface
264 window = widgets.get_widget('interface_properties')
265 self.window = window
266 window.set_title('Properties for ' + interface.get_name())
267 window.set_default_size(-1, gtk.gdk.screen_height() / 3)
269 self.compile_button = widgets.get_widget('compile')
270 self.compile_button.connect('clicked', lambda b: compile.compile(policy, interface))
271 window.set_default_response(gtk.RESPONSE_CANCEL)
273 def response(dialog, resp):
274 if resp == gtk.RESPONSE_CANCEL:
275 window.destroy()
276 elif resp == gtk.RESPONSE_HELP:
277 properties_help.display()
278 window.connect('response', response)
280 notebook = widgets.get_widget('interface_notebook')
281 assert notebook
283 feeds = Feeds(policy, interface, widgets)
285 stability = widgets.get_widget('preferred_stability')
286 stability.set_active(0)
287 if interface.stability_policy:
288 i = [stable, testing, developer].index(interface.stability_policy)
289 if i == -1:
290 warn("Unknown stability policy %s", interface.stability_policy)
291 i = 0
292 else:
293 i = 0
294 stability.set_active(i)
296 def set_stability_policy(combo):
297 i = stability.get_active()
298 if i == 0:
299 new_stability = None
300 else:
301 name = stability.get_model()[i][0].lower()
302 new_stability = stability_levels[name]
303 interface.set_stability_policy(new_stability)
304 writer.save_interface(interface)
305 policy.recalculate()
306 stability.connect('changed', set_stability_policy)
308 self.use_list = ImplementationList(policy, interface, widgets)
310 self.update_list()
312 feeds.tv.grab_focus()
314 def updated():
315 self.update_list()
316 feeds.updated()
317 self.shade_compile()
318 window.connect('destroy', lambda s: policy.watchers.remove(updated))
319 policy.watchers.append(updated)
320 self.shade_compile()
322 if show_versions:
323 notebook.next_page()
325 def destroy(self):
326 self.window.destroy()
328 def shade_compile(self):
329 self.compile_button.set_sensitive(have_source_for(self.policy, self.interface))
331 def update_list(self):
332 ranked_items = self.policy.solver.details.get(self.interface, None)
333 if ranked_items is None:
334 # The Solver didn't get this far, but we should still display them!
335 ranked_items = [(impl, "(solve aborted before here)")
336 for impl in self.interface.implementations.values()]
337 ranked_items.sort()
338 self.use_list.set_items(ranked_items)
340 @tasks.async
341 def add_remote_feed(policy, parent, interface):
342 try:
343 d = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL,
344 _('Enter the URL of the new source of implementations of this interface:'))
345 d.add_button(gtk.STOCK_ADD, gtk.RESPONSE_OK)
346 d.set_default_response(gtk.RESPONSE_OK)
347 entry = gtk.Entry()
349 align = gtk.VBox(False, 0)
350 align.set_border_width(4)
351 align.add(entry)
352 d.vbox.pack_start(align)
353 entry.set_activates_default(True)
355 entry.set_text('')
357 d.vbox.show_all()
359 error_label = gtk.Label('')
360 error_label.set_padding(4, 4)
361 align.pack_start(error_label)
363 d.show()
365 def error(message):
366 if message:
367 error_label.set_text(message)
368 error_label.show()
369 else:
370 error_label.hide()
372 while True:
373 got_response = DialogResponse(d)
374 yield got_response
375 tasks.check(got_response)
376 resp = got_response.response
378 error(None)
379 if resp == gtk.RESPONSE_OK:
380 try:
381 url = entry.get_text()
382 if not url:
383 raise zeroinstall.SafeException(_('Enter a URL'))
384 fetch = policy.fetcher.download_and_import_feed(url, iface_cache)
385 if fetch:
386 d.set_sensitive(False)
387 yield fetch
388 d.set_sensitive(True)
389 tasks.check(fetch)
391 iface = iface_cache.get_interface(url)
393 d.set_sensitive(True)
394 if not iface.name:
395 error('Failed to read interface')
396 return
397 if not iface.feed_for:
398 error("Feed '%s' is not a feed for '%s'." % (iface.get_name(), interface.get_name()))
399 elif interface.uri not in iface.feed_for:
400 error("This is not a feed for '%s'.\nOnly for:\n%s" %
401 (interface.uri, '\n'.join(iface.feed_for)))
402 elif iface.uri in [f.uri for f in interface.feeds]:
403 error("Feed from '%s' has already been added!" % iface.uri)
404 else:
405 interface.extra_feeds.append(Feed(iface.uri, arch = None, user_override = True))
406 writer.save_interface(interface)
407 d.destroy()
408 policy.recalculate()
409 except zeroinstall.SafeException, ex:
410 error(str(ex))
411 else:
412 d.destroy()
413 return
414 except Exception, ex:
415 import traceback
416 traceback.print_exc()
417 policy.handler.report_error(ex)
419 def add_local_feed(policy, interface):
420 sel = gtk.FileSelection(_('Select XML feed file'))
421 sel.set_has_separator(False)
422 def ok(b):
423 from zeroinstall.injector import reader
424 feed = sel.get_filename()
425 try:
426 feed_targets = policy.get_feed_targets(feed)
427 if interface not in feed_targets:
428 raise Exception("Not a valid feed for '%s'; this is a feed for:\n%s" %
429 (interface.uri,
430 '\n'.join([f.uri for f in feed_targets])))
431 if interface.get_feed(feed):
432 dialog.alert(None, 'This feed is already registered.')
433 else:
434 interface.extra_feeds.append(Feed(feed, user_override = True, arch = None))
436 writer.save_interface(interface)
437 sel.destroy()
438 reader.update_from_cache(interface)
439 policy.recalculate()
440 except Exception, ex:
441 dialog.alert(None, "Error in feed file '%s':\n\n%s" % (feed, str(ex)))
443 sel.ok_button.connect('clicked', ok)
444 sel.cancel_button.connect('clicked', lambda b: sel.destroy())
445 sel.show()
447 def edit(policy, interface, show_versions = False):
448 assert isinstance(interface, Interface)
449 if interface in _dialogs:
450 _dialogs[interface].destroy()
451 _dialogs[interface] = Properties(policy, interface, show_versions)
453 properties_help = help_box.HelpBox("Injector Properties Help",
454 ('Interface properties', """
455 This window displays information about an interface. There are two tabs at the top: \
456 Feeds shows the places where the injector looks for implementations of the interface, while \
457 Versions shows the list of implementations found (from all feeds) in order of preference."""),
459 ('The Feeds tab', """
460 At the top is a list of feeds. By default, the injector uses the full name of the interface \
461 as the default feed location (so if you ask it to run the program "http://foo/bar.xml" then it will \
462 by default get the list of versions by downloading "http://foo/bar.xml".
464 You can add and remove feeds using the buttons on the right. The main feed may also add \
465 some extra feeds itself. If you've checked out a developer version of a program, you can use \
466 the 'Add Local Feed...' button to let the injector know about it, for example.
468 Below the list of feeds is a box describing the selected one:
470 - At the top is its short name.
471 - Below that is the address (a URL or filename).
472 - 'Last upstream change' shows the version of the cached copy of the interface file.
473 - 'Last checked' is the last time a fresh copy of the upstream interface file was \
474 downloaded.
475 - Then there is a longer description of the interface."""),
477 ('The Versions tab', """
478 This tab shows a list of all known implementations of the interface, from all the feeds. \
479 The columns have the following meanings:
481 Version gives the version number. High-numbered versions are considered to be \
482 better than low-numbered ones.
484 Released gives the date this entry was added to the feed.
486 Stability is 'stable' if the implementation is believed to be stable, 'buggy' if \
487 it is known to contain serious bugs, and 'testing' if its stability is not yet \
488 known. This information is normally supplied and updated by the author of the \
489 software, but you can override their rating by right-clicking here (overridden \
490 values are shown in upper-case). You can also use the special level 'preferred'.
492 Fetch indicates how much data needs to be downloaded to get this version if you don't \
493 have it. If the implementation has already been downloaded to your computer, \
494 it will say (cached). (local) means that you installed this version manually and \
495 told Zero Install about it by adding a feed. (package) means that this version \
496 is provided by your distribution's package manager, not by Zero Install. \
497 In off-line mode, only cached implementations are considered for use.
499 Arch indicates what kind of computer system the implementation is for, or 'any' \
500 if it works with all types of system.
501 """),
502 ('Sort order', """
503 The implementations are listed in the injector's currently preferred order (the one \
504 at the top will actually be used). Usable implementations all come before unusable \
505 ones.
507 Unusable ones are those for incompatible \
508 architectures, those marked as 'buggy', versions explicitly marked as incompatible with \
509 another interface you are using and, in off-line mode, uncached implementations. Unusable \
510 implementations are shown crossed out.
512 For the usable implementations, the order is as follows:
514 - Preferred implementations come first.
516 - Then, if network use is set to 'Minimal', cached implementations come before \
517 non-cached.
519 - Then, implementations at or above the selected stability level come before all others.
521 - Then, higher-numbered versions come before low-numbered ones.
523 - Then cached come before non-cached (for 'Full' network use mode).
524 """),
526 ('Compiling', """
527 If there is no binary available for your system then you may be able to compile one from \
528 source by clicking on the Compile button. If no source is available, the Compile button will \
529 be shown shaded.
530 """))