1#############################################################################
2##
3## Copyright (C) 2019 Riverbank Computing Limited.
4## Copyright (C) 2006 Thorsten Marek.
5## All right reserved.
6##
7## This file is part of PyQt.
8##
9## You may use this file under the terms of the GPL v2 or the revised BSD
10## license as follows:
11##
12## "Redistribution and use in source and binary forms, with or without
13## modification, are permitted provided that the following conditions are
14## met:
15##   * Redistributions of source code must retain the above copyright
16##     notice, this list of conditions and the following disclaimer.
17##   * Redistributions in binary form must reproduce the above copyright
18##     notice, this list of conditions and the following disclaimer in
19##     the documentation and/or other materials provided with the
20##     distribution.
21##   * Neither the name of the Riverbank Computing Limited nor the names
22##     of its contributors may be used to endorse or promote products
23##     derived from this software without specific prior written
24##     permission.
25##
26## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
37##
38#############################################################################
39
40
41import sys
42import logging
43import os
44import re
45from xml.etree.ElementTree import parse, SubElement
46
47from .objcreator import QObjectCreator
48from .properties import Properties
49
50
51logger = logging.getLogger(__name__)
52DEBUG = logger.debug
53
54QtCore = None
55QtWidgets = None
56
57
58def _parse_alignment(alignment):
59    """ Convert a C++ alignment to the corresponding flags. """
60
61    align_flags = None
62    for qt_align in alignment.split('|'):
63        _, qt_align = qt_align.split('::')
64        align = getattr(QtCore.Qt, qt_align)
65
66        if align_flags is None:
67            align_flags = align
68        else:
69            align_flags |= align
70
71    return align_flags
72
73
74def _layout_position(elem):
75    """ Return either (), (0, alignment), (row, column, rowspan, colspan) or
76    (row, column, rowspan, colspan, alignment) depending on the type of layout
77    and its configuration.  The result will be suitable to use as arguments to
78    the layout.
79    """
80
81    row = elem.attrib.get('row')
82    column = elem.attrib.get('column')
83    alignment = elem.attrib.get('alignment')
84
85    # See if it is a box layout.
86    if row is None or column is None:
87        if alignment is None:
88            return ()
89
90        return (0, _parse_alignment(alignment))
91
92    # It must be a grid or a form layout.
93    row = int(row)
94    column = int(column)
95
96    rowspan = int(elem.attrib.get('rowspan', 1))
97    colspan = int(elem.attrib.get('colspan', 1))
98
99    if alignment is None:
100        return (row, column, rowspan, colspan)
101
102    return (row, column, rowspan, colspan, _parse_alignment(alignment))
103
104
105class WidgetStack(list):
106    topwidget = None
107    def push(self, item):
108        DEBUG("push %s %s" % (item.metaObject().className(),
109                              item.objectName()))
110        self.append(item)
111        if isinstance(item, QtWidgets.QWidget):
112            self.topwidget = item
113
114    def popLayout(self):
115        layout = list.pop(self)
116        DEBUG("pop layout %s %s" % (layout.metaObject().className(),
117                                    layout.objectName()))
118        return layout
119
120    def popWidget(self):
121        widget = list.pop(self)
122        DEBUG("pop widget %s %s" % (widget.metaObject().className(),
123                                    widget.objectName()))
124        for item in reversed(self):
125            if isinstance(item, QtWidgets.QWidget):
126                self.topwidget = item
127                break
128        else:
129            self.topwidget = None
130        DEBUG("new topwidget %s" % (self.topwidget,))
131        return widget
132
133    def peek(self):
134        return self[-1]
135
136    def topIsLayout(self):
137        return isinstance(self[-1], QtWidgets.QLayout)
138
139    def topIsLayoutWidget(self):
140        # A plain QWidget is a layout widget unless it's parent is a
141        # QMainWindow or a container widget.  Note that the corresponding uic
142        # test is a little more complicated as it involves features not
143        # supported by pyuic.
144
145        if type(self[-1]) is not QtWidgets.QWidget:
146            return False
147
148        if len(self) < 2:
149            return False
150
151        parent = self[-2]
152
153        return isinstance(parent, QtWidgets.QWidget) and type(parent) not in (
154                QtWidgets.QMainWindow,
155                QtWidgets.QStackedWidget,
156                QtWidgets.QToolBox,
157                QtWidgets.QTabWidget,
158                QtWidgets.QScrollArea,
159                QtWidgets.QMdiArea,
160                QtWidgets.QWizard,
161                QtWidgets.QDockWidget)
162
163
164class ButtonGroup(object):
165    """ Encapsulate the configuration of a button group and its implementation.
166    """
167
168    def __init__(self):
169        """ Initialise the button group. """
170
171        self.exclusive = True
172        self.object = None
173
174
175class UIParser(object):
176    def __init__(self, qtcore_module, qtgui_module, qtwidgets_module, creatorPolicy):
177        self.factory = QObjectCreator(creatorPolicy)
178        self.wprops = Properties(self.factory, qtcore_module, qtgui_module,
179                qtwidgets_module)
180
181        global QtCore, QtWidgets
182        QtCore = qtcore_module
183        QtWidgets = qtwidgets_module
184
185        self.reset()
186
187    def uniqueName(self, name):
188        """UIParser.uniqueName(string) -> string
189
190        Create a unique name from a string.
191        >>> p = UIParser(QtCore, QtGui, QtWidgets)
192        >>> p.uniqueName("foo")
193        'foo'
194        >>> p.uniqueName("foo")
195        'foo1'
196        """
197        try:
198            suffix = self.name_suffixes[name]
199        except KeyError:
200            self.name_suffixes[name] = 0
201            return name
202
203        suffix += 1
204        self.name_suffixes[name] = suffix
205
206        return "%s%i" % (name, suffix)
207
208    def reset(self):
209        try: self.wprops.reset()
210        except AttributeError: pass
211        self.toplevelWidget = None
212        self.stack = WidgetStack()
213        self.name_suffixes = {}
214        self.defaults = {'spacing': -1, 'margin': -1}
215        self.actions = []
216        self.currentActionGroup = None
217        self.resources = []
218        self.button_groups = {}
219
220    def setupObject(self, clsname, parent, branch, is_attribute=True):
221        name = self.uniqueName(branch.attrib.get('name') or clsname[1:].lower())
222
223        if parent is None:
224            args = ()
225        else:
226            args = (parent, )
227
228        obj = self.factory.createQObject(clsname, name, args, is_attribute)
229
230        self.wprops.setProperties(obj, branch)
231        obj.setObjectName(name)
232
233        if is_attribute:
234            setattr(self.toplevelWidget, name, obj)
235
236        return obj
237
238    def getProperty(self, elem, name):
239        for prop in elem.findall('property'):
240            if prop.attrib['name'] == name:
241                return prop
242
243        return None
244
245    def createWidget(self, elem):
246        self.column_counter = 0
247        self.row_counter = 0
248        self.item_nr = 0
249        self.itemstack = []
250        self.sorting_enabled = None
251
252        widget_class = elem.attrib['class'].replace('::', '.')
253        if widget_class == 'Line':
254            widget_class = 'QFrame'
255
256        # Ignore the parent if it is a container.
257        parent = self.stack.topwidget
258        if isinstance(parent, (QtWidgets.QDockWidget, QtWidgets.QMdiArea,
259                               QtWidgets.QScrollArea, QtWidgets.QStackedWidget,
260                               QtWidgets.QToolBox, QtWidgets.QTabWidget,
261                               QtWidgets.QWizard)):
262            parent = None
263
264        self.stack.push(self.setupObject(widget_class, parent, elem))
265
266        if isinstance(self.stack.topwidget, QtWidgets.QTableWidget):
267            if self.getProperty(elem, 'columnCount') is None:
268                self.stack.topwidget.setColumnCount(len(elem.findall("column")))
269
270            if self.getProperty(elem, 'rowCount') is None:
271                self.stack.topwidget.setRowCount(len(elem.findall("row")))
272
273        self.traverseWidgetTree(elem)
274        widget = self.stack.popWidget()
275
276        if isinstance(widget, QtWidgets.QTreeView):
277            self.handleHeaderView(elem, "header", widget.header())
278
279        elif isinstance(widget, QtWidgets.QTableView):
280            self.handleHeaderView(elem, "horizontalHeader",
281                    widget.horizontalHeader())
282            self.handleHeaderView(elem, "verticalHeader",
283                    widget.verticalHeader())
284
285        elif isinstance(widget, QtWidgets.QAbstractButton):
286            bg_i18n = self.wprops.getAttribute(elem, "buttonGroup")
287            if bg_i18n is not None:
288                # This should be handled properly in case the problem arises
289                # elsewhere as well.
290                try:
291                    # We are compiling the .ui file.
292                    bg_name = bg_i18n.string
293                except AttributeError:
294                    # We are loading the .ui file.
295                    bg_name = bg_i18n
296
297				# Designer allows the creation of .ui files without explicit
298				# button groups, even though uic then issues warnings.  We
299				# handle it in two stages by first making sure it has a name
300				# and then making sure one exists with that name.
301                if not bg_name:
302                    bg_name = 'buttonGroup'
303
304                try:
305                    bg = self.button_groups[bg_name]
306                except KeyError:
307                    bg = self.button_groups[bg_name] = ButtonGroup()
308
309                if bg.object is None:
310                    bg.object = self.factory.createQObject("QButtonGroup",
311                            bg_name, (self.toplevelWidget, ))
312                    setattr(self.toplevelWidget, bg_name, bg.object)
313
314                    bg.object.setObjectName(bg_name)
315
316                    if not bg.exclusive:
317                        bg.object.setExclusive(False)
318
319                bg.object.addButton(widget)
320
321        if self.sorting_enabled is not None:
322            widget.setSortingEnabled(self.sorting_enabled)
323            self.sorting_enabled = None
324
325        if self.stack.topIsLayout():
326            lay = self.stack.peek()
327            lp = elem.attrib['layout-position']
328
329            if isinstance(lay, QtWidgets.QFormLayout):
330                lay.setWidget(lp[0], self._form_layout_role(lp), widget)
331            else:
332                lay.addWidget(widget, *lp)
333
334        topwidget = self.stack.topwidget
335
336        if isinstance(topwidget, QtWidgets.QToolBox):
337            icon = self.wprops.getAttribute(elem, "icon")
338            if icon is not None:
339                topwidget.addItem(widget, icon, self.wprops.getAttribute(elem, "label"))
340            else:
341                topwidget.addItem(widget, self.wprops.getAttribute(elem, "label"))
342
343            tooltip = self.wprops.getAttribute(elem, "toolTip")
344            if tooltip is not None:
345                topwidget.setItemToolTip(topwidget.indexOf(widget), tooltip)
346
347        elif isinstance(topwidget, QtWidgets.QTabWidget):
348            icon = self.wprops.getAttribute(elem, "icon")
349            if icon is not None:
350                topwidget.addTab(widget, icon, self.wprops.getAttribute(elem, "title"))
351            else:
352                topwidget.addTab(widget, self.wprops.getAttribute(elem, "title"))
353
354            tooltip = self.wprops.getAttribute(elem, "toolTip")
355            if tooltip is not None:
356                topwidget.setTabToolTip(topwidget.indexOf(widget), tooltip)
357
358        elif isinstance(topwidget, QtWidgets.QWizard):
359            topwidget.addPage(widget)
360
361        elif isinstance(topwidget, QtWidgets.QStackedWidget):
362            topwidget.addWidget(widget)
363
364        elif isinstance(topwidget, (QtWidgets.QDockWidget, QtWidgets.QScrollArea)):
365            topwidget.setWidget(widget)
366
367        elif isinstance(topwidget, QtWidgets.QMainWindow):
368            if type(widget) == QtWidgets.QWidget:
369                topwidget.setCentralWidget(widget)
370            elif isinstance(widget, QtWidgets.QToolBar):
371                tbArea = self.wprops.getAttribute(elem, "toolBarArea")
372
373                if tbArea is None:
374                    topwidget.addToolBar(widget)
375                else:
376                    topwidget.addToolBar(tbArea, widget)
377
378                tbBreak = self.wprops.getAttribute(elem, "toolBarBreak")
379
380                if tbBreak:
381                    topwidget.insertToolBarBreak(widget)
382
383            elif isinstance(widget, QtWidgets.QMenuBar):
384                topwidget.setMenuBar(widget)
385            elif isinstance(widget, QtWidgets.QStatusBar):
386                topwidget.setStatusBar(widget)
387            elif isinstance(widget, QtWidgets.QDockWidget):
388                dwArea = self.wprops.getAttribute(elem, "dockWidgetArea")
389                topwidget.addDockWidget(QtCore.Qt.DockWidgetArea(dwArea),
390                        widget)
391
392    def handleHeaderView(self, elem, name, header):
393        value = self.wprops.getAttribute(elem, name + "Visible")
394        if value is not None:
395            header.setVisible(value)
396
397        value = self.wprops.getAttribute(elem, name + "CascadingSectionResizes")
398        if value is not None:
399            header.setCascadingSectionResizes(value)
400
401        value = self.wprops.getAttribute(elem, name + "DefaultSectionSize")
402        if value is not None:
403            header.setDefaultSectionSize(value)
404
405        value = self.wprops.getAttribute(elem, name + "HighlightSections")
406        if value is not None:
407            header.setHighlightSections(value)
408
409        value = self.wprops.getAttribute(elem, name + "MinimumSectionSize")
410        if value is not None:
411            header.setMinimumSectionSize(value)
412
413        value = self.wprops.getAttribute(elem, name + "ShowSortIndicator")
414        if value is not None:
415            header.setSortIndicatorShown(value)
416
417        value = self.wprops.getAttribute(elem, name + "StretchLastSection")
418        if value is not None:
419            header.setStretchLastSection(value)
420
421    def createSpacer(self, elem):
422        width = elem.findtext("property/size/width")
423        height = elem.findtext("property/size/height")
424
425        if width is None or height is None:
426            size_args = ()
427        else:
428            size_args = (int(width), int(height))
429
430        sizeType = self.wprops.getProperty(elem, "sizeType",
431                QtWidgets.QSizePolicy.Expanding)
432
433        policy = (QtWidgets.QSizePolicy.Minimum, sizeType)
434
435        if self.wprops.getProperty(elem, "orientation") == QtCore.Qt.Horizontal:
436            policy = policy[1], policy[0]
437
438        spacer = self.factory.createQObject("QSpacerItem",
439                self.uniqueName("spacerItem"), size_args + policy,
440                is_attribute=False)
441
442        if self.stack.topIsLayout():
443            lay = self.stack.peek()
444            lp = elem.attrib['layout-position']
445
446            if isinstance(lay, QtWidgets.QFormLayout):
447                lay.setItem(lp[0], self._form_layout_role(lp), spacer)
448            else:
449                lay.addItem(spacer, *lp)
450
451    def createLayout(self, elem):
452        # We use an internal property to handle margins which will use separate
453        # left, top, right and bottom margins if they are found to be
454        # different.  The following will select, in order of preference,
455        # separate margins, the same margin in all directions, and the default
456        # margin.
457        margin = -1 if self.stack.topIsLayout() else self.defaults['margin']
458        margin = self.wprops.getProperty(elem, 'margin', margin)
459        left = self.wprops.getProperty(elem, 'leftMargin', margin)
460        top = self.wprops.getProperty(elem, 'topMargin', margin)
461        right = self.wprops.getProperty(elem, 'rightMargin', margin)
462        bottom = self.wprops.getProperty(elem, 'bottomMargin', margin)
463
464        # A layout widget should, by default, have no margins.
465        if self.stack.topIsLayoutWidget():
466            if left < 0: left = 0
467            if top < 0: top = 0
468            if right < 0: right = 0
469            if bottom < 0: bottom = 0
470
471        if left >= 0 or top >= 0 or right >= 0 or bottom >= 0:
472            # We inject the new internal property.
473            cme = SubElement(elem, 'property', name='pyuicMargins')
474            SubElement(cme, 'number').text = str(left)
475            SubElement(cme, 'number').text = str(top)
476            SubElement(cme, 'number').text = str(right)
477            SubElement(cme, 'number').text = str(bottom)
478
479        # We use an internal property to handle spacing which will use separate
480        # horizontal and vertical spacing if they are found to be different.
481        # The following will select, in order of preference, separate
482        # horizontal and vertical spacing, the same spacing in both directions,
483        # and the default spacing.
484        spacing = self.wprops.getProperty(elem, 'spacing',
485                self.defaults['spacing'])
486        horiz = self.wprops.getProperty(elem, 'horizontalSpacing', spacing)
487        vert = self.wprops.getProperty(elem, 'verticalSpacing', spacing)
488
489        if horiz >= 0 or vert >= 0:
490            # We inject the new internal property.
491            cme = SubElement(elem, 'property', name='pyuicSpacing')
492            SubElement(cme, 'number').text = str(horiz)
493            SubElement(cme, 'number').text = str(vert)
494
495        classname = elem.attrib["class"]
496        if self.stack.topIsLayout():
497            parent = None
498        else:
499            parent = self.stack.topwidget
500        if "name" not in elem.attrib:
501            elem.attrib["name"] = classname[1:].lower()
502        self.stack.push(self.setupObject(classname, parent, elem))
503        self.traverseWidgetTree(elem)
504
505        layout = self.stack.popLayout()
506        self.configureLayout(elem, layout)
507
508        if self.stack.topIsLayout():
509            top_layout = self.stack.peek()
510            lp = elem.attrib['layout-position']
511
512            if isinstance(top_layout, QtWidgets.QFormLayout):
513                top_layout.setLayout(lp[0], self._form_layout_role(lp), layout)
514            else:
515                top_layout.addLayout(layout, *lp)
516
517    def configureLayout(self, elem, layout):
518        if isinstance(layout, QtWidgets.QGridLayout):
519            self.setArray(elem, 'columnminimumwidth',
520                    layout.setColumnMinimumWidth)
521            self.setArray(elem, 'rowminimumheight',
522                    layout.setRowMinimumHeight)
523            self.setArray(elem, 'columnstretch', layout.setColumnStretch)
524            self.setArray(elem, 'rowstretch', layout.setRowStretch)
525
526        elif isinstance(layout, QtWidgets.QBoxLayout):
527            self.setArray(elem, 'stretch', layout.setStretch)
528
529    def setArray(self, elem, name, setter):
530        array = elem.attrib.get(name)
531        if array:
532            for idx, value in enumerate(array.split(',')):
533                value = int(value)
534                if value > 0:
535                    setter(idx, value)
536
537    def disableSorting(self, w):
538        if self.item_nr == 0:
539            self.sorting_enabled = self.factory.invoke("__sortingEnabled",
540                    w.isSortingEnabled)
541            w.setSortingEnabled(False)
542
543    def handleItem(self, elem):
544        if self.stack.topIsLayout():
545            elem[0].attrib['layout-position'] = _layout_position(elem)
546            self.traverseWidgetTree(elem)
547        else:
548            w = self.stack.topwidget
549
550            if isinstance(w, QtWidgets.QComboBox):
551                text = self.wprops.getProperty(elem, "text")
552                icon = self.wprops.getProperty(elem, "icon")
553
554                if icon:
555                    w.addItem(icon, '')
556                else:
557                    w.addItem('')
558
559                w.setItemText(self.item_nr, text)
560
561            elif isinstance(w, QtWidgets.QListWidget):
562                self.disableSorting(w)
563                item = self.createWidgetItem('QListWidgetItem', elem, w.item,
564                        self.item_nr)
565                w.addItem(item)
566
567            elif isinstance(w, QtWidgets.QTreeWidget):
568                if self.itemstack:
569                    parent, _ = self.itemstack[-1]
570                    _, nr_in_root = self.itemstack[0]
571                else:
572                    parent = w
573                    nr_in_root = self.item_nr
574
575                item = self.factory.createQObject("QTreeWidgetItem",
576                        "item_%d" % len(self.itemstack), (parent, ), False)
577
578                if self.item_nr == 0 and not self.itemstack:
579                    self.sorting_enabled = self.factory.invoke("__sortingEnabled", w.isSortingEnabled)
580                    w.setSortingEnabled(False)
581
582                self.itemstack.append((item, self.item_nr))
583                self.item_nr = 0
584
585                # We have to access the item via the tree when setting the
586                # text.
587                titm = w.topLevelItem(nr_in_root)
588                for child, nr_in_parent in self.itemstack[1:]:
589                    titm = titm.child(nr_in_parent)
590
591                column = -1
592                for prop in elem.findall('property'):
593                    c_prop = self.wprops.convert(prop)
594                    c_prop_name = prop.attrib['name']
595
596                    if c_prop_name == 'text':
597                        column += 1
598                        if c_prop:
599                            titm.setText(column, c_prop)
600                    elif c_prop_name == 'statusTip':
601                        item.setStatusTip(column, c_prop)
602                    elif c_prop_name == 'toolTip':
603                        item.setToolTip(column, c_prop)
604                    elif c_prop_name == 'whatsThis':
605                        item.setWhatsThis(column, c_prop)
606                    elif c_prop_name == 'font':
607                        item.setFont(column, c_prop)
608                    elif c_prop_name == 'icon':
609                        item.setIcon(column, c_prop)
610                    elif c_prop_name == 'background':
611                        item.setBackground(column, c_prop)
612                    elif c_prop_name == 'foreground':
613                        item.setForeground(column, c_prop)
614                    elif c_prop_name == 'flags':
615                        item.setFlags(c_prop)
616                    elif c_prop_name == 'checkState':
617                        item.setCheckState(column, c_prop)
618
619                self.traverseWidgetTree(elem)
620                _, self.item_nr = self.itemstack.pop()
621
622            elif isinstance(w, QtWidgets.QTableWidget):
623                row = int(elem.attrib['row'])
624                col = int(elem.attrib['column'])
625
626                self.disableSorting(w)
627                item = self.createWidgetItem('QTableWidgetItem', elem, w.item,
628                        row, col)
629                w.setItem(row, col, item)
630
631            self.item_nr += 1
632
633    def addAction(self, elem):
634        self.actions.append((self.stack.topwidget, elem.attrib["name"]))
635
636    @staticmethod
637    def any_i18n(*args):
638        """ Return True if any argument appears to be an i18n string. """
639
640        for a in args:
641            if a is not None and not isinstance(a, str):
642                return True
643
644        return False
645
646    def createWidgetItem(self, item_type, elem, getter, *getter_args):
647        """ Create a specific type of widget item. """
648
649        item = self.factory.createQObject(item_type, "item", (), False)
650        props = self.wprops
651
652        # Note that not all types of widget items support the full set of
653        # properties.
654
655        text = props.getProperty(elem, 'text')
656        status_tip = props.getProperty(elem, 'statusTip')
657        tool_tip = props.getProperty(elem, 'toolTip')
658        whats_this = props.getProperty(elem, 'whatsThis')
659
660        if self.any_i18n(text, status_tip, tool_tip, whats_this):
661            self.factory.invoke("item", getter, getter_args)
662
663        if text:
664            item.setText(text)
665
666        if status_tip:
667            item.setStatusTip(status_tip)
668
669        if tool_tip:
670            item.setToolTip(tool_tip)
671
672        if whats_this:
673            item.setWhatsThis(whats_this)
674
675        text_alignment = props.getProperty(elem, 'textAlignment')
676        if text_alignment:
677            item.setTextAlignment(text_alignment)
678
679        font = props.getProperty(elem, 'font')
680        if font:
681            item.setFont(font)
682
683        icon = props.getProperty(elem, 'icon')
684        if icon:
685            item.setIcon(icon)
686
687        background = props.getProperty(elem, 'background')
688        if background:
689            item.setBackground(background)
690
691        foreground = props.getProperty(elem, 'foreground')
692        if foreground:
693            item.setForeground(foreground)
694
695        flags = props.getProperty(elem, 'flags')
696        if flags:
697            item.setFlags(flags)
698
699        check_state = props.getProperty(elem, 'checkState')
700        if check_state:
701            item.setCheckState(check_state)
702
703        return item
704
705    def addHeader(self, elem):
706        w = self.stack.topwidget
707
708        if isinstance(w, QtWidgets.QTreeWidget):
709            props = self.wprops
710            col = self.column_counter
711
712            text = props.getProperty(elem, 'text')
713            if text:
714                w.headerItem().setText(col, text)
715
716            status_tip = props.getProperty(elem, 'statusTip')
717            if status_tip:
718                w.headerItem().setStatusTip(col, status_tip)
719
720            tool_tip = props.getProperty(elem, 'toolTip')
721            if tool_tip:
722                w.headerItem().setToolTip(col, tool_tip)
723
724            whats_this = props.getProperty(elem, 'whatsThis')
725            if whats_this:
726                w.headerItem().setWhatsThis(col, whats_this)
727
728            text_alignment = props.getProperty(elem, 'textAlignment')
729            if text_alignment:
730                w.headerItem().setTextAlignment(col, text_alignment)
731
732            font = props.getProperty(elem, 'font')
733            if font:
734                w.headerItem().setFont(col, font)
735
736            icon = props.getProperty(elem, 'icon')
737            if icon:
738                w.headerItem().setIcon(col, icon)
739
740            background = props.getProperty(elem, 'background')
741            if background:
742                w.headerItem().setBackground(col, background)
743
744            foreground = props.getProperty(elem, 'foreground')
745            if foreground:
746                w.headerItem().setForeground(col, foreground)
747
748            self.column_counter += 1
749
750        elif isinstance(w, QtWidgets.QTableWidget):
751            if len(elem) != 0:
752                if elem.tag == 'column':
753                    item = self.createWidgetItem('QTableWidgetItem', elem,
754                            w.horizontalHeaderItem, self.column_counter)
755                    w.setHorizontalHeaderItem(self.column_counter, item)
756                    self.column_counter += 1
757                elif elem.tag == 'row':
758                    item = self.createWidgetItem('QTableWidgetItem', elem,
759                            w.verticalHeaderItem, self.row_counter)
760                    w.setVerticalHeaderItem(self.row_counter, item)
761                    self.row_counter += 1
762
763    def setZOrder(self, elem):
764        # Designer can generate empty zorder elements.
765        if elem.text is None:
766            return
767
768        # Designer allows the z-order of spacer items to be specified even
769        # though they can't be raised, so ignore any missing raise_() method.
770        try:
771            getattr(self.toplevelWidget, elem.text).raise_()
772        except AttributeError:
773            # Note that uic issues a warning message.
774            pass
775
776    def createAction(self, elem):
777        self.setupObject("QAction", self.currentActionGroup or self.toplevelWidget,
778                         elem)
779
780    def createActionGroup(self, elem):
781        action_group = self.setupObject("QActionGroup", self.toplevelWidget, elem)
782        self.currentActionGroup = action_group
783        self.traverseWidgetTree(elem)
784        self.currentActionGroup = None
785
786    widgetTreeItemHandlers = {
787        "widget"    : createWidget,
788        "addaction" : addAction,
789        "layout"    : createLayout,
790        "spacer"    : createSpacer,
791        "item"      : handleItem,
792        "action"    : createAction,
793        "actiongroup": createActionGroup,
794        "column"    : addHeader,
795        "row"       : addHeader,
796        "zorder"    : setZOrder,
797        }
798
799    def traverseWidgetTree(self, elem):
800        for child in iter(elem):
801            try:
802                handler = self.widgetTreeItemHandlers[child.tag]
803            except KeyError:
804                continue
805
806            handler(self, child)
807
808    def createUserInterface(self, elem):
809        # Get the names of the class and widget.
810        cname = elem.attrib["class"]
811        wname = elem.attrib["name"]
812
813        # If there was no widget name then derive it from the class name.
814        if not wname:
815            wname = cname
816
817            if wname.startswith("Q"):
818                wname = wname[1:]
819
820            wname = wname[0].lower() + wname[1:]
821
822        self.toplevelWidget = self.createToplevelWidget(cname, wname)
823        self.toplevelWidget.setObjectName(wname)
824        DEBUG("toplevel widget is %s",
825              self.toplevelWidget.metaObject().className())
826        self.wprops.setProperties(self.toplevelWidget, elem)
827        self.stack.push(self.toplevelWidget)
828        self.traverseWidgetTree(elem)
829        self.stack.popWidget()
830        self.addActions()
831        self.setBuddies()
832        self.setDelayedProps()
833
834    def addActions(self):
835        for widget, action_name in self.actions:
836            if action_name == "separator":
837                widget.addSeparator()
838            else:
839                DEBUG("add action %s to %s", action_name, widget.objectName())
840                action_obj = getattr(self.toplevelWidget, action_name)
841                if isinstance(action_obj, QtWidgets.QMenu):
842                    widget.addAction(action_obj.menuAction())
843                elif not isinstance(action_obj, QtWidgets.QActionGroup):
844                    widget.addAction(action_obj)
845
846    def setDelayedProps(self):
847        for widget, layout, setter, args in self.wprops.delayed_props:
848            if layout:
849                widget = widget.layout()
850
851            setter = getattr(widget, setter)
852            setter(args)
853
854    def setBuddies(self):
855        for widget, buddy in self.wprops.buddies:
856            DEBUG("%s is buddy of %s", buddy, widget.objectName())
857            try:
858                widget.setBuddy(getattr(self.toplevelWidget, buddy))
859            except AttributeError:
860                DEBUG("ERROR in ui spec: %s (buddy of %s) does not exist",
861                      buddy, widget.objectName())
862
863    def classname(self, elem):
864        DEBUG("uiname is %s", elem.text)
865        name = elem.text
866
867        if name is None:
868            name = ""
869
870        self.uiname = name
871        self.wprops.uiname = name
872        self.setContext(name)
873
874    def setContext(self, context):
875        """
876        Reimplemented by a sub-class if it needs to know the translation
877        context.
878        """
879        pass
880
881    def readDefaults(self, elem):
882        self.defaults['margin'] = int(elem.attrib['margin'])
883        self.defaults['spacing'] = int(elem.attrib['spacing'])
884
885    def setTaborder(self, elem):
886        lastwidget = None
887        for widget_elem in elem:
888            widget = getattr(self.toplevelWidget, widget_elem.text)
889
890            if lastwidget is not None:
891                self.toplevelWidget.setTabOrder(lastwidget, widget)
892
893            lastwidget = widget
894
895    def readResources(self, elem):
896        """
897        Read a "resources" tag and add the module to import to the parser's
898        list of them.
899        """
900        try:
901            iterator = getattr(elem, 'iter')
902        except AttributeError:
903            iterator = getattr(elem, 'getiterator')
904
905        for include in iterator("include"):
906            loc = include.attrib.get("location")
907
908            # Apply the convention for naming the Python files generated by
909            # pyrcc5.
910            if loc and loc.endswith('.qrc'):
911                mname = os.path.basename(loc[:-4] + self._resource_suffix)
912                if mname not in self.resources:
913                    self.resources.append(mname)
914
915    def createConnections(self, elem):
916        def name2object(obj):
917            if obj == self.uiname:
918                return self.toplevelWidget
919            else:
920                return getattr(self.toplevelWidget, obj)
921
922        for conn in iter(elem):
923            signal = conn.findtext('signal')
924            signal_name, signal_args = signal.split('(')
925            signal_args = signal_args[:-1].replace(' ', '')
926            sender = name2object(conn.findtext('sender'))
927            bound_signal = getattr(sender, signal_name)
928
929            slot = self.factory.getSlot(name2object(conn.findtext('receiver')),
930                    conn.findtext('slot').split('(')[0])
931
932            if signal_args == '':
933                bound_signal.connect(slot)
934            else:
935                signal_args = signal_args.split(',')
936
937                if len(signal_args) == 1:
938                    bound_signal[signal_args[0]].connect(slot)
939                else:
940                    bound_signal[tuple(signal_args)].connect(slot)
941
942        QtCore.QMetaObject.connectSlotsByName(self.toplevelWidget)
943
944    def customWidgets(self, elem):
945        def header2module(header):
946            """header2module(header) -> string
947
948            Convert paths to C++ header files to according Python modules
949            >>> header2module("foo/bar/baz.h")
950            'foo.bar.baz'
951            """
952            if header.endswith(".h"):
953                header = header[:-2]
954
955            mpath = []
956            for part in header.split('/'):
957                # Ignore any empty parts or those that refer to the current
958                # directory.
959                if part not in ('', '.'):
960                    if part == '..':
961                        # We should allow this for Python3.
962                        raise SyntaxError("custom widget header file name may not contain '..'.")
963
964                    mpath.append(part)
965
966            return '.'.join(mpath)
967
968        for custom_widget in iter(elem):
969            classname = custom_widget.findtext("class")
970            self.factory.addCustomWidget(classname,
971                                     custom_widget.findtext("extends") or "QWidget",
972                                     header2module(custom_widget.findtext("header")))
973
974    def createToplevelWidget(self, classname, widgetname):
975        raise NotImplementedError
976
977    def buttonGroups(self, elem):
978        for button_group in iter(elem):
979            if button_group.tag == 'buttongroup':
980                bg_name = button_group.attrib['name']
981                bg = ButtonGroup()
982                self.button_groups[bg_name] = bg
983
984                prop = self.getProperty(button_group, 'exclusive')
985                if prop is not None:
986                    if prop.findtext('bool') == 'false':
987                        bg.exclusive = False
988
989    # finalize will be called after the whole tree has been parsed and can be
990    # overridden.
991    def finalize(self):
992        pass
993
994    def parse(self, filename, resource_suffix):
995        if hasattr(filename, 'read'):
996            base_dir = ''
997        else:
998            # Allow the filename to be a QString.
999            filename = str(filename)
1000            base_dir = os.path.dirname(filename)
1001
1002        self.wprops.set_base_dir(base_dir)
1003
1004        self._resource_suffix = resource_suffix
1005
1006        # The order in which the different branches are handled is important.
1007        # The widget tree handler relies on all custom widgets being known, and
1008        # in order to create the connections, all widgets have to be populated.
1009        branchHandlers = (
1010            ("layoutdefault", self.readDefaults),
1011            ("class",         self.classname),
1012            ("buttongroups",  self.buttonGroups),
1013            ("customwidgets", self.customWidgets),
1014            ("widget",        self.createUserInterface),
1015            ("connections",   self.createConnections),
1016            ("tabstops",      self.setTaborder),
1017            ("resources",     self.readResources),
1018        )
1019
1020        document = parse(filename)
1021        root = document.getroot()
1022
1023        if root.tag != 'ui':
1024            raise SyntaxError("not created by Qt Designer")
1025
1026        version = root.attrib.get('version')
1027        if version is None:
1028            raise SyntaxError("missing version number")
1029
1030        # Right now, only version 4.0 is supported.
1031        if version != '4.0':
1032            raise SyntaxError("only Qt Designer files v4.0 are supported")
1033
1034        for tagname, actor in branchHandlers:
1035            elem = document.find(tagname)
1036            if elem is not None:
1037                actor(elem)
1038        self.finalize()
1039        w = self.toplevelWidget
1040        self.reset()
1041        return w
1042
1043    @staticmethod
1044    def _form_layout_role(layout_position):
1045        if layout_position[3] > 1:
1046            role = QtWidgets.QFormLayout.SpanningRole
1047        elif layout_position[1] == 1:
1048            role = QtWidgets.QFormLayout.FieldRole
1049        else:
1050            role = QtWidgets.QFormLayout.LabelRole
1051
1052        return role
1053