From f76e4b1e02885e987f9600190b904d0b36d46f99 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 16 Sep 2026 12:15:27 +0800 Subject: [PATCH] Fixed centering of icons. Disabled zooming in/out on Windows. Added default icons --- resources/images/action_default.svg | 2 + resources/web/dialog/SpeedDial/speeddial.js | 18 ++++-- .../web/dialog/SpeedDial/speeddial.test.js | 11 ++++ resources/web/dialog/SpeedDial/style.css | 5 +- src/slic3r/GUI/NativeCommands.cpp | 56 ++++++++++--------- src/slic3r/GUI/SpeedDialDialog.cpp | 4 ++ tests/slic3rutils/test_action_source.cpp | 17 ++++-- 7 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 resources/images/action_default.svg diff --git a/resources/images/action_default.svg b/resources/images/action_default.svg new file mode 100644 index 0000000000..118f4792a6 --- /dev/null +++ b/resources/images/action_default.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 64550c9e5c..96725c8a8c 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -294,13 +294,21 @@ function actionIcon(a) { return (a && a.icon) ? a.icon : ""; } -// 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. +// Pictogram shown when an action carries none (plugins, icon-less commands/settings). A dedicated +// theme-neutral glyph (resources/images/action_default.svg: frame + ">_" prompt), so no tile is +// blank and no existing action's icon is borrowed. +var DEFAULT_ICON = "action_default"; + +// SVG base name a tile actually renders: the action's own icon, else the placeholder. Pure. +function tileIcon(a) { + return actionIcon(a) || DEFAULT_ICON; +} + +// Put a pictogram into a tile (search row, favourites tile, or tab row). `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 icon = tileIcon(a); var img = document.createElement("img"); img.className = mono ? "tile-icon tab-mono" : "tile-icon"; img.src = ICON_BASE + icon + ".svg"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 38f7c343ae..4c2f606843 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -281,6 +281,17 @@ assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "", assert.equal(ctx.actionIcon(null), "", "a null action (tab row) renders a blank tile"); +// tileIcon: the base name a tile renders - the action's own icon when present, else the placeholder. +assert.equal(ctx.tileIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play", + "an action with an icon keeps it"); +assert.equal(ctx.tileIcon({ id: "x", title: "Go to tab...", icon: "" }), "action_default", + "an empty icon falls back to the placeholder"); +assert.equal(ctx.tileIcon({ id: "x", title: "Plugin action" }), "action_default", + "a missing icon falls back to the placeholder"); +assert.equal(ctx.DEFAULT_ICON, "action_default", "the placeholder is the dedicated default glyph"); +assert.ok(fs.existsSync(__dirname + "/../../../images/action_default.svg"), + "the placeholder SVG ships alongside the page's other icons"); + // 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"); assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 5c29e68cdb..4373ff7067 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -331,12 +331,15 @@ body { border: 1px solid var(--speed-tile-border, #d8d8d8); } -/* Native SVG pictogram in a tile; blank tiles (no icon) have no child. */ +/* Native SVG pictogram in a tile. The Orca icon grid draws 1px strokes from 0.5..14.5 of the 16 + viewBox, so the visible glyph is 0..15 and, centered as-is, leaves one extra pixel on the right/ + bottom (the "Save Project leans left" look). Shift by half a pixel to center the visible artwork. */ .tile-icon { width: 16px; height: 16px; display: block; pointer-events: none; + transform: translate(.5px, .5px); } /* Tab-strip glyphs are drawn white for the dark tab bar; recolor to the shared #949494 gray so diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index ea833acae5..2e24f14326 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -236,12 +236,12 @@ std::vector build_command_catalog() }); // ---- Export pipeline ---- - add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "menu_export_gcode", [](const std::string&) { + add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -251,35 +251,37 @@ std::vector build_command_catalog() plater->export_core_3mf(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { + add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(false); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "menu_export_sliced_file", [](const std::string&) { + add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Calibration ---- - add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "calib_sf", + // The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's + // light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green). + add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature", [](const std::string&) { return calib_command(CalibKind::Temperature); }); - add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed", [](const std::string&) { return calib_command(CalibKind::MaxVolumetric); }); - add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::PressureAdvance); }); - add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance", [](const std::string&) { return calib_command(CalibKind::FlowRatio); }); - add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction", [](const std::string&) { return calib_command(CalibKind::Retraction); }); - add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision", [](const std::string&) { return calib_command(CalibKind::Cornering); }); - add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); - add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "calib_sf", + add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance", [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); - add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "calib_sf", [](const std::string&) { return calib_command(CalibKind::VFA); }); + add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); }); // ---- View ---- // Titles are built with _u8L here (not via a variable) so xgettext can extract them. @@ -322,10 +324,10 @@ std::vector build_command_catalog() }); // ---- Object ---- - add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "menu_delete", [](const std::string&) { + add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) { return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); }); }); - add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "menu_remove", [](const std::string&) { + add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) { return object_op( wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); }); }); @@ -440,7 +442,7 @@ std::vector build_command_catalog() plater->duplicate_plate(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "menu_delete", [](const std::string&) { + add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) { Plater* plater = wxGetApp().plater(); if (!is_fff_plater(plater)) return plate_unavailable(); @@ -511,7 +513,7 @@ std::vector build_command_catalog() }); // ---- Import ---- - add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) { #ifdef __APPLE__ plater->add_model(); @@ -521,39 +523,39 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->import_zip_archive(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_import", [](const std::string&) { + add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->load_config_file(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); // ---- Export extras ---- - add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, false, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "menu_export_stl", [](const std::string&) { + add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_stl(false, false, true, FT_DRC); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "menu_export_toolpaths", [](const std::string&) { + add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) { if (Plater* plater = wxGetApp().plater()) plater->export_toolpaths_to_obj(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "menu_export_config", [](const std::string&) { + add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) { if (MainFrame* mf = wxGetApp().mainframe) mf->export_config(); return AppActionRunResult{AppActionRunResult::Level::Success}; @@ -568,7 +570,7 @@ std::vector build_command_catalog() wxGetApp().ShowUserGuide(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "folder-closed", [](const std::string&) { + add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) { Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); @@ -589,11 +591,11 @@ std::vector build_command_catalog() } return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "ams_refresh_normal", [](const std::string&) { + add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) { wxGetApp().check_new_version_sf(true, 1); return AppActionRunResult{AppActionRunResult::Level::Success}; }); - add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_about", [](const std::string&) { + add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) { Slic3r::GUI::about(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 08f2c45b86..3de0c137bb 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -144,6 +144,10 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent) SetSizer(sizer); SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight))); } + // WebView2's browser accelerator keys include Ctrl +/-/0 and Ctrl+wheel zoom, which would resize + // the page inside the fixed-size popup. No-op on the other backends (wxWidgets 3.3 base virtual). + if (wxWebView* wv = browser()) + wv->EnableBrowserAcceleratorKeys(false); // Re-cut the shape region whenever layout changes the client size; SetShape itself // does not generate size events, so this cannot recurse. Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 1743f09ab3..0eb5edde41 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -171,9 +171,10 @@ TEST_CASE("Native command catalog has unique keys and present titles", "[ActionS } } -// 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. +// Every command's tile pictogram is a theme-neutral SVG (the matching GUI control's icon, or the +// equivalent settings-group icon); an absent icon gets the page's generic placeholder. 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", "[ActionSource][SpeedDial]") { const std::vector& commands = Slic3r::GUI::NativeCommands::catalog(); @@ -193,9 +194,17 @@ TEST_CASE("Native command icons resolve to shipped SVGs", "[ActionSource][SpeedD Expected{"save_project", "menu_save"}, Expected{"sync_ams", "ams_fila_sync"}, Expected{"mode_simple", "advanced"}, - Expected{"calib_temperature", "calib_sf"}, + Expected{"calib_temperature", "param_temperature"}, + Expected{"calib_cornering", "param_precision"}, Expected{"plate_add", "toolbar_add_plate"}, Expected{"add_primitive_cube", "menu_obj_cube"}, + // These previously pointed at blank placeholder SVGs or theme-broken ones. + Expected{"obj_delete", "delete"}, + Expected{"export_gcode", "custom-gcode_gcode"}, + Expected{"import_file", "menu_open"}, + Expected{"help_open_config_folder", "open_project"}, + Expected{"help_check_updates", "refresh"}, + Expected{"help_about", "OrcaSlicer_gradient_circle"}, Expected{"go_to_tab", ""}}) { const std::string* icon = icon_of(e.key); INFO(e.key);