Writing a toolkit plugin - rbdl/rbdl-toolkit GitHub Wiki
This article will describe how to create create a new toolkit plugin by using the toolkit-plugin-template. After a short introduction on how to use this template I will explain in detail the different parts of a plugin that might be useful to somebody creating a new one.
- Clone the toolkit-plugin-template repo
git clone https://github.com/ORB-HD/toolkit-plugin-template
- Within you will find the
create_plugin.py
python script that will create a new plugin. - The script creates a new directory containing the basic code needed for a new functional plugin.
- To build this plugin run you will need to create a build directory and it using cmake:
mkdir build
cd build
cmake -DCUSTOM_QT_PATH=<QT_Lib_PATH> -DCMAKE_BUILD_TYPE=<Release/Debug> -DTOOLKIT_BUILD_PATH=<Toolkit_Build_DIR> -DTOOLKIT_SOURCE_PATH <Toolkit_Source_DIR> <Source_Path_of_Plugin>
make
- Install the plugin to the toolkit plugin directory with
sudo make install
To make it easy to create a plugin which will work with the rbdl-toolkit.AppImage there is another AppImage application, which will perform all the build steps required. It can be downloaded alongside the AppImage release of the toolkit. To use it replace step 4. above with the following command.
toolkit-buildplugin-x86_64.AppImage <Path_to_Plugin_Source_Tree>
You will now have your plugin as a shared library in your current path.
When creating a new plugin using the script there is one required option:
-
--name
CamelCaseName of the plugin you want to create it is used to name the classes and cmake project
Other convenience options are:
-
--dir
parent directory of the plugin project -
--intree
flag to enable when creating a plugin that lives withing the toolkit source tree -
--core
the created plugin will be a core plugin instead of a optional plugin -
--all
enable all other flags described below
When creating a new plugin there are some options to automatically create useful structures making it easy to create a plugin that has settings and add visuals to the scene without having to write all the boiler plate code yourself:
-
--extension
creates a model extension from a template and adds code to add this extension to loaded models -
--3d
add Qt3d includes and namespaces -
--settings
adds function to load settings and integrates plugin settings to the toolkit settings editor -
--cmd
add code to add command line options to the toolkit -
--filereader
add code to read in files and add actions for this to the toolkit ui -
--reload
if the plugin has parts that will need to reload when the model reloads this will add the code needed for this
Plugins get build as a shared library. The toolkit will search it's plugin directory for such libraries to be loaded. Every plugin can be enabled or disabled via the toolkit settings.
When a plugin is enabled it's init function will get called so that the plugin can set up all the interactions it needs:
void CamelCaseNamePlugin::init(ToolkitApp* app) {
//save reference to parent ToolkitApp
parentApp = app;
//setup plugin here
}
This is the most basic init function for a plugin. When the init function gets called it recieves a pointer to the toolkit instance. This is necessary so that the plugin can have access to the models, settings, scene and more. To access these you can use the public functions of the ToolkitApp. It is usefull to save this pointer to the toolkit so that other methods of the plugin can assess the toolkit later.
The Toolkit provides a nice way to store settings in such a way that they will survive restarts of the application and also enable editing these settings without much work.
void CamelCaseNamePlugin::loadCamelCaseNameSettings() {
parentApp->toolkit_settings.beginGroup("CamelCaseNameOptions");
QVariant val = parentApp->toolkit_settings.value("color");
// if this setting does not exist yet, create it with a default value
if (val.isNull()) {
color = QColor::fromRgbF(0., 0., 1., 1.);
parentApp->toolkit_settings.setValue("color", color.rgba());
} else {
//settings exist so read it's value
color = QColor::fromRgba(val.toUInt());
}
//make sure the toolkit settings know the correct type for this setting
parentApp->toolkit_settings.setType("color", color);
parentApp->toolkit_settings.endGroup();
}
This is a simple example of creating a settings group for the plugins settings so that it will not interfere with the settings of different plugins. In the back end the toolkit settings use the QSettings
class. There is just some inconsistency with QSettings
in the sense that it is not able to save the type of the values written to the settings file. This means we have to tell the toolkit settings which type the settings are so that the settings editor is able to provide a useful dialog when editing the settings. This function should also be called during the initialization of the plugin the toolkit knows the settings types directly after loading the plugin.
Reading in files for extra data is a common task, and to make this accessible via the ui we need to add some buttons in the file menu. All this gets setup in the init function. But we also need a method that gets called when pressing the button that then actually handles finding the desired file and loading the data:
void CamelCaseNamePlugin::init(ToolkitApp* app) {
...
// create Action and add to menu
load_file_trigger = new QAction("Load File");
parentApp->addFileAction(load_file_trigger);
// setup action so that when ui gets clicked the function for handling the file load gets called
connect(load_file_trigger, SIGNAL(triggered(bool)), this, SLOT(action_load_data()));
...
}
// creates file dialog to select file to be loaded
void CamelCaseNamePlugin::action_load_data() {
if (parentApp != NULL) {
QFileDialog file_dialog (parentApp, "Select CamelCaseName File");
// if you want to filter for certain file types
//file_dialog.setNameFilter(tr("CamelCaseName File (*.txt)"));
file_dialog.setFileMode(QFileDialog::ExistingFile);
if (file_dialog.exec()) {
QString filepath = file_dialog.selectedFiles().at(0);
// call function to do actually load the data from file
}
} else {
//should never happen
throw RigidBodyDynamics::Errors::RBDLError("CamelCaseNamePlugin was not initialized correctly!");
}
}
When using the --extension
flag these function will look a bit more complicated because the template assumes that loading a file will lead to creating a new model extension and adding that to an existing model. So all of that glue code will already be there.
For more information about model extensions read this Wiki Article. For the purposes of a plugin you can just crease a new extension by sub classing the WrapperExtension
class. And adding a extension to a loaded model is as easy as getting the model via the toolkit and calling the addExtension
function:
RBDLModelWrapper* rbdl_model = nullptr;
// if only one model is loaded select that one
if (parentApp->getLoadedModels()->size() == 1) {
rbdl_model = parentApp->getLoadedModels()->at(0);
} else {
// if multiple models are loaded ask the user which to use
rbdl_model = parentApp->selectModel(nullptr);
}
rbdl_model->addExtension(ext);
The selectModel
function will bring up a selection dialog in which the user can choose the model to use. The nullptr we give this function could also be a filtering function in case we want to filter models that are loaded for certain properties such as what other extensions it might already have loaded.
Plugins can add command line option to the toolkit. This is pretty straight forward and is setup in the init function of the plugin.
void CamelCaseNamePlugin::init(ToolkitApp* app) {
...
QCommandLineOption camelcasename_option( QStringList() << "camelcasename",
"Load CamelCaseName files <file>",
"file"
);
parentApp->addCmdOption(camelcasename_option, [this](QCommandLineParser& parser){
auto data_list = parser.values("camelcasename");
// implement cmd function here
});
...
}