Skip to content

Writing a toolkit plugin

Felix Richter edited this page Sep 16, 2020 · 6 revisions

Toolkit Plugins

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.

Usage

  1. Clone the toolkit-plugin-template repo git clone https://github.com/ORB-HD/toolkit-plugin-template
  2. Within you will find the create_plugin.py python script that will create a new plugin.
  3. The script creates a new directory containing the basic code needed for a new functional plugin.
  4. 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
  1. Install the plugin to the toolkit plugin directory with sudo make install

Build for usage with AppImage

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.

Options

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

Plugin Parts

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.

Initialization

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.

Settings

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.

File Reading

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.

Model Extensions

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.

Command Line Options

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
     });

     ...
}