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