gtk-builder-convert 23.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#!/usr/bin/env python
#
# Copyright (C) 2006-2007 Async Open Source
#                         Henrique Romano <henrique@async.com.br>
#                         Johan Dahlin <jdahlin@async.com.br>
#
# 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

24 25 26 27
"""Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
Converts Glade files into XML files which can be loaded with GtkBuilder.
The [INPUT] file is

Matthias Clasen's avatar
Matthias Clasen committed
28
  -w, --skip-windows     Convert everything but GtkWindow subclasses.
29
  -r, --root             Convert only widget named root and its children
30 31 32 33 34 35 36 37 38 39
  -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
40
import os
41 42 43 44
import sys

from xml.dom import minidom, Node

45 46 47 48 49
WINDOWS = ['GtkWindow',
           'GtkDialog',
           'GtkFileChooserDialog',
           'GtkMessageDialog']

50 51 52 53 54 55 56
# The subprocess is only available in Python 2.4+
try:
    import subprocess
    subprocess # pyflakes
except ImportError:
    subprocess = None

57
def get_child_nodes(node):
58
    assert node.tagName == 'object'
59 60 61 62 63 64 65 66 67
    nodes = []
    for child in node.childNodes:
        if child.nodeType == Node.TEXT_NODE:
            continue
        if child.tagName != 'child':
            continue
        nodes.append(child)
    return nodes

68
def get_properties(node):
69
    assert node.tagName == 'object'
70 71 72 73 74 75 76 77 78 79
    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

80 81
def get_property(node, property_name):
    assert node.tagName == 'object'
82
    properties = get_properties(node)
83 84
    return properties.get(property_name)

85 86 87 88 89 90 91 92 93 94
def get_signal_nodes(node):
    assert node.tagName == 'object'
    signals = []
    for child in node.childNodes:
        if child.nodeType == Node.TEXT_NODE:
            continue
        if child.tagName == 'signal':
            signals.append(child)
    return signals

95 96 97 98 99 100 101 102 103 104
def get_property_nodes(node):
    assert node.tagName == 'object'
    properties = []
    for child in node.childNodes:
        if child.nodeType == Node.TEXT_NODE:
            continue
        if child.tagName == 'property':
            properties.append(child)
    return properties

105 106 107 108 109 110 111 112 113 114
def get_accelerator_nodes(node):
    assert node.tagName == 'object'
    accelerators = []
    for child in node.childNodes:
        if child.nodeType == Node.TEXT_NODE:
            continue
        if child.tagName == 'accelerator':
            accelerators.append(child)
    return accelerators

115 116 117 118 119 120 121 122 123 124 125
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]

126 127 128 129 130 131
def copy_properties(node, props, prop_dict):
    assert node.tagName == 'object'
    for prop_name in props:
        value = get_property(node, prop_name)
        if value is not None:
            prop_dict[prop_name] = value
132

133 134
class GtkBuilderConverter(object):

135
    def __init__(self, skip_windows, root):
136
        self.skip_windows = skip_windows
137
        self.root = root
138 139
        self.root_objects = []
        self.objects = {}
140

141 142 143 144 145 146 147 148 149 150 151 152 153
    #
    # 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):
154 155
        xml = self._dom.toprettyxml("", "")
        return xml.encode('utf-8')
156 157 158 159 160

    #
    # Private
    #

161 162 163 164
    def _get_object(self, name):
        return self.objects.get(name)

    def _get_objects_by_attr(self, attribute, value):
165 166 167
        return [w for w in self._dom.getElementsByTagName("object")
                      if w.getAttribute(attribute) == value]

168 169 170 171 172
    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)
173
                widget = self._get_object(obj_id)
174 175 176 177 178
                if widget is None:
                    break

                count += 1

179 180 181 182 183 184 185 186
        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)
187 188 189 190 191 192
        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)
193 194 195 196 197 198
        return obj

    def _parse(self):
        glade_iface = self._dom.getElementsByTagName("glade-interface")
        assert glade_iface, ("Badly formed XML, there is "
                             "no <glade-interface> tag.")
199
        # Rename glade-interface to interface
200 201 202
        glade_iface[0].tagName = 'interface'
        self._interface = glade_iface[0]

203 204 205 206 207 208
        # 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)

209
        # Strip unsupported tags
210
        for tag in ['requires']:
211 212
            for child in self._dom.getElementsByTagName(tag):
                child.parentNode.removeChild(child)
213

214 215 216
        if self.root:
            self._strip_root(self.root)

217
        # Rename widget to object
218 219 220 221 222
        objects = self._dom.getElementsByTagName("widget")
        for node in objects:
            node.tagName = "object"

        for node in objects:
223
            self._convert(node.getAttribute("class"), node)
224
            self.objects[node.getAttribute('id')] = node
225 226 227 228 229

        # Convert Gazpachos UI tag
        for node in self._dom.getElementsByTagName("ui"):
            self._convert_ui(node)

230 231 232 233 234 235 236
        # 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)

237 238 239 240 241 242 243
    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":
244
            self._convert_menu(node)
245 246 247 248
        elif klass == "GtkMenu":
            # Only convert toplevel popups
            if node.parentNode == self._interface:
                self._convert_menu(node, popup=True)
249 250
        elif klass in WINDOWS and self.skip_windows:
            self._remove_window(node)
251 252 253 254
        self._default_widget_converter(node)

    def _default_widget_converter(self, node):
        klass = node.getAttribute("class")
255
        for prop in get_property_nodes(node):
256 257 258 259 260 261
            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']:
262 263 264 265
                # It does not make sense to convert responses when
                # we're not going to output dialogs
                if self.skip_windows:
                    continue
266 267 268 269
                object_id = node.getAttribute('id')
                response = prop.childNodes[0].data
                self._convert_dialog_response(node, object_id, response)
                prop.parentNode.removeChild(prop)
270 271
            elif prop_name == "adjustment":
                self._convert_adjustment(prop)
272 273 274
            elif prop_name == "items" and klass in ['GtkComboBox',
                                                    'GtkComboBoxEntry']:
                self._convert_combobox_items(node, prop)
275 276
            elif prop_name == "text" and klass == 'GtkTextView':
                self._convert_textview_text(prop)
277

278 279 280 281 282 283
    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)

284
    def _convert_menu(self, node, popup=False):
285 286 287 288 289 290
        if node.hasAttribute('constructor'):
            return

        uimgr = self._create_root_object('GtkUIManager',
                                         template='uimanager')

291 292 293 294
        if popup:
            name = 'popup'
        else:
            name = 'menubar'
295

296
        menu = self._dom.createElement(name)
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
        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):
313 314 315 316 317 318 319 320 321 322
        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')
323 324
        if object_class in ['GtkMenuItem',
                            'GtkImageMenuItem',
325 326
                            'GtkCheckMenuItem',
                            'GtkRadioMenuItem']:
327 328
            menu = self._dom.createElement(name)
        elif object_class == 'GtkSeparatorMenuItem':
329
            return self._dom.createElement('separator')
330 331
        else:
            raise NotImplementedError(object_class)
332

333 334 335 336 337
        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)
338 339
                item = self._convert_menuitem(uimgr, obj_node)
                menu.appendChild(item)
340 341
                child.removeChild(obj_node)
                child.parentNode.removeChild(child)
342
        return menu
343

344 345 346 347 348 349 350 351 352 353 354
    def _menuitem_to_action(self, node, properties):
        copy_properties(node, ['label'], 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)

355 356 357 358
    def _add_action_from_menuitem(self, uimgr, node):
        properties = {}
        object_class = node.getAttribute('class')
        object_id = node.getAttribute('id')
359 360 361 362 363 364 365 366 367 368
        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':
369 370 371 372 373
            name = 'GtkAction'
            children = get_child_nodes(node)
            if (children and
                children[0].getAttribute('internal-child') == 'image'):
                image = get_object_node(children[0])
374
                stock_id = get_property(image, 'stock')
375 376
                if stock_id is not None:
                    properties['stock_id'] = stock_id
377 378 379 380 381
        elif object_class == 'GtkSeparatorMenuItem':
            return
        else:
            raise NotImplementedError(object_class)

382
        if get_property(node, 'use_stock') == 'True':
383 384 385 386
            stock_id = get_property(node, 'label')
            if stock_id is not None:
                properties['stock_id'] = stock_id

387 388 389 390
        properties['name'] = object_id
        action = self._create_object(name,
                                     object_id,
                                     **properties)
391 392 393 394 395 396 397 398 399

        for signal in get_signal_nodes(node):
            signal_name = signal.getAttribute('name')
            if signal_name == 'activate':
                action.appendChild(signal)
            else:
                print 'Unhandled signal %s::%s' % (node.getAttribute('class'),
                                                   signal_name)

400 401 402 403
        if not uimgr.childNodes:
            child = self._dom.createElement('child')
            uimgr.appendChild(child)

404 405
            group = self._create_object('GtkActionGroup', None,
                                        template='actiongroup')
406 407 408 409 410 411 412 413
            child.appendChild(group)
        else:
            group = uimgr.childNodes[0].childNodes[0]

        child = self._dom.createElement('child')
        group.appendChild(child)
        child.appendChild(action)

414
        for accelerator in get_accelerator_nodes(node):
415
            signal_name = accelerator.getAttribute('signal')
416 417 418 419 420 421 422
            if signal_name != 'activate':
                print 'Unhandled accelerator signal for %s::%s' % (
                    node.getAttribute('class'), signal_name)
                continue
            accelerator.removeAttribute('signal')
            child.appendChild(accelerator)

423 424 425
    def _convert_sizegroup(self, node, prop):
        # This is Gazpacho only
        node.removeChild(prop)
426
        obj = self._get_object(prop.childNodes[0].data)
427
        if obj is None:
428
            widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
429 430 431
            if widgets:
                obj = widgets[-1]
            else:
432 433
                obj = self._create_root_object('GtkSizeGroup',
                                               template='sizegroup')
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449

        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:
450 451 452 453
            # If we can't find the parent dialog, give up
            if node == self._dom:
                return

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
            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)

478 479 480
    def _convert_adjustment(self, prop):
        data = prop.childNodes[0].data
        value, lower, upper, step, page, page_size = data.split(' ')
481
        adj = self._create_root_object("GtkAdjustment",
482 483 484 485 486 487 488
                                       template='adjustment',
                                       value=value,
                                       lower=lower,
                                       upper=upper,
                                       step_increment=step,
                                       page_increment=page,
                                       page_size=page_size)
489
        prop.childNodes[0].data = adj.getAttribute('id')
490

491 492 493 494
    def _convert_combobox_items(self, node, prop):
        if not prop.childNodes:
            return
        value = prop.childNodes[0].data
495 496
        model = self._create_root_object("GtkListStore",
                                         template="model")
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539

        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'))

540
    def _convert_textview_text(self, prop):
541 542 543 544
        if not prop.childNodes:
            prop.parentNode.removeChild(prop)
            return

545 546 547
        data = prop.childNodes[0].data
        if prop.hasAttribute('translatable'):
            prop.removeAttribute('translatable')
548 549 550
        tbuffer = self._create_root_object("GtkTextBuffer",
                                           template='textbuffer',
                                           text=data)
551 552
        prop.childNodes[0].data = tbuffer.getAttribute('id')

553 554
    def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
                                   attr_val=None):
555
        for child in get_child_nodes(node):
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
            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("<![CDATA[") or not data.endswith("]]>"):
            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")
587
        for widget in self._get_objects_by_attr("constructor",
588 589 590 591
                                                node.getAttribute("id")):
            widget.getAttributeNode("constructor").value = parent_id
        node.removeAttribute("id")

592
    def _strip_root(self, root_name):
593 594 595 596
        for widget in self._dom.getElementsByTagName("widget"):
            if widget.getAttribute('id') == root_name:
                break
        else:
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
            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)


613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
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()

631 632 633 634 635
def usage():
    print __doc__

def main(args):
    try:
636 637
        opts, args = getopt.getopt(args[1:], "hwr:",
                                   ["help", "skip-windows", "root="])
638 639 640 641 642 643 644 645 646 647 648 649
    except getopt.GetoptError:
        usage()
        return 2

    if len(args) != 2:
        usage()
        return 2

    input_filename, output_filename = args

    skip_windows = False
    split = False
650
    root = None
651 652 653 654
    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit()
655 656
        elif o in ("-r", "--root"):
            root = a
657 658 659
        elif o in ("-w", "--skip-windows"):
            skip_windows = True

660 661
    conv = GtkBuilderConverter(skip_windows=skip_windows,
                               root=root)
662 663 664 665 666 667 668 669
    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
670

671
    return 0
672 673

if __name__ == "__main__":
674
    sys.exit(main(sys.argv))