Enhance mixed filament management: Introduce manual pattern support for mixed filaments, allowing users to define custom layer patterns using '1' and '2' for component selection. Update parsing logic to accommodate new pattern definitions and ensure backward compatibility. Improve GUI to facilitate pattern entry and display, enhancing user experience in mixed filament configurations.

This commit is contained in:
Rad
2026-02-11 16:55:59 +01:00
parent 2d6ec834ec
commit 0178ad32ad
3 changed files with 206 additions and 38 deletions

View File

@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <sstream> #include <sstream>
@@ -261,35 +262,100 @@ static bool parse_row_definition(const std::string &row,
unsigned int &b, unsigned int &b,
bool &enabled, bool &enabled,
bool &custom, bool &custom,
int &mix_b_percent) int &mix_b_percent,
std::string &manual_pattern)
{ {
std::vector<int> values; auto trim_copy = [](const std::string &s) {
std::stringstream ss(row); size_t lo = 0;
std::string token; size_t hi = s.size();
while (std::getline(ss, token, ',')) { while (lo < hi && std::isspace(static_cast<unsigned char>(s[lo])))
if (token.empty()) ++lo;
while (hi > lo && std::isspace(static_cast<unsigned char>(s[hi - 1])))
--hi;
return s.substr(lo, hi - lo);
};
auto parse_int_token = [&trim_copy](const std::string &tok, int &out) {
const std::string t = trim_copy(tok);
if (t.empty())
return false; return false;
try { try {
values.push_back(std::stoi(token)); size_t consumed = 0;
int v = std::stoi(t, &consumed);
if (consumed != t.size())
return false;
out = v;
return true;
} catch (...) { } catch (...) {
return false; return false;
} }
} };
if (values.size() != 4 && values.size() != 5) std::vector<std::string> tokens;
std::stringstream ss(row);
std::string token;
while (std::getline(ss, token, ','))
tokens.emplace_back(trim_copy(token));
if (tokens.size() < 4 || tokens.size() > 6)
return false; return false;
int values[5] = { 0, 0, 1, 1, 50 };
if (tokens.size() == 4) {
// Legacy: a,b,enabled,mix
if (!parse_int_token(tokens[0], values[0]) ||
!parse_int_token(tokens[1], values[1]) ||
!parse_int_token(tokens[2], values[2]) ||
!parse_int_token(tokens[3], values[4]))
return false;
} else {
// Current: a,b,enabled,custom,mix[,pattern]
for (size_t i = 0; i < 5; ++i)
if (!parse_int_token(tokens[i], values[i]))
return false;
}
if (values[0] <= 0 || values[1] <= 0) if (values[0] <= 0 || values[1] <= 0)
return false; return false;
a = unsigned(values[0]); a = unsigned(values[0]);
b = unsigned(values[1]); b = unsigned(values[1]);
enabled = (values[2] != 0); enabled = (values[2] != 0);
custom = (values.size() == 5) ? (values[3] != 0) : true; custom = (tokens.size() == 4) ? true : (values[3] != 0);
mix_b_percent = clamp_int(values.size() == 5 ? values[4] : values[3], 0, 100); mix_b_percent = clamp_int(values[4], 0, 100);
manual_pattern = (tokens.size() == 6) ? tokens[5] : std::string();
return true; return true;
} }
static bool is_pattern_separator(char c)
{
return std::isspace(static_cast<unsigned char>(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';';
}
static bool decode_pattern_step(char c, char &out)
{
switch (std::tolower(static_cast<unsigned char>(c))) {
case '1':
case 'a':
out = '1';
return true;
case '2':
case 'b':
out = '2';
return true;
default:
return false;
}
}
static int mix_percent_from_normalized_pattern(const std::string &pattern)
{
if (pattern.empty())
return 50;
const int count_b = int(std::count(pattern.begin(), pattern.end(), '2'));
return clamp_int(int(std::lround(100.0 * double(count_b) / double(pattern.size()))), 0, 100);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MixedFilamentManager // MixedFilamentManager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -388,6 +454,7 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a,
mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100); mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100);
mf.ratio_a = 1; mf.ratio_a = 1;
mf.ratio_b = 1; mf.ratio_b = 1;
mf.manual_pattern.clear();
mf.enabled = true; mf.enabled = true;
mf.custom = true; mf.custom = true;
m_mixed.push_back(std::move(mf)); m_mixed.push_back(std::move(mf));
@@ -399,6 +466,24 @@ void MixedFilamentManager::clear_custom_entries()
m_mixed.erase(std::remove_if(m_mixed.begin(), m_mixed.end(), [](const MixedFilament &mf) { return mf.custom; }), m_mixed.end()); m_mixed.erase(std::remove_if(m_mixed.begin(), m_mixed.end(), [](const MixedFilament &mf) { return mf.custom; }), m_mixed.end());
} }
std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern)
{
std::string normalized;
normalized.reserve(pattern.size());
for (char c : pattern) {
char step = '\0';
if (decode_pattern_step(c, step)) {
normalized.push_back(step);
continue;
}
if (is_pattern_separator(c))
continue;
// Unknown token => invalid pattern.
return std::string();
}
return normalized;
}
void MixedFilamentManager::apply_gradient_settings(int gradient_mode, void MixedFilamentManager::apply_gradient_settings(int gradient_mode,
float lower_bound, float lower_bound,
float upper_bound, float upper_bound,
@@ -434,6 +519,9 @@ std::string MixedFilamentManager::serialize_custom_entries() const
<< (mf.enabled ? 1 : 0) << ',' << (mf.enabled ? 1 : 0) << ','
<< (mf.custom ? 1 : 0) << ',' << (mf.custom ? 1 : 0) << ','
<< clamp_int(mf.mix_b_percent, 0, 100); << clamp_int(mf.mix_b_percent, 0, 100);
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!normalized_pattern.empty())
ss << ',' << normalized_pattern;
} }
return ss.str(); return ss.str();
} }
@@ -464,7 +552,8 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
bool enabled = true; bool enabled = true;
bool custom = true; bool custom = true;
int mix = 50; int mix = 50;
if (!parse_row_definition(row, a, b, enabled, custom, mix)) { std::string manual_pattern;
if (!parse_row_definition(row, a, b, enabled, custom, mix, manual_pattern)) {
++skipped_rows; ++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row; BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row;
continue; continue;
@@ -485,7 +574,8 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
}); });
if (it_auto != m_mixed.end()) { if (it_auto != m_mixed.end()) {
it_auto->enabled = enabled; it_auto->enabled = enabled;
it_auto->mix_b_percent = mix; it_auto->manual_pattern = normalize_manual_pattern(manual_pattern);
it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern);
++updated_auto; ++updated_auto;
continue; continue;
} }
@@ -497,6 +587,9 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
mf.mix_b_percent = mix; mf.mix_b_percent = mix;
mf.ratio_a = 1; mf.ratio_a = 1;
mf.ratio_b = 1; mf.ratio_b = 1;
mf.manual_pattern = normalize_manual_pattern(manual_pattern);
if (!mf.manual_pattern.empty())
mf.mix_b_percent = mix_percent_from_normalized_pattern(mf.manual_pattern);
mf.enabled = enabled; mf.enabled = enabled;
mf.custom = custom; mf.custom = custom;
m_mixed.push_back(std::move(mf)); m_mixed.push_back(std::move(mf));
@@ -528,6 +621,13 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
const MixedFilament &mf = m_mixed[idx]; const MixedFilament &mf = m_mixed[idx];
// Manual pattern takes precedence when provided. Pattern uses repeating
// steps: '1' => component_a, '2' => component_b.
if (!mf.manual_pattern.empty()) {
const int pos = safe_mod(layer_index, int(mf.manual_pattern.size()));
return mf.manual_pattern[size_t(pos)] == '2' ? mf.component_b : mf.component_a;
}
// Height-weighted cadence can be forced by the local-Z planner. The // Height-weighted cadence can be forced by the local-Z planner. The
// regular gradient height mode keeps historical behavior (custom rows). // regular gradient height mode keeps historical behavior (custom rows).
const bool use_height_weighted = force_height_weighted || (m_gradient_mode == 1 && mf.custom); const bool use_height_weighted = force_height_weighted || (m_gradient_mode == 1 && mf.custom);

View File

@@ -26,6 +26,11 @@ struct MixedFilament
// Blend percentage of component B in [0..100]. // Blend percentage of component B in [0..100].
int mix_b_percent = 50; int mix_b_percent = 50;
// Optional manual layer pattern for this mixed filament, encoded as a
// string of '1' and '2'. '1' means component_a, '2' means component_b.
// Example: "11112222" => AAAABBBB repeating.
std::string manual_pattern;
// Whether this mixed filament is enabled (available for assignment). // Whether this mixed filament is enabled (available for assignment).
bool enabled = true; bool enabled = true;
@@ -42,6 +47,7 @@ struct MixedFilament
ratio_a == rhs.ratio_a && ratio_a == rhs.ratio_a &&
ratio_b == rhs.ratio_b && ratio_b == rhs.ratio_b &&
mix_b_percent == rhs.mix_b_percent && mix_b_percent == rhs.mix_b_percent &&
manual_pattern == rhs.manual_pattern &&
enabled == rhs.enabled && enabled == rhs.enabled &&
custom == rhs.custom; custom == rhs.custom;
} }
@@ -93,6 +99,10 @@ public:
std::string serialize_custom_entries() const; std::string serialize_custom_entries() const;
void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours); void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours);
// Normalize a manual mixed-pattern string into compact '1'/'2' form.
// Accepts separators and A/B aliases. Returns empty string if invalid.
static std::string normalize_manual_pattern(const std::string &pattern);
// ---- Queries -------------------------------------------------------- // ---- Queries --------------------------------------------------------
// True when `filament_id` (1-based) refers to a mixed filament. // True when `filament_id` (1-based) refers to a mixed filament.

View File

@@ -2489,21 +2489,28 @@ void Sidebar::update_mixed_filament_panel()
p->m_sizer_mixed_filaments->Clear(true); p->m_sizer_mixed_filaments->Clear(true);
// Header with title and add button. // Header with title and add buttons (gradient / pattern).
auto *header_row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY); auto *header_row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY);
header_row->SetBackgroundColour(wxColour(255, 255, 255)); header_row->SetBackgroundColour(wxColour(255, 255, 255));
auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); auto *header_sizer = new wxBoxSizer(wxHORIZONTAL);
auto *mixed_title = new wxStaticText(header_row, wxID_ANY, _L("Mixed Filaments")); auto *mixed_title = new wxStaticText(header_row, wxID_ANY, _L("Mixed Filaments"));
mixed_title->SetFont(Label::Head_14); mixed_title->SetFont(Label::Head_14);
header_sizer->Add(mixed_title, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); header_sizer->Add(mixed_title, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
auto *add_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)));
add_btn->SetToolTip(_L("Add custom mixed filament")); auto add_custom_row = [this, preset_bundle, physical_colors, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool pattern_row) {
add_btn->Enable(num_physical >= 2);
add_btn->Bind(wxEVT_BUTTON, [this, preset_bundle, physical_colors, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](wxCommandEvent &) {
if (physical_colors.size() < 2) if (physical_colors.size() < 2)
return; return;
auto &mgr = preset_bundle->mixed_filaments; auto &mgr = preset_bundle->mixed_filaments;
mgr.add_custom_filament(1, 2, 50, physical_colors); mgr.add_custom_filament(1, 2, 50, physical_colors);
if (!mgr.mixed_filaments().empty()) {
MixedFilament &row = mgr.mixed_filaments().back();
if (pattern_row) {
row.manual_pattern = "12";
row.mix_b_percent = 50;
} else {
row.manual_pattern.clear();
}
}
int mode = get_mixed_mode(false) ? 1 : 0; int mode = get_mixed_mode(false) ? 1 : 0;
float lo = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); float lo = get_mixed_float("mixed_filament_height_lower_bound", 0.04f);
@@ -2523,8 +2530,24 @@ void Sidebar::update_mixed_filament_panel()
update_dynamic_filament_list(); update_dynamic_filament_list();
update_mixed_filament_panel(); update_mixed_filament_panel();
}); });
}); };
header_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
auto *grad_label = new wxStaticText(header_row, wxID_ANY, _L("Gradient"));
header_sizer->Add(grad_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
auto *add_gradient_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)));
add_gradient_btn->SetToolTip(_L("Add custom gradient mixed filament"));
add_gradient_btn->Enable(num_physical >= 2);
add_gradient_btn->Bind(wxEVT_BUTTON, [add_custom_row](wxCommandEvent &) { add_custom_row(false); });
header_sizer->Add(add_gradient_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
auto *pattern_label = new wxStaticText(header_row, wxID_ANY, _L("Pattern"));
header_sizer->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
auto *add_pattern_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)));
add_pattern_btn->SetToolTip(_L("Add custom pattern mixed filament"));
add_pattern_btn->Enable(num_physical >= 2);
add_pattern_btn->Bind(wxEVT_BUTTON, [add_custom_row](wxCommandEvent &) { add_custom_row(true); });
header_sizer->Add(add_pattern_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
header_row->SetSizer(header_sizer); header_row->SetSizer(header_sizer);
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2));
p->m_sizer_mixed_filaments->Add(header_row, 0, wxEXPAND); p->m_sizer_mixed_filaments->Add(header_row, 0, wxEXPAND);
@@ -2648,22 +2671,38 @@ void Sidebar::update_mixed_filament_panel()
picker_row->Add(choice_b, 1, wxALIGN_CENTER_VERTICAL); picker_row->Add(choice_b, 1, wxALIGN_CENTER_VERTICAL);
content_sizer->Add(picker_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); content_sizer->Add(picker_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8));
wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]); const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern);
wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]); const bool pattern_row_mode = !normalized_pattern.empty();
auto *blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100)); MixedGradientSelector *blend_selector = nullptr;
auto *blend_row = new wxBoxSizer(wxHORIZONTAL); wxStaticText *blend_label = nullptr;
blend_row->Add(blend_selector, 1, wxEXPAND); wxTextCtrl *pattern_ctrl = nullptr;
content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8));
auto *blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%", if (pattern_row_mode) {
100 - std::clamp(mf.mix_b_percent, 0, 100), auto *pattern_row = new wxBoxSizer(wxHORIZONTAL);
std::clamp(mf.mix_b_percent, 0, 100))); pattern_row->Add(new wxStaticText(row, wxID_ANY, _L("Pattern")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); pattern_ctrl = new wxTextCtrl(row, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition,
ratio_row->AddStretchSpacer(1); wxSize(FromDIP(170), -1), wxTE_PROCESS_ENTER);
ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL); pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B, example: 1/1/1/1/2/2/2/2."));
content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); pattern_row->Add(pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL);
content_sizer->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8));
} else {
wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]);
wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]);
blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100));
auto *blend_row = new wxBoxSizer(wxHORIZONTAL);
blend_row->Add(blend_selector, 1, wxEXPAND);
content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8));
auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, swatch, num_physical, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) { blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%",
100 - std::clamp(mf.mix_b_percent, 0, 100),
std::clamp(mf.mix_b_percent, 0, 100)));
auto *ratio_row = new wxBoxSizer(wxHORIZONTAL);
ratio_row->AddStretchSpacer(1);
ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL);
content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8));
}
auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, pattern_ctrl, swatch, num_physical, pattern_row_mode, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) {
if (num_physical < 1) if (num_physical < 1)
return; return;
@@ -2682,7 +2721,19 @@ void Sidebar::update_mixed_filament_panel()
MixedFilament &cur = mfs[mixed_id]; MixedFilament &cur = mfs[mixed_id];
cur.component_a = unsigned(a); cur.component_a = unsigned(a);
cur.component_b = unsigned(b); cur.component_b = unsigned(b);
cur.mix_b_percent = std::clamp(blend_selector->value(), 0, 100); if (pattern_row_mode) {
std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(pattern_ctrl->GetValue()));
if (normalized.empty())
normalized = "12";
if (into_u8(pattern_ctrl->GetValue()) != normalized)
pattern_ctrl->ChangeValue(from_u8(normalized));
cur.manual_pattern = normalized;
const int count_b = int(std::count(normalized.begin(), normalized.end(), '2'));
cur.mix_b_percent = std::clamp((100 * count_b + int(normalized.size()) / 2) / std::max(1, int(normalized.size())), 0, 100);
} else {
cur.mix_b_percent = std::clamp(blend_selector ? blend_selector->value() : 50, 0, 100);
cur.manual_pattern.clear();
}
cur.custom = true; cur.custom = true;
ConfigOptionStrings *co = preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour"); ConfigOptionStrings *co = preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour");
@@ -2691,10 +2742,12 @@ void Sidebar::update_mixed_filament_panel()
wxColour color_a_wx = parse_mixed_color(colors[size_t(a - 1)]); wxColour color_a_wx = parse_mixed_color(colors[size_t(a - 1)]);
wxColour color_b_wx = parse_mixed_color(colors[size_t(b - 1)]); wxColour color_b_wx = parse_mixed_color(colors[size_t(b - 1)]);
blend_selector->set_colors(color_a_wx, color_b_wx); if (blend_selector)
blend_selector->set_colors(color_a_wx, color_b_wx);
cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent); cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent);
blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent)); if (blend_label)
blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent));
swatch->SetBackgroundColour(wxColour(cur.display_color)); swatch->SetBackgroundColour(wxColour(cur.display_color));
swatch->Refresh(); swatch->Refresh();
@@ -2724,7 +2777,12 @@ void Sidebar::update_mixed_filament_panel()
choice_a->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); choice_a->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); });
choice_b->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); choice_b->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); });
blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); }); if (blend_selector)
blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); });
if (pattern_ctrl) {
pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); });
pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_custom_row](wxFocusEvent &evt) { apply_custom_row(true); evt.Skip(); });
}
} }
row_sizer->Add(content_sizer, 1, wxEXPAND); row_sizer->Add(content_sizer, 1, wxEXPAND);