fix: send the AMS tray's own coordinates on tray selection

command_ams_select_tray split the BBL tray id into ams_id/slot_id inline and untested. Extract build_ams_change_filament_body so the mapping is a named unit that command_ams_select_tray routes verbatim, and lock it with a test: tray 9 (a 6-slot box's slot 5) sends (2, 1), never a fabricated flat lane.
This commit is contained in:
Lam Wei Lun
2026-09-23 17:00:39 +08:00
470 changed files with 26483 additions and 7965 deletions
+23 -12
View File
@@ -122,17 +122,19 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
// clearance so the conflict checker never sees the two touch.
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
// exporting it. Beside the cube at the bed centre, clear of the edge exclusion strips some beds
// carry, with a few mm of clearance so the conflict checker never sees the two touch. The cube and
// the tower's estimated footprint are then pulled inside the printable outline as one rigid pair:
// moving the tower alone would push it back onto the cube on a narrow bed. Returns that move for
// the cube.
Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
{
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
if (area == nullptr || area->values.size() < 3)
return;
return Vec2d::Zero();
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
if (footprint.depth < EPSILON)
return;
return Vec2d::Zero();
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
// The position is the tower's own origin; a rotated tower extends from it in another
// direction, so place the rotated box's extents rather than the origin.
@@ -143,13 +145,22 @@ void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
const Vec2d size = unscale(local.max) - lo;
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
box.translate(Point::new_scale(pos.x(), pos.y()));
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
pos += move.cast<double>();
// A bed too small for the pair keeps the cube at its centre and places the tower alone.
const Polygons bed{Polygon::new_scale(area->values)};
const BoundingBox tower = get_extents(box);
BoundingBox pair = tower;
pair.merge(Point::new_scale(center.x() - 5., center.y() - 5.));
pair.merge(Point::new_scale(center.x() + 5., center.y() + 5.));
const Point room = get_extents(bed).size() - Point::new_scale(2. * margin, 2. * margin);
const bool rigid = pair.size().x() < room.x() && pair.size().y() < room.y();
const Vec2d move = WipeTower::move_box_inside_polygon(rigid ? pair : tower, bed, scaled<coord_t>(margin)).cast<double>();
pos += move;
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
return rigid ? move : Vec2d::Zero();
}
// Slice one centered cube that switches from filament 1 to filament 2 partway up, so exactly one
// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
@@ -157,10 +168,10 @@ void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
{
const Vec2d center = printable_area_center(cfg);
place_wipe_tower(cfg, center);
const Vec2d center = printable_area_center(cfg);
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
Model model;
Print print;
+16 -4
View File
@@ -123,6 +123,8 @@ set(SLIC3R_GUI_SOURCES
GUI/PluginPickerDialog.hpp
GUI/PluginsDialog.cpp
GUI/PluginsDialog.hpp
GUI/Shortcuts.cpp
GUI/Shortcuts.hpp
GUI/SpeedDialDialog.cpp
GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp
@@ -137,8 +139,16 @@ set(SLIC3R_GUI_SOURCES
GUI/TerminalDialog.hpp
GUI/PluginProgressDialog.cpp
GUI/PluginProgressDialog.hpp
GUI/PluginWebDialog.cpp
GUI/PluginWebDialog.hpp
GUI/WebDialog.cpp
GUI/WebDialog.hpp
GUI/DockPanel.cpp
GUI/DockPanel.hpp
GUI/WebPanel.cpp
GUI/WebPanel.hpp
GUI/Widgets/WebHosting.cpp
GUI/Widgets/WebHosting.hpp
GUI/AuiPaneLayout.cpp
GUI/AuiPaneLayout.hpp
GUI/DragCanvas.cpp
GUI/DragCanvas.hpp
GUI/EditGCodeDialog.cpp
@@ -336,6 +346,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
@@ -991,8 +1003,8 @@ elseif (WIN32)
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avformat/avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
+5 -1
View File
@@ -1157,6 +1157,10 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
const float support_normal_z = get_selection_support_normal_z();
// The outline passes below are driven by is_outline, which only the object shaders have; with an
// overlay one (wireframe, x-ray) bound they would just draw the volume again.
const bool shader_can_outline = shader->get_uniform_location("is_outline") >= 0;
// Prime depth_tex on every frame so non-outline draws do not keep the
// default sampler unit 0, which can conflict with other sampler types.
shader->set_uniform("depth_tex", OUTLINE_DEPTH_TEX_UNIT);
@@ -1257,7 +1261,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
//BBS: add outline related logic
if (volume.first->selected && GUI::wxGetApp().show_outline())
if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline())
volume.first->render_with_outline(cnv_size);
else
volume.first->render();
+20
View File
@@ -0,0 +1,20 @@
#include "AuiPaneLayout.hpp"
namespace Slic3r { namespace GUI {
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name)
{
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
const std::string prefix = "name=" + pane_name + ";";
size_t begin = 0;
for (size_t i = 0; i <= layout.size(); ++i) {
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
continue;
if (layout.compare(begin, prefix.size(), prefix) == 0)
return layout.substr(begin, i - begin);
begin = i + 1;
}
return {};
}
}} // namespace Slic3r::GUI
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r { namespace GUI {
// The part a wxAuiManager layout string (wxAuiManager::SavePerspective) holds for `pane_name`, in the
// form wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name);
}} // namespace Slic3r::GUI
+15 -19
View File
@@ -6,6 +6,7 @@
#include "Widgets/Label.hpp"
#include "MsgDialog.hpp"
#include "libslic3r/Print.hpp"
#include "PrePrintChecker.hpp"
#include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
@@ -1639,19 +1640,6 @@ void CalibrationPresetPage::update_combobox_filaments(MachineObject* obj)
select_default_compatible_filament();
}
bool CalibrationPresetPage::is_blocking_printing()
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
MachineObject* obj_ = dev->get_selected_machine();
if (obj_ == nullptr) return true;
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
bool CalibrationPresetPage::is_nozzle_info_synced() const
{
if (!curr_obj || !curr_obj->is_info_ready())
@@ -1733,11 +1721,13 @@ void CalibrationPresetPage::update_show_status()
}
}
//if (is_blocking_printing()) {
// show_status(CaliPresetPageStatus::CaliPresetStatusUnsupportedPrinter);
// return;
//}
//else
bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(
preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle));
}
if (obj_->is_connecting() || !obj_->is_connected()) {
show_status(CaliPresetPageStatus::CaliPresetStatusInConnecting);
return;
@@ -1789,7 +1779,9 @@ void CalibrationPresetPage::update_show_status()
return;
}
show_status(CaliPresetPageStatus::CaliPresetStatusNormal);
show_status(has_optional_printer_model ?
CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel :
CaliPresetPageStatus::CaliPresetStatusNormal);
}
@@ -1843,6 +1835,10 @@ void CalibrationPresetPage::show_status(CaliPresetPageStatus status)
Layout();
Fit();
}
else if (status == CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel) {
update_print_status_msg(PrePrintChecker::get_pre_state_msg(PrintDialogStatus::PrintStatusOptionalPrinterModel), true);
Enable_Send_Button(true);
}
else if (status == CaliPresetPageStatus::CaliPresetStatusNoUserLogin) {
wxString msg_text = _L("No login account, only printers in LAN mode are displayed.");
update_print_status_msg(msg_text, false);
@@ -152,7 +152,8 @@ enum CaliPresetPageStatus
CaliPresetStatusInConnecting,
CaliPresetStatusFilamentIncompatible,
CaliPresetStatusLanModeSDcardNotAvailable,
CaliPresetStatusDifferentNozzleDiameters
CaliPresetStatusDifferentNozzleDiameters,
CaliPresetStatusOptionalPrinterModel
};
class CalibrationPresetPage : public CalibrationWizardPage
@@ -268,7 +269,6 @@ protected:
bool is_nozzle_info_synced() const;
void show_status(CaliPresetPageStatus status);
void Enable_Send_Button(bool enable);
bool is_blocking_printing();
bool need_check_sdcard(MachineObject* obj);
CaliPresetPageStatus get_status() { return m_page_status; }
+59 -33
View File
@@ -1,8 +1,9 @@
#include "DevManager.h"
#include <nlohmann/json.hpp>
#include <exception>
#include "DevManager.h"
#include <libslic3r/AppConfig.hpp>
#include "CloudProvider.hpp"
#include "DevUtil.h"
@@ -48,10 +49,12 @@ namespace {
namespace Slic3r
{
DeviceManager::DeviceManager(NetworkAgent* agent)
DeviceManager::DeviceManager(NetworkAgent* agent, bool enable_refresher, AppConfig* app_config)
{
m_agent = agent;
m_refresher = new DeviceManagerRefresher(this);
m_agent = agent;
m_app_config = app_config;
if (enable_refresher)
m_refresher = new DeviceManagerRefresher(this);
DevPrinterConfigUtil::InitFilePath(resources_dir());
@@ -62,9 +65,14 @@ namespace Slic3r
}
}
AppConfig* DeviceManager::get_app_config() const
{
return m_app_config ? m_app_config : GUI::wxGetApp().app_config;
}
void DeviceManager::load_local_machines_from_config()
{
AppConfig* config = GUI::wxGetApp().app_config;
AppConfig* config = get_app_config();
if (!config)
return;
const auto local_machines = config->get_local_machines();
@@ -90,9 +98,8 @@ namespace Slic3r
}
}
void DeviceManager::update_local_machine(const MachineObject& m)
void DeviceManager::update_local_machine(const MachineObject& m, AppConfig* config)
{
AppConfig* config = GUI::wxGetApp().app_config;
if (config) {
if (m.is_lan_mode_printer()) {
if (m.has_access_right()) {
@@ -113,7 +120,8 @@ namespace Slic3r
DeviceManager::~DeviceManager()
{
delete m_refresher;
if (m_refresher)
delete m_refresher;
for (auto it = localMachineList.begin(); it != localMachineList.end(); it++)
{
@@ -173,14 +181,22 @@ namespace Slic3r
return printer_agent ? printer_agent->get_agent_info().id : "";
}
std::string DeviceManager::get_current_cloud_provider() const
{
const std::string agent_id = get_current_printer_agent_id();
if (!agent_id.empty())
return agent_id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
return GUI::wxGetApp().get_printer_cloud_provider();
}
void DeviceManager::EnableMultiMachine(bool enable)
{
m_agent->enable_multi_machine(enable);
m_enable_mutil_machine = enable;
}
void DeviceManager::start_refresher() { m_refresher->Start(); }
void DeviceManager::stop_refresher() { m_refresher->Stop(); }
void DeviceManager::start_refresher() { if (m_refresher) m_refresher->Start(); }
void DeviceManager::stop_refresher() { if (m_refresher) m_refresher->Stop(); }
void DeviceManager::keep_alive()
@@ -297,6 +313,8 @@ namespace Slic3r
/* update localMachineList */
it = localMachineList.find(dev_id);
AppConfig* config = get_app_config();
if (it != localMachineList.end()) {
// update properties
/* ip changed */
@@ -386,7 +404,6 @@ namespace Slic3r
obj->m_is_online = true;
//load access code
AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
if (config) {
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false);
}
@@ -400,7 +417,7 @@ namespace Slic3r
<< ", ip = " << dev_ip <<", printer_name = " << dev_name
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
}
update_local_machine(*obj);
update_local_machine(*obj, config);
}
catch (...) {
;
@@ -435,11 +452,16 @@ namespace Slic3r
obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->set_access_code(access_code, false);
update_local_machine(*obj);
update_local_machine(*obj, get_app_config());
return obj;
}
void DeviceManager::update_local_machine(const MachineObject& m)
{
update_local_machine(m, GUI::wxGetApp().app_config);
}
int DeviceManager::query_bind_status(std::string& msg, const std::string& provider)
{
if (!m_agent)
@@ -573,13 +595,13 @@ namespace Slic3r
<< " cur_selected=" << selected_machine;
auto my_machine_list = get_my_machine_list(get_current_printer_agent_id());
auto it = my_machine_list.find(dev_id);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: set_selected_machine lookup dev_id=" << dev_id
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set_selected_machine lookup dev_id=" << dev_id
<< " found=" << (it != my_machine_list.end())
<< " my_machine_count=" << my_machine_list.size()
<< " current_agent=" << get_current_printer_agent_id()
<< " provider=" << GUI::wxGetApp().get_printer_cloud_provider();
if (it != my_machine_list.end() && it->second) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: target machine dev_id=" << it->second->get_dev_id()
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: target machine dev_id=" << it->second->get_dev_id()
<< " printer_agent_id=" << it->second->printer_agent_id
<< " connection_type=" << it->second->connection_type()
<< " dev_connection_type=" << it->second->dev_connection_type;
@@ -598,7 +620,7 @@ namespace Slic3r
}
else if (last_selected->second->connection_type() == "cloud") {
const int result = m_agent->set_user_selected_machine("");
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cleared previous cloud selection dev_id="
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: cleared previous cloud selection dev_id="
<< selected_machine << " result=" << result;
}
}
@@ -635,7 +657,8 @@ namespace Slic3r
it->second->reset();
#if !BBL_RELEASE_TO_PUBLIC
it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false);
AppConfig* config = get_app_config();
it->second->connect(config && config->get("enable_ssl_for_mqtt") == "true");
#else
it->second->connect(it->second->local_use_ssl);
#endif
@@ -652,7 +675,7 @@ namespace Slic3r
// diff dev_id, cloud => set_user_selected_machine(new)
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id;
const int result = m_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: set new cloud selection dev_id="
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set new cloud selection dev_id="
<< dev_id << " result=" << result;
it->second->reset();
}
@@ -661,7 +684,8 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new lan machine, dev_id =" << dev_id;
it->second->reset();
#if !BBL_RELEASE_TO_PUBLIC
it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false);
AppConfig* config = get_app_config();
it->second->connect(config && config->get("enable_ssl_for_mqtt") == "true");
#else
it->second->connect(it->second->local_use_ssl);
#endif
@@ -681,7 +705,7 @@ namespace Slic3r
selected_machine = dev_id;
record_user_last_machine(selected_machine);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: DeviceManager selection complete selected_machine="
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: DeviceManager selection complete selected_machine="
<< selected_machine;
return true;
}
@@ -714,7 +738,7 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first;
}
const int result = m_agent->add_subscribe(dev_list);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size()
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size()
<< " result=" << result;
}
@@ -729,7 +753,7 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first;
}
const int result = m_agent->del_subscribe(dev_list);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size()
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size()
<< " result=" << result;
}
@@ -851,14 +875,15 @@ namespace Slic3r
json j = json::parse(body);
const bool has_request_context = j.contains("provider") && j.contains("agent_id") && j.contains("generation");
const std::string current_provider = get_current_cloud_provider();
const std::string provider = j.contains("provider") ? j["provider"].get<std::string>()
: GUI::wxGetApp().get_printer_cloud_provider();
: current_provider;
const std::string agent_id = j.contains("agent_id") ? j["agent_id"].get<std::string>()
: get_current_printer_agent_id();
const std::uint64_t generation = j.value("generation", std::uint64_t(0));
if (has_request_context &&
(provider != GUI::wxGetApp().get_printer_cloud_provider() ||
(provider != current_provider ||
agent_id != get_current_printer_agent_id() ||
generation != (m_agent ? m_agent->get_user_machine_list_generation() : 0))) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ignoring stale response provider="
@@ -903,7 +928,8 @@ namespace Slic3r
if (obj->get_dev_ip().empty())
{
obj->get_dev_ip() = Slic3r::GUI::wxGetApp().app_config->get("ip_address", dev_id);
if (AppConfig* config = get_app_config())
obj->get_dev_ip() = config->get("ip_address", dev_id);
}
userMachineList.insert(std::make_pair(dev_id, obj));
}
@@ -948,7 +974,7 @@ namespace Slic3r
obj->set_access_code(acc_code);
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: parsed cloud machine dev_id=" << dev_id
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parsed cloud machine dev_id=" << dev_id
<< " name=" << obj->get_dev_name()
<< " agent_id=" << obj->printer_agent_id
<< " connection_type=" << obj->connection_type()
@@ -968,7 +994,7 @@ namespace Slic3r
iterat++;
}
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: parse_user_print_info complete provider=" << provider
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parse_user_print_info complete provider=" << provider
<< " parsed_count=" << new_list.size()
<< " stored_count=" << userMachineList.size();
}
@@ -987,29 +1013,29 @@ namespace Slic3r
unsigned int http_code;
std::string body;
int result = m_agent->get_user_print_info(&http_code, &body, provider);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: get_user_print_info provider=" << provider
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: get_user_print_info provider=" << provider
<< " result=" << result << " http_code=" << http_code
<< " body_bytes=" << body.size();
if (result == 0)
{
// parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map.
// on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info.
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: queueing parse_user_print_info on UI thread";
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: queueing parse_user_print_info on UI thread";
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
}
}
void DeviceManager::record_user_last_machine(const std::string& dev_id)
{
if (Slic3r::GUI::wxGetApp().app_config) {
Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id);
if (AppConfig* config = get_app_config()) {
config->set("user_last_selected_machine", dev_id);
}
}
std::string DeviceManager::get_user_last_machine() const
{
if (Slic3r::GUI::wxGetApp().app_config) {
const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine");
if (AppConfig* config = get_app_config()) {
const auto& user_last_machine = config->get("user_last_selected_machine");
if (!user_last_machine.empty()) {
return user_last_machine;
} else if (m_agent) {
+10 -4
View File
@@ -12,6 +12,7 @@ namespace Slic3r
struct BBLocalMachine;
class MachineObject;
class NetworkAgent;
class AppConfig;
namespace GUI {
class GUI_App;
@@ -24,6 +25,7 @@ class DeviceManager
friend class DeviceManagerRefresher;
private:
NetworkAgent* m_agent{ nullptr };
AppConfig* m_app_config{ nullptr };
DeviceManagerRefresher* m_refresher{ nullptr };
bool m_enable_mutil_machine = false;
@@ -35,11 +37,13 @@ private:
std::map<std::string, MachineObject*> userMachineList; /* dev_id -> MachineObject* cloudMachine of User */
public:
DeviceManager(NetworkAgent* agent = nullptr);
DeviceManager(NetworkAgent* agent = nullptr, bool enable_refresher = true,
AppConfig* app_config = nullptr);
~DeviceManager();
public:
NetworkAgent* get_agent() const { return m_agent; }
AppConfig* get_app_config() const;
void set_agent(NetworkAgent* agent);
void start_refresher();
@@ -121,6 +125,7 @@ private:
void keep_alive();
void check_pushing();
std::string get_current_cloud_provider() const;
void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state);
void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id);
@@ -133,14 +138,15 @@ public:
std::string connection_type, std::string bind_state, std::string version,
std::string access_code);
static void update_local_machine(const MachineObject& m);
static void update_local_machine(const MachineObject& m, AppConfig* config);
};
class DeviceManagerRefresher : public wxObject
{
wxTimer* m_timer{ nullptr };
int m_timer_interval_msec = 5000;
wxTimer* m_timer{nullptr};
int m_timer_interval_msec = 5000;
DeviceManager* m_manager{ nullptr };
DeviceManager* m_manager{nullptr};
public:
DeviceManagerRefresher(DeviceManager* manger);
+21 -6
View File
@@ -16,6 +16,7 @@
#include "ReleaseNote.hpp"
#include <thread>
#include <mutex>
#include <charconv>
#include <codecvt>
#include <boost/foreach.hpp>
#include <boost/typeof/typeof.hpp>
@@ -476,7 +477,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
{
this->access_code = code;
if (only_refresh) {
AppConfig* config = GUI::wxGetApp().app_config;
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
if (config) {
if (is_lan_mode_printer()) {
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
@@ -489,7 +490,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
DeviceManager::update_local_machine(*this);
DeviceManager::update_local_machine(*this, config);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
@@ -1504,16 +1505,19 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty
int MachineObject::command_xyz_abs()
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_auto_leveling()
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_go_home()
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_go_home(get_dev_id(), this->is_in_printing(), m_support_mqtt_homing, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1636,11 +1640,13 @@ int MachineObject::command_stop_buzzer()
int MachineObject::command_set_bed(int temp)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
int MachineObject::command_set_nozzle(int temp)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1746,6 +1752,7 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
int MachineObject::command_ams_calibrate(int ams_id)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1784,6 +1791,7 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1806,6 +1814,7 @@ int MachineObject::command_start_camera()
int MachineObject::command_ams_select_tray(std::string tray_id)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
}
@@ -1965,6 +1974,7 @@ int MachineObject::command_ams_air_print_detect(bool air_print_detect)
int MachineObject::command_axis_control(std::string axis, double unit, double input_val, int speed)
{
if (!m_agent) return -1;
return command_with_dialog(m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
m_support_mqtt_axis_control, MachineObject::m_sequence_id++,
is_lan_mode_printer()));
@@ -2608,9 +2618,14 @@ void MachineObject::set_print_state(std::string status)
// why: printer agents can report progress without BBL cloud task identity.
void MachineObject::update_print_progress(const json& value)
{
if (value.is_string())
mc_print_percent = stoi(value.get<std::string>());
else if (value.is_number_integer())
if (value.is_string()) {
const std::string progress = value.get<std::string>();
int parsed_progress;
const auto result = std::from_chars(progress.data(), progress.data() + progress.size(), parsed_progress);
if (result.ec != std::errc{} || result.ptr != progress.data() + progress.size())
return;
mc_print_percent = parsed_progress;
} else if (value.is_number_integer())
mc_print_percent = value.get<int>();
else
return;
@@ -4667,7 +4682,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
if (diff.count() > 10.0f) {
BOOST_LOG_TRIVIAL(trace) << "parse_json timeout = " << diff.count();
}
DeviceManager::update_local_machine(*this);
DeviceManager::update_local_machine(*this, m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config);
return 0;
}
+103
View File
@@ -0,0 +1,103 @@
#include "DockPanel.hpp"
#include "GUI_App.hpp"
#include "Plater.hpp"
#include "Widgets/WebHosting.hpp"
#include <wx/weakref.h>
#include <algorithm>
#include <utility>
namespace Slic3r { namespace GUI {
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title)
{
std::string name = "plugin:" + plugin_key + ":" + title;
std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_');
return name;
}
DockPanel::DockPanel(wxWindow* parent,
const std::string& html,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed)
: WebPanel(parent, web_hosting::orca_bridge_script())
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// A link asking for a new window has nowhere to open from a docked panel.
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
}
DockPanel::~DockPanel()
{
if (m_on_destroyed)
m_on_destroyed();
}
bool DockPanel::on_page_message(const std::string& kind, const nlohmann::json& data)
{
if (kind == "message") {
if (m_on_message)
m_on_message(data);
return true;
}
if (kind == "close") {
request_close();
return true;
}
return false;
}
void DockPanel::push_message(const nlohmann::json& data)
{
if (!m_closing)
post_to_page(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
}
void DockPanel::fire_close()
{
if (m_closing)
return;
m_closing = true;
if (m_on_close) {
CloseHandler on_close = std::move(m_on_close);
m_on_close = nullptr;
on_close();
}
}
void DockPanel::request_close()
{
if (m_closing)
return;
fire_close();
// A page-requested close arrives inside the web view's script callback, so destroy later; another
// close path may have destroyed the panel by then.
wxWeakRef<DockPanel> self(this);
CallAfter([self]() {
if (self)
self->remove_pane();
});
}
void DockPanel::destroy_silently()
{
m_closing = true;
m_on_close = nullptr;
remove_pane();
}
void DockPanel::remove_pane()
{
if (Plater* plater = wxGetApp().plater())
plater->remove_dock_pane(this);
else
Destroy();
}
}} // namespace Slic3r::GUI
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "WebPanel.hpp"
#include <functional>
#include <string>
namespace Slic3r { namespace GUI {
// Stable across sessions so the saved layout finds the pane; free of wxAuiManager layout delimiters.
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
// A WebPanel docked in the Plater, on the plugin-window bridge minus submit. It can be destroyed
// without the GIL, so its hooks must not capture pybind11 objects.
class DockPanel : public WebPanel
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
using CloseHandler = std::function<void()>;
// on_close fires once, on a user or page close. on_destroyed runs on every destruction and must
// touch host-side state only.
DockPanel(wxWindow* parent,
const std::string& html,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed);
~DockPanel() override;
// Main thread only.
void push_message(const nlohmann::json& data);
// Fires on_close, then removes the pane.
void request_close();
// Removes the pane without on_close, for plugin unload. Destroys at once: unload always comes from
// the host, never from this panel's own callbacks.
void destroy_silently();
// Fires on_close at most once; also run by the pane's own close button.
void fire_close();
protected:
std::optional<std::string> page_html() override { return m_html; }
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
private:
void remove_pane();
std::string m_html;
bool m_closing{false};
MessageHandler m_on_message;
CloseHandler m_on_close;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+27 -2
View File
@@ -63,6 +63,7 @@ class PartPlateList;
#ifdef SLIC3R_CAD
class DesignSketchTool; // Design tab: interactive 2D sketch tool
#endif
struct KeyChord;
#if ENABLE_RETINA_GL
class RetinaHelper;
@@ -185,7 +186,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
@@ -626,7 +626,23 @@ private:
bool m_dynamic_background_enabled;
bool m_multisample_allowed;
bool m_moving;
bool m_tab_down;
// The key-down being dispatched, kept for the char event that may follow it.
struct KeyDown
{
int code = WXK_NONE;
bool repeat = false;
};
KeyDown m_key_down;
// A keyboard move or rotation of the selection runs from the key-down that started it to
// that key's release, so a held key becomes one undo step.
struct SelectionEdit
{
enum Kind { None, Move, Rotate };
Kind kind = None;
int key = WXK_NONE; // raw key code of the key-down, matched against the key-up
Vec3d direction{ Vec3d::UnitX() };
};
SelectionEdit m_selection_edit;
bool m_camera_movement;
//BBS: add toolpath outside
bool m_toolpath_outside{ false };
@@ -1094,6 +1110,13 @@ public:
void on_idle(wxIdleEvent& evt);
void on_char(wxKeyEvent& evt);
void on_key(wxKeyEvent& evt);
// Runs the Plater/Preview shortcut bound to chord, swallowing auto-repeats of one-shot
// shortcuts; false when nothing is bound.
bool handle_shortcut(const KeyChord& chord);
void apply_selection_move(bool slow, bool camera_space);
void apply_selection_rotate(double angle_z_rad);
void finish_selection_edit();
void update_shortcut_tooltips();
void on_mouse_wheel(wxMouseEvent& evt);
void on_timer(wxTimerEvent& evt);
void on_render_timer(wxTimerEvent& evt);
@@ -1333,6 +1356,8 @@ private:
//BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
void _render_wireframe_overlay();
bool _is_xray_view_active() const;
void _render_xray_volumes();
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode_overlay(int canvas_width, int canvas_height);
+2
View File
@@ -90,6 +90,8 @@ std::pair<bool, std::string> GLShadersManager::init()
, { "ENABLE_ENVIRONMENT_MAP"sv }
#endif // ENABLE_ENVIRONMENT_MAP
);
// used to render objects as translucent, edge weighted surfaces in the X-Ray view
valid &= append_shader("xray", { prefix + "xray.vs", prefix + "xray.fs" });
// used to render variable layers heights in 3d editor
valid &= append_shader("variable_layer_height", { prefix + "variable_layer_height.vs", prefix + "variable_layer_height.fs" });
// used to render highlight contour around selected triangles inside the multi-material gizmo
+40 -16
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "BindDialog.hpp"
#include "DeviceManager.hpp"
@@ -1117,6 +1118,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
@@ -2385,19 +2388,21 @@ GUI_App::~GUI_App()
bool GUI_App::is_blocking_printing(MachineObject *obj_)
{
DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
if (obj_ == nullptr) {
obj_ = dev->get_selected_machine();
}
if (!obj_)
{
return false;
}
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
const std::string source_model = preset_bundle
? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)
: std::string();
return is_blocking_printing(obj_, source_model);
}
bool GUI_App::is_blocking_printing(MachineObject *obj_, const std::string& source_model)
{
DeviceManager *dev = getDeviceManager();
if (!dev) return true;
if (obj_ == nullptr)
obj_ = dev->get_selected_machine();
if (!obj_)
return false;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
@@ -4665,12 +4670,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;
@@ -8455,7 +8476,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[] = {
@@ -8472,7 +8495,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();
+11 -2
View File
@@ -67,6 +67,9 @@ namespace GUI{
class RemovableDriveManager;
class OtherInstanceMessageHandler;
class ShortcutRegistry;
enum class ShortcutContext : uint8_t;
enum class PreferencesTab;
class MainFrame;
class Sidebar;
class ObjectSettings;
@@ -283,6 +286,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;
@@ -364,6 +368,7 @@ public:
EAppMode get_app_mode() const { return m_app_mode; }
Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; }
bool is_blocking_printing(MachineObject *obj_ = nullptr);
bool is_blocking_printing(MachineObject *obj_, const std::string& source_model);
Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
HMSQuery* get_hms_query() { return hms_query; }
NetworkAgent* getAgent() { return m_agent; }
@@ -480,7 +485,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;
@@ -636,7 +641,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.
@@ -734,6 +740,9 @@ public:
size_t get_instance_hash_int () { return m_instance_hash_int; }
ImGuiWrapper* imgui() { return m_imgui.get(); }
ShortcutRegistry& shortcuts() { return *m_shortcuts; }
// Saves the bindings and refreshes every menu label, tooltip and accelerator table that shows one.
void on_shortcuts_changed();
PrintHostJobQueue& printhost_job_queue() { return *m_printhost_job_queue.get(); }
+3 -7
View File
@@ -6,6 +6,7 @@
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "ObjectDataViewModel.hpp"
@@ -2083,13 +2084,8 @@ wxMenu* MenuFactory::assemble_part_menu()
void MenuFactory::append_menu_item_clone(wxMenu* menu)
{
#ifdef __APPLE__
static const wxString ctrl = ("Ctrl+");
#else
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const wxString ctrl = _L("Ctrl+");
#endif
append_menu_item(menu, wxID_ANY, _L("Clone") + "\t" + ctrl + "K", "",
const std::string accel = wxGetApp().shortcuts().accelerator(Shortcut::CloneSelected);
append_menu_item(menu, wxID_ANY, _L("Clone") + (accel.empty() ? wxString() : "\t" + from_u8(accel)), "",
[](wxCommandEvent&) {
plater()->clone_selection();
}, "", nullptr,
+51 -80
View File
@@ -4,6 +4,7 @@
#include "GUI_Factories.hpp"
//#include "GUI_ObjectLayers.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "BitmapComboBox.hpp"
@@ -245,56 +246,15 @@ ObjectList::ObjectList(wxWindow* parent) :
// Key events are not correctly processed by the wxDataViewCtrl on OSX.
// Our patched wxWidgets process the keyboard accelerators.
// On the other hand, using accelerators will break in-place editing on Windows & Linux/GTK (there is no in-place editing working on OSX for wxDataViewCtrl for now).
// Bind(wxEVT_KEY_DOWN, &ObjectList::OnChar, this);
{
// Accelerators
// wxAcceleratorEntry entries[25];
wxAcceleratorEntry entries[26];
int index = 0;
entries[index++].Set(wxACCEL_CTRL, (int)'C', wxID_COPY);
entries[index++].Set(wxACCEL_CTRL, (int)'X', wxID_CUT);
entries[index++].Set(wxACCEL_CTRL, (int)'V', wxID_PASTE);
entries[index++].Set(wxACCEL_CTRL, (int)'M', wxID_DUPLICATE);
entries[index++].Set(wxACCEL_CTRL, (int)'A', wxID_SELECTALL);
entries[index++].Set(wxACCEL_CTRL, (int)'Z', wxID_UNDO);
entries[index++].Set(wxACCEL_CTRL, (int)'Y', wxID_REDO);
entries[index++].Set(wxACCEL_NORMAL, WXK_BACK, wxID_DELETE);
//entries[index++].Set(wxACCEL_NORMAL, int('+'), wxID_ADD);
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_ADD, wxID_ADD);
//entries[index++].Set(wxACCEL_NORMAL, int('-'), wxID_REMOVE);
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_SUBTRACT, wxID_REMOVE);
//entries[index++].Set(wxACCEL_NORMAL, int('p'), wxID_PRINT);
int numbers_cnt = 0;
for (auto char_number : { '1', '2', '3', '4', '5', '6', '7', '8', '9' }) {
entries[index + numbers_cnt].Set(wxACCEL_NORMAL, int(char_number), wxID_LAST + numbers_cnt+1);
entries[index + 9 + numbers_cnt].Set(wxACCEL_NORMAL, WXK_NUMPAD0 + numbers_cnt - 1, wxID_LAST + numbers_cnt+1);
numbers_cnt++;
// index++;
}
wxAcceleratorTable accel(26, entries);
SetAcceleratorTable(accel);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->copy(); }, wxID_COPY);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->paste(); }, wxID_PASTE);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->select_item_all_children(); }, wxID_SELECTALL);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->remove(); }, wxID_DELETE);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->undo(); }, wxID_UNDO);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->redo(); }, wxID_REDO);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->cut(); }, wxID_CUT);
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->clone(); }, wxID_DUPLICATE);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->increase_instances(); }, wxID_ADD);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->decrease_instances(); }, wxID_REMOVE);
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->toggle_printable_state(); }, wxID_PRINT);
for (int i = 1; i < 10; i++)
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
if (filaments_count() > 1 && i <= filaments_count())
this->set_extruder_for_selected_items(i);
}, wxID_LAST+i);
m_accel = accel;
}
m_shortcut_id_base = wxWindow::NewControlId(int(Shortcut::Count));
for (size_t i = 0; i < size_t(Shortcut::Count); ++i)
this->Bind(wxEVT_MENU, [this, shortcut = Shortcut(i)](wxCommandEvent&) { dispatch_shortcut(shortcut); }, m_shortcut_id_base + int(i));
for (int i = 1; i < 10; i++)
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
if (filaments_count() > 1 && i <= filaments_count())
this->set_extruder_for_selected_items(i);
}, wxID_LAST+i);
update_shortcut_accelerators();
#else //__WXOSX__
Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { key_event(event); }); // doesn't work on OSX
#endif
@@ -1796,36 +1756,10 @@ void ObjectList::decrease_instances()
#ifndef __WXOSX__
void ObjectList::key_event(wxKeyEvent& event)
{
//if (event.GetKeyCode() == WXK_TAB)
// Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
//else
if (event.GetKeyCode() == WXK_DELETE /*|| event.GetKeyCode() == WXK_BACK*/ )
remove();
//else if (event.GetKeyCode() == WXK_F5)
// wxGetApp().plater()->reload_all_from_disk();
else if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL/*WXK_SHIFT*/))
select_item_all_children();
else if (wxGetKeyState(wxKeyCode('C')) && wxGetKeyState(WXK_CONTROL))
copy();
else if (wxGetKeyState(wxKeyCode('V')) && wxGetKeyState(WXK_CONTROL))
paste();
else if (wxGetKeyState(wxKeyCode('Y')) && wxGetKeyState(WXK_CONTROL))
redo();
else if (wxGetKeyState(wxKeyCode('Z')) && wxGetKeyState(WXK_CONTROL))
undo();
else if (wxGetKeyState(wxKeyCode('X')) && wxGetKeyState(WXK_CONTROL))
cut();
else if (wxGetKeyState(wxKeyCode('K')) && wxGetKeyState(WXK_CONTROL))
clone();
else if (event.GetUnicodeKey() == '+')
increase_instances();
else if (event.GetUnicodeKey() == '-')
decrease_instances();
else if (event.GetUnicodeKey() == 'p')
toggle_printable_state();
else if (event.GetUnicodeKey() == 'd')
toggle_auto_drop();
else if (filaments_count() > 1) {
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::ObjectList, KeyChord::from_event(event));
if (shortcut.has_value() && dispatch_shortcut(*shortcut))
return;
if (filaments_count() > 1) {
std::vector<wxChar> numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
wxChar key_char = event.GetUnicodeKey();
if (std::find(numbers.begin(), numbers.end(), key_char) != numbers.end()) {
@@ -1842,6 +1776,43 @@ void ObjectList::key_event(wxKeyEvent& event)
}
#endif /* __WXOSX__ */
#ifdef __WXOSX__
void ObjectList::update_shortcut_accelerators()
{
std::vector<wxAcceleratorEntry> entries;
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
for (Shortcut shortcut : shortcuts_in(ShortcutContext::ObjectList))
if (const KeyChord chord = shortcuts.binding(shortcut); chord.valid())
entries.push_back(chord.to_accelerator_entry(m_shortcut_id_base + int(shortcut)));
for (int i = 1; i < 10; ++i) {
entries.emplace_back(wxACCEL_NORMAL, '0' + i, wxID_LAST + i);
entries.emplace_back(wxACCEL_NORMAL, WXK_NUMPAD0 + i, wxID_LAST + i);
}
m_accel = wxAcceleratorTable(int(entries.size()), entries.data());
SetAcceleratorTable(m_accel);
}
#endif /* __WXOSX__ */
bool ObjectList::dispatch_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::DeleteSelected: remove(); break;
case Shortcut::SelectAll: select_item_all_children(); break;
case Shortcut::Copy: copy(); break;
case Shortcut::Paste: paste(); break;
case Shortcut::Cut: cut(); break;
case Shortcut::Undo: undo(); break;
case Shortcut::Redo: redo(); break;
case Shortcut::CloneSelected: clone(); break;
case Shortcut::AddInstance: increase_instances(); break;
case Shortcut::RemoveInstance: decrease_instances(); break;
case Shortcut::TogglePrintable: toggle_printable_state(); break;
case Shortcut::ToggleAutoDrop: toggle_auto_drop(); break;
default: return false;
}
return true;
}
void ObjectList::OnBeginDrag(wxDataViewEvent &event)
{
const bool mult_sel = multiple_selection();
+8 -1
View File
@@ -40,6 +40,9 @@ typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI {
enum class Shortcut : uint8_t;
struct ObjectVolumeID {
ModelObject* object{ nullptr };
ModelVolume* volume{ nullptr };
@@ -270,7 +273,11 @@ public:
void extruder_editing();
#ifndef __WXOSX__
void key_event(wxKeyEvent& event);
#else
// wxDataViewCtrl never sees key events on macOS, so the bindings are installed as accelerators.
void update_shortcut_accelerators();
#endif /* __WXOSX__ */
bool dispatch_shortcut(Shortcut shortcut);
void copy();
void paste();
@@ -482,8 +489,8 @@ public:
private:
#ifdef __WXOSX__
// void OnChar(wxKeyEvent& event);
wxAcceleratorTable m_accel;
wxWindowID m_shortcut_id_base;
#endif /* __WXOSX__ */
void OnContextMenu(wxDataViewEvent &event);
void list_manipulation(const wxPoint& mouse_pos, bool evt_context_menu = false);
-24
View File
@@ -280,8 +280,6 @@ bool Preview::init(wxWindow* parent, Bed3D& bed, Model* model)
m_canvas->enable_assemble_view_toolbar(false);
// sizer, m_canvas_widget
m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_layers_slider_from_canvas, this);
wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0);
@@ -505,28 +503,6 @@ void Preview::update_layers_slider_mode()
m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder, can_change_color);
}
void Preview::update_layers_slider_from_canvas(wxKeyEvent &event)
{
if (event.HasModifiers()) {
event.Skip();
return;
}
const auto key = event.GetKeyCode();
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
IMSlider *m_moves_slider = m_canvas->get_gcode_viewer().get_moves_slider();
if (key == 'L') {
if(!m_layers_slider->switch_one_layer_mode())
event.Skip();
m_canvas->set_as_dirty();
}
/*else if (key == WXK_SHIFT)
m_layers_slider->UseDefaultColors(false);*/
else
event.Skip();
}
void Preview::update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range)
{
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
-1
View File
@@ -171,7 +171,6 @@ private:
void update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range = false);
void update_layers_slider_mode();
void update_layers_slider_from_canvas(wxKeyEvent &event);
//BBS: add only gcode mode
void load_print_as_fff(bool keep_z_range = false, bool only_gcode = false);
};
@@ -354,7 +354,6 @@ bool GLGizmoAdvancedCut::on_init()
if (!GLGizmoRotate3D::on_init())
return false;
m_shortcut_key = WXK_CONTROL_C;
// initiate info shortcuts
const wxString ctrl = GUI::shortkey_ctrl_prefix();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoAssembly.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoAssembly::on_init()
{
GLGizmoMeasure::on_init();
m_shortcut_key = WXK_CONTROL_Y;
m_shortcut = Shortcut::GizmoAssembly;
return true;
}
+3 -6
View File
@@ -4,6 +4,7 @@
#include <glad/gl.h>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_Colors.hpp"
@@ -299,7 +300,6 @@ GLGizmoBase::GLGizmoBase(GLCanvas3D &parent, const std::string &icon_filename, u
: m_parent(parent)
, m_group_id(-1)
, m_state(Off)
, m_shortcut_key(NO_SHORTCUT_KEY_VALUE)
, m_icon_filename(icon_filename)
, m_sprite_id(sprite_id)
, m_imgui(wxGetApp().imgui())
@@ -515,11 +515,8 @@ void GLGizmoBase::render_input_window(float x, float y, float bottom_limit)
std::string GLGizmoBase::get_name(bool include_shortcut) const
{
int key = get_shortcut_key();
std::string out = on_get_name();
if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z)
out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]";
return out;
const std::string name = on_get_name();
return include_shortcut && m_shortcut.has_value() ? wxGetApp().shortcuts().with_key(name, *m_shortcut) : name;
}
} // namespace GUI
+4 -5
View File
@@ -11,6 +11,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include <cereal/archives/binary.hpp>
#include <optional>
#include <wx/event.h>
@@ -29,6 +30,7 @@ namespace GUI {
class ImGuiWrapper;
enum class Shortcut : uint8_t;
class GLCanvas3D;
enum class CommonGizmosDataID;
class CommonGizmosDataPool;
@@ -72,9 +74,6 @@ public:
NegZ = 1 << 5,
};
// Represents NO key(button on keyboard) value
static const int NO_SHORTCUT_KEY_VALUE = 0;
protected:
struct Grabber
{
@@ -138,7 +137,7 @@ protected:
int m_group_id; // TODO: remove only for rotate
EState m_state;
int m_shortcut_key;
std::optional<Shortcut> m_shortcut; // the registry entry that opens this gizmo
std::string m_icon_filename;
unsigned int m_sprite_id;
int m_hover_id{ -1 };
@@ -169,7 +168,7 @@ public:
EState get_state() const { return m_state; }
void set_state(EState state) { m_state = state; on_set_state(); }
int get_shortcut_key() const { return m_shortcut_key; }
std::optional<Shortcut> shortcut() const { return m_shortcut; }
const std::string& get_icon_filename() const { return m_icon_filename; }
+2 -1
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoBrimEars::on_init()
{
m_new_point_head_radius = get_brim_default_radius();
m_shortcut_key = WXK_CONTROL_E;
m_shortcut = Shortcut::GizmoBrimEars;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
+2 -1
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/format.hpp"
@@ -1310,7 +1311,7 @@ void GLGizmoCut3D::render_cut_line()
bool GLGizmoCut3D::on_init()
{
m_grabbers.emplace_back();
m_shortcut_key = WXK_CONTROL_C;
m_shortcut = Shortcut::GizmoCut;
// initiate info shortcuts
const wxString ctrl = GUI::shortkey_ctrl_prefix();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoEmboss.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/MainFrame.hpp" // to update title when add text
@@ -727,7 +728,7 @@ bool GLGizmoEmboss::on_init()
m_rotate_gizmo.set_highlight_color(gray_color);
// NOTE: It has special handling in GLGizmosManager::handle_shortcut
m_shortcut_key = WXK_CONTROL_T;
m_shortcut = Shortcut::GizmoEmboss;
m_shortcuts = {
{_L("Drag"), _L("Position on surface")}
+10 -20
View File
@@ -8,6 +8,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -78,7 +79,7 @@ std::string GLGizmoFdmSupports::on_get_name() const
bool GLGizmoFdmSupports::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_L;
m_shortcut = Shortcut::GizmoFdmSupports;
m_desc["perform"] = _L("Apply");
m_desc["on_overhangs_only"] = _L("On highlighted overhangs only");
@@ -149,25 +150,14 @@ void GLGizmoFdmSupports::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoFdmSupports::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoFdmSupports::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -26,8 +26,7 @@ public:
state_ready
};
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoFlatten.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
@@ -54,7 +55,7 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
bool GLGizmoFlatten::on_init()
{
m_shortcut_key = WXK_CONTROL_F;
m_shortcut = Shortcut::GizmoFlatten;
return true;
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
@@ -31,7 +32,7 @@ std::string GLGizmoFuzzySkin::on_get_name() const
bool GLGizmoFuzzySkin::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_shortcut = Shortcut::GizmoFuzzySkin;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
-1
View File
@@ -25,7 +25,6 @@ GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filenam
bool GLGizmoHollow::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_desc["enable"] = _(L("Hollow this object"));
m_desc["preview"] = _(L("Preview hollowed and drilled model"));
m_desc["offset"] = _(L("Offset")) + ": ";
+2 -1
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -449,7 +450,7 @@ bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_po
bool GLGizmoMeasure::on_init()
{
m_shortcut_key = WXK_CONTROL_U;
m_shortcut = Shortcut::GizmoMeasure;
const wxString shift = GUI::shortkey_shift_prefix();
+2 -1
View File
@@ -1,5 +1,6 @@
#include "GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "libslic3r/MeshBoolean.hpp"
@@ -104,7 +105,7 @@ bool GLGizmoMeshBoolean::on_mouse(const wxMouseEvent &mouse_event)
bool GLGizmoMeshBoolean::on_init()
{
m_shortcut_key = WXK_CONTROL_B;
m_shortcut = Shortcut::GizmoMeshBoolean;
return true;
}
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Plater.hpp"
@@ -90,7 +91,7 @@ void GLGizmoMmuSegmentation::init_extruders_data()
bool GLGizmoMmuSegmentation::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_N;
m_shortcut = Shortcut::GizmoMmuSegmentation;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -209,30 +210,16 @@ bool GLGizmoMmuSegmentation::on_number_key_down(int number)
return true;
}
bool GLGizmoMmuSegmentation::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'T':
m_current_tool = ImGui::TriangleButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'H':
m_current_tool = ImGui::HeightRangeIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoMmuSegmentation::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolTriangle: m_current_tool = ImGui::TriangleButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolHeightRange: m_current_tool = ImGui::HeightRangeIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
@@ -82,7 +82,7 @@ public:
// BBS
bool on_number_key_down(int number);
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoMove.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
//BBS: GUI refactor
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/AppConfig.hpp"
@@ -61,7 +62,7 @@ bool GLGizmoMove3D::on_init()
m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 };
m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 };
m_shortcut_key = WXK_CONTROL_M;
m_shortcut = Shortcut::GizmoMove;
return true;
}
@@ -192,6 +192,8 @@ public:
~GLGizmoPainterBase() override;
void data_changed(bool is_serializing) override;
virtual bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down);
// Switches the painting tool a Painting-context shortcut names; false when this gizmo has no such tool.
virtual bool on_tool_shortcut(Shortcut shortcut) { return false; }
// Following function renders the triangles and cursor. Having this separated
// from usual on_render method allows to render them before transparent
+2 -1
View File
@@ -3,6 +3,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp"
@@ -555,7 +556,7 @@ bool GLGizmoRotate3D::on_init()
for (unsigned int i = 0; i < 3; ++i)
m_gizmos[i].set_highlight_color(AXES_COLOR[i]);
m_shortcut_key = WXK_CONTROL_R;
m_shortcut = Shortcut::GizmoRotate;
return true;
}
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoScale.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <glad/gl.h>
@@ -135,7 +136,7 @@ bool GLGizmoScale3D::on_init()
// BBS
m_grabbers[4].enabled = false;
m_shortcut_key = WXK_CONTROL_S;
m_shortcut = Shortcut::GizmoScale;
return true;
}
+8 -14
View File
@@ -5,6 +5,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -28,7 +29,7 @@ void GLGizmoSeam::on_shutdown()
bool GLGizmoSeam::on_init()
{
m_shortcut_key = WXK_CONTROL_P;
m_shortcut = Shortcut::GizmoSeam;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -86,19 +87,12 @@ void GLGizmoSeam::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoSeam::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
default:
return false;
break;
bool GLGizmoSeam::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -12,8 +12,7 @@ public:
void render_painter_gizmo() override;
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
@@ -34,7 +34,6 @@ GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& ic
bool GLGizmoSlaSupports::on_init()
{
m_shortcut_key = WXK_CONTROL_L;
m_desc["head_diameter"] = _L("Head diameter") + ": ";
m_desc["lock_supports"] = _L("Lock supports under new islands");
-1
View File
@@ -261,7 +261,6 @@ bool GLGizmoText::on_init()
//m_avail_font_names = init_occt_fonts();
update_font_texture();
m_scale = m_imgui->get_font_size();
m_shortcut_key = WXK_CONTROL_T;
m_grabbers.push_back(Grabber());
+27 -59
View File
@@ -4,6 +4,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -486,15 +487,13 @@ bool GLGizmosManager::is_running() const
return m_current != Undefined;
}
bool GLGizmosManager::handle_shortcut(int key)
bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut)
{
if (!m_enabled)
return false;
auto is_key = [pressed_key = key](int gizmo_key) { return (gizmo_key == pressed_key - 64) || (gizmo_key == pressed_key - 96); };
// allowe open shortcut even when selection is empty
if (GLGizmoBase* gizmo_emboss = m_gizmos[Emboss].get();
is_key(gizmo_emboss->get_shortcut_key())) {
// The text tool opens without a selection because it creates its own object.
if (GLGizmoBase* gizmo_emboss = m_gizmos[Emboss].get(); gizmo_emboss->shortcut() == shortcut) {
dynamic_cast<GLGizmoEmboss *>(gizmo_emboss)->on_shortcut_key();
return true;
}
@@ -502,16 +501,21 @@ bool GLGizmosManager::handle_shortcut(int key)
if (m_parent.get_selection().is_empty())
return false;
auto is_gizmo = [is_key](const std::unique_ptr<GLGizmoBase> &gizmo) {
return gizmo->is_activable() && is_key(gizmo->get_shortcut_key());
};
auto it = std::find_if(m_gizmos.begin(), m_gizmos.end(), is_gizmo);
auto it = std::find_if(m_gizmos.begin(), m_gizmos.end(), [shortcut](const std::unique_ptr<GLGizmoBase> &gizmo) {
return gizmo->is_activable() && gizmo->shortcut() == shortcut;
});
if (it == m_gizmos.end())
return false;
EType gizmo_type = EType(it - m_gizmos.begin());
return open_gizmo(gizmo_type);
return open_gizmo(EType(it - m_gizmos.begin()));
}
bool GLGizmosManager::on_delete_key()
{
const bool processed = (m_current == Cut || m_current == Measure || m_current == Assembly) && gizmo_event(SLAGizmoEventType::Delete);
if (processed)
m_parent.set_as_dirty();
return processed;
}
bool GLGizmosManager::is_dragging() const
@@ -856,15 +860,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
}
break;
}
//skip some keys when gizmo
case 'A':
case 'a':
{
if (is_running()) {
processed = true;
}
break;
}
//case WXK_RETURN:
//{
// if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::ApplyChanges))
@@ -883,12 +878,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
//}
case WXK_BACK:
case WXK_DELETE: {
if ((m_current == Cut || m_current == Measure || m_current == Assembly) && gizmo_event(SLAGizmoEventType::Delete))
processed = true;
break;
}
//case 'A':
//case 'a':
//{
@@ -932,11 +921,6 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt)
}
}
if (!processed && !evt.HasModifiers()) {
if (handle_shortcut(keyCode))
processed = true;
}
if (processed)
m_parent.set_as_dirty();
@@ -1077,40 +1061,24 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
processed = select(digit);
}
}
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
processed = mmu_seg->on_key_down_select_tool_type(keyCode);
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
}
}
else if (m_current == FdmSupports) {
GLGizmoFdmSupports* fdm_support = dynamic_cast<GLGizmoFdmSupports*>(get_current());
if (fdm_support != nullptr && (keyCode == 'F' || keyCode == 'S' || keyCode == 'C' || keyCode == 'G')) {
processed = fdm_support->on_key_down_select_tool_type(keyCode);
}
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
else if (m_current == Seam) {
GLGizmoSeam* seam = dynamic_cast<GLGizmoSeam*>(get_current());
if (seam != nullptr && (keyCode == 'S' || keyCode == 'C')) {
processed = seam->on_key_down_select_tool_type(keyCode);
}
if (processed) {
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
} else if (m_current == Measure || m_current == Assembly) {
else if (m_current == Measure || m_current == Assembly) {
if (keyCode == WXK_CONTROL)
gizmo_event(SLAGizmoEventType::CtrlDown, Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.CmdDown());
else if (keyCode == WXK_SHIFT)
gizmo_event(SLAGizmoEventType::ShiftDown, Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.CmdDown());
}
if (!processed) {
if (auto painter = dynamic_cast<GLGizmoPainterBase*>(get_current()); painter != nullptr) {
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Painting, KeyChord::from_event(evt));
processed = shortcut.has_value() && painter->on_tool_shortcut(*shortcut);
if (processed)
// force extra frame to automatically update window size
wxGetApp().imgui()->set_requires_extra_frame();
}
}
}
if (processed)
+4 -1
View File
@@ -263,7 +263,10 @@ public:
EType get_gizmo_from_name(const std::string& gizmo_name) const;
bool is_running() const;
bool handle_shortcut(int key);
// Opens the gizmo bound to a Plater-context shortcut; false when no gizmo has it or it cannot open now.
bool open_gizmo_by_shortcut(Shortcut shortcut);
// Lets the current gizmo consume the delete key; false when it did not.
bool on_delete_key();
bool is_dragging() const;
+2 -2
View File
@@ -5,6 +5,7 @@
#include "GUI_ObjectList.hpp"
#include "GLCanvas3D.hpp"
#include "MainFrame.hpp"
#include "Preferences.hpp"
#include "Tab.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
@@ -444,9 +445,8 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path)
// open preferences
}
else if (dict["hypertext_type"] == "preferences") {
std::string page = dict["hypertext_preferences_page"];
std::string item = dict["hypertext_preferences_item"];
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [page, item]() { wxGetApp().open_preferences(1, page); } };// 1 is to modify
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [item]() { wxGetApp().open_preferences(PreferencesTab::Control, item); } };
m_loaded_hints.emplace_back(hint_data);
}
else if (dict["hypertext_type"] == "plater") {
+491 -309
View File
@@ -6,359 +6,319 @@
#include "Notebook.hpp"
#include <wx/scrolwin.h>
#include <wx/display.h>
#include <algorithm>
#include <set>
#include "GUI_App.hpp"
#include "wxExtensions.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include "Preferences.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/StaticBox.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/TabCtrl.hpp"
#include <wx/notebook.h>
namespace Slic3r {
namespace GUI {
wxDEFINE_EVENT(EVT_PREFERENCES_SELECT_TAB, wxCommandEvent);
namespace {
KBShortcutsDialog::KBShortcutsDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,_L("Keyboard Shortcuts"),
wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
wxString shortcut_names(const std::vector<Shortcut>& shortcuts)
{
// fonts
const wxFont& font = wxGetApp().normal_font();
const wxFont& bold_font = wxGetApp().bold_font();
SetFont(font);
this->SetSizeHints(wxDefaultSize, wxDefaultSize);
this->SetBackgroundColour(wxColour(255, 255, 255));
wxBoxSizer *m_sizer_top = new wxBoxSizer(wxVERTICAL);
auto m_top_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
m_top_line->SetBackgroundColour(wxColour(166, 169, 170));
m_sizer_top->Add(m_top_line, 0, wxEXPAND, 0);
m_sizer_body = new wxBoxSizer(wxHORIZONTAL);
m_panel_selects = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
m_panel_selects->SetBackgroundColour(wxColour(248, 248, 248));
wxBoxSizer *m_sizer_left = new wxBoxSizer(wxVERTICAL);
m_sizer_left->Add(0, 0, 0, wxEXPAND | wxTOP, FromDIP(20));
m_sizer_left->Add(create_button(0, _L("Global")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(1, _L("Prepare")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(2, _L("Toolbar")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(3, _L("Objects list")), 0, wxEXPAND, 0);
m_sizer_left->Add(create_button(4, _L("Preview")), 0, wxEXPAND, 0);
m_panel_selects->SetSizer(m_sizer_left);
m_panel_selects->Layout();
m_sizer_left->Fit(m_panel_selects);
m_sizer_body->Add(m_panel_selects, 0, wxEXPAND, 0);
m_sizer_right = new wxBoxSizer(wxHORIZONTAL);
m_sizer_right->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(12));
m_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(870), FromDIP(500)), 0);
m_sizer_right->Add(m_simplebook, 1, wxEXPAND, 0);
m_sizer_body->Add(m_sizer_right, 1, wxEXPAND, 0);
m_sizer_top->Add(m_sizer_body, 1, wxEXPAND, 0);
fill_shortcuts();
for (size_t i = 0; i < m_full_shortcuts.size(); ++i) {
wxPanel *page = create_page(m_simplebook, m_full_shortcuts[i], font, bold_font);
m_pages.push_back(page);
m_simplebook->AddPage(page, m_full_shortcuts[i].first.first, i == 0);
wxString names;
for (Shortcut shortcut : shortcuts) {
if (!names.empty())
names += ", ";
names += _(shortcut_info(shortcut).name);
}
return names;
}
Bind(EVT_PREFERENCES_SELECT_TAB, &KBShortcutsDialog::OnSelectTabel, this);
const wxColour ERROR_COLOUR("#D01B1B");
SetSizer(m_sizer_top);
Layout();
Fit();
// The camera action a mouse button drags, as set in Preferences > Control.
const char* mouse_action(const char* preference)
{
const std::string action = wxGetApp().app_config->get(preference);
return action == "1" ? L("Pan View") : action == "2" ? L("Rotate View") : L("None");
}
// Page layout in DIPs; titles and rows are indented as in the Preferences dialog.
constexpr int PAGE_WIDTH = 640;
constexpr int TITLE_MARGIN = DESIGN_LEFT_MARGIN - 10;
constexpr int ROW_MARGIN = DESIGN_LEFT_MARGIN;
constexpr int ROW_GAP = 16;
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::vector<wxString> to_wx(const std::vector<std::string>& parts)
{
std::vector<wxString> out;
for (const std::string& part : parts)
out.push_back(from_u8(part));
return out;
}
// The keys a Global shortcut can use, the second line of its hint and of a rejection.
wxString global_key_advice()
{
return wxString::Format(_L("Use %s or %s, or a key that does not type a character."),
from_u8(KeyChord::modifier_name(wxMOD_CONTROL)), from_u8(KeyChord::modifier_name(wxMOD_ALT)));
}
// The pieces of a chord, spaced out for the dialog: "Ctrl + Shift + A".
wxString join_keys(const std::vector<wxString>& parts)
{
wxString out;
for (const wxString& part : parts)
out += (out.empty() ? "" : " + ") + part;
return out;
}
} // namespace
KBShortcutsDialog::KBShortcutsDialog(wxWindow* parent, ShortcutContext page)
: DPIDialog(parent, wxID_ANY, _L("Keyboard Shortcuts"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
fill_pages();
ScalableButton* probe = new ScalableButton(this, wxID_ANY, "edit");
m_edit_size = probe->GetBestSize();
probe->Destroy();
m_buttons_width = 2 * m_edit_size.x + FromDIP(6);
m_row_text_width = FromDIP(PAGE_WIDTH) - FromDIP(ROW_MARGIN) - FromDIP(TITLE_MARGIN) - 2 * FromDIP(ROW_GAP) - m_buttons_width;
GetTextExtent("W", &m_key_slot, nullptr, nullptr, nullptr, &Label::Head_14);
// The page tabs follow the Preferences dialog.
m_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | wxTR_FULL_ROW_HIGHLIGHT);
m_tabs->Bind(wxEVT_RIGHT_DOWN, [](auto&) {});
m_tabs->SetFont(Label::Body_14);
m_simplebook = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(660), FromDIP(500)));
for (const Page& page : m_pages) {
m_tabs->AppendItem(page.title);
m_simplebook->AddPage(create_page(m_simplebook, page), page.title);
}
const StateColor tab_colour(std::make_pair(wxColour("#6B6B6C"), (int) StateColor::NotChecked), std::make_pair(wxColour("#363636"), (int) StateColor::Normal));
for (size_t i = 0; i < m_tabs->GetCount(); ++i)
m_tabs->SetItemTextColour(i, tab_colour);
m_tabs->Bind(wxEVT_TAB_SEL_CHANGED, [this](wxCommandEvent& e) {
for (size_t i = 0; i < m_tabs->GetCount(); ++i)
m_tabs->SetItemBold(i, int(i) == e.GetSelection());
m_simplebook->SetSelection(e.GetSelection());
});
const auto shown = std::find_if(m_pages.begin(), m_pages.end(), [page](const Page& entry) { return entry.context == page; });
m_tabs->SelectItem(shown == m_pages.end() ? 0 : int(shown - m_pages.begin()));
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(m_tabs, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(5));
sizer->Add(m_simplebook, 1, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent();
// select first
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(0);
event.SetEventObject(this);
wxPostEvent(this, event);
wxGetApp().UpdateDlgDarkUI(this);
}
void KBShortcutsDialog::OnSelectTabel(wxCommandEvent &event)
{
auto id = event.GetInt();
SelectHash::iterator i = m_hash_selector.begin();
while (i != m_hash_selector.end()) {
Select *sel = i->second;
if (id == sel->m_index) {
sel->m_tab_button->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#BFE1DE"))); // ORCA color for selected tab background
sel->m_tab_text->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#BFE1DE"))); // ORCA color for selected tab background
sel->m_tab_text->SetFont(::Label::Head_13);
sel->m_tab_button->Refresh();
sel->m_tab_text->Refresh();
m_simplebook->SetSelection(id);
} else {
sel->m_tab_button->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
sel->m_tab_text->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
sel->m_tab_text->SetFont(::Label::Body_13);
sel->m_tab_button->Refresh();
sel->m_tab_text->Refresh();
}
i++;
}
wxGetApp().UpdateDlgDarkUI(this);
}
wxWindow *KBShortcutsDialog::create_button(int id, wxString text)
{
auto tab_button = new wxWindow(m_panel_selects, wxID_ANY, wxDefaultPosition, wxSize( FromDIP(150), FromDIP(28)), wxTAB_TRAVERSAL);
wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(0, 0, 0, wxEXPAND | wxLEFT, FromDIP(22));
auto stext = new wxStaticText(tab_button, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, 0);
stext->SetFont(::Label::Body_13);
stext->SetForegroundColour(wxColour(38, 46, 48));
stext->Wrap(-1);
sizer->Add(stext, 1, wxALIGN_CENTER, 0);
tab_button->Bind(wxEVT_LEFT_DOWN, [this, id](auto &e) {
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(id);
event.SetEventObject(this);
wxPostEvent(this, event);
});
stext->Bind(wxEVT_LEFT_DOWN, [this, id](wxMouseEvent &e) {
auto event = wxCommandEvent(EVT_PREFERENCES_SELECT_TAB);
event.SetInt(id);
event.SetEventObject(this);
wxPostEvent(this, event);
});
Select *sel = new Select;
sel->m_index = id;
sel->m_tab_button = tab_button;
sel->m_tab_text = stext;
m_hash_selector[sel->m_index] = sel;
tab_button->SetSizer(sizer);
tab_button->Layout();
return tab_button;
}
void KBShortcutsDialog::on_dpi_changed(const wxRect& suggested_rect)
{
m_logo_bmp.msw_rescale();
m_header_bitmap->SetBitmap(m_logo_bmp.bmp());
msw_buttons_rescale(this, em_unit(), { wxID_OK });
m_tabs->Rescale();
Layout();
Fit();
Refresh();
}
void KBShortcutsDialog::fill_shortcuts()
void KBShortcutsDialog::fill_pages()
{
const std::string ctrl = GUI::shortkey_ctrl_prefix();
const std::string alt = GUI::shortkey_alt_prefix();
const std::string shift = L("Shift+");
// A fixed row is listed in the section of the shortcuts it belongs with.
auto fixed = [](ShortcutSection section, std::vector<wxString> keys, const char* description) { return Row{ FixedKey{ std::move(keys), description }, section }; };
auto mouse = [](ShortcutSection section, const wxString& button, const char* preference) { return Row{ MouseAction{ button, preference }, section }; };
auto key = [](const std::string& key) { return _L_CONTEXT(key, "Keyboard Shortcut"); };
auto page = [this](const wxString& title, const wxString& caption, ShortcutContext context, std::vector<Row> fixed_rows) {
Page entry{ title, caption, context, {} };
for (Shortcut shortcut : shortcuts_in(context))
entry.rows.push_back({ shortcut, shortcut_section(shortcut) });
entry.rows.insert(entry.rows.end(), fixed_rows.begin(), fixed_rows.end());
std::stable_sort(entry.rows.begin(), entry.rows.end(), [](const Row& a, const Row& b) { return a.section < b.section; });
m_pages.push_back(std::move(entry));
};
const wxString ctrl = from_u8(KeyChord::modifier_name(wxMOD_CONTROL));
const wxString alt = from_u8(KeyChord::modifier_name(wxMOD_ALT));
const wxString shift = from_u8(KeyChord::modifier_name(wxMOD_SHIFT));
const wxString shift_ctrl = shift + "/" + ctrl; // either one
const wxString any_key = key(L_CONTEXT("Key", "Keyboard Shortcut")); // the key the row's shortcut is bound to
const wxString esc = key(L_CONTEXT("Esc", "Keyboard Shortcut"));
const wxString left_button = _L("Left mouse");
const wxString wheel = _L("Mouse wheel");
using Section = ShortcutSection;
if (wxGetApp().is_editor()) {
Shortcuts global_shortcuts = {
// File
{ ctrl + "N", L("New Project") },
{ ctrl + "O", L("Open Project") },
{ ctrl + "S", L("Save Project") },
{ ctrl + shift + "S", L("Save Project as")},
{ ctrl + shift + "E", L("Publish 3MF") },
// File>Import
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
// File>Export
{ ctrl + "G", L("Export plate sliced file")},
// Slice plate
{ ctrl + "R", L("Slice plate")},
// Send to Print
{ ctrl + shift + "G", L("Print plate")},
// Edit
{ ctrl + "X", L("Cut") },
{ ctrl + "C", L("Copy to clipboard") },
{ ctrl + "V", L("Paste from clipboard") },
// Configuration
{ ctrl + "P", L("Preferences") },
//3D control
#ifdef __APPLE__
{ ctrl + shift + "M", L("Show/Hide 3Dconnexion devices settings dialog") },
#else
{ ctrl + "M", L("Show/Hide 3Dconnexion devices settings dialog") },
#endif // __APPLE
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
fixed(Section::Application, { alt, "1-9, 0" }, L("Run a speed dial favorite while the dial is open")),
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
});
// Switch table page
{ ctrl + L("Tab"), L("Switch table page")},
// Open speed dial
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") },
{ alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") },
//DEL
#ifdef __APPLE__
{"fn+⌫", L("Delete Selected")},
#else
{L_CONTEXT("Del", "Keyboard Shortcut"), L("Delete Selected")},
#endif
// Help
{ "?", L("Show keyboard shortcuts list") }
};
m_full_shortcuts.push_back({{_L("Global shortcuts"), ""}, global_shortcuts});
page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, {
fixed(Section::Selection, { alt, left_button }, L("Select a part")),
fixed(Section::Selection, { ctrl, left_button }, L("Select multiple objects")),
fixed(Section::Selection, { shift, left_button }, L("Select objects by rectangle")),
fixed(Section::Selection, { esc }, L("Deselect All")),
fixed(Section::Objects, { "1-9" }, L("Keyboard 1-9: set filament for object/part")),
fixed(Section::Placement, { shift, any_key }, L("Movement step set to 1mm")),
fixed(Section::Placement, { ctrl, any_key }, L("Movement in camera space")),
mouse(Section::Camera, left_button, "left_mouse_drag_action"),
mouse(Section::Camera, _L("Middle mouse"), "middle_mouse_drag_action"),
mouse(Section::Camera, _L("Right mouse"), "right_mouse_drag_action"),
fixed(Section::Camera, { wheel }, L("Zoom View")),
});
// Retrieve mouse actions from config and map to MouseAction
std::map<std::string, std::string> mouse_actions;
mouse_actions["0"] = L("None");
mouse_actions["1"] = L("Pan View");
mouse_actions["2"] = L("Rotate View");
page(_L("Painting"), _L("Available while a painting gizmo is open: supports, seam, fuzzy skin or color painting."), ShortcutContext::Painting, {
fixed(Section::Gizmos, { esc }, L("Deselect All")),
fixed(Section::Gizmos, { shift, left_button }, L("Move: press to snap by 1mm")),
fixed(Section::PaintingTools, { ctrl, wheel }, L("Support/Color Painting: adjust pen radius")),
fixed(Section::PaintingTools, { alt, wheel }, L("Support/Color Painting: adjust section position")),
});
Shortcuts plater_shortcuts = {
{ L("Left mouse button"), mouse_actions[wxGetApp().app_config->get("left_mouse_drag_action").c_str()]},
{ L("Middle mouse button"), mouse_actions[wxGetApp().app_config->get("middle_mouse_drag_action").c_str()]},
{ L("Right mouse button"), mouse_actions[wxGetApp().app_config->get("right_mouse_drag_action").c_str()]},
{ L("Mouse wheel"), L("Zoom View") },
{ "A", L("Arrange all objects") },
{ shift + "A", L("Arrange objects on selected plates") },
{ "Q", L("Auto orients selected objects or all objects. If there are selected objects, it just orients the selected ones. Otherwise, it will orient all objects in the current project.") },
{ shift + "Q", L("Auto orients all objects on the active plate.") },
{shift + L("Tab"), L("Collapse/Expand the sidebar")},
{ctrl + L("Any arrow"), L("Movement in camera space")},
{alt + L("Left mouse button"), L("Select a part")},
{ctrl + L("Left mouse button"), L("Select multiple objects")},
{shift + L("Left mouse button"), L("Select objects by rectangle")},
{L_CONTEXT("Arrow Up", "Keyboard Shortcut"), L("Move selection 10mm in positive Y direction")},
{L_CONTEXT("Arrow Down", "Keyboard Shortcut"), L("Move selection 10mm in negative Y direction")},
{L_CONTEXT("Arrow Left", "Keyboard Shortcut"), L("Move selection 10mm in negative X direction")},
{L_CONTEXT("Arrow Right", "Keyboard Shortcut"), L("Move selection 10mm in positive X direction")},
{shift + L("Any arrow"), L("Movement step set to 1mm")},
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{"1-9", L("Keyboard 1-9: set filament for object/part")},
{ctrl + "0", L("Camera view - Default")},
{ctrl + "1", L("Camera view - Top")},
{ctrl + "2", L("Camera view - Bottom")},
{ctrl + "3", L("Camera view - Front")},
{ctrl + "4", L("Camera view - Behind")},
{ctrl + "5", L("Camera Angle - Left side")},
{ctrl + "6", L("Camera Angle - Right side")},
{ctrl + "A", L("Select all objects")},
{ctrl + "D", L("Delete All")},
{ctrl + "Z", L("Undo")},
{ctrl + "Y", L("Redo")},
{ "M", L("Gizmo move") },
{ "R", L("Gizmo rotate") },
{ "S", L("Gizmo scale") },
{ "F", L("Gizmo place face on bed") },
{ "C", L("Gizmo cut") },
{ "B", L("Gizmo mesh boolean") },
{ "H", L("Gizmo FDM paint-on fuzzy skin") },
{ "L", L("Gizmo SLA support points") },
{ "P", L("Gizmo FDM paint-on seam") },
{ "T", L("Gizmo text emboss/engrave") },
{ "U", L("Gizmo measure") },
{ "Y", L("Gizmo assemble") },
{ "E", L("Gizmo brim ears") },
{ "I", L("Zoom in") },
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
};
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
Shortcuts gizmos_shortcuts = {
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{shift, L("Move: press to snap by 1mm")},
{ctrl + L("Mouse wheel"), L("Support/Color Painting: adjust pen radius")},
{alt + L("Mouse wheel"), L("Support/Color Painting: adjust section position")},
};
m_full_shortcuts.push_back({{_L("Gizmo"), ""}, gizmos_shortcuts});
Shortcuts object_list_shortcuts = {
{"1-9", L("Set extruder number for the objects and parts") },
{L_CONTEXT("Del", "Keyboard Shortcut"), L("Delete objects, parts, modifiers")},
{L_CONTEXT("Esc", "Keyboard Shortcut"), L("Deselect All")},
{ctrl + "C", L("Copy to clipboard")},
{ctrl + "V", L("Paste from clipboard")},
{ctrl + "X", L("Cut")},
{ctrl + "A", L("Select all objects")},
{ctrl + "K", L("Clone Selected")},
{ctrl + "Z", L("Undo")},
{ctrl + "Y", L("Redo")},
{L_CONTEXT("Space", "Keyboard Shortcut"), L("Select the object/part and press space to change the name")},
{L("Mouse click"), L("Select the object/part and mouse click to change the name")},
};
m_full_shortcuts.push_back({ { _L("Objects List"), "" }, object_list_shortcuts });
page(_L("Objects list"), _L("Available while the object list has focus."), ShortcutContext::ObjectList, {
fixed(Section::Selection, { esc }, L("Deselect All")),
fixed(Section::Objects, { "1-9" }, L("Set extruder number for the objects and parts")),
fixed(Section::Objects, { key(L_CONTEXT("Space", "Keyboard Shortcut")) }, L("Select the object/part and press space to change the name")),
fixed(Section::Objects, { _L("Mouse click") }, L("Select the object/part and mouse click to change the name")),
});
}
Shortcuts preview_shortcuts = {
{ L_CONTEXT("Arrow Up", "Keyboard Shortcut"), L("Vertical slider - Move active thumb Up")},
{ L_CONTEXT("Arrow Down", "Keyboard Shortcut"), L("Vertical slider - Move active thumb Down")},
{ L_CONTEXT("Arrow Left", "Keyboard Shortcut"), L("Horizontal slider - Move active thumb Left")},
{ L_CONTEXT("Arrow Right", "Keyboard Shortcut"), L("Horizontal slider - Move active thumb Right")},
{ "L", L("On/Off one layer mode of the vertical slider")},
{ "C", L("On/Off G-code window")},
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview")},
{shift + L("Any arrow"), L("Move slider 5x faster")},
{shift + L("Mouse wheel"), L("Move slider 5x faster")},
{ctrl + L("Any arrow"), L("Move slider 5x faster")},
{ctrl + L("Mouse wheel"), L("Move slider 5x faster")},
{ L_CONTEXT("Home", "Keyboard Shortcut"), L("Horizontal slider - Move to start position")},
{ L_CONTEXT("End", "Keyboard Shortcut"), L("Horizontal slider - Move to last position")},
};
m_full_shortcuts.push_back({ { _L("Preview"), "" }, preview_shortcuts });
page(_L("Preview"), _L("Available while the 3D view on the Preview tab has focus."), ShortcutContext::Preview, {
fixed(Section::Sliders, { shift_ctrl, any_key }, L("Move slider 5x faster")),
fixed(Section::Sliders, { shift_ctrl, wheel }, L("Scroll slider 5x faster")),
});
}
wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font)
wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
{
wxPanel* main_page = new wxPanel(parent);
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
if (!shortcuts.first.second.empty()) {
main_sizer->AddSpacer(FromDIP(10));
wxBoxSizer* info_sizer = new wxBoxSizer(wxHORIZONTAL);
info_sizer->AddStretchSpacer();
info_sizer->Add(new wxStaticText(main_page, wxID_ANY, shortcuts.first.second), 0);
info_sizer->AddStretchSpacer();
main_sizer->Add(info_sizer, 0, wxEXPAND);
main_sizer->AddSpacer(FromDIP(10));
}
int items_count = (int) shortcuts.second.size();
wxScrolledWindow *scrollable_panel = new wxScrolledWindow(main_page);
wxGetApp().UpdateDarkUI(scrollable_panel);
scrollable_panel->SetScrollbars(20, 20, 50, 50);
scrollable_panel->SetInitialSize(wxSize(FromDIP(850), FromDIP(450)));
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
scrollable_panel->SetBackgroundColour(page_colour);
scrollable_panel->SetScrollRate(0, 20);
const int page_width = FromDIP(PAGE_WIDTH);
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
wxBoxSizer * scrollable_panel_sizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer *grid_sizer = new wxFlexGridSizer(items_count, 2, FromDIP(10), FromDIP(20));
const int title_margin = FromDIP(TITLE_MARGIN);
const int row_margin = FromDIP(ROW_MARGIN);
const int gap = FromDIP(ROW_GAP);
for (int i = 0; i < items_count; ++i) {
const auto &[shortcut, description] = shortcuts.second[i];
// Keyboard keys carry a "Keyboard Shortcut" context so translators keep them in English;
// mouse-input labels are ordinary phrases and use the plain lookup.
const bool is_mouse = shortcut.find("Mouse") != std::string::npos || shortcut.find("mouse") != std::string::npos;
auto key = new wxStaticText(scrollable_panel, wxID_ANY, is_mouse ? _(shortcut) : _L_CONTEXT(shortcut, "Keyboard Shortcut"));
key->SetForegroundColour(wxColour(50, 58, 61));
key->SetFont(bold_font);
grid_sizer->Add(key, 0, wxALIGN_CENTRE_VERTICAL);
wxBoxSizer* scrollable_panel_sizer = new wxBoxSizer(wxVERTICAL);
auto desc = new wxStaticText(scrollable_panel, wxID_ANY, _(description));
desc->SetFont(font);
desc->SetForegroundColour(wxColour(50, 58, 61));
desc->Wrap(FromDIP(600));
grid_sizer->Add(desc, 0, wxALIGN_CENTRE_VERTICAL);
const wxColour note_colour = StateColor::darkModeColorFor(wxColour("#F8F8F8"));
const wxColour note_text = StateColor::darkModeColorFor(wxColour("#6B6B6A"));
StaticBox* note = new StaticBox(scrollable_panel);
note->SetCornerRadius(FromDIP(4));
note->SetBorderWidth(0);
note->SetBackgroundColor(note_colour);
note->SetBackgroundColour(note_colour);
auto note_icon = new wxStaticBitmap(note, wxID_ANY, ScalableBitmap(note, "help", 16).bmp());
auto note_text_ctrl = new wxStaticText(note, wxID_ANY, page.caption);
note_text_ctrl->SetFont(Label::Body_13);
note_text_ctrl->SetForegroundColour(note_text);
note_text_ctrl->SetBackgroundColour(note_colour);
note_text_ctrl->Wrap(page_width - 2 * title_margin - FromDIP(10 + 16 + 8 + 10));
wxBoxSizer* note_sizer = new wxBoxSizer(wxHORIZONTAL);
note_sizer->Add(note_icon, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, FromDIP(10));
note_sizer->Add(note_text_ctrl, 1, wxALIGN_CENTRE_VERTICAL | wxALL, FromDIP(8));
note->SetSizer(note_sizer);
scrollable_panel_sizer->Add(note, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, title_margin);
auto key_parts = [](const Row& row) {
return std::visit(overloaded{
[](Shortcut shortcut) { return to_wx(wxGetApp().shortcuts().binding(shortcut).display_parts()); },
[](const FixedKey& fixed) { return fixed.keys; },
[](const MouseAction& mouse) { return std::vector<wxString>{ mouse.button }; },
}, row.content);
};
auto description = [](const Row& row) {
return std::visit(overloaded{
[](Shortcut shortcut) { return _(shortcut_info(shortcut).name); },
[](const FixedKey& fixed) { return _(fixed.description); },
[](const MouseAction& mouse) { return _(mouse_action(mouse.preference)); },
}, row.content);
};
auto icon_button = [&](const char* icon, const wxString& tooltip) {
auto button = new ScalableButton(scrollable_panel, wxID_ANY, icon);
button->SetBackgroundColour(page_colour);
button->SetToolTip(tooltip);
return button;
};
std::optional<ShortcutSection> section;
for (const Row& row : page.rows) {
if (section != row.section) {
auto heading = new StaticLine(scrollable_panel, false, _(section_name(row.section)));
heading->SetFont(Label::Head_14);
heading->SetForegroundColour(DESIGN_GRAY900_COLOR);
wxBoxSizer* heading_sizer = new wxBoxSizer(wxHORIZONTAL);
heading_sizer->AddSpacer(title_margin);
heading_sizer->Add(heading, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
heading_sizer->AddSpacer(title_margin);
scrollable_panel_sizer->Add(heading_sizer, 0, wxEXPAND | wxTOP, FromDIP(section.has_value() ? 10 : 6));
section = row.section;
}
auto desc = new wxStaticText(scrollable_panel, wxID_ANY, description(row));
desc->SetFont(Label::Body_14);
desc->SetForegroundColour(DESIGN_GRAY900_COLOR);
auto chord_label = [&](long style) {
auto label = new wxStaticText(scrollable_panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, style);
label->SetFont(Label::Head_14);
label->SetForegroundColour(DESIGN_GRAY900_COLOR);
return label;
};
wxStaticText* modifiers = chord_label(0);
wxStaticText* key = chord_label(wxALIGN_CENTRE_HORIZONTAL); // a single key is centred in its column
desc->Wrap(m_row_text_width - set_chord_labels(modifiers, key, key_parts(row)));
wxBoxSizer* buttons = new wxBoxSizer(wxHORIZONTAL);
if (const MouseAction* mouse = std::get_if<MouseAction>(&row.content)) {
auto settings = icon_button("settings", _L("Preferences"));
settings->Bind(wxEVT_BUTTON, [this, preference = mouse->preference](wxCommandEvent&) { open_mouse_preferences(preference); });
buttons->Add(settings, 0, wxALIGN_CENTRE_VERTICAL);
m_preference_rows.push_back({ mouse->preference, desc });
} else if (const Shortcut* editable = std::get_if<Shortcut>(&row.content)) {
const Shortcut shortcut = *editable;
auto change = icon_button("edit", _L("Edit"));
change->Bind(wxEVT_BUTTON, [this, shortcut](wxCommandEvent&) { edit_shortcut(shortcut); });
auto reset = icon_button("undo", _L("Reset"));
reset->Bind(wxEVT_BUTTON, [this, shortcut](wxCommandEvent&) { reset_shortcut(shortcut); });
reset->Show(wxGetApp().shortcuts().is_customized(shortcut));
buttons->Add(change, 0, wxALIGN_CENTRE_VERTICAL | wxRIGHT, FromDIP(6));
buttons->Add(reset, 0, wxALIGN_CENTRE_VERTICAL | wxRESERVE_SPACE_EVEN_IF_HIDDEN);
m_editable_rows.push_back({ shortcut, desc, modifiers, key, reset });
} else {
auto lock = new wxStaticBitmap(scrollable_panel, wxID_ANY, ScalableBitmap(scrollable_panel, "printer_status_lock", 16).bmp());
lock->SetToolTip(_L("Not customizable"));
buttons->Add((m_edit_size.x - lock->GetBestSize().x) / 2, m_edit_size.y); // centred under the edit icons, at their height
buttons->Add(lock, 0, wxALIGN_CENTRE_VERTICAL);
}
if (const int used = buttons->GetMinSize().x; used < m_buttons_width) // a box sizer recomputes its own min size, so pad it
buttons->AddSpacer(m_buttons_width - used);
wxBoxSizer* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->AddSpacer(row_margin);
row_sizer->Add(desc, 1, wxALIGN_CENTRE_VERTICAL);
row_sizer->AddSpacer(gap);
row_sizer->Add(modifiers, 0, wxALIGN_CENTRE_VERTICAL);
row_sizer->Add(key, 0, wxALIGN_CENTRE_VERTICAL);
row_sizer->Add(buttons, 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, gap);
row_sizer->AddSpacer(title_margin);
scrollable_panel_sizer->Add(row_sizer, 0, wxEXPAND | wxTOP, FromDIP(4));
}
scrollable_panel_sizer->Add(grid_sizer, 1, wxEXPAND | wxALL, FromDIP(20));
scrollable_panel_sizer->AddSpacer(title_margin);
scrollable_panel->SetSizer(scrollable_panel_sizer);
main_sizer->Add(scrollable_panel, 1, wxEXPAND);
@@ -367,5 +327,227 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const ShortcutsItem& s
return main_page;
}
void KBShortcutsDialog::edit_shortcut(Shortcut shortcut)
{
ShortcutCaptureDialog dlg(this, shortcut);
if (dlg.ShowModal() != wxID_OK)
return;
const wxString question = wxString::Format(_L("%s is assigned to %s. Reassign it to %s?"),
join_keys(to_wx(dlg.chord().display_parts())), shortcut_names(dlg.conflicts()), _(shortcut_info(shortcut).name));
if (!take_chord_from(shortcut, dlg.conflicts(), question))
return;
wxGetApp().shortcuts().bind(shortcut, dlg.chord());
apply_bindings();
}
void KBShortcutsDialog::reset_shortcut(Shortcut shortcut)
{
const std::vector<Shortcut> conflicts = wxGetApp().shortcuts().conflicts(shortcut, shortcut_info(shortcut).default_chord);
const wxString question = wxString::Format(_L("The default %s is assigned to %s. Reassign it to %s?"),
join_keys(to_wx(shortcut_info(shortcut).default_chord.display_parts())), shortcut_names(conflicts), _(shortcut_info(shortcut).name));
if (!take_chord_from(shortcut, conflicts, question))
return;
wxGetApp().shortcuts().reset(shortcut);
apply_bindings();
}
bool KBShortcutsDialog::take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question)
{
if (conflicts.empty())
return true;
MessageDialog confirm(this, question, _(shortcut_info(shortcut).name), wxICON_QUESTION | wxOK | wxCANCEL);
if (confirm.ShowModal() != wxID_OK)
return false;
for (Shortcut other : conflicts)
wxGetApp().shortcuts().bind(other, KeyChord{});
return true;
}
void KBShortcutsDialog::apply_bindings()
{
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
std::set<wxWindow*> pages;
for (const EditableRow& row : m_editable_rows) {
const int chord_width = set_chord_labels(row.modifiers, row.key, to_wx(shortcuts.binding(row.shortcut).display_parts()));
row.description->SetLabel(_(shortcut_info(row.shortcut).name));
row.description->Wrap(m_row_text_width - chord_width);
row.reset->Show(shortcuts.is_customized(row.shortcut));
pages.insert(row.key->GetParent());
}
for (wxWindow* page : pages)
page->Layout();
wxGetApp().on_shortcuts_changed();
}
int KBShortcutsDialog::set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts)
{
const wxString last = parts.empty() ? wxString() : parts.back();
if (!parts.empty())
parts.pop_back();
modifiers->SetLabel(parts.empty() ? wxString() : join_keys(parts) + " + ");
modifiers->Show(!parts.empty());
key->SetLabel(last);
const int key_width = last.length() == 1 ? m_key_slot : key->GetBestSize().x;
key->SetMinSize(wxSize(key_width, -1));
return (parts.empty() ? 0 : modifiers->GetBestSize().x) + key_width;
}
void KBShortcutsDialog::open_mouse_preferences(const char* preference)
{
// Opened from Preferences > Control, the settings are right behind this dialog.
if (auto preferences = dynamic_cast<PreferencesDialog*>(GetParent()); preferences != nullptr) {
// Runs once this dialog has closed and the focus is back in Preferences.
preferences->CallAfter([preferences, preference] { preferences->select_tab(PreferencesTab::Control, preference); });
EndModal(wxID_OK);
return;
}
wxGetApp().open_preferences(PreferencesTab::Control, preference);
// A language change rebuilds the main frame, taking this dialog with it.
if (GetParent() != wxGetApp().mainframe) {
EndModal(wxID_CANCEL);
return;
}
for (const PreferenceRow& row : m_preference_rows)
row.description->SetLabel(_(mouse_action(row.preference)));
}
ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut)
: DPIDialog(parent, wxID_ANY, _(shortcut_info(shortcut).name), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
, m_shortcut(shortcut)
{
SetBackgroundColour(*wxWHITE);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use.
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
const wxString advice = global_key_advice();
const wxString typing = _L("Global shortcuts also apply while typing.");
const wxString rule = _L("A key that types a character cannot be a global shortcut.");
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms.");
m_rejection = rule + "\n" + advice;
// Wide enough for each sentence on a line of its own where the translation allows, within limits.
int width = FromDIP(450);
for (const wxString& sentence : { typing, rule, advice }) {
int extent = 0;
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
width = std::max(width, extent);
}
width = std::min(width, FromDIP(550));
auto prompt = new Label(this, wxGetApp().normal_font(), wxString::Format(_L("Press the new shortcut for\n\"%s\""), _(shortcut_info(shortcut).name)), LB_AUTO_WRAP);
prompt->SetMinSize(wxSize(width, -1));
sizer->Add(prompt, 0, wxALL, FromDIP(20));
// Keyboard focus stays on this box so the buttons never receive the key presses.
const wxColour box_colour = StateColor::darkModeColorFor(*wxWHITE);
StaticBox* capture = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxSize(width, FromDIP(60)), wxWANTS_CHARS);
capture->SetCornerRadius(FromDIP(4));
capture->SetBorderColorNormal(StateColor::darkModeColorFor(wxColour("#009688"))); // the focused-input colour, since the box always has the focus
capture->SetBackgroundColorNormal(box_colour);
capture->SetBackgroundColour(box_colour);
wxBoxSizer* capture_sizer = new wxBoxSizer(wxVERTICAL);
m_chord_label = new wxStaticText(capture, wxID_ANY, join_keys(to_wx(wxGetApp().shortcuts().binding(shortcut).display_parts())));
m_chord_label->SetFont(::Label::Head_14);
m_chord_label->SetBackgroundColour(box_colour);
capture_sizer->AddStretchSpacer();
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
capture_sizer->AddStretchSpacer();
capture->SetSizer(capture_sizer);
capture->Bind(wxEVT_KEY_DOWN, &ShortcutCaptureDialog::on_key, this);
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
m_status = new Label(this, wxGetApp().normal_font(), m_hint, LB_AUTO_WRAP);
m_status->SetMinSize(wxSize(width, 3 * m_status->GetCharHeight())); // room for three lines, so the dialog keeps its size while keys are tried
m_status_colour = m_status->GetForegroundColour();
sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(20));
auto dlg_btns = new DialogButtons(this, {"Unbind", "OK", "Cancel"}, "", 1 /*left_aligned*/);
dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
m_chord = KeyChord{};
m_conflicts.clear();
EndModal(wxID_OK);
});
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
m_ok = dlg_btns->GetOK();
m_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); });
m_ok->Enable(false);
sizer->Add(dlg_btns, 0, wxEXPAND | wxTOP, FromDIP(10));
SetSizerAndFit(sizer);
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
capture->CallAfter([capture]() { capture->SetFocus(); });
}
void ShortcutCaptureDialog::on_dpi_changed(const wxRect& suggested_rect)
{
Layout();
Fit();
}
void ShortcutCaptureDialog::on_key(wxKeyEvent& evt)
{
if (!evt.HasAnyModifiers()) {
if (evt.GetKeyCode() == WXK_ESCAPE) {
EndModal(wxID_CANCEL);
return;
}
if (evt.GetKeyCode() == WXK_RETURN || evt.GetKeyCode() == WXK_NUMPAD_ENTER) {
if (m_ok->IsEnabled())
EndModal(wxID_OK);
return;
}
}
const KeyChord chord = KeyChord::from_event(evt);
if (!chord.valid())
return;
if (chord.needs_char_event()) {
evt.Skip();
return;
}
record(chord);
}
void ShortcutCaptureDialog::on_char(wxKeyEvent& evt)
{
const KeyChord chord = KeyChord::from_event(evt);
if (chord.is_punctuation())
record(chord);
}
void ShortcutCaptureDialog::record(const KeyChord& chord)
{
m_chord = chord;
m_chord_label->SetLabel(join_keys(to_wx(chord.display_parts())));
m_chord_label->GetParent()->Layout();
auto reject = [this](const wxString& reason) {
m_status->SetForegroundColour(ERROR_COLOUR);
m_status->SetLabel(reason);
m_conflicts.clear();
m_ok->Enable(false);
};
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
if (global && !chord.is_menu_accelerator()) {
reject(m_rejection);
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
reject(wxString::Format(_L("Already used as a step of %s."), _(shortcut_info(*owner).name)));
} else {
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
m_status->SetForegroundColour(m_status_colour);
if (m_conflicts.empty())
m_status->SetLabel(m_hint);
else
m_status->SetLabel(wxString::Format(_L("Already assigned to %s. Press OK to reassign it."), shortcut_names(m_conflicts)));
m_ok->Enable(true);
}
m_status->Refresh(); // a colour change alone does not repaint
Layout();
Fit();
}
} // namespace GUI
} // namespace Slic3r
+95 -28
View File
@@ -3,53 +3,120 @@
#include <wx/wx.h>
#include <map>
#include <variant>
#include <vector>
#include "GUI_Utils.hpp"
#include "Shortcuts.hpp"
#include "wxExtensions.hpp"
#include <wx/simplebook.h>
class Button;
class Label;
class TabCtrl;
namespace Slic3r {
namespace GUI {
class Select
{
public:
int m_index;
wxWindow *m_tab_button;
wxWindow *m_tab_text;
};
WX_DECLARE_HASH_MAP(int, Select *, wxIntegerHash, wxIntegerEqual, SelectHash);
// Lists every shortcut per context and lets the user rebind the assignable ones.
class KBShortcutsDialog : public DPIDialog
{
typedef std::pair<std::string, std::string> Shortcut;
typedef std::vector<Shortcut> Shortcuts;
typedef std::pair<std::pair<wxString, wxString>, Shortcuts> ShortcutsItem;
typedef std::vector<ShortcutsItem> ShortcutsVec;
// A key the user cannot rebind.
struct FixedKey
{
std::vector<wxString> keys; // modifier names and the key, shown joined with "+"
const char* description; // untranslated
};
// A mouse button whose camera action is chosen in Preferences.
struct MouseAction
{
wxString button;
const char* preference; // AppConfig key of the action
};
struct Row
{
std::variant<Shortcut, FixedKey, MouseAction> content;
ShortcutSection section;
};
struct Page
{
wxString title;
wxString caption; // when the page's keys apply
ShortcutContext context;
std::vector<Row> rows;
};
struct EditableRow
{
Shortcut shortcut;
wxStaticText* description;
wxStaticText* modifiers;
wxStaticText* key;
ScalableButton* reset;
};
struct PreferenceRow
{
const char* preference;
wxStaticText* description;
};
ShortcutsVec m_full_shortcuts;
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_header_bitmap;
std::vector<wxPanel*> m_pages;
std::vector<Page> m_pages;
std::vector<EditableRow> m_editable_rows;
std::vector<PreferenceRow> m_preference_rows;
// Row geometry, measured once and shared by every page.
wxSize m_edit_size; // an edit or reset icon
int m_buttons_width = 0; // every row's buttons column, so the right-aligned keys share an edge
int m_row_text_width = 0; // what a description and its chord share; the description wraps at the rest
int m_key_slot = 0; // width of the widest single key, the column single keys line up in
TabCtrl* m_tabs;
wxSimplebook* m_simplebook;
public:
KBShortcutsDialog();
wxWindow* create_button(int id, wxString text);
void OnSelectTabel(wxCommandEvent &event);
wxPanel *m_panel_selects;
wxBoxSizer *m_sizer_right;
wxSimplebook *m_simplebook;
wxBoxSizer * m_sizer_body;
SelectHash m_hash_selector;
KBShortcutsDialog(wxWindow* parent, ShortcutContext page); // opens on the page of that context
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void fill_shortcuts();
wxPanel* create_header(wxWindow* parent, const wxFont& bold_font);
wxPanel* create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font);
void fill_pages();
wxPanel* create_page(wxWindow* parent, const Page& page);
void edit_shortcut(Shortcut shortcut);
void reset_shortcut(Shortcut shortcut);
// Asks question before unbinding conflicts; false when the user declined.
bool take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question);
void apply_bindings(); // refreshes the rows and pushes the change to the rest of the app
// Puts a chord on a row's two labels, a single key in the shared column, and returns the width the chord takes.
int set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts);
void open_mouse_preferences(const char* preference);
};
// Records one key chord for a shortcut, warning about the shortcuts it would take the chord from.
class ShortcutCaptureDialog : public DPIDialog
{
public:
ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut);
// Valid after ShowModal() returned wxID_OK; an invalid chord means "unbind".
const KeyChord& chord() const { return m_chord; }
const std::vector<Shortcut>& conflicts() const { return m_conflicts; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void on_key(wxKeyEvent& evt);
void on_char(wxKeyEvent& evt);
void record(const KeyChord& chord);
Shortcut m_shortcut;
KeyChord m_chord;
std::vector<Shortcut> m_conflicts;
wxStaticText* m_chord_label;
wxString m_hint; // what m_status shows while there is nothing to warn about
wxString m_rejection; // what it shows for a key a Global shortcut cannot use
Label* m_status;
wxColour m_status_colour;
Button* m_ok;
};
} // namespace GUI
+314
View File
@@ -0,0 +1,314 @@
#include "KeyChord.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cctype>
namespace Slic3r { namespace GUI {
namespace {
constexpr int BINDABLE_MODIFIERS = wxMOD_CONTROL | wxMOD_SHIFT | wxMOD_ALT | wxMOD_RAW_CONTROL;
struct KeyName
{
int key;
const char* name; // canonical name, as wx parses it
const char* alias; // accepted when parsing; nullptr when there is none
const char* label; // translation key for KeyChord::display(); nullptr when name is it
};
constexpr std::array<KeyName, 15> special_keys{{
{ WXK_BACK, L_CONTEXT("Backspace", "Keyboard Shortcut"), "Back", nullptr },
{ WXK_TAB, L_CONTEXT("Tab", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_RETURN, L_CONTEXT("Enter", "Keyboard Shortcut"), "Return", nullptr },
{ WXK_ESCAPE, L_CONTEXT("Esc", "Keyboard Shortcut"), "Escape", nullptr },
{ WXK_SPACE, L_CONTEXT("Space", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_DELETE, L_CONTEXT("Del", "Keyboard Shortcut"), "Delete", nullptr },
{ WXK_INSERT, L_CONTEXT("Ins", "Keyboard Shortcut"), "Insert", nullptr },
{ WXK_HOME, L_CONTEXT("Home", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_END, L_CONTEXT("End", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_PAGEUP, L_CONTEXT("PgUp", "Keyboard Shortcut"), "PageUp", nullptr },
{ WXK_PAGEDOWN, L_CONTEXT("PgDn", "Keyboard Shortcut"), "PageDown", nullptr },
// Displayed as "Arrow Left" and so on, which is what the catalogs translate.
{ WXK_LEFT, "Left", nullptr, L_CONTEXT("Arrow Left", "Keyboard Shortcut") },
{ WXK_RIGHT, "Right", nullptr, L_CONTEXT("Arrow Right", "Keyboard Shortcut") },
{ WXK_UP, "Up", nullptr, L_CONTEXT("Arrow Up", "Keyboard Shortcut") },
{ WXK_DOWN, "Down", nullptr, L_CONTEXT("Arrow Down", "Keyboard Shortcut") },
}};
bool equals_ignoring_case(const std::string& a, const char* b)
{
if (b == nullptr)
return false;
size_t i = 0;
for (; i < a.size() && b[i] != '\0'; ++i)
if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i])))
return false;
return i == a.size() && b[i] == '\0';
}
bool is_letter(int key) { return key >= 'A' && key <= 'Z'; }
bool is_digit(int key) { return key >= '0' && key <= '9'; }
bool is_printable(int key) { return key > ' ' && key < 127; }
bool is_symbol(int key) { return is_printable(key) && !std::isalnum(key); }
bool is_function_key(int key) { return key >= WXK_F1 && key <= WXK_F24; }
bool is_special(int key)
{
if (is_function_key(key))
return true;
return std::any_of(special_keys.begin(), special_keys.end(), [key](const KeyName& k) { return k.key == key; });
}
std::string special_key_name(int key)
{
if (is_function_key(key))
return "F" + std::to_string(key - WXK_F1 + 1);
for (const KeyName& k : special_keys)
if (k.key == key)
return k.name;
return {};
}
std::string special_key_label(int key)
{
for (const KeyName& k : special_keys)
if (k.key == key)
return _u8L_CONTEXT(k.label != nullptr ? k.label : k.name, "Keyboard Shortcut");
return special_key_name(key);
}
int parse_key(const std::string& text)
{
if (text.size() == 1) {
const int key = static_cast<unsigned char>(text[0]);
return is_printable(key) ? std::toupper(key) : WXK_NONE;
}
for (const KeyName& k : special_keys)
if (equals_ignoring_case(text, k.name) || equals_ignoring_case(text, k.alias))
return k.key;
if ((text[0] == 'F' || text[0] == 'f') && text.size() <= 3 && std::all_of(text.begin() + 1, text.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); })) {
const int n = std::stoi(text.substr(1));
if (n >= 1 && n <= 24)
return WXK_F1 + n - 1;
}
return WXK_NONE;
}
int parse_modifier(const std::string& text)
{
if (equals_ignoring_case(text, "Ctrl") || equals_ignoring_case(text, "Control") || equals_ignoring_case(text, "Cmd") || equals_ignoring_case(text, "Command"))
return wxMOD_CONTROL;
if (equals_ignoring_case(text, "Shift"))
return wxMOD_SHIFT;
if (equals_ignoring_case(text, "Alt") || equals_ignoring_case(text, "Option"))
return wxMOD_ALT;
if (equals_ignoring_case(text, "RawCtrl"))
return wxMOD_RAW_CONTROL;
return wxMOD_NONE;
}
// Numpad keys act as their main-keyboard counterparts, so one binding covers both.
int fold_numpad(int key)
{
if (key >= WXK_NUMPAD0 && key <= WXK_NUMPAD9)
return '0' + (key - WXK_NUMPAD0);
switch (key) {
case WXK_NUMPAD_ENTER: return WXK_RETURN;
case WXK_NUMPAD_SPACE: return WXK_SPACE;
case WXK_NUMPAD_TAB: return WXK_TAB;
case WXK_NUMPAD_HOME: return WXK_HOME;
case WXK_NUMPAD_END: return WXK_END;
case WXK_NUMPAD_PAGEUP: return WXK_PAGEUP;
case WXK_NUMPAD_PAGEDOWN: return WXK_PAGEDOWN;
case WXK_NUMPAD_LEFT: return WXK_LEFT;
case WXK_NUMPAD_RIGHT: return WXK_RIGHT;
case WXK_NUMPAD_UP: return WXK_UP;
case WXK_NUMPAD_DOWN: return WXK_DOWN;
case WXK_NUMPAD_INSERT: return WXK_INSERT;
case WXK_NUMPAD_DELETE: return WXK_DELETE;
case WXK_NUMPAD_ADD: return '+';
case WXK_NUMPAD_SUBTRACT: return '-';
case WXK_NUMPAD_MULTIPLY: return '*';
case WXK_NUMPAD_DIVIDE: return '/';
case WXK_NUMPAD_DECIMAL: return '.';
case WXK_NUMPAD_EQUAL: return '=';
default: return key;
}
}
// Modifiers in the order the text forms list them; wxMOD_RAW_CONTROL is wxMOD_CONTROL off macOS.
#ifdef __APPLE__
constexpr std::array<int, 4> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT, wxMOD_RAW_CONTROL };
#else
constexpr std::array<int, 3> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT };
#endif
const char* canonical_modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return "Ctrl+";
if (modifier == wxMOD_SHIFT)
return "Shift+";
if (modifier == wxMOD_ALT)
return "Alt+";
return "RawCtrl+";
}
template<typename Prefix>
std::string join_modifiers(int modifiers, Prefix prefix)
{
std::string out;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
out += prefix(modifier);
return out;
}
} // namespace
bool KeyChord::is_punctuation() const { return is_symbol(key) && modifiers == wxMOD_NONE; }
bool KeyChord::needs_char_event() const { return is_symbol(key) && (modifiers & ~wxMOD_SHIFT) == 0; }
bool KeyChord::is_menu_accelerator() const
{
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
}
std::string KeyChord::to_string() const
{
if (!valid())
return {};
std::string out = join_modifiers(modifiers, canonical_modifier_prefix);
if (is_printable(key))
out += char(key);
else
out += special_key_name(key);
return out;
}
std::optional<KeyChord> KeyChord::parse(const std::string& text)
{
if (text.empty())
return std::nullopt;
// The key is whatever follows the last separator; a trailing '+' is the '+' key itself.
size_t key_start = text.size() - 1;
if (text.back() != '+') {
const size_t sep = text.rfind('+');
key_start = sep == std::string::npos ? 0 : sep + 1;
}
KeyChord chord;
chord.key = parse_key(text.substr(key_start));
if (chord.key == WXK_NONE)
return std::nullopt;
const std::string prefix = key_start == 0 ? std::string() : text.substr(0, key_start - 1);
size_t begin = 0;
while (begin < prefix.size()) {
size_t end = prefix.find('+', begin);
if (end == std::string::npos)
end = prefix.size();
const int modifier = parse_modifier(prefix.substr(begin, end - begin));
if (modifier == wxMOD_NONE)
return std::nullopt;
chord.modifiers |= modifier;
begin = end + 1;
}
if (is_letter(chord.key) || is_digit(chord.key) || !is_printable(chord.key) || chord.modifiers == wxMOD_NONE)
return chord;
// Shift is folded into the character for punctuation, so "Shift+/" is not accepted.
return (chord.modifiers & wxMOD_SHIFT) ? std::nullopt : std::optional<KeyChord>(chord);
}
std::string KeyChord::display() const
{
std::string out;
for (const std::string& part : display_parts())
out += (out.empty() ? "" : "+") + part;
return out;
}
std::vector<std::string> KeyChord::display_parts() const
{
std::vector<std::string> parts;
if (!valid())
return parts;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
parts.push_back(modifier_name(modifier));
parts.push_back(is_printable(key) ? std::string(1, char(key)) : special_key_label(key));
return parts;
}
std::string KeyChord::modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return shortkey_ctrl_prefix();
if (modifier == wxMOD_SHIFT)
return _u8L("Shift+");
if (modifier == wxMOD_ALT)
return shortkey_alt_prefix();
#ifdef __APPLE__
if (modifier == wxMOD_RAW_CONTROL)
return u8"⌃+";
#endif
return {};
}
// The catalogue holds the "Ctrl+" prefixes, so the bare name is the prefix without its "+"
// and any space before it ("Strg +" in German).
std::string KeyChord::modifier_name(int modifier)
{
std::string name = modifier_prefix(modifier);
if (!name.empty() && name.back() == '+')
name.pop_back();
while (!name.empty() && name.back() == ' ')
name.pop_back();
return name;
}
wxAcceleratorEntry KeyChord::to_accelerator_entry(int command) const
{
int flags = wxACCEL_NORMAL;
if (modifiers & wxMOD_CONTROL)
flags |= wxACCEL_CTRL;
if (modifiers & wxMOD_SHIFT)
flags |= wxACCEL_SHIFT;
if (modifiers & wxMOD_ALT)
flags |= wxACCEL_ALT;
#ifdef __APPLE__
if (modifiers & wxMOD_RAW_CONTROL)
flags |= wxACCEL_RAW_CTRL;
#endif
return wxAcceleratorEntry(flags, key, command);
}
KeyChord KeyChord::from_event(const wxKeyEvent& evt)
{
KeyChord chord;
chord.modifiers = evt.GetModifiers() & BINDABLE_MODIFIERS;
int key = fold_numpad(evt.GetKeyCode());
if (evt.GetEventType() == wxEVT_CHAR) {
if (key >= 1 && key <= 26 && evt.ControlDown())
key = 'A' + key - 1; // Ctrl+letter arrives as the control character
else if (is_symbol(key))
chord.modifiers &= ~wxMOD_SHIFT; // the character already reflects Shift
}
if (key >= 'a' && key <= 'z')
key -= 'a' - 'A';
if (is_printable(key) || is_special(key))
chord.key = key;
return chord;
}
}} // namespace Slic3r::GUI
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <wx/accel.h>
#include <wx/defs.h>
#include <functional>
#include <optional>
#include <string>
#include <vector>
class wxKeyEvent;
namespace Slic3r { namespace GUI {
// One key press: a key code as wxEVT_KEY_DOWN reports it (letters upper-case, numpad keys
// folded onto their main-keyboard equivalents) plus the wxMOD_* modifiers held with it.
// Printable punctuation is stored as the character it produces, so "+" means the key that
// types "+" on the user's layout.
struct KeyChord
{
int key = WXK_NONE;
int modifiers = wxMOD_NONE;
bool valid() const { return key != WXK_NONE; }
bool operator==(const KeyChord& other) const { return key == other.key && modifiers == other.modifiers; }
bool operator!=(const KeyChord& other) const { return !(*this == other); }
// Bare printable keys other than letters and digits are matched on wxEVT_CHAR, because
// only the char event knows which character a key produces under the active layout.
bool is_punctuation() const;
// True for a printable non-alphanumeric key pressed with nothing but Shift, which only the
// char event that follows can resolve.
bool needs_char_event() const;
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields.
bool is_menu_accelerator() const;
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
std::string to_string() const;
static std::optional<KeyChord> parse(const std::string& text);
// Text for menus, tooltips and the shortcuts dialog, with translated modifier names and the
// command and option glyphs on macOS.
std::string display() const;
// The pieces display() joins with "+": the modifier names, then the key name.
std::vector<std::string> display_parts() const;
// Translated text of one wxMOD_* modifier, as a "Ctrl+" prefix or the bare "Ctrl" name.
static std::string modifier_prefix(int modifier);
static std::string modifier_name(int modifier);
wxAcceleratorEntry to_accelerator_entry(int command) const;
// Builds the chord a key event describes, or an invalid chord for pure modifier presses and
// keys outside the bindable set. wxEVT_CHAR events are normalized to the key codes
// wxEVT_KEY_DOWN reports for letters, digits and special keys.
static KeyChord from_event(const wxKeyEvent& evt);
};
struct KeyChordHash
{
size_t operator()(const KeyChord& chord) const { return std::hash<long long>()((static_cast<long long>(chord.modifiers) << 32) | unsigned(chord.key)); }
};
}} // namespace Slic3r::GUI
+175 -142
View File
@@ -56,6 +56,7 @@
#include "../Utils/PrintHost.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "UnsavedChangesDialog.hpp"
#include "PublishSettingsDialog.hpp"
#include "MsgDialog.hpp"
@@ -310,17 +311,6 @@ static wxIcon main_frame_icon(GUI_App::EAppMode app_mode)
wxDEFINE_EVENT(EVT_SYNC_CLOUD_PRESET, SimpleEvent);
#ifdef __APPLE__
static const wxString ctrl = ("Ctrl+");
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const std::string ctrl_t = u8"\u2318+"; // "⌘" (Mac Command)
#else
static const wxString ctrl = _L("Ctrl+");
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
static const wxString ctrl_t = ctrl;
#endif
static const wxString shift = _L("Shift+");
MainFrame::MainFrame() :
DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_STYLE, "mainframe")
, m_printhost_queue_dlg(new PrintHostQueueDialog(this))
@@ -730,69 +720,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#endif
// Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so
// editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window
// doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields),
// so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference
// (default on) so users can hand Space back to the focused control entirely.
if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() &&
!evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) {
if (focus_keeps_space(wxWindow::FindFocus())) {
evt.Skip(); // let the focused control keep Space
return;
}
// Defer out of the native key-event stack: open_speed_dial() may create a WebView and
// run script, the same window work the codebase avoids doing on native callbacks.
this->CallAfter([] { wxGetApp().open_speed_dial(); });
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable);
if (m_print_enable) {
if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"))
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
}
if (!handle_global_shortcut(KeyChord::from_event(evt)))
evt.Skip();
return;
}
else if (evt.CmdDown() && evt.GetKeyCode() == 'G') { if (can_export_gcode()) { wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); } evt.Skip(); return; }
if (evt.CmdDown() && evt.GetKeyCode() == 'J') { m_printhost_queue_dlg->Show(); return; }
if (evt.CmdDown() && evt.GetKeyCode() == 'N') { m_plater->new_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'O') { m_plater->load_project(); return;}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;}
else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'F') {
if (m_plater && is_prepare_or_preview_tab()) {
m_plater->sidebar().can_search();
}
}
#ifdef __APPLE__
if (evt.CmdDown() && evt.GetKeyCode() == ',')
#else
if (evt.CmdDown() && evt.GetKeyCode() == 'P')
#endif
{
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_preferences();
plater()->get_current_canvas3D()->force_set_focus();
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'I' && !evt.ShiftDown()) {
if (!can_add_models()) return;
if (m_plater) { m_plater->add_file(); }
return;
}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
if (can_export_model()) publish_project();
return;
}
evt.Skip();
});
Bind(wxEVT_SHOW, [](wxShowEvent &evt) {
@@ -813,6 +742,96 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
bind_diff_dialog();
}
bool MainFrame::handle_global_shortcut(const KeyChord& chord)
{
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Global, chord);
if (!shortcut.has_value())
return false;
switch (*shortcut) {
case Shortcut::SlicePlate:
if (m_slice_enable) {
wxGetApp().plater()->update(true, true);
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
break;
case Shortcut::PrintPlate:
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable);
if (m_print_enable) {
if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"))
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
}
return false;
case Shortcut::ExportSlicedFile:
if (can_export_gcode())
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE));
return false;
case Shortcut::PrintHostQueue: m_printhost_queue_dlg->Show(); break;
case Shortcut::SpeedDial:
if (!wxGetApp().app_config->get_bool("enable_speed_dial") || (chord == KeyChord{ WXK_SPACE } && focus_keeps_space(wxWindow::FindFocus())))
return false;
// Deferred out of the native key-event stack: open_speed_dial() may create a WebView and run script.
CallAfter([] { wxGetApp().open_speed_dial(); });
break;
case Shortcut::NewProject: m_plater->new_project(); break;
case Shortcut::OpenProject: m_plater->load_project(); break;
case Shortcut::SaveProjectAs:
if (can_save_as())
m_plater->save_project(true);
break;
case Shortcut::SaveProject:
if (can_save())
m_plater->save_project();
break;
case Shortcut::Search:
if (m_plater && is_prepare_or_preview_tab())
m_plater->sidebar().can_search();
return false;
case Shortcut::Preferences:
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_preferences();
plater()->get_current_canvas3D()->force_set_focus();
break;
case Shortcut::ImportModel:
if (can_add_models() && m_plater)
m_plater->add_file();
break;
case Shortcut::Publish3mf:
if (can_export_model())
publish_project();
break;
case Shortcut::ShowLabels:
if (m_plater && m_plater->is_view3D_shown()) {
m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown());
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}
break;
case Shortcut::ViewDefault:
if (m_plater) {
select_view("plate");
m_plater->get_current_canvas3D()->zoom_to_bed();
}
break;
case Shortcut::ViewTop: select_view("top"); break;
case Shortcut::ViewBottom: select_view("bottom"); break;
case Shortcut::ViewFront: select_view("front"); break;
case Shortcut::ViewRear: select_view("rear"); break;
case Shortcut::ViewLeft: select_view("left"); break;
case Shortcut::ViewRight: select_view("right"); break;
case Shortcut::ViewPlate:
if (m_plater)
m_plater->get_current_canvas3D()->select_plate();
break;
default: return false;
}
return true;
}
void MainFrame::bind_diff_dialog()
{
auto get_tab = [](Preset::Type type) {
@@ -1170,6 +1189,8 @@ void MainFrame::shutdown()
if (m_project != nullptr)
m_project->shutdown();
m_plugin_pages.shutdown();
if (m_plater != nullptr)
m_plater->remove_dock_panes();
#ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr;
@@ -2766,13 +2787,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 +2893,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 +2933,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 +2970,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 +2994,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 +3009,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 +3047,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 +3097,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 +3141,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 +3153,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 +3166,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 +3178,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 +3190,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 +3203,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 +3223,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 +3246,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 +3298,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 +3329,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 +3364,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 +3412,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,17 +3420,12 @@ 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", "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
//parent_menu->Insert(1, preference_item);
#endif
// Help menu
@@ -3406,8 +3439,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 +3450,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 +3557,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 +3761,7 @@ void MainFrame::init_menubar_as_gcodeviewer()
wxMenu* viewMenu = nullptr;
if (m_plater != nullptr) {
viewMenu = new wxMenu();
add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this));
add_common_view_menu_items(viewMenu, std::bind(&MainFrame::can_change_view, this));
}
// helpmenu
+26
View File
@@ -74,6 +74,8 @@ class DesignPanel;
class MainFrame;
class WebViewPanel;
class ParamsDialog;
enum class Shortcut : uint8_t;
struct KeyChord;
#ifdef __WXGTK__
class ResizeEdgePanel;
#endif
@@ -195,6 +197,29 @@ class MainFrame : public DPIFrame
// vector of a MenuBar items changeable in respect to printer technology
std::vector<wxMenuItem*> m_changeable_menu_items;
// Menu items whose label shows a key binding; update_shortcut_labels() rewrites them.
struct ShortcutMenuItem
{
wxMenuItem* item;
Shortcut shortcut;
wxString label;
bool accelerator; // false keeps the binding display-only on macOS, where the menu bar's accelerators are live
};
std::vector<ShortcutMenuItem> m_shortcut_menu_items;
wxString shortcut_label(const wxString& label, Shortcut shortcut, bool accelerator);
template<typename... Args>
wxMenuItem* append_shortcut_item(wxMenu* menu, Shortcut shortcut, bool accelerator, const wxString& label, Args&&... args)
{
wxMenuItem* item = append_menu_item(menu, wxID_ANY, shortcut_label(label, shortcut, accelerator), std::forward<Args>(args)...);
m_shortcut_menu_items.push_back({ item, shortcut, label, accelerator });
return item;
}
// Runs the Global shortcut bound to chord; false when the focused control should see the key as well.
bool handle_global_shortcut(const KeyChord& chord);
void add_common_view_menu_items(wxMenu* view_menu, std::function<bool(void)> can_change_view);
wxMenu* generate_help_menu();
struct FileHistory : wxFileHistory
{
FileHistory(int max) : wxFileHistory(max) {}
@@ -354,6 +379,7 @@ public:
void request_select_tab(const wxString& id);
int get_calibration_curr_tab();
void select_view(const std::string& direction);
void update_shortcut_labels();
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
void on_config_changed(DynamicPrintConfig* cfg) const ;
void set_print_button_to_default(PrintSelectType select_type);
+3 -3
View File
@@ -261,7 +261,7 @@ void MonitorPanel::msw_rescale()
void MonitorPanel::select_machine(std::string machine_sn)
{
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn;
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn;
wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED);
event->SetString(machine_sn);
wxQueueEvent(this, event);
@@ -280,7 +280,7 @@ void MonitorPanel::on_select_printer(wxCommandEvent& event)
{
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
const std::string requested_dev_id = event.GetString().ToStdString();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id="
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id="
<< requested_dev_id << " device_manager=" << (dev ? "set" : "null");
if (!dev) return;
@@ -289,7 +289,7 @@ void MonitorPanel::on_select_printer(wxCommandEvent& event)
}
const bool selected = dev->set_selected_machine(requested_dev_id);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer set_selected_machine result="
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer set_selected_machine result="
<< selected << " selected_dev_id="
<< (dev->get_selected_machine() ? dev->get_selected_machine()->get_dev_id() : "<null>");
if (!selected)
+1 -14
View File
@@ -3,7 +3,6 @@
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "DeviceCore/DevConfigUtil.h"
namespace Slic3r {
namespace GUI {
@@ -52,7 +51,7 @@ void DeviceItem::sync_state()
state_printable = 6;
}
if (is_blocking_printing(obj_)) {
if (wxGetApp().is_blocking_printing(obj_)) {
state_printable = 5;
}
@@ -105,18 +104,6 @@ void DeviceItem::unselected()
}
}
bool DeviceItem::is_blocking_printing(MachineObject* obj_)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
std::string source_model = "";
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
void DeviceItem::update_item(const DeviceItem* item)
{
// Except for the selected status, everything else is updated
-1
View File
@@ -59,7 +59,6 @@ public:
void selected();
void unselected();
bool is_blocking_printing(MachineObject* obj_);
void update_item(const DeviceItem* item);
};
+2 -1
View File
@@ -10,6 +10,7 @@
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "I18N.hpp"
#include "Shortcuts.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "NetworkTestDialog.hpp"
@@ -563,7 +564,7 @@ std::vector<NativeCommand> build_command_catalog()
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
wxGetApp().keyboard_shortcuts();
wxGetApp().keyboard_shortcuts(ShortcutContext::Global);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
+150 -6
View File
@@ -82,10 +82,12 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "GUI_ObjectList.hpp"
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
#endif
#include "AuiPaneLayout.hpp"
#include "GUI_Utils.hpp"
#include "GUI_Factories.hpp"
#include "wxExtensions.hpp"
@@ -6830,6 +6832,14 @@ struct Plater::priv
// GUI elements
AuiMgr m_aui_mgr;
// Live dock panes. `on_close` runs when the user closes one from its close button; `shown` is
// what the owner asked for.
struct DockPane
{
std::function<void()> on_close;
bool shown{true};
};
std::map<wxWindow*, DockPane> m_dock_panes;
wxString m_default_window_layout;
wxPanel* current_panel{ nullptr };
std::vector<wxPanel*> panels;
@@ -6910,6 +6920,7 @@ struct Plater::priv
bool show_render_statistic_dialog{ false };
bool show_wireframe{ false };
bool wireframe_enabled{ true };
bool show_xray{ false };
static const std::regex pattern_bundle;
static const std::regex pattern_3mf;
@@ -7001,6 +7012,11 @@ struct Plater::priv
void update_sidebar(bool force_update = false);
void reset_window_layout();
Sidebar::DockingState get_sidebar_docking_state();
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close);
void remove_dock_pane(wxWindow* window);
void show_dock_pane(wxWindow* window, bool show);
bool dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const;
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
@@ -7581,6 +7597,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
panel_3d->SetSizer(panel_sizer);
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) {
const wxAuiPaneInfo* pane = evt.GetPane();
auto it = pane != nullptr ? m_dock_panes.find(pane->window) : m_dock_panes.end();
if (it != m_dock_panes.end()) {
const std::function<void()> on_close = std::move(it->second.on_close);
m_dock_panes.erase(it);
if (on_close)
on_close();
}
evt.Skip();
});
m_default_window_layout = m_aui_mgr.SavePerspective();
{
@@ -7677,7 +7705,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(); });
@@ -7754,7 +7784,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();
@@ -8215,10 +8245,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);
@@ -8247,6 +8274,14 @@ void Plater::priv::update_sidebar(bool force_update) {
}
}
for (const auto& [window, dock_pane] : m_dock_panes) {
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (pane.IsOk() && pane.IsShown() != dock_pane_visible(dock_pane, pane)) {
pane.Show(!pane.IsShown());
needs_update = true;
}
}
if (needs_update) {
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
m_aui_mgr.Update();
@@ -8256,10 +8291,96 @@ void Plater::priv::update_sidebar(bool force_update) {
void Plater::priv::reset_window_layout()
{
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
// Loading a layout docks and hides every pane it does not list, and the default layout lists no
// dock panes: a floating dock pane is docked again, like the rest of the window.
for (const auto& [window, dock_pane] : m_dock_panes)
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
pane.Show(dock_pane_visible(dock_pane, pane));
sidebar_layout.is_collapsed = false;
update_sidebar(true);
}
bool Plater::priv::dock_pane_visible(const DockPane& dock_pane, const wxAuiPaneInfo& pane) const
{
// A floating pane is a top-level window, so it does not hide with the Plater on other tabs.
return dock_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
}
void Plater::priv::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close)
{
const wxString base_name = wxString::FromUTF8(name);
wxString unique_name = base_name;
for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i)
unique_name = base_name + wxString::Format("#%d", i);
// A restored layout below already holds pixels.
const wxSize pixels = q->FromDIP(size);
wxAuiPaneInfo info;
info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true);
if (dock == "left")
info.Left();
else if (dock == "bottom")
info.Bottom();
else
info.Right();
if (dock == "float")
info.Float();
// Put the pane back where it was the last time the window layout was saved with it open.
const std::string saved = aui_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string());
if (!saved.empty()) {
m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info);
info.Caption(caption).DestroyOnClose(true).Show();
}
// Floating is disabled on Wayland.
if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) {
info.Dock().Floatable(false);
if (info.dock_direction == wxAUI_DOCK_NONE)
info.Right();
}
const DockPane& dock_pane = m_dock_panes[window] = DockPane{std::move(on_close)};
info.Show(dock_pane_visible(dock_pane, info));
m_aui_mgr.AddPane(window, info);
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
// does, for the saved layout.
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
}
evt.Skip();
});
m_aui_mgr.Update();
}
void Plater::priv::remove_dock_pane(wxWindow* window)
{
m_dock_panes.erase(window);
if (m_aui_mgr.DetachPane(window))
m_aui_mgr.Update();
window->Destroy();
}
void Plater::priv::show_dock_pane(wxWindow* window, bool show)
{
const auto it = m_dock_panes.find(window);
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
if (it == m_dock_panes.end() || !pane.IsOk())
return;
it->second.shown = show;
if (pane.IsShown() == dock_pane_visible(it->second, pane))
return;
pane.Show(!pane.IsShown());
m_aui_mgr.Update();
}
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
if (!sidebar_layout.is_enabled) {
return Sidebar::None;
@@ -17854,6 +17975,19 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
void Plater::reset_window_layout() { p->reset_window_layout(); }
void Plater::add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close)
{
p->add_dock_pane(window, name, caption, dock, size, std::move(on_close));
}
void Plater::remove_dock_pane(wxWindow* window) { p->remove_dock_pane(window); }
void Plater::remove_dock_panes()
{
while (!p->m_dock_panes.empty())
p->remove_dock_pane(p->m_dock_panes.begin()->first);
}
void Plater::show_dock_pane(wxWindow* window, bool show) { p->show_dock_pane(window, show); }
//BBS
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
@@ -22493,6 +22627,16 @@ bool Plater::is_wireframe_enabled() const
return p->wireframe_enabled;
}
void Plater::toggle_show_xray()
{
p->show_xray = !p->show_xray;
}
bool Plater::is_show_xray() const
{
return p->show_xray;
}
/*Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name)
: TakeSnapshot(plater, from_u8(snapshot_name)) {}
+14
View File
@@ -478,6 +478,17 @@ public:
void reset_window_layout();
// Dock panes sit alongside the sidebar; `window` must be a child of the Plater. `dock` is
// "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close
// button is destroyed after on_close runs; remove_dock_pane() destroys it without calling on_close.
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
const wxSize& size, std::function<void()> on_close);
void remove_dock_pane(wxWindow* window);
void show_dock_pane(wxWindow* window, bool show);
// Removes every dock pane without calling on_close, for MainFrame::shutdown() (app exit and a
// language switch), while the Plater and any floating frames still exist.
void remove_dock_panes();
// Called after the Preferences dialog is closed and the program settings are saved.
// Update the UI based on the current preferences.
void update_ui_from_settings();
@@ -959,6 +970,9 @@ public:
void enable_wireframe(bool status);
bool is_wireframe_enabled() const;
void toggle_show_xray();
bool is_show_xray() const;
// Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu.
bool PopupMenu(wxMenu *menu, const wxPoint& pos = wxDefaultPosition);
bool PopupMenu(wxMenu *menu, int x, int y) { return this->PopupMenu(menu, wxPoint(x, y)); }
+1 -1
View File
@@ -171,7 +171,7 @@ public:
void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback);
static ::std::string get_print_status_info(PrintDialogStatus status);
wxString get_pre_state_msg(PrintDialogStatus status);
static wxString get_pre_state_msg(PrintDialogStatus status);
static bool is_error(PrintDialogStatus status) { return (PrintStatusErrorBegin < status) && (PrintStatusErrorEnd > status); };
static bool is_error_printer(PrintDialogStatus status) { return (PrintStatusPrinterErrorBegin < status) && (PrintStatusPrinterErrorEnd > status); };
static bool is_error_filament(PrintDialogStatus status) { return (PrintStatusFilamentErrorBegin < status) && (PrintStatusFilamentErrorEnd > status); };
+29 -6
View File
@@ -18,6 +18,7 @@
#include "NetworkTestDialog.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/RadioGroup.hpp"
#include "Shortcuts.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "NetworkPluginDialog.hpp"
@@ -285,6 +286,7 @@ std::tuple<wxBoxSizer*, ComboBox*> PreferencesDialog::create_item_combobox_base(
auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY);
combobox->GetDropDown().SetUseContentWidth(true);
combobox->SetToolTip(tip);
combobox->SetName(param); // select_tab() finds the row by this name
std::vector<wxString>::iterator iter;
for (iter = vlist.begin(); iter != vlist.end(); iter++) {
@@ -1522,6 +1524,19 @@ PreferencesDialog::~PreferencesDialog()
{
}
void PreferencesDialog::select_tab(PreferencesTab tab, const std::string& option)
{
if (const auto index = m_tab_index.find(tab); index != m_tab_index.end())
m_pref_tabs->SelectItem(index->second);
wxWindow* control = option.empty() ? nullptr : m_parent->FindWindow(wxString(option));
if (control == nullptr)
return;
int unit = 1;
m_parent->GetScrollPixelsPerUnit(nullptr, &unit);
m_parent->Scroll(wxDefaultCoord, (m_parent->CalcUnscrolledPosition(control->GetPosition()).y - FromDIP(10)) / unit);
control->SetFocus(); // the focused tint marks the row
}
void PreferencesDialog::on_dpi_changed(const wxRect &suggested_rect) {
m_pref_tabs->Rescale();
@@ -1599,7 +1614,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// GENERAL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("General"));
m_tab_index[PreferencesTab::General] = m_pref_tabs->AppendItem(_L("General"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -1730,8 +1745,8 @@ void PreferencesDialog::create_items()
auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)"));
g_sizer->Add(item_multi_machine);
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"),
_L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."),
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial from the keyboard"),
_L("When enabled, the Speed Dial keyboard shortcut (Space by default) opens the action search from any page."),
"enable_speed_dial");
g_sizer->Add(item_speed_dial);
@@ -1789,7 +1804,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// CONTROL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Control"));
m_tab_index[PreferencesTab::Control] = m_pref_tabs->AppendItem(_L("Control"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -1859,6 +1874,14 @@ void PreferencesDialog::create_items()
auto item_right_mouse_drag = create_item_combobox(_L("Right Mouse Drag"), _L("Set the action that dragging the right mouse button should perform."), "right_mouse_drag_action", ButtonDragActions);
g_sizer->Add(item_right_mouse_drag);
//// CONTROL > Keyboard
g_sizer->Add(create_item_title(_L("Keyboard")), 1, wxEXPAND);
auto item_shortcuts = create_item_button(_L("Keyboard shortcuts"), _L("Edit") + dots, "", _L("Choose the key for each action."), [this]() {
wxGetApp().keyboard_shortcuts(ShortcutContext::Global, this);
});
g_sizer->Add(item_shortcuts);
//// CONTROL > Clear my choice on ...
g_sizer->Add(create_item_title(_L("Clear my choice on...")), 1, wxEXPAND);
@@ -1883,7 +1906,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// GRAPHICS TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Graphics"));
m_tab_index[PreferencesTab::Graphics] = m_pref_tabs->AppendItem(_L("Graphics"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
@@ -2031,7 +2054,7 @@ void PreferencesDialog::create_items()
//////////////////////////
//// ONLINE TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Online"));
m_tab_index[PreferencesTab::Online] = m_pref_tabs->AppendItem(_L("Online"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
g_sizer = f_sizers.back();
g_sizer->AddGrowableCol(0, 1);
+6
View File
@@ -29,6 +29,9 @@ namespace Slic3r { namespace GUI {
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
#define DESIGN_LEFT_MARGIN 25
// The tabs other dialogs open Preferences on.
enum class PreferencesTab { General, Control, Graphics, Online };
class PreferencesDialog : public DPIDialog
{
private:
@@ -38,6 +41,7 @@ protected:
wxBoxSizer * m_sizer_body;
wxScrolledWindow* m_parent;
TabCtrl* m_pref_tabs;
std::map<PreferencesTab, int> m_tab_index; // position of each tab in m_pref_tabs
// bool m_settings_layout_changed {false};
bool m_seq_top_layer_only_changed{false};
@@ -60,6 +64,8 @@ public:
~PreferencesDialog();
void select_tab(PreferencesTab tab, const std::string& option = {}); // and scrolls a combobox option's row into view, focused
wxString m_backup_interval_time;
wxTimer m_filament_height_timer;
+6 -16
View File
@@ -2520,23 +2520,13 @@ void SelectMachineDialog::on_cancel(wxCloseEvent &event)
bool SelectMachineDialog::is_blocking_printing(MachineObject* obj_)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
auto target_model = obj_->printer_type;
std::string source_model = "";
if (m_print_type == PrintFromType::FROM_NORMAL)
return wxGetApp().is_blocking_printing(obj_);
if (m_print_type == PrintFromType::FROM_NORMAL) {
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
}else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) {
if (m_required_data_plate_data_list.size() > 0) {
source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
}
}
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
std::string source_model;
if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty())
source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
return wxGetApp().is_blocking_printing(obj_, source_model);
}
static std::unordered_set<int> _get_used_nozzle_idxes()
+1 -1
View File
@@ -738,7 +738,7 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
if (obj && obj->is_online() && !obj->can_abort() && !obj->is_in_upgrading() && it->second->get_state_selected() == 1 && it->second->state_printable <= 2) {
if (!it->second->is_blocking_printing(obj)) {
if (!wxGetApp().is_blocking_printing(obj)) {
PrintParams params = request_params(obj);
print_params.push_back(params);
}
+1 -11
View File
@@ -1277,7 +1277,7 @@ void SendToPrinterDialog::update_show_status()
reset_timeout();
// reading done
if (is_blocking_printing(obj_)) {
if (wxGetApp().is_blocking_printing(obj_)) {
show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter);
return;
}
@@ -1342,16 +1342,6 @@ void SendToPrinterDialog::update_show_status()
}
}
bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
void SendToPrinterDialog::Enable_Refresh_Button(bool en)
{
if (!en) {
-1
View File
@@ -181,7 +181,6 @@ public:
void reset_timeout();
void update_user_printer();
void update_show_status();
bool is_blocking_printing(MachineObject* obj_);
void prepare(int print_plate_idx);
void check_focus(wxWindow* window);
void check_fcous_state(wxWindow* window);
+377
View File
@@ -0,0 +1,377 @@
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include <algorithm>
#include <map>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
constexpr uint8_t GLOBAL = context_bit(ShortcutContext::Global);
constexpr uint8_t PLATER = context_bit(ShortcutContext::Plater);
constexpr uint8_t PREVIEW = context_bit(ShortcutContext::Preview);
constexpr uint8_t OBJECT_LIST = context_bit(ShortcutContext::ObjectList);
constexpr uint8_t PAINTING = context_bit(ShortcutContext::Painting);
constexpr uint8_t CANVAS = PLATER | PREVIEW;
constexpr int CTRL = wxMOD_CONTROL;
constexpr int SHIFT = wxMOD_SHIFT;
constexpr int CTRL_SHIFT = wxMOD_CONTROL | wxMOD_SHIFT;
#ifdef __APPLE__
constexpr KeyChord PREFERENCES_CHORD{ ',', CTRL };
constexpr KeyChord DELETE_CHORD{ WXK_BACK };
constexpr KeyChord MOUSE3D_CHORD{ 'M', CTRL_SHIFT };
#else
constexpr KeyChord PREFERENCES_CHORD{ 'P', CTRL };
constexpr KeyChord DELETE_CHORD{ WXK_DELETE };
constexpr KeyChord MOUSE3D_CHORD{ 'M', CTRL };
#endif
// Each row: enum value, AppConfig key, description shown in the dialog, contexts the key is
// looked up in, and the default chord. Rows are in the order the dialog lists them, within
// the sections of the Shortcut enum.
// SHORTCUT runs once per key press.
// REPEATING runs again on every auto-repeat of a held key.
// STEPPING repeats, and Shift or Ctrl held with its key select a step variant of it instead
// of another shortcut (ShortcutInfo::modifier_variants).
#define SHORTCUT(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, false, false }
#define REPEATING(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, true, false }
#define STEPPING(id, key, name, contexts, ...) ShortcutInfo{ Shortcut::id, key, name, contexts, __VA_ARGS__, true, true }
constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
// Project
SHORTCUT(NewProject, "new_project", L("New Project"), GLOBAL, { 'N', CTRL }),
SHORTCUT(OpenProject, "open_project", L("Open Project"), GLOBAL, { 'O', CTRL }),
SHORTCUT(SaveProject, "save_project", L("Save Project"), GLOBAL, { 'S', CTRL }),
SHORTCUT(SaveProjectAs, "save_project_as", L("Save Project as"), GLOBAL, { 'S', CTRL_SHIFT }),
SHORTCUT(ImportModel, "import_model", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files"), GLOBAL, { 'I', CTRL }),
SHORTCUT(Publish3mf, "publish_3mf", L("Publish 3MF"), GLOBAL, { 'E', CTRL_SHIFT }),
// Slicing and printing
SHORTCUT(SlicePlate, "slice_plate", L("Slice plate"), GLOBAL, { 'R', CTRL }),
SHORTCUT(ExportSlicedFile, "export_sliced_file", L("Export plate sliced file"), GLOBAL, { 'G', CTRL }),
SHORTCUT(PrintPlate, "print_plate", L("Print plate"), GLOBAL, { 'G', CTRL_SHIFT }),
SHORTCUT(PrintHostQueue, "print_host_queue", L("Print host upload queue"), GLOBAL, { 'J', CTRL }),
// Selection
SHORTCUT(SelectAll, "select_all", L("Select all objects"), PLATER | OBJECT_LIST, { 'A', CTRL }),
SHORTCUT(SelectAllPlates, "select_all_plates", L("Select all objects on all plates"), PLATER, { 'A', CTRL_SHIFT }),
// Editing
REPEATING(Undo, "undo", L("Undo"), PLATER | OBJECT_LIST, { 'Z', CTRL }),
REPEATING(Redo, "redo", L("Redo"), PLATER | OBJECT_LIST, { 'Y', CTRL }),
SHORTCUT(Cut, "cut", L("Cut"), PLATER | OBJECT_LIST, { 'X', CTRL }),
SHORTCUT(Copy, "copy", L("Copy to clipboard"), PLATER | OBJECT_LIST, { 'C', CTRL }),
SHORTCUT(Paste, "paste", L("Paste from clipboard"), PLATER | OBJECT_LIST, { 'V', CTRL }),
SHORTCUT(DeleteSelected, "delete_selected", L("Delete Selected"), PLATER | OBJECT_LIST, DELETE_CHORD),
SHORTCUT(DeleteAll, "delete_all", L("Delete All"), PLATER, { 'D', CTRL }),
SHORTCUT(CloneSelected, "clone_selected", L("Clone Selected"), PLATER | OBJECT_LIST, { 'K', CTRL }),
// Objects
REPEATING(AddInstance, "add_instance", L("Add instance"), PLATER | OBJECT_LIST, { '+' }),
REPEATING(RemoveInstance, "remove_instance", L("Remove instance"), PLATER | OBJECT_LIST, { '-' }),
SHORTCUT(TogglePrintable, "toggle_printable", L("Toggle printable for object/part"), PLATER | OBJECT_LIST, { 'V' }),
SHORTCUT(ToggleAutoDrop, "toggle_auto_drop", L("Auto Drop"), OBJECT_LIST, { 'D' }),
// Placement
SHORTCUT(Arrange, "arrange", L("Arrange all objects"), PLATER, { 'A' }),
SHORTCUT(ArrangePlate, "arrange_plate", L("Arrange objects on selected plates"), PLATER, { 'A', SHIFT }),
SHORTCUT(Orient, "orient", L("Auto orient all/selected objects"), PLATER, { 'Q' }),
SHORTCUT(OrientPlate, "orient_plate", L("Auto orient all objects on current plate"), PLATER, { 'Q', SHIFT }),
STEPPING(MoveSelectionLeft, "move_selection_left", L("Move selection 10mm in negative X direction"), PLATER, { WXK_LEFT }),
STEPPING(MoveSelectionRight, "move_selection_right", L("Move selection 10mm in positive X direction"), PLATER, { WXK_RIGHT }),
STEPPING(MoveSelectionUp, "move_selection_up", L("Move selection 10mm in positive Y direction"), PLATER, { WXK_UP }),
STEPPING(MoveSelectionDown, "move_selection_down", L("Move selection 10mm in negative Y direction"), PLATER, { WXK_DOWN }),
REPEATING(RotateSelectionLeft, "rotate_selection_left", L("Rotate selection 45 degrees counterclockwise"), PLATER, { WXK_PAGEUP }),
REPEATING(RotateSelectionRight, "rotate_selection_right", L("Rotate selection 45 degrees clockwise"), PLATER, { WXK_PAGEDOWN }),
// Gizmos
SHORTCUT(GizmoMove, "gizmo_move", L("Gizmo move"), PLATER, { 'M' }),
SHORTCUT(GizmoRotate, "gizmo_rotate", L("Gizmo rotate"), PLATER, { 'R' }),
SHORTCUT(GizmoScale, "gizmo_scale", L("Gizmo scale"), PLATER, { 'S' }),
SHORTCUT(GizmoFlatten, "gizmo_flatten", L("Gizmo place face on bed"), PLATER, { 'F' }),
SHORTCUT(GizmoCut, "gizmo_cut", L("Gizmo cut"), PLATER, { 'C' }),
SHORTCUT(GizmoMeshBoolean, "gizmo_mesh_boolean", L("Gizmo mesh boolean"), PLATER, { 'B' }),
SHORTCUT(GizmoFdmSupports, "gizmo_fdm_supports", L("Gizmo FDM paint-on supports"), PLATER, { 'L' }),
SHORTCUT(GizmoSeam, "gizmo_seam", L("Gizmo FDM paint-on seam"), PLATER, { 'P' }),
SHORTCUT(GizmoFuzzySkin, "gizmo_fuzzy_skin", L("Gizmo FDM paint-on fuzzy skin"), PLATER, { 'H' }),
SHORTCUT(GizmoMmuSegmentation, "gizmo_mmu_segmentation", L("Gizmo multi-material painting"), PLATER, { 'N' }),
SHORTCUT(GizmoEmboss, "gizmo_emboss", L("Gizmo text emboss/engrave"), PLATER, { 'T' }),
SHORTCUT(GizmoMeasure, "gizmo_measure", L("Gizmo measure"), PLATER, { 'U' }),
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
// Sliders
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
STEPPING(LayerSliderUp, "layer_slider_up", L("Vertical slider - Move active thumb Up"), PREVIEW, { WXK_UP }),
STEPPING(LayerSliderDown, "layer_slider_down", L("Vertical slider - Move active thumb Down"), PREVIEW, { WXK_DOWN }),
STEPPING(MovesSliderLeft, "moves_slider_left", L("Horizontal slider - Move active thumb Left"), PREVIEW, { WXK_LEFT }),
STEPPING(MovesSliderRight, "moves_slider_right", L("Horizontal slider - Move active thumb Right"), PREVIEW, { WXK_RIGHT }),
REPEATING(MovesSliderStart, "moves_slider_start", L("Horizontal slider - Move to start position"), PREVIEW, { WXK_HOME }),
REPEATING(MovesSliderEnd, "moves_slider_end", L("Horizontal slider - Move to last position"), PREVIEW, { WXK_END }),
// Painting tools
SHORTCUT(PaintToolCircle, "paint_tool_circle", L("Circle"), PAINTING, { 'C' }),
SHORTCUT(PaintToolSphere, "paint_tool_sphere", L("Sphere"), PAINTING, { 'S' }),
SHORTCUT(PaintToolFill, "paint_tool_fill", L("Fill"), PAINTING, { 'F' }),
SHORTCUT(PaintToolGapFill, "paint_tool_gap_fill", L("Gap Fill"), PAINTING, { 'G' }),
SHORTCUT(PaintToolTriangle, "paint_tool_triangle", L("Triangle"), PAINTING, { 'T' }),
SHORTCUT(PaintToolHeightRange, "paint_tool_height_range", L("Height Range"), PAINTING, { 'H' }),
// Camera
SHORTCUT(ViewDefault, "view_default", L("Camera view - Default"), GLOBAL, { '0', CTRL }),
SHORTCUT(ViewTop, "view_top", L("Camera view - Top"), GLOBAL, { '1', CTRL }),
SHORTCUT(ViewBottom, "view_bottom", L("Camera view - Bottom"), GLOBAL, { '2', CTRL }),
SHORTCUT(ViewFront, "view_front", L("Camera view - Front"), GLOBAL, { '3', CTRL }),
SHORTCUT(ViewRear, "view_rear", L("Camera view - Behind"), GLOBAL, { '4', CTRL }),
SHORTCUT(ViewLeft, "view_left", L("Camera Angle - Left side"), GLOBAL, { '5', CTRL }),
SHORTCUT(ViewRight, "view_right", L("Camera Angle - Right side"), GLOBAL, { '6', CTRL }),
SHORTCUT(ViewPlate, "view_plate", L("Camera view - Current plate"), GLOBAL, { '7', CTRL }),
REPEATING(ZoomIn, "zoom_in", L("Zoom in"), CANVAS, { 'I' }),
REPEATING(ZoomOut, "zoom_out", L("Zoom out"), CANVAS, { 'O' }),
SHORTCUT(Mouse3DSettings, "mouse3d_settings", L("Show/Hide 3Dconnexion devices settings dialog"), CANVAS, MOUSE3D_CHORD),
// Display
SHORTCUT(ShowLabels, "show_labels", L("Show object labels in 3D scene."), GLOBAL, { 'E', CTRL }),
SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }),
SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }),
SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }),
// Application
SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD),
SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }),
SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }),
SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }),
SHORTCUT(SpeedDial, "speed_dial", L("Open the speed dial"), GLOBAL, { WXK_SPACE }),
SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }),
SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }),
}};
#undef SHORTCUT
#undef REPEATING
#undef STEPPING
// The first shortcut of each section's run in shortcut_table and the section's heading, indexed
// by ShortcutSection.
struct SectionInfo
{
Shortcut first;
const char* name;
};
constexpr std::array<SectionInfo, size_t(ShortcutSection::Count)> section_table = {{
{ Shortcut::NewProject, L("Project") },
{ Shortcut::SlicePlate, L("Slicing and printing") },
{ Shortcut::SelectAll, L("Selection") },
{ Shortcut::Undo, L("Editing") },
{ Shortcut::AddInstance, L("Objects") },
{ Shortcut::Arrange, L("Placement") },
{ Shortcut::GizmoMove, L("Gizmos") },
{ Shortcut::GoToLayer, L("Sliders") },
{ Shortcut::PaintToolCircle, L("Painting tools") },
{ Shortcut::ViewDefault, L("Camera") },
{ Shortcut::ShowLabels, L("Display") },
{ Shortcut::Preferences, L("Application") },
}};
constexpr bool sections_follow_table_order()
{
if (section_table.front().first != Shortcut(0))
return false;
for (size_t i = 1; i < section_table.size(); ++i)
if (section_table[i].first <= section_table[i - 1].first)
return false;
return true;
}
static_assert(sections_follow_table_order(), "section_table must begin at the first shortcut and ascend");
constexpr bool table_is_in_enum_order()
{
for (size_t i = 0; i < shortcut_table.size(); ++i)
if (shortcut_table[i].id != Shortcut(i))
return false;
return true;
}
static_assert(table_is_in_enum_order(), "shortcut_table must list every Shortcut in declaration order");
const char* CONFIG_SECTION = "shortcuts";
const char* UNBOUND = "none";
bool share_context(uint8_t a, uint8_t b) { return (a & b) != 0 || (a & GLOBAL) != 0 || (b & GLOBAL) != 0; }
// The modifiers a modifier_variants shortcut accepts on top of its binding.
constexpr int STEP_MODIFIERS = wxMOD_SHIFT | wxMOD_CONTROL;
// The step modifiers chord adds to binding, 0 when chord is not a step of it; a binding with
// Shift or Ctrl of its own has no steps, so no two bindings share one.
int step_modifiers(const KeyChord& chord, const KeyChord& binding)
{
if (!binding.valid() || (binding.modifiers & STEP_MODIFIERS) != 0 || chord.key != binding.key ||
(chord.modifiers & ~STEP_MODIFIERS) != binding.modifiers)
return 0;
return chord.modifiers & STEP_MODIFIERS;
}
} // namespace
const ShortcutInfo& shortcut_info(Shortcut shortcut) { return shortcut_table[size_t(shortcut)]; }
ShortcutSection shortcut_section(Shortcut shortcut)
{
size_t section = 0;
for (size_t i = 1; i < section_table.size(); ++i)
if (section_table[i].first <= shortcut)
section = i;
return ShortcutSection(section);
}
const char* section_name(ShortcutSection section) { return section_table[size_t(section)].name; }
std::vector<Shortcut> shortcuts_in(ShortcutContext context)
{
std::vector<Shortcut> out;
for (const ShortcutInfo& info : shortcut_table)
if (info.contexts & context_bit(context))
out.push_back(info.id);
return out;
}
ShortcutRegistry::ShortcutRegistry() { rebuild_index(); }
KeyChord ShortcutRegistry::binding(Shortcut shortcut) const
{
const std::optional<KeyChord>& override = m_overrides[size_t(shortcut)];
return override.has_value() ? *override : shortcut_info(shortcut).default_chord;
}
bool ShortcutRegistry::is_customized(Shortcut shortcut) const { return m_overrides[size_t(shortcut)].has_value(); }
std::string ShortcutRegistry::display(Shortcut shortcut) const { return binding(shortcut).display(); }
std::string ShortcutRegistry::with_key(const std::string& text, Shortcut shortcut) const
{
const std::string key = display(shortcut);
return key.empty() ? text : text + " [" + key + "]";
}
std::string ShortcutRegistry::accelerator(Shortcut shortcut) const
{
const KeyChord chord = binding(shortcut);
return chord.is_menu_accelerator() ? chord.to_string() : std::string();
}
std::optional<Shortcut> ShortcutRegistry::lookup(ShortcutContext context, const KeyChord& chord) const
{
if (!chord.valid())
return std::nullopt;
auto it = m_index.find(chord);
if (it == m_index.end())
return std::nullopt;
for (Shortcut shortcut : it->second)
if (shortcut_info(shortcut).contexts & context_bit(context))
return shortcut;
return std::nullopt;
}
std::optional<ShortcutRegistry::Match> ShortcutRegistry::match(ShortcutContext context, const KeyChord& chord) const
{
if (const std::optional<Shortcut> exact = lookup(context, chord); exact.has_value())
return Match{ *exact, 0 };
if ((chord.modifiers & STEP_MODIFIERS) == 0)
return std::nullopt;
for (const ShortcutInfo& info : shortcut_table)
if (info.modifier_variants && (info.contexts & context_bit(context)))
if (const int step = step_modifiers(chord, binding(info.id)); step != 0)
return Match{ info.id, step };
return std::nullopt;
}
std::vector<Shortcut> ShortcutRegistry::conflicts(Shortcut shortcut, const KeyChord& chord) const
{
std::vector<Shortcut> out;
if (!chord.valid())
return out;
const uint8_t contexts = shortcut_info(shortcut).contexts;
for (const ShortcutInfo& other : shortcut_table)
if (other.id != shortcut && share_context(contexts, other.contexts) && binding(other.id) == chord)
out.push_back(other.id);
return out;
}
std::optional<Shortcut> ShortcutRegistry::step_owner(Shortcut shortcut, const KeyChord& chord) const
{
const uint8_t contexts = shortcut_info(shortcut).contexts;
for (const ShortcutInfo& other : shortcut_table)
if (other.modifier_variants && other.id != shortcut && share_context(contexts, other.contexts))
if (const int step = step_modifiers(chord, binding(other.id)); step == wxMOD_SHIFT || step == wxMOD_CONTROL) // the combined step stays assignable
return other.id;
return std::nullopt;
}
void ShortcutRegistry::bind(Shortcut shortcut, const KeyChord& chord)
{
const ShortcutInfo& info = shortcut_info(shortcut);
if (chord == info.default_chord)
m_overrides[size_t(shortcut)].reset();
else
m_overrides[size_t(shortcut)] = chord;
rebuild_index();
}
void ShortcutRegistry::reset(Shortcut shortcut)
{
m_overrides[size_t(shortcut)].reset();
rebuild_index();
}
void ShortcutRegistry::reset_all()
{
m_overrides.fill(std::nullopt);
rebuild_index();
}
void ShortcutRegistry::load(const AppConfig& config)
{
m_overrides.fill(std::nullopt);
if (config.has_section(CONFIG_SECTION)) {
const std::map<std::string, std::string>& section = config.get_section(CONFIG_SECTION);
for (const ShortcutInfo& info : shortcut_table) {
auto it = section.find(info.key);
if (it == section.end())
continue;
if (it->second == UNBOUND)
m_overrides[size_t(info.id)] = KeyChord{};
else if (std::optional<KeyChord> chord = KeyChord::parse(it->second); chord.has_value())
// A Global chord is seen before any text field, so it needs a modifier or a non-typing key.
if ((info.contexts & GLOBAL) == 0 || chord->is_menu_accelerator())
m_overrides[size_t(info.id)] = *chord;
}
}
rebuild_index();
}
void ShortcutRegistry::save(AppConfig& config) const
{
for (const ShortcutInfo& info : shortcut_table) {
const std::optional<KeyChord>& override = m_overrides[size_t(info.id)];
if (override.has_value())
config.set(CONFIG_SECTION, info.key, override->valid() ? override->to_string() : UNBOUND);
else
config.erase(CONFIG_SECTION, info.key);
}
}
void ShortcutRegistry::rebuild_index()
{
m_index.clear();
for (const ShortcutInfo& info : shortcut_table)
if (const KeyChord chord = binding(info.id); chord.valid())
m_index[chord].push_back(info.id);
}
}} // namespace Slic3r::GUI
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include "KeyChord.hpp"
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r {
class AppConfig;
namespace GUI {
// Where a key press is looked up. A shortcut may belong to several contexts; Global ones are
// dispatched by the main frame before any child window sees the key.
enum class ShortcutContext : uint8_t { Global, Plater, Preview, ObjectList, Painting, Count };
constexpr uint8_t context_bit(ShortcutContext context) { return uint8_t(1u << unsigned(context)); }
enum class Shortcut : uint8_t {
// Project
NewProject, OpenProject, SaveProject, SaveProjectAs, ImportModel, Publish3mf,
// Slicing and printing
SlicePlate, ExportSlicedFile, PrintPlate, PrintHostQueue,
// Selection
SelectAll, SelectAllPlates,
// Editing
Undo, Redo, Cut, Copy, Paste, DeleteSelected, DeleteAll, CloneSelected,
// Objects
AddInstance, RemoveInstance, TogglePrintable, ToggleAutoDrop,
// Placement
Arrange, ArrangePlate, Orient, OrientPlate,
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
// Gizmos
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars,
// Sliders
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
// Painting tools
PaintToolCircle, PaintToolSphere, PaintToolFill, PaintToolGapFill, PaintToolTriangle, PaintToolHeightRange,
// Camera
ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings,
// Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application
Preferences, Search, SwitchView, CollapseSidebar, SpeedDial, ReloadDevicePage, KeyboardShortcuts,
Count
};
struct ShortcutInfo
{
Shortcut id;
const char* key; // AppConfig key
const char* name; // untranslated description
uint8_t contexts; // context_bit() mask
KeyChord default_chord;
bool repeatable; // runs on key auto-repeat as well
// Shift or Ctrl held with the bound key select a variant of this action, such as the finer
// move step or the faster slider step.
bool modifier_variants;
};
// Headings of the shortcuts dialog, in listing order.
enum class ShortcutSection : uint8_t {
Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application,
Count
};
const ShortcutInfo& shortcut_info(Shortcut shortcut);
ShortcutSection shortcut_section(Shortcut shortcut);
const char* section_name(ShortcutSection section); // untranslated heading
std::vector<Shortcut> shortcuts_in(ShortcutContext context); // in table order
// Effective key bindings: the built-in defaults overlaid with the user's own. Owns the
// chord -> shortcut index every dispatcher queries.
class ShortcutRegistry
{
public:
ShortcutRegistry();
KeyChord binding(Shortcut shortcut) const; // invalid when unbound
bool is_customized(Shortcut shortcut) const;
std::string display(Shortcut shortcut) const; // "Ctrl+N" for tooltips and the shortcuts dialog
std::string with_key(const std::string& text, Shortcut shortcut) const; // "text [Ctrl+N]", or text alone when unbound
std::string accelerator(Shortcut shortcut) const; // wx accelerator text for menu labels; empty when unbound or not menu-safe
// Matches a key event in one context. Global shortcuts are only found through the Global context.
std::optional<Shortcut> lookup(ShortcutContext context, const KeyChord& chord) const;
// lookup(), then the modifier_variants shortcuts, which also match with Shift or Ctrl added
// to their binding; step_modifiers holds whichever of the two were added.
struct Match
{
Shortcut shortcut;
int step_modifiers;
};
std::optional<Match> match(ShortcutContext context, const KeyChord& chord) const;
// Shortcuts other than shortcut bound to chord in a context it shares; Global shortcuts
// share every context.
std::vector<Shortcut> conflicts(Shortcut shortcut, const KeyChord& chord) const;
// The modifier_variants shortcut in a shared context whose Shift or Ctrl step is chord;
// such a chord is reserved for that step.
std::optional<Shortcut> step_owner(Shortcut shortcut, const KeyChord& chord) const;
void bind(Shortcut shortcut, const KeyChord& chord); // an invalid chord unbinds
void reset(Shortcut shortcut);
void reset_all();
void load(const AppConfig& config);
void save(AppConfig& config) const;
private:
void rebuild_index();
std::array<std::optional<KeyChord>, size_t(Shortcut::Count)> m_overrides;
std::unordered_map<KeyChord, std::vector<Shortcut>, KeyChordHash> m_index;
};
} // namespace GUI
} // namespace Slic3r
+23 -12
View File
@@ -1742,6 +1742,8 @@ void SyncAmsInfoDialog::show_status(PrintDialogStatus status, std::vector<wxStri
update_print_status_msg(msg_text, true, true);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingSuccess) {
update_print_status_msg(wxEmptyString, false, false);
} else if (status == PrintDialogStatus::PrintStatusOptionalPrinterModel) {
update_print_status_msg(PrePrintChecker::get_pre_state_msg(PrintDialogStatus::PrintStatusOptionalPrinterModel), true, true);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingInvalid) {
update_print_status_msg(wxEmptyString, true, false);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingMixInvalid) {
@@ -1861,19 +1863,13 @@ void SyncAmsInfoDialog::on_cancel(wxCloseEvent &event)
bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_)
{
DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
std::string source_model = "";
if (m_print_type == PrintFromType::FROM_NORMAL)
return wxGetApp().is_blocking_printing(obj_);
if (m_print_type == PrintFromType::FROM_NORMAL) {
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) {
if (m_required_data_plate_data_list.size() > 0) { source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; }
}
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
std::string source_model;
if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty())
source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
return wxGetApp().is_blocking_printing(obj_, source_model);
}
bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type)
@@ -2263,6 +2259,18 @@ void SyncAmsInfoDialog::update_show_status()
reset_timeout();
bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
if (m_print_type == PrintFromType::FROM_NORMAL) {
if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(
preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle));
}
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id);
}
if (!obj_->GetConfig()->SupportPrintAllPlates() && m_print_plate_idx == PLATE_ALL_IDX) {
show_status(PrintDialogStatus::PrintStatusNotSupportedPrintAll);
return;
@@ -2356,6 +2364,9 @@ void SyncAmsInfoDialog::update_show_status()
}
}
}
if (has_optional_printer_model)
show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel);
}
bool SyncAmsInfoDialog::has_timelapse_warning()
@@ -1,11 +1,7 @@
#include "PluginWebDialog.hpp"
#include "WebDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include "slic3r/GUI/Widgets/WebHosting.hpp"
#include <wx/event.h>
@@ -13,59 +9,15 @@
namespace Slic3r { namespace GUI {
namespace {
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
// relative URLs resolve against the bundled web resources directory.
wxString web_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
} // namespace
PluginWebDialog::PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
WebDialog::WebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
, m_html(html)
, m_on_message(std::move(on_message))
@@ -75,7 +27,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
{
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
// is swapped in via SetPage once the bootstrap finishes loading.
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
// Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render.
@@ -87,21 +39,22 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
// create_webview() via add_user_scripts(); nothing to add here.
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_LOADED, &WebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &WebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATED, &WebDialog::on_navigated, this, wv->GetId());
}
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
Bind(wxEVT_CLOSE_WINDOW, &WebDialog::on_close_window, this);
}
void PluginWebDialog::add_user_scripts()
void WebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
wv->AddUserScript(wxString::FromUTF8(web_hosting::orca_bridge_script()));
}
}
PluginWebDialog::~PluginWebDialog()
WebDialog::~WebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
// that event is sent from the base ~wxDialog(), after this subclass's members
@@ -111,19 +64,19 @@ PluginWebDialog::~PluginWebDialog()
m_on_destroyed();
}
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
void WebDialog::post_message(WebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
dialog->push_message(data);
}
void PluginWebDialog::request_close(PluginWebDialog* dialog)
void WebDialog::request_close(WebDialog* dialog)
{
if (dialog != nullptr)
dialog->Close();
}
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
void WebDialog::destroy_silently(WebDialog* dialog)
{
if (dialog == nullptr)
return;
@@ -137,24 +90,42 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
dialog->Destroy();
}
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
void WebDialog::on_bootstrap_event(wxWebViewEvent& event)
{
// The first bootstrap load (or its error) triggers the swap to plugin HTML;
// the resulting plugin-page load is ignored (guarded by m_content_loaded).
load_plugin_content();
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
if (!m_content_loaded)
load_page_html();
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a reload.
// A failed navigation is reported against the page that stayed but never commits. Edge ignores the
// base URL and restores SetPage content itself, so nothing matches there.
else if (web_hosting::is_content_url(event.GetURL())) {
if (m_own_page_load)
m_own_page_load = false;
else if (loaded && m_content_navigated)
load_page_html();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void PluginWebDialog::load_plugin_content()
void WebDialog::on_navigated(wxWebViewEvent& event)
{
if (m_content_loaded)
return;
m_content_loaded = true;
if (wxWebView* wv = browser())
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
m_content_navigated = web_hosting::is_content_url(event.GetURL());
event.Skip();
}
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
void WebDialog::load_page_html()
{
m_content_loaded = true;
if (wxWebView* wv = browser()) {
m_own_page_load = true;
wv->SetPage(wxString::FromUTF8(m_html), web_hosting::content_base_url());
}
}
void WebDialog::on_script_message(const nlohmann::json& payload)
{
if (payload.value("channel", std::string()) == "orca") {
const std::string kind = payload.value("kind", std::string());
@@ -174,7 +145,7 @@ void PluginWebDialog::on_script_message(const nlohmann::json& payload)
handle_common_script_command(payload);
}
void PluginWebDialog::push_message(const nlohmann::json& data)
void WebDialog::push_message(const nlohmann::json& data)
{
if (!m_open)
return;
@@ -183,7 +154,7 @@ void PluginWebDialog::push_message(const nlohmann::json& data)
call_web_handler(envelope, wxT("__orcaDispatch"));
}
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
void WebDialog::finish(bool submitted, const nlohmann::json& data)
{
if (!m_open)
return;
@@ -202,7 +173,7 @@ void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
Close();
}
void PluginWebDialog::on_close_window(wxCloseEvent&)
void WebDialog::on_close_window(wxCloseEvent&)
{
if (!m_open) {
// finish() already dispatched submit/close and requested the close.
@@ -222,7 +193,7 @@ void PluginWebDialog::on_close_window(wxCloseEvent&)
Destroy();
}
void PluginWebDialog::fire_submit(const nlohmann::json& data)
void WebDialog::fire_submit(const nlohmann::json& data)
{
if (m_on_submit) {
SubmitHandler cb = std::move(m_on_submit);
@@ -230,7 +201,7 @@ void PluginWebDialog::fire_submit(const nlohmann::json& data)
}
}
void PluginWebDialog::fire_close()
void WebDialog::fire_close()
{
if (m_close_fired)
return;
@@ -1,5 +1,5 @@
#ifndef slic3r_GUI_PluginWebDialog_hpp_
#define slic3r_GUI_PluginWebDialog_hpp_
#ifndef slic3r_GUI_WebDialog_hpp_
#define slic3r_GUI_WebDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
@@ -21,7 +21,7 @@ namespace Slic3r { namespace GUI {
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
//
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
class WebDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
@@ -32,20 +32,20 @@ public:
// user/JS-initiated close (while the window is alive). on_destroyed runs from
// the destructor on every path and must touch host-side state only (no Python
// / no derived members).
PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~PluginWebDialog() override;
WebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
~WebDialog() override;
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
static void request_close(PluginWebDialog* dialog);
static void destroy_for_plugin(PluginWebDialog* dialog);
static void post_message(WebDialog* dialog, const nlohmann::json& data);
static void request_close(WebDialog* dialog);
static void destroy_silently(WebDialog* dialog);
// Push a payload to the page; delivered to handlers registered via
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
@@ -64,7 +64,8 @@ protected:
private:
void on_bootstrap_event(wxWebViewEvent& event);
void load_plugin_content();
void on_navigated(wxWebViewEvent& event);
void load_page_html();
void on_close_window(wxCloseEvent& event);
void fire_submit(const nlohmann::json& data);
void fire_close();
@@ -72,6 +73,8 @@ private:
std::string m_html;
bool m_content_loaded{false};
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
bool m_content_navigated{false}; // a navigation to the base URL has committed
bool m_open{true};
bool m_close_fired{false};
std::optional<nlohmann::json> m_result;
@@ -83,4 +86,4 @@ private:
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginWebDialog_hpp_
#endif // slic3r_GUI_WebDialog_hpp_
+16 -4
View File
@@ -4,13 +4,23 @@
namespace Slic3r { namespace GUI {
WebMediaController::WebMediaController(wxWebView* webview) : m_webview(webview)
namespace {
void initialize_webview(wxWebView* webview)
{
if (!m_webview)
if (!webview)
return;
m_webview->SetBackgroundColour(*wxBLACK);
m_webview->SetPage("<html><head><style>html,body{margin:0;height:100%;background:#000;}</style></head><body></body></html>", "");
webview->SetBackgroundColour(*wxBLACK);
webview->SetPage("<html><head><style>html,body{margin:0;height:100%;background:#000;}</style></head><body></body></html>", "");
}
} // namespace
WebMediaController::WebMediaController(wxWebView* webview)
: m_webview(webview)
{
initialize_webview(m_webview);
}
void WebMediaController::Load(wxURI url)
@@ -71,6 +81,8 @@ void WebMediaController::Stop()
if (m_webview) {
m_webview->RunScript("if(typeof stopCameraRefresh==='function') stopCameraRefresh();");
m_webview->Stop();
m_webview->SetPage("", "about:blank");
m_webview->ClearHistory();
}
m_url.clear();
}
+1 -1
View File
@@ -23,7 +23,7 @@ public:
void Stop() override;
private:
wxWebView* m_webview;
wxWebView* m_webview = nullptr;
std::string m_url;
CameraStreamMode m_stream_mode = CameraStreamMode::http;
};
+111
View File
@@ -0,0 +1,111 @@
#include "WebPanel.hpp"
#include "GUI_App.hpp"
#include "Widgets/WebHosting.hpp"
#include "Widgets/WebView.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <boost/log/trivial.hpp>
#include <wx/sizer.h>
namespace Slic3r { namespace GUI {
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
: wxPanel(parent, wxID_ANY)
{
SetBackgroundColour(wxGetApp().get_window_default_clr());
auto* sizer = new wxBoxSizer(wxVERTICAL);
SetSizer(sizer);
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
m_browser->SetBackgroundColour(GetBackgroundColour());
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(bridge_script));
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebPanel::on_navigated, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebPanel::on_script_message, this);
m_browser->Bind(EVT_WEBVIEW_RECREATED, &WebPanel::on_webview_recreated, this);
sizer->Add(m_browser, 1, wxEXPAND);
}
void WebPanel::on_load_event(wxWebViewEvent& event)
{
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
if (!m_content_loaded) {
// The first bootstrap load (or its error) triggers the swap to the plugin HTML.
m_content_loaded = true;
load_page_html();
} else if (!web_hosting::is_content_url(event.GetURL())) {
// Not our document (a linked page, a substituted error page), or any document on Edge, which ignores
// the base URL and restores SetPage content on a reload itself; either way it takes the app theme.
if (loaded)
apply_theme();
} else if (m_own_page_load) {
m_own_page_load = false;
// The document-start theme script is fixed at creation, so re-apply the app theme.
if (loaded)
apply_theme();
} else if (loaded && m_content_navigated) {
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a
// reload. A failed navigation is reported against the page that stayed but never commits.
load_page_html();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void WebPanel::on_navigated(wxWebViewEvent& event)
{
m_content_navigated = web_hosting::is_content_url(event.GetURL());
event.Skip();
}
void WebPanel::load_page_html()
{
if (const std::optional<std::string> html = page_html()) {
m_own_page_load = true;
m_browser->SetPage(wxString::FromUTF8(*html), web_hosting::content_base_url());
}
}
void WebPanel::on_script_message(wxWebViewEvent& event)
{
const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false);
if (!payload.is_object() || payload.value("channel", std::string()) != "orca")
return;
const std::string kind = payload.value("kind", std::string());
if (!on_page_message(kind, payload.contains("data") ? payload["data"] : nlohmann::json()))
BOOST_LOG_TRIVIAL(warning) << "WebPanel ignored a window.orca '" << kind << "' call; this host does not support it";
}
void WebPanel::on_webview_recreated(wxCommandEvent&)
{
SetBackgroundColour(wxGetApp().get_window_default_clr());
m_browser->SetBackgroundColour(GetBackgroundColour());
Refresh();
// Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page.
apply_theme();
}
void WebPanel::apply_theme()
{
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
}
void WebPanel::post_to_page(const std::string& json)
{
WebView::RunScript(m_browser, wxString::Format(
"(function dispatch(payload, attempts) {\n"
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
"})({data: %s}, 0);",
wxString::FromUTF8(json)));
}
}} // namespace Slic3r::GUI
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <nlohmann/json.hpp>
#include <wx/panel.h>
#include <wx/webview.h>
#include <optional>
#include <string>
namespace Slic3r { namespace GUI {
// Host-supplied HTML in a web view panel, for any window that embeds or derives from it (today plugin
// Pages tabs and docked panels): bootstrap page and swap, theme and bridge scripts, live re-theming, and
// window.orca messages routed to on_page_message().
class WebPanel : public wxPanel
{
public:
WebPanel(wxWindow* parent, const char* bridge_script);
protected:
wxWebView* browser() const { return m_browser; }
// Delivers an already serialised JSON value to the page's window.orca.onMessage handlers,
// waiting briefly for the bridge while the page is still loading. Main thread only.
void post_to_page(const std::string& json);
// The plugin HTML to show once the bootstrap page has loaded, and again when WebKit reloads it;
// std::nullopt leaves it blank.
virtual std::optional<std::string> page_html() = 0;
// A window.orca message from the page; false for a kind this host does not handle (logged).
virtual bool on_page_message(const std::string& kind, const nlohmann::json& data) = 0;
private:
void on_load_event(wxWebViewEvent& event);
void on_navigated(wxWebViewEvent& event);
void on_script_message(wxWebViewEvent& event);
void on_webview_recreated(wxCommandEvent& event);
void apply_theme();
void load_page_html();
wxWebView* m_browser{nullptr};
bool m_content_loaded{false};
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
bool m_content_navigated{false}; // a navigation to the base URL has committed
};
}} // namespace Slic3r::GUI
+69
View File
@@ -0,0 +1,69 @@
#include "WebHosting.hpp"
#include "slic3r/GUI/GUI.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <wx/uri.h>
namespace Slic3r { namespace GUI { namespace web_hosting {
namespace {
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
} // namespace
wxString bootstrap_url()
{
return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string());
}
wxString content_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
bool is_content_url(const wxString& url)
{
// The web view reports the URL it parsed, which escapes anything the resources path holds
// (a space, a non-ASCII character), while content_base_url() is the raw path.
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url();
}
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
}}} // namespace Slic3r::GUI::web_hosting
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <wx/string.h>
namespace Slic3r { namespace GUI { namespace web_hosting {
// Shared by the hosts that show plugin HTML: WebDialog and WebPanel.
// The bundled blank page a plugin web view loads before the plugin HTML is swapped in.
constexpr const char* BOOTSTRAP_PAGE = "web/dialog/WebDialog/blank.html";
// The file:// URL of BOOTSTRAP_PAGE.
wxString bootstrap_url();
// The file:// base URL plugin HTML is loaded against, so relative URLs resolve to bundled resources.
wxString content_base_url();
// Whether `url` is the plugin HTML's base URL, ignoring any fragment. WebKit reports it for the
// injected page, a reload and a failed navigation alike, so a match alone is not a new document.
bool is_content_url(const wxString& url);
// The window.orca bridge of plugin windows and docked panels. Pages tabs ship their own.
const char* orca_bridge_script();
}}} // namespace Slic3r::GUI::web_hosting
+3 -1
View File
@@ -75,6 +75,8 @@ if(document.documentElement)
} // namespace
std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); }
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
@@ -87,7 +89,7 @@ std::string WebViewHostDialog::theme_user_script()
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
std::string WebViewHostDialog::plugin_defaults_user_script()
std::string WebViewHostDialog::element_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
+4 -2
View File
@@ -49,9 +49,11 @@ public:
const std::string& prelude = {},
const std::string& on_inject = {});
// Shared by modeless Pages tabs and PluginWebDialog.
// Shared by WebPanel hosts and WebDialog.
static std::string theme_user_script();
static std::string plugin_defaults_user_script();
static std::string element_defaults_user_script();
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
static std::string theme_apply_script();
protected:
wxWebView* browser() const { return m_browser; }
+23 -1
View File
@@ -172,7 +172,29 @@ public:
virtual int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
{
(void) supports_mqtt_axis_control;
double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z"))
value = -input_val;
std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode;
if (axis == "X" || axis == "Y" || axis == "Z") {
gcode = (boost::format("G91 \nG1 %1%%2% F%3%\nG90 \n") % axis % value_str % speed).str();
} else if (axis == "E") {
gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str();
} else {
return -1;
}
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
/**
* Default LAN account username for this agent's protocol, if it has a fixed one.
+160 -66
View File
@@ -14,16 +14,22 @@
#include <boost/algorithm/string.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <openssl/ssl.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cctype>
#include <map>
#include <memory>
#include <stdexcept>
#include <thread>
#include <variant>
namespace {
@@ -96,6 +102,130 @@ std::string map_moonraker_state(std::string state)
namespace Slic3r {
struct MoonrakerWebsocket::Impl
{
using PlainWebsocket = websocket::stream<beast::tcp_stream>;
using SecureWebsocket = websocket::stream<beast::ssl_stream<beast::tcp_stream>>;
using PlainPtr = std::unique_ptr<PlainWebsocket>;
using SecurePtr = std::unique_ptr<SecureWebsocket>;
explicit Impl(bool secure, std::string api_key) : secure(secure), api_key(std::move(api_key)), ssl_context(net::ssl::context::tls_client)
{
if (this->secure) {
websocket = std::make_unique<SecureWebsocket>(ioc, ssl_context);
} else {
websocket = std::make_unique<PlainWebsocket>(beast::tcp_stream{ioc});
}
}
bool secure;
std::string api_key;
net::io_context ioc;
net::ssl::context ssl_context;
std::variant<PlainPtr, SecurePtr> websocket;
};
MoonrakerWebsocket::MoonrakerWebsocket(bool secure, std::string api_key) : m_impl(std::make_unique<Impl>(secure, std::move(api_key))) {}
MoonrakerWebsocket::~MoonrakerWebsocket() = default;
void MoonrakerWebsocket::connect(const std::string& host, const std::string& port, std::chrono::seconds timeout)
{
tcp::resolver resolver(m_impl->ioc);
std::visit(
[&](auto& websocket_ptr) {
auto& stream = beast::get_lowest_layer(*websocket_ptr);
stream.expires_after(timeout);
stream.connect(resolver.resolve(host, port));
},
m_impl->websocket);
}
void MoonrakerWebsocket::tls_handshake(const std::string& host)
{
if (!m_impl->secure) {
return;
}
auto& websocket = *std::get<Impl::SecurePtr>(m_impl->websocket);
auto& tls_stream = websocket.next_layer();
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), host.c_str())) {
throw std::runtime_error("Moonraker WSS: failed to set TLS server name");
}
// Match Http's existing printer-host behavior: encrypt the connection
// while accepting self-signed/local printer certificates.
tls_stream.set_verify_mode(net::ssl::verify_none);
tls_stream.handshake(net::ssl::stream_base::client);
}
void MoonrakerWebsocket::handshake(const std::string& host, const std::string& target)
{
std::visit(
[&](auto& websocket_ptr) {
websocket_ptr->set_option(websocket::stream_base::decorator([api_key = m_impl->api_key](websocket::request_type& req) {
req.set(http::field::user_agent, "OrcaSlicer");
if (!api_key.empty()) {
req.set("X-Api-Key", api_key);
}
}));
websocket_ptr->handshake(host, target);
},
m_impl->websocket);
}
void MoonrakerWebsocket::text(bool enabled)
{
std::visit([&](auto& websocket_ptr) { websocket_ptr->text(enabled); }, m_impl->websocket);
}
void MoonrakerWebsocket::write(const std::string& body)
{
std::visit([&](auto& websocket_ptr) { websocket_ptr->write(net::buffer(body)); }, m_impl->websocket);
}
MoonrakerWebsocket::ReadResult MoonrakerWebsocket::read(std::string& payload, std::string& error_message)
{
beast::flat_buffer buffer;
beast::error_code error;
std::visit([&](auto& websocket_ptr) { websocket_ptr->read(buffer, error); }, m_impl->websocket);
if (error == beast::error::timeout) {
return ReadResult::timeout;
}
if (error == websocket::error::closed) {
return ReadResult::closed;
}
if (error) {
error_message = error.message();
return ReadResult::error;
}
payload = beast::buffers_to_string(buffer.data());
return ReadResult::message;
}
void MoonrakerWebsocket::close()
{
beast::error_code error;
std::visit([&](auto& websocket_ptr) { websocket_ptr->close(websocket::close_code::normal, error); }, m_impl->websocket);
}
void MoonrakerWebsocket::expires_after(std::chrono::seconds timeout)
{
std::visit([&](auto& websocket_ptr) { beast::get_lowest_layer(*websocket_ptr).expires_after(timeout); }, m_impl->websocket);
}
void MoonrakerWebsocket::abort()
{
std::visit(
[](auto& websocket_ptr) {
beast::error_code error;
beast::get_lowest_layer(*websocket_ptr).socket().shutdown(tcp::socket::shutdown_both, error);
},
m_impl->websocket);
}
const std::string MoonrakerPrinterAgent_VERSION = "1.0.0";
bool moonraker_is_light_name(const std::string& name)
@@ -217,12 +347,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
// why: Moonraker/print-host serves plain HTTP (nginx :80 or Moonraker :7125), never
// https:443; MachineObject::connect defaults use_ssl=true -> forced https -> refused.
// Pin http. (matches feature/printer-agent-port-pristine)
use_ssl = false;
std::string base_url;
std::string api_key;
uint64_t gen;
@@ -1229,7 +1353,7 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman
}
WsEndpoint endpoint;
if (!parse_ws_endpoint(base_url, endpoint) || endpoint.secure) {
if (!parse_ws_endpoint(base_url, endpoint)) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url="
<< base_url;
return false;
@@ -1251,36 +1375,26 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman
for (const auto& port : ports) {
try {
net::io_context ioc;
tcp::resolver resolver{ioc};
beast::tcp_stream stream{ioc};
stream.expires_after(std::chrono::seconds(5));
stream.connect(resolver.resolve(endpoint.host, port));
websocket::stream<beast::tcp_stream> ws{std::move(stream)};
ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) {
req.set(http::field::user_agent, "OrcaSlicer");
if (!api_key.empty()) {
req.set("X-Api-Key", api_key);
}
}));
MoonrakerWebsocket ws{endpoint.secure, api_key};
ws.connect(endpoint.host, port, std::chrono::seconds(5));
ws.tls_handshake(endpoint.host);
std::string host_header = endpoint.host;
if (!port.empty() && port != "80") {
if (!port.empty() && port != (endpoint.secure ? "443" : "80")) {
host_header += ":" + port;
}
ws.handshake(host_header, endpoint.target);
ws.text(true);
ws.write(net::buffer(body));
ws.write(body);
ws.next_layer().expires_after(std::chrono::seconds(2));
beast::flat_buffer buffer;
beast::error_code read_ec;
ws.read(buffer, read_ec);
ws.expires_after(std::chrono::seconds(2));
std::string response;
std::string read_error;
ws.read(response, read_error);
beast::error_code close_ec;
ws.close(websocket::close_code::normal, close_ec);
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over ws to "
ws.close();
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over "
<< (endpoint.secure ? "wss" : "ws") << " to "
<< endpoint.host << ":" << port;
return true;
} catch (const std::exception& e) {
@@ -1732,11 +1846,6 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url;
return;
}
if (endpoint.secure) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url;
return;
}
// Reconnection logic
ws_reconnect_requested.store(false); // Reset reconnect flag
int retry_count = 0;
@@ -1747,15 +1856,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect
try {
net::io_context ioc;
tcp::resolver resolver{ioc};
beast::tcp_stream stream{ioc};
stream.expires_after(std::chrono::seconds(10));
auto const results = resolver.resolve(endpoint.host, endpoint.port);
stream.connect(results);
websocket::stream<beast::tcp_stream> ws{std::move(stream)};
MoonrakerWebsocket ws{endpoint.secure, api_key};
ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10));
ws.tls_handshake(endpoint.host);
// Allow stop_status_stream() to force this socket shut so a blocked
// synchronous ws.read()/ws.write() returns with an error (Beast's
@@ -1770,20 +1873,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
{
std::lock_guard<std::mutex> lock(ws_abort_mutex);
ws_abort_io = [&ws] {
beast::error_code ec;
ws.next_layer().socket().shutdown(tcp::socket::shutdown_both, ec);
ws.abort();
};
}
ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) {
req.set(http::field::user_agent, "OrcaSlicer");
if (!api_key.empty()) {
req.set("X-Api-Key", api_key);
}
}));
std::string host_header = endpoint.host;
if (!endpoint.port.empty() && endpoint.port != "80") {
if (!endpoint.port.empty() && endpoint.port != (endpoint.secure ? "443" : "80")) {
host_header += ":" + endpoint.port;
}
ws.handshake(host_header, endpoint.target);
@@ -1798,7 +1893,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
identify["params"]["type"] = "agent";
identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer";
identify["id"] = 0;
ws.write(net::buffer(identify.dump()));
ws.write(identify.dump());
std::set<std::string> subscribe_objects = {"print_stats", "virtual_sdcard"};
std::set<std::string> available_objects;
@@ -1850,7 +1945,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
}
subscribe["params"]["objects"] = std::move(objects);
subscribe["id"] = 1;
ws.write(net::buffer(subscribe.dump()));
ws.write(subscribe.dump());
// Eager fetch so AMS data is available immediately after connecting,
// without waiting on the loop's own refresh clock below.
@@ -1862,11 +1957,11 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
while (!ws_stop.load()) {
on_status_loop_tick(dev_id);
ws.next_layer().expires_after(std::chrono::seconds(2));
beast::flat_buffer buffer;
beast::error_code ec;
ws.read(buffer, ec);
if (ec == beast::error::timeout) {
ws.expires_after(std::chrono::seconds(2));
std::string payload;
std::string read_error;
const auto read_result = ws.read(payload, read_error);
if (read_result == MoonrakerWebsocket::ReadResult::timeout) {
const auto now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
const auto last_ms = ws_last_emit_ms.load();
@@ -1881,12 +1976,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
}
continue;
}
if (ec == websocket::error::closed) {
if (read_result == MoonrakerWebsocket::ReadResult::closed) {
connection_lost = true;
break;
}
if (ec) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message();
if (read_result == MoonrakerWebsocket::ReadResult::error) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << read_error;
connection_lost = true;
break;
}
@@ -1901,7 +1996,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
ams_last_fetch_ms.store(now_ms);
}
}
handle_ws_message(dev_id, beast::buffers_to_string(buffer.data()), base_url, api_key);
handle_ws_message(dev_id, std::move(payload), base_url, api_key);
// Check if handle_ws_message triggered reconnection request`
if (ws_reconnect_requested.exchange(false)) {
connection_lost = true;
@@ -1909,8 +2004,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
}
}
beast::error_code ec;
ws.close(websocket::close_code::normal, ec);
ws.close();
// Only reset retry count on clean shutdown (not connection_lost)
if (!connection_lost && !ws_stop.load()) {
+34 -1
View File
@@ -10,6 +10,7 @@
#include <set>
#include <string>
#include <thread>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
@@ -20,6 +21,35 @@ namespace Slic3r {
bool moonraker_is_light_name(const std::string& name);
class MoonrakerWebsocket
{
public:
enum class ReadResult
{
message,
timeout,
closed,
error,
};
MoonrakerWebsocket(bool secure, std::string api_key);
~MoonrakerWebsocket();
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
void tls_handshake(const std::string& host);
void handshake(const std::string& host, const std::string& target);
void text(bool enabled);
void write(const std::string& body);
ReadResult read(std::string& payload, std::string& error_message);
void close();
void expires_after(std::chrono::seconds timeout);
void abort();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
class MoonrakerPrinterAgent : public IPrinterAgent
{
public:
@@ -127,6 +157,10 @@ protected:
virtual void on_status_loop_tick(const std::string& dev_id) {}
// Queue work that may use agent state. The command worker is joined during
// destruction, so queued commands cannot outlive the agent.
void enqueue_command(std::function<void()> fn);
private:
int handle_request(const std::string& dev_id, const std::string& json_str);
int send_version_info(const std::string& dev_id);
@@ -242,7 +276,6 @@ private:
std::thread connect_thread;
mutable std::recursive_mutex connect_mutex;
void enqueue_command(std::function<void()> fn);
void run_command_worker();
std::thread cmd_thread;
std::deque<std::function<void()>> cmd_queue;
+6 -6
View File
@@ -953,11 +953,11 @@ std::string NetworkAgent::get_user_selected_machine()
int NetworkAgent::set_user_selected_machine(std::string dev_id)
{
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id
<< " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: result=" << result;
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent";
@@ -980,11 +980,11 @@ int NetworkAgent::stop_subscribe(std::string module)
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
{
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
<< " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->add_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: result=" << result;
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent";
@@ -993,11 +993,11 @@ int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
{
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
<< " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->del_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: result=" << result;
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent";
@@ -1044,6 +1044,12 @@ int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& de
OrcaMqttConnection::StateHandler state_handler)
{
(void) dev_id;
if (!ensure_token_fresh("configure_selected_printer_mqtt"))
{
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
return BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
OrcaMqttConnection::Config cfg;
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
cfg.use_tls = true;
+13 -8
View File
@@ -778,14 +778,8 @@ int OrcaPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string t
return route_send(lan_mode, dev_id, j.dump());
}
int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
std::string OrcaPrinterAgent::build_ams_change_filament_body(int tray_number, int sequence_id)
{
int tray_number = 0;
if (!parse_nonnegative_command_id(tray_id, tray_number)) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid AMS target tray id=" << tray_id;
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
nlohmann::json j;
j["print"]["command"] = "ams_change_filament";
j["print"]["sequence_id"] = std::to_string(sequence_id);
@@ -795,7 +789,18 @@ int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tr
j["print"]["selector"] = "lane";
j["print"]["ams_id"] = tray_number / 4;
j["print"]["slot_id"] = tray_number % 4;
return route_send(lan_mode, dev_id, j.dump());
return j.dump();
}
int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
int tray_number = 0;
if (!parse_nonnegative_command_id(tray_id, tray_number)) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent: invalid AMS target tray id=" << tray_id;
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
return route_send(lan_mode, dev_id, build_ams_change_filament_body(tray_number, sequence_id));
}
int OrcaPrinterAgent::command_set_bed(std::string dev_id, int temp, bool /*supports_mqtt_bed_ctrl*/, int sequence_id, bool lan_mode)
+4
View File
@@ -115,6 +115,10 @@ public:
// the live route_send path).
static std::string canonicalize_ams_payload(const std::string& dev_id, const std::string& json_str, bool* unsupported);
// Test-only: the canonical ams_change_filament body for a BBL tray id
// (ams_id*4 + tray). command_ams_select_tray routes this exact body.
static std::string build_ams_change_filament_body(int tray_number, int sequence_id);
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
+2 -2
View File
@@ -82,10 +82,10 @@ SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPri
void SnapmakerPrinterAgent::start_camera_monitor()
{
std::thread([this] {
enqueue_command([this] {
send_ws_rpc("camera.start_monitor",
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
}).detach();
});
m_camera_last_fire_ms.store(now_ms());
}
+132 -28
View File
@@ -7,8 +7,10 @@
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>
#include <slic3r/GUI/MsgDialog.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <slic3r/GUI/DockPanel.hpp>
#include <slic3r/GUI/PluginProgressDialog.hpp>
#include <slic3r/GUI/PluginWebDialog.hpp>
#include <slic3r/GUI/WebDialog.hpp>
#include <slic3r/GUI/NotificationManager.hpp>
#include <nlohmann/json.hpp>
@@ -20,6 +22,7 @@
#include <wx/defs.h>
#include <wx/window.h>
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <future>
@@ -73,7 +76,7 @@ CallablePtr make_holder(py::object obj)
// Adapt a Python callable to a GUI message handler that acquires the GIL and
// swallows/logs exceptions (a raising handler must not escape into wx events).
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
GUI::WebDialog::MessageHandler make_message_adapter(py::object on_message)
{
CallablePtr holder = make_holder(std::move(on_message));
if (!holder)
@@ -91,7 +94,7 @@ GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
};
}
GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
GUI::WebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
{
CallablePtr holder = make_holder(std::move(on_submit));
if (!holder)
@@ -109,6 +112,25 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
};
}
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
// window is alive. Empty if the plugin passed None.
std::function<void()> make_close_adapter(const CallablePtr& holder)
{
if (!holder)
return nullptr;
return [holder]() {
PythonGILState gil;
if (!gil)
return;
try {
holder->fn();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
PyErr_Clear();
}
};
}
// --------------------------------------------------------------------------
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
// owning plugin so all of a plugin's UI can be torn down on unload.
@@ -350,28 +372,13 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
if (!UiRegistry::instance().is_open(new_id))
return;
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
// teardown), while the dialog is alive. Empty if the plugin passed None.
GUI::PluginWebDialog::CloseHandler on_close;
if (close_holder) {
on_close = [close_holder]() {
PythonGILState gil;
if (!gil)
return;
try {
close_holder->fn();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
PyErr_Clear();
}
};
}
GUI::WebDialog::CloseHandler on_close = make_close_adapter(close_holder);
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
UiRegistry::instance().bind(new_id, dlg, plugin_key);
if (modal) {
dlg->ShowModal();
@@ -387,14 +394,69 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
return py::cast(UiWindowHandle{new_id});
}
// --------------------------------------------------------------------------
// orca.host.ui.create_dock_panel + UiDockPanel handle
// --------------------------------------------------------------------------
constexpr const char* DOCK_POSITIONS[] = {"left", "right", "bottom", "float"};
struct UiDockPanelHandle
{
int id{0};
};
py::object ui_create_dock_panel(const std::string& html, const std::string& title, int width, int height,
py::object on_message, py::object on_close, const std::string& dock)
{
if (std::find(std::begin(DOCK_POSITIONS), std::end(DOCK_POSITIONS), dock) == std::end(DOCK_POSITIONS))
throw std::invalid_argument("orca.host.ui.create_dock_panel dock must be \"left\", \"right\", \"bottom\" or \"float\"");
auto msg_adapter = make_message_adapter(std::move(on_message));
CallablePtr close_holder = make_holder(std::move(on_close));
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
const int w = width > 0 ? width : 320;
const int h = height > 0 ? height : 480;
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
// Deferred and pre-bound for the same reasons as create_window().
const int new_id = UiRegistry::instance().reserve_id();
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
msg_adapter = std::move(msg_adapter),
close_holder = std::move(close_holder)]() mutable {
if (!UiRegistry::instance().is_open(new_id))
return;
GUI::Plater* plater = GUI::wxGetApp().plater();
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
UiRegistry::instance().remove(new_id);
return;
}
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
std::move(on_destroyed));
UiRegistry::instance().bind(new_id, panel, plugin_key);
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
wxSize(w, h), [panel]() { panel->fire_close(); });
});
return py::cast(UiDockPanelHandle{new_id});
}
void handle_post(int id, py::object data)
{
if (wxTheApp == nullptr)
return;
json j = py_to_json(data); // GIL held (binding body)
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::post_message(d, j);
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->push_message(j);
else
GUI::WebDialog::post_message(dynamic_cast<GUI::WebDialog*>(window), j);
});
}
@@ -403,8 +465,23 @@ void handle_close(int id)
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([id]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::request_close(d);
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->request_close();
else
GUI::WebDialog::request_close(dynamic_cast<GUI::WebDialog*>(window));
});
}
void handle_show(int id, bool show)
{
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([id, show]() {
auto* panel = UiRegistry::instance().get_as<GUI::DockPanel>(id);
GUI::Plater* plater = GUI::wxGetApp().plater();
if (panel != nullptr && plater != nullptr)
plater->show_dock_pane(panel, show);
});
}
@@ -563,6 +640,31 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
py::class_<UiDockPanelHandle>(ui, "UiDockPanel", "Handle to a dockable plugin HTML panel created by create_dock_panel().")
.def_property_readonly("id", [](const UiDockPanelHandle& h) { return h.id; })
.def(
"post", [](const UiDockPanelHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
.def(
"show", [](const UiDockPanelHandle& h) { handle_show(h.id, true); }, "Show the panel again after hide().")
.def(
"hide", [](const UiDockPanelHandle& h) { handle_show(h.id, false); }, "Hide the panel without closing it.")
.def(
"close", [](const UiDockPanelHandle& h) { handle_close(h.id); }, "Close the panel (fires on_close).")
.def(
"is_open", [](const UiDockPanelHandle& h) { return UiRegistry::instance().is_open(h.id); },
"Return True until the panel is closed; a hidden panel is still open.");
ui.def("create_dock_panel", &ui_create_dock_panel, py::arg("html"), py::arg("title") = "OrcaSlicer",
py::arg("width") = 320, py::arg("height") = 480, py::arg("on_message") = py::none(),
py::arg("on_close") = py::none(), py::arg("dock") = "right",
"Open an HTML panel docked beside the 3D view and return a UiDockPanel. dock is \"left\", \"right\", "
"\"bottom\" or \"float\", and width/height are in DIPs; the user can move and resize the panel, and a panel "
"opened again comes back where the window layout was last saved. The panel belongs to the Prepare and "
"Preview tabs. on_message(data) is called on the UI thread when the page posts; window.orca.close() "
"or the panel's close button closes it and calls on_close(). A post() made before the page has "
"loaded can be dropped, so have the page request its first data.");
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
@@ -630,8 +732,10 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
// forced teardown (intended); the resource destructor still cleans the registry.
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
GUI::PluginWebDialog::destroy_for_plugin(dialog);
if (auto* dialog = dynamic_cast<GUI::WebDialog*>(window))
GUI::WebDialog::destroy_silently(dialog);
else if (auto* panel = dynamic_cast<GUI::DockPanel*>(window))
panel->destroy_silently();
else if (window != nullptr)
window->Destroy();
}
+19 -79
View File
@@ -1,17 +1,12 @@
#include "PluginPages.hpp"
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/WebView.hpp"
#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/Utils.hpp>
#include <algorithm>
#include <boost/filesystem/path.hpp>
@@ -66,27 +61,11 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
} // namespace
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
, m_cap(std::move(capability))
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
{
auto* topsizer = new wxBoxSizer(wxVERTICAL);
SetSizer(topsizer);
m_browser = WebView::CreateWebView(this, bootstrap_url());
if (m_browser == nullptr) {
wxLogError("Could not initialize plugin page web view");
return;
}
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this);
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this);
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this);
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script()));
m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS);
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
m_cap->set_message_sender([lifetime](const std::string& message) {
@@ -117,89 +96,54 @@ void PluginPage::detach_capability()
m_cap.reset();
}
wxString PluginPage::web_base_url() const
std::optional<std::string> PluginPage::page_html()
{
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + GUI::from_u8(path) + "/";
}
if (m_cap == nullptr)
return std::nullopt;
wxString PluginPage::bootstrap_url() const
{
const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string();
return wxString("file://") + GUI::from_u8(path);
}
void PluginPage::on_bootstrap_event(wxWebViewEvent& event)
{
load_plugin_content();
event.Skip();
}
void PluginPage::load_plugin_content()
{
if (m_content_loaded || m_browser == nullptr || m_cap == nullptr)
return;
m_content_loaded = true;
try {
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
return m_cap->get_ui();
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
detach_capability();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
detach_capability();
}
detach_capability();
return std::nullopt;
}
void PluginPage::on_new_window(wxWebViewEvent& event)
{
const wxString url = event.GetURL();
if (!url.empty() && m_browser != nullptr)
m_browser->LoadURL(url);
if (!url.empty())
browser()->LoadURL(url);
event.Veto();
}
void PluginPage::on_script_message(wxWebViewEvent& event)
bool PluginPage::on_page_message(const std::string& kind, const nlohmann::json& data)
{
if (kind != "message")
return false;
if (!m_cap)
return;
return true;
const wxString payload = event.GetString();
nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false);
if (root.is_discarded() || root.value("channel", std::string()) != "orca" ||
root.value("kind", std::string()) != "message")
return;
const auto data = root.find("data");
try {
m_cap->on_message(data == root.end()
? "null"
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
}
return true;
}
void PluginPage::push_message(const std::string& message)
{
if (m_browser == nullptr)
return;
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
// non-JSON payload needs wrapping as a string literal.
const std::string payload = nlohmann::json::accept(message)
? message
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
WebView::RunScript(m_browser, wxString::Format(
"(function dispatch(payload, attempts) {\n"
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
"})({data: %s}, 0);",
wxString::FromUTF8(payload)));
post_to_page(nlohmann::json::accept(message)
? message
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
}
PluginPages::~PluginPages()
@@ -268,10 +212,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
}
auto* page = new PluginPage(m_parent, std::move(capability));
if (!page->is_valid()) {
page->Destroy();
return false;
}
if (!icon.empty()) {
try {
+7 -11
View File
@@ -1,5 +1,6 @@
#pragma once
#include <slic3r/GUI/WebPanel.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
@@ -11,14 +12,13 @@
#include <vector>
#include <wx/bitmap.h>
#include <wx/panel.h>
#include <wx/webview.h>
class Notebook;
namespace Slic3r {
class PluginPage : public wxPanel
class PluginPage : public GUI::WebPanel
{
public:
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
@@ -26,24 +26,20 @@ public:
PluginPage() = delete;
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
void detach_capability();
void on_bootstrap_event(wxWebViewEvent& event);
void on_new_window(wxWebViewEvent& event);
void on_script_message(wxWebViewEvent& event);
void push_message(const std::string& message);
void set_icon(const wxBitmap& icon) { m_icon = icon; }
const wxBitmap& icon() const { return m_icon; }
protected:
std::optional<std::string> page_html() override;
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
private:
void load_plugin_content();
wxString bootstrap_url() const;
wxString web_base_url() const;
void on_new_window(wxWebViewEvent& event);
wxWebView* m_browser{nullptr};
std::shared_ptr<PagesPluginCapability> m_cap;
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
bool m_content_loaded{false};
wxBitmap m_icon;
};
@@ -10,7 +10,37 @@
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <type_traits>
// IPrinterAgent reports failure through its return values and its callers do not catch, so nothing
// the plugin does may leave the trampoline as an exception: a Python raise, a missing override or a
// wrongly typed return is logged and answered with what NetworkAgent returns when no agent is set.
#define ORCA_PY_AGENT_CATCH(name) \
catch (const std::exception& ex) { this->log_failure(#name, ex.what()); } \
catch (...) { this->log_failure(#name, "unknown error"); }
#define ORCA_PY_AGENT_OVERRIDE(ret, name, ...) \
try { \
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, ret, PrinterAgentPluginCapability, name, ##__VA_ARGS__); \
} ORCA_PY_AGENT_CATCH(name) \
return printer_agent_failure<ret>()
#define ORCA_PY_AGENT_OVERRIDE_DEFAULT(ret, name, ...) \
try { \
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, ret, PrinterAgentPluginCapability, name, ##__VA_ARGS__); \
} ORCA_PY_AGENT_CATCH(name) \
return printer_agent_failure<ret>()
namespace Slic3r {
// NetworkAgent's no-agent answer: -1 for a status code, the empty value (false, "", none) otherwise.
template<typename T> T printer_agent_failure()
{
if constexpr (std::is_same_v<T, int>)
return -1;
else if constexpr (!std::is_void_v<T>)
return T{};
}
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
{
public:
@@ -18,321 +48,259 @@ public:
AgentInfo get_agent_info() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info);
ORCA_PY_AGENT_OVERRIDE(AgentInfo, get_agent_info);
}
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl);
ORCA_PY_AGENT_OVERRIDE(int, connect_printer, dev_id, dev_ip, username, password, use_ssl);
}
int disconnect_printer() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
ORCA_PY_AGENT_OVERRIDE(int, disconnect_printer);
}
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag);
ORCA_PY_AGENT_OVERRIDE(int, send_message, dev_id, json_str, qos, flag);
}
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag);
ORCA_PY_AGENT_OVERRIDE(int, send_message_to_printer, dev_id, json_str, qos, flag);
}
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_refresh_rfid,
dev_id, tray_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_refresh_rfid, dev_id, tray_id, sequence_id, lan_mode);
}
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_calibrate,
dev_id, ams_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_calibrate, dev_id, ams_id, sequence_id, lan_mode);
}
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_ams_select_tray,
dev_id, tray_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_select_tray, dev_id, tray_id, sequence_id, lan_mode);
}
int command_start_camera(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_start_camera, dev_id);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_start_camera, dev_id);
}
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_xyz_abs,
dev_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_xyz_abs, dev_id, sequence_id, lan_mode);
}
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_auto_leveling,
dev_id, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_auto_leveling, dev_id, sequence_id, lan_mode);
}
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_go_home,
dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_go_home, dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
}
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_set_bed,
dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_bed, dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
}
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_set_nozzle,
dev_id, temp, sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_nozzle, dev_id, temp, sequence_id, lan_mode);
}
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val,
int speed, bool is_core_xy, bool supports_mqtt_axis_control,
int sequence_id, bool lan_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, command_axis_control,
dev_id, axis, unit, input_val, speed, is_core_xy, supports_mqtt_axis_control,
sequence_id, lan_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_axis_control, dev_id, axis, unit, input_val, speed,
is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode);
}
bool start_discovery(bool start, bool sending) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending);
ORCA_PY_AGENT_OVERRIDE(bool, start_discovery, start, sending);
}
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect);
// Passed as a pointer: pybind11 copies a reference argument, so the plugin's writes would be lost.
ORCA_PY_AGENT_OVERRIDE(int, bind_detect, dev_ip, sec_link, &detect);
}
std::string get_user_selected_machine() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine);
ORCA_PY_AGENT_OVERRIDE(std::string, get_user_selected_machine);
}
int set_user_selected_machine(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id);
ORCA_PY_AGENT_OVERRIDE(int, set_user_selected_machine, dev_id);
}
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_local_print, params, update_fn, cancel_fn);
}
FilamentSyncMode get_filament_sync_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(FilamentSyncMode, get_filament_sync_mode);
}
CameraStreamMode get_camera_stream_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, CameraStreamMode, PrinterAgentPluginCapability,
get_camera_stream_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(CameraStreamMode, get_camera_stream_mode);
}
std::string get_camera_url() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, std::string, PrinterAgentPluginCapability, get_camera_url);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(std::string, get_camera_url);
}
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id, sync_mode);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(bool, fetch_filament_info, dev_id, sync_mode);
}
int check_cert() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, check_cert);
ORCA_PY_AGENT_OVERRIDE(int, check_cert);
}
void install_device_cert(std::string dev_id, bool lan_only) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(void, install_device_cert, dev_id, lan_only);
}
int ping_bind(std::string ping_code) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
ORCA_PY_AGENT_OVERRIDE(int, ping_bind, ping_code);
}
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn);
ORCA_PY_AGENT_OVERRIDE(int, bind, dev_ip, dev_id, dev_model, sec_link, timezone, improved, update_fn);
}
int unbind(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, unbind, dev_id);
ORCA_PY_AGENT_OVERRIDE(int, unbind, dev_id);
}
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_print, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
}
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn);
ORCA_PY_AGENT_OVERRIDE(int, start_sdcard_print, params, update_fn, cancel_fn);
}
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback);
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, get_hms_snapshot, dev_id, file_name, callback);
}
int set_server_callback(OnServerErrFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_server_callback, fn);
}
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_ssdp_msg_fn, fn);
}
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_printer_connected_fn, fn);
}
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_subscribe_failure_fn, fn);
}
int set_on_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_message_fn, fn);
}
int set_on_user_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_user_message_fn, fn);
}
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_local_connect_fn, fn);
}
int set_on_local_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_on_local_message_fn, fn);
}
int set_queue_on_main_fn(QueueOnMainFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
ORCA_PY_AGENT_OVERRIDE(int, set_queue_on_main_fn, fn);
}
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
// cannot marshal back through a plain override. We dispatch manually: the Python plugin
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
// Not required to be implemented, so a missing override falls back to the base default
// instead of failing, mirroring what PYBIND11_OVERRIDE does for the other optional methods.
// Not required to be implemented, so a missing override answers with the same failure value
// as the other printer-agent operations. Leave the out-param untouched on failure.
int request_bind_ticket(std::string* ticket) override
{
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
return PrinterAgentPluginCapability::request_bind_ticket(ticket);
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
*ticket = result[1].cast<std::string>();
return result[0].cast<int>();
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
return printer_agent_failure<int>();
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
*ticket = result[1].cast<std::string>();
return result[0].cast<int>();
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
} ORCA_PY_AGENT_CATCH(request_bind_ticket)
return printer_agent_failure<int>();
}
private:
void log_failure(const char* operation, const char* error) const
{
BOOST_LOG_TRIVIAL(error) << "Printer agent plugin '" << this->audit_plugin_key() << "': " << operation << " failed: " << error;
}
};
} // namespace Slic3r