Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ set_target_properties(
install(TARGETS pyhpp_plot_graph_viewer
DESTINATION "${PYTHON_SITELIB}/pyhpp_plot")

file(WRITE "${CMAKE_BINARY_DIR}/pyhpp_plot/__init__.py"
"from .graph_viewer import show_graph\n")
install(FILES "${CMAKE_BINARY_DIR}/pyhpp_plot/__init__.py"
# Install Python modules
install(FILES "${CMAKE_SOURCE_DIR}/src/pyhpp_plot/__init__.py"
"${CMAKE_SOURCE_DIR}/src/pyhpp_plot/interactive_viewer.py"
DESTINATION "${PYTHON_SITELIB}/pyhpp_plot")
20 changes: 20 additions & 0 deletions include/hpp/plot/hpp-native-graph.hh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#ifndef HPP_PLOT_HPP_NATIVE_GRAPH_HH
#define HPP_PLOT_HPP_NATIVE_GRAPH_HH

#include <QMenu>
#include <QPushButton>
#include <hpp/manipulation/graph/graph.hh>
#include <hpp/plot/graph-widget.hh>
Expand Down Expand Up @@ -87,6 +88,25 @@ class HppNativeGraphWidget : public GraphWidget {
/// \brief Display edge target constraints in the constraint panel
void displayEdgeTargetConstraints(std::size_t id);

Q_SIGNALS:
/// \brief Emitted before showing node context menu, allows external code to
/// add actions
/// \param nodeId The ID of the node
/// \param nodeName The name of the node (state)
/// \param menu Pointer to the context menu (can add actions before it's
/// shown)
void nodeContextMenuAboutToShow(std::size_t nodeId, QString nodeName,
QMenu* menu);

/// \brief Emitted before showing edge context menu, allows external code to
/// add actions
/// \param edgeId The ID of the edge
/// \param edgeName The name of the edge (transition)
/// \param menu Pointer to the context menu (can add actions before it's
/// shown)
void edgeContextMenuAboutToShow(std::size_t edgeId, QString edgeName,
QMenu* menu);

protected:
/// \brief Fill scene from Graph object
/// Reads nodes and edges directly from the C++ graph structure
Expand Down
6 changes: 6 additions & 0 deletions src/hpp-native-graph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ void HppNativeGraphWidget::nodeContextMenu(QGVNode* node) {

cm.addAction("Clear highlight", [this]() { highlightNode(-1); });

// Allow external code (Python) to add custom actions
Q_EMIT nodeContextMenuAboutToShow(ni.id, ni.name, &cm);

cm.exec(QCursor::pos());

currentId_ = savedId;
Expand Down Expand Up @@ -478,6 +481,9 @@ void HppNativeGraphWidget::edgeContextMenu(QGVEdge* edge) {

cm.addAction("Clear highlight", [this]() { highlightEdge(-1); });

// Allow external code (Python) to add custom actions
Q_EMIT edgeContextMenuAboutToShow(ei.id, ei.name, &cm);

cm.exec(QCursor::pos());

currentId_ = savedId;
Expand Down
21 changes: 21 additions & 0 deletions src/pyhpp_plot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from .graph_viewer import (
MenuActionProxy,
show_graph,
show_graph_blocking,
show_interactive_graph,
)
from .interactive_viewer import (
GraphViewerThread,
InteractiveGraphViewer,
show_interactive_graph_threaded,
)

__all__ = [
"GraphViewerThread",
"InteractiveGraphViewer",
"MenuActionProxy",
"show_graph",
"show_graph_blocking",
"show_interactive_graph",
"show_interactive_graph_threaded",
]
113 changes: 113 additions & 0 deletions src/pyhpp_plot/graph_viewer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ GraphPtr_t extractGraph(bp::object py_graph) {
"'_get_native_graph()' method.");
}

/// Simple wrapper to allow Python to add menu actions
class MenuActionProxy {
public:
MenuActionProxy(QMenu* menu) : menu_(menu) {}

void addAction(const std::string& text, bp::object callback) {
QAction* action = menu_->addAction(QString::fromStdString(text));
// Store callback and connect
QObject::connect(action, &QAction::triggered, [callback]() {
try {
callback();
} catch (const bp::error_already_set&) {
PyErr_Print();
}
});
}

void addSeparator() { menu_->addSeparator(); }

private:
QMenu* menu_;
};

/// This function blocks until the window is closed
void showGraphBlocking(bp::object py_graph) {
GraphPtr_t graph = extractGraph(py_graph);
Expand Down Expand Up @@ -121,12 +144,102 @@ void showGraphBlocking(bp::object py_graph) {
app.exec();
}

/// Interactive version with Python callbacks for context menus
void showInteractiveGraph(bp::object py_graph, bp::object node_callback,
bp::object edge_callback) {
GraphPtr_t graph = extractGraph(py_graph);

if (!graph) {
throw std::runtime_error("Graph is null");
}

int argc = 1;
static char app_name[] = "hpp-plot-interactive";
char* argv[] = {app_name, nullptr};

QApplication app(argc, argv);

// Create main window
QMainWindow window;
window.setWindowTitle(
QString::fromStdString("Constraint Graph: " + graph->name()));
window.resize(1200, 800);

// Create widget
hpp::plot::HppNativeGraphWidget widget(graph, &window);
widget.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
window.setCentralWidget(&widget);

// Connect Qt signals to Python callbacks
if (!node_callback.is_none()) {
QObject::connect(
&widget, &hpp::plot::HppNativeGraphWidget::nodeContextMenuAboutToShow,
[node_callback](std::size_t nodeId, QString nodeName, QMenu* menu) {
try {
MenuActionProxy proxy(menu);
node_callback(nodeId, nodeName.toStdString(), boost::ref(proxy));
} catch (const bp::error_already_set&) {
PyErr_Print();
}
});
}

if (!edge_callback.is_none()) {
QObject::connect(
&widget, &hpp::plot::HppNativeGraphWidget::edgeContextMenuAboutToShow,
[edge_callback](std::size_t edgeId, QString edgeName, QMenu* menu) {
try {
MenuActionProxy proxy(menu);
edge_callback(edgeId, edgeName.toStdString(), boost::ref(proxy));
} catch (const bp::error_already_set&) {
PyErr_Print();
}
});
}

// Show and refresh
window.show();
widget.updateGraph();

// Run event loop (blocking)
app.exec();
}

} // namespace

BOOST_PYTHON_MODULE(graph_viewer) {
// Expose MenuActionProxy for adding menu actions from Python
bp::class_<MenuActionProxy, boost::noncopyable>("MenuActionProxy",
bp::no_init)
.def("addAction", &MenuActionProxy::addAction,
(bp::arg("text"), bp::arg("callback")),
"Add an action to the context menu.\n\n"
"Args:\n"
" text: The text label for the menu action\n"
" callback: Python callable to invoke when action is triggered\n")
.def("addSeparator", &MenuActionProxy::addSeparator,
"Add a separator line to the context menu\n");

bp::def("show_graph", &showGraphBlocking, bp::arg("graph"),
"Show constraint graph in a Qt viewer (blocking).\n\n"
"This function blocks until the viewer window is closed.\n\n"
"Args:\n"
" graph: The Graph object from pyhpp.manipulation\n");

// Alias for backwards compatibility
bp::def("show_graph_blocking", &showGraphBlocking, bp::arg("graph"),
"Alias for show_graph() for backwards compatibility.\n");

bp::def("show_interactive_graph", &showInteractiveGraph,
(bp::arg("graph"), bp::arg("node_callback") = bp::object(),
bp::arg("edge_callback") = bp::object()),
"Show constraint graph with interactive context menu callbacks.\n\n"
"This function blocks until the viewer window is closed.\n"
"Callbacks are invoked when user right-clicks on nodes/edges.\n\n"
"Args:\n"
" graph: The Graph object from pyhpp.manipulation\n"
" node_callback: Optional callback(node_id, node_name, "
"menu_proxy) for node menus\n"
" edge_callback: Optional callback(edge_id, edge_name, "
"menu_proxy) for edge menus\n");
}
Loading