#include "PrintHostDialogs.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "GUI.hpp" #include "GUI_App.hpp" #include "MsgDialog.hpp" #include "I18N.hpp" #include "../Utils/PrintHost.hpp" #include "MainFrame.hpp" #include "libslic3r/AppConfig.hpp" #include "NotificationManager.hpp" namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { static const char *CONFIG_KEY_PATH = "printhost_path"; static const char *CONFIG_KEY_PRINT = "printhost_print"; static const char *CONFIG_KEY_GROUP = "printhost_group"; PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print, const wxArrayString &groups) : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"), wxID_NONE) , txt_filename(new wxTextCtrl(this, wxID_ANY)) , box_print(can_start_print ? new wxCheckBox(this, wxID_ANY, _L("Start printing after upload")) : nullptr) , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr) { #ifdef __APPLE__ txt_filename->OSXDisableAllSmartSubstitutions(); #endif const AppConfig *app_config = wxGetApp().app_config; auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed.")); label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); content_sizer->Add(txt_filename, 0, wxEXPAND); content_sizer->Add(label_dir_hint); content_sizer->AddSpacer(VERT_SPACING); if (box_print != nullptr) { content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING); box_print->SetValue(app_config->get("recent", CONFIG_KEY_PRINT) == "1"); } if (combo_groups != nullptr) { // Repetier specific: Show a selection of file groups. auto *label_group = new wxStaticText(this, wxID_ANY, _L("Group")); content_sizer->Add(label_group); content_sizer->Add(combo_groups, 0, wxBOTTOM, 2*VERT_SPACING); wxString recent_group = from_u8(app_config->get("recent", CONFIG_KEY_GROUP)); if (! recent_group.empty()) combo_groups->SetValue(recent_group); } auto* szr = CreateStdDialogButtonSizer(wxOK | wxCANCEL); auto* btn_ok = szr->GetAffirmativeButton(); btn_sizer->Add(szr); wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH)); if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') { recent_path += '/'; } const auto recent_path_len = recent_path.Length(); recent_path += path.filename().wstring(); wxString stem(path.stem().wstring()); const auto stem_len = stem.Length(); txt_filename->SetValue(recent_path); txt_filename->SetFocus(); wxString suffix = recent_path.substr(recent_path.find_last_of('.')); btn_ok->Bind(wxEVT_BUTTON, [this, suffix](wxCommandEvent&) { wxString path = txt_filename->GetValue(); // .gcode suffix control if (!path.Lower().EndsWith(suffix.Lower())) { wxMessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), suffix), wxString(SLIC3R_APP_NAME), wxYES | wxNO); if (msg_wingow.ShowModal() == wxID_NO) return; } EndDialog(wxID_OK); }); Fit(); CenterOnParent(); #ifdef __linux__ // On Linux with GTK2 when text control lose the focus then selection (colored background) disappears but text color stay white // and as a result the text is invisible with light mode // see https://github.com/prusa3d/PrusaSlicer/issues/4532 // Workaround: Unselect text selection explicitly on kill focus txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) { e.Skip(); txt_filename->SetInsertionPoint(txt_filename->GetLastPosition()); }, txt_filename->GetId()); #endif /* __linux__ */ Bind(wxEVT_SHOW, [=](const wxShowEvent &) { // Another similar case where the function only works with EVT_SHOW + CallAfter, // this time on Mac. CallAfter([=]() { txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len); }); }); } fs::path PrintHostSendDialog::filename() const { return into_path(txt_filename->GetValue()); } bool PrintHostSendDialog::start_print() const { return box_print != nullptr ? box_print->GetValue() : false; } std::string PrintHostSendDialog::group() const { if (combo_groups == nullptr) { return ""; } else { wxString group = combo_groups->GetValue(); return into_u8(group); } } void PrintHostSendDialog::EndModal(int ret) { if (ret == wxID_OK) { // Persist path and print settings wxString path = txt_filename->GetValue(); int last_slash = path.Find('/', true); if (last_slash == wxNOT_FOUND) path.clear(); else path = path.SubString(0, last_slash); AppConfig *app_config = wxGetApp().app_config; app_config->set("recent", CONFIG_KEY_PATH, into_u8(path)); app_config->set("recent", CONFIG_KEY_PRINT, start_print() ? "1" : "0"); if (combo_groups != nullptr) { wxString group = combo_groups->GetValue(); app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group)); } } MsgDialog::EndModal(ret); } wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event); PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id) : wxEvent(winid, eventType) , job_id(job_id) {} PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, int progress) : wxEvent(winid, eventType) , job_id(job_id) , progress(progress) {} PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString error) : wxEvent(winid, eventType) , job_id(job_id) , error(std::move(error)) {} wxEvent *PrintHostQueueDialog::Event::Clone() const { return new Event(*this); } PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _L("Print host upload queue"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , on_progress_evt(this, EVT_PRINTHOST_PROGRESS, &PrintHostQueueDialog::on_progress, this) , on_error_evt(this, EVT_PRINTHOST_ERROR, &PrintHostQueueDialog::on_error, this) , on_cancel_evt(this, EVT_PRINTHOST_CANCEL, &PrintHostQueueDialog::on_cancel, this) { const auto em = GetTextExtent("m").x; auto *topsizer = new wxBoxSizer(wxVERTICAL); std::vector widths; widths.reserve(6); if (!load_user_data(UDT_COLS, widths)) { widths.clear(); for (size_t i = 0; i < 6; i++) widths.push_back(-1); } job_list = new wxDataViewListCtrl(this, wxID_ANY); // Note: Keep these in sync with Column job_list->AppendTextColumn(_L("ID"), wxDATAVIEW_CELL_INERT, widths[0], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendProgressColumn(_L("Progress"), wxDATAVIEW_CELL_INERT, widths[1], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendTextColumn(_L("Status"), wxDATAVIEW_CELL_INERT, widths[2], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendTextColumn(_L("Host"), wxDATAVIEW_CELL_INERT, widths[3], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendTextColumn(_CTX_utf8(L_CONTEXT("Size", "OfFile"), "OfFile"), wxDATAVIEW_CELL_INERT, widths[4], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendTextColumn(_L("Filename"), wxDATAVIEW_CELL_INERT, widths[5], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); job_list->AppendTextColumn(_L("Error Message"), wxDATAVIEW_CELL_INERT, -1, wxALIGN_CENTER, wxDATAVIEW_COL_HIDDEN); auto *btnsizer = new wxBoxSizer(wxHORIZONTAL); btn_cancel = new wxButton(this, wxID_DELETE, _L("Cancel selected")); btn_cancel->Disable(); btn_error = new wxButton(this, wxID_ANY, _L("Show error message")); btn_error->Disable(); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac auto *btn_close = new wxButton(this, wxID_CANCEL, _L("Close")); btnsizer->Add(btn_cancel, 0, wxRIGHT, SPACING); btnsizer->Add(btn_error, 0); btnsizer->AddStretchSpacer(); btnsizer->Add(btn_close); topsizer->Add(job_list, 1, wxEXPAND | wxBOTTOM, SPACING); topsizer->Add(btnsizer, 0, wxEXPAND); SetSizer(topsizer); std::vector size; SetSize(load_user_data(UDT_SIZE, size) ? wxSize(size[0] * em, size[1] * em) : wxSize(HEIGHT * em, WIDTH * em)); Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) { OnSize(evt); save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS); }); std::vector pos; if (load_user_data(UDT_POSITION, pos)) SetPosition(wxPoint(pos[0], pos[1])); Bind(wxEVT_MOVE, [this](wxMoveEvent& evt) { save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS); }); job_list->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxDataViewEvent&) { on_list_select(); }); btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { int selected = job_list->GetSelectedRow(); if (selected == wxNOT_FOUND) { return; } const JobState state = get_state(selected); if (state < ST_ERROR) { GUI::wxGetApp().printhost_job_queue().cancel(selected); } }); btn_error->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { int selected = job_list->GetSelectedRow(); if (selected == wxNOT_FOUND) { return; } GUI::show_error(nullptr, job_list->GetTextValue(selected, COL_ERRORMSG)); }); } void PrintHostQueueDialog::append_job(const PrintHostJob &job) { wxCHECK_RET(!job.empty(), "PrintHostQueueDialog: Attempt to append an empty job"); wxVector fields; fields.push_back(wxVariant(wxString::Format("%d", job_list->GetItemCount() + 1))); fields.push_back(wxVariant(0)); fields.push_back(wxVariant(_L("Enqueued"))); fields.push_back(wxVariant(job.printhost->get_host())); boost::system::error_code ec; boost::uintmax_t size_i = boost::filesystem::file_size(job.upload_data.source_path, ec); std::stringstream stream; if (ec) { stream << "unknown"; size_i = 0; BOOST_LOG_TRIVIAL(error) << ec.message(); } else stream << std::fixed << std::setprecision(2) << ((float)size_i / 1024 / 1024) << "MB"; fields.push_back(wxVariant(stream.str())); fields.push_back(wxVariant(job.upload_data.upload_path.string())); fields.push_back(wxVariant("")); job_list->AppendItem(fields, static_cast(ST_NEW)); // Both strings are UTF-8 encoded. upload_names.emplace_back(job.printhost->get_host(), job.upload_data.upload_path.string()); wxGetApp().notification_manager()->push_upload_job_notification(job_list->GetItemCount(), (float)size_i / 1024 / 1024, job.upload_data.upload_path.string(), job.printhost->get_host()); } void PrintHostQueueDialog::on_dpi_changed(const wxRect &suggested_rect) { const int& em = em_unit(); msw_buttons_rescale(this, em, { wxID_DELETE, wxID_CANCEL, btn_error->GetId() }); SetMinSize(wxSize(HEIGHT * em, WIDTH * em)); Fit(); Refresh(); save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS); } PrintHostQueueDialog::JobState PrintHostQueueDialog::get_state(int idx) { wxCHECK_MSG(idx >= 0 && idx < job_list->GetItemCount(), ST_ERROR, "Out of bounds access to job list"); return static_cast(job_list->GetItemData(job_list->RowToItem(idx))); } void PrintHostQueueDialog::set_state(int idx, JobState state) { wxCHECK_RET(idx >= 0 && idx < job_list->GetItemCount(), "Out of bounds access to job list"); job_list->SetItemData(job_list->RowToItem(idx), static_cast(state)); switch (state) { case ST_NEW: job_list->SetValue(_L("Enqueued"), idx, COL_STATUS); break; case ST_PROGRESS: job_list->SetValue(_L("Uploading"), idx, COL_STATUS); break; case ST_ERROR: job_list->SetValue(_L("Error"), idx, COL_STATUS); break; case ST_CANCELLING: job_list->SetValue(_L("Cancelling"), idx, COL_STATUS); break; case ST_CANCELLED: job_list->SetValue(_L("Cancelled"), idx, COL_STATUS); break; case ST_COMPLETED: job_list->SetValue(_L("Completed"), idx, COL_STATUS); break; } // This might be ambigous call, but user data needs to be saved time to time save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS); } void PrintHostQueueDialog::on_list_select() { int selected = job_list->GetSelectedRow(); if (selected != wxNOT_FOUND) { const JobState state = get_state(selected); btn_cancel->Enable(state < ST_ERROR); btn_error->Enable(state == ST_ERROR); Layout(); } else { btn_cancel->Disable(); } } void PrintHostQueueDialog::on_progress(Event &evt) { wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list"); if (evt.progress < 100) { set_state(evt.job_id, ST_PROGRESS); job_list->SetValue(wxVariant(evt.progress), evt.job_id, COL_PROGRESS); } else { set_state(evt.job_id, ST_COMPLETED); job_list->SetValue(wxVariant(100), evt.job_id, COL_PROGRESS); } on_list_select(); if (evt.progress > 0) { wxVariant nm, hst; job_list->GetValue(nm, evt.job_id, COL_FILENAME); job_list->GetValue(hst, evt.job_id, COL_HOST); wxGetApp().notification_manager()->set_upload_job_notification_percentage(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString()), evt.progress / 100.f); } } void PrintHostQueueDialog::on_error(Event &evt) { wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list"); set_state(evt.job_id, ST_ERROR); auto errormsg = from_u8((boost::format("%1%\n%2%") % _utf8(L("Error uploading to print host:")) % std::string(evt.error.ToUTF8())).str()); job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS); job_list->SetValue(wxVariant(errormsg), evt.job_id, COL_ERRORMSG); // Stashes the error message into a hidden column for later on_list_select(); GUI::show_error(nullptr, errormsg); wxVariant nm, hst; job_list->GetValue(nm, evt.job_id, COL_FILENAME); job_list->GetValue(hst, evt.job_id, COL_HOST); wxGetApp().notification_manager()->upload_job_notification_show_error(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString())); } void PrintHostQueueDialog::on_cancel(Event &evt) { wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list"); set_state(evt.job_id, ST_CANCELLED); job_list->SetValue(wxVariant(0), evt.job_id, COL_PROGRESS); on_list_select(); wxVariant nm, hst; job_list->GetValue(nm, evt.job_id, COL_FILENAME); job_list->GetValue(hst, evt.job_id, COL_HOST); wxGetApp().notification_manager()->upload_job_notification_show_canceled(evt.job_id + 1, boost::nowide::narrow(nm.GetString()), boost::nowide::narrow(hst.GetString())); } void PrintHostQueueDialog::get_active_jobs(std::vector>& ret) { int ic = job_list->GetItemCount(); for (int i = 0; i < ic; i++) { auto item = job_list->RowToItem(i); auto data = job_list->GetItemData(item); JobState st = static_cast(data); if(st == JobState::ST_NEW || st == JobState::ST_PROGRESS) ret.emplace_back(upload_names[i]); } //job_list->data } void PrintHostQueueDialog::save_user_data(int udt) { const auto em = GetTextExtent("m").x; auto *app_config = wxGetApp().app_config; if (udt & UserDataType::UDT_SIZE) { app_config->set("print_host_queue_dialog_height", std::to_string(this->GetSize().x / em)); app_config->set("print_host_queue_dialog_width", std::to_string(this->GetSize().y / em)); } if (udt & UserDataType::UDT_POSITION) { app_config->set("print_host_queue_dialog_x", std::to_string(this->GetPosition().x)); app_config->set("print_host_queue_dialog_y", std::to_string(this->GetPosition().y)); } if (udt & UserDataType::UDT_COLS) { for (size_t i = 0; i < job_list->GetColumnCount() - 1; i++) { app_config->set("print_host_queue_dialog_column_" + std::to_string(i), std::to_string(job_list->GetColumn(i)->GetWidth())); } } } bool PrintHostQueueDialog::load_user_data(int udt, std::vector& vector) { auto* app_config = wxGetApp().app_config; auto hasget = [app_config](const std::string& name, std::vector& vector)->bool { if (app_config->has(name)) { vector.push_back(std::stoi(app_config->get(name))); return true; } return false; }; if (udt & UserDataType::UDT_SIZE) { if (!hasget("print_host_queue_dialog_height",vector)) return false; if (!hasget("print_host_queue_dialog_width", vector)) return false; } if (udt & UserDataType::UDT_POSITION) { if (!hasget("print_host_queue_dialog_x", vector)) return false; if (!hasget("print_host_queue_dialog_y", vector)) return false; } if (udt & UserDataType::UDT_COLS) { for (size_t i = 0; i < 6; i++) { if (!hasget("print_host_queue_dialog_column_" + std::to_string(i), vector)) return false; } } return true; } }}