From d499fbabbe5b2d4aa27946e73aa949861b51a1ea Mon Sep 17 00:00:00 2001 From: Rad Date: Wed, 18 Feb 2026 01:10:27 +0100 Subject: [PATCH] Implement deleted state for mixed filaments: Add a 'deleted' flag to the MixedFilament structure and update related parsing, serialization, and management functions to handle the visibility of mixed filaments in the UI. Adjust logic in the MixedFilamentManager to ensure proper handling of enabled states based on the deleted flag. --- CHANGELOG_v0.9.md | 79 +++ src/libslic3r/MixedFilament.cpp | 33 +- src/libslic3r/MixedFilament.hpp | 4 + src/slic3r/GUI/3DScene.cpp | 18 +- src/slic3r/GUI/GUI_Factories.cpp | 56 ++- src/slic3r/GUI/Plater.cpp | 812 ++++++++++++++++++++++--------- 6 files changed, 747 insertions(+), 255 deletions(-) create mode 100644 CHANGELOG_v0.9.md diff --git a/CHANGELOG_v0.9.md b/CHANGELOG_v0.9.md new file mode 100644 index 0000000000..b882992d09 --- /dev/null +++ b/CHANGELOG_v0.9.md @@ -0,0 +1,79 @@ +## v0.9 Pre-release + +EXPERIMENTAL BUILD - LIMITED TESTING +Based on Snapmaker Orca v2.2.4 + +v0.9 full spectrum pre-release focuses on mixed-filament workflow UX and stability. + +### What's New in v0.9 + +#### Mixed Filaments UI/UX Overhaul (v0.9) +- Replaced the blocking mixed-filament popup with an inline expand/retract editor. +- Added row hover feedback and click-anywhere row activation for custom mixed filaments. +- Added dynamic mixed-filament panel resizing with an expansion cap sized for up to two expanded rows. +- Simplified editor flow to `Simple` mode only (mode selector removed from the inline editor). +- Moved gradient selector inline with filament selectors for faster editing. +- Reworked filament selectors into compact color swatch pickers with `F1/F2/...` labels. +- Simplified pattern editing: + - removed insert dropdown/button flow, + - kept direct filament buttons for appending pattern tokens. +- Moved preview labels into the preview bar itself (`Preview` + ratio overlay), with outlined text for readability. + +#### Mixed Filaments Behavior and Reliability (v0.9) +- Automatic mixed filaments are now read-only in the inline settings editor. +- Automatic mixed filaments can now be deleted from the UI. +- Added persistent deleted-state storage for mixed filament rows so deleted auto rows stay deleted after refresh/restart. +- Excluded deleted rows from enabled/displayed virtual filament mapping. +- Improved change propagation so mixed-filament edits correctly mark config/project dirty states. +- Addressed mixed-filament editor collapse/refresh stability issues. + +#### Local Z Dithering and Prime Tower Handling (v0.9) +- Enhanced Local-Z phase-b tool change handling when wipe/prime tower is enabled. +- Added an unplanned Local-Z tool-change path in wipe tower integration to emit proper tool-change/wipe G-code outside the preplanned per-layer sequence. +- Enabled Local-Z phase-b execution with wipe tower active (still blocked when wiping overrides are active). +- Restored pre-pass extruder state after Local-Z phase-b so subsequent wipe/prime tower planning remains synchronized. +- Improved Local-Z + wipe tower diagnostics/fallback logging for troubleshooting. + +#### Dark Mode and Visual Consistency (v0.9) +- Added dark-mode-aware styling to mixed-filament rows and inline editor controls. +- Fixed dark-mode text contrast for mixed-filament row labels and controls. +- Updated gradient/preview border handling for better dark-mode contrast. + +### Installation + +#### Windows +1. Download `Snapmaker_Orca.zip`. +2. Extract to a folder. +3. Run the executable. + +#### macOS +1. Download the macOS build (`arm64` for Apple Silicon or `x86_64` for Intel). +2. If the release asset is a `.zip`, unzip it first. +3. Open the `.dmg`. +4. Drag `Snapmaker_Orca.app` into `Applications`. +5. Launch the app from `Applications`. + +#### Linux (AppImage) +1. Download `Snapmaker_Orca_Linux_V2.2.4.AppImage`. +2. Run `chmod +x Snapmaker_Orca_Linux_V2.2.4.AppImage`. +3. Run `./Snapmaker_Orca_Linux_V2.2.4.AppImage`. + +### Warning +- Use at your own risk. +- May produce incorrect G-code in edge cases. +- Mixed-filament behavior is still experimental in some scenarios. +- This release has had limited real-printer validation. + +### Features Not Yet Fully Tested +1. Mixed-filament editor behavior on all platform/theme combinations (Windows/macOS/Linux). +2. Dark-mode visual consistency across all desktop environments. +3. Mixed-filament preview/readability with all localization strings and scaling factors. + +### Known Issues +- On-screen color blend preview may not exactly match physical print results. +- Some UI spacing/alignment may vary by OS and system font rendering. +- Older Linux distributions may fail to run this AppImage due to glibc mismatch. + +### Credits +- FilamentMixer color blending integration is powered by the FilamentMixer library by [justinh-rahb](https://github.com/justinh-rahb). +- Library repository: [https://github.com/justinh-rahb/filament-mixer](https://github.com/justinh-rahb/filament-mixer). diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index a6ed6db91c..9edf39302f 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -308,7 +308,8 @@ static bool parse_row_definition(const std::string &row, std::string &gradient_component_ids, std::string &gradient_component_weights, std::string &manual_pattern, - int &distribution_mode) + int &distribution_mode, + bool &deleted) { auto trim_copy = [](const std::string &s) { size_t lo = 0; @@ -373,6 +374,7 @@ static bool parse_row_definition(const std::string &row, gradient_component_weights.clear(); manual_pattern.clear(); distribution_mode = int(MixedFilament::Simple); + deleted = false; size_t token_idx = 5; if (tokens.size() >= 6) { @@ -410,6 +412,12 @@ static bool parse_row_definition(const std::string &row, distribution_mode = clamp_int(parsed_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)); continue; } + if (tok[0] == 'd' || tok[0] == 'D') { + int parsed_deleted = deleted ? 1 : 0; + if (parse_int_token(tok.substr(1), parsed_deleted)) + deleted = parsed_deleted != 0; + continue; + } manual_pattern = tok; } @@ -686,6 +694,7 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen mf.ratio_b = 1; mf.mix_b_percent = 50; mf.enabled = true; + mf.deleted = false; mf.custom = false; // Try to preserve previous settings. @@ -694,6 +703,9 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen prev.component_a == mf.component_a && prev.component_b == mf.component_b) { mf.enabled = prev.enabled; + mf.deleted = prev.deleted; + if (mf.deleted) + mf.enabled = false; break; } } @@ -755,6 +767,7 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a, mf.pointillism_all_filaments = false; mf.distribution_mode = int(MixedFilament::Simple); mf.enabled = true; + mf.deleted = false; mf.custom = true; m_mixed.push_back(std::move(mf)); refresh_display_colors(filament_colours); @@ -823,7 +836,8 @@ std::string MixedFilamentManager::serialize_custom_entries() const << (mf.pointillism_all_filaments ? 1 : 0) << ',' << 'g' << normalized_ids << ',' << 'w' << normalized_weights << ',' - << 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)); + << 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)) << ',' + << 'd' << (mf.deleted ? 1 : 0); const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); if (!normalized_pattern.empty()) ss << ',' << normalized_pattern; @@ -862,8 +876,9 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co std::string gradient_component_weights; std::string manual_pattern; int distribution_mode = int(MixedFilament::Simple); + bool deleted = false; if (!parse_row_definition(row, a, b, enabled, custom, mix, pointillism_all_filaments, - gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode)) { + gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode, deleted)) { ++skipped_rows; BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row; continue; @@ -891,6 +906,9 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co it_auto->manual_pattern = normalize_manual_pattern(manual_pattern); it_auto->distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)); it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern); + it_auto->deleted = deleted; + if (it_auto->deleted) + it_auto->enabled = false; ++updated_auto; continue; } @@ -911,6 +929,9 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co if (!mf.manual_pattern.empty()) mf.mix_b_percent = mix_percent_from_normalized_pattern(mf.manual_pattern); mf.enabled = enabled; + mf.deleted = deleted; + if (mf.deleted) + mf.enabled = false; mf.custom = custom; m_mixed.push_back(std::move(mf)); ++loaded_rows; @@ -1005,7 +1026,7 @@ int MixedFilamentManager::mixed_index_from_filament_id(unsigned int filament_id, const size_t enabled_virtual_idx = size_t(filament_id - num_physical - 1); size_t enabled_seen = 0; for (size_t i = 0; i < m_mixed.size(); ++i) { - if (!m_mixed[i].enabled) + if (!m_mixed[i].enabled || m_mixed[i].deleted) continue; if (enabled_seen == enabled_virtual_idx) return int(i); @@ -1145,7 +1166,7 @@ size_t MixedFilamentManager::enabled_count() const { size_t count = 0; for (const auto &mf : m_mixed) - if (mf.enabled) + if (mf.enabled && !mf.deleted) ++count; return count; } @@ -1154,7 +1175,7 @@ std::vector MixedFilamentManager::display_colors() const { std::vector colors; for (const auto &mf : m_mixed) - if (mf.enabled) + if (mf.enabled && !mf.deleted) colors.push_back(mf.display_color); return colors; } diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index febc3c6b7b..4c6a75cc67 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -59,6 +59,9 @@ struct MixedFilament // Whether this mixed filament is enabled (available for assignment). bool enabled = true; + // True when this mixed filament row was deleted from UI and should stay hidden. + bool deleted = false; + // True when this row was user-created (custom) instead of auto-generated. bool custom = false; @@ -78,6 +81,7 @@ struct MixedFilament pointillism_all_filaments == rhs.pointillism_all_filaments && distribution_mode == rhs.distribution_mode && enabled == rhs.enabled && + deleted == rhs.deleted && custom == rhs.custom; } bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); } diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index d888490973..d10671c047 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1219,14 +1219,22 @@ void GLVolumeCollection::update_colors_by_extruder(const DynamicPrintConfig* con if (filamemts_opt == nullptr) return; - size_t colors_count = (size_t) filamemts_opt->values.size(); - if (colors_count == 0) + std::vector filament_colors = filamemts_opt->values; + if (filament_colors.empty()) return; - colors.resize(colors_count); - for (unsigned int i = 0; i < colors_count; ++i) { + // Include enabled mixed (virtual) filament colors so volume extruder IDs + // assigned to mixed rows render correctly in Prepare view. + if (GUI::wxGetApp().preset_bundle != nullptr) { + const auto mixed_colors = GUI::wxGetApp().preset_bundle->mixed_filaments.display_colors(); + filament_colors.insert(filament_colors.end(), mixed_colors.begin(), mixed_colors.end()); + } + + colors.resize(filament_colors.size()); + + for (size_t i = 0; i < filament_colors.size(); ++i) { ColorRGBA rgba; - const std::string& fil_color = config->opt_string("filament_colour", i); + const std::string& fil_color = filament_colors[i]; if (decode_color(fil_color, rgba)) colors[i] = {fil_color, rgba}; } diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 7d220e0ad0..c46047dce0 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -36,9 +36,41 @@ static PrinterTechnology printer_technology() return wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology(); } +static int physical_filaments_count() +{ + return std::max(wxGetApp().filaments_cnt(), 0); +} + static int filaments_count() { - return wxGetApp().filaments_cnt(); + if (wxGetApp().preset_bundle == nullptr) + return 0; + const int physical = physical_filaments_count(); + const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments; + return static_cast(mixed_mgr.total_filaments(size_t(physical))); +} + +static wxString filament_menu_item_name(const int filament_id_1based) +{ + if (filament_id_1based <= 0) + return _L("Default"); + + if (wxGetApp().preset_bundle == nullptr) + return wxString::Format(_L("Filament %d"), filament_id_1based); + + const int physical = physical_filaments_count(); + if (filament_id_1based <= physical) { + const size_t preset_idx = size_t(filament_id_1based - 1); + const auto &filament_presets = wxGetApp().preset_bundle->filament_presets; + if (preset_idx < filament_presets.size()) { + auto preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[preset_idx]); + if (preset != nullptr) + return from_u8(preset->label(false)); + } + return wxString::Format(_L("Filament %d"), filament_id_1based); + } + + return wxString::Format(_L("Mixed Filament %d"), filament_id_1based); } static bool is_improper_category(const std::string& category, const int filaments_cnt, const bool is_object_settings = true) @@ -905,16 +937,7 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu) bool is_active_extruder = i == initial_extruder; int icon_idx = i == 0 ? 0 : i - 1; - wxString item_name = _L("Default"); - - if (i > 0) { - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]); - if (preset == nullptr) { - item_name = wxString::Format(_L("Filament %d"), i); - } else { - item_name = from_u8(preset->label(false)); - } - } + wxString item_name = filament_menu_item_name(i); if (is_active_extruder) { item_name << " (" + _L("current") + ")"; @@ -1994,16 +2017,7 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu) //bool is_active_extruder = i == initial_extruder; bool is_active_extruder = false; - wxString item_name = _L("Default"); - - if (i > 0) { - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]); - if (preset == nullptr) { - item_name = wxString::Format(_L("Filament %d"), i); - } else { - item_name = from_u8(preset->label(false)); - } - } + wxString item_name = filament_menu_item_name(i); if (is_active_extruder) { item_name << " (" + _L("current") + ")"; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 303b445f76..b0c2152336 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -656,6 +657,7 @@ struct Sidebar::priv Button* m_btn_add_pattern = nullptr; // Add pattern button Button* m_btn_toggle_mixed_filaments = nullptr; // Collapse/expand toggle button bool m_mixed_filaments_collapsed = false; // Collapse state + std::unordered_set m_expanded_mixed_filament_rows; // Expanded row editors wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; @@ -1527,7 +1529,10 @@ Sidebar::Sidebar(Plater *parent) // Create content panel (collapsible) p->m_panel_mixed_filaments_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - p->m_panel_mixed_filaments_content->SetBackgroundColour(wxColour(255, 255, 255)); + { + const bool is_dark = wxGetApp().dark_mode(); + p->m_panel_mixed_filaments_content->SetBackgroundColour(is_dark ? wxColour(45, 45, 49) : wxColour(255, 255, 255)); + } // Content sizer - store in member variable for later use p->m_sizer_mixed_filaments_content = new wxBoxSizer(wxVERTICAL); @@ -2397,6 +2402,7 @@ private: wxAutoBufferedPaintDC dc(this); dc.SetBackground(wxBrush(GetBackgroundColour())); dc.Clear(); + const bool is_dark = wxGetApp().dark_mode(); const wxRect rect = gradient_rect(); if (m_multi_mode && m_multi_colors.size() >= 3) { @@ -2425,7 +2431,7 @@ private: } if (m_multi_weights.size() == m_multi_colors.size()) { - dc.SetTextForeground(wxColour(20, 20, 20)); + dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); dc.SetFont(Label::Body_10); const int pad = FromDIP(2); if (m_multi_colors.size() >= 4) { @@ -2442,12 +2448,12 @@ private: } else { dc.GradientFillLinear(rect, m_left, m_right, wxEAST); } - dc.SetPen(wxPen(wxColour(170, 170, 170), 1)); + dc.SetPen(wxPen(is_dark ? wxColour(100, 100, 106) : wxColour(170, 170, 170), 1)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(rect); if (m_multi_mode) { - dc.SetTextForeground(wxColour(30, 30, 30)); + dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(30, 30, 30)); dc.SetFont(Label::Body_10); const wxString hint = _L("Click to edit"); wxSize text_sz = dc.GetTextExtent(hint); @@ -2891,16 +2897,19 @@ private: // Forward declaration for MixedMixPreview (defined below) class MixedMixPreview; -// Dialog for configuring a single mixed filament -class MixedFilamentConfigDialog : public wxDialog +// Inline editor panel for configuring a single mixed filament +class MixedFilamentConfigPanel : public wxPanel { public: - MixedFilamentConfigDialog(wxWindow *parent, - size_t mixed_id, - const MixedFilament &mf, - size_t num_physical, - const std::vector &physical_colors, - const std::vector &palette); + using OnChangeFn = std::function; + + MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &palette, + OnChangeFn on_change = {}); // Get the updated mixed filament data MixedFilament get_mixed_filament() const { return m_mf; } @@ -2909,7 +2918,7 @@ public: private: void build_ui(); void update_preview(); - void on_apply(); + void update_component_picker_visuals(); size_t m_mixed_id; MixedFilament m_mf; @@ -2922,18 +2931,21 @@ private: wxChoice *m_choice_b = nullptr; wxChoice *m_choice_c = nullptr; wxChoice *m_choice_d = nullptr; - wxChoice *m_distribution_choice = nullptr; + wxPanel *m_picker_a_container = nullptr; + wxPanel *m_picker_b_container = nullptr; + wxPanel *m_picker_a_swatch = nullptr; + wxPanel *m_picker_b_swatch = nullptr; + wxStaticText *m_picker_a_label = nullptr; + wxStaticText *m_picker_b_label = nullptr; MixedGradientSelector *m_blend_selector = nullptr; wxStaticText *m_blend_label = nullptr; wxTextCtrl *m_pattern_ctrl = nullptr; - wxChoice *m_pattern_insert_choice = nullptr; - wxButton *m_pattern_insert_btn = nullptr; std::vector m_pattern_quick_buttons; wxButton *m_add_extra_color_btn = nullptr; MixedMixPreview *m_mix_preview = nullptr; - wxStaticText *m_mix_summary_label = nullptr; wxPanel *m_swatch = nullptr; std::shared_ptr> m_selected_weight_state; + OnChangeFn m_on_change; // Helper functions (copied from update_mixed_filament_panel) static std::vector decode_gradient_ids(const std::string &s); @@ -2962,12 +2974,16 @@ public: void set_data(const std::vector &palette, const std::vector &sequence, bool same_layer_mode, - const wxColour &fallback) + const wxColour &fallback, + const wxString &left_overlay, + const wxString &right_overlay) { m_palette = palette; m_sequence = sequence; m_same_layer = same_layer_mode; m_fallback = fallback; + m_left_overlay = left_overlay; + m_right_overlay = right_overlay; Refresh(); } @@ -3012,10 +3028,15 @@ private: dc.DrawRectangle(x, rect.GetTop(), std::max(1, w), rect.GetHeight()); } } else { - const int bars = std::min(24, std::max(1, int(m_sequence.size()))); + const int bars = 24; const int bar_w = std::max(1, rect.GetWidth() / bars); for (int i = 0; i < bars; ++i) { - const unsigned int extruder_id = m_sequence[size_t(i) % m_sequence.size()]; + size_t idx = 0; + if (m_sequence.size() > size_t(bars)) + idx = (size_t(i) * m_sequence.size()) / size_t(bars); + else + idx = size_t(i) % m_sequence.size(); + const unsigned int extruder_id = m_sequence[idx]; dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); const int x = rect.GetLeft() + i * bar_w; const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; @@ -3024,7 +3045,35 @@ private: } } - dc.SetPen(wxPen(wxColour(170, 170, 170), 1)); + auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) { + if (text.empty()) + return; + dc.SetTextForeground(wxColour(255, 255, 255)); + const int outline_radius = std::max(2, FromDIP(2)); + for (int ox = -outline_radius; ox <= outline_radius; ++ox) { + for (int oy = -outline_radius; oy <= outline_radius; ++oy) { + if (ox == 0 && oy == 0) + continue; + dc.DrawText(text, x + ox, y + oy); + } + } + dc.SetTextForeground(wxColour(22, 22, 22)); + dc.DrawText(text, x, y); + }; + + wxCoord left_w = 0, left_h = 0; + wxCoord right_w = 0, right_h = 0; + dc.GetTextExtent(m_left_overlay, &left_w, &left_h); + dc.GetTextExtent(m_right_overlay, &right_w, &right_h); + const int text_y = rect.GetTop() + std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2); + const int pad = FromDIP(6); + if (!m_left_overlay.empty()) + draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y); + if (!m_right_overlay.empty()) + draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y); + + const bool is_dark = wxGetApp().dark_mode(); + dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(rect); } @@ -3034,10 +3083,12 @@ private: std::vector m_sequence; bool m_same_layer { false }; wxColour m_fallback { wxColour(38, 166, 154) }; + wxString m_left_overlay; + wxString m_right_overlay; }; -// Implementation of MixedFilamentConfigDialog helper functions -std::vector MixedFilamentConfigDialog::decode_gradient_ids(const std::string &s) +// Implementation of MixedFilamentConfigPanel helper functions +std::vector MixedFilamentConfigPanel::decode_gradient_ids(const std::string &s) { std::vector ids; if (s.empty()) return ids; @@ -3050,7 +3101,7 @@ std::vector MixedFilamentConfigDialog::decode_gradient_ids(const s return ids; } -std::string MixedFilamentConfigDialog::encode_gradient_ids(const std::vector &ids) +std::string MixedFilamentConfigPanel::encode_gradient_ids(const std::vector &ids) { std::string out; for (size_t i = 0; i < ids.size(); ++i) { @@ -3060,7 +3111,7 @@ std::string MixedFilamentConfigDialog::encode_gradient_ids(const std::vector MixedFilamentConfigDialog::decode_manual_pattern_ids(const std::string &pattern, unsigned int a, unsigned int b) +std::vector MixedFilamentConfigPanel::decode_manual_pattern_ids(const std::string &pattern, unsigned int a, unsigned int b) { std::vector seq; for (char c : pattern) { @@ -3071,7 +3122,7 @@ std::vector MixedFilamentConfigDialog::decode_manual_pattern_ids(c return seq; } -std::vector MixedFilamentConfigDialog::decode_gradient_weights(const std::string &s, size_t n) +std::vector MixedFilamentConfigPanel::decode_gradient_weights(const std::string &s, size_t n) { std::vector w; std::string buf; @@ -3085,7 +3136,7 @@ std::vector MixedFilamentConfigDialog::decode_gradient_weights(const std::s return w; } -std::vector MixedFilamentConfigDialog::normalize_gradient_weights(const std::vector &w, size_t n) +std::vector MixedFilamentConfigPanel::normalize_gradient_weights(const std::vector &w, size_t n) { std::vector out = w; if (out.size() != n) out.assign(n, n > 0 ? int(100 / n) : 0); @@ -3114,7 +3165,7 @@ std::vector MixedFilamentConfigDialog::normalize_gradient_weights(const std return out; } -std::string MixedFilamentConfigDialog::encode_gradient_weights(const std::vector &w) +std::string MixedFilamentConfigPanel::encode_gradient_weights(const std::vector &w) { std::string out; for (size_t i = 0; i < w.size(); ++i) { @@ -3124,19 +3175,34 @@ std::string MixedFilamentConfigDialog::encode_gradient_weights(const std::vector return out; } -std::vector MixedFilamentConfigDialog::build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b) +std::vector MixedFilamentConfigPanel::build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b) { std::vector seq; - const int count_a = 100 - std::clamp(percent_b, 0, 100); - const int count_b = std::clamp(percent_b, 0, 100); - const int total = std::max(1, count_a + count_b); - seq.reserve(total); - for (int i = 0; i < count_a; ++i) seq.emplace_back(a); - for (int i = 0; i < count_b; ++i) seq.emplace_back(b); + const int b_percent = std::clamp(percent_b, 0, 100); + int ratio_a = std::max(1, 100 - b_percent); + int ratio_b = std::max(1, b_percent); + const int g = std::gcd(ratio_a, ratio_b); + if (g > 1) { + ratio_a /= g; + ratio_b /= g; + } + constexpr int k_max_cycle = 24; + if (ratio_a + ratio_b > k_max_cycle) { + const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); + ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); + ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); + } + const int cycle = std::max(1, ratio_a + ratio_b); + seq.reserve(size_t(cycle)); + for (int pos = 0; pos < cycle; ++pos) { + const int b_before = (pos * ratio_b) / cycle; + const int b_after = ((pos + 1) * ratio_b) / cycle; + seq.emplace_back((b_after > b_before) ? b : a); + } return seq; } -std::vector MixedFilamentConfigDialog::build_weighted_multi_sequence(const std::vector &ids, const std::vector &weights) +std::vector MixedFilamentConfigPanel::build_weighted_multi_sequence(const std::vector &ids, const std::vector &weights) { std::vector seq; if (ids.empty() || weights.empty()) return seq; @@ -3151,7 +3217,7 @@ std::vector MixedFilamentConfigDialog::build_weighted_multi_sequen return seq; } -std::string MixedFilamentConfigDialog::summarize_sequence(const std::vector &seq) +std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) { if (seq.empty()) return ""; std::unordered_map counts; @@ -3161,13 +3227,13 @@ std::string MixedFilamentConfigDialog::summarize_sequence(const std::vector()); std::string out; for (auto &p : sorted) { - if (!out.empty()) out += " + "; - out += wxString::Format("F%d:%d%%", p.second, int(100 * p.first / int(seq.size()))).ToStdString(); + if (!out.empty()) out += "/"; + out += wxString::Format("%d%%", int(100 * p.first / int(seq.size()))).ToStdString(); } return out; } -std::string MixedFilamentConfigDialog::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) +std::string MixedFilamentConfigPanel::blend_from_sequence(const std::vector &colors, const std::vector &seq, const std::string &fallback) { if (seq.empty()) return fallback; std::unordered_map counts; @@ -3191,85 +3257,112 @@ std::string MixedFilamentConfigDialog::blend_from_sequence(const std::vector &physical_colors, - const std::vector &palette) - : wxDialog(parent, wxID_ANY, _L("Configure Mixed Filament"), wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, + size_t mixed_id, + const MixedFilament &mf, + size_t num_physical, + const std::vector &physical_colors, + const std::vector &palette, + OnChangeFn on_change) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) , m_mixed_id(mixed_id) , m_mf(mf) , m_num_physical(num_physical) , m_physical_colors(physical_colors) , m_palette(palette) , m_selected_weight_state(std::make_shared>()) + , m_on_change(on_change) { + if (parent) + SetBackgroundColour(parent->GetBackgroundColour()); + else + SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); build_ui(); } -void MixedFilamentConfigDialog::build_ui() +void MixedFilamentConfigPanel::build_ui() { const int gap = FromDIP(6); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : + (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); + SetBackgroundColour(panel_bg); auto *root = new wxBoxSizer(wxVERTICAL); - // Color swatch at top - auto *header_row = new wxBoxSizer(wxHORIZONTAL); - m_swatch = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(24), FromDIP(24))); - m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); - m_swatch->SetMinSize(wxSize(FromDIP(24), FromDIP(24))); - header_row->Add(m_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - - const int virtual_id = int(m_num_physical + m_mixed_id + 1); - auto *title = new wxStaticText(this, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_id)); - title->SetFont(Label::Head_14); - header_row->Add(title, 0, wxALIGN_CENTER_VERTICAL); - root->Add(header_row, 0, wxEXPAND | wxALL, gap); - // Filament choices wxArrayString filament_choices; for (size_t i = 0; i < m_num_physical; ++i) - filament_choices.Add(wxString::Format("Filament %d", int(i + 1))); + filament_choices.Add(wxString::Format("F%d", int(i + 1))); const int component_a = std::clamp(int(m_mf.component_a), 1, int(m_num_physical)); const int component_b = std::clamp(int(m_mf.component_b), 1, int(m_num_physical)); - auto *picker_row = new wxBoxSizer(wxHORIZONTAL); + const int row_distribution_mode = int(MixedFilament::Simple); + m_mf.distribution_mode = row_distribution_mode; + + // Hidden data controls used as backing state for swatch pickers. m_choice_a = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); m_choice_b = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); m_choice_a->SetSelection(component_a - 1); m_choice_b->SetSelection(component_b - 1); - picker_row->Add(m_choice_a, 1, wxALIGN_CENTER_VERTICAL); - picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, gap); - picker_row->Add(m_choice_b, 1, wxALIGN_CENTER_VERTICAL); - root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_choice_a->Hide(); + m_choice_b->Hide(); - // Distribution mode - const int row_distribution_mode = std::clamp(m_mf.distribution_mode, - int(MixedFilament::LayerCycle), - int(MixedFilament::Simple)); - wxArrayString distribution_choices; - distribution_choices.Add(_L("Layer cycling")); - distribution_choices.Add(_L("Same-layer pointillisme")); - distribution_choices.Add(_L("Simple")); - auto *distribution_row = new wxBoxSizer(wxHORIZONTAL); - distribution_row->Add(new wxStaticText(this, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - m_distribution_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, distribution_choices); - m_distribution_choice->SetSelection(row_distribution_mode); - m_distribution_choice->SetToolTip(_L("Choose whether this mixed row alternates by layer or interleaves colors on the same layer.\n\n" - "Warning: Same-layer pointillisme is extremely experimental and may produce unusable results.")); - distribution_row->Add(m_distribution_choice, 1, wxALIGN_CENTER_VERTICAL); - root->Add(distribution_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto create_component_picker = [this, gap](wxPanel *&container_out, wxPanel *&swatch_out, wxStaticText *&label_out, const wxString &tooltip) { + const int inner_gap = std::max(FromDIP(1), gap / 4); + const bool local_is_dark = wxGetApp().dark_mode(); + const wxColour local_picker_bg = local_is_dark ? wxColour(64, 64, 70) : wxColour(255, 255, 255); + const wxColour local_picker_text = local_is_dark ? wxColour(230, 230, 230) : wxColour(32, 32, 32); + container_out = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); + container_out->SetBackgroundColour(local_picker_bg); + const wxSize picker_size(FromDIP(38), FromDIP(22)); + container_out->SetMinSize(picker_size); + container_out->SetMaxSize(picker_size); + + auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); + swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); + swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); + label_out->SetForegroundColour(local_picker_text); + + auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); + content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); + content_sizer->Add(label_out, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_sizer->Add(content_sizer, 0, wxALIGN_CENTER_VERTICAL); + container_sizer->AddStretchSpacer(1); + container_out->SetSizer(container_sizer); + container_out->SetToolTip(tooltip); + container_out->SetCursor(wxCursor(wxCURSOR_HAND)); + swatch_out->SetCursor(wxCursor(wxCURSOR_HAND)); + label_out->SetCursor(wxCursor(wxCURSOR_HAND)); + }; + + create_component_picker(m_picker_a_container, m_picker_a_swatch, m_picker_a_label, _L("Click to choose a physical filament color")); + create_component_picker(m_picker_b_container, m_picker_b_swatch, m_picker_b_label, _L("Click to choose a physical filament color")); + update_component_picker_visuals(); // Check for pattern mode const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(m_mf.manual_pattern); const bool pattern_row_mode = !normalized_pattern.empty(); + auto *picker_row = new wxBoxSizer(wxHORIZONTAL); + if (!pattern_row_mode) { + picker_row->Add(m_picker_a_container, 0, wxALIGN_CENTER_VERTICAL); + picker_row->Add(new wxStaticText(this, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, std::max(FromDIP(2), gap / 2)); + picker_row->Add(m_picker_b_container, 0, wxALIGN_CENTER_VERTICAL); + } else { + if (m_picker_a_container) m_picker_a_container->Hide(); + if (m_picker_b_container) m_picker_b_container->Hide(); + } + root->Add(picker_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + // Pattern controls (if pattern mode) if (pattern_row_mode) { auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); - pattern_row->Add(new wxStaticText(this, wxID_ANY, _L("Pattern")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *pattern_label = new wxStaticText(this, wxID_ANY, _L("Pattern")); + pattern_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B for component A/B, " @@ -3278,19 +3371,6 @@ void MixedFilamentConfigDialog::build_ui() pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - auto *insert_row = new wxBoxSizer(wxHORIZONTAL); - insert_row->Add(new wxStaticText(this, wxID_ANY, _L("Insert")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - m_pattern_insert_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); - m_pattern_insert_choice->SetSelection(component_a - 1); - m_pattern_insert_choice->SetToolTip(_L("Select a physical filament to append into the pattern.")); - insert_row->Add(m_pattern_insert_choice, 1, wxALIGN_CENTER_VERTICAL); - m_pattern_insert_btn = new wxButton(this, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); - m_pattern_insert_btn->SetToolTip(_L("Append selected filament ID to pattern")); - insert_row->Add(m_pattern_insert_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); - root->Add(insert_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - auto *quick_row = new wxBoxSizer(wxHORIZONTAL); - quick_row->Add(new wxStaticText(this, wxID_ANY, _L("Filaments")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); for (size_t fid = 0; fid < m_num_physical; ++fid) { wxButton *btn = new wxButton(this, wxID_ANY, wxString::Format("%d", int(fid + 1)), @@ -3301,14 +3381,16 @@ void MixedFilamentConfigDialog::build_ui() quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); m_pattern_quick_buttons.emplace_back(btn); } - quick_row->Add(quick_buttons, 1, wxALIGN_CENTER_VERTICAL); - root->Add(quick_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto *filaments_label = new wxStaticText(this, wxID_ANY, _L("Filaments")); + filaments_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); + picker_row->Add(filaments_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(3), gap / 2)); + picker_row->Add(quick_buttons, 0, wxALIGN_CENTER_VERTICAL); } else { // Blend selector for non-pattern mode wxArrayString optional_filament_choices; optional_filament_choices.Add(_L("None")); for (size_t i = 0; i < m_num_physical; ++i) - optional_filament_choices.Add(wxString::Format("Filament %d", int(i + 1))); + optional_filament_choices.Add(wxString::Format("F%d", int(i + 1))); const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); std::vector selected_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(m_mf.gradient_component_ids); @@ -3328,22 +3410,11 @@ void MixedFilamentConfigDialog::build_ui() wxColour color_a = (component_a >= 1 && component_a <= int(m_palette.size())) ? m_palette[component_a - 1] : wxColour("#26A69A"); wxColour color_b = (component_b >= 1 && component_b <= int(m_palette.size())) ? m_palette[component_b - 1] : wxColour("#26A69A"); m_blend_selector = new MixedGradientSelector(this, color_a, color_b, std::clamp(m_mf.mix_b_percent, 0, 100)); - auto *blend_row = new wxBoxSizer(wxHORIZONTAL); - blend_row->Add(m_blend_selector, 1, wxEXPAND); - root->Add(blend_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - + m_blend_selector->SetBackgroundColour(panel_bg); const bool same_layer_mode = row_distribution_mode == int(MixedFilament::SameLayerPointillisme); - m_blend_label = new wxStaticText(this, wxID_ANY, multi_gradient_mode ? - wxString::Format(same_layer_mode ? _L("%d-color pointillisme") : _L("%d-color layer cycle"), - int(selected_gradient_ids.size())) : - wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : - (same_layer_mode ? _L("Pointillisme %d%%/%d%%") : _L("%d%%/%d%%")), - 100 - std::clamp(m_mf.mix_b_percent, 0, 100), - std::clamp(m_mf.mix_b_percent, 0, 100))); - auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); - ratio_row->AddStretchSpacer(1); - ratio_row->Add(m_blend_label, 0, wxALIGN_CENTER_VERTICAL); - root->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + m_blend_label = nullptr; + picker_row->AddSpacer(gap); + picker_row->Add(m_blend_selector, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxLEFT, gap); // Extra colors for multi-gradient if (m_num_physical >= 3 && !simple_mode) { @@ -3379,25 +3450,11 @@ void MixedFilamentConfigDialog::build_ui() // Preview auto *preview_row = new wxBoxSizer(wxHORIZONTAL); - preview_row->Add(new wxStaticText(this, wxID_ANY, _L("Preview")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); m_mix_preview = new MixedMixPreview(this); - preview_row->Add(m_mix_preview, 1, wxALIGN_CENTER_VERTICAL); - m_mix_summary_label = new wxStaticText(this, wxID_ANY, wxEmptyString); - m_mix_summary_label->SetForegroundColour(wxColour(90, 90, 90)); - preview_row->Add(m_mix_summary_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); + m_mix_preview->SetBackgroundColour(panel_bg); + preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - // Enable checkbox - auto *chk_row = new wxBoxSizer(wxHORIZONTAL); - auto *chk = new wxCheckBox(this, wxID_ANY, _L("Enabled")); - chk->SetValue(m_mf.enabled); - chk_row->Add(chk, 0, wxALIGN_CENTER_VERTICAL); - chk->Bind(wxEVT_CHECKBOX, [this, chk](wxCommandEvent&) { m_mf.enabled = chk->GetValue(); }); - root->Add(chk_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - - // Buttons - root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, gap); - // Bind events auto apply_changes = [this]() { m_has_changes = true; @@ -3408,14 +3465,11 @@ void MixedFilamentConfigDialog::build_ui() b = (a == int(m_num_physical)) ? 1 : a + 1; m_choice_b->SetSelection(b - 1); } + update_component_picker_visuals(); m_mf.component_a = unsigned(a); m_mf.component_b = unsigned(b); - m_mf.distribution_mode = m_distribution_choice ? - std::clamp(m_distribution_choice->GetSelection(), - int(MixedFilament::LayerCycle), - int(MixedFilament::Simple)) : - int(MixedFilament::Simple); + m_mf.distribution_mode = int(MixedFilament::Simple); const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); @@ -3456,6 +3510,26 @@ void MixedFilamentConfigDialog::build_ui() m_mf.mix_b_percent = std::clamp(m_blend_selector ? m_blend_selector->value() : 50, 0, 100); m_mf.manual_pattern.clear(); m_mf.pointillism_all_filaments = false; + + const wxColour color_a = (a >= 1 && a <= int(m_palette.size())) ? m_palette[size_t(a - 1)] : wxColour("#26A69A"); + const wxColour color_b = (b >= 1 && b <= int(m_palette.size())) ? m_palette[size_t(b - 1)] : wxColour("#26A69A"); + if (m_blend_selector) { + if (!simple_mode && multi_gradient_mode) { + std::vector corner_colors; + corner_colors.reserve(selected_ids.size()); + for (const unsigned int id : selected_ids) { + if (id >= 1 && id <= m_palette.size()) + corner_colors.emplace_back(m_palette[id - 1]); + } + if (corner_colors.size() >= 3) + m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); + else + m_blend_selector->set_colors(color_a, color_b); + } else { + m_blend_selector->set_colors(color_a, color_b); + } + } + if (multi_gradient_mode) { const std::vector decoded_weights = decode_gradient_weights(m_mf.gradient_component_weights, selected_ids.size()); @@ -3500,22 +3574,73 @@ void MixedFilamentConfigDialog::build_ui() } if (m_mix_preview) { - m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, wxColour(m_mf.display_color)); - } - if (m_mix_summary_label) { const std::string summary = summarize_sequence(preview_sequence); - m_mix_summary_label->SetLabel(summary.empty() ? _L("No mix sequence") : from_u8(summary)); + m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxEmptyString : from_u8(summary)); } if (m_swatch) { m_swatch->SetBackgroundColour(wxColour(m_mf.display_color)); m_swatch->Refresh(); } + if (m_on_change) + m_on_change(m_mf); }; + auto make_color_chip_bitmap = [this](const wxColour &color) { + const int chip_size = FromDIP(14); + wxBitmap bmp(chip_size, chip_size); + wxMemoryDC dc(bmp); + dc.SetBackground(wxBrush(wxColour(255, 255, 255))); + dc.Clear(); + dc.SetPen(wxPen(wxColour(120, 120, 120))); + dc.SetBrush(wxBrush(color)); + dc.DrawRectangle(0, 0, chip_size, chip_size); + dc.SelectObject(wxNullBitmap); + return bmp; + }; + + auto bind_component_picker_popup = [this, apply_changes, make_color_chip_bitmap](wxWindow *target, wxChoice *backing_choice) { + if (!target || !backing_choice) + return; + + target->Bind(wxEVT_LEFT_UP, [this, apply_changes, make_color_chip_bitmap, backing_choice](wxMouseEvent &) { + if (m_num_physical == 0) + return; + + wxMenu menu; + std::vector item_ids; + item_ids.reserve(m_num_physical); + for (size_t i = 0; i < m_num_physical; ++i) { + const int item_id = wxWindow::NewControlId(); + item_ids.emplace_back(item_id); + const bool is_selected = int(i) == backing_choice->GetSelection(); + const wxString item_label = wxString::Format("F%d%s", int(i + 1), is_selected ? " (Selected)" : ""); + auto *menu_item = new wxMenuItem(&menu, item_id, item_label, wxEmptyString, wxITEM_NORMAL); + const wxColour item_color = (i < m_palette.size()) ? m_palette[i] : wxColour("#26A69A"); + menu_item->SetBitmap(make_color_chip_bitmap(item_color)); + menu.Append(menu_item); + } + + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [apply_changes, backing_choice, item_ids](wxCommandEvent &evt) { + const auto it = std::find(item_ids.begin(), item_ids.end(), evt.GetId()); + if (it == item_ids.end()) + return; + backing_choice->SetSelection(int(std::distance(item_ids.begin(), it))); + apply_changes(); + }); + PopupMenu(&menu); + }); + }; + + bind_component_picker_popup(m_picker_a_container, m_choice_a); + bind_component_picker_popup(m_picker_a_swatch, m_choice_a); + bind_component_picker_popup(m_picker_a_label, m_choice_a); + bind_component_picker_popup(m_picker_b_container, m_choice_b); + bind_component_picker_popup(m_picker_b_swatch, m_choice_b); + bind_component_picker_popup(m_picker_b_label, m_choice_b); + m_choice_a->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); m_choice_b->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); - if (m_distribution_choice) - m_distribution_choice->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); if (m_choice_c) m_choice_c->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent&) { apply_changes(); }); if (m_choice_d) @@ -3580,12 +3705,6 @@ void MixedFilamentConfigDialog::build_ui() }; m_pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent&) { apply_changes(); }); m_pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_changes](wxFocusEvent &evt) { apply_changes(); evt.Skip(); }); - if (m_pattern_insert_btn && m_pattern_insert_choice) { - m_pattern_insert_btn->Bind(wxEVT_BUTTON, [apply_changes, append_pattern_token, this](wxCommandEvent&) { - const int sel = m_pattern_insert_choice->GetSelection(); - if (sel >= 0) { append_pattern_token(sel + 1); apply_changes(); } - }); - } for (size_t fid = 0; fid < m_pattern_quick_buttons.size(); ++fid) { wxButton *btn = m_pattern_quick_buttons[fid]; if (btn) { @@ -3598,12 +3717,43 @@ void MixedFilamentConfigDialog::build_ui() } } - SetSizerAndFit(root); - SetMinSize(wxSize(FromDIP(380), GetBestSize().GetHeight())); + SetSizer(root); + Layout(); + SetMinSize(wxSize(-1, GetBestSize().GetHeight())); update_preview(); } -void MixedFilamentConfigDialog::update_preview() +void MixedFilamentConfigPanel::update_component_picker_visuals() +{ + auto update_one = [this](wxChoice *choice, wxPanel *container, wxPanel *swatch, wxStaticText *label) { + if (!choice) + return; + int sel = choice->GetSelection(); + if (sel < 0 && m_num_physical > 0) { + sel = 0; + choice->SetSelection(sel); + } + if (sel < 0) + return; + + const wxColour color = (size_t(sel) < m_palette.size()) ? m_palette[size_t(sel)] : wxColour("#26A69A"); + if (swatch) { + swatch->SetBackgroundColour(color); + swatch->Refresh(); + } + if (label) + label->SetLabel(wxString::Format("F%d", sel + 1)); + if (container) { + container->Layout(); + container->Refresh(); + } + }; + + update_one(m_choice_a, m_picker_a_container, m_picker_a_swatch, m_picker_a_label); + update_one(m_choice_b, m_picker_b_container, m_picker_b_swatch, m_picker_b_label); +} + +void MixedFilamentConfigPanel::update_preview() { const bool simple_mode = m_mf.distribution_mode == int(MixedFilament::Simple); const bool same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); @@ -3622,11 +3772,9 @@ void MixedFilamentConfigDialog::update_preview() } if (m_mix_preview) { - m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, wxColour(m_mf.display_color)); - } - if (m_mix_summary_label) { const std::string summary = summarize_sequence(initial_sequence); - m_mix_summary_label->SetLabel(summary.empty() ? _L("No mix sequence") : from_u8(summary)); + m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, wxColour(m_mf.display_color), + _L("Preview"), summary.empty() ? wxEmptyString : from_u8(summary)); } } @@ -4048,8 +4196,12 @@ void Sidebar::update_mixed_filament_panel() const int compact_gap_x = FromDIP(6); const int compact_gap_y = FromDIP(4); const int compact_row_pad = FromDIP(6); - const wxColour mixed_rows_bg(246, 248, 251); - const wxColour mixed_row_bg(255, 255, 255); + const bool is_dark = wxGetApp().dark_mode(); + const wxColour mixed_rows_bg = is_dark ? wxColour(45, 45, 49) : wxColour(246, 248, 251); + const wxColour mixed_row_bg = is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255); + const wxColour mixed_row_hover_bg = is_dark ? wxColour(62, 62, 68) : wxColour(241, 247, 255); + const wxColour mixed_text_fg = is_dark ? wxColour(232, 232, 232) : wxColour(20, 20, 20); + const wxColour mixed_summary_fg = is_dark ? wxColour(182, 182, 182) : wxColour(96, 96, 96); p->m_panel_mixed_filaments_content->SetBackgroundColour(mixed_rows_bg); // Get the content sizer and clear it @@ -4088,10 +4240,126 @@ void Sidebar::update_mixed_filament_panel() auto *rows_sizer = new wxBoxSizer(wxVERTICAL); rows_scroller->SetSizer(rows_sizer); + auto adjust_rows_scroller_height = [this, rows_scroller]() { + if (!rows_scroller) + return; + const int min_h = FromDIP(68); + const int collapsed_max_h = FromDIP(220); + int two_rows_cap_h = collapsed_max_h; + const auto &children = rows_scroller->GetChildren(); + if (!children.empty()) { + std::vector heights; + heights.reserve(children.GetCount()); + for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) { + wxWindow *child = it->GetData(); + wxPanel *panel = dynamic_cast(child); + if (!panel) + continue; + heights.emplace_back(std::max(panel->GetSize().GetHeight(), panel->GetBestSize().GetHeight())); + } + if (!heights.empty()) { + std::sort(heights.begin(), heights.end(), std::greater()); + const size_t keep = std::min(2, heights.size()); + int rows_h = 0; + for (size_t i = 0; i < keep; ++i) + rows_h += heights[i]; + if (keep > 1) + rows_h += int(keep - 1) * FromDIP(2); + rows_h += FromDIP(8); + two_rows_cap_h = std::max(collapsed_max_h, rows_h); + } + } + + const int max_h = p->m_expanded_mixed_filament_rows.empty() ? collapsed_max_h : two_rows_cap_h; + const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); + const int desired_h = std::clamp(content_h, min_h, max_h); + rows_scroller->SetMinSize(wxSize(-1, desired_h)); + rows_scroller->SetMaxSize(wxSize(-1, desired_h)); + }; + + for (auto it = p->m_expanded_mixed_filament_rows.begin(); it != p->m_expanded_mixed_filament_rows.end();) { + if (*it >= mixed.size() || mixed[*it].deleted) + it = p->m_expanded_mixed_filament_rows.erase(it); + else + ++it; + } + + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &hex : physical_colors) + palette.emplace_back(parse_mixed_color(hex)); + + auto mixed_summary_text = [](const MixedFilament &entry) { + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); + if (!entry.custom) + return wxString::Format("(Filament %u + Filament %u)", unsigned(entry.component_a), unsigned(entry.component_b)); + if (!normalized_pattern.empty()) + return _L("(Pattern)"); + return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); + }; + + auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg](size_t mixed_id, const MixedFilament &updated_mf, bool preserve_enabled = false) { + if (!preset_bundle) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + MixedFilament merged = updated_mf; + if (preserve_enabled) + merged.enabled = mfs[mixed_id].enabled; + mfs[mixed_id] = merged; + + const std::string serialized = mgr.serialize_custom_entries(); + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option("mixed_filament_definitions")) + opt->value = serialized; + else + print_cfg->set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + } + if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) + opt->value = serialized; + else + preset_bundle->project_config.set_key_value("mixed_filament_definitions", new ConfigOptionString(serialized)); + + if (print_cfg) { + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + } + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); + + int mode = 0; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value ? 1 : 0; + else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + mode = opt->value != 0 ? 1 : 0; + float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; + float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? + float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; + int cycle = preset_bundle->project_config.has("mixed_filament_cycle_layers") ? + preset_bundle->project_config.opt_int("mixed_filament_cycle_layers") : 4; + bool advanced = false; + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) + advanced = opt->value; + mode = std::clamp(mode, 0, 1); + lo = std::max(0.01f, lo); + hi = std::max(lo, hi); + cycle = std::max(2, cycle); + mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); + update_dynamic_filament_list(); + }; + for (size_t mixed_id = 0; mixed_id < mixed.size(); ++mixed_id) { MixedFilament &mf = mixed[mixed_id]; - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - const bool pattern_row_mode = !normalized_pattern.empty(); + if (mf.deleted) + continue; + const bool editable_row = mf.custom; auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); row->SetBackgroundColour(mixed_row_bg); @@ -4109,35 +4377,55 @@ void Sidebar::update_mixed_filament_panel() const int virtual_filament_id = int(num_physical + mixed_id + 1); auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); + name_label->SetForegroundColour(mixed_text_fg); header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - wxString summary_text; - if (!mf.custom) { - summary_text = wxString::Format("(Filament %u + Filament %u)", unsigned(mf.component_a), unsigned(mf.component_b)); - } else { - if (pattern_row_mode) summary_text = _L("(Pattern)"); - else summary_text = wxString::Format("(F%u + F%u)", unsigned(mf.component_a), unsigned(mf.component_b)); - } - auto *summary_label = new wxStaticText(header_panel, wxID_ANY, summary_text); - summary_label->SetForegroundColour(wxColour(96, 96, 96)); + auto *summary_label = new wxStaticText(header_panel, wxID_ANY, mixed_summary_text(mf)); + summary_label->SetForegroundColour(mixed_summary_fg); header_sizer->Add(summary_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); header_sizer->AddStretchSpacer(1); + auto *enabled_chk = new wxCheckBox(header_panel, wxID_ANY, _L("Enabled")); + enabled_chk->SetValue(mf.enabled); + enabled_chk->SetForegroundColour(mixed_text_fg); + header_sizer->Add(enabled_chk, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); + enabled_chk->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); + }); + enabled_chk->Bind(wxEVT_CHECKBOX, [mixed_id, enabled_chk, apply_mixed_entry_changes, preset_bundle](wxCommandEvent &) { + if (!preset_bundle || !enabled_chk) + return; + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + MixedFilament updated = mfs[mixed_id]; + updated.enabled = enabled_chk->GetValue(); + apply_mixed_entry_changes(mixed_id, updated, false); + }); + auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); del_btn->SetToolTip(_L("Delete mixed filament")); header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - del_btn->Bind(wxEVT_BUTTON, [this, mixed_id](wxCommandEvent&) { + del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, set_mixed_string, notify_mixed_change](wxCommandEvent&) { if (wxGetApp().preset_bundle) { auto &mgr = wxGetApp().preset_bundle->mixed_filaments; auto &mfs = mgr.mixed_filaments(); if (mixed_id < mfs.size()) { - mfs.erase(mfs.begin() + mixed_id); - - if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - + if (mfs[mixed_id].custom) + mfs.erase(mfs.begin() + mixed_id); + else { + mfs[mixed_id].deleted = true; + mfs[mixed_id].enabled = false; + } + p->m_expanded_mixed_filament_rows.clear(); + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + notify_mixed_change(); + if (wxGetApp().plater()) + wxGetApp().plater()->update_project_dirty_from_presets(); update_mixed_filament_panel(); } } @@ -4146,54 +4434,138 @@ void Sidebar::update_mixed_filament_panel() header_panel->SetSizer(header_sizer); row_sizer->Add(header_panel, 0, wxEXPAND | wxALL, 0); - // Clicking on the row (header panel) opens the configuration dialog - header_panel->Bind(wxEVT_LEFT_UP, [this, mixed_id, num_physical, physical_colors, preset_bundle](wxMouseEvent &) { + auto *editor_host = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + editor_host->SetBackgroundColour(mixed_row_bg); + auto *editor_sizer = new wxBoxSizer(wxVERTICAL); + editor_host->SetSizer(editor_sizer); + editor_host->Hide(); + row_sizer->Add(editor_host, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, compact_row_pad); + + auto set_row_hover = [row, header_panel, editor_host, mixed_row_bg, mixed_row_hover_bg](bool hovered) { + const wxColour bg = hovered ? mixed_row_hover_bg : mixed_row_bg; + if (row) row->SetBackgroundColour(bg); + if (header_panel) header_panel->SetBackgroundColour(bg); + if (editor_host) editor_host->SetBackgroundColour(bg); + if (row) row->Refresh(); + if (header_panel) header_panel->Refresh(); + if (editor_host) editor_host->Refresh(); + }; + + auto row_contains_mouse = [row]() { + if (!row) + return false; + const wxPoint mouse_pos = wxGetMousePosition(); + const wxPoint local = row->ScreenToClient(mouse_pos); + return row->GetClientRect().Contains(local); + }; + + auto ensure_editor = [this, mixed_id, num_physical, physical_colors, palette, preset_bundle, + editor_host, editor_sizer, swatch, summary_label, header_panel, row, + rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { + if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) + return; + auto &mgr = preset_bundle->mixed_filaments; auto &mfs = mgr.mixed_filaments(); if (mixed_id >= mfs.size()) return; - - // Build palette for dialog - std::vector palette; - palette.reserve(physical_colors.size()); - for (const std::string &hex : physical_colors) - palette.emplace_back(parse_mixed_color(hex)); - - MixedFilamentConfigDialog dlg(this, mixed_id, mfs[mixed_id], num_physical, physical_colors, palette); - if (dlg.ShowModal() == wxID_OK && dlg.has_changes()) { - // Apply the changes from the dialog - mfs[mixed_id] = dlg.get_mixed_filament(); - - // Persist the custom entries - if (ConfigOptionString *opt = preset_bundle->project_config.option("mixed_filament_definitions")) - opt->value = mgr.serialize_custom_entries(); - - // Apply global gradient settings - int mode = 0; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value ? 1 : 0; - else if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) - mode = opt->value != 0 ? 1 : 0; - float lo = preset_bundle->project_config.has("mixed_filament_height_lower_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; - float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? - float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; - int cycle = preset_bundle->project_config.has("mixed_filament_cycle_layers") ? - preset_bundle->project_config.opt_int("mixed_filament_cycle_layers") : 4; - bool advanced = false; - if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) - advanced = opt->value; - mode = std::clamp(mode, 0, 1); - lo = std::max(0.01f, lo); - hi = std::max(lo, hi); - cycle = std::max(2, cycle); - mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); - - update_dynamic_filament_list(); - update_mixed_filament_panel(); + + auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, palette, + [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { + apply_mixed_entry_changes(mixed_id, updated_mf, true); + + if (swatch) { + swatch->SetBackgroundColour(wxColour(updated_mf.display_color)); + swatch->Refresh(); + } + if (summary_label) { + summary_label->SetLabel(mixed_summary_text(updated_mf)); + } + if (header_panel) + header_panel->Layout(); + if (row) + row->Layout(); + if (rows_scroller) { + rows_scroller->Layout(); + rows_scroller->FitInside(); + } + }); + + editor_sizer->Add(editor, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(4)); + editor_host->Layout(); + }; + + auto toggle_editor = [this, mixed_id, editor_host, ensure_editor, rows_scroller, adjust_rows_scroller_height]() { + if (!editor_host || !rows_scroller) + return; + + if (editor_host->IsShown()) { + editor_host->Hide(); + p->m_expanded_mixed_filament_rows.erase(mixed_id); + } else { + ensure_editor(); + editor_host->Show(); + p->m_expanded_mixed_filament_rows.insert(mixed_id); } + + rows_scroller->Layout(); + rows_scroller->FitInside(); + adjust_rows_scroller_height(); + p->m_panel_mixed_filaments_content->Layout(); + m_scrolled_sizer->Layout(); + Layout(); + }; + + auto bind_toggle_target = [&toggle_editor](wxWindow *target) { + if (!target) + return; + target->SetCursor(wxCursor(wxCURSOR_HAND)); + target->Bind(wxEVT_LEFT_UP, [toggle_editor](wxMouseEvent &) { + toggle_editor(); + }); + }; + + auto bind_hover_target = [set_row_hover, row_contains_mouse](wxWindow *target) { + if (!target) + return; + target->Bind(wxEVT_ENTER_WINDOW, [set_row_hover](wxMouseEvent &evt) { + set_row_hover(true); + evt.Skip(); + }); + target->Bind(wxEVT_LEAVE_WINDOW, [set_row_hover, row_contains_mouse](wxMouseEvent &evt) { + set_row_hover(row_contains_mouse()); + evt.Skip(); + }); + }; + + header_panel->SetToolTip(editable_row ? + _L("Click to expand/retract mixed filament settings") : + _L("Automatic mixed filament settings are read-only.")); + if (editable_row) { + bind_toggle_target(row); + bind_toggle_target(header_panel); + bind_toggle_target(name_label); + bind_toggle_target(summary_label); + bind_toggle_target(swatch); + } else { + p->m_expanded_mixed_filament_rows.erase(mixed_id); + } + bind_hover_target(row); + bind_hover_target(header_panel); + bind_hover_target(name_label); + bind_hover_target(summary_label); + bind_hover_target(swatch); + + del_btn->Bind(wxEVT_LEFT_UP, [](wxMouseEvent &evt) { + evt.StopPropagation(); + evt.Skip(); }); + if (editable_row && p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { + ensure_editor(); + editor_host->Show(); + } + row->SetSizer(row_sizer); rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); rows_sizer->AddSpacer(FromDIP(2)); @@ -4202,13 +4574,7 @@ void Sidebar::update_mixed_filament_panel() rows_sizer->AddSpacer(FromDIP(2)); rows_scroller->Layout(); rows_scroller->FitInside(); - - const int min_h = FromDIP(68); - const int max_h = FromDIP(220); - const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); - const int desired_h = std::clamp(content_h, min_h, max_h); - rows_scroller->SetMinSize(wxSize(-1, desired_h)); - rows_scroller->SetMaxSize(wxSize(-1, desired_h)); + adjust_rows_scroller_height(); if (prev_rows_view_y > 0) rows_scroller->Scroll(0, prev_rows_view_y);