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
9 from implementation
import ImplementationProperties
10 from requires
import Requires
11 from xmltools
import *
13 from zeroinstall
.zerostore
import unpack
, Stores
16 RESPONSE_SAVE_AND_TEST
= 1
18 gladefile
= os
.path
.join(rox
.app_dir
, '0publish-gui.glade')
20 # Zero Install implementation cache
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
):
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()
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
)
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
)
49 chooser
.set_default_response(g
.RESPONSE_OK
)
50 if chooser
.run() != g
.RESPONSE_OK
:
52 path
= chooser
.get_filename()
54 return FeedEditor(path
)
56 def combo_set_text(combo
, text
):
57 if combo
.get_active_text() or text
:
58 model
= combo
.get_model()
65 combo
.append_text(text
)
70 emptyFeed
= """<?xml version='1.0'?>
71 <interface xmlns="%s">
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())
97 if resp
== g
.RESPONSE_HELP
:
99 elif resp
== RESPONSE_SAVE_AND_TEST
:
101 elif resp
== RESPONSE_SAVE
:
105 self
.window
.connect('response', resp
)
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
)
137 # Default to showing the versions tab
138 self
.wTree
.get_widget('notebook').next_page()
140 self
.doc
= minidom
.parseString(emptyFeed
)
142 key_menu
.set_active(0)
144 root
= self
.impl_model
.get_iter_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()
164 key_model
.append((None, '(unsigned)'))
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
)
174 child
= subprocess
.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr
= subprocess
.PIPE
)
176 def get_keygen_out():
179 yield signing
.InputBlocker(child
.stderr
)
180 data
= os
.read(child
.stderr
.fileno(), 100)
184 self
.update_key_model()
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
)
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
):
202 if b
is a
: return True
206 if is_ancestor_or_self(src
, dest
):
207 # Can't move an element into itself!
210 if position
in (gtk
.TREE_VIEW_DROP_BEFORE
, gtk
.TREE_VIEW_DROP_AFTER
):
211 new_parent
= dest
.parentNode
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'):
221 elif new_parent
.localName
== 'interface':
222 if src
.localName
not in ('implementation', 'group'):
224 elif new_parent
.localName
== 'implementation':
225 if src
.localName
not in ['requires']:
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
239 insert_before(src
, next
)
241 insert_element(src
, new_parent
)
243 insert_element(src
, new_parent
)
244 self
.update_version_model()
246 def add_version(self
):
247 ImplementationProperties(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
)
260 rox
.alert('Select a group, implementation or requirement!')
262 def edit_properties(self
, path
= None, element
= None):
263 assert not (path
and element
)
268 element
= self
.get_selected()
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
)
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
286 value
= singleton_text(root
, name
)
288 self
.wTree
.get_widget('feed_' + name
).set_text(value
)
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
)
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()
310 if line
[0] == self
.key
:
314 model
.append((self
.key
, 'Missing key (%s)' % self
.key
))
315 key_menu
.set_active(i
)
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
])
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')
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)
375 # Spawn a grandchild and exit
376 subprocess
.Popen(['0launch', '--gui', self
.pathname
])
379 traceback
.print_exc()
382 pid
, status
= os
.waitpid(child
, 0)
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
)
397 value
= widget
.get_text()
398 elems
= list(children(root
, name
, attrs
= attrs
))
403 elem
= create_element(root
, name
,
404 before
= ['group', 'implementation', 'requires'])
406 elem
.setAttribute(x
, attrs
[x
])
408 elem
.setAttribute(value_attr
, value
)
411 set_data(elem
, value
)
414 raise Exception('Missing required field "%s"' % name
)
419 update('summary', True)
420 update('description', True)
422 update('icon', attrs
= {'type': 'image/png'}, value_attr
= 'href')
424 uri
= self
.wTree
.get_widget('feed_url').get_text()
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):
437 sign
= signing
.sign_xml
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...
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()
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()
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':
473 elif x
.localName
== 'implementation':
474 if x
.getAttribute('id') == id:
476 return find_impl(self
.doc
.documentElement
)