1 #include "PrintHostDialogs.hpp"
2 
3 #include <algorithm>
4 
5 #include <wx/frame.h>
6 #include <wx/progdlg.h>
7 #include <wx/sizer.h>
8 #include <wx/stattext.h>
9 #include <wx/textctrl.h>
10 #include <wx/checkbox.h>
11 #include <wx/button.h>
12 #include <wx/dataview.h>
13 #include <wx/wupdlock.h>
14 #include <wx/debug.h>
15 
16 #include "GUI.hpp"
17 #include "GUI_App.hpp"
18 #include "MsgDialog.hpp"
19 #include "I18N.hpp"
20 #include "../Utils/PrintHost.hpp"
21 #include "wxExtensions.hpp"
22 #include "MainFrame.hpp"
23 #include "libslic3r/AppConfig.hpp"
24 
25 namespace fs = boost::filesystem;
26 
27 namespace Slic3r {
28 namespace GUI {
29 
30 static const char *CONFIG_KEY_PATH  = "printhost_path";
31 static const char *CONFIG_KEY_PRINT = "printhost_print";
32 static const char *CONFIG_KEY_GROUP = "printhost_group";
33 
PrintHostSendDialog(const fs::path & path,bool can_start_print,const wxArrayString & groups)34 PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print, const wxArrayString &groups)
35     : MsgDialog(static_cast<wxWindow*>(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"), wxID_NONE)
36     , txt_filename(new wxTextCtrl(this, wxID_ANY))
37     , box_print(can_start_print ? new wxCheckBox(this, wxID_ANY, _L("Start printing after upload")) : nullptr)
38     , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr)
39 {
40 #ifdef __APPLE__
41     txt_filename->OSXDisableAllSmartSubstitutions();
42 #endif
43     const AppConfig *app_config = wxGetApp().app_config;
44 
45     auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed."));
46     label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
47 
48     content_sizer->Add(txt_filename, 0, wxEXPAND);
49     content_sizer->Add(label_dir_hint);
50     content_sizer->AddSpacer(VERT_SPACING);
51     if (box_print != nullptr) {
52         content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING);
53         box_print->SetValue(app_config->get("recent", CONFIG_KEY_PRINT) == "1");
54     }
55 
56     if (combo_groups != nullptr) {
57         // Repetier specific: Show a selection of file groups.
58         auto *label_group = new wxStaticText(this, wxID_ANY, _L("Group"));
59         content_sizer->Add(label_group);
60         content_sizer->Add(combo_groups, 0, wxBOTTOM, 2*VERT_SPACING);
61         wxString recent_group = from_u8(app_config->get("recent", CONFIG_KEY_GROUP));
62         if (! recent_group.empty())
63             combo_groups->SetValue(recent_group);
64     }
65 
66     btn_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL));
67 
68     wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH));
69     if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') {
70         recent_path += '/';
71     }
72     const auto recent_path_len = recent_path.Length();
73     recent_path += path.filename().wstring();
74     wxString stem(path.stem().wstring());
75     const auto stem_len = stem.Length();
76 
77     txt_filename->SetValue(recent_path);
78     txt_filename->SetFocus();
79 
80     Fit();
81     CenterOnParent();
82 
83 #ifdef __linux__
84     // On Linux with GTK2 when text control lose the focus then selection (colored background) disappears but text color stay white
85     // and as a result the text is invisible with light mode
86     // see https://github.com/prusa3d/PrusaSlicer/issues/4532
87     // Workaround: Unselect text selection explicitly on kill focus
88     txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) {
89         e.Skip();
90         txt_filename->SetInsertionPoint(txt_filename->GetLastPosition());
91     }, txt_filename->GetId());
92 #endif /* __linux__ */
93 
94     Bind(wxEVT_SHOW, [=](const wxShowEvent &) {
95         // Another similar case where the function only works with EVT_SHOW + CallAfter,
96         // this time on Mac.
97         CallAfter([=]() {
98             txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
99         });
100     });
101 }
102 
filename() const103 fs::path PrintHostSendDialog::filename() const
104 {
105     return into_path(txt_filename->GetValue());
106 }
107 
start_print() const108 bool PrintHostSendDialog::start_print() const
109 {
110     return box_print != nullptr ? box_print->GetValue() : false;
111 }
112 
group() const113 std::string PrintHostSendDialog::group() const
114 {
115      if (combo_groups == nullptr) {
116          return "";
117      } else {
118          wxString group = combo_groups->GetValue();
119          return into_u8(group);
120     }
121 }
122 
EndModal(int ret)123 void PrintHostSendDialog::EndModal(int ret)
124 {
125     if (ret == wxID_OK) {
126         // Persist path and print settings
127         wxString path = txt_filename->GetValue();
128         int last_slash = path.Find('/', true);
129 		if (last_slash == wxNOT_FOUND)
130 			path.clear();
131 		else
132             path = path.SubString(0, last_slash);
133 
134 		AppConfig *app_config = wxGetApp().app_config;
135 		app_config->set("recent", CONFIG_KEY_PATH, into_u8(path));
136         app_config->set("recent", CONFIG_KEY_PRINT, start_print() ? "1" : "0");
137 
138         if (combo_groups != nullptr) {
139             wxString group = combo_groups->GetValue();
140             app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group));
141         }
142     }
143 
144     MsgDialog::EndModal(ret);
145 }
146 
147 
148 
149 wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
150 wxDEFINE_EVENT(EVT_PRINTHOST_ERROR,    PrintHostQueueDialog::Event);
151 wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL,   PrintHostQueueDialog::Event);
152 
Event(wxEventType eventType,int winid,size_t job_id)153 PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id)
154     : wxEvent(winid, eventType)
155     , job_id(job_id)
156 {}
157 
Event(wxEventType eventType,int winid,size_t job_id,int progress)158 PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, int progress)
159     : wxEvent(winid, eventType)
160     , job_id(job_id)
161     , progress(progress)
162 {}
163 
Event(wxEventType eventType,int winid,size_t job_id,wxString error)164 PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString error)
165     : wxEvent(winid, eventType)
166     , job_id(job_id)
167     , error(std::move(error))
168 {}
169 
Clone() const170 wxEvent *PrintHostQueueDialog::Event::Clone() const
171 {
172     return new Event(*this);
173 }
174 
PrintHostQueueDialog(wxWindow * parent)175 PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent)
176     : DPIDialog(parent, wxID_ANY, _L("Print host upload queue"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
177     , on_progress_evt(this, EVT_PRINTHOST_PROGRESS, &PrintHostQueueDialog::on_progress, this)
178     , on_error_evt(this, EVT_PRINTHOST_ERROR, &PrintHostQueueDialog::on_error, this)
179     , on_cancel_evt(this, EVT_PRINTHOST_CANCEL, &PrintHostQueueDialog::on_cancel, this)
180 {
181     const auto em = GetTextExtent("m").x;
182 
183     auto *topsizer = new wxBoxSizer(wxVERTICAL);
184 
185     job_list = new wxDataViewListCtrl(this, wxID_ANY);
186     // Note: Keep these in sync with Column
187     job_list->AppendTextColumn(_L("ID"), wxDATAVIEW_CELL_INERT);
188     job_list->AppendProgressColumn(_L("Progress"), wxDATAVIEW_CELL_INERT);
189     job_list->AppendTextColumn(_L("Status"), wxDATAVIEW_CELL_INERT);
190     job_list->AppendTextColumn(_L("Host"), wxDATAVIEW_CELL_INERT);
191     job_list->AppendTextColumn(_L("Filename"), wxDATAVIEW_CELL_INERT);
192     job_list->AppendTextColumn(_L("Error Message"), wxDATAVIEW_CELL_INERT, -1, wxALIGN_CENTER, wxDATAVIEW_COL_HIDDEN);
193 
194     auto *btnsizer = new wxBoxSizer(wxHORIZONTAL);
195     btn_cancel = new wxButton(this, wxID_DELETE, _L("Cancel selected"));
196     btn_cancel->Disable();
197     btn_error = new wxButton(this, wxID_ANY, _L("Show error message"));
198     btn_error->Disable();
199     // Note: The label needs to be present, otherwise we get accelerator bugs on Mac
200     auto *btn_close = new wxButton(this, wxID_CANCEL, _L("Close"));
201     btnsizer->Add(btn_cancel, 0, wxRIGHT, SPACING);
202     btnsizer->Add(btn_error, 0);
203     btnsizer->AddStretchSpacer();
204     btnsizer->Add(btn_close);
205 
206     topsizer->Add(job_list, 1, wxEXPAND | wxBOTTOM, SPACING);
207     topsizer->Add(btnsizer, 0, wxEXPAND);
208     SetSizer(topsizer);
209 
210     SetSize(wxSize(HEIGHT * em, WIDTH * em));
211 
212     job_list->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxDataViewEvent&) { on_list_select(); });
213 
214     btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
215         int selected = job_list->GetSelectedRow();
216         if (selected == wxNOT_FOUND) { return; }
217 
218         const JobState state = get_state(selected);
219         if (state < ST_ERROR) {
220             // TODO: cancel
221             GUI::wxGetApp().printhost_job_queue().cancel(selected);
222         }
223     });
224 
225     btn_error->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
226         int selected = job_list->GetSelectedRow();
227         if (selected == wxNOT_FOUND) { return; }
228         GUI::show_error(nullptr, job_list->GetTextValue(selected, COL_ERRORMSG));
229     });
230 }
231 
append_job(const PrintHostJob & job)232 void PrintHostQueueDialog::append_job(const PrintHostJob &job)
233 {
234     wxCHECK_RET(!job.empty(), "PrintHostQueueDialog: Attempt to append an empty job");
235 
236     wxVector<wxVariant> fields;
237     fields.push_back(wxVariant(wxString::Format("%d", job_list->GetItemCount() + 1)));
238     fields.push_back(wxVariant(0));
239     fields.push_back(wxVariant(_L("Enqueued")));
240     fields.push_back(wxVariant(job.printhost->get_host()));
241     fields.push_back(wxVariant(job.upload_data.upload_path.string()));
242     fields.push_back(wxVariant(""));
243     job_list->AppendItem(fields, static_cast<wxUIntPtr>(ST_NEW));
244     // Both strings are UTF-8 encoded.
245     upload_names.emplace_back(job.printhost->get_host(), job.upload_data.upload_path.string());
246 }
247 
on_dpi_changed(const wxRect & suggested_rect)248 void PrintHostQueueDialog::on_dpi_changed(const wxRect &suggested_rect)
249 {
250     const int& em = em_unit();
251 
252     msw_buttons_rescale(this, em, { wxID_DELETE, wxID_CANCEL, btn_error->GetId() });
253 
254     SetMinSize(wxSize(HEIGHT * em, WIDTH * em));
255 
256     Fit();
257     Refresh();
258 }
259 
get_state(int idx)260 PrintHostQueueDialog::JobState PrintHostQueueDialog::get_state(int idx)
261 {
262     wxCHECK_MSG(idx >= 0 && idx < job_list->GetItemCount(), ST_ERROR, "Out of bounds access to job list");
263     return static_cast<JobState>(job_list->GetItemData(job_list->RowToItem(idx)));
264 }
265 
set_state(int idx,JobState state)266 void PrintHostQueueDialog::set_state(int idx, JobState state)
267 {
268     wxCHECK_RET(idx >= 0 && idx < job_list->GetItemCount(), "Out of bounds access to job list");
269     job_list->SetItemData(job_list->RowToItem(idx), static_cast<wxUIntPtr>(state));
270 
271     switch (state) {
272         case ST_NEW:        job_list->SetValue(_L("Enqueued"), idx, COL_STATUS); break;
273         case ST_PROGRESS:   job_list->SetValue(_L("Uploading"), idx, COL_STATUS); break;
274         case ST_ERROR:      job_list->SetValue(_L("Error"), idx, COL_STATUS); break;
275         case ST_CANCELLING: job_list->SetValue(_L("Cancelling"), idx, COL_STATUS); break;
276         case ST_CANCELLED:  job_list->SetValue(_L("Cancelled"), idx, COL_STATUS); break;
277         case ST_COMPLETED:  job_list->SetValue(_L("Completed"), idx, COL_STATUS); break;
278     }
279 }
280 
on_list_select()281 void PrintHostQueueDialog::on_list_select()
282 {
283     int selected = job_list->GetSelectedRow();
284     if (selected != wxNOT_FOUND) {
285         const JobState state = get_state(selected);
286         btn_cancel->Enable(state < ST_ERROR);
287         btn_error->Enable(state == ST_ERROR);
288         Layout();
289     } else {
290         btn_cancel->Disable();
291     }
292 }
293 
on_progress(Event & evt)294 void PrintHostQueueDialog::on_progress(Event &evt)
295 {
296     wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
297 
298     if (evt.progress < 100) {
299         set_state(evt.job_id, ST_PROGRESS);
300         job_list->SetValue(wxVariant(evt.progress), evt.job_id, COL_PROGRESS);
301     } else {
302         set_state(evt.job_id, ST_COMPLETED);
303         job_list->SetValue(wxVariant(100), evt.job_id, COL_PROGRESS);
304     }
305 
306     on_list_select();
307 }
308 
on_error(Event & evt)309 void PrintHostQueueDialog::on_error(Event &evt)
310 {
311     wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
312 
313     set_state(evt.job_id, ST_ERROR);
314 
315     auto errormsg = from_u8((boost::format("%1%\n%2%") % _utf8(L("Error uploading to print host:")) % std::string(evt.error.ToUTF8())).str());
316     job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS);
317     job_list->SetValue(wxVariant(errormsg), evt.job_id, COL_ERRORMSG);    // Stashes the error message into a hidden column for later
318 
319     on_list_select();
320 
321     GUI::show_error(nullptr, errormsg);
322 }
323 
on_cancel(Event & evt)324 void PrintHostQueueDialog::on_cancel(Event &evt)
325 {
326     wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
327 
328     set_state(evt.job_id, ST_CANCELLED);
329     job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS);
330 
331     on_list_select();
332 }
get_active_jobs(std::vector<std::pair<std::string,std::string>> & ret)333 void PrintHostQueueDialog::get_active_jobs(std::vector<std::pair<std::string, std::string>>& ret)
334 {
335     int ic = job_list->GetItemCount();
336     for (size_t i = 0; i < ic; i++)
337     {
338         auto item = job_list->RowToItem(i);
339         auto data = job_list->GetItemData(item);
340         JobState st = static_cast<JobState>(data);
341         if(st == JobState::ST_NEW || st == JobState::ST_PROGRESS)
342             ret.emplace_back(upload_names[i]);
343     }
344     //job_list->data
345 }
346 }}
347