Added combo to set MIME type of archive.
[0publish-gui.git] / main.py
blob4e386607db8a6655a2cb247e57a4164f1d535ee6
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 default_name = os.path.basename(self.pathname)
143 if default_name.endswith('.xml'):
144 default_name = default_name[:-4]
145 self.wTree.get_widget('feed_name').set_text(default_name)
146 self.doc = minidom.parseString(emptyFeed)
147 self.key = None
148 key_menu.set_active(0)
150 root = self.impl_model.get_iter_root()
151 if root:
152 sel.select_iter(root)
154 self.wTree.get_widget('generate_key').connect('clicked', lambda b: self.generate_key())
156 self.wTree.get_widget('add_implementation').connect('clicked', lambda b: self.add_version())
157 self.wTree.get_widget('add_archive').connect('clicked', lambda b: self.add_archive())
158 self.wTree.get_widget('add_requires').connect('clicked', lambda b: self.add_requires())
159 self.wTree.get_widget('add_group').connect('clicked', lambda b: self.add_group())
160 self.wTree.get_widget('edit_properties').connect('clicked', lambda b: self.edit_properties())
161 self.wTree.get_widget('remove').connect('clicked', lambda b: self.remove_version())
162 impl_tree.connect('row-activated', lambda tv, path, col: self.edit_properties(path))
163 impl_tree.connect('drag-data-received', self.tree_drag_data_received)
165 def update_key_model(self):
166 key_menu = self.wTree.get_widget('feed_key')
167 key_model = key_menu.get_model()
168 keys = signing.get_secret_keys()
169 key_model.clear()
170 key_model.append((None, '(unsigned)'))
171 for k in keys:
172 key_model.append(k)
174 def generate_key(self):
175 for x in ['xterm', 'konsole']:
176 if available_in_path(x):
177 child = subprocess.Popen([x, '-e', 'gpg', '--gen-key'], stderr = subprocess.PIPE)
178 break
179 else:
180 child = subprocess.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr = subprocess.PIPE)
182 def get_keygen_out():
183 errors = ''
184 while True:
185 yield signing.InputBlocker(child.stderr)
186 data = os.read(child.stderr.fileno(), 100)
187 if not data:
188 break
189 errors += data
190 self.update_key_model()
191 if errors:
192 rox.alert('Errors from terminal: %s' % errors)
194 tasks.Task(get_keygen_out())
196 def tree_drag_data_received(self, treeview, context, x, y, selection, info, time):
197 if not selection: return
198 drop_info = treeview.get_dest_row_at_pos(x, y)
199 if drop_info:
200 model = treeview.get_model()
201 path, position = drop_info
203 src = self.get_selected()
204 dest = model[path][1]
206 def is_ancestor_or_self(a, b):
207 while b:
208 if b is a: return True
209 b = b.parentNode
210 return False
212 if is_ancestor_or_self(src, dest):
213 # Can't move an element into itself!
214 return
216 if position in (gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER):
217 new_parent = dest.parentNode
218 else:
219 new_parent = dest
221 if src.namespaceURI != XMLNS_INTERFACE: return
222 if new_parent.namespaceURI != XMLNS_INTERFACE: return
224 if new_parent.localName == 'group':
225 if src.localName not in ('implementation', 'group', 'requires'):
226 return
227 elif new_parent.localName == 'interface':
228 if src.localName not in ('implementation', 'group'):
229 return
230 elif new_parent.localName == 'implementation':
231 if src.localName not in ['requires']:
232 return
233 else:
234 return
236 remove_element(src)
238 if position == gtk.TREE_VIEW_DROP_BEFORE:
239 insert_before(src, dest)
240 elif position == gtk.TREE_VIEW_DROP_AFTER:
241 next = dest.nextSibling
242 while next and not next.nodeType == Node.ELEMENT_NODE:
243 next = next.nextSibling
244 if next:
245 insert_before(src, next)
246 else:
247 insert_element(src, new_parent)
248 else:
249 for next in child_elements(new_parent):
250 insert_before(src, next)
251 break
252 else:
253 insert_element(src, new_parent)
254 self.update_version_model()
256 def add_version(self):
257 ImplementationProperties(self)
259 def add_group(self):
260 ImplementationProperties(self, is_group = True)
262 def add_requires(self):
263 elem = self.get_selected()
264 if elem.namespaceURI == XMLNS_INTERFACE:
265 if elem.localName not in ('group', 'implementation'):
266 elem = elem.parentNode
267 if elem.localName in ('group', 'implementation'):
268 Requires(self, parent = elem)
269 return
270 rox.alert('Select a group or implementation!')
272 def edit_properties(self, path = None, element = None):
273 assert not (path and element)
275 if element:
276 pass
277 elif path is None:
278 element = self.get_selected()
279 else:
280 element = self.impl_model[path][1]
282 if element.namespaceURI != XMLNS_INTERFACE:
283 rox.alert("Sorry, I don't known how to edit %s elements!" % element.namespaceURI)
285 if element.localName in ('group', 'implementation'):
286 ImplementationProperties(self, element)
287 elif element.localName == 'requires':
288 Requires(self, parent = element.parentNode, element = element)
289 else:
290 rox.alert("Sorry, I don't known how to edit %s elements!" % element.localName)
292 def update_fields(self):
293 root = self.doc.documentElement
295 def set(name):
296 value = singleton_text(root, name)
297 if value:
298 self.wTree.get_widget('feed_' + name).set_text(value)
299 set('name')
300 set('summary')
301 set('homepage')
303 uri = root.getAttribute('uri')
304 if uri:
305 self.wTree.get_widget('feed_url').set_text(uri)
307 for icon in children(root, 'icon'):
308 if icon.getAttribute('type') == 'image/png':
309 href = icon.getAttribute('href')
310 self.wTree.get_widget('feed_icon').set_text(href)
311 break
313 description = singleton_text(root, 'description') or ''
314 paragraphs = [format_para(p) for p in description.split('\n\n')]
315 buffer = self.wTree.get_widget('feed_description').get_buffer()
316 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
317 buffer.insert_at_cursor('\n'.join(paragraphs))
319 key_menu = self.wTree.get_widget('feed_key')
320 model = key_menu.get_model()
321 if self.key:
322 i = 0
323 for line in model:
324 if line[0] == self.key:
325 break
326 i += 1
327 else:
328 model.append((self.key, 'Missing key (%s)' % self.key))
329 key_menu.set_active(i)
330 else:
331 key_menu.set_active(0)
333 self.update_version_model()
335 def add_archives(self, impl_element, iter):
336 for child in child_elements(impl_element):
337 if child.namespaceURI != XMLNS_INTERFACE: continue
338 if child.localName == 'archive':
339 self.impl_model.append(iter, ['Archive ' + child.getAttribute('href'), child])
340 elif child.localName == 'requires':
341 req_iface = child.getAttribute('interface')
342 self.impl_model.append(iter, ['Impl requires %s' % req_iface, child])
343 else:
344 self.impl_model.append(iter, ['<%s>' % child.localName, child])
346 def update_version_model(self):
347 impl_tree = self.wTree.get_widget('impl_tree')
349 # Remember which ones are open
350 expanded_elements = set()
351 impl_tree.map_expanded_rows(lambda tv, path: expanded_elements.add(self.impl_model[path][1]))
353 initial_build = not self.impl_model.get_iter_root()
355 self.impl_model.clear()
356 to_expand = []
358 def add_impls(elem, iter, attrs):
359 """Add all groups, implementations and requirements in elem"""
361 for x in child_elements(elem):
362 if x.namespaceURI != XMLNS_INTERFACE: continue
364 if x.localName == 'requires':
365 req_iface = x.getAttribute('interface')
366 new = self.impl_model.append(iter, ['Group requires %s' % req_iface, x])
368 if x.localName not in ('implementation', 'group'): continue
370 new_attrs = attrs.copy()
371 attributes = x.attributes
372 for i in range(attributes.length):
373 a = attributes.item(i)
374 new_attrs[str(a.name)] = a.value
376 if x.localName == 'implementation':
377 version = new_attrs.get('version', '(missing version number)') + \
378 (new_attrs.get('version-modifier') or '')
379 new = self.impl_model.append(iter, ['Version %s' % version, x])
380 self.add_archives(x, new)
381 elif x.localName == 'group':
382 new = self.impl_model.append(iter, ['Group', x])
383 if initial_build:
384 expanded_elements.add(x)
385 add_impls(x, new, new_attrs)
387 add_impls(self.doc.documentElement, None, attrs = {})
389 def may_expand(model, path, iter):
390 if model[iter][1] in expanded_elements:
391 impl_tree.expand_row(path, False)
392 self.impl_model.foreach(may_expand)
394 def test(self):
395 child = os.fork()
396 if child == 0:
397 try:
398 try:
399 # We are the child
400 # Spawn a grandchild and exit
401 subprocess.Popen(['0launch', '--gui', self.pathname])
402 os._exit(0)
403 except:
404 traceback.print_exc()
405 finally:
406 os._exit(1)
407 pid, status = os.waitpid(child, 0)
408 assert pid == child
409 if status:
410 raise Exception('Failed to run 0launch - status code %d' % status)
412 def update_doc(self):
413 root = self.doc.documentElement
414 def update(name, required = False, attrs = {}, value_attr = None):
415 widget = self.wTree.get_widget('feed_' + name)
416 if isinstance(widget, g.TextView):
417 buffer = widget.get_buffer()
418 text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
419 paras = ['\n'.join(textwrap.wrap(para, 80)) for para in text.split('\n') if para.strip()]
420 value = '\n' + '\n\n'.join(paras)
421 else:
422 value = widget.get_text()
423 elems = list(children(root, name, attrs = attrs))
424 if value:
425 if elems:
426 elem = elems[0]
427 else:
428 elem = create_element(root, name,
429 before = ['group', 'implementation', 'requires'])
430 for x in attrs:
431 elem.setAttribute(x, attrs[x])
432 if value_attr:
433 elem.setAttribute(value_attr, value)
434 set_data(elem, None)
435 else:
436 set_data(elem, value)
437 else:
438 if required:
439 raise Exception('Missing required field "%s"' % name)
440 for e in elems:
441 remove_element(e)
443 update('name', True)
444 update('summary', True)
445 update('description', True)
446 update('homepage')
447 update('icon', attrs = {'type': 'image/png'}, value_attr = 'href')
449 uri = self.wTree.get_widget('feed_url').get_text()
450 if uri:
451 root.setAttribute('uri', uri)
452 elif root.hasAttribute('uri'):
453 root.removeAttribute('uri')
455 key_menu = self.wTree.get_widget('feed_key')
456 key_model = key_menu.get_model()
457 self.key = key_model[key_menu.get_active()][0]
459 def export_stylesheet_and_key(self):
460 dir = os.path.dirname(self.pathname)
461 stylesheet = os.path.join(dir, 'interface.xsl')
462 if not os.path.exists(stylesheet):
463 shutil.copyfile(stylesheet_src, stylesheet)
464 rox.info("I have saved a stylesheet as '%s'. You should upload "
465 "this to your web-server in the same directory as the feed file. "
466 "This allows browsers to display the feed nicely." % stylesheet)
468 exported = signing.export_key(dir, self.key)
469 if exported:
470 rox.info("I have exported your public key as '%s'. You should upload "
471 "this to your web-server in the same directory as the feed file. "
472 "This allows people to check the signature on your feed." % exported)
474 def save(self, callback = None):
475 self.update_doc()
476 if self.key:
477 sign = signing.sign_xml
478 self.export_stylesheet_and_key()
479 else:
480 sign = signing.sign_unsigned
481 data = self.doc.toxml() + '\n'
483 gen = sign(self.pathname, data, self.key, callback)
484 # May require interaction to get the pass-phrase, so run in the background...
485 if gen:
486 tasks.Task(gen)
488 def add_archive(self):
489 archive.AddArchiveBox(self)
491 def xds_load_from_file(self, path):
492 archive.AddArchiveBox(self, local_archive = path)
494 def remove_version(self, path = None):
495 elem = self.get_selected()
496 remove_element(elem)
497 self.update_version_model()
499 def get_selected(self):
500 tree = self.wTree.get_widget('impl_tree')
501 sel = tree.get_selection()
502 model, iter = sel.get_selected()
503 if not iter:
504 raise Exception('Select something first!')
505 return model[iter][1]
507 def find_implementation(self, id):
508 def find_impl(parent):
509 for x in child_elements(parent):
510 if x.namespaceURI != XMLNS_INTERFACE: continue
511 if x.localName == 'group':
512 sub = find_impl(x)
513 if sub: return sub
514 elif x.localName == 'implementation':
515 if x.getAttribute('id') == id:
516 return x
517 return find_impl(self.doc.documentElement)