#!/usr/bin/python3 # # Copyright (C) 2006-2008 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 but 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 output. Examples: gtk-builder-convert preference.glade preferences.ui Report bugs to http://bugzilla.gnome.org/.""" from __future__ import print_function import getopt import os import sys import subprocess from xml.dom import minidom, Node DIALOGS = ['GtkDialog', 'GtkFileChooserDialog', 'GtkMessageDialog'] WINDOWS = ['GtkWindow'] + DIALOGS def get_child_nodes(node): assert node.tagName == 'object' nodes = [] for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName != 'child': continue nodes.append(child) return nodes def get_properties(node): assert node.tagName == 'object' properties = {} for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName != 'property': continue value = child.childNodes[0].data properties[child.getAttribute('name')] = value return properties def get_property(node, property_name): assert node.tagName == 'object' properties = get_properties(node) return properties.get(property_name) def get_property_node(node, property_name): assert node.tagName == 'object' properties = {} for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName != 'property': continue if child.getAttribute('name') == property_name: return child def get_signal_nodes(node): assert node.tagName == 'object' signals = [] for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName == 'signal': signals.append(child) return signals def get_property_nodes(node): assert node.tagName == 'object' properties = [] for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue # FIXME: handle comments if child.tagName == 'property': properties.append(child) return properties def get_accelerator_nodes(node): assert node.tagName == 'object' accelerators = [] for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName == 'accelerator': accelerators.append(child) return accelerators def get_object_node(child_node): assert child_node.tagName == 'child', child_node nodes = [] for node in child_node.childNodes: if node.nodeType != Node.ELEMENT_NODE: continue if node.tagName == 'object': nodes.append(node) assert len(nodes) == 1, nodes return nodes[0] def copy_properties(node, props, prop_dict): assert node.tagName == 'object' for prop_name in props: child = get_property_node(node, prop_name) if child is not None: prop_dict[prop_name] = child return node class GtkBuilderConverter(object): def __init__(self, skip_windows, target_version, root): self.skip_windows = skip_windows self.target_version = target_version 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("", "") if sys.version_info < (3, 0): # Python 2: convert with explicit encoding # (For Python 3, the result is left as text) xml = xml.encode('utf-8') return xml # # 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=None): """ Creates a new tag. Optionally a name template can be provided which will be used to avoid naming collisions. The properties dictionary can either contain string values or Node values. If a node is provided the name of the node will be overridden by the dictionary key. @param obj_class: class of the object (class tag) @param obj_id: identifier of the object (id tag) @param template: name template to use, for example 'button' @param properties: dictionary of properties @type properties: string or Node. @returns: Newly created node of the object """ 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) if properties: for name, value in properties.items(): if isinstance(value, Node): # Reuse the node, so translatable and context still will be # set when converting nodes. See also #509153 prop = value else: prop = self._dom.createElement('property') prop.appendChild(self._dom.createTextNode(value)) prop.setAttribute('name', str(name)) obj.appendChild(prop) self.objects[obj_id] = obj return obj def _create_root_object(self, obj_class, template, properties=None): obj = self._create_object(obj_class, None, template, properties) self.root_objects.append(obj) return obj 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', 'requires-version']: 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) if self._get_object(node.getAttribute('id')) is not None: print("WARNING: duplicate id \"" + node.getAttribute('id') + "\"") self.objects[node.getAttribute('id')] = node # Convert Gazpachos UI tag for node in self._dom.getElementsByTagName("ui"): self._convert_ui(node) # Convert accessibility tag for node in self._dom.getElementsByTagName("accessibility"): self._convert_accessibility(node) # Output the newly created root objects and sort them # by attribute id root_objects = sorted(self.root_objects, key=lambda n: n.getAttribute('id'), reverse=True) for obj in root_objects: 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": self._convert_menu(node) elif klass == "GtkMenu": # Only convert toplevel popups if node.parentNode == self._interface: self._convert_menu(node, popup=True) elif klass in WINDOWS and self.skip_windows: self._remove_window(node) if self.target_version == "3.0": if klass == "GtkComboBoxEntry": node.setAttribute("class","GtkComboBox") prop = self._dom.createElement("property") prop.setAttribute("name", "has-entry") prop.appendChild(self._dom.createTextNode("True")) node.appendChild(prop) elif klass == "GtkDialog": for child in node.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue if child.tagName != 'property': continue if (child.getAttribute("name") not in ("has-separator", "has_separator")): continue; node.removeChild(child) break self._default_widget_converter(node) def _default_widget_converter(self, node): klass = node.getAttribute("class") for prop in get_property_nodes(node): 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_menu(self, node, popup=False): if node.hasAttribute('constructor'): return uimgr = self._create_root_object('GtkUIManager', template='uimanager') if popup: name = 'popup' else: name = 'menubar' menu = self._dom.createElement(name) menu.setAttribute('name', node.getAttribute('id')) node.setAttribute('constructor', uimgr.getAttribute('id')) for child in get_child_nodes(node): obj_node = get_object_node(child) item = self._convert_menuitem(uimgr, obj_node) menu.appendChild(item) child.removeChild(obj_node) child.parentNode.removeChild(child) ui = self._dom.createElement('ui') uimgr.appendChild(ui) ui.appendChild(menu) def _convert_menuitem(self, uimgr, 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', 'GtkCheckMenuItem', 'GtkRadioMenuItem']: menu = self._dom.createElement(name) elif object_class == 'GtkSeparatorMenuItem': return self._dom.createElement('separator') else: raise NotImplementedError(object_class) menu.setAttribute('action', obj_node.getAttribute('id')) self._add_action_from_menuitem(uimgr, obj_node) if children: for child in get_child_nodes(menu_node): obj_node = get_object_node(child) item = self._convert_menuitem(uimgr, obj_node) menu.appendChild(item) child.removeChild(obj_node) child.parentNode.removeChild(child) return menu def _menuitem_to_action(self, node, properties): copy_properties(node, ['label', 'tooltip'], properties) def _togglemenuitem_to_action(self, node, properties): self._menuitem_to_action(node, properties) copy_properties(node, ['active'], properties) def _radiomenuitem_to_action(self, node, properties): self._togglemenuitem_to_action(node, properties) copy_properties(node, ['group'], properties) def _add_action_from_menuitem(self, uimgr, node): properties = {} object_class = node.getAttribute('class') object_id = node.getAttribute('id') if object_class == 'GtkMenuItem': name = 'GtkAction' self._menuitem_to_action(node, properties) elif object_class == 'GtkCheckMenuItem': name = 'GtkToggleAction' self._togglemenuitem_to_action(node, properties) elif object_class == 'GtkRadioMenuItem': name = 'GtkRadioAction' self._radiomenuitem_to_action(node, properties) elif 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]) child = get_property_node(image, 'stock') if child is not None: properties['stock_id'] = child self._menuitem_to_action(node, properties) elif object_class == 'GtkSeparatorMenuItem': return else: raise NotImplementedError(object_class) if get_property(node, 'use_stock') == 'True': if 'label' in properties: properties['stock_id'] = properties['label'] del properties['label'] properties['name'] = object_id action = self._create_object(name, object_id, properties=properties) for signal in get_signal_nodes(node): signal_name = signal.getAttribute('name') if signal_name in ['activate', 'toggled']: action.appendChild(signal) else: print('Unhandled signal %s::%s' % (node.getAttribute('class'), signal_name)) 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) for accelerator in get_accelerator_nodes(node): signal_name = accelerator.getAttribute('signal') if signal_name != 'activate': print('Unhandled accelerator signal for %s::%s' % ( node.getAttribute('class'), signal_name)) continue accelerator.removeAttribute('signal') child.appendChild(accelerator) 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 we can't find the parent dialog, give up if node == self._dom: return if (node.tagName == 'object' and node.getAttribute('class') in DIALOGS): 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.ELEMENT_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): properties = {} if prop.childNodes: data = prop.childNodes[0].data value, lower, upper, step, page, page_size = data.split(' ') properties.update(value=value, lower=lower, upper=upper, step_increment=step, page_increment=page, page_size=page_size) else: prop.appendChild(self._dom.createTextNode("")) adj = self._create_root_object("GtkAdjustment", template='adjustment', properties=properties) prop.childNodes[0].data = adj.getAttribute('id') def _convert_combobox_items(self, node, prop): parent = prop.parentNode if not prop.childNodes: parent.removeChild(prop) return translatable_attr = prop.attributes.get('translatable') translatable = translatable_attr is not None and translatable_attr.value == 'yes' has_context_attr = prop.attributes.get('context') has_context = has_context_attr is not None and has_context_attr.value == 'yes' comments_attr = prop.attributes.get('comments') comments = comments_attr is not None and comments_attr.value or None 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) if value.endswith('\n'): value = value[:-1] for item in value.split('\n'): row = self._dom.createElement('row') data.appendChild(row) col = self._dom.createElement('col') col.setAttribute('id', '0') if translatable: col.setAttribute('translatable', 'yes') if has_context: splitting = item.split('|', 1) if len(splitting) == 2: context, item = splitting col.setAttribute('context', context) if comments is not None: col.setAttribute('comments', comments) col.appendChild(self._dom.createTextNode(item)) row.appendChild(col) 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', properties=dict(text=data)) prop.childNodes[0].data = tbuffer.getAttribute('id') prop.setAttribute('name', 'buffer') def _packing_prop_to_child_attr(self, node, prop_name, prop_val, attr_val=None): for child in get_child_nodes(node): 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 _convert_accessibility(self, node): objectNode = node.parentNode parent_id = objectNode.getAttribute("id") properties = {} for node in node.childNodes: if node.nodeName == 'atkproperty': node.tagName = 'property' properties[node.getAttribute('name')] = node node.parentNode.removeChild(node) elif node.nodeName == 'atkrelation': node.tagName = 'relation' relation_type = node.getAttribute('type') relation_type = relation_type.replace('_', '-') node.setAttribute('type', relation_type) elif node.nodeName == 'atkaction': node.tagName = 'action' if properties: child = self._dom.createElement('child') child.setAttribute("internal-child", "accessible") atkobject = self._create_object( "AtkObject", None, template='a11y-%s' % (parent_id,), properties=properties) child.appendChild(atkobject) objectNode.appendChild(child) 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)) for child in self._interface.childNodes[:]: if child.nodeType != Node.ELEMENT_NODE: continue child.parentNode.removeChild(child) 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, universal_newlines=True) 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", "target-version=", "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 target_version = "2.0" 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 elif o in ("-t", "--target-version"): target_version = a conv = GtkBuilderConverter(skip_windows=skip_windows, target_version=target_version, 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))