Speed Dial: replace tile monograms with native SVG icons

Tiles previously rendered a colored monogram (title/source initials plus an ordinal) with a per-id hue. Show the matching native SVG icon instead:

- AppAction/NativeCommand gain an `icon` field; commands get a curated key->icon table and settings inherit their group header's icon.
- Notebook tracks each page's resource icon name and reports it in tab_options(), so the tab picker can show it too.
- Searcher records the group icon so settings keep it through search.
- Web tile rendering swaps monogramFor/hue for an <img>; drop the now-unused hue/text CSS vars. Add a test asserting every non-empty icon resolves to a shipped SVG.
This commit is contained in:
Lam Wei Lun
2026-09-11 11:36:24 +08:00
parent 792ab183e4
commit 42bee12481
15 changed files with 231 additions and 82 deletions
+18 -1
View File
@@ -232,6 +232,7 @@ private:
this->kind = AppActionKind::Command;
this->group = c.group;
this->input = c.input;
this->icon = c.icon;
}
};
@@ -554,6 +555,19 @@ void ActionRegistry::materialize_setting_actions()
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w), std::string(),
opt.category_local, boost::nowide::narrow(path), opt.mode);
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
// the one shown next to it in the page. Fall back to the page/category icon for groups
// without one. Keys are the English titles the GUI registers.
action->icon = opt.group_icon;
if (action->icon.empty() && !opt.category.empty()) {
if (Tab* tab = wxGetApp().get_tab(opt.type); tab) {
const auto& icons = tab->get_category_icon_map();
auto it = icons.find(wxString(opt.category));
if (it != icons.end())
action->icon = it->second;
}
}
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
action->count = it->value("count", 0);
@@ -738,6 +752,7 @@ nlohmann::json ActionRegistry::snapshot()
{"source", a->source_name()},
{"group", a->group},
{"input", a->input},
{"icon", a->icon},
{"shortcut", ""},
{"mode", mode_key(a->required_mode)}});
};
@@ -800,7 +815,9 @@ nlohmann::json ActionRegistry::tab_options() const
const wxString id = notebook->GetPageName(i);
if (id.empty())
continue;
out.push_back({{"id", id.ToStdString()}, {"title", notebook->GetPageText(i).ToStdString()}});
out.push_back({{"id", id.ToStdString()},
{"title", notebook->GetPageText(i).ToStdString()},
{"icon", notebook->GetPageIcon(i)}});
}
return out;
}
+3
View File
@@ -77,6 +77,9 @@ struct AppAction
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
std::string input;
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
std::string icon;
// Settings mode required to edit this action (SettingActions only). The palette prompts before
// running an action whose mode is above the user's current mode. comSimple for everything else.
ConfigOptionMode required_mode = comSimple;
+93 -1
View File
@@ -25,6 +25,7 @@
#include <cmath>
#include <cstdlib>
#include <exception>
#include <map>
#include <string>
#include <tuple>
#include <utility>
@@ -132,12 +133,103 @@ void select_mode(ConfigOptionMode mode)
app.app_config->save();
}
// Tile pictogram per command: the SVG base name of the icon the matching GUI control already uses
// (menu/toolbar/sidebar). Absent key => blank tile. Keeping this as one table makes the curation
// reviewable and lets a test check every value resolves to a real file.
const std::map<std::string, std::string>& command_icons()
{
static const std::map<std::string, std::string> icons = {
// Slice & Export
{"slice_and_preview", "media_play"},
{"export_gcode", "menu_export_gcode"},
{"export_stl", "menu_export_stl"},
{"export_stl_multi", "menu_export_stl"},
{"export_sliced_file", "menu_export_sliced_file"},
{"export_all_sliced_file", "menu_export_sliced_file"},
{"export_toolpaths_obj", "menu_export_toolpaths"},
{"export_config", "menu_export_config"},
{"export_3mf", "menu_save"},
{"export_drc_single", "menu_export_stl"},
{"export_drc_multi", "menu_export_stl"},
// Commands
{"load_project", "menu_open"},
{"save_project", "menu_save"},
{"save_project_as", "menu_save"},
{"open_preferences", "cog"},
{"go_to_layer", "height_range_layer"},
// Mode: the sidebar mode toggle's own icon (ParamsPanel).
{"mode_simple", "advanced"},
{"mode_advanced", "advanced"},
{"mode_expert", "advanced"},
{"toggle_developer_mode", "advanced"},
// Calibration
{"calib_temperature", "calib_sf"},
{"calib_max_volumetric", "calib_sf"},
{"calib_pressure_advance", "calib_sf"},
{"calib_flow_ratio", "calib_sf"},
{"calib_retraction", "calib_sf"},
{"calib_cornering", "calib_sf"},
{"calib_input_shaping_freq", "calib_sf"},
{"calib_input_shaping_damp", "calib_sf"},
{"calib_vfa", "calib_sf"},
// View
{"reset_window_layout", "toolbar_reset"},
// Object
{"obj_delete", "menu_delete"},
{"obj_delete_all", "menu_remove"},
{"obj_mirror_x", "menu_mirror_x"},
{"obj_mirror_y", "menu_mirror_y"},
{"obj_mirror_z", "menu_mirror_z"},
{"obj_split_objects", "menu_split_objects"},
{"obj_split_parts", "menu_split_parts"},
{"obj_drop", "toolbar_flatten"},
{"obj_instances_up", "instance_add"},
{"obj_instances_down", "instance_remove"},
{"obj_arrange", "toolbar_arrange"},
{"obj_orient", "toolbar_orient"},
// Add Primitive
{"add_primitive_cube", "menu_obj_cube"},
{"add_primitive_cylinder", "menu_obj_cylinder"},
{"add_primitive_sphere", "menu_obj_sphere"},
{"add_primitive_cone", "menu_obj_cone"},
{"add_primitive_disc", "menu_obj_disc"},
{"add_primitive_torus", "menu_obj_torus"},
{"add_primitive_text", "menu_obj_text"},
{"add_primitive_svg", "menu_obj_svg"},
// Plate
{"plate_add", "toolbar_add_plate"},
{"plate_duplicate", "menu_copy"},
{"plate_delete", "menu_delete"},
{"plate_rename", "plate_name_edit"},
{"plate_toggle_lock", "lock_normal"},
{"plate_goto", "go_next_plate"},
// Printer / Presets
{"sync_ams", "ams_fila_sync"},
{"sync_presets", "printer_sync_ok"},
{"preset_bundle", "menu_edit_preset"},
// Import
{"import_file", "menu_import"},
{"import_zip_archive", "menu_import"},
{"import_configs", "menu_import"},
// Help
{"help_open_config_folder", "folder-closed"},
{"help_tip_of_the_day", "info"},
{"help_check_updates", "ams_refresh_normal"},
{"help_about", "OrcaSlicer_about"},
{"open_wiki", "link_wiki_img"},
};
return icons;
}
std::vector<NativeCommand> build_command_catalog()
{
std::vector<NativeCommand> out;
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
std::string input = {}) {
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)});
std::string icon;
if (auto it = command_icons().find(key); it != command_icons().end())
icon = it->second;
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
};
// ---- Slice & Export ----
+3 -1
View File
@@ -10,13 +10,15 @@ namespace Slic3r { namespace GUI {
// A built-in speed-dial command: identity + how to run it. The registry keeps commands as thin
// values (CommandAction) and routes run() here, so this catalog is the single source of truth for
// the behaviour (runner => an owner method) and the presentation (title/group/input).
// the behaviour (runner => an owner method), the presentation (title/group/input), and the tile
// pictogram (icon = an SVG base name under resources/images, "" for no icon).
struct NativeCommand
{
std::string key;
std::string title;
std::string group;
std::string input; // "percent"/"tab" or "" for immediate run
std::string icon; // SVG base name, or "" to render a blank tile
std::function<AppActionRunResult(const std::string& param)> runner;
};
+2
View File
@@ -201,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
Slic3r::GUI::wxGetApp().UpdateDarkUI(btn);
m_pageButtons.insert(m_pageButtons.begin() + n, btn);
m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA
m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name);
m_buttons_sizer->Insert(n, new wxSizerItem(btn));
m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1);
m_sizer->Layout();
@@ -220,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
m_pageIcons.erase(m_pageIcons.begin() + n);
m_buttons_sizer->Remove(n);
#if __WXOSX__
RemoveChild(btn);
+10
View File
@@ -33,6 +33,8 @@ public:
void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const;
// Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap).
const std::string& GetPageIcon(size_t n) const { return m_pageIcons[n]; }
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
@@ -47,6 +49,7 @@ private:
int m_btn_margin;
int m_line_margin;
std::vector<wxString> m_pageLabels; // ORCA
std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty
wxWindow* m_overflow_button{nullptr}; // ORCA
};
@@ -241,6 +244,13 @@ public:
return GetBtnsListCtrl()->GetPageText(n);
}
// Resource icon name the page was inserted with; empty for pages added with a wxBitmap.
std::string GetPageIcon(size_t n) const
{
wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page"));
return GetBtnsListCtrl()->GetPageIcon(n);
}
virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override
{
return false;
+1 -1
View File
@@ -650,7 +650,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
m_opt_map.emplace(opt_id, pair);
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category());
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
return Option(*m_config->def()->get(opt_key), opt_id);
}
+4 -3
View File
@@ -100,7 +100,7 @@ void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type ty
if (!label.IsEmpty())
dst.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(),
_(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring(),
false, opt_mode});
};
@@ -394,6 +394,7 @@ static Option create_option(const std::string &opt_key, const wxString &label, P
(_(label) + suffix_local).ToStdWstring(),
gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(),
into_u8(gc.icon),
gc.category.ToStdWstring(),
GUI::Tab::translate_category(category, type).ToStdWstring()};
}
@@ -447,9 +448,9 @@ void OptionsSearcher::dlg_msw_rescale()
if (search_dialog) search_dialog->msw_rescale();
}
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category)
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
{
groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category};
groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category, icon};
}
//------------------------------------------
// SearchItem
+4 -1
View File
@@ -45,6 +45,7 @@ struct GroupAndCategory
{
wxString group;
wxString category;
wxString icon; // icon of the group's own header, or empty
};
struct Option
@@ -60,6 +61,7 @@ struct Option
std::wstring label_local;
std::wstring group;
std::wstring group_local;
std::string group_icon; // SVG base name of the group's own header icon, or empty
std::wstring category;
std::wstring category_local;
bool multi_category { false };
@@ -128,7 +130,8 @@ public:
bool search();
bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID);
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category);
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
const wxString &icon = wxEmptyString);
size_t size() const { return found_size(); }
+4 -4
View File
@@ -5050,7 +5050,7 @@ void TabPrinter::build_fff()
// Register by hand so the UnsavedChanges dialog can render a row for it.
wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title,
optgroup->config_category());
optgroup->config_category(), optgroup->icon);
}
}
@@ -5765,7 +5765,7 @@ if (is_marlin_flavor)
for (auto &group : m_pages[n_before_extruders]->m_optgroups) {
group->set_config_category_and_type(first_extruder_title, m_type);
for (auto &opt : group->opt_map())
searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title);
searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon);
}
Thaw();
@@ -7869,8 +7869,8 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent)
{
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area");
searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category);
searcher.add_key("bed_custom_model", m_type, gc.group, gc.category);
searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon);
searcher.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon);
}
return sizer;