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-3.0 [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 output.
35 gtk-builder-convert-3.0 preference.glade preferences.ui
37 Report bugs to http://bugzilla.gnome.org/."""
43 from xml.dom import minidom, Node
45 DIALOGS = ['GtkDialog',
46 'GtkFileChooserDialog',
48 WINDOWS = ['GtkWindow'] + DIALOGS
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 if self._get_object(node.getAttribute('id')) is not None:
261 print "WARNING: duplicate id \"" + node.getAttribute('id') + "\""
262 self.objects[node.getAttribute('id')] = node
264 # Convert Gazpachos UI tag
265 for node in self._dom.getElementsByTagName("ui"):
266 self._convert_ui(node)
268 # Convert accessibility tag
269 for node in self._dom.getElementsByTagName("accessibility"):
270 self._convert_accessibility(node)
272 # Output the newly created root objects and sort them
274 # FIXME: Use sorted(self.root_objects,
275 # key=lambda n: n.getAttribute('id'),
277 # when we can depend on python 2.4 or higher
278 root_objects = self.root_objects[:]
279 root_objects.sort(lambda a, b: cmp(b.getAttribute('id'),
280 a.getAttribute('id')))
281 for obj in root_objects:
282 self._interface.childNodes.insert(0, obj)
284 def _convert(self, klass, node):
285 if klass == 'GtkNotebook':
286 self._packing_prop_to_child_attr(node, "type", "tab")
287 elif klass in ['GtkExpander', 'GtkFrame']:
288 self._packing_prop_to_child_attr(
289 node, "type", "label_item", "label")
290 elif klass == "GtkMenuBar":
291 self._convert_menu(node)
292 elif klass == "GtkMenu":
293 # Only convert toplevel popups
294 if node.parentNode == self._interface:
295 self._convert_menu(node, popup=True)
296 elif klass in WINDOWS and self.skip_windows:
297 self._remove_window(node)
298 self._default_widget_converter(node)
300 def _default_widget_converter(self, node):
301 klass = node.getAttribute("class")
302 for prop in get_property_nodes(node):
303 prop_name = prop.getAttribute("name")
304 if prop_name == "sizegroup":
305 self._convert_sizegroup(node, prop)
306 elif prop_name == "tooltip" and klass != "GtkAction":
307 prop.setAttribute("name", "tooltip-text")
308 elif prop_name in ["response_id", 'response-id']:
309 # It does not make sense to convert responses when
310 # we're not going to output dialogs
311 if self.skip_windows:
313 object_id = node.getAttribute('id')
314 response = prop.childNodes[0].data
315 self._convert_dialog_response(node, object_id, response)
316 prop.parentNode.removeChild(prop)
317 elif prop_name == "adjustment":
318 self._convert_adjustment(prop)
319 elif prop_name == "items" and klass in ['GtkComboBox',
321 self._convert_combobox_items(node, prop)
322 elif prop_name == "text" and klass == 'GtkTextView':
323 self._convert_textview_text(prop)
325 def _remove_window(self, node):
326 object_node = get_object_node(get_child_nodes(node)[0])
327 parent = node.parentNode
328 parent.removeChild(node)
329 parent.appendChild(object_node)
331 def _convert_menu(self, node, popup=False):
332 if node.hasAttribute('constructor'):
335 uimgr = self._create_root_object('GtkUIManager',
336 template='uimanager')
343 menu = self._dom.createElement(name)
344 menu.setAttribute('name', node.getAttribute('id'))
345 node.setAttribute('constructor', uimgr.getAttribute('id'))
347 for child in get_child_nodes(node):
348 obj_node = get_object_node(child)
349 item = self._convert_menuitem(uimgr, obj_node)
350 menu.appendChild(item)
351 child.removeChild(obj_node)
352 child.parentNode.removeChild(child)
354 ui = self._dom.createElement('ui')
355 uimgr.appendChild(ui)
359 def _convert_menuitem(self, uimgr, obj_node):
360 children = get_child_nodes(obj_node)
363 child_node = children[0]
364 menu_node = get_object_node(child_node)
365 # Can be GtkImage, which will take care of later.
366 if menu_node.getAttribute('class') == 'GtkMenu':
369 object_class = obj_node.getAttribute('class')
370 if object_class in ['GtkMenuItem',
374 menu = self._dom.createElement(name)
375 elif object_class == 'GtkSeparatorMenuItem':
376 return self._dom.createElement('separator')
378 raise NotImplementedError(object_class)
380 menu.setAttribute('action', obj_node.getAttribute('id'))
381 self._add_action_from_menuitem(uimgr, obj_node)
383 for child in get_child_nodes(menu_node):
384 obj_node = get_object_node(child)
385 item = self._convert_menuitem(uimgr, obj_node)
386 menu.appendChild(item)
387 child.removeChild(obj_node)
388 child.parentNode.removeChild(child)
391 def _menuitem_to_action(self, node, properties):
392 copy_properties(node, ['label', 'tooltip'], properties)
394 def _togglemenuitem_to_action(self, node, properties):
395 self._menuitem_to_action(node, properties)
396 copy_properties(node, ['active'], properties)
398 def _radiomenuitem_to_action(self, node, properties):
399 self._togglemenuitem_to_action(node, properties)
400 copy_properties(node, ['group'], properties)
402 def _add_action_from_menuitem(self, uimgr, node):
404 object_class = node.getAttribute('class')
405 object_id = node.getAttribute('id')
406 if object_class == 'GtkMenuItem':
408 self._menuitem_to_action(node, properties)
409 elif object_class == 'GtkCheckMenuItem':
410 name = 'GtkToggleAction'
411 self._togglemenuitem_to_action(node, properties)
412 elif object_class == 'GtkRadioMenuItem':
413 name = 'GtkRadioAction'
414 self._radiomenuitem_to_action(node, properties)
415 elif object_class == 'GtkImageMenuItem':
417 children = get_child_nodes(node)
419 children[0].getAttribute('internal-child') == 'image'):
420 image = get_object_node(children[0])
421 child = get_property_node(image, 'stock')
422 if child is not None:
423 properties['stock_id'] = child
424 self._menuitem_to_action(node, properties)
425 elif object_class == 'GtkSeparatorMenuItem':
428 raise NotImplementedError(object_class)
430 if get_property(node, 'use_stock') == 'True':
431 if 'label' in properties:
432 properties['stock_id'] = properties['label']
433 del properties['label']
435 properties['name'] = object_id
436 action = self._create_object(name,
438 properties=properties)
439 for signal in get_signal_nodes(node):
440 signal_name = signal.getAttribute('name')
441 if signal_name in ['activate', 'toggled']:
442 action.appendChild(signal)
444 print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
447 if not uimgr.childNodes:
448 child = self._dom.createElement('child')
449 uimgr.appendChild(child)
451 group = self._create_object('GtkActionGroup', None,
452 template='actiongroup')
453 child.appendChild(group)
455 group = uimgr.childNodes[0].childNodes[0]
457 child = self._dom.createElement('child')
458 group.appendChild(child)
459 child.appendChild(action)
461 for accelerator in get_accelerator_nodes(node):
462 signal_name = accelerator.getAttribute('signal')
463 if signal_name != 'activate':
464 print 'Unhandled accelerator signal for %s::%s' % (
465 node.getAttribute('class'), signal_name)
467 accelerator.removeAttribute('signal')
468 child.appendChild(accelerator)
470 def _convert_sizegroup(self, node, prop):
471 # This is Gazpacho only
472 node.removeChild(prop)
473 obj = self._get_object(prop.childNodes[0].data)
475 widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
479 obj = self._create_root_object('GtkSizeGroup',
480 template='sizegroup')
482 widgets = obj.getElementsByTagName("widgets")
484 assert len(widgets) == 1
487 widgets = self._dom.createElement("widgets")
488 obj.appendChild(widgets)
490 member = self._dom.createElement("widget")
491 member.setAttribute("name", node.getAttribute("id"))
492 widgets.appendChild(member)
494 def _convert_dialog_response(self, node, object_name, response):
495 # 1) Get parent dialog node
497 # If we can't find the parent dialog, give up
498 if node == self._dom:
501 if (node.tagName == 'object' and
502 node.getAttribute('class') in DIALOGS):
505 node = node.parentNode
508 # 2) Get dialogs action-widgets tag, create if not found
509 for child in dialog.childNodes:
510 if child.nodeType != Node.ELEMENT_NODE:
512 if child.tagName == 'action-widgets':
516 actions = self._dom.createElement("action-widgets")
517 dialog.appendChild(actions)
519 # 3) Add action-widget tag for the response
520 action = self._dom.createElement("action-widget")
521 action.setAttribute("response", response)
522 action.appendChild(self._dom.createTextNode(object_name))
523 actions.appendChild(action)
525 def _convert_adjustment(self, prop):
528 data = prop.childNodes[0].data
529 value, lower, upper, step, page, page_size = data.split(' ')
530 properties.update(value=value,
537 prop.appendChild(self._dom.createTextNode(""))
539 adj = self._create_root_object("GtkAdjustment",
540 template='adjustment',
541 properties=properties)
542 prop.childNodes[0].data = adj.getAttribute('id')
544 def _convert_combobox_items(self, node, prop):
545 parent = prop.parentNode
546 if not prop.childNodes:
547 parent.removeChild(prop)
550 translatable_attr = prop.attributes.get('translatable')
551 translatable = translatable_attr is not None and translatable_attr.value == 'yes'
552 has_context_attr = prop.attributes.get('context')
553 has_context = has_context_attr is not None and has_context_attr.value == 'yes'
554 comments_attr = prop.attributes.get('comments')
555 comments = comments_attr is not None and comments_attr.value or None
557 value = prop.childNodes[0].data
558 model = self._create_root_object("GtkListStore",
561 columns = self._dom.createElement('columns')
562 model.appendChild(columns)
564 column = self._dom.createElement('column')
565 column.setAttribute('type', 'gchararray')
566 columns.appendChild(column)
568 data = self._dom.createElement('data')
569 model.appendChild(data)
571 if value.endswith('\n'):
573 for item in value.split('\n'):
574 row = self._dom.createElement('row')
575 data.appendChild(row)
577 col = self._dom.createElement('col')
578 col.setAttribute('id', '0')
580 col.setAttribute('translatable', 'yes')
582 splitting = item.split('|', 1)
583 if len(splitting) == 2:
584 context, item = splitting
585 col.setAttribute('context', context)
586 if comments is not None:
587 col.setAttribute('comments', comments)
588 col.appendChild(self._dom.createTextNode(item))
591 model_prop = self._dom.createElement('property')
592 model_prop.setAttribute('name', 'model')
593 model_prop.appendChild(
594 self._dom.createTextNode(model.getAttribute('id')))
595 parent.appendChild(model_prop)
597 parent.removeChild(prop)
599 child = self._dom.createElement('child')
600 node.appendChild(child)
601 cell_renderer = self._create_object('GtkCellRendererText', None,
603 child.appendChild(cell_renderer)
605 attributes = self._dom.createElement('attributes')
606 child.appendChild(attributes)
608 attribute = self._dom.createElement('attribute')
609 attributes.appendChild(attribute)
610 attribute.setAttribute('name', 'text')
611 attribute.appendChild(self._dom.createTextNode('0'))
613 def _convert_textview_text(self, prop):
614 if not prop.childNodes:
615 prop.parentNode.removeChild(prop)
618 data = prop.childNodes[0].data
619 if prop.hasAttribute('translatable'):
620 prop.removeAttribute('translatable')
621 tbuffer = self._create_root_object("GtkTextBuffer",
622 template='textbuffer',
623 properties=dict(text=data))
624 prop.childNodes[0].data = tbuffer.getAttribute('id')
625 prop.setAttribute('name', 'buffer')
627 def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
629 for child in get_child_nodes(node):
630 packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
631 if not packing_props:
633 assert len(packing_props) == 1
634 packing_prop = packing_props[0]
635 properties = packing_prop.getElementsByTagName("property")
636 for prop in properties:
637 if (prop.getAttribute("name") != prop_name or
638 prop.childNodes[0].data != prop_val):
640 packing_prop.removeChild(prop)
641 child.setAttribute(prop_name, attr_val or prop_val)
642 if len(properties) == 1:
643 child.removeChild(packing_prop)
645 def _convert_ui(self, node):
646 cdata = node.childNodes[0]
647 data = cdata.toxml().strip()
648 if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
651 child = minidom.parseString(data).childNodes[0]
652 nodes = child.childNodes[:]
653 for child_node in nodes:
654 node.appendChild(child_node)
655 node.removeChild(cdata)
656 if not node.hasAttribute("id"):
659 # Updating references made by widgets
660 parent_id = node.parentNode.getAttribute("id")
661 for widget in self._get_objects_by_attr("constructor",
662 node.getAttribute("id")):
663 widget.getAttributeNode("constructor").value = parent_id
664 node.removeAttribute("id")
666 def _convert_accessibility(self, node):
667 objectNode = node.parentNode
668 parent_id = objectNode.getAttribute("id")
671 for node in node.childNodes:
672 if node.nodeName == 'atkproperty':
673 node.tagName = 'property'
674 properties[node.getAttribute('name')] = node
675 node.parentNode.removeChild(node)
676 elif node.nodeName == 'atkrelation':
677 node.tagName = 'relation'
678 relation_type = node.getAttribute('type')
679 relation_type = relation_type.replace('_', '-')
680 node.setAttribute('type', relation_type)
681 elif node.nodeName == 'atkaction':
682 node.tagName = 'action'
685 child = self._dom.createElement('child')
686 child.setAttribute("internal-child", "accessible")
688 atkobject = self._create_object(
690 template='a11y-%s' % (parent_id,),
691 properties=properties)
692 child.appendChild(atkobject)
693 objectNode.appendChild(child)
695 def _strip_root(self, root_name):
696 for widget in self._dom.getElementsByTagName("widget"):
697 if widget.getAttribute('id') == root_name:
700 raise SystemExit("Could not find an object called `%s'" % (
703 for child in self._interface.childNodes[:]:
704 if child.nodeType != Node.ELEMENT_NODE:
706 child.parentNode.removeChild(child)
708 self._interface.appendChild(widget)
715 for directory in os.environ['PATH'].split(os.pathsep):
716 filename = os.path.join(directory, 'xmllint')
717 if os.path.exists(filename):
722 s = subprocess.Popen([filename, '--format', '-'],
723 stdin=subprocess.PIPE,
724 stdout=subprocess.PIPE)
725 s.stdin.write(output)
727 return s.stdout.read()
734 opts, args = getopt.getopt(args[1:], "hwr:",
735 ["help", "skip-windows", "root="])
736 except getopt.GetoptError:
744 input_filename, output_filename = args
750 if o in ("-h", "--help"):
753 elif o in ("-r", "--root"):
755 elif o in ("-w", "--skip-windows"):
758 conv = GtkBuilderConverter(skip_windows=skip_windows,
760 conv.parse_file(input_filename)
762 xml = _indent(conv.to_xml())
763 if output_filename == "-":
766 open(output_filename, 'w').write(xml)
767 print "Wrote", output_filename
771 if __name__ == "__main__":
772 sys.exit(main(sys.argv))