Updated Compile GUI for Python 3
[zeroinstall/solver.git] / zeroinstall / 0launch-gui / properties.py
blob343c3fdd78bc9c4f8e224d477cbfccb676931f7b
1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import zeroinstall
5 import os
6 from zeroinstall import _
7 from zeroinstall.support import tasks, unicode
8 from zeroinstall.injector.model import Interface, Feed, stable, testing, developer, stability_levels
9 from zeroinstall.injector import writer, namespaces, gpg
10 from zeroinstall.gtkui import help_box
12 import gtk
13 from logging import warn
15 from dialog import DialogResponse, Template
16 from impl_list import ImplementationList
17 import time
18 import dialog
20 _dialogs = {} # Interface -> Properties
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 have_source_for(config, interface):
33 iface_cache = config.iface_cache
34 # Note: we don't want to actually fetch the source interfaces at
35 # this point, so we check whether:
36 # - We have a feed of type 'src' (not fetched), or
37 # - We have a source implementation in a regular feed
38 for f in iface_cache.get_feed_imports(interface):
39 if f.machine == 'src':
40 return True
41 # Don't have any src feeds. Do we have a source implementation
42 # as part of a regular feed?
43 for x in iface_cache.get_implementations(interface):
44 if x.machine == 'src':
45 return True
46 return False
48 class Description:
49 def __init__(self, widgets):
50 description = widgets.get_widget('description')
51 description.connect('button-press-event', self.button_press)
53 self.buffer = description.get_buffer()
54 self.heading_style = self.buffer.create_tag(underline = True, scale = 1.2)
55 self.link_style = self.buffer.create_tag(underline = True, foreground = 'blue')
56 description.set_size_request(-1, 100)
58 def button_press(self, tv, bev):
59 if bev.type == gtk.gdk.BUTTON_PRESS and bev.button == 1:
60 x, y = tv.window_to_buffer_coords(tv.get_window_type(bev.window),
61 int(bev.x), int(bev.y))
62 itr = tv.get_iter_at_location(x, y)
63 if itr and self.link_style in itr.get_tags():
64 if not itr.begins_tag(self.link_style):
65 itr.backward_to_tag_toggle(self.link_style)
66 end = itr.copy()
67 end.forward_to_tag_toggle(self.link_style)
68 target = itr.get_text(end).strip()
69 import browser
70 browser.open_in_browser(target)
72 def strtime(self, secs):
73 try:
74 from locale import nl_langinfo, D_T_FMT
75 return time.strftime(nl_langinfo(D_T_FMT), time.localtime(secs))
76 except (ImportError, ValueError):
77 return time.ctime(secs)
79 def set_details(self, iface_cache, feed):
80 buffer = self.buffer
81 heading_style = self.heading_style
83 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
85 iter = buffer.get_start_iter()
87 if feed is None:
88 buffer.insert(iter, 'Not yet downloaded.')
89 return
91 if isinstance(feed, Exception):
92 buffer.insert(iter, unicode(feed))
93 return
95 buffer.insert_with_tags(iter,
96 '%s ' % feed.get_name(), heading_style)
97 buffer.insert(iter, '(%s)' % feed.summary)
99 buffer.insert(iter, '\n%s\n' % feed.url)
101 # (converts to local time)
102 if feed.last_modified:
103 buffer.insert(iter, '\n' + _('Last upstream change: %s') % self.strtime(feed.last_modified))
105 if feed.last_checked:
106 buffer.insert(iter, '\n' + _('Last checked: %s') % self.strtime(feed.last_checked))
108 last_check_attempt = iface_cache.get_last_check_attempt(feed.url)
109 if last_check_attempt:
110 if feed.last_checked and feed.last_checked >= last_check_attempt:
111 pass # Don't bother reporting successful attempts
112 else:
113 buffer.insert(iter, '\n' + _('Last check attempt: %s (failed or in progress)') %
114 self.strtime(last_check_attempt))
116 buffer.insert_with_tags(iter, '\n\n' + _('Description') + '\n', heading_style)
118 paragraphs = [format_para(p) for p in (feed.description or "-").split('\n\n')]
120 buffer.insert(iter, '\n\n'.join(paragraphs))
121 buffer.insert(iter, '\n')
123 need_gap = True
124 for x in feed.get_metadata(namespaces.XMLNS_IFACE, 'homepage'):
125 if need_gap:
126 buffer.insert(iter, '\n')
127 need_gap = False
128 buffer.insert(iter, _('Homepage: '))
129 buffer.insert_with_tags(iter, '%s\n' % x.content, self.link_style)
131 if feed.local_path is None:
132 buffer.insert_with_tags(iter, '\n' + _('Signatures') + '\n', heading_style)
133 sigs = iface_cache.get_cached_signatures(feed.url)
134 if sigs:
135 for sig in sigs:
136 if isinstance(sig, gpg.ValidSig):
137 name = _('<unknown>')
138 details = sig.get_details()
139 for item in details:
140 if item[0] == 'uid' and len(item) > 9:
141 name = item[9]
142 break
143 buffer.insert_with_tags(iter, _('Valid signature by "%(name)s"\n- Dated: %(sig_date)s\n- Fingerprint: %(sig_fingerprint)s\n') %
144 {'name': name, 'sig_date': time.strftime('%c', time.localtime(sig.get_timestamp())), 'sig_fingerprint': sig.fingerprint})
145 if not sig.is_trusted():
146 if os.path.isabs(feed.url):
147 buffer.insert_with_tags(iter, _('WARNING: This key is not in the trusted list') + '\n')
148 else:
149 buffer.insert_with_tags(iter, _('WARNING: This key is not in the trusted list (either you removed it, or '
150 'you trust one of the other signatures)') + '\n')
151 else:
152 buffer.insert_with_tags(iter, '%s\n' % sig)
153 else:
154 buffer.insert_with_tags(iter, _('No signature information (old style feed or out-of-date cache)') + '\n')
156 class Feeds:
157 URI = 0
158 ARCH = 1
159 USED = 2
161 def __init__(self, config, arch, interface, widgets):
162 self.config = config
163 self.arch = arch
164 self.interface = interface
166 self.model = gtk.ListStore(str, str, bool)
168 self.description = Description(widgets)
170 self.lines = self.build_model()
171 for line in self.lines:
172 self.model.append(line)
174 add_remote_feed_button = widgets.get_widget('add_remote_feed')
175 add_remote_feed_button.connect('clicked', lambda b: add_remote_feed(config, widgets.get_widget(), interface))
177 add_local_feed_button = widgets.get_widget('add_local_feed')
178 add_local_feed_button.connect('clicked', lambda b: add_local_feed(config, interface))
180 self.remove_feed_button = widgets.get_widget('remove_feed')
181 def remove_feed(button):
182 model, iter = self.tv.get_selection().get_selected()
183 feed_uri = model[iter][Feeds.URI]
184 for x in interface.extra_feeds:
185 if x.uri == feed_uri:
186 if x.user_override:
187 interface.extra_feeds.remove(x)
188 writer.save_interface(interface)
189 import main
190 main.recalculate()
191 return
192 else:
193 dialog.alert(self.remove_feed_button.get_toplevel(),
194 _("Can't remove '%s' as you didn't add it.") % feed_uri)
195 return
196 raise Exception(_("Missing feed '%s'!") % feed_uri)
197 self.remove_feed_button.connect('clicked', remove_feed)
199 self.tv = widgets.get_widget('feeds_list')
200 self.tv.set_model(self.model)
201 text = gtk.CellRendererText()
202 self.tv.append_column(gtk.TreeViewColumn(_('Source'), text, text = Feeds.URI, sensitive = Feeds.USED))
203 self.tv.append_column(gtk.TreeViewColumn(_('Arch'), text, text = Feeds.ARCH, sensitive = Feeds.USED))
205 sel = self.tv.get_selection()
206 sel.set_mode(gtk.SELECTION_BROWSE)
207 sel.connect('changed', self.sel_changed)
208 sel.select_path((0,))
210 def build_model(self):
211 iface_cache = self.config.iface_cache
213 usable_feeds = frozenset(self.config.iface_cache.usable_feeds(self.interface, self.arch))
214 unusable_feeds = frozenset(iface_cache.get_feed_imports(self.interface)) - usable_feeds
216 out = [[self.interface.uri, None, True]]
218 for feed in usable_feeds:
219 out.append([feed.uri, feed.arch, True])
220 for feed in unusable_feeds:
221 out.append([feed.uri, feed.arch, False])
222 return out
224 def sel_changed(self, sel):
225 iface_cache = self.config.iface_cache
227 model, miter = sel.get_selected()
228 if not miter: return # build in progress
229 feed_url = model[miter][Feeds.URI]
230 # Only enable removing user_override feeds
231 enable_remove = False
232 for x in self.interface.extra_feeds:
233 if x.uri == feed_url:
234 if x.user_override and not x.site_package:
235 enable_remove = True
236 break
237 self.remove_feed_button.set_sensitive(enable_remove)
238 try:
239 self.description.set_details(iface_cache, iface_cache.get_feed(feed_url))
240 except zeroinstall.SafeException as ex:
241 self.description.set_details(iface_cache, ex)
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 driver = None
260 def __init__(self, driver, interface, compile, show_versions = False):
261 self.driver = driver
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 %s') % 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(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 target_arch = self.driver.solver.get_arch_for(driver.requirements, interface = interface)
287 feeds = Feeds(driver.config, target_arch, interface, widgets)
289 stability = widgets.get_widget('preferred_stability')
290 stability.set_active(0)
291 if interface.stability_policy:
292 i = [stable, testing, developer].index(interface.stability_policy)
293 i += 1
294 if i == 0:
295 warn(_("Unknown stability policy %s"), interface.stability_policy)
296 else:
297 i = 0
298 stability.set_active(i)
300 def set_stability_policy(combo, stability = stability): # (pygtk bug?)
301 i = stability.get_active()
302 if i == 0:
303 new_stability = None
304 else:
305 name = ['stable', 'testing', 'developer'][i-1]
306 new_stability = stability_levels[name]
307 interface.set_stability_policy(new_stability)
308 writer.save_interface(interface)
309 import main
310 main.recalculate()
311 stability.connect('changed', set_stability_policy)
313 self.use_list = ImplementationList(driver, interface, widgets)
315 self.update_list()
317 feeds.tv.grab_focus()
319 def updated():
320 self.update_list()
321 feeds.updated()
322 self.shade_compile()
323 window.connect('destroy', lambda s: driver.watchers.remove(updated))
324 driver.watchers.append(updated)
325 self.shade_compile()
327 if show_versions:
328 notebook.next_page()
330 def show(self):
331 self.window.show()
333 def destroy(self):
334 self.window.destroy()
336 def shade_compile(self):
337 self.compile_button.set_sensitive(have_source_for(self.driver.config, self.interface))
339 def update_list(self):
340 ranked_items = self.driver.solver.details.get(self.interface, None)
341 if ranked_items is None:
342 # The Solver didn't get this far, but we should still display them!
343 ranked_items = [(impl, _("(solve aborted before here)"))
344 for impl in self.interface.implementations.values()]
345 # Always sort by version
346 ranked_items.sort()
347 self.use_list.set_items(ranked_items)
349 @tasks.async
350 def add_remote_feed(config, parent, interface):
351 try:
352 iface_cache = config.iface_cache
354 d = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL,
355 _('Enter the URL of the new source of implementations of this interface:'))
356 d.add_button(gtk.STOCK_ADD, gtk.RESPONSE_OK)
357 d.set_default_response(gtk.RESPONSE_OK)
358 entry = gtk.Entry()
360 align = gtk.VBox(False, 0)
361 align.set_border_width(4)
362 align.add(entry)
363 d.vbox.pack_start(align)
364 entry.set_activates_default(True)
366 entry.set_text('')
368 d.vbox.show_all()
370 error_label = gtk.Label('')
371 error_label.set_padding(4, 4)
372 align.pack_start(error_label)
374 d.show()
376 def error(message):
377 if message:
378 error_label.set_text(message)
379 error_label.show()
380 else:
381 error_label.hide()
383 while True:
384 got_response = DialogResponse(d)
385 yield got_response
386 tasks.check(got_response)
387 resp = got_response.response
389 error(None)
390 if resp == gtk.RESPONSE_OK:
391 try:
392 url = entry.get_text()
393 if not url:
394 raise zeroinstall.SafeException(_('Enter a URL'))
395 fetch = config.fetcher.download_and_import_feed(url, iface_cache)
396 if fetch:
397 d.set_sensitive(False)
398 yield fetch
399 d.set_sensitive(True)
400 tasks.check(fetch)
402 iface = iface_cache.get_interface(url)
404 d.set_sensitive(True)
405 if not iface.name:
406 error(_('Failed to read interface'))
407 return
408 if not iface.feed_for:
409 error(_("Feed '%(feed)s' is not a feed for '%(feed_for)s'.") % {'feed': iface.get_name(), 'feed_for': interface.get_name()})
410 elif interface.uri not in iface.feed_for:
411 error(_("This is not a feed for '%(uri)s'.\nOnly for:\n%(feed_for)s") %
412 {'uri': interface.uri, 'feed_for': '\n'.join(iface.feed_for)})
413 elif iface.uri in [f.uri for f in interface.extra_feeds]:
414 error(_("Feed from '%s' has already been added!") % iface.uri)
415 else:
416 interface.extra_feeds.append(Feed(iface.uri, arch = None, user_override = True))
417 writer.save_interface(interface)
418 d.destroy()
419 import main
420 main.recalculate()
421 except zeroinstall.SafeException as ex:
422 error(str(ex))
423 else:
424 d.destroy()
425 return
426 except Exception as ex:
427 import traceback
428 traceback.print_exc()
429 config.handler.report_error(ex)
431 def add_local_feed(config, interface):
432 chooser = gtk.FileChooserDialog(_('Select XML feed file'), action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
433 def ok(feed):
434 from zeroinstall.injector import reader
435 try:
436 feed_targets = config.iface_cache.get_feed_targets(feed)
437 if interface not in feed_targets:
438 raise Exception(_("Not a valid feed for '%(uri)s'; this is a feed for:\n%(feed_for)s") %
439 {'uri': interface.uri,
440 'feed_for': '\n'.join([f.uri for f in feed_targets])})
441 if feed in [f.uri for f in interface.extra_feeds]:
442 dialog.alert(None, _('This feed is already registered.'))
443 else:
444 interface.extra_feeds.append(Feed(feed, user_override = True, arch = None))
446 writer.save_interface(interface)
447 chooser.destroy()
448 reader.update_from_cache(interface)
449 import main
450 main.recalculate()
451 except Exception as ex:
452 dialog.alert(None, _("Error in feed file '%(feed)s':\n\n%(exception)s") % {'feed': feed, 'exception': str(ex)})
454 def check_response(widget, response):
455 if response == gtk.RESPONSE_OK:
456 ok(widget.get_filename())
457 elif response == gtk.RESPONSE_CANCEL:
458 widget.destroy()
460 chooser.connect('response', check_response)
461 chooser.show()
463 def edit(driver, interface, compile, show_versions = False):
464 assert isinstance(interface, Interface)
465 if interface in _dialogs:
466 _dialogs[interface].destroy()
467 _dialogs[interface] = Properties(driver, interface, compile, show_versions = show_versions)
468 _dialogs[interface].show()
470 properties_help = help_box.HelpBox(_("Injector Properties Help"),
471 (_('Interface properties'), '\n' +
472 _("""This window displays information about an interface. There are two tabs at the top: \
473 Feeds shows the places where the injector looks for implementations of the interface, while \
474 Versions shows the list of implementations found (from all feeds) in order of preference.""")),
476 (_('The Feeds tab'), '\n' +
477 _("""At the top is a list of feeds. By default, the injector uses the full name of the interface \
478 as the default feed location (so if you ask it to run the program "http://foo/bar.xml" then it will \
479 by default get the list of versions by downloading "http://foo/bar.xml".
481 You can add and remove feeds using the buttons on the right. The main feed may also add \
482 some extra feeds itself. If you've checked out a developer version of a program, you can use \
483 the 'Add Local Feed...' button to let the injector know about it, for example.
485 Below the list of feeds is a box describing the selected one:
487 - At the top is its short name.
488 - Below that is the address (a URL or filename).
489 - 'Last upstream change' shows the version of the cached copy of the interface file.
490 - 'Last checked' is the last time a fresh copy of the upstream interface file was \
491 downloaded.
492 - Then there is a longer description of the interface.""")),
494 (_('The Versions tab'), '\n' +
495 _("""This tab shows a list of all known implementations of the interface, from all the feeds. \
496 The columns have the following meanings:
498 Version gives the version number. High-numbered versions are considered to be \
499 better than low-numbered ones.
501 Released gives the date this entry was added to the feed.
503 Stability is 'stable' if the implementation is believed to be stable, 'buggy' if \
504 it is known to contain serious bugs, and 'testing' if its stability is not yet \
505 known. This information is normally supplied and updated by the author of the \
506 software, but you can override their rating by right-clicking here (overridden \
507 values are shown in upper-case). You can also use the special level 'preferred'.
509 Fetch indicates how much data needs to be downloaded to get this version if you don't \
510 have it. If the implementation has already been downloaded to your computer, \
511 it will say (cached). (local) means that you installed this version manually and \
512 told Zero Install about it by adding a feed. (package) means that this version \
513 is provided by your distribution's package manager, not by Zero Install. \
514 In off-line mode, only cached implementations are considered for use.
516 Arch indicates what kind of computer system the implementation is for, or 'any' \
517 if it works with all types of system.
519 If you want to know why a particular version wasn't chosen, right-click over it \
520 and choose "Explain this decision" from the popup menu.
521 """) + '\n'),
522 (_('Sort order'), '\n' +
523 _("""The implementations are ordered by version number (highest first), with the \
524 currently selected one in bold. This is the "best" usable version.
526 Unusable ones are those for incompatible \
527 architectures, those marked as 'buggy' or 'insecure', versions explicitly marked as incompatible with \
528 another interface you are using and, in off-line mode, uncached implementations. Unusable \
529 implementations are shown crossed out.
531 For the usable implementations, the order is as follows:
533 - Preferred implementations come first.
535 - Then, if network use is set to 'Minimal', cached implementations come before \
536 non-cached.
538 - Then, implementations at or above the selected stability level come before all others.
540 - Then, higher-numbered versions come before low-numbered ones.
542 - Then cached come before non-cached (for 'Full' network use mode).""") + '\n'),
544 (_('Compiling'), '\n' +
545 _("""If there is no binary available for your system then you may be able to compile one from \
546 source by clicking on the Compile button. If no source is available, the Compile button will \
547 be shown shaded.""") + '\n'))