2 # This file was downloaded from
3 # http://git.gnome.org/cgit/gtk+/plain/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 output.
39 gtk-builder-convert preference.glade preferences.ui
41 Report bugs to http://bugzilla.gnome.org/."""
47 from xml.dom import minidom, Node
49 DIALOGS = ['GtkDialog',
50 'GtkFileChooserDialog',
52 WINDOWS = ['GtkWindow'] + DIALOGS
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 if self._get_object(node.getAttribute('id')) is not None:
265 print "WARNING: duplicate id \"" + node.getAttribute('id') + "\""
266 self.objects[node.getAttribute('id')] = node
268 # Convert Gazpachos UI tag
269 for node in self._dom.getElementsByTagName("ui"):
270 self._convert_ui(node)
272 # Convert accessibility tag
273 for node in self._dom.getElementsByTagName("accessibility"):
274 self._convert_accessibility(node)
276 # Output the newly created root objects and sort them
278 # FIXME: Use sorted(self.root_objects,
279 # key=lambda n: n.getAttribute('id'),
281 # when we can depend on python 2.4 or higher
282 root_objects = self.root_objects[:]
283 root_objects.sort(lambda a, b: cmp(b.getAttribute('id'),
284 a.getAttribute('id')))
285 for obj in root_objects:
286 self._interface.childNodes.insert(0, obj)
288 def _convert(self, klass, node):
289 if klass == 'GtkNotebook':
290 self._packing_prop_to_child_attr(node, "type", "tab")
291 elif klass in ['GtkExpander', 'GtkFrame']:
292 self._packing_prop_to_child_attr(
293 node, "type", "label_item", "label")
294 elif klass == "GtkMenuBar":
295 self._convert_menu(node)
296 elif klass == "GtkMenu":
297 # Only convert toplevel popups
298 if node.parentNode == self._interface:
299 self._convert_menu(node, popup=True)
300 elif klass in WINDOWS and self.skip_windows:
301 self._remove_window(node)
302 self._default_widget_converter(node)
304 def _default_widget_converter(self, node):
305 klass = node.getAttribute("class")
306 for prop in get_property_nodes(node):
307 prop_name = prop.getAttribute("name")
308 if prop_name == "sizegroup":
309 self._convert_sizegroup(node, prop)
310 elif prop_name == "tooltip" and klass != "GtkAction":
311 prop.setAttribute("name", "tooltip-text")
312 elif prop_name in ["response_id", 'response-id']:
313 # It does not make sense to convert responses when
314 # we're not going to output dialogs
315 if self.skip_windows:
317 object_id = node.getAttribute('id')
318 response = prop.childNodes[0].data
319 self._convert_dialog_response(node, object_id, response)
320 prop.parentNode.removeChild(prop)
321 elif prop_name == "adjustment":
322 self._convert_adjustment(prop)
323 elif prop_name == "items" and klass in ['GtkComboBox',
325 self._convert_combobox_items(node, prop)
326 elif prop_name == "text" and klass == 'GtkTextView':
327 self._convert_textview_text(prop)
329 def _remove_window(self, node):
330 object_node = get_object_node(get_child_nodes(node)[0])
331 parent = node.parentNode
332 parent.removeChild(node)
333 parent.appendChild(object_node)
335 def _convert_menu(self, node, popup=False):
336 if node.hasAttribute('constructor'):
339 uimgr = self._create_root_object('GtkUIManager',
340 template='uimanager')
347 menu = self._dom.createElement(name)
348 menu.setAttribute('name', node.getAttribute('id'))
349 node.setAttribute('constructor', uimgr.getAttribute('id'))
351 for child in get_child_nodes(node):
352 obj_node = get_object_node(child)
353 item = self._convert_menuitem(uimgr, obj_node)
354 menu.appendChild(item)
355 child.removeChild(obj_node)
356 child.parentNode.removeChild(child)
358 ui = self._dom.createElement('ui')
359 uimgr.appendChild(ui)
363 def _convert_menuitem(self, uimgr, obj_node):
364 children = get_child_nodes(obj_node)
367 child_node = children[0]
368 menu_node = get_object_node(child_node)
369 # Can be GtkImage, which will take care of later.
370 if menu_node.getAttribute('class') == 'GtkMenu':
373 object_class = obj_node.getAttribute('class')
374 if object_class in ['GtkMenuItem',
378 menu = self._dom.createElement(name)
379 elif object_class == 'GtkSeparatorMenuItem':
380 return self._dom.createElement('separator')
382 raise NotImplementedError(object_class)
384 menu.setAttribute('action', obj_node.getAttribute('id'))
385 self._add_action_from_menuitem(uimgr, obj_node)
387 for child in get_child_nodes(menu_node):
388 obj_node = get_object_node(child)
389 item = self._convert_menuitem(uimgr, obj_node)
390 menu.appendChild(item)
391 child.removeChild(obj_node)
392 child.parentNode.removeChild(child)
395 def _menuitem_to_action(self, node, properties):
396 copy_properties(node, ['label', 'tooltip'], properties)
398 def _togglemenuitem_to_action(self, node, properties):
399 self._menuitem_to_action(node, properties)
400 copy_properties(node, ['active'], properties)
402 def _radiomenuitem_to_action(self, node, properties):
403 self._togglemenuitem_to_action(node, properties)
404 copy_properties(node, ['group'], properties)
406 def _add_action_from_menuitem(self, uimgr, node):
408 object_class = node.getAttribute('class')
409 object_id = node.getAttribute('id')
410 if object_class == 'GtkMenuItem':
412 self._menuitem_to_action(node, properties)
413 elif object_class == 'GtkCheckMenuItem':
414 name = 'GtkToggleAction'
415 self._togglemenuitem_to_action(node, properties)
416 elif object_class == 'GtkRadioMenuItem':
417 name = 'GtkRadioAction'
418 self._radiomenuitem_to_action(node, properties)
419 elif object_class == 'GtkImageMenuItem':
421 children = get_child_nodes(node)
423 children[0].getAttribute('internal-child') == 'image'):
424 image = get_object_node(children[0])
425 child = get_property_node(image, 'stock')
426 if child is not None:
427 properties['stock_id'] = child
428 self._menuitem_to_action(node, properties)
429 elif object_class == 'GtkSeparatorMenuItem':
432 raise NotImplementedError(object_class)
434 if get_property(node, 'use_stock') == 'True':
435 if 'label' in properties:
436 properties['stock_id'] = properties['label']
437 del properties['label']
439 properties['name'] = object_id
440 action = self._create_object(name,
442 properties=properties)
443 for signal in get_signal_nodes(node):
444 signal_name = signal.getAttribute('name')
445 if signal_name in ['activate', 'toggled']:
446 action.appendChild(signal)
448 print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
451 if not uimgr.childNodes:
452 child = self._dom.createElement('child')
453 uimgr.appendChild(child)
455 group = self._create_object('GtkActionGroup', None,
456 template='actiongroup')
457 child.appendChild(group)
459 group = uimgr.childNodes[0].childNodes[0]
461 child = self._dom.createElement('child')
462 group.appendChild(child)
463 child.appendChild(action)
465 for accelerator in get_accelerator_nodes(node):
466 signal_name = accelerator.getAttribute('signal')
467 if signal_name != 'activate':
468 print 'Unhandled accelerator signal for %s::%s' % (
469 node.getAttribute('class'), signal_name)
471 accelerator.removeAttribute('signal')
472 child.appendChild(accelerator)
474 def _convert_sizegroup(self, node, prop):
475 # This is Gazpacho only
476 node.removeChild(prop)
477 obj = self._get_object(prop.childNodes[0].data)
479 widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
483 obj = self._create_root_object('GtkSizeGroup',
484 template='sizegroup')
486 widgets = obj.getElementsByTagName("widgets")
488 assert len(widgets) == 1
491 widgets = self._dom.createElement("widgets")
492 obj.appendChild(widgets)
494 member = self._dom.createElement("widget")
495 member.setAttribute("name", node.getAttribute("id"))
496 widgets.appendChild(member)
498 def _convert_dialog_response(self, node, object_name, response):
499 # 1) Get parent dialog node
501 # If we can't find the parent dialog, give up
502 if node == self._dom:
505 if (node.tagName == 'object' and
506 node.getAttribute('class') in DIALOGS):
509 node = node.parentNode
512 # 2) Get dialogs action-widgets tag, create if not found
513 for child in dialog.childNodes:
514 if child.nodeType != Node.ELEMENT_NODE:
516 if child.tagName == 'action-widgets':
520 actions = self._dom.createElement("action-widgets")
521 dialog.appendChild(actions)
523 # 3) Add action-widget tag for the response
524 action = self._dom.createElement("action-widget")
525 action.setAttribute("response", response)
526 action.appendChild(self._dom.createTextNode(object_name))
527 actions.appendChild(action)
529 def _convert_adjustment(self, prop):
532 data = prop.childNodes[0].data
533 value, lower, upper, step, page, page_size = data.split(' ')
534 properties.update(value=value,
541 prop.appendChild(self._dom.createTextNode(""))
543 adj = self._create_root_object("GtkAdjustment",
544 template='adjustment',
545 properties=properties)
546 prop.childNodes[0].data = adj.getAttribute('id')
548 def _convert_combobox_items(self, node, prop):
549 parent = prop.parentNode
550 if not prop.childNodes:
551 parent.removeChild(prop)
554 translatable_attr = prop.attributes.get('translatable')
555 translatable = translatable_attr is not None and translatable_attr.value == 'yes'
556 has_context_attr = prop.attributes.get('context')
557 has_context = has_context_attr is not None and has_context_attr.value == 'yes'
558 comments_attr = prop.attributes.get('comments')
559 comments = comments_attr is not None and comments_attr.value or None
561 value = prop.childNodes[0].data
562 model = self._create_root_object("GtkListStore",
565 columns = self._dom.createElement('columns')
566 model.appendChild(columns)
568 column = self._dom.createElement('column')
569 column.setAttribute('type', 'gchararray')
570 columns.appendChild(column)
572 data = self._dom.createElement('data')
573 model.appendChild(data)
575 if value.endswith('\n'):
577 for item in value.split('\n'):
578 row = self._dom.createElement('row')
579 data.appendChild(row)
581 col = self._dom.createElement('col')
582 col.setAttribute('id', '0')
584 col.setAttribute('translatable', 'yes')
586 splitting = item.split('|', 1)
587 if len(splitting) == 2:
588 context, item = splitting
589 col.setAttribute('context', context)
590 if comments is not None:
591 col.setAttribute('comments', comments)
592 col.appendChild(self._dom.createTextNode(item))
595 model_prop = self._dom.createElement('property')
596 model_prop.setAttribute('name', 'model')
597 model_prop.appendChild(
598 self._dom.createTextNode(model.getAttribute('id')))
599 parent.appendChild(model_prop)
601 parent.removeChild(prop)
603 child = self._dom.createElement('child')
604 node.appendChild(child)
605 cell_renderer = self._create_object('GtkCellRendererText', None,
607 child.appendChild(cell_renderer)
609 attributes = self._dom.createElement('attributes')
610 child.appendChild(attributes)
612 attribute = self._dom.createElement('attribute')
613 attributes.appendChild(attribute)
614 attribute.setAttribute('name', 'text')
615 attribute.appendChild(self._dom.createTextNode('0'))
617 def _convert_textview_text(self, prop):
618 if not prop.childNodes:
619 prop.parentNode.removeChild(prop)
622 data = prop.childNodes[0].data
623 if prop.hasAttribute('translatable'):
624 prop.removeAttribute('translatable')
625 tbuffer = self._create_root_object("GtkTextBuffer",
626 template='textbuffer',
627 properties=dict(text=data))
628 prop.childNodes[0].data = tbuffer.getAttribute('id')
629 prop.setAttribute('name', 'buffer')
631 def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
633 for child in get_child_nodes(node):
634 packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
635 if not packing_props:
637 assert len(packing_props) == 1
638 packing_prop = packing_props[0]
639 properties = packing_prop.getElementsByTagName("property")
640 for prop in properties:
641 if (prop.getAttribute("name") != prop_name or
642 prop.childNodes[0].data != prop_val):
644 packing_prop.removeChild(prop)
645 child.setAttribute(prop_name, attr_val or prop_val)
646 if len(properties) == 1:
647 child.removeChild(packing_prop)
649 def _convert_ui(self, node):
650 cdata = node.childNodes[0]
651 data = cdata.toxml().strip()
652 if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
655 child = minidom.parseString(data).childNodes[0]
656 nodes = child.childNodes[:]
657 for child_node in nodes:
658 node.appendChild(child_node)
659 node.removeChild(cdata)
660 if not node.hasAttribute("id"):
663 # Updating references made by widgets
664 parent_id = node.parentNode.getAttribute("id")
665 for widget in self._get_objects_by_attr("constructor",
666 node.getAttribute("id")):
667 widget.getAttributeNode("constructor").value = parent_id
668 node.removeAttribute("id")
670 def _convert_accessibility(self, node):
671 objectNode = node.parentNode
672 parent_id = objectNode.getAttribute("id")
675 for node in node.childNodes:
676 if node.nodeName == 'atkproperty':
677 node.tagName = 'property'
678 properties[node.getAttribute('name')] = node
679 node.parentNode.removeChild(node)
680 elif node.nodeName == 'atkrelation':
681 node.tagName = 'relation'
682 relation_type = node.getAttribute('type')
683 relation_type = relation_type.replace('_', '-')
684 node.setAttribute('type', relation_type)
685 elif node.nodeName == 'atkaction':
686 node.tagName = 'action'
689 child = self._dom.createElement('child')
690 child.setAttribute("internal-child", "accessible")
692 atkobject = self._create_object(
694 template='a11y-%s' % (parent_id,),
695 properties=properties)
696 child.appendChild(atkobject)
697 objectNode.appendChild(child)
699 def _strip_root(self, root_name):
700 for widget in self._dom.getElementsByTagName("widget"):
701 if widget.getAttribute('id') == root_name:
704 raise SystemExit("Could not find an object called `%s'" % (
707 for child in self._interface.childNodes[:]:
708 if child.nodeType != Node.ELEMENT_NODE:
710 child.parentNode.removeChild(child)
712 self._interface.appendChild(widget)
719 for directory in os.environ['PATH'].split(os.pathsep):
720 filename = os.path.join(directory, 'xmllint')
721 if os.path.exists(filename):
726 s = subprocess.Popen([filename, '--format', '-'],
727 stdin=subprocess.PIPE,
728 stdout=subprocess.PIPE)
729 s.stdin.write(output)
731 return s.stdout.read()
738 opts, args = getopt.getopt(args[1:], "hwr:",
739 ["help", "skip-windows", "root="])
740 except getopt.GetoptError:
748 input_filename, output_filename = args
754 if o in ("-h", "--help"):
757 elif o in ("-r", "--root"):
759 elif o in ("-w", "--skip-windows"):
762 conv = GtkBuilderConverter(skip_windows=skip_windows,
764 conv.parse_file(input_filename)
766 xml = _indent(conv.to_xml())
767 if output_filename == "-":
770 open(output_filename, 'w').write(xml)
771 print "Wrote", output_filename
775 if __name__ == "__main__":
776 sys.exit(main(sys.argv))