1import os
2
3from . import PYSIDE, PYSIDE2, PYQT4, PYQT5
4from .QtWidgets import QComboBox
5
6
7if PYQT5:
8
9    from PyQt5.uic import *
10
11elif PYQT4:
12
13    from PyQt4.uic import *
14
15else:
16
17    __all__ = ['loadUi', 'loadUiType']
18
19    # In PySide, loadUi does not exist, so we define it using QUiLoader, and
20    # then make sure we expose that function. This is adapted from qt-helpers
21    # which was released under a 3-clause BSD license:
22    # qt-helpers - a common front-end to various Qt modules
23    #
24    # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
25    #
26    # All rights reserved.
27    #
28    # Redistribution and use in source and binary forms, with or without
29    # modification, are permitted provided that the following conditions are
30    # met:
31    #
32    #  * Redistributions of source code must retain the above copyright
33    #    notice, this list of conditions and the following disclaimer.
34    #  * Redistributions in binary form must reproduce the above copyright
35    #    notice, this list of conditions and the following disclaimer in the
36    #    documentation and/or other materials provided with the
37    #    distribution.
38    #  * Neither the name of the Glue project nor the names of its contributors
39    #    may be used to endorse or promote products derived from this software
40    #    without specific prior written permission.
41    #
42    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
43    # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
44    # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
45    # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
46    # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
47    # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
48    # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
49    # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
50    # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
51    # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
52    # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
53    #
54    # Which itself was based on the solution at
55    #
56    # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
57    #
58    # which was released under the MIT license:
59    #
60    # Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com>
61    # Modifications by Charl Botha <cpbotha@vxlabs.com>
62    #
63    # Permission is hereby granted, free of charge, to any person obtaining a
64    # copy of this software and associated documentation files (the "Software"),
65    # to deal in the Software without restriction, including without limitation
66    # the rights to use, copy, modify, merge, publish, distribute, sublicense,
67    # and/or sell copies of the Software, and to permit persons to whom the
68    # Software is furnished to do so, subject to the following conditions:
69    #
70    # The above copyright notice and this permission notice shall be included in
71    # all copies or substantial portions of the Software.
72    #
73    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
74    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
75    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
76    # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
77    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
78    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
79    # DEALINGS IN THE SOFTWARE.
80
81    if PYSIDE:
82        from PySide.QtCore import QMetaObject
83        from PySide.QtUiTools import QUiLoader
84        try:
85            from pysideuic import compileUi
86        except ImportError:
87            pass
88    elif PYSIDE2:
89        from PySide2.QtCore import QMetaObject
90        from PySide2.QtUiTools import QUiLoader
91        try:
92            from pyside2uic import compileUi
93        except ImportError:
94            pass
95
96    class UiLoader(QUiLoader):
97        """
98        Subclass of :class:`~PySide.QtUiTools.QUiLoader` to create the user
99        interface in a base instance.
100
101        Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not
102        create a new instance of the top-level widget, but creates the user
103        interface in an existing instance of the top-level class if needed.
104
105        This mimics the behaviour of :func:`PyQt4.uic.loadUi`.
106        """
107
108        def __init__(self, baseinstance, customWidgets=None):
109            """
110            Create a loader for the given ``baseinstance``.
111
112            The user interface is created in ``baseinstance``, which must be an
113            instance of the top-level class in the user interface to load, or a
114            subclass thereof.
115
116            ``customWidgets`` is a dictionary mapping from class name to class
117            object for custom widgets. Usually, this should be done by calling
118            registerCustomWidget on the QUiLoader, but with PySide 1.1.2 on
119            Ubuntu 12.04 x86_64 this causes a segfault.
120
121            ``parent`` is the parent object of this loader.
122            """
123
124            QUiLoader.__init__(self, baseinstance)
125
126            self.baseinstance = baseinstance
127
128            if customWidgets is None:
129                self.customWidgets = {}
130            else:
131                self.customWidgets = customWidgets
132
133        def createWidget(self, class_name, parent=None, name=''):
134            """
135            Function that is called for each widget defined in ui file,
136            overridden here to populate baseinstance instead.
137            """
138
139            if parent is None and self.baseinstance:
140                # supposed to create the top-level widget, return the base
141                # instance instead
142                return self.baseinstance
143
144            else:
145
146                # For some reason, Line is not in the list of available
147                # widgets, but works fine, so we have to special case it here.
148                if class_name in self.availableWidgets() or class_name == 'Line':
149                    # create a new widget for child widgets
150                    widget = QUiLoader.createWidget(self, class_name, parent, name)
151
152                else:
153                    # If not in the list of availableWidgets, must be a custom
154                    # widget. This will raise KeyError if the user has not
155                    # supplied the relevant class_name in the dictionary or if
156                    # customWidgets is empty.
157                    try:
158                        widget = self.customWidgets[class_name](parent)
159                    except KeyError:
160                        raise Exception('No custom widget ' + class_name + ' '
161                                        'found in customWidgets')
162
163                if self.baseinstance:
164                    # set an attribute for the new child widget on the base
165                    # instance, just like PyQt4.uic.loadUi does.
166                    setattr(self.baseinstance, name, widget)
167
168                return widget
169
170    def _get_custom_widgets(ui_file):
171        """
172        This function is used to parse a ui file and look for the <customwidgets>
173        section, then automatically load all the custom widget classes.
174        """
175
176        import sys
177        import importlib
178        from xml.etree.ElementTree import ElementTree
179
180        # Parse the UI file
181        etree = ElementTree()
182        ui = etree.parse(ui_file)
183
184        # Get the customwidgets section
185        custom_widgets = ui.find('customwidgets')
186
187        if custom_widgets is None:
188            return {}
189
190        custom_widget_classes = {}
191
192        for custom_widget in list(custom_widgets):
193
194            cw_class = custom_widget.find('class').text
195            cw_header = custom_widget.find('header').text
196
197            module = importlib.import_module(cw_header)
198
199            custom_widget_classes[cw_class] = getattr(module, cw_class)
200
201        return custom_widget_classes
202
203    def loadUi(uifile, baseinstance=None, workingDirectory=None):
204        """
205        Dynamically load a user interface from the given ``uifile``.
206
207        ``uifile`` is a string containing a file name of the UI file to load.
208
209        If ``baseinstance`` is ``None``, the a new instance of the top-level
210        widget will be created. Otherwise, the user interface is created within
211        the given ``baseinstance``. In this case ``baseinstance`` must be an
212        instance of the top-level widget class in the UI file to load, or a
213        subclass thereof. In other words, if you've created a ``QMainWindow``
214        interface in the designer, ``baseinstance`` must be a ``QMainWindow``
215        or a subclass thereof, too. You cannot load a ``QMainWindow`` UI file
216        with a plain :class:`~PySide.QtGui.QWidget` as ``baseinstance``.
217
218        :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on
219        the created user interface, so you can implemented your slots according
220        to its conventions in your widget class.
221
222        Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise
223        return the newly created instance of the user interface.
224        """
225
226        # We parse the UI file and import any required custom widgets
227        customWidgets = _get_custom_widgets(uifile)
228
229        loader = UiLoader(baseinstance, customWidgets)
230
231        if workingDirectory is not None:
232            loader.setWorkingDirectory(workingDirectory)
233
234        widget = loader.load(uifile)
235        QMetaObject.connectSlotsByName(widget)
236        return widget
237
238    def loadUiType(uifile, from_imports=False):
239        """Load a .ui file and return the generated form class and
240        the Qt base class.
241
242        The "loadUiType" command convert the ui file to py code
243        in-memory first and then execute it in a special frame to
244        retrieve the form_class.
245
246        Credit: https://stackoverflow.com/a/14195313/15954282
247        """
248
249        import sys
250        if sys.version_info >= (3, 0):
251            from io import StringIO
252        else:
253            from io import BytesIO as StringIO
254        from xml.etree.ElementTree import ElementTree
255        from . import QtWidgets
256
257        # Parse the UI file
258        etree = ElementTree()
259        ui = etree.parse(uifile)
260
261        widget_class = ui.find('widget').get('class')
262        form_class = ui.find('class').text
263
264        with open(uifile, 'r') as fd:
265            code_stream = StringIO()
266            frame = {}
267
268            compileUi(fd, code_stream, indent=0, from_imports=from_imports)
269            pyc = compile(code_stream.getvalue(), '<string>', 'exec')
270            exec(pyc, frame)
271
272            # Fetch the base_class and form class based on their type in the
273            # xml from designer
274            form_class = frame['Ui_%s' % form_class]
275            base_class = getattr(QtWidgets, widget_class)
276
277        return form_class, base_class
278