Skip to content

Instantly share code, notes, and snippets.

@Flix01
Last active June 30, 2022 05:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Flix01/a7873b73f0ffb00c87260e5bf13a18d4 to your computer and use it in GitHub Desktop.
Save Flix01/a7873b73f0ffb00c87260e5bf13a18d4 to your computer and use it in GitHub Desktop.
NodeGraphEditor with dynamic enum test (https://github.com/Flix01/imgui/issues/15)
// On Ubuntu, I can compile it with the following command line (provided that imgui.h is two folders up, and that I want to use glfw):
// gcc -o basicExample mainBasic.cpp -I"../../" ../../imgui.cpp ../../imgui_draw.cpp -D"IMGUI_INCLUDE_IMGUI_USER_H" -D"IMGUI_INCLUDE_IMGUI_USER_INL" -I"/usr/include/GLFW" -D"IMGUI_USE_GLFW_BINDING" -L"/usr/lib/x86_64-linux-gnu" -lglfw -lX11 -lm -lGL -lstdc++ -s
// This file is intended to test/answer to https://github.com/Flix01/imgui/issues/15
// RESULT:
// Dynamic enum works!
// And if you can use dynamic_cast<>() making new Node types that use it is easier (non-intrusive)
// Otherwise you must modify the code of CustomEnumEditorNode::render(...) for every new user class you add.
//
// Added also some code to serialize/deserialize the enum names ("TestEnumNames") together
// with the Node Graph Editor itself (to the same file).
//
// Fixed a possible copy/paste bug
#include <imgui.h> // intellisense only
#include <addons/imguinodegrapheditor/imguinodegrapheditor.h> // intellisense only
#include <string.h> //strcpy
//#define NO_DYNAMIC_CAST // More portable, but makes code more intrusive
#ifndef NO_DYNAMIC_CAST
class ITestEnum {
public:
virtual int& getSelectedItem()=0;
virtual ~ITestEnum() {}
};
#endif //NO_DYNAMIC_CAST
#ifndef IM_PLACEMENT_NEW
struct ImPlacementNewDummy {};
inline void* operator new(size_t, ImPlacementNewDummy, void* ptr) { return ptr; }
inline void operator delete(void*, ImPlacementNewDummy, void*) {}
#define IM_PLACEMENT_NEW(_PTR) new(ImPlacementNewDummy(), _PTR)
#endif //IM_PLACEMENT_NEW
// MY DATA STRUCTURE ===============================================================
#define MAX_ENUM_NAME_LENGTH 84 // in bytes
typedef ImVector<char [MAX_ENUM_NAME_LENGTH]> TestEnumNamesType; // so that it works without STL (std::vector<std::string> will be easier to implement)
TestEnumNamesType TestEnumNames;
int TestEnumNamesInsert(const char* name) {
if (!name) return -1;
const int len = strlen(name);
if (len<=0 || len+1>=MAX_ENUM_NAME_LENGTH) return -1;
// We want to add the item in a sorted way. First we must calculate "itemPlacement"
int itemPlacement = 0,comp = 0;
for (int i=0,iSz=TestEnumNames.size();i<iSz;i++) {
comp = strcmp(name,&TestEnumNames[i][0]);
if (comp>0) ++itemPlacement;
else if (comp==0) return -1; // already present
}
// Here we insert "name" at "itemPlacement"
TestEnumNames.resize(TestEnumNames.size()+1);
for (int i=TestEnumNames.size()-1;i>itemPlacement;--i) strcpy(&TestEnumNames[i][0],&TestEnumNames[i-1][0]);
strcpy(&TestEnumNames[itemPlacement][0],name);
return itemPlacement;
}
bool TestEnumNamesDelete(int itemIndex) {
// We must delete the item
int size = TestEnumNames.size();
for (int i=itemIndex;i<size-1;i++) {
strcpy(&TestEnumNames[i][0],&TestEnumNames[i+1][0]);
}
--size;TestEnumNames.resize(size);
return true;
}
// NODE DEFINITIONS ================================================================
namespace ImGui {
enum TestNodeTypes {
TNT_CUSTOM_ENUM_EDITOR_NODE = 0,
TNT_CUSTOM_ENUM_USER_NODE,
TNT_COUNT
};
static const char* TestNodeTypeNames[TNT_COUNT] = {"Custom Enum Editor","Custom Enum User"};
class CustomEnumEditorNode : public Node {
protected:
typedef Node Base; //Base Class
typedef CustomEnumEditorNode ThisClass;
CustomEnumEditorNode() : Base() {}
static const int TYPE = TNT_CUSTOM_ENUM_EDITOR_NODE;
int selectedEnumIndex; // field
char buf[MAX_ENUM_NAME_LENGTH];
bool mustFocusInputText;
public:
virtual const char* getTooltip() const {return "CustomEnumEditorNode tooltip.";}
virtual const char* getInfo() const {return "CustomEnumEditorNode info.\n\nThis is supposed to display some info about this node.";}
virtual void getDefaultTitleBarColors(ImU32& defaultTitleTextColorOut,ImU32& defaultTitleBgColorOut,float& defaultTitleBgColorGradientOut) const {
// [Optional Override] customize Node Title Colors [default values: 0,0,-1.f => do not override == use default values from the Style()]
defaultTitleTextColorOut = IM_COL32(220,220,220,255);defaultTitleBgColorOut = IM_COL32(0,75,0,255);defaultTitleBgColorGradientOut = -1.f;
}
// create:
static ThisClass* Create(const ImVec2& pos) {
// 1) allocation
// MANDATORY (NodeGraphEditor::~NodeGraphEditor() will delete these with ImGui::MemFree(...))
// MANDATORY even with blank ctrs. Reason: ImVector does not call ctrs/dctrs on items.
ThisClass* node = (ThisClass*) ImGui::MemAlloc(sizeof(ThisClass));IM_PLACEMENT_NEW (node) ThisClass();
// 2) main init
node->init("CustomEnumEditorNode",pos,"","",TYPE);
// 3) init fields ( this uses the node->fields variable; otherwise we should have overridden other virtual methods (to render and serialize) )
node->fields.addFieldEnum(&node->selectedEnumIndex,&CustomEnumEditorNode::GetNumEnumItems,&CustomEnumEditorNode::GetTextFromEnumIndex,"###CustomEnumEditor",NULL,&TestEnumNames);
// Please node that in the render(...) method we assume that the dynamic enum FieldInfo is the first one (at node->fields[0]).
// 4) set (or load) field values
node->selectedEnumIndex = 0; // field value
node->buf[0]='\0'; // other (non-field) variables
node->mustFocusInputText = false;
return node;
}
protected:
virtual bool render(float nodeWidth);
virtual bool canBeCopied() const {return false;}
public:
static bool GetTextFromEnumIndex(void* data,int value,const char** pTxt) {
if (!pTxt || !data) return false;
const TestEnumNamesType& vec = *((const TestEnumNamesType*) data);
*pTxt = (value>=0 && value<vec.size()) ? vec[value] : "UNKNOWN";
return true;
}
static int GetNumEnumItems(void* data) {
if (!data) return 0;
const TestEnumNamesType& vec = *((const TestEnumNamesType*) data);
return vec.size();
}
// casts:
inline static ThisClass* Cast(Node* n) {return Node::Cast<ThisClass>(n,TYPE);}
inline static const ThisClass* Cast(const Node* n) {return Node::Cast<ThisClass>(n,TYPE);}
};
class CustomEnumUserNode : public Node
#ifndef NO_DYNAMIC_CAST
, public virtual ITestEnum
#endif //NO_DYNAMIC_CAST
{
protected:
typedef Node Base; //Base Class
typedef CustomEnumUserNode ThisClass;
CustomEnumUserNode() : Base() {}
static const int TYPE = TNT_CUSTOM_ENUM_USER_NODE;
int selectedEnumIndex; // field
virtual const char* getTooltip() const {return "ColorEnumUserNode tooltip.";}
virtual const char* getInfo() const {return "ColorEnumUserNode info.\n\nThis is supposed to display some info about this node.";}
/*virtual void getDefaultTitleBarColors(ImU32& defaultTitleTextColorOut,ImU32& defaultTitleBgColorOut,float& defaultTitleBgColorGradientOut) const {
// [Optional Override] customize Node Title Colors [default values: 0,0,-1.f => do not override == use default values from the Style()]
defaultTitleTextColorOut = IM_COL32(220,220,220,255);defaultTitleBgColorOut = IM_COL32(0,75,0,255);defaultTitleBgColorGradientOut = -1.f;
}*/
public:
int& getSelectedItem() {return selectedEnumIndex;} // ITestEnum if available, but used also if not available (to expose a private variable)
// create:
static ThisClass* Create(const ImVec2& pos) {
// 1) allocation
// MANDATORY (NodeGraphEditor::~NodeGraphEditor() will delete these with ImGui::MemFree(...))
// MANDATORY even with blank ctrs. Reason: ImVector does not call ctrs/dctrs on items.
ThisClass* node = (ThisClass*) ImGui::MemAlloc(sizeof(ThisClass));IM_PLACEMENT_NEW (node) ThisClass();
// 2) main init
node->init("ColorEnumUserNode",pos,"in_a;in_b","out_a;out_b",TYPE);
// 3) init fields ( this uses the node->fields variable; otherwise we should have overridden other virtual methods (to render and serialize) )
node->fields.addFieldEnum(&node->selectedEnumIndex,&CustomEnumEditorNode::GetNumEnumItems,&CustomEnumEditorNode::GetTextFromEnumIndex,"Selection","select your favourite",&TestEnumNames);
// 4) set (or load) field values
node->selectedEnumIndex = -1;
return node;
}
// casts:
inline static ThisClass* Cast(Node* n) {return Node::Cast<ThisClass>(n,TYPE);}
inline static const ThisClass* Cast(const Node* n) {return Node::Cast<ThisClass>(n,TYPE);}
};
bool CustomEnumEditorNode::render(float nodeWidth) // should return "true" if the node has been edited and its values modified (to fire "edited callbacks")
{
bool nodeEdited = false;
if (mustFocusInputText) ImGui::SetKeyboardFocusHere(0);
if (ImGui::InputText("New item",buf,MAX_ENUM_NAME_LENGTH,ImGuiInputTextFlags_EnterReturnsTrue /*| ImGuiInputTextFlags_AutoSelectAll*/)) {
mustFocusInputText=false;
if (strlen(buf)>0) {
const int itemIndex=TestEnumNamesInsert(buf);
if (itemIndex>=0) {
buf[0]='\0';
selectedEnumIndex = itemIndex;
nodeEdited = true;
//Now we must correct all the "selectedItem>=itemPlacement" in all the NodeGraphEditor
ImGui::NodeGraphEditor& nge = getNodeGraphEditor(); // Actually if we use more than one Node Graph Editor with the same node types, the Dynamic Enum is the same, so we should process other editors as well...
# ifndef NO_DYNAMIC_CAST
for (int i=0,iSz=nge.getNumNodes();i<iSz;i++) {
ITestEnum* n = dynamic_cast<ITestEnum*>(nge.getNode(i));
if (n) {
int& selectedIndexEnum = n->getSelectedItem();
if (selectedIndexEnum>=itemIndex) ++selectedIndexEnum; // otherwise node n selected index gets wrong
}
}
ITestEnum* copiedNode = dynamic_cast<ITestEnum*>(nge.getCopiedNode()); // This is the internal node kept for copy/paste operations: we can't forget to process it
if (copiedNode) {
int& selectedIndexEnum = copiedNode->getSelectedItem();
if (selectedIndexEnum>=itemIndex) ++selectedIndexEnum; // otherwise node n selected index gets wrong
}
# else //NO_DYNAMIC_CAST
ImVector<ImGui::Node*> nodes;
// Similiar lines must be repeated for every new Node class definition that uses the dynamic enum
nge.getAllNodesOfType(ImGui::TNT_CUSTOM_ENUM_USER_NODE,&nodes);
for (int i=0,iSz=nodes.size();i<iSz;i++) {
ImGui::CustomEnumUserNode& n = *ImGui::CustomEnumUserNode::Cast(nodes[i]);
int& selectedIndexEnum = n.getSelectedItem();
if (selectedIndexEnum>=itemIndex) ++selectedIndexEnum; // otherwise node n selected index gets wrong
}
ImGui::CustomEnumUserNode* copiedNode = ImGui::CustomEnumUserNode::Cast(nge.getCopiedNode()); // This is the internal node kept for copy/paste operations: we can't forget to process it
if (copiedNode) {
int& selectedIndexEnum = copiedNode->getSelectedItem();
if (selectedIndexEnum>=itemIndex) ++selectedIndexEnum; // otherwise node n selected index gets wrong
}
# endif //NO_DYNAMIC_CAST
mustFocusInputText=true;
}
}
}
else mustFocusInputText=false;
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s","insert a new item");
// Here we render field 0 (without marking the "nodeEdited" flag)
if (fields[0].render(nodeWidth)) mustFocusInputText=true; // We use "mustFocusInputText" to keep the InputText focused while switching enum
if (ImGui::IsItemActive()) mustFocusInputText=true; // This seems to cover the case when we switch to the same item
if (TestEnumNames.size()>0) {
ImGui::SameLine();
if (ImGui::SmallButton("x") && TestEnumNamesDelete(selectedEnumIndex)) {
nodeEdited = true;
mustFocusInputText=true;
//Now we must correct all the "selectedItem>=selectedEnumIndex" in all the NodeGraphEditor
ImGui::NodeGraphEditor& nge = getNodeGraphEditor();
# ifndef NO_DYNAMIC_CAST
for (int i=0,iSz=nge.getNumNodes();i<iSz;i++) {
ITestEnum* n = dynamic_cast<ITestEnum*>(nge.getNode(i));
if (n) {
int& selectedIndexEnum = n->getSelectedItem();
if (selectedIndexEnum==selectedEnumIndex) selectedIndexEnum=-1; // we're deleting the selected item of node n
if (selectedIndexEnum>selectedEnumIndex) --selectedIndexEnum; // otherwise node n selected index gets wrong
}
}
ITestEnum* copiedNode = dynamic_cast<ITestEnum*>(nge.getCopiedNode()); // This is the internal node kept for copy/paste operations: we can't forget to process it
if (copiedNode) {
int& selectedIndexEnum = copiedNode->getSelectedItem();
if (selectedIndexEnum==selectedEnumIndex) selectedIndexEnum=-1; // we're deleting the selected item of node n
if (selectedIndexEnum>selectedEnumIndex) --selectedIndexEnum; // otherwise node n selected index gets wrong
}
# else //NO_DYNAMIC_CAST
ImVector<ImGui::Node*> nodes;
// Similiar lines must be repeated for every new Node class definition that uses the dynamic enum
nge.getAllNodesOfType(ImGui::TNT_CUSTOM_ENUM_USER_NODE,&nodes);
for (int i=0,iSz=nodes.size();i<iSz;i++) {
ImGui::CustomEnumUserNode& n = *ImGui::CustomEnumUserNode::Cast(nodes[i]);
int& selectedIndexEnum = n.getSelectedItem();
if (selectedIndexEnum==selectedEnumIndex) selectedIndexEnum=-1; // we're deleting the selected item of node n
if (selectedIndexEnum>selectedEnumIndex) --selectedIndexEnum; // otherwise node n selected index gets wrong
}
ImGui::CustomEnumUserNode* copiedNode = ImGui::CustomEnumUserNode::Cast(nge.getCopiedNode()); // This is the internal node kept for copy/paste operations: we can't forget to process it
if (copiedNode) {
int& selectedIndexEnum = copiedNode->getSelectedItem();
if (selectedIndexEnum==selectedEnumIndex) selectedIndexEnum=-1; // we're deleting the selected item of node n
if (selectedIndexEnum>selectedEnumIndex) --selectedIndexEnum; // otherwise node n selected index gets wrong
}
# endif //NO_DYNAMIC_CAST
if (--selectedEnumIndex<0) selectedEnumIndex=0;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s","delete");
}
// Now we can draw the other fields normally (no one in this demo)
for (int i=1,isz=fields.size();i<isz;i++) {
FieldInfo& f = fields[i];
nodeEdited|=f.render(nodeWidth);
}
return nodeEdited;
}
static Node* TestNodeFactory(int nt,const ImVec2& pos,const NodeGraphEditor& /*nge*/) {
switch (nt) {
case TNT_CUSTOM_ENUM_EDITOR_NODE: return CustomEnumEditorNode::Create(pos);
case TNT_CUSTOM_ENUM_USER_NODE: return CustomEnumUserNode::Create(pos);
default:
IM_ASSERT(true); // Missing node type creation
return NULL;
}
return NULL;
}
} // namespace ImGui
// END NODE DEFINITIONS ============================================================
// Optional methods to load/save "TestEnumNames"
#if (defined(IMGUIHELPER_H_) && !defined(NO_IMGUIHELPER_SERIALIZATION))
#ifndef NO_IMGUIHELPER_SERIALIZATION_SAVE
bool TestEnumNamesSave(ImGuiHelper::Serializer& s) {
const int size = TestEnumNames.size();
s.save(&size,"num_text_line_items");
s.saveTextLines(size,ImGui::CustomEnumEditorNode::GetTextFromEnumIndex,(void*)&TestEnumNames,"text_line_items");
return true;
}
inline bool TestEnumNamesSave(const char* filename) {
ImGuiHelper::Serializer s(filename);
return TestEnumNamesSave(s);
}
#endif //NO_IMGUIHELPER_SERIALIZATION_SAVE
#ifndef NO_IMGUIHELPER_SERIALIZATION_LOAD
static bool TestEnumNamesTypeLoadCallback(ImGuiHelper::FieldType ft,int numArrayElements,void* pValue,const char* name,void* userPtr) {
TestEnumNamesType* vec = (TestEnumNamesType*) userPtr;
if (strcmp(name,"num_text_line_items")==0) {
vec->resize(*((int*)pValue));
for (int i=0;i<vec->size();i++) (*vec)[i][0]='\0';
if (vec->size()==0) return true;
}
else if (ft==ImGui::FT_TEXTLINE && strcmp(name,"text_line_items")==0) {
IM_ASSERT(numArrayElements<vec->size());
strcpy(&(*vec)[numArrayElements][0],(char*)pValue);
if (numArrayElements==vec->size()-1) return true;
}
return false;
}
bool TestEnumNamesLoad(ImGuiHelper::Deserializer& d, const char ** pOptionalBufferStart=NULL) {
const char* amount = pOptionalBufferStart ? (*pOptionalBufferStart) : 0;
d.parse(TestEnumNamesTypeLoadCallback,(void*)&TestEnumNames,amount);
if (pOptionalBufferStart) *pOptionalBufferStart=amount;
return true;
}
bool TestEnumNamesLoad(const char* filename) {
ImGuiHelper::Deserializer d(filename);
return TestEnumNamesLoad(d);
}
#endif //NO_IMGUIHELPER_SERIALIZATION_LOAD
#endif //NO_IMGUIHELPER_SERIALIZATION
const char* NodeGraphEditorSavePath = "testEnumNamesNodeGraphEditor.nge";
ImGui::NodeGraphEditor nge;
// Mandatory methods
void InitGL() {
if (nge.isInited()) {
// This adds entries to the "add node" context menu
nge.registerNodeTypes(ImGui::TestNodeTypeNames,ImGui::TNT_COUNT,ImGui::TestNodeFactory,NULL,-1); // last 2 args can be used to add only a subset of nodes (or to sort their order inside the context menu)
nge.registerNodeTypeMaxAllowedInstances(ImGui::TNT_CUSTOM_ENUM_EDITOR_NODE,1); // Here we set the max number of allowed instances of the node (1)
nge.show_style_editor = true;
nge.show_load_save_buttons = true;
// optional load the style (for all the editors: better call it in InitGL()):
//NodeGraphEditor::Style::Load(NodeGraphEditor::GetStyle(),"nodeGraphEditor.style");
//--------------------------------------------------------------------------------
// Here we load "TestEnumNames" + the saved node graph editor from a singlefile.
bool nodeGraphEditorSavePathLoaded = false;
# if (defined(IMGUIHELPER_H_) && !defined(NO_IMGUIHELPER_SERIALIZATION) && !defined(NO_IMGUIHELPER_SERIALIZATION_LOAD))
{
ImGuiHelper::Deserializer d(NodeGraphEditorSavePath);
nodeGraphEditorSavePathLoaded = d.isValid();
const char* offset = 0; // Basically offset advances at each loading step
TestEnumNamesLoad(d,&offset); // TestEnumNames
nge.load(d,&offset); // nge
}
# endif
if (!nodeGraphEditorSavePathLoaded) {
// Starting items (sorted alphabetically)
TestEnumNames.resize(3);
strcpy(&TestEnumNames[0][0],"APPLE");
strcpy(&TestEnumNames[1][0],"LEMON");
strcpy(&TestEnumNames[2][0],"ORANGE");
// Optional: starting nodes and links (load from file instead):-----------
nge.addNode(ImGui::TNT_CUSTOM_ENUM_EDITOR_NODE,ImVec2(40,50));
ImGui::Node* colorEnumUserNode1 = nge.addNode(ImGui::TNT_CUSTOM_ENUM_USER_NODE,ImVec2(40,180));
ImGui::Node* colorEnumUserNode2 = nge.addNode(ImGui::TNT_CUSTOM_ENUM_USER_NODE,ImVec2(300,180)); // optionally use e.g.: ImGui::ColorEnumUserNode::Cast(colorEnumUserNode1)->...;
ImGui::Node* colorEnumUserNode3 = nge.addNode(ImGui::TNT_CUSTOM_ENUM_USER_NODE,ImVec2(550,180));
nge.addLink(colorEnumUserNode1, 0, colorEnumUserNode2, 0);
nge.addLink(colorEnumUserNode1, 1, colorEnumUserNode2, 1);
nge.addLink(colorEnumUserNode2, 0, colorEnumUserNode3, 0);
nge.addLink(colorEnumUserNode2, 1, colorEnumUserNode3, 1);
}
}
}
void ResizeGL(int,int) {}
void DestroyGL() {
// We save "TestEnumNames" + the node graph editor together to a single file here
# if (defined(IMGUIHELPER_H_) && !defined(NO_IMGUIHELPER_SERIALIZATION) && !defined(NO_IMGUIHELPER_SERIALIZATION_SAVE))
ImGuiHelper::Serializer s(NodeGraphEditorSavePath);
TestEnumNamesSave(s); // TestEnumNames
nge.save(s); // nge
# endif
}
void DrawGL()
{
ImImpl_ClearColorBuffer(ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); // Warning: it does not clear the depth buffer
static bool open = true;
if (ImGui::Begin("Node Graph Editor", &open, ImVec2(1190,710),0.85f,ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings)) {
nge.render();
}
ImGui::End();
}
#ifndef IMGUI_USE_AUTO_BINDING_WINDOWS // IMGUI_USE_AUTO_ definitions get defined automatically (e.g. do NOT touch them!)
int main(int argc, char** argv)
#else //IMGUI_USE_AUTO_BINDING_WINDOWS
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int iCmdShow) {
#endif //IMGUI_USE_AUTO_BINDING_WINDOWS
{
ImImpl_InitParams* pInitParams = NULL;
# ifndef IMGUI_USE_AUTO_BINDING_WINDOWS
ImImpl_Main(pInitParams,argc,argv);
# else //IMGUI_USE_AUTO_BINDING_WINDOWS
ImImpl_WinMain(pInitParams,hInstance,hPrevInstance,lpCmdLine,iCmdShow);
# endif //IMGUI_USE_AUTO_BINDING_WINDOWS
return 0;
}
@Flix01
Copy link
Author

Flix01 commented Apr 15, 2017

dynamicenum

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment