mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 17:26:47 +00:00
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:
@@ -222,42 +222,29 @@ function shouldRenderActionList(query) {
|
||||
return !!((query || "").trim());
|
||||
}
|
||||
|
||||
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
|
||||
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
|
||||
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
|
||||
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
|
||||
function monogramFor(item, list, titleOf, sourceOf, idOf) {
|
||||
var items = list || [];
|
||||
var title = titleOf(item) || " ";
|
||||
var ti = title.charAt(0).toUpperCase();
|
||||
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
|
||||
if (sameTitle.length <= 1)
|
||||
return ti;
|
||||
var source = sourceOf(item) || " ";
|
||||
var pi = source.charAt(0).toUpperCase();
|
||||
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
|
||||
if (sameSource.length <= 1)
|
||||
return pi + ti;
|
||||
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
|
||||
for (var i = 0; i < sameSource.length; i++)
|
||||
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
|
||||
return pi + ti + (i + 1);
|
||||
return pi + ti;
|
||||
// Tile pictogram base path. The page lives at resources/web/dialog/SpeedDial/, so this climbs to
|
||||
// resources/images/ where the same SVG icons the native GUI controls use are shipped.
|
||||
var ICON_BASE = "../../../images/";
|
||||
|
||||
// SVG base name for an action's tile pictogram, or "" when it has none (commands without a GUI
|
||||
// icon, plugins). Pure so the node-vm test can exercise it.
|
||||
function actionIcon(a) {
|
||||
return (a && a.icon) ? a.icon : "";
|
||||
}
|
||||
|
||||
// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so
|
||||
// they share this ladder (title initial, then source, then a stable ordinal).
|
||||
function tileCode(action, actions) {
|
||||
return monogramFor(action, actions,
|
||||
function (o) { return o.title; },
|
||||
function (o) { return o.source; },
|
||||
function (o) { return o.id; });
|
||||
}
|
||||
|
||||
// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row
|
||||
// with no backing action) renders an empty tile.
|
||||
function fillTile(tile, a) {
|
||||
tile.textContent = a ? tileCode(a, ACTIONS) : "";
|
||||
// Put a pictogram into a tile (search row, favourites tile, or tab row). No icon leaves the tile
|
||||
// blank. `mono` marks the white tab-strip glyphs, which the CSS recolors to the shared gray.
|
||||
function fillTile(tile, a, mono) {
|
||||
tile.textContent = "";
|
||||
var icon = actionIcon(a);
|
||||
if (!icon)
|
||||
return;
|
||||
var img = document.createElement("img");
|
||||
img.className = mono ? "tile-icon tab-mono" : "tile-icon";
|
||||
img.src = ICON_BASE + icon + ".svg";
|
||||
img.alt = "";
|
||||
img.setAttribute("aria-hidden", "true");
|
||||
tile.appendChild(img);
|
||||
}
|
||||
|
||||
// The active list for the main phase. A typed query ranks every action (commands/plugins/settings)
|
||||
@@ -461,13 +448,6 @@ function currentList() {
|
||||
return []; // percent - the input itself is the only field
|
||||
}
|
||||
|
||||
function hue(id) {
|
||||
var h = 0;
|
||||
for (var i = 0; i < id.length; i++)
|
||||
h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
||||
return h % 360;
|
||||
}
|
||||
|
||||
// Build a <div class=className> with the search-match ranges wrapped in <mark>. Used for both the
|
||||
// title and the source eyebrow. Pure (only touches the document factory), so the node-vm test never
|
||||
// calls it and load-time stays DOM-free.
|
||||
@@ -528,7 +508,6 @@ function renderFav() {
|
||||
var a = byId(id);
|
||||
var tile = document.createElement("button");
|
||||
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
||||
tile.style.setProperty("--h", hue(id));
|
||||
fillTile(tile, a);
|
||||
var tileBadge = modeBadge(a, USER_MODE);
|
||||
tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : "");
|
||||
@@ -629,7 +608,6 @@ function renderActionRow(a, i) {
|
||||
|
||||
var tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
tile.style.setProperty("--h", hue(a.id));
|
||||
fillTile(tile, a);
|
||||
|
||||
var left = document.createElement("div");
|
||||
@@ -805,8 +783,7 @@ function renderTabRow(t, i) {
|
||||
|
||||
var tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
tile.style.setProperty("--h", hue(t.id));
|
||||
fillTile(tile, null);
|
||||
fillTile(tile, t, true);
|
||||
|
||||
var left = document.createElement("div");
|
||||
left.className = "row-left";
|
||||
|
||||
@@ -195,21 +195,15 @@ assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b"
|
||||
assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar");
|
||||
assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing");
|
||||
|
||||
// tileCode: monogram ladder - title initial, then title+source initials, then a stable ordinal
|
||||
// by id. The ordinal is keyed by id, not by list order, so frecency reshuffles never renumber tiles.
|
||||
const monoPool = [
|
||||
{ id: "z", title: "Repair", source: "Mesh Tools" },
|
||||
{ id: "a", title: "Repair", source: "Mesh Tools" },
|
||||
{ id: "b", title: "Repair", source: "Filament" }
|
||||
];
|
||||
assert.equal(ctx.tileCode(monoPool[0], monoPool), "MR2",
|
||||
"same title+source resolves to source+title initials with an id-keyed ordinal (id z sorts after id a)");
|
||||
assert.equal(ctx.tileCode(monoPool[1], monoPool), "MR1",
|
||||
"the earlier id is numbered first among same-title+source tiles");
|
||||
assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR",
|
||||
"same title but different source resolves to source+title initials");
|
||||
assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]),
|
||||
"S", "a unique title resolves to the bare title initial");
|
||||
// actionIcon: the SVG base name for a tile's pictogram, or "" when the action has none (blank tile).
|
||||
assert.equal(ctx.actionIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play",
|
||||
"an action's icon base name is returned verbatim");
|
||||
assert.equal(ctx.actionIcon({ id: "x", title: "Go to tab...", icon: "" }), "",
|
||||
"an empty icon renders a blank tile");
|
||||
assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "",
|
||||
"a missing icon field renders a blank tile");
|
||||
assert.equal(ctx.actionIcon(null), "",
|
||||
"a null action (tab row) renders a blank tile");
|
||||
|
||||
// needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode.
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode");
|
||||
|
||||
@@ -60,9 +60,7 @@ body {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
|
||||
font-weight: 700;
|
||||
/* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits
|
||||
/* why: mirror .tile centering - icons/placeholder are 16px, and a bare <button> inherits
|
||||
13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
@@ -310,9 +308,6 @@ body {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -320,6 +315,20 @@ body {
|
||||
border: 1px solid var(--speed-tile-border, #d8d8d8);
|
||||
}
|
||||
|
||||
/* Native SVG pictogram in a tile; blank tiles (no icon) have no child. */
|
||||
.tile-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tab-strip glyphs are drawn white for the dark tab bar; recolor to the shared #949494 gray so
|
||||
they read on both the light and dark tile backgrounds. */
|
||||
.tile-icon.tab-mono {
|
||||
filter: brightness(0) invert(.58);
|
||||
}
|
||||
|
||||
.row-left {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #efefef;
|
||||
--speed-tile-border: #d8d8d8;
|
||||
--speed-tile-text-s: 74%;
|
||||
--speed-tile-text-l: 34%;
|
||||
}
|
||||
|
||||
:root[data-orca-theme="dark"] {
|
||||
@@ -95,8 +93,6 @@
|
||||
/* Speed dial icon tile treatment */
|
||||
--speed-tile-bg: #34343b;
|
||||
--speed-tile-border: #50505a;
|
||||
--speed-tile-text-s: 82%;
|
||||
--speed-tile-text-l: 84%;
|
||||
}
|
||||
|
||||
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "slic3r/GUI/ActionRegistry.hpp"
|
||||
#include "slic3r/GUI/NativeCommands.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -113,6 +115,47 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[speeddi
|
||||
}
|
||||
}
|
||||
|
||||
// Every command's tile pictogram is the SVG the matching GUI control already uses; an absent icon
|
||||
// means a blank tile (like the tab picker). Guard representative names and that every non-empty
|
||||
// value resolves to a shipped file, so a rename/typo cannot leave broken images in the palette.
|
||||
TEST_CASE("Native command icons resolve to shipped SVGs", "[speeddial][actions]")
|
||||
{
|
||||
const std::vector<Slic3r::GUI::NativeCommand>& commands = Slic3r::GUI::NativeCommands::catalog();
|
||||
auto icon_of = [&commands](const std::string& key) -> const std::string* {
|
||||
for (const auto& c : commands)
|
||||
if (c.key == key)
|
||||
return &c.icon;
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
struct Expected
|
||||
{
|
||||
const char* key;
|
||||
const char* icon;
|
||||
};
|
||||
for (const Expected& e : {Expected{"load_project", "menu_open"},
|
||||
Expected{"save_project", "menu_save"},
|
||||
Expected{"sync_ams", "ams_fila_sync"},
|
||||
Expected{"mode_simple", "advanced"},
|
||||
Expected{"calib_temperature", "calib_sf"},
|
||||
Expected{"plate_add", "toolbar_add_plate"},
|
||||
Expected{"add_primitive_cube", "menu_obj_cube"},
|
||||
Expected{"go_to_tab", ""}}) {
|
||||
const std::string* icon = icon_of(e.key);
|
||||
INFO(e.key);
|
||||
REQUIRE(icon != nullptr);
|
||||
CHECK(*icon == e.icon);
|
||||
}
|
||||
|
||||
const boost::filesystem::path images = boost::filesystem::path(PROFILES_DIR).parent_path() / "images";
|
||||
for (const auto& c : commands) {
|
||||
if (c.icon.empty())
|
||||
continue;
|
||||
INFO(c.key << " -> " << c.icon);
|
||||
CHECK(boost::filesystem::exists(images / (c.icon + ".svg")));
|
||||
}
|
||||
}
|
||||
|
||||
// The Help-menu commands, wiki/YouTube links and the developer-mode toggle are part of the palette.
|
||||
// Guard their presence and that they stay grouped with their peers, so a catalog edit cannot drop
|
||||
// or scatter them. Groups are compared to the peer's own group to stay independent of translation.
|
||||
|
||||
Reference in New Issue
Block a user