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.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
#include "PrintHostDialogs.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
|
||||
#include <wx/frame.h>
|
||||
#include <wx/progdlg.h>
|
||||
@@ -11,6 +13,9 @@
|
||||
#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>
|
||||
@@ -18,6 +23,8 @@
|
||||
#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"
|
||||
@@ -35,10 +42,383 @@
|
||||
#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";
|
||||
@@ -262,6 +642,528 @@ void PrintHostSendDialog::EndModal(int ret)
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user