diff --git a/CMakeLists.txt b/CMakeLists.txt index d9aace75..fa845d65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ set(CPP_SOURCE_FILES src/StyleCollection.cpp src/UndoCommands.cpp src/locateNode.cpp + src/PluginsManager.cpp ) set(HPP_HEADER_FILES @@ -128,6 +129,8 @@ set(HPP_HEADER_FILES include/QtNodes/internal/Serializable.hpp include/QtNodes/internal/Style.hpp include/QtNodes/internal/StyleCollection.hpp + include/QtNodes/internal/PluginInterface.hpp + include/QtNodes/internal/PluginsManager.hpp src/DefaultConnectionPainter.hpp src/DefaultHorizontalNodeGeometry.hpp src/DefaultNodePainter.hpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 49494da2..07b7f75d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -16,3 +16,5 @@ add_subdirectory(dynamic_ports) add_subdirectory(lock_nodes_and_connections) +add_subdirectory(plugins_load) + diff --git a/examples/plugins_load/CMakeLists.txt b/examples/plugins_load/CMakeLists.txt new file mode 100644 index 00000000..90aa3a64 --- /dev/null +++ b/examples/plugins_load/CMakeLists.txt @@ -0,0 +1,7 @@ +set(CPPS main.cpp) + +add_executable(plugins_load ${CPPS}) + +target_link_libraries(plugins_load QtNodes) + +add_subdirectory(plugins/plugin_text) diff --git a/examples/plugins_load/main.cpp b/examples/plugins_load/main.cpp new file mode 100644 index 00000000..b64f1b45 --- /dev/null +++ b/examples/plugins_load/main.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using QtNodes::ConnectionStyle; +using QtNodes::DataFlowGraphicsScene; +using QtNodes::DataFlowGraphModel; +using QtNodes::GraphicsView; +using QtNodes::NodeDelegateModelRegistry; +using QtNodes::PluginInterface; +using QtNodes::PluginsManager; + +void loadPluginsFromFolder() +{ + PluginsManager *pluginsManager = PluginsManager::instance(); + std::shared_ptr registry = pluginsManager->registry(); + + // load plugins + pluginsManager->loadPlugins(QDir::cleanPath(QCoreApplication::applicationDirPath() + + QDir::separator() + "plugins"), + QStringList() << "*.node" + << "*.data"); + + for (auto l : pluginsManager->loaders()) { + PluginInterface *plugin = qobject_cast(l.second->instance()); + if (!plugin) + continue; + + plugin->registerDataModels(registry); + } +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QWidget mainWidget; + + // Load plugins and register models + loadPluginsFromFolder(); + + QVBoxLayout *l = new QVBoxLayout(&mainWidget); + + DataFlowGraphModel dataFlowGraphModel(PluginsManager::instance()->registry()); + + auto scene = new DataFlowGraphicsScene(dataFlowGraphModel, &mainWidget); + + auto view = new GraphicsView(scene); + l->addWidget(view); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + QObject::connect(scene, &DataFlowGraphicsScene::sceneLoaded, view, &GraphicsView::centerScene); + + mainWidget.setWindowTitle("Data Flow: Plugins Load"); + mainWidget.resize(800, 600); + mainWidget.show(); + + return app.exec(); +} diff --git a/examples/plugins_load/plugins/plugin_text/CMakeLists.txt b/examples/plugins_load/plugins/plugin_text/CMakeLists.txt new file mode 100644 index 00000000..eb444619 --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/CMakeLists.txt @@ -0,0 +1,16 @@ +if(MSVC) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "$/plugins") +else() + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "$/plugins") +endif() + +file(GLOB_RECURSE CPPS ./*.cpp) +file(GLOB_RECURSE HPPS ./*.hpp) + +add_library(plugin_text SHARED ${CPPS} ${HPPS}) + +target_link_libraries(plugin_text QtNodes) + +target_compile_definitions(plugin_text PUBLIC NODE_EDITOR_SHARED) + +set_target_properties(plugin_text PROPERTIES SUFFIX ".node") diff --git a/examples/plugins_load/plugins/plugin_text/PluginDefinition.cpp b/examples/plugins_load/plugins/plugin_text/PluginDefinition.cpp new file mode 100644 index 00000000..a88dc118 --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/PluginDefinition.cpp @@ -0,0 +1,22 @@ +#include "PluginDefinition.hpp" + +#include "TextModel.hpp" + +Plugin *Plugin::_this_plugin = nullptr; + +Plugin::Plugin() +{ + _this_plugin = this; +} + +Plugin::~Plugin() +{ + // TODO: Unregister all models here +} + +void Plugin::registerDataModels(std::shared_ptr ®) +{ + assert(reg); + + reg->registerModel(); +} diff --git a/examples/plugins_load/plugins/plugin_text/PluginDefinition.hpp b/examples/plugins_load/plugins/plugin_text/PluginDefinition.hpp new file mode 100644 index 00000000..8af43906 --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/PluginDefinition.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +// This needs to be the same as the name of your project file ${PROJECT_NAME} +#ifdef plugin_text_EXPORTS +#define DLL_EXPORT Q_DECL_EXPORT +#else +#define DLL_EXPORT Q_DECL_IMPORT +#endif + +#define PLUGIN_NAME "Text" + +class DLL_EXPORT Plugin + : public QObject + , public QtNodes::PluginInterface +{ + Q_OBJECT + Q_INTERFACES(QtNodes::PluginInterface) + Q_PLUGIN_METADATA(IID PLUGIN_NAME) + +public: + Plugin(); + ~Plugin(); + + QString name() const override { return PLUGIN_NAME; }; + + void registerDataModels(std::shared_ptr ®) override; + +private: + static Plugin *_this_plugin; +}; \ No newline at end of file diff --git a/examples/plugins_load/plugins/plugin_text/TextData.hpp b/examples/plugins_load/plugins/plugin_text/TextData.hpp new file mode 100644 index 00000000..b07fc90c --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/TextData.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +using QtNodes::NodeData; +using QtNodes::NodeDataType; + +/// The class can potentially incapsulate any user data which +/// need to be transferred within the Node Editor graph +class TextData : public NodeData +{ +public: + TextData() {} + + TextData(QString const &text) + : _text(text) + {} + + NodeDataType type() const override { return NodeDataType{"text", "Text"}; } + + QString text() const { return _text; } + +private: + QString _text; +}; diff --git a/examples/plugins_load/plugins/plugin_text/TextModel.cpp b/examples/plugins_load/plugins/plugin_text/TextModel.cpp new file mode 100644 index 00000000..e7aa8b5f --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/TextModel.cpp @@ -0,0 +1,66 @@ +#include "TextModel.hpp" + +#include + +TextModel::TextModel() {} + +unsigned int TextModel::nPorts(PortType portType) const +{ + unsigned int result = 1; + + switch (portType) { + case PortType::In: + result = 1; + break; + + case PortType::Out: + result = 1; + + default: + break; + } + + return result; +} + +void TextModel::onTextEdited() +{ + Q_EMIT dataUpdated(0); +} + +NodeDataType TextModel::dataType(PortType, PortIndex) const +{ + return TextData().type(); +} + +std::shared_ptr TextModel::outData(PortIndex const portIndex) +{ + Q_UNUSED(portIndex); + return std::make_shared(_textEdit->toPlainText()); +} + +QWidget *TextModel::embeddedWidget() +{ + if (!_textEdit) { + _textEdit = new QTextEdit(); + + connect(_textEdit, &QTextEdit::textChanged, this, &TextModel::onTextEdited); + } + + return _textEdit; +} + +void TextModel::setInData(std::shared_ptr data, PortIndex const) +{ + auto textData = std::dynamic_pointer_cast(data); + + QString inputText; + + if (textData) { + inputText = textData->text(); + } else { + inputText = ""; + } + + _textEdit->setText(inputText); +} diff --git a/examples/plugins_load/plugins/plugin_text/TextModel.hpp b/examples/plugins_load/plugins/plugin_text/TextModel.hpp new file mode 100644 index 00000000..75ab7bf1 --- /dev/null +++ b/examples/plugins_load/plugins/plugin_text/TextModel.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include "TextData.hpp" + +#include + +#include + +using QtNodes::NodeData; +using QtNodes::NodeDelegateModel; +using QtNodes::PortIndex; +using QtNodes::PortType; + +class QTextEdit; + +/// The model dictates the number of inputs and outputs for the Node. +/// In this example it has no logic. +class TextModel : public NodeDelegateModel +{ + Q_OBJECT + +public: + TextModel(); + +public: + QString caption() const override { return QString("Text"); } + + bool captionVisible() const override { return true; } + + static QString Name() { return QString("Text"); } + + QString name() const override { return TextModel::Name(); } + +public: + unsigned int nPorts(PortType portType) const override; + + NodeDataType dataType(PortType portType, PortIndex portIndex) const override; + + std::shared_ptr outData(PortIndex const portIndex) override; + + void setInData(std::shared_ptr, PortIndex const) override; + + QWidget *embeddedWidget() override; + + bool resizable() const override { return true; } + +private Q_SLOTS: + + void onTextEdited(); + +private: + QTextEdit *_textEdit = nullptr; +}; diff --git a/include/QtNodes/PluginInterface b/include/QtNodes/PluginInterface new file mode 100644 index 00000000..d93896bd --- /dev/null +++ b/include/QtNodes/PluginInterface @@ -0,0 +1 @@ +#include "internal/PluginInterface.hpp" \ No newline at end of file diff --git a/include/QtNodes/PluginsManager b/include/QtNodes/PluginsManager new file mode 100644 index 00000000..e1a496e3 --- /dev/null +++ b/include/QtNodes/PluginsManager @@ -0,0 +1 @@ +#include "internal/PluginsManager.hpp" \ No newline at end of file diff --git a/include/QtNodes/internal/PluginInterface.hpp b/include/QtNodes/internal/PluginInterface.hpp new file mode 100644 index 00000000..9d1fd33e --- /dev/null +++ b/include/QtNodes/internal/PluginInterface.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace QtNodes { + +class NodeDelegateModelRegistry; + +class PluginInterface +{ +public: + virtual ~PluginInterface() = default; + + virtual QString name() const = 0; + + virtual void registerDataModels(std::shared_ptr ®) = 0; +}; + +} // namespace QtNodes + +Q_DECLARE_INTERFACE(QtNodes::PluginInterface, "QtNodes.PluginInterface/1.0") diff --git a/include/QtNodes/internal/PluginsManager.hpp b/include/QtNodes/internal/PluginsManager.hpp new file mode 100644 index 00000000..a854be44 --- /dev/null +++ b/include/QtNodes/internal/PluginsManager.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "Export.hpp" +#include "PluginInterface.hpp" + +#include +#include +#include +#include + +namespace QtNodes { + +class NodeDelegateModelRegistry; + +class NODE_EDITOR_PUBLIC PluginsManager +{ + PluginsManager(); + + virtual ~PluginsManager(); + +public: + static PluginsManager *instance(); + + std::shared_ptr registry(); + + void loadPlugins(const QString &folderPath = "./plugins", + const QStringList &nameFilters = QStringList()); + + void unloadPlugins(); + + /** + * @brief Load the plug-in from the full file path + * + * @param filePath "C:/plugin_text.dll" + * @return PluginInterface* + */ + PluginInterface *loadPluginFromPath(const QString &filePath); + + std::vector loadPluginFromPaths(const QStringList filePaths, + const QStringList &nameFilters); + + /** + * @brief Unload the plugin from the full file path + * + * @param filePath "C:/plugin_text.dll" + * @return bool + */ + bool unloadPluginFromPath(const QString &filePath); + + /** + * @brief Uninstall a plugin by its name, not its file name + * + * @param pluginName "pluginText" + * @return bool + */ + bool unloadPluginFromName(const QString &pluginName); + + inline std::unordered_map loaders() { return _loaders; }; + +private: + static PluginsManager *_instance; + + std::unordered_map _loaders; // plugin name + + std::shared_ptr _register; +}; + +} // namespace QtNodes \ No newline at end of file diff --git a/src/PluginsManager.cpp b/src/PluginsManager.cpp new file mode 100644 index 00000000..d10c2fa8 --- /dev/null +++ b/src/PluginsManager.cpp @@ -0,0 +1,212 @@ +#include "PluginsManager.hpp" + +#include "NodeDelegateModelRegistry.hpp" + +#include +#include +#include +#include +#include +#include + +#if defined(Q_OS_WIN) +#include +#endif + +namespace QtNodes { + +PluginsManager *PluginsManager::_instance = nullptr; + +PluginsManager::PluginsManager() +{ + if (!_register) + _register = std::make_shared(); +} + +PluginsManager::~PluginsManager() +{ + unloadPlugins(); + + if (PluginsManager::instance()) { + delete PluginsManager::instance(); + PluginsManager::_instance = nullptr; + } +} + +PluginsManager *PluginsManager::instance() +{ + if (_instance == nullptr) + _instance = new PluginsManager(); + return _instance; +} + +std::shared_ptr PluginsManager::registry() +{ + return _register; +} + +/** + * @brief Recursively loads all plugins with the specified suffix according to the folder path. + * If no suffix is specified then the choice is up to the OS. For example, Windows OS selects `*.dll` + * + * ``` + * │ plugins_load + * │ QtNodes.dll + * │ + * └─plugins + * │ + * └─text + * plugin_text.node + * text_dependent.dll + * ``` + * @TODO: Currently only tested and passed under windows, is there a solution for Qt for all three platforms? + * 1. `plugins_Load` can successfully load `plugin_text.node` + * 2. After changing the folder name `text` it still loads successfully + * + * @param folderPath + * @param nameFilters + */ +void PluginsManager::loadPlugins(const QString &folderPath, const QStringList &nameFilters) +{ + QDir pluginsDir; + if (!pluginsDir.exists(folderPath)) { + // Created if folderPath does not exist + pluginsDir.mkpath(folderPath); + } + pluginsDir.cd(folderPath); + + auto IsLibrary = [](const QFileInfo f, const QStringList &nameFilters) { + if (!f.isFile()) + return false; + + if (nameFilters.isEmpty()) + return QLibrary::isLibrary(f.absoluteFilePath()); + + for (auto s : nameFilters) { + if (s.endsWith(f.suffix(), Qt::CaseInsensitive)) + return true; + } + return false; + }; + + QDirIterator it(pluginsDir.path(), + QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Hidden); + while (it.hasNext()) { + it.next(); + QFileInfo f = it.fileInfo(); + if (f.isDir()) { + loadPlugins(it.filePath(), nameFilters); + } else if (f.isFile() && IsLibrary(f, nameFilters)) { +#if defined(Q_OS_WIN) +#ifdef UNICODE + SetDllDirectory(folderPath.toStdWString().c_str()); +#else + SetDllDirectory(folderPath.toStdString().c_str()); +#endif // !UNICODE +#endif + loadPluginFromPath(it.filePath()); + +#if defined(Q_OS_WIN) + SetDllDirectory(NULL); +#endif + } + } +} + +void PluginsManager::unloadPlugins() +{ + for (auto l : _loaders) { + l.second->unload(); + delete l.second; + } + _loaders.clear(); +} + +PluginInterface *PluginsManager::loadPluginFromPath(const QString &filePath) +{ + QPluginLoader *loader = new QPluginLoader(filePath); + + if (loader->isLoaded()) { + PluginInterface *plugin = qobject_cast(loader->instance()); + + QPluginLoader *l = _loaders.find(plugin->name())->second; + plugin = qobject_cast(l->instance()); + + loader->unload(); + delete loader; + + return plugin; + } + + PluginInterface *plugin = qobject_cast(loader->instance()); + if (plugin) { + _loaders[plugin->name()] = loader; + + return plugin; + } else { + qWarning() << loader->errorString(); + + delete loader; + } + + return nullptr; +} + +std::vector PluginsManager::loadPluginFromPaths(const QStringList filePaths, + const QStringList &nameFilters) +{ + std::vector vecPlugins; + vecPlugins.clear(); + + auto IsLibrary = [](const QFileInfo f, const QStringList &nameFilters) { + if (!f.isFile()) + return false; + + if (nameFilters.isEmpty()) + return QLibrary::isLibrary(f.absoluteFilePath()); + + for (auto nf : nameFilters) { + if (nf.endsWith(f.suffix(), Qt::CaseInsensitive)) + return true; + } + return false; + }; + + for (auto path : filePaths) { + QFileInfo f(path); + if (IsLibrary(f, nameFilters)) + vecPlugins.push_back(loadPluginFromPath(path)); + } + return vecPlugins; +} + +bool PluginsManager::unloadPluginFromPath(const QString &filePath) +{ + for (auto l : _loaders) { + if (l.second->fileName() == filePath) { + if (l.second->unload() == false) { + return false; + } + delete l.second; + _loaders.erase(l.first); + return true; + } + } + return false; +} + +bool PluginsManager::unloadPluginFromName(const QString &pluginName) +{ + auto loaderIter = _loaders.find(pluginName); + if (loaderIter != _loaders.end()) { + if (loaderIter->second->unload() == false) { + return false; + } + delete loaderIter->second; + _loaders.erase(loaderIter->first); + return true; + } + return false; +} + +} // namespace QtNodes