mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-22 00:12:34 +00:00
feat(gui): assignable keyboard shortcuts (#15706)
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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&) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user