Creating Your First Editor Plugin
This article describes how to create an Editor plugin and get the first impression of what a plugin consists of and what its general structure is.
UnigineEditor is an application written entirely in C++ relying a lot on the Qt5 framework infrastructure. So, in order to extend its functionality not only C++ programming skills are required, but you should also be familiar with the Qt5 framework, CMake build system.
As an example we shall create a simple plugin implementing the basic functionality of the Editor's Materials Hierarchy window (see the picture below).
Basically the plugin's architecture looks as follows:
The MaterialsPlugin class is an initialization point and complete representation of the plugin. It creates necessary data model and via the base interface associates it with the widget, which displays the hierarchy. The model directly interacts with the Materials Manager and transforms information on materials to the required structure.
The plugin interacts with all Editor's external subsystems:
- Adds a widget to the WindowManager;
- Adds a new item to the main menu bar;
- Updates the state of materials widget when the global selection is changed;
- Updates the global selection when the local selection is changed by sending a SelectionAction to the Undo stack.
See Also#
- Extending Editor Functionality article for more information on UnigineEditor's Plugin System and extensions.
- Editor API Reference for more information on all available classes.
File Structure#
The typical structure of a plugin project should look as follows:
-
project
-
bin - Engine and Editor binaries
- Unigine_x64.dll/so - Engine library (release).
- Unigine_x64d.dll/so - Engine library (debug).
- EditorCore_x64.dll/so - Editor library (release).
- EditorCore_x64d.dll/so - Editor library (debug).
-
lib - import libraries (Windows only)
- Unigine_x64.lib - Engine (release).
- Unigine_x64d.lib - Engine (debug).
- EditorCore_x64.lib - Editor (release).
- EditorCore_x64d.lib - Editor (debug).
-
include - public API
- unigine - Engine's public API.
- editor - Editor's public API.
- cmake
-
src - plugin source files
- Plugin.h
- Plugin.cpp
- Plugin.json.in
- CMakeList.txt
- CMakeList.txt
-
bin - Engine and Editor binaries
The basic components are:
- Engine and Editor binary files;
- Engine and Editor public API header files;
- Plugin source files;
- Meta-json file;
- Template CMake-scripts;
Using Plugin Template#
UNIGINE offers you a template to simplify creation of Editor plugins. The template can be found in the following folder: <UNIGINE_SDK>/utils/project/template/editor-plugin
First, we should create a folder for our new Editor plugin project. Then, simply copy all necessary files from the SDK installation directory to this new created folder as shown in the figure below:
Implementing Plugin's Logic#
Now we can implement our plugin's logic (you can simply copy source code listed below to the corresponding header and implementation files).
Plugin Class#
The files MaterialsPlugin.h and MaterialsPlugin.cpp define the implementation of our plugin. Let's highlight some important points here.
Header File#
The header file MaterialsPlugin.h defines the interface of the plugin class.
namespace Materials {
The plugin is defined in a Materials namespace, which conforms to the coding rules for namespacing in sources.
class MaterialsPlugin : public QObject, public ::Editor::Plugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "com.unigine.EditorPlugin" FILE "Materials.json")
Q_INTERFACES(Editor::Plugin)
All Editor plugins must be derived from Editor::Plugin class and are QObjects. The Q_PLUGIN_METADATA macro is necessary to create a valid Editor plugin. The IID given in the macro must be com.unigine.EditorPlugin, to identify it as an Editor plugin, and FILE must point to the plugin's meta data file as described in Plugin Meta Data.
bool init() override;
void shutdown() override;
The base class defines basic functions that are called during the life cycle of a plugin (on initialization and shutdown), which are here implemented for your new plugin.
Our plugin has three additional custom slots, that are used show the view, set global selection and update the local one when the global is changed.
public slots:
void showView();
void setGlobalSelection(const QModelIndexList &indexes);
void globalSelectionChanged();
MaterialsPlugin.h
#pragma once
#include <editor/Plugin.h>
#include <QObject>
#include <QModelIndex>
namespace Materials {
class MaterialsModel;
class MaterialsView;
}
namespace Materials {
class MaterialsPlugin : public QObject, public ::Editor::Plugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "com.unigine.EditorPlugin" FILE "Materials.json")
Q_INTERFACES(Editor::Plugin)
public:
MaterialsPlugin();
~MaterialsPlugin() override;
bool init() override;
void shutdown() override;
public slots:
void showView();
void setGlobalSelection(const QModelIndexList &indexes);
void globalSelectionChanged();
private:
MaterialsModel *model_;
MaterialsView *view_;
};
} // namespace Materials
Implementation File#
This file contains the actual implementation of our plugin and its logic. For more information about implementing the plugin interface, see the Editor::Plugin API documentation and Plugin Life Cycle.
MaterialsPlugin.cpp
#include "MaterialsPlugin.h"
#include "MaterialsView.h"
#include "MaterialsModel.h"
#include "QtMetaTypes.h"
#include <Actions.h>
#include <Undo.h>
#include <Constants.h>
#include <WindowManager.h>
#include <Selection.h>
#include <Selector.h>
#include <QObject>
#include <QMenu>
#include <algorithm>
#include <iterator>
using Editor::Selection;
using Editor::SelectionAction;
using Editor::WindowManager;
using Editor::SelectorGUIDs;
namespace Materials
{
MaterialsPlugin::MaterialsPlugin()
: model_ (nullptr)
, view_ (nullptr)
{
}
MaterialsPlugin::~MaterialsPlugin() = default;
bool MaterialsPlugin::init()
{
model_ = new MaterialsModel(this);
view_ = new MaterialsView(model_);
view_->setWindowTitle("Plugin - Material List");
view_->setObjectName("PluginMaterialsView");
WindowManager::add(view_, WindowManager::ROOT_AREA_LEFT);
connect(view_, &MaterialsView::selected, this, &MaterialsPlugin::setGlobalSelection);
QMenu *menu = WindowManager::findMenu(Constants::MM_WINDOWS);
menu->addAction("Plugin - Material List", this, &MaterialsPlugin::showView);
connect(Selection::instance(), &Selection::changed,
this, &MaterialsPlugin::globalSelectionChanged);
return true;
}
void MaterialsPlugin::shutdown()
{
}
void MaterialsPlugin::showView()
{
WindowManager::show(view_);
}
void MaterialsPlugin::setGlobalSelection(const QModelIndexList &indexes)
{
disconnect(Selection::instance(), &Selection::changed,
this, &MaterialsPlugin::globalSelectionChanged);
QVector<Unigine::UGUID> guids;
guids.reserve(indexes.size());
std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(guids),
[](const QModelIndex &idx)
{ return idx.data(MaterialsModel::GUID_ROLE).value<Unigine::UGUID>(); });
SelectionAction::applySelection(SelectorGUIDs::createMaterialsSelector(guids));
connect(Selection::instance(), &Selection::changed,
this, &MaterialsPlugin::globalSelectionChanged);
}
void MaterialsPlugin::globalSelectionChanged()
{
using namespace std;
if (auto selector = Selection::getSelectorMaterials())
{
const QVector<Unigine::UGUID> guids = selector->guids();
QModelIndexList indexes;
indexes.reserve(guids.size());
transform(begin(guids), end(guids), back_inserter(indexes),
[this](const Unigine::UGUID &guid) { return model_->index(guid); });
auto it = remove(begin(indexes), end(indexes), QModelIndex());
indexes.erase(it, end(indexes));
QSignalBlocker blocker(view_);
view_->globalSelectionChanged(indexes);
}
else if(auto selector = Selection::getSelectorRuntimes())
{
}
else
{
view_->clearSelection();
}
}
} // namespace Materials
Model Class#
Header File#
MaterialsModel.h
#pragma once
#include <QAbstractItemModel>
namespace Unigine { class UGUID; };
namespace Materials
{
class MaterialsModel : public QAbstractItemModel
{
class Item;
public:
enum Role
{
GUID_ROLE = Qt::UserRole + 1
};
MaterialsModel(QObject *parent = nullptr);
~MaterialsModel() override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex index(const Unigine::UGUID &guid) const;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
protected:
void fetchMore(const QModelIndex &parent) override;
bool canFetchMore(const QModelIndex &parent) const override;
private:
Item *root_;
};
} // namespace Materials
Implementation File#
MaterialsModel.cpp
#include "MaterialsModel.h"
#include <UnigineGUID.h>
#include <UnigineMaterials.h>
#include "QtMetaTypes.h"
#include <QDebug>
using Unigine::UGUID;
namespace Materials
{
////////////////////////////////////////////////////////////////////////////////
// MaterialsModel::Item.
////////////////////////////////////////////////////////////////////////////////
class MaterialsModel::Item
{
public:
static Item *createRootItem(MaterialsModel *model);
Item(Item *parent, MaterialsModel *model, const UGUID &guid);
~Item();
QModelIndex index() const;
void append(Item *item);
Item *child(int idx);
Item *find(const UGUID &guid) const;
int childCount() const;
Item *parent();
int row() const;
QVariant data(int role) const;
void fetchMore();
bool canFetchMore() const;
private:
Item *parent_ = nullptr; // Not owned.
MaterialsModel *model_ = nullptr; // Not owned.
QVector<Item *> children_; // Owned.
UGUID guid_;
};
MaterialsModel::Item *MaterialsModel::Item::createRootItem(MaterialsModel *model)
{
Item *root = new Item(nullptr, model, UGUID());
const int size = Unigine::Materials::getNumMaterials();
for (int i = 0; i < size; ++i)
{
const Unigine::MaterialPtr mat = Unigine::Materials::getMaterial(i);
if (mat->isHidden())
{
continue;
}
if (const Unigine::MaterialPtr &parent_mat = mat->getParent())
{
if (!parent_mat->isHidden())
{
continue;
}
}
root->children_.push_back(new Item(root, model, mat->getGUID()));
}
return root;
}
MaterialsModel::Item::Item(Item *parent, MaterialsModel *model, const UGUID &guid)
: parent_(parent), model_(model), guid_(guid)
{
}
MaterialsModel::Item::~Item()
{
qDeleteAll(children_);
}
QModelIndex MaterialsModel::Item::index() const
{
if (!parent_ || guid_.isEmpty())
{
return QModelIndex();
}
return model_->createIndex(row(), 0, const_cast<Item *>(this));
}
void MaterialsModel::Item::append(Item *item)
{
const int size = children_.size();
model_->beginInsertRows(index(), size, size);
item->parent_ = this;
children_.push_back(item);
model_->endInsertRows();
}
auto MaterialsModel::Item::child(int idx) -> Item *
{
return ((idx >= 0) && (idx < children_.size()))
? children_[idx]
: nullptr;
}
auto MaterialsModel::Item::find(const UGUID &guid) const -> Item *
{
for (Item *child: children_)
{
if (child->guid_ == guid)
{
return child;
}
if (child->canFetchMore())
{
child->fetchMore();
}
if (Item *grandchild = child->find(guid))
{
return grandchild;
}
}
return nullptr;
}
int MaterialsModel::Item::childCount() const
{
if (!guid_.isValid())
{
return children_.size();
}
const Unigine::MaterialPtr mat = Unigine::Materials::findMaterialByGUID(guid_);
int count = mat->getNumChildren();
for (int i = count - 1; i >= 0; --i)
{
if (mat->getChild(i)->isHidden())
{
--count;
}
}
return count;
}
MaterialsModel::Item *MaterialsModel::Item::parent()
{
return parent_;
}
int MaterialsModel::Item::row() const
{
if (parent_)
{
return parent_->children_.indexOf(const_cast<Item *>(this));
}
return 0;
}
QVariant MaterialsModel::Item::data(int role) const
{
if (Qt::DisplayRole == role)
{
const Unigine::MaterialPtr &mat = Unigine::Materials::findMaterialByGUID(guid_);
return QVariant::fromValue(QString(mat->getName()));
}
if (MaterialsModel::GUID_ROLE == role)
{
return QVariant::fromValue(guid_);
}
return QVariant();
}
void MaterialsModel::Item::fetchMore()
{
const Unigine::MaterialPtr &mat = Unigine::Materials::findMaterialByGUID(guid_);
const int size = mat->getNumChildren();
QVector<Item *> items;
items.reserve(size);
for (int i = 0; i < size; ++i)
{
const Unigine::MaterialPtr child_mat = mat->getChild(i);
if (child_mat->isHidden())
{
continue;
}
items.push_back(new Item(this, model_, child_mat->getGUID()));
}
if (items.empty())
{
return;
}
const int cur_size = children_.size();
model_->beginInsertRows(index(), cur_size, cur_size + items.size() - 1);
std::copy(std::begin(items), std::end(items), std::back_inserter(children_));
model_->endInsertRows();
}
bool MaterialsModel::Item::canFetchMore() const
{
return children_.empty() && childCount();
}
////////////////////////////////////////////////////////////////////////////////
// MaterialsModel.
////////////////////////////////////////////////////////////////////////////////
MaterialsModel::MaterialsModel(QObject *parent)
: QAbstractItemModel(parent)
, root_(Item::createRootItem(this))
{
}
MaterialsModel::~MaterialsModel()
{
delete root_;
}
QModelIndex MaterialsModel::index(int row, int column, const QModelIndex &parent) const
{
if (column > 0)
{
return {};
}
Item *parent_item = nullptr;
if (!parent.isValid())
{
parent_item = root_;
}
else
{
parent_item = static_cast<Item *>(parent.internalPointer());
}
Item *child_item = parent_item->child(row);
return child_item
? createIndex(row, column, child_item)
: QModelIndex();
}
QModelIndex MaterialsModel::index(const UGUID &guid) const
{
if (guid.isEmpty())
{
return QModelIndex();
}
Item *item = root_->find(guid);
return item ? item->index() : QModelIndex();
}
QModelIndex MaterialsModel::parent(const QModelIndex &child) const
{
if (!child.isValid())
{
return QModelIndex();
}
Item *child_item = static_cast<Item *>(child.internalPointer());
Item *parent_item = child_item->parent();
return (parent_item != root_)
? createIndex(parent_item->row(), 0, parent_item)
: QModelIndex();
}
int MaterialsModel::rowCount(const QModelIndex &parent) const
{
if (parent.column() > 0)
{
return 0;
}
Item *parent_item = nullptr;
if (!parent.isValid())
{
parent_item = root_;
}
else
{
parent_item = static_cast<Item *>(parent.internalPointer());
}
return parent_item->childCount();
}
int MaterialsModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant MaterialsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
{
return QVariant();
}
Item *item = static_cast<Item *>(index.internalPointer());
return item->data(role);
}
void MaterialsModel::fetchMore(const QModelIndex &parent)
{
if (!parent.isValid())
{
return;
}
Item *item = static_cast<Item *>(parent.internalPointer());
item->fetchMore();
}
bool MaterialsModel::canFetchMore(const QModelIndex &parent) const
{
if (!parent.isValid())
{
return false;
}
Item *item = static_cast<Item *>(parent.internalPointer());
return item->canFetchMore();
}
} // namespace Materials
View Class#
Header File#
MaterialsView.h
#pragma once
#include <QWidget>
#include <QModelIndex>
namespace Unigine { class UGUID; }
class QAbstractItemModel;
class QItemSelection;
class QTreeView;
namespace Materials
{
class MaterialsView : public QWidget
{
Q_OBJECT
public:
MaterialsView(QAbstractItemModel *model, QWidget *parent = nullptr);
~MaterialsView();
signals:
void selected(const QModelIndexList &indexes);
void menuRequested(const QModelIndexList &indexes);
public slots:
void clearSelection();
void globalSelectionChanged(const QModelIndexList &indexes);
private slots:
void selectionChanged(const QItemSelection &selection);
private:
QTreeView *view_ = nullptr;
};
} // namespace Materials
Implementation File#
MaterialsView.cpp
#include "MaterialsView.h"
#include <QAbstractItemModel>
#include <QVBoxLayout>
#include <QTreeView>
namespace Materials
{
MaterialsView::MaterialsView(QAbstractItemModel *model, QWidget *parent)
: QWidget{parent}
{
view_ = new QTreeView();
view_->setAlternatingRowColors(true);
view_->setSelectionMode(QAbstractItemView::ExtendedSelection);
view_->setSelectionBehavior(QAbstractItemView::SelectRows);
view_->setHeaderHidden(true);
auto vl = new QVBoxLayout(this);
vl->setContentsMargins(2, 2, 2, 2);
vl->addWidget(view_);
view_->setModel(model);
connect(view_->selectionModel(), &QItemSelectionModel::selectionChanged
, this, &MaterialsView::selectionChanged);
}
MaterialsView::~MaterialsView() = default;
void MaterialsView::clearSelection()
{
view_->selectionModel()->clearSelection();
}
void MaterialsView::globalSelectionChanged(const QModelIndexList &indexes)
{
if (indexes.empty())
{
clearSelection();
return;
}
view_->setCurrentIndex(indexes.back());
QItemSelection item_selection;
for (const QModelIndex &index: indexes)
{
item_selection.append(QItemSelectionRange(index));
}
view_->selectionModel()->select(item_selection, QItemSelectionModel::ClearAndSelect);
view_->scrollTo(indexes.back());
}
void MaterialsView::selectionChanged(const QItemSelection &selection)
{
emit selected(view_->selectionModel()->selectedIndexes());
}
} // namespace Materials
Auxiliary File#
This file contains registration of additional metatypes to be used in our plugin to make them known to Qt's meta-object system, we need Unigine::UGUID.
QtMetaTypes.h
#pragma once
// including UnigineGUID.h containing definition of the metatype we're going to declare
#include <UnigineGUID.h>
// including QMetaType to be able to declare metatypes
#include <QMetaType>
// registering the Unigine::UGUID type to make it known to Qt's meta-object system
Q_DECLARE_METATYPE(Unigine::UGUID)
Building the Plugin#
In order to build our plugin we also need a couple of files.
We need to create an initial meta data description file (Materials.json.in) to describe our Materials plugin and its dependencies. This file shall be used by CMake to generate the actual plugin .json meta data file.
Materials.json.in
{
"Name" : "Materials",
"Vendor" : "Unigine",
"Description" : "<p>Provides functionality to overview and manipulate the Engine's materials</p><p>The plugin is used to demonstrate the basic usage of the new `Editor Plugin System` implementation. It contains the main plugin, tree model and tree view instances</p>",
"Version" : "@PLUGIN_VERSION@",
"CompatVersion" : "@PLUGIN_COMPAT_VERSION@",
"Dependencies" : []
}
And we also need to modify the CMake build script src/CMakeList.txt. Regarding the described above file structure it should look like this:
src/CMakeList.txt
# Substitute placeholders in the plugin's meta file source with actual version data
# and create the final meta file.
if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/Materials.json.in)
set(PLUGIN_VERSION ${UNIGINE_VERSION})
set(PLUGIN_COMPAT_VERSION ${UNIGINE_VERSION})
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/Materials.json.in
${CMAKE_CURRENT_BINARY_DIR}/Materials.json
)
endif()
# Enable automatic handling for moc, uic, and rcc
set(CMAKE_AUTOMOC TRUE)
set(CMAKE_AUTOUIC TRUE)
set(CMAKE_AUTORCC TRUE)
# Find additional required packages
find_package(Qt5 5.12 CONFIG REQUIRED COMPONENTS Gui)
find_package(Qt5 5.12 CONFIG REQUIRED COMPONENTS Widgets)
add_library(Materials SHARED
# List of sources
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsPlugin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsPlugin.h
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsView.h
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MaterialsModel.h
)
target_include_directories(Materials SYSTEM
PRIVATE
${PROJECT_SOURCE_DIR}/include # <- - - - Path to Editor and Engine libraries
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(Materials
PRIVATE
Unigine::CompilerFlags # <- - - - Interface library with predefined compilation flags
Unigine::Engine # <- - - - - Engine
Unigine::Editor # <- - - - - Public Editor library
Qt5::Core #
Qt5::Gui # <- - - - - Required Qt components for the plugin
Qt5::Widgets #
)
# Set output directories for the Materials plugin target
set_target_properties(Materials
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin
LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin
ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib
CXX_VISIBILITY_PRESET hidden
)
target_compile_definitions(Materials
PRIVATE
QT_NO_DEBUG
)
Then we should change the default plugin project name to "Materials" in the CMakeList.txt in the plugin project's root folder:
CMakeList.txt
# Copyright (C), UNIGINE. All rights reserved.
cmake_minimum_required(VERSION 3.14)
project(Materials CXX) # < - - - Change project name here
include(${PROJECT_SOURCE_DIR}/cmake/Unigine.cmake)
add_subdirectory(${PROJECT_SOURCE_DIR}/src)
Now we can use CMake to build our plugin.
Running the Plugin#
Now as the plugin is built we should copy resulting files to the following folder, so that UnigineEditor's Plugin System could load it automatically:
- %project%/bin/editor — for Release build;
And finally we launch the UnigineEditor to check our plugin. If everything was done properly, you should see the Materials plugin in the list of loaded plugins (Help -> Plugins).