mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-01-13 07:21:08 -06:00
534 lines
20 KiB
C++
534 lines
20 KiB
C++
/*****************************************************************************************
|
|
* *
|
|
* 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 <modules/imgui/include/guipropertycomponent.h>
|
|
|
|
#include <modules/imgui/include/imgui_include.h>
|
|
#include <modules/imgui/include/renderproperties.h>
|
|
#include <openspace/scene/scenegraphnode.h>
|
|
#include <ghoul/misc/misc.h>
|
|
#include <algorithm>
|
|
|
|
//#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<openspace::properties::Property*>& props,
|
|
openspace::properties::Property::Visibility visibility)
|
|
{
|
|
return static_cast<int>(std::count_if(
|
|
props.begin(),
|
|
props.end(),
|
|
[visibility](openspace::properties::Property* p) {
|
|
using V = openspace::properties::Property::Visibility;
|
|
return static_cast<std::underlying_type_t<V>>(visibility) >=
|
|
static_cast<std::underlying_type_t<V>>(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<std::unique_ptr<TreeNode>> children;
|
|
std::vector<openspace::SceneGraphNode*> 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<std::string>& 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<TreeNode>& 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<TreeNode> newNode = std::make_unique<TreeNode>(*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<std::string>(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<TreeNode>& 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<std::unique_ptr<TreeNode>> children = std::move(
|
|
node.children[0]->children
|
|
);
|
|
node.children = std::move(children);
|
|
}
|
|
}
|
|
|
|
void renderTree(const TreeNode& node,
|
|
const std::function<void (openspace::properties::PropertyOwner*)>& renderFunc)
|
|
{
|
|
if (node.path.empty() || ImGui::TreeNode(node.path.c_str())) {
|
|
for (const std::unique_ptr<TreeNode>& 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<PropertyOwner*>& subOwners = owner->propertySubOwners();
|
|
for (PropertyOwner* subOwner : subOwners) {
|
|
const std::vector<Property*>& 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<std::string, std::vector<Property*>> propertiesByGroup;
|
|
std::vector<Property*> 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<Property*>;
|
|
for (const std::pair<const K, V>& 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<properties::PropertyOwner*> 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<SceneGraphNode*>(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<std::string>& ordering = _treeOrdering;
|
|
std::stable_sort(
|
|
owners.begin(),
|
|
owners.end(),
|
|
[&ordering](PropertyOwner* lhs, PropertyOwner* rhs) {
|
|
std::string lhsGroup = dynamic_cast<SceneGraphNode*>(lhs)->guiPath();
|
|
std::string rhsGroup = dynamic_cast<SceneGraphNode*>(rhs)->guiPath();
|
|
|
|
if (lhsGroup.empty()) {
|
|
return false;
|
|
}
|
|
if (rhsGroup.empty()) {
|
|
return true;
|
|
}
|
|
|
|
if (ordering.empty()) {
|
|
return lhsGroup < rhsGroup;
|
|
}
|
|
|
|
std::vector<std::string> lhsToken = ghoul::tokenizeString(
|
|
lhsGroup,
|
|
'/'
|
|
);
|
|
// The first token is always empty
|
|
auto lhsIt = std::find(ordering.begin(), ordering.end(), lhsToken[1]);
|
|
|
|
std::vector<std::string> 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<SceneGraphNode*>(*owners.begin()) &&
|
|
dynamic_cast<SceneGraphNode*>(*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<SceneGraphNode*>(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<SceneGraphNode*>(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<std::string> 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<SceneGraphNode*>(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<std::string, Func> 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<std::underlying_type_t<V>>(_visibility);
|
|
const auto propV = static_cast<std::underlying_type_t<V>>(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
|