#!/usr/bin/env python # # Copyright (C) 2006-2007 Async Open Source # Henrique Romano # Johan Dahlin # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # TODO: # Toolbars """Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT] Converts Glade files into XML files which can be loaded with GtkBuilder. The [INPUT] file is -w, --skip-windows Convert everything bug GtkWindow subclasses. -r, --root Convert only widget named root and its children -h, --help display this help and exit When OUTPUT is -, write to standard input. Examples: gtk-builder-convert preference.glade preferences.ui Report bugs to http://bugzilla.gnome.org/.""" import getopt import os import sys from xml.dom import minidom, Node WINDOWS = ['GtkWindow', 'GtkDialog', 'GtkFileChooserDialog', 'GtkMessageDialog'] # The subprocess is only available in Python 2.4+ try: import subprocess subprocess # pyflakes except ImportError: subprocess = None def get_child_nodes(node): nodes = [] for child in node.childNodes: if child.nodeType == Node.TEXT_NODE: continue if child.tagName != 'child': continue nodes.append(child) return nodes def get_object_properties(node): properties = {} for child in node.childNodes: if child.nodeType == Node.TEXT_NODE: continue if child.tagName != 'property': continue value = child.childNodes[0].data properties[child.getAttribute('name')] = value return properties def get_object_node(child_node): assert child_node.tagName == 'child' nodes = [] for node in child_node.childNodes: if node.nodeType == Node.TEXT_NODE: continue if node.tagName == 'object': nodes.append(node) assert len(nodes) == 1, nodes return nodes[0] class GtkBuilderConverter(object): def __init__(self, skip_windows, root): self.skip_windows = skip_windows self.root = root self.root_objects = [] self.objects = {} # # Public API # def parse_file(self, file): self._dom = minidom.parse(file) self._parse() def parse_buffer(self, buffer): self._dom = minidom.parseString(buffer) self._parse() def to_xml(self): xml = self._dom.toprettyxml("", "") return xml.encode('utf-8') # # Private # def _get_object(self, name): return self.objects.get(name) def _get_objects_by_attr(self, attribute, value): return [w for w in self._dom.getElementsByTagName("object") if w.getAttribute(attribute) == value] def _create_object(self, obj_class, obj_id, template=None, **properties): if template is not None: count = 1 while True: obj_id = template + str(count) widget = self._get_object(obj_id) if widget is None: break count += 1 obj = self._dom.createElement('object') obj.setAttribute('class', obj_class) obj.setAttribute('id', obj_id) for name, value in properties.items(): prop = self._dom.createElement('property') prop.setAttribute('name', name) prop.appendChild(self._dom.createTextNode(value)) obj.appendChild(prop) self.objects[obj_id] = obj return obj def _create_root_object(self, obj_class, template, **properties): obj = self._create_object(obj_class, None, template, **properties) self.root_objects.append(obj) return obj def _get_property(self, node, property_name): properties = get_object_properties(node) return properties.get(property_name) def _parse(self): glade_iface = self._dom.getElementsByTagName("glade-interface") assert glade_iface, ("Badly formed XML, there is " "no tag.") # Rename glade-interface to interface glade_iface[0].tagName = 'interface' self._interface = glade_iface[0] # Remove glade-interface doc type for node in self._dom.childNodes: if node.nodeType == Node.DOCUMENT_TYPE_NODE: if node.name == 'glade-interface': self._dom.removeChild(node) # Strip unsupported tags for tag in ['requires', 'accessibility', 'atkproperty', 'atkrelation']: for child in self._dom.getElementsByTagName(tag): child.parentNode.removeChild(child) if self.root: self._strip_root(self.root) # Rename widget to object objects = self._dom.getElementsByTagName("widget") for node in objects: node.tagName = "object" for node in objects: self._convert(node.getAttribute("class"), node) self.objects[node.getAttribute('id')] = node # Remove empty properties for node in self._dom.getElementsByTagName('property'): if not node.childNodes: node.parentNode.removeChild(node) # Convert Gazpachos UI tag for node in self._dom.getElementsByTagName("ui"): self._convert_ui(node) # Output the newly created root objects and sort them # by attribute id for obj in sorted(self.root_objects, key=lambda n: n.getAttribute('id'), reverse=True): self._interface.childNodes.insert(0, obj) def _convert(self, klass, node): if klass == 'GtkNotebook': self._packing_prop_to_child_attr(node, "type", "tab") elif klass in ['GtkExpander', 'GtkFrame']: self._packing_prop_to_child_attr( node, "type", "label_item", "label") elif klass == "GtkMenuBar": if node.hasAttribute('constructor'): uimgr = self._get_object('uimanager') else: uimgr = self._create_root_object('GtkUIManager', template='uimanager') self._convert_menubar(uimgr, node) elif klass in WINDOWS and self.skip_windows: self._remove_window(node) self._default_widget_converter(node) def _default_widget_converter(self, node): klass = node.getAttribute("class") for prop in node.getElementsByTagName("property"): if prop.parentNode is not node: continue prop_name = prop.getAttribute("name") if prop_name == "sizegroup": self._convert_sizegroup(node, prop) elif prop_name == "tooltip" and klass != "GtkAction": prop.setAttribute("name", "tooltip-text") elif prop_name in ["response_id", 'response-id']: # It does not make sense to convert responses when # we're not going to output dialogs if self.skip_windows: continue object_id = node.getAttribute('id') response = prop.childNodes[0].data self._convert_dialog_response(node, object_id, response) prop.parentNode.removeChild(prop) elif prop_name == "adjustment": self._convert_adjustment(prop) elif prop_name == "items" and klass in ['GtkComboBox', 'GtkComboBoxEntry']: self._convert_combobox_items(node, prop) elif prop_name == "text" and klass == 'GtkTextView': self._convert_textview_text(prop) def _remove_window(self, node): object_node = get_object_node(get_child_nodes(node)[0]) parent = node.parentNode parent.removeChild(node) parent.appendChild(object_node) def _convert_menubar(self, uimgr, node): menubar = self._dom.createElement('menubar') menubar.setAttribute('name', node.getAttribute('id')) node.setAttribute('constructor', uimgr.getAttribute('id')) for child in get_child_nodes(node): obj_node = get_object_node(child) self._convert_menuitem(uimgr, menubar, obj_node) child.removeChild(obj_node) child.parentNode.removeChild(child) ui = self._dom.createElement('ui') uimgr.appendChild(ui) ui.appendChild(menubar) def _convert_menuitem(self, uimgr, menubar, obj_node): children = get_child_nodes(obj_node) name = 'menuitem' if children: child_node = children[0] menu_node = get_object_node(child_node) # Can be GtkImage, which will take care of later. if menu_node.getAttribute('class') == 'GtkMenu': name = 'menu' object_class = obj_node.getAttribute('class') if object_class in ['GtkMenuItem', 'GtkImageMenuItem']: menu = self._dom.createElement(name) elif object_class == 'GtkSeparatorMenuItem': menu = self._dom.createElement('sep') else: raise NotImplementedError(object_class) menu.setAttribute('name', obj_node.getAttribute('id')) menu.setAttribute('action', obj_node.getAttribute('id')) menubar.appendChild(menu) self._add_action_from_menuitem(uimgr, obj_node) if children: for child in get_child_nodes(menu_node): obj_node = get_object_node(child) self._convert_menuitem(uimgr, menu, obj_node) child.removeChild(obj_node) child.parentNode.removeChild(child) def _add_action_from_menuitem(self, uimgr, node): properties = {} object_class = node.getAttribute('class') object_id = node.getAttribute('id') if object_class == 'GtkImageMenuItem': name = 'GtkAction' children = get_child_nodes(node) if (children and children[0].getAttribute('internal-child') == 'image'): image = get_object_node(children[0]) properties['stock_id'] = self._get_property(image, 'stock') elif object_class == 'GtkMenuItem': name = 'GtkAction' label = self._get_property(node, 'label') if label is not None: properties['label'] = label elif object_class == 'GtkSeparatorMenuItem': return else: raise NotImplementedError(object_class) if self._get_property(node, 'use_stock') == 'True': properties['stock_id'] = self._get_property(node, 'label') properties['name'] = object_id action = self._create_object(name, object_id, **properties) if not uimgr.childNodes: child = self._dom.createElement('child') uimgr.appendChild(child) group = self._create_object('GtkActionGroup', None, template='actiongroup') child.appendChild(group) else: group = uimgr.childNodes[0].childNodes[0] child = self._dom.createElement('child') group.appendChild(child) child.appendChild(action) def _convert_sizegroup(self, node, prop): # This is Gazpacho only node.removeChild(prop) obj = self._get_object(prop.childNodes[0].data) if obj is None: widgets = self._get_objects_by_attr("class", "GtkSizeGroup") if widgets: obj = widgets[-1] else: obj = self._create_root_object('GtkSizeGroup', template='sizegroup') widgets = obj.getElementsByTagName("widgets") if widgets: assert len(widgets) == 1 widgets = widgets[0] else: widgets = self._dom.createElement("widgets") obj.appendChild(widgets) member = self._dom.createElement("widget") member.setAttribute("name", node.getAttribute("id")) widgets.appendChild(member) def _convert_dialog_response(self, node, object_name, response): # 1) Get parent dialog node while True: if (node.tagName == 'object' and node.getAttribute('class') == 'GtkDialog'): dialog = node break node = node.parentNode assert node # 2) Get dialogs action-widgets tag, create if not found for child in dialog.childNodes: if child.nodeType == Node.TEXT_NODE: continue if child.tagName == 'action-widgets': actions = child break else: actions = self._dom.createElement("action-widgets") dialog.appendChild(actions) # 3) Add action-widget tag for the response action = self._dom.createElement("action-widget") action.setAttribute("response", response) action.appendChild(self._dom.createTextNode(object_name)) actions.appendChild(action) def _convert_adjustment(self, prop): data = prop.childNodes[0].data value, lower, upper, step, page, page_size = data.split(' ') adj = self._create_root_object("GtkAdjustment", template='adjustment', value=value, lower=lower, upper=upper, step_increment=step, page_increment=page, page_size=page_size) prop.childNodes[0].data = adj.getAttribute('id') def _convert_combobox_items(self, node, prop): if not prop.childNodes: return value = prop.childNodes[0].data model = self._create_root_object("GtkListStore", template="model") columns = self._dom.createElement('columns') model.appendChild(columns) column = self._dom.createElement('column') column.setAttribute('type', 'gchararray') columns.appendChild(column) data = self._dom.createElement('data') model.appendChild(data) for item in value.split('\n'): row = self._dom.createElement('row') data.appendChild(row) col = self._dom.createElement('col') col.setAttribute('id', '0') col.appendChild(self._dom.createTextNode(item)) row.appendChild(col) parent = prop.parentNode model_prop = self._dom.createElement('property') model_prop.setAttribute('name', 'model') model_prop.appendChild( self._dom.createTextNode(model.getAttribute('id'))) parent.appendChild(model_prop) parent.removeChild(prop) child = self._dom.createElement('child') node.appendChild(child) cell_renderer = self._create_object('GtkCellRendererText', None, template='renderer') child.appendChild(cell_renderer) attributes = self._dom.createElement('attributes') child.appendChild(attributes) attribute = self._dom.createElement('attribute') attributes.appendChild(attribute) attribute.setAttribute('name', 'text') attribute.appendChild(self._dom.createTextNode('0')) def _convert_textview_text(self, prop): if not prop.childNodes: prop.parentNode.removeChild(prop) return data = prop.childNodes[0].data if prop.hasAttribute('translatable'): prop.removeAttribute('translatable') tbuffer = self._create_root_object("GtkTextBuffer", template='textbuffer', text=data) prop.childNodes[0].data = tbuffer.getAttribute('id') def _packing_prop_to_child_attr(self, node, prop_name, prop_val, attr_val=None): for child in node.getElementsByTagName("child"): packing_props = [p for p in child.childNodes if p.nodeName == "packing"] if not packing_props: continue assert len(packing_props) == 1 packing_prop = packing_props[0] properties = packing_prop.getElementsByTagName("property") for prop in properties: if (prop.getAttribute("name") != prop_name or prop.childNodes[0].data != prop_val): continue packing_prop.removeChild(prop) child.setAttribute(prop_name, attr_val or prop_val) if len(properties) == 1: child.removeChild(packing_prop) def _convert_ui(self, node): cdata = node.childNodes[0] data = cdata.toxml().strip() if not data.startswith(""): return data = data[9:-3] child = minidom.parseString(data).childNodes[0] nodes = child.childNodes[:] for child_node in nodes: node.appendChild(child_node) node.removeChild(cdata) if not node.hasAttribute("id"): return # Updating references made by widgets parent_id = node.parentNode.getAttribute("id") for widget in self._get_objects_by_attr("constructor", node.getAttribute("id")): widget.getAttributeNode("constructor").value = parent_id node.removeAttribute("id") def _strip_root(self, root_name): for widget in self._dom.getElementsByTagName("widget"): if widget.getAttribute('id') == root_name: break else: raise SystemExit("Could not find an object called `%s'" % ( root_name)) # If it's already a root object, don't do anything if widget.parentNode is self._interface: return for child in self._interface.childNodes[:]: if child.nodeType != Node.ELEMENT_NODE: continue child.parentNode.removeChild(child) widget.parentNode.removeChild(widget) self._interface.appendChild(widget) def _indent(output): if not subprocess: return output for directory in os.environ['PATH'].split(os.pathsep): filename = os.path.join(directory, 'xmllint') if os.path.exists(filename): break else: return output s = subprocess.Popen([filename, '--format', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) s.stdin.write(output) s.stdin.close() return s.stdout.read() def usage(): print __doc__ def main(args): try: opts, args = getopt.getopt(args[1:], "hwr:", ["help", "skip-windows", "root="]) except getopt.GetoptError: usage() return 2 if len(args) != 2: usage() return 2 input_filename, output_filename = args skip_windows = False split = False root = None for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() elif o in ("-r", "--root"): root = a elif o in ("-w", "--skip-windows"): skip_windows = True conv = GtkBuilderConverter(skip_windows=skip_windows, root=root) conv.parse_file(input_filename) xml = _indent(conv.to_xml()) if output_filename == "-": print xml else: open(output_filename, 'w').write(xml) print "Wrote", output_filename return 0 if __name__ == "__main__": sys.exit(main(sys.argv))