1# -*- coding: utf-8 -*- 2""" 3Mini Kross - a scripting solution inspired by Kross (http://kross.dipe.org/) 4Technically this is one of the most important modules in Scripter. 5Via the Qt meta object system it provides access to unwrapped objects. 6This code uses a lot of metaprogramming magic. To fully understand it, 7you have to know about metaclasses in Python 8""" 9from __future__ import with_statement 10import sip 11from PyQt4.QtCore import ( 12 QMetaObject, Q_RETURN_ARG, QString, Q_ARG, 13 QObject, QVariant, Qt, SIGNAL, QMetaMethod) 14from PyQt4.QtGui import QBrush, QFont, QPixmap, qApp, QImage, QPalette 15 16 17variant_converter = { 18 # XXX QList<type>, QMap<*>, longlong 19 "QVariantList": lambda v: from_variantlist(v), 20 "QList<QVariant>": lambda v: v.toList(), 21 "int": lambda v: v.toInt()[0], 22 "double": lambda v: v.toDouble()[0], 23 "char": lambda v: v.toChar(), 24 "QByteArray": lambda v: v.toByteArray(), 25 "QString": lambda v: unicode(v.toString()), 26 "QPoint": lambda v: v.toPoint(), 27 "QPointF": lambda v: v.toPointF(), 28 "QSize": lambda v: v.toSize(), 29 "QLine": lambda v: v.toLine(), 30 "QStringList": lambda v: v.toStringList(), 31 "QTime": lambda v: v.toTime(), 32 "QDateTime": lambda v: v.toDateTime(), 33 "QDate": lambda v: v.toDate(), 34 "QLocale": lambda v: v.toLocale(), 35 "QUrl": lambda v: v.toUrl(), 36 "QRect": lambda v: v.toRect(), 37 "QBrush": lambda v: QBrush(v), 38 "QFont": lambda v: QFont(v), 39 "QPalette": lambda v: QPalette(v), 40 "QPixmap": lambda v: QPixmap(v), 41 "QImage": lambda v: QImage(v), 42 "bool": lambda v: v.toBool(), 43 "QObject*": lambda v: Scripter.fromVariant(v), 44 "QWidget*": lambda v: Scripter.fromVariant(v), 45} 46 47 48 49def from_variantlist(variantlist): 50 """ 51 convert QList<QVariant> to a normal Python list 52 """ 53 return [from_variant(variant) for variant in variantlist.toList()] 54 55 56 57def classname(obj): 58 """ 59 return real class name 60 Unwrapped classes will be represended in PyQt by a known base class. 61 So obj.__class__.__name__ will not return the desired class name 62 """ 63 return obj.metaObject().className() 64 65 66 67def from_variant(variant): 68 """ 69 convert a QVariant to a Python value 70 """ 71 typeName = variant.typeName() 72 convert = variant_converter.get(typeName) 73 if not convert: 74 raise ValueError, "Could not convert value to %s" % typeName 75 else: 76 return convert(variant) 77 78 79 80qtclasses = {} 81 82def supercast(obj): 83 """ 84 cast a QObject subclass to the best known wrapped super class 85 """ 86 if not qtclasses: 87 # To get really all Qt classes I would have to 88 # import QtNetwork, QtXml, QtSvg and QtScript, too. 89 import PyQt4 90 qtclasses.update( 91 dict([(key, value) \ 92 for key, value in PyQt4.QtCore.__dict__.items() + PyQt4.QtGui.__dict__.items() \ 93 if hasattr(value, "__subclasses__") and issubclass(value, QObject)]) 94 ) 95 try: 96 if not issubclass(value, QObject): 97 return obj 98 except TypeError: 99 # no class - no cast... 100 return obj 101 mo = obj.metaObject() 102 while mo: 103 cls = qtclasses.get(str(mo.className())) 104 if cls: 105 return sip.cast(obj, cls) 106 mo = mo.superClass() 107 # This should never be reached 108 return obj 109 110 111 112def wrap(obj, force=False): 113 """ 114 If a class is not known by PyQt it will be automatically 115 casted to a known wrapped super class. 116 But that limits access to methods and propperties of this super class. 117 So instead this functions returns a wrapper class (PyQtClass) 118 which queries the metaObject and provides access to 119 all slots and all properties. 120 """ 121 if isinstance(obj, QString): 122 # prefer Python strings 123 return unicode(obj) 124 elif isinstance(obj, PyQtClass): 125 # already wrapped 126 return obj 127 if obj and isinstance(obj, QObject): 128 if force or obj.__class__.__name__ != obj.metaObject().className(): 129 # Ah this is an unwrapped class 130 obj = create_pyqt_object(obj) 131 return obj 132 133 134 135def is_wrapped(obj): 136 """ 137 checks if a object is wrapped by PyQtClass 138 """ 139 # XXX: Any better/faster check? 140 return hasattr(obj, "qt") 141 142 143 144def unwrap(obj): 145 """ 146 if wrapped returns the wrapped object 147 """ 148 if is_wrapped(obj): 149 obj = obj.qt 150 return obj 151 152 153 154def is_qobject(obj): 155 """ 156 checks if class or wrapped class is a subclass of QObject 157 """ 158 if hasattr(obj, "__bases__") and issubclass(unwrap(obj), QObject): 159 return True 160 else: 161 return False 162 163 164def is_scripter_child(qobj): 165 """ 166 walk up the object tree until Scripter or the root is found 167 """ 168 found = False 169 p = qobj.parent() 170 while p and not found: 171 if str(p.objectName()) == "Scripter": 172 found = True 173 break 174 else: 175 p = p.parent() 176 return found 177 178 179 180class Error(Exception): 181 """ 182 Base error classed. Catch this to handle exceptions comming from C++ 183 """ 184 185 186 187class PyQtClass(object): 188 """ 189 Base class 190 """ 191 192 def __init__(self, instance): 193 self._instance = instance 194 195 196 def __del__(self): 197 """ 198 If this object is deleted it should also delete the wrapped object 199 if it was created explicitly for this use. 200 """ 201 qobj = self._instance 202 if is_scripter_child(qobj): 203 if len(qobj.children()): 204 print "Cannot delete", qobj, "because it has child objects" 205 #else: 206 # print "* deleting", qobj 207 # XXX: or better setdeleted ? 208 sip.delete(qobj) 209 #else: 210 # print "* NOT deleting", qobj 211 212 213 def setProperty(self, name, value): 214 self._instance.setProperty(name, QVariant(value)) 215 216 217 def getProperty(self, name): 218 return wrap(self._instance.property(name)) 219 220 221 def propertyNames(self): 222 return self.__class__.__properties__.keys() 223 224 225 def dynamicPropertyNames(self): 226 return self._instance.dynamicPropertyNames() 227 228 229 def metaObject(self): 230 return self._instance.metaObject() 231 232 233 def connect(self, signal, slot): 234 self._instance.connect(self._instance, SIGNAL(signal), slot) 235 236 237 def disconnect(self, signal, slot): 238 self._instance.disconnect(self._instance, SIGNAL(signal), slot) 239 240 241 def parent(self): 242 return wrap(self._instance.parent()) 243 244 245 def children(self): 246 return [wrap(c) for c in self._instance.children()] 247 248 249 @property 250 def qt(self): 251 return self._instance 252 253 254 def __getitem__(self, key): 255 if isinstance(key, int): 256 length = getattr(self, "length", None) 257 if length is not None: 258 # array protocol 259 try: 260 return getattr(self, str(key)) 261 except AttributeError, e: 262 raise IndexError, key 263 else: 264 return self.children()[key] 265 else: 266 return getattr(self, key) 267 268 269 def __getattr__(self, name): 270 # Make named child objects available as attributes like QtScript 271 for child in self._instance.children(): 272 if str(child.objectName()) == name: 273 obj = wrap(child) 274 # save found object for faster lookup 275 setattr(self, name, obj) 276 return obj 277 # Dynamic object property? 278 variant = self._instance.property(name) 279 if variant.type() != 0: 280 return from_variant(variant) 281 raise AttributeError, name 282 283 284 @property 285 def __members__(self): 286 """ 287 This method is for introspection. 288 Using dir(thispyqtclass_object) returns a list of 289 all children, methods, properties and dynamic properties. 290 """ 291 names = self.__dict__.keys() 292 for c in self._instance.children(): 293 child_name = str(c.objectName()) 294 if child_name: 295 names.append(child_name) 296 # XXX: add unnamed childs? 297 for pn in self._instance.dynamicPropertyNames(): 298 names.append(str(pn)) 299 return names 300 301 302 def __enter__(self): 303 print "__enter__", self 304 305 306 def __exit__(self, exc_type, exc_value, traceback): 307 print "__exit__", self, exc_type, exc_value, traceback 308 309 310 311 312class PyQtProperty(object): 313 314 # slots for more speed 315 __slots__ = ["meta_property", "name", "__doc__", "read_only"] 316 317 318 def __init__(self, meta_property): 319 self.meta_property = meta_property 320 self.name = meta_property.name() 321 self.read_only = not meta_property.isWritable() 322 self.__doc__ = "%s is a %s%s" % ( 323 self.name, meta_property.typeName(), 324 self.read_only and " (read-only)" or "" 325 ) 326 327 328 def get(self, obj): 329 return from_variant(self.meta_property.read(obj._instance)) 330 331 332 def set(self, obj, value): 333 self.meta_property.write(obj._instance, QVariant(value)) 334 335 336 337 338class PyQtMethod(object): 339 340 __slots__ = ["meta_method", "name", "args", "returnType", "__doc__"] 341 342 343 def __init__(self, meta_method): 344 self.meta_method = meta_method 345 self.name, args = str(meta_method.signature()).split("(", 1) 346 self.args = args[:-1].split(",") 347 self.returnType = str(meta_method.typeName()) 348 349 types = [str(t) for t in meta_method.parameterTypes()] 350 names = [str(n) or "arg%i" % (i+1) \ 351 for i, n in enumerate(meta_method.parameterNames())] 352 params = ", ".join("%s %s" % (t, n) for n, t in zip(types, names)) 353 354 self.__doc__ = "%s(%s)%s" % ( 355 self.name, params, 356 self.returnType and (" -> %s" % self.returnType) or "" 357 ) 358 359 360 def instancemethod(self): 361 def wrapper(obj, *args): 362 # XXX: support kwargs? 363 qargs = [Q_ARG(t, v) for t, v in zip(self.args, args)] 364 invoke_args = [obj._instance, self.name] 365 invoke_args.append(Qt.DirectConnection) 366 rtype = self.returnType 367 if rtype: 368 invoke_args.append(Q_RETURN_ARG(rtype)) 369 invoke_args.extend(qargs) 370 try: 371 result = QMetaObject.invokeMethod(*invoke_args) 372 error_msg = str(qApp.property("MIKRO_EXCEPTION").toString()) 373 if error_msg: 374 # clear message 375 qApp.setProperty("MIKRO_EXCEPTION", QVariant()) 376 raise Error(error_msg) 377 except RuntimeError, e: 378 raise TypeError, \ 379 "%s.%s(%r) call failed: %s" % (obj, self.name, args, e) 380 return wrap(result) 381 wrapper.__doc__ = self.__doc__ 382 return wrapper 383 384 385 386 387# Cache on-the-fly-created classes for better speed 388# XXX Should I use weak references? 389pyqt_classes = {} 390 391def create_pyqt_class(metaobject): 392 class_name = str(metaobject.className()) 393 cls = pyqt_classes.get(class_name) 394 if cls: 395 return cls 396 attrs = {} 397 398 properties = attrs["__properties__"] = {} 399 for i in range(metaobject.propertyCount()): 400 prop = PyQtProperty(metaobject.property(i)) 401 prop_name = str(prop.name) 402 #prop_name = prop_name[0].upper() + prop_name[1:] 403 if prop.read_only: 404 # XXX: write set-method which raises an error 405 properties[prop_name] = attrs[prop_name] = property(prop.get, doc=prop.__doc__) 406 else: 407 properties[prop_name] = attrs[prop_name] = property( 408 prop.get, prop.set, doc=prop.__doc__) 409 410 methods = attrs["__methods__"] = {} 411 for i in range(metaobject.methodCount()): 412 meta_method = metaobject.method(i) 413 if meta_method.methodType() != QMetaMethod.Signal: 414 method = PyQtMethod(meta_method) 415 method_name = method.name 416 if method_name in attrs: 417 # There is already a property with this name 418 # So append an underscore 419 method_name += "_" 420 instance_method = method.instancemethod() 421 instance_method.__doc__ = method.__doc__ 422 methods[method_name] = attrs[method_name] = instance_method 423 424 # Python is great :) 425 # It can dynamically create a class with a base class and a dictionary 426 cls = type(class_name, (PyQtClass,), attrs) 427 pyqt_classes[class_name] = cls 428 return cls 429 430 431 432def create_pyqt_object(obj): 433 """ 434 Wrap a QObject and make all slots and properties dynamically available. 435 @type obj: QObject 436 @param obj: an unwrapped QObject 437 @rtype: PyQtClass object 438 @return: dynamicaly created object with all available properties and slots 439 440 This is probably the only function you need from this module. 441 Everything else are helper functions and classes. 442 """ 443 cls = create_pyqt_class(obj.metaObject()) 444 return cls(obj) 445 446 447 448 449 450