#include "Shortcuts.hpp" #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include #include #include namespace Slic3r { namespace GUI { namespace { constexpr uint8_t GLOBAL = context_bit(ShortcutContext::Global); constexpr uint8_t PLATER = context_bit(ShortcutContext::Plater); constexpr uint8_t PREVIEW = context_bit(ShortcutContext::Preview); constexpr uint8_t OBJECT_LIST = context_bit(ShortcutContext::ObjectList); constexpr uint8_t PAINTING = context_bit(ShortcutContext::Painting); constexpr uint8_t CANVAS = PLATER | PREVIEW; constexpr int CTRL = wxMOD_CONTROL; constexpr int SHIFT = wxMOD_SHIFT; constexpr int CTRL_SHIFT = wxMOD_CONTROL | wxMOD_SHIFT; #ifdef __APPLE__ constexpr KeyChord PREFERENCES_CHORD{ ',', CTRL }; constexpr KeyChord DELETE_CHORD{ WXK_BACK }; constexpr KeyChord MOUSE3D_CHORD{ 'M', CTRL_SHIFT }; #else constexpr KeyChord PREFERENCES_CHORD{ 'P', CTRL }; constexpr KeyChord DELETE_CHORD{ WXK_DELETE }; constexpr KeyChord MOUSE3D_CHORD{ 'M', CTRL }; #endif // Each row: enum value, AppConfig key, description shown in the dialog, contexts the key is // looked up in, and the default chord. Rows are in the order the dialog lists them, within // the sections of the Shortcut enum. // SHORTCUT runs once per key press. // REPEATING runs again on every auto-repeat of a held key. // STEPPING repeats, and Shift or Ctrl held with its key select a step variant of it instead // of another shortcut (ShortcutInfo::modifier_variants). #define SHORTCUT(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, false, false } #define REPEATING(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, true, false } #define STEPPING(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, true, true } constexpr std::array shortcut_table = {{ // Project SHORTCUT(NewProject, "new_project", L("New Project"), GLOBAL, { 'N', CTRL }), SHORTCUT(OpenProject, "open_project", L("Open Project"), GLOBAL, { 'O', CTRL }), SHORTCUT(SaveProject, "save_project", L("Save Project"), GLOBAL, { 'S', CTRL }), SHORTCUT(SaveProjectAs, "save_project_as", L("Save Project as"), GLOBAL, { 'S', CTRL_SHIFT }), SHORTCUT(ImportModel, "import_model", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files"), GLOBAL, { 'I', CTRL }), SHORTCUT(Publish3mf, "publish_3mf", L("Publish 3MF"), GLOBAL, { 'E', CTRL_SHIFT }), // Slicing and printing SHORTCUT(SlicePlate, "slice_plate", L("Slice plate"), GLOBAL, { 'R', CTRL }), SHORTCUT(ExportSlicedFile, "export_sliced_file", L("Export plate sliced file"), GLOBAL, { 'G', CTRL }), SHORTCUT(PrintPlate, "print_plate", L("Print plate"), GLOBAL, { 'G', CTRL_SHIFT }), SHORTCUT(PrintHostQueue, "print_host_queue", L("Print host upload queue"), GLOBAL, { 'J', CTRL }), // Selection SHORTCUT(SelectAll, "select_all", L("Select all objects"), PLATER | OBJECT_LIST, { 'A', CTRL }), SHORTCUT(SelectAllPlates, "select_all_plates", L("Select all objects on all plates"), PLATER, { 'A', CTRL_SHIFT }), // Editing REPEATING(Undo, "undo", L("Undo"), PLATER | OBJECT_LIST, { 'Z', CTRL }), REPEATING(Redo, "redo", L("Redo"), PLATER | OBJECT_LIST, { 'Y', CTRL }), SHORTCUT(Cut, "cut", L("Cut"), PLATER | OBJECT_LIST, { 'X', CTRL }), SHORTCUT(Copy, "copy", L("Copy to clipboard"), PLATER | OBJECT_LIST, { 'C', CTRL }), SHORTCUT(Paste, "paste", L("Paste from clipboard"), PLATER | OBJECT_LIST, { 'V', CTRL }), SHORTCUT(DeleteSelected, "delete_selected", L("Delete Selected"), PLATER | OBJECT_LIST, DELETE_CHORD), SHORTCUT(DeleteAll, "delete_all", L("Delete All"), PLATER, { 'D', CTRL }), SHORTCUT(CloneSelected, "clone_selected", L("Clone Selected"), PLATER | OBJECT_LIST, { 'K', CTRL }), // Objects REPEATING(AddInstance, "add_instance", L("Add instance"), PLATER | OBJECT_LIST, { '+' }), REPEATING(RemoveInstance, "remove_instance", L("Remove instance"), PLATER | OBJECT_LIST, { '-' }), SHORTCUT(TogglePrintable, "toggle_printable", L("Toggle printable for object/part"), PLATER | OBJECT_LIST, { 'V' }), SHORTCUT(ToggleAutoDrop, "toggle_auto_drop", L("Auto Drop"), OBJECT_LIST, { 'D' }), // Placement SHORTCUT(Arrange, "arrange", L("Arrange all objects"), PLATER, { 'A' }), SHORTCUT(ArrangePlate, "arrange_plate", L("Arrange objects on selected plates"), PLATER, { 'A', SHIFT }), SHORTCUT(Orient, "orient", L("Auto orient all/selected objects"), PLATER, { 'Q' }), SHORTCUT(OrientPlate, "orient_plate", L("Auto orient all objects on current plate"), PLATER, { 'Q', SHIFT }), STEPPING(MoveSelectionLeft, "move_selection_left", L("Move selection 10mm in negative X direction"), PLATER, { WXK_LEFT }), STEPPING(MoveSelectionRight, "move_selection_right", L("Move selection 10mm in positive X direction"), PLATER, { WXK_RIGHT }), STEPPING(MoveSelectionUp, "move_selection_up", L("Move selection 10mm in positive Y direction"), PLATER, { WXK_UP }), STEPPING(MoveSelectionDown, "move_selection_down", L("Move selection 10mm in negative Y direction"), PLATER, { WXK_DOWN }), REPEATING(RotateSelectionLeft, "rotate_selection_left", L("Rotate selection 45 degrees counterclockwise"), PLATER, { WXK_PAGEUP }), REPEATING(RotateSelectionRight, "rotate_selection_right", L("Rotate selection 45 degrees clockwise"), PLATER, { WXK_PAGEDOWN }), // Gizmos SHORTCUT(GizmoMove, "gizmo_move", L("Gizmo move"), PLATER, { 'M' }), SHORTCUT(GizmoRotate, "gizmo_rotate", L("Gizmo rotate"), PLATER, { 'R' }), SHORTCUT(GizmoScale, "gizmo_scale", L("Gizmo scale"), PLATER, { 'S' }), SHORTCUT(GizmoFlatten, "gizmo_flatten", L("Gizmo place face on bed"), PLATER, { 'F' }), SHORTCUT(GizmoCut, "gizmo_cut", L("Gizmo cut"), PLATER, { 'C' }), SHORTCUT(GizmoMeshBoolean, "gizmo_mesh_boolean", L("Gizmo mesh boolean"), PLATER, { 'B' }), SHORTCUT(GizmoFdmSupports, "gizmo_fdm_supports", L("Gizmo FDM paint-on supports"), PLATER, { 'L' }), SHORTCUT(GizmoSeam, "gizmo_seam", L("Gizmo FDM paint-on seam"), PLATER, { 'P' }), SHORTCUT(GizmoFuzzySkin, "gizmo_fuzzy_skin", L("Gizmo FDM paint-on fuzzy skin"), PLATER, { 'H' }), SHORTCUT(GizmoMmuSegmentation, "gizmo_mmu_segmentation", L("Gizmo multi-material painting"), PLATER, { 'N' }), SHORTCUT(GizmoEmboss, "gizmo_emboss", L("Gizmo text emboss/engrave"), PLATER, { 'T' }), SHORTCUT(GizmoMeasure, "gizmo_measure", L("Gizmo measure"), PLATER, { 'U' }), SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }), SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }), // Sliders SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }), STEPPING(LayerSliderUp, "layer_slider_up", L("Vertical slider - Move active thumb Up"), PREVIEW, { WXK_UP }), STEPPING(LayerSliderDown, "layer_slider_down", L("Vertical slider - Move active thumb Down"), PREVIEW, { WXK_DOWN }), STEPPING(MovesSliderLeft, "moves_slider_left", L("Horizontal slider - Move active thumb Left"), PREVIEW, { WXK_LEFT }), STEPPING(MovesSliderRight, "moves_slider_right", L("Horizontal slider - Move active thumb Right"), PREVIEW, { WXK_RIGHT }), REPEATING(MovesSliderStart, "moves_slider_start", L("Horizontal slider - Move to start position"), PREVIEW, { WXK_HOME }), REPEATING(MovesSliderEnd, "moves_slider_end", L("Horizontal slider - Move to last position"), PREVIEW, { WXK_END }), // Painting tools SHORTCUT(PaintToolCircle, "paint_tool_circle", L("Circle"), PAINTING, { 'C' }), SHORTCUT(PaintToolSphere, "paint_tool_sphere", L("Sphere"), PAINTING, { 'S' }), SHORTCUT(PaintToolFill, "paint_tool_fill", L("Fill"), PAINTING, { 'F' }), SHORTCUT(PaintToolGapFill, "paint_tool_gap_fill", L("Gap Fill"), PAINTING, { 'G' }), SHORTCUT(PaintToolTriangle, "paint_tool_triangle", L("Triangle"), PAINTING, { 'T' }), SHORTCUT(PaintToolHeightRange, "paint_tool_height_range", L("Height Range"), PAINTING, { 'H' }), // Camera SHORTCUT(ViewDefault, "view_default", L("Camera view - Default"), GLOBAL, { '0', CTRL }), SHORTCUT(ViewTop, "view_top", L("Camera view - Top"), GLOBAL, { '1', CTRL }), SHORTCUT(ViewBottom, "view_bottom", L("Camera view - Bottom"), GLOBAL, { '2', CTRL }), SHORTCUT(ViewFront, "view_front", L("Camera view - Front"), GLOBAL, { '3', CTRL }), SHORTCUT(ViewRear, "view_rear", L("Camera view - Behind"), GLOBAL, { '4', CTRL }), SHORTCUT(ViewLeft, "view_left", L("Camera Angle - Left side"), GLOBAL, { '5', CTRL }), SHORTCUT(ViewRight, "view_right", L("Camera Angle - Right side"), GLOBAL, { '6', CTRL }), SHORTCUT(ViewPlate, "view_plate", L("Camera view - Current plate"), GLOBAL, { '7', CTRL }), REPEATING(ZoomIn, "zoom_in", L("Zoom in"), CANVAS, { 'I' }), REPEATING(ZoomOut, "zoom_out", L("Zoom out"), CANVAS, { 'O' }), SHORTCUT(Mouse3DSettings, "mouse3d_settings", L("Show/Hide 3Dconnexion devices settings dialog"), CANVAS, MOUSE3D_CHORD), // Display SHORTCUT(ShowLabels, "show_labels", L("Show object labels in 3D scene."), GLOBAL, { 'E', CTRL }), SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }), SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }), SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }), // Application SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD), SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }), SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }), SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }), SHORTCUT(SpeedDial, "speed_dial", L("Open the speed dial"), GLOBAL, { WXK_SPACE }), SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }), SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }), }}; #undef SHORTCUT #undef REPEATING #undef STEPPING // The first shortcut of each section's run in shortcut_table and the section's heading, indexed // by ShortcutSection. struct SectionInfo { Shortcut first; const char* name; }; constexpr std::array section_table = {{ { Shortcut::NewProject, L("Project") }, { Shortcut::SlicePlate, L("Slicing and printing") }, { Shortcut::SelectAll, L("Selection") }, { Shortcut::Undo, L("Editing") }, { Shortcut::AddInstance, L("Objects") }, { Shortcut::Arrange, L("Placement") }, { Shortcut::GizmoMove, L("Gizmos") }, { Shortcut::GoToLayer, L("Sliders") }, { Shortcut::PaintToolCircle, L("Painting tools") }, { Shortcut::ViewDefault, L("Camera") }, { Shortcut::ShowLabels, L("Display") }, { Shortcut::Preferences, L("Application") }, }}; constexpr bool sections_follow_table_order() { if (section_table.front().first != Shortcut(0)) return false; for (size_t i = 1; i < section_table.size(); ++i) if (section_table[i].first <= section_table[i - 1].first) return false; return true; } static_assert(sections_follow_table_order(), "section_table must begin at the first shortcut and ascend"); constexpr bool table_is_in_enum_order() { for (size_t i = 0; i < shortcut_table.size(); ++i) if (shortcut_table[i].id != Shortcut(i)) return false; return true; } static_assert(table_is_in_enum_order(), "shortcut_table must list every Shortcut in declaration order"); const char* CONFIG_SECTION = "shortcuts"; const char* UNBOUND = "none"; bool share_context(uint8_t a, uint8_t b) { return (a & b) != 0 || (a & GLOBAL) != 0 || (b & GLOBAL) != 0; } // The modifiers a modifier_variants shortcut accepts on top of its binding. constexpr int STEP_MODIFIERS = wxMOD_SHIFT | wxMOD_CONTROL; // The step modifiers chord adds to binding, 0 when chord is not a step of it; a binding with // Shift or Ctrl of its own has no steps, so no two bindings share one. int step_modifiers(const KeyChord& chord, const KeyChord& binding) { if (!binding.valid() || (binding.modifiers & STEP_MODIFIERS) != 0 || chord.key != binding.key || (chord.modifiers & ~STEP_MODIFIERS) != binding.modifiers) return 0; return chord.modifiers & STEP_MODIFIERS; } } // namespace const ShortcutInfo& shortcut_info(Shortcut shortcut) { return shortcut_table[size_t(shortcut)]; } ShortcutSection shortcut_section(Shortcut shortcut) { size_t section = 0; for (size_t i = 1; i < section_table.size(); ++i) if (section_table[i].first <= shortcut) section = i; return ShortcutSection(section); } const char* section_name(ShortcutSection section) { return section_table[size_t(section)].name; } std::vector shortcuts_in(ShortcutContext context) { std::vector out; for (const ShortcutInfo& info : shortcut_table) if (info.contexts & context_bit(context)) out.push_back(info.id); return out; } ShortcutRegistry::ShortcutRegistry() { rebuild_index(); } KeyChord ShortcutRegistry::binding(Shortcut shortcut) const { const std::optional& override = m_overrides[size_t(shortcut)]; return override.has_value() ? *override : shortcut_info(shortcut).default_chord; } bool ShortcutRegistry::is_customized(Shortcut shortcut) const { return m_overrides[size_t(shortcut)].has_value(); } std::string ShortcutRegistry::display(Shortcut shortcut) const { return binding(shortcut).display(); } std::string ShortcutRegistry::with_key(const std::string& text, Shortcut shortcut) const { const std::string key = display(shortcut); return key.empty() ? text : text + " [" + key + "]"; } std::string ShortcutRegistry::accelerator(Shortcut shortcut) const { const KeyChord chord = binding(shortcut); return chord.is_menu_accelerator() ? chord.to_string() : std::string(); } std::optional ShortcutRegistry::lookup(ShortcutContext context, const KeyChord& chord) const { if (!chord.valid()) return std::nullopt; auto it = m_index.find(chord); if (it == m_index.end()) return std::nullopt; for (Shortcut shortcut : it->second) if (shortcut_info(shortcut).contexts & context_bit(context)) return shortcut; return std::nullopt; } std::optional ShortcutRegistry::match(ShortcutContext context, const KeyChord& chord) const { if (const std::optional exact = lookup(context, chord); exact.has_value()) return Match{ *exact, 0 }; if ((chord.modifiers & STEP_MODIFIERS) == 0) return std::nullopt; for (const ShortcutInfo& info : shortcut_table) if (info.modifier_variants && (info.contexts & context_bit(context))) if (const int step = step_modifiers(chord, binding(info.id)); step != 0) return Match{ info.id, step }; return std::nullopt; } std::vector ShortcutRegistry::conflicts(Shortcut shortcut, const KeyChord& chord) const { std::vector out; if (!chord.valid()) return out; const uint8_t contexts = shortcut_info(shortcut).contexts; for (const ShortcutInfo& other : shortcut_table) if (other.id != shortcut && share_context(contexts, other.contexts) && binding(other.id) == chord) out.push_back(other.id); return out; } std::optional ShortcutRegistry::step_owner(Shortcut shortcut, const KeyChord& chord) const { const uint8_t contexts = shortcut_info(shortcut).contexts; for (const ShortcutInfo& other : shortcut_table) if (other.modifier_variants && other.id != shortcut && share_context(contexts, other.contexts)) if (const int step = step_modifiers(chord, binding(other.id)); step == wxMOD_SHIFT || step == wxMOD_CONTROL) // the combined step stays assignable return other.id; return std::nullopt; } void ShortcutRegistry::bind(Shortcut shortcut, const KeyChord& chord) { const ShortcutInfo& info = shortcut_info(shortcut); if (chord == info.default_chord) m_overrides[size_t(shortcut)].reset(); else m_overrides[size_t(shortcut)] = chord; rebuild_index(); } void ShortcutRegistry::reset(Shortcut shortcut) { m_overrides[size_t(shortcut)].reset(); rebuild_index(); } void ShortcutRegistry::reset_all() { m_overrides.fill(std::nullopt); rebuild_index(); } void ShortcutRegistry::load(const AppConfig& config) { m_overrides.fill(std::nullopt); if (config.has_section(CONFIG_SECTION)) { const std::map& section = config.get_section(CONFIG_SECTION); for (const ShortcutInfo& info : shortcut_table) { auto it = section.find(info.key); if (it == section.end()) continue; if (it->second == UNBOUND) m_overrides[size_t(info.id)] = KeyChord{}; else if (std::optional chord = KeyChord::parse(it->second); chord.has_value()) // A Global chord is seen before any text field, so it needs a modifier or a non-typing key. if ((info.contexts & GLOBAL) == 0 || chord->is_menu_accelerator()) m_overrides[size_t(info.id)] = *chord; } } rebuild_index(); } void ShortcutRegistry::save(AppConfig& config) const { for (const ShortcutInfo& info : shortcut_table) { const std::optional& override = m_overrides[size_t(info.id)]; if (override.has_value()) config.set(CONFIG_SECTION, info.key, override->valid() ? override->to_string() : UNBOUND); else config.erase(CONFIG_SECTION, info.key); } } void ShortcutRegistry::rebuild_index() { m_index.clear(); for (const ShortcutInfo& info : shortcut_table) if (const KeyChord chord = binding(info.id); chord.valid()) m_index[chord].push_back(info.id); } }} // namespace Slic3r::GUI