mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 22:31:02 +00:00
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp. * Ignore minilzo's Config Headers in clang-tidy lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes. * Add Missing Includes Across the Remaining Sources and Tests Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo. * Make the GUI and Test Headers Compile on Their Own Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone. * Keep Windows and nanosvg Setup Ahead of the Added Includes OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build. * Add the GUI Includes the First Pass Missed Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass. * Keep the Added Test Includes Below the NOMINMAX Guard test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
582 lines
27 KiB
C++
582 lines
27 KiB
C++
#include "libslic3r/libslic3r.h"
|
|
#include "KBShortcutsDialog.hpp"
|
|
#include "I18N.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "GUI.hpp"
|
|
#include "Notebook.hpp"
|
|
#include <vector>
|
|
#include "slic3r/GUI/Shortcuts.hpp"
|
|
#include <wx/colour.h>
|
|
#include <string>
|
|
#include "slic3r/GUI/GUI_Utils.hpp"
|
|
#include <wx/gdicmn.h>
|
|
#include <wx/dialog.h>
|
|
#include <wx/event.h>
|
|
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
|
#include <utility>
|
|
#include <cstddef>
|
|
#include <wx/panel.h>
|
|
#include <variant>
|
|
#include <optional>
|
|
#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>
|
|
#include <wx/string.h>
|
|
#include <wx/wx.h>
|
|
#include <wx/treebase.h>
|
|
#include <wx/simplebook.h>
|
|
|
|
namespace Slic3r {
|
|
namespace GUI {
|
|
|
|
namespace {
|
|
|
|
wxString shortcut_names(const std::vector<Shortcut>& shortcuts)
|
|
{
|
|
wxString names;
|
|
for (Shortcut shortcut : shortcuts) {
|
|
if (!names.empty())
|
|
names += ", ";
|
|
names += _(shortcut_info(shortcut).name);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
const wxColour ERROR_COLOUR("#D01B1B");
|
|
|
|
// 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();
|
|
wxGetApp().UpdateDlgDarkUI(this);
|
|
}
|
|
|
|
void KBShortcutsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
|
{
|
|
m_tabs->Rescale();
|
|
Layout();
|
|
Fit();
|
|
Refresh();
|
|
}
|
|
|
|
void KBShortcutsDialog::fill_pages()
|
|
{
|
|
// 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()) {
|
|
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
|
|
fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")),
|
|
fixed(Section::SpeedDial, { ctrl, "B" }, L("Pin or unpin the selected action")),
|
|
// wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
|
|
#ifndef __APPLE__
|
|
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
|
|
#endif
|
|
});
|
|
|
|
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")),
|
|
fixed(Section::Display, { alt, wheel }, L("Move section plane")),
|
|
});
|
|
|
|
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")),
|
|
});
|
|
|
|
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")),
|
|
});
|
|
}
|
|
|
|
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")),
|
|
fixed(Section::Display, { alt, wheel }, L("Move section plane")),
|
|
});
|
|
}
|
|
|
|
wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
|
|
{
|
|
wxPanel* main_page = new wxPanel(parent);
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
wxScrolledWindow *scrollable_panel = new wxScrolledWindow(main_page);
|
|
wxGetApp().UpdateDarkUI(scrollable_panel);
|
|
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
|
|
scrollable_panel->SetBackgroundColour(page_colour);
|
|
scrollable_panel->SetScrollRate(0, FromDIP(20));
|
|
const int page_width = FromDIP(PAGE_WIDTH);
|
|
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
|
|
|
|
const int title_margin = FromDIP(TITLE_MARGIN);
|
|
const int row_margin = FromDIP(ROW_MARGIN);
|
|
const int gap = FromDIP(ROW_GAP);
|
|
|
|
wxBoxSizer* scrollable_panel_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
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->AddSpacer(title_margin);
|
|
scrollable_panel->SetSizer(scrollable_panel_sizer);
|
|
|
|
main_sizer->Add(scrollable_panel, 1, wxEXPAND);
|
|
main_page->SetSizer(main_sizer);
|
|
|
|
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->Layout(); // the box is created at its final size, so nothing resizes it into laying the sizer out
|
|
// The hook runs before the window procedure, so Windows does not open its window menu
|
|
// over the dialog on Alt+Space.
|
|
Bind(wxEVT_CHAR_HOOK, &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 (chord.is_system_shortcut()) {
|
|
reject(_L("The system uses this shortcut, so it cannot be assigned."));
|
|
} else 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("Shift and Ctrl with this key belong to %s and cannot be assigned."), _(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
|