From ead8352c9397f09585f4281cd7c2124b640db69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 11:22:10 +0100 Subject: [PATCH 01/10] Remove isActive() for the Python proxy plugin. --- src/proxy/proxypython.cpp | 14 +++++++------- src/proxy/proxypython.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index eb9aa90..39b2813 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -117,7 +117,7 @@ bool ProxyPython::init(IOrganizer *moInfo) { m_MOInfo = moInfo; - if (m_MOInfo && !m_MOInfo->pluginSetting(name(), "enabled").toBool()) { + if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { m_LoadFailure = FAIL_NONE; return false; } @@ -197,9 +197,14 @@ QString ProxyPython::name() const return "Python Proxy"; } +QString ProxyPython::localizedName() const +{ + return tr("Python Proxy"); +} + QString ProxyPython::author() const { - return "Tannin"; + return "Tannin & Holt59"; } QString ProxyPython::description() const @@ -212,11 +217,6 @@ VersionInfo ProxyPython::version() const return VersionInfo(2, 1, 0, VersionInfo::RELEASE_FINAL); } -bool ProxyPython::isActive() const -{ - return m_LoadFailure == FAIL_NOTINIT; -} - QList ProxyPython::settings() const { QList result; diff --git a/src/proxy/proxypython.h b/src/proxy/proxypython.h index a41f856..708ce4f 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -42,10 +42,10 @@ public: virtual bool init(MOBase::IOrganizer *moInfo); virtual QString name() const; + virtual QString localizedName() const; virtual QString author() const; virtual QString description() const; virtual MOBase::VersionInfo version() const; - virtual bool isActive() const; virtual QList settings() const; QStringList pluginList(const QString &pluginPath) const; From 57c774103d26d66c9b924d30c8578627ebb31d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 11:22:50 +0100 Subject: [PATCH 02/10] Better handling of sequence conversions. --- src/runner/converters.h | 16 ++++++++-------- src/runner/pythonutils.h | 24 +++++++++++------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/runner/converters.h b/src/runner/converters.h index 7933fbf..2d90e52 100644 --- a/src/runner/converters.h +++ b/src/runner/converters.h @@ -26,7 +26,7 @@ namespace utils { struct QString_to_python_str { static PyObject* convert(const QString& str) { - // It's safer to explicitly convert to unicode as if we don't, this can return + // It's safer to explicitly convert to unicode as if we don't, this can return // either str or unicode without it being easy to know which to expect bpy::object pyStr = bpy::object(qUtf8Printable(str)); if (SIPBytes_Check(pyStr.ptr())) @@ -368,7 +368,7 @@ namespace utils { template struct Functor_converter { - + template static decltype(auto) wrap(T&& t) { return details::wrap_impl::apply(std::forward(t)); @@ -483,8 +483,8 @@ namespace utils { using namespace QString_converter; bpy::to_python_converter(); bpy::converter::registry::push_back( - &QString_from_python_str::convertible, - &QString_from_python_str::construct, + &QString_from_python_str::convertible, + &QString_from_python_str::construct, bpy::type_id()); } @@ -492,8 +492,8 @@ namespace utils { using namespace QVariant_converter; bpy::to_python_converter(); bpy::converter::registry::push_back( - &QVariant_from_python_obj::convertible, - &QVariant_from_python_obj::construct, + &QVariant_from_python_obj::convertible, + &QVariant_from_python_obj::construct, bpy::type_id()); } @@ -528,8 +528,8 @@ namespace utils { inline void register_functor_converter() { using Converter = Functor_converter; bpy::converter::registry::push_back( - &Converter::convertible, - &Converter::construct, + &Converter::convertible, + &Converter::construct, bpy::type_id>()); } diff --git a/src/runner/pythonutils.h b/src/runner/pythonutils.h index 92bb301..728b3d0 100644 --- a/src/runner/pythonutils.h +++ b/src/runner/pythonutils.h @@ -91,19 +91,19 @@ namespace utils { using value_type = typename Container::value_type; static void* convertible(PyObject* objPtr) { - if (PySequence_Check(objPtr)) return objPtr; + // Check that the object can be iterated or is a sequence. There is no "clean" + // way checking that an object is iterable apparently (PyIter_Check checks that + // an object is an iterator, which is very different). + if (objPtr->ob_type->tp_iter != 0 || PySequence_Check(objPtr)) return objPtr; return nullptr; } static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; Container* result = new (storage) Container(); - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - result->push_back(bpy::extract(source[i])); - } - + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + bpy::stl_input_iterator begin(source), end; + std::copy(begin, end, std::back_inserter(*result)); data->convertible = storage; } }; @@ -132,7 +132,8 @@ namespace utils { using value_type = typename Container::value_type; static void* convertible(PyObject* objPtr) { - if (PySequence_Check(objPtr)) return objPtr; + // See container_from_python. + if (objPtr->ob_type->tp_iter != 0 && PySequence_Check(objPtr)) return objPtr; return nullptr; } @@ -140,11 +141,8 @@ namespace utils { void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; Container* result = new (storage) Container(); bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - int length = bpy::len(source); - for (int i = 0; i < length; ++i) { - result->insert(bpy::extract(source[i])); - } - + bpy::stl_input_iterator begin(source), end; + std::copy(begin, end, std::inserter(*result, result->begin())); data->convertible = storage; } }; From 1ca6fee64db30faf596ad7fbe22bb86e8fa6252f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 11:23:15 +0100 Subject: [PATCH 03/10] Add plugin requirements. --- src/runner/proxypluginwrappers.cpp | 20 +++++++----- src/runner/proxypluginwrappers.h | 13 +++++--- src/runner/pythonrunner.cpp | 50 +++++++++++++++++++++++++++--- src/runner/uibasewrappers.h | 15 ++++++++- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp index 1199de9..1548b40 100644 --- a/src/runner/proxypluginwrappers.cpp +++ b/src/runner/proxypluginwrappers.cpp @@ -27,7 +27,7 @@ namespace boost using namespace MOBase; -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) \ +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(class_name, include_requirements) \ bool class_name::init(MOBase::IOrganizer *moInfo) \ { \ return basicWrapperFunctionImplementation(this, "init", boost::python::ptr(moInfo)); \ @@ -63,17 +63,21 @@ MOBase::VersionInfo class_name::version() const \ return basicWrapperFunctionImplementation(this, "version"); \ } \ \ -bool class_name::isActive() const \ -{ \ - return basicWrapperFunctionImplementation(this, "isActive"); \ -} \ - \ QList class_name::settings() const \ { \ return basicWrapperFunctionImplementation>(this, "settings"); \ } \ QString class_name::localizedName_Default() const { return IPlugin::localizedName(); } \ -IPlugin* class_name::master_Default() const { return IPlugin::master(); } +IPlugin* class_name::master_Default() const { return IPlugin::master(); } \ +BOOST_PP_EXPR_IF(include_requirements, \ + QList class_name::requirements() const { \ + QList reqs = basicWrapperFunctionImplementationWithDefault>(this, &class_name::requirements_Default, "requirements"); \ + reqs.prepend(Requirements::pluginDependency("Python Proxy")); \ + return reqs; \ + } \ + QList class_name::requirements_Default() const { return IPlugin::requirements(); }) + +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(class_name, 1) /// end COMMON_I_PLUGIN_WRAPPER_DEFINITIONS ///////////////////////////// @@ -293,7 +297,7 @@ QString IPluginGameWrapper::getLauncherName() const return basicWrapperFunctionImplementation(this, "getLauncherName"); } -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginGameWrapper) +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(IPluginGameWrapper, 0) std::map IPluginGameWrapper::featureList() const { diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h index f75424f..0b17a84 100644 --- a/src/runner/proxypluginwrappers.h +++ b/src/runner/proxypluginwrappers.h @@ -2,6 +2,7 @@ #define PROXYPLUGINWRAPPERS_H +#include #include #include #include @@ -16,7 +17,7 @@ #endif -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS public: \ +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(include_requirements) public: \ virtual bool init(MOBase::IOrganizer *moInfo) override; \ virtual QString name() const override; \ virtual QString localizedName() const override; \ @@ -24,10 +25,14 @@ virtual IPlugin* master() const override; \ virtual QString author() const override; \ virtual QString description() const override; \ virtual MOBase::VersionInfo version() const override; \ -virtual bool isActive() const override; \ virtual QList settings() const override; \ QString localizedName_Default() const; \ -IPlugin* master_Default() const; +IPlugin* master_Default() const; \ +BOOST_PP_EXPR_IF(include_requirements, \ + virtual QList requirements() const override; \ + QList requirements_Default() const;) + +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(1) // Even though the base interface is not a QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. // QObject must be the first base class because moc assumes the first base class is a QObject @@ -126,7 +131,7 @@ public: virtual QString gameVersion() const override; virtual QString getLauncherName() const override; - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(0) protected: // Apparently, Python developers interpret an underscore in a function name as it being protected diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index a500ad9..09b5db9 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -94,18 +94,18 @@ BOOST_PYTHON_MODULE(mobase) // Containers: utils::register_sequence_container>(); + utils::register_sequence_container>(); utils::register_sequence_container>(); utils::register_sequence_container>(); utils::register_sequence_container>(); utils::register_sequence_container>(); + utils::register_sequence_container>(); utils::register_sequence_container(); utils::register_sequence_container>(); utils::register_sequence_container>(); utils::register_sequence_container>(); // Required for QVariant since this is QVariantList. utils::register_sequence_container>>(); utils::register_sequence_container>(); - - utils::register_sequence_container>(); utils::register_sequence_container>(); utils::register_set_container>(); @@ -149,6 +149,7 @@ BOOST_PYTHON_MODULE(mobase) utils::register_functor_converter const&)>(); utils::register_functor_converter(QString const&)>(); utils::register_functor_converter>(); + utils::register_functor_converter>(); // This one is kept for backward-compatibility while we deprecate onModStateChanged for singl mod. utils::register_functor_converter(); // converter for the onModStateChanged-callback (IModList). @@ -261,6 +262,38 @@ BOOST_PYTHON_MODULE(mobase) Q_DELEGATE(ISaveGameInfoWidget, QWidget, "_widget") ; + // Plugin requirements: + bpy::class_, IPluginRequirement*, boost::noncopyable>("IPluginRequirement", bpy::no_init) + .def("problems", &IPluginRequirement::problems) + .def("description", &IPluginRequirement::description) + ; + + bpy::class_("PluginRequirementFactory") + // pluginDependency + .def("pluginDependency", +[](QStringList const& pluginNames) { + return PluginRequirementFactory::pluginDependency(pluginNames); + }, bpy::return_value_policy(), bpy::arg("plugins")) + .def("pluginDependency", +[](QString const& pluginName) { + return PluginRequirementFactory::pluginDependency(pluginName); + }, bpy::return_value_policy(), bpy::arg("plugin")) + .staticmethod("pluginDependency") + // gameDependency + .def("gameDependency", +[](QStringList const& gameNames) { + return PluginRequirementFactory::gameDependency(gameNames); + }, bpy::return_value_policy(), bpy::arg("games")) + .def("gameDependency", +[](QString const& gameNames) { + return PluginRequirementFactory::gameDependency(gameNames); + }, bpy::return_value_policy(), bpy::arg("game")) + .staticmethod("gameDependency") + // diagnose + .def("diagnose", &PluginRequirementFactory::diagnose, + bpy::return_value_policy(), bpy::arg("diagnose")) + .staticmethod("diagnose") + // basic + .def("basic", &PluginRequirementFactory::basic, + bpy::return_value_policy(), (bpy::arg("checker"), "description")) + .staticmethod("basic"); + bpy::class_("FileInfo", bpy::init<>()) .def_readwrite("filePath", &IOrganizer::FileInfo::filePath) .def_readwrite("archive", &IOrganizer::FileInfo::archive) @@ -279,6 +312,8 @@ BOOST_PYTHON_MODULE(mobase) .def("createMod", &IOrganizer::createMod, bpy::return_value_policy(), bpy::arg("name")) .def("getGame", &IOrganizer::getGame, bpy::return_value_policy(), bpy::arg("name")) .def("modDataChanged", &IOrganizer::modDataChanged, bpy::arg("mod")) + .def("isPluginEnabled", +[](IOrganizer* o, IPlugin* plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) + .def("isPluginEnabled", +[](IOrganizer* o, QString const& plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) .def("pluginSetting", &IOrganizer::pluginSetting, (bpy::arg("plugin_name"), "key")) .def("setPluginSetting", &IOrganizer::setPluginSetting, (bpy::arg("plugin_name"), "key", "value")) .def("persistent", &IOrganizer::persistent, (bpy::arg("plugin_name"), "key", bpy::arg("default") = QVariant())) @@ -771,7 +806,7 @@ BOOST_PYTHON_MODULE(mobase) .def("onModMoved", &MOBase::IModList::onModMoved, bpy::arg("callback")) ; - // Note: localizedName() and master() have to go in all the plugin wrappers declaration, + // Note: localizedName(), master() and requirements have to go in all the plugin wrappers declaration, // since the default functions are specific to each wrapper, otherwise in turns into an // infinite recursion mess. bpy::class_("IPlugin") @@ -782,13 +817,14 @@ BOOST_PYTHON_MODULE(mobase) .def("author", bpy::pure_virtual(&MOBase::IPlugin::author)) .def("description", bpy::pure_virtual(&MOBase::IPlugin::description)) .def("version", bpy::pure_virtual(&MOBase::IPlugin::version)) - .def("isActive", bpy::pure_virtual(&MOBase::IPlugin::isActive)) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginWrapper::requirements_Default) .def("settings", bpy::pure_virtual(&MOBase::IPlugin::settings)) ; bpy::class_, boost::noncopyable>("IPluginDiagnose") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginDiagnoseWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginDiagnoseWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginDiagnoseWrapper::requirements_Default) .def("activeProblems", bpy::pure_virtual(&MOBase::IPluginDiagnose::activeProblems)) .def("shortDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::shortDescription), bpy::arg("key")) @@ -815,6 +851,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginFileMapper") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginFileMapperWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginFileMapperWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginFileMapperWrapper::requirements_Default) .def("mappings", bpy::pure_virtual(&MOBase::IPluginFileMapper::mappings)) ; @@ -947,6 +984,7 @@ BOOST_PYTHON_MODULE(mobase) .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerSimpleWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginInstallerSimpleWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerSimpleWrapper::requirements_Default) // Note: Keeping the variant here even if we always return a tuple to be consistent with the wrapper and // have proper stubs generation. @@ -964,6 +1002,7 @@ BOOST_PYTHON_MODULE(mobase) .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerCustomWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginInstallerCustomWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerCustomWrapper::requirements_Default) // Needs to add both otherwize boost does not understand: .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, bpy::arg("tree")) @@ -977,6 +1016,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginModPage") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginModPageWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginModPageWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginModPageWrapper::requirements_Default) .def("displayName", bpy::pure_virtual(&IPluginModPage::displayName)) .def("icon", bpy::pure_virtual(&IPluginModPage::icon)) @@ -990,6 +1030,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginPreview") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginPreviewWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginPreviewWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginPreviewWrapper::requirements_Default) .def("supportedExtensions", bpy::pure_virtual(&IPluginPreview::supportedExtensions)) .def("genFilePreview", bpy::pure_virtual(&IPluginPreview::genFilePreview), bpy::return_value_policy(), @@ -999,6 +1040,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginTool") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginToolWrapper::localizedName_Default) .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default, bpy::return_value_policy()) + .def("requirements", &MOBase::IPlugin::requirements, &IPluginToolWrapper::requirements_Default) .def("displayName", bpy::pure_virtual(&IPluginTool::displayName)) .def("tooltip", bpy::pure_virtual(&IPluginTool::tooltip)) diff --git a/src/runner/uibasewrappers.h b/src/runner/uibasewrappers.h index a032853..e572e34 100644 --- a/src/runner/uibasewrappers.h +++ b/src/runner/uibasewrappers.h @@ -16,12 +16,25 @@ #include #include #include +#include #include "error.h" #include "gilock.h" #include "pythonwrapperutilities.h" -// This needs to be extendable in Python, so actually needs a wrapper (everything else probably doesn't): +// This can be extended in C++, so why not in Python: +class IPluginRequirementWrapper : public MOBase::IPluginRequirement, public boost::python::wrapper +{ +public: + static constexpr const char* className = "IPluginRequirement"; + using boost::python::wrapper::get_override; + + virtual std::vector problems(MOBase::IOrganizer *o) const override { + return basicWrapperFunctionImplementation>(this, "problems", boost::python::ptr(o)); }; + virtual QString description(unsigned int id) const override { return basicWrapperFunctionImplementation(this, "getCreationTime", id); }; +}; + +// This needs to be extendable in Python, so actually needs a wrapper: class ISaveGameWrapper : public MOBase::ISaveGame, public boost::python::wrapper { public: From e84777d550c7541e6e5e2ab0ca56266501d14cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 13:38:42 +0100 Subject: [PATCH 04/10] Remove automatic plugin dependency for proxy (moved to MO2 core). --- src/runner/proxypluginwrappers.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp index 1548b40..e20bae9 100644 --- a/src/runner/proxypluginwrappers.cpp +++ b/src/runner/proxypluginwrappers.cpp @@ -71,9 +71,7 @@ QString class_name::localizedName_Default() const { return IPlugin::localizedNam IPlugin* class_name::master_Default() const { return IPlugin::master(); } \ BOOST_PP_EXPR_IF(include_requirements, \ QList class_name::requirements() const { \ - QList reqs = basicWrapperFunctionImplementationWithDefault>(this, &class_name::requirements_Default, "requirements"); \ - reqs.prepend(Requirements::pluginDependency("Python Proxy")); \ - return reqs; \ + return basicWrapperFunctionImplementationWithDefault>(this, &class_name::requirements_Default, "requirements"); \ } \ QList class_name::requirements_Default() const { return IPlugin::requirements(); }) From de5fde0f284b39f0d236577390c850c6c3686eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 16:14:28 +0100 Subject: [PATCH 05/10] Cleaner IPluginRequirement. --- src/runner/pythonrunner.cpp | 18 ++++++++++++++---- src/runner/uibasewrappers.h | 5 ++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 09b5db9..eafbb9d 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -263,10 +263,20 @@ BOOST_PYTHON_MODULE(mobase) ; // Plugin requirements: - bpy::class_, IPluginRequirement*, boost::noncopyable>("IPluginRequirement", bpy::no_init) - .def("problems", &IPluginRequirement::problems) - .def("description", &IPluginRequirement::description) - ; + auto iPluginRequirementClass = bpy::class_< + IPluginRequirementWrapper, bpy::bases<>, IPluginRequirement*, boost::noncopyable>("IPluginRequirement", bpy::no_init); + { + bpy::scope scope = iPluginRequirementClass; + + bpy::class_("Problem", + bpy::init((bpy::arg("short_description"), bpy::arg("long_description") = ""))) + .def("shortDescription", &IPluginRequirement::Problem::shortDescription) + .def("longDescription", &IPluginRequirement::Problem::longDescription); + + iPluginRequirementClass + .def("check", &IPluginRequirement::check) + ; + } bpy::class_("PluginRequirementFactory") // pluginDependency diff --git a/src/runner/uibasewrappers.h b/src/runner/uibasewrappers.h index e572e34..a0470de 100644 --- a/src/runner/uibasewrappers.h +++ b/src/runner/uibasewrappers.h @@ -29,9 +29,8 @@ public: static constexpr const char* className = "IPluginRequirement"; using boost::python::wrapper::get_override; - virtual std::vector problems(MOBase::IOrganizer *o) const override { - return basicWrapperFunctionImplementation>(this, "problems", boost::python::ptr(o)); }; - virtual QString description(unsigned int id) const override { return basicWrapperFunctionImplementation(this, "getCreationTime", id); }; + virtual std::optional check(MOBase::IOrganizer *o) const override { + return basicWrapperFunctionImplementation>(this, "check", boost::python::ptr(o)); }; }; // This needs to be extendable in Python, so actually needs a wrapper: From d89eae1646dfcb27b9ef08b18c07e703bfd8c9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 17:25:55 +0100 Subject: [PATCH 06/10] Allow IPluginRequirement to be properly extended from Python. --- src/runner/proxypluginwrappers.cpp | 3 +- src/runner/proxypluginwrappers.h | 5 ++- src/runner/pythonrunner.cpp | 6 ++- src/runner/pythonutils.h | 57 +++++++++++++++++++++++++++++ src/runner/pythonwrapperutilities.h | 26 +++++++++++++ src/runner/uibasewrappers.h | 3 +- 6 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp index e20bae9..860a078 100644 --- a/src/runner/proxypluginwrappers.cpp +++ b/src/runner/proxypluginwrappers.cpp @@ -71,7 +71,8 @@ QString class_name::localizedName_Default() const { return IPlugin::localizedNam IPlugin* class_name::master_Default() const { return IPlugin::master(); } \ BOOST_PP_EXPR_IF(include_requirements, \ QList class_name::requirements() const { \ - return basicWrapperFunctionImplementationWithDefault>(this, &class_name::requirements_Default, "requirements"); \ + return basicWrapperFunctionImplementationWithDefault>( \ + this, &class_name::requirements_Default, m_Requirements, "requirements"); \ } \ QList class_name::requirements_Default() const { return IPlugin::requirements(); }) diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h index 0b17a84..8405669 100644 --- a/src/runner/proxypluginwrappers.h +++ b/src/runner/proxypluginwrappers.h @@ -17,7 +17,10 @@ #endif -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(include_requirements) public: \ +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(include_requirements) \ + BOOST_PP_EXPR_IF(include_requirements, \ + private: mutable boost::python::object m_Requirements; ) \ +public: \ virtual bool init(MOBase::IOrganizer *moInfo) override; \ virtual QString name() const override; \ virtual QString localizedName() const override; \ diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index eafbb9d..fcaff11 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -117,6 +117,8 @@ BOOST_PYTHON_MODULE(mobase) utils::register_associative_container(); + utils::register_optional>(); + // Tuple: bpy::register_tuple>(); // IOrganizer::waitForApplication bpy::register_tuple>(); // IProfile::invalidationActive @@ -264,7 +266,7 @@ BOOST_PYTHON_MODULE(mobase) // Plugin requirements: auto iPluginRequirementClass = bpy::class_< - IPluginRequirementWrapper, bpy::bases<>, IPluginRequirement*, boost::noncopyable>("IPluginRequirement", bpy::no_init); + IPluginRequirementWrapper, bpy::bases<>, IPluginRequirementWrapper*, boost::noncopyable>("IPluginRequirement"); { bpy::scope scope = iPluginRequirementClass; @@ -274,7 +276,7 @@ BOOST_PYTHON_MODULE(mobase) .def("longDescription", &IPluginRequirement::Problem::longDescription); iPluginRequirementClass - .def("check", &IPluginRequirement::check) + .def("check", bpy::pure_virtual(&IPluginRequirement::check)) ; } diff --git a/src/runner/pythonutils.h b/src/runner/pythonutils.h index 728b3d0..6703557 100644 --- a/src/runner/pythonutils.h +++ b/src/runner/pythonutils.h @@ -147,6 +147,49 @@ namespace utils { } }; + + template + struct optional_to_python { + static PyObject* convert(const Optional& optional) { + if (optional) { + return bpy::incref(bpy::object(*optional).ptr()); + } + else { + return bpy::incref(Py_None); + } + } + }; + + + template + struct optional_from_python { + + using value_type = typename Optional::value_type; + + static void* convertible(PyObject* objPtr) { + + if (objPtr == Py_None) { + return objPtr; + } + + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + return bpy::extract(source).check() ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + Optional* result = new (storage) Optional(); + + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + if (!source.is_none()) { + *result = bpy::extract(source)(); + } + + data->convertible = storage; + } + }; + + /** * @brief Register from and to python converters for (at least) map, unordered_map and QMap. * @@ -195,6 +238,20 @@ namespace utils { , bpy::type_id()); }; + /** + * @brief Register from and to python converters for optional. + * + * @tparam T The optional type (std::optional or boost::optional). + */ + template + void register_optional() { + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &optional_from_python::convertible + , &optional_from_python::construct + , bpy::type_id()); + }; + /** * @brief Show a deprecation warning. diff --git a/src/runner/pythonwrapperutilities.h b/src/runner/pythonwrapperutilities.h index b5e0066..a55025c 100644 --- a/src/runner/pythonwrapperutilities.h +++ b/src/runner/pythonwrapperutilities.h @@ -143,4 +143,30 @@ ReturnType basicWrapperFunctionImplementationWithDefault(WrapperTypePtr wrapper, return details::wrapperFunctionImplementation(wrapper, false, fn, nullptr, methodName, args...); } +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling, and store the intermediate result in the given python object, + * falling back to the given function if the method does not exist. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param fn The function to call if the method does not exists. + * @param ref Python object to which the result of `get_override()` should be stored. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * Note: `fn` does not have to be a member-function of `wrapper` but `std::invoke(fn, wrapper, args...)` must be valid. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType basicWrapperFunctionImplementationWithDefault(const WrapperType* wrapper, Fn fn, boost::python::object& ref, const char* methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, false, fn, &ref, methodName, args...); +} + + #endif // PYTHONWRAPPERUTILITIES_H diff --git a/src/runner/uibasewrappers.h b/src/runner/uibasewrappers.h index a0470de..448575d 100644 --- a/src/runner/uibasewrappers.h +++ b/src/runner/uibasewrappers.h @@ -30,7 +30,8 @@ public: using boost::python::wrapper::get_override; virtual std::optional check(MOBase::IOrganizer *o) const override { - return basicWrapperFunctionImplementation>(this, "check", boost::python::ptr(o)); }; + return basicWrapperFunctionImplementation>(this, "check", boost::python::ptr(o)); + }; }; // This needs to be extendable in Python, so actually needs a wrapper: From b20c015643b72abc29239809a05d75fd89cb6acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 21:00:47 +0100 Subject: [PATCH 07/10] master() returns the name of the plugin. --- src/runner/proxypluginwrappers.cpp | 6 +++--- src/runner/proxypluginwrappers.h | 4 ++-- src/runner/pythonrunner.cpp | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp index 860a078..e88cf37 100644 --- a/src/runner/proxypluginwrappers.cpp +++ b/src/runner/proxypluginwrappers.cpp @@ -43,9 +43,9 @@ QString class_name::localizedName() const \ return basicWrapperFunctionImplementationWithDefault(this, &class_name::localizedName_Default, "localizedName"); \ } \ \ -IPlugin* class_name::master() const \ +QString class_name::master() const \ { \ - return basicWrapperFunctionImplementationWithDefault(this, &class_name::master_Default, "master"); \ + return basicWrapperFunctionImplementationWithDefault(this, &class_name::master_Default, "master"); \ } \ \ QString class_name::author() const \ @@ -68,7 +68,7 @@ QList class_name::settings() const \ return basicWrapperFunctionImplementation>(this, "settings"); \ } \ QString class_name::localizedName_Default() const { return IPlugin::localizedName(); } \ -IPlugin* class_name::master_Default() const { return IPlugin::master(); } \ +QString class_name::master_Default() const { return IPlugin::master(); } \ BOOST_PP_EXPR_IF(include_requirements, \ QList class_name::requirements() const { \ return basicWrapperFunctionImplementationWithDefault>( \ diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h index 8405669..46a3bf5 100644 --- a/src/runner/proxypluginwrappers.h +++ b/src/runner/proxypluginwrappers.h @@ -24,13 +24,13 @@ public: \ virtual bool init(MOBase::IOrganizer *moInfo) override; \ virtual QString name() const override; \ virtual QString localizedName() const override; \ -virtual IPlugin* master() const override; \ +virtual QString master() const override; \ virtual QString author() const override; \ virtual QString description() const override; \ virtual MOBase::VersionInfo version() const override; \ virtual QList settings() const override; \ QString localizedName_Default() const; \ -IPlugin* master_Default() const; \ +QString master_Default() const; \ BOOST_PP_EXPR_IF(include_requirements, \ virtual QList requirements() const override; \ QList requirements_Default() const;) diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index fcaff11..2200d0a 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -825,7 +825,7 @@ BOOST_PYTHON_MODULE(mobase) .def("init", bpy::pure_virtual(&MOBase::IPlugin::init), bpy::arg("organizer")) .def("name", bpy::pure_virtual(&MOBase::IPlugin::name)) .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("author", bpy::pure_virtual(&MOBase::IPlugin::author)) .def("description", bpy::pure_virtual(&MOBase::IPlugin::description)) .def("version", bpy::pure_virtual(&MOBase::IPlugin::version)) @@ -835,7 +835,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginDiagnose") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginDiagnoseWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginDiagnoseWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginDiagnoseWrapper::requirements_Default) .def("activeProblems", bpy::pure_virtual(&MOBase::IPluginDiagnose::activeProblems)) @@ -862,7 +862,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginFileMapper") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginFileMapperWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginFileMapperWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginFileMapperWrapper::requirements_Default) .def("mappings", bpy::pure_virtual(&MOBase::IPluginFileMapper::mappings)) ; @@ -897,7 +897,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginGame") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginGameWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginGameWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("detectGame", bpy::pure_virtual(&MOBase::IPluginGame::detectGame)) .def("gameName", bpy::pure_virtual(&MOBase::IPluginGame::gameName)) @@ -995,7 +995,7 @@ BOOST_PYTHON_MODULE(mobase) .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerSimpleWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginInstallerSimpleWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerSimpleWrapper::requirements_Default) // Note: Keeping the variant here even if we always return a tuple to be consistent with the wrapper and @@ -1013,7 +1013,7 @@ BOOST_PYTHON_MODULE(mobase) .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerCustomWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginInstallerCustomWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerCustomWrapper::requirements_Default) // Needs to add both otherwize boost does not understand: @@ -1027,7 +1027,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginModPage") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginModPageWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginModPageWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginModPageWrapper::requirements_Default) .def("displayName", bpy::pure_virtual(&IPluginModPage::displayName)) @@ -1041,7 +1041,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginPreview") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginPreviewWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginPreviewWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginPreviewWrapper::requirements_Default) .def("supportedExtensions", bpy::pure_virtual(&IPluginPreview::supportedExtensions)) @@ -1051,7 +1051,7 @@ BOOST_PYTHON_MODULE(mobase) bpy::class_, boost::noncopyable>("IPluginTool") .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginToolWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default, bpy::return_value_policy()) + .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) .def("requirements", &MOBase::IPlugin::requirements, &IPluginToolWrapper::requirements_Default) .def("displayName", bpy::pure_virtual(&IPluginTool::displayName)) From 27c34af2aa8711095420d3c077e72c59d8a6af1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 21:54:54 +0100 Subject: [PATCH 08/10] Update authors. --- src/proxy/proxypython.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index 39b2813..da98fa2 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -204,7 +204,7 @@ QString ProxyPython::localizedName() const QString ProxyPython::author() const { - return "Tannin & Holt59"; + return "AnyOldName3, Holt59, Silarn, Tannin"; } QString ProxyPython::description() const From cbb911f6a01e140a274734ab8da64c169ff416e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Wed, 11 Nov 2020 22:02:40 +0100 Subject: [PATCH 09/10] Rename the wrapper declaration and definition macros. --- src/runner/proxypluginwrappers.cpp | 9 +++++---- src/runner/proxypluginwrappers.h | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp index e88cf37..2a1efd3 100644 --- a/src/runner/proxypluginwrappers.cpp +++ b/src/runner/proxypluginwrappers.cpp @@ -26,8 +26,9 @@ namespace boost using namespace MOBase; - -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(class_name, include_requirements) \ +// See COMMON_I_PLUGIN_WRAPPER_DECLARATIONS__IMPL in proxypluginwrappers.h for explanation on +// the "include_requirements". +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, include_requirements) \ bool class_name::init(MOBase::IOrganizer *moInfo) \ { \ return basicWrapperFunctionImplementation(this, "init", boost::python::ptr(moInfo)); \ @@ -76,7 +77,7 @@ BOOST_PP_EXPR_IF(include_requirements, \ } \ QList class_name::requirements_Default() const { return IPlugin::requirements(); }) -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(class_name, 1) +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, 1) /// end COMMON_I_PLUGIN_WRAPPER_DEFINITIONS ///////////////////////////// @@ -296,7 +297,7 @@ QString IPluginGameWrapper::getLauncherName() const return basicWrapperFunctionImplementation(this, "getLauncherName"); } -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_(IPluginGameWrapper, 0) +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(IPluginGameWrapper, 0) std::map IPluginGameWrapper::featureList() const { diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h index 46a3bf5..892314d 100644 --- a/src/runner/proxypluginwrappers.h +++ b/src/runner/proxypluginwrappers.h @@ -16,8 +16,9 @@ #include #endif - -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(include_requirements) \ +// The wrapper for IPluginGame cannot override requirements() since it's final, +// so we need to be able to exclude the declarations. +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(include_requirements) \ BOOST_PP_EXPR_IF(include_requirements, \ private: mutable boost::python::object m_Requirements; ) \ public: \ @@ -35,7 +36,7 @@ BOOST_PP_EXPR_IF(include_requirements, \ virtual QList requirements() const override; \ QList requirements_Default() const;) -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(1) +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(1) // Even though the base interface is not a QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. // QObject must be the first base class because moc assumes the first base class is a QObject @@ -134,7 +135,7 @@ public: virtual QString gameVersion() const override; virtual QString getLauncherName() const override; - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_(0) + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(0) protected: // Apparently, Python developers interpret an underscore in a function name as it being protected From b37d3a9c178724a0752379ed856e738b76a8126d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Thu, 12 Nov 2020 21:49:37 +0100 Subject: [PATCH 10/10] Add callbacks for plugin enabled/disabled. --- src/runner/pythonrunner.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 2200d0a..180a61c 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -152,6 +152,7 @@ BOOST_PYTHON_MODULE(mobase) utils::register_functor_converter(QString const&)>(); utils::register_functor_converter>(); utils::register_functor_converter>(); + utils::register_functor_converter>(); // This one is kept for backward-compatibility while we deprecate onModStateChanged for singl mod. utils::register_functor_converter(); // converter for the onModStateChanged-callback (IModList). @@ -379,7 +380,20 @@ BOOST_PYTHON_MODULE(mobase) .def("onProfileRenamed", &IOrganizer::onProfileRenamed, bpy::arg("callback")) .def("onProfileRemoved", &IOrganizer::onProfileRemoved, bpy::arg("callback")) .def("onProfileChanged", &IOrganizer::onProfileChanged, bpy::arg("callback")) + .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged, bpy::arg("callback")) + .def("onPluginEnabled", +[](IOrganizer* o, std::function const& func) { + o->onPluginEnabled(func); + }, bpy::arg("callback")) + .def("onPluginEnabled", +[](IOrganizer* o, QString const& name, std::function const& func) { + o->onPluginEnabled(name, func); + }, (bpy::arg("name"), bpy::arg("callback"))) + .def("onPluginDisabled", +[](IOrganizer* o, std::function const& func) { + o->onPluginDisabled(func); + }, bpy::arg("callback")) + .def("onPluginDisabled", +[](IOrganizer* o, QString const& name, std::function const& func) { + o->onPluginDisabled(name, func); + }, (bpy::arg("name"), bpy::arg("callback"))) // DEPRECATED: .def("getMod", +[](IOrganizer* o, QString const& name) {