Files
OrcaSlicer-KX/src/slic3r/GUI/PrintHostDialogs.cpp
grant0013 8dfd480c52 Merge branch 'upstream/main' into harktech/k2-discovery
Consolidates #13744 (CFS filament sync) into this PR per maintainer
request and resyncs with main. Conflicts resolved:

* src/slic3r/GUI/Plater.cpp -- parallel else-if added by both sides
  (htCrealityPrint + flashforge_local_api branches); kept both.
* src/slic3r/GUI/PhysicalPrinterDialog.cpp -- both sides added a
  host-specific Browse dialog (Creality DNS-SD + Flashforge); kept
  both as early-return branches, fall through to BonjourDialog.
* src/slic3r/GUI/PrintHostDialogs.hpp -- parallel class declarations
  (CrealityPrintHostSendDialog + FlashforgePrintHostSendDialog);
  kept both.
2026-05-28 07:56:06 +00:00

2110 lines
85 KiB
C++

#include "PrintHostDialogs.hpp"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <limits>
#include <wx/frame.h>
#include <wx/progdlg.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/checkbox.h>
#include <wx/button.h>
#include <wx/dataview.h>
#include <wx/dcbuffer.h>
#include <wx/graphics.h>
#include <wx/wrapsizer.h>
#include <wx/wupdlock.h>
#include <wx/debug.h>
#include <wx/msgdlg.h>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/algorithm/string.hpp>
#include <nlohmann/json.hpp>
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "MainFrame.hpp"
#include "libslic3r/AppConfig.hpp"
#include "NotificationManager.hpp"
#include "ExtraRenderers.hpp"
#include "format.hpp"
#include "../Utils/CrealityPrint.hpp"
#include "BitmapComboBox.hpp"
#include "wxExtensions.hpp"
#include <nlohmann/json.hpp>
namespace fs = boost::filesystem;
using json = nlohmann::json;
namespace Slic3r {
namespace GUI {
namespace {
wxColour contrasting_text_color(const wxColour& background)
{
return background.GetLuminance() < 0.60 ? *wxWHITE : wxColour("#303030");
}
long long color_distance_sq(const wxColour& lhs, const wxColour& rhs)
{
const long long dr = static_cast<long long>(lhs.Red()) - static_cast<long long>(rhs.Red());
const long long dg = static_cast<long long>(lhs.Green()) - static_cast<long long>(rhs.Green());
const long long db = static_cast<long long>(lhs.Blue()) - static_cast<long long>(rhs.Blue());
return dr * dr + dg * dg + db * db;
}
class FlashforgeSlotCard : public wxPanel
{
public:
FlashforgeSlotCard(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
SetDoubleBuffered(true);
SetMinSize(wxSize(FromDIP(68), FromDIP(92)));
SetMaxSize(GetMinSize());
Bind(wxEVT_PAINT, &FlashforgeSlotCard::on_paint, this);
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) {
m_hover = true;
SetCursor(wxCursor(m_enabled ? wxCURSOR_HAND : wxCURSOR_NO_ENTRY));
Refresh();
e.Skip();
});
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) {
m_hover = false;
SetCursor(wxCursor(wxCURSOR_ARROW));
Refresh();
e.Skip();
});
Bind(wxEVT_LEFT_DOWN, &FlashforgeSlotCard::on_left_down, this);
}
void set_slot(const Slic3r::FlashforgeMaterialSlot& slot, bool enabled)
{
m_slot_id = slot.slot_id;
m_color = parse_color(slot.material_color);
m_name = slot.material_name.empty() ? _L("Unknown") : from_u8(slot.material_name);
m_empty = !slot.has_filament;
m_enabled = enabled && !m_empty;
Refresh();
}
private:
static wxColour parse_color(const std::string& raw)
{
wxColour color(from_u8(raw));
if (color.IsOk())
return color;
std::string value = raw;
boost::trim(value);
if (!value.empty() && value.front() != '#')
value.insert(value.begin(), '#');
color = wxColour(from_u8(value));
return color.IsOk() ? color : wxColour("#D0D0D0");
}
void on_left_down(wxMouseEvent& e)
{
if (!m_enabled)
return;
wxCommandEvent evt(wxEVT_BUTTON, GetId());
evt.SetInt(m_slot_id);
evt.SetString(m_color.GetAsString(wxC2S_HTML_SYNTAX));
wxPostEvent(this, evt);
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int circle_size = FromDIP(24);
const int outline_width = (m_hover && m_enabled) ? FromDIP(2) : FromDIP(1);
const wxRect body_rect(FromDIP(7), FromDIP(26), size.x - FromDIP(14), size.y - FromDIP(32));
const wxColour badge_color = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
const wxColour body_border = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush((m_hover && m_enabled) ? badge_color.ChangeLightness(130) : badge_color));
gc->DrawEllipse((size.x - circle_size) / 2.0, 0, circle_size, circle_size);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(*wxWHITE);
const wxString slot_txt = wxString::Format("%d", m_slot_id);
const wxSize slot_size = dc.GetTextExtent(slot_txt);
dc.DrawText(slot_txt, (size.x - slot_size.x) / 2, (circle_size - slot_size.y) / 2);
const wxColour bg_color = m_empty ? wxColour("#F6F6F6") : m_color;
gc->SetPen(wxPen(body_border, outline_width));
gc->SetBrush(wxBrush(bg_color));
gc->DrawRoundedRectangle(body_rect.x, body_rect.y, body_rect.width, body_rect.height, FromDIP(8));
dc.SetFont(::Label::Body_12);
dc.SetTextForeground(contrasting_text_color(bg_color));
wxString label = m_empty ? _L("Empty") : m_name;
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8))
dc.SetFont(::Label::Body_10);
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8)) {
while (!label.empty() && dc.GetTextExtent(label + "...").x > body_rect.width - FromDIP(8))
label.RemoveLast();
label += "...";
}
const wxSize label_size = dc.GetTextExtent(label);
dc.DrawText(label, body_rect.x + (body_rect.width - label_size.x) / 2, body_rect.y + (body_rect.height - label_size.y) / 2);
}
private:
int m_slot_id {0};
wxColour m_color {*wxWHITE};
wxString m_name;
bool m_empty {true};
bool m_enabled {false};
bool m_hover {false};
};
class FlashforgeSlotDialog : public DPIDialog
{
public:
FlashforgeSlotDialog(wxWindow* parent, const wxString& material_name)
: DPIDialog(parent, wxID_ANY, _L("Choose a slot for the selected color"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_material_name(material_name)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
auto* root = new wxBoxSizer(wxVERTICAL);
auto* title = new wxStaticText(this, wxID_ANY, _L("Material in the material station"));
title->SetFont(::Label::Head_13);
root->Add(title, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
m_grid = new wxGridSizer(1, 4, FromDIP(10), FromDIP(12));
auto* grid_row = new wxBoxSizer(wxHORIZONTAL);
grid_row->AddStretchSpacer();
grid_row->Add(m_grid, 0);
grid_row->AddStretchSpacer();
root->Add(grid_row, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(18));
auto* tip = new wxStaticText(this, wxID_ANY, _L("Only materials of the same type can be selected."));
tip->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT));
root->Add(tip, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
SetSizer(root);
SetMinSize(wxSize(FromDIP(460), FromDIP(240)));
for (int i = 0; i < 4; ++i) {
auto* card = new FlashforgeSlotCard(this);
card->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
m_selected_slot_id = e.GetInt();
m_selected_color = wxColour(e.GetString());
EndModal(wxID_OK);
});
m_cards.push_back(card);
m_grid->Add(card, 0);
}
wxGetApp().UpdateDlgDarkUI(this);
Layout();
Fit();
CenterOnParent();
Refresh();
}
void update_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
for (size_t i = 0; i < m_cards.size(); ++i) {
Slic3r::FlashforgeMaterialSlot slot;
slot.slot_id = static_cast<int>(i) + 1;
if (const auto it = std::find_if(slots.begin(), slots.end(), [&](const Slic3r::FlashforgeMaterialSlot& item) { return item.slot_id == slot.slot_id; }); it != slots.end())
slot = *it;
const bool enabled = slot.has_filament && matcher(slot);
m_cards[i]->set_slot(slot, enabled);
}
Layout();
Fit();
}
bool has_selection() const { return m_selected_slot_id > 0; }
int selected_slot_id() const { return m_selected_slot_id; }
wxColour selected_color() const { return m_selected_color; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override
{
Fit();
Refresh();
if (suggested_rect.IsEmpty())
return;
SetSize(suggested_rect.GetSize());
}
private:
wxString m_material_name;
wxGridSizer* m_grid {nullptr};
std::vector<FlashforgeSlotCard*> m_cards;
int m_selected_slot_id {0};
wxColour m_selected_color;
};
class FlashforgeMaterialMapWidget : public wxPanel
{
public:
using SelectFn = std::function<void(FlashforgeMaterialMapWidget*)>;
FlashforgeMaterialMapWidget(wxWindow* parent, int tool_id, const wxColour& color, const wxString& material_name, SelectFn on_select)
: wxPanel(parent, wxID_ANY)
, m_tool_id(tool_id)
, m_color(color)
, m_name(material_name.Strip())
, m_select_fn(std::move(on_select))
{
SetDoubleBuffered(true);
const wxSize size(FromDIP(72), FromDIP(58));
SetSize(size);
SetMinSize(size);
SetMaxSize(size);
Bind(wxEVT_PAINT, &FlashforgeMaterialMapWidget::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &FlashforgeMaterialMapWidget::on_left_down, this);
}
int tool_id() const { return m_tool_id; }
int selected_slot_id() const { return m_slot_id; }
bool is_slot_selected() const { return m_slot_id > 0; }
wxString material_name() const { return m_name; }
void set_enable_mapping(bool enable)
{
if (m_mapping_enabled == enable)
return;
m_mapping_enabled = enable;
if (!enable)
reset_slot();
Enable(enable);
Refresh();
}
void set_slot_selection(int slot_id, const wxColour& slot_color)
{
m_slot_id = slot_id;
m_slot_color = slot_color;
Refresh();
}
void reset_slot()
{
m_slot_id = 0;
m_slot_color = wxColour("#DDDDDD");
Refresh();
}
void update_popup_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
m_slots_snapshot = slots;
m_matcher = matcher;
}
wxSize DoGetBestSize() const override
{
return wxSize(FromDIP(72), FromDIP(58));
}
private:
void on_left_down(wxMouseEvent& e)
{
if (!m_mapping_enabled)
return;
FlashforgeSlotDialog dlg(this, m_name);
dlg.update_slots(m_slots_snapshot, m_matcher);
m_selected = true;
Refresh();
if (dlg.ShowModal() == wxID_OK && dlg.has_selection()) {
m_slot_id = dlg.selected_slot_id();
m_slot_color = dlg.selected_color();
if (m_select_fn)
m_select_fn(this);
}
m_selected = false;
Refresh();
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int half_h = size.y / 2;
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush(m_color));
gc->DrawRoundedRectangle(0, 0, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h - FromDIP(3), size.x, FromDIP(3));
gc->SetBrush(wxBrush(m_mapping_enabled ? m_slot_color : wxColour("#DDDDDD")));
gc->DrawRoundedRectangle(0, half_h, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h, size.x, FromDIP(3));
if (m_selected) {
gc->SetPen(wxPen(wxColour("#00AE42"), FromDIP(2)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
} else if (m_color.GetLuminance() > 0.95 || m_slot_color.GetLuminance() > 0.95) {
gc->SetPen(wxPen(wxColour("#ACACAC"), FromDIP(1)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
}
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_color));
wxString top_text = m_name;
if (dc.GetTextExtent(top_text).x > size.x - FromDIP(10)) {
dc.SetFont(::Label::Body_10);
}
wxSize top_size = dc.GetTextExtent(top_text);
dc.DrawText(top_text, (size.x - top_size.x) / 2, (half_h - top_size.y) / 2);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_slot_color));
const wxString bottom_text = m_slot_id > 0 ? wxString::Format("%d", m_slot_id) : "-";
const wxSize bottom_size = dc.GetTextExtent(bottom_text);
dc.DrawText(bottom_text, (size.x - bottom_size.x - FromDIP(10)) / 2, half_h + (half_h - bottom_size.y) / 2);
wxPoint pts[3] = {
wxPoint(size.x - FromDIP(18), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(10), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(14), half_h + half_h / 2 + FromDIP(3))
};
dc.SetBrush(wxBrush(contrasting_text_color(m_slot_color)));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawPolygon(3, pts);
}
private:
int m_tool_id {-1};
wxColour m_color;
wxString m_name;
wxColour m_slot_color {wxColour("#DDDDDD")};
int m_slot_id {0};
bool m_selected {false};
bool m_mapping_enabled {true};
SelectFn m_select_fn;
std::vector<Slic3r::FlashforgeMaterialSlot> m_slots_snapshot;
std::function<bool(const Slic3r::FlashforgeMaterialSlot&)> m_matcher;
};
static FlashforgeMaterialMapWidget* as_ff_map_widget(wxWindow* window)
{
return dynamic_cast<FlashforgeMaterialMapWidget*>(window);
}
} // namespace
static const char *CONFIG_KEY_PATH = "printhost_path";
static const char *CONFIG_KEY_GROUP = "printhost_group";
static const char* CONFIG_KEY_STORAGE = "printhost_storage";
PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups, const wxArrayString& storage_paths, const wxArrayString& storage_names, bool switch_to_device_tab)
: MsgDialog(static_cast<wxWindow*>(wxGetApp().mainframe), _L("Send G-code to printer host"), _L("Upload to Printer Host with the following filename:"), 0) // Set style = 0 to avoid default creation of the "OK" button.
// All buttons will be added later in this constructor
, txt_filename(new wxTextCtrl(this, wxID_ANY))
, combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr)
, combo_storage(storage_names.GetCount() > 1 ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, storage_names, wxCB_READONLY) : nullptr)
, post_upload_action(PrintHostPostUploadAction::None)
, m_paths(storage_paths)
, m_switch_to_device_tab(switch_to_device_tab)
, m_path(path)
, m_post_actions(post_actions)
, m_storage_names(storage_names)
{
#ifdef __APPLE__
txt_filename->OSXDisableAllSmartSubstitutions();
#endif
}
void PrintHostSendDialog::init()
{
const auto& path = m_path;
const auto& storage_paths = m_paths;
const auto& post_actions = m_post_actions;
const auto& storage_names = m_storage_names;
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 (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);
}
if (combo_storage != nullptr) {
// PrusaLink specific: User needs to choose a storage
auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage") + ":");
content_sizer->Add(label_group);
content_sizer->Add(combo_storage, 0, wxBOTTOM, 2 * VERT_SPACING);
combo_storage->SetValue(storage_names.front());
wxString recent_storage = from_u8(app_config->get("recent", CONFIG_KEY_STORAGE));
if (!recent_storage.empty())
combo_storage->SetValue(recent_storage);
} else if (storage_names.GetCount() == 1){
// PrusaLink specific: Show which storage has been detected.
auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage") + ": " + storage_names.front());
content_sizer->Add(label_group);
m_preselected_storage = storage_paths.front();
}
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);
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this, wxID_APPLY);
checkbox->SetValue(m_switch_to_device_tab);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_switch_to_device_tab = e.IsChecked();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload."), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
// .gcode suffix control
auto validate_path = [this](const wxString &path) -> bool {
if (! path.Lower().EndsWith(m_valid_suffix.Lower())) {
MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix), wxString(SLIC3R_APP_NAME), wxYES | wxNO);
if (msg_wingow.ShowModal() == wxID_NO)
return false;
}
return true;
};
auto* btn_ok = add_button(wxID_OK, true, _L("Upload"));
btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::None;
EndDialog(wxID_OK);
}
});
txt_filename->SetFocus();
// if (post_actions.has(PrintHostPostUploadAction::QueuePrint)) {
// auto* btn_print = add_button(wxID_ADD, false, _L("Upload to Queue"));
// btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
// if (validate_path(txt_filename->GetValue())) {
// post_upload_action = PrintHostPostUploadAction::QueuePrint;
// EndDialog(wxID_OK);
// }
// });
// }
if (post_actions.has(PrintHostPostUploadAction::StartPrint)) {
auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print"));
btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::StartPrint;
EndDialog(wxID_OK);
}
});
}
// if (post_actions.has(PrintHostPostUploadAction::StartSimulation)) {
// // Using wxID_MORE as a button identifier to be different from the other buttons, wxID_MORE has no other meaning here.
// auto* btn_simulate = add_button(wxID_MORE, false, _L("Upload and Simulate"));
// btn_simulate->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
// if (validate_path(txt_filename->GetValue())) {
// post_upload_action = PrintHostPostUploadAction::StartSimulation;
// EndDialog(wxID_OK);
// }
// });
// }
add_button(wxID_CANCEL,false, L("Cancel"));
finalize();
#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->SetInsertionPoint(0);
txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
});
});
}
fs::path PrintHostSendDialog::filename() const
{
return into_path(txt_filename->GetValue());
}
PrintHostPostUploadAction PrintHostSendDialog::post_action() const
{
return post_upload_action;
}
std::string PrintHostSendDialog::group() const
{
if (combo_groups == nullptr) {
return "";
} else {
wxString group = combo_groups->GetValue();
return into_u8(group);
}
}
std::string PrintHostSendDialog::storage() const
{
if (!combo_storage)
return GUI::format("%1%", m_preselected_storage);
if (combo_storage->GetSelection() < 0 || combo_storage->GetSelection() >= int(m_paths.size()))
return {};
return into_u8(m_paths[combo_storage->GetSelection()]);
}
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));
if (combo_groups != nullptr) {
wxString group = combo_groups->GetValue();
app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group));
}
if (combo_storage != nullptr) {
wxString storage = combo_storage->GetValue();
app_config->set("recent", CONFIG_KEY_STORAGE, into_u8(storage));
}
}
MsgDialog::EndModal(ret);
}
FlashforgePrintHostSendDialog::FlashforgePrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
const Slic3r::Flashforge* host,
bool supports_material_station,
std::vector<Slic3r::FlashforgeMaterialSlot> slots,
const std::vector<FilamentInfo>& project_filaments)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_host(host)
, m_slots(std::move(slots))
, m_project_filaments(project_filaments)
{
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
}
void FlashforgePrintHostSendDialog::init()
{
const AppConfig* app_config = wxGetApp().app_config;
const auto& path = m_path;
std::string leveling = app_config->get("recent", CONFIG_KEY_LEVELING);
if (!leveling.empty())
m_leveling_before_print = leveling == "1";
std::string timelapse = app_config->get("recent", CONFIG_KEY_TIMELAPSE);
if (!timelapse.empty())
m_time_lapse_video = timelapse == "1";
// Flashforge local printing should default to IFS enabled when supported.
// We don't revive an old stale "0" here.
m_use_material_station = m_supports_material_station;
if (m_supports_material_station && !app_config->has("recent", CONFIG_KEY_IFS))
const_cast<AppConfig*>(app_config)->set("recent", CONFIG_KEY_IFS, "1");
this->SetMinSize(wxSize(560, 420));
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);
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);
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this, wxID_APPLY);
checkbox->SetValue(m_switch_to_device_tab);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
m_switch_to_device_tab = e.IsChecked();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload."));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
}
m_flashforge_options_sizer = new wxBoxSizer(wxVERTICAL);
auto add_option_checkbox = [this](wxBoxSizer* parent, const wxString& label, bool value, std::function<void(bool)> setter, ::CheckBox** out = nullptr) {
auto row = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(value);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [setter](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
setter(e.IsChecked());
e.Skip();
});
row->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto text = new wxStaticText(this, wxID_ANY, label);
text->SetFont(::Label::Body_13);
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
row->Add(text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
parent->Add(row);
parent->AddSpacer(FromDIP(6));
if (out != nullptr)
*out = checkbox;
};
add_option_checkbox(m_flashforge_options_sizer, _L("Leveling before print"), m_leveling_before_print,
[this](bool checked) { m_leveling_before_print = checked; }, &m_checkbox_leveling);
add_option_checkbox(m_flashforge_options_sizer, _L("Time-lapse"), m_time_lapse_video,
[this](bool checked) { m_time_lapse_video = checked; }, &m_checkbox_timelapse);
add_option_checkbox(m_flashforge_options_sizer, _L("Enable IFS"), m_use_material_station,
[this](bool checked) {
m_use_material_station = checked;
if (checked) {
ensure_slots_loaded();
rebuild_mapping_rows();
}
sync_mapping_section_visibility();
}, &m_checkbox_ifs);
if (m_checkbox_ifs != nullptr && !m_supports_material_station)
m_checkbox_ifs->Enable(false);
m_status_text = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_status_text->SetFont(::Label::Body_12);
m_flashforge_options_sizer->Add(m_status_text, 0, wxTOP | wxBOTTOM, FromDIP(4));
m_mapping_section_sizer = new wxBoxSizer(wxVERTICAL);
m_mapping_wrap_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS);
m_mapping_section_sizer->Add(m_mapping_wrap_sizer, 0, wxTOP | wxALIGN_LEFT, FromDIP(10));
m_flashforge_options_sizer->Add(m_mapping_section_sizer, 0, wxEXPAND);
content_sizer->Add(m_flashforge_options_sizer, 0, wxEXPAND);
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
rebuild_mapping_rows();
sync_mapping_section_visibility();
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
auto validate_path = [this](const wxString& filename) -> bool {
if (!filename.Lower().EndsWith(m_valid_suffix.Lower())) {
MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix),
wxString(SLIC3R_APP_NAME), wxYES | wxNO);
if (msg_wingow.ShowModal() == wxID_NO)
return false;
}
return validate_before_close();
};
auto* btn_ok = add_button(wxID_OK, true, _L("Upload"));
btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::None;
EndDialog(wxID_OK);
}
});
if (m_post_actions.has(PrintHostPostUploadAction::StartPrint)) {
auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print"));
btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::StartPrint;
EndDialog(wxID_OK);
}
});
}
add_button(wxID_CANCEL, false, _L("Cancel"));
finalize();
txt_filename->SetFocus();
#ifdef __linux__
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&) {
CallAfter([=]() {
txt_filename->SetInsertionPoint(0);
txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
});
});
}
void FlashforgePrintHostSendDialog::EndModal(int ret)
{
if (ret == wxID_OK) {
AppConfig* app_config = wxGetApp().app_config;
app_config->set("recent", CONFIG_KEY_LEVELING, m_leveling_before_print ? "1" : "0");
app_config->set("recent", CONFIG_KEY_TIMELAPSE, m_time_lapse_video ? "1" : "0");
app_config->set("recent", CONFIG_KEY_IFS, m_use_material_station ? "1" : "0");
}
PrintHostSendDialog::EndModal(ret);
}
std::map<std::string, std::string> FlashforgePrintHostSendDialog::extendedInfo() const
{
json mappings = json::array();
int mapped_count = 0;
if (m_use_material_station) {
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || row.tool_id < 0)
continue;
const int slot_id = card->selected_slot_id();
if (slot_id <= 0)
continue;
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& item) { return item.id == row.tool_id; });
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return slot.slot_id == slot_id; });
if (filament_it == m_project_filaments.end() || slot_it == m_slots.end())
continue;
mappings.push_back({
{"toolId", filament_it->id},
{"slotId", slot_it->slot_id},
{"materialName", slot_it->material_name},
{"toolMaterialColor", filament_it->color},
{"slotMaterialColor", slot_it->material_color}
});
++mapped_count;
}
}
return {
{"levelingBeforePrint", m_leveling_before_print ? "1" : "0"},
{"timeLapseVideo", m_time_lapse_video ? "1" : "0"},
{"useMatlStation", m_use_material_station ? "1" : "0"},
{"gcodeToolCnt", std::to_string(mapped_count)},
{"materialMappings", mappings.dump()}
};
}
void FlashforgePrintHostSendDialog::load_slots()
{
m_slots.clear();
m_slots_loaded = false;
m_supports_material_station = false;
if (m_host == nullptr) {
m_status_text->SetLabel(_L("Flashforge host is not available."));
return;
}
wxString msg;
bool supports_material_station = false;
if (!m_host->fetch_material_slots(m_slots, &supports_material_station, msg)) {
m_status_text->SetLabel(msg.empty() ? _L("Unable to read IFS slots from printer.") : msg);
return;
}
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
m_use_material_station = m_supports_material_station;
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
}
bool FlashforgePrintHostSendDialog::ensure_slots_loaded(bool force_reload)
{
if (!force_reload && (m_slots_loaded || !m_supports_material_station))
return m_slots_loaded;
if (m_status_text != nullptr)
m_status_text->SetLabel(_L("Loading IFS slots from printer..."));
wxBusyCursor wait;
load_slots();
return m_slots_loaded;
}
void FlashforgePrintHostSendDialog::rebuild_mapping_rows()
{
if (m_mapping_wrap_sizer == nullptr)
return;
m_mapping_wrap_sizer->Clear(true);
m_mapping_rows.clear();
if (m_project_filaments.empty()) {
m_mapping_wrap_sizer->Add(new wxStaticText(this, wxID_ANY, _L("Slice the plate first to get project material information.")), 0, wxALL, FromDIP(2));
return;
}
for (const auto& filament : m_project_filaments) {
auto* card = new FlashforgeMaterialMapWidget(this, filament.id, to_wx_colour(filament.color), from_u8(filament.get_display_filament_type()),
[this](FlashforgeMaterialMapWidget* changed_card) {
if (changed_card == nullptr)
return;
for (auto& row : m_mapping_rows) {
if (row.card == changed_card) {
refresh_mapping_card(row);
break;
}
}
});
m_mapping_wrap_sizer->Add(card, 0, wxRIGHT | wxBOTTOM | wxFIXED_MINSIZE, FromDIP(10));
MappingRow row;
row.tool_id = filament.id;
row.card = card;
m_mapping_rows.push_back(row);
}
auto_assign_mappings();
}
void FlashforgePrintHostSendDialog::auto_assign_mappings()
{
for (size_t idx = 0; idx < m_project_filaments.size() && idx < m_mapping_rows.size(); ++idx) {
auto& filament = m_project_filaments[idx];
auto* card = as_ff_map_widget(m_mapping_rows[idx].card);
if (card == nullptr)
continue;
const wxColour filament_color = to_wx_colour(filament.color);
const Slic3r::FlashforgeMaterialSlot* best_slot = nullptr;
long long best_distance = std::numeric_limits<long long>::max();
for (const auto& slot : m_slots) {
if (!slot.has_filament || !slot_matches_filament(slot, filament))
continue;
const long long distance = color_distance_sq(filament_color, to_wx_colour(slot.material_color));
if (best_slot == nullptr || distance < best_distance) {
best_slot = &slot;
best_distance = distance;
}
}
if (best_slot != nullptr)
card->set_slot_selection(best_slot->slot_id, to_wx_colour(best_slot->material_color));
else
card->reset_slot();
refresh_mapping_card(m_mapping_rows[idx]);
}
}
void FlashforgePrintHostSendDialog::refresh_mapping_card(MappingRow& row)
{
auto* card = as_ff_map_widget(row.card);
if (card == nullptr)
return;
const auto* filament = find_filament_by_tool_id(row.tool_id);
card->set_enable_mapping(m_use_material_station);
card->update_popup_slots(m_slots, [this, filament](const FlashforgeMaterialSlot& slot) {
return filament != nullptr && slot_matches_filament(slot, *filament);
});
if (card->selected_slot_id() <= 0) {
card->reset_slot();
return;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
if (slot == nullptr) {
card->reset_slot();
return;
}
card->set_slot_selection(slot->slot_id, to_wx_colour(slot->material_color));
}
void FlashforgePrintHostSendDialog::sync_mapping_section_visibility()
{
if (m_mapping_section_sizer == nullptr)
return;
m_mapping_section_sizer->ShowItems(m_use_material_station && m_supports_material_station);
if (wxSizer* sizer = GetSizer(); sizer != nullptr) {
sizer->Layout();
sizer->Fit(this);
SetMinSize(GetBestSize());
}
Layout();
Fit();
}
const Slic3r::FlashforgeMaterialSlot* FlashforgePrintHostSendDialog::find_slot_by_id(const std::string& slot_id_text) const
{
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return std::to_string(slot.slot_id) == slot_id_text; });
return slot_it == m_slots.end() ? nullptr : &(*slot_it);
}
const FilamentInfo* FlashforgePrintHostSendDialog::find_filament_by_tool_id(int tool_id) const
{
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& filament) { return filament.id == tool_id; });
return filament_it == m_project_filaments.end() ? nullptr : &(*filament_it);
}
bool FlashforgePrintHostSendDialog::slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const
{
if (!slot.has_filament)
return false;
const std::string project_material = normalize_material(!filament.type.empty() ? filament.type : filament.get_display_filament_type());
const std::string slot_material = normalize_material(slot.material_name);
return !project_material.empty() && !slot_material.empty() && project_material == slot_material;
}
bool FlashforgePrintHostSendDialog::validate_before_close()
{
if (!m_use_material_station && m_project_filaments.size() > 1) {
show_error(this, _L("This plate uses multiple materials. Enable IFS and assign each tool to a printer slot."));
return false;
}
if (!m_use_material_station)
return true;
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || !card->is_slot_selected()) {
show_error(this, _L("Each project material must be assigned to an IFS slot before printing."));
return false;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
const auto* filament = find_filament_by_tool_id(row.tool_id);
if (slot == nullptr || filament == nullptr || !slot->has_filament) {
show_error(this, _L("Each project material must be assigned to a loaded IFS slot before printing."));
return false;
}
if (!slot_matches_filament(*slot, *filament)) {
show_error(this, _L("Each project material must match the material loaded in the selected IFS slot."));
return false;
}
}
return true;
}
std::string FlashforgePrintHostSendDialog::normalize_material(const std::string& material) const
{
std::string normalized = boost::to_upper_copy(material);
normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char ch) { return !std::isalnum(ch); }), normalized.end());
if (normalized.empty())
return {};
if (normalized.find("SILK") != std::string::npos)
return "SILK";
if (normalized.find("PLA") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PLACF";
if (normalized.find("PETG") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PETGCF";
if (normalized == "PLA" || normalized == "PLA+" || normalized == "PLAPLUS")
return "PLA";
if (normalized.find("PLA") != std::string::npos)
return "PLA";
if (normalized == "ABS" || normalized.find("ABS") != std::string::npos)
return "ABS";
if (normalized == "ASA" || normalized.find("ASA") != std::string::npos)
return "ABS";
if (normalized.find("PETG") != std::string::npos)
return "PETG";
if (normalized.find("TPU") != std::string::npos || normalized.find("TPE") != std::string::npos || normalized.find("FLEX") != std::string::npos)
return "TPU";
return normalized;
}
wxColour FlashforgePrintHostSendDialog::to_wx_colour(const std::string& color) const
{
wxColour wx_color(from_u8(color));
if (wx_color.IsOk())
return wx_color;
std::string normalized = boost::trim_copy(color);
if (boost::istarts_with(normalized, "0x"))
normalized = normalized.substr(2);
if (!normalized.empty() && normalized.front() == '#')
normalized.erase(normalized.begin());
if (normalized.size() == 8) {
auto hex_to_byte = [](char hi, char lo) -> int {
auto hex_val = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
const int h = hex_val(hi);
const int l = hex_val(lo);
return (h < 0 || l < 0) ? -1 : h * 16 + l;
};
const int r = hex_to_byte(normalized[0], normalized[1]);
const int g = hex_to_byte(normalized[2], normalized[3]);
const int b = hex_to_byte(normalized[4], normalized[5]);
const int a = hex_to_byte(normalized[6], normalized[7]);
if (r >= 0 && g >= 0 && b >= 0 && a >= 0)
return wxColour(r, g, b, a);
}
if (normalized.size() == 6) {
wx_color = wxColour("#" + from_u8(normalized));
if (wx_color.IsOk())
return wx_color;
}
return wxColour("#999999");
}
wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_INFO, 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)
, status(std::move(error))
{}
PrintHostQueueDialog::Event::Event(wxEventType eventType, int winid, size_t job_id, wxString tag, wxString status)
: wxEvent(winid, eventType)
, job_id(job_id)
, tag(std::move(tag))
, status(std::move(status))
{}
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)
, on_info_evt(this, EVT_PRINTHOST_INFO, &PrintHostQueueDialog::on_info, this)
{
const auto em = GetTextExtent("m").x;
auto *topsizer = new wxBoxSizer(wxVERTICAL);
std::vector<int> widths;
widths.reserve(7);
if (!load_user_data(UDT_COLS, widths)) {
widths.clear();
for (size_t i = 0; i < 7; i++)
widths.push_back(-1);
}
job_list = new wxDataViewListCtrl(this, wxID_ANY);
// MSW DarkMode: workaround for the selected item in the list
auto append_text_column = [this](const wxString& label, int width, wxAlignment align = wxALIGN_LEFT,
int flags = wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE) {
#ifdef _WIN32
job_list->AppendColumn(new wxDataViewColumn(label, new TextRenderer(), job_list->GetColumnCount(), width, align, flags));
#else
job_list->AppendTextColumn(label, wxDATAVIEW_CELL_INERT, width, align, flags);
#endif
};
// Note: Keep these in sync with Column
append_text_column(_L("ID"), widths[0]);
job_list->AppendProgressColumn(_L("Progress"), wxDATAVIEW_CELL_INERT, widths[1], wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
append_text_column(_L("Status"),widths[2]);
append_text_column(_L("Host"), widths[3]);
append_text_column(_CTX(L_CONTEXT("Size", "OfFile"), "OfFile"), widths[4]);
append_text_column(_L("Filename"), widths[5]);
append_text_column(_L("Message"), widths[6]);
//append_text_column(_L("Error Message"), -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);
wxGetApp().UpdateDlgDarkUI(this);
wxGetApp().UpdateDVCDarkUI(job_list);
std::vector<int> 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<int> 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<wxVariant> fields;
fields.push_back(wxVariant(wxString::Format("%d", job_list->GetItemCount() + 1)));
fields.push_back(wxVariant(0));
fields.push_back(wxVariant(_L("Queued")));
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(from_path(job.upload_data.upload_path)));
fields.push_back(wxVariant(""));
job_list->AppendItem(fields, static_cast<wxUIntPtr>(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);
}
void PrintHostQueueDialog::on_sys_color_changed()
{
#ifdef _WIN32
wxGetApp().UpdateDlgDarkUI(this);
wxGetApp().UpdateDVCDarkUI(job_list);
#endif
}
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<JobState>(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<wxUIntPtr>(state));
switch (state) {
case ST_NEW: job_list->SetValue(_L("Queued"), 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("Canceling"), idx, COL_STATUS); break;
case ST_CANCELLED: job_list->SetValue(_L("Canceled"), 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, into_u8(nm.GetString()), into_u8(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 = format_wxstr("%1%\n%2%", _L("Error uploading to print host") + ":", evt.status);
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, into_u8(nm.GetString()), into_u8(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, into_u8(nm.GetString()), into_u8(hst.GetString()));
}
void PrintHostQueueDialog::on_info(Event& evt)
{
/*
wxCHECK_RET(evt.job_id < (size_t)job_list->GetItemCount(), "Out of bounds access to job list");
if (evt.tag == L"resolve") {
wxVariant hst(evt.status);
job_list->SetValue(hst, evt.job_id, COL_HOST);
wxGetApp().notification_manager()->set_upload_job_notification_host(evt.job_id + 1, into_u8(evt.status));
} else if (evt.tag == L"complete") {
wxVariant hst(evt.status);
job_list->SetValue(hst, evt.job_id, COL_ERRORMSG);
wxGetApp().notification_manager()->set_upload_job_notification_completed(evt.job_id + 1);
wxGetApp().notification_manager()->set_upload_job_notification_status(evt.job_id + 1, into_u8(evt.status));
} else if(evt.tag == L"complete_with_warning"){
wxVariant hst(evt.status);
job_list->SetValue(hst, evt.job_id, COL_ERRORMSG);
wxGetApp().notification_manager()->set_upload_job_notification_completed_with_warning(evt.job_id + 1);
wxGetApp().notification_manager()->set_upload_job_notification_status(evt.job_id + 1, into_u8(evt.status));
} else if (evt.tag == L"set_complete_off") {
wxGetApp().notification_manager()->set_upload_job_notification_comp_on_100(evt.job_id + 1, false);
}
*/
}
void PrintHostQueueDialog::get_active_jobs(std::vector<std::pair<std::string, std::string>>& 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<JobState>(data);
if(st == JobState::ST_NEW || st == JobState::ST_PROGRESS)
ret.emplace_back(upload_names[i]);
}
}
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<int>& vector)
{
auto* app_config = wxGetApp().app_config;
auto hasget = [app_config](const std::string& name, std::vector<int>& vector)->bool {
if (app_config->has(name)) {
std::string val = app_config->get(name);
if (!val.empty() || val[0]!='\0') {
vector.push_back(std::stoi(val));
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 < 7; i++)
{
if (!hasget("print_host_queue_dialog_column_" + std::to_string(i), vector))
return false;
}
}
return true;
}
ElegooPrintHostSendDialog::ElegooPrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_timeLapse(0)
, m_heatedBedLeveling(0)
, m_BedType(BedType::btPTE)
{}
void ElegooPrintHostSendDialog::init() {
auto preset_bundle = wxGetApp().preset_bundle;
auto model_id = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
if (model_id != "Elegoo-CC" && model_id != "Elegoo-C") {
PrintHostSendDialog::init();
return;
}
const auto& path = m_path;
const auto& storage_paths = m_paths;
const auto& post_actions = m_post_actions;
const auto& storage_names = m_storage_names;
this->SetMinSize(wxSize(500, 300));
const AppConfig* app_config = wxGetApp().app_config;
std::string uploadAndPrint = app_config->get("recent", CONFIG_KEY_UPLOADANDPRINT);
if (!uploadAndPrint.empty())
post_upload_action = static_cast<PrintHostPostUploadAction>(std::stoi(uploadAndPrint));
std::string timeLapse = app_config->get("recent", CONFIG_KEY_TIMELAPSE);
if (!timeLapse.empty())
m_timeLapse = std::stoi(timeLapse);
std::string heatedBedLeveling = app_config->get("recent", CONFIG_KEY_HEATEDBEDLEVELING);
if (!heatedBedLeveling.empty())
m_heatedBedLeveling = std::stoi(heatedBedLeveling);
std::string bedType = app_config->get("recent", CONFIG_KEY_BEDTYPE);
if (!bedType.empty())
m_BedType = static_cast<BedType>(std::stoi(bedType));
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());
wxSizerFlags flags = wxSizerFlags().Border(wxRIGHT, 16).Expand();
content_sizer->Add(txt_filename, flags);
content_sizer->AddSpacer(4);
content_sizer->Add(label_dir_hint);
content_sizer->AddSpacer(VERT_SPACING);
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);
}
if (combo_storage != nullptr) {
// PrusaLink specific: User needs to choose a storage
auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage") + ":");
content_sizer->Add(label_group);
content_sizer->Add(combo_storage, 0, wxBOTTOM, 2 * VERT_SPACING);
combo_storage->SetValue(storage_names.front());
wxString recent_storage = from_u8(app_config->get("recent", CONFIG_KEY_STORAGE));
if (!recent_storage.empty())
combo_storage->SetValue(recent_storage);
} else if (storage_names.GetCount() == 1) {
// PrusaLink specific: Show which storage has been detected.
auto* label_group = new wxStaticText(this, wxID_ANY, _L("Upload to storage") + ": " + storage_names.front());
content_sizer->Add(label_group);
m_preselected_storage = m_paths.front();
}
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);
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this, wxID_APPLY);
checkbox->SetValue(m_switch_to_device_tab);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_switch_to_device_tab = e.IsChecked();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload."), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
}
warning_text = new wxStaticText(this, wxID_ANY,
_L("The selected bed type does not match the file. Please confirm before starting the print."),
wxDefaultPosition, wxDefaultSize, 0);
uploadandprint_sizer = new wxBoxSizer(wxVERTICAL);
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(post_upload_action == PrintHostPostUploadAction::StartPrint);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
if (e.IsChecked()) {
post_upload_action = PrintHostPostUploadAction::StartPrint;
} else {
post_upload_action = PrintHostPostUploadAction::None;
}
refresh();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Upload and Print"), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
}
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(m_timeLapse == 1);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_timeLapse = e.IsChecked() ? 1 : 0;
e.Skip();
});
checkbox_sizer->AddSpacer(16);
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Time-lapse"), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
uploadandprint_sizer->Add(checkbox_sizer);
uploadandprint_sizer->AddSpacer(VERT_SPACING);
}
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(m_heatedBedLeveling == 1);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_heatedBedLeveling = e.IsChecked() ? 1 : 0;
e.Skip();
});
checkbox_sizer->AddSpacer(16);
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Heated Bed Leveling"), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
uploadandprint_sizer->Add(checkbox_sizer);
uploadandprint_sizer->AddSpacer(VERT_SPACING);
}
{
auto radioBoxA = new ::RadioBox(this);
auto radioBoxB = new ::RadioBox(this);
if (m_BedType == BedType::btPC)
radioBoxB->SetValue(true);
else
radioBoxA->SetValue(true);
radioBoxA->Bind(wxEVT_LEFT_DOWN, [this, radioBoxA, radioBoxB](wxMouseEvent& e) {
radioBoxA->SetValue(true);
radioBoxB->SetValue(false);
m_BedType = BedType::btPTE;
refresh();
});
radioBoxB->Bind(wxEVT_LEFT_DOWN, [this, radioBoxA, radioBoxB](wxMouseEvent& e) {
radioBoxA->SetValue(false);
radioBoxB->SetValue(true);
m_BedType = BedType::btPC;
refresh();
});
{
auto radio_sizer = new wxBoxSizer(wxHORIZONTAL);
radio_sizer->AddSpacer(16);
radio_sizer->Add(radioBoxA, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Textured Build Plate (Side A)"), wxDefaultPosition, wxDefaultSize, 0);
radio_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
uploadandprint_sizer->Add(radio_sizer);
uploadandprint_sizer->AddSpacer(VERT_SPACING);
}
{
auto radio_sizer = new wxBoxSizer(wxHORIZONTAL);
radio_sizer->AddSpacer(16);
radio_sizer->Add(radioBoxB, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Smooth Build Plate (Side B)"), wxDefaultPosition, wxDefaultSize, 0);
radio_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
uploadandprint_sizer->Add(radio_sizer);
uploadandprint_sizer->AddSpacer(VERT_SPACING);
}
}
{
auto h_sizer = new wxBoxSizer(wxHORIZONTAL);
warning_text->SetFont(::Label::Body_13);
warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF1001")));
// wrapping the text
warning_text->Wrap(350);
h_sizer->AddSpacer(16);
h_sizer->Add(warning_text);
uploadandprint_sizer->Add(h_sizer);
uploadandprint_sizer->AddSpacer(VERT_SPACING);
}
content_sizer->Add(uploadandprint_sizer);
uploadandprint_sizer->Show(post_upload_action == PrintHostPostUploadAction::StartPrint);
warning_text->Show(post_upload_action == PrintHostPostUploadAction::StartPrint && appBedType() != m_BedType);
uploadandprint_sizer->Layout();
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
// .gcode suffix control
auto validate_path = [this](const wxString& path) -> bool {
if (!path.Lower().EndsWith(m_valid_suffix.Lower())) {
MessageDialog msg_wingow(this,
wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"),
m_valid_suffix),
wxString(SLIC3R_APP_NAME), wxYES | wxNO);
if (msg_wingow.ShowModal() == wxID_NO)
return false;
}
return true;
};
auto* btn_ok = add_button(wxID_OK, true, _L("Upload"));
btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
// post_upload_action = PrintHostPostUploadAction::None;
EndDialog(wxID_OK);
}
});
txt_filename->SetFocus();
add_button(wxID_CANCEL, false, _L("Cancel"));
finalize();
#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->SetInsertionPoint(0);
txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
});
});
}
void ElegooPrintHostSendDialog::EndModal(int ret)
{
if (ret == wxID_OK) {
AppConfig* app_config = wxGetApp().app_config;
app_config->set("recent", CONFIG_KEY_UPLOADANDPRINT, std::to_string(static_cast<int>(post_upload_action)));
app_config->set("recent", CONFIG_KEY_TIMELAPSE, std::to_string(m_timeLapse));
app_config->set("recent", CONFIG_KEY_HEATEDBEDLEVELING, std::to_string(m_heatedBedLeveling));
app_config->set("recent", CONFIG_KEY_BEDTYPE, std::to_string(static_cast<int>(m_BedType)));
}
PrintHostSendDialog::EndModal(ret);
}
BedType ElegooPrintHostSendDialog::appBedType() const
{
std::string str_bed_type = wxGetApp().app_config->get("curr_bed_type");
int bed_type_value = atoi(str_bed_type.c_str());
return static_cast<BedType>(bed_type_value);
}
void ElegooPrintHostSendDialog::refresh()
{
if (uploadandprint_sizer) {
if (post_upload_action == PrintHostPostUploadAction::StartPrint) {
uploadandprint_sizer->Show(true);
} else {
uploadandprint_sizer->Show(false);
}
}
if (warning_text) {
warning_text->Show(post_upload_action == PrintHostPostUploadAction::StartPrint && appBedType() != m_BedType);
}
this->Layout();
this->Fit();
}
CrealityPrintHostSendDialog::CrealityPrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
PrintHost* printhost)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_enableSelfTest(false)
, m_printhost(printhost)
{}
void CrealityPrintHostSendDialog::init()
{
PrintHostSendDialog::init();
auto* creality_host = static_cast<CrealityPrint*>(m_printhost);
bool multi_color;
std::string printer_name;
{
wxBusyCursor wait;
multi_color = creality_host->supports_multi_color_print();
if (multi_color)
printer_name = creality_host->model_name();
}
if (!multi_color)
return;
auto* group_box = new wxStaticBox(this, wxID_ANY,
wxString::Format(_L("Printer: %s"), printer_name));
auto* group_sizer = new wxStaticBoxSizer(group_box, wxVERTICAL);
content_sizer->Add(group_sizer, 0, wxEXPAND);
const AppConfig* app_config = wxGetApp().app_config;
std::string saved = app_config->get("recent", CONFIG_KEY_ENABLESELFTEST);
if (!saved.empty()) {
try { m_enableSelfTest = std::stoi(saved) != 0; } catch (...) {}
}
// Calibration checkbox
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(m_enableSelfTest);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
m_enableSelfTest = e.IsChecked();
AppConfig* ac = wxGetApp().app_config;
ac->set("recent", CONFIG_KEY_ENABLESELFTEST, m_enableSelfTest ? "1" : "0");
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Calibrate before printing"), wxDefaultPosition, wxDefaultSize, 0);
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
group_sizer->Add(checkbox_sizer);
group_sizer->AddSpacer(VERT_SPACING);
}
// --- Color mapping UI ---
// Get gcode filament info from slicer
auto preset_bundle = wxGetApp().preset_bundle;
auto full_config = preset_bundle->full_config();
auto* filament_colors = full_config.option<ConfigOptionStrings>("filament_colour");
auto* filament_types = full_config.option<ConfigOptionStrings>("filament_type");
int gcode_filament_count = filament_colors ? (int)filament_colors->values.size() : 0;
// Query printer for loaded materials
{
wxBusyCursor wait;
std::string boxes_json = creality_host->query_boxes_info();
if (!boxes_json.empty()) {
try {
auto resp = nlohmann::json::parse(boxes_json);
if (resp.contains("boxsInfo") && resp["boxsInfo"].contains("materialBoxs")) {
for (auto& box : resp["boxsInfo"]["materialBoxs"]) {
int box_id = box["id"].get<int>();
int box_type = box.value("type", 0);
// Skip inactive CFS boxes (type 0 with state != 1)
// Spool holder (type 1) is always available
if (box_type == 0 && box.value("state", 0) != 1)
continue;
for (auto& mat : box["materials"]) {
int slot_id = mat["id"].get<int>();
std::string tool_id = "T" + std::to_string(box_id) + std::string(1, 'A' + slot_id);
// Creality uses "#0RRGGBB" (7 hex digits), normalize to "#RRGGBB"
std::string color = mat.value("color", "#FFFFFF");
if (color.size() == 8 && color[0] == '#')
color = "#" + color.substr(2);
m_printer_slots.push_back({
tool_id,
mat.value("type", ""),
color,
box_id,
slot_id
});
}
}
}
} catch (const nlohmann::json::exception& e) {
BOOST_LOG_TRIVIAL(error) << "CrealityPrint dialog: Failed to parse boxsInfo: " << e.what();
}
}
}
if (gcode_filament_count > 0 && !m_printer_slots.empty()) {
auto* label = new wxStaticText(this, wxID_ANY, _L("Filament Mapping:"));
label->SetFont(::Label::Body_13);
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
group_sizer->Add(label);
group_sizer->AddSpacer(4);
for (int i = 0; i < gcode_filament_count; i++) {
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
// Left side: gcode filament color swatch + type
std::string gc_color = (filament_colors && i < (int)filament_colors->values.size())
? filament_colors->values[i] : "#FFFFFF";
std::string gc_type = (filament_types && i < (int)filament_types->values.size())
? filament_types->values[i] : "?";
// Color indicator panel
auto* color_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
color_panel->SetBackgroundColour(wxColour(gc_color));
color_panel->SetMinSize(wxSize(FromDIP(16), FromDIP(16)));
row_sizer->Add(color_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
auto* type_label = new wxStaticText(this, wxID_ANY,
wxString::Format("%d (%s)", i + 1, gc_type.c_str()));
type_label->SetFont(::Label::Body_13);
type_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
type_label->SetMinSize(wxSize(FromDIP(80), -1));
row_sizer->Add(type_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
// Arrow
auto* arrow_label = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
arrow_label->SetFont(::Label::Body_13);
row_sizer->Add(arrow_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
// Right side: dropdown with color icons per slot
int icon_sz = FromDIP(16);
auto* combo = new BitmapComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
for (auto& slot : m_printer_slots) {
wxBitmap* bmp = get_extruder_color_icon(slot.color, "", icon_sz, icon_sz);
wxString label_str;
if (slot.box_id == 0)
label_str = wxString::Format("Ext - %s", slot.type.c_str());
else
label_str = wxString::Format("%s - %s", slot.tool_id.substr(1).c_str(), slot.type.c_str());
combo->Append(label_str, bmp ? *bmp : wxNullBitmap);
}
// Find best default: CFS exact color+type, CFS type-only,
// Ext exact, Ext type-only, else positional
int default_sel = (i < (int)m_printer_slots.size()) ? i : 0;
bool matched = false;
for (int pass = 0; pass < 4 && !matched; pass++) {
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
bool is_ext = (m_printer_slots[s].box_id == 0);
bool type_match = (m_printer_slots[s].type == gc_type);
bool color_match = (wxColour(m_printer_slots[s].color) == wxColour(gc_color));
bool hit = false;
switch (pass) {
case 0: hit = !is_ext && type_match && color_match; break;
case 1: hit = !is_ext && type_match; break;
case 2: hit = is_ext && type_match && color_match; break;
case 3: hit = is_ext && type_match; break;
}
if (hit) {
default_sel = s;
matched = true;
break;
}
}
}
combo->SetSelection(default_sel);
row_sizer->Add(combo, 0, wxALIGN_CENTER_VERTICAL);
group_sizer->Add(row_sizer);
group_sizer->AddSpacer(4);
m_slot_combos.push_back(combo);
}
int ext_slot_idx = -1;
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
if (m_printer_slots[s].box_id == 0) {
ext_slot_idx = s;
break;
}
}
if (ext_slot_idx >= 0) {
for (int ci = 0; ci < (int)m_slot_combos.size(); ci++) {
int sel = m_slot_combos[ci]->GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
m_printer_slots[sel].box_id == 0) {
for (int cj = 0; cj < (int)m_slot_combos.size(); cj++) {
if (cj != ci)
m_slot_combos[cj]->Enable(false);
}
break;
}
}
for (auto* c : m_slot_combos) {
c->Bind(wxEVT_COMBOBOX, [this, ext_slot_idx](wxCommandEvent& e) {
int sel = e.GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
m_printer_slots[sel].box_id == 0) {
for (auto* c2 : m_slot_combos) {
if (c2 != e.GetEventObject())
c2->Enable(false);
}
} else {
for (auto* c2 : m_slot_combos)
c2->Enable(true);
}
e.Skip();
});
}
}
}
this->Layout();
this->Fit();
}
std::map<std::string, std::string> CrealityPrintHostSendDialog::extendedInfo() const
{
std::map<std::string, std::string> info;
info["enableSelfTest"] = m_enableSelfTest ? "1" : "0";
// Color mapping: colorMatch_0, colorMatch_1, ... tab-delimited
for (int i = 0; i < (int)m_slot_combos.size(); i++) {
int sel = m_slot_combos[i]->GetSelection();
if (sel >= 0 && sel < (int)m_printer_slots.size()) {
auto& slot = m_printer_slots[sel];
// id = gcode tool index (T1A for first filament, T1B for second, ...),
// not the destination CFS slot — firmware matches by gcode tool.
std::string gcode_tool = "T1" + std::string(1, 'A' + i);
info["colorMatch_" + std::to_string(i)] =
gcode_tool + "\t" + slot.type + "\t" + slot.color + "\t" +
std::to_string(slot.box_id) + "\t" + std::to_string(slot.material_id);
}
}
return info;
}
}}