diff --git a/F.A.Q..md b/F.A.Q..md new file mode 100644 index 0000000..3d4e641 --- /dev/null +++ b/F.A.Q..md @@ -0,0 +1,119 @@ +## FAQ + +- [Mod Organizer 2 - Plugin Python Proxy](#mod-organizer-2---plugin-python-proxy) + - [TOC](#toc) + - [Introduction](#introduction) + - [The `runner` project](#the-runner-project) + - [Forewords](#forewords) + - [Project organization](#project-organization) + - [`pythonrunner`](#pythonrunner) + - [Registering converters](#registering-converters) + - [Exposing classes](#exposing-classes) + - [Enumerations](#enumerations) + - [Classes](#classes) + - [`proxypluginwrappers`](#proxypluginwrappers) + - [`gamefeatureswrappers`](#gamefeatureswrappers) + - [`uibasewrappers`](#uibasewrappers) + - [The `proxy` project](#the-proxy-project) + - [FAQ](#faq) + - [1. Why is MO2 throwing an exception when I try to create a type inheriting one of MO2 class?](#1-why-is-mo2-throwing-an-exception-when-i-try-to-create-a-type-inheriting-one-of-mo2-class) + - [2. Why are my `get_override` calls failing with `MissingImplementationException` when the Python class has the right methods?](#2-why-are-my-getoverride-calls-failing-with-missingimplementationexception-when-the-python-class-has-the-right-methods) + - [3. Why are `QString` and `QVariant` converters not registered using `register_qclass_converter`?](#3-why-are-qstring-and-qvariant-converters-not-registered-using-registerqclassconverter) + - [4. What is `Q_DELEGATE`? And how do I use it?](#4-what-is-qdelegate-and-how-do-i-use-it) + - [5. Why is it not possible to do `bpy::bases`?](#5-why-is-it-not-possible-to-do-bpybasesqobject) + - [6. Why is the `tr` method not exposed to avoid having to declare it manually on the python side?](#6-why-is-the-tr-method-not-exposed-to-avoid-having-to-declare-it-manually-on-the-python-side) + + +### 1. Why is MO2 throwing an exception when I try to create a type inheriting one of MO2 class? + +**Note:** This should probably also be in the plugin creation tutorial. + +This often happens if you forget to call `super().__init__()` with the right arguments. Even if the list of arguments is empty (as in the example), it must be called: + +```python +class MySaveGame(mobase.ISaveGame): + def __init__(self): + super().__init__() # Mandatory! +``` + +### 2. Why are my `get_override` calls failing with `MissingImplementationException` when the Python class has the right methods? + +In order for `get_override` to work properly, a reference to the initial python object must exists at any time. This can be on the python side (e.g., by having an attribute), or on the C++ side. For easier plugin creation, I would recommend storing on the C++ side. + +Some C++ examples are: +- The `PythonRunner` implementation that holds `boost::python::object` for all the created plugins. +- The `IPluginGame` that holds (on the python side), a `dict` object containing the game features. +- The `SaveGameInfoWrapper` (game feature) that holds the widget (`m_SaveGameWidget`) but also all the saves that were created (`m_SaveGames`). + + +### 3. Why are `QString` and `QVariant` converters not registered using `register_qclass_converter`? + +`register_qclass_converter` is used to register conversion for Qt class that have `PyQt` equivalent. Since +`PyQt` uses standard Python `str` instead of `QString`, we need to use a custom converter. + +While `PyQt` does have `QVariant`, it is not very convenient since Python developer would have to manually +cast to `QVariant`, so instead we use a custom converter that can create a `QVariant` from a multitude of +python types such as `int`, `str`, `List[str]`, etc. + +### 4. What is `Q_DELEGATE`? And how do I use it? + +`Q_DELEGATE` is a macro that can be used **within** a `bpy::class_` declaration, e.g.: + +```cpp +bpy::class_("IDownloadManager", bpy::no_init) + .def("startDownloadURLs", &IDownloadManager::startDownloadURLs) + .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile) + .def("downloadPath", &IDownloadManager::downloadPath) + + Q_DELEGATE(IDownloadManager, QObject, "_object") + ; +``` + +In this case, we indicate that we want to expose the `QObject` interface for `IDownloadManager`. The `Q_DELEGATE` +macro will: +- Create a `__getattr__` method that is used by Python to **delegate** attribute lookup to `QObject` when the + attribute is not found directly in the `IDownloadManager` python class. +- Create a `_object` method to access the underlying `QObject`. + +It makes it possible to do the following in python: +```python +dm = ... # Instance of IDownloadManager + +# We can connect signals declared in the C++ class: +dm.downloadComplete.connect(lambda i: print("Download {} complete!", i)) + +wm = ... # Instance of ISaveGameInfoWidget + +# We can call QWidget method on a ISaveGameInfoWidget object: +wm.setLayout(QtWidgets.QHBoxLayout()) +``` + +Most of the `QObject` interface has no reason to be exposed, so the only cases where you should need `Q_DELEGATE` would +be when: +- You need to expose Qt signals to python - this is the case for `IDownloadManager` and `IModRepositoryBridge`. +- You need to expose a class that inherits `QWidget`. If you do not use `Q_DELEGATE` in this case, python developers + will not be able to call the `QWidget` method on objects of this class. + +### 5. Why is it not possible to do `bpy::bases`? + +I will explain the reason for this here, but you should see the FAQ item above for troubleshooting. + +`QObject` (or any Qt class) is exposed in python using `sip`, while everything in MO2 is exposed using `boost::python`. When doing `bpy::bases`, `boost::python` does not find the `PyTypeObject` that corresponds to `QObject` since it is not exposed through a `bpy::class_` declaration (registering a converter for it is not enough). It is possible, by playing with internal `boost::python` stuff, to make `boost::python` find the `PyTypeObject` for `QObject` but... that is not sufficient. + +`boost::python` and `sip` create classes using their own [meta-classes](https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python). For `boost::python`, it is `Boost.Python.class`. And all classes created by `boost::python` inherits `Boost.Python.instance` which is the "top" boost class. Unfortunately, it is not possible to do inheritance between classes that have different meta-classes in python, so it is not possible to inherit both `QObject` and `Boost.Python.instance`. The only way would be to provide our own meta-class, but this is not possible with `boost::python`. + +### 6. Why is the `tr` method not exposed to avoid having to declare it manually on the python side? + +One issue with `QObject.tr` in `PyQt5` is that the context is dynamic, i.e. if class `B` inherits class `A`, strings declared in class `A` will not be translated by an instance of `B` since the context is the dynamic object (not the static one like in C++), see, https://doc.bccnsoft.com/docs/PyQt5/i18n.html. + +It would be quite easy to provide `tr` since all plugins inherit `IPlugin`: + +```cpp +.def("tr", +[](bpy::object obj, const char *str) { + std::string className = bpy::extract( + obj.attr("__class__").attr("__name__")); + return QCoreApplication::translate(className.data(), str); +}) +``` + +...but the issue is the same, since `className` will be the name of the actual class, not the class containing the strings. It could be possible to go up the class chain to find the first available translation, but I am not sure that it is worth the hassle. \ No newline at end of file