/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2022 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include //#define Debugging_ImGui_TreeNode_Indices namespace { const ImVec2 Size = ImVec2(350, 500); constexpr openspace::properties::Property::PropertyInfo UseTreeInfo = { "TreeLayout", "Use Tree Layout", "If this value is checked, this component will display the properties using a " "tree layout, rather than using a flat map. This value should only be set on " "property windows that display SceneGraphNodes, or the application might crash." }; constexpr openspace::properties::Property::PropertyInfo OrderingInfo = { "Ordering", "Tree Ordering", "This list determines the order of the first tree layer if it is used. Elements " "present in this list will be shown first, with an alphabetical ordering for " "elements not listed." }; constexpr openspace::properties::Property::PropertyInfo IgnoreHiddenInfo = { "IgnoreHidden", "Ignore Hidden Hint", "If this value is 'true', all 'Hidden' hints passed into the SceneGraphNodes are " "ignored and thus all SceneGraphNodes are displayed. If this value is 'false', " "the hidden hints are followed." }; int nVisibleProperties(const std::vector& props, openspace::properties::Property::Visibility visibility) { return static_cast(std::count_if( props.begin(), props.end(), [visibility](openspace::properties::Property* p) { using V = openspace::properties::Property::Visibility; return static_cast>(visibility) >= static_cast>(p->visibility()); } )); } void renderTooltip(openspace::properties::PropertyOwner* propOwner) { const bool shouldDisplay = ImGui::IsItemHovered() && (!propOwner->description().empty()); if (shouldDisplay) { ImGui::SetTooltip("%s", propOwner->description().c_str()); } } struct TreeNode { explicit TreeNode(std::string p) : path(std::move(p)) #ifdef Debugging_ImGui_TreeNode_Indices , index(nextIndex++) #endif // Debugging_ImGui_TreeNode_Indices {} std::string path; std::vector> children; std::vector nodes; #ifdef Debugging_ImGui_TreeNode_Indices int index = 0; static int nextIndex; #endif // Debugging_ImGui_TreeNode_Indices }; #ifdef Debugging_ImGui_TreeNode_Indices int TreeNode::nextIndex = 0; #endif // Debugging_ImGui_TreeNode_Indices void addPathToTree(TreeNode& node, const std::vector& path, openspace::SceneGraphNode* owner) { if (path.empty()) { // No more path, so we have reached a leaf node.nodes.push_back(owner); return; } // Check if any of the children's paths contains the first part of the path const auto it = std::find_if( node.children.begin(), node.children.end(), [p = *path.begin()](const std::unique_ptr& c) { return c->path == p; } ); TreeNode* n; if (it != node.children.end()) { // We have a child, so we use it n = it->get(); } else { // We don't have a child, so we must generate it std::unique_ptr newNode = std::make_unique(*path.begin()); n = newNode.get(); node.children.push_back(std::move(newNode)); } // Recurse into the tree and chop off the first path addPathToTree( *n, std::vector(path.begin() + 1, path.end()), owner ); } void simplifyTree(TreeNode& node) { // Merging consecutive nodes if they only have a single child for (const std::unique_ptr& c : node.children) { simplifyTree(*c); } if ((node.children.size() == 1) && (node.nodes.empty())) { node.path = node.path + "/" + node.children[0]->path; node.nodes = std::move(node.children[0]->nodes); std::vector> children = std::move( node.children[0]->children ); node.children = std::move(children); } } void renderTree(const TreeNode& node, const std::function& renderFunc) { if (node.path.empty() || ImGui::TreeNode(node.path.c_str())) { for (const std::unique_ptr& c : node.children) { renderTree(*c, renderFunc); } for (openspace::SceneGraphNode* n : node.nodes) { renderFunc(n); } if (!node.path.empty()) { ImGui::TreePop(); } } } } // namespace namespace openspace::gui { GuiPropertyComponent::GuiPropertyComponent(std::string identifier, std::string guiName, UseTreeLayout useTree) : GuiComponent(std::move(identifier), std::move(guiName)) , _useTreeLayout(UseTreeInfo, useTree) , _treeOrdering(OrderingInfo) , _ignoreHiddenHint(IgnoreHiddenInfo) { addProperty(_useTreeLayout); addProperty(_treeOrdering); addProperty(_ignoreHiddenHint); } void GuiPropertyComponent::setSource(SourceFunction function) { _function = std::move(function); } void GuiPropertyComponent::setVisibility(properties::Property::Visibility visibility) { _visibility = visibility; } void GuiPropertyComponent::setHasRegularProperties(bool hasOnlyRegularProperties) { _hasOnlyRegularProperties = hasOnlyRegularProperties; } void GuiPropertyComponent::renderPropertyOwner(properties::PropertyOwner* owner) { using namespace properties; if (owner->propertiesRecursive().empty()) { return; } const int nThisProperty = nVisibleProperties(owner->properties(), _visibility); ImGui::PushID(owner->identifier().c_str()); const std::vector& subOwners = owner->propertySubOwners(); for (PropertyOwner* subOwner : subOwners) { const std::vector& properties = subOwner->propertiesRecursive(); int count = nVisibleProperties(properties, _visibility); if (count == 0) { continue; } if (subOwners.size() == 1 && (nThisProperty == 0)) { renderPropertyOwner(subOwner); } else { const bool opened = ImGui::TreeNode(subOwner->guiName().c_str()); renderTooltip(subOwner); if (opened) { renderPropertyOwner(subOwner); ImGui::TreePop(); } } } if (!subOwners.empty()) { ImGui::Spacing(); } std::map> propertiesByGroup; std::vector remainingProperies; for (properties::Property* p : owner->properties()) { const std::string& group = p->groupIdentifier(); if (group.empty()) { remainingProperies.push_back(p); } else { propertiesByGroup[group].push_back(p); } } using K = std::string; using V = std::vector; for (const std::pair& p : propertiesByGroup) { const std::string& groupName = owner->propertyGroupName(p.first); if (ImGui::TreeNode(groupName.c_str())) { for (properties::Property* prop : p.second) { renderProperty(prop, owner); } ImGui::TreePop(); } } if (!propertiesByGroup.empty()) { ImGui::Spacing(); } for (properties::Property* prop : remainingProperies) { renderProperty(prop, owner); } ImGui::PopID(); } void GuiPropertyComponent::render() { ImGui::SetNextWindowCollapsed(_isCollapsed); bool v = _isEnabled; ImGui::SetNextWindowSize(Size, ImGuiCond_FirstUseEver); ImGui::SetNextWindowBgAlpha(0.75f); ImGui::Begin(guiName().c_str(), &v); _isEnabled = v; _isCollapsed = ImGui::IsWindowCollapsed(); using namespace properties; if (_function) { std::vector owners = _function(); std::sort( owners.begin(), owners.end(), [](properties::PropertyOwner* lhs, properties::PropertyOwner* rhs) { return lhs->guiName() < rhs->guiName(); } ); if (_useTreeLayout) { for (properties::PropertyOwner* owner : owners) { ghoul_assert( dynamic_cast(owner), "When using the tree layout, all owners must be SceneGraphNodes" ); (void)owner; // using [[maybe_unused]] in the for loop gives an error } // Sort: // if guigrouping, sort by name and shortest first, but respect the user // specified ordering // then all w/o guigroup const std::vector& ordering = _treeOrdering; std::stable_sort( owners.begin(), owners.end(), [&ordering](PropertyOwner* lhs, PropertyOwner* rhs) { std::string lhsGroup = dynamic_cast(lhs)->guiPath(); std::string rhsGroup = dynamic_cast(rhs)->guiPath(); if (lhsGroup.empty()) { return false; } if (rhsGroup.empty()) { return true; } if (ordering.empty()) { return lhsGroup < rhsGroup; } std::vector lhsToken = ghoul::tokenizeString( lhsGroup, '/' ); // The first token is always empty auto lhsIt = std::find(ordering.begin(), ordering.end(), lhsToken[1]); std::vector rhsToken = ghoul::tokenizeString( rhsGroup, '/' ); // The first token is always empty auto rhsIt = std::find(ordering.begin(), ordering.end(), rhsToken[1]); if (lhsIt != ordering.end() && rhsIt != ordering.end()) { if (lhsToken[1] != rhsToken[1]) { // If both top-level groups are in the ordering list, the // order of the iterators gives us the order of the groups return lhsIt < rhsIt; } else { return lhsGroup < rhsGroup; } } else if (lhsIt != ordering.end() && rhsIt == ordering.end()) { // If only one of them is in the list, we have a sorting return true; } else if (lhsIt == ordering.end() && rhsIt != ordering.end()) { return false; } else { return lhsGroup < rhsGroup; } } ); } // If the owners list is empty, we wnat to do the normal thing (-> nothing) // Otherwise, check if the first owner has a GUI group // This makes the assumption that the tree layout is only used if the owners are // SceenGraphNodes (checked above) const bool noGuiGroups = owners.empty() || (dynamic_cast(*owners.begin()) && dynamic_cast(*owners.begin())->guiPath().empty()); auto renderProp = [&](properties::PropertyOwner* pOwner) { const int count = nVisibleProperties( pOwner->propertiesRecursive(), _visibility ); if (count == 0) { return; } auto header = [&]() -> bool { if (owners.size() > 1) { // Create a header in case we have multiple owners return ImGui::CollapsingHeader(pOwner->guiName().c_str()); } else if (!pOwner->identifier().empty()) { // If the owner has a name, print it first ImGui::Text("%s", pOwner->guiName().c_str()); ImGui::Spacing(); return true; } else { // Otherwise, do nothing return true; } }; if (header()) { renderPropertyOwner(pOwner); } }; if (!_useTreeLayout || noGuiGroups) { if (!_ignoreHiddenHint) { // Remove all of the nodes that we want hidden first owners.erase( std::remove_if( owners.begin(), owners.end(), [](properties::PropertyOwner* p) { SceneGraphNode* s = dynamic_cast(p); return s && s->hasGuiHintHidden(); } ), owners.end() ); } std::for_each(owners.begin(), owners.end(), renderProp); } else { // _useTreeLayout && gui groups exist TreeNode root(""); for (properties::PropertyOwner* pOwner : owners) { // We checked above that pOwner is a SceneGraphNode SceneGraphNode* nOwner = static_cast(pOwner); if (!_ignoreHiddenHint && nOwner->hasGuiHintHidden()) { continue; } const std::string guiPath = nOwner->guiPath(); if (guiPath.empty()) { // We know that we are done now since we stable_sort:ed them above break; } std::vector paths = ghoul::tokenizeString( guiPath.substr(1), '/' ); addPathToTree(root, paths, nOwner); } simplifyTree(root); renderTree(root, renderProp); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 20.f); for (properties::PropertyOwner* pOwner : owners) { // We checked above that pOwner is a SceneGraphNode SceneGraphNode* nOwner = static_cast(pOwner); if (!nOwner->guiPath().empty()) { continue; } renderProp(pOwner); } } } ImGui::End(); } void GuiPropertyComponent::renderProperty(properties::Property* prop, properties::PropertyOwner* owner) { using Func = std::function< void(properties::Property*, const std::string&, IsRegularProperty, ShowToolTip, double) >; static const std::map FunctionMapping = { { "BoolProperty", &renderBoolProperty }, { "DoubleProperty", &renderDoubleProperty }, { "IntProperty", &renderIntProperty }, { "IVec2Property", &renderIVec2Property }, { "IVec3Property", &renderIVec3Property }, { "IVec4Property", &renderIVec4Property }, { "FloatProperty", &renderFloatProperty }, { "Vec2Property", &renderVec2Property }, { "Vec3Property", &renderVec3Property }, { "Vec4Property", &renderVec4Property }, { "DVec2Property", &renderDVec2Property }, { "DVec3Property", &renderDVec3Property }, { "DVec4Property", &renderDVec4Property }, { "DMat2Property", &renderDMat2Property }, { "DMat3Property", &renderDMat3Property }, { "DMat4Property", &renderDMat4Property }, { "StringProperty", &renderStringProperty }, { "DoubleListProperty", &renderDoubleListProperty }, { "IntListProperty", &renderIntListProperty }, { "StringListProperty", &renderStringListProperty }, { "OptionProperty", &renderOptionProperty }, { "TriggerProperty", &renderTriggerProperty }, { "SelectionProperty", &renderSelectionProperty } }; // Check if the visibility of the property is high enough to be displayed using V = properties::Property::Visibility; const auto v = static_cast>(_visibility); const auto propV = static_cast>(prop->visibility()); if (v >= propV) { auto it = FunctionMapping.find(prop->className()); if (it != FunctionMapping.end()) { if (owner) { it->second( prop, owner->identifier(), IsRegularProperty(_hasOnlyRegularProperties), ShowToolTip(_showHelpTooltip), _tooltipDelay ); } else { it->second( prop, "", IsRegularProperty(_hasOnlyRegularProperties), ShowToolTip(_showHelpTooltip), _tooltipDelay ); } } } } } // namespace openspace::gui