#include "GUI.hpp"//"slic3r_gui.hpp" #include "Field.hpp" //#include #include #include #include #include "PrintConfig.hpp" #include #include "GUI_App.hpp" namespace Slic3r { namespace GUI { wxString double_to_string(double const value, const int max_precision /*= 4*/) { if (value - int(value) == 0) return wxString::Format(_T("%i"), int(value)); int precision = max_precision; for (size_t p = 1; p < max_precision; p++) { double cur_val = pow(10, p)*value; if (cur_val - int(cur_val) == 0) { precision = p; break; } } return wxNumberFormatter::ToString(value, precision, wxNumberFormatter::Style_None); } void Field::PostInitialize() { auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); m_Undo_btn = new MyButton(m_parent, wxID_ANY, "", wxDefaultPosition,wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER); m_Undo_to_sys_btn = new MyButton(m_parent, wxID_ANY, "", wxDefaultPosition,wxDefaultSize, wxBU_EXACTFIT | wxNO_BORDER); if (wxMSW) { m_Undo_btn->SetBackgroundColour(color); m_Undo_to_sys_btn->SetBackgroundColour(color); } m_Undo_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent) { on_back_to_initial_value(); })); m_Undo_to_sys_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent) { on_back_to_sys_value(); })); //set default bitmap wxBitmap bmp; bmp.LoadFile(from_u8(var("bullet_white.png")), wxBITMAP_TYPE_PNG); set_undo_bitmap(&bmp); set_undo_to_sys_bitmap(&bmp); switch (m_opt.type) { case coPercents: case coFloats: case coStrings: case coBools: case coInts: { auto tag_pos = m_opt_id.find("#"); if (tag_pos != std::string::npos) m_opt_idx = stoi(m_opt_id.substr(tag_pos + 1, m_opt_id.size())); break; } default: break; } BUILD(); } void Field::on_kill_focus(wxEvent& event) { // Without this, there will be nasty focus bugs on Windows. // Also, docs for wxEvent::Skip() say "In general, it is recommended to skip all // non-command events to allow the default handling to take place." event.Skip(); // call the registered function if it is available if (m_on_kill_focus!=nullptr) m_on_kill_focus(); } void Field::on_change_field() { // std::cerr << "calling Field::_on_change \n"; if (m_on_change != nullptr && !m_disable_change_event) m_on_change(m_opt_id, get_value()); } void Field::on_back_to_initial_value() { if (m_back_to_initial_value != nullptr && m_is_modified_value) m_back_to_initial_value(m_opt_id); } void Field::on_back_to_sys_value() { if (m_back_to_sys_value != nullptr && m_is_nonsys_value) m_back_to_sys_value(m_opt_id); } wxString Field::get_tooltip_text(const wxString& default_string) { wxString tooltip_text(""); wxString tooltip = _(m_opt.tooltip); if (tooltip.length() > 0) tooltip_text = tooltip + "\n" + _(L("default value")) + "\t: " + (boost::iends_with(m_opt_id, "_gcode") ? "\n" : "") + default_string + (boost::iends_with(m_opt_id, "_gcode") ? "" : "\n") + _(L("parameter name")) + "\t: " + m_opt_id; return tooltip_text; } bool Field::is_matched(const std::string& string, const std::string& pattern) { std::regex regex_pattern(pattern, std::regex_constants::icase); // use ::icase to make the matching case insensitive like /i in perl return std::regex_match(string, regex_pattern); } void Field::get_value_by_opt_type(wxString& str) { switch (m_opt.type) { case coInt: m_value = wxAtoi(str); break; case coPercent: case coPercents: case coFloats: case coFloat:{ if (m_opt.type == coPercent && str.Last() == '%') str.RemoveLast(); else if (str.Last() == '%') { wxString label = m_Label->GetLabel(); if (label.Last() == '\n') label.RemoveLast(); while (label.Last() == ' ') label.RemoveLast(); if (label.Last() == ':') label.RemoveLast(); show_error(m_parent, wxString::Format(_(L("%s doesn't support percentage")), label)); set_value(double_to_string(m_opt.min), true); m_value = double(m_opt.min); break; } double val; if(!str.ToCDouble(&val)) { show_error(m_parent, _(L("Input value contains incorrect symbol(s).\nUse, please, only digits"))); set_value(double_to_string(val), true); } if (m_opt.min > val || val > m_opt.max) { show_error(m_parent, _(L("Input value is out of range"))); if (m_opt.min > val) val = m_opt.min; if (val > m_opt.max) val = m_opt.max; set_value(double_to_string(val), true); } m_value = val; break; } case coString: case coStrings: case coFloatOrPercent: m_value = str.ToStdString(); break; default: break; } } void TextCtrl::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); wxString text_value = wxString(""); switch (m_opt.type) { case coFloatOrPercent: { text_value = double_to_string(m_opt.default_value->getFloat()); if (static_cast(m_opt.default_value)->percent) text_value += "%"; break; } case coPercent: { text_value = wxString::Format(_T("%i"), int(m_opt.default_value->getFloat())); text_value += "%"; break; } case coPercents: case coFloats: case coFloat: { double val = m_opt.type == coFloats ? static_cast(m_opt.default_value)->get_at(m_opt_idx) : m_opt.type == coFloat ? m_opt.default_value->getFloat() : static_cast(m_opt.default_value)->get_at(m_opt_idx); text_value = double_to_string(val); break; } case coString: text_value = static_cast(m_opt.default_value)->value; break; case coStrings: { const ConfigOptionStrings *vec = static_cast(m_opt.default_value); if (vec == nullptr || vec->empty()) break; //for the case of empty default value text_value = vec->get_at(m_opt_idx); break; } default: break; } const long style = m_opt.multiline ? wxTE_MULTILINE : 0 | m_process_enter ? wxTE_PROCESS_ENTER : 0; auto temp = new wxTextCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, style); temp->SetToolTip(get_tooltip_text(text_value)); temp->Bind(wxEVT_LEFT_DOWN, ([temp](wxEvent& event) { //! to allow the default handling event.Skip(); //! eliminating the g-code pop up text description bool flag = false; #ifdef __WXGTK__ // I have no idea why, but on GTK flag works in other way flag = true; #endif // __WXGTK__ temp->GetToolTip()->Enable(flag); }), temp->GetId()); #if !defined(__WXGTK__) temp->Bind(wxEVT_KILL_FOCUS, ([this, temp](wxEvent& e) { e.Skip();// on_kill_focus(e); temp->GetToolTip()->Enable(true); }), temp->GetId()); #endif // __WXGTK__ if (m_process_enter) { temp->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent& evt) { on_change_field(); }), temp->GetId()); } else { temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent& evt) { #ifdef __WXGTK__ if (bChangedValueEvent) #endif //__WXGTK__ on_change_field(); }), temp->GetId()); #ifdef __WXGTK__ // to correct value updating on GTK we should: // call on_change_field() on wxEVT_KEY_UP instead of wxEVT_TEXT // and prevent value updating on wxEVT_KEY_DOWN temp->Bind(wxEVT_KEY_DOWN, &TextCtrl::change_field_value, this); temp->Bind(wxEVT_KEY_UP, &TextCtrl::change_field_value, this); #endif //__WXGTK__ } // select all text using Ctrl+A temp->Bind(wxEVT_CHAR, ([temp](wxKeyEvent& event) { if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL)) temp->SetSelection(-1, -1); //select all event.Skip(); })); // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); } boost::any& TextCtrl::get_value() { wxString ret_str = static_cast(window)->GetValue(); get_value_by_opt_type(ret_str); return m_value; } void TextCtrl::enable() { dynamic_cast(window)->Enable(); dynamic_cast(window)->SetEditable(true); } void TextCtrl::disable() { dynamic_cast(window)->Disable(); dynamic_cast(window)->SetEditable(false); } #ifdef __WXGTK__ void TextCtrl::change_field_value(wxEvent& event) { if (bChangedValueEvent = event.GetEventType()==wxEVT_KEY_UP) on_change_field(); event.Skip(); }; #endif //__WXGTK__ void CheckBox::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); bool check_value = m_opt.type == coBool ? m_opt.default_value->getBool() : m_opt.type == coBools ? static_cast(m_opt.default_value)->get_at(m_opt_idx) : false; auto temp = new wxCheckBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size); temp->SetValue(check_value); if (m_opt.readonly) temp->Disable(); temp->Bind(wxEVT_CHECKBOX, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId()); temp->SetToolTip(get_tooltip_text(check_value ? "true" : "false")); // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); } boost::any& CheckBox::get_value() { // boost::any m_value; bool value = dynamic_cast(window)->GetValue(); if (m_opt.type == coBool) m_value = static_cast(value); else m_value = static_cast(value); return m_value; } int undef_spin_val = -9999; //! Probably, It's not necessary void SpinCtrl::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); wxString text_value = wxString(""); int default_value = 0; switch (m_opt.type) { case coInt: default_value = m_opt.default_value->getInt(); text_value = wxString::Format(_T("%i"), default_value); break; case coInts: { const ConfigOptionInts *vec = static_cast(m_opt.default_value); if (vec == nullptr || vec->empty()) break; for (size_t id = 0; id < vec->size(); ++id) { default_value = vec->get_at(id); text_value += wxString::Format(_T("%i"), default_value); } break; } default: break; } const int min_val = m_opt.min == INT_MIN ? 0: m_opt.min; const int max_val = m_opt.max < 2147483647 ? m_opt.max : 2147483647; auto temp = new wxSpinCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, 0, min_val, max_val, default_value); // temp->Bind(wxEVT_SPINCTRL, ([this](wxCommandEvent e) { tmp_value = undef_spin_val; on_change_field(); }), temp->GetId()); // temp->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { tmp_value = undef_spin_val; on_kill_focus(e); }), temp->GetId()); temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { // # On OSX / Cocoa, wxSpinCtrl::GetValue() doesn't return the new value // # when it was changed from the text control, so the on_change callback // # gets the old one, and on_kill_focus resets the control to the old value. // # As a workaround, we get the new value from $event->GetString and store // # here temporarily so that we can return it from $self->get_value std::string value = e.GetString().utf8_str().data(); if (is_matched(value, "^\\d+$")) tmp_value = std::stoi(value); on_change_field(); // # We don't reset tmp_value here because _on_change might put callbacks // # in the CallAfter queue, and we want the tmp value to be available from // # them as well. }), temp->GetId()); temp->SetToolTip(get_tooltip_text(text_value)); // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); } void Choice::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); wxComboBox* temp; if (!m_opt.gui_type.empty() && m_opt.gui_type.compare("select_open") != 0) temp = new wxComboBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size); else temp = new wxComboBox(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, NULL, wxCB_READONLY); // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); if (m_opt.enum_labels.empty() && m_opt.enum_values.empty()) { } else{ for (auto el : m_opt.enum_labels.empty() ? m_opt.enum_values : m_opt.enum_labels) { const wxString& str = _(el);//m_opt_id == "support" ? _(el) : el; temp->Append(str); } set_selection(); } temp->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId()); temp->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId()); temp->SetToolTip(get_tooltip_text(temp->GetValue())); } void Choice::set_selection() { wxString text_value = wxString(""); switch (m_opt.type) { case coFloat: case coPercent: { double val = m_opt.default_value->getFloat(); text_value = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 1); size_t idx = 0; for (auto el : m_opt.enum_values) { if (el.compare(text_value) == 0) break; ++idx; } // if (m_opt.type == coPercent) text_value += "%"; idx == m_opt.enum_values.size() ? dynamic_cast(window)->SetValue(text_value) : dynamic_cast(window)->SetSelection(idx); break; } case coEnum:{ int id_value = static_cast*>(m_opt.default_value)->value; //!! dynamic_cast(window)->SetSelection(id_value); break; } case coInt:{ int val = m_opt.default_value->getInt(); //!! text_value = wxString::Format(_T("%i"), int(val)); size_t idx = 0; for (auto el : m_opt.enum_values) { if (el.compare(text_value) == 0) break; ++idx; } idx == m_opt.enum_values.size() ? dynamic_cast(window)->SetValue(text_value) : dynamic_cast(window)->SetSelection(idx); break; } case coStrings:{ text_value = static_cast(m_opt.default_value)->get_at(m_opt_idx); size_t idx = 0; for (auto el : m_opt.enum_values) { if (el.compare(text_value) == 0) break; ++idx; } idx == m_opt.enum_values.size() ? dynamic_cast(window)->SetValue(text_value) : dynamic_cast(window)->SetSelection(idx); break; } } } void Choice::set_value(const std::string& value, bool change_event) //! Redundant? { m_disable_change_event = !change_event; size_t idx=0; for (auto el : m_opt.enum_values) { if (el.compare(value) == 0) break; ++idx; } idx == m_opt.enum_values.size() ? dynamic_cast(window)->SetValue(value) : dynamic_cast(window)->SetSelection(idx); m_disable_change_event = false; } void Choice::set_value(const boost::any& value, bool change_event) { m_disable_change_event = !change_event; switch (m_opt.type) { case coInt: case coFloat: case coPercent: case coString: case coStrings: { wxString text_value; if (m_opt.type == coInt) text_value = wxString::Format(_T("%i"), int(boost::any_cast(value))); else text_value = boost::any_cast(value); auto idx = 0; for (auto el : m_opt.enum_values) { if (el.compare(text_value) == 0) break; ++idx; } idx == m_opt.enum_values.size() ? dynamic_cast(window)->SetValue(text_value) : dynamic_cast(window)->SetSelection(idx); break; } case coEnum: { int val = boost::any_cast(value); if (m_opt_id.compare("external_fill_pattern") == 0) { if (!m_opt.enum_values.empty()) { std::string key; t_config_enum_values map_names = ConfigOptionEnum::get_enum_values(); for (auto it : map_names) { if (val == it.second) { key = it.first; break; } } size_t idx = 0; for (auto el : m_opt.enum_values) { if (el.compare(key) == 0) break; ++idx; } val = idx == m_opt.enum_values.size() ? 0 : idx; } else val = 0; } dynamic_cast(window)->SetSelection(val); break; } default: break; } m_disable_change_event = false; } //! it's needed for _update_serial_ports() void Choice::set_values(const std::vector& values) { if (values.empty()) return; m_disable_change_event = true; // # it looks that Clear() also clears the text field in recent wxWidgets versions, // # but we want to preserve it auto ww = dynamic_cast(window); auto value = ww->GetValue(); ww->Clear(); ww->Append(""); for (auto el : values) ww->Append(wxString(el)); ww->SetValue(value); m_disable_change_event = false; } boost::any& Choice::get_value() { // boost::any m_value; wxString ret_str = static_cast(window)->GetValue(); // options from right panel std::vector right_panel_options{ "support", "scale_unit" }; for (auto rp_option: right_panel_options) if (m_opt_id == rp_option) return m_value = boost::any(ret_str); if (m_opt.type != coEnum) /*m_value = */get_value_by_opt_type(ret_str); else { int ret_enum = static_cast(window)->GetSelection(); if (m_opt_id.compare("external_fill_pattern") == 0) { if (!m_opt.enum_values.empty()) { std::string key = m_opt.enum_values[ret_enum]; t_config_enum_values map_names = ConfigOptionEnum::get_enum_values(); int value = map_names.at(key); m_value = static_cast(value); } else m_value = static_cast(0); } if (m_opt_id.compare("fill_pattern") == 0) m_value = static_cast(ret_enum); else if (m_opt_id.compare("gcode_flavor") == 0) m_value = static_cast(ret_enum); else if (m_opt_id.compare("support_material_pattern") == 0) m_value = static_cast(ret_enum); else if (m_opt_id.compare("seam_position") == 0) m_value = static_cast(ret_enum); else if (m_opt_id.compare("host_type") == 0) m_value = static_cast(ret_enum); } return m_value; } void ColourPicker::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); // Validate the color wxString clr_str(static_cast(m_opt.default_value)->get_at(m_opt_idx)); wxColour clr(clr_str); if (! clr.IsOk()) { clr = wxTransparentColour; } auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size); // // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([this](wxCommandEvent e) { on_change_field(); }), temp->GetId()); temp->SetToolTip(get_tooltip_text(clr_str)); } boost::any& ColourPicker::get_value() { // boost::any m_value; auto colour = static_cast(window)->GetColour(); auto clr_str = wxString::Format(wxT("#%02X%02X%02X"), colour.Red(), colour.Green(), colour.Blue()); m_value = clr_str.ToStdString(); return m_value; } void PointCtrl::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); auto temp = new wxBoxSizer(wxHORIZONTAL); // $self->wxSizer($sizer); // wxSize field_size(40, -1); auto default_pt = static_cast(m_opt.default_value)->values.at(0); double val = default_pt(0); wxString X = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None); val = default_pt(1); wxString Y = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None); x_textctrl = new wxTextCtrl(m_parent, wxID_ANY, X, wxDefaultPosition, field_size); y_textctrl = new wxTextCtrl(m_parent, wxID_ANY, Y, wxDefaultPosition, field_size); temp->Add(new wxStaticText(m_parent, wxID_ANY, "x : "), 0, wxALIGN_CENTER_VERTICAL, 0); temp->Add(x_textctrl); temp->Add(new wxStaticText(m_parent, wxID_ANY, " y : "), 0, wxALIGN_CENTER_VERTICAL, 0); temp->Add(y_textctrl); x_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), x_textctrl->GetId()); y_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { on_change_field(); }), y_textctrl->GetId()); // // recast as a wxWindow to fit the calling convention sizer = dynamic_cast(temp); x_textctrl->SetToolTip(get_tooltip_text(X+", "+Y)); y_textctrl->SetToolTip(get_tooltip_text(X+", "+Y)); } void PointCtrl::set_value(const Vec2d& value, bool change_event) { m_disable_change_event = !change_event; double val = value(0); x_textctrl->SetValue(val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None)); val = value(1); y_textctrl->SetValue(val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None)); m_disable_change_event = false; } void PointCtrl::set_value(const boost::any& value, bool change_event) { Vec2d pt(Vec2d::Zero()); const Vec2d *ptf = boost::any_cast(&value); if (!ptf) { ConfigOptionPoints* pts = boost::any_cast(value); pt = pts->values.at(0); } else pt = *ptf; set_value(pt, change_event); } boost::any& PointCtrl::get_value() { double x, y; x_textctrl->GetValue().ToDouble(&x); y_textctrl->GetValue().ToDouble(&y); return m_value = Vec2d(x, y); } void StaticText::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); wxString legend(static_cast(m_opt.default_value)->value); auto temp = new wxStaticText(m_parent, wxID_ANY, legend, wxDefaultPosition, size); temp->SetFont(wxGetApp().bold_font()); // // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); temp->SetToolTip(get_tooltip_text(legend)); } void SliderCtrl::BUILD() { auto size = wxSize(wxDefaultSize); if (m_opt.height >= 0) size.SetHeight(m_opt.height); if (m_opt.width >= 0) size.SetWidth(m_opt.width); auto temp = new wxBoxSizer(wxHORIZONTAL); auto def_val = static_cast(m_opt.default_value)->value; auto min = m_opt.min == INT_MIN ? 0 : m_opt.min; auto max = m_opt.max == INT_MAX ? 100 : m_opt.max; m_slider = new wxSlider(m_parent, wxID_ANY, def_val * m_scale, min * m_scale, max * m_scale, wxDefaultPosition, size); wxSize field_size(40, -1); m_textctrl = new wxTextCtrl(m_parent, wxID_ANY, wxString::Format("%d", m_slider->GetValue()/m_scale), wxDefaultPosition, field_size); temp->Add(m_slider, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL, 0); temp->Add(m_textctrl, 0, wxALIGN_CENTER_VERTICAL, 0); m_slider->Bind(wxEVT_SLIDER, ([this](wxCommandEvent e) { if (!m_disable_change_event) { int val = boost::any_cast(get_value()); m_textctrl->SetLabel(wxString::Format("%d", val)); on_change_field(); } }), m_slider->GetId()); m_textctrl->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { std::string value = e.GetString().utf8_str().data(); if (is_matched(value, "^-?\\d+(\\.\\d*)?$")) { m_disable_change_event = true; m_slider->SetValue(stoi(value)*m_scale); m_disable_change_event = false; on_change_field(); } }), m_textctrl->GetId()); m_sizer = dynamic_cast(temp); } void SliderCtrl::set_value(const boost::any& value, bool change_event) { m_disable_change_event = !change_event; m_slider->SetValue(boost::any_cast(value)*m_scale); int val = boost::any_cast(get_value()); m_textctrl->SetLabel(wxString::Format("%d", val)); m_disable_change_event = false; } boost::any& SliderCtrl::get_value() { // int ret_val; // x_textctrl->GetValue().ToDouble(&val); return m_value = int(m_slider->GetValue()/m_scale); } } // GUI } // Slic3r