Spaces:
Running
Running
| ############################################################################# | |
| ## | |
| ## Copyright (C) 2024 Riverbank Computing Limited. | |
| ## Copyright (C) 2006 Thorsten Marek. | |
| ## All right reserved. | |
| ## | |
| ## This file is part of PyQt. | |
| ## | |
| ## You may use this file under the terms of the GPL v2 or the revised BSD | |
| ## license as follows: | |
| ## | |
| ## "Redistribution and use in source and binary forms, with or without | |
| ## modification, are permitted provided that the following conditions are | |
| ## met: | |
| ## * Redistributions of source code must retain the above copyright | |
| ## notice, this list of conditions and the following disclaimer. | |
| ## * Redistributions in binary form must reproduce the above copyright | |
| ## notice, this list of conditions and the following disclaimer in | |
| ## the documentation and/or other materials provided with the | |
| ## distribution. | |
| ## * Neither the name of the Riverbank Computing Limited nor the names | |
| ## of its contributors may be used to endorse or promote products | |
| ## derived from this software without specific prior written | |
| ## permission. | |
| ## | |
| ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| ## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| ## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." | |
| ## | |
| ############################################################################# | |
| import logging | |
| import os.path | |
| import sys | |
| from .enum_map import EnumMap | |
| from .exceptions import NoSuchClassError, UnsupportedPropertyError | |
| from .icon_cache import IconCache | |
| logger = logging.getLogger(__name__) | |
| DEBUG = logger.debug | |
| QtCore = None | |
| QtGui = None | |
| QtWidgets = None | |
| def int_list(prop): | |
| return [int(child.text) for child in prop] | |
| def float_list(prop): | |
| return [float(child.text) for child in prop] | |
| bool_ = lambda v: v == "true" | |
| def qfont_style_strategy(v): | |
| return getattr(QtGui.QFont.StyleStrategy, v) | |
| def needsWidget(func): | |
| func.needsWidget = True | |
| return func | |
| # A translation table for converting ASCII lower case to upper case. | |
| _ascii_trans_table = bytes.maketrans(b'abcdefghijklmnopqrstuvwxyz', | |
| b'ABCDEFGHIJKLMNOPQRSTUVWXYZ') | |
| def ascii_upper(s): | |
| """ Convert a string to ASCII upper case irrespective of the current | |
| locale. | |
| """ | |
| return s.translate(_ascii_trans_table) | |
| class Properties(object): | |
| def __init__(self, factory, qtcore_module, qtgui_module, qtwidgets_module): | |
| self.factory = factory | |
| global QtCore, QtGui, QtWidgets | |
| QtCore = qtcore_module | |
| QtGui = qtgui_module | |
| QtWidgets = qtwidgets_module | |
| self._base_dir = '' | |
| self.reset() | |
| def set_base_dir(self, base_dir): | |
| """ Set the base directory to be used for all relative filenames. """ | |
| self._base_dir = base_dir | |
| self.icon_cache.set_base_dir(base_dir) | |
| def reset(self): | |
| self.buddies = [] | |
| self.delayed_props = [] | |
| self.icon_cache = IconCache(self.factory, QtGui) | |
| def _pyEnumMember(self, cpp_name): | |
| if '::' not in cpp_name: | |
| cpp_name = 'Qt::' + cpp_name | |
| cpp_name = EnumMap.get(cpp_name, cpp_name) | |
| parts = cpp_name.split('::') | |
| if parts[0] == 'Qt': | |
| scope = QtCore.Qt | |
| else: | |
| scope = self.factory.findQObjectType(parts[0]) | |
| if scope is None: | |
| raise NoSuchClassError(parts[0]) | |
| for tail in parts[1:]: | |
| scope = getattr(scope, tail) | |
| return scope | |
| def _set(self, prop): | |
| expr = [self._pyEnumMember(v) for v in prop.text.split('|')] | |
| value = expr[0] | |
| for v in expr[1:]: | |
| value |= v | |
| return value | |
| def _enum(self, prop): | |
| return self._pyEnumMember(prop.text) | |
| def _number(self, prop): | |
| return int(prop.text) | |
| _UInt = _uInt = _longLong = _uLongLong = _number | |
| def _double(self, prop): | |
| return float(prop.text) | |
| def _bool(self, prop): | |
| return prop.text == 'true' | |
| def _stringlist(self, prop): | |
| return [self._string(p, notr='true') for p in prop] | |
| def _string(self, prop, notr=None): | |
| text = prop.text | |
| if text is None: | |
| return "" | |
| if prop.get('notr', notr) == 'true': | |
| return text | |
| disambig = prop.get('comment') | |
| return QtWidgets.QApplication.translate(self.uiname, text, disambig) | |
| _char = _string | |
| def _cstring(self, prop): | |
| return str(prop.text) | |
| def _color(self, prop): | |
| args = int_list(prop) | |
| # Handle the optional alpha component. | |
| alpha = int(prop.get("alpha", "255")) | |
| if alpha != 255: | |
| args.append(alpha) | |
| return QtGui.QColor(*args) | |
| def _point(self, prop): | |
| return QtCore.QPoint(*int_list(prop)) | |
| def _pointf(self, prop): | |
| return QtCore.QPointF(*float_list(prop)) | |
| def _rect(self, prop): | |
| return QtCore.QRect(*int_list(prop)) | |
| def _rectf(self, prop): | |
| return QtCore.QRectF(*float_list(prop)) | |
| def _size(self, prop): | |
| return QtCore.QSize(*int_list(prop)) | |
| def _sizef(self, prop): | |
| return QtCore.QSizeF(*float_list(prop)) | |
| def _pixmap(self, prop): | |
| if prop.text: | |
| fname = prop.text.replace("\\", "\\\\") | |
| if self._base_dir != '' and fname[0] != ':' and not os.path.isabs(fname): | |
| fname = os.path.join(self._base_dir, fname) | |
| return QtGui.QPixmap(fname) | |
| # Don't bother to set the property if the pixmap is empty. | |
| return None | |
| def _iconset(self, prop): | |
| return self.icon_cache.get_icon(prop) | |
| def _url(self, prop): | |
| return QtCore.QUrl(prop[0].text) | |
| def _locale(self, prop): | |
| lang = getattr(QtCore.QLocale.Language, prop.attrib['language']) | |
| country = getattr(QtCore.QLocale.Country, prop.attrib['country']) | |
| return QtCore.QLocale(lang, country) | |
| def _date(self, prop): | |
| return QtCore.QDate(*int_list(prop)) | |
| def _datetime(self, prop): | |
| args = int_list(prop) | |
| return QtCore.QDateTime(QtCore.QDate(*args[-3:]), QtCore.QTime(*args[:-3])) | |
| def _time(self, prop): | |
| return QtCore.QTime(*int_list(prop)) | |
| def _gradient(self, prop): | |
| name = 'gradient' | |
| # Create the specific gradient. | |
| gtype = prop.get('type', '') | |
| if gtype == 'LinearGradient': | |
| startx = float(prop.get('startx')) | |
| starty = float(prop.get('starty')) | |
| endx = float(prop.get('endx')) | |
| endy = float(prop.get('endy')) | |
| gradient = self.factory.createQtObject('QLinearGradient', name, | |
| ctor_args=(startx, starty, endx, endy), is_attribute=False) | |
| elif gtype == 'ConicalGradient': | |
| centralx = float(prop.get('centralx')) | |
| centraly = float(prop.get('centraly')) | |
| angle = float(prop.get('angle')) | |
| gradient = self.factory.createQtObject('QConicalGradient', name, | |
| ctor_args=(centralx, centraly, angle), is_attribute=False) | |
| elif gtype == 'RadialGradient': | |
| centralx = float(prop.get('centralx')) | |
| centraly = float(prop.get('centraly')) | |
| radius = float(prop.get('radius')) | |
| focalx = float(prop.get('focalx')) | |
| focaly = float(prop.get('focaly')) | |
| gradient = self.factory.createQtObject('QRadialGradient', name, | |
| ctor_args=(centralx, centraly, radius, focalx, focaly), | |
| is_attribute=False) | |
| else: | |
| raise UnsupportedPropertyError(prop.tag) | |
| # Set the common values. | |
| spread = prop.get('spread') | |
| if spread: | |
| gradient.setSpread(getattr(QtGui.QGradient.Spread, spread)) | |
| cmode = prop.get('coordinatemode') | |
| if cmode: | |
| gradient.setCoordinateMode( | |
| getattr(QtGui.QGradient.CoordinateMode, cmode)) | |
| # Get the gradient stops. | |
| for gstop in prop: | |
| if gstop.tag != 'gradientstop': | |
| raise UnsupportedPropertyError(gstop.tag) | |
| position = float(gstop.get('position')) | |
| color = self._color(gstop[0]) | |
| gradient.setColorAt(position, color) | |
| return gradient | |
| def _palette(self, prop): | |
| palette = self.factory.createQtObject('QPalette', 'palette', | |
| is_attribute=False) | |
| for palette_elem in prop: | |
| sub_palette = getattr(QtGui.QPalette.ColorGroup, | |
| palette_elem.tag.title()) | |
| for role, color in enumerate(palette_elem): | |
| if color.tag == 'color': | |
| # Handle simple colour descriptions where the role is | |
| # implied by the colour's position. | |
| palette.setColor(sub_palette, | |
| QtGui.QPalette.ColorRole(role), self._color(color)) | |
| elif color.tag == 'colorrole': | |
| role = getattr(QtGui.QPalette.ColorRole, color.get('role')) | |
| brush = self._brush(color[0]) | |
| palette.setBrush(sub_palette, role, brush) | |
| else: | |
| raise UnsupportedPropertyError(color.tag) | |
| return palette | |
| def _brush(self, prop): | |
| brushstyle = prop.get('brushstyle') | |
| if brushstyle in ('LinearGradientPattern', 'ConicalGradientPattern', 'RadialGradientPattern'): | |
| gradient = self._gradient(prop[0]) | |
| brush = self.factory.createQtObject('QBrush', 'brush', | |
| ctor_args=(gradient, ), is_attribute=False) | |
| else: | |
| color = self._color(prop[0]) | |
| brush = self.factory.createQtObject('QBrush', 'brush', | |
| ctor_args=(color, ), is_attribute=False) | |
| brushstyle = getattr(QtCore.Qt.BrushStyle, brushstyle) | |
| brush.setStyle(brushstyle) | |
| return brush | |
| #@needsWidget | |
| def _sizepolicy(self, prop, widget): | |
| values = [int(child.text) for child in prop] | |
| if len(values) == 2: | |
| # Qt v4.3.0 and later. | |
| horstretch, verstretch = values | |
| hsizetype = getattr(QtWidgets.QSizePolicy.Policy, prop.get('hsizetype')) | |
| vsizetype = getattr(QtWidgets.QSizePolicy.Policy, prop.get('vsizetype')) | |
| else: | |
| hsizetype, vsizetype, horstretch, verstretch = values | |
| hsizetype = QtWidgets.QSizePolicy.Policy(hsizetype) | |
| vsizetype = QtWidgets.QSizePolicy.Policy(vsizetype) | |
| sizePolicy = self.factory.createQtObject('QSizePolicy', 'sizePolicy', | |
| ctor_args=(hsizetype, vsizetype), is_attribute=False) | |
| sizePolicy.setHorizontalStretch(horstretch) | |
| sizePolicy.setVerticalStretch(verstretch) | |
| sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) | |
| return sizePolicy | |
| _sizepolicy = needsWidget(_sizepolicy) | |
| # font needs special handling/conversion of all child elements. | |
| _font_attributes = (("Family", lambda s: s), | |
| ("PointSize", int), | |
| ("Bold", bool_), | |
| ("Italic", bool_), | |
| ("Underline", bool_), | |
| ("Weight", int), | |
| ("StrikeOut", bool_), | |
| ("Kerning", bool_), | |
| ("StyleStrategy", qfont_style_strategy)) | |
| def _font(self, prop): | |
| newfont = self.factory.createQtObject('QFont', 'font', | |
| is_attribute=False) | |
| for attr, converter in self._font_attributes: | |
| v = prop.findtext("./%s" % (attr.lower(),)) | |
| if v is None: | |
| continue | |
| getattr(newfont, "set%s" % (attr,))(converter(v)) | |
| return newfont | |
| def _cursor(self, prop): | |
| return QtGui.QCursor(QtCore.Qt.CursorShape(int(prop.text))) | |
| def _cursorShape(self, prop): | |
| return QtGui.QCursor(getattr(QtCore.Qt.CursorShape, prop.text)) | |
| def convert(self, prop, widget=None): | |
| try: | |
| func = getattr(self, "_" + prop[0].tag) | |
| except AttributeError: | |
| raise UnsupportedPropertyError(prop[0].tag) | |
| else: | |
| args = {} | |
| if getattr(func, "needsWidget", False): | |
| assert widget is not None | |
| args["widget"] = widget | |
| return func(prop[0], **args) | |
| def _getChild(self, elem_tag, elem, name, default=None): | |
| for prop in elem.findall(elem_tag): | |
| if prop.attrib["name"] == name: | |
| return self.convert(prop) | |
| else: | |
| return default | |
| def getProperty(self, elem, name, default=None): | |
| return self._getChild("property", elem, name, default) | |
| def getAttribute(self, elem, name, default=None): | |
| return self._getChild("attribute", elem, name, default) | |
| def setProperties(self, widget, elem): | |
| # Lines are sunken unless the frame shadow is explicitly set. | |
| set_sunken = (elem.attrib.get('class') == 'Line') | |
| for prop in elem.findall('property'): | |
| prop_name = prop.attrib['name'] | |
| DEBUG("setting property %s" % (prop_name,)) | |
| if prop_name == 'frameShadow': | |
| set_sunken = False | |
| try: | |
| stdset = bool(int(prop.attrib['stdset'])) | |
| except KeyError: | |
| stdset = True | |
| if not stdset: | |
| self._setViaSetProperty(widget, prop) | |
| elif hasattr(self, prop_name): | |
| getattr(self, prop_name)(widget, prop) | |
| else: | |
| prop_value = self.convert(prop, widget) | |
| if prop_value is not None: | |
| getattr(widget, 'set%s%s' % (ascii_upper(prop_name[0]), prop_name[1:]))(prop_value) | |
| if set_sunken: | |
| widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) | |
| # SPECIAL PROPERTIES | |
| # If a property has a well-known value type but needs special, | |
| # context-dependent handling, the default behaviour can be overridden here. | |
| # Delayed properties will be set after the whole widget tree has been | |
| # populated. | |
| def _delayed_property(self, widget, prop): | |
| prop_value = self.convert(prop) | |
| if prop_value is not None: | |
| prop_name = prop.attrib["name"] | |
| self.delayed_props.append((widget, False, | |
| 'set%s%s' % (ascii_upper(prop_name[0]), prop_name[1:]), | |
| prop_value)) | |
| # These properties will be set with a widget.setProperty call rather than | |
| # calling the set<property> function. | |
| def _setViaSetProperty(self, widget, prop): | |
| prop_value = self.convert(prop, widget) | |
| if prop_value is not None: | |
| prop_name = prop.attrib['name'] | |
| # This appears to be a Designer/uic hack where stdset=0 means that | |
| # the viewport should be used. | |
| if prop[0].tag == 'cursorShape': | |
| widget.viewport().setProperty(prop_name, prop_value) | |
| else: | |
| widget.setProperty(prop_name, prop_value) | |
| # Ignore the property. | |
| def _ignore(self, widget, prop): | |
| pass | |
| # Define properties that use the canned handlers. | |
| currentIndex = _delayed_property | |
| currentRow = _delayed_property | |
| showDropIndicator = _setViaSetProperty | |
| intValue = _setViaSetProperty | |
| value = _setViaSetProperty | |
| objectName = _ignore | |
| margin = _ignore | |
| leftMargin = _ignore | |
| topMargin = _ignore | |
| rightMargin = _ignore | |
| bottomMargin = _ignore | |
| spacing = _ignore | |
| horizontalSpacing = _ignore | |
| verticalSpacing = _ignore | |
| # tabSpacing is actually the spacing property of the widget's layout. | |
| def tabSpacing(self, widget, prop): | |
| prop_value = self.convert(prop) | |
| if prop_value is not None: | |
| self.delayed_props.append((widget, True, 'setSpacing', prop_value)) | |
| # buddy setting has to be done after the whole widget tree has been | |
| # populated. We can't use delay here because we cannot get the actual | |
| # buddy yet. | |
| def buddy(self, widget, prop): | |
| buddy_name = prop[0].text | |
| if buddy_name: | |
| self.buddies.append((widget, buddy_name)) | |
| # geometry is handled specially if set on the toplevel widget. | |
| def geometry(self, widget, prop): | |
| if widget.objectName() == self.uiname: | |
| geom = int_list(prop[0]) | |
| widget.resize(geom[2], geom[3]) | |
| else: | |
| widget.setGeometry(self._rect(prop[0])) | |
| def orientation(self, widget, prop): | |
| # If the class is a QFrame, it's a line. | |
| if widget.metaObject().className() == 'QFrame': | |
| # Designer v6.7.0 and later use fully qualified enum names. | |
| widget.setFrameShape( | |
| {'Qt::Orientation::Horizontal': QtWidgets.QFrame.Shape.HLine, | |
| 'Qt::Horizontal': QtWidgets.QFrame.Shape.HLine, | |
| 'Qt::Orientation::Vertical' : QtWidgets.QFrame.Shape.VLine, | |
| 'Qt::Vertical' : QtWidgets.QFrame.Shape.VLine}[prop[0].text]) | |
| else: | |
| widget.setOrientation(self._enum(prop[0])) | |
| # The isWrapping attribute of QListView is named inconsistently, it should | |
| # be wrapping. | |
| def isWrapping(self, widget, prop): | |
| widget.setWrapping(self.convert(prop)) | |
| # This is a pseudo-property injected to deal with margins. | |
| def pyuicMargins(self, widget, prop): | |
| widget.setContentsMargins(*int_list(prop)) | |
| # This is a pseudo-property injected to deal with spacing. | |
| def pyuicSpacing(self, widget, prop): | |
| horiz, vert = int_list(prop) | |
| if horiz == vert: | |
| widget.setSpacing(horiz) | |
| else: | |
| if horiz >= 0: | |
| widget.setHorizontalSpacing(horiz) | |
| if vert >= 0: | |
| widget.setVerticalSpacing(vert) | |