X2D Support (#13388)

# Description

Adresses #13294 

- Adds the X2D printer definition, machine presets, process presets,
filament presets, BBL profile index entries, CLI config entries,
filament blacklist updates, and printer/load/calibration/cover assets.
- Updates dual-nozzle handling to use configured toolhead labels and
match Bambu X2D hotend placeholders.
- Adds X2D-specific wipe tower cooling placeholder support and 3MF
filament/nozzle change sequence metadata import/export plumbing.

# Note

I own a P2S and an X2D. That's all. I frankly have no idea if my changes
cause regression on other printers, and have no capability to test. I
know that for my X2D, which runs an AMS, .2mm nozzles, SuperTack, and in
LAN mode, this has been working without issue.

# Screenshots/Recordings/Graphs

<img width="606" height="380" alt="Dual nozzle control"
src="https://github.com/user-attachments/assets/0d1c1063-4621-4097-b97c-d739557bf18c"
/>

*Dual nozzle control*

<img width="726" height="260" alt="image"
src="https://github.com/user-attachments/assets/270355b7-ca67-4ca3-ad19-582b8f11411b"
/>

*Multi nozzle filament override*

<img width="416" height="202" alt="X2D Machine config and dual nozzle
support"
src="https://github.com/user-attachments/assets/6a5c07b2-0d20-4819-8f42-d60731313249"
/>

*X2D Machine config and dual nozzle support*

<img width="397" height="142" alt="Filament for Supports test prints"
src="https://github.com/user-attachments/assets/3c7546bd-0e27-4d56-89b7-d9ca18c976f9"
/>

*Filament for Supports has been used in over 20 hours of test prints*

<img width="210" height="263" alt="Left vs Right filament distinction"
src="https://github.com/user-attachments/assets/03322268-b669-4f14-8d77-c4d96843d219"
/>

*Left vs Right filament distinction*

<img width="557" height="327" alt="Custom filament mapping"
src="https://github.com/user-attachments/assets/c1c4396f-7359-474e-80bd-78fec22f9c82"
/>

*Custom filament mapping*

<img width="556" height="314" alt="Auto map"
src="https://github.com/user-attachments/assets/d83e3217-edce-4340-886e-043962003a30"
/>

*Auto map*

<img width="689" height="664" alt="LAN mode send print with X2D preview
and no errors"
src="https://github.com/user-attachments/assets/76009bbf-31d3-4a6c-979c-8643b487c824"
/>

*LAN mode send print with X2D preview and no errors, dual nozzle
selection*


## Tests

- 20 hours of dual-nozzle printing.
- 100% CTest tests passed
- Validated 208 changed JSON files.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Fix #13294
This commit is contained in:
glowstab
2026-05-10 02:21:13 +08:00
committed by GitHub
parent 7bef75b2cd
commit 9956ad5b48
242 changed files with 53863 additions and 83 deletions
+49 -1
View File
@@ -1,5 +1,8 @@
#include "DevConfigUtil.h"
#include <algorithm>
#include <cctype>
#include <wx/dir.h>
#include <boost/filesystem.hpp>
@@ -246,4 +249,49 @@ std::map<std::string, std::vector<std::string>> DevPrinterConfigUtil::get_all_su
return subseries;
}
};
std::string DevPrinterConfigUtil::get_toolhead_display_name(
const std::string& type_str,
int ext_id,
ToolHeadComponent component,
ToolHeadNameCase name_case,
bool short_name)
{
static const std::map<ToolHeadComponent, std::string> comp_keys = {
{ ToolHeadComponent::Extruder, "extruder" },
{ ToolHeadComponent::Nozzle, "nozzle" },
{ ToolHeadComponent::Hotend, "hotend" }
};
const int case_index = static_cast<int>(name_case);
const std::string role_key = std::to_string(ext_id);
const std::string& comp_key = comp_keys.at(component);
std::string result;
auto names_json = get_value_from_config<json>(type_str, "tool_head_display_names");
if (!names_json.is_null() && names_json.contains(role_key) && names_json[role_key].contains(comp_key)) {
auto& arr = names_json[role_key][comp_key];
if (arr.is_array() && case_index < static_cast<int>(arr.size()))
result = arr[case_index].get<std::string>();
}
if (result.empty()) {
const std::string side = ext_id == DEPUTY_EXTRUDER_ID ? "Left" : "Right";
const std::string component_name = component == ToolHeadComponent::Extruder ? "Extruder" :
component == ToolHeadComponent::Hotend ? "Hotend" : "Nozzle";
result = side + " " + component_name;
if (name_case == ToolHeadNameCase::SentenceCase && result.size() > side.size() + 1)
result[side.size() + 1] = static_cast<char>(std::tolower(static_cast<unsigned char>(result[side.size() + 1])));
else if (name_case == ToolHeadNameCase::LowerCase)
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
}
if (short_name) {
auto sp = result.find(' ');
if (sp != std::string::npos)
result = result.substr(0, sp);
}
return result;
}
};
+18 -1
View File
@@ -30,6 +30,17 @@ public:
~dePrinterConfigFactory() = default;
};
enum class ToolHeadComponent {
Extruder,
Nozzle,
Hotend
};
enum class ToolHeadNameCase {
TitleCase = 0,
SentenceCase = 1,
LowerCase = 2
};
class DevPrinterConfigUtil
{
@@ -70,6 +81,12 @@ public:
/*extruder*/
static bool get_printer_can_set_nozzle(std::string type_str) { return get_value_from_config<bool>(type_str, "enable_set_nozzle_info"); }// can set nozzle from studio
static std::string get_toolhead_display_name(
const std::string& type_str,
int ext_id,
ToolHeadComponent component,
ToolHeadNameCase name_case = ToolHeadNameCase::TitleCase,
bool short_name = false);
/*print job*/
static bool support_ams_ext_mix_print(std::string type_str) { return get_value_from_config<bool>(type_str, "print", "support_ams_ext_mix_print"); }
@@ -200,4 +217,4 @@ static std::string _parse_printer_type(const std::string& type_str)
return type_str;
}
};// namespace Slic3r
};// namespace Slic3r
+6
View File
@@ -6103,6 +6103,9 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
plate_data_item->is_label_object_enabled = m_plate_list[i]->m_gcode_result->label_object_enabled;
plate_data_item->limit_filament_maps = m_plate_list[i]->m_gcode_result->limit_filament_maps;
plate_data_item->layer_filaments = m_plate_list[i]->m_gcode_result->layer_filaments;
plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence;
plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence;
plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment;
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time);
Print *print = nullptr;
m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr);
@@ -6178,6 +6181,9 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
gcode_result->label_object_enabled = plate_data_list[i]->is_label_object_enabled;
gcode_result->timelapse_warning_code = plate_data_list[i]->timelapse_warning_code;
m_plate_list[index]->set_timelapse_warning_code(plate_data_list[i]->timelapse_warning_code);
gcode_result->filament_change_sequence = plate_data_list[i]->filament_change_sequence;
gcode_result->nozzle_change_sequence = plate_data_list[i]->nozzle_change_sequence;
gcode_result->optimal_assignment = plate_data_list[i]->optimal_assignment;
m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info;
gcode_result->warnings = plate_data_list[i]->warnings;
gcode_result->filament_maps = plate_data_list[i]->filament_maps;
+21 -2
View File
@@ -169,6 +169,8 @@
#include "DeviceCore/DevFilaSystem.h"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevDefs.h"
using boost::optional;
namespace fs = boost::filesystem;
@@ -443,6 +445,7 @@ struct ExtruderGroup : StaticGroup
}
void update_ams();
void SetTitle(const wxString& title);
void sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1);
@@ -1263,6 +1266,16 @@ void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> con
update_ams();
}
void ExtruderGroup::SetTitle(const wxString& title)
{
m_label = title;
int tW, tH, descent, externalLeading;
GetTextExtent(m_label.IsEmpty() ? "Orca" : m_label, &tW, &tH, &descent, &externalLeading, &m_font);
m_label_height = tH - externalLeading;
m_label_width = tW;
Refresh();
}
bool Sidebar::priv::switch_diameter(bool single)
{
wxString diameter;
@@ -1272,13 +1285,16 @@ bool Sidebar::priv::switch_diameter(bool single)
auto diameter_left = left_extruder->combo_diameter->GetValue();
auto diameter_right = right_extruder->combo_diameter->GetValue();
if (diameter_left != diameter_right) {
std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase));
auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase));
MessageDialog dlg(this->plater,
_L("The software does not support using different diameter of nozzles for one print. "
"If the left and right nozzles are inconsistent, we can only proceed with single-head printing. "
"Please confirm which nozzle you would like to use for this project."),
_L("Switch diameter"), wxYES_NO | wxNO_DEFAULT);
dlg.SetButtonLabel(wxID_YES, wxString::Format(_L("Left nozzle: %smm"), diameter_left));
dlg.SetButtonLabel(wxID_NO, wxString::Format(_L("Right nozzle: %smm"), diameter_right));
dlg.SetButtonLabel(wxID_YES, wxString::Format("%s: %smm", left_name, diameter_left));
dlg.SetButtonLabel(wxID_NO, wxString::Format("%s: %smm", right_name, diameter_right));
int result = dlg.ShowModal();
if (result == wxID_YES)
diameter = diameter_left;
@@ -2661,6 +2677,9 @@ void Sidebar::update_presets(Preset::Type preset_type)
};
auto image_path = get_cur_select_bed_image();
if (is_dual_extruder) {
std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle);
p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase)));
p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase)));
AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder);
AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder);
//if (!p->is_switching_diameter) {
+1 -1
View File
@@ -5153,7 +5153,7 @@ void PrinterInfoBox::UpdatePlate(const std::string& plate_name)
name = _L("Textured PEI Plate");
m_bed_image->SetBitmap(create_scaled_bitmap("bed_pei", this, 40));
}
else if (plate_name == "SuperTack Plate") {
else if (plate_name == "Supertack Plate" || plate_name == "SuperTack Plate") {
name = _L("Cool Plate (SuperTack)");
m_bed_image->SetBitmap(create_scaled_bitmap("bed_cool_supertack", this, 40));
}
+4 -2
View File
@@ -23,6 +23,7 @@
#include "PrePrintChecker.hpp"
#include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevFilaSystem.h"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevMapping.h"
@@ -1572,10 +1573,11 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
if (target_machine_nozzle_id < flow_type_of_machine.size()) {
if (flow_type_of_machine[target_machine_nozzle_id] != used_extruders_flow[it->first]) {
wxString pos;
auto sai_nz_pt = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
if (target_machine_nozzle_id == DEPUTY_EXTRUDER_ID) {
pos = _L("left nozzle");
pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase));
} else if ((target_machine_nozzle_id == MAIN_EXTRUDER_ID)) {
pos = _L("right nozzle");
pos = _L(DevPrinterConfigUtil::get_toolhead_display_name(sai_nz_pt, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase));
}
error_message = wxString::Format(_L("The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). "
+93 -6
View File
@@ -61,6 +61,7 @@
#endif // WIN32
#include <algorithm>
#include <cstdint>
namespace Slic3r {
@@ -499,6 +500,22 @@ void Tab::create_preset_tab()
}
#endif
if (dynamic_cast<TabFilament *>(this)) {
m_variant_combo = new MultiSwitchButton(panel);
m_variant_combo->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this](auto &evt) {
evt.Skip();
switch_excluder(evt.GetInt());
reload_config();
update_changed_ui();
toggle_options();
if (m_active_page)
m_active_page->update_visibility(m_mode, true);
m_page_view->GetParent()->Layout();
});
m_variant_combo->Hide();
m_main_sizer->Add(m_variant_combo, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, m_em_unit);
}
this->SetSizer(m_main_sizer);
//this->Layout();
m_page_view = m_parent->get_paged_view();
@@ -1295,6 +1312,8 @@ void Tab::msw_rescale()
{
m_mode_view->Rescale();
}
if (m_variant_combo)
m_variant_combo->Rescale();
if (m_detach_preset_btn)
m_detach_preset_btn->msw_rescale();
@@ -1360,6 +1379,8 @@ void Tab::sys_color_changed()
m_active_page->sys_color_changed();
if (m_extruder_switch)
m_extruder_switch->Rescale();
if (m_variant_combo)
m_variant_combo->Rescale();
//BBS: GUI refactor
//Layout();
@@ -3747,7 +3768,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
// "filament_seam_gap"
};
const int selection = 0; //m_variant_combo->GetSelection(); // TODO: Orca hack
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
auto opt = dynamic_cast<ConfigOptionVectorBase *>(m_config->option("filament_retraction_length"));
const int extruder_idx = selection < 0 || selection >= static_cast<int>(opt->size()) ? 0 : selection;
@@ -4277,9 +4298,11 @@ void TabFilament::toggle_options()
toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control"));
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", 0u);
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const unsigned int variant_idx = (unsigned int) std::max(selection, 0);
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx);
bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0";
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + 0u);
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx);
}
if (m_active_page->title() == L("Setting Overrides"))
@@ -4297,7 +4320,8 @@ void TabFilament::toggle_options()
toggle_option("filament_multitool_ramming_flow", multitool_ramming);
bool is_BBL_multi_extruder = is_BBL_printer && printer_cfg.option<ConfigOptionFloats>("nozzle_diameter")->size() > 1;
const int extruder_idx = 0; // m_variant_combo->GetSelection(); // TODO: Orca hack
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const int extruder_idx = std::max(selection, 0);
toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + extruder_idx);
toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", extruder_idx), 256 + extruder_idx);
}
@@ -6306,6 +6330,9 @@ bool Tab::tree_sel_change_delayed(wxCommandEvent& event)
if (m_extruder_switch) {
m_main_sizer->Show(m_extruder_switch, !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
} else if (m_variant_combo) {
m_main_sizer->Show(m_variant_combo, m_variant_combo->IsEnabled() && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
}
auto throw_if_canceled = std::function<void()>([this](){
@@ -6976,6 +7003,41 @@ void Tab::set_just_edit(bool just_edit)
/// </summary>
/// <param name="extruder_id"></param>
std::vector<wxString> Tab::generate_extruder_options()
{
std::vector<wxString> options;
if (m_type != Preset::TYPE_FILAMENT)
return options;
auto *variants = m_config->option<ConfigOptionStrings>("filament_extruder_variant");
if (!variants)
return options;
const std::vector<std::string> known_nozzle_types = {
get_nozzle_volume_type_string(NozzleVolumeType::nvtHighFlow),
get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard),
};
for (const std::string &variant : variants->values) {
std::string drive;
std::string nozzle;
for (const std::string &nozzle_type : known_nozzle_types) {
if (variant.size() > nozzle_type.size() &&
variant.substr(variant.size() - nozzle_type.size()) == nozzle_type &&
variant[variant.size() - nozzle_type.size() - 1] == ' ') {
drive = variant.substr(0, variant.size() - nozzle_type.size() - 1);
nozzle = nozzle_type;
break;
}
}
options.push_back(nozzle.empty() ? from_u8(variant) : wxString::Format(wxT("%s: %s"), from_u8(drive), from_u8(nozzle)));
}
return options;
}
void Tab::update_extruder_variants(int extruder_id)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << extruder_id;
@@ -7000,11 +7062,26 @@ void Tab::update_extruder_variants(int extruder_id)
GetParent()->Layout();
return;
}
} else if (m_variant_combo) {
if (extruder_id >= 0)
return;
const int selection = m_variant_combo->GetSelection();
auto options = generate_extruder_options();
m_variant_combo->SetOptions(options);
if (!options.empty())
m_variant_combo->SetSelection(selection < 0 || selection >= (int) options.size() ? 0 : selection);
m_variant_combo->Enable(options.size() > 1);
}
switch_excluder(extruder_id);
if (m_extruder_switch) {
m_main_sizer->Show(m_extruder_switch, m_active_page && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
} else if (m_variant_combo) {
m_main_sizer->Show(m_variant_combo, m_variant_combo->IsEnabled() && m_active_page && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
}
}
@@ -7018,7 +7095,7 @@ void Tab::switch_excluder(int extruder_id)
{}, {"", "filament_extruder_variant"}, // Preset::TYPE_FILAMENT filament don't use id anymore
{}, {"printer_extruder_id", "printer_extruder_variant"}, // Preset::TYPE_PRINTER
};
if (extruder_id >= nozzle_volumes->size() || extruder_id >= extruders->size())
if (!m_variant_combo && (extruder_id >= nozzle_volumes->size() || extruder_id >= extruders->size()))
extruder_id = 0;
if (m_extruder_switch && m_type != Preset::TYPE_PRINTER) {
int current_extruder = m_extruder_switch->GetValue() ? 1 : 0;
@@ -7026,15 +7103,25 @@ void Tab::switch_excluder(int extruder_id)
extruder_id = current_extruder;
else if (extruder_id != current_extruder)
return;
} else if (m_variant_combo) {
int current_variant = m_variant_combo->GetSelection();
if (current_variant < 0)
current_variant = 0;
if (extruder_id == -1)
extruder_id = current_variant;
else if (extruder_id != current_variant)
return;
}
auto get_index_for_extruder =
[this, &extruders, &nozzle_volumes, variant_keys = variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, int stride = 1) {
return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first,
ExtruderType(extruders->values[extruder_id]), NozzleVolumeType(nozzle_volumes->values[extruder_id]), variant_keys.second, stride);
};
auto index = get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id);
auto index = m_variant_combo ? extruder_id : get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id);
if (index < 0)
return;
if (m_variant_combo)
m_variant_combo->SetClientData(reinterpret_cast<void *>(static_cast<std::uintptr_t>(index)));
for (auto page : m_pages) {
bool is_extruder = false;
if (m_type == Preset::TYPE_PRINTER) {
+3
View File
@@ -45,6 +45,7 @@
class TabCtrl;
class ModeSwitchButton;
class SwitchButton;
class MultiSwitchButton;
namespace Slic3r {
@@ -305,6 +306,7 @@ public:
ModeSwitchButton *m_mode_view = nullptr;
SwitchButton *m_extruder_switch = nullptr;
MultiSwitchButton *m_variant_combo = nullptr;
public:
// BBS
@@ -426,6 +428,7 @@ public:
void update_extruder_variants(int extruder_id = -1);
void switch_excluder(int extruder_id = -1);
std::vector<wxString> generate_extruder_options();
protected:
void create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const std::string& path, widget_t widget);
+191 -1
View File
@@ -1,4 +1,5 @@
#include "SwitchButton.hpp"
#include "Button.hpp"
#include "Label.hpp"
#include "StaticBox.hpp"
@@ -19,7 +20,10 @@
#include <wx/dcclient.h>
#include <wx/dcgraph.h>
#include <algorithm>
wxDEFINE_EVENT(wxCUSTOMEVT_SWITCH_POS, wxCommandEvent);
wxDEFINE_EVENT(wxCUSTOMEVT_MULTISWITCH_SELECTION, wxCommandEvent);
SwitchButton::SwitchButton(wxWindow* parent, wxWindowID id)
: wxBitmapToggleButton(parent, id, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxBU_EXACTFIT)
@@ -391,6 +395,192 @@ void ModeSwitchButton::update_tooltip()
SetToolTip(m_tooltips[m_selection]);
}
MultiSwitchButton::MultiSwitchButton(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style)
: StaticBox(parent, id, pos, size, style)
, m_bg_color(StateColor(
std::make_pair(0xE8E8E8, (int) StateColor::NotChecked),
std::make_pair(0x009688, (int) StateColor::Normal)))
, m_text_color(StateColor(
std::make_pair(0x6B6B6B, (int) StateColor::NotChecked),
std::make_pair(0xFFFFFE, (int) StateColor::Normal)))
, m_button_radius(10.0)
, m_button_padding(10, 6)
{
SetCornerRadius(m_button_radius);
SetBorderWidth(0);
sizer = new wxBoxSizer(wxHORIZONTAL);
auto *hsizer = new wxBoxSizer(wxVERTICAL);
hsizer->Add(sizer, 1, wxEXPAND);
SetSizer(hsizer);
SetMinSize(wxSize(-1, 20));
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MultiSwitchButton::button_clicked, this);
SetFont(Label::Body_12);
}
MultiSwitchButton::~MultiSwitchButton()
{
DeleteAllOptions();
}
int MultiSwitchButton::AppendOption(const wxString &option, void *clientData)
{
Button *btn = new Button();
btn->Create(this, option, "", wxBORDER_NONE);
btn->SetFont(GetFont());
btn->SetBackgroundColor(m_bg_color);
btn->SetTextColor(m_text_color);
btn->SetCornerRadius(m_button_radius);
btn->SetPaddingSize(m_button_padding);
btn->SetClientData(clientData);
btns.push_back(btn);
sizer->Add(btn, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL);
wxSize text_size = btn->GetTextExtent(option);
btn->SetMinSize(wxSize(text_size.x + m_button_padding.x * 2 + 6, -1));
return int(btns.size()) - 1;
}
void MultiSwitchButton::SetOptions(const std::vector<wxString> &options)
{
DeleteAllOptions();
for (const auto &option : options)
AppendOption(option);
Layout();
Refresh();
}
void MultiSwitchButton::DeleteAllOptions()
{
sel = -1;
for (auto *btn : btns) {
if (btn)
btn->Destroy();
}
btns.clear();
if (sizer)
sizer->Clear();
}
unsigned int MultiSwitchButton::GetCount() const
{
return (unsigned int) btns.size();
}
int MultiSwitchButton::GetSelection() const
{
return sel;
}
void MultiSwitchButton::SetSelection(int index)
{
if (index < 0 || index >= (int) btns.size() || index == sel)
return;
sel = index;
update_button_styles();
send_selection_event();
Refresh();
}
wxString MultiSwitchButton::GetSelectedText() const
{
return sel >= 0 && sel < (int) btns.size() ? btns[sel]->GetLabel() : wxString();
}
wxString MultiSwitchButton::GetOptionText(unsigned int index) const
{
return index < btns.size() ? btns[index]->GetLabel() : wxString();
}
void MultiSwitchButton::SetOptionText(unsigned int index, const wxString &text)
{
if (index >= btns.size())
return;
btns[index]->SetLabel(text);
}
void *MultiSwitchButton::GetOptionData(unsigned int index) const
{
return index < btns.size() ? btns[index]->GetClientData() : nullptr;
}
void MultiSwitchButton::SetOptionData(unsigned int index, void *clientData)
{
if (index >= btns.size())
return;
btns[index]->SetClientData(clientData);
}
void MultiSwitchButton::update_button_styles()
{
for (int i = 0; i < (int) btns.size(); ++i) {
btns[i]->SetValue(i == sel);
btns[i]->SetBackgroundColor(m_bg_color);
btns[i]->SetTextColor(m_text_color);
btns[i]->Refresh();
}
}
void MultiSwitchButton::SetBackgroundColor(const StateColor &color)
{
m_bg_color = color;
update_button_styles();
}
void MultiSwitchButton::SetTextColor(const StateColor &color)
{
m_text_color = color;
update_button_styles();
}
void MultiSwitchButton::SetButtonCornerRadius(double radius)
{
m_button_radius = radius;
SetCornerRadius(radius);
for (auto *btn : btns)
btn->SetCornerRadius(radius);
Layout();
Refresh();
}
void MultiSwitchButton::SetButtonPadding(const wxSize &padding)
{
m_button_padding = padding;
for (auto *btn : btns)
btn->SetPaddingSize(padding);
Layout();
Refresh();
}
void MultiSwitchButton::Rescale()
{
for (auto *btn : btns)
btn->Rescale();
}
void MultiSwitchButton::button_clicked(wxCommandEvent &event)
{
SetFocus();
auto *btn = event.GetEventObject();
auto iter = std::find(btns.begin(), btns.end(), btn);
SetSelection(iter == btns.end() ? -1 : int(iter - btns.begin()));
}
bool MultiSwitchButton::send_selection_event()
{
wxCommandEvent evt(wxCUSTOMEVT_MULTISWITCH_SELECTION, GetId());
evt.SetEventObject(this);
evt.SetInt(sel);
evt.SetString(GetSelectedText());
GetEventHandler()->ProcessEvent(evt);
return true;
}
SwitchBoard::SwitchBoard(wxWindow *parent, wxString leftL, wxString right, wxSize size)
: wxWindow(parent, wxID_ANY, wxDefaultPosition, size)
{
@@ -554,4 +744,4 @@ bool SwitchBoard::Enable(bool enable /* = true */)
is_enable = enable;
Refresh();
return true;
}
}
+52
View File
@@ -5,9 +5,14 @@
#include "StateColor.hpp"
#include "StaticBox.hpp"
#include <vector>
#include <wx/sizer.h>
#include <wx/tglbtn.h>
wxDECLARE_EVENT(wxCUSTOMEVT_SWITCH_POS, wxCommandEvent);
wxDECLARE_EVENT(wxCUSTOMEVT_MULTISWITCH_SELECTION, wxCommandEvent);
class Button;
class SwitchButton : public wxBitmapToggleButton
{
@@ -76,6 +81,53 @@ private:
wxString m_tooltips[3];
};
class MultiSwitchButton : public StaticBox
{
public:
MultiSwitchButton(wxWindow *parent = nullptr, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition,
const wxSize &size = wxDefaultSize, long style = 0);
~MultiSwitchButton();
int AppendOption(const wxString &option, void *clientData = nullptr);
void SetOptions(const std::vector<wxString> &options);
void DeleteAllOptions();
unsigned int GetCount() const;
int GetSelection() const;
void SetSelection(int index);
wxString GetSelectedText() const;
wxString GetOptionText(unsigned int index) const;
void SetOptionText(unsigned int index, const wxString &text);
void *GetOptionData(unsigned int index) const;
void SetOptionData(unsigned int index, void *clientData);
void SetBackgroundColor(const StateColor &color);
void SetTextColor(const StateColor &color);
void SetButtonCornerRadius(double radius);
void SetButtonPadding(const wxSize &padding);
void Rescale();
protected:
void button_clicked(wxCommandEvent &event);
void update_button_styles();
bool send_selection_event();
private:
std::vector<Button *> btns;
wxBoxSizer *sizer = nullptr;
int sel = -1;
StateColor m_bg_color;
StateColor m_text_color;
double m_button_radius;
wxSize m_button_padding;
};
class SwitchBoard : public wxWindow
{
public: