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