/Orange/canvas/utils/propertybindings.py

https://gitlab.com/zaverichintan/orange3 · Python · 327 lines · 174 code · 39 blank · 114 comment · 14 complexity · e90918a680ab2b84b1aea0879cb7ce83 MD5 · raw file

  1. """
  2. Qt Property Bindings (`propertybindings`)
  3. -----------------------------------------
  4. """
  5. import sys
  6. import ast
  7. from collections import defaultdict
  8. from operator import add
  9. from PyQt4.QtCore import QObject, QEvent
  10. from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
  11. from functools import reduce
  12. def find_meta_property(obj, name):
  13. """
  14. Return a named (`name`) `QMetaProperty` of a `QObject` instance `obj`.
  15. If a property by taht name does not exist raise an AttributeError.
  16. """
  17. meta = obj.metaObject()
  18. index = meta.indexOfProperty(name)
  19. if index == -1:
  20. raise AttributeError("%s does no have a property named %r." %
  21. (meta.className(), name))
  22. return meta.property(index)
  23. def find_notifier(obj, name):
  24. """
  25. Return the notifier signal name (`str`) for the property of
  26. `object` (instance of `QObject`).
  27. .. todo: Should it return a QMetaMethod instead?
  28. """
  29. prop_meta = find_meta_property(obj, name)
  30. if not prop_meta.hasNotifySignal():
  31. raise TypeError("%s does not have a notifier signal." %
  32. name)
  33. notifier = prop_meta.notifySignal()
  34. name = notifier.signature().split("(")[0]
  35. return name
  36. class AbstractBoundProperty(QObject):
  37. """
  38. An abstract base class for property bindings.
  39. """
  40. changed = Signal([], [object])
  41. """Emited when the property changes"""
  42. def __init__(self, obj, propertyName, parent=None):
  43. QObject.__init__(self, parent)
  44. self.obj = obj
  45. self.propertyName = propertyName
  46. self.obj.destroyed.connect(self._on_destroyed)
  47. self._source = None
  48. def set(self, value):
  49. """
  50. Set `value` to the property.
  51. """
  52. return self.obj.setProperty(self.propertyName, value)
  53. def get(self):
  54. """
  55. Return the property value.
  56. """
  57. return self.obj.property(self.propertyName)
  58. @Slot()
  59. def notifyChanged(self):
  60. """
  61. Notify the binding of a change in the property value.
  62. The default implementation emits the `changed` signals.
  63. """
  64. val = self.get()
  65. self.changed.emit()
  66. self.changed[object].emit(val)
  67. def _on_destroyed(self):
  68. self.obj = None
  69. def bindTo(self, source):
  70. """
  71. Bind this property to `source` (instance of `AbstractBoundProperty`).
  72. """
  73. if self._source != source:
  74. if self._source:
  75. self.unbind()
  76. self._source = source
  77. source.changed.connect(self.update)
  78. source.destroyed.connect(self.unbind)
  79. self.set(source.get())
  80. self.notifyChanged()
  81. def unbind(self):
  82. """
  83. Unbind the currently bound property (set with `bindTo`).
  84. """
  85. self._source.destroyed.disconnect(self.unbind)
  86. self._source.changed.disconnect(self.update)
  87. self._source = None
  88. def update(self):
  89. """
  90. Update the property value from `source` property (`bindTo`).
  91. """
  92. if self._source:
  93. source_val = self._source.get()
  94. curr_val = self.get()
  95. if source_val != curr_val:
  96. self.set(source_val)
  97. def reset(self):
  98. """
  99. Reset the property if possible.
  100. """
  101. raise NotImplementedError
  102. class PropertyBindingExpr(AbstractBoundProperty):
  103. def __init__(self, expression, globals={}, locals={}, parent=None):
  104. QObject.__init__(self, parent)
  105. self.ast = ast.parse(expression, mode="eval")
  106. self.code = compile(self.ast, "<unknown>", "eval")
  107. self.expression = expression
  108. self.globals = dict(globals)
  109. self.locals = dict(locals)
  110. self._sources = {}
  111. names = self.code.co_names
  112. for name in names:
  113. v = locals.get(name, globals.get(name))
  114. if isinstance(v, AbstractBoundProperty):
  115. self._sources[name] = v
  116. v.changed.connect(self.notifyChanged)
  117. v.destroyed.connect(self._on_destroyed)
  118. def sources(self):
  119. """Return all source property bindings appearing in the
  120. expression namespace.
  121. """
  122. return list(self._sources)
  123. def set(self, value):
  124. raise NotImplementedError("Cannot set a value of an expression")
  125. def get(self):
  126. locals = dict(self.locals)
  127. locals.update(dict((name, source.get())
  128. for name, source in self._sources.items()))
  129. try:
  130. value = eval(self.code, self.globals, locals)
  131. except Exception:
  132. raise
  133. return value
  134. def bindTo(self, source):
  135. raise NotImplementedError("Cannot bind an expression")
  136. def _on_destroyed(self):
  137. source = self.sender()
  138. self._sources.remove(source)
  139. class PropertyBinding(AbstractBoundProperty):
  140. """
  141. A Property binding of a QObject's property registered with Qt's
  142. meta class object system.
  143. """
  144. def __init__(self, obj, propertyName, notifier=None, parent=None):
  145. AbstractBoundProperty.__init__(self, obj, propertyName, parent)
  146. if notifier is None:
  147. notifier = find_notifier(obj, propertyName)
  148. if notifier is not None:
  149. signal = getattr(obj, notifier)
  150. signal.connect(self.notifyChanged)
  151. else:
  152. signal = None
  153. self.notifierSignal = signal
  154. def _on_destroyed(self):
  155. self.notifierSignal = None
  156. AbstractBoundProperty._on_destroyed(self)
  157. def reset(self):
  158. meta_prop = find_meta_property(self, self.obj, self.propertyName)
  159. if meta_prop.isResetable():
  160. meta_prop.reset(self.obj)
  161. else:
  162. return AbstractBoundProperty.reset(self)
  163. class DynamicPropertyBinding(AbstractBoundProperty):
  164. """
  165. A Property binding of a QObject's dynamic property.
  166. """
  167. def __init__(self, obj, propertyName, parent=None):
  168. AbstractBoundProperty.__init__(self, obj, propertyName, parent)
  169. obj.installEventFilter(self)
  170. def eventFilter(self, obj, event):
  171. if obj is self.obj and event.type() == QEvent.DynamicPropertyChange:
  172. if event.propertyName() == self.propertyName:
  173. self.notifyChanged()
  174. return AbstractBoundProperty.eventFilter(self, obj, event)
  175. class BindingManager(QObject):
  176. AutoSubmit = 0
  177. ManualSubmit = 1
  178. # Note: This should also apply to Gnome
  179. Default = 0 if sys.platform == "darwin" else 1
  180. def __init__(self, parent=None, submitPolicy=Default):
  181. QObject.__init__(self, parent)
  182. self._bindings = defaultdict(list)
  183. self._modified = set()
  184. self.__submitPolicy = submitPolicy
  185. def setSubmitPolicy(self, policy):
  186. if self.__submitPolicy != policy:
  187. self.__submitPolicy = policy
  188. if policy == BindingManager.AutoSubmit:
  189. self.commit()
  190. def submitPolicy(self):
  191. return self.__submitPolicy
  192. def bind(self, target, source):
  193. if isinstance(target, tuple):
  194. target = binding_for(*target + (self, ))
  195. if source is None:
  196. return UnboundBindingWrapper(target, self)
  197. else:
  198. if isinstance(source, tuple):
  199. source = binding_for(*source + (self,))
  200. source.changed.connect(self.__on_changed)
  201. self._bindings[source].append((target, source))
  202. self.__on_changed(source)
  203. return None
  204. def bindings(self):
  205. """Return (target, source) binding tuples.
  206. """
  207. return reduce(add, self._bindings.items(), [])
  208. def commit(self):
  209. self.__update()
  210. def __on_changed(self, sender=None):
  211. if sender is None:
  212. sender = self.sender()
  213. self._modified.add(sender)
  214. if self.__submitPolicy == BindingManager.AutoSubmit:
  215. self.__update()
  216. def __update(self):
  217. for modified in list(self._modified):
  218. self._modified.remove(modified)
  219. for target, source in self._bindings.get(modified, []):
  220. target.set(source.get())
  221. class UnboundBindingWrapper(object):
  222. def __init__(self, target, manager):
  223. self.target = target
  224. self.manager = manager
  225. self.__source = None
  226. def to(self, source):
  227. if self.__source is None:
  228. if isinstance(source, tuple):
  229. source = binding_for(*source + (self.manager,))
  230. self.manager.bind(self.target, source)
  231. self.__source = source
  232. else:
  233. raise ValueError("Can only call 'to' once.")
  234. def binding_for(obj, name, parent=None):
  235. """
  236. Return a suitable binding for property `name` of an `obj`.
  237. Currently only supports PropertyBinding and DynamicPropertyBinding.
  238. """
  239. if isinstance(obj, QObject):
  240. meta = obj.metaObject()
  241. index = meta.indexOfProperty(name)
  242. if index == -1:
  243. boundprop = DynamicPropertyBinding(obj, name, parent)
  244. else:
  245. boundprop = PropertyBinding(obj, name, parent)
  246. else:
  247. raise TypeError
  248. return boundprop