1 /*
2 For general Scribus (>=1.3.2) copyright and licensing information please refer
3 to the COPYING file provided with the program. Following this notice may exist
4 a copyright and/or license notice that predates the release of Scribus 1.3.2
5 for which a new license (GPL+exception) is in place.
6 */
7 #include "cmdgetsetprop.h"
8 #include "cmdutil.h"
9 
10 #include <QMetaObject>
11 #include <QMetaProperty>
12 #include <QList>
13 #include <QObject>
14 #include <QObjectList>
15 
getQObjectFromPyArg(PyObject * arg)16 QObject* getQObjectFromPyArg(PyObject* arg)
17 {
18 	if (PyUnicode_Check(arg))
19 		// It's a string. Look for a pageItem by that name. Do NOT accept a
20 		// selection.
21 		return getPageItemByName(PyUnicode_asQString(arg));
22 	if (PyCapsule_CheckExact(arg))
23 	{
24 		// It's a PyCObject, ie a wrapped pointer. Check it's not nullptr
25 		// and return it.
26 		// FIXME: Try to check that its a pointer to a QObject instance
27 		QObject* tempObject = (QObject*) PyCapsule_GetPointer(arg, nullptr);
28 		if (!tempObject)
29 		{
30 			PyErr_SetString(PyExc_TypeError, "INTERNAL: Passed nullptr PyCObject");
31 			return nullptr;
32 		}
33 		return tempObject;
34 	}
35 	// It's not a type we know what to do with
36 	PyErr_SetString(PyExc_TypeError, QObject::tr("Argument must be page item name, or PyCObject instance").toLocal8Bit().constData());
37 	return nullptr;
38 }
39 
40 
wrapQObject(QObject * obj)41 PyObject* wrapQObject(QObject* obj)
42 {
43 	return PyCapsule_New((void*) obj, nullptr, nullptr);
44 }
45 
46 
getpropertytype(QObject * obj,const char * propname,bool includesuper)47 const char* getpropertytype(QObject* obj, const char* propname, bool includesuper)
48 {
49 	const QMetaObject* objmeta = obj->metaObject();
50 	int i = objmeta->indexOfProperty(propname);
51 	if (i == -1)
52 		return nullptr;
53 	QMetaProperty propmeta = objmeta->property(i);
54 	if (!propmeta.isValid())
55 		return nullptr;
56 	const char* type = propmeta.typeName();
57 	return type;
58 }
59 
60 
scribus_propertyctype(PyObject *,PyObject * args,PyObject * kw)61 PyObject* scribus_propertyctype(PyObject* /*self*/, PyObject* args, PyObject* kw)
62 {
63 	PyObject* objArg = nullptr;
64 	char* propertyname = nullptr;
65 	int includesuper = 1;
66 	char* kwargs[] = {const_cast<char*>("object"),
67 					  const_cast<char*>("property"),
68 					  const_cast<char*>("includesuper"),
69 					  nullptr};
70 	if (!PyArg_ParseTupleAndKeywords(args, kw, "Oes|i", kwargs,
71 				&objArg, "ascii", &propertyname, &includesuper))
72 		return nullptr;
73 
74 	// Get the QObject* the object argument refers to
75 	QObject* obj = getQObjectFromPyArg(objArg);
76 	if (!obj)
77 		return nullptr;
78 	objArg = nullptr; // no need to decref, it's borrowed
79 
80 	// Look up the property and retrive its type information
81 	const char* type = getpropertytype( (QObject*) obj, propertyname, includesuper);
82 	if (type == nullptr)
83 	{
84 		PyErr_SetString(PyExc_KeyError, QObject::tr("Property not found").toLocal8Bit().constData());
85 		return nullptr;
86 	}
87 	return PyUnicode_FromString(type);
88 }
89 
convert_QStringList_to_PyListObject(QStringList & origlist)90 PyObject* convert_QStringList_to_PyListObject(QStringList& origlist)
91 {
92 	PyObject* resultList = PyList_New(0);
93 	if (!resultList)
94 		return nullptr;
95 
96 	for ( QStringList::Iterator it = origlist.begin(); it != origlist.end(); ++it )
97 		if (PyList_Append(resultList, PyUnicode_FromString((*it).toUtf8().data())) == -1)
98 			return nullptr;
99 
100 	return resultList;
101 }
102 
103 
convert_QObjectList_to_PyListObject(QObjectList * origlist)104 PyObject* convert_QObjectList_to_PyListObject(QObjectList* origlist)
105 {
106 	PyObject* resultList = PyList_New(0);
107 	if (!resultList)
108 		return nullptr;
109 
110 	PyObject* objPtr = nullptr;
111 	// Loop over the objects in the list and add them to the python
112 	// list wrapped in PyCObjects .
113 	for (int i = 0; i < origlist->count(); ++i)
114 	{
115 		// Wrap up the object pointer
116 		objPtr = wrapQObject(origlist->at(i));
117 		if (!objPtr)
118 		{
119 			// Failed to wrap the object. An exception is already set.
120 			Py_DECREF(resultList);
121 			return nullptr;
122 		}
123 		// and add it to the list
124 		if (PyList_Append(resultList, (PyObject*)objPtr) == -1)
125 			return nullptr;
126 	}
127 	return resultList;
128 }
129 
130 /*Qt4 we either need to copy QObject::qChildHelper or rewrite this
131 
132 PyObject* scribus_getchildren(PyObject* , PyObject* args, PyObject* kw)
133 {
134 	PyObject* objArg = nullptr;
135 	char* ofclass = nullptr;
136 	char* ofname = nullptr;
137 	int recursive = 0;
138 	int regexpmatch = 0;
139 	char* kwnames[] = {const_cast<char*>("object"),
140 					   const_cast<char*>("ofclass"),
141 					   const_cast<char*>("ofname"),
142 					   const_cast<char*>("regexpmatch"),
143 					   const_cast<char*>("recursive"),
144 					   nullptr};
145 	if (!PyArg_ParseTupleAndKeywords(args, kw, "O|esesii", kwnames,
146 				&objArg, "ascii", &ofclass, "ascii", &ofname, &regexpmatch, &recursive))
147 		return nullptr;
148 
149 	// Get the QObject* the object argument refers to
150 	QObject* obj = getQObjectFromPyArg(objArg);
151 	if (!obj)
152 		return nullptr;
153 	objArg = nullptr; // no need to decref, it's borrowed
154 
155 	// Our job is to return a Python list containing the children of this
156 	// widget (as PyCObjects).
157 //qt4 FIXME	QObjectList* children;
158 //qt4 FIXME	children = obj->queryList(ofclass, ofname, regexpmatch, recursive);
159 	PyObject* itemlist = 0;
160 //qt4 FIXME 	itemlist = convert_QObjectList_to_PyListObject(children);
161 //qt4 FIXME	delete children;
162 	return itemlist;
163 }
164 
165 
166 // Perform a recursive (by default) search for the named child, possibly of a
167 // select class.
168 PyObject* scribus_getchild(PyObject* , PyObject* args, PyObject* kw)
169 {
170 	PyObject* objArg = nullptr;
171 	char* childname = nullptr;
172 	char* ofclass = nullptr;
173 	bool recursive = true;
174 	char* kwnames[] = {const_cast<char*>("object"),
175 					   const_cast<char*>("childname"),
176 					   const_cast<char*>("ofclass"),
177 					   const_cast<char*>("recursive"),
178 					   nullptr};
179 	if (!PyArg_ParseTupleAndKeywords(args, kw, "Oes|esi", kwnames,
180 				&objArg, "ascii", &childname, "ascii", &ofclass, &recursive))
181 		return nullptr;
182 
183 	// Get the QObject* the object argument refers to
184 	QObject* obj = getQObjectFromPyArg(objArg);
185 	if (!obj)
186 		return nullptr;
187 	objArg = nullptr; // no need to decref, it's borrowed
188 
189 	// Search for the child, possibly restricting the search to children
190 	// of a particular type, and possibly recursively searching through
191 	// grandchildren etc.
192 	QObject* child = obj->child(childname, ofclass, recursive);
193 	if (child == nullptr)
194 	{
195 		PyErr_SetString(PyExc_KeyError, QObject::tr("Child not found").toLocal8Bit().constData());
196 		return nullptr;
197 	}
198 
199 	return wrapQObject(child);
200 }
201 */
202 
scribus_getpropertynames(PyObject *,PyObject * args,PyObject * kw)203 PyObject* scribus_getpropertynames(PyObject* /*self*/, PyObject* args, PyObject* kw)
204 {
205 	PyObject* objArg = nullptr;
206 	int includesuper = 1;
207 	char* kwargs[] = {const_cast<char*>("object"),
208 					  const_cast<char*>("includesuper"),
209 					  nullptr};
210 	if (!PyArg_ParseTupleAndKeywords(args, kw, "O|i", kwargs,
211 				&objArg, &includesuper))
212 		return nullptr;
213 
214 	// Get the QObject* the object argument refers to
215 	QObject* obj = getQObjectFromPyArg(objArg);
216 	if (!obj)
217 		return nullptr;
218 	objArg = nullptr; // no need to decref, it's borrowed
219 
220 	// Retrive the object's meta object so we can query it
221 	const QMetaObject* objmeta = obj->metaObject();
222 	if (!objmeta)
223 		return nullptr;
224 
225 	// Return the list of properties
226 	QStringList propertyNames;
227 	int propertyOffset = includesuper ? 0 : objmeta->propertyOffset();
228 	for (int i = propertyOffset; i < objmeta->propertyCount(); ++i)
229 		propertyNames << QString::fromLatin1(objmeta->property(i).name());
230 
231 	return convert_QStringList_to_PyListObject(propertyNames);
232 }
233 
234 
scribus_getproperty(PyObject *,PyObject * args,PyObject * kw)235 PyObject* scribus_getproperty(PyObject* /*self*/, PyObject* args, PyObject* kw)
236 {
237 	PyObject* objArg = nullptr;
238 	char* propertyName = nullptr;
239 	char* kwargs[] = {const_cast<char*>("object"),
240 					  const_cast<char*>("property"),
241 					  nullptr};
242 	if (!PyArg_ParseTupleAndKeywords(args, kw, "Oes", kwargs,
243 				&objArg, "ascii", &propertyName))
244 		return nullptr;
245 
246 	// Get the QObject* the object argument refers to
247 	QObject* obj = getQObjectFromPyArg(objArg);
248 	if (!obj)
249 		return nullptr;
250 	objArg = nullptr; // no need to decref, it's borrowed
251 
252 	// Get the QMetaProperty for the property, so we can check
253 	// if it's a set/enum and do name/value translation.
254 	const QMetaObject* objmeta = obj->metaObject();
255 	int i = objmeta->indexOfProperty(propertyName);
256 	if (i == -1)
257 	{
258 		PyErr_SetString(PyExc_ValueError,
259 				QObject::tr("Property not found").toLocal8Bit().data());
260 		return nullptr;
261 	}
262 
263 	QMetaProperty propmeta = objmeta->property(i);
264 	if (!propmeta.isValid())
265 	{
266 		PyErr_SetString(PyExc_ValueError,
267 				QObject::tr("Invalid property").toLocal8Bit().data());
268 		return nullptr;
269 	}
270 
271 	// Get the property value as a variant type
272 	QVariant prop = obj->property(propertyName);
273 
274 	// Convert the property to an instance of the closest matching Python type.
275 	PyObject* resultobj = nullptr;
276 	// NUMERIC TYPES
277 	if (prop.type() == QVariant::Int)
278 		resultobj = PyLong_FromLong(prop.toInt());
279 	else if (prop.type() == QVariant::Double)
280 		resultobj = PyFloat_FromDouble(prop.toDouble());
281 	// BOOLEAN
282 	else if (prop.type() == QVariant::Bool)
283 		resultobj = PyBool_FromLong(prop.toBool());
284 	// STRING TYPES
285 	else if (prop.type() == QVariant::ByteArray)
286 	{
287 		QByteArray ba = prop.toByteArray();
288 		resultobj = PyBytes_FromStringAndSize(ba.data(), ba.size());
289 	}
290 	else if (prop.type() == QVariant::String)
291 		resultobj = PyUnicode_FromString(prop.toString().toUtf8().data());
292 	// HIGHER ORDER TYPES
293 	else if (prop.type() == QVariant::Point)
294 	{
295 		// Return a QPoint as an (x,y) tuple.
296 		QPoint pt = prop.toPoint();
297 		return Py_BuildValue("(ii)", pt.x(), pt.y());
298 	}
299 	else if (prop.type() == QVariant::Rect)
300 	{
301 		// Return a QRect as an (x,y,width,height) tuple.
302 		// FIXME: We should really construct and return an object that
303 		// matches the API of QRect and has properties to keep
304 		// left/top/right/bottom and x/y/width/height in sync.
305 		QRect r = prop.toRect();
306 		return Py_BuildValue("(iiii)", r.x(), r.y(), r.width(), r.height());
307 	}
308 	else if (prop.type() == QVariant::StringList)
309 	{
310 		QStringList tmp = prop.toStringList();
311 		return convert_QStringList_to_PyListObject(tmp);
312 	}
313 	// UNHANDLED TYPE
314 	else
315 	{
316 		PyErr_SetString(PyExc_TypeError, QObject::tr("Couldn't convert result type '%1'.").arg(prop.typeName()).toLocal8Bit().constData() );
317 		return resultobj;
318 	}
319 
320 	// Return the resulting Python object
321 	if (resultobj == nullptr)
322 	{
323 		// An exception was set while assigning to resultobj
324 		assert(PyErr_Occurred());
325 		return nullptr;
326 	}
327 	return resultobj;
328 }
329 
330 
331 
scribus_setproperty(PyObject *,PyObject * args,PyObject * kw)332 PyObject* scribus_setproperty(PyObject* /*self*/, PyObject* args, PyObject* kw)
333 {
334 	PyObject* objArg = nullptr;
335 	char* propertyName = nullptr;
336 	PyObject* objValue = nullptr;
337 	char* kwargs[] = {const_cast<char*>("object"),
338 					  const_cast<char*>("property"),
339 					  const_cast<char*>("value"),
340 					  nullptr};
341 	if (!PyArg_ParseTupleAndKeywords(args, kw, "OesO", kwargs,
342 				&objArg, "ascii", &propertyName, &objValue))
343 		return nullptr;
344 
345 	// We're going to hang on to the value object for a while, so
346 	// claim a reference to it.
347 	Py_INCREF(objValue);
348 
349 	// Get the QObject* the object argument refers to
350 	QObject* obj = getQObjectFromPyArg(objArg);
351 	if (!obj)
352 		return nullptr;
353 	objArg = nullptr; // no need to decref, it's borrowed
354 
355 	const char* propertyTypeName = getpropertytype(obj, propertyName, true);
356 	if (propertyTypeName == nullptr)
357 		return nullptr;
358 	QString propertyType = QString::fromLatin1(propertyTypeName);
359 
360 	// Did we know how to convert the value argument to the right type?
361 	bool matched = false;
362 	// Did the set call succceed?
363 	bool success = false;
364 
365 	// Check the C++ type of the property, and try to convert the passed
366 	// PyObject to something sensible looking for it.
367 	// FIXME: handle enums/sets
368 	// NUMERIC TYPES
369 	// These are unfortuately a TOTAL PITA because of the multitude of
370 	// C and Python numeric types. TODO This needs to be broken out into a subroutine.
371 	if (propertyType == "bool")
372 	{
373 		matched = true;
374 		if (PyObject_IsTrue(objValue) == 0)
375 			success = obj->setProperty(propertyName, 0);
376 		else if (PyObject_IsTrue(objValue) == 1)
377 			success = obj->setProperty(propertyName, 1);
378 		else if (PyLong_Check(objValue))
379 			success = obj->setProperty(propertyName, PyLong_AsLong(objValue) == 0);
380 		else if (PyLong_Check(objValue))
381 			success = obj->setProperty(propertyName, PyLong_AsLong(objValue) == 0);
382 		else
383 			matched = false;
384 	}
385 	else if (propertyType == "int")
386 	{
387 		matched = true;
388 		if (PyLong_Check(objValue))
389 			success = obj->setProperty(propertyName, (int) PyLong_AsLong(objValue));
390 		else if (PyLong_Check(objValue))
391 			success = obj->setProperty(propertyName, (int) PyLong_AsLong(objValue));
392 		else
393 			matched = false;
394 	}
395 	else if (propertyType == "double")
396 	{
397 		matched = true;
398 		// FIXME: handle int, long  and bool too
399 		if (PyFloat_Check(objValue))
400 			success = obj->setProperty(propertyName, PyFloat_AsDouble(objValue));
401 		else
402 			matched = false;
403 	}
404 	// STRING TYPES
405 	else if (propertyType == "QString")
406 	{
407 		matched = true;
408 		if (PyBytes_Check(objValue))
409 			success = obj->setProperty(propertyName, QString::fromUtf8(PyBytes_AsString(objValue)));
410 		else if (PyUnicode_Check(objValue))
411 		{
412 			// Get a pointer to the internal buffer of the Py_Unicode object, which is UCS2 formatted
413 			const unsigned short * ucs2Data = (const unsigned short *) PyUnicode_AS_UNICODE(objValue);
414 			// and make a new QString from it (the string is copied)
415 			success = obj->setProperty(propertyName, QString::fromUtf16(ucs2Data));
416 		}
417 		else
418 			matched = false;
419 	}
420 	else if (propertyType == "QCString")
421 	{
422 		matched = true;
423 		if (PyBytes_Check(objValue))
424 		{
425 			// FIXME: should raise an exception instead of mangling the string when
426 			// out of charset chars present.
427 			QString utfString = QString::fromUtf8(PyBytes_AsString(objValue));
428 			success = obj->setProperty(propertyName, utfString.toLatin1());
429 		}
430 		else if (PyUnicode_Check(objValue))
431 		{
432 			// Get a pointer to the internal buffer of the Py_Unicode object, which is UCS2 formatted
433 			const unsigned short * utf16Data = (const unsigned short *)PyUnicode_AS_UNICODE(objValue);
434 			// and make a new QString from it (the string is copied)
435 			success = obj->setProperty(propertyName, QString::fromUtf16(utf16Data).toLatin1());
436 		}
437 		else
438 			matched = false;
439 	}
440 	// HIGHER ORDER TYPES
441 	// ... which I can't be stuffed supporting yet. FIXME.
442 	else
443 	{
444 		Py_DECREF(objValue);
445 		PyErr_SetString(PyExc_TypeError,
446 				QObject::tr("Property type '%1' not supported").arg(propertyType).toLocal8Bit().constData());
447 		return nullptr;
448 	}
449 
450 	// If `matched' is false, we recognised the C type but weren't able to
451 	// convert the passed Python value to anything suitable.
452 	if (!matched)
453 	{
454 		// Get a string representation of the object
455 		PyObject* objRepr = PyObject_Repr(objValue);
456 		Py_DECREF(objValue); // We're done with it now
457 		if (!objRepr)
458 			return nullptr;
459 		// Extract the repr() string
460 		QString reprString = PyUnicode_asQString(objRepr);
461 		Py_DECREF(objRepr);
462 
463 		// And return an error
464 		PyErr_SetString(PyExc_TypeError, QObject::tr("Couldn't convert '%1' to property type '%2'").arg(reprString, propertyType).toLocal8Bit().constData());
465 		return nullptr;
466 	}
467 
468 	// `success' is the return value of the setProperty() call
469 	if (!success)
470 	{
471 		Py_DECREF(objValue);
472 		PyErr_SetString(PyExc_ValueError, QObject::tr("Types matched, but setting property failed.").toLocal8Bit().constData());
473 		return nullptr;
474 	}
475 
476 	Py_DECREF(objValue);
477 	Py_RETURN_NONE;
478 }
479 
480 /*! HACK: this removes "warning: 'blah' defined but not used" compiler warnings
481 with header files structure untouched (docstrings are kept near declarations)
482 PV */
cmdgetsetpropdocwarnings()483 void cmdgetsetpropdocwarnings()
484 {
485 	QStringList s;
486 	s << scribus_getproperty__doc__
487 	  << scribus_getpropertynames__doc__
488 	  << scribus_propertyctype__doc__
489 	  << scribus_setproperty__doc__;
490 }
491