From 1e2573665a6b2e40c505ed36930d2aed05f6e4fb Mon Sep 17 00:00:00 2001 From: kwokcb Date: Sun, 16 Aug 2026 20:44:49 -0400 Subject: [PATCH 01/14] Keyboardless graph editor (start) - RMB add node - RMB on hamburger in node -> popup menu. Add only delete for now Fix: CTRL-F search without also trigger focus (F). --- source/MaterialXGraphEditor/Graph.cpp | 150 ++++++++++++++++++++------ source/MaterialXGraphEditor/Graph.h | 7 ++ source/MaterialXGraphEditor/Main.cpp | 3 + 3 files changed, 128 insertions(+), 32 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 15ebba758b..0c2e99e56c 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -145,6 +145,9 @@ Graph::Graph(const std::string& materialFilename, _layoutPending(false), _needsNavigation(false), _delete(false), + _nodeMenuToOpen(-1), + _nodeMenuNode(-1), + _nodeToDelete(-1), _fileDialogSave(FileDialog::EnterNewFilename), _popup(false), _shaderPopup(false), @@ -1995,6 +1998,40 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa ed::PinRect(iconMin, iconMax); } +void Graph::drawNodeMenu(UiNodePtr node) +{ + const float buttonSize = ImGui::GetTextLineHeight(); + const ImVec2 headerPosition = ImGui::GetCursorScreenPos(); + const ImVec2 nodeSize = ed::GetNodeSize(node->getId()); + const ImVec2 buttonPosition = headerPosition + ImVec2(nodeSize.x - buttonSize - 6.0f, -2.0f); + const ImVec2 buttonMax = buttonPosition + ImVec2(buttonSize, buttonSize); + _nodeMenuRects.emplace_back(buttonPosition.x, buttonPosition.y, buttonMax.x, buttonMax.y); + const bool hovered = ImGui::IsMouseHoveringRect(buttonPosition, buttonMax, true); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImU32 color = hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(190, 190, 190, 255); + const float lineInset = buttonSize * 0.25f; + for (int line = 0; line < 3; ++line) + { + const float y = buttonPosition.y + buttonSize * (0.3f + line * 0.2f); + drawList->AddLine(ImVec2(buttonPosition.x + lineInset, y), + ImVec2(buttonPosition.x + buttonSize - lineInset, y), color, 1.5f); + } + if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + { + _nodeMenuToOpen = node->getId(); + } +} + +bool Graph::isNodeMenuHovered() const +{ + const ImVec2 mousePosition = ImGui::GetMousePos(); + return std::any_of(_nodeMenuRects.begin(), _nodeMenuRects.end(), [mousePosition](const ImVec4& rect) + { + return mousePosition.x >= rect.x && mousePosition.x <= rect.z && + mousePosition.y >= rect.y && mousePosition.y <= rect.w; + }); +} + void Graph::buildGroupNode(UiNodePtr node) { const float commentAlpha = 0.75f; @@ -2005,6 +2042,7 @@ void Graph::buildGroupNode(UiNodePtr node) ed::BeginNode(node->getId()); ImGui::PushID(node->getId()); + drawNodeMenu(node); std::string temp = node->getMessage(); ImVec2 messageSize = ImGui::CalcTextSize(temp.c_str()); @@ -2222,6 +2260,7 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(55, 55, 55, 255)), 0.f); + drawNodeMenu(node); ImGui::Indent(hdrTextIndent); ImGui::Text("%s", node->getName().c_str()); ImGui::Unindent(hdrTextIndent); @@ -2291,6 +2330,7 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3.f), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(85, 85, 85, 255)), 0.f); + drawNodeMenu(node); ImGui::Indent(hdrTextIndent); ImGui::Text("%s", node->getName().c_str()); ImGui::Unindent(hdrTextIndent); @@ -2365,6 +2405,7 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(35, 35, 35, 255)), 0); + drawNodeMenu(node); ImGui::Indent(hdrTextIndent); ImGui::Text("%s", node->getName().c_str()); ImGui::Unindent(hdrTextIndent); @@ -2444,6 +2485,7 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(35, 35, 35, 255)), 0); + drawNodeMenu(node); ImGui::Indent(hdrTextIndent); ImGui::Text("%s", node->getName().c_str()); ImGui::Unindent(hdrTextIndent); @@ -3007,6 +3049,33 @@ void Graph::deleteNode(UiNodePtr node) _state.nodes.erase(_state.nodes.begin() + nodeNum); } +bool Graph::deleteNodeById(ed::NodeId nodeId) +{ + if (int(nodeId.Get()) <= 0) + { + return false; + } + + const int nodePosition = findNode(int(nodeId.Get())); + if (nodePosition >= 0 && !readOnly()) + { + _renderer->setMaterialCompilation(true); + _frameCount = ImGui::GetFrameCount(); + deleteNode(_state.nodes[nodePosition]); + _delete = true; + ed::DeselectNode(nodeId); + ed::DeleteNode(nodeId); + _currUiNode = nullptr; + return true; + } + + if (readOnly()) + { + _popup = true; + } + return false; +} + void Graph::addNodeGraphPins() { for (UiNodePtr node : _state.nodes) @@ -3948,7 +4017,8 @@ void Graph::showHelp() const if (ImGui::TreeNode("Navigation")) { ImGui::BulletText("F : Frame selected nodes in graph."); - ImGui::BulletText("RIGHT MOUSE button to pan."); + ImGui::BulletText("LEFT MOUSE button to drag nodes; MIDDLE MOUSE button to pan."); + ImGui::BulletText("RIGHT MOUSE button to add a node; right-click a node hamburger for its menu."); ImGui::BulletText("SCROLL WHEEL to zoom."); ImGui::BulletText("\"<\" BUTTON to view parent of current graph"); ImGui::TreePop(); @@ -4000,10 +4070,17 @@ void Graph::drawHelpMarker(const char* content) void Graph::addNodePopup(bool cursor) { ImGuiIO& io = ImGui::GetIO(); - bool open_AddPopup = (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - !io.WantTextInput && - ImGui::IsKeyReleased(ImGuiKey_Tab)) || - (_pinFilterType != mx::EMPTY_STRING && ImGui::IsMouseReleased(0)); + // If the hamburger was right-clicked this frame, its menu will be opened below; + // suppress the generic Add Node popup so the two never conflict. + const bool rmbOnHamburger = _nodeMenuToOpen > 0 || isNodeMenuHovered(); + bool open_AddPopup = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + !io.WantTextInput && + (ImGui::IsKeyReleased(ImGuiKey_Tab) || + (ImGui::IsMouseReleased(1) && !rmbOnHamburger)); + // Link-drag to add a node uses the left mouse button (drag button). Release on the + // background with a pending pin filter to offer adding a new node of that type. + open_AddPopup = open_AddPopup || + (_pinFilterType != mx::EMPTY_STRING && ImGui::IsMouseReleased(0)); static char input[32]{ "" }; if (open_AddPopup) { @@ -4137,7 +4214,6 @@ void Graph::searchNodePopup(bool cursor) } if (ImGui::BeginPopup("search")) { - ed::NavigateToSelection(); static ImGuiTextFilter filter; ImGui::Text("Search for Node:"); static char input[16]{ "" }; @@ -4365,14 +4441,10 @@ void Graph::drawGraph(ImVec2 mousePos) { ed::Suspend(); - // Set up popups for adding a node when tab is pressed - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); - ImGui::SetNextWindowSizeConstraints(ImVec2(250.0f, 300.0f), ImVec2(-1.0f, 500.0f)); - addNodePopup(TextCursor); searchNodePopup(TextCursor); addPinPopup(); readOnlyPopup(); - ImGui::PopStyleVar(); + _nodeMenuRects.clear(); ed::Resume(); @@ -4482,6 +4554,36 @@ void Graph::drawGraph(ImVec2 mousePos) // Set y-position of first node std::vector outputNum = createNodes(_state.isCompoundNodeGraph); + // Open the node menu after node rendering so the popup does not affect node layout. + ed::Suspend(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); + ImGui::SetNextWindowSizeConstraints(ImVec2(250.0f, 300.0f), ImVec2(-1.0f, 500.0f)); + addNodePopup(TextCursor); + ImGui::PopStyleVar(); + if (_nodeMenuToOpen > 0) + { + _nodeMenuNode = _nodeMenuToOpen; + _nodeMenuToOpen = -1; + ImGui::OpenPopup("node menu"); + } + if (ImGui::BeginPopup("node menu")) + { + if (ImGui::MenuItem("Delete")) + { + _nodeToDelete = _nodeMenuNode; + _nodeMenuNode = -1; + } + ImGui::EndPopup(); + } + ed::Resume(); + + if (_nodeToDelete > 0) + { + deleteNodeById(ed::NodeId(_nodeToDelete)); + linkGraph(); + _nodeToDelete = -1; + } + // Address copy information if applicable and relink graph if a new node has been added if (_addNewNode) { @@ -4610,27 +4712,9 @@ void Graph::drawGraph(ImVec2 mousePos) { if (selectedNodes.size() > 0) { - _frameCount = ImGui::GetFrameCount(); - _renderer->setMaterialCompilation(true); for (ed::NodeId id : selectedNodes) { - - if (int(id.Get()) > 0) - { - int pos = findNode(int(id.Get())); - if (pos >= 0 && !readOnly()) - { - deleteNode(_state.nodes[pos]); - _delete = true; - ed::DeselectNode(id); - ed::DeleteNode(id); - _currUiNode = nullptr; - } - else if (readOnly()) - { - _popup = true; - } - } + deleteNodeById(id); } linkGraph(); } @@ -4658,8 +4742,10 @@ void Graph::drawGraph(ImVec2 mousePos) _isCut = false; } - // Hotkey to frame selected node(s) - else if (ImGui::IsKeyReleased(ImGuiKey_F) && !_fileDialogSave.isOpened()) + // Hotkey to frame selected node(s). Never fire while the search popup is + // open or while Ctrl is held (Ctrl+F is reserved for search). + else if (!io2.KeyCtrl && !ImGui::IsPopupOpen("search") && + ImGui::IsKeyReleased(ImGuiKey_F) && !_fileDialogSave.isOpened()) { ed::NavigateToSelection(); } diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 3c4ff81dc7..a119d449c1 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -188,6 +188,8 @@ class Graph // Based on the pin icon function in the ImGui Node Editor blueprints-example.cpp void drawPinIcon(const std::string& type, bool connected, int alpha, float xOffset = 0.0f, bool offsetInY = false); + void drawNodeMenu(UiNodePtr node); + bool isNodeMenuHovered() const; UiPinPtr getPin(ed::PinId id); void drawInputPin(UiPinPtr pin); @@ -217,6 +219,7 @@ class Graph void addNode(const std::string& category, const std::string& name, const std::string& type); void deleteNode(UiNodePtr node); + bool deleteNodeById(ed::NodeId nodeId); // Build the initial graph of a loaded document including shader, material and nodegraph node void setUiNodeInfo(UiNodePtr node, const std::string& type, const std::string& category); @@ -375,6 +378,9 @@ class Graph bool _layoutPending; bool _needsNavigation; bool _delete; + int _nodeMenuToOpen; + int _nodeMenuNode; + int _nodeToDelete; // file dialog information FileDialog _fileDialog; @@ -415,6 +421,7 @@ class Graph // Options bool _saveNodePositions; + std::vector _nodeMenuRects; // Diagnostic entries collected by linkGraph() for invalid connections. std::vector _diagnostics; diff --git a/source/MaterialXGraphEditor/Main.cpp b/source/MaterialXGraphEditor/Main.cpp index 167f620f29..f489819027 100644 --- a/source/MaterialXGraphEditor/Main.cpp +++ b/source/MaterialXGraphEditor/Main.cpp @@ -266,6 +266,9 @@ int main(int argc, char* const argv[]) // Create editor config and context. ed::Config config; config.SettingsFile = nullptr; + config.DragButtonIndex = 0; + config.NavigateButtonIndex = 2; + config.ContextMenuButtonIndex = 1; ed::EditorContext* editorContext = ed::CreateEditor(&config); const float ZOOM_LEVELS[] = { 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f }; for (auto& level : ZOOM_LEVELS) From 0bfd2022017eeedd7823185d177d63e1b30b5e52 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Sun, 16 Aug 2026 21:12:27 -0400 Subject: [PATCH 02/14] Cleanup hamburger drawing Add "rename" to menu. --- source/MaterialXGraphEditor/Graph.cpp | 97 +++++++++++++++++++++------ source/MaterialXGraphEditor/Graph.h | 2 + 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 0c2e99e56c..d0c721e338 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2001,20 +2001,22 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa void Graph::drawNodeMenu(UiNodePtr node) { const float buttonSize = ImGui::GetTextLineHeight(); - const ImVec2 headerPosition = ImGui::GetCursorScreenPos(); - const ImVec2 nodeSize = ed::GetNodeSize(node->getId()); - const ImVec2 buttonPosition = headerPosition + ImVec2(nodeSize.x - buttonSize - 6.0f, -2.0f); - const ImVec2 buttonMax = buttonPosition + ImVec2(buttonSize, buttonSize); - _nodeMenuRects.emplace_back(buttonPosition.x, buttonPosition.y, buttonMax.x, buttonMax.y); - const bool hovered = ImGui::IsMouseHoveringRect(buttonPosition, buttonMax, true); + // Place the icon inline to the right of the title as a real layout item, so the node + // expands to fit it and the title text is never clipped or overlapped. + ImGui::SameLine(); + ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); + const ImVec2 bbMin = ImGui::GetItemRectMin(); + const ImVec2 bbMax = ImGui::GetItemRectMax(); + _nodeMenuRects.emplace_back(bbMin.x, bbMin.y, bbMax.x, bbMax.y); + const bool hovered = ImGui::IsMouseHoveringRect(bbMin, bbMax, true); ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImU32 color = hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(190, 190, 190, 255); - const float lineInset = buttonSize * 0.25f; + const float barWidth = buttonSize * 0.5f; + const float barStart = bbMin.x + (buttonSize - barWidth) * 0.5f; for (int line = 0; line < 3; ++line) { - const float y = buttonPosition.y + buttonSize * (0.3f + line * 0.2f); - drawList->AddLine(ImVec2(buttonPosition.x + lineInset, y), - ImVec2(buttonPosition.x + buttonSize - lineInset, y), color, 1.5f); + const float y = bbMin.y + buttonSize * (0.3f + line * 0.2f); + drawList->AddLine(ImVec2(barStart, y), ImVec2(barStart + barWidth, y), color, 1.5f); } if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { @@ -2042,13 +2044,13 @@ void Graph::buildGroupNode(UiNodePtr node) ed::BeginNode(node->getId()); ImGui::PushID(node->getId()); - drawNodeMenu(node); std::string temp = node->getMessage(); ImVec2 messageSize = ImGui::CalcTextSize(temp.c_str()); ImGui::PushItemWidth(messageSize.x + 15); ImGui::InputText("##edit", &temp); node->setMessage(temp); + drawNodeMenu(node); ImGui::PopItemWidth(); ed::Group(ImVec2(300, 200)); ImGui::PopID(); @@ -2260,9 +2262,9 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(55, 55, 55, 255)), 0.f); - drawNodeMenu(node); ImGui::Indent(hdrTextIndent); - ImGui::Text("%s", node->getName().c_str()); + ImGui::TextUnformatted(node->getName().c_str()); + drawNodeMenu(node); ImGui::Unindent(hdrTextIndent); ImGui::Dummy(ImVec2(0, hdrBottomSpacing)); @@ -2330,9 +2332,9 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3.f), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(85, 85, 85, 255)), 0.f); - drawNodeMenu(node); ImGui::Indent(hdrTextIndent); - ImGui::Text("%s", node->getName().c_str()); + ImGui::TextUnformatted(node->getName().c_str()); + drawNodeMenu(node); ImGui::Unindent(hdrTextIndent); ImGui::Dummy(ImVec2(0, hdrBottomSpacing)); @@ -2405,9 +2407,9 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(35, 35, 35, 255)), 0); - drawNodeMenu(node); ImGui::Indent(hdrTextIndent); - ImGui::Text("%s", node->getName().c_str()); + ImGui::TextUnformatted(node->getName().c_str()); + drawNodeMenu(node); ImGui::Unindent(hdrTextIndent); ImGui::Dummy(ImVec2(0, hdrBottomSpacing)); @@ -2485,9 +2487,9 @@ std::vector Graph::createNodes(bool nodegraph) ImGui::GetCursorScreenPos() + ImVec2(-hdrPadL, 3), ImGui::GetCursorScreenPos() + ImVec2(ed::GetNodeSize(node->getId()).x - hdrPadL - 2.f * hdrInset, ImGui::GetTextLineHeight() + hdrPadB), ImColor(ImColor(35, 35, 35, 255)), 0); - drawNodeMenu(node); ImGui::Indent(hdrTextIndent); - ImGui::Text("%s", node->getName().c_str()); + ImGui::TextUnformatted(node->getName().c_str()); + drawNodeMenu(node); ImGui::Unindent(hdrTextIndent); ImGui::Dummy(ImVec2(0, hdrBottomSpacing)); for (UiPinPtr pin : node->getInputPins()) @@ -3076,6 +3078,51 @@ bool Graph::deleteNodeById(ed::NodeId nodeId) return false; } +void Graph::renameNode(UiNodePtr node, const std::string& newName) +{ + if (!node || newName.empty() || node->getName() == newName) + { + return; + } + + std::string validName = newName; + if (node->getNode()) + { + validName = node->getNode()->getParent()->createValidChildName(newName); + node->getNode()->setName(validName); + // Keep downstream references pointing at this node (mirrors the property editor). + for (UiNodePtr uiNode : node->getOutputConnections()) + { + if (!uiNode->getInput() && uiNode->getNode()) + { + for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) + { + if (input->getConnectedNode() == node->getNode()) + { + uiNode->getNode()->setConnectedNode(input->getName(), node->getNode()); + } + } + } + } + } + else if (node->getInput()) + { + validName = node->getInput()->getParent()->createValidChildName(newName); + node->getInput()->setName(validName); + } + else if (node->getOutput()) + { + validName = node->getOutput()->getParent()->createValidChildName(newName); + node->getOutput()->setName(validName); + } + else if (node->getNodeGraph()) + { + validName = node->getNodeGraph()->getParent()->createValidChildName(newName); + node->getNodeGraph()->setName(validName); + } + node->setName(validName); +} + void Graph::addNodeGraphPins() { for (UiNodePtr node : _state.nodes) @@ -4564,10 +4611,22 @@ void Graph::drawGraph(ImVec2 mousePos) { _nodeMenuNode = _nodeMenuToOpen; _nodeMenuToOpen = -1; + const int renamePos = findNode(_nodeMenuNode); + _nodeMenuRename = (renamePos >= 0) ? _state.nodes[renamePos]->getName() : std::string(); ImGui::OpenPopup("node menu"); } if (ImGui::BeginPopup("node menu")) { + const int renamePos = findNode(_nodeMenuNode); + if (renamePos >= 0) + { + ImGui::Text("Rename"); + if (ImGui::InputText("##node_rename", &_nodeMenuRename, ImGuiInputTextFlags_EnterReturnsTrue)) + { + renameNode(_state.nodes[renamePos], _nodeMenuRename); + } + ImGui::Separator(); + } if (ImGui::MenuItem("Delete")) { _nodeToDelete = _nodeMenuNode; diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index a119d449c1..5911d3ad17 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -220,6 +220,7 @@ class Graph void deleteNode(UiNodePtr node); bool deleteNodeById(ed::NodeId nodeId); + void renameNode(UiNodePtr node, const std::string& newName); // Build the initial graph of a loaded document including shader, material and nodegraph node void setUiNodeInfo(UiNodePtr node, const std::string& type, const std::string& category); @@ -381,6 +382,7 @@ class Graph int _nodeMenuToOpen; int _nodeMenuNode; int _nodeToDelete; + std::string _nodeMenuRename; // file dialog information FileDialog _fileDialog; From fda60c725e315f1217b4cce75fbb4fa033d088b9 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Wed, 19 Aug 2026 13:21:04 -0400 Subject: [PATCH 03/14] - Fix rename logic which actually does not work in the property editor. - Bypass imgui node editor F key invocation which is always triggered regardless of modifiers. This is the main source of the bug. --- source/MaterialXGraphEditor/Graph.cpp | 149 +++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 12 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index d0c721e338..cd07afc33a 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -3085,40 +3085,156 @@ void Graph::renameNode(UiNodePtr node, const std::string& newName) return; } + // Renaming modifies the graph, so mark the material for recompilation and + // trigger a render refresh (mirrors deleteNodeById). + _renderer->setMaterialCompilation(true); + _frameCount = ImGui::GetFrameCount(); + std::string validName = newName; if (node->getNode()) { - validName = node->getNode()->getParent()->createValidChildName(newName); - node->getNode()->setName(validName); - // Keep downstream references pointing at this node (mirrors the property editor). + mx::NodePtr mxNode = node->getNode(); + validName = mxNode->getParent()->createValidChildName(newName); + + // Collect downstream references while the node still resolves by its old + // name (name-based resolution breaks as soon as the element is renamed). + std::vector> downstreamInputs; + std::vector downstreamOutputs; for (UiNodePtr uiNode : node->getOutputConnections()) { - if (!uiNode->getInput() && uiNode->getNode()) + if (!uiNode->getInput()) { - for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) + if (uiNode->getNode()) { - if (input->getConnectedNode() == node->getNode()) + for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) { - uiNode->getNode()->setConnectedNode(input->getName(), node->getNode()); + if (input->getConnectedNode() == mxNode) + { + downstreamInputs.emplace_back(uiNode->getNode(), input->getName()); + } + } + } + else if (uiNode->getOutput()) + { + // A graph output port directly connected to this node. + if (uiNode->getOutput()->getConnectedNode() == mxNode) + { + downstreamOutputs.push_back(uiNode->getOutput()); } } } } + + // Now rename the node element. + mxNode->setName(validName); + + // Re-point downstream references at the renamed node. + for (const auto& entry : downstreamInputs) + { + entry.first->setConnectedNode(entry.second, mxNode); + } + for (mx::OutputPtr output : downstreamOutputs) + { + output->setConnectedNode(mxNode); + } } else if (node->getInput()) { - validName = node->getInput()->getParent()->createValidChildName(newName); - node->getInput()->setName(validName); + mx::InputPtr uiNodeInput = node->getInput(); + validName = uiNodeInput->getParent()->createValidChildName(newName); + + // Keep downstream references pointing at this input (mirrors the property editor). + for (UiNodePtr uiNode : node->getOutputConnections()) + { + // Is not an input port + if (uiNode->getInput() == nullptr) + { + // Is a node + if (uiNode->getNode()) + { + for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) + { + if (input->getInterfaceInput() == uiNodeInput) + { + uiNodeInput->setName(validName); + input->setConnectedInterfaceName(validName); + } + } + } + // Is a node graph + else if (uiNode->getNodeGraph()) + { + for (mx::InputPtr input : uiNode->getNodeGraph()->getActiveInputs()) + { + if (input->getInterfaceName() == uiNodeInput->getName()) + { + uiNodeInput->setName(validName); + input->setConnectedInterfaceName(validName); + } + } + } + else + { + // Is an output port + mx::OutputPtr outputPtr = uiNode->getOutput(); + if (outputPtr) + { + outputPtr->setConnectedNode(node->getNode()); + } + } + } + } + uiNodeInput->setName(validName); } else if (node->getOutput()) { validName = node->getOutput()->getParent()->createValidChildName(newName); + const std::string oldOutputName = node->getOutput()->getName(); + const std::string nodeGraphName = node->getOutput()->getParent()->getName(); node->getOutput()->setName(validName); + + // Keep downstream references pointing at this output. External inputs that use + // this nodegraph's output are stored via "nodegraph" + "output" attributes, so + // update any that reference the old output name (mirrors link setup). + mx::DocumentPtr document = node->getOutput()->getDocument(); + if (document) + { + for (mx::ElementPtr elem : document->traverseTree()) + { + mx::InputPtr input = elem->asA(); + if (input && + input->getNodeGraphString() == nodeGraphName && + input->getOutputString() == oldOutputName) + { + input->setOutputString(validName); + } + } + } } else if (node->getNodeGraph()) { validName = node->getNodeGraph()->getParent()->createValidChildName(newName); node->getNodeGraph()->setName(validName); + // Update the UI name up-front so downstream lookups by name match (mirrors the property editor). + node->setName(validName); + + // Keep downstream references pointing at this nodegraph (mirrors the property editor). + for (UiNodePtr uiNode : _state.nodes) + { + if (!uiNode->getInput()) + { + std::vector inputs = uiNode->getInputPins(); + for (size_t i = 0; i < inputs.size(); i++) + { + const std::string& inputName = inputs[i]->getName(); + UiNodePtr inputNode = uiNode->getConnectedNode(inputName); + if (inputNode && inputNode->getName() == validName && uiNode->getNode()) + { + uiNode->getNode()->getInput(inputName)->setAttribute("nodegraph", validName); + } + } + } + } } node->setName(validName); } @@ -4250,9 +4366,10 @@ void Graph::addNodePopup(bool cursor) void Graph::searchNodePopup(bool cursor) { const ImGuiIO& io = ImGui::GetIO(); + const bool searchModifier = io.KeyCtrl || io.KeySuper; const bool open_search = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !io.WantTextInput && - io.KeyCtrl && + searchModifier && ImGui::IsKeyReleased(ImGuiKey_F); if (open_search) { @@ -4802,8 +4919,8 @@ void Graph::drawGraph(ImVec2 mousePos) } // Hotkey to frame selected node(s). Never fire while the search popup is - // open or while Ctrl is held (Ctrl+F is reserved for search). - else if (!io2.KeyCtrl && !ImGui::IsPopupOpen("search") && + // open or while a search modifier (Ctrl / Cmd) is held (Ctrl/Cmd+F is search). + else if (!io2.KeyCtrl && !io2.KeySuper && !ImGui::IsPopupOpen("search") && ImGui::IsKeyReleased(ImGuiKey_F) && !_fileDialogSave.isOpened()) { ed::NavigateToSelection(); @@ -4978,7 +5095,15 @@ void Graph::drawGraph(ImVec2 mousePos) ed::Resume(); } + // Suppress the node editor's built-in F shortcut when Ctrl/Cmd+F is used for node + // search. The editor processes its shortcuts inside ed::End(), so disable them for + // this frame and restore them immediately after. + if ((io2.KeyCtrl || io2.KeySuper) && ImGui::IsKeyPressed(ImGuiKey_F)) + { + ed::EnableShortcuts(false); + } ed::End(); + ed::EnableShortcuts(true); // Diagnostic panel — drawn below the node editor inside the same right-pane container. if (!_diagnostics.empty()) From eff17f3b9dfa75701896e5f912c90992af96c0fb Mon Sep 17 00:00:00 2001 From: kwokcb Date: Wed, 19 Aug 2026 13:36:11 -0400 Subject: [PATCH 04/14] Take hamburger width into account for output pin placement. --- source/MaterialXGraphEditor/Graph.cpp | 24 +++++++++++++++++++++--- source/MaterialXGraphEditor/Graph.h | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index cd07afc33a..9b5e0ff1c9 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2000,7 +2000,7 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa void Graph::drawNodeMenu(UiNodePtr node) { - const float buttonSize = ImGui::GetTextLineHeight(); + const float buttonSize = computeHamburgerSize(); // Place the icon inline to the right of the title as a real layout item, so the node // expands to fit it and the title text is never clipped or overlapped. ImGui::SameLine(); @@ -2102,6 +2102,11 @@ float Graph::computeIconSize() return std::max(MIN_PIN_ICON_SIZE, BASE_PIN_ICON_SIZE * getUiScaleFromFont()); } +float Graph::computeHamburgerSize() +{ + return ImGui::GetTextLineHeight(); +} + float Graph::computePinOffset(bool righAligned) { @@ -2125,8 +2130,21 @@ void Graph::drawOutputPins(UiNodePtr node, const std::string& longestInputLabel) if (w > maxLabelWidth) maxLabelWidth = w; } - // Content width = max label width (paddings are handled by the editor) - const float contentWidth = maxLabelWidth; + // Content width = max label width (paddings are handled by the editor). + // When pins sit on the node's border, the output row must span the node's full + // content width so the pin lands on the right edge. The title row includes the + // hamburger button (drawn inline after the node name) which can be wider than + // the output labels, so match the title row width here. + float contentWidth = maxLabelWidth; + if (_pinsOnBorder) + { + const float titleTextWidth = ImGui::CalcTextSize(node->getName().c_str()).x; + const float titleRowWidth = titleTextWidth + ImGui::GetStyle().ItemSpacing.x + computeHamburgerSize(); + if (titleRowWidth > contentWidth) + { + contentWidth = titleRowWidth; + } + } // Offset the icon so its center lands on the node's right edge // if pin on border option is enabled. diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 5911d3ad17..47b53ed4dd 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -185,6 +185,8 @@ class Graph static float computeIconSize(); // Compute pin offset based on icon size and aligment (left vs right) static float computePinOffset(bool rightAligned = false); + // Size of the node hamburger menu button drawn inline after the title. + static float computeHamburgerSize(); // Based on the pin icon function in the ImGui Node Editor blueprints-example.cpp void drawPinIcon(const std::string& type, bool connected, int alpha, float xOffset = 0.0f, bool offsetInY = false); From 5cba4ea5b13a02097223cefd2c7d5a2077423ca8 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Wed, 19 Aug 2026 14:06:55 -0400 Subject: [PATCH 05/14] - Consolidate renameNode() to new corrected logic in renameNode(). - Disable hamburger on read-only nodes and disallow rename / delete on any read-only graphs. --- source/MaterialXGraphEditor/Graph.cpp | 139 +++++--------------------- 1 file changed, 23 insertions(+), 116 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 9b5e0ff1c9..5cdc7c4be7 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2018,7 +2018,7 @@ void Graph::drawNodeMenu(UiNodePtr node) const float y = bbMin.y + buttonSize * (0.3f + line * 0.2f); drawList->AddLine(ImVec2(barStart, y), ImVec2(barStart + barWidth, y), color, 1.5f); } - if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right) && !readOnly()) { _nodeMenuToOpen = node->getId(); } @@ -3103,6 +3103,12 @@ void Graph::renameNode(UiNodePtr node, const std::string& newName) return; } + // Read-only graphs cannot be modified. + if (readOnly()) + { + return; + } + // Renaming modifies the graph, so mark the material for recompilation and // trigger a render refresh (mirrors deleteNodeById). _renderer->setMaterialCompilation(true); @@ -3254,6 +3260,12 @@ void Graph::renameNode(UiNodePtr node, const std::string& newName) } } } + else if (node->getCategory() == "group") + { + // Group nodes have no backing element; just update the UI name, keeping the + // raw name (mirrors the property editor's group handling). + validName = newName; + } node->setName(validName); } @@ -3747,130 +3759,25 @@ void Graph::propertyEditor() // Set and edit name ImGui::Text("Name: "); ImGui::SameLine(); + const bool readOnlyGraph = readOnly(); std::string original = _currUiNode->getName(); std::string temp = original; float availableWidth = ImGui::GetContentRegionAvail().x; ImGui::PushItemWidth(availableWidth); - ImGui::InputText("##edit", &temp); + // Commit the rename on Enter or when the field loses focus, not on every keystroke. + // The name is not editable in read-only graphs. + const bool nameSubmitted = ImGui::InputText("##edit", &temp, + ImGuiInputTextFlags_EnterReturnsTrue | (readOnlyGraph ? ImGuiInputTextFlags_ReadOnly : 0)); + const bool nameEdited = ImGui::IsItemDeactivatedAfterEdit(); ImGui::PopItemWidth(); std::string docString = "NodeDef Doc String: \n"; - if (_currUiNode->getNode()) - { - if (temp != original) - { - std::string name = _currUiNode->getNode()->getParent()->createValidChildName(temp); - std::vector downstreamNodes = _currUiNode->getOutputConnections(); - for (UiNodePtr uiNode : downstreamNodes) - { - if (!uiNode->getInput() && uiNode->getNode()) - { - for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) - { - if (input->getConnectedNode() == _currUiNode->getNode()) - { - _currUiNode->getNode()->setName(name); - uiNode->getNode()->setConnectedNode(input->getName(), _currUiNode->getNode()); - } - } - } - } - _currUiNode->setName(name); - _currUiNode->getNode()->setName(name); - } - } - else if (_currUiNode->getInput()) + // Rename through the shared renameNode() path so the node menu and the + // property editor behave identically. + if (!readOnlyGraph && (nameSubmitted || nameEdited) && temp != original) { - mx::InputPtr currUINodeInput = _currUiNode->getInput(); - - if (temp != original) - { - std::string name = currUINodeInput->getParent()->createValidChildName(temp); - - for (UiNodePtr uiNode : _currUiNode->getOutputConnections()) - { - // Is not an input port - if (uiNode->getInput() == nullptr) - { - // Is a node - if (uiNode->getNode()) - { - for (mx::InputPtr input : uiNode->getNode()->getActiveInputs()) - { - if (input->getInterfaceInput() == currUINodeInput) - { - currUINodeInput->setName(name); - input->setConnectedInterfaceName(name); - } - } - } - // Is a node graph - else if (uiNode->getNodeGraph()) - { - for (mx::InputPtr input : uiNode->getNodeGraph()->getActiveInputs()) - { - if (input->getInterfaceName() == currUINodeInput->getName()) - { - currUINodeInput->setName(name); - input->setConnectedInterfaceName(name); - } - } - } - else - { - // Is an output port - mx::OutputPtr outputPtr = uiNode->getOutput(); - if (outputPtr) - { - outputPtr->setConnectedNode(_currUiNode->getNode()); - } - } - } - } - - currUINodeInput->setName(name); - _currUiNode->setName(name); - } - } - else if (_currUiNode->getOutput()) - { - if (temp != original) - { - std::string name = _currUiNode->getOutput()->getParent()->createValidChildName(temp); - _currUiNode->getOutput()->setName(name); - _currUiNode->setName(name); - } - } - else if (_currUiNode->getCategory() == "group") - { - _currUiNode->setName(temp); - } - else if (_currUiNode->getCategory() == "nodegraph") - { - if (temp != original) - { - std::string name = _currUiNode->getNodeGraph()->getParent()->createValidChildName(temp); - _currUiNode->getNodeGraph()->setName(name); - _currUiNode->setName(name); - - for (UiNodePtr node : _state.nodes) - { - if (!node->getInput()) - { - std::vector inputs = node->getInputPins(); - for (size_t i = 0; i < inputs.size(); i++) - { - const std::string& inputName = inputs[i]->getName(); - UiNodePtr inputNode = node->getConnectedNode(inputName); - if (inputNode && inputNode->getName() == name && node->getNode()) - { - node->getNode()->getInput(inputName)->setAttribute("nodegraph", name); - } - } - } - } - } + renameNode(_currUiNode, temp); } const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing() * 1.3f; From ea7ea5831d5f1d9a5d570618c47423d6aba4289a Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 20 Aug 2026 13:59:32 -0400 Subject: [PATCH 06/14] Fix hamburger placement to be right aligned. - Extract out node width computation utility for us bye hamburger as well as previous pin placement. --- source/MaterialXGraphEditor/Graph.cpp | 75 +++++++++++++++++++-------- source/MaterialXGraphEditor/Graph.h | 7 +++ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 5cdc7c4be7..f46a5f1cad 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2001,9 +2001,14 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa void Graph::drawNodeMenu(UiNodePtr node) { const float buttonSize = computeHamburgerSize(); - // Place the icon inline to the right of the title as a real layout item, so the node - // expands to fit it and the title text is never clipped or overlapped. - ImGui::SameLine(); + // Offset the button to the right edge of the node. The node content width is + // computed from the title, inputs and outputs (the same width used to place the + // output pins on the right border), so the button's right edge aligns with the + // node's right edge rather than sitting directly after the title text. + const float nodeContentWidth = computeNodeWidth(node, computeLongestInputLabel(node)); + const float titleWidth = ImGui::CalcTextSize(node->getName().c_str()).x; + const float gap = (nodeContentWidth > 0.0f) ? (nodeContentWidth - titleWidth - buttonSize) : 0.0f; + ImGui::SameLine(0.0f, std::max(gap, 0.0f)); ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); const ImVec2 bbMin = ImGui::GetItemRectMin(); const ImVec2 bbMax = ImGui::GetItemRectMax(); @@ -2107,6 +2112,42 @@ float Graph::computeHamburgerSize() return ImGui::GetTextLineHeight(); } +float Graph::computeNodeWidth(UiNodePtr node, const std::string& longestInputLabel) +{ + // Widest of the title, the shown input labels and the output labels. This is the + // single source of truth for the node's content width, used to offset the + // hamburger to the right edge and to indent output labels onto the right border. + float width = ImGui::CalcTextSize(longestInputLabel.c_str()).x; + for (UiPinPtr pin : node->getOutputPins()) + { + width = std::max(width, ImGui::CalcTextSize(pin->getName().c_str()).x); + } + width = std::max(width, ImGui::CalcTextSize(node->getName().c_str()).x); + return width; +} + +std::string Graph::computeLongestInputLabel(UiNodePtr node) +{ + std::string longest = node->getName(); + for (UiPinPtr pin : node->getInputPins()) + { + bool shown = true; + if (node->getNode()) + { + shown = node->getShowAllInputs() || (pin->getConnected() || node->getNode()->getInput(pin->getName())); + } + else if (node->getNodeGraph()) + { + shown = node->getShowAllInputs() || (pin->getConnected() || node->getNodeGraph()->getInput(pin->getName())); + } + if (shown && pin->getName().size() > longest.size()) + { + longest = pin->getName(); + } + } + return longest; +} + float Graph::computePinOffset(bool righAligned) { @@ -2122,28 +2163,18 @@ float Graph::computePinOffset(bool righAligned) void Graph::drawOutputPins(UiNodePtr node, const std::string& longestInputLabel) { - // 1. Find the widest label among input and output pins. - float maxLabelWidth = ImGui::CalcTextSize(longestInputLabel.c_str()).x; + // Width the output label row spans. When pins are drawn inside the node, the row + // only needs to span the widest pin label so the icons line up in a column. When + // pins sit on the node's border, the row must span the node's full content width + // (the widest of the title, inputs and outputs) so the pin lands on the right edge. + float contentWidth = ImGui::CalcTextSize(longestInputLabel.c_str()).x; for (UiPinPtr pin : node->getOutputPins()) { - float w = ImGui::CalcTextSize(pin->getName().c_str()).x; - if (w > maxLabelWidth) maxLabelWidth = w; + contentWidth = std::max(contentWidth, ImGui::CalcTextSize(pin->getName().c_str()).x); } - - // Content width = max label width (paddings are handled by the editor). - // When pins sit on the node's border, the output row must span the node's full - // content width so the pin lands on the right edge. The title row includes the - // hamburger button (drawn inline after the node name) which can be wider than - // the output labels, so match the title row width here. - float contentWidth = maxLabelWidth; if (_pinsOnBorder) { - const float titleTextWidth = ImGui::CalcTextSize(node->getName().c_str()).x; - const float titleRowWidth = titleTextWidth + ImGui::GetStyle().ItemSpacing.x + computeHamburgerSize(); - if (titleRowWidth > contentWidth) - { - contentWidth = titleRowWidth; - } + contentWidth = std::max(contentWidth, computeNodeWidth(node, longestInputLabel)); } // Offset the icon so its center lands on the node's right edge @@ -2383,7 +2414,7 @@ std::vector Graph::createNodes(bool nodegraph) pin->setConnected(true); } { - const float pinOffset = computePinOffset(); + const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0f; ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { @@ -2459,7 +2490,7 @@ std::vector Graph::createNodes(bool nodegraph) } { - const float pinOffset = computePinOffset(); + const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0f; ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 47b53ed4dd..050a940770 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -187,6 +187,13 @@ class Graph static float computePinOffset(bool rightAligned = false); // Size of the node hamburger menu button drawn inline after the title. static float computeHamburgerSize(); + // Width of the node's content: the widest of the title, the shown input labels + // and the output labels. This is the single source of truth used both to offset + // the hamburger menu to the right edge of the node and to indent output labels + // so their pins land on the right border. + static float computeNodeWidth(UiNodePtr node, const std::string& longestInputLabel); + // Longest label among the input pins that will be shown on the node. + static std::string computeLongestInputLabel(UiNodePtr node); // Based on the pin icon function in the ImGui Node Editor blueprints-example.cpp void drawPinIcon(const std::string& type, bool connected, int alpha, float xOffset = 0.0f, bool offsetInY = false); From 21b81c0b345576a71c26aad846e585c5a566afcf Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 20 Aug 2026 15:17:38 -0400 Subject: [PATCH 07/14] Fix up to handle unexpected imgui side-effects for layouts which messed up pin layouts. --- source/MaterialXGraphEditor/Graph.cpp | 75 ++++++++------------------- source/MaterialXGraphEditor/Graph.h | 6 +-- 2 files changed, 24 insertions(+), 57 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index f46a5f1cad..5cdc7c4be7 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2001,14 +2001,9 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa void Graph::drawNodeMenu(UiNodePtr node) { const float buttonSize = computeHamburgerSize(); - // Offset the button to the right edge of the node. The node content width is - // computed from the title, inputs and outputs (the same width used to place the - // output pins on the right border), so the button's right edge aligns with the - // node's right edge rather than sitting directly after the title text. - const float nodeContentWidth = computeNodeWidth(node, computeLongestInputLabel(node)); - const float titleWidth = ImGui::CalcTextSize(node->getName().c_str()).x; - const float gap = (nodeContentWidth > 0.0f) ? (nodeContentWidth - titleWidth - buttonSize) : 0.0f; - ImGui::SameLine(0.0f, std::max(gap, 0.0f)); + // Place the icon inline to the right of the title as a real layout item, so the node + // expands to fit it and the title text is never clipped or overlapped. + ImGui::SameLine(); ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); const ImVec2 bbMin = ImGui::GetItemRectMin(); const ImVec2 bbMax = ImGui::GetItemRectMax(); @@ -2112,42 +2107,6 @@ float Graph::computeHamburgerSize() return ImGui::GetTextLineHeight(); } -float Graph::computeNodeWidth(UiNodePtr node, const std::string& longestInputLabel) -{ - // Widest of the title, the shown input labels and the output labels. This is the - // single source of truth for the node's content width, used to offset the - // hamburger to the right edge and to indent output labels onto the right border. - float width = ImGui::CalcTextSize(longestInputLabel.c_str()).x; - for (UiPinPtr pin : node->getOutputPins()) - { - width = std::max(width, ImGui::CalcTextSize(pin->getName().c_str()).x); - } - width = std::max(width, ImGui::CalcTextSize(node->getName().c_str()).x); - return width; -} - -std::string Graph::computeLongestInputLabel(UiNodePtr node) -{ - std::string longest = node->getName(); - for (UiPinPtr pin : node->getInputPins()) - { - bool shown = true; - if (node->getNode()) - { - shown = node->getShowAllInputs() || (pin->getConnected() || node->getNode()->getInput(pin->getName())); - } - else if (node->getNodeGraph()) - { - shown = node->getShowAllInputs() || (pin->getConnected() || node->getNodeGraph()->getInput(pin->getName())); - } - if (shown && pin->getName().size() > longest.size()) - { - longest = pin->getName(); - } - } - return longest; -} - float Graph::computePinOffset(bool righAligned) { @@ -2163,18 +2122,28 @@ float Graph::computePinOffset(bool righAligned) void Graph::drawOutputPins(UiNodePtr node, const std::string& longestInputLabel) { - // Width the output label row spans. When pins are drawn inside the node, the row - // only needs to span the widest pin label so the icons line up in a column. When - // pins sit on the node's border, the row must span the node's full content width - // (the widest of the title, inputs and outputs) so the pin lands on the right edge. - float contentWidth = ImGui::CalcTextSize(longestInputLabel.c_str()).x; + // 1. Find the widest label among input and output pins. + float maxLabelWidth = ImGui::CalcTextSize(longestInputLabel.c_str()).x; for (UiPinPtr pin : node->getOutputPins()) { - contentWidth = std::max(contentWidth, ImGui::CalcTextSize(pin->getName().c_str()).x); + float w = ImGui::CalcTextSize(pin->getName().c_str()).x; + if (w > maxLabelWidth) maxLabelWidth = w; } + + // Content width = max label width (paddings are handled by the editor). + // When pins sit on the node's border, the output row must span the node's full + // content width so the pin lands on the right edge. The title row includes the + // hamburger button (drawn inline after the node name) which can be wider than + // the output labels, so match the title row width here. + float contentWidth = maxLabelWidth; if (_pinsOnBorder) { - contentWidth = std::max(contentWidth, computeNodeWidth(node, longestInputLabel)); + const float titleTextWidth = ImGui::CalcTextSize(node->getName().c_str()).x; + const float titleRowWidth = titleTextWidth + ImGui::GetStyle().ItemSpacing.x + computeHamburgerSize(); + if (titleRowWidth > contentWidth) + { + contentWidth = titleRowWidth; + } } // Offset the icon so its center lands on the node's right edge @@ -2414,7 +2383,7 @@ std::vector Graph::createNodes(bool nodegraph) pin->setConnected(true); } { - const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0f; + const float pinOffset = computePinOffset(); ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { @@ -2490,7 +2459,7 @@ std::vector Graph::createNodes(bool nodegraph) } { - const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0f; + const float pinOffset = computePinOffset(); ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 050a940770..da7821ceac 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -187,10 +187,8 @@ class Graph static float computePinOffset(bool rightAligned = false); // Size of the node hamburger menu button drawn inline after the title. static float computeHamburgerSize(); - // Width of the node's content: the widest of the title, the shown input labels - // and the output labels. This is the single source of truth used both to offset - // the hamburger menu to the right edge of the node and to indent output labels - // so their pins land on the right border. + // Deterministic node content width (widest of title, shown inputs and outputs) used + // to place the hamburger at the node's right edge without feeding back into layout. static float computeNodeWidth(UiNodePtr node, const std::string& longestInputLabel); // Longest label among the input pins that will be shown on the node. static std::string computeLongestInputLabel(UiNodePtr node); From 7d7e9fd7bf4c1673f3e8ddc1e81431f6e4c7eb02 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 20 Aug 2026 22:37:03 -0400 Subject: [PATCH 08/14] Make hamburger have fixed alignment. Disable RMB add popup on readonly graphs --- source/MaterialXGraphEditor/Graph.cpp | 87 +++++++++++++++++++++++---- source/MaterialXGraphEditor/Graph.h | 8 ++- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 5cdc7c4be7..3fa4f16583 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -27,6 +27,7 @@ const int FILTER_ALPHA = 50; const float BASE_UI_FONT_SIZE = 18.0f; const float BASE_PIN_ICON_SIZE = 18.0f; const float MIN_PIN_ICON_SIZE = 18.0f; +const float HDR_TEXT_INDENT = 4.0f; const std::array NODE_GROUP_ORDER = { "texture2d", @@ -2001,9 +2002,22 @@ void Graph::drawPinIcon(const std::string& type, bool connected, int alpha, floa void Graph::drawNodeMenu(UiNodePtr node) { const float buttonSize = computeHamburgerSize(); - // Place the icon inline to the right of the title as a real layout item, so the node - // expands to fit it and the title text is never clipped or overlapped. - ImGui::SameLine(); + + if (node->getCategory() == "group") + { + // Groups draw their title in a hint above the body, so keep the button inline + // next to the message text. + ImGui::SameLine(); + } + else + { + // Right-align the button to the node's content right edge. + const float contentWidth = computeNodeContentWidth(node); + const float titleWidth = ImGui::CalcTextSize(node->getName().c_str()).x; + const float gap = std::max(0.0f, contentWidth - HDR_TEXT_INDENT - titleWidth - buttonSize); + ImGui::SameLine(0.0f, gap); + } + ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); const ImVec2 bbMin = ImGui::GetItemRectMin(); const ImVec2 bbMax = ImGui::GetItemRectMax(); @@ -2107,6 +2121,55 @@ float Graph::computeHamburgerSize() return ImGui::GetTextLineHeight(); } +float Graph::computeNodeContentWidth(UiNodePtr node) +{ + // Compute the node's content width (widest of the title row, shown + // input rows and output rows). + const float iconSize = computeIconSize(); + const float inputSpacing = 5.0f * getUiScaleFromFont(); + const float iconOffset = std::max(0.0f, iconSize * 0.5f - ed::GetStyle().NodePadding.x); + + // Title row: indent + title text + item spacing + hamburger button. + float width = HDR_TEXT_INDENT + + ImGui::CalcTextSize(node->getName().c_str()).x + + ImGui::GetStyle().ItemSpacing.x + + computeHamburgerSize(); + + // Widest shown input row: (icon layout offset) + input spacing + label. + width = std::max(width, iconOffset + inputSpacing + + ImGui::CalcTextSize(computeLongestInputLabel(node).c_str()).x); + + // Widest output row: label text only (icons contribute no layout width). + for (UiPinPtr pin : node->getOutputPins()) + { + width = std::max(width, ImGui::CalcTextSize(pin->getName().c_str()).x); + } + + return width; +} + +std::string Graph::computeLongestInputLabel(UiNodePtr node) +{ + std::string longest = node->getName(); + for (UiPinPtr pin : node->getInputPins()) + { + bool shown = true; + if (node->getNode()) + { + shown = node->getShowAllInputs() || (pin->getConnected() || node->getNode()->getInput(pin->getName())); + } + else if (node->getNodeGraph()) + { + shown = node->getShowAllInputs() || (pin->getConnected() || node->getNodeGraph()->getInput(pin->getName())); + } + if (shown && pin->getName().size() > longest.size()) + { + longest = pin->getName(); + } + } + return longest; +} + float Graph::computePinOffset(bool righAligned) { @@ -2138,12 +2201,7 @@ void Graph::drawOutputPins(UiNodePtr node, const std::string& longestInputLabel) float contentWidth = maxLabelWidth; if (_pinsOnBorder) { - const float titleTextWidth = ImGui::CalcTextSize(node->getName().c_str()).x; - const float titleRowWidth = titleTextWidth + ImGui::GetStyle().ItemSpacing.x + computeHamburgerSize(); - if (titleRowWidth > contentWidth) - { - contentWidth = titleRowWidth; - } + contentWidth = computeNodeContentWidth(node); } // Offset the icon so its center lands on the node's right edge @@ -2216,7 +2274,7 @@ std::vector Graph::createNodes(bool nodegraph) const float hdrPadL = nodeEditorStyle.NodePadding.x - hdrInset; const float hdrPadT = nodeEditorStyle.NodePadding.y - hdrInset; const float hdrPadB = hdrPadT; - const float hdrTextIndent = 4.0f; + const float hdrTextIndent = HDR_TEXT_INDENT; const float hdrBottomSpacing = hdrPadB; const float hdrRounding = std::max(nodeEditorStyle.NodeRounding - hdrInset, 0.0f); @@ -2383,7 +2441,7 @@ std::vector Graph::createNodes(bool nodegraph) pin->setConnected(true); } { - const float pinOffset = computePinOffset(); + const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0; ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { @@ -2459,7 +2517,7 @@ std::vector Graph::createNodes(bool nodegraph) } { - const float pinOffset = computePinOffset(); + const float pinOffset = _pinsOnBorder ? computePinOffset() : 0.0; ed::BeginPin(pin->getPinId(), ed::PinKind::Input); if (!_pinFilterType.empty()) { @@ -4157,6 +4215,11 @@ void Graph::drawHelpMarker(const char* content) void Graph::addNodePopup(bool cursor) { + if (readOnly()) + { + return; + } + ImGuiIO& io = ImGui::GetIO(); // If the hamburger was right-clicked this frame, its menu will be opened below; // suppress the generic Add Node popup so the two never conflict. diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index da7821ceac..03f5c84a99 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -187,9 +187,11 @@ class Graph static float computePinOffset(bool rightAligned = false); // Size of the node hamburger menu button drawn inline after the title. static float computeHamburgerSize(); - // Deterministic node content width (widest of title, shown inputs and outputs) used - // to place the hamburger at the node's right edge without feeding back into layout. - static float computeNodeWidth(UiNodePtr node, const std::string& longestInputLabel); + // Deterministic node content width (widest of the title row, shown input rows and + // output rows). Computed purely from the node's data and fixed layout constants - it + // never reads back the node's rendered size, so right-aligning the hamburger cannot + // feed back into the layout and make the node grow. + static float computeNodeContentWidth(UiNodePtr node); // Longest label among the input pins that will be shown on the node. static std::string computeLongestInputLabel(UiNodePtr node); From d3980d9493a56e35210f6a3f92e0bbdc5a02192c Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 20 Aug 2026 23:00:46 -0400 Subject: [PATCH 09/14] Comment cleanup. --- source/MaterialXGraphEditor/Graph.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 03f5c84a99..d85b5e217c 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -187,10 +187,7 @@ class Graph static float computePinOffset(bool rightAligned = false); // Size of the node hamburger menu button drawn inline after the title. static float computeHamburgerSize(); - // Deterministic node content width (widest of the title row, shown input rows and - // output rows). Computed purely from the node's data and fixed layout constants - it - // never reads back the node's rendered size, so right-aligning the hamburger cannot - // feed back into the layout and make the node grow. + // Determine node content width static float computeNodeContentWidth(UiNodePtr node); // Longest label among the input pins that will be shown on the node. static std::string computeLongestInputLabel(UiNodePtr node); From 6befba4d49e5617a5871b071814d65af8d9a3581 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Sun, 13 Sep 2026 10:40:14 -0400 Subject: [PATCH 10/14] Fix double UI scale on Retina (Mac). --- source/MaterialXGraphEditor/Main.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/source/MaterialXGraphEditor/Main.cpp b/source/MaterialXGraphEditor/Main.cpp index f489819027..6f15f909b8 100644 --- a/source/MaterialXGraphEditor/Main.cpp +++ b/source/MaterialXGraphEditor/Main.cpp @@ -211,11 +211,18 @@ int main(int argc, char* const argv[]) io.LogFilename = NULL; // Derive a single effective UI scale from device DPI and optional CLI multiplier. + float deviceScale = 1.0f; +#if !defined(__APPLE__) + // On macOS (Retina) the framebuffer tracks the content scale, and ImGui already + // converts logical units to pixels using io.DisplayFramebufferScale, so scaling + // the UI by the content scale as well doubles the scale. On Windows and X11 + // the framebuffer is unscaled, so the content scale is the only way to get the DPI. float xscale = 1.0f, yscale = 1.0f; glfwGetWindowContentScale(window, &xscale, &yscale); - const float deviceScale = (xscale > yscale) ? xscale : yscale; - const float effectiveUiScale = deviceScale * ((uiScale > 0.0f) ? uiScale : 1.0f); - const float scaledFontSize = std::max(1.0f, fontSize * effectiveUiScale); + deviceScale = (xscale > yscale) ? xscale : yscale; +#endif +const float effectiveUiScale = deviceScale * ((uiScale > 0.0f) ? uiScale : 1.0f); +const float scaledFontSize = std::max(1.0f, fontSize * effectiveUiScale); ImFont* customFont = nullptr; if (!fontFilename.empty()) From 294acd51ca8c25e23137b90ea68e8e2cdba66705 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Wed, 16 Sep 2026 13:23:07 -0400 Subject: [PATCH 11/14] Revert "Fix double UI scale on Retina (Mac)." This reverts commit 6befba4d49e5617a5871b071814d65af8d9a3581. --- source/MaterialXGraphEditor/Main.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/source/MaterialXGraphEditor/Main.cpp b/source/MaterialXGraphEditor/Main.cpp index 6f15f909b8..f489819027 100644 --- a/source/MaterialXGraphEditor/Main.cpp +++ b/source/MaterialXGraphEditor/Main.cpp @@ -211,18 +211,11 @@ int main(int argc, char* const argv[]) io.LogFilename = NULL; // Derive a single effective UI scale from device DPI and optional CLI multiplier. - float deviceScale = 1.0f; -#if !defined(__APPLE__) - // On macOS (Retina) the framebuffer tracks the content scale, and ImGui already - // converts logical units to pixels using io.DisplayFramebufferScale, so scaling - // the UI by the content scale as well doubles the scale. On Windows and X11 - // the framebuffer is unscaled, so the content scale is the only way to get the DPI. float xscale = 1.0f, yscale = 1.0f; glfwGetWindowContentScale(window, &xscale, &yscale); - deviceScale = (xscale > yscale) ? xscale : yscale; -#endif -const float effectiveUiScale = deviceScale * ((uiScale > 0.0f) ? uiScale : 1.0f); -const float scaledFontSize = std::max(1.0f, fontSize * effectiveUiScale); + const float deviceScale = (xscale > yscale) ? xscale : yscale; + const float effectiveUiScale = deviceScale * ((uiScale > 0.0f) ? uiScale : 1.0f); + const float scaledFontSize = std::max(1.0f, fontSize * effectiveUiScale); ImFont* customFont = nullptr; if (!fontFilename.empty()) From b2de360be1f4f260c8b3967de400b86460d079c9 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Wed, 16 Sep 2026 21:30:05 -0400 Subject: [PATCH 12/14] Restore previous mouse mappings and make hamburger menu trigger on LMB. --- source/MaterialXGraphEditor/Graph.cpp | 40 ++++++++++++--------------- source/MaterialXGraphEditor/Graph.h | 2 -- source/MaterialXGraphEditor/Main.cpp | 3 -- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 3fa4f16583..171e0c4a26 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -2018,13 +2018,15 @@ void Graph::drawNodeMenu(UiNodePtr node) ImGui::SameLine(0.0f, gap); } - ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); + // Give "hamburger" button priority if pressed. + // While up, ignores other mouse interaction like node draggin, or "add node". + const bool pressed = ImGui::InvisibleButton("##node_menu", ImVec2(buttonSize, buttonSize)); const ImVec2 bbMin = ImGui::GetItemRectMin(); - const ImVec2 bbMax = ImGui::GetItemRectMax(); - _nodeMenuRects.emplace_back(bbMin.x, bbMin.y, bbMax.x, bbMax.y); - const bool hovered = ImGui::IsMouseHoveringRect(bbMin, bbMax, true); + const bool hovered = ImGui::IsItemHovered(); ImDrawList* drawList = ImGui::GetWindowDrawList(); - const ImU32 color = hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(190, 190, 190, 255); + // Highlight on hover. + const ImVec4 hoverColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); + const ImU32 color = hovered ? IM_COL32(160, 192, 255, 255) : IM_COL32(190, 190, 190, 255); const float barWidth = buttonSize * 0.5f; const float barStart = bbMin.x + (buttonSize - barWidth) * 0.5f; for (int line = 0; line < 3; ++line) @@ -2032,22 +2034,12 @@ void Graph::drawNodeMenu(UiNodePtr node) const float y = bbMin.y + buttonSize * (0.3f + line * 0.2f); drawList->AddLine(ImVec2(barStart, y), ImVec2(barStart + barWidth, y), color, 1.5f); } - if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right) && !readOnly()) + if (pressed && !readOnly()) { _nodeMenuToOpen = node->getId(); } } -bool Graph::isNodeMenuHovered() const -{ - const ImVec2 mousePosition = ImGui::GetMousePos(); - return std::any_of(_nodeMenuRects.begin(), _nodeMenuRects.end(), [mousePosition](const ImVec4& rect) - { - return mousePosition.x >= rect.x && mousePosition.x <= rect.z && - mousePosition.y >= rect.y && mousePosition.y <= rect.w; - }); -} - void Graph::buildGroupNode(UiNodePtr node) { const float commentAlpha = 0.75f; @@ -4221,17 +4213,22 @@ void Graph::addNodePopup(bool cursor) } ImGuiIO& io = ImGui::GetIO(); - // If the hamburger was right-clicked this frame, its menu will be opened below; - // suppress the generic Add Node popup so the two never conflict. - const bool rmbOnHamburger = _nodeMenuToOpen > 0 || isNodeMenuHovered(); bool open_AddPopup = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !io.WantTextInput && - (ImGui::IsKeyReleased(ImGuiKey_Tab) || - (ImGui::IsMouseReleased(1) && !rmbOnHamburger)); + ImGui::IsKeyReleased(ImGuiKey_Tab); // Link-drag to add a node uses the left mouse button (drag button). Release on the // background with a pending pin filter to offer adding a new node of that type. open_AddPopup = open_AddPopup || (_pinFilterType != mx::EMPTY_STRING && ImGui::IsMouseReleased(0)); + + // Clicking a node or the canvas with the context menu button (RMB) + // brings up "Add Node". Ignores click+drag. + // While up menu prevents other menus (like hamburger menu) from coming up). + ed::NodeId contextNodeId; + const bool contextMenuOnNode = ed::ShowNodeContextMenu(&contextNodeId); + const bool contextMenuOnBackground = ed::ShowBackgroundContextMenu(); + open_AddPopup = open_AddPopup || contextMenuOnNode || contextMenuOnBackground; + static char input[32]{ "" }; if (open_AddPopup) { @@ -4596,7 +4593,6 @@ void Graph::drawGraph(ImVec2 mousePos) searchNodePopup(TextCursor); addPinPopup(); readOnlyPopup(); - _nodeMenuRects.clear(); ed::Resume(); diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index d85b5e217c..fe987c38cf 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -195,7 +195,6 @@ class Graph // Based on the pin icon function in the ImGui Node Editor blueprints-example.cpp void drawPinIcon(const std::string& type, bool connected, int alpha, float xOffset = 0.0f, bool offsetInY = false); void drawNodeMenu(UiNodePtr node); - bool isNodeMenuHovered() const; UiPinPtr getPin(ed::PinId id); void drawInputPin(UiPinPtr pin); @@ -429,7 +428,6 @@ class Graph // Options bool _saveNodePositions; - std::vector _nodeMenuRects; // Diagnostic entries collected by linkGraph() for invalid connections. std::vector _diagnostics; diff --git a/source/MaterialXGraphEditor/Main.cpp b/source/MaterialXGraphEditor/Main.cpp index f489819027..167f620f29 100644 --- a/source/MaterialXGraphEditor/Main.cpp +++ b/source/MaterialXGraphEditor/Main.cpp @@ -266,9 +266,6 @@ int main(int argc, char* const argv[]) // Create editor config and context. ed::Config config; config.SettingsFile = nullptr; - config.DragButtonIndex = 0; - config.NavigateButtonIndex = 2; - config.ContextMenuButtonIndex = 1; ed::EditorContext* editorContext = ed::CreateEditor(&config); const float ZOOM_LEVELS[] = { 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f }; for (auto& level : ZOOM_LEVELS) From 0d26c91f7bccc35219af16af721491e4fb1ad32e Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 17 Sep 2026 11:51:57 -0400 Subject: [PATCH 13/14] Update graph editor embedded help and developer guide. --- documents/DeveloperGuide/GraphEditor.md | 11 +++++++++++ source/MaterialXGraphEditor/Graph.cpp | 6 ++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/documents/DeveloperGuide/GraphEditor.md b/documents/DeveloperGuide/GraphEditor.md index 43b7f03a58..8ecb5c5c93 100644 --- a/documents/DeveloperGuide/GraphEditor.md +++ b/documents/DeveloperGuide/GraphEditor.md @@ -41,6 +41,17 @@ Another type of node present in the `Add Node` pop-up is the group, or backgroun To search the editor window for a specific node use `CTRL` + `F` to bring up the search bar. +A menu of actions that can be performed on a node can accessed by clicking with the `LEFT` mouse button on the "hamburger" icon +( ) located at the upper right corner of the node. Current actions include renaming and deleting the node. + +### Mouse Actions + +- `LEFT MOUSE button` : Drag to position nodes. +- `RIGHT MOUSE button` : Click to bring up "Add Node" menu. +- `RIGHT MOUSE button` : Drag to pan. +- `SCROLL WHEEL` : To zoom. +- `LEFT MOUSE button` : Click on node's icon to bring up action menu. + ## Node Property Editor When a node is selected in the graph, its information is displayed on the left-hand column in the `Node Property Editor`. This editor displays the name of the node, its category, its inputs, the input name, types and values. Inputs that are connected to other nodes will not display a value. diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 171e0c4a26..760988b102 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -4155,14 +4155,16 @@ void Graph::showHelp() const if (ImGui::TreeNode("Navigation")) { ImGui::BulletText("F : Frame selected nodes in graph."); - ImGui::BulletText("LEFT MOUSE button to drag nodes; MIDDLE MOUSE button to pan."); - ImGui::BulletText("RIGHT MOUSE button to add a node; right-click a node hamburger for its menu."); + ImGui::BulletText("LEFT MOUSE button to drag nodes.."); + ImGui::BulletText("RIGHT MOUSE button drag to pan"); ImGui::BulletText("SCROLL WHEEL to zoom."); ImGui::BulletText("\"<\" BUTTON to view parent of current graph"); ImGui::TreePop(); } if (ImGui::TreeNode("Editing")) { + ImGui::BulletText("LEFT MOUSE button click on node hamburger icon for node menu."); + ImGui::BulletText("RIGHT MOUSE button click to add a node."); ImGui::BulletText("TAB : Show popup menu to add new nodes."); ImGui::BulletText("CTRL-C : Copy selected nodes to clipboard."); ImGui::BulletText("CTRL-V : Paste clipboard to graph."); From 9637243617d43453986dbfdf63836d9109a9b5d2 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Thu, 17 Sep 2026 14:16:40 -0400 Subject: [PATCH 14/14] Fix up padding so previous + node menu popups are consistent. --- source/MaterialXGraphEditor/Graph.cpp | 29 +++++++++++++++++++++------ source/MaterialXGraphEditor/Graph.h | 1 + 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 760988b102..14f4421582 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -29,6 +29,17 @@ const float BASE_PIN_ICON_SIZE = 18.0f; const float MIN_PIN_ICON_SIZE = 18.0f; const float HDR_TEXT_INDENT = 4.0f; +// Padding applied to every editor popup so its contents are not flush to the border. +// Owned by the popup-drawing functions so call sites cannot change it by reordering. +const ImVec2 POPUP_WINDOW_PADDING(8.0f, 8.0f); + +// Size bounds for the "Add Node" popup window and for its group submenus. +// A negative maximum is treated as unbounded by ImGui::CalcNextWindowSizeConstraints. +const ImVec2 ADD_NODE_POPUP_MIN_SIZE(250.0f, 300.0f); +const ImVec2 ADD_NODE_POPUP_MAX_SIZE(-1.0f, 500.0f); +const ImVec2 ADD_NODE_SUBMENU_MIN_SIZE(100.0f, 10.0f); +const ImVec2 ADD_NODE_SUBMENU_MAX_SIZE(-1.0f, 300.0f); + const std::array NODE_GROUP_ORDER = { "texture2d", "texture3d", @@ -2025,7 +2036,6 @@ void Graph::drawNodeMenu(UiNodePtr node) const bool hovered = ImGui::IsItemHovered(); ImDrawList* drawList = ImGui::GetWindowDrawList(); // Highlight on hover. - const ImVec4 hoverColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); const ImU32 color = hovered ? IM_COL32(160, 192, 255, 255) : IM_COL32(190, 190, 190, 255); const float barWidth = buttonSize * 0.5f; const float barStart = bbMin.x + (buttonSize - barWidth) * 0.5f; @@ -4238,6 +4248,8 @@ void Graph::addNodePopup(bool cursor) ImGui::OpenPopup("add node"); _menuFilterType = _pinFilterType; } + ImGui::SetNextWindowSizeConstraints(ADD_NODE_POPUP_MIN_SIZE, ADD_NODE_POPUP_MAX_SIZE); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, POPUP_WINDOW_PADDING); if (ImGui::BeginPopup("add node")) { ImGui::Text("Add Node"); @@ -4282,7 +4294,6 @@ void Graph::addNodePopup(bool cursor) // Filter out list of nodes if (subs.size() > 0) { - ImGui::SetNextWindowSizeConstraints(ImVec2(250.0f, 300.0f), ImVec2(-1.0f, 500.0f)); std::string str(node.getName()); std::string nodeName = node.getName(); @@ -4313,7 +4324,7 @@ void Graph::addNodePopup(bool cursor) } else { - ImGui::SetNextWindowSizeConstraints(ImVec2(100, 10), ImVec2(-1, 300)); + ImGui::SetNextWindowSizeConstraints(ADD_NODE_SUBMENU_MIN_SIZE, ADD_NODE_SUBMENU_MAX_SIZE); if (ImGui::BeginMenu(node.getGroup().c_str())) { std::string name = node.getName(); @@ -4348,6 +4359,7 @@ void Graph::addNodePopup(bool cursor) ImGui::EndPopup(); open_AddPopup = false; } + ImGui::PopStyleVar(); } void Graph::searchNodePopup(bool cursor) @@ -4363,6 +4375,7 @@ void Graph::searchNodePopup(bool cursor) cursor = true; ImGui::OpenPopup("search"); } + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, POPUP_WINDOW_PADDING); if (ImGui::BeginPopup("search")) { static ImGuiTextFilter filter; @@ -4392,6 +4405,7 @@ void Graph::searchNodePopup(bool cursor) } ImGui::EndPopup(); } + ImGui::PopStyleVar(); } bool Graph::isPinHovered() @@ -4424,9 +4438,11 @@ void Graph::addPinPopup() value = "\nValue: " + pin->getInput()->getValueString(); } const std::string message("Name: " + pin->getName() + "\nType: " + pin->getType() + value + connected); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, POPUP_WINDOW_PADDING); ImGui::BeginTooltip(); ImGui::TextUnformatted(message.c_str()); ImGui::EndTooltip(); + ImGui::PopStyleVar(); ed::Resume(); } } @@ -4439,11 +4455,13 @@ void Graph::readOnlyPopup() ImGui::OpenPopup("Read Only"); _popup = false; } + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, POPUP_WINDOW_PADDING); if (ImGui::BeginPopup("Read Only")) { ImGui::Text("This graph is Read Only"); ImGui::EndPopup(); } + ImGui::PopStyleVar(); } void Graph::shaderPopup() @@ -4706,10 +4724,7 @@ void Graph::drawGraph(ImVec2 mousePos) // Open the node menu after node rendering so the popup does not affect node layout. ed::Suspend(); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); - ImGui::SetNextWindowSizeConstraints(ImVec2(250.0f, 300.0f), ImVec2(-1.0f, 500.0f)); addNodePopup(TextCursor); - ImGui::PopStyleVar(); if (_nodeMenuToOpen > 0) { _nodeMenuNode = _nodeMenuToOpen; @@ -4718,6 +4733,7 @@ void Graph::drawGraph(ImVec2 mousePos) _nodeMenuRename = (renamePos >= 0) ? _state.nodes[renamePos]->getName() : std::string(); ImGui::OpenPopup("node menu"); } + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, POPUP_WINDOW_PADDING); if (ImGui::BeginPopup("node menu")) { const int renamePos = findNode(_nodeMenuNode); @@ -4737,6 +4753,7 @@ void Graph::drawGraph(ImVec2 mousePos) } ImGui::EndPopup(); } + ImGui::PopStyleVar(); ed::Resume(); if (_nodeToDelete > 0) diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index fe987c38cf..993f80d2e9 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -287,6 +287,7 @@ class Graph void addPinPopup(); bool readOnly(); void readOnlyPopup(); + void nodeMenuPopup(); // Compiling shaders message void shaderPopup();