diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e179f05b..a110e062e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -178,6 +178,7 @@ set(UI_TEST_SOURCES ${GUI_TESTDIR}/test_harness_gui.cpp ${GUI_TESTDIR}/test_field_checkbox.cpp ${GUI_TESTDIR}/test_field_spinctrl.cpp + ${GUI_TESTDIR}/test_field_textbox.cpp ) set(SLIC3R_TEST_SOURCES ${TESTDIR}/test_harness.cpp diff --git a/src/GUI/OptionsGroup/Field.hpp b/src/GUI/OptionsGroup/Field.hpp index 726aa8caa..5ec216a36 100644 --- a/src/GUI/OptionsGroup/Field.hpp +++ b/src/GUI/OptionsGroup/Field.hpp @@ -9,6 +9,8 @@ #include "Log.hpp" #include "wx/spinctrl.h" +#include "wx/checkbox.h" +#include "wx/textctrl.h" namespace Slic3r { namespace GUI { @@ -37,9 +39,9 @@ public: /// Getter functions for UI_Window items. virtual bool get_bool() { Slic3r::Log::warn(this->LogChannel(), "get_bool does not exist"s); return false; } //< return false all the time if this is not implemented. virtual int get_int() { Slic3r::Log::warn(this->LogChannel(), "get_int does not exist"s); return 0; } //< return 0 all the time if this is not implemented. + virtual std::string get_string() { Slic3r::Log::warn(this->LogChannel(), "get_string does not exist"s); return 0; } //< return 0 all the time if this is not implemented. - /// Function to call when the contents of this change. - std::function on_change {nullptr}; + /// Function to call when focus leaves. std::function on_kill_focus {nullptr}; protected: @@ -71,7 +73,7 @@ public: _check->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { if (this->on_kill_focus != nullptr) {this->on_kill_focus("");} e.Skip(); }); } - ~UI_Checkbox() = default; + ~UI_Checkbox() { _check->Destroy(); } /// Returns a bare pointer to the underlying checkbox, usually for test interface wxCheckBox* check() { return _check; } @@ -84,6 +86,9 @@ public: /// implements set_value virtual void set_value(boost::any value) override { this->_check->SetValue(boost::any_cast(value)); } + /// Function to call when the contents of this change. + std::function on_change {nullptr}; + protected: wxCheckBox* _check {nullptr}; @@ -113,12 +118,15 @@ public: _spin->Bind(wxEVT_SPINCTRL, [this](wxCommandEvent& e) { this->_on_change(""); e.Skip(); }); _spin->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { if (this->on_kill_focus != nullptr) {this->on_kill_focus("");} e.Skip(); }); } - ~UI_SpinCtrl() = default; + ~UI_SpinCtrl() { _spin->Destroy();} int get_int() { return this->_spin->GetValue(); } void set_value(boost::any value) { this->_spin->SetValue(boost::any_cast(value)); } /// Access method for the underlying SpinCtrl wxSpinCtrl* spinctrl() { return _spin; } + + /// Function to call when the contents of this change. + std::function on_change {nullptr}; protected: virtual std::string LogChannel() { return "UI_SpinCtrl"s; } @@ -134,6 +142,53 @@ private: int _tmp {0}; }; +class UI_TextCtrl : public UI_Window { +public: + UI_TextCtrl(wxWindow* parent, Slic3r::ConfigOptionDef _opt, wxWindowID id = wxID_ANY) : UI_Window(parent, _opt) { + int style {0}; + if (opt.multiline) { + style |= wxHSCROLL; + style |= wxTE_MULTILINE; + } else { + style |= wxTE_PROCESS_ENTER; + } + /// Initialize and set defaults, if available. + _text = new wxTextCtrl(parent, id, + (opt.default_value != NULL ? wxString(opt.default_value->getString()) : wxString()), + wxDefaultPosition, + _default_size(), + style); + + window = _text; + + // Set up event handlers + _text->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent& e) { this->_on_change(""); e.Skip(); }); + _text->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { if (this->on_kill_focus != nullptr) {this->on_kill_focus("");} e.Skip(); }); + } + ~UI_TextCtrl() { _text->Destroy(); } + std::string get_string() { return this->_text->GetValue().ToStdString(); } + void set_value(boost::any value) { this->_text->SetValue(boost::any_cast(value)); } + + /// Access method for the underlying SpinCtrl + wxTextCtrl* textctrl() { return _text; } + + /// Function to call when the contents of this change. + std::function on_change {nullptr}; + +protected: + virtual std::string LogChannel() { return "UI_TextCtrl"s; } + + void _on_change(std::string opt_id) { + if (!this->disable_change_event && this->window->IsEnabled() && this->on_change != nullptr) { + this->on_change(opt_id, this->get_string()); + } + } + +private: + wxTextCtrl* _text {nullptr}; + int _tmp {0}; +}; + } } // Namespace Slic3r::GUI #endif // SLIC3R_FIELD_HPP diff --git a/src/test/GUI/test_field_spinctrl.cpp b/src/test/GUI/test_field_spinctrl.cpp index fc2b92b27..e7bcd3f72 100644 --- a/src/test/GUI/test_field_spinctrl.cpp +++ b/src/test/GUI/test_field_spinctrl.cpp @@ -40,7 +40,7 @@ SCENARIO( "Receiving a Spinctrl event") { wxMilliSleep(250); GIVEN ( "A UI Spinctrl") { auto exec_counter {0}; - auto changefunc {[&exec_counter] (const std::string& opt_id, bool value) { exec_counter++; }}; + auto changefunc {[&exec_counter] (const std::string& opt_id, int value) { exec_counter++; }}; auto test_field {Slic3r::GUI::UI_SpinCtrl(wxTheApp->GetTopWindow(), Slic3r::ConfigOptionDef())}; test_field.on_change = changefunc; @@ -81,7 +81,7 @@ SCENARIO( "Changing the text via entry works on pressing enter") { wxMilliSleep(500); GIVEN ( "A UI Spinctrl") { auto exec_counter {0}; - auto changefunc {[&exec_counter] (const std::string& opt_id, bool value) { exec_counter++; }}; + auto changefunc {[&exec_counter] (const std::string& opt_id, int value) { exec_counter++; }}; auto test_field {Slic3r::GUI::UI_SpinCtrl(wxTheApp->GetTopWindow(), Slic3r::ConfigOptionDef())}; test_field.on_change = changefunc; diff --git a/src/test/GUI/test_field_textbox.cpp b/src/test/GUI/test_field_textbox.cpp new file mode 100644 index 000000000..d0c8dc5dd --- /dev/null +++ b/src/test/GUI/test_field_textbox.cpp @@ -0,0 +1,216 @@ +#include + +#ifndef WX_PRECOMP + #include "wx/app.h" + #include "wx/sizer.h" + #include "wx/uiaction.h" +#endif // WX_PRECOMP +#include +#include +#include "testableframe.h" +#include "OptionsGroup/Field.hpp" +#include "ConfigBase.hpp" + +using namespace Slic3r::GUI; +using namespace std::string_literals; + +SCENARIO( "TextCtrl initializes with default value if available.") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxMilliSleep(250); + + Slic3r::ConfigOptionDef simple_option; + simple_option.type = coInt; + auto* intopt { new Slic3r::ConfigOptionInt(7) }; + simple_option.default_value = intopt; // no delete, it's taken care of by ConfigOptionDef + GIVEN ( "A UI Textctrl") { + auto test_field {Slic3r::GUI::UI_TextCtrl(wxTheApp->GetTopWindow(), simple_option)}; + + wxTheApp->GetTopWindow()->Show(); + wxTheApp->GetTopWindow()->Fit(); + + REQUIRE(test_field.get_string() == "7"s); + } +} + +SCENARIO( "Receiving a Textctrl event") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxMilliSleep(250); + GIVEN ( "A UI Textctrl") { + auto exec_counter {0}; + auto changefunc {[&exec_counter] (const std::string& opt_id, std::string value) { exec_counter++; }}; + auto test_field {Slic3r::GUI::UI_TextCtrl(wxTheApp->GetTopWindow(), Slic3r::ConfigOptionDef())}; + + test_field.on_change = changefunc; + + wxTheApp->GetTopWindow()->Show(); + wxTheApp->GetTopWindow()->Fit(); + WHEN( "A text event occurs") { + exec_counter = 0; + auto ev {wxCommandEvent(wxEVT_TEXT_ENTER, test_field.textctrl()->GetId())}; + ev.SetEventObject(test_field.textctrl()); + test_field.textctrl()->ProcessWindowEvent(ev); + wxYield(); + wxMilliSleep(250); + THEN( "on_change is executed.") { + REQUIRE(exec_counter == 1); + } + } + WHEN( "A text event occurs and change event is disabled") { + exec_counter = 0; + test_field.disable_change_event = false; + auto ev {wxCommandEvent(wxEVT_TEXT_ENTER, test_field.textctrl()->GetId())}; + ev.SetEventObject(test_field.textctrl()); + test_field.textctrl()->ProcessWindowEvent(ev); + wxYield(); + wxMilliSleep(250); + THEN( "on_change is not executed.") { + REQUIRE(exec_counter == 1); + } + } + } +} + +SCENARIO( "TextCtrl: Changing the text via entry works on pressing enter") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxUIActionSimulator sim; + wxMilliSleep(500); + GIVEN ( "A UI Textctrl") { + auto exec_counter {0}; + auto changefunc {[&exec_counter] (const std::string& opt_id, std::string value) { exec_counter++; }}; + auto test_field {Slic3r::GUI::UI_TextCtrl(wxTheApp->GetTopWindow(), Slic3r::ConfigOptionDef())}; + + test_field.on_change = changefunc; + + wxTheApp->GetTopWindow()->Show(); + wxTheApp->GetTopWindow()->Fit(); + WHEN( "A number is entered followed by enter key") { + exec_counter = 0; + test_field.textctrl()->SetFocus(); + wxYield(); + wxMilliSleep(250); + sim.Char('3'); + wxMilliSleep(250); + sim.Char(WXK_RETURN); + wxMilliSleep(250); + wxYield(); + THEN( "on_change is executed.") { + REQUIRE(exec_counter == 1); + } + THEN( "get_string returns entered value.") { + REQUIRE(test_field.get_string() == "3"s); + } + } + WHEN( "A number is entered followed by enter key and change event is disabled") { + exec_counter = 0; + test_field.disable_change_event = true; + test_field.textctrl()->SetFocus(); + wxYield(); + wxMilliSleep(250); + sim.Char('3'); + wxMilliSleep(250); + sim.Char(WXK_RETURN); + wxMilliSleep(250); + wxYield(); + THEN( "on_change is not executed.") { + REQUIRE(exec_counter == 0); + } + THEN( "get_string returns entered value.") { + REQUIRE(test_field.get_string() == "3"s); + } + } + WHEN( "A number is entered and focus is lost") { + auto killfunc {[&exec_counter](const std::string& opt_id) { exec_counter += 1; }}; + test_field.on_kill_focus = killfunc; + + auto ev {wxFocusEvent(wxEVT_KILL_FOCUS, test_field.textctrl()->GetId())}; + ev.SetEventObject(test_field.textctrl()); + + exec_counter = 0; + test_field.textctrl()->SetValue("3"); + test_field.textctrl()->SetFocus(); + wxYield(); + wxMilliSleep(250); + sim.Char('7'); + wxYield(); + wxMilliSleep(250); + test_field.textctrl()->ProcessWindowEvent(ev); + wxMilliSleep(250); + wxYield(); + THEN( "on_kill_focus is executed and on_change are both executed.") { + REQUIRE(exec_counter == 2); + } + THEN( "get_string returns updated value.") { + REQUIRE(test_field.get_string() == "7"s); + } + THEN( "get_bool returns 0.") { + REQUIRE(test_field.get_bool() == 0); + } + THEN( "get_int returns 0.") { + REQUIRE(test_field.get_int() == 0); + } + } + + + } +} + +SCENARIO( "Multiline doesn't update other than on focus change.") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxUIActionSimulator sim; + wxMilliSleep(500); + + Slic3r::ConfigOptionDef simple_option; + simple_option.type = coInt; + simple_option.multiline = true; + auto* stropt { new Slic3r::ConfigOptionString("7") }; + GIVEN ( "A UI Textctrl") { + auto exec_counter {0}; + auto changefunc {[&exec_counter] (const std::string& opt_id, std::string value) { exec_counter++; }}; + auto test_field {Slic3r::GUI::UI_TextCtrl(wxTheApp->GetTopWindow(), Slic3r::ConfigOptionDef())}; + + test_field.on_change = changefunc; + + wxTheApp->GetTopWindow()->Show(); + wxTheApp->GetTopWindow()->Fit(); + WHEN( "pressing enter") { + auto killfunc {[&exec_counter](const std::string& opt_id) { exec_counter += 1; }}; + test_field.on_kill_focus = killfunc; + + auto ev {wxFocusEvent(wxEVT_KILL_FOCUS, test_field.textctrl()->GetId())}; + ev.SetEventObject(test_field.textctrl()); + + exec_counter = 0; + test_field.textctrl()->SetValue("3"); + test_field.textctrl()->SetFocus(); + wxYield(); + wxMilliSleep(250); + sim.Char('7'); + wxYield(); + wxMilliSleep(250); + test_field.textctrl()->ProcessWindowEvent(ev); + wxMilliSleep(250); + wxYield(); + THEN( "on_kill_focus is executed and on_change are both executed.") { + REQUIRE(exec_counter == 2); + } + THEN( "get_string returns updated value.") { + REQUIRE(test_field.get_string() == "77"s); + } + THEN( "get_bool returns 0.") { + REQUIRE(test_field.get_bool() == 0); + } + THEN( "get_int returns 0.") { + REQUIRE(test_field.get_int() == 0); + } + } + } +}