From dc0d3a67a2873d81d11514fe6dedd2f0ff19ef13 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Thu, 30 Jul 2020 13:30:45 -0400 Subject: [PATCH 1/9] Add our current progress on the Turorial class --- source/web/Tutorial.h | 898 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 source/web/Tutorial.h diff --git a/source/web/Tutorial.h b/source/web/Tutorial.h new file mode 100644 index 0000000000..fbccff069a --- /dev/null +++ b/source/web/Tutorial.h @@ -0,0 +1,898 @@ +#ifndef EMP_TUTORIAL_H +#define EMP_TUTORIAL_H + +#ifdef __EMSCRIPTEN__ + #include "web/web.h" +#endif + +#include "base/vector.h" +#include "base/Ptr.h" +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ + namespace UI = emp::web; +#endif +class Trigger; +class State; +class Tutorial; + + + +// TODO: + +// util methods: StateHasTrigger(trigger_id), etc +// improve overlayeffect (take doc?) + + +class Trigger { + + friend class Tutorial; + friend class State; + +protected: + + virtual ~Trigger(){;} + + emp::Ptr tutorial_ptr; // pointer to the Tutorial so we can notify it when this trigger fires. + + bool active = false; + + // The same trigger can be used to move between multiple pairs of states, so store all the pairs here. + std::unordered_map next_state_map; + + std::function callback; + + + bool IsActive() {return active;} + + void Notify( ); + + void SetTutorial(emp::Ptr tut) {tutorial_ptr = tut;} + + // Given a state, what state are we set to move to next? + std::string GetNextState(std::string current_state) { + emp_assert(HasState(current_state)); + return next_state_map[current_state]; + } + + // not used, either delete or make Tutorial method for it + std::vector GetStates() { + std::vector states; + for (auto state_pair : next_state_map) + states.push_back(state_pair.first); + return states; + } + + // Does the given state contain this trigger? + bool HasState(std::string state_name) { + return (next_state_map.find(state_name) != next_state_map.end()); + } + + // How many states contain this trigger? + size_t GetStateCount() { + return next_state_map.size(); + } + + void ManualFire(std::string current_state) { + emp_assert(HasState(current_state)); + Notify(); + } + + void SetCallback(std::function cb) { callback = cb; } + + + virtual void Activate() = 0; + virtual void Deactivate() = 0; + + + // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. + // Makes it simpler to override those functions in custom classes. + void PerformActivation() { + emp_assert(!active); + Activate(); + active = true; + } + void PerformDeactivation() { + emp_assert(active); + Deactivate(); + active = false; + } + + // Add a pair of states that this trigger is associated with (it can move the tutorial from state to next_state). + void AddStatePair(std::string state, std::string next_state) { + emp_assert(!HasState(state)); + next_state_map[state] = next_state; + } + + void RemoveState(std::string state_name) { + emp_assert(HasState(state_name)); + next_state_map.erase(state_name); + // deactivate if active? + } + +}; + + + +#ifdef __EMSCRIPTEN__ +template +class EventListenerTrigger : public Trigger { + + friend class Tutorial; + friend class State; + +protected: + + UI::internal::WidgetFacet& widget; + std::string event_name; + std::string handler_id; + +public: + + EventListenerTrigger(UI::internal::WidgetFacet& _widget, const std::string& _event_name): + widget(_widget), event_name(_event_name) { + } + + void Activate() override{ + std::cout << "In Activate! Event name: " << event_name.c_str() << std::endl; + widget.On(event_name.c_str(), [this]() { this->Notify(); }, event_name + "_tutorial_handler"); + + } + + + void Deactivate() override { + std::cout << "In Deactivate! Event name: " << event_name.c_str() << std::endl; + widget.RemoveListener(event_name, event_name + "_tutorial_handler"); + } + +}; +#endif + + + + +class ManualTrigger : public Trigger { + +friend class Tutorial; +friend class State; + +private: + + ManualTrigger() {} + + void Activate() override {} + void Deactivate() override {} + +}; + + + +class VisualEffect { + + friend class Tutorial; + friend class State; + + +private: + + bool active = false; + + // Set of all states using this visual + std::unordered_set states_set; + + virtual void Activate() = 0; + virtual void Deactivate() = 0; + + + // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. + // Makes it simpler to override those functions in custom classes. + void PerformActivation() { + emp_assert(!active); + Activate(); + active = true; + } + void PerformDeactivation() { + emp_assert(active); + Deactivate(); + active = false; + } + + + void AddState(std::string state_name) { + states_set.insert(state_name); + } + + void RemoveState(std::string state_name) { + states_set.erase(state_name); + } + +}; + + +#ifdef __EMSCRIPTEN__ +template +class CSSEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +protected: + + UI::internal::WidgetFacet& widget; + + std::unordered_map new_attributes_map; + std::unordered_map saved_attributes_map; + + CSSEffect(UI::internal::WidgetFacet& _widget, std::string attr, std::string val) : widget(_widget) { + new_attributes_map[attr] = val; + } + + void Activate() override { + for (auto attr_pair : new_attributes_map) { + saved_attributes_map[attr_pair.first] = widget.GetCSS(attr_pair.first); // store the starting value to reset it after + widget.SetCSS(attr_pair.first, attr_pair.second); + } + } + + void Deactivate() override { + for (auto attr_pair : new_attributes_map) { + widget.SetCSS(attr_pair.first, saved_attributes_map[attr_pair.first]); + //std::cout << "resetting attribute " << attr_pair.first << " to " << saved_attributes_map[attr_pair.first] << std::endl; + } + } + + +}; +#endif + + +#ifdef __EMSCRIPTEN__ +template +class PopoverEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +UI::Div parent_widget; +UI::internal::WidgetFacet& widget; +UI::Div popover_container; +UI::Div popover_text; +UI::Div popover_arrow; +//UI::Widget& original_parent_widget; +std::string message; +std::string popover_id; + public: + + //PopoverEffect(UI::internal::WidgetFacet& _widget, UI::Widget & _orig_parent, std::string _message) : + PopoverEffect(UI::internal::WidgetFacet& _widget, std::string _message) : + parent_widget(_widget.GetID() + "_popover_parent"), + widget(_widget), + popover_container(_widget.GetID() + "_popover_container"), + popover_text(_widget.GetID() + "_popover_text"), + popover_arrow(_widget.GetID() + "_popover_arrow"), + //original_parent_widget(_orig_parent), + message(_message){ + } + + void Activate() { + + emp_assert(parent_widget != nullptr); + + std::cout << "Adding popover" << std::endl; + widget.WrapWithInPlace(parent_widget); + std::cout << "1" << std::endl; + parent_widget.SetCSS("position", "relative"); + std::cout << "2" << std::endl; + popover_text << message; + popover_text.SetAttr("class", "popup_text"); + popover_arrow.SetAttr("class", "popup_arrow"); + std::cout << "3" << std::endl; + popover_container << popover_text; + popover_container << popover_arrow; + popover_container.SetAttr("class", "popup_container popup_show"); + std::cout << "4" << std::endl; + parent_widget << popover_container; + if(widget.GetCSS("float") != ""){ + parent_widget.SetCSS("float", widget.GetCSS("float")); + } + std::cout << "finished adding" << std::endl; + } + + + void Deactivate() { + std::cout << "Removing popover" << std::endl; + popover_container.SetAttr("class", "popup_container"); + std::cout << "1" << std::endl; + parent_widget->RemoveChild(widget); + std::cout << "2" << std::endl; + parent_widget->parent->ReplaceChild(parent_widget, widget); + std::cout << "Removed popover" << std::endl; +} + +}; +#endif + + +#ifdef __EMSCRIPTEN__ +class OverlayEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +private: + + UI::Div& parent; + UI::Div overlay; + std::string color; + float opacity; + int z_index; + + OverlayEffect(UI::Div& _parent, std::string _color, float _opacity, int _z_index) : parent(_parent), color(_color), opacity(_opacity), z_index(_z_index) {} + + void Activate() { + + UI::Div over("overlay"); + overlay = over; + + overlay.SetCSS("background-color", color); + overlay.SetCSS("opacity", opacity); + overlay.SetCSS("z_index", z_index); + overlay.SetCSS("position", "absolute"); + overlay.SetCSS("width", "100%"); + overlay.SetCSS("height", "100%"); + overlay.SetCSS("top", "0px"); + overlay.SetCSS("left", "0px"); + parent << overlay; + + std::cout << "Added overlay" << std::endl; + + } + + void Deactivate() { + + overlay -> parent ->RemoveChild(overlay); +} + +}; +#endif + + +class State { + + friend class Tutorial; + +private: + + std::unordered_set trigger_id_set; + std::unordered_set visual_id_set; + + + std::string name; + std::function callback; + + State(){;} + State(std::string _name) : name(_name) {} + + void SetCallback(std::function cb) { callback = cb; } + + bool HasTrigger(std::string trigger_id) { + return (trigger_id_set.find(trigger_id) != trigger_id_set.end()); + } + + bool HasVisualEffect(std::string visual_id) { + return (visual_id_set.find(visual_id) != visual_id_set.end()); + } + + // add the trigger id to set of id's + void AddTrigger(std::string trigger_id) { + trigger_id_set.insert(trigger_id); + } + + // remove the trigger id from set of id's + void RemoveTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + trigger_id_set.erase(trigger_id); + } + + // add the visual id to set of id's + void AddVisualEffect(std::string visual_id) { + emp_assert(HasVisualEffect(visual_id)); + visual_id_set.insert(visual_id); + } + + // remove the visual id from set of id's + void RemoveVisualEffect(std::string visual_id) { + visual_id_set.erase(visual_id); + } + + // Activate all triggers and visuals for this state. Called when the state is entered. + void Activate(const std::unordered_map>& trigger_ptr_map, + const std::unordered_map>& visual_ptr_map) { + std::cout << "Activate state: " << name << std::endl; + std::cout << "Activateing " << trigger_id_set.size() << " triggers!" << std::endl; + std::cout << "Activateing " << visual_id_set.size() << " visuals!" << std::endl; + + // Activate all triggers + for(auto trigger_id : trigger_id_set) { + trigger_ptr_map.at(trigger_id) -> PerformActivation(); + //trigger_ptr_map.at(trigger_id) -> SetActive(); + + } + + // Activate all visuals + for(auto visual_id : visual_id_set) { + visual_ptr_map.at(visual_id) -> PerformActivation(); + //visual_ptr_map.at(visual_id) -> SetActive(); + } + + } + + // Deactivate all triggers and visuals for this state. Called when the state is exited. + void Deactivate(const std::unordered_map>& trigger_ptr_map, + const std::unordered_map>& visual_ptr_map) { + + // Deactivate all triggers + for(auto trigger_id : trigger_id_set) { + trigger_ptr_map.at(trigger_id) -> PerformDeactivation(); + //trigger_ptr_map.at(trigger_id) -> SetInactive(); + } + + // Deactivate all visuals + for(auto visual_id : visual_id_set) { + visual_ptr_map.at(visual_id) -> PerformDeactivation(); + //visual_ptr_map.at(visual_id) -> SetInactive(); + } + + std::cout << "Deactivate state: " << name << std::endl; + std::cout << "Removing " << trigger_id_set.size() << " triggers!" << std::endl; + std::cout << "Removing " << visual_id_set.size() << " visuals!" << std::endl; + } + + // how many Triggers does this state have? + size_t GetTriggerCount() { + return trigger_id_set.size(); + } + + // how many VisualEffects does this state have? + size_t GetVisualCount() { + return visual_id_set.size(); + } + + +}; + + + +class Tutorial { + + friend void Trigger::Notify(); // Trigger's Notify() can access our private members. Needed so it can call OnTrigger() + + +private: + + bool active = false; + + std::unordered_map states; // Store all the states for this Tutorial + std::unordered_map> trigger_ptr_map; // Store all the triggers for this Tutorial + std::unordered_map> visual_ptr_map; // Store all the visualeffects for this Tutorial + + std::string current_state; + + size_t num_triggers_added = 0; + size_t num_visuals_added = 0; + + // Retrieve a State object given its ID. + State & GetState(std::string & state_name) { + //add assert + return states.at(state_name); + } + + + // Is the given trigger id an existing trigger? + bool HasTrigger(std::string trigger_id) { + return trigger_ptr_map.find(trigger_id) != trigger_ptr_map.end(); + } + + void DeleteTrigger(std::string trigger_id) { + delete trigger_ptr_map[trigger_id]; + trigger_ptr_map.erase(trigger_id); + } + + // Retrieve a pointer to the Trigger with the given ID + emp::Ptr GetTrigger(std::string trigger_id) { + return trigger_ptr_map[trigger_id]; + } + + bool HasVisualEffect(std::string visual_id) { + return visual_ptr_map.find(visual_id) != visual_ptr_map.end(); + } + + // Retrieve a pointer to the Trigger with the given ID + emp::Ptr GetVisualEffect(std::string visual_id) { + return visual_ptr_map[visual_id]; + } + + // A Trigger calls this when it's fired, passing a pointer to itself. + void OnTrigger(emp::Ptr trigger) { + + std::cout << "Leaving state " << current_state << std::endl; + + // Deactivate current state + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + //move to the next state + current_state = trigger -> GetNextState(current_state) ; + GetState(current_state).Activate(trigger_ptr_map, visual_ptr_map); + + std::cout << "Entering state " << current_state << std::endl; + + // Stop here if this state has no triggers + if (GetState(current_state).GetTriggerCount() == 0) + { + Stop(); + } + + // execute callbacks for the state and trigger + if (GetState(current_state).callback) GetState(current_state).callback(); + if (trigger -> callback) trigger -> callback(); + + } + + + + +public: + + // -------------------------------- INTERFACE ---------------------------------------- + + // These are the only functions to be called outside of this file :P + + + bool IsActive() { + return active; + } + + std::string GetCurrentState() { + if (active) return current_state; + return ""; + } + + // Launch into the tutorial at a particular state + void StartAtState(std::string state_name){ + if (active) + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + // todo: make sure there's a trigger, if not then stop here + + current_state = state_name; + std::cout << "visual size in Start: " << GetState(current_state).GetVisualCount() << std::endl; + GetState(current_state).Activate(trigger_ptr_map, visual_ptr_map); + active = true; + + // state callback, if any + if (GetState(current_state).callback) + GetState(current_state).callback(); + } + + // End the tutorial + void Stop() { + + if (!active) + return; + + // Deactivate current state + if (HasState(current_state)) + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + active = false; + + std::cout << "Tutorial Finished!" << std::endl; +#ifdef __EMSCRIPTEN__ + EM_ASM( {alert("Tutorial Complete!");} ); +#endif + } + + // Create and store a new state with given name + Tutorial& AddState(std::string state_name, std::function callback=nullptr) { + emp_assert(!HasState(state_name)); + states.emplace(std::make_pair(state_name, State(state_name))); + GetState(state_name).SetCallback(callback); + + return *this; + } + + // Is the given state name an existing state? + bool HasState(std::string state_name) { + return states.find(state_name) != states.end(); // any other checks? + } + + Tutorial& AddManualTrigger(std::string cur_state, std::string next_state, std::string trigger_id="", + std::function callback=nullptr) { + + if (trigger_id.empty()) + trigger_id = std::string("unnamed_trigger_") + std::to_string(num_triggers_added); + + emp_assert(!HasState(trigger_id)); + + emp::Ptr trigger_ptr = new ManualTrigger(); + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + trigger_ptr -> SetCallback(callback); + + num_triggers_added++; + + return *this; + } + + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddEventListenerTrigger(std::string cur_state, std::string next_state, + UI::internal::WidgetFacet& w, std::string event_name, + std::string trigger_id="", std::function callback=nullptr) + { + if (trigger_id.empty()) + trigger_id = std::string("unnamed_trigger_") + std::to_string(num_triggers_added); + + emp_assert(!HasState(trigger_id)); + + emp::Ptr trigger_ptr = new EventListenerTrigger(w, event_name); + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + trigger_ptr -> SetCallback(callback); + + num_triggers_added++; + + return *this; + } +#endif + + + Tutorial& AddExistingTrigger(std::string cur_state, std::string next_state, std::string trigger_id) { + + emp::Ptr trigger_ptr = trigger_ptr_map[trigger_id]; + trigger_ptr -> AddStatePair(cur_state, next_state); + GetState(cur_state).AddTrigger(trigger_id); + + return *this; + } + + + template + Tutorial& AddCustomTrigger(std::string cur_state, std::string next_state, Args&&... args, + std::string trigger_id, std::function callback=nullptr) { + + std::cout << "The trigger id is: " << trigger_id << std::endl; + + + static_assert(std::is_base_of::value, "T must derive from Trigger"); + emp::Ptr trigger_ptr = new T(std::forward(args)...); + //std::cout << "Created trigger of type: " << static_cast(trigger_ptr)->GetType() << std::endl; + + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + num_triggers_added++; + + return *this; + + } + + + void RemoveTrigger(std::string trigger_id, std::string state_name) { + emp_assert(HasTrigger(trigger_id)); + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + + // deactivate the trigger if active + if (trigger_ptr -> IsActive()) + trigger_ptr -> Deactivate(); + + // remove state from trigger + trigger_ptr -> RemoveState(state_name); + + // remove the trigger from state + GetState(state_name).RemoveTrigger(trigger_id); + + // remove from tutorial if necessary + if (trigger_ptr -> GetStateCount() == 0) + DeleteTrigger(trigger_id); + + } + + + void FireTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + GetTrigger(trigger_id) -> ManualFire(current_state); + } + + + void ActivateTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + trigger_ptr -> PerformActivation(); + } + + void DeactivateTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + trigger_ptr -> PerformDeactivation(); + } + + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddCSSEffect(std::string state_name, UI::internal::WidgetFacet& w, + std::string attr, std::string val, std::string visual_id="") + { + emp::Ptr visual_ptr = new CSSEffect(w, attr, val); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddPopoverEffect(std::string state_name, UI::internal::WidgetFacet& w, + std::string message, std::string visual_id="") + { + emp::Ptr visual_ptr = new PopoverEffect(w, message); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + + +#ifdef __EMSCRIPTEN__ + Tutorial& AddOverlayEffect(std::string state_name, UI::Div& parent, std::string color="black", float opacity=0.4, int z_index=100, std::string visual_id="") { + + emp_assert(HasState(state_name)); + emp_assert(!HasVisualEffect(visual_id)); + + emp::Ptr visual_ptr = new OverlayEffect(parent, color, opacity, z_index); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + + template + Tutorial& AddCustomVisualEffect(std::string state_name, Args&&... args, + std::string visual_id) { + + emp_assert(!HasVisualEffect(visual_id)); + + static_assert(std::is_base_of::value, "T must derive from VisualEffect"); + emp::Ptr visual_ptr = new T(std::forward(args)...); + + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state) { + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + + } + + void ActivateVisualEffect(std::string visual_id) { + emp_assert(HasTrigger(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + visual_ptr -> PerformActivation(); + } + + void DeactivateVisualEffect(std::string visual_id) { + emp_assert(HasTrigger(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + visual_ptr -> PerformDeactivation(); + } + + + void SetStateCallback(std::string state_name, std::function fun) { + emp_assert(HasState(state_name)); + GetState(state_name).callback = fun; + } + + void SetTriggerCallback(std::string trigger_id, std::function fun) { + emp_assert(HasTrigger(trigger_id)); + GetTrigger(trigger_id) -> callback = fun; + } + + + // bool IsTriggerActive() {} + + // bool + + +}; + + + +void Trigger::Notify(){ + tutorial_ptr -> OnTrigger(this); +} + +#endif From 358290cb25e1121d7e3a89e4be3558eb8e9d0b61 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Thu, 30 Jul 2020 15:31:27 -0400 Subject: [PATCH 2/9] Add ReplaceChild method to Div.h Div.ReplaceChild functions the same as RemoveChild(old) followed by AddChild(new), only it places the new child in the old child's position in the element ordering --- source/web/Div.h | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/source/web/Div.h b/source/web/Div.h index c4fc10e301..633a0b6fee 100644 --- a/source/web/Div.h +++ b/source/web/Div.h @@ -191,6 +191,45 @@ namespace web { } } + /// Remove the old widget, putting the new widget in its place (i.e., same index) + /// @param the Widget to remove + /// @param the Widget to add + void ReplaceChild(Widget & old_child, Widget new_child) override { + // ensure child is present + emp_assert(1 == std::count( + std::begin(m_children), + std::end(m_children), + old_child + )); + // unregister and remove child + Unregister(*std::find( + std::begin(m_children), + std::end(m_children), + old_child + )); + m_children.emplace(std::find( + std::begin(m_children), + std::end(m_children), + old_child + ), new_child); + m_children.erase( + std::remove( + std::begin(m_children), + std::end(m_children), + old_child + ), + std::end(m_children) + ); + + old_child->parent = nullptr; + // update info for new child + new_child->parent = this; + Register(new_child); + new_child->DoActivate(false); + // render changes + if (state == Widget::ACTIVE) ReplaceHTML(); + } + void DoActivate(bool top_level=true) override { for (auto & child : m_children) child->DoActivate(false); internal::WidgetInfo::DoActivate(top_level); From 2e65daa75a4520969b31e837daa85c981fce39e8 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Thu, 30 Jul 2020 15:35:33 -0400 Subject: [PATCH 3/9] Add Document.AddInput method Document had AddX methods for most web classes (Dic, Button, Canvas, etc) but not Input. This follows the lead of those methods. --- source/web/Document.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/web/Document.h b/source/web/Document.h index 233088047f..e3c6a19f75 100644 --- a/source/web/Document.h +++ b/source/web/Document.h @@ -94,6 +94,11 @@ namespace web { info->Append(new_widget); return new_widget; } + template web::Input AddInput(T &&... args) { + web::Input new_widget(std::forward(args)...); + info->Append(new_widget); + return new_widget; + } template web::Table AddTable(T &&... args) { web::Table new_widget(std::forward(args)...); info->Append(new_widget); From 4b3472bab8bbaaf5c94399f83d741b0904d5a969 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Thu, 30 Jul 2020 15:49:33 -0400 Subject: [PATCH 4/9] Change Input class to use oninput for callbacks See Issue 303 Issue: 303 --- source/web/Input.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/web/Input.h b/source/web/Input.h index 993d918386..e06677a9a1 100644 --- a/source/web/Input.h +++ b/source/web/Input.h @@ -81,7 +81,7 @@ namespace web { if (value != "") HTML << " value=\"" << value << "\""; // Add a current value if there is one. if (step != "") HTML << " step=\"" << step << "\""; // Add a step if there is one. HTML << " id=\"" << id << "\""; // Indicate ID. - HTML << " onchange=\"" << onchange_info << "\""; // Indicate action on change. + HTML << " oninput=\"" << onchange_info << "\""; // Indicate action on change. HTML << ">" << label << ""; // Close and label the Input. } From f3ce317c8de44efa51c563b1a8adc5b97bd582e1 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Fri, 31 Jul 2020 15:16:30 -0400 Subject: [PATCH 5/9] Add Listeners changes Listeners.h was modified to handle multiple event listeners for each event on a single element (supported by jQuery). All code changes should support backwards compatibility except for GetMap because the map itself has changed. --- source/web/Listeners.h | 111 +++++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 27 deletions(-) diff --git a/source/web/Listeners.h b/source/web/Listeners.h index 029c682e70..d37c5c5b05 100644 --- a/source/web/Listeners.h +++ b/source/web/Listeners.h @@ -8,6 +8,8 @@ */ +// NEW CODE + #ifndef EMP_WEB_LISTENERS_H #define EMP_WEB_LISTENERS_H @@ -27,43 +29,60 @@ namespace web { /// Track a set of JavaScript Listeners with their callback IDs. class Listeners { private: - std::map listeners; ///< Map triggers to callback IDs + std::map> listeners; ///< Map triggers to list of callback IDs. The outer map associates event names with an inner map, which maps handler IDs to their callbacks. public: Listeners() { ; } Listeners(const Listeners &) = default; Listeners & operator=(const Listeners &) = default; - /// How many listeners are we tracking? - size_t GetSize() const { return listeners.size(); } + /// How many listeners are we tracking? + // Should we just store this? + size_t GetSize() const { + size_t count = 0; + for (auto event_pair : listeners) + for (auto handler_pair : event_pair.second) { + count++; + } + return count; + } /// Use a pre-calculated function ID with a new listener. - Listeners & Set(const std::string & name, size_t fun_id) { - emp_assert(!Has(name)); - listeners[name] = fun_id; + Listeners & Set(const std::string & event_name, size_t fun_id, std::string handler_id="default") { + emp_assert(!HasHandler(event_name, handler_id)); + listeners[event_name][handler_id] = fun_id; return *this; } /// Calculate its own function ID with JSWrap. template - Listeners & Set(const std::string & name, const std::function & in_fun) { - emp_assert(!Has(name)); - listeners[name] = JSWrap(in_fun); + Listeners & Set(const std::string & event_name, const std::function & in_fun, std::string handler_id="default") { + emp_assert(!HasHandler(event_name, handler_id)); + listeners[event_name][handler_id] = JSWrap(in_fun); return *this; } - /// Determine if a specified listener exists. + /// Determine if any listener exists with the specified event name. bool Has(const std::string & event_name) const { return listeners.find(event_name) != listeners.end(); } - /// Get the ID associated with a specific listener. - size_t GetID(const std::string & event_name) { - emp_assert(Has(event_name)); - return listeners[event_name]; + /// Determine if any listener exists with the specified event name and handler ID. + bool HasHandler(const std::string & event_name, std::string handler_id = "default") const { + if (listeners.find(event_name) != listeners.end()) { + const std::map& m = listeners.at(event_name); + return m.find(handler_id) != m.end(); + } + return false; + } + + /// Get the ID associated with a specific listener. + size_t GetID(const std::string & event_name, std::string handler_id = "default") { + emp_assert(HasHandler(event_name, handler_id)); + return listeners[event_name][handler_id]; } - const std::map & GetMap() const { + const std::map> & GetMap() const { return listeners; } @@ -73,32 +92,56 @@ namespace web { listeners.clear(); } - /// Remove a specific listener. + /// Remove all listeners with the given event name. void Remove(const std::string & event_name) { // @CAO: Delete function to be called. listeners.erase(event_name); } + /// Remove the listener with the given event name and handler ID. + void Remove(const std::string & event_name, std::string handler_id = "default") { + // @CAO: Delete function to be called. + if (Has(event_name)) + { + listeners[event_name].erase(handler_id); + std::cout << "removed listener " << handler_id << std::endl; + } + } + /// Apply all of the listeners being tracked. - void Apply(const std::string & widget_id) { + void Apply(const std::string & widget_id, bool add_before_onclick = false) { // Find the current object only once. #ifdef __EMSCRIPTEN__ EM_ASM_ARGS({ var id = UTF8ToString($0); emp_i.cur_obj = $( '#' + id ); }, widget_id.c_str()); + if(add_before_onclick){ + EM_ASM({ + var onclick_handler = emp_i.onclick; + emp_i.removeProp('onclick'); + }); + } #endif for (auto event_pair : listeners) { -#ifdef __EMSCRIPTEN__ - EM_ASM_ARGS({ - var name = UTF8ToString($0); - emp_i.cur_obj.on( name, function(evt) { emp.Callback($1, evt); } ); - }, event_pair.first.c_str(), event_pair.second); -#else - std::cout << "Setting '" << widget_id << "' listener '" << event_pair.first - << "' to '" << event_pair.second << "'."; + for (auto handler_pair : event_pair.second) { + size_t & fun_id = handler_pair.second; + #ifdef __EMSCRIPTEN__ + EM_ASM_ARGS({ + var name = UTF8ToString($0); + emp_i.cur_obj.on( name, function(evt) { emp.Callback($1, evt); } ); + }, event_pair.first.c_str(), fun_id); + if(add_before_onclick){ + EM_ASM({ + emp_i.click(onclick_handler); + }); + } + #else + std::cout << "Setting '" << widget_id << "' listener '" << event_pair.first + << "' to '" << fun_id << "'."; #endif + } } } @@ -106,13 +149,27 @@ namespace web { /// Apply a SPECIFIC listener. static void Apply(const std::string & widget_id, const std::string event_name, - size_t fun_id) { + size_t fun_id, bool add_before_onclick = false) { #ifdef __EMSCRIPTEN__ EM_ASM_ARGS({ var id = UTF8ToString($0); var name = UTF8ToString($1); + if($3){ + var onclick_handler = $('#' + id).prop('onclick'); + if(onclick_handler){ + $('#' + id).prop('onclick', null); + alert('bumping onclick back ' + '$(#' + id + ').prop(onclick, null)'); + } + } $( '#' + id ).on( name, function(evt) { emp.Callback($2, evt); } ); - }, widget_id.c_str(), event_name.c_str(), fun_id); + if($3){ + if(onclick_handler){ + //$('#' + id).click(onclick_handler); + //$('#' + id).on('click', onclick_handler); + //$('#' + id).attr('onclick', 'onclick_handler();'); + } + } + }, widget_id.c_str(), event_name.c_str(), fun_id, add_before_onclick); #else std::cout << "Setting '" << widget_id << "' listener '" << event_name << "' to function id '" << fun_id << "'."; From 0d06db542b98c7983ae55035929f31feba92b44d Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Fri, 31 Jul 2020 15:31:07 -0400 Subject: [PATCH 6/9] Propogate Listener changes into Widget and subs Listener class has new option arguments (previous commit) and this propogates those through the wrappers present in Widget and derived clases. Also adds option "in_place" arg to Widget::WrapWith. Keeps the original element's index into the the order of the parent element's children --- source/web/Table.h | 10 +++-- source/web/Tutorial.h | 2 +- source/web/Widget.h | 89 ++++++++++++++++++++++++------------- source/web/_TableCell.h | 4 +- source/web/_TableCol.h | 5 ++- source/web/_TableColGroup.h | 5 ++- source/web/_TableRow.h | 5 ++- source/web/_TableRowGroup.h | 5 ++- 8 files changed, 79 insertions(+), 46 deletions(-) diff --git a/source/web/Table.h b/source/web/Table.h index 4aa8015bb9..582f98f638 100644 --- a/source/web/Table.h +++ b/source/web/Table.h @@ -537,13 +537,15 @@ namespace web { } /// Apply CSS to appropriate component based on current state. - void DoListen(const std::string & event_name, size_t fun_id) override { - parent_t::DoListen(event_name, fun_id); + void DoListen(const std::string & event_name, size_t fun_id, + const std::string handler_id="default", + bool add_before_onclick = false) override { + parent_t::DoListen(event_name, fun_id, handler_id, add_before_onclick); } public: - TableWidget(size_t r, size_t c, const std::string & in_id="") - : WidgetFacet(in_id), cur_row(0), cur_col(0) + TableWidget(size_t r, size_t c, const std::string & in_id=""): + WidgetFacet(in_id), cur_row(0), cur_col(0) { emp_assert(r > 0 && c > 0); // Ensure that we have rows and columns! info = new internal::TableInfo(in_id); diff --git a/source/web/Tutorial.h b/source/web/Tutorial.h index fbccff069a..6c45ec6b52 100644 --- a/source/web/Tutorial.h +++ b/source/web/Tutorial.h @@ -286,7 +286,7 @@ std::string popover_id; emp_assert(parent_widget != nullptr); std::cout << "Adding popover" << std::endl; - widget.WrapWithInPlace(parent_widget); + widget.WrapWith(parent_widget, true); std::cout << "1" << std::endl; parent_widget.SetCSS("position", "relative"); std::cout << "2" << std::endl; diff --git a/source/web/Widget.h b/source/web/Widget.h index 8c31103356..df9101e0a8 100644 --- a/source/web/Widget.h +++ b/source/web/Widget.h @@ -138,6 +138,7 @@ namespace web { bool IsD3Visualiation() const { return GetInfoTypeName() == "D3VisualizationInfo"; } const std::string & GetID() const; ///< What is the HTML string ID for this Widget? + /// Retrieve a specific CSS trait associated with this Widget. /// Note: CSS-related options may be overridden in derived classes that have multiple styles. @@ -253,6 +254,7 @@ namespace web { virtual void AddChild(Widget in) { ; } virtual void RemoveChild(Widget & child) { ; } + virtual void ReplaceChild(Widget & old_child, Widget new_child) { ; } // Record dependants. Dependants are only acted upon when this widget's action is // triggered (e.g. a button is pressed) @@ -581,9 +583,9 @@ namespace web { } /// Listener options may be overridden in derived classes that have multiple listen targets. /// By default DoListen will track new listens and set them up immediately, if active. - virtual void DoListen(const std::string & event_name, size_t fun_id) { - info->extras.listen.Set(event_name, fun_id); - if (IsActive()) Listeners::Apply(info->id, event_name, fun_id); + virtual void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", bool add_before_onclick = false) { + info->extras.listen.Set(event_name, fun_id, handler_id); + if (IsActive()) Listeners::Apply(info->id, event_name, fun_id, add_before_onclick); } public: @@ -645,37 +647,45 @@ namespace web { /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function as no arguments. - return_t & On(const std::string & event_name, const std::function & fun) { + return_t & On(const std::string & event_name, const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes a mouse event as an argument, with full info about mouse. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } - + /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes a keyboard event as an argument, with full info about keyboard. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes two doubles which will be filled in with mouse coordinates. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); auto fun_cb = [this, fun](MouseEvent evt){ double x = evt.clientX - GetXPos(); @@ -683,54 +693,60 @@ namespace web { fun(x,y); }; size_t fun_id = JSWrap(fun_cb); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); + return (return_t &) *this; + } + + return_t & RemoveListener(const std::string & event_name, const std::string handler_id="default") { + info->extras.listen.Remove(event_name, handler_id); + info -> ReplaceHTML(); return (return_t &) *this; } /// Provide a function to be called when the window is resized. - template return_t & OnResize(T && arg) { return On("resize", arg); } + template return_t & OnResize(T && arg, const std::string handler_id="default") { return On("resize", arg, handler_id); } /// Provide a function to be called when the mouse button is clicked in this Widget. - template return_t & OnClick(T && arg) { return On("click", arg); } + template return_t & OnClick(T && arg, const std::string handler_id="default") { return On("click", arg, handler_id); } /// Provide a function to be called when the mouse button is double clicked in this Widget. - template return_t & OnDoubleClick(T && arg) { return On("dblclick", arg); } + template return_t & OnDoubleClick(T && arg, const std::string handler_id="default") { return On("dblclick", arg, handler_id); } /// Provide a function to be called when the mouse button is pushed down in this Widget. - template return_t & OnMouseDown(T && arg) { return On("mousedown", arg); } + template return_t & OnMouseDown(T && arg, const std::string handler_id="default") { return On("mousedown", arg, handler_id); } /// Provide a function to be called when the mouse button is released in this Widget. - template return_t & OnMouseUp(T && arg) { return On("mouseup", arg); } + template return_t & OnMouseUp(T && arg, const std::string handler_id="default") { return On("mouseup", arg, handler_id); } /// Provide a function to be called whenever the mouse moves in this Widget. - template return_t & OnMouseMove(T && arg) { return On("mousemove", arg); } + template return_t & OnMouseMove(T && arg, const std::string handler_id="default") { return On("mousemove", arg, handler_id); } /// Provide a function to be called whenever the mouse leaves the Widget. - template return_t & OnMouseOut(T && arg) { return On("mouseout", arg); } + template return_t & OnMouseOut(T && arg, const std::string handler_id="default") { return On("mouseout", arg, handler_id); } /// Provide a function to be called whenever the mouse moves over the Widget. - template return_t & OnMouseOver(T && arg) { return On("mouseover", arg); } + template return_t & OnMouseOver(T && arg, const std::string handler_id="default") { return On("mouseover", arg, handler_id); } /// Provide a function to be called whenever the mouse wheel moves in this Widget. - template return_t & OnMouseWheel(T && arg) { return On("mousewheel", arg); } + template return_t & OnMouseWheel(T && arg, const std::string handler_id="default") { return On("mousewheel", arg, handler_id); } /// Provide a function to be called whenever a key is pressed down in this Widget. - template return_t & OnKeydown(T && arg) { return On("keydown", arg); } + template return_t & OnKeydown(T && arg, const std::string handler_id="default") { return On("keydown", arg, handler_id); } /// Provide a function to be called whenever a key is pressed down and released in this Widget. - template return_t & OnKeypress(T && arg) { return On("keypress", arg); } + template return_t & OnKeypress(T && arg, const std::string handler_id="default") { return On("keypress", arg, handler_id); } /// Provide a function to be called whenever a key is pressed released in this Widget. - template return_t & OnKeyup(T && arg) { return On("keyup", arg); } + template return_t & OnKeyup(T && arg, const std::string handler_id="default") { return On("keyup", arg, handler_id); } /// Provide a function to be called whenever text is copied in this Widget. - template return_t & OnCopy(T && arg) { return On("copy", arg); } + template return_t & OnCopy(T && arg, const std::string handler_id="default") { return On("copy", arg, handler_id); } /// Provide a function to be called whenever text is cut in this Widget. - template return_t & OnCut(T && arg) { return On("cut", arg); } + template return_t & OnCut(T && arg, const std::string handler_id="default") { return On("cut", arg, handler_id); } /// Provide a function to be called whenever text is pasted in this Widget. - template return_t & OnPaste(T && arg) { return On("paste", arg); } + template return_t & OnPaste(T && arg, const std::string handler_id="default") { return On("paste", arg, handler_id); } /// Create a tooltip for this Widget. return_t & SetTitle(const std::string & _in) { return SetAttr("title", _in); } @@ -858,8 +874,9 @@ namespace web { /// Wrap a wrapper around this Widget. /// @param wrapper the wrapper that will be placed around this Widget + /// @param if in_place in true, wrapper widget maintains same position in parent div /// @return this Widget - return_t & WrapWith(Widget wrapper) { + return_t & WrapWith(Widget wrapper, bool in_place) { // if this Widget is already nested within a parent // we'll need to wedge the wrapper between this Widget and the parent @@ -872,10 +889,17 @@ namespace web { )); const auto parent_info = Info((return_t &) *this)->parent; - - // switch out parent's existing child for wrapper - parent_info->RemoveChild((return_t &) *this); - parent_info->AddChild(wrapper); + + if(in_place){ + this->Deactivate(false); + // switch out parent's existing child for wrapper + parent_info->ReplaceChild((return_t &) *this, wrapper); + } + else{ + // remove existing element and add wrapper + parent_info->RemoveChild((return_t &) *this); + parent_info->AddChild(wrapper); + } } else if (Info(wrapper)->ptr_count == 1) { emp::LibraryWarning( "Only one reference held to wrapper. ", @@ -885,6 +909,7 @@ namespace web { // put this Widget inside of the wrapper wrapper << (return_t &) *this; + if(in_place) wrapper.Redraw(); return (return_t &) *this; } diff --git a/source/web/_TableCell.h b/source/web/_TableCell.h index 4025305090..414443475b 100644 --- a/source/web/_TableCell.h +++ b/source/web/_TableCell.h @@ -37,7 +37,9 @@ namespace web { } /// Update a listener for this cell (override default Table) - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, + const std::string handler_id="default", + bool add_before_onclick = false) override { Info()->rows[cur_row].data[cur_col].extras.listen.Set(event_name, fun_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableCol.h b/source/web/_TableCol.h index dfcd342840..8ea35b8293 100644 --- a/source/web/_TableCol.h +++ b/source/web/_TableCol.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->cols.size() == 0) Info()->cols.resize(GetNumCols()); - Info()->cols[cur_col].extras.listen.Set(event_name, fun_id); + Info()->cols[cur_col].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableColGroup.h b/source/web/_TableColGroup.h index 86ecc703fb..9f09616141 100644 --- a/source/web/_TableColGroup.h +++ b/source/web/_TableColGroup.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->col_groups.size() == 0) Info()->col_groups.resize(GetNumCols()); - Info()->col_groups[cur_col].extras.listen.Set(event_name, fun_id); + Info()->col_groups[cur_col].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableRow.h b/source/web/_TableRow.h index 90e783076b..9d8d46d4d6 100644 --- a/source/web/_TableRow.h +++ b/source/web/_TableRow.h @@ -34,8 +34,9 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { - Info()->rows[cur_row].extras.listen.Set(event_name, fun_id); + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { + Info()->rows[cur_row].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableRowGroup.h b/source/web/_TableRowGroup.h index 2c59a40fef..5f6453ec7d 100644 --- a/source/web/_TableRowGroup.h +++ b/source/web/_TableRowGroup.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->row_groups.size() == 0) Info()->row_groups.resize(GetNumRows()); - Info()->row_groups[cur_row].extras.listen.Set(event_name, fun_id); + Info()->row_groups[cur_row].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } From 33ff019da733492479482b0af3ad6061893c30c0 Mon Sep 17 00:00:00 2001 From: Austin Ferguson Date: Fri, 31 Jul 2020 15:35:47 -0400 Subject: [PATCH 7/9] Add beginning of C++ (i.e. Catch) Tutorial tests --- tests/web/Tutorial_cpp.cc | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/web/Tutorial_cpp.cc diff --git a/tests/web/Tutorial_cpp.cc b/tests/web/Tutorial_cpp.cc new file mode 100644 index 0000000000..c55fd8e010 --- /dev/null +++ b/tests/web/Tutorial_cpp.cc @@ -0,0 +1,57 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2016-2017. +// Released under the MIT Software license; see doc/LICENSE + +#define CATCH_CONFIG_MAIN +#include "third-party/Catch/single_include/catch2/catch.hpp" + +#include +#include + +#include "web/Tutorial.h" + +TEST_CASE("Test AddState/HasState", "[web][Tutorial]") { + // Create a tutorial, it should not have state "test_state" at start + // It should have the state "test_state" after we add it! + Tutorial tut; + REQUIRE(!tut.HasState("test_state")); + tut.AddState("test_state"); + REQUIRE(tut.HasState("test_state")); + REQUIRE(!tut.HasState("test_state_2")); + tut.AddState("test_state_2"); + REQUIRE(tut.HasState("test_state_2")); + REQUIRE(tut.HasState("test_state")); +} + +TEST_CASE("Test StartAtState/Stop/IsActive/GetCurrentState", "[web][Tutorial]") { + // Create a tutorial with two states + // Tutorial should start as inactive with current state "" + // StartAtState should change active status and the current state + + // Setup + Tutorial tut; + REQUIRE(!tut.HasState("state_1")); + REQUIRE(!tut.HasState("state_2")); + tut.AddState("state_1"); + tut.AddState("state_2"); + REQUIRE(tut.HasState("state_1")); + REQUIRE(tut.HasState("state_2")); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); + // Activate state_1 + tut.StartAtState("state_1"); + REQUIRE(tut.IsActive()); + REQUIRE(tut.GetCurrentState() == "state_1"); + // End tutorial + tut.Stop(); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); + // Re-activate, this time with state_2 + tut.StartAtState("state_2"); + REQUIRE(tut.IsActive()); + REQUIRE(tut.GetCurrentState() == "state_2"); + // Stop again + tut.Stop(); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); +} From 2d4cdd66c3ffaef520cb7426cabd74d418400aa5 Mon Sep 17 00:00:00 2001 From: Dylan Rainbow Date: Wed, 14 Oct 2020 18:30:34 -0700 Subject: [PATCH 8/9] Added Tutorial web tests Includes tests for EventListenerTrigger, OverlayEffect, and CSSEffect --- tests/web/Tutorial.cc | 692 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 tests/web/Tutorial.cc diff --git a/tests/web/Tutorial.cc b/tests/web/Tutorial.cc new file mode 100644 index 0000000000..68640df6ad --- /dev/null +++ b/tests/web/Tutorial.cc @@ -0,0 +1,692 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2020. +// Released under the MIT Software license; see doc/LICENSE + +#include +#include + +#include "base/assert.h" +#include "web/_MochaTestRunner.h" +#include "web/Document.h" +#include "web/Element.h" +#include "web/Tutorial.h" +#include "web/web.h" + + + // event listener trigger - events added on enter state, added on manual activation, removed on manual deactivation, removed on removal + +//Test events added on state enter +struct Test_EventListenerTrigger_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.AddEventListenerTrigger("state1", "state2", div, "hover", "hovertrigger"); + tut.StartAtState("state1"); + } + + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events added on state enter", function() + { + + describe("#testdiv", function() { + + it('should have an event listener on click', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events" )['click'], null); + }); + + it('should have an event listener on hover', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events" )['hover'], null); + }); + }); + }); + }); + } +}; + + +// Test listeners removed on exit state +struct Test_EventListenerTrigger_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.AddEventListenerTrigger("state1", "state2", div, "hover", "hovertrigger"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on state exit", function() + { + + describe("#testdiv", function() { + + it('should NOT have any event listeners', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } +}; + + + +// Test listeners removed on manual deactivation +struct Test_EventListenerTrigger_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.DeactivateTrigger("clicktrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('should NOT have any event listeners', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } + +}; + + +// Test listeners re-added on manual activation +struct Test_EventListenerTrigger_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.DeactivateTrigger("clicktrigger"); + tut.ActivateTrigger("clicktrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events added on manual activation", function() + { + + describe("#testdiv", function() { + + it('should have an event listener on click after manual activation', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events") ["click"], null); + }); + }); + }); + }); + } + +}; + + +// Test event listeners removed after trigger removal +struct Test_EventListenerTrigger_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.RemoveTrigger("clicktrigger", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on trigger removal", function() + { + + describe("#testdiv", function() { + + it('should NOT have an event listener after trigger removal', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } + +}; + + +// overlayeffect - added to parent div on enter state, removed on exit state, added on manual activation, removed on manual deactivation, removed on removal + +// Test overlay added to parent div on enter state +struct Test_OverlayEffect_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc); + tut.StartAtState("state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay added on state enter", function() + { + + describe("#testdiv", function() { + + it('doc should have one overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 1); + chai.assert(overlays[0].parentNode = document); + }); + }); + }); + }); + } + +}; + + + +// Test overlay removed on exit state +struct Test_OverlayEffect_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on state exit", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + +// Test overlay removed on manual deactivation +struct Test_OverlayEffect_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("overlay"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + +// Test overlay added on manual activation after manual deactivation +struct Test_OverlayEffect_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("overlay"); + tut.ActivateVisualEffect("overlay"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay added on manual activation after manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should have one overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 1); + chai.assert(overlays[0].parentNode = document); + }); + }); + }); + }); + } + +}; + + + +// Test overlay removed on removal +struct Test_OverlayEffect_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.RemoveVisualEffect("overlay", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + + + +// css effect - attr changed on enter state, manual activation. reverted on exit state, manual deactivation, removal. + +// Test css attribute changed on enter state +struct Test_CSSEffect_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute changed on enter state", function() + { + + describe("#testdiv", function() { + + it('div background color should be seagreen', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(testdiv.style.backgroundColor, "seagreen"); + }); + }); + }); + }); + } + +}; + + +// Test css attribute reverted on exit state +struct Test_CSSEffect_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on exit state", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + + + +// Test css attribute changed on manual activation after manual deactivation +struct Test_CSSEffect_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("css_effect"); + tut.ActivateVisualEffect("css_effect"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute changed on enter state", function() + { + + describe("#testdiv", function() { + + it('div background color should be seagreen', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(testdiv.style.backgroundColor, "seagreen"); + }); + }); + }); + }); + } + +}; + + + +// Test css attribute reverted on manual deactivation +struct Test_CSSEffect_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("css_effect"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + +// Test css attribute reverted on effect removal +struct Test_CSSEffect_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.RemoveVisualEffect("css_effect", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on removal", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + + + +emp::web::MochaTestRunner test_runner; +int main() { + + test_runner.Initialize({"emp_test_container"}); + + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + + + test_runner.Run(); +} From 27dc1a89e341cd12e498cdadb4ca94f716fadc0b Mon Sep 17 00:00:00 2001 From: Dylan Rainbow Date: Tue, 27 Oct 2020 08:12:08 -0700 Subject: [PATCH 9/9] Misc additions, fixes for mocha web tests (should all be passing) --- source/web/Tutorial.h | 238 ++++++++++++++++++++++++++++++------------ 1 file changed, 174 insertions(+), 64 deletions(-) diff --git a/source/web/Tutorial.h b/source/web/Tutorial.h index 6c45ec6b52..3eb2a6328f 100644 --- a/source/web/Tutorial.h +++ b/source/web/Tutorial.h @@ -25,10 +25,6 @@ class Tutorial; -// TODO: - -// util methods: StateHasTrigger(trigger_id), etc -// improve overlayeffect (take doc?) class Trigger { @@ -76,7 +72,7 @@ class Trigger { } // How many states contain this trigger? - size_t GetStateCount() { + int GetStateCount() { return next_state_map.size(); } @@ -95,12 +91,13 @@ class Trigger { // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. // Makes it simpler to override those functions in custom classes. void PerformActivation() { - emp_assert(!active); + if (active) return; + std::cout << "in Trigger Perform activation" << std::endl; Activate(); active = true; } void PerformDeactivation() { - emp_assert(active); + if (!active) return; Deactivate(); active = false; } @@ -108,13 +105,14 @@ class Trigger { // Add a pair of states that this trigger is associated with (it can move the tutorial from state to next_state). void AddStatePair(std::string state, std::string next_state) { emp_assert(!HasState(state)); + emp_assert(state != next_state); next_state_map[state] = next_state; } void RemoveState(std::string state_name) { emp_assert(HasState(state_name)); next_state_map.erase(state_name); - // deactivate if active? + } }; @@ -146,7 +144,6 @@ class EventListenerTrigger : public Trigger { } - void Deactivate() override { std::cout << "In Deactivate! Event name: " << event_name.c_str() << std::endl; widget.RemoveListener(event_name, event_name + "_tutorial_handler"); @@ -173,7 +170,6 @@ friend class State; }; - class VisualEffect { friend class Tutorial; @@ -190,16 +186,15 @@ class VisualEffect { virtual void Activate() = 0; virtual void Deactivate() = 0; - // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. // Makes it simpler to override those functions in custom classes. void PerformActivation() { - emp_assert(!active); + if (active) return; Activate(); active = true; } void PerformDeactivation() { - emp_assert(active); + if (!active) return; Deactivate(); active = false; } @@ -213,6 +208,12 @@ class VisualEffect { states_set.erase(state_name); } + // How many states contain this visual? + int GetStateCount() { + return states_set.size(); + } + + bool IsActive() {return active;} }; @@ -286,7 +287,7 @@ std::string popover_id; emp_assert(parent_widget != nullptr); std::cout << "Adding popover" << std::endl; - widget.WrapWith(parent_widget, true); + widget.WrapWithInPlace(parent_widget); std::cout << "1" << std::endl; parent_widget.SetCSS("position", "relative"); std::cout << "2" << std::endl; @@ -333,14 +334,17 @@ class OverlayEffect : public VisualEffect { std::string color; float opacity; int z_index; + bool intercept_mouse; - OverlayEffect(UI::Div& _parent, std::string _color, float _opacity, int _z_index) : parent(_parent), color(_color), opacity(_opacity), z_index(_z_index) {} + OverlayEffect(UI::Div& _parent, std::string _color, float _opacity, int _z_index, bool _intercept_mouse) : + parent(_parent), color(_color), opacity(_opacity), z_index(_z_index), intercept_mouse(_intercept_mouse) {std::cout << "Overlay Constructor" << std::endl;} void Activate() { UI::Div over("overlay"); overlay = over; + overlay.SetAttr("class", "Tutorial-Overlay-Effect"); overlay.SetCSS("background-color", color); overlay.SetCSS("opacity", opacity); overlay.SetCSS("z_index", z_index); @@ -349,15 +353,17 @@ class OverlayEffect : public VisualEffect { overlay.SetCSS("height", "100%"); overlay.SetCSS("top", "0px"); overlay.SetCSS("left", "0px"); - parent << overlay; + if (!intercept_mouse) + overlay.SetCSS("pointer-events", "none"); - std::cout << "Added overlay" << std::endl; + parent << overlay; } void Deactivate() { - overlay -> parent ->RemoveChild(overlay); + overlay -> parent -> RemoveChild(overlay); + std::cout << "removed overlay" << std::endl; } }; @@ -392,6 +398,7 @@ class State { // add the trigger id to set of id's void AddTrigger(std::string trigger_id) { + emp_assert(!HasTrigger(trigger_id)); trigger_id_set.insert(trigger_id); } @@ -403,7 +410,7 @@ class State { // add the visual id to set of id's void AddVisualEffect(std::string visual_id) { - emp_assert(HasVisualEffect(visual_id)); + emp_assert(!HasVisualEffect(visual_id)); visual_id_set.insert(visual_id); } @@ -461,7 +468,7 @@ class State { } // how many VisualEffects does this state have? - size_t GetVisualCount() { + size_t GetVisualEffectCount() { return visual_id_set.size(); } @@ -472,7 +479,7 @@ class State { class Tutorial { - friend void Trigger::Notify(); // Trigger's Notify() can access our private members. Needed so it can call OnTrigger() + friend void Trigger::Notify(); // Trigger's Notify() can access our private members. Needed so it can call OnTrigger(). private: @@ -494,25 +501,21 @@ class Tutorial { return states.at(state_name); } - - // Is the given trigger id an existing trigger? - bool HasTrigger(std::string trigger_id) { - return trigger_ptr_map.find(trigger_id) != trigger_ptr_map.end(); - } - void DeleteTrigger(std::string trigger_id) { delete trigger_ptr_map[trigger_id]; trigger_ptr_map.erase(trigger_id); } + void DeleteVisualEffect(std::string visual_id) { + delete visual_ptr_map[visual_id]; + visual_ptr_map.erase(visual_id); + } + // Retrieve a pointer to the Trigger with the given ID emp::Ptr GetTrigger(std::string trigger_id) { return trigger_ptr_map[trigger_id]; } - bool HasVisualEffect(std::string visual_id) { - return visual_ptr_map.find(visual_id) != visual_ptr_map.end(); - } // Retrieve a pointer to the Trigger with the given ID emp::Ptr GetVisualEffect(std::string visual_id) { @@ -539,9 +542,10 @@ class Tutorial { Stop(); } - // execute callbacks for the state and trigger - if (GetState(current_state).callback) GetState(current_state).callback(); + // execute callbacks for the trigger and state if (trigger -> callback) trigger -> callback(); + if (GetState(current_state).callback) GetState(current_state).callback(); + } @@ -555,24 +559,49 @@ class Tutorial { // These are the only functions to be called outside of this file :P - bool IsActive() { - return active; - } + bool IsActive() { + return active; + } - std::string GetCurrentState() { - if (active) return current_state; - return ""; + std::string GetCurrentState() { + if (active) return current_state; + return ""; + } + + // Is the given trigger id an existing trigger? + bool HasTrigger(std::string trigger_id) { + return trigger_ptr_map.find(trigger_id) != trigger_ptr_map.end(); + } + + // Is the given visual id an existing trigger? + bool HasVisualEffect(std::string visual_id) { + std::cout << "In HasVisualEffect" << std::endl; + std::cout << (visual_ptr_map.find(visual_id) != visual_ptr_map.end()) << std::endl; + return visual_ptr_map.find(visual_id) != visual_ptr_map.end(); + } + + // Is the given state name an existing state? + bool HasState(std::string state_name) { + return states.find(state_name) != states.end(); // any other checks? } // Launch into the tutorial at a particular state void StartAtState(std::string state_name){ + + // Deactivate current state if (active) GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); - // todo: make sure there's a trigger, if not then stop here - current_state = state_name; - std::cout << "visual size in Start: " << GetState(current_state).GetVisualCount() << std::endl; + + // Stop here if new state is an end state + if (GetState(current_state).GetTriggerCount() == 0) + { + Stop(); + return; + } + + std::cout << "visual size in Start: " << GetState(current_state).GetVisualEffectCount() << std::endl; GetState(current_state).Activate(trigger_ptr_map, visual_ptr_map); active = true; @@ -594,9 +623,6 @@ class Tutorial { active = false; std::cout << "Tutorial Finished!" << std::endl; -#ifdef __EMSCRIPTEN__ - EM_ASM( {alert("Tutorial Complete!");} ); -#endif } // Create and store a new state with given name @@ -608,10 +634,7 @@ class Tutorial { return *this; } - // Is the given state name an existing state? - bool HasState(std::string state_name) { - return states.find(state_name) != states.end(); // any other checks? - } + Tutorial& AddManualTrigger(std::string cur_state, std::string next_state, std::string trigger_id="", std::function callback=nullptr) { @@ -683,7 +706,7 @@ class Tutorial { template Tutorial& AddCustomTrigger(std::string cur_state, std::string next_state, Args&&... args, - std::string trigger_id, std::function callback=nullptr) { + std::string trigger_id="", std::function callback=nullptr) { std::cout << "The trigger id is: " << trigger_id << std::endl; @@ -709,7 +732,7 @@ class Tutorial { } - void RemoveTrigger(std::string trigger_id, std::string state_name) { + Tutorial& RemoveTrigger(std::string trigger_id, std::string state_name) { emp_assert(HasTrigger(trigger_id)); emp::Ptr trigger_ptr = GetTrigger(trigger_id); @@ -728,27 +751,37 @@ class Tutorial { if (trigger_ptr -> GetStateCount() == 0) DeleteTrigger(trigger_id); + return *this; } - void FireTrigger(std::string trigger_id) { + Tutorial& FireTrigger(std::string trigger_id) { emp_assert(HasTrigger(trigger_id)); GetTrigger(trigger_id) -> ManualFire(current_state); + + return *this; } - void ActivateTrigger(std::string trigger_id) { + Tutorial& ActivateTrigger(std::string trigger_id) { emp_assert(HasTrigger(trigger_id)); + std::cout << "Trying to activate trigger" << std::endl; emp::Ptr trigger_ptr = GetTrigger(trigger_id); trigger_ptr -> PerformActivation(); + + return *this; } - void DeactivateTrigger(std::string trigger_id) { + Tutorial& DeactivateTrigger(std::string trigger_id) { emp_assert(HasTrigger(trigger_id)); + std::cout << "Try to deactivate trigger" << std::endl; + emp::Ptr trigger_ptr = GetTrigger(trigger_id); trigger_ptr -> PerformDeactivation(); + + return *this; } @@ -805,17 +838,22 @@ class Tutorial { #ifdef __EMSCRIPTEN__ - Tutorial& AddOverlayEffect(std::string state_name, UI::Div& parent, std::string color="black", float opacity=0.4, int z_index=100, std::string visual_id="") { + Tutorial& AddOverlayEffect(std::string state_name, UI::Div& parent, std::string color="black", float opacity=0.4, + int z_index=1000, bool intercept_mouse=false, std::string visual_id="") { + std::cout << "Add Overlay Effect" << std::endl; emp_assert(HasState(state_name)); - emp_assert(!HasVisualEffect(visual_id)); + - emp::Ptr visual_ptr = new OverlayEffect(parent, color, opacity, z_index); + emp::Ptr visual_ptr = new OverlayEffect(parent, color, opacity, z_index, intercept_mouse); visual_ptr -> AddState(state_name); if (visual_id.empty()) visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + std::cout << visual_id << std::endl; + emp_assert(!HasVisualEffect(visual_id)); + visual_ptr_map[visual_id] = visual_ptr; GetState(state_name).AddVisualEffect(visual_id); @@ -856,36 +894,108 @@ class Tutorial { } - void ActivateVisualEffect(std::string visual_id) { - emp_assert(HasTrigger(visual_id)); + Tutorial& RemoveVisualEffect(std::string visual_id, std::string state_name) { + emp_assert(HasVisualEffect(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + + // deactivate the trigger if active + if (visual_ptr -> IsActive()) + visual_ptr -> Deactivate(); + + // remove state from visual + visual_ptr -> RemoveState(state_name); + + // remove visual from state + GetState(state_name).RemoveVisualEffect(visual_id); + + // remove from tutorial if necessary + if (visual_ptr -> GetStateCount() == 0) + DeleteVisualEffect(visual_id); + + return *this; + } + + Tutorial& ActivateVisualEffect(std::string visual_id) { + emp_assert(HasVisualEffect(visual_id)); emp::Ptr visual_ptr = GetVisualEffect(visual_id); visual_ptr -> PerformActivation(); + + return *this; } - void DeactivateVisualEffect(std::string visual_id) { - emp_assert(HasTrigger(visual_id)); + Tutorial& DeactivateVisualEffect(std::string visual_id) { + std::cout << "In DeactivateVisualEffect" << std::endl; + std::cout << visual_id << std::endl; + emp_assert(HasVisualEffect(visual_id)); emp::Ptr visual_ptr = GetVisualEffect(visual_id); visual_ptr -> PerformDeactivation(); + + return *this; } - void SetStateCallback(std::string state_name, std::function fun) { + Tutorial& SetStateCallback(std::string state_name, std::function fun) { emp_assert(HasState(state_name)); GetState(state_name).callback = fun; + + return *this; } - void SetTriggerCallback(std::string trigger_id, std::function fun) { + Tutorial& SetTriggerCallback(std::string trigger_id, std::function fun) { emp_assert(HasTrigger(trigger_id)); GetTrigger(trigger_id) -> callback = fun; + + return *this; + } + + + + + bool IsTriggerActive(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + return GetTrigger(trigger_id) -> IsActive(); + } + + int GetTriggerCount(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + return GetTrigger(trigger_id) -> GetStateCount(); + } + + + bool IsVisualEffectActive(std::string visual_id) { + emp_assert(HasVisualEffect(visual_id)); + + return GetVisualEffect(visual_id) -> IsActive(); } + int GetStateVisualEffectCount(std::string state_name, std::string visual_id) { + emp_assert(HasState(state_name)); + emp_assert(HasVisualEffect(visual_id)); + + return GetState(state_name).GetVisualEffectCount(); + } + + + bool StateHasTrigger(std::string state_name, std::string trigger_id) { + emp_assert(HasState(state_name)); + emp_assert(HasTrigger(trigger_id)); - // bool IsTriggerActive() {} + return GetState(state_name).HasTrigger(trigger_id); + } - // bool + bool StateHasVisual(std::string state_name, std::string visual_id) { + emp_assert(HasState(state_name)); + emp_assert(HasVisualEffect(visual_id)); + return GetState(state_name).HasVisualEffect(visual_id); + } + + };