3 # This file was downloaded from
4 # http://svn.gnome.org/svn/gtk+/tags/GTK_2_14_7/gtk/gtk-builder-convert
7 # Copyright (C) 2006-2008 Async Open Source
8 # Henrique Romano <henrique@async.com.br>
9 # Johan Dahlin <jdahlin@async.com.br>
11 # This program is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU Lesser General Public License
13 # as published by the Free Software Foundation; either version 2
14 # of the License, or (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU Lesser General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 """Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
29 Converts Glade files into XML files which can be loaded with GtkBuilder.
32 -w, --skip-windows Convert everything but GtkWindow subclasses.
33 -r, --root Convert only widget named root and its children
34 -h, --help display this help and exit
36 When OUTPUT is -, write to standard input.
39 gtk-builder-convert preference.glade preferences.ui
41 Report bugs to http://bugzilla.gnome.org/."""
47 from xml.dom import minidom, Node
49 WINDOWS = ['GtkWindow',
51 'GtkFileChooserDialog',
54 # The subprocess is only available in Python 2.4+
61 def get_child_nodes(node):
62 assert node.tagName == 'object'
64 for child in node.childNodes:
65 if child.nodeType != Node.ELEMENT_NODE:
67 if child.tagName != 'child':
72 def get_properties(node):
73 assert node.tagName == 'object'
75 for child in node.childNodes:
76 if child.nodeType != Node.ELEMENT_NODE:
78 if child.tagName != 'property':
80 value = child.childNodes[0].data
81 properties[child.getAttribute('name')] = value
84 def get_property(node, property_name):
85 assert node.tagName == 'object'
86 properties = get_properties(node)
87 return properties.get(property_name)
89 def get_property_node(node, property_name):
90 assert node.tagName == 'object'
92 for child in node.childNodes:
93 if child.nodeType != Node.ELEMENT_NODE:
95 if child.tagName != 'property':
97 if child.getAttribute('name') == property_name:
100 def get_signal_nodes(node):
101 assert node.tagName == 'object'
103 for child in node.childNodes:
104 if child.nodeType != Node.ELEMENT_NODE:
106 if child.tagName == 'signal':
107 signals.append(child)
110 def get_property_nodes(node):
111 assert node.tagName == 'object'
113 for child in node.childNodes:
114 if child.nodeType != Node.ELEMENT_NODE:
116 # FIXME: handle comments
117 if child.tagName == 'property':
118 properties.append(child)
121 def get_accelerator_nodes(node):
122 assert node.tagName == 'object'
124 for child in node.childNodes:
125 if child.nodeType != Node.ELEMENT_NODE:
127 if child.tagName == 'accelerator':
128 accelerators.append(child)
131 def get_object_node(child_node):
132 assert child_node.tagName == 'child', child_node
134 for node in child_node.childNodes:
135 if node.nodeType != Node.ELEMENT_NODE:
137 if node.tagName == 'object':
139 assert len(nodes) == 1, nodes
142 def copy_properties(node, props, prop_dict):
143 assert node.tagName == 'object'
144 for prop_name in props:
145 child = get_property_node(node, prop_name)
146 if child is not None:
147 prop_dict[prop_name] = child
151 class GtkBuilderConverter(object):
153 def __init__(self, skip_windows, root):
154 self.skip_windows = skip_windows
156 self.root_objects = []
163 def parse_file(self, file):
164 self._dom = minidom.parse(file)
167 def parse_buffer(self, buffer):
168 self._dom = minidom.parseString(buffer)
172 xml = self._dom.toprettyxml("", "")
173 return xml.encode('utf-8')
179 def _get_object(self, name):
180 return self.objects.get(name)
182 def _get_objects_by_attr(self, attribute, value):
183 return [w for w in self._dom.getElementsByTagName("object")
184 if w.getAttribute(attribute) == value]
186 def _create_object(self, obj_class, obj_id, template=None, properties=None):
188 Creates a new <object> tag.
189 Optionally a name template can be provided which will be used
190 to avoid naming collisions.
191 The properties dictionary can either contain string values or Node
192 values. If a node is provided the name of the node will be overridden
193 by the dictionary key.
195 @param obj_class: class of the object (class tag)
196 @param obj_id: identifier of the object (id tag)
197 @param template: name template to use, for example 'button'
198 @param properties: dictionary of properties
199 @type properties: string or Node.
200 @returns: Newly created node of the object
202 if template is not None:
205 obj_id = template + str(count)
206 widget = self._get_object(obj_id)
212 obj = self._dom.createElement('object')
213 obj.setAttribute('class', obj_class)
214 obj.setAttribute('id', obj_id)
216 for name, value in properties.items():
217 if isinstance(value, Node):
218 # Reuse the node, so translatable and context still will be
219 # set when converting nodes. See also #509153
222 prop = self._dom.createElement('property')
223 prop.appendChild(self._dom.createTextNode(value))
225 prop.setAttribute('name', str(name))
226 obj.appendChild(prop)
227 self.objects[obj_id] = obj
230 def _create_root_object(self, obj_class, template, properties=None):
231 obj = self._create_object(obj_class, None, template, properties)
232 self.root_objects.append(obj)
236 glade_iface = self._dom.getElementsByTagName("glade-interface")
237 assert glade_iface, ("Badly formed XML, there is "
238 "no <glade-interface> tag.")
239 # Rename glade-interface to interface
240 glade_iface[0].tagName = 'interface'
241 self._interface = glade_iface[0]
243 # Remove glade-interface doc type
244 for node in self._dom.childNodes:
245 if node.nodeType == Node.DOCUMENT_TYPE_NODE:
246 if node.name == 'glade-interface':
247 self._dom.removeChild(node)
249 # Strip unsupported tags
250 for tag in ['requires', 'requires-version']:
251 for child in self._dom.getElementsByTagName(tag):
252 child.parentNode.removeChild(child)
255 self._strip_root(self.root)
257 # Rename widget to object
258 objects = self._dom.getElementsByTagName("widget")
260 node.tagName = "object"
263 self._convert(node.getAttribute("class"), node)
264 self.objects[node.getAttribute('id')] = node
266 # Convert Gazpachos UI tag
267 for node in self._dom.getElementsByTagName("ui"):
268 self._convert_ui(node)
270 # Convert accessibility tag
271 for node in self._dom.getElementsByTagName("accessibility"):
272 self._convert_accessibility(node)
274 # Output the newly created root objects and sort them
276 for obj in sorted(self.root_objects,
277 key=lambda n: n.getAttribute('id'),
279 self._interface.childNodes.insert(0, obj)
281 def _convert(self, klass, node):
282 if klass == 'GtkNotebook':
283 self._packing_prop_to_child_attr(node, "type", "tab")
284 elif klass in ['GtkExpander', 'GtkFrame']:
285 self._packing_prop_to_child_attr(
286 node, "type", "label_item", "label")
287 elif klass == "GtkMenuBar":
288 self._convert_menu(node)
289 elif klass == "GtkMenu":
290 # Only convert toplevel popups
291 if node.parentNode == self._interface:
292 self._convert_menu(node, popup=True)
293 elif klass in WINDOWS and self.skip_windows:
294 self._remove_window(node)
295 self._default_widget_converter(node)
297 def _default_widget_converter(self, node):
298 klass = node.getAttribute("class")
299 for prop in get_property_nodes(node):
300 prop_name = prop.getAttribute("name")
301 if prop_name == "sizegroup":
302 self._convert_sizegroup(node, prop)
303 elif prop_name == "tooltip" and klass != "GtkAction":
304 prop.setAttribute("name", "tooltip-text")
305 elif prop_name in ["response_id", 'response-id']:
306 # It does not make sense to convert responses when
307 # we're not going to output dialogs
308 if self.skip_windows:
310 object_id = node.getAttribute('id')
311 response = prop.childNodes[0].data
312 self._convert_dialog_response(node, object_id, response)
313 prop.parentNode.removeChild(prop)
314 elif prop_name == "adjustment":
315 self._convert_adjustment(prop)
316 elif prop_name == "items" and klass in ['GtkComboBox',
318 self._convert_combobox_items(node, prop)
319 elif prop_name == "text" and klass == 'GtkTextView':
320 self._convert_textview_text(prop)
322 def _remove_window(self, node):
323 object_node = get_object_node(get_child_nodes(node)[0])
324 parent = node.parentNode
325 parent.removeChild(node)
326 parent.appendChild(object_node)
328 def _convert_menu(self, node, popup=False):
329 if node.hasAttribute('constructor'):
332 uimgr = self._create_root_object('GtkUIManager',
333 template='uimanager')
340 menu = self._dom.createElement(name)
341 menu.setAttribute('name', node.getAttribute('id'))
342 node.setAttribute('constructor', uimgr.getAttribute('id'))
344 for child in get_child_nodes(node):
345 obj_node = get_object_node(child)
346 item = self._convert_menuitem(uimgr, obj_node)
347 menu.appendChild(item)
348 child.removeChild(obj_node)
349 child.parentNode.removeChild(child)
351 ui = self._dom.createElement('ui')
352 uimgr.appendChild(ui)
356 def _convert_menuitem(self, uimgr, obj_node):
357 children = get_child_nodes(obj_node)
360 child_node = children[0]
361 menu_node = get_object_node(child_node)
362 # Can be GtkImage, which will take care of later.
363 if menu_node.getAttribute('class') == 'GtkMenu':
366 object_class = obj_node.getAttribute('class')
367 if object_class in ['GtkMenuItem',
371 menu = self._dom.createElement(name)
372 elif object_class == 'GtkSeparatorMenuItem':
373 return self._dom.createElement('separator')
375 raise NotImplementedError(object_class)
377 menu.setAttribute('action', obj_node.getAttribute('id'))
378 self._add_action_from_menuitem(uimgr, obj_node)
380 for child in get_child_nodes(menu_node):
381 obj_node = get_object_node(child)
382 item = self._convert_menuitem(uimgr, obj_node)
383 menu.appendChild(item)
384 child.removeChild(obj_node)
385 child.parentNode.removeChild(child)
388 def _menuitem_to_action(self, node, properties):
389 copy_properties(node, ['label', 'tooltip'], properties)
391 def _togglemenuitem_to_action(self, node, properties):
392 self._menuitem_to_action(node, properties)
393 copy_properties(node, ['active'], properties)
395 def _radiomenuitem_to_action(self, node, properties):
396 self._togglemenuitem_to_action(node, properties)
397 copy_properties(node, ['group'], properties)
399 def _add_action_from_menuitem(self, uimgr, node):
401 object_class = node.getAttribute('class')
402 object_id = node.getAttribute('id')
403 if object_class == 'GtkMenuItem':
405 self._menuitem_to_action(node, properties)
406 elif object_class == 'GtkCheckMenuItem':
407 name = 'GtkToggleAction'
408 self._togglemenuitem_to_action(node, properties)
409 elif object_class == 'GtkRadioMenuItem':
410 name = 'GtkRadioAction'
411 self._radiomenuitem_to_action(node, properties)
412 elif object_class == 'GtkImageMenuItem':
414 children = get_child_nodes(node)
416 children[0].getAttribute('internal-child') == 'image'):
417 image = get_object_node(children[0])
418 child = get_property_node(image, 'stock')
419 if child is not None:
420 properties['stock_id'] = child
421 self._menuitem_to_action(node, properties)
422 elif object_class == 'GtkSeparatorMenuItem':
425 raise NotImplementedError(object_class)
427 if get_property(node, 'use_stock') == 'True':
428 if 'label' in properties:
429 properties['stock_id'] = properties['label']
430 del properties['label']
432 properties['name'] = object_id
433 action = self._create_object(name,
435 properties=properties)
436 for signal in get_signal_nodes(node):
437 signal_name = signal.getAttribute('name')
438 if signal_name in ['activate', 'toggled']:
439 action.appendChild(signal)
441 print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
444 if not uimgr.childNodes:
445 child = self._dom.createElement('child')
446 uimgr.appendChild(child)
448 group = self._create_object('GtkActionGroup', None,
449 template='actiongroup')
450 child.appendChild(group)
452 group = uimgr.childNodes[0].childNodes[0]
454 child = self._dom.createElement('child')
455 group.appendChild(child)
456 child.appendChild(action)
458 for accelerator in get_accelerator_nodes(node):
459 signal_name = accelerator.getAttribute('signal')
460 if signal_name != 'activate':
461 print 'Unhandled accelerator signal for %s::%s' % (
462 node.getAttribute('class'), signal_name)
464 accelerator.removeAttribute('signal')
465 child.appendChild(accelerator)
467 def _convert_sizegroup(self, node, prop):
468 # This is Gazpacho only
469 node.removeChild(prop)
470 obj = self._get_object(prop.childNodes[0].data)
472 widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
476 obj = self._create_root_object('GtkSizeGroup',
477 template='sizegroup')
479 widgets = obj.getElementsByTagName("widgets")
481 assert len(widgets) == 1
484 widgets = self._dom.createElement("widgets")
485 obj.appendChild(widgets)
487 member = self._dom.createElement("widget")
488 member.setAttribute("name", node.getAttribute("id"))
489 widgets.appendChild(member)
491 def _convert_dialog_response(self, node, object_name, response):
492 # 1) Get parent dialog node
494 # If we can't find the parent dialog, give up
495 if node == self._dom:
498 if (node.tagName == 'object' and
499 node.getAttribute('class') == 'GtkDialog'):
502 node = node.parentNode
505 # 2) Get dialogs action-widgets tag, create if not found
506 for child in dialog.childNodes:
507 if child.nodeType != Node.ELEMENT_NODE:
509 if child.tagName == 'action-widgets':
513 actions = self._dom.createElement("action-widgets")
514 dialog.appendChild(actions)
516 # 3) Add action-widget tag for the response
517 action = self._dom.createElement("action-widget")
518 action.setAttribute("response", response)
519 action.appendChild(self._dom.createTextNode(object_name))
520 actions.appendChild(action)
522 def _convert_adjustment(self, prop):
525 data = prop.childNodes[0].data
526 value, lower, upper, step, page, page_size = data.split(' ')
527 properties.update(value=value,
534 prop.appendChild(self._dom.createTextNode(""))
536 adj = self._create_root_object("GtkAdjustment",
537 template='adjustment',
538 properties=properties)
539 prop.childNodes[0].data = adj.getAttribute('id')
541 def _convert_combobox_items(self, node, prop):
542 parent = prop.parentNode
543 if not prop.childNodes:
544 parent.removeChild(prop)
546 value = prop.childNodes[0].data
547 model = self._create_root_object("GtkListStore",
550 columns = self._dom.createElement('columns')
551 model.appendChild(columns)
553 column = self._dom.createElement('column')
554 column.setAttribute('type', 'gchararray')
555 columns.appendChild(column)
557 data = self._dom.createElement('data')
558 model.appendChild(data)
560 for item in value.split('\n'):
561 row = self._dom.createElement('row')
562 data.appendChild(row)
564 col = self._dom.createElement('col')
565 col.setAttribute('id', '0')
566 col.appendChild(self._dom.createTextNode(item))
569 model_prop = self._dom.createElement('property')
570 model_prop.setAttribute('name', 'model')
571 model_prop.appendChild(
572 self._dom.createTextNode(model.getAttribute('id')))
573 parent.appendChild(model_prop)
575 parent.removeChild(prop)
577 child = self._dom.createElement('child')
578 node.appendChild(child)
579 cell_renderer = self._create_object('GtkCellRendererText', None,
581 child.appendChild(cell_renderer)
583 attributes = self._dom.createElement('attributes')
584 child.appendChild(attributes)
586 attribute = self._dom.createElement('attribute')
587 attributes.appendChild(attribute)
588 attribute.setAttribute('name', 'text')
589 attribute.appendChild(self._dom.createTextNode('0'))
591 def _convert_textview_text(self, prop):
592 if not prop.childNodes:
593 prop.parentNode.removeChild(prop)
596 data = prop.childNodes[0].data
597 if prop.hasAttribute('translatable'):
598 prop.removeAttribute('translatable')
599 tbuffer = self._create_root_object("GtkTextBuffer",
600 template='textbuffer',
601 properties=dict(text=data))
602 prop.childNodes[0].data = tbuffer.getAttribute('id')
603 prop.setAttribute('name', 'buffer')
605 def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
607 for child in get_child_nodes(node):
608 packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
609 if not packing_props:
611 assert len(packing_props) == 1
612 packing_prop = packing_props[0]
613 properties = packing_prop.getElementsByTagName("property")
614 for prop in properties:
615 if (prop.getAttribute("name") != prop_name or
616 prop.childNodes[0].data != prop_val):
618 packing_prop.removeChild(prop)
619 child.setAttribute(prop_name, attr_val or prop_val)
620 if len(properties) == 1:
621 child.removeChild(packing_prop)
623 def _convert_ui(self, node):
624 cdata = node.childNodes[0]
625 data = cdata.toxml().strip()
626 if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
629 child = minidom.parseString(data).childNodes[0]
630 nodes = child.childNodes[:]
631 for child_node in nodes:
632 node.appendChild(child_node)
633 node.removeChild(cdata)
634 if not node.hasAttribute("id"):
637 # Updating references made by widgets
638 parent_id = node.parentNode.getAttribute("id")
639 for widget in self._get_objects_by_attr("constructor",
640 node.getAttribute("id")):
641 widget.getAttributeNode("constructor").value = parent_id
642 node.removeAttribute("id")
644 def _convert_accessibility(self, node):
645 objectNode = node.parentNode
646 parent_id = objectNode.getAttribute("id")
649 for node in node.childNodes:
650 if node.nodeName == 'atkproperty':
651 node.tagName = 'property'
652 properties[node.getAttribute('name')] = node
653 node.parentNode.removeChild(node)
654 elif node.nodeName == 'atkrelation':
655 node.tagName = 'relation'
656 relation_type = node.getAttribute('type')
657 relation_type = relation_type.replace('_', '-')
658 node.setAttribute('type', relation_type)
659 elif node.nodeName == 'atkaction':
660 node.tagName = 'action'
663 child = self._dom.createElement('child')
664 child.setAttribute("internal-child", "accessible")
666 atkobject = self._create_object(
668 template='a11y-%s' % (parent_id,),
669 properties=properties)
670 child.appendChild(atkobject)
671 objectNode.appendChild(child)
673 def _strip_root(self, root_name):
674 for widget in self._dom.getElementsByTagName("widget"):
675 if widget.getAttribute('id') == root_name:
678 raise SystemExit("Could not find an object called `%s'" % (
681 for child in self._interface.childNodes[:]:
682 if child.nodeType != Node.ELEMENT_NODE:
684 child.parentNode.removeChild(child)
686 self._interface.appendChild(widget)
693 for directory in os.environ['PATH'].split(os.pathsep):
694 filename = os.path.join(directory, 'xmllint')
695 if os.path.exists(filename):
700 s = subprocess.Popen([filename, '--format', '-'],
701 stdin=subprocess.PIPE,
702 stdout=subprocess.PIPE)
703 s.stdin.write(output)
705 return s.stdout.read()
712 opts, args = getopt.getopt(args[1:], "hwr:",
713 ["help", "skip-windows", "root="])
714 except getopt.GetoptError:
722 input_filename, output_filename = args
728 if o in ("-h", "--help"):
731 elif o in ("-r", "--root"):
733 elif o in ("-w", "--skip-windows"):
736 conv = GtkBuilderConverter(skip_windows=skip_windows,
738 conv.parse_file(input_filename)
740 xml = _indent(conv.to_xml())
741 if output_filename == "-":
744 open(output_filename, 'w').write(xml)
745 print "Wrote", output_filename
749 if __name__ == "__main__":
750 sys.exit(main(sys.argv))