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