Stop parsing autopackages when we have the information we need, or we get to the...
[0publish-gui.git] / main.py
blobead1a6fe36918c2a820af1c6a4d1812d5f9eadfb
1 from xml.dom import Node, minidom
3 import rox, os, pango, sys, textwrap, traceback, subprocess, time, urlparse, shutil
4 from rox import g, tasks, loading
5 import gtk.glade
7 import signing
8 import archive
9 from implementation import ImplementationProperties
10 from requires import Requires
11 from xmltools import *
13 from zeroinstall.zerostore import unpack, Stores
15 RESPONSE_SAVE = 0
16 RESPONSE_SAVE_AND_TEST = 1
18 gladefile = os.path.join(rox.app_dir, '0publish-gui.glade')
20 # Zero Install implementation cache
21 stores = Stores()
23 stylesheet_src = os.path.join(os.path.dirname(__file__), 'interface.xsl')
25 def available_in_path(prog):
26 for d in os.environ['PATH'].split(':'):
27 path = os.path.join(d, prog)
28 if os.path.isfile(path):
29 return True
30 return False
32 def choose_feed():
33 tree = gtk.glade.XML(gladefile, 'no_file_specified')
34 box = tree.get_widget('no_file_specified')
35 tree.get_widget('new_button').grab_focus()
36 resp = box.run()
37 box.destroy()
38 if resp == 0:
39 chooser = g.FileChooserDialog('Choose a location for the new feed',
40 None, g.FILE_CHOOSER_ACTION_SAVE)
41 chooser.set_current_name('MyProg.xml')
42 chooser.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
43 chooser.add_button(g.STOCK_NEW, g.RESPONSE_OK)
44 elif resp == 1:
45 chooser = g.FileChooserDialog('Choose the feed to edit',
46 None, g.FILE_CHOOSER_ACTION_OPEN)
47 chooser.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
48 chooser.add_button(g.STOCK_OPEN, g.RESPONSE_OK)
49 else:
50 sys.exit(1)
51 chooser.set_default_response(g.RESPONSE_OK)
52 if chooser.run() != g.RESPONSE_OK:
53 sys.exit(1)
54 path = chooser.get_filename()
55 chooser.destroy()
56 return FeedEditor(path)
58 def combo_set_text(combo, text):
59 if combo.get_active_text() or text:
60 model = combo.get_model()
61 i = 0
62 for row in model:
63 if row[0] == text:
64 combo.set_active(i)
65 return
66 i += 1
67 combo.append_text(text)
68 combo.set_active(i)
69 else:
70 return
72 def list_attrs(element):
73 attrs = element.attributes
74 names = []
75 for x in range(attrs.length):
76 attr = attrs.item(x)
78 if attr.name in ['id', 'version-modifier']: continue
79 if element.localName == 'implementation' and attr.name == 'version': continue
81 names.append(attr.name)
82 if names:
83 return ' (%s)' % ', '.join(names)
84 else:
85 return ''
87 emptyFeed = """<?xml version='1.0'?>
88 <interface xmlns="%s">
89 <name>Name</name>
90 </interface>
91 """ % (XMLNS_INTERFACE)
93 element_target = ('INTERNAL:FeedEditor/Element', gtk.TARGET_SAME_WIDGET, 0)
95 class FeedEditor(loading.XDSLoader):
96 def __init__(self, pathname):
97 loading.XDSLoader.__init__(self, None)
99 self.pathname = pathname
101 self.wTree = gtk.glade.XML(gladefile, 'main')
102 self.window = self.wTree.get_widget('main')
103 self.window.connect('destroy', rox.toplevel_unref)
104 self.xds_proxy_for(self.window)
106 help = gtk.glade.XML(gladefile, 'main_help')
107 help_box = help.get_widget('main_help')
108 help_box.set_default_size(g.gdk.screen_width() / 4,
109 g.gdk.screen_height() / 4)
110 help_box.connect('delete-event', lambda box, ev: True)
111 help_box.connect('response', lambda box, resp: box.hide())
113 def resp(box, resp):
114 if resp == g.RESPONSE_HELP:
115 help_box.present()
116 elif resp == RESPONSE_SAVE_AND_TEST:
117 self.save(self.test)
118 elif resp == RESPONSE_SAVE:
119 self.save()
120 else:
121 box.destroy()
122 self.window.connect('response', resp)
123 rox.toplevel_ref()
125 key_menu = self.wTree.get_widget('feed_key')
126 key_model = g.ListStore(str, str)
127 key_menu.set_model(key_model)
128 cell = g.CellRendererText()
129 cell.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
130 key_menu.pack_start(cell)
131 key_menu.add_attribute(cell, 'text', 1)
133 self.update_key_model()
135 self.impl_model = g.TreeStore(str, object)
136 impl_tree = self.wTree.get_widget('impl_tree')
137 impl_tree.set_model(self.impl_model)
138 text = g.CellRendererText()
139 column = g.TreeViewColumn('', text)
140 column.add_attribute(text, 'text', 0)
141 impl_tree.append_column(column)
143 impl_tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [element_target], gtk.gdk.ACTION_MOVE)
144 impl_tree.enable_model_drag_dest([element_target], gtk.gdk.ACTION_MOVE)
146 sel = impl_tree.get_selection()
147 sel.set_mode(g.SELECTION_BROWSE)
149 if os.path.exists(self.pathname):
150 data, _, self.key = signing.check_signature(self.pathname)
151 self.doc = minidom.parseString(data)
152 self.update_fields()
154 # Default to showing the versions tab
155 self.wTree.get_widget('notebook').next_page()
156 else:
157 default_name = os.path.basename(self.pathname)
158 if default_name.endswith('.xml'):
159 default_name = default_name[:-4]
160 self.wTree.get_widget('feed_name').set_text(default_name)
161 self.doc = minidom.parseString(emptyFeed)
162 self.key = None
163 key_menu.set_active(0)
165 root = self.impl_model.get_iter_root()
166 if root:
167 sel.select_iter(root)
169 self.wTree.get_widget('generate_key').connect('clicked', lambda b: self.generate_key())
171 self.wTree.get_widget('add_implementation').connect('clicked', lambda b: self.add_version())
172 self.wTree.get_widget('add_archive').connect('clicked', lambda b: self.add_archive())
173 self.wTree.get_widget('add_requires').connect('clicked', lambda b: self.add_requires())
174 self.wTree.get_widget('add_group').connect('clicked', lambda b: self.add_group())
175 self.wTree.get_widget('edit_properties').connect('clicked', lambda b: self.edit_properties())
176 self.wTree.get_widget('remove').connect('clicked', lambda b: self.remove_version())
177 impl_tree.connect('row-activated', lambda tv, path, col: self.edit_properties(path))
178 impl_tree.connect('drag-data-received', self.tree_drag_data_received)
180 def update_key_model(self):
181 key_menu = self.wTree.get_widget('feed_key')
182 key_model = key_menu.get_model()
183 keys = signing.get_secret_keys()
184 key_model.clear()
185 key_model.append((None, '(unsigned)'))
186 for k in keys:
187 key_model.append(k)
189 def generate_key(self):
190 for x in ['xterm', 'konsole']:
191 if available_in_path(x):
192 child = subprocess.Popen([x, '-e', 'gpg', '--gen-key'], stderr = subprocess.PIPE)
193 break
194 else:
195 child = subprocess.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr = subprocess.PIPE)
197 def get_keygen_out():
198 errors = ''
199 while True:
200 yield signing.InputBlocker(child.stderr)
201 data = os.read(child.stderr.fileno(), 100)
202 if not data:
203 break
204 errors += data
205 self.update_key_model()
206 if errors:
207 rox.alert('Errors from terminal: %s' % errors)
209 tasks.Task(get_keygen_out())
211 def tree_drag_data_received(self, treeview, context, x, y, selection, info, time):
212 if not selection: return
213 drop_info = treeview.get_dest_row_at_pos(x, y)
214 if drop_info:
215 model = treeview.get_model()
216 path, position = drop_info
218 src = self.get_selected()
219 dest = model[path][1]
221 def is_ancestor_or_self(a, b):
222 while b:
223 if b is a: return True
224 b = b.parentNode
225 return False
227 if is_ancestor_or_self(src, dest):
228 # Can't move an element into itself!
229 return
231 if position in (gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER):
232 new_parent = dest.parentNode
233 else:
234 new_parent = dest
236 if src.namespaceURI != XMLNS_INTERFACE: return
237 if new_parent.namespaceURI != XMLNS_INTERFACE: return
239 if new_parent.localName == 'group':
240 if src.localName not in ('implementation', 'group', 'requires'):
241 return
242 elif new_parent.localName == 'interface':
243 if src.localName not in ('implementation', 'group'):
244 return
245 elif new_parent.localName == 'implementation':
246 if src.localName not in ['requires']:
247 return
248 else:
249 return
251 remove_element(src)
253 if position == gtk.TREE_VIEW_DROP_BEFORE:
254 insert_before(src, dest)
255 elif position == gtk.TREE_VIEW_DROP_AFTER:
256 next = dest.nextSibling
257 while next and not next.nodeType == Node.ELEMENT_NODE:
258 next = next.nextSibling
259 if next:
260 insert_before(src, next)
261 else:
262 insert_element(src, new_parent)
263 else:
264 for next in child_elements(new_parent):
265 insert_before(src, next)
266 break
267 else:
268 insert_element(src, new_parent)
269 self.update_version_model()
271 def add_version(self):
272 ImplementationProperties(self)
274 def add_group(self):
275 ImplementationProperties(self, is_group = True)
277 def add_requires(self):
278 elem = self.get_selected()
279 if elem.namespaceURI == XMLNS_INTERFACE:
280 if elem.localName not in ('group', 'implementation'):
281 elem = elem.parentNode
282 if elem.localName in ('group', 'implementation'):
283 Requires(self, parent = elem)
284 return
285 rox.alert('Select a group or implementation!')
287 def edit_properties(self, path = None, element = None):
288 assert not (path and element)
290 if element:
291 pass
292 elif path is None:
293 element = self.get_selected()
294 else:
295 element = self.impl_model[path][1]
297 if element.namespaceURI != XMLNS_INTERFACE:
298 rox.alert("Sorry, I don't known how to edit %s elements!" % element.namespaceURI)
300 if element.localName in ('group', 'implementation'):
301 ImplementationProperties(self, element)
302 elif element.localName == 'requires':
303 Requires(self, parent = element.parentNode, element = element)
304 else:
305 rox.alert("Sorry, I don't known how to edit %s elements!" % element.localName)
307 def update_fields(self):
308 root = self.doc.documentElement
310 def set(name):
311 value = singleton_text(root, name)
312 if value:
313 self.wTree.get_widget('feed_' + name).set_text(value)
314 set('name')
315 set('summary')
316 set('homepage')
318 uri = root.getAttribute('uri')
319 if uri:
320 self.wTree.get_widget('feed_url').set_text(uri)
322 for icon in children(root, 'icon'):
323 if icon.getAttribute('type') == 'image/png':
324 href = icon.getAttribute('href')
325 self.wTree.get_widget('feed_icon').set_text(href)
326 break
328 description = singleton_text(root, 'description') or ''
329 paragraphs = [format_para(p) for p in description.split('\n\n')]
330 buffer = self.wTree.get_widget('feed_description').get_buffer()
331 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
332 buffer.insert_at_cursor('\n'.join(paragraphs))
334 key_menu = self.wTree.get_widget('feed_key')
335 model = key_menu.get_model()
336 if self.key:
337 i = 0
338 for line in model:
339 if line[0] == self.key:
340 break
341 i += 1
342 else:
343 model.append((self.key, 'Missing key (%s)' % self.key))
344 key_menu.set_active(i)
345 else:
346 key_menu.set_active(0)
348 self.update_version_model()
350 def add_archives(self, impl_element, iter):
351 for child in child_elements(impl_element):
352 if child.namespaceURI != XMLNS_INTERFACE: continue
353 if child.localName == 'archive':
354 self.impl_model.append(iter, ['Archive ' + child.getAttribute('href'), child])
355 elif child.localName == 'requires':
356 req_iface = child.getAttribute('interface')
357 self.impl_model.append(iter, ['Impl requires %s' % req_iface, child])
358 else:
359 self.impl_model.append(iter, ['<%s>' % child.localName, child])
361 def update_version_model(self):
362 impl_tree = self.wTree.get_widget('impl_tree')
364 # Remember which ones are open
365 expanded_elements = set()
366 impl_tree.map_expanded_rows(lambda tv, path: expanded_elements.add(self.impl_model[path][1]))
368 initial_build = not self.impl_model.get_iter_root()
370 self.impl_model.clear()
371 to_expand = []
373 def add_impls(elem, iter, attrs):
374 """Add all groups, implementations and requirements in elem"""
376 for x in child_elements(elem):
377 if x.namespaceURI != XMLNS_INTERFACE: continue
379 if x.localName == 'requires':
380 req_iface = x.getAttribute('interface')
381 new = self.impl_model.append(iter, ['Group requires %s' % req_iface, x])
383 if x.localName not in ('implementation', 'group'): continue
385 new_attrs = attrs.copy()
386 attributes = x.attributes
387 for i in range(attributes.length):
388 a = attributes.item(i)
389 new_attrs[str(a.name)] = a.value
391 if x.localName == 'implementation':
392 version = new_attrs.get('version', '(missing version number)') + \
393 (new_attrs.get('version-modifier') or '')
394 new = self.impl_model.append(iter, ['Version %s%s' % (version, list_attrs(x)), x])
395 self.add_archives(x, new)
396 elif x.localName == 'group':
397 new = self.impl_model.append(iter, ['Group%s' % list_attrs(x), x])
398 if initial_build:
399 expanded_elements.add(x)
400 add_impls(x, new, new_attrs)
402 add_impls(self.doc.documentElement, None, attrs = {})
404 def may_expand(model, path, iter):
405 if model[iter][1] in expanded_elements:
406 impl_tree.expand_row(path, False)
407 self.impl_model.foreach(may_expand)
409 def test(self):
410 child = os.fork()
411 if child == 0:
412 try:
413 try:
414 # We are the child
415 # Spawn a grandchild and exit
416 subprocess.Popen(['0launch', '--gui', self.pathname])
417 os._exit(0)
418 except:
419 traceback.print_exc()
420 finally:
421 os._exit(1)
422 pid, status = os.waitpid(child, 0)
423 assert pid == child
424 if status:
425 raise Exception('Failed to run 0launch - status code %d' % status)
427 def update_doc(self):
428 root = self.doc.documentElement
429 def update(name, required = False, attrs = {}, value_attr = None):
430 widget = self.wTree.get_widget('feed_' + name)
431 if isinstance(widget, g.TextView):
432 buffer = widget.get_buffer()
433 text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
434 paras = ['\n'.join(textwrap.wrap(para, 80)) for para in text.split('\n') if para.strip()]
435 value = '\n' + '\n\n'.join(paras)
436 else:
437 value = widget.get_text()
438 elems = list(children(root, name, attrs = attrs))
439 if value:
440 if elems:
441 elem = elems[0]
442 else:
443 elem = create_element(root, name,
444 before = ['group', 'implementation', 'requires'])
445 for x in attrs:
446 elem.setAttribute(x, attrs[x])
447 if value_attr:
448 elem.setAttribute(value_attr, value)
449 set_data(elem, None)
450 else:
451 set_data(elem, value)
452 else:
453 if required:
454 raise Exception('Missing required field "%s"' % name)
455 for e in elems:
456 remove_element(e)
458 update('name', True)
459 update('summary', True)
460 update('description', True)
461 update('homepage')
462 update('icon', attrs = {'type': 'image/png'}, value_attr = 'href')
464 uri = self.wTree.get_widget('feed_url').get_text()
465 if uri:
466 root.setAttribute('uri', uri)
467 elif root.hasAttribute('uri'):
468 root.removeAttribute('uri')
470 key_menu = self.wTree.get_widget('feed_key')
471 key_model = key_menu.get_model()
472 self.key = key_model[key_menu.get_active()][0]
474 def export_stylesheet_and_key(self):
475 dir = os.path.dirname(self.pathname)
476 stylesheet = os.path.join(dir, 'interface.xsl')
477 if not os.path.exists(stylesheet):
478 shutil.copyfile(stylesheet_src, stylesheet)
479 rox.info("I have saved a stylesheet as '%s'. You should upload "
480 "this to your web-server in the same directory as the feed file. "
481 "This allows browsers to display the feed nicely." % stylesheet)
483 exported = signing.export_key(dir, self.key)
484 if exported:
485 rox.info("I have exported your public key as '%s'. You should upload "
486 "this to your web-server in the same directory as the feed file. "
487 "This allows people to check the signature on your feed." % exported)
489 def save(self, callback = None):
490 self.update_doc()
491 if self.key:
492 sign = signing.sign_xml
493 self.export_stylesheet_and_key()
494 else:
495 sign = signing.sign_unsigned
496 data = self.doc.toxml() + '\n'
498 gen = sign(self.pathname, data, self.key, callback)
499 # May require interaction to get the pass-phrase, so run in the background...
500 if gen:
501 tasks.Task(gen)
503 def add_archive(self):
504 archive.AddArchiveBox(self)
506 def xds_load_from_file(self, path):
507 archive.AddArchiveBox(self, local_archive = path)
509 def remove_version(self, path = None):
510 elem = self.get_selected()
511 remove_element(elem)
512 self.update_version_model()
514 def get_selected(self):
515 tree = self.wTree.get_widget('impl_tree')
516 sel = tree.get_selection()
517 model, iter = sel.get_selected()
518 if not iter:
519 raise Exception('Select something first!')
520 return model[iter][1]
522 def find_implementation(self, id):
523 def find_impl(parent):
524 for x in child_elements(parent):
525 if x.namespaceURI != XMLNS_INTERFACE: continue
526 if x.localName == 'group':
527 sub = find_impl(x)
528 if sub: return sub
529 elif x.localName == 'implementation':
530 if x.getAttribute('id') == id:
531 return x
532 return find_impl(self.doc.documentElement)