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