More help text.
[0publish-gui.git] / main.py
blob32521fc1fa250e4ab841a8c283f4f504780c536d
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 emptyFeed = """<?xml version='1.0'?>
73 <interface xmlns="%s">
74 <name>Name</name>
75 </interface>
76 """ % (XMLNS_INTERFACE)
78 element_target = ('INTERNAL:FeedEditor/Element', gtk.TARGET_SAME_WIDGET, 0)
80 class FeedEditor(loading.XDSLoader):
81 def __init__(self, pathname):
82 loading.XDSLoader.__init__(self, None)
84 self.pathname = pathname
86 self.wTree = gtk.glade.XML(gladefile, 'main')
87 self.window = self.wTree.get_widget('main')
88 self.window.connect('destroy', rox.toplevel_unref)
89 self.xds_proxy_for(self.window)
91 help = gtk.glade.XML(gladefile, 'main_help')
92 help_box = help.get_widget('main_help')
93 help_box.set_default_size(g.gdk.screen_width() / 4,
94 g.gdk.screen_height() / 4)
95 help_box.connect('delete-event', lambda box, ev: True)
96 help_box.connect('response', lambda box, resp: box.hide())
98 def resp(box, resp):
99 if resp == g.RESPONSE_HELP:
100 help_box.present()
101 elif resp == RESPONSE_SAVE_AND_TEST:
102 self.save(self.test)
103 elif resp == RESPONSE_SAVE:
104 self.save()
105 else:
106 box.destroy()
107 self.window.connect('response', resp)
108 rox.toplevel_ref()
110 key_menu = self.wTree.get_widget('feed_key')
111 key_model = g.ListStore(str, str)
112 key_menu.set_model(key_model)
113 cell = g.CellRendererText()
114 cell.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
115 key_menu.pack_start(cell)
116 key_menu.add_attribute(cell, 'text', 1)
118 self.update_key_model()
120 self.impl_model = g.TreeStore(str, object)
121 impl_tree = self.wTree.get_widget('impl_tree')
122 impl_tree.set_model(self.impl_model)
123 text = g.CellRendererText()
124 column = g.TreeViewColumn('', text)
125 column.add_attribute(text, 'text', 0)
126 impl_tree.append_column(column)
128 impl_tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [element_target], gtk.gdk.ACTION_MOVE)
129 impl_tree.enable_model_drag_dest([element_target], gtk.gdk.ACTION_MOVE)
131 sel = impl_tree.get_selection()
132 sel.set_mode(g.SELECTION_BROWSE)
134 if os.path.exists(self.pathname):
135 data, _, self.key = signing.check_signature(self.pathname)
136 self.doc = minidom.parseString(data)
137 self.update_fields()
139 # Default to showing the versions tab
140 self.wTree.get_widget('notebook').next_page()
141 else:
142 self.doc = minidom.parseString(emptyFeed)
143 self.key = None
144 key_menu.set_active(0)
146 root = self.impl_model.get_iter_root()
147 if root:
148 sel.select_iter(root)
150 self.wTree.get_widget('generate_key').connect('clicked', lambda b: self.generate_key())
152 self.wTree.get_widget('add_implementation').connect('clicked', lambda b: self.add_version())
153 self.wTree.get_widget('add_archive').connect('clicked', lambda b: self.add_archive())
154 self.wTree.get_widget('add_requires').connect('clicked', lambda b: self.add_requires())
155 self.wTree.get_widget('add_group').connect('clicked', lambda b: self.add_group())
156 self.wTree.get_widget('edit_properties').connect('clicked', lambda b: self.edit_properties())
157 self.wTree.get_widget('remove').connect('clicked', lambda b: self.remove_version())
158 impl_tree.connect('row-activated', lambda tv, path, col: self.edit_properties(path))
159 impl_tree.connect('drag-data-received', self.tree_drag_data_received)
161 def update_key_model(self):
162 key_menu = self.wTree.get_widget('feed_key')
163 key_model = key_menu.get_model()
164 keys = signing.get_secret_keys()
165 key_model.clear()
166 key_model.append((None, '(unsigned)'))
167 for k in keys:
168 key_model.append(k)
170 def generate_key(self):
171 for x in ['xterm', 'konsole']:
172 if available_in_path(x):
173 child = subprocess.Popen([x, '-e', 'gpg', '--gen-key'], stderr = subprocess.PIPE)
174 break
175 else:
176 child = subprocess.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr = subprocess.PIPE)
178 def get_keygen_out():
179 errors = ''
180 while True:
181 yield signing.InputBlocker(child.stderr)
182 data = os.read(child.stderr.fileno(), 100)
183 if not data:
184 break
185 errors += data
186 self.update_key_model()
187 if errors:
188 rox.alert('Errors from terminal: %s' % errors)
190 tasks.Task(get_keygen_out())
192 def tree_drag_data_received(self, treeview, context, x, y, selection, info, time):
193 if not selection: return
194 drop_info = treeview.get_dest_row_at_pos(x, y)
195 if drop_info:
196 model = treeview.get_model()
197 path, position = drop_info
199 src = self.get_selected()
200 dest = model[path][1]
202 def is_ancestor_or_self(a, b):
203 while b:
204 if b is a: return True
205 b = b.parentNode
206 return False
208 if is_ancestor_or_self(src, dest):
209 # Can't move an element into itself!
210 return
212 if position in (gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER):
213 new_parent = dest.parentNode
214 else:
215 new_parent = dest
217 if src.namespaceURI != XMLNS_INTERFACE: return
218 if new_parent.namespaceURI != XMLNS_INTERFACE: return
220 if new_parent.localName == 'group':
221 if src.localName not in ('implementation', 'group', 'requires'):
222 return
223 elif new_parent.localName == 'interface':
224 if src.localName not in ('implementation', 'group'):
225 return
226 elif new_parent.localName == 'implementation':
227 if src.localName not in ['requires']:
228 return
229 else:
230 return
232 remove_element(src)
234 if position == gtk.TREE_VIEW_DROP_BEFORE:
235 insert_before(src, dest)
236 elif position == gtk.TREE_VIEW_DROP_AFTER:
237 next = dest.nextSibling
238 while next and not next.nodeType == Node.ELEMENT_NODE:
239 next = next.nextSibling
240 if next:
241 insert_before(src, next)
242 else:
243 insert_element(src, new_parent)
244 else:
245 insert_element(src, new_parent)
246 self.update_version_model()
248 def add_version(self):
249 ImplementationProperties(self)
251 def add_group(self):
252 ImplementationProperties(self, is_group = True)
254 def add_requires(self):
255 elem = self.get_selected()
256 if elem.namespaceURI == XMLNS_INTERFACE:
257 if elem.localName not in ('group', 'implementation'):
258 elem = elem.parentNode
259 if elem.localName in ('group', 'implementation'):
260 Requires(self, parent = elem)
261 return
262 rox.alert('Select a group, implementation or requirement!')
264 def edit_properties(self, path = None, element = None):
265 assert not (path and element)
267 if element:
268 pass
269 elif path is None:
270 element = self.get_selected()
271 else:
272 element = self.impl_model[path][1]
274 if element.namespaceURI != XMLNS_INTERFACE:
275 rox.alert("Sorry, I don't known how to edit %s elements!" % element.namespaceURI)
277 if element.localName in ('group', 'implementation'):
278 ImplementationProperties(self, element)
279 elif element.localName == 'requires':
280 Requires(self, parent = element.parentNode, element = element)
281 else:
282 rox.alert("Sorry, I don't known how to edit %s elements!" % element.localName)
284 def update_fields(self):
285 root = self.doc.documentElement
287 def set(name):
288 value = singleton_text(root, name)
289 if value:
290 self.wTree.get_widget('feed_' + name).set_text(value)
291 set('name')
292 set('summary')
293 set('homepage')
295 for icon in children(root, 'icon'):
296 if icon.getAttribute('type') == 'image/png':
297 href = icon.getAttribute('href')
298 self.wTree.get_widget('feed_icon').set_text(href)
299 break
301 description = singleton_text(root, 'description') or ''
302 paragraphs = [format_para(p) for p in description.split('\n\n')]
303 buffer = self.wTree.get_widget('feed_description').get_buffer()
304 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
305 buffer.insert_at_cursor('\n'.join(paragraphs))
307 key_menu = self.wTree.get_widget('feed_key')
308 model = key_menu.get_model()
309 if self.key:
310 i = 0
311 for line in model:
312 if line[0] == self.key:
313 break
314 i += 1
315 else:
316 model.append((self.key, 'Missing key (%s)' % self.key))
317 key_menu.set_active(i)
318 else:
319 key_menu.set_active(0)
321 self.update_version_model()
323 def add_archives(self, impl_element, iter):
324 for child in child_elements(impl_element):
325 if child.namespaceURI != XMLNS_INTERFACE: continue
326 if child.localName == 'archive':
327 self.impl_model.append(iter, ['Archive ' + child.getAttribute('href'), child])
328 elif child.localName == 'requires':
329 req_iface = child.getAttribute('interface')
330 self.impl_model.append(iter, ['Impl requires %s' % req_iface, child])
331 else:
332 self.impl_model.append(iter, ['<%s>' % child.localName, child])
334 def update_version_model(self):
335 self.impl_model.clear()
336 impl_tree = self.wTree.get_widget('impl_tree')
337 to_expand = []
339 def add_impls(elem, iter, attrs):
340 """Add all groups, implementations and requirements in elem"""
342 for x in child_elements(elem):
343 if x.namespaceURI != XMLNS_INTERFACE: continue
345 if x.localName == 'requires':
346 req_iface = x.getAttribute('interface')
347 new = self.impl_model.append(iter, ['Group requires %s' % req_iface, x])
349 if x.localName not in ('implementation', 'group'): continue
351 new_attrs = attrs.copy()
352 attributes = x.attributes
353 for i in range(attributes.length):
354 a = attributes.item(i)
355 new_attrs[str(a.name)] = a.value
357 if x.localName == 'implementation':
358 version = new_attrs.get('version', '(missing version number)') + \
359 (new_attrs.get('version-modifier') or '')
360 new = self.impl_model.append(iter, ['Version %s' % version, x])
361 self.add_archives(x, new)
362 elif x.localName == 'group':
363 new = self.impl_model.append(iter, ['Group', x])
364 to_expand.append(self.impl_model.get_path(new))
365 add_impls(x, new, new_attrs)
367 add_impls(self.doc.documentElement, None, attrs = {})
369 for path in to_expand:
370 impl_tree.expand_row(path, False)
372 def test(self):
373 child = os.fork()
374 if child == 0:
375 try:
376 try:
377 # We are the child
378 # Spawn a grandchild and exit
379 subprocess.Popen(['0launch', '--gui', self.pathname])
380 os._exit(0)
381 except:
382 traceback.print_exc()
383 finally:
384 os._exit(1)
385 pid, status = os.waitpid(child, 0)
386 assert pid == child
387 if status:
388 raise Exception('Failed to run 0launch - status code %d' % status)
390 def update_doc(self):
391 root = self.doc.documentElement
392 def update(name, required = False, attrs = {}, value_attr = None):
393 widget = self.wTree.get_widget('feed_' + name)
394 if isinstance(widget, g.TextView):
395 buffer = widget.get_buffer()
396 text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
397 paras = ['\n'.join(textwrap.wrap(para, 80)) for para in text.split('\n') if para.strip()]
398 value = '\n' + '\n\n'.join(paras)
399 else:
400 value = widget.get_text()
401 elems = list(children(root, name, attrs = attrs))
402 if value:
403 if elems:
404 elem = elems[0]
405 else:
406 elem = create_element(root, name,
407 before = ['group', 'implementation', 'requires'])
408 for x in attrs:
409 elem.setAttribute(x, attrs[x])
410 if value_attr:
411 elem.setAttribute(value_attr, value)
412 set_data(elem, None)
413 else:
414 set_data(elem, value)
415 else:
416 if required:
417 raise Exception('Missing required field "%s"' % name)
418 for e in elems:
419 remove_element(e)
421 update('name', True)
422 update('summary', True)
423 update('description', True)
424 update('homepage')
425 update('icon', attrs = {'type': 'image/png'}, value_attr = 'href')
427 uri = self.wTree.get_widget('feed_url').get_text()
428 if uri:
429 root.setAttribute('uri', uri)
430 elif root.hasAttribute('uri'):
431 root.removeAttribute('uri')
433 key_menu = self.wTree.get_widget('feed_key')
434 key_model = key_menu.get_model()
435 self.key = key_model[key_menu.get_active()][0]
437 def export_stylesheet_and_key(self):
438 dir = os.path.dirname(self.pathname)
439 stylesheet = os.path.join(dir, 'interface.xsl')
440 if not os.path.exists(stylesheet):
441 shutil.copyfile(stylesheet_src, stylesheet)
442 rox.info("I have saved a stylesheet as '%s'. You should upload "
443 "this to your web-server in the same directory as the feed file. "
444 "This allows browsers to display the feed nicely." % stylesheet)
446 exported = signing.export_key(dir, self.key)
447 if exported:
448 rox.info("I have exported your public key as '%s'. You should upload "
449 "this to your web-server in the same directory as the feed file. "
450 "This allows people to check the signature on your feed." % exported)
452 def save(self, callback = None):
453 self.update_doc()
454 if self.key:
455 sign = signing.sign_xml
456 self.export_stylesheet_and_key()
457 else:
458 sign = signing.sign_unsigned
459 data = self.doc.toxml() + '\n'
461 gen = sign(self.pathname, data, self.key, callback)
462 # May require interaction to get the pass-phrase, so run in the background...
463 if gen:
464 tasks.Task(gen)
466 def add_archive(self):
467 archive.AddArchiveBox(self)
469 def xds_load_from_file(self, path):
470 archive.AddArchiveBox(self, local_archive = path)
472 def remove_version(self, path = None):
473 elem = self.get_selected()
474 remove_element(elem)
475 self.update_version_model()
477 def get_selected(self):
478 tree = self.wTree.get_widget('impl_tree')
479 sel = tree.get_selection()
480 model, iter = sel.get_selected()
481 if not iter:
482 raise Exception('Select something first!')
483 return model[iter][1]
485 def find_implementation(self, id):
486 def find_impl(parent):
487 for x in child_elements(parent):
488 if x.namespaceURI != XMLNS_INTERFACE: continue
489 if x.localName == 'group':
490 sub = find_impl(x)
491 if sub: return sub
492 elif x.localName == 'implementation':
493 if x.getAttribute('id') == id:
494 return x
495 return find_impl(self.doc.documentElement)