Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace linear node list with tree representation #34

Merged
merged 12 commits into from
Feb 9, 2024
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
Loading