diff --git a/source/MaterialXGraphEditor/Graph.cpp b/source/MaterialXGraphEditor/Graph.cpp index 74298308ef..8ddae3ef9a 100644 --- a/source/MaterialXGraphEditor/Graph.cpp +++ b/source/MaterialXGraphEditor/Graph.cpp @@ -697,10 +697,7 @@ void Graph::updateMaterials(mx::InputPtr input /* = nullptr */, mx::ValuePtr val { if (!input) { - mx::ElementPtr elem = nullptr; - { - elem = _graphDoc->getDescendant(renderablePath); - } + const mx::ElementPtr elem = _graphDoc->getDescendant(renderablePath); mx::TypedElementPtr typedElem = elem ? elem->asA() : nullptr; _renderer->updateMaterials(typedElem); } @@ -1009,6 +1006,8 @@ void Graph::showPropertyEditorValue(UiNodePtr node, mx::InputPtr input, const mx nodeInput->setValueString(temp); nodeInput->setValue(temp, nodeInput->getType()); updateMaterials(); + + _currUiNode->buildUiTokenMap(); // Re-build token map } } } @@ -1062,16 +1061,16 @@ void Graph::setUiNodeInfo(UiNodePtr node, const std::string& type, const std::st } else { - if (node->getNode()) + if (mx::ConstNodePtr mxNode = node->getNode()) { - mx::NodeDefPtr nodeDef = node->getNode()->getNodeDef(node->getNode()->getName()); + mx::NodeDefPtr nodeDef = mxNode->getNodeDef(mxNode->getName()); if (nodeDef) { for (mx::InputPtr input : nodeDef->getActiveInputs()) { - if (node->getNode()->getInput(input->getName())) + if (mxNode->getInput(input->getName())) { - input = node->getNode()->getInput(input->getName()); + input = mxNode->getInput(input->getName()); } UiPinPtr inPin = std::make_shared(_state.nextUiId, node, ax::NodeEditor::PinKind::Input, input); node->getInputPins().push_back(inPin); @@ -1081,9 +1080,9 @@ void Graph::setUiNodeInfo(UiNodePtr node, const std::string& type, const std::st for (mx::OutputPtr output : nodeDef->getActiveOutputs()) { - if (node->getNode()->getOutput(output->getName())) + if (mxNode->getOutput(output->getName())) { - output = node->getNode()->getOutput(output->getName()); + output = mxNode->getOutput(output->getName()); } UiPinPtr outPin = std::make_shared(_state.nextUiId, node, ax::NodeEditor::PinKind::Output, output); node->getOutputPins().push_back(outPin); @@ -1091,6 +1090,8 @@ void Graph::setUiNodeInfo(UiNodePtr node, const std::string& type, const std::st ++_state.nextUiId; } } + + node->buildUiTokenMap(); // Build initial token map } else if (node->getInput()) { @@ -3662,41 +3663,63 @@ void Graph::propertyEditor() showPropertyEditorOutputConnections(_currUiNode);; } - - // Find tokens within currUiNode - mx::ConstNodePtr node = _currUiNode->getNode(); - if (node != nullptr) + + // Draw token table + if (const auto& currTokenMap = _currUiNode->getUiTokenMap(); !currTokenMap.empty()) { - mx::StringResolverPtr resolver = node->createStringResolver(); - const mx::StringMap& tokens = resolver->getFilenameSubstitutions(); + ImGui::Text("Tokens"); + ImGui::SameLine(); + drawHelpMarker("All tokens that are within scope of the selected node. Token values will be string-substituted into listed 'Affected Inputs'."); + + int tokenCount = static_cast(currTokenMap.size() + 1u); // Add 1 to account for header row + ImVec2 tableHeight(0.0f, TEXT_BASE_HEIGHT * std::min(SCROLL_LINE_COUNT, tokenCount)); - if (!tokens.empty()) + // Use `ImGuiTableFlags_SizingFixedFit` to set default column width to fit content + if (ImGui::BeginTable("tokens_node_table", 4, tableFlags | ImGuiTableFlags_SizingFixedFit, tableHeight)) { - ImGui::Text("Tokens"); - - ImVec2 tableSize(0.0f, TEXT_BASE_HEIGHT * std::min(SCROLL_LINE_COUNT, static_cast(tokens.size()))); - bool haveTable = ImGui::BeginTable("tokens_node_table", 2, tableFlags, tableSize); - if (haveTable) + ImGui::SetWindowFontScale(_fontScale); + + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Value"); + ImGui::TableSetupColumn("Source Element"); + ImGui::TableSetupColumn("Affected Inputs"); + + // Set tooltips for each of table's columns + constexpr std::array tableHeadersTooltips = { "", "Press to set token value.", "The graph element where the token is declared.", "Node inputs which reference the token." }; + drawTableHeadersRowWithTooltips(tableHeadersTooltips); + + for (const auto& [tokenName, tokenPtr] : currTokenMap) { - ImGui::SetWindowFontScale(_fontScale); + ImGui::TableNextRow(); // Start new row + ImGui::PushID(&tokenName); - for (const auto& [token, value] : tokens) - { - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::PushID(&token); + // Name + ImGui::TableNextColumn(); + ImGui::Text("%s", tokenName.c_str()); - ImGui::Text("%s", token.c_str()); - ImGui::TableNextColumn(); - ImGui::Text("%s", value.c_str()); + // Value + ImGui::TableNextColumn(); + std::string tokenValue = tokenPtr->getValue(); - ImGui::PopID(); + if (ImGui::InputText("##token_value", &tokenValue, ImGuiInputTextFlags_EnterReturnsTrue)) + { + tokenPtr->setValue(tokenValue); // Write out new token value + updateMaterials(); // Trigger update of material } - - ImGui::EndTable(); - ImGui::SetWindowFontScale(1.0f); + + // Source Element + ImGui::TableNextColumn(); + ImGui::Text("%s", tokenPtr->getSourceElementString().c_str()); + + // Affected Inputs + ImGui::TableNextColumn(); + ImGui::Text("%s", tokenPtr->getAffectedInputsString().c_str()); + + ImGui::PopID(); } + + ImGui::EndTable(); + ImGui::SetWindowFontScale(1.0f); // Restore font scale } } @@ -3760,6 +3783,20 @@ void Graph::showHelp() const ImGui::BulletText("\"Node Info\" Will toggle showing node information."); } } +void Graph::drawHelpMarker(const char* content) +{ + constexpr float WRAP_POSITION = 32.f; // Compile-time definition of text-wrap position + + ImGui::TextDisabled(HELP_MARKER_TEXT); // Draw help marker + if (!ImGui::IsItemHovered()) + return; // If help marker isn't hovered return early + + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * WRAP_POSITION); + ImGui::TextUnformatted(content); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); +} void Graph::addNodePopup(bool cursor) { diff --git a/source/MaterialXGraphEditor/Graph.h b/source/MaterialXGraphEditor/Graph.h index 0bbeb652ed..e228683ca0 100644 --- a/source/MaterialXGraphEditor/Graph.h +++ b/source/MaterialXGraphEditor/Graph.h @@ -272,6 +272,35 @@ class Graph void showHelp() const; + // A compile-time constant member variable that corresponds to the function below. Defined in header as visibility is desirable here. + static constexpr char HELP_MARKER_TEXT[] = "(?)"; + // Static helper function to draw a marker via ImGui which shows a tooltip when hovered + static void drawHelpMarker(const char* content); + + // Static helper function to display tooltips for headers in an ImGui table + template static void drawTableHeadersRowWithTooltips(const std::array& tooltips) + { + const int columnCount = ImGui::TableGetColumnCount(); + if (columnCount == 0 || columnCount != N) + return; // Given array size should match number of columns in table + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int col = 0; col < columnCount; ++col) + { + if (!ImGui::TableSetColumnIndex(col)) + continue; // Do not draw if column is not visible + ImGui::TableHeader(ImGui::TableGetColumnName(col)); // Header name + + std::string colTooltip = tooltips[col]; + if (!colTooltip.empty() && ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(tooltips[col]); + ImGui::EndTooltip(); + } + } + } + private: mx::StringVec _geomFilter; mx::StringVec _mtlxFilter; @@ -357,5 +386,4 @@ class Graph // Options bool _saveNodePositions; }; - #endif diff --git a/source/MaterialXGraphEditor/UiNode.cpp b/source/MaterialXGraphEditor/UiNode.cpp index 7a76e0f0a2..5f9e65f35f 100644 --- a/source/MaterialXGraphEditor/UiNode.cpp +++ b/source/MaterialXGraphEditor/UiNode.cpp @@ -95,6 +95,65 @@ mx::NodeGraphPtr UiNode::getNodeGraph() const return _element ? _element->asA() : nullptr; } +void UiNode::buildUiTokenMap() +{ + _uiTokenMap.clear(); // Assume we want clean slate + + mx::ElementPtr currElem = getNode(); + while (currElem) + { + if (mx::ConstInterfaceElementPtr interfaceElem = currElem->asA()) + { + UiToken::applyTokenMapping(&_uiTokenMap, interfaceElem, currElem); + + // If the node is a nodegraph, check for tokens on corresponding nodedef + if (mx::ConstNodeGraphPtr nodegraph = currElem->asA()) + { + if (mx::NodeDefPtr nodedef = nodegraph->getNodeDef()) + UiToken::applyTokenMapping(&_uiTokenMap, nodedef, nodedef); + } + + // If the node is a custom node instance, check for tokens on corresponding nodedef + if (mx::NodePtr node = currElem->asA()) + { + if (mx::NodeDefPtr nodedef = node->getNodeDef()) + UiToken::applyTokenMapping(&_uiTokenMap, nodedef, nodedef); + } + } + currElem = currElem->getParent(); + } + + // Traverse through inputs and determine which tokens their value depends on + for (const auto& input : getNode()->getActiveInputs()) + { + if (input->getType() != "filename") + continue; + + mx::StringResolverPtr inputResolver = input->createStringResolver(); + const mx::StringMap& inputTokens = inputResolver->getFilenameSubstitutions(); + + mx::StringMap inputTokensRenormalized; + for (const auto& entry : inputTokens) + { + // Store tokens without excess delimiters + inputTokensRenormalized[entry.first] = entry.first.substr(1, entry.first.size() - 2); + } + + std::string inputValue = input->getValueString(); + if (inputValue.empty() && input->hasInterfaceName()) + inputValue = input->getInterfaceInput()->getValueString(); // Get value from referenced interface + + for (const auto& entry : inputTokens) + { + if (inputValue.find(entry.first) != std::string::npos) + { + // Append to affected inputs of corresponding entry in token map + _uiTokenMap[inputTokensRenormalized[entry.first]]->addAffectedInput(input); + } + } + } +} + // return the uiNode connected with input name UiNodePtr UiNode::getConnectedNode(const std::string& name) { diff --git a/source/MaterialXGraphEditor/UiNode.h b/source/MaterialXGraphEditor/UiNode.h index 00f4276aec..bcba2c7684 100644 --- a/source/MaterialXGraphEditor/UiNode.h +++ b/source/MaterialXGraphEditor/UiNode.h @@ -10,14 +10,18 @@ #include +#include + namespace mx = MaterialX; namespace ed = ax::NodeEditor; class UiNode; class UiPin; +class UiToken; using UiNodePtr = std::shared_ptr; using UiPinPtr = std::shared_ptr; +using UiTokenPtr = std::shared_ptr; // An edge between two UiNodes, storing the two nodes and connecting input. class UiEdge @@ -160,6 +164,76 @@ class UiPin bool _connected; }; +class UiToken +{ + public: + UiToken(const mx::TokenPtr& token, const mx::ElementPtr& elem) : _tokenPtr(token), _sourceElement(elem) { } + + std::string getValue() const { return _tokenPtr->getValueString(); } + void setValue(const std::string& val) const + { + _tokenPtr->setValueString(val); + } + + std::string getSourceElementString() const + { + std::string _sourceElementName = _sourceElement->getName(); + return _sourceElementName.empty() ? "" : _sourceElementName; + } + + void addAffectedInput(const mx::InputPtr& input) + { + _affectedInputs.push_back(input); + _isAffectedInputsDirty = true; + } + + std::string getAffectedInputsString() + { + if (_isAffectedInputsDirty) + buildAffectedInputsStream(); + return _affectedInputsStream.str(); + } + + const std::vector& getAffectedInputs() const { return _affectedInputs; }; + + // Handle update of given map pointer by iterating through active tokens of an interface element + static void applyTokenMapping(std::unordered_map* uiTokenMapPtr, const mx::ConstInterfaceElementPtr& interfaceElem, mx::ElementPtr sourceElem) + { + std::vector tokens = interfaceElem->getActiveTokens(); + for (auto token : tokens) + { + std::string key = token->getName(); + + // Insert into map, but do not allow parent values to override child values + uiTokenMapPtr->try_emplace(key, std::make_shared(token, sourceElem)); + } + } + + private: + const mx::TokenPtr _tokenPtr; + const mx::ElementPtr _sourceElement; + + std::vector _affectedInputs{}; + std::ostringstream _affectedInputsStream{}; + + // Track whether changes were made to inputs in order to re-build stream accordingly + bool _isAffectedInputsDirty{ true }; + + void buildAffectedInputsStream() + { + if (!_isAffectedInputsDirty) + return; + _affectedInputsStream.clear(); + for (size_t i = 0; i < _affectedInputs.size(); ++i) + { + _affectedInputsStream << _affectedInputs[i]->getName(); + if (i < _affectedInputs.size() - 1) + _affectedInputsStream << ", "; + } + _isAffectedInputsDirty = false; + } +}; + // The visual representation of a node in a graph. class UiNode { @@ -248,6 +322,11 @@ class UiNode std::vector& getOutputPins() { return _outputPins; } const std::vector& getOutputPins() const { return _outputPins; } + const std::unordered_map& getUiTokenMap() const { return _uiTokenMap; } + + // Build a map of relevant UI info for tokens in scope of this node. Should be called lazily (i.e. only when needed) + void buildUiTokenMap(); + // Edge collection accessors std::vector& getEdges() { return _edges; } const std::vector& getEdges() const { return _edges; } @@ -277,6 +356,8 @@ class UiNode std::vector _outputPins; std::vector _edges; + std::unordered_map _uiTokenMap; + bool _showAllInputs; bool _showOutputsInEditor; };