diff --git a/Hesiod/CMakeLists.txt b/Hesiod/CMakeLists.txt index d23742439..91251a489 100644 --- a/Hesiod/CMakeLists.txt +++ b/Hesiod/CMakeLists.txt @@ -10,7 +10,8 @@ set(CMAKE_AUTOMOC_VERBOSE ON) # Source files # ------------------------------ file(GLOB_RECURSE HESIOD_GUI_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp) -file(GLOB_RECURSE HESIOD_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) +file(GLOB_RECURSE HESIOD_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) if(HESIOD_MINIMAL_NODE_SET) # option for a minimal set of nodes for quick compile time when tempering with @@ -59,7 +60,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE hesiod_core) target_include_directories( hesiod_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_BINARY_DIR}/include - ${OPENGL_INCLUDE_DIRS} ${GLEW_INCLUDE_DIRS} ${GLUT_INCLUDE_DIRS}) + ${OPENGL_INCLUDE_DIRS} ${GLEW_INCLUDE_DIRS} ${GLUT_INCLUDE_DIRS}) # ------------------------------ # Compiler features @@ -72,24 +73,24 @@ target_compile_features(hesiod_core PUBLIC cxx_std_20) target_link_libraries( hesiod_core PUBLIC hesiod_options - hesiod_platform - hesiod_qt_logging - args - spdlog::spdlog - nlohmann_json::nlohmann_json - highmap - gnode - gnodegui - meta - meta_qt - Qt6::Core - Qt6::OpenGL - Qt6::Widgets - Qt6::OpenGLWidgets - Qt6::WebEngineWidgets - qterrain-renderer - qtexture_downloader - ZLIB::ZLIB) + hesiod_platform + hesiod_qt_logging + args + spdlog::spdlog + nlohmann_json::nlohmann_json + highmap + gnode + gnodegui + meta + meta_qt + Qt6::Core + Qt6::OpenGL + Qt6::Widgets + Qt6::OpenGLWidgets + Qt6::WebEngineWidgets + qterrain-renderer + qtexture_downloader + ZLIB::ZLIB) # ------------------------------ # Precompiled Headers (PCH) @@ -134,10 +135,20 @@ message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") if(HESIOD_ENABLE_TESTS) find_package(Qt6 REQUIRED COMPONENTS Test) - add_executable(test_graph_editor ${CMAKE_SOURCE_DIR}/tests/gui/test_graph_editor.cpp) + add_library(test_model_headers OBJECT + ${CMAKE_SOURCE_DIR}/tests/model/model_headers.cpp) + set_target_properties(test_model_headers PROPERTIES AUTOMOC OFF) + target_include_directories(test_model_headers + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(test_model_headers PRIVATE gnode highmap meta) + target_compile_features(test_model_headers PRIVATE cxx_std_20) + + add_executable(test_graph_editor + ${CMAKE_SOURCE_DIR}/tests/gui/test_graph_editor.cpp) + add_dependencies(test_graph_editor test_model_headers) target_link_libraries(test_graph_editor PRIVATE hesiod_core Qt6::Test) add_test(NAME graph_editor COMMAND test_graph_editor) - set_tests_properties(graph_editor PROPERTIES - WORKING_DIRECTORY ${EXECUTABLE_OUTPUT_PATH} - ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + set_tests_properties( + graph_editor PROPERTIES WORKING_DIRECTORY ${EXECUTABLE_OUTPUT_PATH} + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") endif() diff --git a/Hesiod/include/hesiod/gui/hesiod_node_proxy.hpp b/Hesiod/include/hesiod/gui/hesiod_node_proxy.hpp new file mode 100644 index 000000000..63a733ebc --- /dev/null +++ b/Hesiod/include/hesiod/gui/hesiod_node_proxy.hpp @@ -0,0 +1,32 @@ +/* Copyright (c) 2026 Otto Link. Distributed under the terms of the GNU General + * Public License. The full license is in the file LICENSE, distributed with + * this software. */ +#pragma once +#include "gnodegui/node_proxy.hpp" + +namespace hesiod +{ +class BaseNode; + +// The view owns the proxy; the graph owns the model. +class HesiodNodeProxy : public gngui::NodeProxy +{ +public: + HesiodNodeProxy(std::weak_ptr model, QObject *owner); + + std::string get_id() const override; + void set_id(const std::string &id) override; + std::string get_caption() const override; + std::string get_category() const override; + std::string get_comment() const override; + std::string get_tool_tip_text() const override; + int get_nports() const override; + std::string get_port_caption(int index) const override; + gngui::PortType get_port_type(int index) const override; + std::string get_data_type(int index) const override; + void *get_data_ref(int index) const override; + +private: + std::weak_ptr model; +}; +} // namespace hesiod diff --git a/Hesiod/include/hesiod/model/nodes/base_node.hpp b/Hesiod/include/hesiod/model/nodes/base_node.hpp index 73b4ace4e..b6bdc7a4a 100644 --- a/Hesiod/include/hesiod/model/nodes/base_node.hpp +++ b/Hesiod/include/hesiod/model/nodes/base_node.hpp @@ -11,7 +11,6 @@ #include #include "gnode/node.hpp" -#include "gnodegui/node_proxy.hpp" #include "meta/core/container_group.hpp" @@ -57,6 +56,7 @@ class BaseNode : public gnode::Node, public std::enable_shared_from_this decltype(auto) val(const std::string &key) const diff --git a/Hesiod/include/hesiod/model/nodes/port_catalog.hpp b/Hesiod/include/hesiod/model/nodes/port_catalog.hpp index a51b70790..506168753 100644 --- a/Hesiod/include/hesiod/model/nodes/port_catalog.hpp +++ b/Hesiod/include/hesiod/model/nodes/port_catalog.hpp @@ -7,7 +7,7 @@ #include #include -#include "gnodegui/node_proxy.hpp" // gngui::PortType +#include "gnode/port.hpp" namespace hesiod { @@ -19,7 +19,7 @@ struct PortInfo { std::string name; std::string data_type; - gngui::PortType direction; + gnode::PortType direction; }; /** @@ -45,7 +45,7 @@ class PortCatalog */ bool is_offerable(const std::string &node_type, const std::string &data_type, - gngui::PortType wanted_direction) const; + gnode::PortType wanted_direction) const; /// Ports of a node type, or nullptr when the type is unknown. const std::vector *find(const std::string &node_type) const; @@ -65,6 +65,6 @@ class PortCatalog */ std::optional select_port(const BaseNode &node, const std::string &data_type, - gngui::PortType wanted_direction); + gnode::PortType wanted_direction); } // namespace hesiod diff --git a/Hesiod/src/app/hesiod_application.cpp b/Hesiod/src/app/hesiod_application.cpp index 2d39fcc55..461e291ae 100644 --- a/Hesiod/src/app/hesiod_application.cpp +++ b/Hesiod/src/app/hesiod_application.cpp @@ -565,7 +565,7 @@ void HesiodApplication::on_export_batch() BaseNode *p_base = p_graph->get_node_ref_by_id(nid); NodeExportStatus st; st.node_id = nid; - st.node_label = p_base ? p_base->get_caption() : nid; + st.node_label = p_base ? p_base->get_label() : nid; st.node_type = p_base ? p_base->get_node_type() : ""; st.state = NodeComputeState::Pending; scheduled_nodes.push_back(st); diff --git a/Hesiod/src/cli/check_port_links.cpp b/Hesiod/src/cli/check_port_links.cpp index 31280ec61..b9fce4027 100644 --- a/Hesiod/src/cli/check_port_links.cpp +++ b/Hesiod/src/cli/check_port_links.cpp @@ -23,7 +23,7 @@ int conventional_rule_changed_outcome = 0; void expect_offerable(const PortCatalog &catalog, const std::string &node_type, const std::string &data_type, - gngui::PortType wanted, + gnode::PortType wanted, bool expected) { const bool got = catalog.is_offerable(node_type, data_type, wanted); @@ -32,7 +32,7 @@ void expect_offerable(const PortCatalog &catalog, Logger::log()->error("check-port-links: {} [{}, want {}]: offerable={} expected={}", node_type, data_type, - wanted == gngui::PortType::IN ? "IN" : "OUT", + wanted == gnode::PortType::IN ? "IN" : "OUT", got, expected); failures++; @@ -41,7 +41,7 @@ void expect_offerable(const PortCatalog &catalog, void expect_selected(const std::string &node_type, const std::string &data_type, - gngui::PortType wanted, + gnode::PortType wanted, const std::string &expected_port) { auto config = std::make_shared(); @@ -64,7 +64,7 @@ void expect_selected(const std::string &node_type, "check-port-links: {} [{}, want {}]: selected '{}' expected '{}'", node_type, data_type, - wanted == gngui::PortType::IN ? "IN" : "OUT", + wanted == gnode::PortType::IN ? "IN" : "OUT", got_str, expected_port); failures++; @@ -110,7 +110,7 @@ void sweep_all_node_types(const PortCatalog &catalog) } for (const std::string &data_type : data_types) - for (gngui::PortType wanted : {gngui::PortType::IN, gngui::PortType::OUT}) + for (gnode::PortType wanted : {gnode::PortType::IN, gnode::PortType::OUT}) { const bool offered = catalog.is_offerable(node_type, data_type, wanted); const std::optional selected = hesiod::select_port(*p_base, @@ -125,7 +125,7 @@ void sweep_all_node_types(const PortCatalog &catalog) "node selected={} (documentation drift?)", node_type, data_type, - wanted == gngui::PortType::IN ? "IN" : "OUT", + wanted == gnode::PortType::IN ? "IN" : "OUT", offered, selected.has_value()); failures++; @@ -164,7 +164,7 @@ void sweep_all_node_types(const PortCatalog &catalog) std::string lower; for (char c : label) lower += static_cast(std::tolower(static_cast(c))); - const bool conventional = (wanted == gngui::PortType::IN) + const bool conventional = (wanted == gnode::PortType::IN) ? (lower == "input" || lower == "in") : (lower == "output" || lower == "out"); if (conventional) @@ -182,7 +182,7 @@ void sweep_all_node_types(const PortCatalog &catalog) "check-port-links: {} [{}, want {}]: oracle expected='{}' got='{}'", node_type, data_type, - wanted == gngui::PortType::IN ? "IN" : "OUT", + wanted == gnode::PortType::IN ? "IN" : "OUT", expected ? *expected : std::string(""), selected ? *selected : std::string("")); failures++; @@ -216,40 +216,40 @@ int run_check_port_links() // IslandChain's only VirtualArray port is its OUTPUT, so dragging a // VirtualArray from an output (wanting an input) must NOT offer it. // This is the case that aborted the application. - expect_offerable(catalog, "IslandChain", "VirtualArray", gngui::PortType::IN, false); + expect_offerable(catalog, "IslandChain", "VirtualArray", gnode::PortType::IN, false); // Dragging backwards from an input (wanting an output) must offer it. - expect_offerable(catalog, "IslandChain", "VirtualArray", gngui::PortType::OUT, true); + expect_offerable(catalog, "IslandChain", "VirtualArray", gnode::PortType::OUT, true); // Its Path input is offerable when a Path is dragged from an output. - expect_offerable(catalog, "IslandChain", "Path", gngui::PortType::IN, true); + expect_offerable(catalog, "IslandChain", "Path", gnode::PortType::IN, true); // Ordinary filters accept a VirtualArray input. - expect_offerable(catalog, "Laplace", "VirtualArray", gngui::PortType::IN, true); - expect_offerable(catalog, "Bump", "VirtualArray", gngui::PortType::IN, true); + expect_offerable(catalog, "Laplace", "VirtualArray", gnode::PortType::IN, true); + expect_offerable(catalog, "Bump", "VirtualArray", gnode::PortType::IN, true); // Incompatible type is never offered. - expect_offerable(catalog, "Laplace", "VirtualTexture", gngui::PortType::IN, false); + expect_offerable(catalog, "Laplace", "VirtualTexture", gnode::PortType::IN, false); // Unknown node type fails OPEN (never hide a real node if docs drift). - expect_offerable(catalog, "NoSuchNodeType", "VirtualArray", gngui::PortType::IN, true); + expect_offerable(catalog, "NoSuchNodeType", "VirtualArray", gnode::PortType::IN, true); // --- pinned port-selection cases (live node, true declaration order) // Conventional name wins: Laplace declares an "input" port. - expect_selected("Laplace", "VirtualArray", gngui::PortType::IN, "input"); + expect_selected("Laplace", "VirtualArray", gnode::PortType::IN, "input"); // No conventional name: Bump declares dx, dy, control, envelope -> first // declared wins. NOTE this is "dx" only because selection reads the LIVE // node; the documentation's alphabetical key order would have given // "control", which is why the catalog must never be used for selection. - expect_selected("Bump", "VirtualArray", gngui::PortType::IN, "dx"); + expect_selected("Bump", "VirtualArray", gnode::PortType::IN, "dx"); // Backwards drag: wanting an OUTPUT of type VirtualArray. - expect_selected("IslandChain", "VirtualArray", gngui::PortType::OUT, "output"); + expect_selected("IslandChain", "VirtualArray", gnode::PortType::OUT, "output"); // Forwards drag onto IslandChain has no VirtualArray input at all. - expect_selected("IslandChain", "VirtualArray", gngui::PortType::IN, ""); + expect_selected("IslandChain", "VirtualArray", gnode::PortType::IN, ""); sweep_all_node_types(catalog); diff --git a/Hesiod/src/gui/hesiod_node_proxy.cpp b/Hesiod/src/gui/hesiod_node_proxy.cpp new file mode 100644 index 000000000..56bfb3857 --- /dev/null +++ b/Hesiod/src/gui/hesiod_node_proxy.cpp @@ -0,0 +1,94 @@ +/* Copyright (c) 2026 Otto Link. Distributed under the terms of the GNU General + * Public License. The full license is in the file LICENSE, distributed with + * this software. */ +#include "hesiod/gui/hesiod_node_proxy.hpp" + +#include + +#include "hesiod/model/nodes/base_node.hpp" + +namespace hesiod +{ +HesiodNodeProxy::HesiodNodeProxy(std::weak_ptr model, QObject *owner) + : model(std::move(model)) +{ + this->setParent(owner); +} + +std::string HesiodNodeProxy::get_id() const +{ + if (auto node = model.lock()) + return node->get_id(); + return {}; +} + +void HesiodNodeProxy::set_id(const std::string &id) +{ + if (auto node = model.lock()) + node->set_id(id); +} + +std::string HesiodNodeProxy::get_caption() const +{ + if (auto node = model.lock()) + return node->get_label(); + return {}; +} + +std::string HesiodNodeProxy::get_category() const +{ + if (auto node = model.lock()) + return node->get_category(); + return {}; +} + +std::string HesiodNodeProxy::get_comment() const +{ + if (auto node = model.lock()) + return node->get_comment(); + return {}; +} + +std::string HesiodNodeProxy::get_tool_tip_text() const +{ + if (auto node = model.lock()) + return node->get_documentation_short_html(); + return {}; +} + +int HesiodNodeProxy::get_nports() const +{ + if (auto node = model.lock()) + return node->get_nports(); + return 0; +} + +std::string HesiodNodeProxy::get_port_caption(int index) const +{ + if (auto node = model.lock()) + return node->get_port_label(index); + return {}; +} + +gngui::PortType HesiodNodeProxy::get_port_type(int index) const +{ + if (auto node = model.lock()) + return node->get_port_type(index) == gnode::PortType::OUT ? gngui::PortType::OUT + : gngui::PortType::IN; + return gngui::PortType::IN; +} + +std::string HesiodNodeProxy::get_data_type(int index) const +{ + if (auto node = model.lock()) + return node->get_data_type(index); + return {}; +} + +void *HesiodNodeProxy::get_data_ref(int index) const +{ + if (auto node = model.lock()) + return node->get_value_ref_void(index); + return nullptr; +} +} // namespace hesiod diff --git a/Hesiod/src/gui/widgets/data_preview.cpp b/Hesiod/src/gui/widgets/data_preview.cpp index 65e4c8ba7..ba0cc85e3 100644 --- a/Hesiod/src/gui/widgets/data_preview.cpp +++ b/Hesiod/src/gui/widgets/data_preview.cpp @@ -28,7 +28,7 @@ DataPreview::DataPreview(std::weak_ptr model, QWidget *parent) throw std::invalid_argument("DataPreview::DataPreview: p_model_node is nullptr"); Logger::log()->trace("DataPreview::DataPreview, node {}({})", - p_model->get_caption(), + p_model->get_label(), p_model->get_id()); AppContext &ctx = HSD_CTX; @@ -43,7 +43,7 @@ DataPreview::DataPreview(std::weak_ptr model, QWidget *parent) // Select first output, or fallback to first port this->preview_port_index = 0; for (int k = 0; k < p_model->get_nports(); ++k) - if (p_model->get_port_type(k) == gngui::PortType::OUT) + if (p_model->get_port_type(k) == gnode::PortType::OUT) { this->preview_port_index = k; break; @@ -89,7 +89,7 @@ void DataPreview::contextMenuEvent(QContextMenuEvent *event) context_menu.addSection("Data"); for (int k = 0; k < p_model->get_nports(); ++k) { - const std::string caption = p_model->get_port_caption(k); + const std::string caption = p_model->get_port_label(k); QAction *action = context_menu.addAction(QString::fromStdString(caption)); action->setCheckable(true); if (k == preview_port_index) @@ -110,7 +110,7 @@ void DataPreview::contextMenuEvent(QContextMenuEvent *event) // Port selection for (int k = 0; k < p_model->get_nports(); ++k) - if (p_model->get_port_caption(k) == label) + if (p_model->get_port_label(k) == label) { preview_port_index = k; update_preview(); @@ -130,7 +130,7 @@ void DataPreview::update_preview() return; } - void *blind_ptr = p_model->get_data_ref(preview_port_index); + void *blind_ptr = p_model->get_value_ref_void(preview_port_index); const std::string data_type = p_model->get_data_type(preview_port_index); AppContext &ctx = HSD_CTX; diff --git a/Hesiod/src/gui/widgets/graph_node_widget.cpp b/Hesiod/src/gui/widgets/graph_node_widget.cpp index 1ce522a61..9c44e4fa7 100644 --- a/Hesiod/src/gui/widgets/graph_node_widget.cpp +++ b/Hesiod/src/gui/widgets/graph_node_widget.cpp @@ -18,6 +18,7 @@ #include "hesiod/app/hesiod_application.hpp" #include "hesiod/gui/graph_editor.hpp" +#include "hesiod/gui/hesiod_node_proxy.hpp" #include "hesiod/gui/widgets/custom_qmenu.hpp" #include "hesiod/gui/widgets/graph_config_widgets/graph_config_dialog.hpp" #include "hesiod/gui/widgets/graph_node_widget.hpp" @@ -426,10 +427,10 @@ void GraphNodeWidget::on_connection_dropped(const std::string &node_id, const std::string dragged_type = map_type_name( p_node_from->get_data_type(from_index)); - const gngui::PortType dragged_dir = p_node_from->get_port_type(from_index); - const gngui::PortType wanted_dir = (dragged_dir == gngui::PortType::OUT) - ? gngui::PortType::IN - : gngui::PortType::OUT; + const gnode::PortType dragged_dir = p_node_from->get_port_type(from_index); + const gnode::PortType wanted_dir = (dragged_dir == gnode::PortType::OUT) + ? gnode::PortType::IN + : gnode::PortType::OUT; // Filter GraphViewer's inventory for the duration of its blocking menu. const std::map full_inventory = get_node_inventory(); @@ -477,13 +478,13 @@ void GraphNodeWidget::on_connection_dropped(const std::string &node_id, "{} port of type {}, " "leaving it unconnected", node_to, - wanted_dir == gngui::PortType::IN ? "input" : "output", + wanted_dir == gnode::PortType::IN ? "input" : "output", dragged_type); batch.commit(); return; } - const bool dragged_is_output = (dragged_dir == gngui::PortType::OUT); + const bool dragged_is_output = (dragged_dir == gnode::PortType::OUT); const std::string id_out = dragged_is_output ? node_id : node_to; const std::string port_out = dragged_is_output ? port_id : *port_to; @@ -654,9 +655,8 @@ void GraphNodeWidget::on_new_graphics_node_request(const std::string &node_id, BaseNode *p_node = gno->get_node_ref_by_id(node_id); if (!p_node) throw std::runtime_error("Cannot display a missing model node."); - auto *p_proxy = new gngui::TypedNodeProxy(p_node->get_shared()); - p_proxy->setParent(this); - auto *widget = node_widget_factory(p_node->get_caption(), p_node->get_shared(), this); + auto *p_proxy = new HesiodNodeProxy(p_node->get_shared(), this); + auto *widget = node_widget_factory(p_node->get_label(), p_node->get_shared(), this); this->add_node(p_proxy, scene_pos, node_id); this->get_graphics_node_by_id(node_id)->set_widget(widget); diff --git a/Hesiod/src/gui/widgets/node_info_dialog.cpp b/Hesiod/src/gui/widgets/node_info_dialog.cpp index 96cff2901..42b9b1003 100644 --- a/Hesiod/src/gui/widgets/node_info_dialog.cpp +++ b/Hesiod/src/gui/widgets/node_info_dialog.cpp @@ -160,7 +160,7 @@ void NodeInfoDialog::setup_layout() // --- main label { - std::string str = ptrs.node->get_caption() + "/" + ptrs.node->get_id(); + std::string str = ptrs.node->get_label() + "/" + ptrs.node->get_id(); QLabel *label = new QLabel(str.c_str()); this->layout->addWidget(label); } @@ -251,7 +251,7 @@ void NodeInfoDialog::update_info_content() auto cfg = ptrs.node->get_config_ref(); std::vector rows = { - {"Type", ptrs.node->get_caption()}, + {"Type", ptrs.node->get_label()}, {"Category", ptrs.node->get_category()}, {"ID", ptrs.node->get_id()}, {"Created", timestamp(info.time_creation)}, @@ -310,14 +310,14 @@ void NodeInfoDialog::update_ports_content() for (int k = 0; k < ptrs.node->get_nports(); k++) { Row new_row; - new_row.caption = ptrs.node->get_port_caption(k); + new_row.caption = ptrs.node->get_port_label(k); new_row.is_connected = ptrs.node->is_port_connected(k); std::string str_ct = new_row.is_connected ? "✓" : " "; std::string str_in = std::format("→[{}] ", str_ct); std::string str_out = std::format(" [{}]→", str_ct); - new_row.type = (ptrs.node->get_port_type(k) == gngui::PortType::IN) ? str_in + new_row.type = (ptrs.node->get_port_type(k) == gnode::PortType::IN) ? str_in : str_out; new_row.data_type = map_type_name(ptrs.node->get_data_type(k)); diff --git a/Hesiod/src/gui/widgets/node_settings_widget.cpp b/Hesiod/src/gui/widgets/node_settings_widget.cpp index 2cc3d4f91..fca81ef8b 100644 --- a/Hesiod/src/gui/widgets/node_settings_widget.cpp +++ b/Hesiod/src/gui/widgets/node_settings_widget.cpp @@ -165,7 +165,7 @@ void NodeSettingsWidget::update_content() continue; } - const QString node_caption = QString::fromStdString(p_node->get_caption()); + const QString node_caption = QString::fromStdString(p_node->get_label()); const bool add_toolbar = HSD_CTX.app_settings.node_editor .show_node_toolbar_in_settings_pan; diff --git a/Hesiod/src/gui/widgets/viewers/viewer.cpp b/Hesiod/src/gui/widgets/viewers/viewer.cpp index ef9f41843..48d828a8b 100644 --- a/Hesiod/src/gui/widgets/viewers/viewer.cpp +++ b/Hesiod/src/gui/widgets/viewers/viewer.cpp @@ -452,10 +452,10 @@ void Viewer::update_widgets() } else if (BaseNode *p_node = this->safe_get_node()) { - std::string new_title = this->label + " - " + p_node->get_caption() + "(" + + std::string new_title = this->label + " - " + p_node->get_label() + "(" + p_node->get_id() + ")"; this->setWindowTitle(new_title.c_str()); - this->button_pin_current_node->set_label(p_node->get_caption().c_str()); + this->button_pin_current_node->set_label(p_node->get_label().c_str()); } // --- update combo content @@ -469,7 +469,7 @@ void Viewer::update_widgets() { combo_options.reserve(p_node->get_nports()); for (int k = 0; k < p_node->get_nports(); ++k) - combo_options.push_back(p_node->get_port_caption(k)); + combo_options.push_back(p_node->get_port_label(k)); } } diff --git a/Hesiod/src/gui/widgets/viewers/wild_guess_view_param.cpp b/Hesiod/src/gui/widgets/viewers/wild_guess_view_param.cpp index 85705c887..f198e0dfe 100644 --- a/Hesiod/src/gui/widgets/viewers/wild_guess_view_param.cpp +++ b/Hesiod/src/gui/widgets/viewers/wild_guess_view_param.cpp @@ -33,12 +33,12 @@ std::string helper_get_preferred_port_inout(const BaseNode &node { const std::string port_label = node.get_port_label(k); - if (node.get_port_type(k) == gngui::PortType::OUT && !is_excluded(port_label)) + if (node.get_port_type(k) == gnode::PortType::OUT && !is_excluded(port_label)) { value = port_label; break; // OUT has priority } - else if (in_candidate == -1 && node.get_port_type(k) == gngui::PortType::IN && + else if (in_candidate == -1 && node.get_port_type(k) == gnode::PortType::IN && !is_excluded(port_label)) { in_candidate = k; diff --git a/Hesiod/src/model/nodes/base_node.cpp b/Hesiod/src/model/nodes/base_node.cpp index 1354401cc..d1cb2ef76 100644 --- a/Hesiod/src/model/nodes/base_node.cpp +++ b/Hesiod/src/model/nodes/base_node.cpp @@ -76,7 +76,7 @@ const GraphConfig &BaseNode::cfg() const if (!ptr) { Logger::log()->critical("BaseNode::get_category: Config ptr is nullptr, node: {}/{}", - this->get_caption(), + this->get_label(), this->get_id()); throw std::runtime_error("Config ptr is nullptr."); } @@ -197,7 +197,7 @@ std::shared_ptr BaseNode::get_config_ref() const if (!ptr) { Logger::log()->critical("BaseNode::get_category: Config ptr is nullptr, node: {}/{}", - this->get_caption(), + this->get_label(), this->get_id()); throw std::runtime_error("Config ptr is nullptr."); } @@ -391,6 +391,13 @@ std::string BaseNode::get_documentation_short_html() const return html; } +std::string BaseNode::get_comment() const { return this->comment; } + +gnode::PortType BaseNode::get_port_type(int port_index) const +{ + return gnode::Node::get_port_type(this->get_port_label(port_index)); +} + std::string BaseNode::get_id() const { return gnode::Node::get_id(); } float BaseNode::get_memory_usage() const @@ -404,7 +411,7 @@ float BaseNode::get_memory_usage() const for (int k = 0; k < this->get_nports(); k++) { // only outputs carry data - if (this->get_port_type(k) == gngui::PortType::IN) + if (this->get_port_type(k) == gnode::PortType::IN) continue; if (this->get_data_type(k) == typeid(hmap::VirtualArray).name()) @@ -571,9 +578,9 @@ nlohmann::json BaseNode::node_parameters_to_json() const for (int k = 0; k < this->get_nports(); k++) { nlohmann::json port_info; - const std::string caption = this->get_port_caption(k); + const std::string caption = this->get_port_label(k); - port_info["type"] = (this->get_port_type(k) == gngui::PortType::IN) ? "input" + port_info["type"] = (this->get_port_type(k) == gnode::PortType::IN) ? "input" : "output"; port_info["caption"] = caption; port_info["data_type"] = map_type_name(this->get_data_type(k)); @@ -650,14 +657,14 @@ nlohmann::json BaseNode::node_parameters_to_json() const void BaseNode::propagate_config_change() { Logger::log()->trace("BaseNode::propagate_config_change: node {}/{}", - this->get_caption(), + this->get_label(), this->get_id()); const GraphConfig &cfg = *this->get_config_ref(); // go through the data and modify is needed (only outputs hold data) for (int k = 0; k < this->get_nports(); k++) - if (this->get_port_type(k) == gngui::PortType::OUT) + if (this->get_port_type(k) == gnode::PortType::OUT) { const std::string type = this->get_data_type(k); diff --git a/Hesiod/src/model/nodes/base_node_proxy.cpp b/Hesiod/src/model/nodes/base_node_proxy.cpp deleted file mode 100644 index 9dc7269cb..000000000 --- a/Hesiod/src/model/nodes/base_node_proxy.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright (c) 2023 Otto Link. Distributed under the terms of the GNU General - * Public License. The full license is in the file LICENSE, distributed with - * this software. */ -#include - -#include "gnode/graph.hpp" -#include "hesiod/app/hesiod_application.hpp" -#include "hesiod/logger.hpp" -#include "hesiod/model/nodes/base_node.hpp" - -namespace hesiod -{ - -std::string BaseNode::get_caption() const { return this->get_label(); } - -std::string BaseNode::get_comment() const { return this->comment; } - -void *BaseNode::get_data_ref(int port_index) -{ - return this->get_value_ref_void(port_index); -} - -std::string BaseNode::get_data_type(int port_index) const -{ - return gnode::Node::get_data_type(port_index); -} - -int BaseNode::get_nports() const { return gnode::Node::get_nports(); } - -std::string BaseNode::get_port_caption(int port_index) const -{ - return gnode::Node::get_port_label(port_index); -}; - -gngui::PortType BaseNode::get_port_type(int port_index) const -{ - gnode::PortType ptype = gnode::Node::get_port_type(this->get_port_label(port_index)); - - if (ptype == gnode::PortType::IN) - return gngui::PortType::IN; - else - return gngui::PortType::OUT; -} - -std::string BaseNode::get_tool_tip_text() { return this->get_documentation_short_html(); } - -} // namespace hesiod diff --git a/Hesiod/src/model/nodes/port_catalog.cpp b/Hesiod/src/model/nodes/port_catalog.cpp index cf0ede4a5..17b76ad1b 100644 --- a/Hesiod/src/model/nodes/port_catalog.cpp +++ b/Hesiod/src/model/nodes/port_catalog.cpp @@ -36,8 +36,8 @@ PortCatalog PortCatalog::from_documentation() info.name = port_name; info.data_type = port["data_type"].get(); info.direction = (port["type"].get() == "input") - ? gngui::PortType::IN - : gngui::PortType::OUT; + ? gnode::PortType::IN + : gnode::PortType::OUT; infos.push_back(std::move(info)); } @@ -55,7 +55,7 @@ const std::vector *PortCatalog::find(const std::string &node_type) con bool PortCatalog::is_offerable(const std::string &node_type, const std::string &data_type, - gngui::PortType wanted_direction) const + gnode::PortType wanted_direction) const { const std::vector *infos = this->find(node_type); @@ -73,14 +73,14 @@ bool PortCatalog::is_offerable(const std::string &node_type, namespace { -bool is_conventional_name(const std::string &name, gngui::PortType direction) +bool is_conventional_name(const std::string &name, gnode::PortType direction) { std::string lower; lower.reserve(name.size()); for (char c : name) lower += static_cast(std::tolower(static_cast(c))); - if (direction == gngui::PortType::IN) + if (direction == gnode::PortType::IN) return lower == "input" || lower == "in"; return lower == "output" || lower == "out"; @@ -90,7 +90,7 @@ bool is_conventional_name(const std::string &name, gngui::PortType direction) std::optional select_port(const BaseNode &node, const std::string &data_type, - gngui::PortType wanted_direction) + gnode::PortType wanted_direction) { std::optional first_match; diff --git a/docs/graph-editor-refactor.md b/docs/graph-editor-refactor.md index 14e4bdd6e..d018e99a7 100644 --- a/docs/graph-editor-refactor.md +++ b/docs/graph-editor-refactor.md @@ -30,7 +30,7 @@ deletion. They do not yet test Hesiod model computation. GNodeGUI PR #12 is merged. It supplies the dependency for the Hesiod extraction below; by itself it retains the legacy behavior for existing callers. -## 2. Extract GraphEditor and move topology edits (this change) +## 2. Extract GraphEditor and move topology edits (PR #771) GraphNodeWidget now owns one GraphEditor and delegates node creation/deletion, connection/disconnection, replacement, chain insertion, paste/duplicate, import @@ -79,18 +79,19 @@ cmake --build build --target hesiod test_graph_editor ctest --test-dir build -R '^graph_editor$' --output-on-failure ``` -## 3. Move the Hesiod node proxy into the GUI layer +## 3. Move the Hesiod node proxy into the GUI layer (this change) -Introduce an explicit Hesiod adapter implementing GNodeGUI's NodeProxy interface. -Move GUI port conversion and presentation responsibilities out of BaseNode. Use -GNode's port direction type in model APIs and convert it at the adapter boundary. -Give the proxy a clear owner and make identifier/lifetime behavior explicit. +`HesiodNodeProxy` adapts GNode's accessors and port directions to GNodeGUI. +BaseNode no longer includes GNodeGUI or implements its proxy interface. +PortCatalog and select_port also use GNode's direction type. -BaseNode currently includes `gnodegui/node_proxy.hpp`, which includes Qt. Thus -GraphNode's header has no direct Qt include, but the model implementation is not -yet Qt-independent. Verify removal with a model-header compilation check without -Qt include paths, as well as the normal application build. Avoid changing saved -node identifiers, port IDs or captions as an incidental effect of this move. +The widget owns each proxy through QObject parenting; the model reference is weak. +Identifiers, captions and serialized fields retain their previous values. Expired +models return empty proxy values. + +Tests cover the adapter, ownership and model expiry. Building `test_graph_editor` +also compiles BaseNode, GraphNode and PortCatalog headers without Qt or GNodeGUI +include paths. Model implementations still use Qt and application services. ## 4. Loading, settings and remaining update paths @@ -102,8 +103,9 @@ widgets and retain legacy port migration behavior. Route GUI settings, configuration and reload requests through the editor's update policy. Inventory other direct calls, including NodeAttributesWidget and special node widgets, to complete the editor update policy. The widget blocker was removed -in step 2. Domain-driven broadcasting and headless execution remain valid model callers; the editor is the single entry -point for interactive edits, not a compulsory Qt dependency for all computation. +in step 2. Domain-driven broadcasting and headless execution remain valid model +callers; the editor is the single entry point for interactive edits, not a +compulsory Qt dependency for all computation. Finish with project round-trip, paste/import, legacy project, selection/viewer lifetime and Broadcast/Receive regression checks. The existing editor widget API diff --git a/tests/gui/test_graph_editor.cpp b/tests/gui/test_graph_editor.cpp index 78f01d37d..ecb58b83e 100644 --- a/tests/gui/test_graph_editor.cpp +++ b/tests/gui/test_graph_editor.cpp @@ -14,6 +14,7 @@ #include "gnodegui/style.hpp" #include "hesiod/app/hesiod_application.hpp" #include "hesiod/gui/graph_editor.hpp" +#include "hesiod/gui/hesiod_node_proxy.hpp" #include "hesiod/gui/widgets/graph_node_widget.hpp" #include "hesiod/model/graph/graph_manager.hpp" #include "hesiod/model/graph/graph_node.hpp" @@ -58,13 +59,13 @@ bool consistent(GraphNode &graph, gngui::GraphViewer &view) view_links.size() == json.at("links").size(); } -class UnavailableOutputProxy : public gngui::TypedNodeProxy +class UnavailableOutputProxy : public HesiodNodeProxy { public: - using gngui::TypedNodeProxy::TypedNodeProxy; + using HesiodNodeProxy::HesiodNodeProxy; std::string get_port_id(int index) const override { - return index == 1 ? "unavailable" : TypedNodeProxy::get_port_id(index); + return index == 1 ? "unavailable" : HesiodNodeProxy::get_port_id(index); } }; @@ -97,9 +98,8 @@ struct Fixture auto node = graph->get_node_ref_by_id(id)->get_shared(); gngui::NodeProxy *proxy = fail_reconnection ? static_cast( - new UnavailableOutputProxy(node)) - : new gngui::TypedNodeProxy(node); - proxy->setParent(&view); + new UnavailableOutputProxy(node, &view)) + : new HesiodNodeProxy(node, &view); view.add_node(proxy, position, id); if (fail_presentation) throw std::runtime_error("Injected presentation failure"); @@ -153,6 +153,74 @@ class GraphEditorTest : public QObject { Q_OBJECT private Q_SLOTS: + void proxy_preserves_node_identity_and_port_values() + { + Fixture f; + const auto id = f.add(); + auto *node = f.graph->get_node_ref_by_id(id); + const auto *proxy = f.view.get_graphics_node_by_id(id)->get_proxy_ref(); + QVERIFY(dynamic_cast(proxy)); + QCOMPARE(proxy->get_id(), id); + QCOMPARE(proxy->get_caption(), std::string("Thru")); + QCOMPARE(proxy->get_category(), node->get_category()); + QCOMPARE(proxy->get_tool_tip_text(), node->get_documentation_short_html()); + QCOMPARE(proxy->get_nports(), 2); + QCOMPARE(proxy->get_port_id(0), std::string("input")); + QCOMPARE(proxy->get_port_id(1), std::string("output")); + QCOMPARE(proxy->get_port_caption(0), std::string("input")); + QCOMPARE(proxy->get_port_caption(1), std::string("output")); + QCOMPARE(proxy->get_port_type(0), gngui::PortType::IN); + QCOMPARE(proxy->get_port_type(1), gngui::PortType::OUT); + QCOMPARE(proxy->get_data_type(1), std::string(typeid(hmap::VirtualArray).name())); + QCOMPARE(proxy->get_data_ref(1), node->get_value_ref_void(1)); + node->set_comment("Updated comment"); + QCOMPARE(proxy->get_comment(), std::string("Updated comment")); + QCOMPARE(f.view.json_to()["nodes"][0]["id"].get(), id); + QCOMPARE(node->json_to()["id"].get(), id); + } + + void proxy_does_not_extend_model_lifetime() + { + QObject owner; + auto node = std::make_shared(); + node->set_id("before"); + auto *proxy = new HesiodNodeProxy(node, &owner); + proxy->set_id("after"); + QCOMPARE(node->get_id(), std::string("after")); + std::weak_ptr weak = node; + node.reset(); + QVERIFY(weak.expired()); + proxy->set_id("expired"); + QVERIFY(proxy->get_id().empty()); + QVERIFY(proxy->get_caption().empty()); + QVERIFY(proxy->get_category().empty()); + QVERIFY(proxy->get_comment().empty()); + QVERIFY(proxy->get_tool_tip_text().empty()); + QCOMPARE(proxy->get_nports(), 0); + QVERIFY(proxy->get_port_id(0).empty()); + QVERIFY(proxy->get_port_caption(0).empty()); + QVERIFY(proxy->get_data_type(0).empty()); + QCOMPARE(proxy->get_port_type(0), gngui::PortType::IN); + QVERIFY(proxy->get_data_ref(0) == nullptr); + } + + void widget_owns_its_node_proxies() + { + auto graph = std::make_shared("owner", small_config()); + QPointer proxy; + { + GraphNodeWidget widget(graph); + widget.scene()->setParent(&widget); + const auto id = widget.on_new_node_request("Thru", {}); + proxy = widget.get_graphics_node_by_id(id)->get_proxy_ref(); + QCOMPARE(proxy->parent(), &widget); + widget.erase_node(id); + QVERIFY(proxy); + } + QVERIFY(proxy.isNull()); + QCOMPARE(graph->get_nodes().size(), size_t(1)); + } + void nested_batches_compute_only_after_commit() { Fixture f; diff --git a/tests/model/model_headers.cpp b/tests/model/model_headers.cpp new file mode 100644 index 000000000..d94d51b53 --- /dev/null +++ b/tests/model/model_headers.cpp @@ -0,0 +1,20 @@ +/* Copyright (c) 2026 Otto Link. Distributed under the terms of the GNU General + * Public License. The full license is in the file LICENSE, distributed with + * this software. */ +#include +#include + +#if __has_include() || __has_include("gnodegui/node_proxy.hpp") +#error "Compile model headers without Qt or GNodeGUI include paths" +#endif + +#include "hesiod/model/graph/graph_node.hpp" +#include "hesiod/model/nodes/base_node.hpp" +#include "hesiod/model/nodes/port_catalog.hpp" + +static_assert( + std::is_same_v().get_port_type(0)), + gnode::PortType>); +static_assert(std::is_same_v() + .get_port_type(std::string{})), + gnode::PortType>);