qt_utils.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. # $Id$
  2. '''Provides a nice interface to Qt's infrastructure.
  3. In this case "nice" means a Pythonic style and additional error checking.
  4. '''
  5. from PyQt4 import QtCore
  6. from inspect import getargspec, isbuiltin
  7. from re import compile as regex
  8. class _SignalWrapper(object):
  9. '''Wraps a Qt signal in a Python object to make it easier to connect to.
  10. '''
  11. signature = property(
  12. # PyLint does not recognise that "self" is a "_SignalWrapper".
  13. # pylint: disable-msg=W0212
  14. lambda self: self.__signature
  15. )
  16. def __init__(self, obj, signature, numArgs):
  17. self.__object = obj
  18. self.__signature = signature
  19. self.__numArgs = numArgs
  20. self.__macroSignature = QtCore.SIGNAL(signature)
  21. def __str__(self):
  22. return 'SIGNAL(%s)' % self.__signature
  23. def connect(self, slot, connType = QtCore.Qt.AutoCompatConnection):
  24. # Sanity check on slot.
  25. if not callable(slot):
  26. raise TypeError('Slot type not callable: %s' % type(slot))
  27. if not isbuiltin(slot):
  28. # Slot is implemented in Python; check arguments.
  29. args, varargs_, varkw_, defaults = getargspec(slot)
  30. numSlotArgs = len(args)
  31. if numSlotArgs != 0 and args[0] == 'self':
  32. numSlotArgs -= 1
  33. if defaults is not None:
  34. numSlotArgs -= len(defaults)
  35. if numSlotArgs > self.__numArgs:
  36. raise TypeError(
  37. 'Slot requires %d arguments, while signal only supplies %d'
  38. % ( numSlotArgs, self.__numArgs )
  39. )
  40. # Note: It is allowed for a slot to have less arguments than the
  41. # signal: the superfluous arguments are ignored.
  42. # Make connection.
  43. ok = QtCore.QObject.connect(
  44. self.__object, self.__macroSignature, slot, connType
  45. )
  46. # Note: I have never seen False being returned in practice, even on
  47. # failed connections.
  48. assert ok, 'Failed to connect to "%s"' % self.__signature
  49. def disconnect(self, slot):
  50. # Break connection.
  51. ok = QtCore.QObject.disconnect(
  52. self.__object, self.__macroSignature, slot
  53. )
  54. assert ok, 'Failed to disconnect'
  55. def emit(self, *args):
  56. if len(args) != self.__numArgs:
  57. raise TypeError(
  58. '%s emitted with %d arguments'
  59. % ( self.__signature, len(args) )
  60. )
  61. self.__object.emit(self.__macroSignature, *args)
  62. class _SignalDescriptor(object):
  63. '''Base class for signal declaration descriptors.
  64. '''
  65. _native = property() # abstract
  66. def __init__(self, *argTypes):
  67. self.__argTypes = argTypes
  68. self.__name = None
  69. self.__signature = None
  70. def __getName(self, obj):
  71. for clazz in obj.__class__.__mro__:
  72. for name, member in clazz.__dict__.iteritems():
  73. if member == self:
  74. return name
  75. else:
  76. raise AttributeError('Not a member of given object')
  77. def __get__(self, obj, objType = None): # pylint: disable-msg=W0613
  78. try:
  79. # Optimize for the common case.
  80. return obj.__dict__[self.__name]
  81. except KeyError:
  82. # Either self.__name is None or the object has no wrapper stored.
  83. storageName = self.__name
  84. if storageName is None:
  85. name = self.__getName(obj)
  86. self.__name = storageName = 'signal$' + name
  87. self.__signature = signature = '%s(%s)' % (
  88. name, ', '.join(self.__argTypes)
  89. )
  90. # Sanity checks on signal.
  91. if not isinstance(obj, QtCore.QObject):
  92. raise TypeError('Not a subclass of QObject: %s' % type(obj))
  93. if self._native and obj.metaObject().indexOfSignal(
  94. QtCore.QMetaObject.normalizedSignature(signature).data()
  95. ) == -1:
  96. raise AttributeError(
  97. 'No signal matching signature: %s' % signature
  98. )
  99. # If signal is defined in Python, Qt does not know about it.
  100. # However, since no-one except the Signal class should be
  101. # instantiating us, it's safe to assume the signal exists.
  102. signal = _SignalWrapper(obj, self.__signature, len(self.__argTypes))
  103. obj.__dict__[storageName] = signal
  104. return signal
  105. def __set__(self, obj, value):
  106. raise AttributeError('Cannot write signals')
  107. def __delete__(self, obj):
  108. raise AttributeError('Cannot delete signals')
  109. class QtSignal(_SignalDescriptor):
  110. '''Descriptor which makes it easy to access inherited Qt signals.
  111. Usage:
  112. class X(QtGui.QSomeWidget):
  113. valueChanged = QtSignal('int')
  114. def setValue(self, newValue):
  115. ...
  116. valueChanged.emit(newValue)
  117. ...
  118. x = X()
  119. x.valueChanged.connect(slot)
  120. '''
  121. _native = True
  122. class Signal(_SignalDescriptor):
  123. '''Descriptor which makes it easy to create signals in Python.
  124. Usage:
  125. class X(QtCore.QObject):
  126. valueChanged = Signal('int')
  127. def setValue(self, newValue):
  128. ...
  129. valueChanged.emit(newValue)
  130. ...
  131. x = X()
  132. x.valueChanged.connect(slot)
  133. '''
  134. _native = False
  135. _reSignature = regex(r'(\w+)\s*\(\s*((?:[\w:]+(?:\s*,\s*[\w:]+)*)?)\s*\)')
  136. def connect(obj, signature, slot, connType = QtCore.Qt.AutoCompatConnection):
  137. '''Connects a Qt native signal to a slot.
  138. '''
  139. match = _reSignature.match(signature)
  140. argTypes = match.group(2)
  141. if argTypes:
  142. numSignalArgs = argTypes.count(',') + 1
  143. else:
  144. numSignalArgs = 0
  145. _SignalWrapper(obj, signature, numSignalArgs).connect(slot, connType)
  146. # This is a possible implementation, but it is untested
  147. #def disconnect(obj, signature, slot):
  148. # '''Disconnects a Qt native signal to a slot.
  149. # '''
  150. # match = _reSignature.match(signature)
  151. # argTypes = match.group(2)
  152. # if argTypes:
  153. # numSignalArgs = argTypes.count(',') + 1
  154. # else:
  155. # numSignalArgs = 0
  156. # _SignalWrapper(obj, signature, numSignalArgs).disconnect(slot)