Skip to content

Commit

Permalink
Replace linear node list with tree representation (#34)
Browse files Browse the repository at this point in the history
* fixed clang tidy warnings

* refactored code to reduce complexity of main file

* replaced linear list of nodes with foldable structure

Co-authored-by: Dominik <[email protected]>

Co-authored-by: Jonas Otto <[email protected]>
  • Loading branch information
authaldo and ottojo authored Feb 9, 2024
1 parent 8c3e5b1 commit 1583ff5
Show file tree
Hide file tree
Showing 11 changed files with 755 additions and 419 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ set(OpenGL_GL_PREFERENCE GLVND)
find_package(OpenGL REQUIRED)
target_link_libraries(imgui PUBLIC glfw OpenGL::GL)

add_executable(${PROJECT_NAME} src/rig_reconfigure.cpp src/service_wrapper.cpp src/parameter_tree.cpp external/lodepng/lodepng.cpp)
add_executable(${PROJECT_NAME} src/rig_reconfigure.cpp src/service_wrapper.cpp src/parameter_tree.cpp
src/utils.cpp src/node_window.cpp src/parameter_window.cpp external/lodepng/lodepng.cpp)

# uncomment for checking the executable with tsan
#target_compile_options(${PROJECT_NAME} PRIVATE -g -fsanitize=thread)
Expand Down
26 changes: 26 additions & 0 deletions include/node_window.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @file node_window.hpp
* @author Dominik Authaler
* @date 13.01.2024
*
* Code related to the node window within the graphical user interface.
*/

#ifndef RIG_RECONFIGURE_NODE_WINDOW_HPP
#define RIG_RECONFIGURE_NODE_WINDOW_HPP

#include "utils.hpp"
#include "service_wrapper.hpp"

/**
* Renders the window for the node selection.
* @param[in] windowName Window name.
* @param[in] nodeNames List with the available nodes.
* @param[in, out] serviceWrapper Service wrapper for issuing ROS requests.
* @param[in, out] selectedNode Full name of the currently selected node.
* @param[in, out] status Status.
*/
void renderNodeWindow(const char *windowName, const std::vector<std::string> &nodeNames,
ServiceWrapper &serviceWrapper, std::string &selectedNode, Status &status);

#endif //RIG_RECONFIGURE_NODE_WINDOW_HPP
5 changes: 4 additions & 1 deletion include/parameter_tree.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ class ParameterTree {

void add(const ROSParameter &parameter);
void clear();
std::shared_ptr<ParameterGroup> getRoot();
[[nodiscard]] std::shared_ptr<ParameterGroup> getRoot();
[[nodiscard]] std::size_t getMaxParamNameLength() const;
[[nodiscard]] std::string getAppliedFilter() const;

[[nodiscard]] ParameterTree filter(const std::string &filterString) const;

Expand All @@ -81,6 +82,8 @@ class ParameterTree {

// bookkeeping for a nicer visualization
std::size_t maxParamNameLength = 0;

std::string appliedFilter;
};

#endif // RIG_RECONFIGURE_PARAMETER_TREE_HPP
32 changes: 32 additions & 0 deletions include/parameter_window.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @file parameter_window.hpp
* @author Dominik Authaler
* @date 13.01.2024
*
* Code related to the parameter window within the graphical user interface.
*/

#ifndef RIG_RECONFIGURE_PARAMETER_WINDOW_HPP
#define RIG_RECONFIGURE_PARAMETER_WINDOW_HPP

#include <imgui.h>
#include <string>

#include "parameter_tree.hpp"
#include "service_wrapper.hpp"
#include "utils.hpp"

/**
* Renders the window for the parameter modification.
* @param[in] windowName Window name.
* @param[in] curSelectedNode Name of the currently selected node.
* @param[in, out] serviceWrapper Service wrapper for issuing ROS requests.
* @param[in, out] filteredParameterTree Filtered parameter tree.
* @param[in, out] filter Filter string input.
* @param[in, out] status Status for displaying errors.
*/
void renderParameterWindow(const char *windowName, const std::string &curSelectedNode,
ServiceWrapper &serviceWrapper, ParameterTree &filteredParameterTree,
std::string &filter, Status &status);

#endif //RIG_RECONFIGURE_PARAMETER_WINDOW_HPP
68 changes: 68 additions & 0 deletions include/utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @file utils.hpp
* @author Dominik Authaler
* @date 12.01.2024
*
* Collection of utility functions.
*/

#ifndef RIG_RECONFIGURE_UTILS_HPP
#define RIG_RECONFIGURE_UTILS_HPP

#include <string>
#include <imgui.h>
#include <filesystem>

#include <GLFW/glfw3.h> // will drag system OpenGL headers

struct Status {
enum class Type { NONE, NO_NODES_AVAILABLE, PARAMETER_CHANGED, SERVICE_TIMEOUT };

Type type = Type::NONE;
std::string text;
};

/**
* Utility imgui function for partly highlighted text.
* @param text String which should be displayed.
* @param start Starting index of the highlighted part.
* @param end End index of the highlighted part.
* @param highlightColor Color used for the highlighted part, the remaining text is displayed using the default text
* color.
*/
void highlightedText(const std::string &text, std::size_t start, std::size_t end,
const ImVec4 &highlightColor);

/**
* Utility imgui function for partly highlighted text which can be selected.
* @param text String which should be displayed.
* @param start Starting index of the highlighted part.
* @param end End index of the highlighted part.
* @param highlightColor Color used for the highlighted part, the remaining text is displayed using the default text
* color.
*/
bool highlightedSelectableText(const std::string &text, std::size_t start, std::size_t end,
const ImVec4 &highlightColor);

/**
* Searches for the resource directory.
* @param execPath Executable path.
* @return Path to the resource directory.
*/
std::filesystem::path findResourcePath(const std::string &execPath);

/**
* Loads an icon for the provided window.
* @param windowPtr Window for which the icon should be loaded.
* @param resourcePath Path to the icon data.
*/
void loadWindowIcon(GLFWwindow *windowPtr, const std::filesystem::path &resourcePath);

/**
* Prints the corresponding error.
* @param error Error code.
* @param description Detailed error description.
*/
void glfw_error_callback(int error, const char *description);

#endif //RIG_RECONFIGURE_UTILS_HPP
196 changes: 196 additions & 0 deletions src/node_window.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* @file node_window.cpp
* @author Dominik Authaler
* @date 13.01.2024
*
* Code related to the node window within the graphical user interface.
*/

#include "node_window.hpp"

#include <imgui.h>
#include <string>

// height of the box in which the nodes are visualized
static constexpr auto BOX_HEIGHT = 500;

// utility structures and functions (definitions mainly follow at the end of the file)
struct TreeNode {
std::string name; // node name (leaf node) / namespace (other)
std::string fullName; // full node name for easier usage

std::vector<std::shared_ptr<TreeNode>> children;
};

class NodeTree {
public:
explicit NodeTree(const std::vector<std::string> &nodes);

std::shared_ptr<TreeNode> getRoot();

private:
struct SortComparator {
inline bool operator() (const std::shared_ptr<TreeNode>& node1, const std::shared_ptr<TreeNode>& node2);
};

/**
* Adds a new node within the node tree.
* @param curNode Root node of the (sub-)tree.
* @param name (Partial) node name that is considered for inserting the node within the tree.
* @param fullName Full name of the new node (stored for convenient access to selected nodes).
*/
void addNode(const std::shared_ptr<TreeNode>& curNode, const std::string &name, const std::string &fullName);

/**
* Reorders children of nodes:
* - leaf nodes before inner nodes
* - alphabetically within same groups
*/
void sortAlphabetically(const std::shared_ptr<TreeNode>& curNode);

std::shared_ptr<TreeNode> root;
};

void visualizeNodeTree(const std::shared_ptr<const TreeNode>& root, std::string &selectedNode);

void renderNodeWindow(const char *windowName, const std::vector<std::string> &nodeNames,
ServiceWrapper &serviceWrapper, std::string &selectedNode, Status &status) {
ImGui::Begin(windowName);

if (nodeNames.empty()) {
ImGui::Text("No nodes available!");
} else {
ImGui::Text("Available nodes:");

// organize nodes as a (sorted) tree and visualize them in a foldable structure
NodeTree tree(nodeNames);

// the list box creates a highlighted area in which scrolling is possible
if (ImGui::BeginListBox("##Nodes", ImVec2(-1, BOX_HEIGHT))) {
visualizeNodeTree(tree.getRoot(), selectedNode);
ImGui::EndListBox();
}
}

if (ImGui::Button("Refresh")) {
serviceWrapper.pushRequest(std::make_shared<Request>(Request::Type::QUERY_NODE_NAMES));

if (status.type == Status::Type::SERVICE_TIMEOUT) {
status.text.clear();
status.type = Status::Type::NONE;
}
}

ImGui::End();
}

// implementation of utility + member functions
NodeTree::NodeTree(const std::vector<std::string> &nodes) : root(std::make_shared<TreeNode>()) {
for (const auto &node : nodes) {
// we ignore the leading slash for building the tree
addNode(root, node.substr(1), node);
}

sortAlphabetically(root);
}

std::shared_ptr<TreeNode> NodeTree::getRoot() {
return root;
}

void NodeTree::sortAlphabetically(const std::shared_ptr<TreeNode>& curNode) {
for (const auto &child : curNode->children) {
sortAlphabetically(child);
}

std::sort(curNode->children.begin(), curNode->children.end(), SortComparator());
}

void NodeTree::addNode(const std::shared_ptr<TreeNode> &curNode, const std::string &name, const std::string &fullName) {

auto prefixEnd = name.find('/');
if (prefixEnd == std::string::npos) {
curNode->children.emplace_back(std::make_shared<TreeNode>(TreeNode{name, fullName}));
return;
}

// extract first prefix and find corresponding node
auto prefix = name.substr(0, prefixEnd);
auto remainingName = name.substr(prefixEnd + 1);

std::shared_ptr<TreeNode> nextNode = nullptr;
for (const auto &child : curNode->children) {
if (child->name.starts_with(prefix)) {
nextNode = child;
break;
}
}

if (nextNode == nullptr) {
nextNode = std::make_shared<TreeNode>(TreeNode{name, fullName});
curNode->children.emplace_back(nextNode);
} else {
// found an existing node, check whether we have to subdivide it
const auto idx = name.find('/');
if (nextNode->children.empty() && idx != std::string::npos && nextNode->name.find('/') != std::string::npos) {
// nextNode is leaf with prefix (namespace with single child is collapsed)
auto nextNodePrefix = nextNode->name.substr(0, idx);
auto nextNodeRemainingName = nextNode->name.substr(idx + 1);

nextNode->children.emplace_back(std::make_shared<TreeNode>(TreeNode{nextNodeRemainingName, nextNode->fullName}));

nextNode->name = nextNodePrefix;
nextNode->fullName.clear();
} else if (nextNode->children.empty() && idx != std::string::npos && nextNode->name == prefix) {
// nextNode is a leaf node with same name as next namespace token
// create sibling to nextNode with same name, and add node below that
nextNode = std::make_shared<TreeNode>(TreeNode{prefix, fullName});
curNode->children.emplace_back(nextNode);
}

addNode(nextNode, remainingName, fullName);
}
}

bool NodeTree::SortComparator::operator()(const std::shared_ptr<TreeNode> &node1,
const std::shared_ptr<TreeNode> &node2) {
bool res;

if (node1->children.empty() && !node2->children.empty()) {
res = true;
} else if (!node1->children.empty() && node2->children.empty()) {
res = false;
} else {
res = (node1->name < node2->name);
}

return res;
}

void visualizeNodeTree(const std::shared_ptr<const TreeNode>& root, std::string &selectedNode) {
if (root->children.empty()) {
// leaf node
// push "leaf" to id stack to prevent ID collision between node and namespace with same name
ImGui::PushID("leaf");
if (ImGui::Selectable(root->name.c_str(), selectedNode == root->fullName)) {

selectedNode = root->fullName;
}
ImGui::PopID();
} else {
// inner node
if (!root->name.empty()) {
if (ImGui::TreeNode(root->name.c_str())) {
for (const auto &child : root->children) {
visualizeNodeTree(child, selectedNode);
}

ImGui::TreePop();
}
} else {
for (const auto &child : root->children) {
visualizeNodeTree(child, selectedNode);
}
}
}
}
6 changes: 6 additions & 0 deletions src/parameter_tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ void ParameterTree::add(const ROSParameter &parameter) {
}
void ParameterTree::clear() {
root = std::make_shared<ParameterGroup>();
appliedFilter.clear();
}

void ParameterTree::add(const std::shared_ptr<ParameterGroup> &curNode, const TreeElement &parameter) {
Expand Down Expand Up @@ -63,10 +64,15 @@ std::size_t ParameterTree::getMaxParamNameLength() const {
return maxParamNameLength;
}

std::string ParameterTree::getAppliedFilter() const {
return appliedFilter;
}

ParameterTree ParameterTree::filter(const std::string &filterString) const {
ParameterTree filteredTree;

filteredTree.maxParamNameLength = maxParamNameLength;
filteredTree.appliedFilter = filterString;

// first pass: filter all parameters
filter(filteredTree.getRoot(), root, filterString);
Expand Down
Loading

0 comments on commit 1583ff5

Please sign in to comment.