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 logging
42import os.path
43import sys
44
45from .exceptions import NoSuchClassError, UnsupportedPropertyError
46from .icon_cache import IconCache
47
48if sys.hexversion >= 0x03000000:
49    from .port_v3.ascii_upper import ascii_upper
50else:
51    from .port_v2.ascii_upper import ascii_upper
52
53
54logger = logging.getLogger(__name__)
55DEBUG = logger.debug
56
57
58QtCore = None
59QtGui = None
60QtWidgets = None
61
62
63def int_list(prop):
64    return [int(child.text) for child in prop]
65
66def float_list(prop):
67    return [float(child.text) for child in prop]
68
69bool_ = lambda v: v == "true"
70
71def qfont_enum(v):
72    return getattr(QtGui.QFont, v)
73
74def needsWidget(func):
75    func.needsWidget = True
76    return func
77
78
79class Properties(object):
80    def __init__(self, factory, qtcore_module, qtgui_module, qtwidgets_module):
81        self.factory = factory
82
83        global QtCore, QtGui, QtWidgets
84        QtCore = qtcore_module
85        QtGui = qtgui_module
86        QtWidgets = qtwidgets_module
87
88        self._base_dir = ''
89
90        self.reset()
91
92    def set_base_dir(self, base_dir):
93        """ Set the base directory to be used for all relative filenames. """
94
95        self._base_dir = base_dir
96        self.icon_cache.set_base_dir(base_dir)
97
98    def reset(self):
99        self.buddies = []
100        self.delayed_props = []
101        self.icon_cache = IconCache(self.factory, QtGui)
102
103    def _pyEnumMember(self, cpp_name):
104        try:
105            prefix, membername = cpp_name.split("::")
106        except ValueError:
107            prefix = 'Qt'
108            membername = cpp_name
109
110        if prefix == 'Qt':
111            return getattr(QtCore.Qt, membername)
112
113        scope = self.factory.findQObjectType(prefix)
114        if scope is None:
115            raise NoSuchClassError(prefix)
116
117        return getattr(scope, membername)
118
119    def _set(self, prop):
120        expr = [self._pyEnumMember(v) for v in prop.text.split('|')]
121
122        value = expr[0]
123        for v in expr[1:]:
124            value |= v
125
126        return value
127
128    def _enum(self, prop):
129        return self._pyEnumMember(prop.text)
130
131    def _number(self, prop):
132        return int(prop.text)
133
134    _UInt = _uInt = _longLong = _uLongLong = _number
135
136    def _double(self, prop):
137        return float(prop.text)
138
139    def _bool(self, prop):
140        return prop.text == 'true'
141
142    def _stringlist(self, prop):
143        return [self._string(p, notr='true') for p in prop]
144
145    def _string(self, prop, notr=None):
146        text = prop.text
147
148        if text is None:
149            return ""
150
151        if prop.get('notr', notr) == 'true':
152            return text
153
154        disambig = prop.get('comment')
155
156        return QtWidgets.QApplication.translate(self.uiname, text, disambig)
157
158    _char = _string
159
160    def _cstring(self, prop):
161        return str(prop.text)
162
163    def _color(self, prop):
164        args = int_list(prop)
165
166        # Handle the optional alpha component.
167        alpha = int(prop.get("alpha", "255"))
168
169        if alpha != 255:
170            args.append(alpha)
171
172        return QtGui.QColor(*args)
173
174    def _point(self, prop):
175        return QtCore.QPoint(*int_list(prop))
176
177    def _pointf(self, prop):
178        return QtCore.QPointF(*float_list(prop))
179
180    def _rect(self, prop):
181        return QtCore.QRect(*int_list(prop))
182
183    def _rectf(self, prop):
184        return QtCore.QRectF(*float_list(prop))
185
186    def _size(self, prop):
187        return QtCore.QSize(*int_list(prop))
188
189    def _sizef(self, prop):
190        return QtCore.QSizeF(*float_list(prop))
191
192    def _pixmap(self, prop):
193        if prop.text:
194            fname = prop.text.replace("\\", "\\\\")
195            if self._base_dir != '' and fname[0] != ':' and not os.path.isabs(fname):
196                fname = os.path.join(self._base_dir, fname)
197
198            return QtGui.QPixmap(fname)
199
200        # Don't bother to set the property if the pixmap is empty.
201        return None
202
203    def _iconset(self, prop):
204        return self.icon_cache.get_icon(prop)
205
206    def _url(self, prop):
207        return QtCore.QUrl(prop[0].text)
208
209    def _locale(self, prop):
210        lang = getattr(QtCore.QLocale, prop.attrib['language'])
211        country = getattr(QtCore.QLocale, prop.attrib['country'])
212        return QtCore.QLocale(lang, country)
213
214    def _date(self, prop):
215        return QtCore.QDate(*int_list(prop))
216
217    def _datetime(self, prop):
218        args = int_list(prop)
219        return QtCore.QDateTime(QtCore.QDate(*args[-3:]), QtCore.QTime(*args[:-3]))
220
221    def _time(self, prop):
222        return QtCore.QTime(*int_list(prop))
223
224    def _gradient(self, prop):
225        name = 'gradient'
226
227        # Create the specific gradient.
228        gtype = prop.get('type', '')
229
230        if gtype == 'LinearGradient':
231            startx = float(prop.get('startx'))
232            starty = float(prop.get('starty'))
233            endx = float(prop.get('endx'))
234            endy = float(prop.get('endy'))
235            gradient = self.factory.createQObject('QLinearGradient', name,
236                    (startx, starty, endx, endy), is_attribute=False)
237
238        elif gtype == 'ConicalGradient':
239            centralx = float(prop.get('centralx'))
240            centraly = float(prop.get('centraly'))
241            angle = float(prop.get('angle'))
242            gradient = self.factory.createQObject('QConicalGradient', name,
243                    (centralx, centraly, angle), is_attribute=False)
244
245        elif gtype == 'RadialGradient':
246            centralx = float(prop.get('centralx'))
247            centraly = float(prop.get('centraly'))
248            radius = float(prop.get('radius'))
249            focalx = float(prop.get('focalx'))
250            focaly = float(prop.get('focaly'))
251            gradient = self.factory.createQObject('QRadialGradient', name,
252                    (centralx, centraly, radius, focalx, focaly),
253                    is_attribute=False)
254
255        else:
256            raise UnsupportedPropertyError(prop.tag)
257
258        # Set the common values.
259        spread = prop.get('spread')
260        if spread:
261            gradient.setSpread(getattr(QtGui.QGradient, spread))
262
263        cmode = prop.get('coordinatemode')
264        if cmode:
265            gradient.setCoordinateMode(getattr(QtGui.QGradient, cmode))
266
267        # Get the gradient stops.
268        for gstop in prop:
269            if gstop.tag != 'gradientstop':
270                raise UnsupportedPropertyError(gstop.tag)
271
272            position = float(gstop.get('position'))
273            color = self._color(gstop[0])
274
275            gradient.setColorAt(position, color)
276
277        return gradient
278
279    def _palette(self, prop):
280        palette = self.factory.createQObject("QPalette", "palette", (),
281                is_attribute=False)
282
283        for palette_elem in prop:
284            sub_palette = getattr(QtGui.QPalette, palette_elem.tag.title())
285            for role, color in enumerate(palette_elem):
286                if color.tag == 'color':
287                    # Handle simple colour descriptions where the role is
288                    # implied by the colour's position.
289                    palette.setColor(sub_palette,
290                            QtGui.QPalette.ColorRole(role), self._color(color))
291                elif color.tag == 'colorrole':
292                    role = getattr(QtGui.QPalette, color.get('role'))
293                    brush = self._brush(color[0])
294                    palette.setBrush(sub_palette, role, brush)
295                else:
296                    raise UnsupportedPropertyError(color.tag)
297
298        return palette
299
300    def _brush(self, prop):
301        brushstyle = prop.get('brushstyle')
302
303        if brushstyle in ('LinearGradientPattern', 'ConicalGradientPattern', 'RadialGradientPattern'):
304            gradient = self._gradient(prop[0])
305            brush = self.factory.createQObject("QBrush", "brush", (gradient, ),
306                    is_attribute=False)
307        else:
308            color = self._color(prop[0])
309            brush = self.factory.createQObject("QBrush", "brush", (color, ),
310                    is_attribute=False)
311
312            brushstyle = getattr(QtCore.Qt, brushstyle)
313            brush.setStyle(brushstyle)
314
315        return brush
316
317    #@needsWidget
318    def _sizepolicy(self, prop, widget):
319        values = [int(child.text) for child in prop]
320
321        if len(values) == 2:
322            # Qt v4.3.0 and later.
323            horstretch, verstretch = values
324            hsizetype = getattr(QtWidgets.QSizePolicy, prop.get('hsizetype'))
325            vsizetype = getattr(QtWidgets.QSizePolicy, prop.get('vsizetype'))
326        else:
327            hsizetype, vsizetype, horstretch, verstretch = values
328            hsizetype = QtWidgets.QSizePolicy.Policy(hsizetype)
329            vsizetype = QtWidgets.QSizePolicy.Policy(vsizetype)
330
331        sizePolicy = self.factory.createQObject('QSizePolicy', 'sizePolicy',
332                (hsizetype, vsizetype), is_attribute=False)
333        sizePolicy.setHorizontalStretch(horstretch)
334        sizePolicy.setVerticalStretch(verstretch)
335        sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth())
336        return sizePolicy
337    _sizepolicy = needsWidget(_sizepolicy)
338
339    # font needs special handling/conversion of all child elements.
340    _font_attributes = (("Family",          lambda s: s),
341                        ("PointSize",       int),
342                        ("Bold",            bool_),
343                        ("Italic",          bool_),
344                        ("Underline",       bool_),
345                        ("Weight",          int),
346                        ("StrikeOut",       bool_),
347                        ("Kerning",         bool_),
348                        ("StyleStrategy",   qfont_enum))
349
350    def _font(self, prop):
351        newfont = self.factory.createQObject("QFont", "font", (),
352                                                     is_attribute = False)
353        for attr, converter in self._font_attributes:
354            v = prop.findtext("./%s" % (attr.lower(),))
355            if v is None:
356                continue
357
358            getattr(newfont, "set%s" % (attr,))(converter(v))
359        return newfont
360
361    def _cursor(self, prop):
362        return QtGui.QCursor(QtCore.Qt.CursorShape(int(prop.text)))
363
364    def _cursorShape(self, prop):
365        return QtGui.QCursor(getattr(QtCore.Qt, prop.text))
366
367    def convert(self, prop, widget=None):
368        try:
369            func = getattr(self, "_" + prop[0].tag)
370        except AttributeError:
371            raise UnsupportedPropertyError(prop[0].tag)
372        else:
373            args = {}
374            if getattr(func, "needsWidget", False):
375                assert widget is not None
376                args["widget"] = widget
377
378            return func(prop[0], **args)
379
380
381    def _getChild(self, elem_tag, elem, name, default=None):
382        for prop in elem.findall(elem_tag):
383            if prop.attrib["name"] == name:
384                return self.convert(prop)
385        else:
386            return default
387
388    def getProperty(self, elem, name, default=None):
389        return self._getChild("property", elem, name, default)
390
391    def getAttribute(self, elem, name, default=None):
392        return self._getChild("attribute", elem, name, default)
393
394    def setProperties(self, widget, elem):
395        # Lines are sunken unless the frame shadow is explicitly set.
396        set_sunken = (elem.attrib.get('class') == 'Line')
397
398        for prop in elem.findall('property'):
399            prop_name = prop.attrib['name']
400            DEBUG("setting property %s" % (prop_name,))
401
402            if prop_name == 'frameShadow':
403                set_sunken = False
404
405            try:
406                stdset = bool(int(prop.attrib['stdset']))
407            except KeyError:
408                stdset = True
409
410            if not stdset:
411                self._setViaSetProperty(widget, prop)
412            elif hasattr(self, prop_name):
413                getattr(self, prop_name)(widget, prop)
414            else:
415                prop_value = self.convert(prop, widget)
416                if prop_value is not None:
417                    getattr(widget, 'set%s%s' % (ascii_upper(prop_name[0]), prop_name[1:]))(prop_value)
418
419        if set_sunken:
420            widget.setFrameShadow(QtWidgets.QFrame.Sunken)
421
422    # SPECIAL PROPERTIES
423    # If a property has a well-known value type but needs special,
424    # context-dependent handling, the default behaviour can be overridden here.
425
426    # Delayed properties will be set after the whole widget tree has been
427    # populated.
428    def _delayed_property(self, widget, prop):
429        prop_value = self.convert(prop)
430        if prop_value is not None:
431            prop_name = prop.attrib["name"]
432            self.delayed_props.append((widget, False,
433                    'set%s%s' % (ascii_upper(prop_name[0]), prop_name[1:]),
434                    prop_value))
435
436    # These properties will be set with a widget.setProperty call rather than
437    # calling the set<property> function.
438    def _setViaSetProperty(self, widget, prop):
439        prop_value = self.convert(prop, widget)
440        if prop_value is not None:
441            prop_name = prop.attrib['name']
442
443            # This appears to be a Designer/uic hack where stdset=0 means that
444            # the viewport should be used.
445            if prop[0].tag == 'cursorShape':
446                widget.viewport().setProperty(prop_name, prop_value)
447            else:
448                widget.setProperty(prop_name, prop_value)
449
450    # Ignore the property.
451    def _ignore(self, widget, prop):
452        pass
453
454    # Define properties that use the canned handlers.
455    currentIndex = _delayed_property
456    currentRow = _delayed_property
457
458    showDropIndicator = _setViaSetProperty
459    intValue = _setViaSetProperty
460    value = _setViaSetProperty
461
462    objectName = _ignore
463    margin = _ignore
464    leftMargin = _ignore
465    topMargin = _ignore
466    rightMargin = _ignore
467    bottomMargin = _ignore
468    spacing = _ignore
469    horizontalSpacing = _ignore
470    verticalSpacing = _ignore
471
472    # tabSpacing is actually the spacing property of the widget's layout.
473    def tabSpacing(self, widget, prop):
474        prop_value = self.convert(prop)
475        if prop_value is not None:
476            self.delayed_props.append((widget, True, 'setSpacing', prop_value))
477
478    # buddy setting has to be done after the whole widget tree has been
479    # populated.  We can't use delay here because we cannot get the actual
480    # buddy yet.
481    def buddy(self, widget, prop):
482        buddy_name = prop[0].text
483        if buddy_name:
484            self.buddies.append((widget, buddy_name))
485
486    # geometry is handled specially if set on the toplevel widget.
487    def geometry(self, widget, prop):
488        if widget.objectName() == self.uiname:
489            geom = int_list(prop[0])
490            widget.resize(geom[2], geom[3])
491        else:
492            widget.setGeometry(self._rect(prop[0]))
493
494    def orientation(self, widget, prop):
495        # If the class is a QFrame, it's a line.
496        if widget.metaObject().className() == 'QFrame':
497            widget.setFrameShape(
498                {'Qt::Horizontal': QtWidgets.QFrame.HLine,
499                 'Qt::Vertical'  : QtWidgets.QFrame.VLine}[prop[0].text])
500        else:
501            widget.setOrientation(self._enum(prop[0]))
502
503    # The isWrapping attribute of QListView is named inconsistently, it should
504    # be wrapping.
505    def isWrapping(self, widget, prop):
506        widget.setWrapping(self.convert(prop))
507
508    # This is a pseudo-property injected to deal with margins.
509    def pyuicMargins(self, widget, prop):
510        widget.setContentsMargins(*int_list(prop))
511
512    # This is a pseudo-property injected to deal with spacing.
513    def pyuicSpacing(self, widget, prop):
514        horiz, vert = int_list(prop)
515
516        if horiz == vert:
517            widget.setSpacing(horiz)
518        else:
519            if horiz >= 0:
520                widget.setHorizontalSpacing(horiz)
521
522            if vert >= 0:
523                widget.setVerticalSpacing(vert)
524