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