]> Pileus Git - ~andy/gtk/blob - gtk/gtk-builder-convert
Don't export emit_ok_response
[~andy/gtk] / gtk / gtk-builder-convert
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2006-2008 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 #  Toolbars
23
24 """Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
25 Converts Glade files into XML files which can be loaded with GtkBuilder.
26 The [INPUT] file is
27
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
31
32 When OUTPUT is -, write to standard input.
33
34 Examples:
35   gtk-builder-convert preference.glade preferences.ui
36
37 Report bugs to http://bugzilla.gnome.org/."""
38
39 import getopt
40 import os
41 import sys
42
43 from xml.dom import minidom, Node
44
45 WINDOWS = ['GtkWindow',
46            'GtkDialog',
47            'GtkFileChooserDialog',
48            'GtkMessageDialog']
49
50 # The subprocess is only available in Python 2.4+
51 try:
52     import subprocess
53     subprocess # pyflakes
54 except ImportError:
55     subprocess = None
56
57 def get_child_nodes(node):
58     assert node.tagName == 'object'
59     nodes = []
60     for child in node.childNodes:
61         if child.nodeType != Node.ELEMENT_NODE:
62             continue
63         if child.tagName != 'child':
64             continue
65         nodes.append(child)
66     return nodes
67
68 def get_properties(node):
69     assert node.tagName == 'object'
70     properties = {}
71     for child in node.childNodes:
72         if child.nodeType != Node.ELEMENT_NODE:
73             continue
74         if child.tagName != 'property':
75             continue
76         value = child.childNodes[0].data
77         properties[child.getAttribute('name')] = value
78     return properties
79
80 def get_property(node, property_name):
81     assert node.tagName == 'object'
82     properties = get_properties(node)
83     return properties.get(property_name)
84
85 def get_property_node(node, property_name):
86     assert node.tagName == 'object'
87     properties = {}
88     for child in node.childNodes:
89         if child.nodeType != Node.ELEMENT_NODE:
90             continue
91         if child.tagName != 'property':
92             continue
93         if child.getAttribute('name') == property_name:
94             return child
95
96 def get_signal_nodes(node):
97     assert node.tagName == 'object'
98     signals = []
99     for child in node.childNodes:
100         if child.nodeType != Node.ELEMENT_NODE:
101             continue
102         if child.tagName == 'signal':
103             signals.append(child)
104     return signals
105
106 def get_property_nodes(node):
107     assert node.tagName == 'object'
108     properties = []
109     for child in node.childNodes:
110         if child.nodeType != Node.ELEMENT_NODE:
111             continue
112         # FIXME: handle comments
113         if child.tagName == 'property':
114             properties.append(child)
115     return properties
116
117 def get_accelerator_nodes(node):
118     assert node.tagName == 'object'
119     accelerators = []
120     for child in node.childNodes:
121         if child.nodeType != Node.ELEMENT_NODE:
122             continue
123         if child.tagName == 'accelerator':
124             accelerators.append(child)
125     return accelerators
126
127 def get_object_node(child_node):
128     assert child_node.tagName == 'child', child_node
129     nodes = []
130     for node in child_node.childNodes:
131         if node.nodeType != Node.ELEMENT_NODE:
132             continue
133         if node.tagName == 'object':
134             nodes.append(node)
135     assert len(nodes) == 1, nodes
136     return nodes[0]
137
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
144
145     return node
146
147 class GtkBuilderConverter(object):
148
149     def __init__(self, skip_windows, root):
150         self.skip_windows = skip_windows
151         self.root = root
152         self.root_objects = []
153         self.objects = {}
154
155     #
156     # Public API
157     #
158
159     def parse_file(self, file):
160         self._dom = minidom.parse(file)
161         self._parse()
162
163     def parse_buffer(self, buffer):
164         self._dom = minidom.parseString(buffer)
165         self._parse()
166
167     def to_xml(self):
168         xml = self._dom.toprettyxml("", "")
169         return xml.encode('utf-8')
170
171     #
172     # Private
173     #
174
175     def _get_object(self, name):
176         return self.objects.get(name)
177
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]
181
182     def _create_object(self, obj_class, obj_id, template=None, properties=None):
183         """
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.
190
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
197         """
198         if template is not None:
199             count = 1
200             while True:
201                 obj_id = template + str(count)
202                 widget = self._get_object(obj_id)
203                 if widget is None:
204                     break
205
206                 count += 1
207
208         obj = self._dom.createElement('object')
209         obj.setAttribute('class', obj_class)
210         obj.setAttribute('id', obj_id)
211         if properties:
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
216                     prop = value
217                 else:
218                     prop = self._dom.createElement('property')
219                     prop.appendChild(self._dom.createTextNode(value))
220
221                 prop.setAttribute('name', str(name))
222                 obj.appendChild(prop)
223         self.objects[obj_id] = obj
224         return obj
225
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)
229         return obj
230
231     def _parse(self):
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]
238
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)
244
245         # Strip unsupported tags
246         for tag in ['requires', 'requires-version']:
247             for child in self._dom.getElementsByTagName(tag):
248                 child.parentNode.removeChild(child)
249
250         if self.root:
251             self._strip_root(self.root)
252
253         # Rename widget to object
254         objects = self._dom.getElementsByTagName("widget")
255         for node in objects:
256             node.tagName = "object"
257
258         for node in objects:
259             self._convert(node.getAttribute("class"), node)
260             self.objects[node.getAttribute('id')] = node
261
262         # Convert Gazpachos UI tag
263         for node in self._dom.getElementsByTagName("ui"):
264             self._convert_ui(node)
265
266         # Convert accessibility tag
267         for node in self._dom.getElementsByTagName("accessibility"):
268             self._convert_accessibility(node)
269
270         # Output the newly created root objects and sort them
271         # by attribute id
272         # FIXME: Use sorted(self.root_objects,
273         #                   key=lambda n: n.getAttribute('id'),
274         #                   reverse=True):
275         # when we can depend on python 2.4 or higher
276         root_objects = self.root_objects[:]
277         root_objects.sort(lambda a, b: cmp(b.getAttribute('id'),
278                                            a.getAttribute('id')))
279         for obj in root_objects:
280             self._interface.childNodes.insert(0, obj)
281
282     def _convert(self, klass, node):
283         if klass == 'GtkNotebook':
284             self._packing_prop_to_child_attr(node, "type", "tab")
285         elif klass in ['GtkExpander', 'GtkFrame']:
286             self._packing_prop_to_child_attr(
287                 node, "type", "label_item", "label")
288         elif klass == "GtkMenuBar":
289             self._convert_menu(node)
290         elif klass == "GtkMenu":
291             # Only convert toplevel popups
292             if node.parentNode == self._interface:
293                 self._convert_menu(node, popup=True)
294         elif klass in WINDOWS and self.skip_windows:
295             self._remove_window(node)
296         self._default_widget_converter(node)
297
298     def _default_widget_converter(self, node):
299         klass = node.getAttribute("class")
300         for prop in get_property_nodes(node):
301             prop_name = prop.getAttribute("name")
302             if prop_name == "sizegroup":
303                 self._convert_sizegroup(node, prop)
304             elif prop_name == "tooltip" and klass != "GtkAction":
305                 prop.setAttribute("name", "tooltip-text")
306             elif prop_name in ["response_id", 'response-id']:
307                 # It does not make sense to convert responses when
308                 # we're not going to output dialogs
309                 if self.skip_windows:
310                     continue
311                 object_id = node.getAttribute('id')
312                 response = prop.childNodes[0].data
313                 self._convert_dialog_response(node, object_id, response)
314                 prop.parentNode.removeChild(prop)
315             elif prop_name == "adjustment":
316                 self._convert_adjustment(prop)
317             elif prop_name == "items" and klass in ['GtkComboBox',
318                                                     'GtkComboBoxEntry']:
319                 self._convert_combobox_items(node, prop)
320             elif prop_name == "text" and klass == 'GtkTextView':
321                 self._convert_textview_text(prop)
322
323     def _remove_window(self, node):
324         object_node = get_object_node(get_child_nodes(node)[0])
325         parent = node.parentNode
326         parent.removeChild(node)
327         parent.appendChild(object_node)
328
329     def _convert_menu(self, node, popup=False):
330         if node.hasAttribute('constructor'):
331             return
332
333         uimgr = self._create_root_object('GtkUIManager',
334                                          template='uimanager')
335
336         if popup:
337             name = 'popup'
338         else:
339             name = 'menubar'
340
341         menu = self._dom.createElement(name)
342         menu.setAttribute('name', node.getAttribute('id'))
343         node.setAttribute('constructor', uimgr.getAttribute('id'))
344
345         for child in get_child_nodes(node):
346             obj_node = get_object_node(child)
347             item = self._convert_menuitem(uimgr, obj_node)
348             menu.appendChild(item)
349             child.removeChild(obj_node)
350             child.parentNode.removeChild(child)
351
352         ui = self._dom.createElement('ui')
353         uimgr.appendChild(ui)
354
355         ui.appendChild(menu)
356
357     def _convert_menuitem(self, uimgr, obj_node):
358         children = get_child_nodes(obj_node)
359         name = 'menuitem'
360         if children:
361             child_node = children[0]
362             menu_node = get_object_node(child_node)
363             # Can be GtkImage, which will take care of later.
364             if menu_node.getAttribute('class') == 'GtkMenu':
365                 name = 'menu'
366
367         object_class = obj_node.getAttribute('class')
368         if object_class in ['GtkMenuItem',
369                             'GtkImageMenuItem',
370                             'GtkCheckMenuItem',
371                             'GtkRadioMenuItem']:
372             menu = self._dom.createElement(name)
373         elif object_class == 'GtkSeparatorMenuItem':
374             return self._dom.createElement('separator')
375         else:
376             raise NotImplementedError(object_class)
377
378         menu.setAttribute('action', obj_node.getAttribute('id'))
379         self._add_action_from_menuitem(uimgr, obj_node)
380         if children:
381             for child in get_child_nodes(menu_node):
382                 obj_node = get_object_node(child)
383                 item = self._convert_menuitem(uimgr, obj_node)
384                 menu.appendChild(item)
385                 child.removeChild(obj_node)
386                 child.parentNode.removeChild(child)
387         return menu
388
389     def _menuitem_to_action(self, node, properties):
390         copy_properties(node, ['label', 'tooltip'], properties)
391
392     def _togglemenuitem_to_action(self, node, properties):
393         self._menuitem_to_action(node, properties)
394         copy_properties(node, ['active'], properties)
395
396     def _radiomenuitem_to_action(self, node, properties):
397         self._togglemenuitem_to_action(node, properties)
398         copy_properties(node, ['group'], properties)
399
400     def _add_action_from_menuitem(self, uimgr, node):
401         properties = {}
402         object_class = node.getAttribute('class')
403         object_id = node.getAttribute('id')
404         if object_class == 'GtkMenuItem':
405             name = 'GtkAction'
406             self._menuitem_to_action(node, properties)
407         elif object_class == 'GtkCheckMenuItem':
408             name = 'GtkToggleAction'
409             self._togglemenuitem_to_action(node, properties)
410         elif object_class == 'GtkRadioMenuItem':
411             name = 'GtkRadioAction'
412             self._radiomenuitem_to_action(node, properties)
413         elif object_class == 'GtkImageMenuItem':
414             name = 'GtkAction'
415             children = get_child_nodes(node)
416             if (children and
417                 children[0].getAttribute('internal-child') == 'image'):
418                 image = get_object_node(children[0])
419                 child = get_property_node(image, 'stock')
420                 if child is not None:
421                     properties['stock_id'] = child
422             self._menuitem_to_action(node, properties)
423         elif object_class == 'GtkSeparatorMenuItem':
424             return
425         else:
426             raise NotImplementedError(object_class)
427
428         if get_property(node, 'use_stock') == 'True':
429             if 'label' in properties:
430                 properties['stock_id'] = properties['label']
431                 del properties['label']
432
433         properties['name'] = object_id
434         action = self._create_object(name,
435                                      object_id,
436                                      properties=properties)
437         for signal in get_signal_nodes(node):
438             signal_name = signal.getAttribute('name')
439             if signal_name in ['activate', 'toggled']:
440                 action.appendChild(signal)
441             else:
442                 print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
443                                                    signal_name)
444
445         if not uimgr.childNodes:
446             child = self._dom.createElement('child')
447             uimgr.appendChild(child)
448
449             group = self._create_object('GtkActionGroup', None,
450                                         template='actiongroup')
451             child.appendChild(group)
452         else:
453             group = uimgr.childNodes[0].childNodes[0]
454
455         child = self._dom.createElement('child')
456         group.appendChild(child)
457         child.appendChild(action)
458
459         for accelerator in get_accelerator_nodes(node):
460             signal_name = accelerator.getAttribute('signal')
461             if signal_name != 'activate':
462                 print 'Unhandled accelerator signal for %s::%s' % (
463                     node.getAttribute('class'), signal_name)
464                 continue
465             accelerator.removeAttribute('signal')
466             child.appendChild(accelerator)
467
468     def _convert_sizegroup(self, node, prop):
469         # This is Gazpacho only
470         node.removeChild(prop)
471         obj = self._get_object(prop.childNodes[0].data)
472         if obj is None:
473             widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
474             if widgets:
475                 obj = widgets[-1]
476             else:
477                 obj = self._create_root_object('GtkSizeGroup',
478                                                template='sizegroup')
479
480         widgets = obj.getElementsByTagName("widgets")
481         if widgets:
482             assert len(widgets) == 1
483             widgets = widgets[0]
484         else:
485             widgets = self._dom.createElement("widgets")
486             obj.appendChild(widgets)
487
488         member = self._dom.createElement("widget")
489         member.setAttribute("name", node.getAttribute("id"))
490         widgets.appendChild(member)
491
492     def _convert_dialog_response(self, node, object_name, response):
493         # 1) Get parent dialog node
494         while True:
495             # If we can't find the parent dialog, give up
496             if node == self._dom:
497                 return
498
499             if (node.tagName == 'object' and
500                 node.getAttribute('class') == 'GtkDialog'):
501                 dialog = node
502                 break
503             node = node.parentNode
504             assert node
505
506         # 2) Get dialogs action-widgets tag, create if not found
507         for child in dialog.childNodes:
508             if child.nodeType != Node.ELEMENT_NODE:
509                 continue
510             if child.tagName == 'action-widgets':
511                 actions = child
512                 break
513         else:
514             actions = self._dom.createElement("action-widgets")
515             dialog.appendChild(actions)
516
517         # 3) Add action-widget tag for the response
518         action = self._dom.createElement("action-widget")
519         action.setAttribute("response", response)
520         action.appendChild(self._dom.createTextNode(object_name))
521         actions.appendChild(action)
522
523     def _convert_adjustment(self, prop):
524         properties = {}
525         if prop.childNodes:
526             data = prop.childNodes[0].data
527             value, lower, upper, step, page, page_size = data.split(' ')
528             properties.update(value=value,
529                               lower=lower,
530                               upper=upper,
531                               step_increment=step,
532                               page_increment=page,
533                               page_size=page_size)
534         else:
535             prop.appendChild(self._dom.createTextNode(""))
536
537         adj = self._create_root_object("GtkAdjustment",
538                                        template='adjustment',
539                                        properties=properties)
540         prop.childNodes[0].data = adj.getAttribute('id')
541
542     def _convert_combobox_items(self, node, prop):
543         parent = prop.parentNode
544         if not prop.childNodes:
545             parent.removeChild(prop)
546             return
547         value = prop.childNodes[0].data
548         model = self._create_root_object("GtkListStore",
549                                          template="model")
550
551         columns = self._dom.createElement('columns')
552         model.appendChild(columns)
553
554         column = self._dom.createElement('column')
555         column.setAttribute('type', 'gchararray')
556         columns.appendChild(column)
557
558         data = self._dom.createElement('data')
559         model.appendChild(data)
560
561         for item in value.split('\n'):
562             row = self._dom.createElement('row')
563             data.appendChild(row)
564
565             col = self._dom.createElement('col')
566             col.setAttribute('id', '0')
567             col.setAttribute('translatable', 'yes')
568             col.appendChild(self._dom.createTextNode(item))
569             row.appendChild(col)
570
571         model_prop = self._dom.createElement('property')
572         model_prop.setAttribute('name', 'model')
573         model_prop.appendChild(
574             self._dom.createTextNode(model.getAttribute('id')))
575         parent.appendChild(model_prop)
576
577         parent.removeChild(prop)
578
579         child = self._dom.createElement('child')
580         node.appendChild(child)
581         cell_renderer = self._create_object('GtkCellRendererText', None,
582                                             template='renderer')
583         child.appendChild(cell_renderer)
584
585         attributes = self._dom.createElement('attributes')
586         child.appendChild(attributes)
587
588         attribute = self._dom.createElement('attribute')
589         attributes.appendChild(attribute)
590         attribute.setAttribute('name', 'text')
591         attribute.appendChild(self._dom.createTextNode('0'))
592
593     def _convert_textview_text(self, prop):
594         if not prop.childNodes:
595             prop.parentNode.removeChild(prop)
596             return
597
598         data = prop.childNodes[0].data
599         if prop.hasAttribute('translatable'):
600             prop.removeAttribute('translatable')
601         tbuffer = self._create_root_object("GtkTextBuffer",
602                                            template='textbuffer',
603                                            properties=dict(text=data))
604         prop.childNodes[0].data = tbuffer.getAttribute('id')
605         prop.setAttribute('name', 'buffer')
606
607     def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
608                                    attr_val=None):
609         for child in get_child_nodes(node):
610             packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
611             if not packing_props:
612                 continue
613             assert len(packing_props) == 1
614             packing_prop = packing_props[0]
615             properties = packing_prop.getElementsByTagName("property")
616             for prop in properties:
617                 if (prop.getAttribute("name") != prop_name or
618                     prop.childNodes[0].data != prop_val):
619                     continue
620                 packing_prop.removeChild(prop)
621                 child.setAttribute(prop_name, attr_val or prop_val)
622             if len(properties) == 1:
623                 child.removeChild(packing_prop)
624
625     def _convert_ui(self, node):
626         cdata = node.childNodes[0]
627         data = cdata.toxml().strip()
628         if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
629             return
630         data = data[9:-3]
631         child = minidom.parseString(data).childNodes[0]
632         nodes = child.childNodes[:]
633         for child_node in nodes:
634             node.appendChild(child_node)
635         node.removeChild(cdata)
636         if not node.hasAttribute("id"):
637             return
638
639         # Updating references made by widgets
640         parent_id = node.parentNode.getAttribute("id")
641         for widget in self._get_objects_by_attr("constructor",
642                                                 node.getAttribute("id")):
643             widget.getAttributeNode("constructor").value = parent_id
644         node.removeAttribute("id")
645
646     def _convert_accessibility(self, node):
647         objectNode = node.parentNode
648         parent_id = objectNode.getAttribute("id")
649
650         properties = {}
651         for node in node.childNodes:
652             if node.nodeName == 'atkproperty':
653                 node.tagName = 'property'
654                 properties[node.getAttribute('name')] = node
655                 node.parentNode.removeChild(node)
656             elif node.nodeName == 'atkrelation':
657                 node.tagName = 'relation'
658                 relation_type = node.getAttribute('type')
659                 relation_type = relation_type.replace('_', '-')
660                 node.setAttribute('type', relation_type)
661             elif node.nodeName == 'atkaction':
662                 node.tagName = 'action'
663
664         if properties:
665             child = self._dom.createElement('child')
666             child.setAttribute("internal-child", "accessible")
667
668             atkobject = self._create_object(
669                 "AtkObject", None,
670                 template='a11y-%s' % (parent_id,),
671                 properties=properties)
672             child.appendChild(atkobject)
673             objectNode.appendChild(child)
674
675     def _strip_root(self, root_name):
676         for widget in self._dom.getElementsByTagName("widget"):
677             if widget.getAttribute('id') == root_name:
678                 break
679         else:
680             raise SystemExit("Could not find an object called `%s'" % (
681                 root_name))
682
683         for child in self._interface.childNodes[:]:
684             if child.nodeType != Node.ELEMENT_NODE:
685                 continue
686             child.parentNode.removeChild(child)
687
688         self._interface.appendChild(widget)
689
690
691 def _indent(output):
692     if not subprocess:
693         return output
694
695     for directory in os.environ['PATH'].split(os.pathsep):
696         filename = os.path.join(directory, 'xmllint')
697         if os.path.exists(filename):
698             break
699     else:
700         return output
701
702     s = subprocess.Popen([filename, '--format', '-'],
703                          stdin=subprocess.PIPE,
704                          stdout=subprocess.PIPE)
705     s.stdin.write(output)
706     s.stdin.close()
707     return s.stdout.read()
708
709 def usage():
710     print __doc__
711
712 def main(args):
713     try:
714         opts, args = getopt.getopt(args[1:], "hwr:",
715                                    ["help", "skip-windows", "root="])
716     except getopt.GetoptError:
717         usage()
718         return 2
719
720     if len(args) != 2:
721         usage()
722         return 2
723
724     input_filename, output_filename = args
725
726     skip_windows = False
727     split = False
728     root = None
729     for o, a in opts:
730         if o in ("-h", "--help"):
731             usage()
732             sys.exit()
733         elif o in ("-r", "--root"):
734             root = a
735         elif o in ("-w", "--skip-windows"):
736             skip_windows = True
737
738     conv = GtkBuilderConverter(skip_windows=skip_windows,
739                                root=root)
740     conv.parse_file(input_filename)
741
742     xml = _indent(conv.to_xml())
743     if output_filename == "-":
744         print xml
745     else:
746         open(output_filename, 'w').write(xml)
747         print "Wrote", output_filename
748
749     return 0
750
751 if __name__ == "__main__":
752     sys.exit(main(sys.argv))