]> Pileus Git - ~andy/gtk/blob - gtk/gtk-builder-convert
Convert to getopt, improved documentation, change the script to require
[~andy/gtk] / gtk / gtk-builder-convert
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2006-2007 Async Open Source
4 #                         Henrique Romano <henrique@async.com.br>
5 #                         Johan Dahlin <jdahlin@async.com.br>
6 #
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.
11 #
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.
16 #
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.
20 #
21 # TODO:
22 #  GtkComboBox.items -> GtkListStore
23 #  GtkTextView.text -> GtkTextBuffer
24 #  Toolbars
25
26 """Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
27 Converts Glade files into XML files which can be loaded with GtkBuilder.
28 The [INPUT] file is
29
30   -w, --skip-windows     Convert everything bug GtkWindow subclasses.
31   -h, --help             display this help and exit
32
33 When OUTPUT is -, write to standard input.
34
35 Examples:
36   gtk-builder-convert preference.glade preferences.ui
37
38 Report bugs to http://bugzilla.gnome.org/."""
39
40 import getopt
41 import os
42 import sys
43
44 from xml.dom import minidom, Node
45
46 WINDOWS = ['GtkWindow',
47            'GtkDialog',
48            'GtkFileChooserDialog',
49            'GtkMessageDialog']
50
51 # The subprocess is only available in Python 2.4+
52 try:
53     import subprocess
54     subprocess # pyflakes
55 except ImportError:
56     subprocess = None
57
58 def get_child_nodes(node):
59     nodes = []
60     for child in node.childNodes:
61         if child.nodeType == Node.TEXT_NODE:
62             continue
63         if child.tagName != 'child':
64             continue
65         nodes.append(child)
66     return nodes
67
68 def get_object_properties(node):
69     properties = {}
70     for child in node.childNodes:
71         if child.nodeType == Node.TEXT_NODE:
72             continue
73         if child.tagName != 'property':
74             continue
75         value = child.childNodes[0].data
76         properties[child.getAttribute('name')] = value
77     return properties
78
79 def get_object_node(child_node):
80     assert child_node.tagName == 'child'
81     nodes = []
82     for node in child_node.childNodes:
83         if node.nodeType == Node.TEXT_NODE:
84             continue
85         if node.tagName == 'object':
86             nodes.append(node)
87     assert len(nodes) == 1, nodes
88     return nodes[0]
89
90 class GtkBuilderConverter(object):
91
92     def __init__(self, skip_windows):
93         self.skip_windows = skip_windows
94
95     #
96     # Public API
97     #
98
99     def parse_file(self, file):
100         self._dom = minidom.parse(file)
101         self._parse()
102
103     def parse_buffer(self, buffer):
104         self._dom = minidom.parseString(buffer)
105         self._parse()
106
107     def to_xml(self):
108         xml = self._dom.toprettyxml("", "")
109         return xml.encode('utf-8')
110
111     #
112     # Private
113     #
114
115     def _get_widget(self, name):
116         result = self._get_widgets_by_attr("id", name)
117         if len(result) > 1:
118             raise ValueError(
119                 "It is not possible to have more than one "
120                 "widget with the same id (`%s')" % name)
121         elif len(result) == 1:
122             return result[0]
123         return None
124
125     def _get_widgets_by_attr(self, attribute, value):
126         return [w for w in self._dom.getElementsByTagName("object")
127                       if w.getAttribute(attribute) == value]
128
129     def _create_object(self, obj_class, obj_id, **properties):
130         obj = self._dom.createElement('object')
131         obj.setAttribute('class', obj_class)
132         obj.setAttribute('id', obj_id)
133         for name, value in properties.items():
134             prop = self._dom.createElement('property')
135             prop.setAttribute('name', name)
136             prop.appendChild(self._dom.createTextNode(value))
137             obj.appendChild(prop)
138         return obj
139
140     def _get_property(self, node, property_name):
141         properties = get_object_properties(node)
142         return properties.get(property_name)
143
144     def _parse(self):
145         glade_iface = self._dom.getElementsByTagName("glade-interface")
146         assert glade_iface, ("Badly formed XML, there is "
147                              "no <glade-interface> tag.")
148         glade_iface[0].tagName = 'interface'
149         self._interface = glade_iface[0]
150
151         # Remove glade-interface doc type
152         for node in self._dom.childNodes:
153             if node.nodeType == Node.DOCUMENT_TYPE_NODE:
154                 if node.name == 'glade-interface':
155                     self._dom.removeChild(node)
156
157         # Strip requires
158         requires = self._dom.getElementsByTagName("requires")
159         for require in requires:
160             require.parentNode.childNodes.remove(require)
161
162         for child in self._dom.getElementsByTagName("accessibility"):
163             child.parentNode.removeChild(child)
164
165         for node in self._dom.getElementsByTagName("widget"):
166             node.tagName = "object"
167
168         for node in self._dom.getElementsByTagName("object"):
169             self._convert(node.getAttribute("class"), node)
170
171         # Convert Gazpachos UI tag
172         for node in self._dom.getElementsByTagName("ui"):
173             self._convert_ui(node)
174
175     def _convert(self, klass, node):
176         if klass == 'GtkNotebook':
177             self._packing_prop_to_child_attr(node, "type", "tab")
178         elif klass in ['GtkExpander', 'GtkFrame']:
179             self._packing_prop_to_child_attr(
180                 node, "type", "label_item", "label")
181         elif klass == "GtkMenuBar":
182             if node.hasAttribute('constructor'):
183                 uimgr = self._get_widget('uimanager')
184             else:
185                 uimgr = node.ownerDocument.createElement('object')
186                 uimgr.setAttribute('class', 'GtkUIManager')
187                 uimgr.setAttribute('id', 'uimanager1')
188                 self._interface.childNodes.insert(0, uimgr)
189             self._convert_menubar(uimgr, node)
190         elif klass in WINDOWS and self.skip_windows:
191             self._remove_window(node)
192
193         self._default_widget_converter(node)
194
195     def _default_widget_converter(self, node):
196         klass = node.getAttribute("class")
197         for prop in node.getElementsByTagName("property"):
198             if prop.parentNode is not node:
199                 continue
200             prop_name = prop.getAttribute("name")
201             if prop_name == "sizegroup":
202                 self._convert_sizegroup(node, prop)
203             elif prop_name == "tooltip" and klass != "GtkAction":
204                 prop.setAttribute("name", "tooltip-text")
205             elif prop_name in ["response_id", 'response-id']:
206                 # It does not make sense to convert responses when
207                 # we're not going to output dialogs
208                 if self.skip_windows:
209                     continue
210                 object_id = node.getAttribute('id')
211                 response = prop.childNodes[0].data
212                 self._convert_dialog_response(node, object_id, response)
213                 prop.parentNode.removeChild(prop)
214
215     def _remove_window(self, node):
216         object_node = get_object_node(get_child_nodes(node)[0])
217         parent = node.parentNode
218         parent.removeChild(node)
219         parent.appendChild(object_node)
220
221     def _convert_menubar(self, uimgr, node):
222         menubar = self._dom.createElement('menubar')
223         menubar.setAttribute('name', node.getAttribute('id'))
224         node.setAttribute('constructor', uimgr.getAttribute('id'))
225
226         for child in get_child_nodes(node):
227             obj_node = get_object_node(child)
228             self._convert_menuitem(uimgr, menubar, obj_node)
229             child.removeChild(obj_node)
230             child.parentNode.removeChild(child)
231
232         ui = self._dom.createElement('ui')
233         uimgr.appendChild(ui)
234
235         ui.appendChild(menubar)
236
237     def _convert_menuitem(self, uimgr, menubar, obj_node):
238         children = get_child_nodes(obj_node)
239         name = 'menuitem'
240         if children:
241             child_node = children[0]
242             menu_node = get_object_node(child_node)
243             # Can be GtkImage, which will take care of later.
244             if menu_node.getAttribute('class') == 'GtkMenu':
245                 name = 'menu'
246
247         object_class = obj_node.getAttribute('class')
248         if object_class in ['GtkMenuItem', 'GtkImageMenuItem']:
249             menu = self._dom.createElement(name)
250         elif object_class == 'GtkSeparatorMenuItem':
251             menu = self._dom.createElement('sep')
252         else:
253             raise NotImplementedError(object_class)
254         menu.setAttribute('name', obj_node.getAttribute('id'))
255         menu.setAttribute('action', obj_node.getAttribute('id'))
256         menubar.appendChild(menu)
257         self._add_action_from_menuitem(uimgr, obj_node)
258         if children:
259             for child in get_child_nodes(menu_node):
260                 obj_node = get_object_node(child)
261                 self._convert_menuitem(uimgr, menu, obj_node)
262                 child.removeChild(obj_node)
263                 child.parentNode.removeChild(child)
264
265     def _add_action_from_menuitem(self, uimgr, node):
266         properties = {}
267         object_class = node.getAttribute('class')
268         object_id = node.getAttribute('id')
269         if object_class == 'GtkImageMenuItem':
270             name = 'GtkAction'
271             children = get_child_nodes(node)
272             if (children and
273                 children[0].getAttribute('internal-child') == 'image'):
274                 image = get_object_node(children[0])
275                 properties['stock_id'] = self._get_property(image, 'stock')
276         elif object_class == 'GtkMenuItem':
277             name = 'GtkAction'
278             label = self._get_property(node, 'label')
279             if label is not None:
280                 properties['label'] = label
281         elif object_class == 'GtkSeparatorMenuItem':
282             return
283         else:
284             raise NotImplementedError(object_class)
285
286         if self._get_property(node, 'use_stock') == 'True':
287             properties['stock_id'] = self._get_property(node, 'label')
288         properties['name'] = object_id
289         action = self._create_object(name,
290                                      object_id,
291                                      **properties)
292         if not uimgr.childNodes:
293             child = self._dom.createElement('child')
294             uimgr.appendChild(child)
295
296             group = self._create_object('GtkActionGroup', 'actiongroup1')
297             child.appendChild(group)
298         else:
299             group = uimgr.childNodes[0].childNodes[0]
300
301         child = self._dom.createElement('child')
302         group.appendChild(child)
303         child.appendChild(action)
304
305     def _convert_sizegroup(self, node, prop):
306         # This is Gazpacho only
307         node.removeChild(prop)
308         obj = self._get_widget(prop.childNodes[0].data)
309         if obj is None:
310             widgets = self._get_widgets_by_attr("class", "GtkSizeGroup")
311             if widgets:
312                 obj = widgets[-1]
313             else:
314                 obj = self._dom.createElement("object")
315                 obj.setAttribute("class", "GtkSizeGroup")
316                 obj.setAttribute("id", "sizegroup1")
317                 self._interface.insertBefore(
318                     obj, self._interface.childNodes[0])
319
320         widgets = obj.getElementsByTagName("widgets")
321         if widgets:
322             assert len(widgets) == 1
323             widgets = widgets[0]
324         else:
325             widgets = self._dom.createElement("widgets")
326             obj.appendChild(widgets)
327
328         member = self._dom.createElement("widget")
329         member.setAttribute("name", node.getAttribute("id"))
330         widgets.appendChild(member)
331
332     def _convert_dialog_response(self, node, object_name, response):
333         # 1) Get parent dialog node
334         while True:
335             if (node.tagName == 'object' and
336                 node.getAttribute('class') == 'GtkDialog'):
337                 dialog = node
338                 break
339             node = node.parentNode
340             assert node
341
342         # 2) Get dialogs action-widgets tag, create if not found
343         for child in dialog.childNodes:
344             if child.nodeType == Node.TEXT_NODE:
345                 continue
346             if child.tagName == 'action-widgets':
347                 actions = child
348                 break
349         else:
350             actions = self._dom.createElement("action-widgets")
351             dialog.appendChild(actions)
352
353         # 3) Add action-widget tag for the response
354         action = self._dom.createElement("action-widget")
355         action.setAttribute("response", response)
356         action.appendChild(self._dom.createTextNode(object_name))
357         actions.appendChild(action)
358
359     def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
360                                    attr_val=None):
361         for child in node.getElementsByTagName("child"):
362             packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
363             if not packing_props:
364                 continue
365             assert len(packing_props) == 1
366             packing_prop = packing_props[0]
367             properties = packing_prop.getElementsByTagName("property")
368             for prop in properties:
369                 if (prop.getAttribute("name") != prop_name or
370                     prop.childNodes[0].data != prop_val):
371                     continue
372                 packing_prop.removeChild(prop)
373                 child.setAttribute(prop_name, attr_val or prop_val)
374             if len(properties) == 1:
375                 child.removeChild(packing_prop)
376
377     def _convert_ui(self, node):
378         cdata = node.childNodes[0]
379         data = cdata.toxml().strip()
380         if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
381             return
382         data = data[9:-3]
383         child = minidom.parseString(data).childNodes[0]
384         nodes = child.childNodes[:]
385         for child_node in nodes:
386             node.appendChild(child_node)
387         node.removeChild(cdata)
388         if not node.hasAttribute("id"):
389             return
390
391         # Updating references made by widgets
392         parent_id = node.parentNode.getAttribute("id")
393         for widget in self._get_widgets_by_attr("constructor",
394                                                 node.getAttribute("id")):
395             widget.getAttributeNode("constructor").value = parent_id
396         node.removeAttribute("id")
397
398 def _indent(output):
399     if not subprocess:
400         return output
401
402     for directory in os.environ['PATH'].split(os.pathsep):
403         filename = os.path.join(directory, 'xmllint')
404         if os.path.exists(filename):
405             break
406     else:
407         return output
408
409     s = subprocess.Popen([filename, '--format', '-'],
410                          stdin=subprocess.PIPE,
411                          stdout=subprocess.PIPE)
412     s.stdin.write(output)
413     s.stdin.close()
414     return s.stdout.read()
415
416 def usage():
417     print __doc__
418
419 def main(args):
420     try:
421         opts, args = getopt.getopt(args[1:], "hw",
422                                    ["help", "skip-windows"])
423     except getopt.GetoptError:
424         usage()
425         return 2
426
427     if len(args) != 2:
428         usage()
429         return 2
430
431     input_filename, output_filename = args
432
433     skip_windows = False
434     split = False
435     for o, a in opts:
436         if o in ("-h", "--help"):
437             usage()
438             sys.exit()
439         elif o in ("-w", "--skip-windows"):
440             skip_windows = True
441
442     conv = GtkBuilderConverter(skip_windows=skip_windows)
443     conv.parse_file(input_filename)
444
445     xml = _indent(conv.to_xml())
446     if output_filename == "-":
447         print xml
448     else:
449         open(output_filename, 'w').write(xml)
450         print "Wrote", output_filename
451
452     return 0
453
454 if __name__ == "__main__":
455     sys.exit(main(sys.argv))