diff --git a/src/fw/core/hash.h b/src/fw/core/hash.h index eeced19bb..8dbf30f49 100644 --- a/src/fw/core/hash.h +++ b/src/fw/core/hash.h @@ -7,10 +7,12 @@ namespace fw { namespace hash { - inline static size_t hash(char* buffer, size_t buf_size, size_t seed = 0) - { return XXHash32::hash(buffer, buf_size, seed); } + using hash_t = u32_t; - inline static size_t hash(const char* str, size_t seed = 0) - { return XXHash32::hash(str, strlen(str), seed); } + inline static hash_t hash(char* buffer, size_t buf_size, uint32_t seed = 0) + { return XXHash32::hash(buffer, (uint64_t)buf_size, seed); } + + inline static hash_t hash(const char* str, uint32_t seed = 0) + { return XXHash32::hash(str, (uint64_t)strlen(str), seed); } }; } \ No newline at end of file diff --git a/src/fw/gui-example/AppExampleView.h b/src/fw/gui-example/AppExampleView.h index 1f81bdb62..918c26b27 100644 --- a/src/fw/gui-example/AppExampleView.h +++ b/src/fw/gui-example/AppExampleView.h @@ -18,12 +18,16 @@ namespace fw dock_window( "top", fw::AppView::Dockspace_TOP ); } - void on_draw_splashscreen() override + void draw_splashscreen() override { - ImGui::TextWrapped( "Welcome to the framework-gui-example app.\nThis demonstrates how to use the framework-gui library." ); - ImGui::Separator(); - ImGui::TextWrapped( "\nFor your information, this is the splashscreen window of the app.\n" - "You can inject your custom code by editing Example::on_draw_splashscreen()\n" ); + if ( AppView::begin_splashscreen() ) + { + ImGui::TextWrapped( "Welcome to the framework-gui-example app.\nThis demonstrates how to use the framework-gui library." ); + ImGui::Separator(); + ImGui::TextWrapped( "\nFor your information, this is the splashscreen window of the app.\n" + "You can inject your custom code by editing Example::draw_splashscreen()\n" ); + AppView::end_splashscreen(); + } } }; } \ No newline at end of file diff --git a/src/fw/gui/Action.cpp b/src/fw/gui/Action.cpp new file mode 100644 index 000000000..d6734f0fa --- /dev/null +++ b/src/fw/gui/Action.cpp @@ -0,0 +1,15 @@ +#include "Action.h" +#include "EventManager.h" + +using namespace fw; + +void IAction::trigger() const +{ + EventManager& event_manager = EventManager::get_instance(); + event_manager.dispatch( make_event() ); +} + +IEvent* IAction::make_event() const +{ + return new IEvent(event_id); +} \ No newline at end of file diff --git a/src/fw/gui/Action.h b/src/fw/gui/Action.h new file mode 100644 index 000000000..9755a21af --- /dev/null +++ b/src/fw/gui/Action.h @@ -0,0 +1,87 @@ +#pragma once +#include "Event.h" +#include "core/types.h" +#include +#include + +namespace fw +{ + /** Data describing a shortcut (ex: "Reset": Ctrl + Alt + R) */ + struct Shortcut + { + SDL_Keycode key = SDLK_UNKNOWN; // a key to be pressed + SDL_Keymod mod = KMOD_NONE; // modifiers (alt, ctrl, etc.) + std::string description; + std::string to_string() const; + }; + + /** + * The purpose of any IAction is to trigger a given basic event (identified by an EventID) + * when a key shortcut is pressed. + */ + class IAction + { + public: + explicit IAction( + EventID event_id, + const char* label = "action", + Shortcut&& shortcut = {}, + u64_t userdata = {} + ) + : label(label) + , event_id(event_id) + , shortcut(std::move(shortcut)) + , userdata(userdata) + {} + std::string label; + EventID event_id; + Shortcut shortcut; + u64_t userdata; + + void trigger() const; // Trigger action, will dispatch an event with default values + virtual IEvent* make_event() const; // Make a new event with default values + }; + + /** + * The purpose of an Action is similar to IAction for events requiring some data to be constructed + */ + template + class Action : public IAction + { + public: + static_assert( !std::is_base_of_v ); // Ensure EventT implements IEvent + + using event_t = EventT; // Type of the event triggered by this action + using event_data_t = typename EventT::data_t; // Type of the payload for the events triggered by this action + + event_data_t event_data; // Initial data used when making a new event + + Action( + const char* label, + Shortcut&& shortcut + ) + : IAction(EventT::id, label, std::move(shortcut) ) + {} + + Action( + const char* label, // Text to display for this action + Shortcut&& shortcut, // Shortcut able to trigger this action + u64_t userdata // Custom user data (typically to store flags) + ) + : IAction(EventT::id, label, std::move(shortcut), userdata) + {} + + Action( + const char* label, // Text to display for this action + Shortcut&& shortcut, // Shortcut able to trigger this action + event_data_t event_data, // Initial data of a default event + u64_t userdata = {} // Custom user data (typically to store flags) + ) + : IAction(EventT::id, label, std::move(shortcut), userdata) + , event_data( event_data ) + {} + + EventT* make_event() const override // Make a new event using default event_data + { return new EventT( event_data ); } + }; +} \ No newline at end of file diff --git a/src/fw/gui/ActionManager.cpp b/src/fw/gui/ActionManager.cpp new file mode 100644 index 000000000..09c27314b --- /dev/null +++ b/src/fw/gui/ActionManager.cpp @@ -0,0 +1,73 @@ +#include "ActionManager.h" +#include "core/assertions.h" +#include "core/async.h" +#include "core/log.h" +#include "core/reflection/type.h" +#include +#include +#include + +using namespace fw; + +ActionManager* ActionManager::s_instance = nullptr; + +ActionManager::ActionManager() +{ + LOG_VERBOSE("fw::ActionManager", "Constructor ...\n"); + FW_EXPECT(!s_instance, "cannot have two instances at a time"); + s_instance = this; + LOG_VERBOSE("fw::ActionManager", "Constructor " OK "\n"); +} + +ActionManager::~ActionManager() +{ + LOG_VERBOSE("fw::ActionManager", "Destructor ...\n"); + s_instance = nullptr; + for( auto action : m_actions ) + { + delete action; + } + LOG_VERBOSE("fw::ActionManager", "Destructor " OK "\n"); +} + +ActionManager& ActionManager::get_instance() +{ + FW_EXPECT(s_instance, "No instance found."); + return *s_instance; +} + +const IAction* ActionManager::get_action_with_id(EventID id) +{ + auto found = m_actions_by_id.find(id); + if ( found == m_actions_by_id.end() ) + { + string128 str; + str.append_fmt("Unable to find an action bound to EventId %i\n", id); + FW_EXPECT(false, str.c_str() ); + } + return found->second; +} + +const std::vector& ActionManager::get_actions() const +{ + return m_actions; +} + +void ActionManager::add_action( IAction* _action )// Add a new action (can be triggered via shortcut) +{ + m_actions.push_back( _action ); + m_actions_by_id.insert(std::pair{_action->event_id, _action}); + LOG_MESSAGE("ActionManager", "Action '%s' bound to the event_id %i\n", _action->label.c_str(), _action->event_id); +} + +std::string Shortcut::to_string() const +{ + std::string result; + + if (mod & KMOD_CTRL) result += "Ctrl + "; + if (mod & KMOD_ALT) result += "Alt + "; + if (key) result += SDL_GetKeyName(key); + if (!description.empty()) result += description; + + return result; +} diff --git a/src/fw/gui/ActionManager.h b/src/fw/gui/ActionManager.h new file mode 100644 index 000000000..cce28a03c --- /dev/null +++ b/src/fw/gui/ActionManager.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Action.h" +#include "Event.h" +#include "core/reflection/func_type.h" +#include "core/types.h" + +namespace fw +{ + class ActionManager + { + public: + ActionManager(); + ActionManager(const ActionManager&) = delete; + ~ActionManager(); + + const std::vector& get_actions() const; // Get all the actions bound to any event + + template + Action* new_action(Args&&... args) + { + static_assert(std::is_base_of_v ); + auto* action = new Action(std::forward(args)...); + add_action(action); + return action; + } + + const IAction* get_action_with_id(EventID id); // Get the action bound to a given event type + static ActionManager& get_instance(); + + template + static ActionT* get_action() // Helper to get a given action type from the ActionManager instance + { return s_instance->get_action(); } + + private: + + template + ActionT* _get_action() const + { + static_assert( std::is_base_of_v ); + auto* action = get_action_with_id( ActionT::event_id ); + return dynamic_cast( action ); + } + + void add_action( IAction* _action); + static ActionManager* s_instance; + std::vector m_actions; + std::unordered_multimap m_actions_by_id; + }; +} \ No newline at end of file diff --git a/src/fw/gui/ActionManagerView.cpp b/src/fw/gui/ActionManagerView.cpp new file mode 100644 index 000000000..3fb5ec3db --- /dev/null +++ b/src/fw/gui/ActionManagerView.cpp @@ -0,0 +1,33 @@ +#include "ActionManagerView.h" +#include "ActionManager.h" +#include "imgui.h" + +using namespace fw; + +void ActionManagerView::draw(ActionManager* manager) +{ + if ( ImGui::BeginTable("Actions", 2) ) + { + ImGui::TableSetupColumn("Action"); + ImGui::TableSetupColumn("Shortcut"); + ImGui::TableHeadersRow(); + + for( auto& action : manager->get_actions()) + { + ImGui::PushID(action); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if( ImGui::SmallButton("trigger") ) + { + action->trigger(); + } + ImGui::SameLine(); + ImGui::Text("%s", action->label.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%s", action->shortcut.to_string().c_str()); // TODO: handle shortcut edition + ImGui::PopID(); + } + ImGui::EndTable(); + } +} + diff --git a/src/fw/gui/ActionManagerView.h b/src/fw/gui/ActionManagerView.h new file mode 100644 index 000000000..d0e0a792b --- /dev/null +++ b/src/fw/gui/ActionManagerView.h @@ -0,0 +1,12 @@ +#pragma once + +namespace fw +{ + // Forward declaration + class ActionManager; + + class ActionManagerView + { + public: static void draw(ActionManager* manager); + }; +} \ No newline at end of file diff --git a/src/fw/gui/App.cpp b/src/fw/gui/App.cpp index 313a7ef29..4bf1c7291 100644 --- a/src/fw/gui/App.cpp +++ b/src/fw/gui/App.cpp @@ -18,6 +18,7 @@ App::App(Config& _config, AppView* _view) , should_stop(false) , font_manager(_config.font_manager) , event_manager() + , action_manager() , texture_manager() , m_sdl_window(nullptr) , m_sdl_gl_context() @@ -240,31 +241,29 @@ void App::handle_events() // With mode key only if( event.key.keysym.mod & (KMOD_CTRL | KMOD_ALT) ) { - for(const auto& _binded_event: event_manager.get_binded_events() ) + for(const IAction* _action: action_manager.get_actions() ) { // first, priority to shortcuts with mod - if ( _binded_event.shortcut.mod != KMOD_NONE - && _binded_event.event_t - && (_binded_event.shortcut.mod & event.key.keysym.mod) - && _binded_event.shortcut.key == event.key.keysym.sym + if ( _action->shortcut.mod != KMOD_NONE + && _action->event_id && ( _action->shortcut.mod & event.key.keysym.mod) + && _action->shortcut.key == event.key.keysym.sym ) { - event_manager.push(_binded_event.event_t); + event_manager.dispatch( _action->event_id ); break; } } } else // without any mod key { - for(const auto& _binded_event: event_manager.get_binded_events() ) + for(const IAction* _action: action_manager.get_actions() ) { // first, priority to shortcuts with mod - if ( _binded_event.shortcut.mod == KMOD_NONE - && _binded_event.event_t - && _binded_event.shortcut.key == event.key.keysym.sym + if ( _action->shortcut.mod == KMOD_NONE + && _action->event_id && _action->shortcut.key == event.key.keysym.sym ) { - event_manager.push(_binded_event.event_t); + event_manager.dispatch( _action->event_id ); break; } } @@ -340,9 +339,9 @@ int App::main(int argc, char *argv[]) if( all_time <= 0 ) all_time = 1; dt = u32_t(0.9f*float(dt) + 0.1f*float(all_time)); // Smooth value u32_t fps = 1000 / dt; - char title[255]; - snprintf( title, 255, "%s | %i fps (dt %d ms, frame %d ms)", config.app_window_label.c_str(), fps, dt, frame_time ); - title[254] = '\0'; + char title[256]; + snprintf( title, 256, "%s | %i fps (dt %d ms, frame %d ms)", config.app_window_label.c_str(), fps, dt, frame_time ); + title[255] = '\0'; SDL_SetWindowTitle( m_sdl_window, title ); } } diff --git a/src/fw/gui/App.h b/src/fw/gui/App.h index 82c5b13d6..c3158eb12 100644 --- a/src/fw/gui/App.h +++ b/src/fw/gui/App.h @@ -11,6 +11,8 @@ #include "FontManager.h" #include "AppView.h" #include "TextureManager.h" +#include "EventManager.h" +#include "ActionManager.h" namespace fw { @@ -28,6 +30,7 @@ namespace fw TextureManager texture_manager; // Manages Texture resources FontManager font_manager; // Manages Font resources EventManager event_manager; // Manages Events and BindedEvents (shortcuts/button triggered) + ActionManager action_manager; // Manages Events and BindedEvents (shortcuts/button triggered) bool should_stop; // Set this field true to tell the application to stop its main loop the next frame Config& config; // Application configuration (names, colors, fonts) diff --git a/src/fw/gui/AppView.cpp b/src/fw/gui/AppView.cpp index 577c20ea0..ba2a7eea3 100644 --- a/src/fw/gui/AppView.cpp +++ b/src/fw/gui/AppView.cpp @@ -43,7 +43,7 @@ bool AppView::draw() } // Splashscreen - draw_splashscreen_window(); + draw_splashscreen(); // Setup main window @@ -182,7 +182,15 @@ void AppView::dock_window(const char* window_name, Dockspace dockspace)const ImGui::DockBuilderDockWindow(window_name, m_dockspaces[dockspace]); } -void AppView::draw_splashscreen_window() +void AppView::draw_splashscreen() +{ + if ( AppView::begin_splashscreen() ) + { + AppView::end_splashscreen(); + } +} + +bool AppView::begin_splashscreen() { if (m_app->config.splashscreen && !ImGui::IsPopupOpen(m_app->config.splashscreen_window_label)) { @@ -194,14 +202,15 @@ void AppView::draw_splashscreen_window() auto flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize; - if (ImGui::BeginPopupModal(m_app->config.splashscreen_window_label, &m_app->config.splashscreen, flags)) - { - on_draw_splashscreen(); - ImGui::EndPopup(); - } + return ImGui::BeginPopupModal(m_app->config.splashscreen_window_label, &m_app->config.splashscreen, flags); +} + +void AppView::end_splashscreen() +{ + ImGui::EndPopup(); } -void AppView::draw_status_window() const +void AppView::draw_status_window() { if (ImGui::Begin(k_status_window_name)) { diff --git a/src/fw/gui/AppView.h b/src/fw/gui/AppView.h index 8fc0174de..eb12aee11 100644 --- a/src/fw/gui/AppView.h +++ b/src/fw/gui/AppView.h @@ -68,16 +68,19 @@ namespace fw virtual void on_init() {}; virtual void on_draw() {}; virtual void on_reset_layout() {}; - virtual void on_draw_splashscreen() {}; + virtual void draw_splashscreen(); // If needed, use begin/end_splashscreen static methods to override this. Ex: if ( AppView::begin_splashscreen(m_app->config) ) { /* your code here */; AppView::end_splashscreen(); } bool draw(); + void draw_status_window(); ImGuiID get_dockspace(Dockspace)const; bool pick_file_path(std::string& _out_path, DialogType) const; // pick a file and store its path in _out_path void dock_window(const char* window_name, Dockspace)const; // Must be called ON_RESET_LAYOUT void set_layout_initialized(bool b); + + protected: + bool begin_splashscreen(); + void end_splashscreen(); private: - void draw_splashscreen_window(); - void draw_status_window() const; App * m_app; bool m_is_layout_initialized; std::array m_dockspaces{}; diff --git a/src/fw/gui/CMakeLists.txt b/src/fw/gui/CMakeLists.txt index e09b297f2..3448f6167 100644 --- a/src/fw/gui/CMakeLists.txt +++ b/src/fw/gui/CMakeLists.txt @@ -6,6 +6,9 @@ ndbl_log_title_header() # add imgui executable add_library( framework-gui + Action.cpp + ActionManager.cpp + ActionManagerView.cpp App.cpp AppView.cpp EventManager.cpp diff --git a/src/fw/gui/Config.h b/src/fw/gui/Config.h index 2f8f4030a..a57401c18 100644 --- a/src/fw/gui/Config.h +++ b/src/fw/gui/Config.h @@ -112,6 +112,11 @@ namespace fw colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.55f); + colors[ImGuiCol_TableBorderLight] = ImVec4(0.20f, 0.20f, 0.20f, 0.80f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.20f, 0.20f, 0.20f, 0.90f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.60f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.40f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); _style.WindowBorderSize = 1.0f; _style.FrameBorderSize = 1.0f; diff --git a/src/fw/gui/Event.h b/src/fw/gui/Event.h new file mode 100644 index 000000000..94d0da27b --- /dev/null +++ b/src/fw/gui/Event.h @@ -0,0 +1,80 @@ +#pragma once +#include "core/types.h" +#include + +namespace fw +{ + typedef u16_t EventID; + /** + * Enumerate event types + * Can be extended starting at EventID_USER_DEFINED + */ + enum EventID_ : u16_t + { + // Declare common event types + + EventID_NULL = 0, + + EventID_FILE_SAVE, + EventID_FILE_SAVE_AS, + EventID_FILE_NEW, + EventID_FILE_CLOSE, + EventID_FILE_BROWSE, + EventID_UNDO, + EventID_REDO, + EventID_REQUEST_EXIT, + EventID_REQUEST_SHOW_WINDOW, + + EventID_FILE_OPENED, + + EventID_USER_DEFINED = 0xff, + }; + + /** Basic event, can be extended via CustomEvent */ + class IEvent + { + public: + const EventID id; + constexpr explicit IEvent(EventID id): id(id) {} + virtual ~IEvent() = default; + }; + + struct null_data_t {}; + + /** Template to extend IEvent with a specific payload */ + template + class Event : public fw::IEvent + { + public: + constexpr static EventID id = id_value; + using data_t = DataT; // type required to construct this Event + + DataT data; + + explicit Event(DataT _data = {}) + : IEvent(id_value) + , data( _data ) + {} + }; + + // Below, few basic events (not requiring any payload) + + using Event_NULL = Event; + using Event_FileSave = Event; + using Event_FileSaveAs = Event; + using Event_FileClose = Event; + using Event_FileBrowse = Event; + using Event_FileNew = Event; + using Event_Exit = Event; + using Event_Undo = Event; + using Event_Redo = Event; + + // Here, an event requiring the following payload + + struct EventPayload_ShowWindow + { + std::string window_id; // String identifying a given window (user defined) + bool visible = true; // Window visibility (desired state) + }; + using Event_ShowWindow = Event; +} \ No newline at end of file diff --git a/src/fw/gui/EventManager.cpp b/src/fw/gui/EventManager.cpp index 4c21c600a..3ef7d4e14 100644 --- a/src/fw/gui/EventManager.cpp +++ b/src/fw/gui/EventManager.cpp @@ -1,58 +1,16 @@ #include "EventManager.h" +#include "core/assertions.h" +#include "core/async.h" +#include "core/log.h" +#include "core/reflection/type.h" #include #include #include -#include "core/log.h" -#include "core/assertions.h" -#include "core/async.h" using namespace fw; EventManager* EventManager::s_instance = nullptr; -void EventManager::push_event(Event &_event) -{ - m_events.push(_event); -} - -size_t EventManager::poll_event(Event &_event) -{ - size_t count = m_events.size(); - - if( count ) - { - _event = m_events.front(); - m_events.pop(); - } - - return count; -} - -void EventManager::push(EventType _type) -{ - Event simple_event = { _type }; - push_event(simple_event); -} - -void EventManager::bind(const BindedEvent &binded_cmd) -{ - m_binded_events.push_back(binded_cmd); - m_binded_events_by_type.insert({binded_cmd.event_t, binded_cmd}); -} - -const BindedEvent &EventManager::get_binded(uint16_t type) -{ - return m_binded_events_by_type.at(type); -} -const std::vector &EventManager::get_binded_events() const -{ - return m_binded_events; -} -EventManager& EventManager::get_instance() -{ - FW_EXPECT(s_instance, "No instance found."); - return *s_instance; -} EventManager::~EventManager() { LOG_VERBOSE("fw::EventManager", "Destructor ...\n"); @@ -67,25 +25,42 @@ EventManager::EventManager() LOG_VERBOSE("fw::EventManager", "Constructor " OK "\n"); } -void EventManager::push_async(EventType type, u64_t delay) +EventManager& EventManager::get_instance() { - fw::async::get_instance().add_task( - std::async(std::launch::async, [this, type, delay]() -> void { - std::this_thread::sleep_for(std::chrono::milliseconds{delay}); - push(type); - }) - ); + FW_EXPECT(s_instance, "No instance found."); + return *s_instance; } +void EventManager::dispatch(IEvent* _event) +{ + m_events.push(_event); +} -std::string Shortcut::to_string() const +IEvent* EventManager::poll_event() { - std::string result; + if ( m_events.empty() ) + { + return nullptr; + } - if (mod & KMOD_CTRL) result += "Ctrl + "; - if (mod & KMOD_ALT) result += "Alt + "; - if (key) result += SDL_GetKeyName(key); - if (!description.empty()) result += description; + IEvent* next_event = m_events.front(); + m_events.pop(); + return next_event; +} - return result; +IEvent* EventManager::dispatch( EventID _event_id ) +{ + auto new_event = new IEvent{ _event_id }; + dispatch(new_event ); + return new_event; } + +void EventManager::dispatch_delayed(u64_t delay, IEvent* event) +{ + fw::async::get_instance().add_task( + std::async(std::launch::async, [this, event, delay]() -> void { + std::this_thread::sleep_for(std::chrono::milliseconds{delay}); + dispatch(event); + }) + ); +} \ No newline at end of file diff --git a/src/fw/gui/EventManager.h b/src/fw/gui/EventManager.h index 3c3b47af8..7f7a5c4a4 100644 --- a/src/fw/gui/EventManager.h +++ b/src/fw/gui/EventManager.h @@ -4,61 +4,15 @@ #include #include #include -#include +#include +#include "Action.h" +#include "Event.h" +#include "core/reflection/func_type.h" #include "core/types.h" namespace fw { - struct Shortcut - { - SDL_Keycode key = SDLK_UNKNOWN; // a key to be pressed - SDL_Keymod mod = KMOD_NONE; // modifiers (alt, ctrl, etc.) - std::string description; - std::string to_string() const; - }; - - - // Declare some event types. - // EventType can be extended starting at EventType_USER_DEFINED - typedef u16_t EventType; - enum EventType_ : u16_t - { - // Declare common event types - - EventType_none = 0, - EventType_save_file_triggered, // operation on files - EventType_save_file_as_triggered, - EventType_new_file_triggered, - EventType_close_file_triggered, - EventType_browse_file_triggered, - EventType_file_opened, - EventType_undo_triggered, // history - EventType_redo_triggered, - EventType_exit_triggered, // general - EventType_show_splashscreen_triggered, - - EventType_USER_DEFINED = 0xff, - }; - - struct SimpleEvent { - EventType type; - }; - - union Event - { - EventType type; - SimpleEvent common; - }; - - struct BindedEvent - { - std::string label; - u16_t event_t; - Shortcut shortcut; - u16_t condition; - }; - class EventManager { public: @@ -66,20 +20,39 @@ namespace fw EventManager(const EventManager&) = delete; ~EventManager(); - void push_event(Event& _event); - size_t poll_event(Event& _event); - void push(EventType _type); - void push_async(EventType, u64_t); // Push an event with a delay in millisecond - const std::vector& get_binded_events() const; - void bind(const BindedEvent& binded_cmd); - const BindedEvent& get_binded(u16_t type); - - static EventManager& get_instance(); - + IEvent* dispatch(EventID); // Create and push a basic event to the queue + void dispatch(IEvent* _event); // Push an existing event to the queue. + void dispatch_delayed(u64_t, IEvent* ); // Does the same as dispatch(Event*) with a delay in millisecond. A delay of 0ms will be processed after a regular dispatch though. + IEvent* poll_event(); // Pop the first event in the queue + + template + void dispatch(Args... args) + { + static_assert(std::is_base_of_v ); + IEvent* event = new EventT(args...); + dispatch(event); + } + + template + void dispatch_delayed(u64_t delay_in_ms, typename EventT::data_t data) + { + static_assert(std::is_base_of_v ); + IEvent* event = new EventT(data); + dispatch_delayed(delay_in_ms, event); + } + + template + IEvent* dispatch(typename EventT::data_t&& state = {} ) + { + static_assert( std::is_base_of_v ); + auto new_event = new EventT(state); + dispatch(new_event); + return new_event; + } + + static EventManager& get_instance(); private: - static EventManager* s_instance; - std::queue m_events; - std::vector m_binded_events; - std::map m_binded_events_by_type; + static EventManager* s_instance; + std::queue m_events; }; } \ No newline at end of file diff --git a/src/fw/gui/ImGuiEx.cpp b/src/fw/gui/ImGuiEx.cpp index 2d827346b..a28c3a1a5 100644 --- a/src/fw/gui/ImGuiEx.cpp +++ b/src/fw/gui/ImGuiEx.cpp @@ -227,15 +227,6 @@ void ImGuiEx::BeginFrame() s_is_any_tooltip_open = false; } -void ImGuiEx::MenuItemBindedToEvent(uint16_t type, bool selected, bool enable) -{ - auto binded_evt = EventManager::get_instance().get_binded(type); - if (ImGui::MenuItem( binded_evt.label.c_str(), binded_evt.shortcut.to_string().c_str(), selected, enable)) - { - EventManager::get_instance().push(binded_evt.event_t); - } -} - void ImGuiEx::BulletTextWrapped(const char* str) { ImGui::Bullet(); ImGui::SameLine(); @@ -270,4 +261,4 @@ void ImGuiEx::Image(Texture* _texture) (float)_texture->height }; ImGui::Image((void *)(intptr_t)_texture->gl_handler, size); -} \ No newline at end of file +} diff --git a/src/fw/gui/ImGuiEx.h b/src/fw/gui/ImGuiEx.h index 7619a2d2c..2d29f8f9e 100644 --- a/src/fw/gui/ImGuiEx.h +++ b/src/fw/gui/ImGuiEx.h @@ -6,9 +6,10 @@ #define IMGUI_DEFINE_MATH_OPERATORS #endif -#include +#include "ActionManager.h" #include "EventManager.h" #include "core/types.h" +#include #include namespace fw @@ -106,7 +107,17 @@ namespace fw static void EndTooltip(); static ImRect& EnlargeToInclude(ImRect& _rect, ImRect _other); - static void MenuItemBindedToEvent(uint16_t type, bool selected = false, bool enable = true); + template + static void MenuItem(bool selected = false, bool enable = true) // Shorthand to get a given action from the manager and draw a MenuItem from it. + { + const IAction* action = ActionManager::get_instance().get_action_with_id(EventT::id); + + if (ImGui::MenuItem( action->label.c_str(), action->shortcut.to_string().c_str(), selected, enable)) + { + action->trigger(); + } + }; + static void BulletTextWrapped(const char*); static ImRect GetContentRegion(Space); diff --git a/src/nodable/core/Graph.cpp b/src/nodable/core/Graph.cpp index cd31b873e..707c32795 100644 --- a/src/nodable/core/Graph.cpp +++ b/src/nodable/core/Graph.cpp @@ -85,8 +85,6 @@ UpdateResult Graph::update() } - set_dirty(false); - return result; } @@ -95,6 +93,7 @@ void Graph::add(PoolID _node) FW_ASSERT(std::find(m_node_registry.begin(), m_node_registry.end(), _node->poolid()) == m_node_registry.end()) m_node_registry.push_back(_node->poolid()); _node->parent_graph = this; + set_dirty(); // To express this graph changed LOG_VERBOSE("Graph", "registerNode %s (%s)\n", _node->name.c_str(), _node->get_type()->get_name()) } @@ -103,6 +102,7 @@ void Graph::remove(PoolID _node) auto found = std::find(m_node_registry.begin(), m_node_registry.end(), _node); FW_ASSERT(found != m_node_registry.end()); m_node_registry.erase(found); + set_dirty(); // To express this graph changed } void Graph::ensure_has_root() @@ -254,6 +254,7 @@ void Graph::remove(DirectedEdge edge) { LOG_WARNING("Graph", "Unable to unregister edge\n") } + set_dirty(); // To express this graph changed } DirectedEdge* Graph::connect_to_variable(Slot& _out, VariableNode& _variable ) @@ -412,7 +413,7 @@ DirectedEdge* Graph::connect(Slot& _first, Slot& _second, ConnectFlags _flags) FW_ASSERT(false);// This connection type is not yet implemented } } - set_dirty(); + set_dirty(); // To express this graph changed return &edge; } @@ -458,7 +459,7 @@ void Graph::disconnect( const DirectedEdge& _edge, ConnectFlags flags) FW_EXPECT(!type, "Not yet implemented yet"); } - set_dirty(); + set_dirty(); // To express this graph changed } PoolID Graph::create_scope() @@ -510,3 +511,61 @@ PoolID Graph::create_literal(const fw::type *_type) add(node); return node; } + +PoolID Graph::create_node( NodeType _type, const fw::func_type* _signature ) +{ + switch ( _type ) + { + /* + * TODO: We could consider narowwing the enum to few cases (BLOCK, VARIABLE, LITERAL, OPERATOR, FUNCTION) + * and rely more on _signature (ex: a bool variable could be simply "bool" or "bool bool(bool)") + */ + case NodeType_BLOCK_CONDITION: return create_cond_struct(); + case NodeType_BLOCK_FOR_LOOP: return create_for_loop(); + case NodeType_BLOCK_WHILE_LOOP: return create_while_loop(); + case NodeType_BLOCK_SCOPE: return create_scope(); + case NodeType_BLOCK_PROGRAM: clear(); return create_root(); + + case NodeType_VARIABLE_BOOLEAN: return create_variable_decl(); + case NodeType_VARIABLE_DOUBLE: return create_variable_decl(); + case NodeType_VARIABLE_INTEGER: return create_variable_decl(); + case NodeType_VARIABLE_STRING: return create_variable_decl(); + + case NodeType_LITERAL_BOOLEAN: return create_literal(); + case NodeType_LITERAL_DOUBLE: return create_literal(); + case NodeType_LITERAL_INTEGER: return create_literal(); + case NodeType_LITERAL_STRING: return create_literal(); + + case NodeType_INVOKABLE: + { + FW_EXPECT(_signature != nullptr, "_signature is expected when dealing with functions or operators") + auto& language = Nodlang::get_instance(); + // Currently, we handle operators and functions the exact same way + const auto invokable = language.find_function(_signature); + bool is_operator = language.find_operator_fct( invokable->get_type() ) != nullptr; + return create_function(invokable.get(), is_operator); + } + default: + FW_EXPECT( false, "Unhandled NodeType."); + } +} + +PoolID Graph::create_variable_decl(const fw::type* _type, const char* _name, PoolID _scope) +{ + if( !_scope) + { + _scope = get_root()->get_component(); + } + + // Create variable + PoolID var_node = create_variable(_type, _name, _scope ); + var_node->set_declared(true); + Token token(Token_t::keyword_operator, " = "); + token.m_word_start_pos = 1; + token.m_word_size = 1; + var_node->assignment_operator_token = token; + + // TODO: attach a default Literal? + + return var_node; +} diff --git a/src/nodable/core/Graph.h b/src/nodable/core/Graph.h index 825a49ea0..98d1157c6 100644 --- a/src/nodable/core/Graph.h +++ b/src/nodable/core/Graph.h @@ -27,6 +27,23 @@ namespace ndbl ConnectFlag_ALLOW_SIDE_EFFECTS = 1 << 0, }; + enum NodeType : u16_t { + NodeType_BLOCK_CONDITION, + NodeType_BLOCK_FOR_LOOP, + NodeType_BLOCK_WHILE_LOOP, + NodeType_BLOCK_SCOPE, + NodeType_BLOCK_PROGRAM, + NodeType_VARIABLE_BOOLEAN, + NodeType_VARIABLE_DOUBLE, + NodeType_VARIABLE_INTEGER, + NodeType_VARIABLE_STRING, + NodeType_LITERAL_BOOLEAN, + NodeType_LITERAL_DOUBLE, + NodeType_LITERAL_INTEGER, + NodeType_LITERAL_STRING, + NodeType_INVOKABLE, + }; + /** * @brief To manage a graph (nodes and edges) */ @@ -40,8 +57,15 @@ namespace ndbl // node related + PoolID create_node(); // Create a raw node. + PoolID create_node(NodeType, const fw::func_type* _signature = nullptr); // Create a given node type in a simple way. PoolID create_root(); PoolID create_variable(const fw::type *_type, const std::string &_name, PoolID _scope); + PoolID create_variable_decl(const fw::type* _type, const char* _name, PoolID _scope); + template + PoolID create_variable_decl(const char* _name = "var", PoolID _scope = {}) + { return create_variable_decl( fw::type::get(), _name, _scope); } + PoolID create_literal(const fw::type *_type); template PoolID create_literal() { return create_literal(fw::type::get()); } @@ -53,7 +77,6 @@ namespace ndbl PoolID create_cond_struct(); PoolID create_for_loop(); PoolID create_while_loop(); - PoolID create_node(); void destroy(PoolID _node); void ensure_has_root(); PoolID get_root()const { return m_root; } diff --git a/src/nodable/core/Node.cpp b/src/nodable/core/Node.cpp index b78310f7d..d59c50674 100644 --- a/src/nodable/core/Node.cpp +++ b/src/nodable/core/Node.cpp @@ -213,7 +213,7 @@ Slot* Node::find_slot_by_property_type(SlotFlags flags, const fw::type* _type) { for(Slot* slot : filter_slots( flags ) ) { - if( slot->get_property()->get_type()->equals( _type ) ) + if( fw::type::is_implicitly_convertible( slot->get_property()->get_type(), _type ) ) { return slot; } @@ -355,3 +355,9 @@ bool Node::should_be_constrain_to_follow_output( PoolID _output ) co const auto& _outputs = outputs(); return predecessors().empty() && _outputs.size() >= 1 && _outputs.back() == _output->m_id; } + +bool Node::can_be_instruction() const +{ + // TODO: handle case where a variable has inputs/outputs but not connected to the code flow + return slot_count(SlotFlag_TYPE_CODEFLOW) > 0 && inputs().empty() && outputs().empty(); +} diff --git a/src/nodable/core/Node.h b/src/nodable/core/Node.h index d15e6fee4..c8c60bf11 100644 --- a/src/nodable/core/Node.h +++ b/src/nodable/core/Node.h @@ -64,6 +64,7 @@ namespace ndbl { virtual void init(); bool is_instruction() const; + bool can_be_instruction() const; // Slot related //------------- diff --git a/src/nodable/core/SlotFlag.h b/src/nodable/core/SlotFlag.h index b1266f664..195c65c96 100644 --- a/src/nodable/core/SlotFlag.h +++ b/src/nodable/core/SlotFlag.h @@ -30,7 +30,7 @@ namespace ndbl SlotFlag_TYPE_MASK = SlotFlag_TYPE_CODEFLOW | SlotFlag_TYPE_HIERARCHICAL | SlotFlag_TYPE_VALUE, }; - static SlotFlags flip_order(SlotFlags flags) + static SlotFlags get_complementary_flags(SlotFlags flags) { return (i8_t)(flags ^ SlotFlag_ORDER_MASK); } diff --git a/src/nodable/core/language/Nodlang.cpp b/src/nodable/core/language/Nodlang.cpp index 05bd13a94..cbd140d2c 100644 --- a/src/nodable/core/language/Nodlang.cpp +++ b/src/nodable/core/language/Nodlang.cpp @@ -615,6 +615,9 @@ PoolID Nodlang::parse_program() parser_state.scope.pop(); commit_transaction(); + // Avoid an unnecessary serialization of the graph (would happen once after each parsing) + parser_state.graph->set_dirty(false); + return root; } @@ -1011,7 +1014,8 @@ Token Nodlang::parse_token(char* buffer, size_t buffer_size, size_t& global_curs Token_t type = Token_t::identifier; - auto keyword_found = m_token_t_by_keyword.find( hash::hash(buffer + start_pos, cursor - start_pos) ); + auto hash = hash::hash(buffer + start_pos, cursor - start_pos); + auto keyword_found = m_token_t_by_keyword.find( hash ); if (keyword_found != m_token_t_by_keyword.end()) { // a keyword has priority over identifier @@ -1873,6 +1877,27 @@ std::string &Nodlang::serialize_cond_struct(std::string &_out, const IfNode*_con // Language definition ------------------------------------------------------------------------------------------------------------ +std::shared_ptr Nodlang::find_function(const char* _signature_hint) const +{ + if (_signature_hint == nullptr) + { + return nullptr; + } + + auto hash = fw::hash::hash(_signature_hint); + return find_function( hash ); +} + +std::shared_ptr Nodlang::find_function(fw::hash::hash_t _hash) const +{ + auto found = m_functions_by_signature.find(_hash); + if ( found != m_functions_by_signature.end()) + { + return found->second; + } + return nullptr; +} + std::shared_ptr Nodlang::find_function(const func_type* _type) const { if (!_type) diff --git a/src/nodable/core/language/Nodlang.h b/src/nodable/core/language/Nodlang.h index 8353fcc2b..b3db583f1 100644 --- a/src/nodable/core/language/Nodlang.h +++ b/src/nodable/core/language/Nodlang.h @@ -9,6 +9,7 @@ #include "core/VariableNode.h" #include "fw/core/reflection/reflection" #include "fw/core/system.h" +#include "fw/core/hash.h" #include "core/Token.h" #include "core/TokenRibbon.h" @@ -121,7 +122,9 @@ namespace ndbl{ std::string& serialize_property(std::string &_out, const Property*) const; // Language definition ------------------------------------------------------------------------- + public: + invokable_ptr find_function( const char* _signature ) const; // Find a function by signature as string (ex: "int multiply(int,int)" ) invokable_ptr find_function(const fw::func_type*) const; // Find a function by signature (strict first, then cast allowed) invokable_ptr find_function_exact(const fw::func_type*) const; // Find a function by signature (no cast allowed). invokable_ptr find_function_fallback(const fw::func_type*) const; // Find a function by signature (casts allowed). @@ -139,7 +142,8 @@ namespace ndbl{ int get_precedence(const fw::iinvokable*)const; // Get the precedence of a given function (precedence may vary because function could be an operator implementation). template void load_library(); // Instantiate a library from its type (uses reflection to get all its static methods). - + private: + invokable_ptr find_function(fw::hash::hash_t _hash) const; private: struct { std::vector> keywords; @@ -151,6 +155,7 @@ namespace ndbl{ operators_vec m_operators; // the allowed operators (!= implementations). Invokable_vec m_operators_impl; // operators' implementations. Invokable_vec m_functions; // all the functions (including operator's). + std::unordered_map> m_functions_by_signature; // Functions indexed by signature hash std::unordered_map m_single_char_by_keyword; std::unordered_map m_keyword_by_token_t; // token_t to string (ex: Token_t::keyword_double => "double"). std::unordered_map m_keyword_by_type_id; @@ -158,6 +163,7 @@ namespace ndbl{ std::unordered_map m_token_t_by_keyword; // keyword reserved by the language (ex: int, string, operator, if, for, etc.) std::unordered_map m_token_t_by_type_id; std::unordered_map m_type_by_token_t; // token_t to type. Works only if token_t refers to a type keyword. + }; template diff --git a/src/nodable/gui/Action.h b/src/nodable/gui/Action.h new file mode 100644 index 000000000..7c9dc85f6 --- /dev/null +++ b/src/nodable/gui/Action.h @@ -0,0 +1,28 @@ +#pragma once +#include "Event.h" +#include "core/Graph.h" +#include "fw/core/reflection/func_type.h" +#include "fw/gui/Action.h" + +namespace ndbl +{ + using fw::Action; + using fw::Event; + + // Actions specific to Nodable, more actions defined in framework's Action.h + + // 1) Basic actions (simple events) + + using Action_DeleteNode = Action; + using Action_ArrangeNode = Action; + using Action_ToggleFolding = Action; + using Action_SelectNext = Action; + using Action_ToggleIsolate = Action; + using Action_SelectionChange = Action; + using Action_MoveGraph = Action; + + // 2) Advanced actions (custom events) + + using Action_FrameSelection = Action; + using Action_CreateNode = Action; +} \ No newline at end of file diff --git a/src/nodable/gui/Condition.h b/src/nodable/gui/Condition.h index cd19ec126..c29a6e1fd 100644 --- a/src/nodable/gui/Condition.h +++ b/src/nodable/gui/Condition.h @@ -9,11 +9,14 @@ namespace ndbl Condition_ENABLE_IF_HAS_SELECTION = 1 << 0, Condition_ENABLE_IF_HAS_NO_SELECTION = 1 << 1, Condition_ENABLE_IF_HAS_GRAPH = 1 << 3, + Condition_DISABLE_IF_DRAGGING_THIS_SLOT = 1 << 4, + Condition_DISABLE_IF_DRAGGING_NON_THIS_SLOT= 1 << 5, + Condition_ONLY_FROM_GRAPH_EDITOR_CONTEXTUAL= 1 << 6, Condition_ENABLE = Condition_ENABLE_IF_HAS_SELECTION | Condition_ENABLE_IF_HAS_NO_SELECTION | Condition_ENABLE_IF_HAS_GRAPH, - Condition_HIGHLIGHTED_IN_GRAPH_EDITOR = 1 << 4, - Condition_HIGHLIGHTED_IN_TEXT_EDITOR = 1 << 5, + Condition_HIGHLIGHTED_IN_GRAPH_EDITOR = 1 << 10, + Condition_HIGHLIGHTED_IN_TEXT_EDITOR = 1 << 11, Condition_HIGHLIGHTED = Condition_HIGHLIGHTED_IN_GRAPH_EDITOR | Condition_HIGHLIGHTED_IN_TEXT_EDITOR, }; diff --git a/src/nodable/gui/Event.h b/src/nodable/gui/Event.h index d60236f2d..91bd6acca 100644 --- a/src/nodable/gui/Event.h +++ b/src/nodable/gui/Event.h @@ -1,5 +1,10 @@ #pragma once +#include + +#include "Event.h" +#include "FrameMode.h" #include "SlotView.h" +#include "core/Graph.h" #include "fw/core/Pool.h" #include "fw/gui/EventManager.h" #include "nodable/core/SlotRef.h" @@ -10,48 +15,90 @@ namespace ndbl class NodeView; using fw::PoolID; - enum EventType_: fw::EventType + enum EventID_ : fw::EventID { - EventType_delete_node_action_triggered = fw::EventType_USER_DEFINED, // operation on nodes - EventType_arrange_node_action_triggered, - EventType_select_successor_node_action_triggered, - EventType_toggle_folding_selected_node_action_triggered, - EventType_node_view_selected, - EventType_node_view_deselected, - EventType_frame_all_node_views, - EventType_frame_selected_node_views, - EventType_slot_dropped, - EventType_slot_disconnected, - EventType_toggle_isolate_selection - }; - - struct NodeViewEvent + EventID_DELETE_NODE = fw::EventID_USER_DEFINED, // operation on nodes + EventID_ARRANGE_NODE, + EventID_SELECT_NEXT, + EventID_TOGGLE_FOLDING, + EventID_REQUEST_CREATE_NODE, + EventID_REQUEST_CREATE_BLOCK, + EventID_REQUEST_FRAME_SELECTION, + EventID_MOVE_SELECTION, + EventID_TOGGLE_ISOLATE, + EventID_SLOT_DROPPED, + EventID_SLOT_DISCONNECTED, + EventID_SELECTION_CHANGE, + }; + + using Event_ToggleIsolate = fw::Event; + using Event_MoveSelection = fw::Event; + + class GraphView; + struct EventPayload_FrameNodeViews { - fw::EventType type; - PoolID view; + FrameMode mode; + EventPayload_FrameNodeViews(FrameMode mode) + : mode(mode) + {} }; + using Event_FrameSelection = fw::Event; - struct ToggleFoldingEvent + struct EventPayload_SlotPair { + SlotRef first; + SlotRef second; + EventPayload_SlotPair(SlotRef&& first = {}, SlotRef&& second = {}) + : first(first) + , second(second) + {} + }; + using Event_SlotDisconnected = fw::Event; + using Event_SlotDropped = fw::Event; + + struct EventPayload_Node { - fw::EventType type; - bool recursive; + PoolID node; }; + using Event_DeleteNode = fw::Event; + using Event_ArrangeNode = fw::Event; + using Event_SelectNext = fw::Event; - struct SlotEvent + enum ToggleFoldingMode { - fw::EventType type; - SlotRef first; - SlotRef second; + NON_RECURSIVELY = 0, + RECURSIVELY = 1, }; + struct EventPayload_ToggleFoldingEvent + { + ToggleFoldingMode mode; + }; + using Event_ToggleFolding = fw::Event; + + struct EventPayload_NodeViewSelectionChange + { + PoolID new_selection; + PoolID old_selection; + }; + using Event_SelectionChange = fw::Event; - union Event + struct EventPayload_CreateNode { - fw::EventType type; - fw::Event event; - fw::SimpleEvent common; - NodeViewEvent node; - SlotEvent slot; - ToggleFoldingEvent toggle_folding; + NodeType node_type; // The note type to create + const fw::func_type* node_signature; // The signature of the node that must be created + SlotView* dragged_slot = nullptr; // The slot view being dragged. + Graph* graph = nullptr; // The graph to create the node into + ImVec2 node_view_local_pos; // The desired position for the new node view + + explicit EventPayload_CreateNode(NodeType node_type ) + : node_type(node_type) + , node_signature(nullptr) + {} + + EventPayload_CreateNode(NodeType node_type, const fw::func_type* signature ) + : node_type(node_type) + , node_signature(signature) + {} }; + using Event_CreateNode = fw::Event; }// namespace ndbl diff --git a/src/nodable/gui/FrameMode.h b/src/nodable/gui/FrameMode.h new file mode 100644 index 000000000..838b2a408 --- /dev/null +++ b/src/nodable/gui/FrameMode.h @@ -0,0 +1,10 @@ +#pragma once + +namespace ndbl +{ + enum FrameMode + { + FRAME_SELECTION_ONLY = 0, + FRAME_ALL = 1 + }; +} \ No newline at end of file diff --git a/src/nodable/gui/GraphView.cpp b/src/nodable/gui/GraphView.cpp index 43f8ab768..c6c54c0da 100644 --- a/src/nodable/gui/GraphView.cpp +++ b/src/nodable/gui/GraphView.cpp @@ -2,12 +2,16 @@ #include #include // std::shared_ptr +#include #include #include "core/types.h" #include "fw/core/log.h" #include "fw/core/system.h" +#include "Action.h" +#include "Condition.h" #include "Config.h" +#include "Event.h" #include "Nodable.h" #include "NodeView.h" #include "Physics.h" @@ -28,6 +32,8 @@ using namespace ndbl; using namespace ndbl::assembly; using namespace fw; +const char* k_context_menu_popup = "GraphView.CreateNodeContextMenu"; + REGISTER { fw::registration::push_class("GraphView").extends(); @@ -36,24 +42,8 @@ REGISTER GraphView::GraphView(Graph* graph) : fw::View() , m_graph(graph) - , m_new_node_desired_position(-1, -1) -{ - const Nodlang& language = Nodlang::get_instance(); - for (auto& each_fct : language.get_api()) - { - const fw::func_type* type = each_fct->get_type(); - bool is_operator = language.find_operator_fct(type) != nullptr; - - auto create_node = [this, each_fct, is_operator]() -> PoolID - { - return m_graph->create_function(each_fct.get(), is_operator); - }; - - std::string label; - language.serialize_func_sig(label, type); - std::string category = is_operator ? k_operator_menu_label : k_function_menu_label; - add_contextual_menu_item(category, label, create_node, type); - } + , m_create_node_context_menu() +{ } bool GraphView::draw() @@ -63,120 +53,15 @@ bool GraphView::draw() ImDrawList* draw_list = ImGui::GetWindowDrawList(); Nodable & app = Nodable::get_instance(); const bool enable_edition = app.virtual_machine.is_program_stopped(); - PoolID new_node_id; - ImVec2 origin = ImGui::GetCursorScreenPos(); auto node_registry = Pool::get_pool()->get( m_graph->get_node_registry() ); const SlotView* dragged_slot = SlotView::get_dragged(); const SlotView* hovered_slot = SlotView::get_hovered(); + bool drop_behavior_requires_a_new_node = false; + bool is_any_node_dragged = false; + bool is_any_node_hovered = false; - /* - * Function to draw an invocable menu (operators or functions) - */ - auto draw_invokable_menu = [&]( - const SlotView* dragged_slot_view, - const std::string& _key) -> void - { - char menuLabel[255]; - snprintf( menuLabel, 255, ICON_FA_CALCULATOR" %s", _key.c_str()); - - if (ImGui::BeginMenu(menuLabel)) - { - auto range = m_contextual_menus.equal_range(_key); - for (auto it = range.first; it != range.second; it++) - { - FunctionMenuItem menu_item = it->second; - - /* - * First we determine if the current menu_item points to a function with compatible signature. - */ - bool has_compatible_signature; - - if ( !dragged_slot ) - { - has_compatible_signature = true; - } - else if ( !dragged_slot->is_this() ) - { - const fw::type* dragged_property_type = dragged_slot->get_property_type(); - - if ( dragged_slot->allows( SlotFlag_ORDER_FIRST ) ) - { - has_compatible_signature = menu_item.function_signature->has_an_arg_of_type(dragged_property_type); - } - else - { - has_compatible_signature = menu_item.function_signature->get_return_type()->equals(dragged_property_type); - } - } - - /* - * Then, since we know signature compatibility, we add or not a new MenuItem. - */ - if ( has_compatible_signature && ImGui::MenuItem( menu_item.label.c_str() )) - { - if ( menu_item.create_node_fct ) - { - new_node_id = menu_item.create_node_fct()->poolid(); - } - else - { - LOG_WARNING("GraphView", "The function associated to the key %s is nullptr", - menu_item.label.c_str()) - } - } - } - - ImGui::EndMenu(); - } - }; - - auto create_variable = [&](const fw::type* _type, const char* _name, PoolID _scope) -> PoolID - { - if( !_scope) - { - _scope = m_graph->get_root()->get_component(); - } - - PoolID var_node = m_graph->create_variable(_type, _name, _scope ); - var_node->set_declared(true); - - Token token(Token_t::keyword_operator, " = "); - token.m_word_start_pos = 1; - token.m_word_size = 1; - - var_node->assignment_operator_token = token; - return var_node; - }; - - /* - Grid - Draw X vertical and Y horizontal lines every grid_size pixels - */ - const int grid_size = app.config.ui_graph_grid_size; - const int grid_subdiv_size = app.config.ui_graph_grid_size / app.config.ui_graph_grid_subdivs; - const int vertical_line_count = int(m_screen_space_content_region.GetSize().x) / grid_subdiv_size; - const int horizontal_line_count = int(m_screen_space_content_region.GetSize().y) / grid_subdiv_size; - ImColor grid_color = app.config.ui_graph_grid_color_major; - ImColor grid_color_light = app.config.ui_graph_grid_color_minor; - - for(int coord = 0; coord <= vertical_line_count; ++coord) - { - float pos = m_screen_space_content_region.GetTL().x + float(coord) * grid_subdiv_size; - const ImVec2 line_start{pos, m_screen_space_content_region.GetTL().y}; - const ImVec2 line_end{pos, m_screen_space_content_region.GetBL().y}; - bool is_major = coord % app.config.ui_graph_grid_subdivs == 0; - draw_list->AddLine(line_start, line_end, is_major ? grid_color : grid_color_light); - } - - for(int coord = 0; coord <= horizontal_line_count; ++coord) - { - float pos = m_screen_space_content_region.GetTL().y + float(coord) * grid_subdiv_size; - const ImVec2 line_start{m_screen_space_content_region.GetTL().x, pos}; - const ImVec2 line_end{m_screen_space_content_region.GetBR().x, pos}; - bool is_major = coord % app.config.ui_graph_grid_subdivs == 0; - draw_list->AddLine(line_start, line_end, is_major ? grid_color : grid_color_light); - } - + // Draw grid in the background + draw_grid( draw_list, app.config ); /* Draw Code Flow. @@ -202,7 +87,7 @@ bool GraphView::draw() continue; } - for( auto adjacent_slot : slot->adjacent() ) + for( const auto& adjacent_slot : slot->adjacent() ) { Node* each_successor_node = adjacent_slot->get_node(); NodeView* each_successor_view = NodeView::substitute_with_parent_if_not_visible( each_successor_node->get_component().get() ); @@ -210,7 +95,7 @@ bool GraphView::draw() if ( each_successor_view && each_view->is_visible() && each_successor_view->is_visible() ) { ImRect start = each_view->get_slot_rect( *slot, app.config, slot_index ); - ImRect end = each_successor_view->get_slot_rect( *adjacent_slot.get(), app.config, 0 );// there is only 1 previous slot + ImRect end = each_successor_view->get_slot_rect( *adjacent_slot, app.config, 0 );// there is only 1 previous slot fw::ImGuiEx::DrawVerticalWire( ImGui::GetWindowDrawList(), @@ -228,16 +113,22 @@ bool GraphView::draw() // slot Drag'n Drop if ( ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) ) { + // Get the current dragged slot, or the slot that was dragged when context menu opened + const SlotView* _dragged_slot = dragged_slot ? dragged_slot : m_create_node_context_menu.dragged_slot; + // Draw temporary edge - if (dragged_slot) + if ( _dragged_slot ) { - if ( dragged_slot->slot().type() == SlotFlag_TYPE_CODEFLOW ) + // When dragging, edge follows mouse cursor. Otherwise, it sticks the contextual menu. + ImVec2 edge_end = m_create_node_context_menu.dragged_slot ? m_create_node_context_menu.opened_at_screen_pos : ImGui::GetMousePos(); + + if ( _dragged_slot->slot().type() == SlotFlag_TYPE_CODEFLOW ) { // Thick line fw::ImGuiEx::DrawVerticalWire( ImGui::GetWindowDrawList(), - dragged_slot->rect(app.config).GetCenter(), - hovered_slot ? hovered_slot->rect(app.config).GetCenter(): ImGui::GetMousePos(), + _dragged_slot->rect(app.config).GetCenter(), + hovered_slot ? hovered_slot->rect(app.config).GetCenter(): edge_end, app.config.ui_codeflow_color, app.config.ui_codeflow_shadowColor, app.config.ui_node_slot_size.x * app.config.ui_codeflow_thickness_ratio, @@ -248,31 +139,18 @@ bool GraphView::draw() { // Simple line ImGui::GetWindowDrawList()->AddLine( - dragged_slot->position(), - hovered_slot ? hovered_slot->position() : ImGui::GetMousePos(), + _dragged_slot->position(), + hovered_slot ? hovered_slot->position() : edge_end, ImGui::ColorConvertFloat4ToU32(app.config.ui_node_borderHighlightedColor), app.config.ui_wire_bezier_thickness ); } } - // Drops ? - bool require_new_node = false; - SlotView::drop_behavior(require_new_node, enable_edition); - - // Need a need node ? - if (require_new_node) - { - if (!ImGui::IsPopupOpen(k_context_menu_popup) ) - { - ImGui::OpenPopup(k_context_menu_popup); - m_new_node_desired_position = ImGui::GetMousePos() - origin; - } - } + // Determine whether the current dragged SlotView should be dropped or not, and if a new node is required + SlotView::drop_behavior( drop_behavior_requires_a_new_node, enable_edition); } - bool isAnyNodeDragged = false; - bool isAnyNodeHovered = false; { /* Wires @@ -377,13 +255,13 @@ bool GraphView::draw() each_node_view->pinned( true ); } - isAnyNodeDragged |= NodeView::get_dragged() == each_node_view->poolid(); - isAnyNodeHovered |= each_node_view->is_hovered(); + is_any_node_dragged |= NodeView::get_dragged() == each_node_view->poolid(); + is_any_node_hovered |= each_node_view->is_hovered(); } } } - isAnyNodeDragged |= SlotView::is_dragging(); + is_any_node_dragged |= SlotView::is_dragging(); // Virtual Machine cursor if ( app.virtual_machine.is_program_running() ) @@ -412,7 +290,7 @@ bool GraphView::draw() /* Deselection (by double click) */ - if ( NodeView::is_any_selected() && !isAnyNodeHovered && ImGui::IsMouseDoubleClicked(0) && ImGui::IsWindowFocused()) + if ( NodeView::is_any_selected() && !is_any_node_hovered && ImGui::IsMouseDoubleClicked(0) && ImGui::IsWindowFocused()) { NodeView::set_selected({}); } @@ -420,157 +298,48 @@ bool GraphView::draw() /* Mouse PAN (global) */ - if (ImGui::IsMouseDragging(0) && ImGui::IsWindowFocused() && !isAnyNodeDragged ) + if (ImGui::IsMouseDragging(0) && ImGui::IsWindowFocused() && !is_any_node_dragged ) { translate_view(ImGui::GetMouseDragDelta()); ImGui::ResetMouseDragDelta(); } - /* - Mouse right-click popup menu - */ + // Decides whether contextual menu should be opened. + if ( drop_behavior_requires_a_new_node || (enable_edition && !is_any_node_hovered && ImGui::IsMouseClicked(1) ) ) + { + if ( !ImGui::IsPopupOpen( k_context_menu_popup ) ) + { + ImGui::OpenPopup( k_context_menu_popup ); + m_create_node_context_menu.reset_state( SlotView::get_dragged() ); + SlotView::reset_dragged(); + } + } - if ( enable_edition && !isAnyNodeHovered && ImGui::BeginPopupContextWindow(k_context_menu_popup) ) + // Defines contextual menu popup (not rendered if popup is closed) + if ( ImGui::BeginPopup(k_context_menu_popup) ) { // Title : fw::ImGuiEx::ColoredShadowedText( ImVec2( 1, 1 ), ImColor( 0.00f, 0.00f, 0.00f, 1.00f ), ImColor( 1.00f, 1.00f, 1.00f, 0.50f ), "Create new node :" ); ImGui::Separator(); - - if ( !dragged_slot ) - { - draw_invokable_menu( dragged_slot, k_operator_menu_label ); - draw_invokable_menu( dragged_slot, k_function_menu_label ); - ImGui::Separator(); - } - - if ( dragged_slot ) - { - SlotFlags slot_type = dragged_slot->slot().type(); - switch ( slot_type ) - { - case SlotFlag_TYPE_CODEFLOW: - { - if ( ImGui::MenuItem( ICON_FA_CODE " Condition" ) ) - new_node_id = m_graph->create_cond_struct(); - if ( ImGui::MenuItem( ICON_FA_CODE " For Loop" ) ) - new_node_id = m_graph->create_for_loop(); - if ( ImGui::MenuItem( ICON_FA_CODE " While Loop" ) ) - new_node_id = m_graph->create_while_loop(); - - ImGui::Separator(); - - if ( ImGui::MenuItem( ICON_FA_CODE " Scope" ) ) - new_node_id = m_graph->create_scope(); - - ImGui::Separator(); - - if ( ImGui::MenuItem( ICON_FA_CODE " Program" ) ) - { - m_graph->clear(); - new_node_id = m_graph->create_root(); - } - break; - } - default: - { - if ( !dragged_slot->is_this() ) - { - if ( ImGui::MenuItem( ICON_FA_DATABASE " Variable" ) ) - { - new_node_id = create_variable( dragged_slot->get_property_type(), "var", {} ); - } - - // we allow literal only if connected to variables. - // why? behavior when connecting a literal to a non var node is to digest it. - if ( dragged_slot->get_node()->get_type()->is() && ImGui::MenuItem( ICON_FA_FILE "Literal" ) ) - { - new_node_id = m_graph->create_literal( dragged_slot->get_property_type() ); - } - } - else - { - if ( ImGui::BeginMenu( "Variable" ) ) - { - if ( ImGui::MenuItem( ICON_FA_DATABASE " Boolean" ) ) - new_node_id = create_variable( fw::type::get(), "var", {} ); - - if ( ImGui::MenuItem( ICON_FA_DATABASE " Double" ) ) - new_node_id = create_variable( fw::type::get(), "var", {} ); - - if ( ImGui::MenuItem( ICON_FA_DATABASE " Int (16bits)" ) ) - new_node_id = create_variable( fw::type::get(), "var", {} ); - - if ( ImGui::MenuItem( ICON_FA_DATABASE " String" ) ) - new_node_id = create_variable( fw::type::get(), "var", {} ); - - ImGui::EndMenu(); - } - - if ( ImGui::BeginMenu( "Literal" ) ) - { - if ( ImGui::MenuItem( ICON_FA_FILE " Boolean" ) ) - new_node_id = m_graph->create_literal( fw::type::get() ); - - if ( ImGui::MenuItem( ICON_FA_FILE " Double" ) ) - new_node_id = m_graph->create_literal( fw::type::get() ); - - if ( ImGui::MenuItem( ICON_FA_FILE " Int (16bits)" ) ) - new_node_id = m_graph->create_literal( fw::type::get() ); - - if ( ImGui::MenuItem( ICON_FA_FILE " String" ) ) - new_node_id = m_graph->create_literal( fw::type::get() ); - - ImGui::EndMenu(); - } - } - } - } - } - /* * In case user has created a new node we need to connect it to the m_graph depending * on if a slot is being dragged and what is its nature. */ - if ( new_node_id ) + if ( Action_CreateNode* action = m_create_node_context_menu.draw_search_input( 10 ) ) { - - // dragging node slot ? - if ( dragged_slot ) - { - SlotFlags complementary_flags = flip_order( dragged_slot->slot().static_flags() ); - Slot* complementary_slot = new_node_id->find_slot_by_property_type( complementary_flags, dragged_slot->get_property()->get_type() ); - ConnectFlags connect_flags = ConnectFlag_ALLOW_SIDE_EFFECTS; - - Slot* out = &dragged_slot->slot(); - Slot* in = complementary_slot; - - if( out->has_flags(SlotFlag_ORDER_SECOND) ) std::swap(out, in); - - m_graph->connect( *out, *in, connect_flags ); - - SlotView::reset_dragged(); - } - else if (new_node_id != m_graph->get_root() && app.config.experimental_graph_autocompletion ) - { - m_graph->ensure_has_root(); - // m_graph->connect( new_node, m_graph->get_root(), RelType::CHILD ); - } - - // set new_node's view position - if( PoolID view = new_node_id->get_component() ) - { - view->set_position(m_new_node_desired_position, fw::Space_Local); - } + // Generate an event from this action, add some info to the state and dispatch it. + auto& event_manager = EventManager::get_instance(); + auto* event = action->make_event(); + event->data.graph = m_graph; + event->data.dragged_slot = m_create_node_context_menu.dragged_slot; + event->data.node_view_local_pos = m_create_node_context_menu.opened_at_pos; + event_manager.dispatch(event); + + ImGui::CloseCurrentPopup(); } - ImGui::EndPopup(); - } - - // reset dragged if right click - if ( ImGui::IsMouseClicked(1) ) - { - ImGui::CloseCurrentPopup(); - SlotView::reset_dragged(); + } else { + m_create_node_context_menu.reset_state(); } // add some empty space @@ -579,13 +348,32 @@ bool GraphView::draw() return changed; } -void GraphView::add_contextual_menu_item( - const std::string &_category, - const std::string &_label, - std::function(void)> _function, - const fw::func_type *_signature) +void GraphView::draw_grid( ImDrawList* draw_list, const Config& config ) const { - m_contextual_menus.insert( {_category, {_label, _function, _signature }} ); + const int grid_size = config.ui_graph_grid_size; + const int grid_subdiv_size = config.ui_graph_grid_size / config.ui_graph_grid_subdivs; + const int vertical_line_count = int( m_screen_space_content_region.GetSize().x) / grid_subdiv_size; + const int horizontal_line_count = int( m_screen_space_content_region.GetSize().y) / grid_subdiv_size; + ImColor grid_color = config.ui_graph_grid_color_major; + ImColor grid_color_light = config.ui_graph_grid_color_minor; + + for(int coord = 0; coord <= vertical_line_count; ++coord) + { + float pos = m_screen_space_content_region.GetTL().x + float(coord) * float(grid_subdiv_size); + const ImVec2 line_start{pos, m_screen_space_content_region.GetTL().y}; + const ImVec2 line_end{pos, m_screen_space_content_region.GetBL().y}; + bool is_major = coord % config.ui_graph_grid_subdivs == 0; + draw_list->AddLine(line_start, line_end, is_major ? grid_color : grid_color_light); + } + + for(int coord = 0; coord <= horizontal_line_count; ++coord) + { + float pos = m_screen_space_content_region.GetTL().y + float(coord) * float(grid_subdiv_size); + const ImVec2 line_start{ m_screen_space_content_region.GetTL().x, pos}; + const ImVec2 line_end{ m_screen_space_content_region.GetBR().x, pos}; + bool is_major = coord % config.ui_graph_grid_subdivs == 0; + draw_list->AddLine(line_start, line_end, is_major ? grid_color : grid_color_light); + } } bool GraphView::update(float delta_time, i16_t subsample_count) @@ -709,3 +497,171 @@ void GraphView::translate_view(ImVec2 delta) // TODO: implement a better solution, storing an offset. And then substract it in draw(); // m_view_origin += delta; } + +void GraphView::add_action_to_context_menu( Action_CreateNode* _action ) +{ + m_create_node_context_menu.items.push_back(_action); +} + +void GraphView::frame( FrameMode mode ) +{ + // TODO: use an ImRect instead of a FrameMode enum, it will be easier to handle undo/redo + if ( mode == FRAME_ALL ) + { + return frame_all_node_views(); + } + return frame_selected_node_views(); +} + +Action_CreateNode* CreateNodeContextMenu::draw_search_input( size_t _result_max_count ) +{ + bool validated; + + if ( must_be_reset_flag ) + { + ImGui::SetKeyboardFocusHere(); + + // + update_cache_based_on_signature(); + + // Initial search + update_cache_based_on_user_input( 100 ); + + // Ensure we reset once + must_be_reset_flag = false; + } + + // Draw search input and update_cache_based_on_user_input on input change + if ( ImGui::InputText("Search", search_input, 255, ImGuiInputTextFlags_EscapeClearsAll )) + { + update_cache_based_on_user_input( 100 ); + } + + if ( !items_matching_search.empty() ) + { + // When a single item is filtered, pressing enter will press the item's button. + if ( items_matching_search.size() == 1) + { + auto action = items_matching_search.front(); + if ( ImGui::SmallButton( action->label.c_str()) || ImGui::IsKeyDown( ImGuiKey_Enter ) ) + { + return action; + } + } + else + { + size_t more = items_matching_search.size() > _result_max_count ? items_matching_search.size() : 0; + if ( more ) + { + ImGui::Text("Found %zu result(s)", items_matching_search.size() ); + } + // Otherwise, user has to move with arrow keys and press enter to trigger the highlighted button. + auto it = items_matching_search.begin(); + while( it != items_matching_search.end() && std::distance(items_matching_search.begin(), it) != _result_max_count) + { + auto* action = *it; + if ( ImGui::Button( action->label.c_str()) || // User can click on the button... + (ImGui::IsKeyDown( ImGuiKey_Enter ) && ImGui::IsItemFocused() ) // ...or press enter if this item is the first + ) + { + return action; + } + it++; + } + if ( more ) + { + ImGui::Text(".. %zu more ..", more ); + } + } + } + else + { + ImGui::Text("No matches..."); + } + + return nullptr; +} +void CreateNodeContextMenu::update_cache_based_on_signature() +{ + items_with_compatible_signature.clear(); + + // 1) When NO slot is dragged + //--------------------------- + + if ( !dragged_slot ) + { + // When no slot is dragged, user can create any node + items_with_compatible_signature = items; + return; + } + + // 2) When a slot is dragged + //-------------------------- + + for (auto& action: items ) + { + const type* dragged_property_type = dragged_slot->get_property_type(); + + switch ( action->event_data.node_type ) + { + case NodeType_BLOCK_CONDITION: + case NodeType_BLOCK_FOR_LOOP: + case NodeType_BLOCK_WHILE_LOOP: + case NodeType_BLOCK_SCOPE: + case NodeType_BLOCK_PROGRAM: + // Blocks are only for code flow slots + if ( !dragged_slot->allows(SlotFlag_TYPE_CODEFLOW) ) + continue; + break; + + default: + + if ( dragged_slot->allows(SlotFlag_TYPE_CODEFLOW)) + { + // we can connect anything to a code flow slot + } + else if ( action->event_data.node_signature ) + { + // discard incompatible signatures + + if ( dragged_slot->allows( SlotFlag_ORDER_FIRST ) && + !action->event_data.node_signature->has_an_arg_of_type(dragged_property_type) + ) + continue; + + if ( !action->event_data.node_signature->get_return_type()->equals(dragged_property_type) ) + continue; + + } + } + items_with_compatible_signature.push_back( action ); + } +} + +void CreateNodeContextMenu::update_cache_based_on_user_input( size_t _limit ) +{ + items_matching_search.clear(); + for ( auto& menu_item : items_with_compatible_signature ) + { + if( menu_item->label.find( search_input ) != std::string::npos ) + { + items_matching_search.push_back(menu_item); + if ( items_matching_search.size() == _limit ) + { + break; + } + } + } +} + +void CreateNodeContextMenu::reset_state( SlotView* _dragged_slot ) +{ + must_be_reset_flag = true; + search_input[0] = '\0'; + opened_at_pos = ImGui::GetMousePos() - ImGui::GetCursorScreenPos(); + opened_at_screen_pos = ImGui::GetMousePos(); + dragged_slot = _dragged_slot; + + items_matching_search.clear(); + items_with_compatible_signature.clear(); +} diff --git a/src/nodable/gui/GraphView.h b/src/nodable/gui/GraphView.h index f16c14297..c211d014d 100644 --- a/src/nodable/gui/GraphView.h +++ b/src/nodable/gui/GraphView.h @@ -10,8 +10,12 @@ #include "core/Component.h" // base class #include "core/IScope.h" -#include "types.h" +#include "Action.h" +#include "Config.h" #include "NodeViewConstraint.h" +#include "SlotView.h" +#include "core/Scope.h" +#include "types.h" namespace ndbl { @@ -19,11 +23,22 @@ namespace ndbl class Nodable; class Graph; - typedef struct { - std::string label; - std::function(void)> create_node_fct; - const fw::func_type* function_signature; - } FunctionMenuItem; + struct CreateNodeContextMenu + { + bool must_be_reset_flag = false; + ImVec2 opened_at_pos = ImVec2(-1,-1); // relative + ImVec2 opened_at_screen_pos = ImVec2(-1,-1); // absolute (screen space) + SlotView* dragged_slot = nullptr; // The slot being dragged when the context menu opened. + char search_input[255] = "\0"; // The search input entered by the user. + std::vector items; // All the available items + std::vector items_with_compatible_signature; // Only the items having a compatible signature (with the slot dragged) + std::vector items_matching_search; // Only the items having a compatible signature AND matching the search_input. + + Action_CreateNode* draw_search_input( size_t _result_max_count ); // Return the triggered action, user has to deal with the Action. + void reset_state(SlotView* _dragged_slot = nullptr); + void update_cache_based_on_signature(); + void update_cache_based_on_user_input( size_t _limit ); + }; class GraphView: public fw::View { @@ -35,27 +50,21 @@ namespace ndbl bool update(); bool update(float /* delta_time */); bool update(float /* delta_time */, i16_t /* subsample_count */); - void add_contextual_menu_item( - const std::string &_category, - const std::string &_label, - std::function(void)> _function, - const fw::func_type *_signature); void frame_all_node_views(); void frame_selected_node_views(); void translate_all(ImVec2 /* delta */, const std::vector&); void unfold(); // unfold the graph until it is stabilized + void add_action_to_context_menu( Action_CreateNode* _action); + void frame( FrameMode mode ); private: + void draw_grid( ImDrawList*, const Config& ) const; void frame_views(const std::vector &_views, bool _align_top_left_corner); void translate_view(ImVec2 vec2); Graph* m_graph; ImVec2 m_view_origin; - ImVec2 m_new_node_desired_position; - std::multimap m_contextual_menus; - static constexpr const char* k_context_menu_popup = "GraphView.ContextMenu"; - static constexpr const char* k_operator_menu_label = "Operators"; - static constexpr const char* k_function_menu_label = "Functions"; + CreateNodeContextMenu m_create_node_context_menu; REFLECT_DERIVED_CLASS() }; diff --git a/src/nodable/gui/HybridFile.cpp b/src/nodable/gui/HybridFile.cpp index b243701d0..fc1b83702 100644 --- a/src/nodable/gui/HybridFile.cpp +++ b/src/nodable/gui/HybridFile.cpp @@ -17,7 +17,7 @@ using namespace fw; HybridFile::HybridFile(std::string _name) : name(std::move(_name)) - , changed(true) + , is_content_dirty(true) , view(*this) , m_history(&Nodable::get_instance().config.experimental_hybrid_history) { @@ -39,6 +39,14 @@ HybridFile::HybridFile(std::string _name) m_graph = new Graph(&Nodable::get_instance().node_factory); m_graph_view = new GraphView(m_graph); + for( IAction* action : ActionManager::get_instance().get_actions() ) // Fill the "create node" context menu + { + if ( auto create_node_action = dynamic_cast(action)) + { + m_graph_view->add_action_to_context_menu( create_node_action ); + } + } + LOG_VERBOSE( "File", "Constructor being called.\n") } @@ -56,22 +64,23 @@ HybridFile::~HybridFile() bool HybridFile::write_to_disk() { - if(path.empty() ) + if( path.empty() ) { + LOG_WARNING("File", "No path defined, unable to save file\n"); return false; } - if (changed) - { - std::ofstream out_fstream(path.c_str()); - std::string content = view.get_text(); - out_fstream.write(content.c_str(), content.size()); - changed = false; - LOG_MESSAGE("File", "%s saved\n", name.c_str()); - } else { + if ( !is_content_dirty ) + { LOG_MESSAGE("File", "Nothing to save\n"); } + std::ofstream out_fstream(path.c_str()); + std::string content = view.get_text(); + out_fstream.write(content.c_str(), content.size()); // TODO: size can exceed fstream! + is_content_dirty = false; + LOG_MESSAGE("File", "%s saved\n", name.c_str()); + return true; } @@ -94,7 +103,7 @@ bool HybridFile::load() std::string content((std::istreambuf_iterator(file_stream)), std::istreambuf_iterator()); set_text(content); - changed = false; + is_content_dirty = false; LOG_MESSAGE("HybridFile", "\"%s\" loaded (%s).\n", name.c_str(), path.c_str()) @@ -133,13 +142,16 @@ UpdateResult HybridFile::update() // bool isolate_selection = Nodable::get_instance().config.isolate_selection; + // 1) Handle when view changes (graph or text) + //-------------------------------------------- + if( view.changed() ) { - if( view.focused_text_changed() && !view.graph_changed() ) + if( view.focused_text_changed() && !view.is_graph_dirty() ) { update_graph_from_text(isolate_selection); } - else if ( view.graph_changed() ) + else if ( view.is_graph_dirty() ) { update_text_from_graph(isolate_selection); } @@ -149,15 +161,31 @@ UpdateResult HybridFile::update() // This is not supposed to happens, that's why there is an assert to be aware of is FW_ASSERT(false); } - view.changed(false); + view.set_dirty( false ); } - return m_graph->update(); + // 2) Handle when graph (not the graph view) changes + //-------------------------------------------------- + + if ( m_graph->is_dirty() ) + { + // Refresh text + update_text_from_graph(isolate_selection); + + // Refresh constraints + auto physics_components = NodeUtils::get_components( m_graph->get_node_registry() ); + Physics::destroy_constraints( physics_components ); + Physics::create_constraints( m_graph->get_node_registry() ); + + m_graph->set_dirty(false); + } + + return m_graph->update(); // ~ garbage collection } UpdateResult HybridFile::update_graph_from_text(bool isolate_selection) { - // Destroy all physics' constraints + // Destroy all physics constraints auto physics_components = NodeUtils::get_components( m_graph->get_node_registry() ); Physics::destroy_constraints( physics_components ); @@ -170,7 +198,6 @@ UpdateResult HybridFile::update_graph_from_text(bool isolate_selection) return UpdateResult::SUCCESS_WITH_CHANGES; } return UpdateResult::SUCCES_WITHOUT_CHANGES; - } size_t HybridFile::size() const diff --git a/src/nodable/gui/HybridFile.h b/src/nodable/gui/HybridFile.h index 6b071367c..a7bc40dd0 100644 --- a/src/nodable/gui/HybridFile.h +++ b/src/nodable/gui/HybridFile.h @@ -38,7 +38,7 @@ namespace ndbl observe::Event graph_changed; ghc::filesystem::path path; // file path on disk std::string name; // friendly name - bool changed; // true if changes needs to be saved + bool is_content_dirty; // true if changes needs to be saved HybridFileView view; explicit HybridFile(std::string _name); diff --git a/src/nodable/gui/HybridFileView.cpp b/src/nodable/gui/HybridFileView.cpp index 6326c60ca..179878d52 100644 --- a/src/nodable/gui/HybridFileView.cpp +++ b/src/nodable/gui/HybridFileView.cpp @@ -21,7 +21,7 @@ HybridFileView::HybridFileView(HybridFile& _file) : fw::View() , m_text_editor() , m_focused_text_changed(false) - , m_graph_changed(false) + , m_is_graph_dirty(false) , m_file(_file) , m_child1_size(0.3f) , m_child2_size(0.7f) @@ -51,7 +51,7 @@ HybridFileView::HybridFileView(HybridFile& _file) graph_view->translate_all( ImVec2(-1000.f, -1000.0f) , views); // frame all (33ms delayed) - fw::EventManager::get_instance().push_async(EventType_frame_all_node_views, 33); + fw::EventManager::get_instance().dispatch_delayed( 33, {FRAME_ALL} ); } } }); @@ -153,7 +153,7 @@ bool HybridFileView::draw() m_text_editor.IsTextChanged() || (app.config.isolate_selection && is_selected_text_modified); - if (m_text_editor.IsTextChanged()) m_file.changed = true; + if (m_text_editor.IsTextChanged()) m_file.is_content_dirty = true; } ImGui::EndChild(); @@ -175,7 +175,7 @@ bool HybridFileView::draw() { // Draw graph View::use_available_region(graph_view); - m_graph_changed = graph_view->draw(); + m_is_graph_dirty = graph_view->draw(); // Draw overlay: shortcuts ImRect overlay_rect = fw::ImGuiEx::GetContentRegion(fw::Space_Screen); @@ -353,3 +353,18 @@ size_t HybridFileView::size() const { return m_text_editor.Size(); } + +void HybridFileView::refresh_overlay(Condition _condition ) +{ + for (const IAction* _action: ActionManager::get_instance().get_actions()) + { + if( ( _action->userdata & _condition) == _condition && (_action->userdata & Condition_HIGHLIGHTED) ) + { + std::string label = _action->label.substr(0, 12); + std::string shortcut_str = _action->shortcut.to_string(); + OverlayType_ overlay_type = _action->userdata & Condition_HIGHLIGHTED_IN_TEXT_EDITOR ? OverlayType_TEXT + : OverlayType_GRAPH; + push_overlay({label, shortcut_str}, overlay_type); + } + } +} diff --git a/src/nodable/gui/HybridFileView.h b/src/nodable/gui/HybridFileView.h index 4f28581cf..7b9c14e60 100644 --- a/src/nodable/gui/HybridFileView.h +++ b/src/nodable/gui/HybridFileView.h @@ -6,6 +6,7 @@ #include "fw/core/reflection/reflection" #include "fw/gui/View.h" +#include "Condition.h" #include "types.h" namespace ndbl @@ -22,7 +23,8 @@ namespace ndbl }; using OverlayType = int; - enum OverlayType_ { + enum OverlayType_ + { OverlayType_TEXT, OverlayType_GRAPH, OverlayType_COUNT @@ -42,10 +44,10 @@ namespace ndbl void init(); bool draw() override; - bool changed() const { return m_focused_text_changed || m_graph_changed; } + bool changed() const { return m_focused_text_changed || m_is_graph_dirty; } bool focused_text_changed() const { return m_focused_text_changed; } - bool graph_changed() const { return m_graph_changed; } - void changed(bool b) { m_focused_text_changed = m_graph_changed = b; } + bool is_graph_dirty() const { return m_is_graph_dirty; } + void set_dirty(bool b) { m_focused_text_changed = m_is_graph_dirty = b; } void set_text(const std::string&); std::string get_selected_text()const; std::string get_text()const; @@ -58,16 +60,16 @@ namespace ndbl void draw_info_panel()const; void experimental_clipboard_auto_paste(bool); bool experimental_clipboard_auto_paste()const { return m_experimental_clipboard_auto_paste; } - void push_overlay(OverlayData, OverlayType) ; void clear_overlay(); + void push_overlay(OverlayData, OverlayType) ; + void refresh_overlay(Condition condition); void draw_overlay(const char* title, const std::vector& overlay_data, ImRect rect, ImVec2 position); - - size_t size() const; + size_t size() const; private: std::array, OverlayType_COUNT> m_overlay_data; bool m_focused_text_changed; - bool m_graph_changed; + bool m_is_graph_dirty; HybridFile& m_file; std::string m_text_overlay_window_name; std::string m_graph_overlay_window_name; diff --git a/src/nodable/gui/Nodable.cpp b/src/nodable/gui/Nodable.cpp index b4f4fca26..1e6ae43b2 100644 --- a/src/nodable/gui/Nodable.cpp +++ b/src/nodable/gui/Nodable.cpp @@ -1,5 +1,6 @@ #include "Nodable.h" +#include "Action.h" #include "Condition.h" #include "Event.h" #include "GraphView.h" @@ -12,11 +13,11 @@ #include "commands/Cmd_ConnectEdge.h" #include "commands/Cmd_DisconnectEdge.h" #include "commands/Cmd_Group.h" -#include "core/Slot.h" #include "core/DataAccess.h" #include "core/InvokableComponent.h" #include "core/LiteralNode.h" #include "core/NodeUtils.h" +#include "core/Slot.h" #include "core/VariableNode.h" #include "fw/core/assertions.h" #include "fw/core/system.h" @@ -29,6 +30,14 @@ using fw::View; Nodable *Nodable::s_instance = nullptr; +template +static fw::func_type* create_variable_node_signature() +{ return fw::func_type_builder::with_id("variable"); } + +template +static fw::func_type* create_literal_node_signature() +{ return fw::func_type_builder::with_id("literal"); } + Nodable::Nodable() : App(config.common, new NodableView(this) ) , current_file(nullptr) @@ -101,98 +110,54 @@ bool Nodable::on_init() fw::Pool::init(); // Bind commands to shortcuts - using fw::EventType; - event_manager.bind( - {"Delete", - EventType_delete_node_action_triggered, - {SDLK_DELETE, KMOD_NONE}, - Condition_ENABLE}); - event_manager.bind( - {"Arrange", - EventType_arrange_node_action_triggered, - {SDLK_a, KMOD_NONE}, - Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR}); - event_manager.bind( - {"Fold", - EventType_toggle_folding_selected_node_action_triggered, - {SDLK_x, KMOD_NONE}, - Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR}); - event_manager.bind( - {"Next", - EventType_select_successor_node_action_triggered, - {SDLK_n, KMOD_NONE}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_SAVE " Save", - fw::EventType_save_file_triggered, - {SDLK_s, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_SAVE " Save as", - fw::EventType_save_file_as_triggered, - {SDLK_s, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_TIMES " Close", - fw::EventType_close_file_triggered, - {SDLK_w, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_FOLDER_OPEN " Open", - fw::EventType_browse_file_triggered, - {SDLK_o, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_FILE " New", - fw::EventType_new_file_triggered, - {SDLK_n, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {"Splashscreen", - fw::EventType_show_splashscreen_triggered, - {SDLK_F1}, - Condition_ENABLE}); - event_manager.bind( - {ICON_FA_SIGN_OUT_ALT " Exit", - fw::EventType_exit_triggered, - {SDLK_F4, KMOD_ALT}, - Condition_ENABLE}); - event_manager.bind( - {"Undo", - fw::EventType_undo_triggered, - {SDLK_z, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {"Redo", - fw::EventType_redo_triggered, - {SDLK_y, KMOD_CTRL}, - Condition_ENABLE}); - event_manager.bind( - {"Isolate", - EventType_toggle_isolate_selection, - {SDLK_i, KMOD_CTRL}, - Condition_ENABLE | Condition_HIGHLIGHTED_IN_TEXT_EDITOR}); - event_manager.bind( - {"Deselect", - fw::EventType_none, - {0, KMOD_NONE, "Double click on bg"}, - Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR}); - event_manager.bind( - {"Move Graph", - fw::EventType_none, - {0, KMOD_NONE, "Drag background"}, - Condition_ENABLE | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR}); - event_manager.bind( - {"Frame Selection", - EventType_frame_selected_node_views, - {SDLK_f, KMOD_NONE}, - Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR}); - event_manager.bind( - {"Frame All", - EventType_frame_all_node_views, - {SDLK_f, KMOD_LCTRL}, - Condition_ENABLE}); - + action_manager.new_action( "Delete", Shortcut{ SDLK_DELETE, KMOD_NONE } ); + action_manager.new_action( "Arrange", Shortcut{ SDLK_a, KMOD_NONE }, Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR ); + action_manager.new_action( "Fold", Shortcut{ SDLK_x, KMOD_NONE }, Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR ); + action_manager.new_action( "Next", Shortcut{ SDLK_n, KMOD_NONE } ); + action_manager.new_action( ICON_FA_SAVE " Save", Shortcut{ SDLK_s, KMOD_CTRL } ); + action_manager.new_action( ICON_FA_SAVE " Save as", Shortcut{ SDLK_s, KMOD_CTRL } ); + action_manager.new_action( ICON_FA_TIMES " Close", Shortcut{ SDLK_w, KMOD_CTRL } ); + action_manager.new_action( ICON_FA_FOLDER_OPEN " Open", Shortcut{ SDLK_o, KMOD_CTRL } ); + action_manager.new_action( ICON_FA_FILE " New", Shortcut{ SDLK_n, KMOD_CTRL } ); + action_manager.new_action( "Splashscreen", Shortcut{ SDLK_F1 }, EventPayload_ShowWindow{ "splashscreen" } ); + action_manager.new_action( ICON_FA_SIGN_OUT_ALT " Exit", Shortcut{ SDLK_F4, KMOD_ALT } ); + action_manager.new_action( "Undo", Shortcut{ SDLK_z, KMOD_CTRL } ); + action_manager.new_action( "Redo", Shortcut{ SDLK_y, KMOD_CTRL } ); + action_manager.new_action( "Isolate", Shortcut{ SDLK_i, KMOD_CTRL }, Condition_ENABLE | Condition_HIGHLIGHTED_IN_TEXT_EDITOR ); + action_manager.new_action( "Deselect", Shortcut{ 0, KMOD_NONE, "Double click on bg" }, Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR ); + action_manager.new_action( "Move Graph", Shortcut{ 0, KMOD_NONE, "Drag background" }, Condition_ENABLE | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR ); + action_manager.new_action( "Frame Selection", Shortcut{ SDLK_f, KMOD_NONE }, EventPayload_FrameNodeViews{ FRAME_SELECTION_ONLY }, Condition_ENABLE_IF_HAS_SELECTION | Condition_HIGHLIGHTED_IN_GRAPH_EDITOR ); + action_manager.new_action( "Frame All", Shortcut{ SDLK_f, KMOD_LCTRL }, EventPayload_FrameNodeViews{ FRAME_ALL } ); + + // Prepare context menu items + // 1) Blocks + action_manager.new_action( ICON_FA_CODE " Condition", Shortcut{}, EventPayload_CreateNode{ NodeType_BLOCK_CONDITION } ); + action_manager.new_action( ICON_FA_CODE " For Loop", Shortcut{}, EventPayload_CreateNode{ NodeType_BLOCK_FOR_LOOP } ); + action_manager.new_action( ICON_FA_CODE " While Loop", Shortcut{}, EventPayload_CreateNode{ NodeType_BLOCK_WHILE_LOOP } ); + action_manager.new_action( ICON_FA_CODE " Scope", Shortcut{}, EventPayload_CreateNode{ NodeType_BLOCK_SCOPE } ); + action_manager.new_action( ICON_FA_CODE " Program", Shortcut{}, EventPayload_CreateNode{ NodeType_BLOCK_PROGRAM } ); + + // 2) Variables + action_manager.new_action( ICON_FA_DATABASE " Boolean Variable", Shortcut{}, EventPayload_CreateNode{ NodeType_VARIABLE_BOOLEAN, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_DATABASE " Double Variable", Shortcut{}, EventPayload_CreateNode{ NodeType_VARIABLE_DOUBLE, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_DATABASE " Integer Variable", Shortcut{}, EventPayload_CreateNode{ NodeType_VARIABLE_INTEGER, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_DATABASE " String Variable", Shortcut{}, EventPayload_CreateNode{ NodeType_VARIABLE_STRING, create_variable_node_signature() } ); + + // 3) Literals + action_manager.new_action( ICON_FA_FILE " Boolean Literal", Shortcut{}, EventPayload_CreateNode{ NodeType_LITERAL_BOOLEAN, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_FILE " Double Literal", Shortcut{}, EventPayload_CreateNode{ NodeType_LITERAL_DOUBLE, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_FILE " Integer Literal", Shortcut{}, EventPayload_CreateNode{ NodeType_LITERAL_INTEGER, create_variable_node_signature() } ); + action_manager.new_action( ICON_FA_FILE " String Literal", Shortcut{}, EventPayload_CreateNode{ NodeType_LITERAL_STRING, create_variable_node_signature() } ); + + // 4) Functions/Operators from the API + const Nodlang& language = Nodlang::get_instance(); + for ( auto& each_fct: language.get_api() ) + { + const fw::func_type* func_type = each_fct->get_type(); + std::string label; + language.serialize_func_sig( label, func_type ); + action_manager.new_action( label.c_str(), Shortcut{}, EventPayload_CreateNode{ NodeType_INVOKABLE, func_type } ); + } return true; } @@ -217,44 +182,17 @@ void Nodable::on_update() // 2. Handle events - // shorthand to push all shortcuts to a file view overlay depending on conditions - auto push_overlay_shortcuts = [&](ndbl::HybridFileView& _view, Condition _condition) -> void { - for (const auto& _binded_event: event_manager.get_binded_events()) - { - if( (_binded_event.condition & _condition) == _condition) - { - if (_binded_event.condition & Condition_HIGHLIGHTED_IN_GRAPH_EDITOR) - { - _view.push_overlay( - { - _binded_event.label.substr(0, 12), - _binded_event.shortcut.to_string() - }, OverlayType_GRAPH); - } - if ( _binded_event.condition & Condition_HIGHLIGHTED_IN_TEXT_EDITOR) - { - _view.push_overlay( - { - _binded_event.label.substr(0,12), - _binded_event.shortcut.to_string() - }, OverlayType_TEXT); - } - } - - } - }; - - // Nodable events ( SDL_ API inspired, but with custom events) - ndbl::Event event{}; + // Nodable events auto selected_view = NodeView::get_selected(); GraphView* graph_view = current_file ? current_file->get_graph_view() : nullptr; History* curr_file_history = current_file ? current_file->get_history() : nullptr; - while(event_manager.poll_event((fw::Event&)event) ) + IEvent* event = nullptr; + while( (event = event_manager.poll_event()) ) { - switch ( event.type ) + switch ( event->id ) { - case EventType_toggle_isolate_selection: + case EventID_TOGGLE_ISOLATE: { config.isolate_selection = !config.isolate_selection; if(current_file) @@ -264,30 +202,30 @@ void Nodable::on_update() break; } - case fw::EventType_exit_triggered: + case fw::EventID_REQUEST_EXIT: { should_stop = true; break; } - case fw::EventType_close_file_triggered: + case fw::EventID_FILE_CLOSE: { if(current_file) close_file(current_file); break; } - case fw::EventType_undo_triggered: + case fw::EventID_UNDO: { if(curr_file_history) curr_file_history->undo(); break; } - case fw::EventType_redo_triggered: + case fw::EventID_REDO: { if(curr_file_history) curr_file_history->redo(); break; } - case fw::EventType_browse_file_triggered: + case fw::EventID_FILE_BROWSE: { std::string path; if( m_view->pick_file_path(path, fw::AppView::DIALOG_Browse)) @@ -300,13 +238,13 @@ void Nodable::on_update() } - case fw::EventType_new_file_triggered: + case fw::EventID_FILE_NEW: { new_file(); break; } - case fw::EventType_save_file_as_triggered: + case fw::EventID_FILE_SAVE_AS: { if (current_file) { @@ -320,7 +258,7 @@ void Nodable::on_update() break; } - case fw::EventType_save_file_triggered: + case fw::EventID_FILE_SAVE: { if (current_file) { @@ -340,44 +278,44 @@ void Nodable::on_update() break; } - case fw::EventType_show_splashscreen_triggered: + case Event_ShowWindow::id: { - config.common.splashscreen = true; - break; - } - case EventType_frame_selected_node_views: - { - if (graph_view) graph_view->frame_selected_node_views(); + auto _event = reinterpret_cast(event); + if ( _event->data.window_id == "splashscreen" ) + { + config.common.splashscreen = _event->data.visible; + } break; } - case EventType_frame_all_node_views: + case Event_FrameSelection::id: { - if (graph_view) graph_view->frame_all_node_views(); + auto _event = reinterpret_cast( event ); + FW_EXPECT(graph_view, "a graph_view is required"); + graph_view->frame(_event->data.mode); break; } - case EventType_node_view_selected: - { - current_file->view.clear_overlay(); - push_overlay_shortcuts(current_file->view, Condition_ENABLE_IF_HAS_SELECTION); - LOG_MESSAGE( "App", "NodeView selected\n") - break; - } - case fw::EventType_file_opened: + + case Event_SelectionChange::id: { - if (!current_file) break; + auto _event = reinterpret_cast( event ); + + Condition_ condition = _event->data.new_selection ? Condition_ENABLE_IF_HAS_SELECTION + : Condition_ENABLE_IF_HAS_NO_SELECTION; current_file->view.clear_overlay(); - push_overlay_shortcuts(current_file->view, Condition_ENABLE_IF_HAS_NO_SELECTION); + current_file->view.refresh_overlay( condition ); break; } - case EventType_node_view_deselected: + case EventID_FILE_OPENED: { current_file->view.clear_overlay(); - push_overlay_shortcuts(current_file->view, Condition_ENABLE_IF_HAS_NO_SELECTION ); + current_file->view.refresh_overlay( Condition_ENABLE_IF_HAS_NO_SELECTION ); break; } - case EventType_delete_node_action_triggered: + case Event_DeleteNode::id: { + // TODO: store a ref to the view in the event, use selected as fallback if not present + if ( selected_view && !ImGui::IsAnyItemFocused() ) { Node* selected_node = selected_view->get_owner().get(); @@ -386,37 +324,43 @@ void Nodable::on_update() break; } - case EventType_arrange_node_action_triggered: + case Event_ArrangeNode::id: { if ( selected_view ) selected_view->arrange_recursively(); break; } - case EventType_select_successor_node_action_triggered: + case Event_SelectNext::id: { + // TODO: store a ref to the view in the event, use selected as fallback if not present + if (!selected_view) break; - PoolID possible_successor = selected_view->get_owner()->successors().front(); - if (!possible_successor) break; - if (PoolID successor_view = possible_successor->get_component() ) + std::vector> successors = selected_view->get_owner()->successors(); + if (!successors.empty()) { - NodeView::set_selected(successor_view); + if (PoolID successor_view = successors.front()->get_component() ) + { + NodeView::set_selected(successor_view); + } } break; } - case EventType_toggle_folding_selected_node_action_triggered: + + case Event_ToggleFolding::id: { if ( !selected_view ) break; - event.toggle_folding.recursive ? selected_view->expand_toggle_rec() - : selected_view->expand_toggle(); + auto _event = reinterpret_cast(event); + _event->data.mode == RECURSIVELY ? selected_view->expand_toggle_rec() + : selected_view->expand_toggle(); break; } - case EventType_slot_dropped: + case Event_SlotDropped::id: { - SlotRef tail = event.slot.first; - SlotRef head = event.slot.second; - - if (head.flags & SlotFlag_ORDER_SECOND ) std::swap(tail, head); // guarantee src to be the output + auto _event = reinterpret_cast(event); + SlotRef tail = _event->data.first; + SlotRef head = _event->data.second; + if (tail.flags & SlotFlag_ORDER_SECOND ) std::swap(tail, head); // guarantee src to be the output DirectedEdge edge(tail, head); auto cmd = std::make_shared(edge); curr_file_history->push_command(cmd); @@ -424,9 +368,10 @@ void Nodable::on_update() break; } - case EventType_slot_disconnected: + case Event_SlotDisconnected::id: { - SlotRef slot = event.slot.first; + auto _event = reinterpret_cast(event); + SlotRef slot = _event->data.first; auto cmd_grp = std::make_shared("Disconnect All Edges"); for( const auto& adjacent_slot: slot->adjacent() ) @@ -440,6 +385,54 @@ void Nodable::on_update() break; } + case Event_CreateNode::id: + { + auto _event = reinterpret_cast(event); + + // 1) create the node + PoolID new_node_id = current_file->get_graph()->create_node( _event->data.node_type, _event->data.node_signature ); + + // 2) handle connections + if ( !_event->data.dragged_slot ) + { + // Experimental: we try to connect a parent-less child + if ( new_node_id != _event->data.graph->get_root() && config.experimental_graph_autocompletion ) + { + _event->data.graph->ensure_has_root(); + // m_graph->connect( new_node, m_graph->get_root(), RelType::CHILD ); + } + } + else + { + Slot* complementary_slot = new_node_id->find_slot_by_property_type( + get_complementary_flags( _event->data.dragged_slot->slot().static_flags() ), + _event->data.dragged_slot->get_property()->get_type() ); + + if ( !complementary_slot ) + { + // TODO: this case should not happens, instead we should check ahead of time whether or not this not can be attached + LOG_ERROR( "GraphView", "unable to connect this node" ) + } + else + { + Slot* out = &_event->data.dragged_slot->slot(); + Slot* in = complementary_slot; + + if ( out->has_flags( SlotFlag_ORDER_SECOND ) ) std::swap( out, in ); + + _event->data.graph->connect( *out, *in, ConnectFlag_ALLOW_SIDE_EFFECTS ); + } + } + + // set new_node's view position, select it + if ( auto view = new_node_id->get_component() ) + { + view->set_position( _event->data.node_view_local_pos, fw::Space_Local ); + NodeView::set_selected( view ); + } + break; + } + default: { LOG_VERBOSE("App", "Ignoring and event, this case is not handled\n") @@ -484,7 +477,7 @@ HybridFile *Nodable::add_file(HybridFile* _file) FW_EXPECT(_file, "File is nullptr"); m_loaded_files.push_back( _file ); current_file = _file; - event_manager.push(fw::EventType_file_opened); + event_manager.dispatch( fw::EventID_FILE_OPENED ); return _file; } diff --git a/src/nodable/gui/NodableView.cpp b/src/nodable/gui/NodableView.cpp index 0af461b6a..918b385f0 100644 --- a/src/nodable/gui/NodableView.cpp +++ b/src/nodable/gui/NodableView.cpp @@ -2,23 +2,26 @@ #include -#include "fw/core/log.h" -#include "fw/core/system.h" -#include "fw/gui/Texture.h" -#include "core/NodeUtils.h" +#include "Action.h" #include "Config.h" #include "Event.h" +#include "History.h" #include "HybridFile.h" #include "HybridFileView.h" -#include "History.h" #include "Nodable.h" #include "NodeView.h" -#include "build_info.h" #include "Physics.h" #include "PropertyView.h" +#include "build_info.h" +#include "core/NodeUtils.h" +#include "fw/core/log.h" +#include "fw/core/system.h" +#include "fw/gui/Texture.h" +#include "gui/ActionManagerView.h" using namespace ndbl; using namespace ndbl::assembly; +using namespace fw; NodableView::NodableView(Nodable * _app) : fw::AppView(_app) @@ -63,51 +66,49 @@ void NodableView::on_draw() History* current_file_history = current_file ? current_file->get_history() : nullptr; if (ImGui::BeginMenu("File")) { - bool has_file = current_file; - bool changed = current_file != nullptr && current_file->changed; - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_new_file_triggered); - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_browse_file_triggered); + bool has_file = current_file != nullptr; + bool is_current_file_content_dirty = current_file != nullptr && current_file->is_content_dirty; + ImGuiEx::MenuItem(); + ImGuiEx::MenuItem(); ImGui::Separator(); - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_save_file_as_triggered, false, has_file); - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_save_file_triggered, false, has_file && changed); + ImGuiEx::MenuItem(false, has_file); + ImGuiEx::MenuItem(false, has_file && is_current_file_content_dirty ); ImGui::Separator(); - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_close_file_triggered, false, has_file); + ImGuiEx::MenuItem(false, has_file); auto auto_paste = has_file && current_file->view.experimental_clipboard_auto_paste(); - if (ImGui::MenuItem(ICON_FA_COPY " Auto-paste clipboard", "", auto_paste, has_file ) && current_file ) { + if (ImGui::MenuItem(ICON_FA_COPY " Auto-paste clipboard", "", auto_paste, has_file ) && has_file ) { current_file->view.experimental_clipboard_auto_paste(!auto_paste); } - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_exit_triggered); + fw::ImGuiEx::MenuItem(); ImGui::EndMenu(); } bool vm_is_stopped = virtual_machine.is_program_stopped(); - if (ImGui::BeginMenu("Edit")) { - if (current_file_history) { - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_undo_triggered); - fw::ImGuiEx::MenuItemBindedToEvent(fw::EventType_redo_triggered); + if (ImGui::BeginMenu("Edit")) + { + if (current_file_history) + { + ImGuiEx::MenuItem(); + ImGuiEx::MenuItem(); ImGui::Separator(); } auto has_selection = NodeView::is_any_selected(); if (ImGui::MenuItem("Delete", "Del.", false, has_selection && vm_is_stopped)) { - event_manager.push(EventType_delete_node_action_triggered); + event_manager.dispatch( EventID_DELETE_NODE ); } - fw::ImGuiEx::MenuItemBindedToEvent(EventType_arrange_node_action_triggered, false, has_selection); - fw::ImGuiEx::MenuItemBindedToEvent(EventType_toggle_folding_selected_node_action_triggered, false, - has_selection); + fw::ImGuiEx::MenuItem( false, has_selection ); + fw::ImGuiEx::MenuItem( false,has_selection ); if (ImGui::MenuItem("Expand/Collapse recursive", nullptr, false, has_selection)) { - Event event{}; - event.toggle_folding.type = EventType_toggle_folding_selected_node_action_triggered; - event.toggle_folding.recursive = true; - event_manager.push_event((fw::Event &) event); + event_manager.dispatch( { RECURSIVELY } ); } ImGui::EndMenu(); } @@ -147,7 +148,7 @@ void NodableView::on_draw() ImGui::Separator(); - fw::ImGuiEx::MenuItemBindedToEvent(EventType_toggle_isolate_selection, config.isolate_selection); + fw::ImGuiEx::MenuItem(config.isolate_selection ); ImGui::EndMenu(); } @@ -503,10 +504,10 @@ void NodableView::draw_startup_window(ImGuiID dockspace_id) { ImVec2 btn_size(center_area.x * 0.44f, 40.0f); if (ImGui::Button(ICON_FA_FILE" New File", btn_size)) - event_manager.push(fw::EventType_new_file_triggered); + event_manager.dispatch( fw::EventID_FILE_NEW ); ImGui::SameLine(); if (ImGui::Button(ICON_FA_FOLDER_OPEN" Open ...", btn_size)) - event_manager.push(fw::EventType_browse_file_triggered); + event_manager.dispatch( fw::EventID_FILE_BROWSE ); ImGui::NewLine(); ImGui::Separator(); @@ -550,7 +551,7 @@ void NodableView::draw_file_window(ImGuiID dockspace_id, bool redock_all, Hybrid ImGui::SetNextWindowDockID(dockspace_id, redock_all ? ImGuiCond_Always : ImGuiCond_Appearing); ImGuiWindowFlags window_flags = - (file->changed ? ImGuiWindowFlags_UnsavedDocument : 0) | ImGuiWindowFlags_NoScrollbar; + (file->is_content_dirty ? ImGuiWindowFlags_UnsavedDocument : 0) | ImGuiWindowFlags_NoScrollbar; auto child_bg = ImGui::GetStyle().Colors[ImGuiCol_ChildBg]; child_bg.w = 0; @@ -672,6 +673,11 @@ void NodableView::draw_config_window() { ImGui::SliderInt("grid subdivisions", &config.ui_graph_grid_subdivs, 1, 16); } + if (ImGui::CollapsingHeader("Shortcuts", ImGuiTreeNodeFlags_SpanAvailWidth)) + { + ActionManagerView::draw(&m_app->action_manager); + } + if ( m_app->config.common.debug && ImGui::CollapsingHeader("Pool")) { ImGui::Text("Pool stats:"); @@ -685,37 +691,41 @@ void NodableView::draw_config_window() { ImGui::End(); } -void NodableView::on_draw_splashscreen() +void NodableView::draw_splashscreen() { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if ( AppView::begin_splashscreen() ) + { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - // Image - ImGui::SameLine((ImGui::GetContentRegionAvail().x - m_logo->width) * 0.5f); // center img - fw::ImGuiEx::Image(m_logo); + // Image + ImGui::SameLine((ImGui::GetContentRegionAvail().x - m_logo->width) * 0.5f); // center img + fw::ImGuiEx::Image(m_logo); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(50.0f, 30.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(50.0f, 30.0f)); - // disclaimer - ImGui::TextWrapped( - "DISCLAIMER: This software is a prototype, do not expect too much from it. Use at your own risk."); + // disclaimer + ImGui::TextWrapped( + "DISCLAIMER: This software is a prototype, do not expect too much from it. Use at your own risk."); - ImGui::NewLine(); - ImGui::NewLine(); + ImGui::NewLine(); + ImGui::NewLine(); - // credits - const char *credit = "by Berdal84"; - ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(credit).x); - ImGui::TextWrapped("%s", credit); + // credits + const char *credit = "by Berdal84"; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(credit).x); + ImGui::TextWrapped("%s", credit); - // build version - ImGui::TextWrapped("%s", BuildInfo::version); + // build version + ImGui::TextWrapped("%s", BuildInfo::version); - // close on left/rightmouse btn click - if (ImGui::IsMouseClicked(0) || ImGui::IsMouseClicked(1)) - { - m_app->config.common.splashscreen = false; + // close on left/rightmouse btn click + if (ImGui::IsMouseClicked(0) || ImGui::IsMouseClicked(1)) + { + m_app->config.common.splashscreen = false; + } + ImGui::PopStyleVar(); // ImGuiStyleVar_FramePadding + AppView::end_splashscreen(); } - ImGui::PopStyleVar(); // ImGuiStyleVar_FramePadding } void NodableView::draw_history_bar(History *currentFileHistory) { @@ -841,7 +851,7 @@ void NodableView::draw_toolbar_window() { if (ImGui::Button( config.isolate_selection ? ICON_FA_CROP " isolation mode: ON " : ICON_FA_CROP " isolation mode: OFF", button_size)) { - m_app->event_manager.push(EventType_toggle_isolate_selection); + m_app->event_manager.dispatch( EventID_TOGGLE_ISOLATE ); } ImGui::SameLine(); ImGui::EndGroup(); diff --git a/src/nodable/gui/NodableView.h b/src/nodable/gui/NodableView.h index 00316faa9..f8ef6d042 100644 --- a/src/nodable/gui/NodableView.h +++ b/src/nodable/gui/NodableView.h @@ -31,7 +31,7 @@ namespace ndbl void on_init() override; void on_draw() override; - void on_draw_splashscreen() override; + void draw_splashscreen() override; void on_reset_layout() override; // draw_xxx_window diff --git a/src/nodable/gui/NodeView.cpp b/src/nodable/gui/NodeView.cpp index 6bd2ba73c..4c5f272a3 100644 --- a/src/nodable/gui/NodeView.cpp +++ b/src/nodable/gui/NodeView.cpp @@ -161,25 +161,14 @@ void NodeView::update_labels_from_name(const Node* _node) void NodeView::set_selected(PoolID new_selection) { + fw::EventManager& event_manager = fw::EventManager::get_instance(); + if( s_selected == new_selection ) return; - // Handle de-selection - if( s_selected ) - { - Event event{ EventType_node_view_deselected }; - event.node.view = s_selected; - fw::EventManager::get_instance().push_event((fw::Event&)event); - s_selected.reset(); - } + EventPayload_NodeViewSelectionChange event{ new_selection, s_selected }; + event_manager.dispatch(event); - // Handle selection - if( new_selection ) - { - Event event{ EventType_node_view_selected }; - event.node.view = new_selection; - fw::EventManager::get_instance().push_event((fw::Event&)event); - s_selected = new_selection; - } + s_selected = new_selection; } PoolID NodeView::get_selected() @@ -297,7 +286,7 @@ bool NodeView::draw() std::unordered_map count_by_flags{{SlotFlag_NEXT, 0}, {SlotFlag_PREV, 0}}; for ( SlotView& slot_view : m_slot_views ) { - if( slot_view.slot().capacity() && slot_view.slot().type() == SlotFlag_TYPE_CODEFLOW ) + if( slot_view.slot().capacity() && slot_view.slot().type() == SlotFlag_TYPE_CODEFLOW && (node->is_instruction() || node->can_be_instruction() ) ) { int& count = count_by_flags[slot_view.slot().static_flags()]; ImRect rect = get_slot_rect( slot_view, config, count ); @@ -1056,6 +1045,18 @@ NodeView* NodeView::substitute_with_parent_if_not_visible(NodeView* _view, bool return parent_view; } +std::vector NodeView::substitute_with_parent_if_not_visible(const std::vector& _in, bool _recursive) +{ + std::vector out; + out.reserve(_in.size()); // Wort but more probable case + for(auto each : _in) + { + auto each_or_substitute = NodeView::substitute_with_parent_if_not_visible(each, _recursive); + out.push_back(each_or_substitute); + } + return std::move(out); +}; + void NodeView::expand_toggle_rec() { return set_expanded_rec(!m_expanded); @@ -1129,4 +1130,10 @@ void NodeView::set_color( const ImVec4* _color, ColorType _type ) ImColor NodeView::get_color( ColorType _type ) const { return ImColor(*m_colors[_type]); -} \ No newline at end of file +} + +bool NodeView::none_is_visible( std::vector _views ) +{ + auto is_visible = [](const NodeView* view) { return view->is_visible(); }; + return std::find_if(_views.begin(), _views.end(), is_visible) == _views.end(); +} diff --git a/src/nodable/gui/NodeView.h b/src/nodable/gui/NodeView.h index bef8d69e9..147c8a3b3 100644 --- a/src/nodable/gui/NodeView.h +++ b/src/nodable/gui/NodeView.h @@ -99,12 +99,15 @@ namespace ndbl static void set_view_detail(NodeViewDetail _viewDetail); // Change view detail globally static NodeViewDetail get_view_detail() { return s_view_detail; } static NodeView* substitute_with_parent_if_not_visible(NodeView* _view, bool _recursive = true); + static std::vector substitute_with_parent_if_not_visible(const std::vector& _in, bool _recurse = true ); ImVec2 get_slot_pos( const Slot& ); ImRect get_slot_rect( const Slot& _slot, const Config& _config, i8_t _count ) const; ImRect get_slot_rect( const SlotView &_slot_view, const Config &_config, i8_t _pos ) const; ImVec2 get_slot_normal( const Slot& slot) const; void set_color( const ImVec4* _color, ColorType _type = Color_FILL ); ImColor get_color(ColorType _type) const; + static bool none_is_visible( std::vector vector1 ); + private: void set_adjacent_visible(SlotFlags flags, bool _visible, bool _recursive); diff --git a/src/nodable/gui/NodeViewConstraint.cpp b/src/nodable/gui/NodeViewConstraint.cpp index 47477bc8e..bdd86133d 100644 --- a/src/nodable/gui/NodeViewConstraint.cpp +++ b/src/nodable/gui/NodeViewConstraint.cpp @@ -13,58 +13,44 @@ using namespace fw; NodeViewConstraint::NodeViewConstraint(const char* _name, ConstrainFlags _flags) : m_flags(_flags) -, m_filter(always) +, m_should_apply(always) , m_is_active(true) , m_name(_name) { } -void NodeViewConstraint::apply(float _dt) +/** TODO: move this in a class (not NodeViewConstrain) */ +std::vector get_rect(const std::vector& _in_views) { - bool should_apply = m_is_active && m_filter(this); - if(!should_apply) - { - return; - } - - /* - * To get a clean list of node views. - * Substitute each not visible view by their respective parent. - */ - auto get_clean = [](std::vector _in) + std::vector _out; + for (auto each_target : _in_views ) { - std::vector out; - out.reserve(_in.size()); - for(auto each : _in) + ImRect rect; + if( !(each_target->pinned() || !each_target->is_visible()) ) { - out.push_back(NodeView::substitute_with_parent_if_not_visible(each)); + rect = each_target->get_rect( true ); } - return std::move(out); - }; + _out.push_back(rect); + } + return std::move(_out); +} - std::vector clean_drivers = get_clean( Pool::get_pool()->get( m_drivers ) ); - std::vector clean_targets = get_clean( Pool::get_pool()->get( m_targets ) ); +void NodeViewConstraint::apply(float _dt) +{ + // Check if this constrain should apply + if(!m_is_active && m_should_apply(this)) return; - //debug - if( fw::ImGuiEx::debug ) - { - for (auto each_target: clean_targets) - { - for (auto each_driver: clean_drivers) - { - fw::ImGuiEx::DebugLine( - each_driver->get_position(fw::Space_Screen), - each_target->get_position(fw::Space_Screen), - IM_COL32(0, 0, 255, 30), 1.0f); - } - } - } + // Gather only visible views or their parent (recursively) + auto pool = Pool::get_pool(); + std::vector clean_drivers = NodeView::substitute_with_parent_if_not_visible( pool->get( m_drivers ), true ); + std::vector clean_targets = NodeView::substitute_with_parent_if_not_visible( pool->get( m_targets ), true ); + + // If we still have no targets or drivers visible, it's not necessary to go further + if ( NodeView::none_is_visible(clean_targets)) return; + if ( NodeView::none_is_visible(clean_drivers)) return; - auto none_is_visible = [](const std::vector& _views)-> bool { - auto is_visible = [](const NodeView* view) { return view->is_visible(); }; - return std::find_if(_views.begin(), _views.end(), is_visible) == _views.end(); - }; - if (none_is_visible(clean_targets) || none_is_visible(clean_drivers)) return; + // To control visually + draw_debug_lines( clean_drivers, clean_targets ); const Config& config = Nodable::get_instance().config; @@ -78,76 +64,80 @@ void NodeViewConstraint::apply(float _dt) NodeView* driver = clean_drivers[0]; const bool align_bbox_bottom = m_flags & ConstrainFlag_ALIGN_BBOX_BOTTOM; const float y_direction = align_bbox_bottom ? 1.0f : -1.0f; - float size_x_total = 0.0f; - ImVec2 driver_pos = driver->get_position(fw::Space_Local); - ImVec2 cursor_pos = driver_pos; + ImVec2 virtual_cursor = driver->get_position(fw::Space_Local); const Node& driver_owner = *driver->get_owner(); - std::vector target_rects; + auto target_rects = get_rect( clean_targets ); + + // Determine horizontal alignment + //------------------------------- - // Compute each target_rect and size_x_total : - //----------------------- - for (auto each_target : clean_targets) + Align halign = Align_CENTER; + + // Align right when driver is an instruction without being connected to a predecessor + if ( driver_owner.is_instruction() && !driver_owner.predecessors().empty() && not align_bbox_bottom ) { - ImRect rect; - if( !(each_target->pinned() || !each_target->is_visible()) ) - { - rect = each_target->get_rect( true ); - } - target_rects.push_back(rect); - size_x_total += rect.GetWidth(); + halign = Align_END; } - // Determine x position start: - //--------------------------- + // Determine virtual_cursor.x from alignment + //---------------------------------- - // x alignment - // - // We add an indentation when driver is an instruction without being connected to a predecessor - const bool align_right = driver_owner.is_instruction() && !driver_owner.predecessors().empty() && not align_bbox_bottom; - if ( align_right ) + switch( halign ) { - cursor_pos.x += driver->get_size().x / 4.0f - + config.ui_node_spacing; + case Align_START: + { + FW_EXPECT(false, "not implemented") + } - // Otherwise we simply align vertically - } else { - cursor_pos.x -= size_x_total / 2.0f; + case Align_END: + { + virtual_cursor.x += driver->get_size().x / 4.0f + config.ui_node_spacing; + break; + } + + case Align_CENTER: + { + float size_x_total = 0.0f; + std::for_each( target_rects.begin(), target_rects.end(),[&](auto each ) { size_x_total += each.GetSize().x; }); + virtual_cursor.x -= size_x_total / 2.0f; + } } // Constraint in row: //------------------- - cursor_pos.y += y_direction * driver->get_size().y / 2.0f; + virtual_cursor.y += y_direction * driver->get_size().y / 2.0f; for (int target_index = 0; target_index < clean_targets.size(); target_index++) { NodeView* each_target = clean_targets[target_index]; - if ( !each_target->pinned() && each_target->is_visible() ) + const Node& target_owner = *each_target->get_owner(); + + // Guards + if ( !each_target->is_visible() ) continue; + if ( each_target->pinned() ) continue; + if ( !target_owner.should_be_constrain_to_follow_output( driver_owner.poolid() ) && !align_bbox_bottom ) continue; + + // Compute new position for this input view + ImRect& target_rect = target_rects[target_index]; + + ImVec2 relative_pos( + target_rect.GetWidth() / 2.0f, + y_direction * (target_rect.GetHeight() / 2.0f + config.ui_node_spacing) + ); + + if ( align_bbox_bottom ) relative_pos += y_direction * config.ui_node_spacing; + + // Add a vertical space to avoid having too much wires aligned on x-axis + // useful for "for" nodes. + if( halign == Align_END && clean_targets.size() > 1 ) { - // Compute new position for this input view - ImRect& target_rect = target_rects[target_index]; - - ImVec2 relative_pos( - target_rect.GetWidth() / 2.0f, - y_direction * (target_rect.GetHeight() / 2.0f + config.ui_node_spacing) - ); - - if ( align_bbox_bottom ) relative_pos += y_direction * config.ui_node_spacing; - - if( align_right && clean_targets.size() > 1 ) - { - // add a vertical space to avoid having too much wires aligned on x-axis - int reverse_y_spacing = (clean_targets.size() - 1 - target_index) * config.ui_node_spacing * 1.5f; - relative_pos.y += y_direction * reverse_y_spacing; - } - - const Node& target_owner = *each_target->get_owner(); - const bool constrained = target_owner.should_be_constrain_to_follow_output( driver_owner.poolid() ); - if ( constrained || align_bbox_bottom ) - { - auto target_physics = target_owner.get_component(); - target_physics->add_force_to_translate_to(cursor_pos + relative_pos + m_offset, config.ui_node_speed, true); - cursor_pos.x += target_rect.GetWidth() + config.ui_node_spacing; - } + float reverse_y_spacing = float(clean_targets.size() - 1 - target_index) * config.ui_node_spacing * 1.5f; + relative_pos.y += y_direction * reverse_y_spacing; } + + auto target_physics = target_owner.get_component(); + target_physics->add_force_to_translate_to( virtual_cursor + relative_pos + m_offset, config.ui_node_speed, true); + virtual_cursor.x += target_rect.GetWidth() + config.ui_node_spacing; + } break; } @@ -197,6 +187,23 @@ void NodeViewConstraint::apply(float _dt) } } +void NodeViewConstraint::draw_debug_lines(const std::vector& _drivers,const std::vector& _targets ) +{ + if( ImGuiEx::debug ) + { + for (auto each_target: _targets ) + { + for (auto each_driver: _drivers ) + { + ImGuiEx::DebugLine( + each_driver->get_position( Space_Screen), + each_target->get_position( Space_Screen), + IM_COL32(0, 0, 255, 30), 1.0f); + } + } + } +} + void NodeViewConstraint::add_target(PoolID _target) { FW_ASSERT( _target ); diff --git a/src/nodable/gui/NodeViewConstraint.h b/src/nodable/gui/NodeViewConstraint.h index 03b3e9868..0394a7d2e 100644 --- a/src/nodable/gui/NodeViewConstraint.h +++ b/src/nodable/gui/NodeViewConstraint.h @@ -11,6 +11,17 @@ namespace ndbl { class NodeView; using fw::PoolID; + enum Align { + Align_START, + Align_CENTER, + Align_END, + }; + + enum Direction { + Direction_ROW , + Direction_COLUMN, + }; + typedef int ConstrainFlags; enum ConstrainFlag_ { @@ -41,11 +52,12 @@ namespace ndbl { */ class NodeViewConstraint { public: + // Lambda returning true if this constrain should apply. using Filter = std::function; NodeViewConstraint(const char* _name, ConstrainFlags ); void apply(float _dt); - void apply_when(const Filter& _lambda) { m_filter = _lambda; } + void apply_when(const Filter& _lambda) { m_should_apply = _lambda; } void add_target(PoolID); void add_driver(PoolID); void add_targets(const std::vector>&); @@ -61,9 +73,12 @@ namespace ndbl { private: const char* m_name; bool m_is_active; - Filter m_filter; // Lambda returning true if this constrain should apply. + Filter m_should_apply; ConstrainFlags m_flags; std::vector> m_drivers; // driving the targets std::vector> m_targets; + static void draw_debug_lines( + const std::vector& _drivers, + const std::vector& _targets); }; } \ No newline at end of file diff --git a/src/nodable/gui/SlotView.cpp b/src/nodable/gui/SlotView.cpp index 9c5e82460..738364f18 100644 --- a/src/nodable/gui/SlotView.cpp +++ b/src/nodable/gui/SlotView.cpp @@ -85,10 +85,8 @@ void SlotView::behavior(SlotView& _view, bool _readonly) { if ( ImGui::MenuItem(ICON_FA_TRASH " Disconnect")) { - Event event{}; - event.type = EventType_slot_disconnected; - event.slot.first = _view.m_slot; - fw::EventManager::get_instance().push_event((fw::Event&)event); + auto& event_manager = fw::EventManager::get_instance(); + event_manager.dispatch({ _view.slot() }); } ImGui::EndPopup(); @@ -148,11 +146,8 @@ void SlotView::drop_behavior(bool &require_new_node, bool _enable_edition) { if ( s_hovered ) { - SlotEvent evt{}; - evt.type = EventType_slot_dropped; - evt.first = s_dragged->m_slot; - evt.second = s_hovered->m_slot; - fw::EventManager::get_instance().push_event((fw::Event&)evt); + auto& event_manager = fw::EventManager::get_instance(); + event_manager.dispatch({ s_dragged->m_slot, s_hovered->m_slot}); reset_hovered(); reset_dragged();