From 2c7b39f0fd6fdb0a3a0f9a466189396e3d78f943 Mon Sep 17 00:00:00 2001 From: Joseph Lenox Date: Wed, 4 Jul 2018 01:05:09 -0500 Subject: [PATCH] Partially implemented UI_Point. Tests do not all pass (set_value is inserting garbage when a serialized string is passed in). --- src/CMakeLists.txt | 2 + src/GUI/OptionsGroup/Field.hpp | 59 ++++++++ src/GUI/OptionsGroup/UI_Point.cpp | 69 +++++++++ src/test/GUI/test_field_point.cpp | 229 ++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 src/GUI/OptionsGroup/UI_Point.cpp create mode 100644 src/test/GUI/test_field_point.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e12546d51..08bbed83c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -181,6 +181,7 @@ set(UI_TEST_SOURCES ${GUI_TESTDIR}/test_field_textbox.cpp ${GUI_TESTDIR}/test_field_choice.cpp ${GUI_TESTDIR}/test_field_numchoice.cpp + ${GUI_TESTDIR}/test_field_point.cpp ) set(SLIC3R_TEST_SOURCES ${TESTDIR}/test_harness.cpp @@ -261,6 +262,7 @@ IF(wxWidgets_FOUND) ${GUI_LIBDIR}/misc_ui.cpp ${GUI_LIBDIR}/OptionsGroup/UI_NumChoice.cpp ${GUI_LIBDIR}/OptionsGroup/UI_Choice.cpp + ${GUI_LIBDIR}/OptionsGroup/UI_Point.cpp ) target_compile_features(slic3r_gui PUBLIC cxx_std_14) #only build GUI lib if building with wx diff --git a/src/GUI/OptionsGroup/Field.hpp b/src/GUI/OptionsGroup/Field.hpp index 0166f2d22..ae99d4385 100644 --- a/src/GUI/OptionsGroup/Field.hpp +++ b/src/GUI/OptionsGroup/Field.hpp @@ -15,6 +15,8 @@ #include "wx/textctrl.h" #include "wx/combobox.h" #include "wx/arrstr.h" +#include "wx/stattext.h" +#include "wx/sizer.h" namespace Slic3r { namespace GUI { @@ -245,6 +247,7 @@ public: /// Function to call when the contents of this change. std::function on_change {nullptr}; + protected: virtual std::string LogChannel() override { return "UI_NumChoice"s; } @@ -262,7 +265,63 @@ private: std::regex show_value_flag {"\bshow_value\b"}; }; +class UI_Point { +public: + UI_Point(wxWindow* parent, Slic3r::ConfigOptionDef _opt, wxWindowID id = wxID_ANY); + ~UI_Point() { _lbl_x->Destroy(); _lbl_y->Destroy(); _ctrl_x->Destroy(); _ctrl_y->Destroy(); } + std::string get_string(); + + Pointf get_point(); + + /// Return the underlying sizer. + wxSizer* get_sizer() { return _sizer; }; + + + void set_value(boost::any value); + + /// Function to call when the contents of this change. + std::function value)> on_change {nullptr}; + std::function on_kill_focus {nullptr}; + + wxTextCtrl* ctrl_x() { return _ctrl_x;} + wxTextCtrl* ctrl_y() { return _ctrl_y;} + + wxStaticText* lbl_x() { return _lbl_x;} + wxStaticText* lbl_y() { return _lbl_y;} + + void enable() { _ctrl_x->Enable(); _ctrl_y->Enable(); } + void disable() { _ctrl_x->Disable(); _ctrl_y->Disable(); } + void toggle(bool en = true) { en ? this->enable() : this->disable(); } + +protected: + virtual std::string LogChannel() { return "UI_Point"s; } + +private: + wxSize field_size {40, 1}; + wxStaticText* _lbl_x {nullptr}; + wxStaticText* _lbl_y {nullptr}; + + wxTextCtrl* _ctrl_x {nullptr}; + wxTextCtrl* _ctrl_y {nullptr}; + + wxBoxSizer* _sizer {nullptr}; + + void _set_value(Slic3r::Pointf value); + void _set_value(std::string value); + + /// Remove extra zeroes generated from std::to_string on doubles + std::string trim_zeroes(std::string in) { + std::string result {""}; + std::regex strip_zeroes("(0*)$"); + std::regex_replace (std::back_inserter(result), in.begin(), in.end(), strip_zeroes, ""); + if (result.back() == '.') result.append("0"); + return result; + } + wxString trim_zeroes(wxString in) { return wxString(trim_zeroes(in.ToStdString())); } + + +}; } } // Namespace Slic3r::GUI diff --git a/src/GUI/OptionsGroup/UI_Point.cpp b/src/GUI/OptionsGroup/UI_Point.cpp new file mode 100644 index 000000000..bfe846973 --- /dev/null +++ b/src/GUI/OptionsGroup/UI_Point.cpp @@ -0,0 +1,69 @@ +#include "OptionsGroup/Field.hpp" +#include "misc_ui.hpp" +#include "Log.hpp" + +#include + +namespace Slic3r { namespace GUI { + +using namespace std::string_literals; + +std::string UI_Point::get_string() { + std::string result {""}; + result.append(trim_zeroes(_ctrl_x->GetValue().ToStdString())); + result.append(";"s); + result.append(trim_zeroes(_ctrl_y->GetValue().ToStdString())); + return result; +} + +Slic3r::Pointf UI_Point::get_point() { + return Pointf(std::stod(this->_ctrl_x->GetValue().ToStdString()), std::stod(this->_ctrl_y->GetValue().ToStdString())); +} + +void UI_Point::set_value(boost::any value) { + // type detection and handing off to children + if (value.type() == typeid(Slic3r::Pointf)) { + this->_set_value(boost::any_cast(value)); + } else if (value.type() == typeid(std::string)) { + this->_set_value(boost::any_cast(value)); + } else if (value.type() == typeid(wxString)) { + this->_set_value(boost::any_cast(value).ToStdString()); + } else { + Slic3r::Log::warn(this->LogChannel(), LOG_WSTRING("Type " << value.type().name() << " is not handled in set_value.")); + } +} + +void UI_Point::_set_value(Slic3r::Pointf value) { + /// load the controls directly from the value + this->_ctrl_x->SetValue(trim_zeroes(std::to_string(value.x))); + this->_ctrl_y->SetValue(trim_zeroes(std::to_string(value.y))); +} + +void UI_Point::_set_value(std::string value) { + /// parse the string into the two parts. + std::regex format_regex("([0-9.]+);([0-9.]+)"); + auto f_begin { std::sregex_iterator(value.begin(), value.end(), format_regex) }; + auto f_end { std::sregex_iterator() }; + + if (f_begin != f_end) { + auto iter = f_begin; + this->_ctrl_x->SetValue(trim_zeroes(iter->str())); + iter++; + if (iter != f_end) + this->_ctrl_y->SetValue(trim_zeroes(iter->str())); + } + + +} + +UI_Point::UI_Point(wxWindow* parent, Slic3r::ConfigOptionDef _opt, wxWindowID id) { + Slic3r::Pointf def_val {_opt.default_value == nullptr ? Pointf() : Pointf(*(dynamic_cast(_opt.default_value))) }; + + this->_ctrl_x = new wxTextCtrl(parent, wxID_ANY, trim_zeroes(wxString::FromDouble(def_val.x)), wxDefaultPosition, this->field_size); + this->_ctrl_y = new wxTextCtrl(parent, wxID_ANY, trim_zeroes(wxString::FromDouble(def_val.y)), wxDefaultPosition, this->field_size); + + this->_lbl_x = new wxStaticText(parent, wxID_ANY, wxString("x:")); + this->_lbl_y = new wxStaticText(parent, wxID_ANY, wxString("y:")); +} + +} } // Namespace Slic3r::GUI diff --git a/src/test/GUI/test_field_point.cpp b/src/test/GUI/test_field_point.cpp new file mode 100644 index 000000000..7ace9e8d2 --- /dev/null +++ b/src/test/GUI/test_field_point.cpp @@ -0,0 +1,229 @@ +#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" +#include "Point.hpp" + +using namespace std::string_literals; + +SCENARIO( "UI_Point: default values from options and basic accessor methods") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxUIActionSimulator sim; + wxMilliSleep(500); + + GIVEN( "A UI point method and a X,Y coordinate (3.2, 10.2) as the default_value") { + auto simple_option {ConfigOptionDef()}; + auto* default_point {new ConfigOptionPoint(Pointf(3.2, 10.2))}; + simple_option.default_value = default_point; + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + + THEN( "get_string() returns '3.2;10.2'.") { + REQUIRE(test_field.get_string() == "3.2;10.2"s); + } + THEN( "get_point() yields a Pointf structure with x = 3.2, y = 10.2") { + REQUIRE(test_field.get_point().x == 3.2); + REQUIRE(test_field.get_point().y == 10.2); + } + } + GIVEN( "A UI point method and a tooltip in simple_option") { + auto simple_option {ConfigOptionDef()}; + auto* default_point {new ConfigOptionPoint(Pointf(3.2, 10.2))}; + simple_option.default_value = default_point; + + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + THEN( "Tooltip for both labels and textctrls matches simple_option") { + REQUIRE(test_field.ctrl_x()->GetToolTipText().ToStdString() == simple_option.tooltip ); + REQUIRE(test_field.lbl_x()->GetToolTipText().ToStdString() == simple_option.tooltip ); + REQUIRE(test_field.ctrl_y()->GetToolTipText().ToStdString() == simple_option.tooltip ); + REQUIRE(test_field.lbl_y()->GetToolTipText().ToStdString() == simple_option.tooltip ); + } + THEN( "get_point() yields a Pointf structure with x = 3.2, y = 10.2") { + REQUIRE(test_field.get_point().x == 3.2); + REQUIRE(test_field.get_point().y == 10.2); + } + } +} + +SCENARIO( "UI_Point: set_value works with several types of inputs") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxUIActionSimulator sim; + wxMilliSleep(500); + GIVEN( "A UI point method with no default value.") { + auto simple_option {ConfigOptionDef()}; + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + WHEN( "set_value is called with a Pointf(19.0, 2.1)") { + test_field.set_value(Pointf(19.0, 2.1)); + THEN( "get_point() returns a Pointf(19.0, 2.1)") { + REQUIRE(test_field.get_point() == Pointf(19.0, 2.1)); + } + THEN( "get_string() returns '19.0;2.1'") { + REQUIRE(test_field.get_string() == "19.0;2.1"s); + } + THEN( "X TextCtrl contains X coordinate") { + REQUIRE(test_field.ctrl_x()->GetValue() == wxString("19.0"s)); + } + THEN( "Y TextCtrl contains Y coordinate") { + REQUIRE(test_field.ctrl_y()->GetValue() == wxString("2.1"s)); + } + } + WHEN( "set_value is called with a string of the form '30.9;211.2'") { + test_field.set_value("30.9;211.2"s); + THEN( "get_point() returns a Pointf(30.9, 211.2)") { + REQUIRE(test_field.get_point() == Pointf(30.9, 211.2)); + } + THEN( "get_string() returns '30.9;211.2'") { + REQUIRE(test_field.get_string() == "30.9;211.2"s); + } + THEN( "X TextCtrl contains X coordinate") { + REQUIRE(test_field.ctrl_x()->GetValue() == wxString("30.9"s)); + } + THEN( "Y TextCtrl contains Y coordinate") { + REQUIRE(test_field.ctrl_y()->GetValue() == wxString("211.2"s)); + } + } + WHEN( "set_value is called with a wxString of the form '30.9;211.2'") { + test_field.set_value(wxString("30.9;211.2"s)); + THEN( "get_point() returns a Pointf(30.9, 211.2)") { + REQUIRE(test_field.get_point() == Pointf(30.9, 211.2)); + } + THEN( "get_string() returns '30.9;211.2'") { + REQUIRE(test_field.get_string() == "30.9;211.2"s); + } + THEN( "X TextCtrl contains X coordinate") { + REQUIRE(test_field.ctrl_x()->GetValue() == wxString("30.9"s)); + } + THEN( "Y TextCtrl contains Y coordinate") { + REQUIRE(test_field.ctrl_y()->GetValue() == wxString("211.2"s)); + } + } + } +} + +SCENARIO( "UI_Point: Event responses") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxMilliSleep(250); + GIVEN ( "A UI_Point with no default value and a registered on_change method that increments a counter.") { + auto simple_option {ConfigOptionDef()}; + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + auto event_count {0}; + auto changefunc {[&event_count] (const std::string& opt_id, std::tuple value) { event_count++; }}; + auto killfunc {[&event_count](const std::string& opt_id) { event_count += 1; }}; + + test_field.on_change = changefunc; + test_field.on_kill_focus = killfunc; + + WHEN( "kill focus event is received on X") { + event_count = 0; + THEN( "on_kill_focus is executed.") { + REQUIRE(event_count == 1); + } + } + WHEN( "kill focus event is received on Y") { + event_count = 0; + THEN( "on_kill_focus is executed.") { + REQUIRE(event_count == 1); + } + } + WHEN( "enter key pressed event is received on X") { + event_count = 0; + THEN( "on_change is executed.") { + REQUIRE(event_count == 1); + } + } + WHEN( "enter key pressed event is received on Y") { + event_count = 0; + THEN( "on_change is executed.") { + REQUIRE(event_count == 1); + } + } + } +} + +SCENARIO( "UI_Point: Enable/Disable") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxMilliSleep(250); + GIVEN ( "A UI_Point with no default value.") { + auto simple_option {ConfigOptionDef()}; + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + WHEN( "disable() is called") { + test_field.disable(); + THEN( "IsEnabled == False for X and Y textctrls") { + REQUIRE(test_field.ctrl_x()->IsEnabled() == false); + REQUIRE(test_field.ctrl_y()->IsEnabled() == false); + } + } + WHEN( "enable() is called") { + test_field.enable(); + THEN( "IsEnabled == True for X and Y textctrls") { + REQUIRE(test_field.ctrl_x()->IsEnabled() == true); + REQUIRE(test_field.ctrl_y()->IsEnabled() == true); + } + } + WHEN( "toggle() is called with false argument") { + test_field.toggle(false); + THEN( "IsEnabled == False for X and Y textctrls") { + REQUIRE(test_field.ctrl_x()->IsEnabled() == false); + REQUIRE(test_field.ctrl_y()->IsEnabled() == false); + } + } + WHEN( "toggle() is called with true argument") { + test_field.toggle(true); + THEN( "IsEnabled == True for X and Y textctrls") { + REQUIRE(test_field.ctrl_x()->IsEnabled() == true); + REQUIRE(test_field.ctrl_y()->IsEnabled() == true); + } + } + } +} + +SCENARIO( "UI_Point: get_sizer()") { + wxTestableFrame* old = dynamic_cast(wxTheApp->GetTopWindow()); + old->Destroy(); + wxTheApp->SetTopWindow(new wxTestableFrame()); + wxMilliSleep(250); + GIVEN ( "A UI_Point with no default value.") { + auto simple_option {ConfigOptionDef()}; + auto test_field {Slic3r::GUI::UI_Point(wxTheApp->GetTopWindow(), simple_option)}; + WHEN( "get_sizer() is called") { + THEN( "get_sizer() returns a wxSizer that has 2 direct children in it that are sizers.") { + REQUIRE(test_field.get_sizer()->GetItemCount() == 2); + auto tmp {test_field.get_sizer()->GetChildren().begin()}; + REQUIRE((*tmp)->IsSizer() == true); + tmp++; + REQUIRE((*tmp)->IsSizer() == true); + } + THEN( "The two children have two wxWindows as their children") { + auto tmp_sizer {test_field.get_sizer()->GetChildren().begin()}; + auto tmp {(*tmp_sizer)->GetSizer()->GetChildren().begin()}; + REQUIRE((*tmp)->IsWindow() == true); + tmp++; + REQUIRE((*tmp)->IsWindow() == true); + + // now for the other one + tmp_sizer++; + tmp = (*tmp_sizer)->GetSizer()->GetChildren().begin(); + REQUIRE((*tmp)->IsWindow() == true); + tmp++; + REQUIRE((*tmp)->IsWindow() == true); + } + } + } +}