properties.py 18 KB

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