Better error if Python version is too old.
[0publish-gui.git] / main.py
blobaa50cd88caa0f3bdefcb026cb0eb73a20a9e668a
1 from xml.dom import Node, minidom
3 import rox, os, pango, sys, textwrap, traceback, subprocess, time, urlparse
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 def available_in_path(prog):
24 for d in os.environ['PATH'].split(':'):
25 path = os.path.join(d, prog)
26 if os.path.isfile(path):
27 return True
28 return False
30 def choose_feed():
31 tree = gtk.glade.XML(gladefile, 'no_file_specified')
32 box = tree.get_widget('no_file_specified')
33 tree.get_widget('new_button').grab_focus()
34 resp = box.run()
35 box.destroy()
36 if resp == 0:
37 chooser = g.FileChooserDialog('Choose a location for the new feed',
38 None, g.FILE_CHOOSER_ACTION_SAVE)
39 chooser.set_current_name('MyProg.xml')
40 chooser.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
41 chooser.add_button(g.STOCK_NEW, g.RESPONSE_OK)
42 elif resp == 1:
43 chooser = g.FileChooserDialog('Choose the feed to edit',
44 None, g.FILE_CHOOSER_ACTION_OPEN)
45 chooser.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
46 chooser.add_button(g.STOCK_OPEN, g.RESPONSE_OK)
47 else:
48 sys.exit(1)
49 chooser.set_default_response(g.RESPONSE_OK)
50 if chooser.run() != g.RESPONSE_OK:
51 sys.exit(1)
52 path = chooser.get_filename()
53 chooser.destroy()
54 return FeedEditor(path)
56 def combo_set_text(combo, text):
57 if combo.get_active_text() or text:
58 model = combo.get_model()
59 i = 0
60 for row in model:
61 if row[0] == text:
62 combo.set_active(i)
63 return
64 i += 1
65 combo.append_text(text)
66 combo.set_active(i)
67 else:
68 return
70 emptyFeed = """<?xml version='1.0'?>
71 <interface xmlns="%s">
72 <name>Name</name>
73 </interface>
74 """ % (XMLNS_INTERFACE)
76 element_target = ('INTERNAL:FeedEditor/Element', gtk.TARGET_SAME_WIDGET, 0)
78 class FeedEditor(loading.XDSLoader):
79 def __init__(self, pathname):
80 loading.XDSLoader.__init__(self, None)
82 self.pathname = pathname
84 self.wTree = gtk.glade.XML(gladefile, 'main')
85 self.window = self.wTree.get_widget('main')
86 self.window.connect('destroy', rox.toplevel_unref)
87 self.xds_proxy_for(self.window)
89 help = gtk.glade.XML(gladefile, 'main_help')
90 help_box = help.get_widget('main_help')
91 help_box.set_default_size(g.gdk.screen_width() / 4,
92 g.gdk.screen_height() / 4)
93 help_box.connect('delete-event', lambda box, ev: True)
94 help_box.connect('response', lambda box, resp: box.hide())
96 def resp(box, resp):
97 if resp == g.RESPONSE_HELP:
98 help_box.present()
99 elif resp == RESPONSE_SAVE_AND_TEST:
100 self.save(self.test)
101 elif resp == RESPONSE_SAVE:
102 self.save()
103 else:
104 box.destroy()
105 self.window.connect('response', resp)
106 rox.toplevel_ref()
108 key_menu = self.wTree.get_widget('feed_key')
109 key_model = g.ListStore(str, str)
110 key_menu.set_model(key_model)
111 cell = g.CellRendererText()
112 cell.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
113 key_menu.pack_start(cell)
114 key_menu.add_attribute(cell, 'text', 1)
116 self.update_key_model()
118 self.impl_model = g.TreeStore(str, object)
119 impl_tree = self.wTree.get_widget('impl_tree')
120 impl_tree.set_model(self.impl_model)
121 text = g.CellRendererText()
122 column = g.TreeViewColumn('', text)
123 column.add_attribute(text, 'text', 0)
124 impl_tree.append_column(column)
126 impl_tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [element_target], gtk.gdk.ACTION_MOVE)
127 impl_tree.enable_model_drag_dest([element_target], gtk.gdk.ACTION_MOVE)
129 sel = impl_tree.get_selection()
130 sel.set_mode(g.SELECTION_BROWSE)
132 if os.path.exists(self.pathname):
133 data, _, self.key = signing.check_signature(self.pathname)
134 self.doc = minidom.parseString(data)
135 self.update_fields()
137 # Default to showing the versions tab
138 self.wTree.get_widget('notebook').next_page()
139 else:
140 self.doc = minidom.parseString(emptyFeed)
141 self.key = None
142 key_menu.set_active(0)
144 root = self.impl_model.get_iter_root()
145 if root:
146 sel.select_iter(root)
148 self.wTree.get_widget('generate_key').connect('clicked', lambda b: self.generate_key())
150 self.wTree.get_widget('add_implementation').connect('clicked', lambda b: self.add_version())
151 self.wTree.get_widget('add_archive').connect('clicked', lambda b: self.add_archive())
152 self.wTree.get_widget('add_requires').connect('clicked', lambda b: self.add_requires())
153 self.wTree.get_widget('add_group').connect('clicked', lambda b: self.add_group())
154 self.wTree.get_widget('edit_properties').connect('clicked', lambda b: self.edit_properties())
155 self.wTree.get_widget('remove').connect('clicked', lambda b: self.remove_version())
156 impl_tree.connect('row-activated', lambda tv, path, col: self.edit_properties(path))
157 impl_tree.connect('drag-data-received', self.tree_drag_data_received)
159 def update_key_model(self):
160 key_menu = self.wTree.get_widget('feed_key')
161 key_model = key_menu.get_model()
162 keys = signing.get_secret_keys()
163 key_model.clear()
164 key_model.append((None, '(unsigned)'))
165 for k in keys:
166 key_model.append(k)
168 def generate_key(self):
169 for x in ['xterm', 'konsole']:
170 if available_in_path(x):
171 child = subprocess.Popen([x, '-e', 'gpg', '--gen-key'], stderr = subprocess.PIPE)
172 break
173 else:
174 child = subprocess.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr = subprocess.PIPE)
176 def get_keygen_out():
177 errors = ''
178 while True:
179 yield signing.InputBlocker(child.stderr)
180 data = os.read(child.stderr.fileno(), 100)
181 if not data:
182 break
183 errors += data
184 self.update_key_model()
185 if errors:
186 rox.alert('Errors from terminal: %s' % errors)
188 tasks.Task(get_keygen_out())
190 def tree_drag_data_received(self, treeview, context, x, y, selection, info, time):
191 if not selection: return
192 drop_info = treeview.get_dest_row_at_pos(x, y)
193 if drop_info:
194 model = treeview.get_model()
195 path, position = drop_info
197 src = self.get_selected()
198 dest = model[path][1]
200 def is_ancestor_or_self(a, b):
201 while b:
202 if b is a: return True
203 b = b.parentNode
204 return False
206 if is_ancestor_or_self(src, dest):
207 # Can't move an element into itself!
208 return
210 if position in (gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER):
211 new_parent = dest.parentNode
212 else:
213 new_parent = dest
215 if src.namespaceURI != XMLNS_INTERFACE: return
216 if new_parent.namespaceURI != XMLNS_INTERFACE: return
218 if new_parent.localName == 'group':
219 if src.localName not in ('implementation', 'group', 'requires'):
220 return
221 elif new_parent.localName == 'interface':
222 if src.localName not in ('implementation', 'group'):
223 return
224 elif new_parent.localName == 'implementation':
225 if src.localName not in ['requires']:
226 return
227 else:
228 return
230 remove_element(src)
232 if position == gtk.TREE_VIEW_DROP_BEFORE:
233 insert_before(src, dest)
234 elif position == gtk.TREE_VIEW_DROP_AFTER:
235 next = dest.nextSibling
236 while next and not next.nodeType == Node.ELEMENT_NODE:
237 next = next.nextSibling
238 if next:
239 insert_before(src, next)
240 else:
241 insert_element(src, new_parent)
242 else:
243 insert_element(src, new_parent)
244 self.update_version_model()
246 def add_version(self):
247 ImplementationProperties(self)
249 def add_group(self):
250 ImplementationProperties(self, is_group = True)
252 def add_requires(self):
253 elem = self.get_selected()
254 if elem.namespaceURI == XMLNS_INTERFACE:
255 if elem.localName not in ('group', 'implementation'):
256 elem = elem.parentNode
257 if elem.localName in ('group', 'implementation'):
258 Requires(self, parent = elem)
259 return
260 rox.alert('Select a group, implementation or requirement!')
262 def edit_properties(self, path = None, element = None):
263 assert not (path and element)
265 if element:
266 pass
267 elif path is None:
268 element = self.get_selected()
269 else:
270 element = self.impl_model[path][1]
272 if element.namespaceURI != XMLNS_INTERFACE:
273 rox.alert("Sorry, I don't known how to edit %s elements!" % element.namespaceURI)
275 if element.localName in ('group', 'implementation'):
276 ImplementationProperties(self, element)
277 elif element.localName == 'requires':
278 Requires(self, parent = element.parentNode, element = element)
279 else:
280 rox.alert("Sorry, I don't known how to edit %s elements!" % element.localName)
282 def update_fields(self):
283 root = self.doc.documentElement
285 def set(name):
286 value = singleton_text(root, name)
287 if value:
288 self.wTree.get_widget('feed_' + name).set_text(value)
289 set('name')
290 set('summary')
291 set('homepage')
293 for icon in children(root, 'icon'):
294 if icon.getAttribute('type') == 'image/png':
295 href = icon.getAttribute('href')
296 self.wTree.get_widget('feed_icon').set_text(href)
297 break
299 description = singleton_text(root, 'description') or ''
300 paragraphs = [format_para(p) for p in description.split('\n\n')]
301 buffer = self.wTree.get_widget('feed_description').get_buffer()
302 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
303 buffer.insert_at_cursor('\n'.join(paragraphs))
305 key_menu = self.wTree.get_widget('feed_key')
306 model = key_menu.get_model()
307 if self.key:
308 i = 0
309 for line in model:
310 if line[0] == self.key:
311 break
312 i += 1
313 else:
314 model.append((self.key, 'Missing key (%s)' % self.key))
315 key_menu.set_active(i)
316 else:
317 key_menu.set_active(0)
319 self.update_version_model()
321 def add_archives(self, impl_element, iter):
322 for child in child_elements(impl_element):
323 if child.namespaceURI != XMLNS_INTERFACE: continue
324 if child.localName == 'archive':
325 self.impl_model.append(iter, ['Archive ' + child.getAttribute('href'), child])
326 elif child.localName == 'requires':
327 req_iface = child.getAttribute('interface')
328 self.impl_model.append(iter, ['Impl requires %s' % req_iface, child])
329 else:
330 self.impl_model.append(iter, ['<%s>' % child.localName, child])
332 def update_version_model(self):
333 self.impl_model.clear()
334 impl_tree = self.wTree.get_widget('impl_tree')
335 to_expand = []
337 def add_impls(elem, iter, attrs):
338 """Add all groups, implementations and requirements in elem"""
340 for x in child_elements(elem):
341 if x.namespaceURI != XMLNS_INTERFACE: continue
343 if x.localName == 'requires':
344 req_iface = x.getAttribute('interface')
345 new = self.impl_model.append(iter, ['Group requires %s' % req_iface, x])
347 if x.localName not in ('implementation', 'group'): continue
349 new_attrs = attrs.copy()
350 attributes = x.attributes
351 for i in range(attributes.length):
352 a = attributes.item(i)
353 new_attrs[str(a.name)] = a.value
355 if x.localName == 'implementation':
356 version = new_attrs.get('version', '(missing version number)')
357 new = self.impl_model.append(iter, ['Version %s' % version, x])
358 self.add_archives(x, new)
359 elif x.localName == 'group':
360 new = self.impl_model.append(iter, ['Group', x])
361 to_expand.append(self.impl_model.get_path(new))
362 add_impls(x, new, new_attrs)
364 add_impls(self.doc.documentElement, None, attrs = {})
366 for path in to_expand:
367 impl_tree.expand_row(path, False)
369 def test(self):
370 child = os.fork()
371 if child == 0:
372 try:
373 try:
374 # We are the child
375 # Spawn a grandchild and exit
376 subprocess.Popen(['0launch', '--gui', self.pathname])
377 os._exit(0)
378 except:
379 traceback.print_exc()
380 finally:
381 os._exit(1)
382 pid, status = os.waitpid(child, 0)
383 assert pid == child
384 if status:
385 raise Exception('Failed to run 0launch - status code %d' % status)
387 def update_doc(self):
388 root = self.doc.documentElement
389 def update(name, required = False, attrs = {}, value_attr = None):
390 widget = self.wTree.get_widget('feed_' + name)
391 if isinstance(widget, g.TextView):
392 buffer = widget.get_buffer()
393 text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
394 paras = ['\n'.join(textwrap.wrap(para, 80)) for para in text.split('\n') if para.strip()]
395 value = '\n' + '\n\n'.join(paras)
396 else:
397 value = widget.get_text()
398 elems = list(children(root, name, attrs = attrs))
399 if value:
400 if elems:
401 elem = elems[0]
402 else:
403 elem = create_element(root, name,
404 before = ['group', 'implementation', 'requires'])
405 for x in attrs:
406 elem.setAttribute(x, attrs[x])
407 if value_attr:
408 elem.setAttribute(value_attr, value)
409 set_data(elem, None)
410 else:
411 set_data(elem, value)
412 else:
413 if required:
414 raise Exception('Missing required field "%s"' % name)
415 for e in elems:
416 remove_element(e)
418 update('name', True)
419 update('summary', True)
420 update('description', True)
421 update('homepage')
422 update('icon', attrs = {'type': 'image/png'}, value_attr = 'href')
424 uri = self.wTree.get_widget('feed_url').get_text()
425 if uri:
426 root.setAttribute('uri', uri)
427 elif root.hasAttribute('uri'):
428 root.removeAttribute('uri')
430 key_menu = self.wTree.get_widget('feed_key')
431 key_model = key_menu.get_model()
432 self.key = key_model[key_menu.get_active()][0]
434 def save(self, callback = None):
435 self.update_doc()
436 if self.key:
437 sign = signing.sign_xml
438 else:
439 sign = signing.sign_unsigned
440 data = self.doc.toxml() + '\n'
442 gen = sign(self.pathname, data, self.key, callback)
443 # May require interaction to get the pass-phrase, so run in the background...
444 if gen:
445 tasks.Task(gen)
447 def add_archive(self):
448 archive.AddArchiveBox(self)
450 def xds_load_from_file(self, path):
451 archive.AddArchiveBox(self, local_archive = path)
453 def remove_version(self, path = None):
454 elem = self.get_selected()
455 remove_element(elem)
456 self.update_version_model()
458 def get_selected(self):
459 tree = self.wTree.get_widget('impl_tree')
460 sel = tree.get_selection()
461 model, iter = sel.get_selected()
462 if not iter:
463 raise Exception('Select something first!')
464 return model[iter][1]
466 def find_implementation(self, id):
467 def find_impl(parent):
468 for x in child_elements(parent):
469 if x.namespaceURI != XMLNS_INTERFACE: continue
470 if x.localName == 'group':
471 sub = find_impl(x)
472 if sub: return sub
473 elif x.localName == 'implementation':
474 if x.getAttribute('id') == id:
475 return x
476 return find_impl(self.doc.documentElement)