mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
* build: enable /Zc:lambda for MSVC MSVC keeps its legacy lambda processor under /std:c++17, which rejects reading a constexpr constant inside a lambda that does not capture it (C3493). No other compiler requires that capture, and clang reports it as an unused one, so the two cannot both be satisfied without the flag. /Zc:lambda selects the conforming lambda parser that clang and GCC already use. It is implied by /std:c++20 and /permissive-, so it is only needed while we are on C++17. clang-cl is conforming already and does not take the flag. It requires VS2019 16.8, so build_release_vs.bat now says 16.8+. * build: clear 237 unused lambda capture warnings 236 captures across 81 files, 142 of them `this`. Removing an unused capture changes no behavior; clang does not report a capture whose type has a non-trivial destructor, so nothing held only to extend an object's lifetime is in this set. Nine of them are the second half of the warning, "is not required to be captured for this use", where the capture is a const or constexpr value the body does read. Those depend on the /Zc:lambda change in the previous commit. One of them, in FillRectilinear.cpp, had been worked around with an #ifndef __APPLE__ guard around the capture list, which is now gone. GUI_ObjectTableSettings.cpp captured its reset button only to read it inside #ifdef __WXOSX_MAC__. That branch now takes the button from the event it is already handling. * build: fail configure on MSVC older than 19.28 instead of dropping /Zc:lambda cl.exe answers an unrecognized /Zc: sub-option with warning D9002 and keeps going, so on VS2019 before 16.8 the flag is silently ignored and the build instead dies with C3493 in FillRectilinear.cpp, nowhere near the cause. * fix: delete three locals that are now unused Their only remaining use was the lambda capture this branch removed. The Clang builds set -Wno-unused-variable, so the build never flagged them. --------- Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2110 lines
85 KiB
C++
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(_L_CONTEXT(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](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;
|
|
}
|
|
|
|
}}
|