diff options
author | Vojtech Kral <vojtech@kral.hk> | 2018-03-15 20:06:26 +0300 |
---|---|---|
committer | bubnikv <bubnikv@gmail.com> | 2018-03-15 20:06:26 +0300 |
commit | c88d2780ced7bb91e79e2e9a5ef4a58506d7d175 (patch) | |
tree | d9305d5b066ce7aa6beffcf65848971890fd041a /xs/src | |
parent | 8d4b6035728e0bcf241c65c48c16cbccf4ae71c5 (diff) |
Octoprint (#796)
* Octoprint: GUI for CA file, improvements
* Octoprint: Add GUI for Bonjour lookup, bugfixes
* Octoprint: Bonjour browser: Cleanup Perl interaction
* Octoprint: Bonjour: Perform several broadcast, UI fixes
* Octoprint: Add files to localization list
* Http: Disable CA File setting on SSL backends that don't support it
Diffstat (limited to 'xs/src')
-rw-r--r-- | xs/src/slic3r/GUI/BonjourDialog.cpp | 200 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/BonjourDialog.hpp | 49 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/Field.cpp | 20 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/Field.hpp | 42 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/GUI.cpp | 24 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/GUI.hpp | 5 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/OptionsGroup.hpp | 4 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/Tab.cpp | 119 | ||||
-rw-r--r-- | xs/src/slic3r/GUI/Tab.hpp | 14 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/Bonjour.cpp | 155 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/Bonjour.hpp | 20 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/Http.cpp | 46 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/Http.hpp | 1 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/OctoPrint.cpp | 42 | ||||
-rw-r--r-- | xs/src/slic3r/Utils/OctoPrint.hpp | 7 |
15 files changed, 549 insertions, 199 deletions
diff --git a/xs/src/slic3r/GUI/BonjourDialog.cpp b/xs/src/slic3r/GUI/BonjourDialog.cpp new file mode 100644 index 000000000..34fac9a91 --- /dev/null +++ b/xs/src/slic3r/GUI/BonjourDialog.cpp @@ -0,0 +1,200 @@ +#include "slic3r/Utils/Bonjour.hpp" // On Windows, boost needs to be included before wxWidgets headers + +#include "BonjourDialog.hpp" + +#include <set> +#include <mutex> + +#include <wx/sizer.h> +#include <wx/button.h> +#include <wx/listctrl.h> +#include <wx/stattext.h> +#include <wx/timer.h> + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/Utils/Bonjour.hpp" + + +namespace Slic3r { + + +struct BonjourReplyEvent : public wxEvent +{ + BonjourReply reply; + + BonjourReplyEvent(wxEventType eventType, int winid, BonjourReply &&reply) : + wxEvent(winid, eventType), + reply(std::move(reply)) + {} + + virtual wxEvent *Clone() const + { + return new BonjourReplyEvent(*this); + } +}; + +wxDEFINE_EVENT(EVT_BONJOUR_REPLY, BonjourReplyEvent); + +wxDECLARE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent); +wxDEFINE_EVENT(EVT_BONJOUR_COMPLETE, wxCommandEvent); + +class ReplySet: public std::set<BonjourReply> {}; + +struct LifetimeGuard +{ + std::mutex mutex; + BonjourDialog *dialog; + + LifetimeGuard(BonjourDialog *dialog) : dialog(dialog) {} +}; + + +BonjourDialog::BonjourDialog(wxWindow *parent) : + wxDialog(parent, wxID_ANY, _(L("Network lookup"))), + list(new wxListView(this, wxID_ANY, wxDefaultPosition, wxSize(800, 300))), + replies(new ReplySet), + label(new wxStaticText(this, wxID_ANY, "")), + timer(new wxTimer()), + timer_state(0) +{ + wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL); + + vsizer->Add(label, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10); + + list->SetSingleStyle(wxLC_SINGLE_SEL); + list->SetSingleStyle(wxLC_SORT_DESCENDING); + list->AppendColumn(_(L("Address")), wxLIST_FORMAT_LEFT, 50); + list->AppendColumn(_(L("Hostname")), wxLIST_FORMAT_LEFT, 100); + list->AppendColumn(_(L("Service name")), wxLIST_FORMAT_LEFT, 200); + list->AppendColumn(_(L("OctoPrint version")), wxLIST_FORMAT_LEFT, 50); + + vsizer->Add(list, 1, wxEXPAND | wxALL, 10); + + wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL); + button_sizer->Add(new wxButton(this, wxID_OK, "OK"), 0, wxALL, 10); + button_sizer->Add(new wxButton(this, wxID_CANCEL, "Cancel"), 0, wxALL, 10); + // ^ Note: The Ok/Cancel labels are translated by wxWidgets + + vsizer->Add(button_sizer, 0, wxALIGN_CENTER); + SetSizerAndFit(vsizer); + + Bind(EVT_BONJOUR_REPLY, &BonjourDialog::on_reply, this); + + Bind(EVT_BONJOUR_COMPLETE, [this](wxCommandEvent &) { + this->timer_state = 0; + }); + + Bind(wxEVT_TIMER, &BonjourDialog::on_timer, this); +} + +BonjourDialog::~BonjourDialog() +{ + // Needed bacuse of forward defs +} + +bool BonjourDialog::show_and_lookup() +{ + Show(); // Because we need GetId() to work before ShowModal() + + timer->Stop(); + timer->SetOwner(this); + timer_state = 1; + timer->Start(1000); + wxTimerEvent evt_dummy; + on_timer(evt_dummy); + + // The background thread needs to queue messages for this dialog + // and for that it needs a valid pointer to it (mandated by the wxWidgets API). + // Here we put the pointer under a shared_ptr and protect it by a mutex, + // so that both threads can access it safely. + auto dguard = std::make_shared<LifetimeGuard>(this); + + bonjour = std::move(Bonjour("octoprint") + .set_retries(3) + .set_timeout(4) + .on_reply([dguard](BonjourReply &&reply) { + std::lock_guard<std::mutex> lock_guard(dguard->mutex); + auto dialog = dguard->dialog; + if (dialog != nullptr) { + auto evt = new BonjourReplyEvent(EVT_BONJOUR_REPLY, dialog->GetId(), std::move(reply)); + wxQueueEvent(dialog, evt); + } + }) + .on_complete([dguard]() { + std::lock_guard<std::mutex> lock_guard(dguard->mutex); + auto dialog = dguard->dialog; + if (dialog != nullptr) { + auto evt = new wxCommandEvent(EVT_BONJOUR_COMPLETE, dialog->GetId()); + wxQueueEvent(dialog, evt); + } + }) + .lookup() + ); + + bool res = ShowModal() == wxID_OK && list->GetFirstSelected() >= 0; + { + // Tell the background thread the dialog is going away... + std::lock_guard<std::mutex> lock_guard(dguard->mutex); + dguard->dialog = nullptr; + } + return res; +} + +wxString BonjourDialog::get_selected() const +{ + auto sel = list->GetFirstSelected(); + return sel >= 0 ? list->GetItemText(sel) : wxString(); +} + + +// Private + +void BonjourDialog::on_reply(BonjourReplyEvent &e) +{ + if (replies->find(e.reply) != replies->end()) { + // We already have this reply + return; + } + + replies->insert(std::move(e.reply)); + + auto selected = get_selected(); + list->DeleteAllItems(); + + // The whole list is recreated so that we benefit from it already being sorted in the set. + // (And also because wxListView's sorting API is bananas.) + for (const auto &reply : *replies) { + auto item = list->InsertItem(0, reply.full_address); + list->SetItem(item, 1, reply.hostname); + list->SetItem(item, 2, reply.service_name); + list->SetItem(item, 3, reply.version); + } + + for (int i = 0; i < 4; i++) { + this->list->SetColumnWidth(i, wxLIST_AUTOSIZE); + if (this->list->GetColumnWidth(i) < 100) { this->list->SetColumnWidth(i, 100); } + } + + if (!selected.IsEmpty()) { + // Attempt to preserve selection + auto hit = list->FindItem(-1, selected); + if (hit >= 0) { list->SetItemState(hit, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } + } +} + +void BonjourDialog::on_timer(wxTimerEvent &) +{ + const auto search_str = _(L("Searching for devices")); + + if (timer_state > 0) { + const std::string dots(timer_state, '.'); + label->SetLabel(wxString::Format("%s %s", search_str, dots)); + timer_state = (timer_state) % 3 + 1; + } else { + label->SetLabel(wxString::Format("%s: %s", search_str, _(L("Finished.")))); + timer->Stop(); + } +} + + +} diff --git a/xs/src/slic3r/GUI/BonjourDialog.hpp b/xs/src/slic3r/GUI/BonjourDialog.hpp new file mode 100644 index 000000000..e3f53790b --- /dev/null +++ b/xs/src/slic3r/GUI/BonjourDialog.hpp @@ -0,0 +1,49 @@ +#ifndef slic3r_BonjourDialog_hpp_ +#define slic3r_BonjourDialog_hpp_ + +#include <memory> + +#include <wx/dialog.h> + +class wxListView; +class wxStaticText; +class wxTimer; +class wxTimerEvent; + + +namespace Slic3r { + +class Bonjour; +class BonjourReplyEvent; +class ReplySet; + + +class BonjourDialog: public wxDialog +{ +public: + BonjourDialog(wxWindow *parent); + BonjourDialog(BonjourDialog &&) = delete; + BonjourDialog(const BonjourDialog &) = delete; + BonjourDialog &operator=(BonjourDialog &&) = delete; + BonjourDialog &operator=(const BonjourDialog &) = delete; + ~BonjourDialog(); + + bool show_and_lookup(); + wxString get_selected() const; +private: + wxListView *list; + std::unique_ptr<ReplySet> replies; + wxStaticText *label; + std::shared_ptr<Bonjour> bonjour; + std::unique_ptr<wxTimer> timer; + unsigned timer_state; + + void on_reply(BonjourReplyEvent &); + void on_timer(wxTimerEvent &); +}; + + + +} + +#endif diff --git a/xs/src/slic3r/GUI/Field.cpp b/xs/src/slic3r/GUI/Field.cpp index aed7ba12f..c2fc5e4e4 100644 --- a/xs/src/slic3r/GUI/Field.cpp +++ b/xs/src/slic3r/GUI/Field.cpp @@ -261,7 +261,7 @@ void SpinCtrl::BUILD() { // # 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 +// # 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); @@ -365,9 +365,9 @@ void Choice::set_selection() } } -void Choice::set_value(const std::string value) //! Redundant? +void Choice::set_value(const std::string value, bool change_event) //! Redundant? { - m_disable_change_event = true; + m_disable_change_event = !change_event; size_t idx=0; for (auto el : m_opt.enum_values) @@ -384,9 +384,9 @@ void Choice::set_value(const std::string value) //! Redundant? m_disable_change_event = false; } -void Choice::set_value(boost::any value) +void Choice::set_value(boost::any value, bool change_event) { - m_disable_change_event = true; + m_disable_change_event = !change_event; switch (m_opt.type){ case coInt: @@ -429,7 +429,7 @@ void Choice::set_values(const std::vector<std::string> values) return; m_disable_change_event = true; -// # it looks that Clear() also clears the text field in recent wxWidgets versions, +// # it looks that Clear() also clears the text field in recent wxWidgets versions, // # but we want to preserve it auto ww = dynamic_cast<wxComboBox*>(window); auto value = ww->GetValue(); @@ -541,9 +541,9 @@ void PointCtrl::BUILD() y_textctrl->SetToolTip(get_tooltip_text(X+", "+Y)); } -void PointCtrl::set_value(const Pointf value) +void PointCtrl::set_value(const Pointf value, bool change_event) { - m_disable_change_event = true; + m_disable_change_event = !change_event; double val = value.x; x_textctrl->SetValue(val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None)); @@ -553,7 +553,7 @@ void PointCtrl::set_value(const Pointf value) m_disable_change_event = false; } -void PointCtrl::set_value(boost::any value) +void PointCtrl::set_value(boost::any value, bool change_event) { Pointf pt; Pointf *ptf = boost::any_cast<Pointf>(&value); @@ -579,7 +579,7 @@ void PointCtrl::set_value(boost::any value) // return; // } // } - set_value(pt); + set_value(pt, change_event); } boost::any PointCtrl::get_value() diff --git a/xs/src/slic3r/GUI/Field.hpp b/xs/src/slic3r/GUI/Field.hpp index db8ad4c9f..2ddb5d9f8 100644 --- a/xs/src/slic3r/GUI/Field.hpp +++ b/xs/src/slic3r/GUI/Field.hpp @@ -78,7 +78,7 @@ public: /// Sets a value for this control. /// subclasses should overload with a specific version /// Postcondition: Method does not fire the on_change event. - virtual void set_value(boost::any value) = 0; + virtual void set_value(boost::any value, bool change_event) = 0; /// Gets a boost::any representing this control. /// subclasses should overload with a specific version @@ -134,13 +134,13 @@ public: void BUILD(); wxWindow* window {nullptr}; - virtual void set_value(std::string value) { - m_disable_change_event = true; + virtual void set_value(std::string value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxTextCtrl*>(window)->SetValue(wxString(value)); m_disable_change_event = false; } - virtual void set_value(boost::any value) { - m_disable_change_event = true; + virtual void set_value(boost::any value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxTextCtrl*>(window)->SetValue(boost::any_cast<wxString>(value)); m_disable_change_event = false; } @@ -161,13 +161,13 @@ public: wxWindow* window{ nullptr }; void BUILD() override; - void set_value(const bool value) { - m_disable_change_event = true; + void set_value(const bool value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxCheckBox*>(window)->SetValue(value); m_disable_change_event = false; } - void set_value(boost::any value) { - m_disable_change_event = true; + void set_value(boost::any value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxCheckBox*>(window)->SetValue(boost::any_cast<bool>(value)); m_disable_change_event = false; } @@ -189,13 +189,13 @@ public: wxWindow* window{ nullptr }; void BUILD() override; - void set_value(const std::string value) { - m_disable_change_event = true; + void set_value(const std::string value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxSpinCtrl*>(window)->SetValue(value); m_disable_change_event = false; } - void set_value(boost::any value) { - m_disable_change_event = true; + void set_value(boost::any value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxSpinCtrl*>(window)->SetValue(boost::any_cast<int>(value)); m_disable_change_event = false; } @@ -218,8 +218,8 @@ public: void BUILD() override; void set_selection(); - void set_value(const std::string value); - void set_value(boost::any value); + void set_value(const std::string value, bool change_event = false); + void set_value(boost::any value, bool change_event = false); void set_values(const std::vector<std::string> values); boost::any get_value() override; @@ -237,13 +237,13 @@ public: wxWindow* window{ nullptr }; void BUILD() override; - void set_value(const std::string value) { - m_disable_change_event = true; + void set_value(const std::string value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value); m_disable_change_event = false; } - void set_value(boost::any value) { - m_disable_change_event = true; + void set_value(boost::any value, bool change_event = false) { + m_disable_change_event = !change_event; dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(boost::any_cast<wxString>(value)); m_disable_change_event = false; } @@ -267,8 +267,8 @@ public: void BUILD() override; - void set_value(const Pointf value); - void set_value(boost::any value); + void set_value(const Pointf value, bool change_event = false); + void set_value(boost::any value, bool change_event = false); boost::any get_value() override; void enable() override { diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp index 262d41a79..0410b7969 100644 --- a/xs/src/slic3r/GUI/GUI.cpp +++ b/xs/src/slic3r/GUI/GUI.cpp @@ -358,24 +358,17 @@ void open_preferences_dialog(int event_preferences) dlg->ShowModal(); } -void create_preset_tabs(bool no_controller, bool is_disabled_button_browse, bool is_user_agent, - int event_value_change, int event_presets_changed, - int event_button_browse, int event_button_test) +void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed) { add_created_tab(new TabPrint (g_wxTabPanel, no_controller)); add_created_tab(new TabFilament (g_wxTabPanel, no_controller)); - add_created_tab(new TabPrinter (g_wxTabPanel, no_controller, is_disabled_button_browse, is_user_agent)); + add_created_tab(new TabPrinter (g_wxTabPanel, no_controller)); for (size_t i = 0; i < g_wxTabPanel->GetPageCount(); ++ i) { Tab *tab = dynamic_cast<Tab*>(g_wxTabPanel->GetPage(i)); if (! tab) continue; tab->set_event_value_change(wxEventType(event_value_change)); tab->set_event_presets_changed(wxEventType(event_presets_changed)); - if (tab->name() == "printer"){ - TabPrinter* tab_printer = static_cast<TabPrinter*>(tab); - tab_printer->set_event_button_browse(wxEventType(event_button_browse)); - tab_printer->set_event_button_test(wxEventType(event_button_test)); - } } } @@ -591,19 +584,6 @@ wxString from_u8(const std::string &str) return wxString::FromUTF8(str.c_str()); } -wxWindow *get_widget_by_id(int id) -{ - if (g_wxMainFrame == nullptr) { - throw std::runtime_error("Main frame not set"); - } - - wxWindow *window = g_wxMainFrame->FindWindow(id); - if (window == nullptr) { - throw std::runtime_error((boost::format("Could not find widget by ID: %1%") % id).str()); - } - - return window; -} void add_frequently_changed_parameters(wxWindow* parent, wxBoxSizer* sizer, wxFlexGridSizer* preset_sizer) { diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp index 2baa10cb9..084b6de46 100644 --- a/xs/src/slic3r/GUI/GUI.hpp +++ b/xs/src/slic3r/GUI/GUI.hpp @@ -86,9 +86,7 @@ void add_debug_menu(wxMenuBar *menu, int event_language_change); void open_preferences_dialog(int event_preferences); // Create a new preset tab (print, filament and printer), -void create_preset_tabs(bool no_controller, bool is_disabled_button_browse, bool is_user_agent, - int event_value_change, int event_presets_changed, - int event_button_browse, int event_button_test); +void create_preset_tabs(bool no_controller, int event_value_change, int event_presets_changed); TabIface* get_preset_tab_iface(char *name); // add it at the end of the tab panel. @@ -127,7 +125,6 @@ wxString L_str(const std::string &str); // Return wxString from std::string in UTF8 wxString from_u8(const std::string &str); -wxWindow *get_widget_by_id(int id); void add_frequently_changed_parameters(wxWindow* parent, wxBoxSizer* sizer, wxFlexGridSizer* preset_sizer); diff --git a/xs/src/slic3r/GUI/OptionsGroup.hpp b/xs/src/slic3r/GUI/OptionsGroup.hpp index 42db22225..aa0563866 100644 --- a/xs/src/slic3r/GUI/OptionsGroup.hpp +++ b/xs/src/slic3r/GUI/OptionsGroup.hpp @@ -97,9 +97,9 @@ public: if (m_fields.find(id) == m_fields.end()) return nullptr; return m_fields.at(id).get(); } - bool set_value(t_config_option_key id, boost::any value) { + bool set_value(t_config_option_key id, boost::any value, bool change_event = false) { if (m_fields.find(id) == m_fields.end()) return false; - m_fields.at(id)->set_value(value); + m_fields.at(id)->set_value(value, change_event); return true; } boost::any get_value(t_config_option_key id) { diff --git a/xs/src/slic3r/GUI/Tab.cpp b/xs/src/slic3r/GUI/Tab.cpp index e2dfa6f27..d0f9f0ce3 100644 --- a/xs/src/slic3r/GUI/Tab.cpp +++ b/xs/src/slic3r/GUI/Tab.cpp @@ -3,6 +3,9 @@ #include "PresetBundle.hpp" #include "PresetHints.hpp" #include "../../libslic3r/Utils.hpp" +#include "slic3r/Utils/Http.hpp" +#include "slic3r/Utils/OctoPrint.hpp" +#include "BonjourDialog.hpp" #include <wx/app.h> #include <wx/button.h> @@ -14,6 +17,7 @@ #include <wx/treectrl.h> #include <wx/imaglist.h> #include <wx/settings.h> +#include <wx/filedlg.h> #include <boost/algorithm/string/predicate.hpp> @@ -1102,39 +1106,18 @@ void TabPrinter::build() } optgroup = page->new_optgroup(_(L("OctoPrint upload"))); - // # append two buttons to the Host line - auto octoprint_host_browse = [this] (wxWindow* parent) { + + auto octoprint_host_browse = [this, optgroup] (wxWindow* parent) { auto btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+"\u2026", wxDefaultPosition, wxDefaultSize, wxBU_LEFT); -// btn->SetFont($Slic3r::GUI::small_font); btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG)); auto sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(btn); - if (m_is_disabled_button_browse) - btn->Disable(); - - btn->Bind(wxEVT_BUTTON, [this, parent](wxCommandEvent e){ - if (m_event_button_browse > 0){ - wxCommandEvent event(m_event_button_browse); - event.SetString("Button BROWSE was clicked!"); - g_wxMainFrame->ProcessWindowEvent(event); + btn->Bind(wxEVT_BUTTON, [this, parent, optgroup](wxCommandEvent e) { + BonjourDialog dialog(parent); + if (dialog.show_and_lookup()) { + optgroup->set_value("octoprint_host", std::move(dialog.get_selected()), true); } -// // # look for devices -// auto entries; -// { -// my $res = Net::Bonjour->new('http'); -// $res->discover; -// $entries = [$res->entries]; -// } -// if (@{$entries}) { -// my $dlg = Slic3r::GUI::BonjourBrowser->new($self, $entries); -// $self->_load_key_value('octoprint_host', $dlg->GetValue . ":".$dlg->GetPort) -// if $dlg->ShowModal == wxID_OK; -// } -// else { -// auto msg_window = new wxMessageDialog(parent, "No Bonjour device found", "Device Browser", wxOK | wxICON_INFORMATION); -// msg_window->ShowModal(); -// } }); return sizer; @@ -1143,33 +1126,23 @@ void TabPrinter::build() auto octoprint_host_test = [this](wxWindow* parent) { auto btn = m_octoprint_host_test_btn = new wxButton(parent, wxID_ANY, _(L("Test")), wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT); -// btn->SetFont($Slic3r::GUI::small_font); btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("wrench.png")), wxBITMAP_TYPE_PNG)); auto sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(btn); - btn->Bind(wxEVT_BUTTON, [this, parent](wxCommandEvent e) { - if (m_event_button_test > 0){ - wxCommandEvent event(m_event_button_test); - event.SetString("Button TEST was clicked!"); - g_wxMainFrame->ProcessWindowEvent(event); + btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) { + OctoPrint octoprint(m_config); + wxString msg; + if (octoprint.test(msg)) { + show_info(this, _(L("Connection to OctoPrint works correctly.")), _(L("Success!"))); + } else { + const auto text = wxString::Format("%s: %s\n\n%s", + _(L("Could not connect to OctoPrint")), msg, _(L("Note: OctoPrint version at least 1.1.0 is required.")) + ); + show_error(this, text); } -// my $ua = LWP::UserAgent->new; -// $ua->timeout(10); -// -// my $res = $ua->get( -// "http://".$self->{config}->octoprint_host . "/api/version", -// 'X-Api-Key' = > $self->{config}->octoprint_apikey, -// ); -// if ($res->is_success) { -// show_info(parent, "Connection to OctoPrint works correctly.", "Success!"); -// } -// else { -// show_error(parent, -// "I wasn't able to connect to OctoPrint (".$res->status_line . "). " -// . "Check hostname and OctoPrint version (at least 1.1.0 is required)."); -// } - }); + }); + return sizer; }; @@ -1179,6 +1152,45 @@ void TabPrinter::build() optgroup->append_line(host_line); optgroup->append_single_option_line("octoprint_apikey"); + if (Http::ca_file_supported()) { + + Line cafile_line = optgroup->create_single_option_line("octoprint_cafile"); + + auto octoprint_cafile_browse = [this, optgroup] (wxWindow* parent) { + auto btn = new wxButton(parent, wxID_ANY, _(L(" Browse "))+"\u2026", wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + btn->SetBitmap(wxBitmap(from_u8(Slic3r::var("zoom.png")), wxBITMAP_TYPE_PNG)); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(btn); + + btn->Bind(wxEVT_BUTTON, [this, optgroup] (wxCommandEvent e){ + static const auto filemasks = _(L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*")); + wxFileDialog openFileDialog(this, _(L("Open CA certificate file")), "", "", filemasks, wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_CANCEL) { + optgroup->set_value("octoprint_cafile", std::move(openFileDialog.GetPath()), true); + } + }); + + return sizer; + }; + + cafile_line.append_widget(octoprint_cafile_browse); + optgroup->append_line(cafile_line); + + auto octoprint_cafile_hint = [this, optgroup] (wxWindow* parent) { + auto txt = new wxStaticText(parent, wxID_ANY, + _(L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate."))); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt); + return sizer; + }; + + Line cafile_hint { "", "" }; + cafile_hint.full_width = 1; + cafile_hint.widget = std::move(octoprint_cafile_hint); + optgroup->append_line(cafile_hint); + + } + optgroup = page->new_optgroup(_(L("Firmware"))); optgroup->append_single_option_line("gcode_flavor"); @@ -1337,13 +1349,8 @@ void TabPrinter::update(){ m_serial_test_btn->Disable(); } - en = !m_config->opt_string("octoprint_host").empty(); - if ( en && m_is_user_agent) - m_octoprint_host_test_btn->Enable(); - else - m_octoprint_host_test_btn->Disable(); - get_field("octoprint_apikey")->toggle(en); - + m_octoprint_host_test_btn->Enable(!m_config->opt_string("octoprint_host").empty()); + bool have_multiple_extruders = m_extruders_count > 1; get_field("toolchange_gcode")->toggle(have_multiple_extruders); get_field("single_extruder_multi_material")->toggle(have_multiple_extruders); diff --git a/xs/src/slic3r/GUI/Tab.hpp b/xs/src/slic3r/GUI/Tab.hpp index e2dc51ee4..4f65f1475 100644 --- a/xs/src/slic3r/GUI/Tab.hpp +++ b/xs/src/slic3r/GUI/Tab.hpp @@ -214,11 +214,6 @@ public: //Slic3r::GUI::Tab::Printer; class TabPrinter : public Tab { - bool m_is_disabled_button_browse; - bool m_is_user_agent; - // similar event by clicking Buttons "Browse" & "Test" - wxEventType m_event_button_browse = 0; - wxEventType m_event_button_test = 0; public: wxButton* m_serial_test_btn; wxButton* m_octoprint_host_test_btn; @@ -228,10 +223,7 @@ public: std::vector<PageShp> m_extruder_pages; TabPrinter() {} - TabPrinter(wxNotebook* parent, bool no_controller, bool is_disabled_btn_browse, bool is_user_agent) : - Tab(parent, _(L("Printer Settings")), "printer", no_controller), - m_is_disabled_button_browse(is_disabled_btn_browse), - m_is_user_agent(is_user_agent) {} + TabPrinter(wxNotebook* parent, bool no_controller) : Tab(parent, _(L("Printer Settings")), "printer", no_controller) {} ~TabPrinter(){} void build() override; @@ -240,10 +232,6 @@ public: void extruders_count_changed(size_t extruders_count); void build_extruder_pages(); void on_preset_loaded() override; - - // Set the events to the callbacks posted to the main frame window (currently implemented in Perl). - void set_event_button_browse(wxEventType evt) { m_event_button_browse = evt; } - void set_event_button_test(wxEventType evt) { m_event_button_test = evt; } }; class SavePresetWindow :public wxDialog diff --git a/xs/src/slic3r/Utils/Bonjour.cpp b/xs/src/slic3r/Utils/Bonjour.cpp index 6107e2c60..09d9b5873 100644 --- a/xs/src/slic3r/Utils/Bonjour.cpp +++ b/xs/src/slic3r/Utils/Bonjour.cpp @@ -1,9 +1,7 @@ #include "Bonjour.hpp" -#include <iostream> // XXX #include <cstdint> #include <algorithm> -#include <unordered_map> #include <array> #include <vector> #include <string> @@ -23,16 +21,18 @@ namespace asio = boost::asio; using boost::asio::ip::udp; -// TODO: Fuzzing test (done without TXT) -// FIXME: check char retype to unsigned - - namespace Slic3r { // Minimal implementation of a MDNS/DNS-SD client // This implementation is extremely simple, only the bits that are useful -// for very basic MDNS discovery are present. +// for basic MDNS discovery of OctoPi devices are present. +// However, the bits that are present are implemented with security in mind. +// Only fully correct DNS replies are allowed through. +// While decoding the decoder will bail the moment it encounters anything fishy. +// At least that's the idea. To help prove this is actually the case, +// the implementations has been tested with AFL. + struct DnsName: public std::string { @@ -48,8 +48,7 @@ struct DnsName: public std::string return boost::none; } - // Check for recursion depth to prevent parsing names that are nested too deeply - // or end up cyclic: + // Check for recursion depth to prevent parsing names that are nested too deeply or end up cyclic: if (depth >= MAX_RECURSION) { return boost::none; } @@ -443,6 +442,30 @@ private: } }; +std::ostream& operator<<(std::ostream &os, const DnsMessage &msg) +{ + os << "DnsMessage(ID: " << msg.header.id << ", " + << "Q: " << (msg.question ? msg.question->name.c_str() : "none") << ", " + << "A: " << (msg.rr_a ? msg.rr_a->ip.to_string() : "none") << ", " + << "AAAA: " << (msg.rr_aaaa ? msg.rr_aaaa->ip.to_string() : "none") << ", " + << "services: ["; + + enum { SRV_PRINT_MAX = 3 }; + unsigned i = 0; + for (const auto &sdpair : msg.sdmap) { + os << sdpair.first << ", "; + + if (++i >= SRV_PRINT_MAX) { + os << "..."; + break; + } + } + + os << "])"; + + return os; +} + struct BonjourRequest { @@ -515,6 +538,7 @@ struct Bonjour::priv const std::string protocol; const std::string service_dn; unsigned timeout; + unsigned retries; uint16_t rq_id; std::vector<char> buffer; @@ -524,6 +548,7 @@ struct Bonjour::priv priv(std::string service, std::string protocol); + std::string strip_service_dn(const std::string &service_name) const; void udp_receive(udp::endpoint from, size_t bytes); void lookup_perform(); }; @@ -533,11 +558,26 @@ Bonjour::priv::priv(std::string service, std::string protocol) : protocol(std::move(protocol)), service_dn((boost::format("_%1%._%2%.local") % this->service % this->protocol).str()), timeout(10), + retries(1), rq_id(0) { buffer.resize(DnsMessage::MAX_SIZE); } +std::string Bonjour::priv::strip_service_dn(const std::string &service_name) const +{ + if (service_name.size() <= service_dn.size()) { + return service_name; + } + + auto needle = service_name.rfind(service_dn); + if (needle == service_name.size() - service_dn.size()) { + return service_name.substr(0, needle - 1); + } else { + return service_name; + } +} + void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes) { if (bytes == 0 || !replyfn) { @@ -557,7 +597,10 @@ void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes) } const auto &srv = *sdpair.second.srv; - BonjourReply reply(ip, sdpair.first, srv.hostname); + auto service_name = strip_service_dn(sdpair.first); + + std::string path; + std::string version; if (sdpair.second.txt) { static const std::string tag_path = "path="; @@ -565,13 +608,14 @@ void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes) for (const auto &value : sdpair.second.txt->values) { if (value.size() > tag_path.size() && value.compare(0, tag_path.size(), tag_path) == 0) { - reply.path = value.substr(tag_path.size()); + path = std::move(value.substr(tag_path.size())); } else if (value.size() > tag_version.size() && value.compare(0, tag_version.size(), tag_version) == 0) { - reply.version = value.substr(tag_version.size()); + version = std::move(value.substr(tag_version.size())); } } } + BonjourReply reply(ip, srv.port, std::move(service_name), srv.hostname, std::move(path), std::move(version)); replyfn(std::move(reply)); } } @@ -595,15 +639,26 @@ void Bonjour::priv::lookup_perform() udp::endpoint mcast(BonjourRequest::MCAST_IP4, BonjourRequest::MCAST_PORT); socket.send_to(asio::buffer(brq->data), mcast); - bool timeout = false; + bool expired = false; + bool retry = false; asio::deadline_timer timer(io_service); - timer.expires_from_now(boost::posix_time::seconds(10)); - timer.async_wait([=, &timeout](const error_code &error) { - timeout = true; - if (self->completefn) { - self->completefn(); + retries--; + std::function<void(const error_code &)> timer_handler = [&](const error_code &error) { + if (retries == 0 || error) { + expired = true; + if (self->completefn) { + self->completefn(); + } + } else { + retry = true; + retries--; + timer.expires_from_now(boost::posix_time::seconds(timeout)); + timer.async_wait(timer_handler); } - }); + }; + + timer.expires_from_now(boost::posix_time::seconds(timeout)); + timer.async_wait(timer_handler); udp::endpoint recv_from; const auto recv_handler = [&](const error_code &error, size_t bytes) { @@ -612,8 +667,11 @@ void Bonjour::priv::lookup_perform() socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler); while (io_service.run_one()) { - if (timeout) { + if (expired) { socket.cancel(); + } else if (retry) { + retry = false; + socket.send_to(asio::buffer(brq->data), mcast); } else { buffer.resize(DnsMessage::MAX_SIZE); socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler); @@ -626,13 +684,39 @@ void Bonjour::priv::lookup_perform() // API - public part -BonjourReply::BonjourReply(boost::asio::ip::address ip, std::string service_name, std::string hostname) : +BonjourReply::BonjourReply(boost::asio::ip::address ip, uint16_t port, std::string service_name, std::string hostname, std::string path, std::string version) : ip(std::move(ip)), + port(port), service_name(std::move(service_name)), hostname(std::move(hostname)), - path("/"), - version("Unknown") -{} + path(path.empty() ? std::move(std::string("/")) : std::move(path)), + version(version.empty() ? std::move(std::string("Unknown")) : std::move(version)) +{ + std::string proto; + std::string port_suffix; + if (port == 443) { proto = "https://"; } + if (port != 443 && port != 80) { port_suffix = std::to_string(port).insert(0, 1, ':'); } + if (this->path[0] != '/') { this->path.insert(0, 1, '/'); } + full_address = proto + ip.to_string() + port_suffix; + if (this->path != "/") { full_address += path; } +} + +bool BonjourReply::operator==(const BonjourReply &other) const +{ + return this->full_address == other.full_address + && this->service_name == other.service_name; +} + +bool BonjourReply::operator<(const BonjourReply &other) const +{ + if (this->ip != other.ip) { + // So that the common case doesn't involve string comparison + return this->ip < other.ip; + } else { + auto cmp = this->full_address.compare(other.full_address); + return cmp != 0 ? cmp < 0 : this->service_name < other.service_name; + } +} std::ostream& operator<<(std::ostream &os, const BonjourReply &reply) { @@ -641,6 +725,7 @@ std::ostream& operator<<(std::ostream &os, const BonjourReply &reply) return os; } + Bonjour::Bonjour(std::string service, std::string protocol) : p(new priv(std::move(service), std::move(protocol))) {} @@ -660,6 +745,12 @@ Bonjour& Bonjour::set_timeout(unsigned timeout) return *this; } +Bonjour& Bonjour::set_retries(unsigned retries) +{ + if (p && retries > 0) { p->retries = retries; } + return *this; +} + Bonjour& Bonjour::on_reply(ReplyFn fn) { if (p) { p->replyfn = std::move(fn); } @@ -677,7 +768,7 @@ Bonjour::Ptr Bonjour::lookup() auto self = std::make_shared<Bonjour>(std::move(*this)); if (self->p) { - auto io_thread = std::thread([self](){ + auto io_thread = std::thread([self]() { self->p->lookup_perform(); }); self->p->io_thread = std::move(io_thread); @@ -687,18 +778,4 @@ Bonjour::Ptr Bonjour::lookup() } -void Bonjour::pokus() // XXX -{ - auto bonjour = Bonjour("octoprint") - .set_timeout(15) - .on_reply([](BonjourReply &&reply) { - std::cerr << "BonjourReply: " << reply << std::endl; - }) - .on_complete([](){ - std::cerr << "MDNS lookup complete" << std::endl; - }) - .lookup(); -} - - } diff --git a/xs/src/slic3r/Utils/Bonjour.hpp b/xs/src/slic3r/Utils/Bonjour.hpp index 285625c04..63f34638c 100644 --- a/xs/src/slic3r/Utils/Bonjour.hpp +++ b/xs/src/slic3r/Utils/Bonjour.hpp @@ -1,26 +1,31 @@ #ifndef slic3r_Bonjour_hpp_ #define slic3r_Bonjour_hpp_ +#include <cstdint> #include <memory> #include <string> #include <functional> -// #include <ostream> #include <boost/asio/ip/address.hpp> namespace Slic3r { -// TODO: reply data structure struct BonjourReply { boost::asio::ip::address ip; + uint16_t port; std::string service_name; std::string hostname; + std::string full_address; std::string path; std::string version; - BonjourReply(boost::asio::ip::address ip, std::string service_name, std::string hostname); + BonjourReply() = delete; + BonjourReply(boost::asio::ip::address ip, uint16_t port, std::string service_name, std::string hostname, std::string path, std::string version); + + bool operator==(const BonjourReply &other) const; + bool operator<(const BonjourReply &other) const; }; std::ostream& operator<<(std::ostream &, const BonjourReply &); @@ -32,7 +37,7 @@ private: struct priv; public: typedef std::shared_ptr<Bonjour> Ptr; - typedef std::function<void(BonjourReply &&reply)> ReplyFn; + typedef std::function<void(BonjourReply &&)> ReplyFn; typedef std::function<void()> CompleteFn; Bonjour(std::string service, std::string protocol = "tcp"); @@ -40,12 +45,15 @@ public: ~Bonjour(); Bonjour& set_timeout(unsigned timeout); + Bonjour& set_retries(unsigned retries); + // ^ Note: By default there is 1 retry (meaning 1 broadcast is sent). + // Timeout is per one retry, ie. total time spent listening = retries * timeout. + // If retries > 1, then care needs to be taken as more than one reply from the same service may be received. + Bonjour& on_reply(ReplyFn fn); Bonjour& on_complete(CompleteFn fn); Ptr lookup(); - - static void pokus(); // XXX: remove private: std::unique_ptr<priv> p; }; diff --git a/xs/src/slic3r/Utils/Http.cpp b/xs/src/slic3r/Utils/Http.cpp index 45a350a59..de28904e2 100644 --- a/xs/src/slic3r/Utils/Http.cpp +++ b/xs/src/slic3r/Utils/Http.cpp @@ -3,7 +3,6 @@ #include <cstdlib> #include <functional> #include <thread> -#include <iostream> #include <tuple> #include <boost/format.hpp> @@ -45,7 +44,9 @@ struct Http::priv priv(const std::string &url); ~priv(); + static bool ca_file_supported(::CURL *curl); static size_t writecb(void *data, size_t size, size_t nmemb, void *userp); + std::string curl_error(CURLcode curlcode); std::string body_size_error(); void http_perform(); }; @@ -71,6 +72,29 @@ Http::priv::~priv() ::curl_slist_free_all(headerlist); } +bool Http::priv::ca_file_supported(::CURL *curl) +{ +#ifdef _WIN32 + bool res = false; +#else + bool res = true; +#endif + + if (curl == nullptr) { return res; } + +#if LIBCURL_VERSION_MAJOR >= 7 && LIBCURL_VERSION_MINOR >= 48 + ::curl_tlssessioninfo *tls; + if (::curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &tls) == CURLE_OK) { + if (tls->backend == CURLSSLBACKEND_SCHANNEL || tls->backend == CURLSSLBACKEND_DARWINSSL) { + // With Windows and OS X native SSL support, cert files cannot be set + res = false; + } + } +#endif + + return res; +} + size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp) { auto self = static_cast<priv*>(userp); @@ -88,6 +112,14 @@ size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp) return realsize; } +std::string Http::priv::curl_error(CURLcode curlcode) +{ + return (boost::format("%1% (%2%)") + % ::curl_easy_strerror(curlcode) + % curlcode + ).str(); +} + std::string Http::priv::body_size_error() { return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str(); @@ -121,7 +153,7 @@ void Http::priv::http_perform() if (res == CURLE_WRITE_ERROR) { error = std::move(body_size_error()); } else { - error = ::curl_easy_strerror(res); + error = std::move(curl_error(res)); }; if (errorfn) { @@ -180,7 +212,7 @@ Http& Http::remove_header(std::string name) Http& Http::ca_file(const std::string &name) { - if (p) { + if (p && priv::ca_file_supported(p->curl)) { ::curl_easy_setopt(p->curl, CURLOPT_CAINFO, name.c_str()); } @@ -257,5 +289,13 @@ Http Http::post(std::string url) return http; } +bool Http::ca_file_supported() +{ + ::CURL *curl = ::curl_easy_init(); + bool res = priv::ca_file_supported(curl); + if (curl != nullptr) { ::curl_easy_cleanup(curl); } + return res; +} + } diff --git a/xs/src/slic3r/Utils/Http.hpp b/xs/src/slic3r/Utils/Http.hpp index c591e17c5..6ac5fcce1 100644 --- a/xs/src/slic3r/Utils/Http.hpp +++ b/xs/src/slic3r/Utils/Http.hpp @@ -41,6 +41,7 @@ public: Ptr perform(); void perform_sync(); + static bool ca_file_supported(); private: Http(const std::string &url); diff --git a/xs/src/slic3r/Utils/OctoPrint.cpp b/xs/src/slic3r/Utils/OctoPrint.cpp index 58530833b..5bf51f470 100644 --- a/xs/src/slic3r/Utils/OctoPrint.cpp +++ b/xs/src/slic3r/Utils/OctoPrint.cpp @@ -20,16 +20,19 @@ OctoPrint::OctoPrint(DynamicPrintConfig *config) : cafile(config->opt_string("octoprint_cafile")) {} -std::string OctoPrint::test() const +bool OctoPrint::test(wxString &msg) const { // Since the request is performed synchronously here, - // it is ok to refer to `res` from within the closure - std::string res; + // it is ok to refer to `msg` from within the closure - auto http = Http::get(std::move(make_url("api/version"))); + bool res = true; + + auto url = std::move(make_url("api/version")); + auto http = Http::get(std::move(url)); set_auth(http); http.on_error([&](std::string, std::string error, unsigned status) { - res = format_error(error, status); + res = false; + msg = format_error(error, status); }) .perform_sync(); @@ -43,21 +46,26 @@ void OctoPrint::send_gcode(int windowId, int completeEvt, int errorEvt, const st http.form_add("print", print ? "true" : "false") .form_add_file("file", filename) .on_complete([=](std::string body, unsigned status) { - wxWindow *window = GUI::get_widget_by_id(windowId); + wxWindow *window = wxWindow::FindWindowById(windowId); + if (window == nullptr) { return; } + wxCommandEvent* evt = new wxCommandEvent(completeEvt); - evt->SetString("G-code file successfully uploaded to the OctoPrint server"); + evt->SetString(_(L("G-code file successfully uploaded to the OctoPrint server"))); evt->SetInt(100); wxQueueEvent(window, evt); }) .on_error([=](std::string body, std::string error, unsigned status) { - wxWindow *window = GUI::get_widget_by_id(windowId); + wxWindow *window = wxWindow::FindWindowById(windowId); + if (window == nullptr) { return; } wxCommandEvent* evt_complete = new wxCommandEvent(completeEvt); evt_complete->SetInt(100); wxQueueEvent(window, evt_complete); wxCommandEvent* evt_error = new wxCommandEvent(errorEvt); - evt_error->SetString(wxString::Format("Error while uploading to the OctoPrint server: %s", format_error(error, status))); + evt_error->SetString(wxString::Format("%s: %s", + _(L("Error while uploading to the OctoPrint server")), + format_error(error, status))); wxQueueEvent(window, evt_error); }) .perform(); @@ -85,19 +93,15 @@ std::string OctoPrint::make_url(const std::string &path) const } } -std::string OctoPrint::format_error(std::string error, unsigned status) +wxString OctoPrint::format_error(std::string error, unsigned status) { - if (status != 0) { - std::string res{"HTTP "}; - res.append(std::to_string(status)); + const wxString wxerror = error; - if (status == 401) { - res.append(": Invalid API key"); - } - - return std::move(res); + if (status != 0) { + return wxString::Format("HTTP %u: %s", status, + (status == 401 ? _(L("Invalid API key")) : wxerror)); } else { - return std::move(error); + return std::move(wxerror); } } diff --git a/xs/src/slic3r/Utils/OctoPrint.hpp b/xs/src/slic3r/Utils/OctoPrint.hpp index eca3baa63..1f544295c 100644 --- a/xs/src/slic3r/Utils/OctoPrint.hpp +++ b/xs/src/slic3r/Utils/OctoPrint.hpp @@ -2,8 +2,8 @@ #define slic3r_OctoPrint_hpp_ #include <string> +#include <wx/string.h> -// #include "Http.hpp" // XXX: ? namespace Slic3r { @@ -16,8 +16,7 @@ class OctoPrint public: OctoPrint(DynamicPrintConfig *config); - std::string test() const; - // XXX: style + bool test(wxString &curl_msg) const; void send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print = false) const; private: std::string host; @@ -26,7 +25,7 @@ private: void set_auth(Http &http) const; std::string make_url(const std::string &path) const; - static std::string format_error(std::string error, unsigned status); + static wxString format_error(std::string error, unsigned status); }; |