From e6780d947d605882026aa6ab412c7a8ccb5818c0 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 16 Mar 2021 00:48:56 +0100 Subject: [PATCH] Add support for browsing in the python plugin api --- .../integrations/python/pybrowseractioninfo.h | 121 +++++++++++++ .../integrations/python/pybrowseresult.h | 171 ++++++++++++++++++ .../integrations/python/pybrowseritem.h | 118 ++++++++++++ .../integrations/python/pybrowseritemresult.h | 164 +++++++++++++++++ .../integrations/python/pynymeamodule.h | 8 + .../integrations/pythonintegrationplugin.cpp | 79 +++++++- .../integrations/pythonintegrationplugin.h | 3 + libnymea-core/libnymea-core.pro | 4 + libnymea/types/browseritem.h | 1 + plugins/mock/integrationpluginmock.cpp | 2 +- plugins/pymock/integrationpluginpymock.json | 1 + plugins/pymock/integrationpluginpymock.py | 23 ++- 12 files changed, 690 insertions(+), 5 deletions(-) create mode 100644 libnymea-core/integrations/python/pybrowseractioninfo.h create mode 100644 libnymea-core/integrations/python/pybrowseresult.h create mode 100644 libnymea-core/integrations/python/pybrowseritem.h create mode 100644 libnymea-core/integrations/python/pybrowseritemresult.h diff --git a/libnymea-core/integrations/python/pybrowseractioninfo.h b/libnymea-core/integrations/python/pybrowseractioninfo.h new file mode 100644 index 00000000..bcfb298c --- /dev/null +++ b/libnymea-core/integrations/python/pybrowseractioninfo.h @@ -0,0 +1,121 @@ +#ifndef PYBROWSERACTIONINFO_H +#define PYBROWSERACTIONINFO_H + +#include +#include "structmember.h" + +#include "pything.h" + +#include "integrations/browseractioninfo.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * + * When using this, make sure to call PyBrowserActionInfo_setInfo() while holding the GIL to initialize + * stuff after constructing it. Also set info to nullptr while holding the GIL when the info object vanishes. + * + * The BrowserActionInfo class is not threadsafe and self->info is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the setup and destroyed it but the PyThingSetupInfo is not garbage collected yet. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + +typedef struct { + PyObject_HEAD + BrowserActionInfo* info; + PyThing *pyThing; + PyObject *pyItemId; +} PyBrowserActionInfo; + + +static PyObject* PyBrowserActionInfo_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) { + PyBrowserActionInfo *self = (PyBrowserActionInfo*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyBrowserActionInfo"; + return (PyObject*)self; +} + +void PyBrowserActionInfo_setInfo(PyBrowserActionInfo *self, BrowserActionInfo *info, PyThing *pyThing) +{ + self->info = info; + self->pyThing = pyThing; + Py_INCREF(pyThing); + self->pyItemId = PyUnicode_FromString(info->browserAction().itemId().toUtf8()); +} + +static void PyBrowserActionInfo_dealloc(PyBrowserActionInfo * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyBrowserActionInfo"; + Py_DECREF(self->pyThing); + Py_DECREF(self->pyItemId); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyBrowserActionInfo_finish(PyBrowserActionInfo* self, PyObject* args) { + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\")"); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->info) { + QMetaObject::invokeMethod(self->info, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + + Py_RETURN_NONE; +} + +static PyMemberDef PyBrowserActionInfo_members[] = { + {"thing", T_OBJECT_EX, offsetof(PyBrowserActionInfo, pyThing), 0, "Thing this action is for"}, + {"itemId", T_OBJECT_EX, offsetof(PyBrowserActionInfo, pyItemId), 0, "The browser item id to be executed"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyBrowserActionInfo_methods[] = { + { "finish", (PyCFunction)PyBrowserActionInfo_finish, METH_VARARGS, "finish an action" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyBrowserActionInfoType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.BrowserActionInfo", /* tp_name */ + sizeof(PyBrowserActionInfo), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyBrowserActionInfo_dealloc, /* tp_dealloc */ +}; + +static void registerBrowserActionInfoType(PyObject *module) +{ + PyBrowserActionInfoType.tp_new = (newfunc)PyBrowserActionInfo_new; + PyBrowserActionInfoType.tp_flags = Py_TPFLAGS_DEFAULT; + PyBrowserActionInfoType.tp_methods = PyBrowserActionInfo_methods; + PyBrowserActionInfoType.tp_members = PyBrowserActionInfo_members; + PyBrowserActionInfoType.tp_doc = "The BrowserActionInfo is used to execute browser items"; + + if (PyType_Ready(&PyBrowserActionInfoType) < 0) { + return; + } + PyModule_AddObject(module, "BrowserActionInfo", (PyObject *)&PyBrowserActionInfoType); +} + + +#pragma GCC diagnostic pop + +#endif // PYBROWSERACTIONINFO_H diff --git a/libnymea-core/integrations/python/pybrowseresult.h b/libnymea-core/integrations/python/pybrowseresult.h new file mode 100644 index 00000000..7dd5a9b9 --- /dev/null +++ b/libnymea-core/integrations/python/pybrowseresult.h @@ -0,0 +1,171 @@ +#ifndef PYBROWSERESULT_H +#define PYBROWSERESULT_H + +#include +#include "structmember.h" + +#include "pything.h" + +#include "integrations/browseresult.h" +#include "pybrowseritem.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * + * When using this, make sure to call PyBrowseResult_setBrowseResult() while holding the GIL to initialize + * stuff after constructing it. Also set broeseResult to nullptr while holding the GIL when the browseResult object vanishes. + * + * The BrowseResult class is not threadsafe and self->browseResult is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the setup and destroyed it but the PyThingSetupInfo is not garbage collected yet. + * + * For reading access, we keep copies of the BrowseResults properties here. + * + */ + +typedef struct { + PyObject_HEAD + BrowseResult* browseResult; + PyThing *pyThing; + PyObject *pyItemId; + PyObject *pyLocale; +} PyBrowseResult; + + +static PyObject* PyBrowseResult_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) { + PyBrowseResult *self = (PyBrowseResult*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyBrowseResult"; + return (PyObject*)self; +} + +void PyBrowseResult_setBrowseResult(PyBrowseResult *self, BrowseResult *browseResult, PyThing *pyThing) +{ + self->browseResult = browseResult; + self->pyThing = pyThing; + Py_INCREF(pyThing); + self->pyItemId = PyUnicode_FromString(browseResult->itemId().toUtf8()); + self->pyLocale = PyUnicode_FromString(browseResult->locale().name().toUtf8()); +} + + +static void PyBrowseResult_dealloc(PyBrowseResult* self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyBrowseResult"; + Py_DECREF(self->pyThing); + Py_DECREF(self->pyItemId); + Py_DECREF(self->pyLocale); + Py_TYPE(self)->tp_free(self); +} + +static PyObject* PyBrowseResult_addItem(PyBrowseResult *self, PyObject *args) { + Q_UNUSED(self) + PyObject *pyObj = nullptr; + if (!PyArg_ParseTuple(args, "O", &pyObj)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in addItem call. Expected: addItem(BrowserItem)"); + return nullptr; + } + if (pyObj->ob_type != &PyBrowserItemType) { + PyErr_SetString(PyExc_ValueError, "Invalid argument to BrowseResult.addItem(BrowserItem). Not a BrowserItem."); + return nullptr; + } + PyBrowserItem *pyBrowserItem = (PyBrowserItem*)pyObj; + + QString id; + if (pyBrowserItem->pyId) { + id = QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyId)); + } + QString displayName; + if (pyBrowserItem->pyDisplayName) { + displayName = QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyDisplayName)); + } + + BrowserItem browserItem(id, displayName); + if (pyBrowserItem->pyDescription) { + browserItem.setDescription(QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyDescription))); + } + + if (pyBrowserItem->pyThumbnail) { + browserItem.setThumbnail(QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyThumbnail))); + } + + browserItem.setBrowsable(pyBrowserItem->browsable); + browserItem.setExecutable(pyBrowserItem->executable); + browserItem.setDisabled(pyBrowserItem->disabled); + browserItem.setIcon(static_cast(pyBrowserItem->icon)); + + if (self->browseResult) { + QMetaObject::invokeMethod(self->browseResult, "addItem", Qt::QueuedConnection, Q_ARG(BrowserItem, browserItem)); + } + Py_RETURN_NONE; +} + +static PyObject* PyBrowseResult_finish(PyBrowseResult* self, PyObject* args) { + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\")"); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->browseResult) { + QMetaObject::invokeMethod(self->browseResult, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + + Py_RETURN_NONE; +} + + +static PyMemberDef PyBrowseResult_members[] = { + {"thing", T_OBJECT_EX, offsetof(PyBrowseResult, pyThing), 0, "Thing this browse request is for"}, + {"itemId", T_OBJECT_EX, offsetof(PyBrowseResult, pyItemId), 0, "The itemId of the item that should be browsed. Empty if the root item is requested"}, + {"locale", T_OBJECT_EX, offsetof(PyBrowseResult, pyLocale), 0, "The locale strings should be translated to."}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyBrowseResult_methods[] = { + { "addItem", (PyCFunction)PyBrowseResult_addItem, METH_VARARGS, "Add a browser item to the result"}, + { "finish", (PyCFunction)PyBrowseResult_finish, METH_VARARGS, "Finish a browse request" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyBrowseResultType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.BrowseResult", /* tp_name */ + sizeof(PyBrowseResult), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyBrowseResult_dealloc, /* tp_dealloc */ +}; + +static void registerBrowseResultType(PyObject *module) +{ + PyBrowseResultType.tp_new = (newfunc)PyBrowseResult_new; + PyBrowseResultType.tp_flags = Py_TPFLAGS_DEFAULT; + PyBrowseResultType.tp_methods = PyBrowseResult_methods; + PyBrowseResultType.tp_members = PyBrowseResult_members; + PyBrowseResultType.tp_doc = "The BrowseResult is used fetch browser entries from things"; + + if (PyType_Ready(&PyBrowseResultType) < 0) { + return; + } + PyModule_AddObject(module, "BrowseResult", (PyObject *)&PyBrowseResultType); +} + + +#pragma GCC diagnostic pop + + +#endif // PYBROWSERESULT_H diff --git a/libnymea-core/integrations/python/pybrowseritem.h b/libnymea-core/integrations/python/pybrowseritem.h new file mode 100644 index 00000000..e5d4825a --- /dev/null +++ b/libnymea-core/integrations/python/pybrowseritem.h @@ -0,0 +1,118 @@ +#ifndef PYBROWSERITEM_H +#define PYBROWSERITEM_H + +#include +#include "structmember.h" + +#include + +#include "types/browseritem.h" +#include "loggingcategories.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + + +typedef struct { + PyObject_HEAD + PyObject* pyId; + PyObject* pyDisplayName; + PyObject* pyDescription; + PyObject* pyThumbnail; + bool browsable = false; + bool executable = false; + bool disabled = false; + int icon = (int)BrowserItem::BrowserIconNone; +} PyBrowserItem; + + +static PyMemberDef PyBrowserItem_members[] = { + {"id", T_OBJECT_EX, offsetof(PyBrowserItem, pyId), 0, "BrowserItem id"}, + {"displayName", T_OBJECT_EX, offsetof(PyBrowserItem, pyDisplayName), 0, "The name of this item"}, + {"description", T_OBJECT_EX, offsetof(PyBrowserItem, pyDescription), 0, "The description of this item"}, + {"tumbnail", T_OBJECT_EX, offsetof(PyBrowserItem, pyThumbnail), 0, "An URL pointing to the thumbnail"}, + {"browsable", T_OBJECT_EX, offsetof(PyBrowserItem, browsable), 0, "A boolean if this item can be browsed (e.g. a folder)"}, + {"executable", T_OBJECT_EX, offsetof(PyBrowserItem, executable), 0, "A boolean if this item can be launched"}, + {"disabled", T_OBJECT_EX, offsetof(PyBrowserItem, disabled), 0, "A boolean if this item is disabled"}, + {"icon", T_OBJECT_EX, offsetof(PyBrowserItem, icon), 0, "The icon to be used"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static int PyBrowserItem_init(PyBrowserItem *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"id", "displayName", "description", "thumbnail", "browsable", "executable", "disabled", "icon", nullptr}; + PyObject *id = nullptr, *displayName = nullptr, *description = nullptr, *thumbnail = nullptr; + bool browsable = false, executable = false, disabled = false; + int icon = (int)BrowserItem::BrowserIconNone; + + qCDebug(dcPythonIntegrations()) << "+++ PyBrowserItem"; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOObbbi", kwlist, &id, &displayName, &description, &thumbnail, &browsable, &executable, &disabled, &icon)) + return -1; + + if (id) { + Py_INCREF(id); + self->pyId = id; + } + if (displayName) { + Py_INCREF(displayName); + self->pyDisplayName = displayName; + } + if (description) { + Py_INCREF(description); + self->pyDescription = description; + } + if (thumbnail) { + Py_INCREF(thumbnail); + self->pyThumbnail = thumbnail; + } + self->browsable = browsable; + self->executable = executable; + self->disabled = disabled; + self->icon = icon; + return 0; +} + +static void PyBrowserItem_dealloc(PyBrowserItem* self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyBrowserItem"; + Py_XDECREF(self->pyId); + Py_XDECREF(self->pyDisplayName); + Py_XDECREF(self->pyDescription); + Py_XDECREF(self->pyThumbnail); + Py_TYPE(self)->tp_free(self); +} + +static PyTypeObject PyBrowserItemType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.BrowserItem", /* tp_name */ + sizeof(PyBrowserItem), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyBrowserItem_dealloc, /* tp_dealloc */ +}; + + + +static void registerBrowserItemType(PyObject *module) +{ + PyBrowserItemType.tp_new = PyType_GenericNew; + PyBrowserItemType.tp_members = PyBrowserItem_members; + PyBrowserItemType.tp_init = reinterpret_cast(PyBrowserItem_init); + PyBrowserItemType.tp_doc = "BrowserItems are used to return entries in a things browser to nymea."; + PyBrowserItemType.tp_flags = Py_TPFLAGS_DEFAULT; + + if (PyType_Ready(&PyBrowserItemType) < 0) { + return; + } + PyModule_AddObject(module, "BrowserItem", reinterpret_cast(&PyBrowserItemType)); + + QMetaEnum browserIconEnum = QMetaEnum::fromType(); + for (int i = 0; i < browserIconEnum.keyCount(); i++) { + PyModule_AddObject(module, browserIconEnum.key(i), PyLong_FromLong(browserIconEnum.value(i))); + } +} + +#pragma GCC diagnostic pop + +#endif // PYBROWSERITEM_H diff --git a/libnymea-core/integrations/python/pybrowseritemresult.h b/libnymea-core/integrations/python/pybrowseritemresult.h new file mode 100644 index 00000000..c9abb29e --- /dev/null +++ b/libnymea-core/integrations/python/pybrowseritemresult.h @@ -0,0 +1,164 @@ +#ifndef PYBROWSERITEMRESULT_H +#define PYBROWSERITEMRESULT_H + +#include +#include "structmember.h" + +#include "pything.h" + +#include "integrations/browseritemresult.h" +#include "pybrowseritem.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * + * When using this, make sure to call PyBrowseResult_setBrowseResult() while holding the GIL to initialize + * stuff after constructing it. Also set broeseResult to nullptr while holding the GIL when the browseResult object vanishes. + * + * The BrowseResult class is not threadsafe and self->browseResult is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the setup and destroyed it but the PyThingSetupInfo is not garbage collected yet. + * + * For reading access, we keep copies of the BrowseResults properties here. + * + */ + +typedef struct { + PyObject_HEAD + BrowserItemResult* browserItemResult; + PyThing *pyThing; + PyObject *pyItemId; + PyObject *pyLocale; +} PyBrowserItemResult; + + +static PyObject* PyBrowserItemResult_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) { + PyBrowserItemResult *self = (PyBrowserItemResult*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyBrowserItemResult"; + return (PyObject*)self; +} + +void PyBrowserItemResult_setBrowserItemResult(PyBrowserItemResult *self, BrowserItemResult *browserItemResult, PyThing *pyThing) +{ + self->browserItemResult = browserItemResult; + self->pyThing = pyThing; + Py_INCREF(pyThing); + self->pyItemId = PyUnicode_FromString(browserItemResult->itemId().toUtf8()); + self->pyLocale = PyUnicode_FromString(browserItemResult->locale().name().toUtf8()); +} + + +static void PyBrowserItemResult_dealloc(PyBrowserItemResult* self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyBrowserItemResult"; + Py_DECREF(self->pyThing); + Py_DECREF(self->pyItemId); + Py_DECREF(self->pyLocale); + Py_TYPE(self)->tp_free(self); +} + +static PyObject* PyBrowserItemResult_finish(PyBrowserItemResult* self, PyObject* args) { + PyObject *pyObj; + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "|Ois", &pyObj, &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(BrowserItem) or finish(ThingError, message = \"\")"); + return nullptr; + } + + if (pyObj != nullptr) { + if (pyObj->ob_type != &PyBrowserItemType) { + PyErr_SetString(PyExc_ValueError, "Invalid argument to BrowserItemResult.finish(BrowserItem). Not a BrowserItem."); + return nullptr; + } + PyBrowserItem *pyBrowserItem = (PyBrowserItem*)pyObj; + QString id; + if (pyBrowserItem->pyId) { + id = QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyId)); + } + QString displayName; + if (pyBrowserItem->pyDisplayName) { + displayName = QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyDisplayName)); + } + + BrowserItem browserItem(id, displayName); + if (pyBrowserItem->pyDescription) { + browserItem.setDescription(QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyDescription))); + } + + if (pyBrowserItem->pyThumbnail) { + browserItem.setThumbnail(QString::fromUtf8(PyUnicode_AsUTF8(pyBrowserItem->pyThumbnail))); + } + + browserItem.setBrowsable(pyBrowserItem->browsable); + browserItem.setExecutable(pyBrowserItem->executable); + browserItem.setDisabled(pyBrowserItem->disabled); + browserItem.setIcon(static_cast(pyBrowserItem->icon)); + + if (self->browserItemResult) { + QMetaObject::invokeMethod(self->browserItemResult, "finish", Qt::QueuedConnection, Q_ARG(BrowserItem, browserItem)); + } + Py_RETURN_NONE; + } + + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->browserItemResult) { + QMetaObject::invokeMethod(self->browserItemResult, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + + Py_RETURN_NONE; +} + + +static PyMemberDef PyBrowserItemResult_members[] = { + {"thing", T_OBJECT_EX, offsetof(PyBrowserItemResult, pyThing), 0, "Thing this browse request is for"}, + {"itemId", T_OBJECT_EX, offsetof(PyBrowserItemResult, pyItemId), 0, "The itemId of the item to be returned."}, + {"locale", T_OBJECT_EX, offsetof(PyBrowserItemResult, pyLocale), 0, "The locale strings should be translated to."}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyBrowserItemResult_methods[] = { + { "finish", (PyCFunction)PyBrowserItemResult_finish, METH_VARARGS, "Finish a browser item request" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyBrowserItemResultType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.BrowserItemResult", /* tp_name */ + sizeof(PyBrowserItemResult), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyBrowserItemResult_dealloc, /* tp_dealloc */ +}; + +static void registerBrowserItemResultType(PyObject *module) +{ + PyBrowserItemResultType.tp_new = (newfunc)PyBrowserItemResult_new; + PyBrowserItemResultType.tp_flags = Py_TPFLAGS_DEFAULT; + PyBrowserItemResultType.tp_methods = PyBrowserItemResult_methods; + PyBrowserItemResultType.tp_members = PyBrowserItemResult_members; + PyBrowserItemResultType.tp_doc = "The BrowserItemResult is used fetch an individual entry from the thing browser"; + + if (PyType_Ready(&PyBrowserItemResultType) < 0) { + return; + } + PyModule_AddObject(module, "BrowserItemResult", (PyObject *)&PyBrowserItemResultType); +} + + +#pragma GCC diagnostic pop + +#endif // PYBROWSERITEMRESULT_H diff --git a/libnymea-core/integrations/python/pynymeamodule.h b/libnymea-core/integrations/python/pynymeamodule.h index 8936ee65..489b8270 100644 --- a/libnymea-core/integrations/python/pynymeamodule.h +++ b/libnymea-core/integrations/python/pynymeamodule.h @@ -13,6 +13,10 @@ #include "pythingpairinginfo.h" #include "pypluginstorage.h" #include "pyapikeystorage.h" +#include "pybrowseresult.h" +#include "pybrowseritem.h" +#include "pybrowseractioninfo.h" +#include "pybrowseritemresult.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Winvalid-offsetof" @@ -31,6 +35,10 @@ static int nymea_exec(PyObject *m) { registerThingActionInfoType(m); registerPluginStorageType(m); registerApiKeyStorageType(m); + registerBrowseResultType(m); + registerBrowserItemType(m); + registerBrowserActionInfoType(m); + registerBrowserItemResultType(m); return 0; } diff --git a/libnymea-core/integrations/pythonintegrationplugin.cpp b/libnymea-core/integrations/pythonintegrationplugin.cpp index ee3b334f..4db252a9 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.cpp +++ b/libnymea-core/integrations/pythonintegrationplugin.cpp @@ -570,13 +570,11 @@ void PythonIntegrationPlugin::executeAction(ThingActionInfo *info) PyEval_ReleaseThread(m_threadState); connect(info, &ThingActionInfo::destroyed, this, [=](){ - qCDebug(dcPythonIntegrations()) << "Info destroyed"; + qCDebug(dcPythonIntegrations()) << "ThingActionInfo destroyed"; PyEval_RestoreThread(m_threadState); - qCDebug(dcPythonIntegrations()) << "Info destroyed2"; pyInfo->info = nullptr; Py_DECREF(pyInfo); PyEval_ReleaseThread(m_threadState); - qCDebug(dcPythonIntegrations()) << "Info destroyed3"; }); bool success = callPluginFunction("executeAction", reinterpret_cast(pyInfo)); @@ -591,6 +589,81 @@ void PythonIntegrationPlugin::thingRemoved(Thing *thing) callPluginFunction("thingRemoved", reinterpret_cast(pyThing)); } +void PythonIntegrationPlugin::browseThing(BrowseResult *result) +{ + PyThing *pyThing = m_things.value(result->thing()); + + PyEval_RestoreThread(m_threadState); + + PyBrowseResult *pyBrowseResult = (PyBrowseResult*)PyObject_CallObject((PyObject*)&PyBrowseResultType, NULL); + PyBrowseResult_setBrowseResult(pyBrowseResult, result, pyThing); + + PyEval_ReleaseThread(m_threadState); + + connect(result, &BrowseResult::destroyed, this, [=](){ + qCDebug(dcPythonIntegrations()) << "BrowseResult destroyed"; + PyEval_RestoreThread(m_threadState); + pyBrowseResult->browseResult = nullptr; + Py_DECREF(pyBrowseResult); + PyEval_ReleaseThread(m_threadState); + }); + + bool success = callPluginFunction("browseThing", reinterpret_cast(pyBrowseResult)); + if (!success) { + result->finish(Thing::ThingErrorUnsupportedFeature); + } +} + +void PythonIntegrationPlugin::executeBrowserItem(BrowserActionInfo *info) +{ + PyThing *pyThing = m_things.value(info->thing()); + + PyEval_RestoreThread(m_threadState); + + PyBrowserActionInfo *pyBrowserActionInfo = (PyBrowserActionInfo*)PyObject_CallObject((PyObject*)&PyBrowserActionInfoType, NULL); + PyBrowserActionInfo_setInfo(pyBrowserActionInfo, info, pyThing); + + PyEval_ReleaseThread(m_threadState); + + connect(info, &BrowserActionInfo::destroyed, this, [=](){ + qCDebug(dcPythonIntegrations()) << "BrowserActionInfo destroyed"; + PyEval_RestoreThread(m_threadState); + pyBrowserActionInfo->info = nullptr; + Py_DECREF(pyBrowserActionInfo); + PyEval_ReleaseThread(m_threadState); + }); + + bool success = callPluginFunction("executeBrowserItem", reinterpret_cast(pyBrowserActionInfo)); + if (!success) { + info->finish(Thing::ThingErrorUnsupportedFeature); + } +} + +void PythonIntegrationPlugin::browserItem(BrowserItemResult *result) +{ + PyThing *pyThing = m_things.value(result->thing()); + + PyEval_RestoreThread(m_threadState); + + PyBrowserItemResult *pyBrowserItemResult = (PyBrowserItemResult*)PyObject_CallObject((PyObject*)&PyBrowserItemResultType, NULL); + PyBrowserItemResult_setBrowserItemResult(pyBrowserItemResult, result, pyThing); + + PyEval_ReleaseThread(m_threadState); + + connect(result, &BrowserItemResult::destroyed, this, [=](){ + qCDebug(dcPythonIntegrations()) << "BrowseItemResult destroyed"; + PyEval_RestoreThread(m_threadState); + pyBrowserItemResult->browserItemResult = nullptr; + Py_DECREF(pyBrowserItemResult); + PyEval_ReleaseThread(m_threadState); + }); + + bool success = callPluginFunction("browserItem", reinterpret_cast(pyBrowserItemResult)); + if (!success) { + result->finish(Thing::ThingErrorUnsupportedFeature); + } +} + void PythonIntegrationPlugin::exportIds() { qCDebug(dcThingManager()) << "Exporting plugin IDs:"; diff --git a/libnymea-core/integrations/pythonintegrationplugin.h b/libnymea-core/integrations/pythonintegrationplugin.h index d57c1305..52b6862b 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.h +++ b/libnymea-core/integrations/pythonintegrationplugin.h @@ -37,6 +37,9 @@ public: void postSetupThing(Thing *thing) override; void executeAction(ThingActionInfo *info) override; void thingRemoved(Thing *thing) override; + void browseThing(BrowseResult *result) override; + void executeBrowserItem(BrowserActionInfo *info) override; + void browserItem(BrowserItemResult *result) override; static PyObject* pyConfiguration(PyObject* self, PyObject* args); diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index a98f1598..f0c60d68 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -47,6 +47,10 @@ HEADERS += nymeacore.h \ integrations/apikeysprovidersloader.h \ integrations/plugininfocache.h \ integrations/python/pyapikeystorage.h \ + integrations/python/pybrowseractioninfo.h \ + integrations/python/pybrowseresult.h \ + integrations/python/pybrowseritem.h \ + integrations/python/pybrowseritemresult.h \ integrations/python/pypluginstorage.h \ integrations/thingmanagerimplementation.h \ integrations/translator.h \ diff --git a/libnymea/types/browseritem.h b/libnymea/types/browseritem.h index b7ef3ce6..0b3a0c6b 100644 --- a/libnymea/types/browseritem.h +++ b/libnymea/types/browseritem.h @@ -122,6 +122,7 @@ protected: QList m_actionTypeIds; }; +Q_DECLARE_METATYPE(BrowserItem) Q_DECLARE_OPERATORS_FOR_FLAGS(BrowserItem::ExtendedPropertiesFlags) diff --git a/plugins/mock/integrationpluginmock.cpp b/plugins/mock/integrationpluginmock.cpp index 7acca96f..9e28f5f7 100644 --- a/plugins/mock/integrationpluginmock.cpp +++ b/plugins/mock/integrationpluginmock.cpp @@ -1031,7 +1031,7 @@ void IntegrationPluginMock::generateBrowseItems() item = BrowserItem("004", "Item 3", false, true); item.setDescription("I have a nice thumbnail"); item.setIcon(BrowserItem::BrowserIconFile); - item.setThumbnail("https://github.com/guh/nymea/raw/master/icons/nymea-logo-256x256.png"); + item.setThumbnail("https://github.com/nymea/nymea/raw/master/icons/nymea-logo-256x256.png"); item.setActionTypeIds({mockAddToFavoritesBrowserItemActionTypeId}); m_virtualFs->addChild(new VirtualFsNode(item)); diff --git a/plugins/pymock/integrationpluginpymock.json b/plugins/pymock/integrationpluginpymock.json index 9ec7c8e9..f4b07b45 100644 --- a/plugins/pymock/integrationpluginpymock.json +++ b/plugins/pymock/integrationpluginpymock.json @@ -49,6 +49,7 @@ "displayName": "Python mock thing", "createMethods": ["user"], "setupMethod": "justAdd", + "browsable": true, "eventTypes": [ { "id": "de6c2425-0dee-413f-8f4c-bb0929e83c0d", diff --git a/plugins/pymock/integrationpluginpymock.py b/plugins/pymock/integrationpluginpymock.py index fea164a3..0ccf4e6c 100644 --- a/plugins/pymock/integrationpluginpymock.py +++ b/plugins/pymock/integrationpluginpymock.py @@ -7,7 +7,7 @@ loopRunning = False def init(): global loopRunning - loopRunning = True + loopRunning = True logger.log("Python mock plugin init") logger.warn("Python mock warning") @@ -139,6 +139,27 @@ def thingSettingChanged(thing, paramTypeId, value): logger.log("Thing setting changed:", thing.name, paramTypeId, value) +def browseThing(result): + logger.log("browseThing called", result.thing.name, result.itemId) + if result.itemId == "": + result.addItem(nymea.BrowserItem("001", "Item 0", "I'm a folder", browsable=True, icon=nymea.BrowserIconFolder)) + result.addItem(nymea.BrowserItem("002", "Item 1", "I'm executable", executable=True, icon=nymea.BrowserIconApplication)) + result.addItem(nymea.BrowserItem("003", "Item 2", "I'm a file", icon=nymea.BrowserIconFile)) + result.addItem(nymea.BrowserItem("004", "Item 3", "I have a nice thumbnail", thumbnail="https://github.com/nymea/nymea/raw/master/icons/nymea-logo-256x256.png")) + result.addItem(nymea.BrowserItem("005", "Item 4", "I'm disabled", disabled=True, icon=nymea.BrowserIconFile)) + result.addItem(nymea.BrowserItem("favorites", "Favorites", "I'm the best!", icon=nymea.BrowserIconFavorites)) + + if result.itemId == "001": + result.addItem(nymea.BrowserItem("011", "Item in subdir", "I'm in a subfolder", icon=nymea.BrowserIconFile)) + + result.finish(nymea.ThingErrorNoError) + + +def executeBrowserItem(info): + logger.log("executeBrowserItem called for thing", info.thing.name, "and item", info.itemId) + info.finish(nymea.ThingErrorNoError) + + # Intentionally commented out to also have a test case for unimplmented functions # def thingRemoved(thing): # logger.log("thingRemoved for", thing.name)