mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
Rework the parallel modes editor from the UI review
Addresses the interface notes on the IDEX/IQEX modes editor. Add Mode moves from below the rows to the top of the panel, beside a "?" button that now carries the overview text as its tooltip. At the bottom the button shifted down the page every time a mode was added, so where it sat depended on how many modes already existed. It is an Orca Button in the Confirm style, width matched to the mode column it creates a row in, and the panel opens on the legend rather than on a paragraph. Remove moves out of the right-hand column, where it sat one icon away from Edit -- a destructive control beside the one pressed most -- to under the name field it deletes, and its icon becomes a boxed minus rather than an X, which read as "close". Reset joins Edit in the right-hand column, which is now top-aligned so the icons hold position regardless of row height. Tool tiles are square at 24px, and the header spacer tracks that width so the column titles stay over their columns when the grid changes shape. The two text fields were landing on GTK's near-black default border, invisible against the panel: measured 45,45,49 against a 43,43,43 background, where the settings fields above use 74,74,81. wxTextCtrl cannot color its own border, so each sits in a one pixel frame taking the color TextInput derives for the theme, and carries wxBORDER_NONE so Windows and macOS do not draw a native edge inside it. The G-code boxes also take the monospace face EditGCodeDialog uses. Bed zone fills drop to roughly half opacity in the Standard theme. They cover whole quadrants for a whole session, so at swatch saturation they dominate the scene. The collision strip is dimmed less, since it marks where a head hits something. The deuteranopia, tritanopia and high contrast themes keep their alphas: those are chosen for discriminability, which is the opposite trade. Also fixes the icon size never applying. All four ScalableButton call sites passed eight arguments, so the size bound to use_default_disabled_bitmap and bmp_px_cnt kept its default of 16. Both are passed now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7fad16137
commit
dafc4ce39c
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><line x1="2.5" y1="2.5" x2="13.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/><line x1="13.5" y1="2.5" x2="2.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="0.5" width="14" height="14" rx="1" ry="1" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/><line x1="4.5" y1="7.5" x2="10.5" y2="7.5" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round;opacity:1"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 368 B |
@@ -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<int,int> 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<BoundingBoxf3>& zones, const Polygon& hull);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -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<int, ImexRole> 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{};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<std::string>& also
|
||||
}
|
||||
|
||||
IMEXModesCtrl::RoleStyle IMEXModesCtrl::role_style(std::optional<ImexRole> 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<std::string> 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<int>(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<std::string> 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;
|
||||
|
||||
@@ -25,13 +25,13 @@ namespace GUI {
|
||||
// IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab
|
||||
//
|
||||
// A tool tile holds an `std::optional<ImexRole>`: 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<std::string>& 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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user