Use Glade for the interface properties box.
[zeroinstall.git] / zeroinstall / 0launch-gui / properties.py
blobc8ebbfe6ad43ceef8a57e53dd8e141253a8052e1
1 import zeroinstall
2 from zeroinstall.injector.model import *
3 from zeroinstall.injector.iface_cache import iface_cache
4 from zeroinstall.injector import writer, namespaces, gpg
6 import gtk, sys, os
7 import sets # Note: for Python 2.3; frozenset is only in Python 2.4
8 from logging import warn
10 import help_box
11 from dialog import Dialog
12 from gui import policy, 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(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 if interface.last_check_attempt:
119 if interface.last_checked and interface.last_checked >= interface.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(interface.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, interface, widgets):
170 self.interface = interface
172 self.model = gtk.ListStore(str, str, bool)
174 self.description = Description(widgets)
176 self.lines = self.build_model()
177 for line in self.lines:
178 self.model.append(line)
180 add_remote_feed_button = widgets.get_widget('add_remote_feed')
181 add_remote_feed_button.connect('clicked', lambda b: add_remote_feed(widgets.get_widget(), interface))
183 add_local_feed_button = widgets.get_widget('add_local_feed')
184 add_local_feed_button.connect('clicked', lambda b: add_local_feed(interface))
186 self.remove_feed_button = widgets.get_widget('remove_feed')
187 def remove_feed(button):
188 model, iter = self.tv.get_selection().get_selected()
189 feed_uri = model[iter][Feeds.URI]
190 for x in interface.feeds:
191 if x.uri == feed_uri:
192 if x.user_override:
193 interface.feeds.remove(x)
194 writer.save_interface(interface)
195 policy.recalculate()
196 return
197 else:
198 dialog.alert(self.get_toplevel(),
199 _("Can't remove '%s' as you didn't add it.") % feed_uri)
200 return
201 raise Exception("Missing feed '%s'!" % feed_uri)
202 self.remove_feed_button.connect('clicked', remove_feed)
204 self.tv = widgets.get_widget('feeds_list')
205 self.tv.set_model(self.model)
206 text = gtk.CellRendererText()
207 self.tv.append_column(gtk.TreeViewColumn('Source', text, text = Feeds.URI, sensitive = Feeds.USED))
208 self.tv.append_column(gtk.TreeViewColumn('Arch', text, text = Feeds.ARCH, sensitive = Feeds.USED))
210 sel = self.tv.get_selection()
211 sel.set_mode(gtk.SELECTION_BROWSE)
212 sel.connect('changed', self.sel_changed)
213 sel.select_path((0,))
215 def build_model(self):
216 usable_feeds = sets.ImmutableSet(policy.usable_feeds(self.interface))
217 unusable_feeds = sets.ImmutableSet(self.interface.feeds) - usable_feeds
219 out = [[self.interface.uri, None, True]]
221 if self.interface.feeds:
222 for feed in usable_feeds:
223 out.append([feed.uri, feed.arch, True])
224 for feed in unusable_feeds:
225 out.append([feed.uri, feed.arch, False])
226 return out
228 def sel_changed(self, sel):
229 model, miter = sel.get_selected()
230 if not miter: return # build in progress
231 iface = model[miter][Feeds.URI]
232 # Only enable removing user_override feeds
233 enable_remove = False
234 for x in self.interface.feeds:
235 if x.uri == iface:
236 if x.user_override:
237 enable_remove = True
238 self.remove_feed_button.set_sensitive( enable_remove )
239 self.description.set_details(iface_cache.get_interface(iface))
241 def updated(self):
242 new_lines = self.build_model()
243 if new_lines != self.lines:
244 self.lines = new_lines
245 self.model.clear()
246 for line in self.lines:
247 self.model.append(line)
248 self.tv.get_selection().select_path((0,))
249 else:
250 self.sel_changed(self.tv.get_selection())
252 class Properties:
253 interface = None
254 use_list = None
256 def __init__(self, interface, show_versions = False):
257 widgets = Template('interface_properties')
259 self.interface = interface
261 window = widgets.get_widget('interface_properties')
262 window.set_title('Properties for ' + interface.get_name())
263 window.set_default_size(-1, gtk.gdk.screen_height() / 3)
265 self.compile_button = widgets.get_widget('compile')
266 self.compile_button.connect('clicked', lambda b: compile.compile(interface))
267 window.set_default_response(gtk.RESPONSE_CANCEL)
269 def response(dialog, resp):
270 if resp == gtk.RESPONSE_CANCEL:
271 window.destroy()
272 elif resp == gtk.RESPONSE_HELP:
273 properties_help.display()
274 window.connect('response', response)
276 notebook = widgets.get_widget('interface_notebook')
277 assert notebook
279 feeds = Feeds(interface, widgets)
281 stability = widgets.get_widget('preferred_stability')
282 stability.set_active(0)
283 if interface.stability_policy:
284 i = [stable, testing, developer].index(interface.stability_policy)
285 if i == -1:
286 warn("Unknown stability policy %s", interface.stability_policy)
287 i = 0
288 else:
289 i = 0
290 stability.set_active(i)
292 def set_stability_policy(combo):
293 i = stability.get_active()
294 if i == 0:
295 new_stability = None
296 else:
297 name = stability.get_model()[i][0].lower()
298 new_stability = stability_levels[name]
299 interface.set_stability_policy(new_stability)
300 writer.save_interface(interface)
301 policy.recalculate()
302 stability.connect('changed', set_stability_policy)
304 self.use_list = ImplementationList(interface, widgets)
306 self.update_list()
308 feeds.tv.grab_focus()
310 def updated():
311 self.update_list()
312 feeds.updated()
313 self.shade_compile()
314 window.connect('destroy', lambda s: policy.watchers.remove(updated))
315 policy.watchers.append(updated)
316 self.shade_compile()
318 if show_versions:
319 notebook.next_page()
321 def shade_compile(self):
322 self.compile_button.set_sensitive(have_source_for(self.interface))
324 def update_list(self):
325 impls = policy.get_ranked_implementations(self.interface)
326 self.use_list.set_items(impls)
328 def add_remote_feed(parent, interface):
329 d = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL,
330 _('Enter the URL of the new source of implementations of this interface:'))
331 d.add_button(gtk.STOCK_ADD, gtk.RESPONSE_OK)
332 d.set_default_response(gtk.RESPONSE_OK)
333 entry = gtk.Entry()
335 align = gtk.VBox(False, 0)
336 align.set_border_width(4)
337 align.add(entry)
338 d.vbox.pack_start(align)
339 entry.set_activates_default(True)
341 entry.set_text('')
343 d.vbox.show_all()
345 error_label = gtk.Label('')
346 error_label.set_padding(4, 4)
347 align.pack_start(error_label)
349 def error(message):
350 if message:
351 error_label.set_text(message)
352 error_label.show()
353 else:
354 error_label.hide()
356 def download_done(iface):
357 d.set_sensitive(True)
358 if not iface.name:
359 error('Failed to read interface')
360 return
361 if not iface.feed_for:
362 error("Interface '%s' is not a feed." % iface.get_name())
363 elif interface.uri not in iface.feed_for:
364 error("Interface is not a feed for '%s'.\nOnly for:\n%s" %
365 (interface.uri, '\n'.join(iface.feed_for)))
366 elif iface.uri in [f.uri for f in interface.feeds]:
367 error("Feed from '%s' has already been added!" % iface.uri)
368 else:
369 interface.feeds.append(Feed(iface.uri, arch = None, user_override = True))
370 writer.save_interface(interface)
371 d.destroy()
372 policy.recalculate()
374 def response(d, resp):
375 error(None)
376 if resp == gtk.RESPONSE_OK:
377 try:
378 url = entry.get_text()
379 if not url:
380 raise SafeException(_('Enter a URL'))
381 iface = iface_cache.get_interface(url)
382 policy.begin_iface_download(iface) # Force a refresh
383 d.set_sensitive(False)
384 policy.handler.add_dl_callback(url, lambda: download_done(iface))
385 except SafeException, ex:
386 error(str(ex))
387 else:
388 d.destroy()
389 return
390 d.connect('response', response)
391 d.show()
393 def add_local_feed(interface):
394 sel = gtk.FileSelection(_('Select XML feed file'))
395 sel.set_has_separator(False)
396 def ok(b):
397 from xml.dom import minidom
398 from zeroinstall.injector import reader
399 feed = sel.get_filename()
400 try:
401 feed_targets = policy.get_feed_targets(feed)
402 if interface not in feed_targets:
403 raise Exception("Not a valid feed for '%s'; this is a feed for:\n%s" %
404 (interface.uri,
405 '\n'.join([f.uri for f in feed_targets])))
406 if interface.get_feed(feed):
407 dialog.alert(None, 'This feed is already registered.')
408 else:
409 interface.feeds.append(Feed(feed, user_override = True, arch = None))
411 writer.save_interface(interface)
412 sel.destroy()
413 reader.update_from_cache(interface)
414 policy.recalculate()
415 except Exception, ex:
416 dialog.alert(None, "Error in feed file '%s':\n\n%s" % (feed, str(ex)))
418 sel.ok_button.connect('clicked', ok)
419 sel.cancel_button.connect('clicked', lambda b: sel.destroy())
420 sel.show()
422 def edit(interface, show_versions = False):
423 assert isinstance(interface, Interface)
424 if interface in _dialogs:
425 _dialogs[interface].destroy()
426 _dialogs[interface] = Properties(interface, show_versions)
428 properties_help = help_box.HelpBox("Injector Properties Help",
429 ('Interface properties', """
430 This window displays information about an interface. There are two tabs at the top: \
431 Feeds shows the places where the injector looks for implementations of the interface, while \
432 Versions shows the list of implementations found (from all feeds) in order of preference."""),
434 ('The Feeds tab', """
435 At the top is a list of feeds. By default, the injector uses the full name of the interface \
436 as the default feed location (so if you ask it to run the program "http://foo/bar.xml" then it will \
437 by default get the list of versions by downloading "http://foo/bar.xml".
439 You can add and remove feeds using the buttons on the right. The main feed may also add \
440 some extra feeds itself. If you've checked out a developer version of a program, you can use \
441 the 'Add Local Feed...' button to let the injector know about it, for example.
443 Below the list of feeds is a box describing the selected one:
445 - At the top is its short name.
446 - Below that is the address (a URL or filename).
447 - 'Last upstream change' shows the version of the cached copy of the interface file.
448 - 'Last checked' is the last time a fresh copy of the upstream interface file was \
449 downloaded.
450 - Then there is a longer description of the interface."""),
452 ('The Versions tab', """
453 This tab shows a list of all known implementations of the interface, from all the feeds. \
454 The columns have the following meanings:
456 Version gives the version number. High-numbered versions are considered to be \
457 better than low-numbered ones.
459 Released gives the date this entry was added to the feed.
461 Stability is 'stable' if the implementation is believed to be stable, 'buggy' if \
462 it is known to contain serious bugs, and 'testing' if its stability is not yet \
463 known. This information is normally supplied and updated by the author of the \
464 software, but you can override their rating (overridden values are shown in upper-case). \
465 You can also use the special level 'preferred'.
467 C(ached) indicates whether the implementation is already stored on your computer. \
468 In off-line mode, only cached implementations are considered for use.
470 Arch indicates what kind of computer system the implementation is for, or 'any' \
471 if it works with all types of system.
472 """),
473 ('Sort order', """
474 The implementations are listed in the injector's currently preferred order (the one \
475 at the top will actually be used). Usable implementations all come before unusable \
476 ones.
478 Unusable ones are those for incompatible \
479 architectures, those marked as 'buggy', versions explicitly marked as incompatible with \
480 another interface you are using and, in off-line mode, uncached implementations. Unusable \
481 implementations are shown crossed out.
483 For the usable implementations, the order is as follows:
485 - Preferred implementations come first.
487 - Then, if network use is set to 'Minimal', cached implementations come before \
488 non-cached.
490 - Then, implementations at or above the selected stability level come before all others.
492 - Then, higher-numbered versions come before low-numbered ones.
494 - Then cached come before non-cached (for 'Full' network use mode).
495 """),
497 ('Compiling', """
498 If there is no binary available for your system then you may be able to compile one from \
499 source by clicking on the Compile button. If no source is available, the Compile button will \
500 be shown shaded.
501 """))