3 # Copyright (C) 2006-2008 Async Open Source
4 # Henrique Romano <henrique@async.com.br>
5 # Johan Dahlin <jdahlin@async.com.br>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 """Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
25 Converts Glade files into XML files which can be loaded with GtkBuilder.
28 -w, --skip-windows Convert everything but GtkWindow subclasses.
29 -r, --root Convert only widget named root and its children
30 -h, --help display this help and exit
32 When OUTPUT is -, write to standard input.
35 gtk-builder-convert preference.glade preferences.ui
37 Report bugs to http://bugzilla.gnome.org/."""
43 from xml.dom import minidom, Node
45 WINDOWS = ['GtkWindow',
47 'GtkFileChooserDialog',
50 # The subprocess is only available in Python 2.4+
57 def get_child_nodes(node):
58 assert node.tagName == 'object'
60 for child in node.childNodes:
61 if child.nodeType != Node.ELEMENT_NODE:
63 if child.tagName != 'child':
68 def get_properties(node):
69 assert node.tagName == 'object'
71 for child in node.childNodes:
72 if child.nodeType != Node.ELEMENT_NODE:
74 if child.tagName != 'property':
76 value = child.childNodes[0].data
77 properties[child.getAttribute('name')] = value
80 def get_property(node, property_name):
81 assert node.tagName == 'object'
82 properties = get_properties(node)
83 return properties.get(property_name)
85 def get_property_node(node, property_name):
86 assert node.tagName == 'object'
88 for child in node.childNodes:
89 if child.nodeType != Node.ELEMENT_NODE:
91 if child.tagName != 'property':
93 if child.getAttribute('name') == property_name:
96 def get_signal_nodes(node):
97 assert node.tagName == 'object'
99 for child in node.childNodes:
100 if child.nodeType != Node.ELEMENT_NODE:
102 if child.tagName == 'signal':
103 signals.append(child)
106 def get_property_nodes(node):
107 assert node.tagName == 'object'
109 for child in node.childNodes:
110 if child.nodeType != Node.ELEMENT_NODE:
112 # FIXME: handle comments
113 if child.tagName == 'property':
114 properties.append(child)
117 def get_accelerator_nodes(node):
118 assert node.tagName == 'object'
120 for child in node.childNodes:
121 if child.nodeType != Node.ELEMENT_NODE:
123 if child.tagName == 'accelerator':
124 accelerators.append(child)
127 def get_object_node(child_node):
128 assert child_node.tagName == 'child', child_node
130 for node in child_node.childNodes:
131 if node.nodeType != Node.ELEMENT_NODE:
133 if node.tagName == 'object':
135 assert len(nodes) == 1, nodes
138 def copy_properties(node, props, prop_dict):
139 assert node.tagName == 'object'
140 for prop_name in props:
141 child = get_property_node(node, prop_name)
142 if child is not None:
143 prop_dict[prop_name] = child
147 class GtkBuilderConverter(object):
149 def __init__(self, skip_windows, root):
150 self.skip_windows = skip_windows
152 self.root_objects = []
159 def parse_file(self, file):
160 self._dom = minidom.parse(file)
163 def parse_buffer(self, buffer):
164 self._dom = minidom.parseString(buffer)
168 xml = self._dom.toprettyxml("", "")
169 return xml.encode('utf-8')
175 def _get_object(self, name):
176 return self.objects.get(name)
178 def _get_objects_by_attr(self, attribute, value):
179 return [w for w in self._dom.getElementsByTagName("object")
180 if w.getAttribute(attribute) == value]
182 def _create_object(self, obj_class, obj_id, template=None, properties=None):
184 Creates a new <object> tag.
185 Optionally a name template can be provided which will be used
186 to avoid naming collisions.
187 The properties dictionary can either contain string values or Node
188 values. If a node is provided the name of the node will be overridden
189 by the dictionary key.
191 @param obj_class: class of the object (class tag)
192 @param obj_id: identifier of the object (id tag)
193 @param template: name template to use, for example 'button'
194 @param properties: dictionary of properties
195 @type properties: string or Node.
196 @returns: Newly created node of the object
198 if template is not None:
201 obj_id = template + str(count)
202 widget = self._get_object(obj_id)
208 obj = self._dom.createElement('object')
209 obj.setAttribute('class', obj_class)
210 obj.setAttribute('id', obj_id)
212 for name, value in properties.items():
213 if isinstance(value, Node):
214 # Reuse the node, so translatable and context still will be
215 # set when converting nodes. See also #509153
218 prop = self._dom.createElement('property')
219 prop.appendChild(self._dom.createTextNode(value))
221 prop.setAttribute('name', str(name))
222 obj.appendChild(prop)
223 self.objects[obj_id] = obj
226 def _create_root_object(self, obj_class, template, properties=None):
227 obj = self._create_object(obj_class, None, template, properties)
228 self.root_objects.append(obj)
232 glade_iface = self._dom.getElementsByTagName("glade-interface")
233 assert glade_iface, ("Badly formed XML, there is "
234 "no <glade-interface> tag.")
235 # Rename glade-interface to interface
236 glade_iface[0].tagName = 'interface'
237 self._interface = glade_iface[0]
239 # Remove glade-interface doc type
240 for node in self._dom.childNodes:
241 if node.nodeType == Node.DOCUMENT_TYPE_NODE:
242 if node.name == 'glade-interface':
243 self._dom.removeChild(node)
245 # Strip unsupported tags
246 for tag in ['requires', 'requires-version']:
247 for child in self._dom.getElementsByTagName(tag):
248 child.parentNode.removeChild(child)
251 self._strip_root(self.root)
253 # Rename widget to object
254 objects = self._dom.getElementsByTagName("widget")
256 node.tagName = "object"
259 self._convert(node.getAttribute("class"), node)
260 self.objects[node.getAttribute('id')] = node
262 # Convert Gazpachos UI tag
263 for node in self._dom.getElementsByTagName("ui"):
264 self._convert_ui(node)
266 # Convert accessibility tag
267 for node in self._dom.getElementsByTagName("accessibility"):
268 self._convert_accessibility(node)
270 # Output the newly created root objects and sort them
272 # FIXME: Use sorted(self.root_objects,
273 # key=lambda n: n.getAttribute('id'),
275 # when we can depend on python 2.4 or higher
276 root_objects = self.root_objects[:]
277 root_objects.sort(lambda a, b: cmp(b.getAttribute('id'),
278 a.getAttribute('id')))
279 for obj in root_objects:
280 self._interface.childNodes.insert(0, obj)
282 def _convert(self, klass, node):
283 if klass == 'GtkNotebook':
284 self._packing_prop_to_child_attr(node, "type", "tab")
285 elif klass in ['GtkExpander', 'GtkFrame']:
286 self._packing_prop_to_child_attr(
287 node, "type", "label_item", "label")
288 elif klass == "GtkMenuBar":
289 self._convert_menu(node)
290 elif klass == "GtkMenu":
291 # Only convert toplevel popups
292 if node.parentNode == self._interface:
293 self._convert_menu(node, popup=True)
294 elif klass in WINDOWS and self.skip_windows:
295 self._remove_window(node)
296 self._default_widget_converter(node)
298 def _default_widget_converter(self, node):
299 klass = node.getAttribute("class")
300 for prop in get_property_nodes(node):
301 prop_name = prop.getAttribute("name")
302 if prop_name == "sizegroup":
303 self._convert_sizegroup(node, prop)
304 elif prop_name == "tooltip" and klass != "GtkAction":
305 prop.setAttribute("name", "tooltip-text")
306 elif prop_name in ["response_id", 'response-id']:
307 # It does not make sense to convert responses when
308 # we're not going to output dialogs
309 if self.skip_windows:
311 object_id = node.getAttribute('id')
312 response = prop.childNodes[0].data
313 self._convert_dialog_response(node, object_id, response)
314 prop.parentNode.removeChild(prop)
315 elif prop_name == "adjustment":
316 self._convert_adjustment(prop)
317 elif prop_name == "items" and klass in ['GtkComboBox',
319 self._convert_combobox_items(node, prop)
320 elif prop_name == "text" and klass == 'GtkTextView':
321 self._convert_textview_text(prop)
323 def _remove_window(self, node):
324 object_node = get_object_node(get_child_nodes(node)[0])
325 parent = node.parentNode
326 parent.removeChild(node)
327 parent.appendChild(object_node)
329 def _convert_menu(self, node, popup=False):
330 if node.hasAttribute('constructor'):
333 uimgr = self._create_root_object('GtkUIManager',
334 template='uimanager')
341 menu = self._dom.createElement(name)
342 menu.setAttribute('name', node.getAttribute('id'))
343 node.setAttribute('constructor', uimgr.getAttribute('id'))
345 for child in get_child_nodes(node):
346 obj_node = get_object_node(child)
347 item = self._convert_menuitem(uimgr, obj_node)
348 menu.appendChild(item)
349 child.removeChild(obj_node)
350 child.parentNode.removeChild(child)
352 ui = self._dom.createElement('ui')
353 uimgr.appendChild(ui)
357 def _convert_menuitem(self, uimgr, obj_node):
358 children = get_child_nodes(obj_node)
361 child_node = children[0]
362 menu_node = get_object_node(child_node)
363 # Can be GtkImage, which will take care of later.
364 if menu_node.getAttribute('class') == 'GtkMenu':
367 object_class = obj_node.getAttribute('class')
368 if object_class in ['GtkMenuItem',
372 menu = self._dom.createElement(name)
373 elif object_class == 'GtkSeparatorMenuItem':
374 return self._dom.createElement('separator')
376 raise NotImplementedError(object_class)
378 menu.setAttribute('action', obj_node.getAttribute('id'))
379 self._add_action_from_menuitem(uimgr, obj_node)
381 for child in get_child_nodes(menu_node):
382 obj_node = get_object_node(child)
383 item = self._convert_menuitem(uimgr, obj_node)
384 menu.appendChild(item)
385 child.removeChild(obj_node)
386 child.parentNode.removeChild(child)
389 def _menuitem_to_action(self, node, properties):
390 copy_properties(node, ['label', 'tooltip'], properties)
392 def _togglemenuitem_to_action(self, node, properties):
393 self._menuitem_to_action(node, properties)
394 copy_properties(node, ['active'], properties)
396 def _radiomenuitem_to_action(self, node, properties):
397 self._togglemenuitem_to_action(node, properties)
398 copy_properties(node, ['group'], properties)
400 def _add_action_from_menuitem(self, uimgr, node):
402 object_class = node.getAttribute('class')
403 object_id = node.getAttribute('id')
404 if object_class == 'GtkMenuItem':
406 self._menuitem_to_action(node, properties)
407 elif object_class == 'GtkCheckMenuItem':
408 name = 'GtkToggleAction'
409 self._togglemenuitem_to_action(node, properties)
410 elif object_class == 'GtkRadioMenuItem':
411 name = 'GtkRadioAction'
412 self._radiomenuitem_to_action(node, properties)
413 elif object_class == 'GtkImageMenuItem':
415 children = get_child_nodes(node)
417 children[0].getAttribute('internal-child') == 'image'):
418 image = get_object_node(children[0])
419 child = get_property_node(image, 'stock')
420 if child is not None:
421 properties['stock_id'] = child
422 self._menuitem_to_action(node, properties)
423 elif object_class == 'GtkSeparatorMenuItem':
426 raise NotImplementedError(object_class)
428 if get_property(node, 'use_stock') == 'True':
429 if 'label' in properties:
430 properties['stock_id'] = properties['label']
431 del properties['label']
433 properties['name'] = object_id
434 action = self._create_object(name,
436 properties=properties)
437 for signal in get_signal_nodes(node):
438 signal_name = signal.getAttribute('name')
439 if signal_name in ['activate', 'toggled']:
440 action.appendChild(signal)
442 print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
445 if not uimgr.childNodes:
446 child = self._dom.createElement('child')
447 uimgr.appendChild(child)
449 group = self._create_object('GtkActionGroup', None,
450 template='actiongroup')
451 child.appendChild(group)
453 group = uimgr.childNodes[0].childNodes[0]
455 child = self._dom.createElement('child')
456 group.appendChild(child)
457 child.appendChild(action)
459 for accelerator in get_accelerator_nodes(node):
460 signal_name = accelerator.getAttribute('signal')
461 if signal_name != 'activate':
462 print 'Unhandled accelerator signal for %s::%s' % (
463 node.getAttribute('class'), signal_name)
465 accelerator.removeAttribute('signal')
466 child.appendChild(accelerator)
468 def _convert_sizegroup(self, node, prop):
469 # This is Gazpacho only
470 node.removeChild(prop)
471 obj = self._get_object(prop.childNodes[0].data)
473 widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
477 obj = self._create_root_object('GtkSizeGroup',
478 template='sizegroup')
480 widgets = obj.getElementsByTagName("widgets")
482 assert len(widgets) == 1
485 widgets = self._dom.createElement("widgets")
486 obj.appendChild(widgets)
488 member = self._dom.createElement("widget")
489 member.setAttribute("name", node.getAttribute("id"))
490 widgets.appendChild(member)
492 def _convert_dialog_response(self, node, object_name, response):
493 # 1) Get parent dialog node
495 # If we can't find the parent dialog, give up
496 if node == self._dom:
499 if (node.tagName == 'object' and
500 node.getAttribute('class') == 'GtkDialog'):
503 node = node.parentNode
506 # 2) Get dialogs action-widgets tag, create if not found
507 for child in dialog.childNodes:
508 if child.nodeType != Node.ELEMENT_NODE:
510 if child.tagName == 'action-widgets':
514 actions = self._dom.createElement("action-widgets")
515 dialog.appendChild(actions)
517 # 3) Add action-widget tag for the response
518 action = self._dom.createElement("action-widget")
519 action.setAttribute("response", response)
520 action.appendChild(self._dom.createTextNode(object_name))
521 actions.appendChild(action)
523 def _convert_adjustment(self, prop):
526 data = prop.childNodes[0].data
527 value, lower, upper, step, page, page_size = data.split(' ')
528 properties.update(value=value,
535 prop.appendChild(self._dom.createTextNode(""))
537 adj = self._create_root_object("GtkAdjustment",
538 template='adjustment',
539 properties=properties)
540 prop.childNodes[0].data = adj.getAttribute('id')
542 def _convert_combobox_items(self, node, prop):
543 parent = prop.parentNode
544 if not prop.childNodes:
545 parent.removeChild(prop)
547 value = prop.childNodes[0].data
548 model = self._create_root_object("GtkListStore",
551 columns = self._dom.createElement('columns')
552 model.appendChild(columns)
554 column = self._dom.createElement('column')
555 column.setAttribute('type', 'gchararray')
556 columns.appendChild(column)
558 data = self._dom.createElement('data')
559 model.appendChild(data)
561 for item in value.split('\n'):
562 row = self._dom.createElement('row')
563 data.appendChild(row)
565 col = self._dom.createElement('col')
566 col.setAttribute('id', '0')
567 col.setAttribute('translatable', 'yes')
568 col.appendChild(self._dom.createTextNode(item))
571 model_prop = self._dom.createElement('property')
572 model_prop.setAttribute('name', 'model')
573 model_prop.appendChild(
574 self._dom.createTextNode(model.getAttribute('id')))
575 parent.appendChild(model_prop)
577 parent.removeChild(prop)
579 child = self._dom.createElement('child')
580 node.appendChild(child)
581 cell_renderer = self._create_object('GtkCellRendererText', None,
583 child.appendChild(cell_renderer)
585 attributes = self._dom.createElement('attributes')
586 child.appendChild(attributes)
588 attribute = self._dom.createElement('attribute')
589 attributes.appendChild(attribute)
590 attribute.setAttribute('name', 'text')
591 attribute.appendChild(self._dom.createTextNode('0'))
593 def _convert_textview_text(self, prop):
594 if not prop.childNodes:
595 prop.parentNode.removeChild(prop)
598 data = prop.childNodes[0].data
599 if prop.hasAttribute('translatable'):
600 prop.removeAttribute('translatable')
601 tbuffer = self._create_root_object("GtkTextBuffer",
602 template='textbuffer',
603 properties=dict(text=data))
604 prop.childNodes[0].data = tbuffer.getAttribute('id')
605 prop.setAttribute('name', 'buffer')
607 def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
609 for child in get_child_nodes(node):
610 packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
611 if not packing_props:
613 assert len(packing_props) == 1
614 packing_prop = packing_props[0]
615 properties = packing_prop.getElementsByTagName("property")
616 for prop in properties:
617 if (prop.getAttribute("name") != prop_name or
618 prop.childNodes[0].data != prop_val):
620 packing_prop.removeChild(prop)
621 child.setAttribute(prop_name, attr_val or prop_val)
622 if len(properties) == 1:
623 child.removeChild(packing_prop)
625 def _convert_ui(self, node):
626 cdata = node.childNodes[0]
627 data = cdata.toxml().strip()
628 if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
631 child = minidom.parseString(data).childNodes[0]
632 nodes = child.childNodes[:]
633 for child_node in nodes:
634 node.appendChild(child_node)
635 node.removeChild(cdata)
636 if not node.hasAttribute("id"):
639 # Updating references made by widgets
640 parent_id = node.parentNode.getAttribute("id")
641 for widget in self._get_objects_by_attr("constructor",
642 node.getAttribute("id")):
643 widget.getAttributeNode("constructor").value = parent_id
644 node.removeAttribute("id")
646 def _convert_accessibility(self, node):
647 objectNode = node.parentNode
648 parent_id = objectNode.getAttribute("id")
651 for node in node.childNodes:
652 if node.nodeName == 'atkproperty':
653 node.tagName = 'property'
654 properties[node.getAttribute('name')] = node
655 node.parentNode.removeChild(node)
656 elif node.nodeName == 'atkrelation':
657 node.tagName = 'relation'
658 relation_type = node.getAttribute('type')
659 relation_type = relation_type.replace('_', '-')
660 node.setAttribute('type', relation_type)
661 elif node.nodeName == 'atkaction':
662 node.tagName = 'action'
665 child = self._dom.createElement('child')
666 child.setAttribute("internal-child", "accessible")
668 atkobject = self._create_object(
670 template='a11y-%s' % (parent_id,),
671 properties=properties)
672 child.appendChild(atkobject)
673 objectNode.appendChild(child)
675 def _strip_root(self, root_name):
676 for widget in self._dom.getElementsByTagName("widget"):
677 if widget.getAttribute('id') == root_name:
680 raise SystemExit("Could not find an object called `%s'" % (
683 for child in self._interface.childNodes[:]:
684 if child.nodeType != Node.ELEMENT_NODE:
686 child.parentNode.removeChild(child)
688 self._interface.appendChild(widget)
695 for directory in os.environ['PATH'].split(os.pathsep):
696 filename = os.path.join(directory, 'xmllint')
697 if os.path.exists(filename):
702 s = subprocess.Popen([filename, '--format', '-'],
703 stdin=subprocess.PIPE,
704 stdout=subprocess.PIPE)
705 s.stdin.write(output)
707 return s.stdout.read()
714 opts, args = getopt.getopt(args[1:], "hwr:",
715 ["help", "skip-windows", "root="])
716 except getopt.GetoptError:
724 input_filename, output_filename = args
730 if o in ("-h", "--help"):
733 elif o in ("-r", "--root"):
735 elif o in ("-w", "--skip-windows"):
738 conv = GtkBuilderConverter(skip_windows=skip_windows,
740 conv.parse_file(input_filename)
742 xml = _indent(conv.to_xml())
743 if output_filename == "-":
746 open(output_filename, 'w').write(xml)
747 print "Wrote", output_filename
751 if __name__ == "__main__":
752 sys.exit(main(sys.argv))