diff --git a/resources/images/imex_remove.svg b/resources/images/imex_remove.svg index 6c18a21d66..3b1046c855 100644 --- a/resources/images/imex_remove.svg +++ b/resources/images/imex_remove.svg @@ -1 +1 @@ - + diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index c7725ff2b1..9a55f4ce63 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -49,7 +49,7 @@ inline constexpr const char* kImexPrimaryMode = "primary"; // "no such mode": a missing script is a mode with no script, and the row's index // still has to be reported to {imex_mode_index}. Callers that need tools already // treat an empty tools string as "no tools" (imex_primary_tool_for_mode() returns -1, -// parse_imex_active_tools() returns {}), which is the behaviour they had before. +// parse_imex_active_tools() returns {}), which is the behavior they had before. // * `ragged` separates "the profile omitted this row" from "the author wrote an empty // entry" — a distinction every open-coded call site used to lose. // @@ -293,7 +293,7 @@ std::map parse_imex_head_filament_map(const std::string& s); // // `pem` has one entry per logical filament slot, so its size is the slot count and the // result is always a valid index into it (or -1). An override naming a slot at or past -// that size cannot be honoured — every consumer indexes a per-filament array, and the +// that size cannot be honored — every consumer indexes a per-filament array, and the // ConfigOption get_at() several of them use clamps rather than failing, which would turn a // corrupt 3MF's "1:9999" into a silently wrong filament. Such an override is ignored and // the printer's own routing applies, exactly as if the override were absent. @@ -339,14 +339,14 @@ enum class ImexRole { Primary, Copy, Mirror, Span }; // `letter` is the suffix the role carries in the `imex_mode_active_tools` on-disk format // ("0:P,1:C,2:M,3:S"). That string is written into printer profiles and into 3MF projects, // so the letters are FORMAT: never renumber, rename or reorder them, only append. The table -// order is also the order the modes editor cycles its tiles through and lists its colour +// order is also the order the modes editor cycles its tiles through and lists its color // legend in, which is why Primary comes first. // // Everything that used to open-code the letters or a parallel small-int encoding of the // enum now goes through this table or through ImexRole itself, so adding a fifth role means // editing the enum, this table, and the places that must genuinely make a new decision // about it (the transform switch, the zone/marker classification, the editor's per-role -// colour + label) — not a scattered set of int mappings that fail silently when missed. +// color + label) — not a scattered set of int mappings that fail silently when missed. struct ImexRoleDesc { ImexRole role; char letter; @@ -584,7 +584,7 @@ Vec2d compute_imex_slice_offset(bool firmware_managed, // the one definition of "overlap" shared by the object and prime-tower placement // checks. Area-based: Clipper yields no result for shapes sharing only an edge, so // a hull flush against a zone boundary is NOT reported, it has to cross. That -// matches the long-standing object behaviour. An empty hull never violates. +// matches the long-standing object behavior. An empty hull never violates. bool imex_hull_violates_zones(const std::vector& zones, const Polygon& hull); } // namespace Slic3r diff --git a/src/libslic3r/IMEXZones.cpp b/src/libslic3r/IMEXZones.cpp index 33007a628a..718b8f5983 100644 --- a/src/libslic3r/IMEXZones.cpp +++ b/src/libslic3r/IMEXZones.cpp @@ -64,7 +64,7 @@ ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg, // (imex_head_filament_map). // // Only Primary / Copy / Mirror land in this map. A Span head shares the primary's zone - // instead of owning one, so it deliberately gets no entry (and hence no zone centre -- + // instead of owning one, so it deliberately gets no entry (and hence no zone center -- // see ImexZoneLayout::head_zone_centers), and an extra Primary entry beyond the first is // ignored. Any further role must decide here whether it owns a cell. std::map zone_roles; @@ -107,12 +107,12 @@ ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg, // that assignment when the printer is reconfigured as a 2-tool IDEX, and the // `primary < n_rows * n_cols` bound above then drops it from zone_roles. // - // Every zone, strip, ghost centre and slice offset below is derived from pri_col/pri_row, - // which would still be sitting at their (0,0) initialisers — a position no active tool + // Every zone, strip, ghost center and slice offset below is derived from pri_col/pri_row, + // which would still be sitting at their (0,0) initializers — a position no active tool // occupies. That yields a primary zone and a secondary zone that both claim the whole bed: // imex_primary_zone() reports the plate clear while the blocking boxes report it full, and // under imex_firmware_managed_zones compute_imex_slice_offset() shifts the slice by the bed - // centre. Degrade to "this plate has no IMEX zones" instead — the same answer the earlier + // center. Degrade to "this plate has no IMEX zones" instead — the same answer the earlier // early returns give, and the one every consumer already handles. if (out.primary_head < 0) return ImexZoneLayout{}; diff --git a/src/libslic3r/IMEXZones.hpp b/src/libslic3r/IMEXZones.hpp index d1181fc70b..45bc959101 100644 --- a/src/libslic3r/IMEXZones.hpp +++ b/src/libslic3r/IMEXZones.hpp @@ -27,18 +27,18 @@ struct ImexZoneLayout // Physical T-index of the active mode's Primary tool; -1 when the layout is empty. int primary_head = -1; - // Physical head -> XY centre of that head's zone. One entry per tool the mode makes + // Physical head -> XY center of that head's zone. One entry per tool the mode makes // active, Primary included. A Span tool has no entry: it shares the primary's zone - // rather than owning one. Ghost placement offsets by centre[head] - centre[primary]. + // rather than owning one. Ghost placement offsets by center[head] - center[primary]. // - // EXCEPTION, read before using the X component. These centres are built from the head's + // EXCEPTION, read before using the X component. These centers are built from the head's // OWN grid cell, but an AGGREGATED representative's zone rectangle is pinned to the // primary's column and then expanded to a full-width row strip. So for an aggregated head - // whose own column differs from the primary's, centre[head].x names a column its painted - // zone does not have, and `centre[head] - centre[primary]` carries a bogus X offset. + // whose own column differs from the primary's, center[head].x names a column its painted + // zone does not have, and `center[head] - center[primary]` carries a bogus X offset. // Reachable at imex_tools_per_gantry >= 3 when the primary-column tool on the aggregated // gantry is inactive (e.g. tpg=3, gantry_count=2, mode "0:P,1:S,4:M,5:M"). - // Both current consumers pin X to the primary's centre for aggregated heads -- + // Both current consumers pin X to the primary's center for aggregated heads -- // PartPlate::calc_imex_ghosts() via bed_x_center, GCodeViewer via sec_center_of() -- and a // new consumer must do the same until this is either pinned here or the aggregate flag is // exposed on the layout. diff --git a/src/slic3r/GUI/IMEXModesCtrl.cpp b/src/slic3r/GUI/IMEXModesCtrl.cpp index 19c7c7eded..4df2162e42 100644 --- a/src/slic3r/GUI/IMEXModesCtrl.cpp +++ b/src/slic3r/GUI/IMEXModesCtrl.cpp @@ -11,18 +11,24 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/wxExtensions.hpp" namespace Slic3r { namespace GUI { +// Icon size for every button in this control -- the help button beside the legend and the +// edit / remove buttons on each row. One constant so the three cannot drift apart. +static constexpr int kImexIconPx = 20; + IMEXModesCtrl::IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout) : wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout) { - // Pull the app's window-default colour explicitly. Without this, GTK gives + // Pull the app's window-default color explicitly. Without this, GTK gives // child wxPanels a slightly lighter "widget bg" instead of the app's dark // theme — making chromeless ScalableButtons inside the panel render with a - // visible light box around the icon. Sub-panels inherit this colour. + // visible light box around the icon. Sub-panels inherit this color. SetBackgroundColour(wxGetApp().get_window_default_clr()); m_outer = new wxBoxSizer(wxVERTICAL); @@ -38,15 +44,13 @@ IMEXModesCtrl::IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layou m_hdr_panel->SetBackgroundColour(GetBackgroundColour()); rebuild_info_and_header(); - auto* add_btn = new wxButton(this, wxID_ANY, _L("+ Add Mode"), - wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); }); - + // Add Mode is built by rebuild_info_and_header() into the info panel, above the rows: + // at the bottom it moved down the page every time a mode was added, so its position + // depended on how many modes you already had. m_outer->Add(m_info_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); m_outer->Add(m_hdr_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(2)); m_outer->Add(m_rows_sizer, 0, wxEXPAND); m_outer->AddSpacer(FromDIP(4)); - m_outer->Add(add_btn, 0); // SetSizer(), not SetSizerAndFit(): no row exists yet, so there is nothing to fit to -- // rows arrive from add_row() / load_from_config() / set_grid_size(). Deliberately NOT // followed by m_outer->SetSizeHints(this) once they do: this panel owns a sizer, so @@ -55,23 +59,37 @@ IMEXModesCtrl::IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layou // for via GetEffectiveMinSize(). An explicitly set min size takes PRIORITY over the best // size in that call, so a size hint would pin the reserved height to the row count that // happened to be on screen when it ran -- adding a mode after that would clip the bottom - // row and the "+ Add Mode" button, which is the very failure the hint is meant to avoid. + // row, which is the very failure the hint is meant to avoid. SetSizer(m_outer); } +// A wxTextCtrl cannot paint its own border on GTK -- Orca sidesteps that in ::TextInput by +// drawing one on a StaticBox, but that has no multiline form and nothing in the tree uses it +// that way. A one-pixel panel behind the control gives the same visible edge for both the +// single-line name and the multiline G-code box, in the color the settings fields above use: +// the inputs here were landing on GTK's near-black default, invisible against the panel. +static wxPanel* framed_input(wxWindow* parent) +{ + wxColour clr(0xDB, 0xDB, 0xDB); + if (wxGetApp().dark_mode()) + clr = StateColor::darkModeColorFor(clr); + auto* frame = new wxPanel(parent, wxID_ANY); + frame->SetBackgroundColour(clr); + frame->SetSizer(new wxBoxSizer(wxVERTICAL)); + return frame; +} + void IMEXModesCtrl::rebuild_info_and_header() { - // --- Info panel: instruction text + colour legend --- + // --- Info panel: help button + color legend --- m_info_panel->DestroyChildren(); auto* info_sizer = new wxBoxSizer(wxVERTICAL); - // Keep this to one line. Per-role detail lives in the legend tooltips below, so the - // panel does not open with a paragraph the user has to read before touching anything. + // The overview rides on the "?" button at the head of the legend rather than sitting in + // the panel as body text: it is read once and then only gets in the way, while the legend + // beside it is the part worth keeping on screen. Per-role detail is on the swatches. const wxString instructions = _L("Each mode names the tool heads that take part and the role each one plays. " - "Click a tool button to cycle its role — hover a colour below for what each role does."); - auto* inst = new wxStaticText(m_info_panel, wxID_ANY, instructions); - inst->Wrap(FromDIP(620)); - info_sizer->Add(inst, 0, wxBOTTOM, FromDIP(6)); + "Click a tool button to cycle its role — hover a color below for what each role does."); // Color legend — swatches sized to the body text height so they read as // matched pairs with their labels regardless of system DPI / font scale, @@ -80,6 +98,24 @@ void IMEXModesCtrl::rebuild_info_and_header() { // tiles cycle in. A role added to that table appears here with no edit of its own. auto* leg_sizer = new wxBoxSizer(wxHORIZONTAL); const int swatch_side = m_info_panel->GetCharHeight(); + + // Action row: Add Mode, then the "?" carrying the overview. Width-matched to the mode + // column so the button sits over the field it creates. + auto* act_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* add_btn = new Button(m_info_panel, _L("Add Mode")); + add_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Window); + // Width-matched to the mode column. The height has to be a real number: Button takes its + // min size literally, so a -1 there collapses it to a sliver rather than meaning "auto". + add_btn->SetMinSize(wxSize(FromDIP(130), add_btn->GetBestSize().GetHeight())); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); }); + act_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + + auto* help_btn = new ScalableButton(m_info_panel, wxID_ANY, "question", wxEmptyString, + wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx); + help_btn->SetToolTip(instructions); + act_sizer->Add(help_btn, 0, wxALIGN_CENTER_VERTICAL); + info_sizer->Add(act_sizer, 0, wxBOTTOM, FromDIP(6)); for (const ImexRoleDesc& d : kImexRoleTable) { if (!role_offered(d.role)) continue; const RoleStyle style = role_style(d.role); @@ -97,21 +133,21 @@ void IMEXModesCtrl::rebuild_info_and_header() { // --- Column header row --- // Spacers sized to align with the mode-row fields below. Name field: FromDIP(130) + FromDIP(6) - // gap; tool grid: n_cols*(FromDIP(36)+FromDIP(2))-FromDIP(2) + FromDIP(6) gap. grid_px tracks + // gap; tool grid: n_cols*(FromDIP(24)+FromDIP(2))-FromDIP(2) + FromDIP(6) gap. grid_px tracks // the live column count, so the G-code header stays aligned after a grid change. m_hdr_panel->DestroyChildren(); auto* hdr_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* hdr_name = new wxStaticText(m_hdr_panel, wxID_ANY, _L("Name")); + auto* hdr_name = new wxStaticText(m_hdr_panel, wxID_ANY, _L("Mode")); auto* hdr_tools = new wxStaticText(m_hdr_panel, wxID_ANY, _L("Tools")); auto* hdr_gcode = new wxStaticText(m_hdr_panel, wxID_ANY, _L("G-code")); - hdr_name->SetToolTip(_L("How the mode is labelled in the plate's IDEX/IQEX mode selector. " + hdr_name->SetToolTip(_L("How the mode is labeled in the plate's IDEX/IQEX mode selector. " "Required — a mode with no name cannot be selected.")); hdr_tools->SetToolTip(_L("Which tool heads take part in the mode and what role each one plays. " "Click a tile to cycle its role.")); hdr_gcode->SetToolTip(_L("G-code run at print start to put the printer into this mode " "(for example a Klipper SET_PRINT_MODE call). The slicer only emits " "the Primary tool's paths; the firmware drives the others.")); - const int grid_px = m_n_cols * FromDIP(38) - FromDIP(2); // approx grid panel width + const int grid_px = m_n_cols * FromDIP(26) - FromDIP(2); // tile 24 + 2px gap, less the trailing gap const int name_col_px = FromDIP(136); hdr_sizer->Add(hdr_name, 0, wxALIGN_CENTER_VERTICAL); hdr_sizer->AddSpacer(std::max(0, name_col_px - hdr_name->GetBestSize().x)); @@ -227,8 +263,8 @@ std::string IMEXModesCtrl::unique_mode_name(const std::vector& also } IMEXModesCtrl::RoleStyle IMEXModesCtrl::role_style(std::optional role) { - // Grey / "Inactive" is the no-role answer; every role gets an explicit case so a new - // one is a compile-time -Wswitch prompt rather than a tile that silently renders grey. + // Gray / "Inactive" is the no-role answer; every role gets an explicit case so a new + // one is a compile-time -Wswitch prompt rather than a tile that silently renders gray. if (!role) return { wxColour(90, 90, 90), *wxWHITE, "Inactive" }; switch (*role) { @@ -345,6 +381,11 @@ void IMEXModesCtrl::add_row(const std::string& name, r.panel = new wxPanel(this, wxID_ANY); r.panel->SetBackgroundColour(GetBackgroundColour()); auto* sizer = new wxBoxSizer(wxHORIZONTAL); + // The mode column is vertical: name on top, and for a deletable row the remove button + // beneath it. Remove used to sit next to Edit in the right-hand column, one icon apart + // from a button people press often -- a destructive action does not belong there. + auto* name_col = new wxBoxSizer(wxVERTICAL); + wxPanel* name_frame = nullptr; // border frame for the name field; null on the Primary row if (is_primary) { r.orig_name = kImexPrimaryMode; @@ -354,19 +395,26 @@ void IMEXModesCtrl::add_row(const std::string& name, wxFont f = lbl->GetFont(); f.SetWeight(wxFONTWEIGHT_BOLD); lbl->SetFont(f); - sizer->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + name_col->Add(lbl, 0, wxLEFT | wxRIGHT, 1); // match the 1px frame the other rows carry } else { // A nameless mode cannot be selected on a plate and used to be dropped on // save together with its tool roles and G-code, so a name is pre-filled // here and restored below if the field is left empty. That keeps the - // "+ Add Mode → assign tools → never typed a name" path from losing work + // "Add Mode → assign tools → never typed a name" path from losing work // without ever showing an error. const std::string nm = name.empty() ? unique_mode_name({}) : name; // The name as first shown, i.e. the one the plate's mode selector offered while this // row looked like this. remove_row() reports it alongside the current name, because // renaming is not routed through on_mode_removed and the two then diverge. r.orig_name = nm; - r.name = new wxTextCtrl(r.panel, wxID_ANY, from_u8(nm), wxDefaultPosition, FromDIP(wxSize(130, -1))); + name_frame = framed_input(r.panel); + r.name = new wxTextCtrl(name_frame, wxID_ANY, from_u8(nm), wxDefaultPosition, FromDIP(wxSize(130, -1))); + name_frame->GetSizer()->Add(r.name, 1, wxEXPAND | wxALL, 1); + // An unset background makes the field inherit GTK's widget default rather than the + // app's, which reads as a lighter box against this panel. Set the light-theme color + // explicitly, then let UpdateDarkUI swap it for the dark palette. + r.name->SetBackgroundColour(*wxWHITE); + wxGetApp().UpdateDarkUI(r.name); r.name->SetHint(_L("Mode name (required)")); r.name->SetToolTip(_L("Name of this parallel mode, as it appears in the plate's IDEX/IQEX mode " "selector. Stored in the project by name, so renaming a mode that " @@ -425,7 +473,7 @@ void IMEXModesCtrl::add_row(const std::string& name, if (it != tool_roles.end()) role = it->second; auto* btn = new wxButton(grid_panel, wxID_ANY, wxEmptyString, - wxDefaultPosition, FromDIP(wxSize(36, 26)), wxBU_EXACTFIT); + wxDefaultPosition, FromDIP(wxSize(24, 24)), wxBU_EXACTFIT); apply_btn(btn, tool_idx, role); int btn_pos = (int)r.btns.size(); @@ -493,11 +541,23 @@ void IMEXModesCtrl::add_row(const std::string& name, // from_u8(): `gcode` arrives from the preset as UTF-8. Handing the raw std::string to // wxString would decode it through the current locale's encoding instead. - r.gcode = new wxTextCtrl(r.panel, wxID_ANY, from_u8(gcode), - wxDefaultPosition, FromDIP(wxSize(220, 54)), wxTE_MULTILINE); + auto* gcode_frame = framed_input(r.panel); + // wxBORDER_NONE matters off GTK: a wxTextCtrl's default border resolves to a themed or + // sunken edge on Windows and to NSBezelBorder on macOS, which would sit immediately inside + // the frame below and read as two borders. + r.gcode = new wxTextCtrl(gcode_frame, wxID_ANY, from_u8(gcode), + wxDefaultPosition, FromDIP(wxSize(220, 54)), + wxTE_MULTILINE | wxBORDER_NONE); + gcode_frame->GetSizer()->Add(r.gcode, 1, wxEXPAND | wxALL, 1); + // Same monospace face EditGCodeDialog gives its editor, so G-code reads the same + // wherever it is edited; and the same explicit-light-then-UpdateDarkUI treatment as the + // name field above, for the same reason. + r.gcode->SetFont(wxGetApp().code_font()); + r.gcode->SetBackgroundColour(*wxWHITE); + wxGetApp().UpdateDarkUI(r.gcode); // Tooltip only, no SetHint(): wxTextEntry has no native placeholder for a // multiline control, so wxWidgets emulates one by writing the hint into the - // control as grey text — indistinguishable from real G-code in this box. + // control as gray text — indistinguishable from real G-code in this box. r.gcode->SetToolTip(_L("G-code emitted once at the start of a print that uses this mode, before " "the machine start G-code. This is where the printer is put into the " "matching firmware mode — for example a Klipper SET_PRINT_MODE call or a " @@ -508,17 +568,22 @@ void IMEXModesCtrl::add_row(const std::string& name, r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); }); if (!is_primary) { - sizer->Add(r.name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + name_frame->SetSizerAndFit(name_frame->GetSizer()); + name_col->Add(name_frame, 0); } + sizer->Add(name_col, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); - sizer->Add(r.gcode, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + sizer->Add(gcode_frame, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - // Edit + remove (for non-primary) — stacked vertically. + // Edit on top, per-row reset under it, in one column top-aligned with the G-code box. + // Remove is not here -- it sits under the name field, away from the button people press + // most. The column is top- rather than centre-justified so the edit icon keeps the same + // position whatever a row's height turns out to be. auto* btn_col = new wxBoxSizer(wxVERTICAL); wxTextCtrl* gcode_ctrl = r.gcode; auto* ph_btn = new ScalableButton(r.panel, wxID_ANY, "edit", wxEmptyString, wxDefaultSize, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, 16); + wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx); ph_btn->SetToolTip(_L("Edit G-code / browse placeholders")); ph_btn->Bind(wxEVT_BUTTON, [this, gcode_ctrl](wxCommandEvent&) { // EditGCodeDialog takes and returns UTF-8 (get_edited_gcode() is a ToUTF8()). @@ -531,7 +596,7 @@ void IMEXModesCtrl::add_row(const std::string& name, if (!is_primary) { auto* rm = new ScalableButton(r.panel, wxID_ANY, "imex_remove", wxEmptyString, wxDefaultSize, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, 16); + wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx); rm->SetToolTip(_L("Remove mode")); rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) { const std::vector removed = remove_row(this_panel); @@ -544,27 +609,22 @@ void IMEXModesCtrl::add_row(const std::string& name, if (removed_cb && !removed.empty()) removed_cb(removed); }); - btn_col->Add(rm, 0); + name_col->Add(rm, 0, wxALIGN_RIGHT | wxTOP, FromDIP(2)); } - sizer->Add(btn_col, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - - // Per-row reset gets its own column on the right so the icon reads as - // distinct from the edit/remove column. Reset only renders when this row - // has a counterpart in the saved preset (user-added rows beyond the saved - // mode count get no reset — the X button covers "remove user-added row"). - auto* reset_col = new wxBoxSizer(wxVERTICAL); + // Reset only renders when this row has a counterpart in the saved preset (user-added rows + // beyond the saved mode count get no reset — remove covers "drop the row I just added"). wxPanel* this_panel_for_reset = r.panel; if (row_has_parent_counterpart(static_cast(m_rows.size()))) { r.reset_btn = new ScalableButton(r.panel, wxID_ANY, "dot", wxEmptyString, wxDefaultSize, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, 16); + wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx); r.reset_btn->SetToolTip(_L("Discard in-session edits to this mode (snap back to saved value)")); r.reset_btn->Bind(wxEVT_BUTTON, [this, this_panel_for_reset](wxCommandEvent&) { reset_row_to_parent(this_panel_for_reset); }); - reset_col->Add(r.reset_btn, 0, wxALIGN_CENTER_VERTICAL); + btn_col->Add(r.reset_btn, 0, wxTOP, FromDIP(2)); } - sizer->Add(reset_col, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(btn_col, 0, wxALIGN_TOP | wxRIGHT, FromDIP(4)); r.panel->SetSizerAndFit(sizer); m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); @@ -606,7 +666,7 @@ std::vector IMEXModesCtrl::remove_row(wxPanel* panel) { Layout(); // Defer widget destruction so any in-flight GTK events for - // panel's children (including the × button we're inside) finish + // panel's children (including the remove button we're inside) finish // processing before the wxEvtHandlers are freed. wxTheApp->CallAfter([panel]() { panel->Destroy(); }); return removed_names; diff --git a/src/slic3r/GUI/IMEXModesCtrl.hpp b/src/slic3r/GUI/IMEXModesCtrl.hpp index f449843ebd..7bff02fc35 100644 --- a/src/slic3r/GUI/IMEXModesCtrl.hpp +++ b/src/slic3r/GUI/IMEXModesCtrl.hpp @@ -25,13 +25,13 @@ namespace GUI { // IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab // // A tool tile holds an `std::optional`: the role the mode gives that tool, or -// nullopt for Inactive (grey — the tool does not take part in this mode). Inactive is the +// nullopt for Inactive (gray — the tool does not take part in this mode). Inactive is the // editor's own state and the only reason the tile is not a bare ImexRole; it is the absence // of a role, so it is spelled as one rather than as an extra enumerator that libslic3r would // then have to keep rejecting. Roles themselves are never re-encoded here — no int state // table, no second copy of the on-disk letters. // -// nullopt (grey) — not participating in this mode +// nullopt (gray) — not participating in this mode // ImexRole::Primary (green) — the tool the slicer generates paths for // ImexRole::Copy (blue) — firmware duplicates Primary at an offset // ImexRole::Mirror (amber) — firmware mirrors Primary about an axis @@ -41,7 +41,7 @@ namespace GUI { // offered on tools sharing primary's gantry, and // only on multi-gantry printers (n_rows >= 2). // -// Colour and legend text are the editor's business and live in role_style(); the tile cycle +// Color and legend text are the editor's business and live in role_style(); the tile cycle // order and the serialized letters come from kImexRoleTable in IMEXHelpers.hpp, so the // editor cannot drift from the slicer's reading of a mode string. // @@ -128,8 +128,8 @@ private: // opened under a different language. std::string unique_mode_name(const std::vector& also_taken) const; - // Everything the editor draws for one role, in ONE place: a new role needs a colour and - // a legend name here and nowhere else in this file. nullopt is Inactive (grey). + // Everything the editor draws for one role, in ONE place: a new role needs a color and + // a legend name here and nowhere else in this file. nullopt is Inactive (gray). // `label` is deliberately untranslated — it names a role, and the legend sits next to // the same letters the preset stores. struct RoleStyle { wxColour bg; wxColour fg; const char* label; }; @@ -216,7 +216,7 @@ private: // only tools and gcode are restored. void reset_row_to_parent(wxPanel* panel); - // Rebuild the instruction text, colour legend and column headers for the CURRENT grid. + // Rebuild the instruction text, color legend and column headers for the CURRENT grid. // These depend on the grid shape -- the Span role is only offered on a multi-gantry printer, // and the header spacing is derived from the column count -- so they cannot be built once in // the constructor: adding a second gantry has to reveal Span without a save-and-reopen cycle. diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index cad36c3ed4..b616c146d3 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1272,11 +1272,18 @@ void PartPlate::render_imex_zones(bool force_default_color) }; // Standard (Okabe-Ito orange + sky blue) + // Alphas are deliberately low: these fills cover whole quadrants of the bed and sit under + // the model for the entire session, so at the saturation that reads well on a swatch they + // dominate the scene. The zone a tool owns only has to be legible, not loud. + // The three themes below keep their original alphas -- they are chosen for discriminability + // (and k_high_contrast explicitly for low vision), which is the opposite trade. static const IMEXTheme k_standard = { - { 0.337f, 0.706f, 0.914f, 0.45f }, // copy — sky blue #56B4E9 - { 0.902f, 0.624f, 0.000f, 0.45f }, // mirror — orange #E69F00 - { 0.850f, 0.100f, 0.100f, 0.55f }, // danger — red - { 0.300f, 0.900f, 0.200f, 0.40f }, // margin — lime green + { 0.337f, 0.706f, 0.914f, 0.22f }, // copy — sky blue #56B4E9 + { 0.902f, 0.624f, 0.000f, 0.22f }, // mirror — orange #E69F00 + // Dimmed less than the fills: this strip marks where a head collides, so it stays the + // most prominent thing on the bed even after the surrounding zones recede. + { 0.850f, 0.100f, 0.100f, 0.40f }, // danger — red + { 0.300f, 0.900f, 0.200f, 0.20f }, // margin — lime green }; // Deuteranopia / Protanopia: avoids red-green confusion. // Danger strip uses strong blue-violet (red invisible to protanopes). diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c505e9e35c..fa3b2f4ffe 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5836,7 +5836,13 @@ if (is_marlin_flavor) plater->update(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND); + // Align the left edge with the option-line labels above. A full-width line's + // widget is inset by 15 (OptionsGroup::append_line), while an ordinary line's + // custom control is inset by 10 and then paints its label further in again, so + // the two do not line up without this. One number, tuned against the settings + // page. Unscaled on purpose: the 15 and 10 it compensates for are raw pixels too, + // so a DIP-scaled correction here would drift apart from them on a HiDPI display. + sizer->Add(m_imex_modes_ctrl, 1, wxEXPAND | wxLEFT, 8); return sizer; }; modes_og->append_line(line);