feat(gui): assignable keyboard shortcuts (#15706)

This commit is contained in:
Kris Austin
2026-09-21 17:36:04 -03:00
committed by GitHub
parent bfe458507b
commit 7292e19a1d
52 changed files with 2685 additions and 1299 deletions
+132
View File
@@ -0,0 +1,132 @@
# Keyboard Shortcuts
## Why it exists
Key events arrive in several windows (the main frame's char hook, the 3D canvases, the
gizmo manager and the object list), and the same keys are shown again in menu labels,
toolbar tooltips, gizmo names and the shortcuts dialog. The registry is the one table all
of them read. Each binding is defined once; dispatchers look key events up there, labels
are derived from it, and a change the user makes updates all of them.
## Data model
`KeyChord` (`src/slic3r/GUI/KeyChord.hpp`) is one key press: the key code as
`wxEVT_KEY_DOWN` reports it, plus the `wxMOD_*` modifiers held with it. It has two
text forms. The canonical one (`Ctrl+Shift+S`) is platform-neutral and doubles as the wx
accelerator string and the config format. The display one uses translated modifier
names and the command and option glyphs on macOS. `KeyChord::from_event()` turns any wx
key event into the same key code and modifiers, so a chord recorded in the dialog is
equal to the chord a dispatcher builds from the key press.
`Shortcut` is the enum of every user-facing binding. `shortcut_table` in
`src/slic3r/GUI/Shortcuts.cpp` gives each one a config key, a description, a context
mask, a default chord, a `repeatable` flag and a `modifier_variants` flag, in the order
the dialog lists them; a `static_assert` keeps the table and the enum in step.
`ShortcutRegistry` overlays the user's overrides on the defaults and keeps a
chord-to-shortcut index for lookups. It reads and writes the `shortcuts` section of
`AppConfig`. Only overrides are stored, so a default can change between releases
without touching anyone's config; `none` records a shortcut the user unbound.
## Contexts
A key press is looked up in the context of the window that received it.
| Context | Dispatcher | Examples |
|--------------|-----------------------------------------------------------|-------------------------------|
| `Global` | `MainFrame`'s `wxEVT_CHAR_HOOK`, before any child sees it | New project, camera views |
| `Plater` | `GLCanvas3D` of the 3D and assembly views | Arrange, gizmo activation |
| `Preview` | `GLCanvas3D` of the G-code preview | One-layer mode, jump to layer |
| `ObjectList` | the object list | Copy, delete, auto drop |
| `Painting` | `GLGizmosManager` while a painting gizmo is open | Circle, sphere, fill tools |
A shortcut can belong to several contexts, which is how copy and paste are a single
binding for the canvas and the object list. Two shortcuts can share a chord when their
contexts do not overlap; `C` is the cut gizmo in the 3D view, the G-code window in the
preview and the circle tool while painting. A Global chord is dispatched before every
other context, so the dialog treats it as conflicting with all of them.
A Global shortcut has to include Ctrl or Alt or use a key that types nothing, since a
bare printable key in the frame hook would swallow that character in every text field.
The dialog refuses such chords and `ShortcutRegistry::load()` drops them from the config.
Space counts as typing. The speed dial's default is the one bare Space, and
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
combo boxes), so it opens the dial from the canvases and the tab strip only.
## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
on the keyboard layout: the key labelled `Q` on an AZERTY keyboard and the key in the
same position under a Cyrillic layout both report `Q`. Numpad keys fold onto their main
keyboard equivalents, so `Ctrl+1` and `Ctrl+Numpad 1` are one binding.
Punctuation matches on `wxEVT_CHAR`, because only the char event knows which character
a key produced under the active layout. `+` is Shift and `=` on a US keyboard and a key
of its own on a German one, and the binding means the character in both cases. The
canvas looks a key up on key-down first and, when nothing matched, once more on the char
event, for punctuation chords only. The dialog records chords the same way: a printable
non-alphanumeric key pressed with nothing but Shift is taken from the char event that
follows.
wxGTK does not report key auto-repeat, so the canvases share one record of the keys
seen going down and swallow the repeats of every shortcut not marked `repeatable`. Zoom
and undo repeat, for example; a toggle such as Tab does not. The record is shared because
a shortcut can move the focus to another canvas while its key is still held; a key
released while no canvas had the focus is dropped on the next press.
A few shortcuts have `modifier_variants`: Shift or Ctrl added to their binding selects a
step of the same action (1 mm and camera-space moves of the selection, five-step slider
moves). Only a binding without Shift or Ctrl of its own has steps, so no two bindings
share one. `ShortcutRegistry::match()` looks the exact chord up first and only then, when
nothing is bound to it, looks for such a shortcut whose binding is the chord minus those
modifiers, reporting which were added; a binding on Ctrl+Shift+key therefore wins over
the combined step. The Shift and Ctrl steps themselves are reserved. `step_owner()` names
the shortcut they belong to, the capture dialog refuses to assign them, and
`conflicts()` reports exact chords only. A binding made before its key became a stepping
key keeps its chord and shadows that one step. A move or rotation of the selection
started from the keyboard runs until the key that started it is released, or the
canvas loses focus, so a held key is one undo step.
## Labels
Menu labels, toolbar tooltips, gizmo names, the context menu and the shortcuts dialog
read the registry, so a rebinding shows up in all of them. Each tracked menu item keeps
its base label; `MainFrame::update_shortcut_labels()` appends the current binding
again after an edit, which also installs the new wx accelerator.
A chord that is unsafe as a menu accelerator, meaning a bare printable key, is appended
to the label as plain text so the menu cannot take it away from text fields. The macOS
edit menu shows its clipboard and undo entries that way, because a system-menu key
equivalent for Cmd+C would run instead of the text field's own copy.
On macOS the object list receives no key events at all, so its bindings are installed as
a `wxAcceleratorTable`, regenerated from the registry after each edit.
## Editing
The shortcuts dialog has a page per context, each opening with a line that says when its
keys apply. A page lists the shortcuts under the headings of `section_table`, with the
fixed keys that cannot change (mouse buttons, the step modifiers, Esc, the digit keys
that pick a filament) sorted into the same sections. The mouse drag rows describe the
camera actions chosen in Preferences; their button opens Preferences > Control with that
option scrolled into view and focused, instead of editing a key.
Editing a row opens a capture dialog that records the next chord, names the shortcuts it
would take the chord from, and on confirmation unbinds those and binds this one.
Resetting a row asks the same question when its default is now held by another
shortcut, so a reset cannot leave two shortcuts on one chord. Each change is written to
the config at once and pushed to the menus, tooltips and accelerator tables through
`GUI_App::on_shortcuts_changed()`. The dialog opens from the Help menu and Preferences >
Control on the Global page, and from the `?` key on the page of the view that received it.
## Adding a shortcut
1. Add the enum value to `Shortcut` and its row to `shortcut_table`, in the position
the dialog should list it; the row's section heading is the `section_table` entry
above it, so a new section needs an entry there too. Pick a default that does not
collide inside its contexts; the `[Shortcuts]` tests check every default against the
others.
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
3. Where the UI shows the key, ask the registry (`display()` for tooltips,
`accelerator()` for menu labels); no label holds a literal key name.
+4
View File
@@ -123,6 +123,8 @@ set(SLIC3R_GUI_SOURCES
GUI/PluginPickerDialog.hpp
GUI/PluginsDialog.cpp
GUI/PluginsDialog.hpp
GUI/Shortcuts.cpp
GUI/Shortcuts.hpp
GUI/SpeedDialDialog.cpp
GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp
@@ -335,6 +337,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/Worker.hpp
GUI/KBShortcutsDialog.cpp
GUI/KBShortcutsDialog.hpp
GUI/KeyChord.cpp
GUI/KeyChord.hpp
GUI/LibVGCode/LibVGCodeWrapper.hpp
GUI/LibVGCode/LibVGCodeWrapper.cpp
GUI/LinuxDisplayBackend.cpp
+287 -537
View File
@@ -29,6 +29,7 @@
#include "MainFrame.hpp"
#include "WipeTowerDialog.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_Colors.hpp"
#include "Mouse3DController.hpp"
@@ -77,6 +78,7 @@
#include <float.h>
#include <algorithm>
#include <cmath>
#include <set>
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
@@ -1055,7 +1057,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
@@ -1174,7 +1175,6 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
, m_dynamic_background_enabled(false)
, m_multisample_allowed(false)
, m_moving(false)
, m_tab_down(false)
, m_camera_movement(false)
, m_cursor_type(Standard)
, m_color_by("volume")
@@ -3229,6 +3229,9 @@ void GLCanvas3D::bind_event_handlers()
m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
m_canvas->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& evt) {
// The key-up that would commit a keyboard edit goes to whatever took the focus.
if (m_selection_edit.kind != SelectionEdit::None)
finish_selection_edit();
ImGui::SetWindowFocus(nullptr);
render();
evt.Skip();
@@ -3358,8 +3361,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
// see include/wx/defs.h enum wxKeyCode
int keyCode = evt.GetKeyCode();
int ctrlMask = wxMOD_CONTROL;
int shiftMask = wxMOD_SHIFT;
auto imgui = wxGetApp().imgui();
if (imgui->update_key_data(evt)) {
@@ -3367,6 +3368,11 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
return;
}
#ifdef SLIC3R_CAD
const int ctrlMask = wxMOD_CONTROL;
const int shiftMask = wxMOD_SHIFT;
#endif
// Design tab: Delete/Backspace removes the selected sketch entities while a
// sketch tool is active and the canvas has focus (dialog text fields are separate
// wx controls, so this never eats their editing keys).
@@ -3425,12 +3431,6 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
}
#endif
bool is_in_painting_mode = false;
GLGizmoPainterBase *current_gizmo_painter = dynamic_cast<GLGizmoPainterBase *>(get_gizmos_manager().get_current());
if (current_gizmo_painter != nullptr) {
is_in_painting_mode = true;
}
//BBS: add orient deactivate logic
if (keyCode == WXK_ESCAPE
&& (_deactivate_arrange_menu() || _deactivate_orient_menu()))
@@ -3439,361 +3439,271 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
if (m_gizmos.on_char(evt))
return;
if ((evt.GetModifiers() & ctrlMask) != 0) {
// CTRL is pressed
switch (keyCode) {
#ifdef __APPLE__
case 'a':
case 'A':
#else /* __APPLE__ */
case WXK_CONTROL_A:
#endif /* __APPLE__ */
if (!is_in_painting_mode && !m_layers_editing.is_enabled()) {
if (evt.ShiftDown())
post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL));
else
post_event(SimpleEvent(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL));
}
if (const KeyChord chord = KeyChord::from_event(evt); chord.is_punctuation() && handle_shortcut(chord))
return;
if (evt.HasModifiers()) {
evt.Skip();
return;
}
auto obj_list = wxGetApp().obj_list();
switch (keyCode)
{
case WXK_ESCAPE: { deselect_all(); break; }
// BBS: use keypad to change extruder
case '1': {
if (!m_timer_set_color.IsRunning()) {
m_timer_set_color.StartOnce(500);
break;
}
}
case '0': //Color logic for material 10
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': {
if (m_timer_set_color.IsRunning()) {
if (keyCode < '7') keyCode += 10;
m_timer_set_color.Stop();
}
if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation)
obj_list->set_extruder_for_selected_items(keyCode - '0');
break;
#ifdef __APPLE__
case 'c':
case 'C':
#else /* __APPLE__ */
case WXK_CONTROL_C:
#endif /* __APPLE__ */
if (!is_in_painting_mode)
post_event(SimpleEvent(EVT_GLTOOLBAR_COPY));
break;
#ifdef __APPLE__
case 'm':
case 'M':
#else /* __APPLE__ */
case WXK_CONTROL_M:
#endif /* __APPLE__ */
{
#ifdef _WIN32
if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "true") {
#endif //_WIN32
#ifdef __APPLE__
// On OSX use Cmd+Shift+M to "Show/Hide 3Dconnexion devices settings dialog"
if ((evt.GetModifiers() & shiftMask) != 0) {
#endif // __APPLE__
Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller();
controller.show_settings_dialog(!controller.is_settings_dialog_shown());
m_dirty = true;
#ifdef __APPLE__
}
else
// and Cmd+M to minimize application
wxGetApp().mainframe->Iconize();
#endif // __APPLE__
#ifdef _WIN32
}
#endif //_WIN32
break;
}
#ifdef __APPLE__
case 'v':
case 'V':
#else /* __APPLE__ */
case WXK_CONTROL_V:
#endif /* __APPLE__ */
if (!is_in_painting_mode)
post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE));
break;
#ifdef __APPLE__
case 'x':
case 'X':
#else /* __APPLE__ */
case WXK_CONTROL_X:
#endif /* __APPLE__ */
if (!is_in_painting_mode)
post_event(SimpleEvent(EVT_GLTOOLBAR_CUT));
break;
#ifdef __APPLE__
case 'f':
case 'F':
#else /* __APPLE__ */
case WXK_CONTROL_F:
#endif /* __APPLE__ */
break;
#ifdef __APPLE__
case 'y':
case 'Y':
#else /* __APPLE__ */
case WXK_CONTROL_Y:
#endif /* __APPLE__ */
if (m_canvas_type == CanvasView3D || m_canvas_type == CanvasAssembleView) {
post_event(SimpleEvent(EVT_GLCANVAS_REDO));
}
break;
#ifdef __APPLE__
case 'z':
case 'Z':
#else /* __APPLE__ */
case WXK_CONTROL_Z:
#endif /* __APPLE__ */
// only support redu/undo in CanvasView3D
if (m_canvas_type == CanvasView3D || m_canvas_type == CanvasAssembleView) {
post_event(SimpleEvent(EVT_GLCANVAS_UNDO));
}
break;
// BBS
#ifdef __APPLE__
case 'E':
case 'e':
#else /* __APPLE__ */
case WXK_CONTROL_E:
#endif /* __APPLE__ */
{ m_labels.show(!m_labels.is_shown()); m_dirty = true; break; }
case '0': {
select_view("plate");
zoom_to_bed();
break; }
case '1': { select_view("top"); break; }
case '2': { select_view("bottom"); break; }
case '3': { select_view("front"); break; }
case '4': { select_view("rear"); break; }
case '5': { select_view("left"); break; }
case '6': { select_view("right"); break; }
case '7': { select_plate(); break; }
//case WXK_BACK:
//case WXK_DELETE:
#ifdef __APPLE__
case 'd':
case 'D':
#else /* __APPLE__ */
case WXK_CONTROL_D:
#endif /* __APPLE__ */
post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL));
break;
#ifdef __APPLE__
case 'k':
case 'K':
#else /* __APPLE__ */
case WXK_CONTROL_K:
#endif /* __APPLE__ */
post_event(SimpleEvent(EVT_GLTOOLBAR_CLONE));
break;
default: evt.Skip();
}
} else {
auto obj_list = wxGetApp().obj_list();
switch (keyCode)
{
//case WXK_BACK:
case WXK_DELETE: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; }
// BBS
#ifdef __APPLE__
case WXK_BACK: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; }
#endif
case WXK_ESCAPE: { deselect_all(); break; }
case WXK_F5: {
if (wxGetApp().mainframe->is_printer_view())
wxGetApp().mainframe->load_printer_url();
//if ((wxGetApp().is_editor() && !wxGetApp().plater()->model().objects.empty()) ||
// (wxGetApp().is_gcode_viewer() && !wxGetApp().plater()->get_last_loaded_gcode().empty()))
// post_event(SimpleEvent(EVT_GLCANVAS_RELOAD_FROM_DISK));
break;
}
// BBS: use keypad to change extruder
case '1': {
if (!m_timer_set_color.IsRunning()) {
m_timer_set_color.StartOnce(500);
break;
}
}
case '0': //Color logic for material 10
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': {
if (m_timer_set_color.IsRunning()) {
if (keyCode < '7') keyCode += 10;
m_timer_set_color.Stop();
}
if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation)
obj_list->set_extruder_for_selected_items(keyCode - '0');
break;
}
case '+': {
if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt));
else
post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, +1));
break;
}
case '-': {
if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt));
else
post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, -1));
break;
}
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
case 'A':
case 'a':
{
if ((evt.GetModifiers() & shiftMask) != 0)
post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE_PARTPLATE));
else
post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE));
break;
}
//case 'B':
//case 'b': { zoom_to_bed(); break; }
case 'C':
case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; }
//case 'G':
//case 'g': {
// if ((evt.GetModifiers() & shiftMask) != 0) {
// if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
// post_event(wxKeyEvent(EVT_GLCANVAS_JUMP_TO, evt));
// }
// break;
//}
case 'I':
case 'i': { _update_camera_zoom(1.0); break; }
//case 'K':
//case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; }
//case 'L':
//case 'l': {
//if (!m_main_toolbar.is_enabled()) {
// m_gcode_viewer.enable_legend(!m_gcode_viewer.is_legend_enabled());
// m_dirty = true;
// wxGetApp().plater()->update_preview_bottom_toolbar();
//}
//break;
//}
case 'O':
case 'o': { _update_camera_zoom(-1.0); break; }
case 'q':
case 'Q':
{
if ((evt.GetModifiers() & shiftMask) != 0)
post_event(SimpleEvent(EVT_GLCANVAS_ORIENT_PARTPLATE));
else
post_event(SimpleEvent(EVT_GLCANVAS_ORIENT));
break;
}
//case 'Z':
//case 'z': {
// if (!m_selection.is_empty())
// zoom_to_selection();
// else {
// if (!m_volumes.empty())
// zoom_to_volumes();
// else
// _zoom_to_box(m_gcode_viewer.get_paths_bounding_box());
// }
// break;
//}
case 'v':
case 'V': { post_event(SimpleEvent(EVT_GLCANVAS_PRINTABLE)); break; }
default: { evt.Skip(); break; }
}
}
default: { evt.Skip(); break; }
}
}
class TranslationProcessor
bool GLCanvas3D::handle_shortcut(const KeyChord& chord)
{
using UpAction = std::function<void(void)>;
using DownAction = std::function<void(const Vec3d&, bool, bool)>;
const ShortcutContext context = m_canvas_type == CanvasPreview ? ShortcutContext::Preview : ShortcutContext::Plater;
const std::optional<ShortcutRegistry::Match> match = wxGetApp().shortcuts().match(context, chord);
if (!match.has_value())
return false;
const Shortcut shortcut = match->shortcut;
const int held = match->step_modifiers;
if (m_key_down.repeat && !shortcut_info(shortcut).repeatable)
return true;
UpAction m_up_action{ nullptr };
DownAction m_down_action{ nullptr };
const bool painting = dynamic_cast<GLGizmoPainterBase*>(m_gizmos.get_current()) != nullptr;
const bool can_edit = m_canvas_type == CanvasView3D || m_canvas_type == CanvasAssembleView;
auto edit_selection = [this](SelectionEdit::Kind kind, const Vec3d& direction) {
if (!m_gizmos.is_enabled() || m_selection.is_empty() || m_canvas_type == CanvasAssembleView)
return false;
m_selection_edit.kind = kind;
m_selection_edit.key = m_key_down.code;
m_selection_edit.direction = direction;
return true;
};
auto move_selection = [&](const Vec3d& direction) {
if (edit_selection(SelectionEdit::Move, direction))
apply_selection_move((held & wxMOD_SHIFT) != 0, (held & wxMOD_CONTROL) != 0);
};
auto rotate_selection = [&](double angle_z_rad) {
if (edit_selection(SelectionEdit::Rotate, Vec3d::UnitZ()))
apply_selection_rotate(angle_z_rad);
};
auto step_slider = [this, held](auto&& step) {
IMSlider* layers = get_gcode_viewer().get_layers_slider();
IMSlider* moves = get_gcode_viewer().get_moves_slider();
step(layers, moves, held != 0 ? 5 : 1);
if (layers->is_dirty() && layers->is_one_layer())
layers->SetLowerValue(layers->GetHigherValue());
m_dirty = true;
};
bool m_running{ false };
Vec3d m_direction{ Vec3d::UnitX() };
public:
TranslationProcessor(UpAction up_action, DownAction down_action)
: m_up_action(up_action), m_down_action(down_action)
{
}
void process(wxKeyEvent& evt)
{
const int keyCode = evt.GetKeyCode();
wxEventType type = evt.GetEventType();
if (type == wxEVT_KEY_UP) {
switch (keyCode)
{
case WXK_NUMPAD_LEFT: case WXK_LEFT:
case WXK_NUMPAD_RIGHT: case WXK_RIGHT:
case WXK_NUMPAD_UP: case WXK_UP:
case WXK_NUMPAD_DOWN: case WXK_DOWN:
{
m_running = false;
m_up_action();
break;
}
default: { break; }
}
}
else if (type == wxEVT_KEY_DOWN) {
bool apply = false;
switch (keyCode)
{
case WXK_SHIFT:
{
if (m_running)
apply = true;
break;
}
case WXK_NUMPAD_LEFT:
case WXK_LEFT:
{
m_direction = -Vec3d::UnitX();
apply = true;
break;
}
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT:
{
m_direction = Vec3d::UnitX();
apply = true;
break;
}
case WXK_NUMPAD_UP:
case WXK_UP:
{
m_direction = Vec3d::UnitY();
apply = true;
break;
}
case WXK_NUMPAD_DOWN:
case WXK_DOWN:
{
m_direction = -Vec3d::UnitY();
apply = true;
break;
}
default: { break; }
}
if (apply) {
m_running = true;
m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown());
}
switch (shortcut) {
case Shortcut::SelectAll:
if (!painting && !m_layers_editing.is_enabled())
post_event(SimpleEvent(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL));
break;
case Shortcut::SelectAllPlates:
if (!painting && !m_layers_editing.is_enabled())
post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL));
break;
case Shortcut::Copy: if (!painting) post_event(SimpleEvent(EVT_GLTOOLBAR_COPY)); break;
case Shortcut::Paste: if (!painting) post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE)); break;
case Shortcut::Cut: if (!painting) post_event(SimpleEvent(EVT_GLTOOLBAR_CUT)); break;
case Shortcut::Undo: if (can_edit) post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); break;
case Shortcut::Redo: if (can_edit) post_event(SimpleEvent(EVT_GLCANVAS_REDO)); break;
case Shortcut::DeleteSelected:
if (!m_gizmos.on_delete_key())
post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE));
break;
case Shortcut::DeleteAll: post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break;
case Shortcut::CloneSelected: post_event(SimpleEvent(EVT_GLTOOLBAR_CLONE)); break;
case Shortcut::AddInstance: post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); break;
case Shortcut::RemoveInstance: post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); break;
case Shortcut::TogglePrintable: post_event(SimpleEvent(EVT_GLCANVAS_PRINTABLE)); break;
case Shortcut::Arrange:
if (!m_gizmos.is_running())
post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE));
break;
case Shortcut::ArrangePlate:
if (!m_gizmos.is_running())
post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE_PARTPLATE));
break;
case Shortcut::Orient: post_event(SimpleEvent(EVT_GLCANVAS_ORIENT)); break;
case Shortcut::OrientPlate: post_event(SimpleEvent(EVT_GLCANVAS_ORIENT_PARTPLATE)); break;
case Shortcut::RotateSelectionLeft: rotate_selection(0.25 * M_PI); break;
case Shortcut::RotateSelectionRight: rotate_selection(-0.25 * M_PI); break;
case Shortcut::MoveSelectionLeft: move_selection(-Vec3d::UnitX()); break;
case Shortcut::MoveSelectionRight: move_selection(Vec3d::UnitX()); break;
case Shortcut::MoveSelectionUp: move_selection(Vec3d::UnitY()); break;
case Shortcut::MoveSelectionDown: move_selection(-Vec3d::UnitY()); break;
case Shortcut::ZoomIn: _update_camera_zoom(1.0); break;
case Shortcut::ZoomOut: _update_camera_zoom(-1.0); break;
case Shortcut::SwitchView: post_event(SimpleEvent(EVT_GLCANVAS_TAB)); break;
case Shortcut::CollapseSidebar:
if (!wxGetApp().is_gcode_viewer())
post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR));
break;
case Shortcut::ShowWireframe:
wxGetApp().plater()->toggle_show_wireframe();
m_dirty = true;
break;
case Shortcut::Mouse3DSettings: {
#ifdef _WIN32
if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "true") {
#endif //_WIN32
Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller();
controller.show_settings_dialog(!controller.is_settings_dialog_shown());
m_dirty = true;
#ifdef _WIN32
}
#endif //_WIN32
break;
}
};
case Shortcut::ReloadDevicePage:
if (wxGetApp().mainframe->is_printer_view())
wxGetApp().mainframe->load_printer_url();
break;
case Shortcut::KeyboardShortcuts: post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break;
case Shortcut::ToggleGcodeWindow:
wxGetApp().toggle_show_gcode_window();
m_dirty = true;
request_extra_frame();
break;
case Shortcut::ToggleOneLayerMode:
get_gcode_viewer().get_layers_slider()->switch_one_layer_mode();
m_dirty = true;
break;
case Shortcut::GoToLayer:
if (!m_gizmos.is_enabled()) {
get_gcode_viewer().get_layers_slider()->show_go_to_layer(true);
m_dirty = true;
}
break;
case Shortcut::LayerSliderUp:
case Shortcut::LayerSliderDown:
step_slider([up = shortcut == Shortcut::LayerSliderUp](IMSlider* layers, IMSlider* moves, int increment) {
const int delta = up ? increment : -increment;
if (layers->GetSelection() == ssHigher) {
layers->SetHigherValue(layers->GetHigherValue() + delta);
moves->SetHigherValue(moves->GetMaxValue());
}
else if (layers->GetSelection() == ssLower)
layers->SetLowerValue(layers->GetLowerValue() + delta);
});
break;
case Shortcut::MovesSliderLeft:
step_slider([](IMSlider* layers, IMSlider* moves, int increment) {
if (moves->GetHigherValue() == moves->GetMinValue() && layers->GetHigherValue() > layers->GetMinValue()) {
layers->SetHigherValue(layers->GetHigherValue() - 1);
moves->SetHigherValue(moves->GetMaxValue());
}
else
moves->SetHigherValue(moves->GetHigherValue() - increment);
});
break;
case Shortcut::MovesSliderRight:
step_slider([](IMSlider* layers, IMSlider* moves, int increment) {
if (moves->GetHigherValue() == moves->GetMaxValue() && layers->GetHigherValue() < layers->GetMaxValue()) {
layers->SetHigherValue(layers->GetHigherValue() + 1);
moves->SetHigherValue(moves->GetMinValue());
}
else
moves->SetHigherValue(moves->GetHigherValue() + increment);
});
break;
case Shortcut::MovesSliderStart:
case Shortcut::MovesSliderEnd:
step_slider([start = shortcut == Shortcut::MovesSliderStart](IMSlider*, IMSlider* moves, int) {
moves->SetHigherValue(start ? moves->GetMinValue() : moves->GetMaxValue());
moves->set_as_dirty();
});
break;
default:
if (!m_gizmos.open_gizmo_by_shortcut(shortcut))
return false;
m_dirty = true;
break;
}
return true;
}
void GLCanvas3D::apply_selection_move(bool slow, bool camera_space)
{
m_selection.setup_cache();
const double multiplier = slow ? 1.0 : 10.0;
Vec3d displacement;
if (camera_space) {
Eigen::Matrix<double, 3, 3, Eigen::DontAlign> inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3);
displacement = multiplier * (inv_view_3x3 * m_selection_edit.direction);
displacement.z() = 0.0;
}
else
displacement = multiplier * m_selection_edit.direction;
TransformationType trafo_type;
trafo_type.set_relative();
m_selection.translate(displacement, trafo_type);
m_dirty = true;
}
void GLCanvas3D::apply_selection_rotate(double angle_z_rad)
{
m_selection.setup_cache();
m_selection.rotate(angle_z_rad * m_selection_edit.direction, TransformationType(TransformationType::World_Relative_Joint));
m_dirty = true;
}
void GLCanvas3D::finish_selection_edit()
{
const SelectionEdit::Kind kind = m_selection_edit.kind;
m_selection_edit.kind = SelectionEdit::None;
if (kind == SelectionEdit::Move)
do_move(L("Tool move"));
else
do_rotate(L("Tool Rotate"));
m_gizmos.update_data();
// Let the plater know that the dragging finished, so a delayed refresh
// of the scene with the background processing data should be performed.
post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
// updates camera target constraints
refresh_camera_scene_box();
m_dirty = true;
}
// Keys held since their last key-up, shared by every canvas because a shortcut can move the
// focus, and with it the key-up, to another one.
static std::set<int> s_keys_down;
static bool key_repeats(int key)
{
for (auto it = s_keys_down.begin(); it != s_keys_down.end();) // drops keys released while no canvas had the focus
it = wxGetKeyState(wxKeyCode(*it)) ? std::next(it) : s_keys_down.erase(it);
return !s_keys_down.insert(key).second;
}
static void key_released(int key) { s_keys_down.erase(key); }
void GLCanvas3D::on_key(wxKeyEvent& evt)
{
@@ -3811,45 +3721,11 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
}
#endif
static GLCanvas3D const * thiz = nullptr;
static TranslationProcessor translationProcessor(nullptr, nullptr);
if (thiz != this) {
thiz = this;
translationProcessor = TranslationProcessor(
[this]() {
do_move(L("Tool move"));
m_gizmos.update_data();
// BBS
//wxGetApp().obj_manipul()->set_dirty();
// Let the plater know that the dragging finished, so a delayed refresh
// of the scene with the background processing data should be performed.
post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
// updates camera target constraints
refresh_camera_scene_box();
m_dirty = true;
},
[this](const Vec3d& direction, bool slow, bool camera_space) {
m_selection.setup_cache();
double multiplier = slow ? 1.0 : 10.0;
Vec3d displacement;
if (camera_space) {
Eigen::Matrix<double, 3, 3, Eigen::DontAlign> inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3);
displacement = multiplier * (inv_view_3x3 * direction);
displacement.z() = 0.0;
}
else
displacement = multiplier * direction;
TransformationType trafo_type;
trafo_type.set_relative();
m_selection.translate(displacement, trafo_type);
m_dirty = true;
}
);}
const int keyCode = evt.GetKeyCode();
if (evt.GetEventType() == wxEVT_KEY_DOWN)
m_key_down = { keyCode, key_repeats(keyCode) };
else if (evt.GetEventType() == wxEVT_KEY_UP)
key_released(keyCode);
auto imgui = wxGetApp().imgui();
if (imgui->update_key_data(evt))
@@ -3863,23 +3739,8 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
wxGetApp().plater()->toggle_render_statistic_dialog();
m_dirty = true;
#endif
} else if ((evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_RETURN) ||
(evt.ShiftDown() && evt.AltDown() && keyCode == WXK_RETURN)) {
wxGetApp().plater()->toggle_show_wireframe();
m_dirty = true;
}
else if (m_tab_down && keyCode == WXK_TAB && !evt.HasAnyModifiers()) {
// Enable switching between 3D and Preview with Tab
// m_canvas->HandleAsNavigationKey(evt); // XXX: Doesn't work in some cases / on Linux
post_event(SimpleEvent(EVT_GLCANVAS_TAB));
}
else if (keyCode == WXK_TAB && evt.ShiftDown() && !evt.ControlDown() && ! wxGetApp().is_gcode_viewer()) {
// Collapse side-panel with Shift+Tab
post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR));
}
else if (keyCode == WXK_SHIFT) {
translationProcessor.process(evt);
if (m_picking_enabled && m_rectangle_selection.is_dragging()) {
_update_selection_from_hover();
m_rectangle_selection.stop_dragging();
@@ -3905,70 +3766,15 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
}
else if (keyCode == WXK_CONTROL)
m_dirty = true;
else if (m_gizmos.is_enabled() && !m_selection.is_empty() && m_canvas_type != CanvasAssembleView) {
translationProcessor.process(evt);
switch (keyCode)
{
case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP:
case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN:
{
do_rotate(L("Tool Rotate"));
m_gizmos.update_data();
// BBS
//wxGetApp().obj_manipul()->set_dirty();
// Let the plater know that the dragging finished, so a delayed refresh
// of the scene with the background processing data should be performed.
post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
// updates camera target constraints
refresh_camera_scene_box();
m_dirty = true;
break;
}
default: { break; }
}
}
// BBS: add select view logic
if (evt.ControlDown()) {
switch (keyCode) {
case '0':
case WXK_NUMPAD0: //0 on numpad
{ select_view("plate");
zoom_to_bed();
break;
}
case '1':
case WXK_NUMPAD1: //1 on numpad
{ select_view("top"); break; }
case '2':
case WXK_NUMPAD2: //2 on numpad
{ select_view("bottom"); break; }
case '3':
case WXK_NUMPAD3: //3 on numpad
{ select_view("front"); break; }
case '4':
case WXK_NUMPAD4: //4 on numpad
{ select_view("rear"); break; }
case '5':
case WXK_NUMPAD5: //5 on numpad
{ select_view("left"); break; }
case '6':
case WXK_NUMPAD6: //6 on numpad
{ select_view("right"); break; }
case '7':
case WXK_NUMPAD7: //7 on numpad
{ select_plate(); break; }
default: break;
}
}
else if (m_selection_edit.kind != SelectionEdit::None && keyCode == m_selection_edit.key)
finish_selection_edit();
}
else if (evt.GetEventType() == wxEVT_KEY_DOWN) {
m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers();
if (handle_shortcut(KeyChord::from_event(evt)))
return;
if (keyCode == WXK_SHIFT) {
translationProcessor.process(evt);
if (m_selection_edit.kind == SelectionEdit::Move)
apply_selection_move(true, evt.CmdDown());
if (m_picking_enabled /*&& (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)*/)
{
@@ -3985,68 +3791,6 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
}
else if (keyCode == WXK_CONTROL)
m_dirty = true;
else if (m_gizmos.is_enabled() && !m_selection.is_empty() && m_canvas_type != CanvasAssembleView) {
auto _do_rotate = [this](double angle_z_rad) {
m_selection.setup_cache();
m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint));
m_dirty = true;
// wxGetApp().obj_manipul()->set_dirty();
};
translationProcessor.process(evt);
switch (keyCode)
{
case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: { _do_rotate(0.25 * M_PI); break; }
case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { _do_rotate(-0.25 * M_PI); break; }
default: { break; }
}
} else if (!m_gizmos.is_enabled()) {
// DoubleSlider navigation in Preview
if (m_canvas_type == CanvasPreview) {
IMSlider *m_layers_slider = get_gcode_viewer().get_layers_slider();
IMSlider *m_moves_slider = get_gcode_viewer().get_moves_slider();
int increment = (evt.CmdDown() || evt.ShiftDown()) ? 5 : 1;
if ((evt.CmdDown() || evt.ShiftDown()) && evt.GetKeyCode() == 'G') {
m_layers_slider->show_go_to_layer(true);
}
else if (keyCode == WXK_UP || keyCode == WXK_DOWN) {
int new_pos;
if (m_layers_slider->GetSelection() == ssHigher) {
new_pos = keyCode == WXK_UP ? m_layers_slider->GetHigherValue() + increment : m_layers_slider->GetHigherValue() - increment;
m_layers_slider->SetHigherValue(new_pos);
m_moves_slider->SetHigherValue(m_moves_slider->GetMaxValue());
}
else if (m_layers_slider->GetSelection() == ssLower) {
new_pos = keyCode == WXK_UP ? m_layers_slider->GetLowerValue() + increment : m_layers_slider->GetLowerValue() - increment;
m_layers_slider->SetLowerValue(new_pos);
}
} else if (keyCode == WXK_LEFT) {
if (m_moves_slider->GetHigherValue() == m_moves_slider->GetMinValue() && (m_layers_slider->GetHigherValue() > m_layers_slider->GetMinValue())) {
m_layers_slider->SetHigherValue(m_layers_slider->GetHigherValue() - 1);
m_moves_slider->SetHigherValue(m_moves_slider->GetMaxValue());
} else {
m_moves_slider->SetHigherValue(m_moves_slider->GetHigherValue() - increment);
}
} else if (keyCode == WXK_RIGHT) {
if (m_moves_slider->GetHigherValue() == m_moves_slider->GetMaxValue() && (m_layers_slider->GetHigherValue() < m_layers_slider->GetMaxValue())) {
m_layers_slider->SetHigherValue(m_layers_slider->GetHigherValue() + 1);
m_moves_slider->SetHigherValue(m_moves_slider->GetMinValue());
} else {
m_moves_slider->SetHigherValue(m_moves_slider->GetHigherValue() + increment);
}
} else if (keyCode == WXK_HOME || keyCode == WXK_END) {
const int new_pos = keyCode == WXK_HOME ? m_moves_slider->GetMinValue() : m_moves_slider->GetMaxValue();
m_moves_slider->SetHigherValue(new_pos);
m_moves_slider->set_as_dirty();
}
if (m_layers_slider->is_dirty() && m_layers_slider->is_one_layer())
m_layers_slider->SetLowerValue(m_layers_slider->GetHigherValue());
m_dirty = true;
}
}
}
}
else return;
@@ -7115,7 +6859,6 @@ bool GLCanvas3D::_init_main_toolbar()
item.name = "add";
item.icon_filename = m_is_dark ? "toolbar_open_dark.svg" : "toolbar_open.svg";
item.tooltip = _utf8(L("Add")) + " [" + GUI::shortkey_ctrl_prefix() + "I]";
item.sprite_id = 0;
item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ADD)); };
item.enabling_callback = []()->bool {return wxGetApp().plater()->can_add_model(); };
@@ -7133,7 +6876,6 @@ bool GLCanvas3D::_init_main_toolbar()
item.name = "orient";
item.icon_filename = m_is_dark ? "toolbar_orient_dark.svg" : "toolbar_orient.svg";
item.tooltip = _utf8(L("Auto orient all/selected objects")) + " [Q]\n" + _utf8(L("Auto orient all objects on current plate")) + " [" + _utf8(L("Shift+")) + "Q]";
item.sprite_id++;
item.left.render_callback = nullptr;
item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); };
@@ -7155,7 +6897,6 @@ bool GLCanvas3D::_init_main_toolbar()
item.name = "arrange";
item.icon_filename = m_is_dark ? "toolbar_arrange_dark.svg" : "toolbar_arrange.svg";
item.tooltip = _utf8(L("Arrange all objects")) + " [A]\n" + _utf8(L("Arrange objects on selected plates")) + " [" + _utf8(L("Shift+")) + "A]";
item.sprite_id++;
item.left.action_callback = []() {};
item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); };
@@ -7179,7 +6920,6 @@ bool GLCanvas3D::_init_main_toolbar()
item.name = "more";
item.icon_filename = m_is_dark ? "instance_add_dark.svg" : "instance_add.svg";
item.tooltip = _utf8(L("Add instance")) + " [+]";
item.sprite_id++;
item.left.render_callback = nullptr;
item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); };
@@ -7191,7 +6931,6 @@ bool GLCanvas3D::_init_main_toolbar()
item.name = "fewer";
item.icon_filename = m_is_dark ? "instance_remove_dark.svg" : "instance_remove.svg";
item.tooltip = _utf8(L("Remove instance")) + " [-]";
item.sprite_id++;
item.left.render_callback = nullptr;
item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); };
@@ -7241,9 +6980,20 @@ bool GLCanvas3D::_init_main_toolbar()
if (!m_main_toolbar.add_item(item))
return false;
update_shortcut_tooltips();
return true;
}
void GLCanvas3D::update_shortcut_tooltips()
{
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
m_main_toolbar.set_tooltip(m_main_toolbar.get_item_id("add"), shortcuts.with_key(_u8L("Add"), Shortcut::ImportModel));
m_main_toolbar.set_tooltip(m_main_toolbar.get_item_id("orient"), shortcuts.with_key(_u8L("Auto orient all/selected objects"), Shortcut::Orient) + "\n" + shortcuts.with_key(_u8L("Auto orient all objects on current plate"), Shortcut::OrientPlate));
m_main_toolbar.set_tooltip(m_main_toolbar.get_item_id("arrange"), shortcuts.with_key(_u8L("Arrange all objects"), Shortcut::Arrange) + "\n" + shortcuts.with_key(_u8L("Arrange objects on selected plates"), Shortcut::ArrangePlate));
m_main_toolbar.set_tooltip(m_main_toolbar.get_item_id("more"), shortcuts.with_key(_u8L("Add instance"), Shortcut::AddInstance));
m_main_toolbar.set_tooltip(m_main_toolbar.get_item_id("fewer"), shortcuts.with_key(_u8L("Remove instance"), Shortcut::RemoveInstance));
}
//BBS: GUI refactor: GLToolbar
bool GLCanvas3D::_init_select_plate_toolbar()
{
+25 -2
View File
@@ -63,6 +63,7 @@ class PartPlateList;
#ifdef SLIC3R_CAD
class DesignSketchTool; // Design tab: interactive 2D sketch tool
#endif
struct KeyChord;
#if ENABLE_RETINA_GL
class RetinaHelper;
@@ -185,7 +186,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
@@ -626,7 +626,23 @@ private:
bool m_dynamic_background_enabled;
bool m_multisample_allowed;
bool m_moving;
bool m_tab_down;
// The key-down being dispatched, kept for the char event that may follow it.
struct KeyDown
{
int code = WXK_NONE;
bool repeat = false;
};
KeyDown m_key_down;
// A keyboard move or rotation of the selection runs from the key-down that started it to
// that key's release, so a held key becomes one undo step.
struct SelectionEdit
{
enum Kind { None, Move, Rotate };
Kind kind = None;
int key = WXK_NONE; // raw key code of the key-down, matched against the key-up
Vec3d direction{ Vec3d::UnitX() };
};
SelectionEdit m_selection_edit;
bool m_camera_movement;
//BBS: add toolpath outside
bool m_toolpath_outside{ false };
@@ -1094,6 +1110,13 @@ public:
void on_idle(wxIdleEvent& evt);
void on_char(wxKeyEvent& evt);
void on_key(wxKeyEvent& evt);
// Runs the Plater/Preview shortcut bound to chord, swallowing auto-repeats of one-shot
// shortcuts; false when nothing is bound.
bool handle_shortcut(const KeyChord& chord);
void apply_selection_move(bool slow, bool camera_space);
void apply_selection_rotate(double angle_z_rad);
void finish_selection_edit();
void update_shortcut_tooltips();
void on_mouse_wheel(wxMouseEvent& evt);
void on_timer(wxTimerEvent& evt);
void on_render_timer(wxTimerEvent& evt);
+26 -4
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "BindDialog.hpp"
#include "DeviceManager.hpp"
#include "HMS.hpp"
@@ -1120,6 +1121,8 @@ GUI_App::GUI_App()
{
//app config initializes early becasuse it is used in instance checking in OrcaSlicer.cpp
this->init_app_config();
m_shortcuts = std::make_unique<ShortcutRegistry>();
m_shortcuts->load(*app_config);
this->init_download_path();
// Note: the WebView2 runtime check (init_webview_runtime) used to run here, but
// the constructor executes before wxWidgets is fully initialized and before the
@@ -4677,12 +4680,28 @@ void GUI_App::system_info()
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts()
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{
KBShortcutsDialog dlg;
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
dlg.ShowModal();
}
void GUI_App::on_shortcuts_changed()
{
m_shortcuts->save(*app_config);
app_config->save();
if (mainframe == nullptr)
return;
mainframe->update_shortcut_labels();
if (Plater* plater = this->plater(); plater != nullptr) {
if (GLCanvas3D* canvas = plater->get_view3D_canvas3D(); canvas != nullptr)
canvas->update_shortcut_tooltips();
#ifdef __WXOSX__
obj_list()->update_shortcut_accelerators();
#endif
}
}
void GUI_App::troubleshoot()
{
TroubleshootDialog dlg;
@@ -8465,7 +8484,9 @@ void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::strin
}
}
void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_option)
void GUI_App::open_preferences() { open_preferences(PreferencesTab::General); }
void GUI_App::open_preferences(PreferencesTab tab, const std::string& highlight_option)
{
// Render settings the canvas reads every frame; a change needs one redraw to show.
static constexpr const char* opengl_render_setting_keys[] = {
@@ -8482,7 +8503,8 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
// the dialog needs to be destroyed before the call to recreate_GUI()
// or sometimes the application crashes into wxDialogBase() destructor
// so we put it into an inner scope
PreferencesDialog dlg(mainframe, open_on_tab, highlight_option);
PreferencesDialog dlg(mainframe);
dlg.select_tab(tab, highlight_option);
dlg.ShowModal();
need_recreate_gui = dlg.recreate_GUI();
pending_language = dlg.pending_language();
+10 -2
View File
@@ -70,6 +70,9 @@ namespace GUI{
class RemovableDriveManager;
class OtherInstanceMessageHandler;
class ShortcutRegistry;
enum class ShortcutContext : uint8_t;
enum class PreferencesTab;
class MainFrame;
class Sidebar;
class ObjectSettings;
@@ -286,6 +289,7 @@ private:
std::unique_ptr<RemovableDriveManager> m_removable_drive_manager;
std::unique_ptr<ImGuiWrapper> m_imgui;
std::unique_ptr<ShortcutRegistry> m_shortcuts;
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
@@ -483,7 +487,7 @@ public:
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
void import_model(wxWindow *parent, wxArrayString& input_files) const;
@@ -639,7 +643,8 @@ public:
wxString current_language_code_safe() const;
bool is_localized() const { return m_wxLocale->GetLocale() != "English"; }
void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_preferences(); // on the General tab
void open_preferences(PreferencesTab tab, const std::string& highlight_option = std::string());
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
// Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open.
@@ -737,6 +742,9 @@ public:
size_t get_instance_hash_int () { return m_instance_hash_int; }
ImGuiWrapper* imgui() { return m_imgui.get(); }
ShortcutRegistry& shortcuts() { return *m_shortcuts; }
// Saves the bindings and refreshes every menu label, tooltip and accelerator table that shows one.
void on_shortcuts_changed();
PrintHostJobQueue& printhost_job_queue() { return *m_printhost_job_queue.get(); }
+3 -7
View File
@@ -6,6 +6,7 @@
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "ObjectDataViewModel.hpp"
@@ -2083,13 +2084,8 @@ wxMenu* MenuFactory::assemble_part_menu()
void MenuFactory::append_menu_item_clone(wxMenu* menu)
{
#ifdef __APPLE__
static const wxString ctrl = ("Ctrl+");
#else
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const wxString ctrl = _L("Ctrl+");
#endif
append_menu_item(menu, wxID_ANY, _L("Clone") + "\t" + ctrl + "K", "",
const std::string accel = wxGetApp().shortcuts().accelerator(Shortcut::CloneSelected);
append_menu_item(menu, wxID_ANY, _L("Clone") + (accel.empty() ? wxString() : "\t" + from_u8(accel)), "",
[](wxCommandEvent&) {
plater()->clone_selection();
}, "", nullptr,
+51 -80
View File
@@ -4,6 +4,7 @@
#include "GUI_Factories.hpp"
//#include "GUI_ObjectLayers.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "BitmapComboBox.hpp"
@@ -245,56 +246,15 @@ ObjectList::ObjectList(wxWindow* parent) :
// Key events are not correctly processed by the wxDataViewCtrl on OSX.
// Our patched wxWidgets process the keyboard accelerators.
// On the other hand, using accelerators will break in-place editing on Windows & Linux/GTK (there is no in-place editing working on OSX for wxDataViewCtrl for now).
// Bind(wxEVT_KEY_DOWN, &ObjectList::OnChar, this);
{
// Accelerators
// wxAcceleratorEntry entries[25];
wxAcceleratorEntry entries[26];
int index = 0;
entries[index++].Set(wxACCEL_CTRL, (int)'C', wxID_COPY);
entries[index++].Set(wxACCEL_CTRL, (int)'X', wxID_CUT);
entries[index++].Set(wxACCEL_CTRL, (int)'V', wxID_PASTE);
entries[index++].Set(wxACCEL_CTRL, (int)'M', wxID_DUPLICATE);
entries[index++].Set(wxACCEL_CTRL, (int)'A', wxID_SELECTALL);
entries[index++].Set(wxACCEL_CTRL, (int)'Z', wxID_UNDO);
entries[index++].Set(wxACCEL_CTRL, (int)'Y', wxID_REDO);
entries[index++].Set(wxACCEL_NORMAL, WXK_BACK, wxID_DELETE);
//entries[index++].Set(wxACCEL_NORMAL, int('+'), wxID_ADD);
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_ADD, wxID_ADD);
//entries[index++].Set(wxACCEL_NORMAL, int('-'), wxID_REMOVE);
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_SUBTRACT, wxID_REMOVE);
//entries[index++].Set(wxACCEL_NORMAL, int('p'), wxID_PRINT);
int numbers_cnt = 0;
for (auto char_number : { '1', '2', '3', '4', '5', '6', '7', '8', '9' }) {
entries[index + numbers_cnt].Set(wxACCEL_NORMAL, int(char_number), wxID_LAST + numbers_cnt+1);
entries[index + 9 + numbers_cnt].Set(wxACCEL_NORMAL, WXK_NUMPAD0 + numbers_cnt - 1, wxID_LAST + numbers_cnt+1);
numbers_cnt++;
// index++;
}
wxAcceleratorTable accel(26, entries);
SetAcceleratorTable(accel);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->copy(); }, wxID_COPY);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->paste(); }, wxID_PASTE);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->select_item_all_children(); }, wxID_SELECTALL);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->remove(); }, wxID_DELETE);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->undo(); }, wxID_UNDO);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->redo(); }, wxID_REDO);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->cut(); }, wxID_CUT);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->clone(); }, wxID_DUPLICATE);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->increase_instances(); }, wxID_ADD);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->decrease_instances(); }, wxID_REMOVE);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->toggle_printable_state(); }, wxID_PRINT);
for (int i = 1; i < 10; i++)
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
if (filaments_count() > 1 && i <= filaments_count())
this->set_extruder_for_selected_items(i);
}, wxID_LAST+i);
m_accel = accel;
}
m_shortcut_id_base = wxWindow::NewControlId(int(Shortcut::Count));
for (size_t i = 0; i < size_t(Shortcut::Count); ++i)
this->Bind(wxEVT_MENU, [this, shortcut = Shortcut(i)](wxCommandEvent&) { dispatch_shortcut(shortcut); }, m_shortcut_id_base + int(i));
for (int i = 1; i < 10; i++)
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
if (filaments_count() > 1 && i <= filaments_count())
this->set_extruder_for_selected_items(i);
}, wxID_LAST+i);
update_shortcut_accelerators();
#else //__WXOSX__
Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { key_event(event); }); // doesn't work on OSX
#endif
@@ -1796,36 +1756,10 @@ void ObjectList::decrease_instances()
#ifndef __WXOSX__
void ObjectList::key_event(wxKeyEvent& event)
{
//if (event.GetKeyCode() == WXK_TAB)
// Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
//else
if (event.GetKeyCode() == WXK_DELETE /*|| event.GetKeyCode() == WXK_BACK*/ )
remove();
//else if (event.GetKeyCode() == WXK_F5)
// wxGetApp().plater()->reload_all_from_disk();
else if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL/*WXK_SHIFT*/))
select_item_all_children();
else if (wxGetKeyState(wxKeyCode('C')) && wxGetKeyState(WXK_CONTROL))
copy();
else if (wxGetKeyState(wxKeyCode('V')) && wxGetKeyState(WXK_CONTROL))
paste();
else if (wxGetKeyState(wxKeyCode('Y')) && wxGetKeyState(WXK_CONTROL))
redo();
else if (wxGetKeyState(wxKeyCode('Z')) && wxGetKeyState(WXK_CONTROL))
undo();
else if (wxGetKeyState(wxKeyCode('X')) && wxGetKeyState(WXK_CONTROL))
cut();
else if (wxGetKeyState(wxKeyCode('K')) && wxGetKeyState(WXK_CONTROL))
clone();
else if (event.GetUnicodeKey() == '+')
increase_instances();
else if (event.GetUnicodeKey() == '-')
decrease_instances();
else if (event.GetUnicodeKey() == 'p')
toggle_printable_state();
else if (event.GetUnicodeKey() == 'd')
toggle_auto_drop();
else if (filaments_count() > 1) {
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::ObjectList, KeyChord::from_event(event));
if (shortcut.has_value() && dispatch_shortcut(*shortcut))
return;
if (filaments_count() > 1) {
std::vector<wxChar> numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
wxChar key_char = event.GetUnicodeKey();
if (std::find(numbers.begin(), numbers.end(), key_char) != numbers.end()) {
@@ -1842,6 +1776,43 @@ void ObjectList::key_event(wxKeyEvent& event)
}
#endif /* __WXOSX__ */
#ifdef __WXOSX__
void ObjectList::update_shortcut_accelerators()
{
std::vector<wxAcceleratorEntry> entries;
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
for (Shortcut shortcut : shortcuts_in(ShortcutContext::ObjectList))
if (const KeyChord chord = shortcuts.binding(shortcut); chord.valid())
entries.push_back(chord.to_accelerator_entry(m_shortcut_id_base + int(shortcut)));
for (int i = 1; i < 10; ++i) {
entries.emplace_back(wxACCEL_NORMAL, '0' + i, wxID_LAST + i);
entries.emplace_back(wxACCEL_NORMAL, WXK_NUMPAD0 + i, wxID_LAST + i);
}
m_accel = wxAcceleratorTable(int(entries.size()), entries.data());
SetAcceleratorTable(m_accel);
}
#endif /* __WXOSX__ */
bool ObjectList::dispatch_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::DeleteSelected: remove(); break;
case Shortcut::SelectAll: select_item_all_children(); break;
case Shortcut::Copy: copy(); break;
case Shortcut::Paste: paste(); break;
case Shortcut::Cut: cut(); break;
case Shortcut::Undo: undo(); break;
case Shortcut::Redo: redo(); break;
case Shortcut::CloneSelected: clone(); break;
case Shortcut::AddInstance: increase_instances(); break;
case Shortcut::RemoveInstance: decrease_instances(); break;
case Shortcut::TogglePrintable: toggle_printable_state(); break;
case Shortcut::ToggleAutoDrop: toggle_auto_drop(); break;
default: return false;
}
return true;
}
void ObjectList::OnBeginDrag(wxDataViewEvent &event)
{
const bool mult_sel = multiple_selection();
+8 -1
View File
@@ -40,6 +40,9 @@ typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI {
enum class Shortcut : uint8_t;
struct ObjectVolumeID {
ModelObject* object{ nullptr };
ModelVolume* volume{ nullptr };
@@ -270,7 +273,11 @@ public:
void extruder_editing();
#ifndef __WXOSX__
void key_event(wxKeyEvent& event);
#else
// wxDataViewCtrl never sees key events on macOS, so the bindings are installed as accelerators.
void update_shortcut_accelerators();
#endif /* __WXOSX__ */
bool dispatch_shortcut(Shortcut shortcut);
void copy();
void paste();
@@ -482,8 +489,8 @@ public:
private:
#ifdef __WXOSX__
// void OnChar(wxKeyEvent& event);
wxAcceleratorTable m_accel;
wxWindowID m_shortcut_id_base;
#endif /* __WXOSX__ */
void OnContextMenu(wxDataViewEvent &event);
void list_manipulation(const wxPoint& mouse_pos, bool evt_context_menu = false);
-24
View File
@@ -280,8 +280,6 @@ bool Preview::init(wxWindow* parent, Bed3D& bed, Model* model)
m_canvas->enable_assemble_view_toolbar(false);
// sizer, m_canvas_widget
m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_layers_slider_from_canvas, this);
wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0);
@@ -505,28 +503,6 @@ void Preview::update_layers_slider_mode()
m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder, can_change_color);
}
void Preview::update_layers_slider_from_canvas(wxKeyEvent &event)
{
if (event.HasModifiers()) {
event.Skip();
return;
}
const auto key = event.GetKeyCode();
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
IMSlider *m_moves_slider = m_canvas->get_gcode_viewer().get_moves_slider();
if (key == 'L') {
if(!m_layers_slider->switch_one_layer_mode())
event.Skip();
m_canvas->set_as_dirty();
}
/*else if (key == WXK_SHIFT)
m_layers_slider->UseDefaultColors(false);*/
else
event.Skip();
}
void Preview::update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range)
{
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
-1
View File
@@ -171,7 +171,6 @@ private:
void update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range = false);
void update_layers_slider_mode();
void update_layers_slider_from_canvas(wxKeyEvent &event);
//BBS: add only gcode mode
void load_print_as_fff(bool keep_z_range = false, bool only_gcode = false);
};
@@ -354,7 +354,6 @@ bool GLGizmoAdvancedCut::on_init()
if (!GLGizmoRotate3D::on_init())
return false;
m_shortcut_key = WXK_CONTROL_C;
// initiate info shortcuts
const wxString ctrl = GUI::shortkey_ctrl_prefix();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoAssembly.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoAssembly::on_init()
{
GLGizmoMeasure::on_init();
m_shortcut_key = WXK_CONTROL_Y;
m_shortcut = Shortcut::GizmoAssembly;
return true;
}
+3 -6
View File
@@ -4,6 +4,7 @@
#include <glad/gl.h>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_Colors.hpp"
@@ -299,7 +300,6 @@ GLGizmoBase::GLGizmoBase(GLCanvas3D &parent, const std::string &icon_filename, u
: m_parent(parent)
, m_group_id(-1)
, m_state(Off)
, m_shortcut_key(NO_SHORTCUT_KEY_VALUE)
, m_icon_filename(icon_filename)
, m_sprite_id(sprite_id)
, m_imgui(wxGetApp().imgui())
@@ -515,11 +515,8 @@ void GLGizmoBase::render_input_window(float x, float y, float bottom_limit)
std::string GLGizmoBase::get_name(bool include_shortcut) const
{
int key = get_shortcut_key();
std::string out = on_get_name();
if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z)
out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]";
return out;
const std::string name = on_get_name();
return include_shortcut && m_shortcut.has_value() ? wxGetApp().shortcuts().with_key(name, *m_shortcut) : name;
}
} // namespace GUI
+4 -5
View File
@@ -11,6 +11,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include <cereal/archives/binary.hpp>
#include <optional>
#include <wx/event.h>
@@ -29,6 +30,7 @@ namespace GUI {
class ImGuiWrapper;
enum class Shortcut : uint8_t;
class GLCanvas3D;
enum class CommonGizmosDataID;
class CommonGizmosDataPool;
@@ -72,9 +74,6 @@ public:
NegZ = 1 << 5,
};
// Represents NO key(button on keyboard) value
static const int NO_SHORTCUT_KEY_VALUE = 0;
protected:
struct Grabber
{
@@ -138,7 +137,7 @@ protected:
int m_group_id; // TODO: remove only for rotate
EState m_state;
int m_shortcut_key;
std::optional<Shortcut> m_shortcut; // the registry entry that opens this gizmo
std::string m_icon_filename;
unsigned int m_sprite_id;
int m_hover_id{ -1 };
@@ -169,7 +168,7 @@ public:
EState get_state() const { return m_state; }
void set_state(EState state) { m_state = state; on_set_state(); }
int get_shortcut_key() const { return m_shortcut_key; }
std::optional<Shortcut> shortcut() const { return m_shortcut; }
const std::string& get_icon_filename() const { return m_icon_filename; }
+2 -1
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoBrimEars::on_init()
{
m_new_point_head_radius = get_brim_default_radius();
m_shortcut_key = WXK_CONTROL_E;
m_shortcut = Shortcut::GizmoBrimEars;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
+2 -1
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/format.hpp"
@@ -1310,7 +1311,7 @@ void GLGizmoCut3D::render_cut_line()
bool GLGizmoCut3D::on_init()
{
m_grabbers.emplace_back();
m_shortcut_key = WXK_CONTROL_C;
m_shortcut = Shortcut::GizmoCut;
// initiate info shortcuts
const wxString ctrl = GUI::shortkey_ctrl_prefix();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoEmboss.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/MainFrame.hpp" // to update title when add text
@@ -727,7 +728,7 @@ bool GLGizmoEmboss::on_init()
m_rotate_gizmo.set_highlight_color(gray_color);
// NOTE: It has special handling in GLGizmosManager::handle_shortcut
m_shortcut_key = WXK_CONTROL_T;
m_shortcut = Shortcut::GizmoEmboss;
m_shortcuts = {
{_L("Drag"), _L("Position on surface")}
+10 -20
View File
@@ -8,6 +8,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -78,7 +79,7 @@ std::string GLGizmoFdmSupports::on_get_name() const
bool GLGizmoFdmSupports::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_L;
m_shortcut = Shortcut::GizmoFdmSupports;
m_desc["perform"] = _L("Apply");
m_desc["on_overhangs_only"] = _L("On highlighted overhangs only");
@@ -149,25 +150,14 @@ void GLGizmoFdmSupports::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoFdmSupports::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoFdmSupports::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -26,8 +26,7 @@ public:
state_ready
};
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoFlatten.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
@@ -54,7 +55,7 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
bool GLGizmoFlatten::on_init()
{
m_shortcut_key = WXK_CONTROL_F;
m_shortcut = Shortcut::GizmoFlatten;
return true;
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
@@ -31,7 +32,7 @@ std::string GLGizmoFuzzySkin::on_get_name() const
bool GLGizmoFuzzySkin::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_shortcut = Shortcut::GizmoFuzzySkin;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
-1
View File
@@ -25,7 +25,6 @@ GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filenam
bool GLGizmoHollow::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_desc["enable"] = _(L("Hollow this object"));
m_desc["preview"] = _(L("Preview hollowed and drilled model"));
m_desc["offset"] = _(L("Offset")) + ": ";
+2 -1
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -449,7 +450,7 @@ bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_po
bool GLGizmoMeasure::on_init()
{
m_shortcut_key = WXK_CONTROL_U;
m_shortcut = Shortcut::GizmoMeasure;
const wxString shift = GUI::shortkey_shift_prefix();
+2 -1
View File
@@ -1,5 +1,6 @@
#include "GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "libslic3r/MeshBoolean.hpp"
@@ -104,7 +105,7 @@ bool GLGizmoMeshBoolean::on_mouse(const wxMouseEvent &mouse_event)
bool GLGizmoMeshBoolean::on_init()
{
m_shortcut_key = WXK_CONTROL_B;
m_shortcut = Shortcut::GizmoMeshBoolean;
return true;
}
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Plater.hpp"
@@ -90,7 +91,7 @@ void GLGizmoMmuSegmentation::init_extruders_data()
bool GLGizmoMmuSegmentation::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_N;
m_shortcut = Shortcut::GizmoMmuSegmentation;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -209,30 +210,16 @@ bool GLGizmoMmuSegmentation::on_number_key_down(int number)
return true;
}
bool GLGizmoMmuSegmentation::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'T':
m_current_tool = ImGui::TriangleButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'H':
m_current_tool = ImGui::HeightRangeIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoMmuSegmentation::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolTriangle: m_current_tool = ImGui::TriangleButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolHeightRange: m_current_tool = ImGui::HeightRangeIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
@@ -82,7 +82,7 @@ public:
// BBS
bool on_number_key_down(int number);
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoMove.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
//BBS: GUI refactor
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/AppConfig.hpp"
@@ -61,7 +62,7 @@ bool GLGizmoMove3D::on_init()
m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 };
m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 };
m_shortcut_key = WXK_CONTROL_M;
m_shortcut = Shortcut::GizmoMove;
return true;
}
@@ -192,6 +192,8 @@ public:
~GLGizmoPainterBase() override;
void data_changed(bool is_serializing) override;
virtual bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down);
// Switches the painting tool a Painting-context shortcut names; false when this gizmo has no such tool.
virtual bool on_tool_shortcut(Shortcut shortcut) { return false; }
// Following function renders the triangles and cursor. Having this separated
// from usual on_render method allows to render them before transparent
+2 -1
View File
@@ -3,6 +3,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp"
@@ -555,7 +556,7 @@ bool GLGizmoRotate3D::on_init()
for (unsigned int i = 0; i < 3; ++i)
m_gizmos[i].set_highlight_color(AXES_COLOR[i]);
m_shortcut_key = WXK_CONTROL_R;
m_shortcut = Shortcut::GizmoRotate;
return true;
}
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoScale.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <glad/gl.h>
@@ -135,7 +136,7 @@ bool GLGizmoScale3D::on_init()
// BBS
m_grabbers[4].enabled = false;
m_shortcut_key = WXK_CONTROL_S;
m_shortcut = Shortcut::GizmoScale;
return true;
}
+8 -14
View File
@@ -5,6 +5,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -28,7 +29,7 @@ void GLGizmoSeam::on_shutdown()
bool GLGizmoSeam::on_init()
{
m_shortcut_key = WXK_CONTROL_P;
m_shortcut = Shortcut::GizmoSeam;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -86,19 +87,12 @@ void GLGizmoSeam::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoSeam::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
default:
return false;
break;
bool GLGizmoSeam::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -12,8 +12,7 @@ public:
void render_painter_gizmo() override;
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
@@ -34,7 +34,6 @@ GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& ic
bool GLGizmoSlaSupports::on_init()
{
m_shortcut_key = WXK_CONTROL_L;
m_desc["head_diameter"] = _L("Head diameter") + ": ";
m_desc["lock_supports"] = _L("Lock supports under new islands");
-1
View File
@@ -261,7 +261,6 @@ bool GLGizmoText::on_init()
//m_avail_font_names = init_occt_fonts();
update_font_texture();
m_scale = m_imgui->get_font_size();
m_shortcut_key = WXK_CONTROL_T;
m_grabbers.push_back(Grabber());
+27 -59
View File
@@ -4,6 +4,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -486,15 +487,13 @@ bool GLGizmosManager::is_running() const
return m_current != Undefined;
}
bool GLGizmosManager::handle_shortcut(int key)
bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut)
{
if (!m_enabled)
return false;
auto is_key = [pressed_key = key](int gizmo_key) { return (gizmo_key == pressed_key - 64) || (gizmo_key == pressed_key - 96); };
// allowe open shortcut even when selection is empty
if (GLGizmoBase* gizmo_emboss = m_gizmos[Emboss].get();
is_key(gizmo_emboss->get_shortcut_key())) {
// The text tool opens without a selection because it creates its own object.
if (GLGizmoBase* gizmo_emboss = m_gizmos[Emboss].get(); gizmo_emboss->shortcut() == shortcut) {
dynamic_cast<GLGizmoEmboss *>(gizmo_emboss)->on_shortcut_key();
return true;
}
@@ -502,16 +501,21 @@ bool GLGizmosManager::handle_shortcut(int key)
if (m_parent.get_selection().is_empty())
return false;
auto is_gizmo = [is_key](const std::unique_ptr<GLGizmoBase> &gizmo) {
return gizmo->is_activable() && is_key(gizmo->get_shortcut_key());
};
auto it = std::find_if(m_gizmos.begin(), m_gizmos.end(), is_gizmo);
auto it = std::find_if(m_gizmos.begin(), m_gizmos.end(), [shortcut](const std::unique_ptr<GLGizmoBase> &gizmo) {
return gizmo->is_activable() && gizmo->shortcut() == shortcut;
});
if (it == m_gizmos.end())
return false;
EType gizmo_type = EType(it - m_gizmos.begin());
return open_gizmo(gizmo_type);
return open_gizmo(EType(it - m_gizmos.begin()));
}
bool GLGizmosManager::on_delete_key()
{
const bool processed = (m_current == Cut || m_current == Measure || m_current == Assembly) && gizmo_event(SLAGizmoEventType::Delete);
if (processed)
m_parent.set_as_dirty();
return processed;
}
bool GLGizmosManager::is_dragging() const
@@ -856,15 +860,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
}
break;
}
//skip some keys when gizmo
case 'A':
case 'a':
{
if (is_running()) {
processed = true;
}
break;
}
//case WXK_RETURN:
//{
// if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::ApplyChanges))
@@ -883,12 +878,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
//}
case WXK_BACK:
case WXK_DELETE: {
if ((m_current == Cut || m_current == Measure || m_current == Assembly) && gizmo_event(SLAGizmoEventType::Delete))
processed = true;
break;
}
//case 'A':
//case 'a':
//{
@@ -932,11 +921,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
}
}
if (!processed && !evt.HasModifiers()) {
if (handle_shortcut(keyCode))
processed = true;
}
if (processed)
m_parent.set_as_dirty();
@@ -1077,40 +1061,24 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
processed = select(digit);
}
}
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
processed = mmu_seg->on_key_down_select_tool_type(keyCode);
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
}
}
else if (m_current == FdmSupports) {
GLGizmoFdmSupports* fdm_support = dynamic_cast<GLGizmoFdmSupports*>(get_current());
if (fdm_support != nullptr && (keyCode == 'F' || keyCode == 'S' || keyCode == 'C' || keyCode == 'G')) {
processed = fdm_support->on_key_down_select_tool_type(keyCode);
}
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
else if (m_current == Seam) {
GLGizmoSeam* seam = dynamic_cast<GLGizmoSeam*>(get_current());
if (seam != nullptr && (keyCode == 'S' || keyCode == 'C')) {
processed = seam->on_key_down_select_tool_type(keyCode);
}
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
} else if (m_current == Measure || m_current == Assembly) {
else if (m_current == Measure || m_current == Assembly) {
if (keyCode == WXK_CONTROL)
gizmo_event(SLAGizmoEventType::CtrlDown, Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.CmdDown());
else if (keyCode == WXK_SHIFT)
gizmo_event(SLAGizmoEventType::ShiftDown, Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.CmdDown());
}
if (!processed) {
if (auto painter = dynamic_cast<GLGizmoPainterBase*>(get_current()); painter != nullptr) {
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Painting, KeyChord::from_event(evt));
processed = shortcut.has_value() && painter->on_tool_shortcut(*shortcut);
if (processed)
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
}
if (processed)
+4 -1
View File
@@ -263,7 +263,10 @@ public:
EType get_gizmo_from_name(const std::string& gizmo_name) const;
bool is_running() const;
bool handle_shortcut(int key);
// Opens the gizmo bound to a Plater-context shortcut; false when no gizmo has it or it cannot open now.
bool open_gizmo_by_shortcut(Shortcut shortcut);
// Lets the current gizmo consume the delete key; false when it did not.
bool on_delete_key();
bool is_dragging() const;
+2 -2
View File
@@ -5,6 +5,7 @@
#include "GUI_ObjectList.hpp"
#include "GLCanvas3D.hpp"
#include "MainFrame.hpp"
#include "Preferences.hpp"
#include "Tab.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
@@ -444,9 +445,8 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path)
// open preferences
}
else if (dict["hypertext_type"] == "preferences") {
std::string page = dict["hypertext_preferences_page"];
std::string item = dict["hypertext_preferences_item"];
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [page, item]() { wxGetApp().open_preferences(1, page); } };// 1 is to modify
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [item]() { wxGetApp().open_preferences(PreferencesTab::Control, item); } };
m_loaded_hints.emplace_back(hint_data);
}
else if (dict["hypertext_type"] == "plater") {
+491 -309
View File
@@ -6,359 +6,319 @@
#include "Notebook.hpp"
#include <wx/scrolwin.h>
#include <wx/display.h>
#include <algorithm>
#include <set>
#include "GUI_App.hpp"
#include "wxExtensions.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include "Preferences.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/StaticBox.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/TabCtrl.hpp"
#include <wx/notebook.h>
namespace Slic3r {
namespace GUI {
wxDEFINE_EVENT(EVT_PREFERENCES_SELECT_TAB, wxCommandEvent);
namespace {
KBShortcutsDialog::KBShortcutsDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,_L("Keyboard Shortcuts"),
wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
wxString shortcut_names(const std::vector<Shortcut>& shortcuts)
{
// fonts
const wxFont& font = wxGetApp().normal_font();
const wxFont& bold_font = wxGetApp().bold_font();
SetFont(font);
this->SetSizeHints(wxDefaultSize, wxDefaultSize);
this->SetBackgroundColour(wxColour(255, 255, 255));
wxBoxSizer *m_sizer_top = new wxBoxSizer(wxVERTICAL);
auto m_top_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
m_top_line->SetBackgroundColour(wxColour(166, 169, 170));
m_sizer_top->Add(m_top_line, 0, wxEXPAND, 0);
m_sizer_body = new wxBoxSizer(wxHORIZONTAL);
m_panel_selects = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
m_panel_selects->SetBackgroundColour(wxColour(248, 248, 248));
wxBoxSizer *m_sizer_left = new wxBoxSizer(wxVERTICAL);
m_sizer_left->Add(0, 0, 0, wxEXPAND | wxTOP, FromDIP(20));
m_sizer_left->Add(create_button(0, _L("Global")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(1, _L("Prepare")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(2, _L("Toolbar")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(3, _L("Objects list")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(4, _L("Preview")), 0, wxEXPAND, 0);
m_panel_selects->SetSizer(m_sizer_left);
m_panel_selects->Layout();
m_sizer_left->Fit(m_panel_selects);
m_sizer_body->Add(m_panel_selects, 0, wxEXPAND, 0);
m_sizer_right = new wxBoxSizer(wxHORIZONTAL);
m_sizer_right->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(12));
m_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(870), FromDIP(500)), 0);
m_sizer_right->Add(m_simplebook, 1, wxEXPAND, 0);
m_sizer_body->Add(m_sizer_right, 1, wxEXPAND, 0);
m_sizer_top->Add(m_sizer_body, 1, wxEXPAND, 0);
fill_shortcuts();
for (size_t i = 0; i < m_full_shortcuts.size(); ++i) {
wxPanel *page = create_page(m_simplebook, m_full_shortcuts[i], font, bold_font);
m_pages.push_back(page);
m_simplebook->AddPage(page, m_full_shortcuts[i].first.first, i == 0);
wxString names;
for (Shortcut shortcut : shortcuts) {
if (!names.empty())
names += ", ";
names += _(shortcut_info(shortcut).name);
}
return names;
}
Bind(EVT_PREFERENCES_SELECT_TAB, &KBShortcutsDialog::OnSelectTabel, this);
const wxColour ERROR_COLOUR("#D01B1B");
SetSizer(m_sizer_top);
Layout();
Fit();
// The camera action a mouse button drags, as set in Preferences > Control.
const char* mouse_action(const char* preference)
{
const std::string action = wxGetApp().app_config->get(preference);
return action == "1" ? L("Pan View") : action == "2" ? L("Rotate View") : L("None");
}
// Page layout in DIPs; titles and rows are indented as in the Preferences dialog.
constexpr int PAGE_WIDTH = 640;
constexpr int TITLE_MARGIN = DESIGN_LEFT_MARGIN - 10;
constexpr int ROW_MARGIN = DESIGN_LEFT_MARGIN;
constexpr int ROW_GAP = 16;
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::vector<wxString> to_wx(const std::vector<std::string>& parts)
{
std::vector<wxString> out;
for (const std::string& part : parts)
out.push_back(from_u8(part));
return out;
}
// The keys a Global shortcut can use, the second line of its hint and of a rejection.
wxString global_key_advice()
{
return wxString::Format(_L("Use %s or %s, or a key that does not type a character."),
from_u8(KeyChord::modifier_name(wxMOD_CONTROL)), from_u8(KeyChord::modifier_name(wxMOD_ALT)));
}
// The pieces of a chord, spaced out for the dialog: "Ctrl + Shift + A".
wxString join_keys(const std::vector<wxString>& parts)
{
wxString out;
for (const wxString& part : parts)
out += (out.empty() ? "" : " + ") + part;
return out;
}
} // namespace
KBShortcutsDialog::KBShortcutsDialog(wxWindow* parent, ShortcutContext page)
: DPIDialog(parent, wxID_ANY, _L("Keyboard Shortcuts"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
fill_pages();
ScalableButton* probe = new ScalableButton(this, wxID_ANY, "edit");
m_edit_size = probe->GetBestSize();
probe->Destroy();
m_buttons_width = 2 * m_edit_size.x + FromDIP(6);
m_row_text_width = FromDIP(PAGE_WIDTH) - FromDIP(ROW_MARGIN) - FromDIP(TITLE_MARGIN) - 2 * FromDIP(ROW_GAP) - m_buttons_width;
GetTextExtent("W", &m_key_slot, nullptr, nullptr, nullptr, &Label::Head_14);
// The page tabs follow the Preferences dialog.
m_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | wxTR_FULL_ROW_HIGHLIGHT);
m_tabs->Bind(wxEVT_RIGHT_DOWN, [](auto&) {});
m_tabs->SetFont(Label::Body_14);
m_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(660), FromDIP(500)));
for (const Page& page : m_pages) {
m_tabs->AppendItem(page.title);
m_simplebook->AddPage(create_page(m_simplebook, page), page.title);
}
const StateColor tab_colour(std::make_pair(wxColour("#6B6B6C"), (int) StateColor::NotChecked), std::make_pair(wxColour("#363636"), (int) StateColor::Normal));
for (size_t i = 0; i < m_tabs->GetCount(); ++i)
m_tabs->SetItemTextColour(i, tab_colour);
m_tabs->Bind(wxEVT_TAB_SEL_CHANGED, [this](wxCommandEvent& e) {
for (size_t i = 0; i < m_tabs->GetCount(); ++i)
m_tabs->SetItemBold(i, int(i) == e.GetSelection());
m_simplebook->SetSelection(e.GetSelection());
});
const auto shown = std::find_if(m_pages.begin(), m_pages.end(), [page](const Page& entry) { return entry.context == page; });
m_tabs->SelectItem(shown == m_pages.end() ? 0 : int(shown - m_pages.begin()));
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(m_tabs, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(5));
sizer->Add(m_simplebook, 1, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent();
// select first
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(0);
event.SetEventObject(this);
wxPostEvent(this, event);
wxGetApp().UpdateDlgDarkUI(this);
}
void KBShortcutsDialog::OnSelectTabel(wxCommandEvent &event)
{
auto id = event.GetInt();
SelectHash::iterator i = m_hash_selector.begin();
while (i != m_hash_selector.end()) {
Select *sel = i->second;
if (id == sel->m_index) {
sel->m_tab_button->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#BFE1DE"))); // ORCA color for selected tab background
sel->m_tab_text->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#BFE1DE"))); // ORCA color for selected tab background
sel->m_tab_text->SetFont(::Label::Head_13);
sel->m_tab_button->Refresh();
sel->m_tab_text->Refresh();
m_simplebook->SetSelection(id);
} else {
sel->m_tab_button->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
sel->m_tab_text->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
sel->m_tab_text->SetFont(::Label::Body_13);
sel->m_tab_button->Refresh();
sel->m_tab_text->Refresh();
}
i++;
}
wxGetApp().UpdateDlgDarkUI(this);
}
wxWindow *KBShortcutsDialog::create_button(int id, wxString text)
{
auto tab_button = new wxWindow(m_panel_selects, wxID_ANY, wxDefaultPosition, wxSize( FromDIP(150), FromDIP(28)), wxTAB_TRAVERSAL);
wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(22));
auto stext = new wxStaticText(tab_button, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, 0);
stext->SetFont(::Label::Body_13);
stext->SetForegroundColour(wxColour(38, 46, 48));
stext->Wrap(-1);
sizer->Add(stext, 1, wxALIGN_CENTER, 0);
tab_button->Bind(wxEVT_LEFT_DOWN, [this, id](auto &e) {
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(id);
event.SetEventObject(this);
wxPostEvent(this, event);
});
stext->Bind(wxEVT_LEFT_DOWN, [this, id](wxMouseEvent &e) {
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(id);
event.SetEventObject(this);
wxPostEvent(this, event);
});
Select *sel = new Select;
sel->m_index = id;
sel->m_tab_button = tab_button;
sel->m_tab_text = stext;
m_hash_selector[sel->m_index] = sel;
tab_button->SetSizer(sizer);
tab_button->Layout();
return tab_button;
}
void KBShortcutsDialog::on_dpi_changed(const wxRect& suggested_rect)
{
m_logo_bmp.msw_rescale();
m_header_bitmap->SetBitmap(m_logo_bmp.bmp());
msw_buttons_rescale(this, em_unit(), { wxID_OK });
m_tabs->Rescale();
Layout();
Fit();
Refresh();
}
void KBShortcutsDialog::fill_shortcuts()
void KBShortcutsDialog::fill_pages()
{
const std::string ctrl = GUI::shortkey_ctrl_prefix();
const std::string alt = GUI::shortkey_alt_prefix();
const std::string shift = L("Shift+");
// A fixed row is listed in the section of the shortcuts it belongs with.
auto fixed = [](ShortcutSection section, std::vector<wxString> keys, const char* description) { return Row{ FixedKey{ std::move(keys), description }, section }; };
auto mouse = [](ShortcutSection section, const wxString& button, const char* preference) { return Row{ MouseAction{ button, preference }, section }; };
auto key = [](const std::string& key) { return _L_CONTEXT(key, "Keyboard Shortcut"); };
auto page = [this](const wxString& title, const wxString& caption, ShortcutContext context, std::vector<Row> fixed_rows) {
Page entry{ title, caption, context, {} };
for (Shortcut shortcut : shortcuts_in(context))
entry.rows.push_back({ shortcut, shortcut_section(shortcut) });
entry.rows.insert(entry.rows.end(), fixed_rows.begin(), fixed_rows.end());
std::stable_sort(entry.rows.begin(), entry.rows.end(), [](const Row& a, const Row& b) { return a.section < b.section; });
m_pages.push_back(std::move(entry));
};
const wxString ctrl = from_u8(KeyChord::modifier_name(wxMOD_CONTROL));
const wxString alt = from_u8(KeyChord::modifier_name(wxMOD_ALT));
const wxString shift = from_u8(KeyChord::modifier_name(wxMOD_SHIFT));
const wxString shift_ctrl = shift + "/" + ctrl; // either one
const wxString any_key = key(L_CONTEXT("Key", "Keyboard Shortcut")); // the key the row's shortcut is bound to
const wxString esc = key(L_CONTEXT("Esc", "Keyboard Shortcut"));
const wxString left_button = _L("Left mouse");
const wxString wheel = _L("Mouse wheel");
using Section = ShortcutSection;
if (wxGetApp().is_editor()) {
Shortcuts global_shortcuts = {
// File
{ ctrl + "N", L("New Project") },
{ ctrl + "O", L("Open Project") },
{ ctrl + "S", L("Save Project") },
{ ctrl + shift + "S", L("Save Project as")},
{ ctrl + shift + "E", L("Publish 3MF") },
// File>Import
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
// File>Export
{ ctrl + "G", L("Export plate sliced file")},
// Slice plate
{ ctrl + "R", L("Slice plate")},
// Send to Print
{ ctrl + shift + "G", L("Print plate")},
// Edit
{ ctrl + "X", L("Cut") },
{ ctrl + "C", L("Copy to clipboard") },
{ ctrl + "V", L("Paste from clipboard") },
// Configuration
{ ctrl + "P", L("Preferences") },
//3D control
#ifdef __APPLE__
{ ctrl + shift + "M", L("Show/Hide 3Dconnexion devices settings dialog") },
#else
{ ctrl + "M", L("Show/Hide 3Dconnexion devices settings dialog") },
#endif // __APPLE
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
fixed(Section::Application, { alt, "1-9, 0" }, L("Run a speed dial favorite while the dial is open")),
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
});
// Switch table page
{ ctrl + L("Tab"), L("Switch table page")},
// Open speed dial
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") },
{ alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") },
//DEL
#ifdef __APPLE__
{"fn+⌫", L("Delete Selected")},
#else
{L_CONTEXT("Del", "Keyboard Shortcut"), L("Delete Selected")},
#endif
// Help
{ "?", L("Show keyboard shortcuts list") }
};
m_full_shortcuts.push_back({{_L("Global shortcuts"), ""}, global_shortcuts});
page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, {
fixed(Section::Selection, { alt, left_button }, L("Select a part")),
fixed(Section::Selection, { ctrl, left_button }, L("Select multiple objects")),
fixed(Section::Selection, { shift, left_button }, L("Select objects by rectangle")),
fixed(Section::Selection, { esc }, L("Deselect All")),
fixed(Section::Objects, { "1-9" }, L("Keyboard 1-9: set filament for object/part")),
fixed(Section::Placement, { shift, any_key }, L("Movement step set to 1mm")),
fixed(Section::Placement, { ctrl, any_key }, L("Movement in camera space")),
mouse(Section::Camera, left_button, "left_mouse_drag_action"),
mouse(Section::Camera, _L("Middle mouse"), "middle_mouse_drag_action"),
mouse(Section::Camera, _L("Right mouse"), "right_mouse_drag_action"),
fixed(Section::Camera, { wheel }, L("Zoom View")),
});
// Retrieve mouse actions from config and map to MouseAction
std::map<std::string, std::string> mouse_actions;
mouse_actions["0"] = L("None");
mouse_actions["1"] = L("Pan View");
mouse_actions["2"] = L("Rotate View");
page(_L("Painting"), _L("Available while a painting gizmo is open: supports, seam, fuzzy skin or color painting."), ShortcutContext::Painting, {
fixed(Section::Gizmos, { esc }, L("Deselect All")),
fixed(Section::Gizmos, { shift, left_button }, L("Move: press to snap by 1mm")),
fixed(Section::PaintingTools, { ctrl, wheel }, L("Support/Color Painting: adjust pen radius")),
fixed(Section::PaintingTools, { alt, wheel }, L("Support/Color Painting: adjust section position")),
});
Shortcuts plater_shortcuts = {
{ L("Left mouse button"), mouse_actions[wxGetApp().app_config->get("left_mouse_drag_action").c_str()]},
{ L("Middle mouse button"), mouse_actions[wxGetApp().app_config->get("middle_mouse_drag_action").c_str()]},
{ L("Right mouse button"), mouse_actions[wxGetApp().app_config->get("right_mouse_drag_action").c_str()]},
{ L("Mouse wheel"), L("Zoom View") },
{ "A", L("Arrange all objects") },
{ shift + "A", L("Arrange objects on selected plates") },
{ "Q", L("Auto orients selected objects or all objects. If there are selected objects, it just orients the selected ones. Otherwise, it will orient all objects in the current project.") },
{ shift + "Q", L("Auto orients all objects on the active plate.") },
{shift + L("Tab"), L("Collapse/Expand the sidebar")},
{ctrl + L("Any arrow"), L("Movement in camera space")},
{alt + L("Left mouse button"), L("Select a part")},
{ctrl + L("Left mouse button"), L("Select multiple objects")},
{shift + L("Left mouse button"), L("Select objects by rectangle")},
{L_CONTEXT("Arrow Up", "Keyboard Shortcut"), L("Move selection 10mm in positive Y direction")},
{L_CONTEXT("Arrow Down", "Keyboard Shortcut"), L("Move selection 10mm in negative Y direction")},
{L_CONTEXT("Arrow Left", "Keyboard Shortcut"), L("Move selection 10mm in negative X direction")},
{L_CONTEXT("Arrow Right", "Keyboard Shortcut"), L("Move selection 10mm in positive X direction")},
{shift + L("Any arrow"), L("Movement step set to 1mm")},
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{"1-9", L("Keyboard 1-9: set filament for object/part")},
{ctrl + "0", L("Camera view - Default")},
{ctrl + "1", L("Camera view - Top")},
{ctrl + "2", L("Camera view - Bottom")},
{ctrl + "3", L("Camera view - Front")},
{ctrl + "4", L("Camera view - Behind")},
{ctrl + "5", L("Camera Angle - Left side")},
{ctrl + "6", L("Camera Angle - Right side")},
{ctrl + "A", L("Select all objects")},
{ctrl + "D", L("Delete All")},
{ctrl + "Z", L("Undo")},
{ctrl + "Y", L("Redo")},
{ "M", L("Gizmo move") },
{ "R", L("Gizmo rotate") },
{ "S", L("Gizmo scale") },
{ "F", L("Gizmo place face on bed") },
{ "C", L("Gizmo cut") },
{ "B", L("Gizmo mesh boolean") },
{ "H", L("Gizmo FDM paint-on fuzzy skin") },
{ "L", L("Gizmo SLA support points") },
{ "P", L("Gizmo FDM paint-on seam") },
{ "T", L("Gizmo text emboss/engrave") },
{ "U", L("Gizmo measure") },
{ "Y", L("Gizmo assemble") },
{ "E", L("Gizmo brim ears") },
{ "I", L("Zoom in") },
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
};
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
Shortcuts gizmos_shortcuts = {
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{shift, L("Move: press to snap by 1mm")},
{ctrl + L("Mouse wheel"), L("Support/Color Painting: adjust pen radius")},
{alt + L("Mouse wheel"), L("Support/Color Painting: adjust section position")},
};
m_full_shortcuts.push_back({{_L("Gizmo"), ""}, gizmos_shortcuts});
Shortcuts object_list_shortcuts = {
{"1-9", L("Set extruder number for the objects and parts") },
{L_CONTEXT("Del", "Keyboard Shortcut"), L("Delete objects, parts, modifiers")},
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{ctrl + "C", L("Copy to clipboard")},
{ctrl + "V", L("Paste from clipboard")},
{ctrl + "X", L("Cut")},
{ctrl + "A", L("Select all objects")},
{ctrl + "K", L("Clone Selected")},
{ctrl + "Z", L("Undo")},
{ctrl + "Y", L("Redo")},
{L_CONTEXT("Space", "Keyboard Shortcut"), L("Select the object/part and press space to change the name")},
{L("Mouse click"), L("Select the object/part and mouse click to change the name")},
};
m_full_shortcuts.push_back({ { _L("Objects List"), "" }, object_list_shortcuts });
page(_L("Objects list"), _L("Available while the object list has focus."), ShortcutContext::ObjectList, {
fixed(Section::Selection, { esc }, L("Deselect All")),
fixed(Section::Objects, { "1-9" }, L("Set extruder number for the objects and parts")),
fixed(Section::Objects, { key(L_CONTEXT("Space", "Keyboard Shortcut")) }, L("Select the object/part and press space to change the name")),
fixed(Section::Objects, { _L("Mouse click") }, L("Select the object/part and mouse click to change the name")),
});
}
Shortcuts preview_shortcuts = {
{ L_CONTEXT("Arrow Up", "Keyboard Shortcut"), L("Vertical slider - Move active thumb Up")},
{ L_CONTEXT("Arrow Down", "Keyboard Shortcut"), L("Vertical slider - Move active thumb Down")},
{ L_CONTEXT("Arrow Left", "Keyboard Shortcut"), L("Horizontal slider - Move active thumb Left")},
{ L_CONTEXT("Arrow Right", "Keyboard Shortcut"), L("Horizontal slider - Move active thumb Right")},
{ "L", L("On/Off one layer mode of the vertical slider")},
{ "C", L("On/Off G-code window")},
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview")},
{shift + L("Any arrow"), L("Move slider 5x faster")},
{shift + L("Mouse wheel"), L("Move slider 5x faster")},
{ctrl + L("Any arrow"), L("Move slider 5x faster")},
{ctrl + L("Mouse wheel"), L("Move slider 5x faster")},
{ L_CONTEXT("Home", "Keyboard Shortcut"), L("Horizontal slider - Move to start position")},
{ L_CONTEXT("End", "Keyboard Shortcut"), L("Horizontal slider - Move to last position")},
};
m_full_shortcuts.push_back({ { _L("Preview"), "" }, preview_shortcuts });
page(_L("Preview"), _L("Available while the 3D view on the Preview tab has focus."), ShortcutContext::Preview, {
fixed(Section::Sliders, { shift_ctrl, any_key }, L("Move slider 5x faster")),
fixed(Section::Sliders, { shift_ctrl, wheel }, L("Scroll slider 5x faster")),
});
}
wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font)
wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
{
wxPanel* main_page = new wxPanel(parent);
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
if (!shortcuts.first.second.empty()) {
main_sizer->AddSpacer(FromDIP(10));
wxBoxSizer* info_sizer = new wxBoxSizer(wxHORIZONTAL);
info_sizer->AddStretchSpacer();
info_sizer->Add(new wxStaticText(main_page, wxID_ANY, shortcuts.first.second), 0);
info_sizer->AddStretchSpacer();
main_sizer->Add(info_sizer, 0, wxEXPAND);
main_sizer->AddSpacer(FromDIP(10));
}
int items_count = (int) shortcuts.second.size();
wxScrolledWindow *scrollable_panel = new wxScrolledWindow(main_page);
wxGetApp().UpdateDarkUI(scrollable_panel);
scrollable_panel->SetScrollbars(20, 20, 50, 50);
scrollable_panel->SetInitialSize(wxSize(FromDIP(850), FromDIP(450)));
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
scrollable_panel->SetBackgroundColour(page_colour);
scrollable_panel->SetScrollRate(0, 20);
const int page_width = FromDIP(PAGE_WIDTH);
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
wxBoxSizer * scrollable_panel_sizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer *grid_sizer = new wxFlexGridSizer(items_count, 2, FromDIP(10), FromDIP(20));
const int title_margin = FromDIP(TITLE_MARGIN);
const int row_margin = FromDIP(ROW_MARGIN);
const int gap = FromDIP(ROW_GAP);
for (int i = 0; i < items_count; ++i) {
const auto &[shortcut, description] = shortcuts.second[i];
// Keyboard keys carry a "Keyboard Shortcut" context so translators keep them in English;
// mouse-input labels are ordinary phrases and use the plain lookup.
const bool is_mouse = shortcut.find("Mouse") != std::string::npos || shortcut.find("mouse") != std::string::npos;
auto key = new wxStaticText(scrollable_panel, wxID_ANY, is_mouse ? _(shortcut) : _L_CONTEXT(shortcut, "Keyboard Shortcut"));
key->SetForegroundColour(wxColour(50, 58, 61));
key->SetFont(bold_font);
grid_sizer->Add(key, 0, wxALIGN_CENTRE_VERTICAL);
wxBoxSizer* scrollable_panel_sizer = new wxBoxSizer(wxVERTICAL);
auto desc = new wxStaticText(scrollable_panel, wxID_ANY, _(description));
desc->SetFont(font);
desc->SetForegroundColour(wxColour(50, 58, 61));
desc->Wrap(FromDIP(600));
grid_sizer->Add(desc, 0, wxALIGN_CENTRE_VERTICAL);
const wxColour note_colour = StateColor::darkModeColorFor(wxColour("#F8F8F8"));
const wxColour note_text = StateColor::darkModeColorFor(wxColour("#6B6B6A"));
StaticBox* note = new StaticBox(scrollable_panel);
note->SetCornerRadius(FromDIP(4));
note->SetBorderWidth(0);
note->SetBackgroundColor(note_colour);
note->SetBackgroundColour(note_colour);
auto note_icon = new wxStaticBitmap(note, wxID_ANY, ScalableBitmap(note, "help", 16).bmp());
auto note_text_ctrl = new wxStaticText(note, wxID_ANY, page.caption);
note_text_ctrl->SetFont(Label::Body_13);
note_text_ctrl->SetForegroundColour(note_text);
note_text_ctrl->SetBackgroundColour(note_colour);
note_text_ctrl->Wrap(page_width - 2 * title_margin - FromDIP(10 + 16 + 8 + 10));
wxBoxSizer* note_sizer = new wxBoxSizer(wxHORIZONTAL);
note_sizer->Add(note_icon, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, FromDIP(10));
note_sizer->Add(note_text_ctrl, 1, wxALIGN_CENTRE_VERTICAL | wxALL, FromDIP(8));
note->SetSizer(note_sizer);
scrollable_panel_sizer->Add(note, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, title_margin);
auto key_parts = [](const Row& row) {
return std::visit(overloaded{
[](Shortcut shortcut) { return to_wx(wxGetApp().shortcuts().binding(shortcut).display_parts()); },
[](const FixedKey& fixed) { return fixed.keys; },
[](const MouseAction& mouse) { return std::vector<wxString>{ mouse.button }; },
}, row.content);
};
auto description = [](const Row& row) {
return std::visit(overloaded{
[](Shortcut shortcut) { return _(shortcut_info(shortcut).name); },
[](const FixedKey& fixed) { return _(fixed.description); },
[](const MouseAction& mouse) { return _(mouse_action(mouse.preference)); },
}, row.content);
};
auto icon_button = [&](const char* icon, const wxString& tooltip) {
auto button = new ScalableButton(scrollable_panel, wxID_ANY, icon);
button->SetBackgroundColour(page_colour);
button->SetToolTip(tooltip);
return button;
};
std::optional<ShortcutSection> section;
for (const Row& row : page.rows) {
if (section != row.section) {
auto heading = new StaticLine(scrollable_panel, false, _(section_name(row.section)));
heading->SetFont(Label::Head_14);
heading->SetForegroundColour(DESIGN_GRAY900_COLOR);
wxBoxSizer* heading_sizer = new wxBoxSizer(wxHORIZONTAL);
heading_sizer->AddSpacer(title_margin);
heading_sizer->Add(heading, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
heading_sizer->AddSpacer(title_margin);
scrollable_panel_sizer->Add(heading_sizer, 0, wxEXPAND | wxTOP, FromDIP(section.has_value() ? 10 : 6));
section = row.section;
}
auto desc = new wxStaticText(scrollable_panel, wxID_ANY, description(row));
desc->SetFont(Label::Body_14);
desc->SetForegroundColour(DESIGN_GRAY900_COLOR);
auto chord_label = [&](long style) {
auto label = new wxStaticText(scrollable_panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, style);
label->SetFont(Label::Head_14);
label->SetForegroundColour(DESIGN_GRAY900_COLOR);
return label;
};
wxStaticText* modifiers = chord_label(0);
wxStaticText* key = chord_label(wxALIGN_CENTRE_HORIZONTAL); // a single key is centred in its column
desc->Wrap(m_row_text_width - set_chord_labels(modifiers, key, key_parts(row)));
wxBoxSizer* buttons = new wxBoxSizer(wxHORIZONTAL);
if (const MouseAction* mouse = std::get_if<MouseAction>(&row.content)) {
auto settings = icon_button("settings", _L("Preferences"));
settings->Bind(wxEVT_BUTTON, [this, preference = mouse->preference](wxCommandEvent&) { open_mouse_preferences(preference); });
buttons->Add(settings, 0, wxALIGN_CENTRE_VERTICAL);
m_preference_rows.push_back({ mouse->preference, desc });
} else if (const Shortcut* editable = std::get_if<Shortcut>(&row.content)) {
const Shortcut shortcut = *editable;
auto change = icon_button("edit", _L("Edit"));
change->Bind(wxEVT_BUTTON, [this, shortcut](wxCommandEvent&) { edit_shortcut(shortcut); });
auto reset = icon_button("undo", _L("Reset"));
reset->Bind(wxEVT_BUTTON, [this, shortcut](wxCommandEvent&) { reset_shortcut(shortcut); });
reset->Show(wxGetApp().shortcuts().is_customized(shortcut));
buttons->Add(change, 0, wxALIGN_CENTRE_VERTICAL | wxRIGHT, FromDIP(6));
buttons->Add(reset, 0, wxALIGN_CENTRE_VERTICAL | wxRESERVE_SPACE_EVEN_IF_HIDDEN);
m_editable_rows.push_back({ shortcut, desc, modifiers, key, reset });
} else {
auto lock = new wxStaticBitmap(scrollable_panel, wxID_ANY, ScalableBitmap(scrollable_panel, "printer_status_lock", 16).bmp());
lock->SetToolTip(_L("Not customizable"));
buttons->Add((m_edit_size.x - lock->GetBestSize().x) / 2, m_edit_size.y); // centred under the edit icons, at their height
buttons->Add(lock, 0, wxALIGN_CENTRE_VERTICAL);
}
if (const int used = buttons->GetMinSize().x; used < m_buttons_width) // a box sizer recomputes its own min size, so pad it
buttons->AddSpacer(m_buttons_width - used);
wxBoxSizer* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->AddSpacer(row_margin);
row_sizer->Add(desc, 1, wxALIGN_CENTRE_VERTICAL);
row_sizer->AddSpacer(gap);
row_sizer->Add(modifiers, 0, wxALIGN_CENTRE_VERTICAL);
row_sizer->Add(key, 0, wxALIGN_CENTRE_VERTICAL);
row_sizer->Add(buttons, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, gap);
row_sizer->AddSpacer(title_margin);
scrollable_panel_sizer->Add(row_sizer, 0, wxEXPAND | wxTOP, FromDIP(4));
}
scrollable_panel_sizer->Add(grid_sizer, 1, wxEXPAND | wxALL, FromDIP(20));
scrollable_panel_sizer->AddSpacer(title_margin);
scrollable_panel->SetSizer(scrollable_panel_sizer);
main_sizer->Add(scrollable_panel, 1, wxEXPAND);
@@ -367,5 +327,227 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const ShortcutsItem& s
return main_page;
}
void KBShortcutsDialog::edit_shortcut(Shortcut shortcut)
{
ShortcutCaptureDialog dlg(this, shortcut);
if (dlg.ShowModal() != wxID_OK)
return;
const wxString question = wxString::Format(_L("%s is assigned to %s. Reassign it to %s?"),
join_keys(to_wx(dlg.chord().display_parts())), shortcut_names(dlg.conflicts()), _(shortcut_info(shortcut).name));
if (!take_chord_from(shortcut, dlg.conflicts(), question))
return;
wxGetApp().shortcuts().bind(shortcut, dlg.chord());
apply_bindings();
}
void KBShortcutsDialog::reset_shortcut(Shortcut shortcut)
{
const std::vector<Shortcut> conflicts = wxGetApp().shortcuts().conflicts(shortcut, shortcut_info(shortcut).default_chord);
const wxString question = wxString::Format(_L("The default %s is assigned to %s. Reassign it to %s?"),
join_keys(to_wx(shortcut_info(shortcut).default_chord.display_parts())), shortcut_names(conflicts), _(shortcut_info(shortcut).name));
if (!take_chord_from(shortcut, conflicts, question))
return;
wxGetApp().shortcuts().reset(shortcut);
apply_bindings();
}
bool KBShortcutsDialog::take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question)
{
if (conflicts.empty())
return true;
MessageDialog confirm(this, question, _(shortcut_info(shortcut).name), wxICON_QUESTION | wxOK | wxCANCEL);
if (confirm.ShowModal() != wxID_OK)
return false;
for (Shortcut other : conflicts)
wxGetApp().shortcuts().bind(other, KeyChord{});
return true;
}
void KBShortcutsDialog::apply_bindings()
{
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
std::set<wxWindow*> pages;
for (const EditableRow& row : m_editable_rows) {
const int chord_width = set_chord_labels(row.modifiers, row.key, to_wx(shortcuts.binding(row.shortcut).display_parts()));
row.description->SetLabel(_(shortcut_info(row.shortcut).name));
row.description->Wrap(m_row_text_width - chord_width);
row.reset->Show(shortcuts.is_customized(row.shortcut));
pages.insert(row.key->GetParent());
}
for (wxWindow* page : pages)
page->Layout();
wxGetApp().on_shortcuts_changed();
}
int KBShortcutsDialog::set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts)
{
const wxString last = parts.empty() ? wxString() : parts.back();
if (!parts.empty())
parts.pop_back();
modifiers->SetLabel(parts.empty() ? wxString() : join_keys(parts) + " + ");
modifiers->Show(!parts.empty());
key->SetLabel(last);
const int key_width = last.length() == 1 ? m_key_slot : key->GetBestSize().x;
key->SetMinSize(wxSize(key_width, -1));
return (parts.empty() ? 0 : modifiers->GetBestSize().x) + key_width;
}
void KBShortcutsDialog::open_mouse_preferences(const char* preference)
{
// Opened from Preferences > Control, the settings are right behind this dialog.
if (auto preferences = dynamic_cast<PreferencesDialog*>(GetParent()); preferences != nullptr) {
// Runs once this dialog has closed and the focus is back in Preferences.
preferences->CallAfter([preferences, preference] { preferences->select_tab(PreferencesTab::Control, preference); });
EndModal(wxID_OK);
return;
}
wxGetApp().open_preferences(PreferencesTab::Control, preference);
// A language change rebuilds the main frame, taking this dialog with it.
if (GetParent() != wxGetApp().mainframe) {
EndModal(wxID_CANCEL);
return;
}
for (const PreferenceRow& row : m_preference_rows)
row.description->SetLabel(_(mouse_action(row.preference)));
}
ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut)
: DPIDialog(parent, wxID_ANY, _(shortcut_info(shortcut).name), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
, m_shortcut(shortcut)
{
SetBackgroundColour(*wxWHITE);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use.
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
const wxString advice = global_key_advice();
const wxString typing = _L("Global shortcuts also apply while typing.");
const wxString rule = _L("A key that types a character cannot be a global shortcut.");
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms.");
m_rejection = rule + "\n" + advice;
// Wide enough for each sentence on a line of its own where the translation allows, within limits.
int width = FromDIP(450);
for (const wxString& sentence : { typing, rule, advice }) {
int extent = 0;
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
width = std::max(width, extent);
}
width = std::min(width, FromDIP(550));
auto prompt = new Label(this, wxGetApp().normal_font(), wxString::Format(_L("Press the new shortcut for\n\"%s\""), _(shortcut_info(shortcut).name)), LB_AUTO_WRAP);
prompt->SetMinSize(wxSize(width, -1));
sizer->Add(prompt, 0, wxALL, FromDIP(20));
// Keyboard focus stays on this box so the buttons never receive the key presses.
const wxColour box_colour = StateColor::darkModeColorFor(*wxWHITE);
StaticBox* capture = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxSize(width, FromDIP(60)), wxWANTS_CHARS);
capture->SetCornerRadius(FromDIP(4));
capture->SetBorderColorNormal(StateColor::darkModeColorFor(wxColour("#009688"))); // the focused-input colour, since the box always has the focus
capture->SetBackgroundColorNormal(box_colour);
capture->SetBackgroundColour(box_colour);
wxBoxSizer* capture_sizer = new wxBoxSizer(wxVERTICAL);
m_chord_label = new wxStaticText(capture, wxID_ANY, join_keys(to_wx(wxGetApp().shortcuts().binding(shortcut).display_parts())));
m_chord_label->SetFont(::Label::Head_14);
m_chord_label->SetBackgroundColour(box_colour);
capture_sizer->AddStretchSpacer();
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
capture_sizer->AddStretchSpacer();
capture->SetSizer(capture_sizer);
capture->Bind(wxEVT_KEY_DOWN, &ShortcutCaptureDialog::on_key, this);
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
m_status = new Label(this, wxGetApp().normal_font(), m_hint, LB_AUTO_WRAP);
m_status->SetMinSize(wxSize(width, 3 * m_status->GetCharHeight())); // room for three lines, so the dialog keeps its size while keys are tried
m_status_colour = m_status->GetForegroundColour();
sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(20));
auto dlg_btns = new DialogButtons(this, {"Unbind", "OK", "Cancel"}, "", 1 /*left_aligned*/);
dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
m_chord = KeyChord{};
m_conflicts.clear();
EndModal(wxID_OK);
});
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
m_ok = dlg_btns->GetOK();
m_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); });
m_ok->Enable(false);
sizer->Add(dlg_btns, 0, wxEXPAND | wxTOP, FromDIP(10));
SetSizerAndFit(sizer);
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
capture->CallAfter([capture]() { capture->SetFocus(); });
}
void ShortcutCaptureDialog::on_dpi_changed(const wxRect& suggested_rect)
{
Layout();
Fit();
}
void ShortcutCaptureDialog::on_key(wxKeyEvent& evt)
{
if (!evt.HasAnyModifiers()) {
if (evt.GetKeyCode() == WXK_ESCAPE) {
EndModal(wxID_CANCEL);
return;
}
if (evt.GetKeyCode() == WXK_RETURN || evt.GetKeyCode() == WXK_NUMPAD_ENTER) {
if (m_ok->IsEnabled())
EndModal(wxID_OK);
return;
}
}
const KeyChord chord = KeyChord::from_event(evt);
if (!chord.valid())
return;
if (chord.needs_char_event()) {
evt.Skip();
return;
}
record(chord);
}
void ShortcutCaptureDialog::on_char(wxKeyEvent& evt)
{
const KeyChord chord = KeyChord::from_event(evt);
if (chord.is_punctuation())
record(chord);
}
void ShortcutCaptureDialog::record(const KeyChord& chord)
{
m_chord = chord;
m_chord_label->SetLabel(join_keys(to_wx(chord.display_parts())));
m_chord_label->GetParent()->Layout();
auto reject = [this](const wxString& reason) {
m_status->SetForegroundColour(ERROR_COLOUR);
m_status->SetLabel(reason);
m_conflicts.clear();
m_ok->Enable(false);
};
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
if (global && !chord.is_menu_accelerator()) {
reject(m_rejection);
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
reject(wxString::Format(_L("Already used as a step of %s."), _(shortcut_info(*owner).name)));
} else {
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
m_status->SetForegroundColour(m_status_colour);
if (m_conflicts.empty())
m_status->SetLabel(m_hint);
else
m_status->SetLabel(wxString::Format(_L("Already assigned to %s. Press OK to reassign it."), shortcut_names(m_conflicts)));
m_ok->Enable(true);
}
m_status->Refresh(); // a colour change alone does not repaint
Layout();
Fit();
}
} // namespace GUI
} // namespace Slic3r
+95 -28
View File
@@ -3,53 +3,120 @@
#include <wx/wx.h>
#include <map>
#include <variant>
#include <vector>
#include "GUI_Utils.hpp"
#include "Shortcuts.hpp"
#include "wxExtensions.hpp"
#include <wx/simplebook.h>
class Button;
class Label;
class TabCtrl;
namespace Slic3r {
namespace GUI {
class Select
{
public:
int m_index;
wxWindow *m_tab_button;
wxWindow *m_tab_text;
};
WX_DECLARE_HASH_MAP(int, Select *, wxIntegerHash, wxIntegerEqual, SelectHash);
// Lists every shortcut per context and lets the user rebind the assignable ones.
class KBShortcutsDialog : public DPIDialog
{
typedef std::pair<std::string, std::string> Shortcut;
typedef std::vector<Shortcut> Shortcuts;
typedef std::pair<std::pair<wxString, wxString>, Shortcuts> ShortcutsItem;
typedef std::vector<ShortcutsItem> ShortcutsVec;
// A key the user cannot rebind.
struct FixedKey
{
std::vector<wxString> keys; // modifier names and the key, shown joined with "+"
const char* description; // untranslated
};
// A mouse button whose camera action is chosen in Preferences.
struct MouseAction
{
wxString button;
const char* preference; // AppConfig key of the action
};
struct Row
{
std::variant<Shortcut, FixedKey, MouseAction> content;
ShortcutSection section;
};
struct Page
{
wxString title;
wxString caption; // when the page's keys apply
ShortcutContext context;
std::vector<Row> rows;
};
struct EditableRow
{
Shortcut shortcut;
wxStaticText* description;
wxStaticText* modifiers;
wxStaticText* key;
ScalableButton* reset;
};
struct PreferenceRow
{
const char* preference;
wxStaticText* description;
};
ShortcutsVec m_full_shortcuts;
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_header_bitmap;
std::vector<wxPanel*> m_pages;
std::vector<Page> m_pages;
std::vector<EditableRow> m_editable_rows;
std::vector<PreferenceRow> m_preference_rows;
// Row geometry, measured once and shared by every page.
wxSize m_edit_size; // an edit or reset icon
int m_buttons_width = 0; // every row's buttons column, so the right-aligned keys share an edge
int m_row_text_width = 0; // what a description and its chord share; the description wraps at the rest
int m_key_slot = 0; // width of the widest single key, the column single keys line up in
TabCtrl* m_tabs;
wxSimplebook* m_simplebook;
public:
KBShortcutsDialog();
wxWindow* create_button(int id, wxString text);
void OnSelectTabel(wxCommandEvent &event);
wxPanel *m_panel_selects;
wxBoxSizer *m_sizer_right;
wxSimplebook *m_simplebook;
wxBoxSizer * m_sizer_body;
SelectHash m_hash_selector;
KBShortcutsDialog(wxWindow* parent, ShortcutContext page); // opens on the page of that context
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void fill_shortcuts();
wxPanel* create_header(wxWindow* parent, const wxFont& bold_font);
wxPanel* create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font);
void fill_pages();
wxPanel* create_page(wxWindow* parent, const Page& page);
void edit_shortcut(Shortcut shortcut);
void reset_shortcut(Shortcut shortcut);
// Asks question before unbinding conflicts; false when the user declined.
bool take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question);
void apply_bindings(); // refreshes the rows and pushes the change to the rest of the app
// Puts a chord on a row's two labels, a single key in the shared column, and returns the width the chord takes.
int set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts);
void open_mouse_preferences(const char* preference);
};
// Records one key chord for a shortcut, warning about the shortcuts it would take the chord from.
class ShortcutCaptureDialog : public DPIDialog
{
public:
ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut);
// Valid after ShowModal() returned wxID_OK; an invalid chord means "unbind".
const KeyChord& chord() const { return m_chord; }
const std::vector<Shortcut>& conflicts() const { return m_conflicts; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void on_key(wxKeyEvent& evt);
void on_char(wxKeyEvent& evt);
void record(const KeyChord& chord);
Shortcut m_shortcut;
KeyChord m_chord;
std::vector<Shortcut> m_conflicts;
wxStaticText* m_chord_label;
wxString m_hint; // what m_status shows while there is nothing to warn about
wxString m_rejection; // what it shows for a key a Global shortcut cannot use
Label* m_status;
wxColour m_status_colour;
Button* m_ok;
};
} // namespace GUI
+314
View File
@@ -0,0 +1,314 @@
#include "KeyChord.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cctype>
namespace Slic3r { namespace GUI {
namespace {
constexpr int BINDABLE_MODIFIERS = wxMOD_CONTROL | wxMOD_SHIFT | wxMOD_ALT | wxMOD_RAW_CONTROL;
struct KeyName
{
int key;
const char* name; // canonical name, as wx parses it
const char* alias; // accepted when parsing; nullptr when there is none
const char* label; // translation key for KeyChord::display(); nullptr when name is it
};
constexpr std::array<KeyName, 15> special_keys{{
{ WXK_BACK, L_CONTEXT("Backspace", "Keyboard Shortcut"), "Back", nullptr },
{ WXK_TAB, L_CONTEXT("Tab", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_RETURN, L_CONTEXT("Enter", "Keyboard Shortcut"), "Return", nullptr },
{ WXK_ESCAPE, L_CONTEXT("Esc", "Keyboard Shortcut"), "Escape", nullptr },
{ WXK_SPACE, L_CONTEXT("Space", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_DELETE, L_CONTEXT("Del", "Keyboard Shortcut"), "Delete", nullptr },
{ WXK_INSERT, L_CONTEXT("Ins", "Keyboard Shortcut"), "Insert", nullptr },
{ WXK_HOME, L_CONTEXT("Home", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_END, L_CONTEXT("End", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_PAGEUP, L_CONTEXT("PgUp", "Keyboard Shortcut"), "PageUp", nullptr },
{ WXK_PAGEDOWN, L_CONTEXT("PgDn", "Keyboard Shortcut"), "PageDown", nullptr },
// Displayed as "Arrow Left" and so on, which is what the catalogs translate.
{ WXK_LEFT, "Left", nullptr, L_CONTEXT("Arrow Left", "Keyboard Shortcut") },
{ WXK_RIGHT, "Right", nullptr, L_CONTEXT("Arrow Right", "Keyboard Shortcut") },
{ WXK_UP, "Up", nullptr, L_CONTEXT("Arrow Up", "Keyboard Shortcut") },
{ WXK_DOWN, "Down", nullptr, L_CONTEXT("Arrow Down", "Keyboard Shortcut") },
}};
bool equals_ignoring_case(const std::string& a, const char* b)
{
if (b == nullptr)
return false;
size_t i = 0;
for (; i < a.size() && b[i] != '\0'; ++i)
if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i])))
return false;
return i == a.size() && b[i] == '\0';
}
bool is_letter(int key) { return key >= 'A' && key <= 'Z'; }
bool is_digit(int key) { return key >= '0' && key <= '9'; }
bool is_printable(int key) { return key > ' ' && key < 127; }
bool is_symbol(int key) { return is_printable(key) && !std::isalnum(key); }
bool is_function_key(int key) { return key >= WXK_F1 && key <= WXK_F24; }
bool is_special(int key)
{
if (is_function_key(key))
return true;
return std::any_of(special_keys.begin(), special_keys.end(), [key](const KeyName& k) { return k.key == key; });
}
std::string special_key_name(int key)
{
if (is_function_key(key))
return "F" + std::to_string(key - WXK_F1 + 1);
for (const KeyName& k : special_keys)
if (k.key == key)
return k.name;
return {};
}
std::string special_key_label(int key)
{
for (const KeyName& k : special_keys)
if (k.key == key)
return _u8L_CONTEXT(k.label != nullptr ? k.label : k.name, "Keyboard Shortcut");
return special_key_name(key);
}
int parse_key(const std::string& text)
{
if (text.size() == 1) {
const int key = static_cast<unsigned char>(text[0]);
return is_printable(key) ? std::toupper(key) : WXK_NONE;
}
for (const KeyName& k : special_keys)
if (equals_ignoring_case(text, k.name) || equals_ignoring_case(text, k.alias))
return k.key;
if ((text[0] == 'F' || text[0] == 'f') && text.size() <= 3 && std::all_of(text.begin() + 1, text.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); })) {
const int n = std::stoi(text.substr(1));
if (n >= 1 && n <= 24)
return WXK_F1 + n - 1;
}
return WXK_NONE;
}
int parse_modifier(const std::string& text)
{
if (equals_ignoring_case(text, "Ctrl") || equals_ignoring_case(text, "Control") || equals_ignoring_case(text, "Cmd") || equals_ignoring_case(text, "Command"))
return wxMOD_CONTROL;
if (equals_ignoring_case(text, "Shift"))
return wxMOD_SHIFT;
if (equals_ignoring_case(text, "Alt") || equals_ignoring_case(text, "Option"))
return wxMOD_ALT;
if (equals_ignoring_case(text, "RawCtrl"))
return wxMOD_RAW_CONTROL;
return wxMOD_NONE;
}
// Numpad keys act as their main-keyboard counterparts, so one binding covers both.
int fold_numpad(int key)
{
if (key >= WXK_NUMPAD0 && key <= WXK_NUMPAD9)
return '0' + (key - WXK_NUMPAD0);
switch (key) {
case WXK_NUMPAD_ENTER: return WXK_RETURN;
case WXK_NUMPAD_SPACE: return WXK_SPACE;
case WXK_NUMPAD_TAB: return WXK_TAB;
case WXK_NUMPAD_HOME: return WXK_HOME;
case WXK_NUMPAD_END: return WXK_END;
case WXK_NUMPAD_PAGEUP: return WXK_PAGEUP;
case WXK_NUMPAD_PAGEDOWN: return WXK_PAGEDOWN;
case WXK_NUMPAD_LEFT: return WXK_LEFT;
case WXK_NUMPAD_RIGHT: return WXK_RIGHT;
case WXK_NUMPAD_UP: return WXK_UP;
case WXK_NUMPAD_DOWN: return WXK_DOWN;
case WXK_NUMPAD_INSERT: return WXK_INSERT;
case WXK_NUMPAD_DELETE: return WXK_DELETE;
case WXK_NUMPAD_ADD: return '+';
case WXK_NUMPAD_SUBTRACT: return '-';
case WXK_NUMPAD_MULTIPLY: return '*';
case WXK_NUMPAD_DIVIDE: return '/';
case WXK_NUMPAD_DECIMAL: return '.';
case WXK_NUMPAD_EQUAL: return '=';
default: return key;
}
}
// Modifiers in the order the text forms list them; wxMOD_RAW_CONTROL is wxMOD_CONTROL off macOS.
#ifdef __APPLE__
constexpr std::array<int, 4> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT, wxMOD_RAW_CONTROL };
#else
constexpr std::array<int, 3> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT };
#endif
const char* canonical_modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return "Ctrl+";
if (modifier == wxMOD_SHIFT)
return "Shift+";
if (modifier == wxMOD_ALT)
return "Alt+";
return "RawCtrl+";
}
template<typename Prefix>
std::string join_modifiers(int modifiers, Prefix prefix)
{
std::string out;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
out += prefix(modifier);
return out;
}
} // namespace
bool KeyChord::is_punctuation() const { return is_symbol(key) && modifiers == wxMOD_NONE; }
bool KeyChord::needs_char_event() const { return is_symbol(key) && (modifiers & ~wxMOD_SHIFT) == 0; }
bool KeyChord::is_menu_accelerator() const
{
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
}
std::string KeyChord::to_string() const
{
if (!valid())
return {};
std::string out = join_modifiers(modifiers, canonical_modifier_prefix);
if (is_printable(key))
out += char(key);
else
out += special_key_name(key);
return out;
}
std::optional<KeyChord> KeyChord::parse(const std::string& text)
{
if (text.empty())
return std::nullopt;
// The key is whatever follows the last separator; a trailing '+' is the '+' key itself.
size_t key_start = text.size() - 1;
if (text.back() != '+') {
const size_t sep = text.rfind('+');
key_start = sep == std::string::npos ? 0 : sep + 1;
}
KeyChord chord;
chord.key = parse_key(text.substr(key_start));
if (chord.key == WXK_NONE)
return std::nullopt;
const std::string prefix = key_start == 0 ? std::string() : text.substr(0, key_start - 1);
size_t begin = 0;
while (begin < prefix.size()) {
size_t end = prefix.find('+', begin);
if (end == std::string::npos)
end = prefix.size();
const int modifier = parse_modifier(prefix.substr(begin, end - begin));
if (modifier == wxMOD_NONE)
return std::nullopt;
chord.modifiers |= modifier;
begin = end + 1;
}
if (is_letter(chord.key) || is_digit(chord.key) || !is_printable(chord.key) || chord.modifiers == wxMOD_NONE)
return chord;
// Shift is folded into the character for punctuation, so "Shift+/" is not accepted.
return (chord.modifiers & wxMOD_SHIFT) ? std::nullopt : std::optional<KeyChord>(chord);
}
std::string KeyChord::display() const
{
std::string out;
for (const std::string& part : display_parts())
out += (out.empty() ? "" : "+") + part;
return out;
}
std::vector<std::string> KeyChord::display_parts() const
{
std::vector<std::string> parts;
if (!valid())
return parts;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
parts.push_back(modifier_name(modifier));
parts.push_back(is_printable(key) ? std::string(1, char(key)) : special_key_label(key));
return parts;
}
std::string KeyChord::modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return shortkey_ctrl_prefix();
if (modifier == wxMOD_SHIFT)
return _u8L("Shift+");
if (modifier == wxMOD_ALT)
return shortkey_alt_prefix();
#ifdef __APPLE__
if (modifier == wxMOD_RAW_CONTROL)
return u8"⌃+";
#endif
return {};
}
// The catalogue holds the "Ctrl+" prefixes, so the bare name is the prefix without its "+"
// and any space before it ("Strg +" in German).
std::string KeyChord::modifier_name(int modifier)
{
std::string name = modifier_prefix(modifier);
if (!name.empty() && name.back() == '+')
name.pop_back();
while (!name.empty() && name.back() == ' ')
name.pop_back();
return name;
}
wxAcceleratorEntry KeyChord::to_accelerator_entry(int command) const
{
int flags = wxACCEL_NORMAL;
if (modifiers & wxMOD_CONTROL)
flags |= wxACCEL_CTRL;
if (modifiers & wxMOD_SHIFT)
flags |= wxACCEL_SHIFT;
if (modifiers & wxMOD_ALT)
flags |= wxACCEL_ALT;
#ifdef __APPLE__
if (modifiers & wxMOD_RAW_CONTROL)
flags |= wxACCEL_RAW_CTRL;
#endif
return wxAcceleratorEntry(flags, key, command);
}
KeyChord KeyChord::from_event(const wxKeyEvent& evt)
{
KeyChord chord;
chord.modifiers = evt.GetModifiers() & BINDABLE_MODIFIERS;
int key = fold_numpad(evt.GetKeyCode());
if (evt.GetEventType() == wxEVT_CHAR) {
if (key >= 1 && key <= 26 && evt.ControlDown())
key = 'A' + key - 1; // Ctrl+letter arrives as the control character
else if (is_symbol(key))
chord.modifiers &= ~wxMOD_SHIFT; // the character already reflects Shift
}
if (key >= 'a' && key <= 'z')
key -= 'a' - 'A';
if (is_printable(key) || is_special(key))
chord.key = key;
return chord;
}
}} // namespace Slic3r::GUI
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <wx/accel.h>
#include <wx/defs.h>
#include <functional>
#include <optional>
#include <string>
#include <vector>
class wxKeyEvent;
namespace Slic3r { namespace GUI {
// One key press: a key code as wxEVT_KEY_DOWN reports it (letters upper-case, numpad keys
// folded onto their main-keyboard equivalents) plus the wxMOD_* modifiers held with it.
// Printable punctuation is stored as the character it produces, so "+" means the key that
// types "+" on the user's layout.
struct KeyChord
{
int key = WXK_NONE;
int modifiers = wxMOD_NONE;
bool valid() const { return key != WXK_NONE; }
bool operator==(const KeyChord& other) const { return key == other.key && modifiers == other.modifiers; }
bool operator!=(const KeyChord& other) const { return !(*this == other); }
// Bare printable keys other than letters and digits are matched on wxEVT_CHAR, because
// only the char event knows which character a key produces under the active layout.
bool is_punctuation() const;
// True for a printable non-alphanumeric key pressed with nothing but Shift, which only the
// char event that follows can resolve.
bool needs_char_event() const;
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields.
bool is_menu_accelerator() const;
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
std::string to_string() const;
static std::optional<KeyChord> parse(const std::string& text);
// Text for menus, tooltips and the shortcuts dialog, with translated modifier names and the
// command and option glyphs on macOS.
std::string display() const;
// The pieces display() joins with "+": the modifier names, then the key name.
std::vector<std::string> display_parts() const;
// Translated text of one wxMOD_* modifier, as a "Ctrl+" prefix or the bare "Ctrl" name.
static std::string modifier_prefix(int modifier);
static std::string modifier_name(int modifier);
wxAcceleratorEntry to_accelerator_entry(int command) const;
// Builds the chord a key event describes, or an invalid chord for pure modifier presses and
// keys outside the bindable set. wxEVT_CHAR events are normalized to the key codes
// wxEVT_KEY_DOWN reports for letters, digits and special keys.
static KeyChord from_event(const wxKeyEvent& evt);
};
struct KeyChordHash
{
size_t operator()(const KeyChord& chord) const { return std::hash<long long>()((static_cast<long long>(chord.modifiers) << 32) | unsigned(chord.key)); }
};
}} // namespace Slic3r::GUI
+175 -139
View File
@@ -56,6 +56,7 @@
#include "../Utils/PrintHost.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "UnsavedChangesDialog.hpp"
#include "PublishSettingsDialog.hpp"
#include "MsgDialog.hpp"
@@ -310,17 +311,6 @@ static wxIcon main_frame_icon(GUI_App::EAppMode app_mode)
wxDEFINE_EVENT(EVT_SYNC_CLOUD_PRESET, SimpleEvent);
#ifdef __APPLE__
static const wxString ctrl = ("Ctrl+");
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const std::string ctrl_t = u8"\u2318+"; // "⌘" (Mac Command)
#else
static const wxString ctrl = _L("Ctrl+");
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const wxString ctrl_t = ctrl;
#endif
static const wxString shift = _L("Shift+");
MainFrame::MainFrame() :
DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_STYLE, "mainframe")
, m_printhost_queue_dlg(new PrintHostQueueDialog(this))
@@ -730,69 +720,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#endif
// Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so
// editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window
// doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields),
// so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference
// (default on) so users can hand Space back to the focused control entirely.
if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() &&
!evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) {
if (focus_keeps_space(wxWindow::FindFocus())) {
evt.Skip(); // let the focused control keep Space
return;
}
// Defer out of the native key-event stack: open_speed_dial() may create a WebView and
// run script, the same window work the codebase avoids doing on native callbacks.
this->CallAfter([] { wxGetApp().open_speed_dial(); });
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable);
if (m_print_enable) {
if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"))
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
}
if (!handle_global_shortcut(KeyChord::from_event(evt)))
evt.Skip();
return;
}
else if (evt.CmdDown() && evt.GetKeyCode() == 'G') { if (can_export_gcode()) { wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); } evt.Skip(); return; }
if (evt.CmdDown() && evt.GetKeyCode() == 'J') { m_printhost_queue_dlg->Show(); return; }
if (evt.CmdDown() && evt.GetKeyCode() == 'N') { m_plater->new_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'O') { m_plater->load_project(); return;}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;}
else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'F') {
if (m_plater && is_prepare_or_preview_tab()) {
m_plater->sidebar().can_search();
}
}
#ifdef __APPLE__
if (evt.CmdDown() && evt.GetKeyCode() == ',')
#else
if (evt.CmdDown() && evt.GetKeyCode() == 'P')
#endif
{
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_preferences();
plater()->get_current_canvas3D()->force_set_focus();
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'I' && !evt.ShiftDown()) {
if (!can_add_models()) return;
if (m_plater) { m_plater->add_file(); }
return;
}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
if (can_export_model()) publish_project();
return;
}
evt.Skip();
});
Bind(wxEVT_SHOW, [](wxShowEvent &evt) {
@@ -813,6 +742,96 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
bind_diff_dialog();
}
bool MainFrame::handle_global_shortcut(const KeyChord& chord)
{
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Global, chord);
if (!shortcut.has_value())
return false;
switch (*shortcut) {
case Shortcut::SlicePlate:
if (m_slice_enable) {
wxGetApp().plater()->update(true, true);
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
break;
case Shortcut::PrintPlate:
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable);
if (m_print_enable) {
if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"))
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
}
return false;
case Shortcut::ExportSlicedFile:
if (can_export_gcode())
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE));
return false;
case Shortcut::PrintHostQueue: m_printhost_queue_dlg->Show(); break;
case Shortcut::SpeedDial:
if (!wxGetApp().app_config->get_bool("enable_speed_dial") || (chord == KeyChord{ WXK_SPACE } && focus_keeps_space(wxWindow::FindFocus())))
return false;
// Deferred out of the native key-event stack: open_speed_dial() may create a WebView and run script.
CallAfter([] { wxGetApp().open_speed_dial(); });
break;
case Shortcut::NewProject: m_plater->new_project(); break;
case Shortcut::OpenProject: m_plater->load_project(); break;
case Shortcut::SaveProjectAs:
if (can_save_as())
m_plater->save_project(true);
break;
case Shortcut::SaveProject:
if (can_save())
m_plater->save_project();
break;
case Shortcut::Search:
if (m_plater && is_prepare_or_preview_tab())
m_plater->sidebar().can_search();
return false;
case Shortcut::Preferences:
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_preferences();
plater()->get_current_canvas3D()->force_set_focus();
break;
case Shortcut::ImportModel:
if (can_add_models() && m_plater)
m_plater->add_file();
break;
case Shortcut::Publish3mf:
if (can_export_model())
publish_project();
break;
case Shortcut::ShowLabels:
if (m_plater && m_plater->is_view3D_shown()) {
m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown());
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}
break;
case Shortcut::ViewDefault:
if (m_plater) {
select_view("plate");
m_plater->get_current_canvas3D()->zoom_to_bed();
}
break;
case Shortcut::ViewTop: select_view("top"); break;
case Shortcut::ViewBottom: select_view("bottom"); break;
case Shortcut::ViewFront: select_view("front"); break;
case Shortcut::ViewRear: select_view("rear"); break;
case Shortcut::ViewLeft: select_view("left"); break;
case Shortcut::ViewRight: select_view("right"); break;
case Shortcut::ViewPlate:
if (m_plater)
m_plater->get_current_canvas3D()->select_plate();
break;
default: return false;
}
return true;
}
void MainFrame::bind_diff_dialog()
{
auto get_tab = [](Preset::Type type) {
@@ -2766,13 +2785,31 @@ static const wxString sep = " - ";
static const wxString sep = "\t";
#endif
static wxMenu* generate_help_menu()
wxString MainFrame::shortcut_label(const wxString& label, Shortcut shortcut, bool accelerator)
{
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
if (accelerator) {
const std::string accel = shortcuts.accelerator(shortcut);
if (!accel.empty())
return label + " " + from_u8(accel);
}
const std::string text = shortcuts.display(shortcut);
return text.empty() ? label : label + sep + from_u8(text);
}
void MainFrame::update_shortcut_labels()
{
for (const ShortcutMenuItem& entry : m_shortcut_menu_items)
entry.item->SetItemLabel(shortcut_label(entry.label, entry.shortcut, entry.accelerator));
}
wxMenu* MainFrame::generate_help_menu()
{
wxMenu* helpMenu = new wxMenu();
// shortcut key
append_menu_item(helpMenu, wxID_ANY, _L("Keyboard Shortcuts") + sep + "&?", _L("Show the list of keyboard shortcuts"),
[](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); });
append_shortcut_item(helpMenu, Shortcut::KeyboardShortcuts, false, _L("Keyboard Shortcuts"), _L("Show the list of keyboard shortcuts"),
[](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(ShortcutContext::Global); });
// Show Beginner's Tutorial
append_menu_item(helpMenu, wxID_ANY, _L("Setup Wizard"), _L("Setup Wizard"), [](wxCommandEvent &) {wxGetApp().ShowUserGuide();});
@@ -2854,29 +2891,28 @@ static void add_common_publish_menu_items(wxMenu* publish_menu, MainFrame* mainF
#endif
}
static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, std::function<bool(void)> can_change_view)
void MainFrame::add_common_view_menu_items(wxMenu* view_menu, std::function<bool(void)> can_change_view)
{
// The camera control accelerators are captured by GLCanvas3D::on_char().
append_menu_item(view_menu, wxID_ANY, _L("Default View") + "\t" + ctrl + "0", _L("Default View"), [mainFrame](wxCommandEvent&) {
mainFrame->select_view("plate");
mainFrame->plater()->get_current_canvas3D()->zoom_to_bed();
append_shortcut_item(view_menu, Shortcut::ViewDefault, true, _L("Default View"), _L("Default View"), [this](wxCommandEvent&) {
select_view("plate");
plater()->get_current_canvas3D()->zoom_to_bed();
},
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
//view_menu->AppendSeparator();
//TRN To be shown in the main menu View->Top
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Top", "Camera View") + "\t" + ctrl + "1", _L("Top View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("top"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_shortcut_item(view_menu, Shortcut::ViewTop, true, _L_CONTEXT("Top", "Camera View"), _L("Top View"), [this](wxCommandEvent&) { select_view("top"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
//TRN To be shown in the main menu View->Bottom
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Bottom", "Camera View") + "\t" + ctrl + "2", _L("Bottom View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("bottom"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Front", "Camera View") + "\t" + ctrl + "3", _L("Front View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("front"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Rear", "Camera View") + "\t" + ctrl + "4", _L("Rear View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("rear"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Left", "Camera View") + "\t" + ctrl + "5", _L("Left View"),[mainFrame](wxCommandEvent &) {mainFrame->select_view("left"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Right", "Camera View") + "\t" + ctrl + "6", _L("Right View"),[mainFrame](wxCommandEvent &) { mainFrame->select_view("right"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_shortcut_item(view_menu, Shortcut::ViewBottom, true, _L_CONTEXT("Bottom", "Camera View"), _L("Bottom View"), [this](wxCommandEvent&) { select_view("bottom"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
append_shortcut_item(view_menu, Shortcut::ViewFront, true, _L_CONTEXT("Front", "Camera View"), _L("Front View"), [this](wxCommandEvent&) { select_view("front"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
append_shortcut_item(view_menu, Shortcut::ViewRear, true, _L_CONTEXT("Rear", "Camera View"), _L("Rear View"), [this](wxCommandEvent&) { select_view("rear"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
append_shortcut_item(view_menu, Shortcut::ViewLeft, true, _L_CONTEXT("Left", "Camera View"), _L("Left View"), [this](wxCommandEvent &) { select_view("left"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
append_shortcut_item(view_menu, Shortcut::ViewRight, true, _L_CONTEXT("Right", "Camera View"), _L("Right View"), [this](wxCommandEvent &) { select_view("right"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
}
void MainFrame::init_menubar_as_editor()
@@ -2895,17 +2931,17 @@ void MainFrame::init_menubar_as_editor()
[this] { return m_plater != nullptr && wxGetApp().app_config->get("app", "single_instance") == "false"; }, this);
#endif
// New Project
append_menu_item(fileMenu, wxID_ANY, _L("New Project") + "\t" + ctrl + "N", _L("Start a new project"),
append_shortcut_item(fileMenu, Shortcut::NewProject, true, _L("New Project"), _L("Start a new project"),
[this](wxCommandEvent&) { if (m_plater) m_plater->new_project(); }, "", nullptr,
[this](){return can_start_new_project(); }, this);
// Open Project
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Open Project") + dots + "\t" + ctrl + "O", _L("Open a project file"),
append_shortcut_item(fileMenu, Shortcut::OpenProject, true, _L("Open Project") + dots, _L("Open a project file"),
[this](wxCommandEvent&) { if (m_plater) m_plater->load_project(); }, "menu_open", nullptr,
[this](){return can_open_project(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Open Project") + dots + "\t" + ctrl + "O", _L("Open a project file"),
append_shortcut_item(fileMenu, Shortcut::OpenProject, true, _L("Open Project") + dots, _L("Open a project file"),
[this](wxCommandEvent&) { if (m_plater) m_plater->load_project(); }, "", nullptr,
[this](){return can_open_project(); }, this);
#endif
@@ -2932,21 +2968,21 @@ void MainFrame::init_menubar_as_editor()
// BBS: close save project
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Save Project") + "\t" + ctrl + "S", _L("Save current project to file"),
append_shortcut_item(fileMenu, Shortcut::SaveProject, true, _L("Save Project"), _L("Save current project to file"),
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(); }, "menu_save", nullptr,
[this](){return m_plater != nullptr && can_save(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Save Project") + "\t" + ctrl + "S", _L("Save current project to file"),
append_shortcut_item(fileMenu, Shortcut::SaveProject, true, _L("Save Project"), _L("Save current project to file"),
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(); }, "", nullptr,
[this](){return m_plater != nullptr && can_save(); }, this);
#endif
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Save Project as") + dots + "\t" + ctrl + shift + "S", _L("Save current project as"),
append_shortcut_item(fileMenu, Shortcut::SaveProjectAs, true, _L("Save Project as") + dots, _L("Save current project as"),
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(true); }, "menu_save", nullptr,
[this](){return m_plater != nullptr && can_save_as(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Save Project as") + dots + "\t" + ctrl + shift + "S", _L("Save current project as"),
append_shortcut_item(fileMenu, Shortcut::SaveProjectAs, true, _L("Save Project as") + dots, _L("Save current project as"),
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(true); }, "", nullptr,
[this](){return m_plater != nullptr && can_save_as(); }, this);
#endif
@@ -2956,11 +2992,11 @@ void MainFrame::init_menubar_as_editor()
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
append_shortcut_item(fileMenu, Shortcut::Publish3mf, true, _L("Publish 3MF") + dots, _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "menu_publish", nullptr,
[this](){return can_export_model(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
append_shortcut_item(fileMenu, Shortcut::Publish3mf, true, _L("Publish 3MF") + dots, _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "", nullptr,
[this](){return can_export_model(); }, this);
#endif
@@ -2971,13 +3007,13 @@ void MainFrame::init_menubar_as_editor()
// BBS
wxMenu *import_menu = new wxMenu();
#ifndef __APPLE__
append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"),
append_shortcut_item(import_menu, Shortcut::ImportModel, true, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots, _L("Load a model"),
[this](wxCommandEvent&) { if (m_plater) {
m_plater->add_file();
} }, "menu_import", nullptr,
[this](){return can_add_models(); }, this);
#else
append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"),
append_shortcut_item(import_menu, Shortcut::ImportModel, true, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots, _L("Load a model"),
[this](wxCommandEvent&) { if (m_plater) { m_plater->add_model(); } }, "", nullptr,
[this](){return can_add_models(); }, this);
#endif
@@ -3009,7 +3045,7 @@ void MainFrame::init_menubar_as_editor()
[this](wxCommandEvent&) { if (m_plater) m_plater->export_core_3mf(); }, "menu_export_sliced_file", nullptr,
[this](){return can_export_model(); }, this);
// BBS export .gcode.3mf
append_menu_item(export_menu, wxID_ANY, _L("Export plate sliced file") + dots + "\t" + ctrl + "G", _L("Export current sliced file"),
append_shortcut_item(export_menu, Shortcut::ExportSlicedFile, true, _L("Export plate sliced file") + dots, _L("Export current sliced file"),
[this](wxCommandEvent&) { if (m_plater) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); }, "menu_export_sliced_file", nullptr,
[this](){return can_export_gcode(); }, this);
@@ -3059,37 +3095,37 @@ void MainFrame::init_menubar_as_editor()
};
#ifndef __APPLE__
// BBS undo
append_menu_item(editMenu, wxID_ANY, _L("Undo") + "\t" + ctrl + "Z",
append_shortcut_item(editMenu, Shortcut::Undo, true, _L("Undo"),
_L("Undo"), [this](wxCommandEvent&) { m_plater->undo(); },
"menu_undo", nullptr, [this](){return m_plater->can_undo(); }, this);
// BBS redo
append_menu_item(editMenu, wxID_ANY, _L("Redo") + "\t" + ctrl + "Y",
append_shortcut_item(editMenu, Shortcut::Redo, true, _L("Redo"),
_L("Redo"), [this](wxCommandEvent&) { m_plater->redo(); },
"menu_redo", nullptr, [this](){return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_menu_item(editMenu, wxID_ANY, _L("Cut") + "\t" + ctrl + "X",
append_shortcut_item(editMenu, Shortcut::Cut, true, _L("Cut"),
_L("Cut selection to clipboard"), [this](wxCommandEvent&) {m_plater->cut_selection_to_clipboard(); },
"menu_cut", nullptr, [this]() {return m_plater->can_copy_to_clipboard(); }, this);
// BBS Copy
append_menu_item(editMenu, wxID_ANY, _L("Copy") + "\t" + ctrl + "C",
append_shortcut_item(editMenu, Shortcut::Copy, true, _L("Copy"),
_L("Copy selection to clipboard"), [this](wxCommandEvent&) { m_plater->copy_selection_to_clipboard(); },
"menu_copy", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this);
// BBS Paste
append_menu_item(editMenu, wxID_ANY, _L("Paste") + "\t" + ctrl + "V",
append_shortcut_item(editMenu, Shortcut::Paste, true, _L("Paste"),
_L("Paste clipboard"), [this](wxCommandEvent&) { m_plater->paste_from_clipboard(); },
"menu_paste", nullptr, [this](){return m_plater->can_paste_from_clipboard(); }, this);
// BBS Delete selected
append_menu_item(editMenu, wxID_ANY, _L("Delete Selected") + "\t" + _L_CONTEXT("Del", "Keyboard Shortcut"),
append_shortcut_item(editMenu, Shortcut::DeleteSelected, true, _L("Delete Selected"),
_L("Deletes the current selection"),[this](wxCommandEvent&) { m_plater->remove_selected(); },
"menu_remove", nullptr, [this](){return can_delete(); }, this);
//BBS: delete all
append_menu_item(editMenu, wxID_ANY, _L("Delete All") + "\t" + ctrl + "D",
append_shortcut_item(editMenu, Shortcut::DeleteAll, true, _L("Delete All"),
_L("Deletes all objects"),[this](wxCommandEvent&) { m_plater->delete_all_objects_from_model(); },
"menu_remove", nullptr, [this](){return can_delete_all(); }, this);
editMenu->AppendSeparator();
// BBS Clone Selected
append_menu_item(editMenu, wxID_ANY, _L("Clone Selected") /*+ "\t" + ctrl + "M"*/,
append_shortcut_item(editMenu, Shortcut::CloneSelected, true, _L("Clone Selected"),
_L("Clone copies of selections"),[this](wxCommandEvent&) {
m_plater->clone_selection();
},
@@ -3103,7 +3139,7 @@ void MainFrame::init_menubar_as_editor()
editMenu->AppendSeparator();
#else
// BBS undo
append_menu_item(editMenu, wxID_ANY, _L("Undo") + sep + ctrl_t + "Z",
append_shortcut_item(editMenu, Shortcut::Undo, false, _L("Undo"),
_L("Undo"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3115,7 +3151,7 @@ void MainFrame::init_menubar_as_editor()
m_plater->undo(); },
"", nullptr, [this](){return m_plater->can_undo(); }, this);
// BBS redo
append_menu_item(editMenu, wxID_ANY, _L("Redo") + sep + ctrl_t + "Y",
append_shortcut_item(editMenu, Shortcut::Redo, false, _L("Redo"),
_L("Redo"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3128,7 +3164,7 @@ void MainFrame::init_menubar_as_editor()
"", nullptr, [this](){return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_menu_item(editMenu, wxID_ANY, _L("Cut") + sep + ctrl_t + "X",
append_shortcut_item(editMenu, Shortcut::Cut, false, _L("Cut"),
_L("Cut selection to clipboard"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3140,7 +3176,7 @@ void MainFrame::init_menubar_as_editor()
m_plater->cut_selection_to_clipboard(); },
"", nullptr, [this]() {return m_plater->can_copy_to_clipboard(); }, this);
// BBS Copy
append_menu_item(editMenu, wxID_ANY, _L("Copy") + sep + ctrl_t + "C",
append_shortcut_item(editMenu, Shortcut::Copy, false, _L("Copy"),
_L("Copy selection to clipboard"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3152,7 +3188,7 @@ void MainFrame::init_menubar_as_editor()
m_plater->copy_selection_to_clipboard(); },
"", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this);
// BBS Paste
append_menu_item(editMenu, wxID_ANY, _L("Paste") + sep + ctrl_t + "V",
append_shortcut_item(editMenu, Shortcut::Paste, false, _L("Paste"),
_L("Paste clipboard"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3165,14 +3201,14 @@ void MainFrame::init_menubar_as_editor()
"", nullptr, [this](){return m_plater->can_paste_from_clipboard(); }, this);
#if 0
// BBS Delete selected
append_menu_item(editMenu, wxID_ANY, _L("Delete Selected") + "\t" + _L_CONTEXT("Backspace", "Keyboard Shortcut"),
append_shortcut_item(editMenu, Shortcut::DeleteSelected, true, _L("Delete Selected"),
_L("Deletes the current selection"),[this](wxCommandEvent&) {
m_plater->remove_selected();
},
"", nullptr, [this](){return can_delete(); }, this);
#endif
//BBS: delete all
append_menu_item(editMenu, wxID_ANY, _L("Delete All") + "\t" + ctrl + "D",
append_shortcut_item(editMenu, Shortcut::DeleteAll, true, _L("Delete All"),
_L("Deletes all objects"),[this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3185,7 +3221,7 @@ void MainFrame::init_menubar_as_editor()
"", nullptr, [this](){return can_delete_all(); }, this);
editMenu->AppendSeparator();
// BBS Clone Selected
append_menu_item(editMenu, wxID_ANY, _L("Clone Selected") + "\t" + ctrl + "K",
append_shortcut_item(editMenu, Shortcut::CloneSelected, true, _L("Clone Selected"),
_L("Clone copies of selections"),[this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3208,7 +3244,7 @@ void MainFrame::init_menubar_as_editor()
#endif
// BBS Select All
append_menu_item(editMenu, wxID_ANY, _L("Select All") + sep + ctrl_t + "A",
append_shortcut_item(editMenu, Shortcut::SelectAll, false, _L("Select All"),
_L("Selects all objects"), [this, handle_key_event](wxCommandEvent&) {
wxKeyEvent e;
e.SetEventType(wxEVT_KEY_DOWN);
@@ -3260,7 +3296,7 @@ void MainFrame::init_menubar_as_editor()
wxMenu* viewMenu = nullptr;
if (m_plater) {
viewMenu = new wxMenu();
add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this));
add_common_view_menu_items(viewMenu, std::bind(&MainFrame::can_change_view, this));
viewMenu->AppendSeparator();
//BBS perspective view
@@ -3291,13 +3327,14 @@ void MainFrame::init_menubar_as_editor()
[]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
viewMenu->AppendSeparator();
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &G-code Window") + sep + "C", _L("Show G-code window in Preview scene."),
wxMenuItem* gcode_window = append_menu_check_item(viewMenu, wxID_ANY, shortcut_label(_L("Show &G-code Window"), Shortcut::ToggleGcodeWindow, true), _L("Show G-code window in Preview scene."),
[this](wxCommandEvent &) {
wxGetApp().toggle_show_gcode_window();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
[]() { return wxGetApp().show_gcode_window(); }, this);
m_shortcut_menu_items.push_back({ gcode_window, Shortcut::ToggleGcodeWindow, _L("Show &G-code Window"), true });
append_menu_check_item(
viewMenu, wxID_ANY, _L("Show 3D Navigator"), _L("Show 3D navigator in Prepare and Preview scene."),
@@ -3325,9 +3362,10 @@ void MainFrame::init_menubar_as_editor()
this);
viewMenu->AppendSeparator();
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &Labels") + "\t" + ctrl + "E", _L("Show object labels in 3D scene."),
wxMenuItem* show_labels = append_menu_check_item(viewMenu, wxID_ANY, shortcut_label(_L("Show &Labels"), Shortcut::ShowLabels, true), _L("Show object labels in 3D scene."),
[this](wxCommandEvent&) { m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown()); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this,
[this]() { return m_plater->is_view3D_shown(); }, [this]() { return m_plater->are_view3D_labels_shown(); }, this);
m_shortcut_menu_items.push_back({ show_labels, Shortcut::ShowLabels, _L("Show &Labels"), true });
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &Overhang"), _L("Show object overhang highlight in 3D scene."),
[this](wxCommandEvent &) {
@@ -3372,8 +3410,6 @@ void MainFrame::init_menubar_as_editor()
//auto preference_item = new wxMenuItem(parent_menu, OrcaSlicerMenuPreferences + bambu_studio_id_base, _L("Preferences") + "\t" + ctrl + ",", "");
#else
wxMenu* parent_menu = m_topbar->GetTopMenu();
auto preference_item = new wxMenuItem(parent_menu, ConfigMenuPreferences + config_id_base, _L("Preferences") + "\t" + ctrl + "P", "");
#endif
#ifdef __APPLE__
@@ -3382,15 +3418,15 @@ void MainFrame::init_menubar_as_editor()
parent_menu, wxID_ANY, _L(about_title), "",
[](wxCommandEvent &) { Slic3r::GUI::about();},
"", nullptr, []() { return true; }, this, 0);
append_menu_item(
parent_menu, wxID_ANY, _L("Preferences") + "\t" + ctrl + ",", "",
append_shortcut_item(
parent_menu, Shortcut::Preferences, true, _L("Preferences"), "",
[](wxCommandEvent &) {
wxGetApp().open_preferences();
},
"", nullptr, []() { return true; }, this, 1);
parent_menu->AppendSeparator();
append_menu_item(
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
append_shortcut_item(
parent_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
//parent_menu->Insert(1, preference_item);
@@ -3406,8 +3442,8 @@ void MainFrame::init_menubar_as_editor()
m_topbar->AddDropDownSubMenu(viewMenu, _L("View"));
//BBS add Preference
append_menu_item(
m_topbar->GetTopMenu(), wxID_ANY, _L("Preferences") + "\t" + ctrl + "P", "",
append_shortcut_item(
m_topbar->GetTopMenu(), Shortcut::Preferences, true, _L("Preferences"), "",
[](wxCommandEvent &) {
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_preferences();
@@ -3417,8 +3453,8 @@ void MainFrame::init_menubar_as_editor()
auto top_menu = m_topbar->GetTopMenu();
top_menu->AppendSeparator();
append_menu_item(
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + "Space", "",
append_shortcut_item(
top_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
top_menu->AppendSeparator();
@@ -3524,8 +3560,8 @@ void MainFrame::init_menubar_as_editor()
#else
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
append_shortcut_item(
fileMenu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
append_menu_item(
@@ -3728,7 +3764,7 @@ void MainFrame::init_menubar_as_gcodeviewer()
wxMenu* viewMenu = nullptr;
if (m_plater != nullptr) {
viewMenu = new wxMenu();
add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this));
add_common_view_menu_items(viewMenu, std::bind(&MainFrame::can_change_view, this));
}
// helpmenu
+26
View File
@@ -74,6 +74,8 @@ class DesignPanel;
class MainFrame;
class WebViewPanel;
class ParamsDialog;
enum class Shortcut : uint8_t;
struct KeyChord;
#ifdef __WXGTK__
class ResizeEdgePanel;
#endif
@@ -195,6 +197,29 @@ class MainFrame : public DPIFrame
// vector of a MenuBar items changeable in respect to printer technology
std::vector<wxMenuItem*> m_changeable_menu_items;
// Menu items whose label shows a key binding; update_shortcut_labels() rewrites them.
struct ShortcutMenuItem
{
wxMenuItem* item;
Shortcut shortcut;
wxString label;
bool accelerator; // false keeps the binding display-only on macOS, where the menu bar's accelerators are live
};
std::vector<ShortcutMenuItem> m_shortcut_menu_items;
wxString shortcut_label(const wxString& label, Shortcut shortcut, bool accelerator);
template<typename... Args>
wxMenuItem* append_shortcut_item(wxMenu* menu, Shortcut shortcut, bool accelerator, const wxString& label, Args&&... args)
{
wxMenuItem* item = append_menu_item(menu, wxID_ANY, shortcut_label(label, shortcut, accelerator), std::forward<Args>(args)...);
m_shortcut_menu_items.push_back({ item, shortcut, label, accelerator });
return item;
}
// Runs the Global shortcut bound to chord; false when the focused control should see the key as well.
bool handle_global_shortcut(const KeyChord& chord);
void add_common_view_menu_items(wxMenu* view_menu, std::function<bool(void)> can_change_view);
wxMenu* generate_help_menu();
struct FileHistory : wxFileHistory
{
FileHistory(int max) : wxFileHistory(max) {}
@@ -354,6 +379,7 @@ public:
void request_select_tab(const wxString& id);
int get_calibration_curr_tab();
void select_view(const std::string& direction);
void update_shortcut_labels();
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
void on_config_changed(DynamicPrintConfig* cfg) const ;
void set_print_button_to_default(PrintSelectType select_type);
+2 -1
View File
@@ -10,6 +10,7 @@
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "I18N.hpp"
#include "Shortcuts.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "NetworkTestDialog.hpp"
@@ -563,7 +564,7 @@ std::vector<NativeCommand> build_command_catalog()
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
wxGetApp().keyboard_shortcuts();
wxGetApp().keyboard_shortcuts(ShortcutContext::Global);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
+6 -6
View File
@@ -82,6 +82,7 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "GUI_ObjectList.hpp"
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
@@ -7595,7 +7596,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_PRINTABLE, [this](SimpleEvent& evt) { this->sidebar->obj_list()->toggle_printable_state(); });
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [this](SimpleEvent&) {
wxGetApp().keyboard_shortcuts(view3D->get_canvas3d()->get_gizmos_manager().is_paint_gizmo() ? ShortcutContext::Painting : ShortcutContext::Plater);
});
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
@@ -7672,7 +7675,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); });
// Preview events:
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(ShortcutContext::Preview); });
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); });
preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent &) {
preview->get_canvas3d()->set_as_dirty();
@@ -8133,10 +8136,7 @@ void Plater::priv::collapse_sidebar(bool collapse)
sidebar_layout.is_collapsed = collapse;
// Now update the tooltip in the toolbar.
std::string new_tooltip = collapse
? _u8L("Expand sidebar")
: _u8L("Collapse sidebar");
new_tooltip += " [" + _u8L("Shift+") + _u8L("Tab") + "]";
const std::string new_tooltip = wxGetApp().shortcuts().with_key(collapse ? _u8L("Expand sidebar") : _u8L("Collapse sidebar"), Shortcut::CollapseSidebar);
int id = collapse_toolbar.get_item_id("collapse_sidebar");
collapse_toolbar.set_tooltip(id, new_tooltip);
+29 -6
View File
@@ -18,6 +18,7 @@
#include "NetworkTestDialog.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/RadioGroup.hpp"
#include "Shortcuts.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "NetworkPluginDialog.hpp"
@@ -285,6 +286,7 @@ std::tuple<wxBoxSizer*, ComboBox*> PreferencesDialog::create_item_combobox_base(
auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY);
combobox->GetDropDown().SetUseContentWidth(true);
combobox->SetToolTip(tip);
combobox->SetName(param); // select_tab() finds the row by this name
std::vector<wxString>::iterator iter;
for (iter = vlist.begin(); iter != vlist.end(); iter++) {
@@ -1522,6 +1524,19 @@ PreferencesDialog::~PreferencesDialog()
{
}
void PreferencesDialog::select_tab(PreferencesTab tab, const std::string& option)
{
if (const auto index = m_tab_index.find(tab); index != m_tab_index.end())
m_pref_tabs->SelectItem(index->second);
wxWindow* control = option.empty() ? nullptr : m_parent->FindWindow(wxString(option));
if (control == nullptr)
return;
int unit = 1;
m_parent->GetScrollPixelsPerUnit(nullptr, &unit);
m_parent->Scroll(wxDefaultCoord, (m_parent->CalcUnscrolledPosition(control->GetPosition()).y - FromDIP(10)) / unit);
control->SetFocus(); // the focused tint marks the row
}
void PreferencesDialog::on_dpi_changed(const wxRect &suggested_rect) {
m_pref_tabs->Rescale();
@@ -1599,7 +1614,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// GENERAL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("General"));
m_tab_index[PreferencesTab::General] = m_pref_tabs->AppendItem(_L("General"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -1730,8 +1745,8 @@ void PreferencesDialog::create_items()
auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)"));
g_sizer->Add(item_multi_machine);
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"),
_L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."),
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial from the keyboard"),
_L("When enabled, the Speed Dial keyboard shortcut (Space by default) opens the action search from any page."),
"enable_speed_dial");
g_sizer->Add(item_speed_dial);
@@ -1789,7 +1804,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// CONTROL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Control"));
m_tab_index[PreferencesTab::Control] = m_pref_tabs->AppendItem(_L("Control"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -1859,6 +1874,14 @@ void PreferencesDialog::create_items()
auto item_right_mouse_drag = create_item_combobox(_L("Right Mouse Drag"), _L("Set the action that dragging the right mouse button should perform."), "right_mouse_drag_action", ButtonDragActions);
g_sizer->Add(item_right_mouse_drag);
//// CONTROL > Keyboard
g_sizer->Add(create_item_title(_L("Keyboard")), 1, wxEXPAND);
auto item_shortcuts = create_item_button(_L("Keyboard shortcuts"), _L("Edit") + dots, "", _L("Choose the key for each action."), [this]() {
wxGetApp().keyboard_shortcuts(ShortcutContext::Global, this);
});
g_sizer->Add(item_shortcuts);
//// CONTROL > Clear my choice on ...
g_sizer->Add(create_item_title(_L("Clear my choice on...")), 1, wxEXPAND);
@@ -1883,7 +1906,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// GRAPHICS TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Graphics"));
m_tab_index[PreferencesTab::Graphics] = m_pref_tabs->AppendItem(_L("Graphics"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -2031,7 +2054,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// ONLINE TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Online"));
m_tab_index[PreferencesTab::Online] = m_pref_tabs->AppendItem(_L("Online"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
+6
View File
@@ -29,6 +29,9 @@ namespace Slic3r { namespace GUI {
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
#define DESIGN_LEFT_MARGIN 25
// The tabs other dialogs open Preferences on.
enum class PreferencesTab { General, Control, Graphics, Online };
class PreferencesDialog : public DPIDialog
{
private:
@@ -38,6 +41,7 @@ protected:
wxBoxSizer * m_sizer_body;
wxScrolledWindow* m_parent;
TabCtrl* m_pref_tabs;
std::map<PreferencesTab, int> m_tab_index; // position of each tab in m_pref_tabs
// bool m_settings_layout_changed {false};
bool m_seq_top_layer_only_changed{false};
@@ -60,6 +64,8 @@ public:
~PreferencesDialog();
void select_tab(PreferencesTab tab, const std::string& option = {}); // and scrolls a combobox option's row into view, focused
wxString m_backup_interval_time;
wxTimer m_filament_height_timer;
+377
View File
@@ -0,0 +1,377 @@
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include <algorithm>
#include <map>
#include <utility>
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<ShortcutInfo, size_t(Shortcut::Count)> 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<SectionInfo, size_t(ShortcutSection::Count)> 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<Shortcut> shortcuts_in(ShortcutContext context)
{
std::vector<Shortcut> 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<KeyChord>& 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<Shortcut> 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> ShortcutRegistry::match(ShortcutContext context, const KeyChord& chord) const
{
if (const std::optional<Shortcut> 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<Shortcut> ShortcutRegistry::conflicts(Shortcut shortcut, const KeyChord& chord) const
{
std::vector<Shortcut> 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<Shortcut> 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<std::string, std::string>& 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<KeyChord> 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<KeyChord>& 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
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include "KeyChord.hpp"
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r {
class AppConfig;
namespace GUI {
// Where a key press is looked up. A shortcut may belong to several contexts; Global ones are
// dispatched by the main frame before any child window sees the key.
enum class ShortcutContext : uint8_t { Global, Plater, Preview, ObjectList, Painting, Count };
constexpr uint8_t context_bit(ShortcutContext context) { return uint8_t(1u << unsigned(context)); }
enum class Shortcut : uint8_t {
// Project
NewProject, OpenProject, SaveProject, SaveProjectAs, ImportModel, Publish3mf,
// Slicing and printing
SlicePlate, ExportSlicedFile, PrintPlate, PrintHostQueue,
// Selection
SelectAll, SelectAllPlates,
// Editing
Undo, Redo, Cut, Copy, Paste, DeleteSelected, DeleteAll, CloneSelected,
// Objects
AddInstance, RemoveInstance, TogglePrintable, ToggleAutoDrop,
// Placement
Arrange, ArrangePlate, Orient, OrientPlate,
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
// Gizmos
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars,
// Sliders
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
// Painting tools
PaintToolCircle, PaintToolSphere, PaintToolFill, PaintToolGapFill, PaintToolTriangle, PaintToolHeightRange,
// Camera
ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings,
// Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application
Preferences, Search, SwitchView, CollapseSidebar, SpeedDial, ReloadDevicePage, KeyboardShortcuts,
Count
};
struct ShortcutInfo
{
Shortcut id;
const char* key; // AppConfig key
const char* name; // untranslated description
uint8_t contexts; // context_bit() mask
KeyChord default_chord;
bool repeatable; // runs on key auto-repeat as well
// Shift or Ctrl held with the bound key select a variant of this action, such as the finer
// move step or the faster slider step.
bool modifier_variants;
};
// Headings of the shortcuts dialog, in listing order.
enum class ShortcutSection : uint8_t {
Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application,
Count
};
const ShortcutInfo& shortcut_info(Shortcut shortcut);
ShortcutSection shortcut_section(Shortcut shortcut);
const char* section_name(ShortcutSection section); // untranslated heading
std::vector<Shortcut> shortcuts_in(ShortcutContext context); // in table order
// Effective key bindings: the built-in defaults overlaid with the user's own. Owns the
// chord -> shortcut index every dispatcher queries.
class ShortcutRegistry
{
public:
ShortcutRegistry();
KeyChord binding(Shortcut shortcut) const; // invalid when unbound
bool is_customized(Shortcut shortcut) const;
std::string display(Shortcut shortcut) const; // "Ctrl+N" for tooltips and the shortcuts dialog
std::string with_key(const std::string& text, Shortcut shortcut) const; // "text [Ctrl+N]", or text alone when unbound
std::string accelerator(Shortcut shortcut) const; // wx accelerator text for menu labels; empty when unbound or not menu-safe
// Matches a key event in one context. Global shortcuts are only found through the Global context.
std::optional<Shortcut> lookup(ShortcutContext context, const KeyChord& chord) const;
// lookup(), then the modifier_variants shortcuts, which also match with Shift or Ctrl added
// to their binding; step_modifiers holds whichever of the two were added.
struct Match
{
Shortcut shortcut;
int step_modifiers;
};
std::optional<Match> match(ShortcutContext context, const KeyChord& chord) const;
// Shortcuts other than shortcut bound to chord in a context it shares; Global shortcuts
// share every context.
std::vector<Shortcut> conflicts(Shortcut shortcut, const KeyChord& chord) const;
// The modifier_variants shortcut in a shared context whose Shift or Ctrl step is chord;
// such a chord is reserved for that step.
std::optional<Shortcut> step_owner(Shortcut shortcut, const KeyChord& chord) const;
void bind(Shortcut shortcut, const KeyChord& chord); // an invalid chord unbinds
void reset(Shortcut shortcut);
void reset_all();
void load(const AppConfig& config);
void save(AppConfig& config) const;
private:
void rebuild_index();
std::array<std::optional<KeyChord>, size_t(Shortcut::Count)> m_overrides;
std::unordered_map<KeyChord, std::vector<Shortcut>, KeyChordHash> m_index;
};
} // namespace GUI
} // namespace Slic3r
+1
View File
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_sort.cpp
test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp
test_shortcuts.cpp
../fff_print/test_helpers.cpp
)
+329
View File
@@ -0,0 +1,329 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/KeyChord.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include <wx/event.h>
using namespace Slic3r;
using namespace Slic3r::GUI;
namespace {
wxKeyEvent key_event(wxEventType type, int key_code, int modifiers = wxMOD_NONE)
{
wxKeyEvent evt(type);
evt.m_keyCode = key_code;
evt.SetControlDown(modifiers & wxMOD_CONTROL);
evt.SetShiftDown(modifiers & wxMOD_SHIFT);
evt.SetAltDown(modifiers & wxMOD_ALT);
return evt;
}
} // namespace
TEST_CASE("KeyChord round-trips through its canonical text", "[Shortcuts]")
{
const auto [chord, text] = GENERATE(table<KeyChord, std::string>({
{ { 'N', wxMOD_CONTROL }, "Ctrl+N" },
{ { 'S', wxMOD_CONTROL | wxMOD_SHIFT }, "Ctrl+Shift+S" },
{ { WXK_RETURN, wxMOD_SHIFT | wxMOD_ALT }, "Shift+Alt+Enter" },
{ { WXK_TAB, wxMOD_SHIFT }, "Shift+Tab" },
{ { WXK_DELETE }, "Del" },
{ { WXK_F5 }, "F5" },
{ { WXK_F12, wxMOD_CONTROL }, "Ctrl+F12" },
{ { '+' }, "+" },
{ { '-', wxMOD_CONTROL }, "Ctrl+-" },
{ { '?' }, "?" },
{ { ',', wxMOD_CONTROL }, "Ctrl+," },
}));
CAPTURE(text);
CHECK(chord.to_string() == text);
REQUIRE(KeyChord::parse(text).has_value());
CHECK(*KeyChord::parse(text) == chord);
}
TEST_CASE("KeyChord::parse accepts aliases and rejects malformed text", "[Shortcuts]")
{
CHECK(KeyChord::parse("control+n") == KeyChord{ 'N', wxMOD_CONTROL });
CHECK(KeyChord::parse("Cmd+Shift+Delete") == KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT });
CHECK(KeyChord::parse("PageUp") == KeyChord{ WXK_PAGEUP });
CHECK(KeyChord::parse("f3") == KeyChord{ WXK_F3 });
CHECK_FALSE(KeyChord::parse("").has_value());
CHECK_FALSE(KeyChord::parse("Ctrl+").has_value());
CHECK_FALSE(KeyChord::parse("Meta+A").has_value());
CHECK_FALSE(KeyChord::parse("F25").has_value());
CHECK_FALSE(KeyChord::parse("Shift+/").has_value()); // Shift is part of the punctuation character
}
TEST_CASE("Key events normalize to the key-down key codes", "[Shortcuts]")
{
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, 'A', wxMOD_CONTROL)) == KeyChord{ 'A', wxMOD_CONTROL });
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD5, wxMOD_CONTROL)) == KeyChord{ '5', wxMOD_CONTROL });
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD_ADD)) == KeyChord{ '+' });
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD_PAGEUP)) == KeyChord{ WXK_PAGEUP });
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_SHIFT, wxMOD_SHIFT)).valid());
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_CONTROL, wxMOD_CONTROL)).valid());
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, 'a')) == KeyChord{ 'A' });
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, 'A', wxMOD_SHIFT)) == KeyChord{ 'A', wxMOD_SHIFT });
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, WXK_CONTROL_C, wxMOD_CONTROL)) == KeyChord{ 'C', wxMOD_CONTROL });
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, '+', wxMOD_SHIFT)) == KeyChord{ '+' });
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, WXK_DELETE)) == KeyChord{ WXK_DELETE });
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_CHAR, 0x444)).valid()); // a Cyrillic letter is not bindable
}
TEST_CASE("Punctuation chords are the ones matched on char events", "[Shortcuts]")
{
CHECK(KeyChord{ '+' }.is_punctuation());
CHECK(KeyChord{ '?' }.is_punctuation());
CHECK_FALSE(KeyChord{ 'A' }.is_punctuation());
CHECK_FALSE(KeyChord{ '1' }.is_punctuation());
CHECK_FALSE(KeyChord{ '=', wxMOD_CONTROL }.is_punctuation());
CHECK_FALSE(KeyChord{ WXK_DELETE }.is_punctuation());
}
TEST_CASE("Chords that only the char event can resolve are recognized", "[Shortcuts]")
{
CHECK(KeyChord{ '/', wxMOD_SHIFT }.needs_char_event());
CHECK(KeyChord{ '-' }.needs_char_event());
CHECK_FALSE(KeyChord{ '=', wxMOD_CONTROL }.needs_char_event());
CHECK_FALSE(KeyChord{ 'A', wxMOD_SHIFT }.needs_char_event());
CHECK_FALSE(KeyChord{ '1' }.needs_char_event());
CHECK_FALSE(KeyChord{ WXK_F5 }.needs_char_event());
}
TEST_CASE("Only modified or non-printable chords qualify as menu accelerators", "[Shortcuts]")
{
CHECK(KeyChord{ 'N', wxMOD_CONTROL }.is_menu_accelerator());
CHECK(KeyChord{ 'N', wxMOD_ALT }.is_menu_accelerator());
CHECK(KeyChord{ WXK_DELETE }.is_menu_accelerator());
CHECK(KeyChord{ WXK_F5 }.is_menu_accelerator());
CHECK_FALSE(KeyChord{ 'A' }.is_menu_accelerator());
CHECK_FALSE(KeyChord{ 'A', wxMOD_SHIFT }.is_menu_accelerator());
CHECK_FALSE(KeyChord{ '?' }.is_menu_accelerator());
CHECK_FALSE(KeyChord{ WXK_SPACE }.is_menu_accelerator());
CHECK_FALSE(KeyChord{}.is_menu_accelerator());
ShortcutRegistry registry;
CHECK(registry.accelerator(Shortcut::NewProject) == "Ctrl+N");
#ifdef __APPLE__
CHECK(registry.accelerator(Shortcut::DeleteSelected) == "Backspace");
#else
CHECK(registry.accelerator(Shortcut::DeleteSelected) == "Del");
#endif
CHECK(registry.accelerator(Shortcut::Arrange).empty());
CHECK(registry.accelerator(Shortcut::ArrangePlate).empty());
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
}
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
{
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);
CHECK(entry.GetFlags() == (wxACCEL_CTRL | wxACCEL_SHIFT));
CHECK(entry.GetKeyCode() == 'S');
CHECK(entry.GetCommand() == 42);
const wxAcceleratorEntry bare = KeyChord{ WXK_BACK }.to_accelerator_entry(7);
CHECK(bare.GetFlags() == wxACCEL_NORMAL);
CHECK(bare.GetKeyCode() == WXK_BACK);
}
#ifndef __APPLE__
TEST_CASE("Display text matches the canonical text without translations", "[Shortcuts]")
{
CHECK(KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT }.display() == "Ctrl+Shift+Del");
CHECK(KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT }.display_parts() == std::vector<std::string>{ "Ctrl", "Shift", "Del" });
CHECK(KeyChord{ '+' }.display() == "+");
CHECK(KeyChord{ WXK_UP, wxMOD_SHIFT }.display() == "Shift+Arrow Up"); // the arrows keep the old dialog's names
CHECK(KeyChord{ WXK_UP, wxMOD_SHIFT }.to_string() == "Shift+Up");
CHECK(KeyChord{}.display().empty());
}
#endif
TEST_CASE("Every shortcut is listed under the section of its table row", "[Shortcuts]")
{
CHECK(shortcut_section(Shortcut::NewProject) == ShortcutSection::Project);
CHECK(shortcut_section(Shortcut::Publish3mf) == ShortcutSection::Project);
CHECK(shortcut_section(Shortcut::SlicePlate) == ShortcutSection::SlicingAndPrinting);
CHECK(shortcut_section(Shortcut::GizmoBrimEars) == ShortcutSection::Gizmos);
CHECK(shortcut_section(Shortcut::MovesSliderEnd) == ShortcutSection::Sliders);
CHECK(shortcut_section(Shortcut::ViewDefault) == ShortcutSection::Camera);
CHECK(shortcut_section(Shortcut::KeyboardShortcuts) == ShortcutSection::Application);
CHECK(std::string(section_name(ShortcutSection::SlicingAndPrinting)) == "Slicing and printing");
}
TEST_CASE("Default bindings never collide inside a context", "[Shortcuts]")
{
ShortcutRegistry registry;
for (size_t i = 0; i < size_t(Shortcut::Count); ++i) {
const Shortcut shortcut = Shortcut(i);
CAPTURE(shortcut_info(shortcut).key);
CHECK(registry.conflicts(shortcut, registry.binding(shortcut)).empty());
}
}
TEST_CASE("Shift and Ctrl variants of stepping shortcuts are left unbound", "[Shortcuts]")
{
ShortcutRegistry registry;
for (size_t i = 0; i < size_t(Shortcut::Count); ++i) {
const ShortcutInfo& info = shortcut_info(Shortcut(i));
if (!info.modifier_variants)
continue;
CAPTURE(info.key);
const KeyChord chord = registry.binding(info.id);
for (int modifier : { int(wxMOD_SHIFT), int(wxMOD_CONTROL), int(wxMOD_SHIFT | wxMOD_CONTROL) })
for (size_t c = 0; c < size_t(ShortcutContext::Count); ++c)
if (info.contexts & context_bit(ShortcutContext(c)))
CHECK_FALSE(registry.lookup(ShortcutContext(c), KeyChord{ chord.key, chord.modifiers | modifier }).has_value());
}
}
TEST_CASE("Lookups are scoped to the context of the key press", "[Shortcuts]")
{
ShortcutRegistry registry;
const KeyChord ctrl_n{ 'N', wxMOD_CONTROL };
const KeyChord ctrl_c{ 'C', wxMOD_CONTROL };
const KeyChord a{ 'A' };
const KeyChord c{ 'C' };
CHECK(registry.lookup(ShortcutContext::Global, ctrl_n) == Shortcut::NewProject);
CHECK_FALSE(registry.lookup(ShortcutContext::Plater, ctrl_n).has_value());
CHECK(registry.lookup(ShortcutContext::Plater, ctrl_c) == Shortcut::Copy);
CHECK(registry.lookup(ShortcutContext::ObjectList, ctrl_c) == Shortcut::Copy);
CHECK_FALSE(registry.lookup(ShortcutContext::Global, ctrl_c).has_value());
CHECK(registry.lookup(ShortcutContext::Plater, a) == Shortcut::Arrange);
CHECK_FALSE(registry.lookup(ShortcutContext::Preview, a).has_value());
CHECK(registry.lookup(ShortcutContext::Plater, c) == Shortcut::GizmoCut);
CHECK(registry.lookup(ShortcutContext::Preview, c) == Shortcut::ToggleGcodeWindow);
CHECK(registry.lookup(ShortcutContext::Painting, c) == Shortcut::PaintToolCircle);
}
TEST_CASE("Stepping shortcuts match with Shift or Ctrl added to their binding", "[Shortcuts]")
{
ShortcutRegistry registry;
using Match = ShortcutRegistry::Match;
auto same = [](const std::optional<Match>& match, Shortcut shortcut, int step_modifiers) {
return match.has_value() && match->shortcut == shortcut && match->step_modifiers == step_modifiers;
};
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP }), Shortcut::LayerSliderUp, 0));
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_SHIFT }), Shortcut::LayerSliderUp, wxMOD_SHIFT));
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT }), Shortcut::MoveSelectionLeft, wxMOD_CONTROL | wxMOD_SHIFT));
CHECK(same(registry.match(ShortcutContext::Plater, { 'A', wxMOD_SHIFT }), Shortcut::ArrangePlate, 0)); // an exact binding wins
CHECK_FALSE(registry.match(ShortcutContext::Plater, { 'Q', wxMOD_CONTROL }).has_value()); // Orient has no variants
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_ALT }).has_value()); // Alt is not a step modifier
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_HOME, wxMOD_SHIFT }).has_value()); // Home has no variants
// A binding with Shift or Ctrl of its own has no steps.
registry.bind(Shortcut::LayerSliderUp, { WXK_UP, wxMOD_CONTROL });
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_CONTROL }), Shortcut::LayerSliderUp, 0));
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value());
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_SHIFT }).has_value());
// An exact binding on the combined step wins over it.
registry.bind(Shortcut::Arrange, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT });
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT }), Shortcut::Arrange, 0));
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_SHIFT }), Shortcut::MoveSelectionLeft, wxMOD_SHIFT));
}
TEST_CASE("Conflicts cover shared contexts and every Global shortcut", "[Shortcuts]")
{
ShortcutRegistry registry;
CHECK(registry.conflicts(Shortcut::Arrange, { 'N', wxMOD_CONTROL }) == std::vector<Shortcut>{ Shortcut::NewProject });
CHECK(registry.conflicts(Shortcut::NewProject, { 'A' }) == std::vector<Shortcut>{ Shortcut::Arrange });
CHECK(registry.conflicts(Shortcut::ToggleGcodeWindow, { 'C' }).empty());
CHECK(registry.conflicts(Shortcut::ZoomIn, { 'C' }) == std::vector<Shortcut>{ Shortcut::GizmoCut, Shortcut::ToggleGcodeWindow });
CHECK(registry.conflicts(Shortcut::Arrange, { 'A' }).empty()); // a shortcut never conflicts with itself
// Only the exact chord conflicts; the steps of a stepping shortcut are reserved instead.
CHECK(registry.conflicts(Shortcut::GoToLayer, { WXK_UP, wxMOD_SHIFT }).empty());
CHECK(registry.conflicts(Shortcut::LayerSliderUp, { 'G', wxMOD_SHIFT }) == std::vector<Shortcut>{ Shortcut::GoToLayer });
}
TEST_CASE("Shift and Ctrl with a stepping shortcut's key are reserved for its steps", "[Shortcuts]")
{
ShortcutRegistry registry;
CHECK(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_SHIFT }) == Shortcut::LayerSliderUp);
CHECK(registry.step_owner(Shortcut::NewProject, { WXK_LEFT, wxMOD_CONTROL }) == Shortcut::MoveSelectionLeft); // Global shares every context
CHECK_FALSE(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value()); // the combined step is free
CHECK_FALSE(registry.step_owner(Shortcut::MoveSelectionLeft, { WXK_LEFT, wxMOD_SHIFT }).has_value()); // its own step
CHECK_FALSE(registry.step_owner(Shortcut::PaintToolCircle, { WXK_UP, wxMOD_SHIFT }).has_value()); // Painting shares no context
registry.bind(Shortcut::LayerSliderUp, { WXK_UP, wxMOD_CONTROL });
CHECK_FALSE(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value()); // a modified binding has no steps
}
TEST_CASE("Custom bindings replace the default and survive a config round trip", "[Shortcuts]")
{
ShortcutRegistry registry;
const KeyChord w{ 'W' };
registry.bind(Shortcut::Arrange, w);
CHECK(registry.is_customized(Shortcut::Arrange));
CHECK(registry.lookup(ShortcutContext::Plater, w) == Shortcut::Arrange);
CHECK_FALSE(registry.lookup(ShortcutContext::Plater, { 'A' }).has_value());
AppConfig config;
registry.save(config);
CHECK(config.get("shortcuts", "arrange") == "W");
CHECK_FALSE(config.has("shortcuts", "orient"));
ShortcutRegistry loaded;
loaded.load(config);
CHECK(loaded.lookup(ShortcutContext::Plater, w) == Shortcut::Arrange);
CHECK(loaded.binding(Shortcut::Orient) == KeyChord{ 'Q' });
SECTION("rebinding to the default clears the override")
{
registry.bind(Shortcut::Arrange, { 'A' });
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
registry.save(config);
CHECK_FALSE(config.has("shortcuts", "arrange"));
}
SECTION("an invalid chord unbinds and persists as none")
{
registry.bind(Shortcut::Arrange, KeyChord{});
CHECK_FALSE(registry.binding(Shortcut::Arrange).valid());
registry.save(config);
CHECK(config.get("shortcuts", "arrange") == "none");
loaded.load(config);
CHECK_FALSE(loaded.lookup(ShortcutContext::Plater, { 'A' }).has_value());
CHECK_FALSE(loaded.lookup(ShortcutContext::Plater, w).has_value());
}
SECTION("reset_all restores every default")
{
registry.reset_all();
CHECK(registry.lookup(ShortcutContext::Plater, { 'A' }) == Shortcut::Arrange);
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
}
}
TEST_CASE("A Global shortcut refuses a config binding that would swallow typing", "[Shortcuts]")
{
AppConfig config;
// A string literal would pick AppConfig::set's bool overload.
config.set("shortcuts", "save_project", std::string("S"));
config.set("shortcuts", "new_project", std::string("F9"));
ShortcutRegistry registry;
registry.load(config);
CHECK(registry.binding(Shortcut::SaveProject) == KeyChord{ 'S', wxMOD_CONTROL });
CHECK(registry.binding(Shortcut::NewProject) == KeyChord{ WXK_F9 });
}
TEST_CASE("Unreadable config entries fall back to the default binding", "[Shortcuts]")
{
AppConfig config;
config.set("shortcuts", "arrange", std::string("Hyper+Q"));
config.set("shortcuts", "no_such_shortcut", std::string("Ctrl+Q"));
ShortcutRegistry registry;
registry.load(config);
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
CHECK(registry.lookup(ShortcutContext::Plater, { 'A' }) == Shortcut::Arrange);
}