mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 10:21:00 +00:00
Merge branch 'main' into feat/plugin-lifecycle-evts
This commit is contained in:
@@ -283,9 +283,17 @@ void AppConfig::set_defaults()
|
||||
set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap));
|
||||
}
|
||||
|
||||
if (get(SETTING_OPENGL_SCENE_CACHE).empty())
|
||||
set_bool(SETTING_OPENGL_SCENE_CACHE, true);
|
||||
|
||||
if (get(SETTING_OPENGL_SKIP_IDENTICAL_FRAMES).empty())
|
||||
set_bool(SETTING_OPENGL_SKIP_IDENTICAL_FRAMES, true);
|
||||
|
||||
// The getter already defaults, parses and clamps; write back what it resolves to.
|
||||
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
|
||||
|
||||
set(SETTING_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count()));
|
||||
|
||||
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
|
||||
|
||||
@@ -1684,6 +1692,22 @@ int AppConfig::get_plugin_pages_visible_count() const
|
||||
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
|
||||
}
|
||||
|
||||
int AppConfig::get_speed_dial_recent_count() const
|
||||
{
|
||||
std::string value = get(SETTING_SPEED_DIAL_RECENT_COUNT);
|
||||
if (value.empty())
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
|
||||
int recent_count = SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
try {
|
||||
recent_count = std::stoi(value);
|
||||
}
|
||||
catch (...) {
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
}
|
||||
return std::clamp(recent_count, SPEED_DIAL_RECENT_COUNT_MIN, SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_skipped_network_versions() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
|
||||
@@ -33,6 +33,8 @@ using namespace nlohmann;
|
||||
#define SETTING_OPENGL_AA_SAMPLES "opengl_antialiasing_samples"
|
||||
#define SETTING_OPENGL_FXAA_ENABLED "opengl_fxaa_enabled"
|
||||
#define SETTING_OPENGL_FPS_CAP "opengl_fps_cap"
|
||||
#define SETTING_OPENGL_SCENE_CACHE "opengl_scene_cache"
|
||||
#define SETTING_OPENGL_SKIP_IDENTICAL_FRAMES "opengl_skip_identical_frames"
|
||||
#define SETTING_OPENGL_SHOW_FPS_OVERLAY "opengl_show_fps_overlay"
|
||||
#define SETTING_OPENGL_REALISTIC_MODE "opengl_realistic_mode"
|
||||
#define SETTING_OPENGL_REALISTIC_PHONG "opengl_realistic_phong"
|
||||
@@ -46,6 +48,11 @@ using namespace nlohmann;
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
|
||||
|
||||
#define SETTING_SPEED_DIAL_RECENT_COUNT "speed_dial_recent_count"
|
||||
#define SPEED_DIAL_RECENT_COUNT_MIN 0
|
||||
#define SPEED_DIAL_RECENT_COUNT_DEFAULT 5
|
||||
#define SPEED_DIAL_RECENT_COUNT_MAX 10
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
|
||||
#else
|
||||
@@ -394,6 +401,9 @@ public:
|
||||
// dropdown on the last tab.
|
||||
int get_plugin_pages_visible_count() const;
|
||||
|
||||
// Number of recently launched actions shown at the top of the Speed Dial; 0 hides them.
|
||||
int get_speed_dial_recent_count() const;
|
||||
|
||||
std::vector<std::string> get_skipped_network_versions() const;
|
||||
void add_skipped_network_version(const std::string& version);
|
||||
bool is_network_version_skipped(const std::string& version) const;
|
||||
|
||||
@@ -776,7 +776,9 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key, double rati
|
||||
{
|
||||
// Get stored option value.
|
||||
const ConfigOption *raw_opt = this->option(opt_key);
|
||||
assert(raw_opt != nullptr);
|
||||
// Mirror the single-arg overload — assert() is a no-op under NDEBUG.
|
||||
if (raw_opt == nullptr)
|
||||
throw ConfigurationError("ConfigBase::get_abs_value(): \"" + opt_key + "\" is not defined");
|
||||
if (raw_opt->type() != coFloatOrPercent)
|
||||
throw ConfigurationError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent");
|
||||
// Compute absolute value.
|
||||
|
||||
+11
-3
@@ -2379,9 +2379,9 @@ namespace DoExport {
|
||||
static const unsigned int MAX_TAGS_COUNT = 5;
|
||||
std::vector<std::pair<std::string, std::string>> ret;
|
||||
|
||||
auto check = [&ret](const std::string& source, const std::string& gcode) {
|
||||
auto check = [&ret, is_bbl_printer = print.is_BBL_printer()](const std::string& source, const std::string& gcode) {
|
||||
std::vector<std::string> tags;
|
||||
if (GCodeProcessor::contains_reserved_tags(gcode, MAX_TAGS_COUNT, tags)) {
|
||||
if (GCodeProcessor::contains_reserved_tags(gcode, MAX_TAGS_COUNT, tags, is_bbl_printer)) {
|
||||
if (!tags.empty()) {
|
||||
size_t i = 0;
|
||||
while (ret.size() < MAX_TAGS_COUNT && i < tags.size()) {
|
||||
@@ -3073,6 +3073,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// file_start_gcode runs before the parser copy that normally restores these, so set them here.
|
||||
PlaceholderParser::update_timestamp(top_config);
|
||||
PlaceholderParser::update_user_name(top_config);
|
||||
top_config.set_key_value("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
|
||||
top_config.set_key_value("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
|
||||
top_config.set_key_value("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
|
||||
top_config.set_key_value("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||
top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||
top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config);
|
||||
@@ -3702,6 +3706,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->placeholder_parser().set("hold_chamber_temp_for_flat_print", new ConfigOptionBool(hold_chamber_temp_for_flat_print));
|
||||
}
|
||||
|
||||
this->placeholder_parser().set("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
|
||||
@@ -5669,7 +5677,7 @@ LayerResult GCode::process_layer(
|
||||
m_layer = &layer;
|
||||
m_object_layer_over_raft = false;
|
||||
|
||||
if (!m_config.time_lapse_gcode.value.empty() && !is_BBL_Printer()) {
|
||||
if (!need_insert_timelapse_gcode_for_traditional && !m_config.time_lapse_gcode.value.empty() && !is_BBL_Printer()) {
|
||||
DynamicConfig config;
|
||||
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
|
||||
config.set_key_value("layer_z", new ConfigOptionFloat(print_z));
|
||||
|
||||
@@ -710,6 +710,7 @@ private:
|
||||
|
||||
// Always check gcode placeholders when building in debug mode.
|
||||
#if !defined(NDEBUG)
|
||||
#undef ORCA_CHECK_GCODE_PLACEHOLDERS
|
||||
#define ORCA_CHECK_GCODE_PLACEHOLDERS 1
|
||||
#endif
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
|
||||
" WIPE_TOWER_START",
|
||||
" WIPE_TOWER_END",
|
||||
" PA_CHANGE:",
|
||||
"@PRINT_TIME_TOTAL_SEC@",
|
||||
"@PRINT_TIME_DAY@",
|
||||
"@PRINT_TIME_HOUR@",
|
||||
"@PRINT_TIME_MINUTE@",
|
||||
"@PRINT_TIME_SEC@",
|
||||
"@USED_FILAMENT_LENGTH@"
|
||||
};
|
||||
@@ -98,6 +102,10 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
||||
" WIPE_TOWER_START",
|
||||
" WIPE_TOWER_END",
|
||||
" PA_CHANGE:",
|
||||
"@PRINT_TIME_TOTAL_SEC@",
|
||||
"@PRINT_TIME_DAY@",
|
||||
"@PRINT_TIME_HOUR@",
|
||||
"@PRINT_TIME_MINUTE@",
|
||||
"@PRINT_TIME_SEC@",
|
||||
"@USED_FILAMENT_LENGTH@"
|
||||
};
|
||||
@@ -1215,22 +1223,76 @@ void GCodeProcessor::run_post_process()
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Process inline placeholders (print_time_sec and used_filament_length)
|
||||
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
|
||||
auto process_inline_placeholders = [&](std::string& gcode_line) {
|
||||
bool processed = false;
|
||||
|
||||
const std::string& print_time_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
|
||||
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
|
||||
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
|
||||
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
|
||||
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
|
||||
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
|
||||
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
|
||||
|
||||
// Replace print_time_sec
|
||||
size_t pos = gcode_line.find(print_time_placeholder);
|
||||
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
||||
if (print_time_total_sec < 0.0)
|
||||
print_time_total_sec = 0.0;
|
||||
|
||||
int total_seconds = static_cast<int>(print_time_total_sec);
|
||||
int print_time_day = total_seconds / 86400;
|
||||
int day_remainder_seconds = total_seconds % 86400;
|
||||
int print_time_hour = day_remainder_seconds / 3600;
|
||||
int print_time_minute = (day_remainder_seconds % 3600) / 60;
|
||||
int print_time_sec = day_remainder_seconds % 60;
|
||||
|
||||
// Replace print_time_total_sec
|
||||
size_t pos = gcode_line.find(print_time_total_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
double print_time_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
||||
char buf[64];
|
||||
sprintf(buf, "%.2f", print_time_sec);
|
||||
gcode_line.replace(pos, print_time_placeholder.length(), buf);
|
||||
sprintf(buf, "%.2f", print_time_total_sec);
|
||||
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_placeholder, pos + strlen(buf));
|
||||
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_day
|
||||
pos = gcode_line.find(print_time_day_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_day);
|
||||
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_hour
|
||||
pos = gcode_line.find(print_time_hour_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_hour);
|
||||
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_minute
|
||||
pos = gcode_line.find(print_time_minute_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_minute);
|
||||
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace print_time_sec
|
||||
pos = gcode_line.find(print_time_sec_placeholder);
|
||||
while (pos != std::string::npos) {
|
||||
char buf[64];
|
||||
sprintf(buf, "%d", print_time_sec);
|
||||
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
|
||||
processed = true;
|
||||
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
|
||||
}
|
||||
|
||||
// Replace used_filament_length
|
||||
@@ -2609,7 +2671,7 @@ bool GCodeProcessor::contains_reserved_tag(const std::string& gcode, std::string
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool GCodeProcessor::contains_reserved_tags(const std::string& gcode, unsigned int max_count, std::vector<std::string>& found_tag)
|
||||
bool GCodeProcessor::contains_reserved_tags(const std::string& gcode, unsigned int max_count, std::vector<std::string>& found_tag, bool is_bbl_printer)
|
||||
{
|
||||
max_count = std::max(max_count, 1U);
|
||||
|
||||
@@ -2618,7 +2680,7 @@ bool GCodeProcessor::contains_reserved_tags(const std::string& gcode, unsigned i
|
||||
CNumericLocalesSetter locales_setter;
|
||||
|
||||
GCodeReader parser;
|
||||
auto& _tags = s_IsBBLPrinter ? Reserved_Tags : Reserved_Tags_compatible;
|
||||
auto& _tags = is_bbl_printer ? Reserved_Tags : Reserved_Tags_compatible;
|
||||
parser.parse_buffer(gcode, [&ret, &found_tag, max_count, _tags](GCodeReader& parser, const GCodeReader::GCodeLine& line) {
|
||||
std::string comment = line.raw();
|
||||
if (comment.length() > 2 && comment.front() == ';') {
|
||||
|
||||
@@ -512,6 +512,10 @@ class Print;
|
||||
Wipe_Tower_Start,
|
||||
Wipe_Tower_End,
|
||||
PA_Change,
|
||||
Print_Time_Total_Sec_Placeholder,
|
||||
Print_Time_Day_Placeholder,
|
||||
Print_Time_Hour_Placeholder,
|
||||
Print_Time_Minute_Placeholder,
|
||||
Print_Time_Sec_Placeholder,
|
||||
Used_Filament_Length_Placeholder,
|
||||
};
|
||||
@@ -521,7 +525,7 @@ class Print;
|
||||
static bool contains_reserved_tag(const std::string& gcode, std::string& found_tag);
|
||||
// checks the given gcode for reserved tags and returns true when finding any
|
||||
// (the first max_count found tags are returned into found_tag)
|
||||
static bool contains_reserved_tags(const std::string& gcode, unsigned int max_count, std::vector<std::string>& found_tag);
|
||||
static bool contains_reserved_tags(const std::string& gcode, unsigned int max_count, std::vector<std::string>& found_tag, bool is_bbl_printer);
|
||||
|
||||
static int get_gcode_last_filament(const std::string &gcode_str);
|
||||
static bool get_last_z_from_gcode(const std::string& gcode_str, double& z);
|
||||
@@ -1543,5 +1547,3 @@ class Print;
|
||||
} /* namespace Slic3r */
|
||||
|
||||
#endif /* slic3r_GCodeProcessor_hpp_ */
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,12 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
|
||||
const bool need_wipe_tower = smooth_timelapse || wrapping;
|
||||
|
||||
// Fewer than two filaments cannot make a tool change, so only wrapping detection or smooth
|
||||
// timelapse print a tower then. The flush volume is no proof of one: it is read from the
|
||||
// matrix of every configured slot, nonzero even when a single one of them is used.
|
||||
if (filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
// A tower printed for one of the reasons above has no tool change to purge for; both
|
||||
// planners give it the idle depth below and nothing more.
|
||||
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
|
||||
@@ -151,14 +157,6 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
|
||||
}
|
||||
|
||||
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
|
||||
// no tower for one that is built collapses the validation hull to a point.
|
||||
// A tool change is a reason on its own (see the base commit); Type1 already reserves
|
||||
// per filament, Type2 has only the volume, which can resolve to zero.
|
||||
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
|
||||
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
|
||||
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
|
||||
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
|
||||
@@ -171,7 +169,10 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeT
|
||||
if (!purges.empty())
|
||||
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
else {
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
// Type2 squares the tower from its purge volume; Type1 with no purge list (a lone
|
||||
// filament kept for timelapse or wrapping) sizes for the idle depth.
|
||||
const bool has_purge = !type1 && volume > EPSILON;
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
}
|
||||
footprint.width = footprint.depth = side;
|
||||
|
||||
@@ -120,9 +120,6 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
fill_no_overlap
|
||||
);
|
||||
|
||||
if (this->layer()->lower_layer != nullptr)
|
||||
// Cummulative sum of polygons over all the regions.
|
||||
g.lower_slices = &this->layer()->lower_layer->lslices;
|
||||
if (this->layer()->upper_layer != NULL)
|
||||
g.upper_slices = &this->layer()->upper_layer->lslices;
|
||||
|
||||
@@ -135,6 +132,13 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges);
|
||||
g.solid_infill_flow = this->flow(frSolidInfill);
|
||||
|
||||
// Cumulative sum of polygons over all the regions, less what the lower layer could not print.
|
||||
ExPolygons lower_slices;
|
||||
if (this->layer()->lower_layer != nullptr) {
|
||||
lower_slices = g.printable_slices(this->layer()->lower_layer->lslices);
|
||||
g.lower_slices = &lower_slices;
|
||||
}
|
||||
|
||||
if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode)
|
||||
g.process_arachne();
|
||||
else
|
||||
|
||||
@@ -2885,6 +2885,21 @@ bool PerimeterGeneratorLoop::is_internal_contour() const
|
||||
return true;
|
||||
}
|
||||
|
||||
// ORCA: Arachne drops features below min_feature_size, classic builds nothing thinner than a third of the
|
||||
// nozzle. Both describe the layer below, a union of regions sharing neither nozzle nor generator, so every
|
||||
// ambiguity resolves low: it may keep a sliver that was never printed, but it never drops one that was.
|
||||
ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
|
||||
{
|
||||
double min_width = *std::min_element(print_config->nozzle_diameter.values.begin(),
|
||||
print_config->nozzle_diameter.values.end()) / 3.;
|
||||
if (object_config->wall_generator.value == PerimeterGeneratorType::Arachne) {
|
||||
const double min_feature_size = Arachne::make_paths_params(layer_id, *object_config, *print_config).min_feature_size;
|
||||
// Spiral vase can put a classic layer under an Arachne one, so there both limits apply.
|
||||
min_width = print_config->spiral_mode ? std::min(min_width, min_feature_size) : min_feature_size;
|
||||
}
|
||||
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
||||
}
|
||||
|
||||
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
|
||||
{
|
||||
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
|
||||
|
||||
@@ -149,6 +149,8 @@ public:
|
||||
//BBS
|
||||
double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; }
|
||||
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
|
||||
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
|
||||
ExPolygons printable_slices(const ExPolygons &slices) const;
|
||||
|
||||
private:
|
||||
std::vector<Polygons> generate_lower_polygons_series(float width);
|
||||
|
||||
@@ -152,6 +152,7 @@ static t_config_enum_values s_keys_map_PrintHostType {
|
||||
{ "octoprint", htOctoPrint },
|
||||
{ "crealityprint", htCrealityPrint },
|
||||
{ "duet", htDuet },
|
||||
{ "ultimaker", htUltiMaker },
|
||||
{ "flashair", htFlashAir },
|
||||
{ "astrobox", htAstroBox },
|
||||
{ "repetier", htRepetier },
|
||||
@@ -5401,6 +5402,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("prusaconnect");
|
||||
def->enum_values.push_back("octoprint");
|
||||
def->enum_values.push_back("duet");
|
||||
def->enum_values.push_back("ultimaker");
|
||||
def->enum_values.push_back("flashair");
|
||||
def->enum_values.push_back("astrobox");
|
||||
def->enum_values.push_back("repetier");
|
||||
@@ -5417,6 +5419,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back("PrusaConnect");
|
||||
def->enum_labels.push_back("Octo/Klipper");
|
||||
def->enum_labels.push_back("Duet");
|
||||
def->enum_labels.push_back("UltiMaker");
|
||||
def->enum_labels.push_back("FlashAir");
|
||||
def->enum_labels.push_back("AstroBox");
|
||||
def->enum_labels.push_back("Repetier");
|
||||
@@ -6607,7 +6610,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->tooltip = L("G-code written at the very top of the output file, before any other content. "
|
||||
"Useful for adding metadata that printer firmware reads from the first lines of the file "
|
||||
"(e.g. estimated print time, filament usage). "
|
||||
"Supports placeholders like {print_time_sec} and {used_filament_length}.");
|
||||
"Supports placeholders like {print_time_total_sec}, {print_time_day}, {print_time_hour}, {print_time_minute}, {print_time_sec} and {used_filament_length}.");
|
||||
def->multiline = true;
|
||||
def->full_width = true;
|
||||
def->height = 8;
|
||||
@@ -12556,10 +12559,26 @@ PrintStatisticsConfigDef::PrintStatisticsConfigDef()
|
||||
def->label = L("Used filament");
|
||||
def->tooltip = L("Total length of filament used in the print.");
|
||||
|
||||
def = this->add("print_time_sec", coString);
|
||||
def->label = L("Print time (seconds)");
|
||||
def = this->add("print_time_total_sec", coString);
|
||||
def->label = L("Print time (total seconds)");
|
||||
def->tooltip = L("Total estimated print time in seconds. Replaced with actual value during post-processing.");
|
||||
|
||||
def = this->add("print_time_day", coString);
|
||||
def->label = L("Print time (days component)");
|
||||
def->tooltip = L("Estimated print time day component (normal mode). Replaced with actual value during post-processing.");
|
||||
|
||||
def = this->add("print_time_hour", coString);
|
||||
def->label = L("Print time (hours component)");
|
||||
def->tooltip = L("Estimated print time hour component (normal mode). Replaced with actual value during post-processing.");
|
||||
|
||||
def = this->add("print_time_minute", coString);
|
||||
def->label = L("Print time (minutes component)");
|
||||
def->tooltip = L("Estimated print time minute component (normal mode). Replaced with actual value during post-processing.");
|
||||
|
||||
def = this->add("print_time_sec", coString);
|
||||
def->label = L("Print time (seconds component)");
|
||||
def->tooltip = L("Estimated print time second component (normal mode). Replaced with actual value during post-processing.");
|
||||
|
||||
def = this->add("used_filament_length", coString);
|
||||
def->label = L("Filament length (meters)");
|
||||
def->tooltip = L("Total filament length used in meters. Replaced with actual value during post-processing.");
|
||||
|
||||
@@ -98,7 +98,7 @@ enum class WipeTowerType {
|
||||
};
|
||||
|
||||
enum PrintHostType {
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htUltiMaker, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker
|
||||
};
|
||||
|
||||
enum AuthorizationType {
|
||||
|
||||
+14
-3
@@ -11,12 +11,23 @@ namespace Slic3r {
|
||||
float CalibPressureAdvance::find_optimal_PA_speed(const DynamicPrintConfig &config, double line_width, double layer_height, int extruder_id, int filament_idx)
|
||||
{
|
||||
const double general_suggested_min_speed = 100.0;
|
||||
double filament_max_volumetric_speed = config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->get_at(filament_idx);
|
||||
// Read defensively — CLI callers may hand us a config missing optional keys.
|
||||
auto vector_at = [&config](const char *key, int idx) -> double {
|
||||
if (const auto *o = config.option<ConfigOptionFloats>(key)) return o->get_at(idx);
|
||||
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
|
||||
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloats>()->get_at(idx) : 0.0;
|
||||
};
|
||||
auto nullable_at = [&config](const char *key, int idx) -> double {
|
||||
if (const auto *o = config.option<ConfigOptionFloatsNullable>(key)) return o->get_at(idx);
|
||||
const ConfigOptionDef *d = config.def() ? config.def()->get(key) : nullptr;
|
||||
return (d && d->default_value) ? d->get_default_value<ConfigOptionFloatsNullable>()->get_at(idx) : 0.0;
|
||||
};
|
||||
double filament_max_volumetric_speed = vector_at("filament_max_volumetric_speed", filament_idx);
|
||||
// todo multi_extruders:
|
||||
const float nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter")->get_at(extruder_id);
|
||||
const float nozzle_diameter = vector_at("nozzle_diameter", extruder_id);
|
||||
if (line_width <= 0.) line_width = Flow::auto_extrusion_width(frPerimeter, nozzle_diameter);
|
||||
Flow pattern_line = Flow(line_width, layer_height, nozzle_diameter);
|
||||
auto pa_speed = std::min(std::max(general_suggested_min_speed, config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->get_at(extruder_id)),
|
||||
auto pa_speed = std::min(std::max(general_suggested_min_speed, nullable_at("outer_wall_speed", extruder_id)),
|
||||
filament_max_volumetric_speed / pattern_line.mm3_per_mm());
|
||||
|
||||
return std::floor(pa_speed);
|
||||
|
||||
@@ -14,8 +14,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/2DBed.hpp
|
||||
GUI/3DBed.cpp
|
||||
GUI/3DBed.hpp
|
||||
GUI/Widgets/StaticGroup.cpp
|
||||
GUI/Widgets/StaticGroup.hpp
|
||||
GUI/3DScene.cpp
|
||||
GUI/3DScene.hpp
|
||||
GUI/Widgets/FilamentLoad.cpp
|
||||
@@ -125,10 +123,14 @@ 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
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
GUI/NativeCommands.hpp
|
||||
GUI/PluginsConfigDialog.cpp
|
||||
GUI/PluginsConfigDialog.hpp
|
||||
GUI/ProcessRunner.cpp
|
||||
@@ -335,6 +337,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Jobs/Worker.hpp
|
||||
GUI/KBShortcutsDialog.cpp
|
||||
GUI/KBShortcutsDialog.hpp
|
||||
GUI/KeyChord.cpp
|
||||
GUI/KeyChord.hpp
|
||||
GUI/LibVGCode/LibVGCodeWrapper.hpp
|
||||
GUI/LibVGCode/LibVGCodeWrapper.cpp
|
||||
GUI/LinuxDisplayBackend.cpp
|
||||
@@ -469,6 +473,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SavePresetDialog.hpp
|
||||
GUI/SceneRaycaster.cpp
|
||||
GUI/SceneRaycaster.hpp
|
||||
GUI/SceneCache.cpp
|
||||
GUI/SceneCache.hpp
|
||||
GUI/PartSkipCommon.hpp
|
||||
GUI/PartSkipDialog.cpp
|
||||
GUI/PartSkipDialog.hpp
|
||||
@@ -476,6 +482,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/SkipPartCanvas.hpp
|
||||
GUI/Search.cpp
|
||||
GUI/Search.hpp
|
||||
GUI/SettingsIndex.cpp
|
||||
GUI/SettingsIndex.hpp
|
||||
GUI/Selection.cpp
|
||||
GUI/Selection.hpp
|
||||
GUI/SelectMachine.cpp
|
||||
@@ -772,6 +780,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/SimplyPrint.hpp
|
||||
Utils/TCPConsole.cpp
|
||||
Utils/TCPConsole.hpp
|
||||
Utils/UltiMaker.cpp
|
||||
Utils/UltiMaker.hpp
|
||||
Utils/UndoRedo.cpp
|
||||
Utils/UndoRedo.hpp
|
||||
Utils/WebSocketClient.hpp
|
||||
|
||||
@@ -507,10 +507,10 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
|
||||
glsafe(::glClearStencil(0));
|
||||
glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
|
||||
glsafe(::glStencilFunc(GL_ALWAYS, 0xFF, 0xFF));
|
||||
if (tverts_range == std::make_pair<size_t, size_t>(0, -1))
|
||||
model.render(shader);
|
||||
else
|
||||
model.render(this->tverts_range, shader);
|
||||
// This pass paints the visible surface, so it must go through simple_render() to keep
|
||||
// per-triangle MMU paint colors; the later is_outline passes only draw the flat silhouette
|
||||
// highlight and are fine using the single-color model.
|
||||
simple_render(shader, model_objects, colors);
|
||||
glsafe(::glStencilFunc(GL_NOTEQUAL, 0xFF, 0xFF));
|
||||
glsafe(::glStencilMask(0x00));
|
||||
shader->set_uniform("is_outline", true);
|
||||
@@ -670,6 +670,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
} while (0);
|
||||
|
||||
if (color_volume && !picking) {
|
||||
const bool brighten_selected = selected && !disabled && !force_native_color && !force_neutral_color;
|
||||
|
||||
// when force_transparent, we need to keep the alpha
|
||||
if (force_native_color && render_color.is_transparent()) {
|
||||
for (auto &extruder_color : extruder_colors)
|
||||
@@ -691,6 +693,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
|
||||
if (brighten_selected)
|
||||
new_color = brighten_color(new_color, 1.25f);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - color_idx)/255.0f;
|
||||
}
|
||||
@@ -702,6 +706,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
if (idx <= extruder_colors.size()) {
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]);
|
||||
if (brighten_selected)
|
||||
new_color = brighten_color(new_color, 1.25f);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - (idx - 1))/255.0f;
|
||||
}
|
||||
@@ -711,6 +717,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
else {
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[0]);
|
||||
if (brighten_selected)
|
||||
new_color = brighten_color(new_color, 1.25f);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - 0) / 255.0f;
|
||||
}
|
||||
@@ -1149,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);
|
||||
@@ -1249,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();
|
||||
|
||||
@@ -3,20 +3,48 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "NativeCommands.hpp"
|
||||
#include "Notebook.hpp"
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(std::min(ids.size(), limit));
|
||||
for (const auto& id : ids) {
|
||||
if (out.size() >= limit)
|
||||
break;
|
||||
if (std::find(out.begin(), out.end(), id) == out.end())
|
||||
out.push_back(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kConfigSection = "speed_dial";
|
||||
@@ -28,14 +56,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
|
||||
}
|
||||
|
||||
nlohmann::json read_section(const char* key, nlohmann::json fallback)
|
||||
{
|
||||
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
|
||||
}
|
||||
{ return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
|
||||
|
||||
void write_section(const char* key, const nlohmann::json& j)
|
||||
{
|
||||
wxGetApp().app_config->set(kConfigSection, key, j.dump());
|
||||
}
|
||||
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
|
||||
|
||||
std::vector<std::string> read_string_array(const char* key)
|
||||
{
|
||||
@@ -53,7 +76,7 @@ double frecency_score(int count, long long last, long long now)
|
||||
if (count <= 0)
|
||||
return 0.0;
|
||||
constexpr double HALF_LIFE_DAYS = 30.0;
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
double age = std::max(0.0, double(now - last) / 86400.0);
|
||||
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
|
||||
}
|
||||
|
||||
@@ -79,26 +102,25 @@ struct PluginScriptAction : AppAction
|
||||
// The id an action for (plugin_key, capability) would have - lets refresh_capability
|
||||
// remove a gone capability without materialising the action.
|
||||
static std::string id_for(const std::string& plugin_key, const std::string& capability)
|
||||
{
|
||||
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
|
||||
}
|
||||
{ return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
|
||||
|
||||
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
|
||||
: AppAction(kIdPrefix,
|
||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||
plugin_key_in, // source_key
|
||||
std::move(source_name)),
|
||||
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
|
||||
capability_in.empty() ? plugin_key_in : capability_in, // title
|
||||
plugin_key_in, // source_key
|
||||
std::move(source_name))
|
||||
, plugin_key(std::move(plugin_key_in))
|
||||
, capability(std::move(capability_in))
|
||||
{}
|
||||
|
||||
AppActionRunResult run() const override
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
std::string error;
|
||||
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
|
||||
if (!error.empty())
|
||||
return {AppActionRunResult::Level::Error, from_u8(error)};
|
||||
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const bool skipped = result.status == PluginResult::Skipped;
|
||||
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
|
||||
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
|
||||
result.message.empty() ? fallback : from_u8(result.message)};
|
||||
@@ -107,8 +129,7 @@ struct PluginScriptAction : AppAction
|
||||
|
||||
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
|
||||
// enabled script capability.
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
|
||||
const std::string& source_name)
|
||||
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (!manager.is_plugin_loaded(plugin_key))
|
||||
@@ -119,8 +140,151 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
|
||||
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
|
||||
}
|
||||
|
||||
// ---- built-in command actions (the speed dial "commands" section) ------
|
||||
|
||||
constexpr const char* kSettingPrefix = "orca_setting";
|
||||
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
|
||||
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
|
||||
|
||||
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
|
||||
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
|
||||
std::string setting_type_context(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_FILAMENT:
|
||||
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
|
||||
case Preset::TYPE_PRINTER: return _u8L("Printer");
|
||||
case Preset::TYPE_PRINT:
|
||||
case Preset::TYPE_SLA_PRINT:
|
||||
default: return _u8L("Process");
|
||||
}
|
||||
}
|
||||
|
||||
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
|
||||
const char* mode_key(ConfigOptionMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case comAdvanced: return "advanced";
|
||||
case comExpert: return "expert";
|
||||
case comDevelop: return "develop";
|
||||
default: return "simple";
|
||||
}
|
||||
}
|
||||
|
||||
// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option.
|
||||
// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys
|
||||
// the action; title/group/source are purely for display + search. run() performs the jump, and
|
||||
// the generic registry run() bumps stats so a jump shows up in "recents" like any other action.
|
||||
struct SettingAction : AppAction
|
||||
{
|
||||
std::string opt_key;
|
||||
Preset::Type type;
|
||||
std::wstring category; // English category, forwarded to jump_to_option (it localizes)
|
||||
|
||||
static std::string id_for(const std::string& opt_key, Preset::Type type)
|
||||
{ return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); }
|
||||
|
||||
SettingAction(std::string opt_key_in,
|
||||
Preset::Type type_in,
|
||||
std::string title,
|
||||
std::string group,
|
||||
std::wstring category_in,
|
||||
std::string source_name,
|
||||
ConfigOptionMode mode_in)
|
||||
: AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name))
|
||||
, opt_key(std::move(opt_key_in))
|
||||
, type(type_in)
|
||||
, category(std::move(category_in))
|
||||
{
|
||||
// A setting is a single-phase command: activating it jumps the sidebar to the option
|
||||
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = std::move(group);
|
||||
this->required_mode = mode_in;
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
wxGetApp().sidebar().jump_to_option(opt_key, type, category);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
};
|
||||
|
||||
// Seed one action's persisted state (favourite flag + frecency counters) from an already-parsed
|
||||
// stats blob and capped favourite list. Shared by the dynamic materialisers.
|
||||
void seed_from(const nlohmann::json& stats, const std::vector<std::string>& favs, const std::string& id, AppAction& a)
|
||||
{
|
||||
a.favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
|
||||
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
|
||||
a.count = it->value("count", 0);
|
||||
a.last = it->value("last", 0LL);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop actions whose id starts with `prefix` but that were not seen in this pass (a stale materialisation).
|
||||
void drop_stale(std::unordered_map<std::string, std::shared_ptr<AppAction>>& actions, const char* prefix,
|
||||
const std::unordered_set<std::string>& seen)
|
||||
{
|
||||
for (auto it = actions.begin(); it != actions.end();) {
|
||||
if (it->first.rfind(prefix, 0) == 0 && !seen.count(it->first))
|
||||
it = actions.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a
|
||||
// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so
|
||||
// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to
|
||||
// Plate N" whose plate is deleted simply stops resolving (visibleFavourites drops dead pins).
|
||||
struct PlateAction : AppAction
|
||||
{
|
||||
int plate_index;
|
||||
|
||||
static std::string id_for(int index) { return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); }
|
||||
|
||||
PlateAction(int index, std::string title, std::string source_name)
|
||||
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name)), plate_index(index)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Plate");
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{ return NativeCommands::run("plate_goto", std::to_string(plate_index)); }
|
||||
};
|
||||
|
||||
// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every
|
||||
// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed
|
||||
// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a
|
||||
// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose
|
||||
// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the
|
||||
// project through MainFrame::open_recent_project so the existing missing-file handling is reused.
|
||||
struct RecentProjectAction : AppAction
|
||||
{
|
||||
std::string file_path;
|
||||
|
||||
static std::string id_for(const std::string& path) { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); }
|
||||
|
||||
RecentProjectAction(std::string path, std::string title, std::string source)
|
||||
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = _u8L("Recent Projects");
|
||||
}
|
||||
|
||||
AppActionRunResult run(const std::string& /*param*/) const override
|
||||
{
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path));
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ActionRegistry::~ActionRegistry() = default;
|
||||
|
||||
void ActionRegistry::init()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
@@ -151,18 +315,12 @@ void ActionRegistry::init()
|
||||
// Subscribe before enumerating so a concurrent load cannot land between the initial
|
||||
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
|
||||
// id and the m_actions scan in refresh_source is idempotent.
|
||||
manager.subscribe_on_load_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback(
|
||||
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
|
||||
manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
|
||||
manager.subscribe_on_capability_load_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Added);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
|
||||
manager.subscribe_on_capability_unload_callback(
|
||||
[on_capability](const PluginCapabilityId& capability) {
|
||||
on_capability(capability, ActionChange::Removed);
|
||||
});
|
||||
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
|
||||
|
||||
// enumerate current script capabilities
|
||||
std::unordered_map<std::string, std::string> source_names;
|
||||
@@ -173,12 +331,40 @@ void ActionRegistry::init()
|
||||
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& source_name = it == source_names.end() ? key : it->second;
|
||||
if (auto action = make_action(key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
|
||||
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
|
||||
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
|
||||
// NativeCommands; the registry only stores and dispatches the result.
|
||||
for (const NativeCommand& c : NativeCommands::catalog())
|
||||
upsert(NativeCommands::make_action(c));
|
||||
}
|
||||
|
||||
void ActionRegistry::relocalize_builtins()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
if (!m_started)
|
||||
return;
|
||||
|
||||
// Drop only the built-in commands; plugins and the dynamically materialised families are
|
||||
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
|
||||
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
|
||||
std::vector<std::string> stale;
|
||||
for (const auto& [id, action] : m_actions)
|
||||
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
|
||||
stale.push_back(id);
|
||||
for (const std::string& id : stale)
|
||||
remove(id);
|
||||
|
||||
NativeCommands::rebuild_catalog();
|
||||
for (const NativeCommand& c : NativeCommands::catalog())
|
||||
upsert(NativeCommands::make_action(c));
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
|
||||
@@ -198,7 +384,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
if (change == ActionChange::Removed)
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const std::string source_name = find_loaded_source_name(manager, plugin_key);
|
||||
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
|
||||
if (!capability)
|
||||
@@ -208,8 +394,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
|
||||
ActionChange change)
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
@@ -233,7 +418,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
|
||||
return;
|
||||
|
||||
seed_state(*action);
|
||||
std::string id = action->id();
|
||||
std::string id = action->id();
|
||||
std::shared_ptr<AppAction> stored = std::move(action);
|
||||
m_actions.insert_or_assign(std::move(id), std::move(stored));
|
||||
}
|
||||
@@ -246,11 +431,13 @@ void ActionRegistry::remove(const std::string& id)
|
||||
|
||||
void ActionRegistry::seed_state(AppAction& a) const
|
||||
{
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
// Favourites carry the quick-launch order, so the persisted list is the source of truth
|
||||
// (not re-derived from the frecency sort). Cap it so stale configs can't exceed kFavLimit.
|
||||
auto favs = favourite_ids();
|
||||
a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end();
|
||||
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
auto it = stats.find(a.id());
|
||||
auto it = stats.find(a.id());
|
||||
if (it != stats.end() && it->is_object()) {
|
||||
a.count = it->value("count", 0);
|
||||
a.last = it->value("last", 0LL);
|
||||
@@ -260,6 +447,14 @@ void ActionRegistry::seed_state(AppAction& a) const
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const
|
||||
{
|
||||
stats = read_section("stats", nlohmann::json::object());
|
||||
if (!stats.is_object())
|
||||
stats = nlohmann::json::object();
|
||||
favs = favourite_ids();
|
||||
}
|
||||
|
||||
// ---- read surface -----------------------------------------------------------
|
||||
|
||||
const AppAction* ActionRegistry::by_id(const std::string& id) const
|
||||
@@ -269,14 +464,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
|
||||
return it == m_actions.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
AppAction* ActionRegistry::find(const std::string& id)
|
||||
{
|
||||
return const_cast<AppAction*>(by_id(id));
|
||||
}
|
||||
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
|
||||
|
||||
// ---- dispatch + write-through ----------------------------------------------
|
||||
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
auto it = m_actions.find(id);
|
||||
@@ -286,13 +478,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
// nested event loop; a queued source refresh can erase the entry while the
|
||||
// keep-alive preserves the action until run returns.
|
||||
std::shared_ptr<AppAction> keep = it->second;
|
||||
AppActionRunResult o = keep->run();
|
||||
AppActionRunResult o = keep->run(param);
|
||||
if (o.level == AppActionRunResult::Level::Busy)
|
||||
return o;
|
||||
|
||||
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
|
||||
nlohmann::json stats = read_section("stats", nlohmann::json::object());
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
|
||||
stats = nlohmann::json::object();
|
||||
nlohmann::json& e = stats[id];
|
||||
if (!e.is_object())
|
||||
@@ -300,22 +492,37 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
|
||||
e["count"] = e.value("count", 0) + 1;
|
||||
e["last"] = (long long) std::time(nullptr);
|
||||
write_section("stats", stats);
|
||||
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; }
|
||||
if (AppAction* live = find(id)) {
|
||||
live->count = e["count"];
|
||||
live->last = e["last"];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
void ActionRegistry::set_favourite(const std::string& id, bool on)
|
||||
bool ActionRegistry::set_favourite(const std::string& id, bool on)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
auto favs = read_string_array("favourite_actions");
|
||||
// Start from the capped, deduped list so a persisted config can never be written back larger.
|
||||
auto favs = favourite_ids();
|
||||
auto it = std::find(favs.begin(), favs.end(), id);
|
||||
if (on && it == favs.end())
|
||||
if (on && it == favs.end()) {
|
||||
if (favs.size() >= kFavLimit)
|
||||
return false; // bar is full - the caller surfaces a hint
|
||||
favs.push_back(id);
|
||||
}
|
||||
if (!on && it != favs.end())
|
||||
favs.erase(it);
|
||||
write_section("favourite_actions", nlohmann::json(favs));
|
||||
if (AppAction* live = find(id))
|
||||
live->favourite = on;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> ActionRegistry::favourite_ids() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
// Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config.
|
||||
return cap_favourites(read_string_array("favourite_actions"), kFavLimit);
|
||||
}
|
||||
|
||||
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
||||
@@ -325,16 +532,184 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
|
||||
std::vector<std::string> next;
|
||||
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
|
||||
for (const auto& id : ids)
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
|
||||
std::find(next.begin(), next.end(), id) == next.end())
|
||||
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
|
||||
next.push_back(id);
|
||||
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
|
||||
for (const auto& id : cur)
|
||||
if (std::find(next.begin(), next.end(), id) == next.end())
|
||||
next.push_back(id);
|
||||
// never write the bar back larger than the quick-launch slots
|
||||
next = cap_favourites(next, kFavLimit);
|
||||
write_section("favourite_actions", nlohmann::json(next));
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_setting_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is
|
||||
// populated (Tab::add_key feeds it at build time), and it already mirrors the current
|
||||
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
|
||||
// including those above the user's current mode, and can prompt to switch before jumping.
|
||||
const std::vector<Search::Option>& options = wxGetApp().sidebar().settings_index().all_options();
|
||||
|
||||
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
|
||||
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const Search::Option& opt : options) {
|
||||
// The row's live state drives both the hidden filter and the title (labels can change at
|
||||
// runtime, e.g. brim_width -> "Brim ear radius"). Hidden rows are skipped, not marked seen.
|
||||
Tab* tab = wxGetApp().get_tab(opt.type);
|
||||
Tab::SettingRowState row;
|
||||
if (tab)
|
||||
row = tab->setting_row_state(opt.opt_key());
|
||||
if (!row.visible)
|
||||
continue;
|
||||
|
||||
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
|
||||
seen.insert(id);
|
||||
|
||||
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
|
||||
// ("overhang reversal" still finds "Reverse on even").
|
||||
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
|
||||
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
|
||||
if (title.empty())
|
||||
title = search_label;
|
||||
|
||||
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
|
||||
// renders group || source and searches source + " " + group, so putting the whole path in
|
||||
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
|
||||
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
|
||||
if (!opt.category_local.empty())
|
||||
path += L" : " + opt.category_local;
|
||||
if (!opt.group_local.empty())
|
||||
path += L" : " + opt.group_local;
|
||||
|
||||
// title = the label the settings row draws; group stays empty so the source path (above) is
|
||||
// the single display/search breadcrumb rather than being duplicated.
|
||||
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
|
||||
boost::nowide::narrow(path), opt.mode);
|
||||
if (title != search_label)
|
||||
action->full_label = search_label;
|
||||
|
||||
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
|
||||
// the one shown next to it in the page. Fall back to the page/category icon for groups
|
||||
// without one. Keys are the English titles the GUI registers.
|
||||
action->icon = opt.group_icon;
|
||||
if (action->icon.empty() && !opt.category.empty() && tab) {
|
||||
const auto& icons = tab->get_category_icon_map();
|
||||
auto it = icons.find(wxString(opt.category));
|
||||
if (it != icons.end())
|
||||
action->icon = it->second;
|
||||
}
|
||||
|
||||
// Footer description + wiki affordance; only settings whose row declared a wiki path have one.
|
||||
action->tooltip = opt.tooltip;
|
||||
if (!opt.wiki_path.empty())
|
||||
action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path));
|
||||
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop SettingActions whose option no longer exists in the current configs (e.g. the printer
|
||||
// technology / UI mode changed). Non-setting actions are untouched.
|
||||
drop_stale(m_actions, kSettingPrefix, seen);
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_plate_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Plates are a filament (FFF) feature: SLA has a single plate and no plate UI, and gcode-only
|
||||
// mode has no editable project - so no "Go to Plate N" actions are offered there.
|
||||
Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr;
|
||||
if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) {
|
||||
// Drop any stale plate actions (e.g. the printer technology switched to SLA).
|
||||
drop_stale(m_actions, kPlateGotoPrefix, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted
|
||||
// "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list();
|
||||
std::unordered_set<std::string> seen;
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
PartPlate* plate = list[i];
|
||||
if (!plate)
|
||||
continue;
|
||||
const std::string id = PlateAction::id_for(int(i));
|
||||
seen.insert(id);
|
||||
|
||||
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
|
||||
std::string title(_u8L("Go to Plate"));
|
||||
title += " " + std::to_string(i + 1);
|
||||
const std::string name = plate->get_plate_name();
|
||||
if (!name.empty())
|
||||
title += " (" + name + ")";
|
||||
|
||||
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop plate actions whose index no longer exists (a plate was deleted / moved to the front).
|
||||
drop_stale(m_actions, kPlateGotoPrefix, seen);
|
||||
}
|
||||
|
||||
void ActionRegistry::materialize_recent_project_actions()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
// Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent
|
||||
// project keeps its recency/favourite when the recents list reorders - the id is path-keyed.
|
||||
nlohmann::json stats;
|
||||
std::vector<std::string> favs;
|
||||
load_persisted(stats, favs);
|
||||
|
||||
// app_config stores recents oldest-first; the palette shows newest-first.
|
||||
std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects();
|
||||
std::reverse(recents.begin(), recents.end());
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const std::string& path : recents) {
|
||||
// Skip projects whose file is gone; the stale id is dropped below.
|
||||
boost::system::error_code ec;
|
||||
if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec))
|
||||
continue;
|
||||
|
||||
const std::string id = RecentProjectAction::id_for(path);
|
||||
seen.insert(id);
|
||||
|
||||
// Title = file basename; source/eyebrow = the full path so search can match either.
|
||||
boost::filesystem::path p(path);
|
||||
std::string title = p.filename().string();
|
||||
if (title.empty())
|
||||
title = path;
|
||||
|
||||
auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path);
|
||||
seed_from(stats, favs, id, *action);
|
||||
auto const action_id = action->id();
|
||||
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
|
||||
m_actions.insert_or_assign(action_id, app_action);
|
||||
}
|
||||
|
||||
// Drop recent-project actions whose file no longer exists / was removed from the recents list.
|
||||
drop_stale(m_actions, kRecentProjectPrefix, seen);
|
||||
}
|
||||
|
||||
bool ActionRegistry::should_ask(const std::string& id) const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
@@ -351,11 +726,31 @@ void ActionRegistry::suppress_ask(const std::string& id)
|
||||
write_section("ask_suppressed", nlohmann::json(arr));
|
||||
}
|
||||
|
||||
// ---- snapshot ---------------------------------------------------------------
|
||||
|
||||
nlohmann::json ActionRegistry::snapshot() const
|
||||
bool ActionRegistry::tooltip_expanded() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
const nlohmann::json j = read_section("tooltip_expanded", nlohmann::json(true));
|
||||
return j.is_boolean() ? j.get<bool>() : true;
|
||||
}
|
||||
|
||||
void ActionRegistry::set_tooltip_expanded(bool expanded)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
write_section("tooltip_expanded", nlohmann::json(expanded));
|
||||
}
|
||||
|
||||
// ---- snapshot ---------------------------------------------------------------
|
||||
|
||||
nlohmann::json ActionRegistry::snapshot()
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
// Settings and plates are first-class actions; make sure the current visible option set and the
|
||||
// live plate list are materialised before we serialise the pool (tabs_list is built by the time
|
||||
// the palette opens).
|
||||
materialize_setting_actions();
|
||||
materialize_plate_actions();
|
||||
materialize_recent_project_actions();
|
||||
|
||||
std::vector<const AppAction*> sorted;
|
||||
sorted.reserve(m_actions.size());
|
||||
for (const auto& entry : m_actions)
|
||||
@@ -374,17 +769,85 @@ nlohmann::json ActionRegistry::snapshot() const
|
||||
return a->id() < b->id();
|
||||
});
|
||||
|
||||
auto action_to_json = [](const AppAction* a) {
|
||||
return nlohmann::json({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"full_label", a->full_label},
|
||||
{"source", a->source_name()},
|
||||
{"group", a->group},
|
||||
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
|
||||
{"input", a->input},
|
||||
{"icon", a->icon},
|
||||
{"mode", mode_key(a->required_mode)},
|
||||
{"desc", a->tooltip},
|
||||
{"wiki", !a->help_url.empty()}});
|
||||
};
|
||||
|
||||
nlohmann::json actions = nlohmann::json::array();
|
||||
for (const AppAction* a : sorted)
|
||||
actions.push_back({{"id", a->id()},
|
||||
{"title", a->title()},
|
||||
{"source", a->source_name()},
|
||||
{"shortcut", ""}});
|
||||
actions.push_back(action_to_json(a));
|
||||
|
||||
// why: favourites is the ORDERED pin list - it must come from favourite_actions
|
||||
// as stored, not be re-derived from the frecency-sorted actions (that would
|
||||
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
|
||||
nlohmann::json favourites(read_string_array("favourite_actions"));
|
||||
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
|
||||
// reorder the favourites bar). Drop pins with no live action (an option hidden by the
|
||||
// current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
|
||||
// invisible pins can't silently fill the quick-launch cap. Order is preserved.
|
||||
std::vector<std::string> favs = favourite_ids();
|
||||
std::vector<std::string> live_favs;
|
||||
live_favs.reserve(favs.size());
|
||||
for (const auto& id : favs)
|
||||
if (m_actions.count(id))
|
||||
live_favs.push_back(id);
|
||||
if (live_favs.size() != favs.size())
|
||||
write_section("favourite_actions", nlohmann::json(live_favs));
|
||||
nlohmann::json favourites(live_favs);
|
||||
|
||||
// Recent = the last-N launched actions by recency (only actions with a run history). N is a
|
||||
// user preference; 0 hides recents without affecting the frecency order below.
|
||||
const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count());
|
||||
std::vector<const AppAction*> recent;
|
||||
for (const auto& entry : m_actions)
|
||||
if (entry.second->last > 0)
|
||||
recent.push_back(entry.second.get());
|
||||
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
|
||||
if (a->last != b->last)
|
||||
return a->last > b->last;
|
||||
return a->id() < b->id();
|
||||
});
|
||||
if (recent.size() > recent_limit)
|
||||
recent.resize(recent_limit);
|
||||
nlohmann::json recent_json = nlohmann::json::array();
|
||||
for (const AppAction* a : recent)
|
||||
recent_json.push_back(action_to_json(a));
|
||||
|
||||
return {{"actions", std::move(actions)},
|
||||
{"favourites", std::move(favourites)},
|
||||
{"recent", std::move(recent_json)},
|
||||
{"user_mode", mode_key(wxGetApp().get_mode())},
|
||||
{"tooltip_expanded", tooltip_expanded()}};
|
||||
}
|
||||
|
||||
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
|
||||
|
||||
nlohmann::json ActionRegistry::tab_options() const
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
if (!wxTheApp || wxGetApp().is_closing())
|
||||
return out;
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf || !mf->m_tabpanel)
|
||||
return out;
|
||||
Notebook* notebook = mf->m_tabpanel;
|
||||
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
|
||||
const wxString id = notebook->GetPageName(i);
|
||||
if (id.empty())
|
||||
continue;
|
||||
out.push_back({{"id", id.ToStdString()},
|
||||
{"title", notebook->GetPageLabel(i).ToStdString()},
|
||||
{"icon", notebook->GetPageIcon(i)}});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <libslic3r/Config.hpp>
|
||||
|
||||
#include <wx/string.h>
|
||||
#include <wx/thread.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -18,14 +21,25 @@ namespace Slic3r { namespace GUI {
|
||||
// How a source's action set changed. Drives the registry's refresh handlers.
|
||||
enum class ActionChange { Added, Removed };
|
||||
|
||||
// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
|
||||
enum class AppActionKind { Plugin, Command };
|
||||
|
||||
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
|
||||
// actions translate their runner-specific result into this generic shape.
|
||||
struct AppActionRunResult
|
||||
{
|
||||
enum class Level { Success, Info, Error, Busy };
|
||||
|
||||
Level level = Level::Info;
|
||||
wxString message; // empty = "nothing worth showing"
|
||||
Level level = Level::Info;
|
||||
wxString message; // empty = "nothing worth showing"
|
||||
};
|
||||
|
||||
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
|
||||
// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first
|
||||
// argument (which would make calls like AppAction("orca_command", ...) ambiguous).
|
||||
struct AppActionId
|
||||
{
|
||||
std::string id;
|
||||
};
|
||||
|
||||
// A speed-dial action: identity + user-state seeded from config + how to run itself.
|
||||
@@ -52,12 +66,35 @@ struct AppAction
|
||||
}
|
||||
|
||||
// seeded from AppConfig for the snapshot / sort:
|
||||
bool favourite = false;
|
||||
int count = 0;
|
||||
long long last = 0; // epoch seconds
|
||||
bool favourite = false;
|
||||
int count = 0;
|
||||
long long last = 0; // epoch seconds
|
||||
|
||||
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
|
||||
// source name); Command sets a section label (e.g. "Commands", "Mode").
|
||||
AppActionKind kind = AppActionKind::Plugin;
|
||||
std::string group;
|
||||
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
|
||||
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
|
||||
std::string input;
|
||||
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
|
||||
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
|
||||
std::string icon;
|
||||
// Settings mode required to edit this action (SettingActions only). The palette prompts before
|
||||
// running an action whose mode is above the user's current mode. comSimple for everything else.
|
||||
ConfigOptionMode required_mode = comSimple;
|
||||
// Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip).
|
||||
std::string tooltip;
|
||||
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
|
||||
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
|
||||
std::string full_label;
|
||||
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
|
||||
std::string help_url;
|
||||
|
||||
virtual ~AppAction() = default;
|
||||
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
|
||||
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
|
||||
// commands (e.g. a layer percentage); plugins ignore it.
|
||||
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
|
||||
|
||||
protected:
|
||||
// The definition is constructor-set and immutable. Refreshes replace an action
|
||||
@@ -65,10 +102,17 @@ protected:
|
||||
// why: source_key (not the display name) carries identity, so renaming the source's
|
||||
// display name leaves the id - and its persisted stats/favourite - intact.
|
||||
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
|
||||
: m_id(compose_id(prefix, title, source_key)),
|
||||
m_title(std::move(title)),
|
||||
m_source_key(std::move(source_key)),
|
||||
m_source_name(std::move(source_name)) {}
|
||||
: m_id(compose_id(prefix, title, source_key))
|
||||
, m_title(std::move(title))
|
||||
, m_source_key(std::move(source_key))
|
||||
, m_source_name(std::move(source_name))
|
||||
{}
|
||||
|
||||
// Explicit-id ctor: for actions whose id must NOT be derived from the display title
|
||||
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
|
||||
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
|
||||
: m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
|
||||
{}
|
||||
|
||||
private:
|
||||
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
|
||||
@@ -77,26 +121,53 @@ private:
|
||||
std::string m_source_name; // display name of the action's source
|
||||
};
|
||||
|
||||
// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native
|
||||
// command catalog and the dynamically materialised setting/plate/recent actions so every built-in
|
||||
// action re-keys together.
|
||||
inline constexpr const char* kOrcaSourceKey = "orca";
|
||||
inline constexpr const char* kOrcaSourceName = "OrcaSlicer";
|
||||
|
||||
// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch
|
||||
// first. Developer settings are handled as a separate prompt by the Speed Dial.
|
||||
inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode)
|
||||
{
|
||||
return setting_mode > current_mode;
|
||||
}
|
||||
|
||||
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
|
||||
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
|
||||
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
|
||||
|
||||
// Self-contained sink and single owner of runnable actions for the app session.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the
|
||||
// current script capabilities into actions.
|
||||
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
|
||||
// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
|
||||
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
|
||||
// which upsert()/remove() actions. The registry keeps the only action list and
|
||||
// restores persisted user state as actions arrive.
|
||||
// 3. Consumers use by_id(), snapshot(), and run() without knowing the source.
|
||||
// 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of
|
||||
// snapshot(), because their membership follows live state (the current configs, plate list, recents).
|
||||
// 4. Consumers use by_id(), snapshot(), and run() without knowing the source.
|
||||
//
|
||||
// note: there is exactly one source (script plugins), so it lives inline here rather
|
||||
// than behind a polymorphic source interface.
|
||||
// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
|
||||
// dispatch, and materialises the dynamic families inline rather than behind a source interface.
|
||||
class ActionRegistry
|
||||
{
|
||||
public:
|
||||
~ActionRegistry();
|
||||
|
||||
// Subscribes to the plugin loader and enumerates its current actions. Call once
|
||||
// on the UI thread after the plugin system is up; wires the initial list and live
|
||||
// updates together.
|
||||
void init();
|
||||
|
||||
// Rebuilds the built-in command actions in the current UI locale. The command catalog copies
|
||||
// translated titles/groups at construction, so after a live language switch the stored titles
|
||||
// are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so
|
||||
// favourites/run history survive. UI thread only. No-op before init().
|
||||
void relocalize_builtins();
|
||||
|
||||
// Takes ownership, seeds persisted state, then inserts the action or replaces
|
||||
// the action with the same id. A null action is ignored.
|
||||
void upsert(std::unique_ptr<AppAction> action);
|
||||
@@ -105,31 +176,72 @@ public:
|
||||
void remove(const std::string& id);
|
||||
|
||||
// Always-clean read surface. UI thread only.
|
||||
const AppAction* by_id(const std::string& id) const;
|
||||
const AppAction* by_id(const std::string& id) const;
|
||||
|
||||
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
|
||||
static constexpr size_t kFavLimit = 10;
|
||||
|
||||
// Dispatch + write-through (registry is the only thing that touches AppConfig).
|
||||
AppActionRunResult run(const std::string& id); // runs + bumps stats
|
||||
void set_favourite(const std::string& id, bool on);
|
||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
|
||||
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
|
||||
// caller can surface a "favourites are full" hint instead of silently dropping the pin.
|
||||
bool set_favourite(const std::string& id, bool on);
|
||||
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
|
||||
|
||||
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
|
||||
// visible bar the palette renders.
|
||||
std::vector<std::string> favourite_ids() const;
|
||||
|
||||
// Run-confirm gate, keyed by action id (per-action "don't ask again").
|
||||
bool should_ask(const std::string& id) const;
|
||||
void suppress_ask(const std::string& id);
|
||||
|
||||
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
|
||||
nlohmann::json snapshot() const;
|
||||
// Footer expand/collapse preference. Global (applies to every action) and persisted; absent
|
||||
// means expanded, so a fresh config picks the richer default with no migration.
|
||||
bool tooltip_expanded() const;
|
||||
void set_tooltip_expanded(bool expanded);
|
||||
|
||||
// Flat, frecency-sorted snapshot for the webview:
|
||||
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
|
||||
nlohmann::json snapshot();
|
||||
|
||||
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
|
||||
// as [{id,title,icon},...], using the page's real label (not the compact-blanked button text).
|
||||
// Live by construction - built-in tabs (Home/Prepare/Preview/Device/Project/Calibration) and
|
||||
// plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a page appears/disappears with
|
||||
// the notebook. Plugin tabs hidden in the overflow menu (many plugins) aren't separate pages and
|
||||
// are not listed. Call on the UI thread; null-safe.
|
||||
nlohmann::json tab_options() const;
|
||||
|
||||
private:
|
||||
void seed_state(AppAction& a) const; // favourite/stats from config
|
||||
AppAction* find(const std::string& id);
|
||||
void seed_state(AppAction& a) const; // favourite/stats from config
|
||||
AppAction* find(const std::string& id);
|
||||
|
||||
// Read the persisted stats blob + capped favourite list once for a materialisation pass.
|
||||
void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const;
|
||||
|
||||
// (Re)materialise the current visible config settings as SettingActions from the live
|
||||
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
|
||||
// Called at the top of snapshot() so the palette always reflects the current configs.
|
||||
void materialize_setting_actions();
|
||||
|
||||
// (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate
|
||||
// directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no
|
||||
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
|
||||
void materialize_plate_actions();
|
||||
|
||||
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
|
||||
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
|
||||
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
|
||||
void materialize_recent_project_actions();
|
||||
|
||||
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
|
||||
// one plugin's whole action set; refresh_capability touches a single capability.
|
||||
void refresh_source(const std::string& plugin_key, ActionChange change);
|
||||
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
|
||||
|
||||
bool m_started = false; // init() runs exactly once; guards double-subscription
|
||||
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
||||
bool m_started = false; // init() runs exactly once; guards double-subscription
|
||||
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
@@ -82,7 +82,7 @@ CameraPopup::CameraPopup(wxWindow *parent)
|
||||
top_sizer->Add(m_text_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
|
||||
top_sizer->Add(m_switch_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
|
||||
|
||||
m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &e) {
|
||||
m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent &e) {
|
||||
wxGetApp().app_config->set("liveview", "auto_retry", e.IsChecked());
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
@@ -997,13 +997,17 @@ void GCodeViewer::SequentialView::GCodeWindow::stop_mapping_file()
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished mapping file " << m_filename;
|
||||
}
|
||||
}
|
||||
void GCodeViewer::SequentialView::render(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
|
||||
void GCodeViewer::SequentialView::render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type)
|
||||
{
|
||||
if (has_render_path && m_show_marker) {
|
||||
if (has_render_path && m_show_marker)
|
||||
// marker.set_world_offset(current_offset);
|
||||
marker.render(canvas_width, canvas_height, view_type);
|
||||
}
|
||||
|
||||
void GCodeViewer::SequentialView::render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
|
||||
{
|
||||
if (has_render_path && m_show_marker)
|
||||
marker.render_position_window(viewer, canvas_width, canvas_height, view_type);
|
||||
}
|
||||
|
||||
//float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height();
|
||||
// BBS
|
||||
@@ -1618,7 +1622,7 @@ void GCodeViewer::reset()
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: add canvas width and height
|
||||
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
|
||||
{
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
render_shells(canvas_width, canvas_height);
|
||||
@@ -1628,6 +1632,20 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
|
||||
render_toolpaths();
|
||||
|
||||
auto current = m_viewer.get_view_visible_range();
|
||||
auto endpoints = m_viewer.get_view_full_range();
|
||||
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
|
||||
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
|
||||
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
|
||||
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
|
||||
m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type());
|
||||
}
|
||||
|
||||
void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin)
|
||||
{
|
||||
if (m_viewer.get_extrusion_roles().empty())
|
||||
return;
|
||||
|
||||
float legend_height = 0.0f;
|
||||
render_legend(legend_height, canvas_width, canvas_height, right_margin);
|
||||
|
||||
@@ -1636,16 +1654,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
m_user_mode = wxGetApp().get_mode();
|
||||
}
|
||||
|
||||
//BBS fixed bottom_margin for space to render horiz slider
|
||||
int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
|
||||
auto current = m_viewer.get_view_visible_range();
|
||||
auto endpoints = m_viewer.get_view_full_range();
|
||||
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
|
||||
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
|
||||
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
|
||||
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
|
||||
// BBS fixed buttom margin. m_moves_slider.pos_y
|
||||
m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type());
|
||||
m_sequential_view.render_overlay(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, sequential_view_height(canvas_height), right_margin * m_scale, m_viewer.get_view_type());
|
||||
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
if (is_legend_shown()) {
|
||||
@@ -1686,6 +1695,14 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
render_slider(canvas_width, canvas_height);
|
||||
}
|
||||
|
||||
int GCodeViewer::sequential_view_height(int canvas_height) const
|
||||
{
|
||||
//BBS fixed bottom_margin for space to render horiz slider
|
||||
const int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
|
||||
// BBS fixed buttom margin. m_moves_slider.pos_y
|
||||
return canvas_height - bottom_margin * m_scale;
|
||||
}
|
||||
|
||||
#define ENABLE_CALIBRATION_THUMBNAIL_OUTPUT 0
|
||||
#if ENABLE_CALIBRATION_THUMBNAIL_OUTPUT
|
||||
static void debug_calibration_output_thumbnail(const ThumbnailData& thumbnail_data)
|
||||
|
||||
@@ -153,7 +153,10 @@ public:
|
||||
GCodeWindow gcode_window;
|
||||
float m_scale = 1.0;
|
||||
bool m_show_marker = false;
|
||||
void render(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type);
|
||||
// The tool marker at the current move, drawn in 3D.
|
||||
void render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
|
||||
// The marker's position window and the G-code window, both ImGui.
|
||||
void render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type);
|
||||
};
|
||||
struct ExtruderFilament
|
||||
{
|
||||
@@ -272,7 +275,10 @@ public:
|
||||
//BBS: add all plates filament statistics
|
||||
void render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show = true) const;
|
||||
//BBS: GUI refactor: add canvas width and height
|
||||
void render(int canvas_width, int canvas_height, int right_margin);
|
||||
// Shells, toolpaths and the sequential marker, drawn in 3D.
|
||||
void render_scene(int canvas_width, int canvas_height);
|
||||
// Legend, sliders, the marker's position window and the G-code window, all ImGui.
|
||||
void render_overlay(int canvas_width, int canvas_height, int right_margin);
|
||||
//BBS
|
||||
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
|
||||
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
|
||||
@@ -362,6 +368,8 @@ public:
|
||||
private:
|
||||
//BBS: always load shell at preview
|
||||
//void load_shells(const Print& print);
|
||||
// Canvas height minus the room the horizontal slider takes.
|
||||
int sequential_view_height(int canvas_height) const;
|
||||
void render_toolpaths();
|
||||
void render_shells(int canvas_width, int canvas_height);
|
||||
|
||||
|
||||
+686
-703
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include "GLToolbar.hpp"
|
||||
#include "Event.hpp"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "GCodeViewer.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "SceneRaycaster.hpp"
|
||||
#include "SceneCache.hpp"
|
||||
#include "IMToolbar.hpp"
|
||||
#include "slic3r/GUI/3DBed.hpp"
|
||||
#include "libslic3r/Slicing.hpp"
|
||||
@@ -33,6 +35,7 @@ class wxTimerEvent;
|
||||
class wxPaintEvent;
|
||||
class wxGLCanvas;
|
||||
class wxGLContext;
|
||||
struct ImDrawData;
|
||||
|
||||
// Support for Retina OpenGL on Mac OS.
|
||||
// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support, enable it as well.
|
||||
@@ -60,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;
|
||||
@@ -169,7 +173,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
|
||||
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
|
||||
@@ -183,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);
|
||||
@@ -403,16 +405,23 @@ class GLCanvas3D
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> m_measuring_start;
|
||||
int m_fps_out = -1;
|
||||
int m_fps_running = 0;
|
||||
// Frames that redrew the 3D scene rather than reusing the cached one.
|
||||
int m_scene_fps_out = 0;
|
||||
int m_scene_fps_running = 0;
|
||||
public:
|
||||
void increment_fps_counter() { ++m_fps_running; }
|
||||
void increment_scene_fps_counter() { ++m_scene_fps_running; }
|
||||
int get_fps() { return m_fps_out; }
|
||||
int get_scene_fps() const { return m_scene_fps_out; }
|
||||
int get_fps_and_reset_if_needed() {
|
||||
auto cur_time = std::chrono::high_resolution_clock::now();
|
||||
int elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(cur_time-m_measuring_start).count();
|
||||
if (elapsed_ms > 1000 || m_fps_out == -1) {
|
||||
m_measuring_start = cur_time;
|
||||
m_fps_out = int (1000. * m_fps_running / elapsed_ms);
|
||||
m_scene_fps_out = int (1000. * m_scene_fps_running / elapsed_ms);
|
||||
m_fps_running = 0;
|
||||
m_scene_fps_running = 0;
|
||||
}
|
||||
return m_fps_out;
|
||||
}
|
||||
@@ -534,6 +543,10 @@ private:
|
||||
bool m_in_render;
|
||||
wxTimer m_timer;
|
||||
wxTimer m_timer_set_color;
|
||||
// Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame.
|
||||
wxTimer m_fps_overlay_timer;
|
||||
// True during the frame the timer requested, which is not counted.
|
||||
bool m_fps_overlay_tick{ false };
|
||||
LayersEditing m_layers_editing;
|
||||
Mouse m_mouse;
|
||||
GLGizmosManager m_gizmos;
|
||||
@@ -601,6 +614,8 @@ private:
|
||||
|
||||
// Screen is only refreshed from the OnIdle handler if it is dirty.
|
||||
bool m_dirty;
|
||||
// A frame is needed, and only for the overlay.
|
||||
bool m_overlay_dirty{ false };
|
||||
bool m_initialized;
|
||||
//BBS: add flag to controll rendering
|
||||
bool m_render_preview{ true };
|
||||
@@ -611,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 };
|
||||
@@ -756,6 +787,11 @@ public:
|
||||
unsigned int m_ssao_color_texture_id{ 0 };
|
||||
unsigned int m_ssao_depth_texture_id{ 0 };
|
||||
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
|
||||
// The last scene pass, for frames that only rebuild the overlay.
|
||||
SceneCache m_scene_cache;
|
||||
// Signature of the overlay on screen; empty after render(), a paint request or a frame drawn but
|
||||
// not shown, so the next frame is presented regardless.
|
||||
std::optional<size_t> m_presented_signature;
|
||||
GLModel m_plate_shadow_mask;
|
||||
std::string m_plate_shadow_mask_key;
|
||||
// Depth-based shadow map used to cast object shadows onto other objects and themselves.
|
||||
@@ -1074,10 +1110,18 @@ 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);
|
||||
void on_set_color_timer(wxTimerEvent& evt);
|
||||
void on_fps_overlay_timer(wxTimerEvent& evt);
|
||||
void on_mouse(wxMouseEvent& evt);
|
||||
void on_gesture(wxGestureEvent& evt);
|
||||
void on_paint(wxPaintEvent& evt);
|
||||
@@ -1275,7 +1319,7 @@ private:
|
||||
void _zoom_to_box(const BoundingBoxf3& box, double margin_factor = DefaultCameraZoomToBoxMarginFactor);
|
||||
void _update_camera_zoom(double zoom);
|
||||
|
||||
void _refresh_if_shown_on_screen();
|
||||
void _refresh_if_shown_on_screen(bool scene_dirty = true);
|
||||
|
||||
void _picking_pass();
|
||||
void _rectangular_selection_picking_pass();
|
||||
@@ -1283,9 +1327,21 @@ private:
|
||||
bool _is_ssao_enabled() const;
|
||||
int _get_effective_fps_cap() const;
|
||||
bool _is_fps_overlay_enabled() const;
|
||||
bool _is_scene_cache_enabled() const;
|
||||
bool _is_scene_cacheable() const;
|
||||
bool _is_frame_skipping_enabled() const;
|
||||
void _render_fps_overlay(int fps) const;
|
||||
void _render_fxaa_pass(unsigned int width, unsigned int height);
|
||||
void _render_ssao_pass(unsigned int width, unsigned int height);
|
||||
// scene_dirty is false only for a frame that its requester knows to be overlay-only.
|
||||
void _render_frame(bool scene_dirty, bool only_init = false);
|
||||
void _render_scene(const Camera& camera, const Size& cnv_size);
|
||||
// Request a frame that only rebuilds the overlay.
|
||||
void _set_overlay_as_dirty() { m_overlay_dirty = true; }
|
||||
// These read the hover state _picking_pass() sets.
|
||||
SceneCache::Key _scene_cache_key(const Camera& camera) const;
|
||||
bool _can_reuse_cached_scene(const Camera& camera) const;
|
||||
void _capture_scene_cache(const Camera& camera);
|
||||
void _render_background();
|
||||
void _render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes);
|
||||
// Build the light-space depth shadow map (consumed by gouraud/phong for object & self shadows)
|
||||
@@ -1300,8 +1356,11 @@ 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);
|
||||
//BBS: render a plane for assemble
|
||||
void _render_plane() const;
|
||||
void _render_selection();
|
||||
@@ -1311,6 +1370,8 @@ private:
|
||||
#endif // ENABLE_RENDER_SELECTION_CENTER
|
||||
void _check_and_update_toolbar_icon_scale();
|
||||
void _render_overlays();
|
||||
void _render_overlay_toolbars();
|
||||
size_t _overlay_signature(const ImDrawData* draw_data) const;
|
||||
void _render_style_editor();
|
||||
void _render_volumes_for_picking(const Camera& camera) const;
|
||||
void _render_current_gizmo() const;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -707,6 +707,29 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
void GLTexture::copy_from_framebuffer(unsigned int& tex_id, std::array<unsigned int, 2>& tex_size, unsigned int width, unsigned int height, int filter)
|
||||
{
|
||||
if (tex_id == 0) {
|
||||
glsafe(::glGenTextures(1, &tex_id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, tex_id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
|
||||
}
|
||||
else
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, tex_id));
|
||||
|
||||
if (tex_size[0] != width || tex_size[1] != height) {
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
|
||||
tex_size = { width, height };
|
||||
}
|
||||
|
||||
// Copying from the default framebuffer resolves its multisampling.
|
||||
glsafe(::glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
}
|
||||
|
||||
static bool to_squared_power_of_two(const std::string& filename, int max_size_px, int& w, int& h)
|
||||
{
|
||||
auto is_power_of_two = [](int v) { return v != 0 && (v & (v - 1)) == 0; };
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <thread>
|
||||
|
||||
#include <wx/colour.h>
|
||||
@@ -132,6 +133,9 @@ namespace GUI {
|
||||
|
||||
static void render_texture(unsigned int tex_id, float left, float right, float bottom, float top);
|
||||
static void render_sub_texture(unsigned int tex_id, float left, float right, float bottom, float top, const Quad_UVs& uvs);
|
||||
// Copies the bound read framebuffer into an RGBA texture, creating it on first use and
|
||||
// reallocating it when the size changes.
|
||||
static void copy_from_framebuffer(unsigned int& tex_id, std::array<unsigned int, 2>& tex_size, unsigned int width, unsigned int height, int filter);
|
||||
|
||||
private:
|
||||
bool load_from_png(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/dcmemory.h>
|
||||
@@ -199,7 +201,10 @@ void GLToolbarItem::render(unsigned int tex_id, float left, float right, float b
|
||||
};
|
||||
|
||||
GLTexture::render_sub_texture(tex_id, left, right, bottom, top, uvs(tex_width, tex_height, icon_size));
|
||||
}
|
||||
|
||||
void GLToolbarItem::render_window(float left, float right, float bottom, float top) const
|
||||
{
|
||||
if (is_pressed())
|
||||
{
|
||||
if ((m_last_action_type == Left) && m_data.left.can_render())
|
||||
@@ -215,13 +220,6 @@ void GLToolbarItem::render_image(unsigned int tex_id, float left, float right, f
|
||||
//GLTexture::Quad_UVs image_uvs = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } };
|
||||
|
||||
GLTexture::render_sub_texture(tex_id, left, right, bottom, top, image_uvs);
|
||||
|
||||
if (is_pressed()) {
|
||||
if ((m_last_action_type == Left) && m_data.left.can_render())
|
||||
m_data.left.render_callback(left, right, bottom, top);
|
||||
else if ((m_last_action_type == Right) && m_data.right.can_render())
|
||||
m_data.right.render_callback(left, right, bottom, top);
|
||||
}
|
||||
}
|
||||
|
||||
BackgroundTexture::Metadata::Metadata()
|
||||
@@ -544,11 +542,35 @@ void GLToolbar::render(const GLCanvas3D& parent,GLToolbarItem::EType type)
|
||||
switch (m_layout.type)
|
||||
{
|
||||
default:
|
||||
case Layout::Horizontal: { render_horizontal(parent,type); break; }
|
||||
case Layout::Vertical: { render_vertical(parent); break; }
|
||||
case Layout::Horizontal: { render_horizontal(parent, type, true); break; }
|
||||
case Layout::Vertical: { render_vertical(parent, true); break; }
|
||||
}
|
||||
}
|
||||
|
||||
void GLToolbar::render_item_windows(const GLCanvas3D& parent)
|
||||
{
|
||||
if (!m_enabled || m_items.empty())
|
||||
return;
|
||||
|
||||
switch (m_layout.type)
|
||||
{
|
||||
default:
|
||||
case Layout::Horizontal: { render_horizontal(parent, GLToolbarItem::Action, false); break; }
|
||||
case Layout::Vertical: { render_vertical(parent, false); break; }
|
||||
}
|
||||
}
|
||||
|
||||
size_t GLToolbar::get_state_hash() const
|
||||
{
|
||||
size_t hash = 0;
|
||||
boost::hash_combine(hash, m_enabled);
|
||||
for (const GLToolbarItem* item : m_items) {
|
||||
boost::hash_combine(hash, (int)item->get_state());
|
||||
boost::hash_combine(hash, item->is_visible());
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool GLToolbar::on_mouse(wxMouseEvent& evt, GLCanvas3D& parent)
|
||||
{
|
||||
if (!m_enabled)
|
||||
@@ -1354,7 +1376,7 @@ void GLToolbar::render_arrow(const GLCanvas3D& parent, GLToolbarItem* highlighte
|
||||
}
|
||||
}
|
||||
|
||||
void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType type)
|
||||
void GLToolbar::render_horizontal(const GLCanvas3D& parent, GLToolbarItem::EType type, bool draw_icons)
|
||||
{
|
||||
const Size cnv_size = parent.get_canvas_size();
|
||||
const float cnv_w = (float)cnv_size.get_width();
|
||||
@@ -1385,7 +1407,8 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
|
||||
right = left + width * 0.5;
|
||||
const float bottom = top - height;
|
||||
|
||||
render_background(left, top, right, bottom, border_w, border_h);
|
||||
if (draw_icons)
|
||||
render_background(left, top, right, bottom, border_w, border_h);
|
||||
|
||||
left += border_w;
|
||||
top -= border_h;
|
||||
@@ -1400,7 +1423,9 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
|
||||
else {
|
||||
//BBS GUI refactor
|
||||
item->render_left_pos = left;
|
||||
if (!item->is_action_with_text_image()) {
|
||||
if (!draw_icons)
|
||||
item->render_window(left, left + icons_size_x, top - icons_size_y, top);
|
||||
else if (!item->is_action_with_text_image()) {
|
||||
unsigned int tex_id = m_icons_texture.get_id();
|
||||
int tex_width = m_icons_texture.get_width();
|
||||
int tex_height = m_icons_texture.get_height();
|
||||
@@ -1412,7 +1437,8 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
|
||||
if (item->is_action_with_text())
|
||||
{
|
||||
float scaled_text_size = item->get_extra_size_ratio() * icons_size_x;
|
||||
item->render_text(left + icons_size_x, left + icons_size_x + scaled_text_size, top - icons_size_y, top);
|
||||
if (draw_icons)
|
||||
item->render_text(left + icons_size_x, left + icons_size_x + scaled_text_size, top - icons_size_y, top);
|
||||
left += scaled_text_size;
|
||||
}
|
||||
left += icon_stride;
|
||||
@@ -1420,7 +1446,7 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
|
||||
}
|
||||
}
|
||||
|
||||
void GLToolbar::render_vertical(const GLCanvas3D& parent)
|
||||
void GLToolbar::render_vertical(const GLCanvas3D& parent, bool draw_icons)
|
||||
{
|
||||
const Size cnv_size = parent.get_canvas_size();
|
||||
const float cnv_w = (float)cnv_size.get_width();
|
||||
@@ -1449,7 +1475,8 @@ void GLToolbar::render_vertical(const GLCanvas3D& parent)
|
||||
const float right = left + width;
|
||||
const float bottom = top - height;
|
||||
|
||||
render_background(left, top, right, bottom, border_w, border_h);
|
||||
if (draw_icons)
|
||||
render_background(left, top, right, bottom, border_w, border_h);
|
||||
|
||||
left += border_w;
|
||||
top -= border_h;
|
||||
@@ -1462,6 +1489,11 @@ void GLToolbar::render_vertical(const GLCanvas3D& parent)
|
||||
if (item->is_separator())
|
||||
top -= separator_stride;
|
||||
else {
|
||||
if (!draw_icons) {
|
||||
item->render_window(left, left + icons_size_x, top - icons_size_y, top);
|
||||
top -= icon_stride;
|
||||
continue;
|
||||
}
|
||||
unsigned int tex_id;
|
||||
int tex_width, tex_height;
|
||||
if (item->is_action_with_text_image()) {
|
||||
|
||||
@@ -231,6 +231,8 @@ public:
|
||||
int generate_image_texture();
|
||||
|
||||
void render(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int tex_width, unsigned int tex_height, unsigned int icon_size) const;
|
||||
// The ImGui window a pressed item shows, given the icon's rectangle.
|
||||
void render_window(float left, float right, float bottom, float top) const;
|
||||
void render_image(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int tex_width, unsigned int tex_height, unsigned int icon_size) const;
|
||||
private:
|
||||
void set_visible(bool visible) { m_data.visible = visible; }
|
||||
@@ -410,6 +412,10 @@ public:
|
||||
bool update_items_state();
|
||||
|
||||
void render(const GLCanvas3D& parent,GLToolbarItem::EType type = GLToolbarItem::Action);
|
||||
// The ImGui windows of pressed items, built with the same layout as render().
|
||||
void render_item_windows(const GLCanvas3D& parent);
|
||||
// Hash of the state render() draws from: enabled, and each item's state and visibility.
|
||||
size_t get_state_hash() const;
|
||||
void render_arrow(const GLCanvas3D& parent, GLToolbarItem* highlighted_item);
|
||||
|
||||
bool on_mouse(wxMouseEvent& evt, GLCanvas3D& parent);
|
||||
@@ -438,8 +444,8 @@ private:
|
||||
int contains_mouse_vertical(const Vec2d& mouse_pos, const GLCanvas3D& parent) const;
|
||||
|
||||
void render_background(float left, float top, float right, float bottom, float border_w, float border_h) const;
|
||||
void render_horizontal(const GLCanvas3D &parent, GLToolbarItem::EType type);
|
||||
void render_vertical(const GLCanvas3D& parent);
|
||||
void render_horizontal(const GLCanvas3D &parent, GLToolbarItem::EType type, bool draw_icons);
|
||||
void render_vertical(const GLCanvas3D& parent, bool draw_icons);
|
||||
|
||||
bool generate_icons_texture();
|
||||
|
||||
|
||||
+126
-14
@@ -3,6 +3,7 @@
|
||||
#include "libslic3r/Technologies.hpp"
|
||||
#include "libslic3r/Platform.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "BindDialog.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "HMS.hpp"
|
||||
@@ -1120,6 +1121,8 @@ GUI_App::GUI_App()
|
||||
{
|
||||
//app config initializes early becasuse it is used in instance checking in OrcaSlicer.cpp
|
||||
this->init_app_config();
|
||||
m_shortcuts = std::make_unique<ShortcutRegistry>();
|
||||
m_shortcuts->load(*app_config);
|
||||
this->init_download_path();
|
||||
// Note: the WebView2 runtime check (init_webview_runtime) used to run here, but
|
||||
// the constructor executes before wxWidgets is fully initialized and before the
|
||||
@@ -2646,6 +2649,10 @@ void GUI_App::init_app_config()
|
||||
}
|
||||
#endif // _WIN32
|
||||
}
|
||||
// Speed Dial opens on a bare Space from any page by default. Seed the flag so Preferences and the
|
||||
// MainFrame shortcut read the same value; an existing config (true or false) is left untouched.
|
||||
if (app_config->get("enable_speed_dial").empty())
|
||||
app_config->set_bool("enable_speed_dial", true);
|
||||
set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level")));
|
||||
|
||||
}
|
||||
@@ -4625,6 +4632,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter";
|
||||
m_is_recreating_gui = true;
|
||||
|
||||
// The palette injects its translated strings once, at creation; drop the cached dialog so the
|
||||
// next open rebuilds it in the current locale (and can't outlive the old mainframe).
|
||||
if (m_speed_dial_dialog) {
|
||||
m_speed_dial_dialog->Destroy();
|
||||
m_speed_dial_dialog = nullptr;
|
||||
}
|
||||
|
||||
mainframe->shutdown();
|
||||
ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE);
|
||||
@@ -4689,12 +4702,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;
|
||||
@@ -8193,6 +8222,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int mode)
|
||||
update_mode();
|
||||
}
|
||||
|
||||
void GUI_App::set_mode(ConfigOptionMode mode)
|
||||
{
|
||||
const bool was_developer = app_config->get_bool("developer_mode");
|
||||
if (was_developer)
|
||||
app_config->set_bool("developer_mode", false);
|
||||
save_mode(mode);
|
||||
if (was_developer)
|
||||
app_config->save();
|
||||
}
|
||||
|
||||
void GUI_App::enable_developer_mode()
|
||||
{
|
||||
app_config->set_bool("developer_mode", true);
|
||||
update_mode();
|
||||
}
|
||||
|
||||
// Update view mode according to selected menu
|
||||
void GUI_App::update_mode()
|
||||
{
|
||||
@@ -8341,6 +8386,65 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::refresh_plugins()
|
||||
{
|
||||
// The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker
|
||||
// and report completion through the notification manager -- the speed dial needs no dialog.
|
||||
std::thread([]() {
|
||||
wxString error;
|
||||
try {
|
||||
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
|
||||
} catch (const std::exception& ex) {
|
||||
error = from_u8(ex.what());
|
||||
} catch (...) {
|
||||
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
|
||||
}
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
wxTheApp->CallAfter([error]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
if (error.IsEmpty())
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
into_u8(_L("Plugins refreshed.")));
|
||||
else
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::install_local_plugin()
|
||||
{
|
||||
if (mainframe == nullptr)
|
||||
return;
|
||||
|
||||
wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
|
||||
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dialog.ShowModal() != wxID_OK)
|
||||
return;
|
||||
|
||||
wxString message;
|
||||
const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message);
|
||||
if (message.IsEmpty())
|
||||
return; // user cancelled the overwrite prompt
|
||||
|
||||
Plater* plater = this->plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(message));
|
||||
}
|
||||
|
||||
void GUI_App::open_terminal_dialog()
|
||||
{
|
||||
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
|
||||
@@ -8402,14 +8506,18 @@ 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)
|
||||
{
|
||||
static constexpr const char* opengl_fxaa_setting_key = "opengl_fxaa_enabled";
|
||||
static constexpr const char* opengl_fps_cap_setting_key = "opengl_fps_cap";
|
||||
static constexpr const char* opengl_show_fps_overlay_setting_key = "opengl_show_fps_overlay";
|
||||
const std::string previous_opengl_fxaa = app_config->get(opengl_fxaa_setting_key);
|
||||
const std::string previous_opengl_fps_cap = app_config->get(opengl_fps_cap_setting_key);
|
||||
const std::string previous_opengl_show_fps_overlay = app_config->get(opengl_show_fps_overlay_setting_key);
|
||||
// Render settings the canvas reads every frame; a change needs one redraw to show.
|
||||
static constexpr const char* opengl_render_setting_keys[] = {
|
||||
SETTING_OPENGL_FXAA_ENABLED, SETTING_OPENGL_FPS_CAP, SETTING_OPENGL_SHOW_FPS_OVERLAY, SETTING_OPENGL_SCENE_CACHE,
|
||||
SETTING_OPENGL_SKIP_IDENTICAL_FRAMES
|
||||
};
|
||||
std::vector<std::string> previous_opengl_render_settings;
|
||||
for (const char* key : opengl_render_setting_keys)
|
||||
previous_opengl_render_settings.emplace_back(app_config->get(key));
|
||||
|
||||
bool need_recreate_gui = false;
|
||||
std::string pending_language;
|
||||
@@ -8417,7 +8525,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();
|
||||
@@ -8449,10 +8558,10 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
|
||||
}
|
||||
}
|
||||
|
||||
const bool opengl_fxaa_changed = app_config->get(opengl_fxaa_setting_key) != previous_opengl_fxaa;
|
||||
const bool opengl_fps_cap_changed = app_config->get(opengl_fps_cap_setting_key) != previous_opengl_fps_cap;
|
||||
const bool opengl_show_fps_overlay_changed = app_config->get(opengl_show_fps_overlay_setting_key) != previous_opengl_show_fps_overlay;
|
||||
if ((opengl_fxaa_changed || opengl_fps_cap_changed || opengl_show_fps_overlay_changed) && !need_recreate_gui && this->plater_ != nullptr) {
|
||||
bool opengl_render_settings_changed = false;
|
||||
for (size_t i = 0; i < previous_opengl_render_settings.size(); ++i)
|
||||
opengl_render_settings_changed |= app_config->get(opengl_render_setting_keys[i]) != previous_opengl_render_settings[i];
|
||||
if (opengl_render_settings_changed && !need_recreate_gui && this->plater_ != nullptr) {
|
||||
this->plater_->set_current_canvas_as_dirty();
|
||||
this->plater_->get_current_canvas3D()->force_set_focus();
|
||||
}
|
||||
@@ -8466,6 +8575,9 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
|
||||
this->plater_->get_current_canvas3D()->force_set_focus();
|
||||
return;
|
||||
}
|
||||
// Built-in Speed Dial command titles are copied from the catalog at init and don't follow a
|
||||
// live locale switch; rebuild them in the new language before the GUI (and palette) rebuilds.
|
||||
m_action_registry.relocalize_builtins();
|
||||
}
|
||||
|
||||
if (need_recreate_gui)
|
||||
|
||||
@@ -70,6 +70,9 @@ namespace GUI{
|
||||
|
||||
class RemovableDriveManager;
|
||||
class OtherInstanceMessageHandler;
|
||||
class ShortcutRegistry;
|
||||
enum class ShortcutContext : uint8_t;
|
||||
enum class PreferencesTab;
|
||||
class MainFrame;
|
||||
class Sidebar;
|
||||
class ObjectSettings;
|
||||
@@ -286,6 +289,7 @@ private:
|
||||
std::unique_ptr<RemovableDriveManager> m_removable_drive_manager;
|
||||
|
||||
std::unique_ptr<ImGuiWrapper> m_imgui;
|
||||
std::unique_ptr<ShortcutRegistry> m_shortcuts;
|
||||
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
|
||||
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
|
||||
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
|
||||
@@ -483,7 +487,7 @@ public:
|
||||
|
||||
void recreate_GUI(const wxString& message);
|
||||
void system_info();
|
||||
void keyboard_shortcuts();
|
||||
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
|
||||
void troubleshoot();
|
||||
void load_project(wxWindow *parent, wxString& input_file) const;
|
||||
void import_model(wxWindow *parent, wxArrayString& input_files) const;
|
||||
@@ -599,6 +603,11 @@ public:
|
||||
std::string get_saved_mode_str();
|
||||
std::string get_mode_str();
|
||||
void save_mode(const /*ConfigOptionMode*/int mode) ;
|
||||
// Switch to `mode` from the Speed Dial: a developer-mode override hides the saved mode
|
||||
// (get_mode returns comDevelop), so clear it first and persist the choice.
|
||||
void set_mode(ConfigOptionMode mode);
|
||||
// Turn the developer-mode override on and refresh the UI (used before jumping to a Developer setting).
|
||||
void enable_developer_mode();
|
||||
void update_mode();
|
||||
void update_internal_development();
|
||||
void show_ip_address_enter_dialog(wxString title = wxEmptyString);
|
||||
@@ -634,9 +643,13 @@ 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.
|
||||
void refresh_plugins();
|
||||
void install_local_plugin();
|
||||
void open_terminal_dialog();
|
||||
void open_speed_dial();
|
||||
ActionRegistry& action_registry() { return m_action_registry; }
|
||||
@@ -729,6 +742,9 @@ public:
|
||||
size_t get_instance_hash_int () { return m_instance_hash_int; }
|
||||
|
||||
ImGuiWrapper* imgui() { return m_imgui.get(); }
|
||||
ShortcutRegistry& shortcuts() { return *m_shortcuts; }
|
||||
// Saves the bindings and refreshes every menu label, tooltip and accelerator table that shows one.
|
||||
void on_shortcuts_changed();
|
||||
|
||||
PrintHostJobQueue& printhost_job_queue() { return *m_printhost_job_queue.get(); }
|
||||
|
||||
|
||||
+119
-105
@@ -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"
|
||||
@@ -578,113 +579,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
|
||||
return sub_menu;
|
||||
}
|
||||
|
||||
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table keeps
|
||||
// the menu label, the files to load and the per-model behavior in a single place. Labels are wrapped
|
||||
// in L() so they are picked up for translation. Shared with the command palette.
|
||||
const std::vector<MenuFactory::HandyModel>& MenuFactory::handy_models()
|
||||
{
|
||||
static const std::vector<HandyModel> models = {
|
||||
{"orca_cube", L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{"orcasliced_combo", L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{"orca_badge", L("Orca Badge"), {"OrcaBadge.3mf"}},
|
||||
{"orca_tolerance_test", L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
|
||||
{"3dbenchy", L("3DBenchy"), {"3DBenchy.drc"}},
|
||||
{"cali_cat", L("Cali Cat"), {"calicat.drc"}},
|
||||
{"autodesk_fdm_test", L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
|
||||
{"voron_cube", L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
|
||||
{"stanford_bunny", L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
|
||||
{"orca_string_hell", L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
|
||||
};
|
||||
return models;
|
||||
}
|
||||
|
||||
void MenuFactory::load_handy_model(std::size_t index)
|
||||
{
|
||||
const std::vector<HandyModel>& models = handy_models();
|
||||
if (index >= models.size())
|
||||
return;
|
||||
const HandyModel& model = models[index];
|
||||
|
||||
std::vector<boost::filesystem::path> input_files;
|
||||
input_files.reserve(model.file_names.size());
|
||||
for (const auto& file_name : model.file_names)
|
||||
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
|
||||
|
||||
Plater* pl = plater();
|
||||
if (!pl)
|
||||
return;
|
||||
pl->load_files(input_files, LoadStrategy::LoadModel);
|
||||
if (model.arrange_after_import) {
|
||||
pl->set_prepare_state(Job::PREPARE_STATE_MENU);
|
||||
pl->arrange();
|
||||
}
|
||||
|
||||
// Suggest to change settings for stringhell
|
||||
// This serves as mini tutorial for new users
|
||||
if (model.is_stringhell) {
|
||||
wxGetApp().CallAfter([=] {
|
||||
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
|
||||
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
|
||||
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
|
||||
if (is_only_one_wall_top && min_width_top_surface > 0) {
|
||||
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
|
||||
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
|
||||
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
|
||||
"Yes - Change these settings automatically\n"
|
||||
"No - Do not change these settings for me");
|
||||
|
||||
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dialog.ShowModal() == wxID_YES) {
|
||||
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
|
||||
}
|
||||
wxGetApp().plater()->update();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: add submenu for adding handy models
|
||||
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
|
||||
auto sub_menu = new wxMenu;
|
||||
|
||||
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table
|
||||
// keeps the menu label, the files to load and the per-model behavior in a single place and
|
||||
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
|
||||
// with that). Labels are wrapped in L() so they are picked up for translation.
|
||||
struct HandyModel
|
||||
{
|
||||
const char* label;
|
||||
std::vector<std::string> file_names;
|
||||
bool arrange_after_import = false;
|
||||
bool is_stringhell = false;
|
||||
};
|
||||
static const std::vector<HandyModel> handy_models = {
|
||||
{L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{L("Orca Badge"), {"OrcaBadge.3mf"}},
|
||||
{L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
|
||||
{L("3DBenchy"), {"3DBenchy.drc"}},
|
||||
{L("Cali Cat"), {"calicat.drc"}},
|
||||
{L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
|
||||
{L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
|
||||
{L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
|
||||
{L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
|
||||
};
|
||||
|
||||
for (const auto& model : handy_models) {
|
||||
append_menu_item(
|
||||
sub_menu, wxID_ANY, _(model.label), "",
|
||||
[&model](wxCommandEvent&) {
|
||||
std::vector<boost::filesystem::path> input_files;
|
||||
input_files.reserve(model.file_names.size());
|
||||
for (const auto& file_name : model.file_names)
|
||||
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
|
||||
|
||||
plater()->load_files(input_files, LoadStrategy::LoadModel);
|
||||
if (model.arrange_after_import) {
|
||||
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
|
||||
plater()->arrange();
|
||||
}
|
||||
|
||||
// Suggest to change settings for stringhell
|
||||
// This serves as mini tutorial for new users
|
||||
if (model.is_stringhell) {
|
||||
wxGetApp().CallAfter([=] {
|
||||
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
|
||||
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
|
||||
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
|
||||
if (is_only_one_wall_top && min_width_top_surface > 0) {
|
||||
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
|
||||
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
|
||||
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
|
||||
"Yes - Change these settings automatically\n"
|
||||
"No - Do not change these settings for me");
|
||||
|
||||
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dialog.ShowModal() == wxID_YES) {
|
||||
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
|
||||
}
|
||||
wxGetApp().plater()->update();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"", menu);
|
||||
const std::vector<HandyModel>& models = handy_models();
|
||||
for (std::size_t i = 0; i < models.size(); ++i) {
|
||||
append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
|
||||
[i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", menu);
|
||||
}
|
||||
|
||||
|
||||
return sub_menu;
|
||||
}
|
||||
|
||||
// Create a Text/SVG volume through the matching gizmo. `type == INVALID` means "create a new object".
|
||||
// Shared by the add menu and the command palette.
|
||||
static void add_volume_with_gizmo(GLGizmosManager::EType gizmo_type, ModelVolumeType type)
|
||||
{
|
||||
Plater* pl = plater();
|
||||
if (!pl)
|
||||
return;
|
||||
const GLCanvas3D* canvas = pl->canvas3D();
|
||||
if (!canvas)
|
||||
return;
|
||||
GLGizmoBase* gizmo_base = canvas->get_gizmos_manager().get_gizmo(gizmo_type);
|
||||
if (!gizmo_base)
|
||||
return;
|
||||
|
||||
ModelVolumeType volume_type = type;
|
||||
// no selected object means create new object
|
||||
if (volume_type == ModelVolumeType::INVALID)
|
||||
volume_type = ModelVolumeType::MODEL_PART;
|
||||
|
||||
auto screen_position = canvas->get_popup_menu_position();
|
||||
if (gizmo_type == GLGizmosManager::Emboss) {
|
||||
auto* emboss = dynamic_cast<GLGizmoEmboss*>(gizmo_base);
|
||||
if (emboss == nullptr)
|
||||
return;
|
||||
if (screen_position.has_value())
|
||||
emboss->create_volume(volume_type, *screen_position);
|
||||
else
|
||||
emboss->create_volume(volume_type);
|
||||
} else if (gizmo_type == GLGizmosManager::Svg) {
|
||||
auto* svg = dynamic_cast<GLGizmoSVG*>(gizmo_base);
|
||||
if (svg == nullptr)
|
||||
return;
|
||||
if (screen_position.has_value())
|
||||
svg->create_volume(volume_type, *screen_position);
|
||||
else
|
||||
svg->create_volume(volume_type);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuFactory::add_text_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Emboss, type); }
|
||||
void MenuFactory::add_svg_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Svg, type); }
|
||||
|
||||
static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) {
|
||||
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) {
|
||||
const GLCanvas3D *canvas = plater()->canvas3D();
|
||||
const GLGizmosManager &mng = canvas->get_gizmos_manager();
|
||||
GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type);
|
||||
|
||||
ModelVolumeType volume_type = type;
|
||||
// no selected object means create new object
|
||||
if (volume_type == ModelVolumeType::INVALID)
|
||||
volume_type = ModelVolumeType::MODEL_PART;
|
||||
|
||||
auto screen_position = canvas->get_popup_menu_position();
|
||||
if (gizmo_type == GLGizmosManager::Emboss) {
|
||||
auto emboss = dynamic_cast<GLGizmoEmboss *>(gizmo_base);
|
||||
assert(emboss != nullptr);
|
||||
if (emboss == nullptr) return;
|
||||
if (screen_position.has_value()) {
|
||||
emboss->create_volume(volume_type, *screen_position);
|
||||
} else {
|
||||
emboss->create_volume(volume_type);
|
||||
}
|
||||
} else if (gizmo_type == GLGizmosManager::Svg) {
|
||||
auto svg = dynamic_cast<GLGizmoSVG *>(gizmo_base);
|
||||
assert(svg != nullptr);
|
||||
if (svg == nullptr) return;
|
||||
if (screen_position.has_value()) {
|
||||
svg->create_volume(volume_type, *screen_position);
|
||||
} else {
|
||||
svg->create_volume(volume_type);
|
||||
}
|
||||
}
|
||||
};
|
||||
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
|
||||
|
||||
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
|
||||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
|
||||
@@ -2065,13 +2084,8 @@ wxMenu* MenuFactory::assemble_part_menu()
|
||||
|
||||
void MenuFactory::append_menu_item_clone(wxMenu* menu)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
static const wxString ctrl = ("Ctrl+");
|
||||
#else
|
||||
// FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent?
|
||||
static const wxString ctrl = _L("Ctrl+");
|
||||
#endif
|
||||
append_menu_item(menu, wxID_ANY, _L("Clone") + "\t" + ctrl + "K", "",
|
||||
const std::string accel = wxGetApp().shortcuts().accelerator(Shortcut::CloneSelected);
|
||||
append_menu_item(menu, wxID_ANY, _L("Clone") + (accel.empty() ? wxString() : "\t" + from_u8(accel)), "",
|
||||
[](wxCommandEvent&) {
|
||||
plater()->clone_selection();
|
||||
}, "", nullptr,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
|
||||
@@ -51,6 +52,23 @@ public:
|
||||
static std::vector<wxBitmap> get_text_volume_bitmaps();
|
||||
static std::vector<wxBitmap> get_svg_volume_bitmaps();
|
||||
|
||||
// Orca: handy models shipped under <resources>/handy_models. The menu and the command palette
|
||||
// share this table so the model list and its per-model behavior live in one place.
|
||||
struct HandyModel
|
||||
{
|
||||
const char* key;
|
||||
const char* label;
|
||||
std::vector<std::string> file_names;
|
||||
bool arrange_after_import = false;
|
||||
bool is_stringhell = false;
|
||||
};
|
||||
static const std::vector<HandyModel>& handy_models();
|
||||
static void load_handy_model(std::size_t index);
|
||||
|
||||
// Add a Text/SVG volume through the Emboss/SVG gizmo. Shared by the add menu and the palette.
|
||||
static void add_text_volume(ModelVolumeType type);
|
||||
static void add_svg_volume(ModelVolumeType type);
|
||||
|
||||
MenuFactory();
|
||||
~MenuFactory() = default;
|
||||
|
||||
|
||||
@@ -5,6 +5,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"
|
||||
@@ -247,56 +248,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
|
||||
@@ -1820,36 +1780,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()) {
|
||||
@@ -1866,6 +1800,43 @@ void ObjectList::key_event(wxKeyEvent& event)
|
||||
}
|
||||
#endif /* __WXOSX__ */
|
||||
|
||||
#ifdef __WXOSX__
|
||||
void ObjectList::update_shortcut_accelerators()
|
||||
{
|
||||
std::vector<wxAcceleratorEntry> entries;
|
||||
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
|
||||
for (Shortcut shortcut : shortcuts_in(ShortcutContext::ObjectList))
|
||||
if (const KeyChord chord = shortcuts.binding(shortcut); chord.valid())
|
||||
entries.push_back(chord.to_accelerator_entry(m_shortcut_id_base + int(shortcut)));
|
||||
for (int i = 1; i < 10; ++i) {
|
||||
entries.emplace_back(wxACCEL_NORMAL, '0' + i, wxID_LAST + i);
|
||||
entries.emplace_back(wxACCEL_NORMAL, WXK_NUMPAD0 + i, wxID_LAST + i);
|
||||
}
|
||||
m_accel = wxAcceleratorTable(int(entries.size()), entries.data());
|
||||
SetAcceleratorTable(m_accel);
|
||||
}
|
||||
#endif /* __WXOSX__ */
|
||||
|
||||
bool ObjectList::dispatch_shortcut(Shortcut shortcut)
|
||||
{
|
||||
switch (shortcut) {
|
||||
case Shortcut::DeleteSelected: remove(); break;
|
||||
case Shortcut::SelectAll: select_item_all_children(); break;
|
||||
case Shortcut::Copy: copy(); break;
|
||||
case Shortcut::Paste: paste(); break;
|
||||
case Shortcut::Cut: cut(); break;
|
||||
case Shortcut::Undo: undo(); break;
|
||||
case Shortcut::Redo: redo(); break;
|
||||
case Shortcut::CloneSelected: clone(); break;
|
||||
case Shortcut::AddInstance: increase_instances(); break;
|
||||
case Shortcut::RemoveInstance: decrease_instances(); break;
|
||||
case Shortcut::TogglePrintable: toggle_printable_state(); break;
|
||||
case Shortcut::ToggleAutoDrop: toggle_auto_drop(); break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ObjectList::OnBeginDrag(wxDataViewEvent &event)
|
||||
{
|
||||
const bool mult_sel = multiple_selection();
|
||||
|
||||
@@ -40,6 +40,9 @@ typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
|
||||
#define FIX_THROUGH_CGAL_ALWAYS 1
|
||||
|
||||
namespace GUI {
|
||||
|
||||
enum class Shortcut : uint8_t;
|
||||
|
||||
struct ObjectVolumeID {
|
||||
ModelObject* object{ nullptr };
|
||||
ModelVolume* volume{ nullptr };
|
||||
@@ -270,7 +273,11 @@ public:
|
||||
void extruder_editing();
|
||||
#ifndef __WXOSX__
|
||||
void key_event(wxKeyEvent& event);
|
||||
#else
|
||||
// wxDataViewCtrl never sees key events on macOS, so the bindings are installed as accelerators.
|
||||
void update_shortcut_accelerators();
|
||||
#endif /* __WXOSX__ */
|
||||
bool dispatch_shortcut(Shortcut shortcut);
|
||||
|
||||
void copy();
|
||||
void paste();
|
||||
@@ -482,8 +489,8 @@ public:
|
||||
|
||||
private:
|
||||
#ifdef __WXOSX__
|
||||
// void OnChar(wxKeyEvent& event);
|
||||
wxAcceleratorTable m_accel;
|
||||
wxWindowID m_shortcut_id_base;
|
||||
#endif /* __WXOSX__ */
|
||||
void OnContextMenu(wxDataViewEvent &event);
|
||||
void list_manipulation(const wxPoint& mouse_pos, bool evt_context_menu = false);
|
||||
|
||||
@@ -280,8 +280,6 @@ bool Preview::init(wxWindow* parent, Bed3D& bed, Model* model)
|
||||
m_canvas->enable_assemble_view_toolbar(false);
|
||||
|
||||
// sizer, m_canvas_widget
|
||||
m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_layers_slider_from_canvas, this);
|
||||
|
||||
wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
main_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0);
|
||||
|
||||
@@ -505,28 +503,6 @@ void Preview::update_layers_slider_mode()
|
||||
m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder, can_change_color);
|
||||
}
|
||||
|
||||
void Preview::update_layers_slider_from_canvas(wxKeyEvent &event)
|
||||
{
|
||||
if (event.HasModifiers()) {
|
||||
event.Skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto key = event.GetKeyCode();
|
||||
|
||||
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
|
||||
IMSlider *m_moves_slider = m_canvas->get_gcode_viewer().get_moves_slider();
|
||||
if (key == 'L') {
|
||||
if(!m_layers_slider->switch_one_layer_mode())
|
||||
event.Skip();
|
||||
m_canvas->set_as_dirty();
|
||||
}
|
||||
/*else if (key == WXK_SHIFT)
|
||||
m_layers_slider->UseDefaultColors(false);*/
|
||||
else
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void Preview::update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range)
|
||||
{
|
||||
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
|
||||
|
||||
@@ -171,7 +171,6 @@ private:
|
||||
|
||||
void update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range = false);
|
||||
void update_layers_slider_mode();
|
||||
void update_layers_slider_from_canvas(wxKeyEvent &event);
|
||||
//BBS: add only gcode mode
|
||||
void load_print_as_fff(bool keep_z_range = false, bool only_gcode = false);
|
||||
};
|
||||
|
||||
@@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window);
|
||||
#ifdef __WXOSX__
|
||||
void dataview_remove_insets(wxDataViewCtrl* dv);
|
||||
void staticbox_remove_margin(wxStaticBox* sb);
|
||||
// Clip a top-level window (and its webview) to a rounded rect with a native layer.
|
||||
void set_window_corner_radius(wxWindow* win, int radius);
|
||||
#endif
|
||||
|
||||
#ifdef __WXGTK__
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/sysctl.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <wx/osx/cocoa/dataview.h>
|
||||
#import "GUI_Utils.hpp"
|
||||
|
||||
@@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) {
|
||||
[nativeBox setBorderWidth:0];
|
||||
}
|
||||
|
||||
// wxOSX SetShape only clears the window background; it cannot clip to a region. Clipping the
|
||||
// window's view layer to a rounded rect is what actually rounds the opaque webview inside.
|
||||
void set_window_corner_radius(wxWindow* win, int radius) {
|
||||
if (!win)
|
||||
return;
|
||||
NSView* view = (NSView*)win->GetHandle();
|
||||
if (!view)
|
||||
return;
|
||||
NSWindow* window = [view window];
|
||||
[window setOpaque:NO];
|
||||
[window setBackgroundColor:[NSColor clearColor]];
|
||||
[view setWantsLayer:YES];
|
||||
[[view layer] setCornerRadius:radius];
|
||||
[[view layer] setMasksToBounds:YES];
|
||||
}
|
||||
|
||||
bool is_debugger_present()
|
||||
// Returns true if the current process is being debugged (either
|
||||
// running under the debugger or has a debugger attached post facto).
|
||||
|
||||
@@ -354,7 +354,6 @@ bool GLGizmoAdvancedCut::on_init()
|
||||
if (!GLGizmoRotate3D::on_init())
|
||||
return false;
|
||||
|
||||
m_shortcut_key = WXK_CONTROL_C;
|
||||
|
||||
// initiate info shortcuts
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoAssembly.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
@@ -46,7 +47,7 @@ bool GLGizmoAssembly::on_init()
|
||||
{
|
||||
GLGizmoMeasure::on_init();
|
||||
|
||||
m_shortcut_key = WXK_CONTROL_Y;
|
||||
m_shortcut = Shortcut::GizmoAssembly;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,6 +70,7 @@ bool GLGizmoAssembly::on_is_activable() const
|
||||
|
||||
void GLGizmoAssembly::on_render_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
render_dimensioning_if_scene_reused();
|
||||
static std::optional<Measure::SurfaceFeature> last_feature;
|
||||
static EMode last_mode = EMode::FeatureSelection;
|
||||
static SelectedFeatures last_selected_features;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <glad/gl.h>
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_Colors.hpp"
|
||||
|
||||
@@ -299,7 +300,6 @@ GLGizmoBase::GLGizmoBase(GLCanvas3D &parent, const std::string &icon_filename, u
|
||||
: m_parent(parent)
|
||||
, m_group_id(-1)
|
||||
, m_state(Off)
|
||||
, m_shortcut_key(NO_SHORTCUT_KEY_VALUE)
|
||||
, m_icon_filename(icon_filename)
|
||||
, m_sprite_id(sprite_id)
|
||||
, m_imgui(wxGetApp().imgui())
|
||||
@@ -515,11 +515,8 @@ void GLGizmoBase::render_input_window(float x, float y, float bottom_limit)
|
||||
|
||||
std::string GLGizmoBase::get_name(bool include_shortcut) const
|
||||
{
|
||||
int key = get_shortcut_key();
|
||||
std::string out = on_get_name();
|
||||
if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z)
|
||||
out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]";
|
||||
return out;
|
||||
const std::string name = on_get_name();
|
||||
return include_shortcut && m_shortcut.has_value() ? wxGetApp().shortcuts().with_key(name, *m_shortcut) : name;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "slic3r/GUI/3DScene.hpp"
|
||||
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include <optional>
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
@@ -29,6 +30,7 @@ namespace GUI {
|
||||
|
||||
|
||||
class ImGuiWrapper;
|
||||
enum class Shortcut : uint8_t;
|
||||
class GLCanvas3D;
|
||||
enum class CommonGizmosDataID;
|
||||
class CommonGizmosDataPool;
|
||||
@@ -72,9 +74,6 @@ public:
|
||||
NegZ = 1 << 5,
|
||||
};
|
||||
|
||||
// Represents NO key(button on keyboard) value
|
||||
static const int NO_SHORTCUT_KEY_VALUE = 0;
|
||||
|
||||
protected:
|
||||
struct Grabber
|
||||
{
|
||||
@@ -138,7 +137,7 @@ protected:
|
||||
|
||||
int m_group_id; // TODO: remove only for rotate
|
||||
EState m_state;
|
||||
int m_shortcut_key;
|
||||
std::optional<Shortcut> m_shortcut; // the registry entry that opens this gizmo
|
||||
std::string m_icon_filename;
|
||||
unsigned int m_sprite_id;
|
||||
int m_hover_id{ -1 };
|
||||
@@ -169,7 +168,7 @@ public:
|
||||
EState get_state() const { return m_state; }
|
||||
void set_state(EState state) { m_state = state; on_set_state(); }
|
||||
|
||||
int get_shortcut_key() const { return m_shortcut_key; }
|
||||
std::optional<Shortcut> shortcut() const { return m_shortcut; }
|
||||
|
||||
const std::string& get_icon_filename() const { return m_icon_filename; }
|
||||
|
||||
@@ -179,6 +178,8 @@ public:
|
||||
bool is_selectable() const { return on_is_selectable(); }
|
||||
CommonGizmosDataID get_requirements() const { return on_get_requirements(); }
|
||||
virtual bool wants_enter_leave_snapshots() const { return false; }
|
||||
// True when what on_render() draws would change if the cursor moved.
|
||||
virtual bool render_follows_cursor() const { return false; }
|
||||
virtual std::string get_gizmo_entering_text() const { assert(false); return ""; }
|
||||
virtual std::string get_gizmo_leaving_text() const { assert(false); return ""; }
|
||||
virtual std::string get_action_snapshot_name() const;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -167,6 +167,8 @@ protected:
|
||||
|
||||
std::string on_get_name() const override;
|
||||
bool on_is_activable() const override;
|
||||
// The preview ear is drawn only while the cursor is on the model.
|
||||
bool render_follows_cursor() const override { return render_hover_point.has_value(); }
|
||||
//bool on_is_selectable() const override;
|
||||
virtual CommonGizmosDataID on_get_requirements() const override;
|
||||
void on_load(cereal::BinaryInputArchive& ar) override;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
@@ -1310,7 +1311,7 @@ void GLGizmoCut3D::render_cut_line()
|
||||
bool GLGizmoCut3D::on_init()
|
||||
{
|
||||
m_grabbers.emplace_back();
|
||||
m_shortcut_key = WXK_CONTROL_C;
|
||||
m_shortcut = Shortcut::GizmoCut;
|
||||
|
||||
// initiate info shortcuts
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoEmboss.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp" // to update title when add text
|
||||
@@ -727,7 +728,7 @@ bool GLGizmoEmboss::on_init()
|
||||
m_rotate_gizmo.set_highlight_color(gray_color);
|
||||
|
||||
// NOTE: It has special handling in GLGizmosManager::handle_shortcut
|
||||
m_shortcut_key = WXK_CONTROL_T;
|
||||
m_shortcut = Shortcut::GizmoEmboss;
|
||||
|
||||
m_shortcuts = {
|
||||
{_L("Drag"), _L("Position on surface")}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
@@ -78,7 +79,7 @@ std::string GLGizmoFdmSupports::on_get_name() const
|
||||
bool GLGizmoFdmSupports::on_init()
|
||||
{
|
||||
// BBS
|
||||
m_shortcut_key = WXK_CONTROL_L;
|
||||
m_shortcut = Shortcut::GizmoFdmSupports;
|
||||
|
||||
m_desc["perform"] = _L("Apply");
|
||||
m_desc["on_overhangs_only"] = _L("On highlighted overhangs only");
|
||||
@@ -149,25 +150,14 @@ void GLGizmoFdmSupports::render_painter_gizmo()
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
// BBS
|
||||
bool GLGizmoFdmSupports::on_key_down_select_tool_type(int keyCode) {
|
||||
switch (keyCode)
|
||||
{
|
||||
case 'F':
|
||||
m_current_tool = ImGui::FillButtonIcon;
|
||||
break;
|
||||
case 'S':
|
||||
m_current_tool = ImGui::SphereButtonIcon;
|
||||
break;
|
||||
case 'C':
|
||||
m_current_tool = ImGui::CircleButtonIcon;
|
||||
break;
|
||||
case 'G':
|
||||
m_current_tool = ImGui::GapFillIcon;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
bool GLGizmoFdmSupports::on_tool_shortcut(Shortcut shortcut)
|
||||
{
|
||||
switch (shortcut) {
|
||||
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
|
||||
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
|
||||
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
|
||||
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ public:
|
||||
state_ready
|
||||
};
|
||||
|
||||
//BBS
|
||||
bool on_key_down_select_tool_type(int keyCode);
|
||||
bool on_tool_shortcut(Shortcut shortcut) override;
|
||||
|
||||
protected:
|
||||
void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoFlatten.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
|
||||
|
||||
@@ -54,7 +55,7 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
|
||||
|
||||
bool GLGizmoFlatten::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_F;
|
||||
m_shortcut = Shortcut::GizmoFlatten;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/MsgDialog.hpp"
|
||||
@@ -31,7 +32,7 @@ std::string GLGizmoFuzzySkin::on_get_name() const
|
||||
|
||||
bool GLGizmoFuzzySkin::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_H;
|
||||
m_shortcut = Shortcut::GizmoFuzzySkin;
|
||||
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
const wxString alt = GUI::shortkey_alt_prefix();
|
||||
|
||||
@@ -25,7 +25,6 @@ GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filenam
|
||||
|
||||
bool GLGizmoHollow::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_H;
|
||||
m_desc["enable"] = _(L("Hollow this object"));
|
||||
m_desc["preview"] = _(L("Preview hollowed and drilled model"));
|
||||
m_desc["offset"] = _(L("Offset")) + ": ";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
@@ -449,7 +450,7 @@ bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_po
|
||||
|
||||
bool GLGizmoMeasure::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_U;
|
||||
m_shortcut = Shortcut::GizmoMeasure;
|
||||
|
||||
const wxString shift = GUI::shortkey_shift_prefix();
|
||||
|
||||
@@ -563,8 +564,30 @@ void GLGizmoMeasure::init_plane_glmodel(GripperType gripper_type, const Measure:
|
||||
}
|
||||
}
|
||||
|
||||
bool GLGizmoMeasure::render_follows_cursor() const
|
||||
{
|
||||
// The two raycasts on_render() starts with, without their side effects.
|
||||
if (m_editing_distance)
|
||||
return false;
|
||||
|
||||
const Vec2d mouse_position = m_parent.get_local_mouse_position();
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
Vec3f hit = Vec3f::Zero();
|
||||
Vec3f normal = Vec3f::Zero();
|
||||
for (const auto& item : m_gripper_id_raycast_map) {
|
||||
if (item.second->get_id() > 0 && item.second->get_raycaster()->closest_hit(mouse_position, item.second->get_transform(), camera, hit, normal))
|
||||
return true;
|
||||
}
|
||||
for (const auto& item : m_mesh_raycaster_map) {
|
||||
if (item.second->get_raycaster()->unproject_on_mesh(mouse_position, item.second->get_transform(), camera, hit, normal))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GLGizmoMeasure::on_render()
|
||||
{
|
||||
m_rendered_this_frame = true;
|
||||
#if ENABLE_MEASURE_GIZMO_DEBUG
|
||||
render_debug_dialog();
|
||||
#endif // ENABLE_MEASURE_GIZMO_DEBUG
|
||||
@@ -701,35 +724,36 @@ void GLGizmoMeasure::on_render()
|
||||
reset_gripper_pick(GripperType::UNDEFINE, true);
|
||||
|
||||
m_curr_feature = curr_feature;
|
||||
if (!m_curr_feature.has_value())
|
||||
return;
|
||||
m_curr_feature->volume = m_last_hit_volume;
|
||||
m_curr_feature->world_tran = m_mesh_raycaster_map[m_last_hit_volume]->get_transform();
|
||||
// The selected features are drawn below whether or not one is hovered.
|
||||
if (m_curr_feature.has_value()) {
|
||||
m_curr_feature->volume = m_last_hit_volume;
|
||||
m_curr_feature->world_tran = m_mesh_raycaster_map[m_last_hit_volume]->get_transform();
|
||||
|
||||
switch (m_curr_feature->get_type()) {
|
||||
default: { assert(false); break; }
|
||||
case Measure::SurfaceFeatureType::Point:
|
||||
{
|
||||
m_gripper_id_raycast_map[GripperType::POINT] = std::make_shared<PickRaycaster>(POINT_ID, *m_sphere.mesh_raycaster);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Edge:
|
||||
{
|
||||
m_gripper_id_raycast_map[GripperType::EDGE] = std::make_shared<PickRaycaster>(EDGE_ID, *m_cylinder.mesh_raycaster);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Circle: {
|
||||
m_curr_circle.last_circle_feature = nullptr;
|
||||
m_curr_circle.inv_zoom = 0;
|
||||
init_circle_glmodel(GripperType::CIRCLE, *m_curr_feature, m_curr_circle,inv_zoom);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Plane: {
|
||||
update_world_plane_features(m_curr_measuring.get(), *m_curr_feature);
|
||||
m_curr_plane.plane_idx = -1;
|
||||
init_plane_glmodel(GripperType::PLANE, *m_curr_feature, m_curr_plane);
|
||||
break;
|
||||
}
|
||||
switch (m_curr_feature->get_type()) {
|
||||
default: { assert(false); break; }
|
||||
case Measure::SurfaceFeatureType::Point:
|
||||
{
|
||||
m_gripper_id_raycast_map[GripperType::POINT] = std::make_shared<PickRaycaster>(POINT_ID, *m_sphere.mesh_raycaster);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Edge:
|
||||
{
|
||||
m_gripper_id_raycast_map[GripperType::EDGE] = std::make_shared<PickRaycaster>(EDGE_ID, *m_cylinder.mesh_raycaster);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Circle: {
|
||||
m_curr_circle.last_circle_feature = nullptr;
|
||||
m_curr_circle.inv_zoom = 0;
|
||||
init_circle_glmodel(GripperType::CIRCLE, *m_curr_feature, m_curr_circle,inv_zoom);
|
||||
break;
|
||||
}
|
||||
case Measure::SurfaceFeatureType::Plane: {
|
||||
update_world_plane_features(m_curr_measuring.get(), *m_curr_feature);
|
||||
m_curr_plane.plane_idx = -1;
|
||||
init_plane_glmodel(GripperType::PLANE, *m_curr_feature, m_curr_plane);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2136,8 +2160,18 @@ void GLGizmoMeasure::init_render_input_window()
|
||||
m_same_model_object = is_two_volume_in_same_model_object();
|
||||
}
|
||||
|
||||
void GLGizmoMeasure::render_dimensioning_if_scene_reused()
|
||||
{
|
||||
// The labels are ImGui and live one frame; the lines drawn with them land under the cached
|
||||
// scene, which is drawn after the overlay is built.
|
||||
if (!m_rendered_this_frame)
|
||||
render_dimensioning();
|
||||
m_rendered_this_frame = false;
|
||||
}
|
||||
|
||||
void GLGizmoMeasure::on_render_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
render_dimensioning_if_scene_reused();
|
||||
static std::optional<Measure::SurfaceFeature> last_feature;
|
||||
static EMode last_mode = EMode::FeatureSelection;
|
||||
static SelectedFeatures last_selected_features;
|
||||
|
||||
@@ -228,6 +228,9 @@ protected:
|
||||
void restore_scene_raycasters_state();
|
||||
|
||||
void render_dimensioning();
|
||||
// Builds the dimension labels on a frame that reused the cached scene and so skipped on_render().
|
||||
void render_dimensioning_if_scene_reused();
|
||||
bool m_rendered_this_frame{ false };
|
||||
|
||||
#if ENABLE_MEASURE_GIZMO_DEBUG
|
||||
void render_debug_dialog();
|
||||
@@ -255,6 +258,9 @@ protected:
|
||||
bool on_init() override;
|
||||
std::string on_get_name() const override;
|
||||
bool on_is_activable() const override;
|
||||
// The hover is resolved inside on_render(), against the mesh and against the grippers of the
|
||||
// selected features, which sit off the mesh.
|
||||
bool render_follows_cursor() const override;
|
||||
void on_render() override;
|
||||
void on_set_state() override;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "GLGizmoMeshBoolean.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "libslic3r/MeshBoolean.hpp"
|
||||
@@ -104,7 +105,7 @@ bool GLGizmoMeshBoolean::on_mouse(const wxMouseEvent &mouse_event)
|
||||
|
||||
bool GLGizmoMeshBoolean::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_B;
|
||||
m_shortcut = Shortcut::GizmoMeshBoolean;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
@@ -90,7 +91,7 @@ void GLGizmoMmuSegmentation::init_extruders_data()
|
||||
bool GLGizmoMmuSegmentation::on_init()
|
||||
{
|
||||
// BBS
|
||||
m_shortcut_key = WXK_CONTROL_N;
|
||||
m_shortcut = Shortcut::GizmoMmuSegmentation;
|
||||
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
const wxString alt = GUI::shortkey_alt_prefix();
|
||||
@@ -209,30 +210,16 @@ bool GLGizmoMmuSegmentation::on_number_key_down(int number)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoMmuSegmentation::on_key_down_select_tool_type(int keyCode) {
|
||||
switch (keyCode)
|
||||
{
|
||||
case 'F':
|
||||
m_current_tool = ImGui::FillButtonIcon;
|
||||
break;
|
||||
case 'T':
|
||||
m_current_tool = ImGui::TriangleButtonIcon;
|
||||
break;
|
||||
case 'S':
|
||||
m_current_tool = ImGui::SphereButtonIcon;
|
||||
break;
|
||||
case 'C':
|
||||
m_current_tool = ImGui::CircleButtonIcon;
|
||||
break;
|
||||
case 'H':
|
||||
m_current_tool = ImGui::HeightRangeIcon;
|
||||
break;
|
||||
case 'G':
|
||||
m_current_tool = ImGui::GapFillIcon;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
bool GLGizmoMmuSegmentation::on_tool_shortcut(Shortcut shortcut)
|
||||
{
|
||||
switch (shortcut) {
|
||||
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
|
||||
case Shortcut::PaintToolTriangle: m_current_tool = ImGui::TriangleButtonIcon; break;
|
||||
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
|
||||
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
|
||||
case Shortcut::PaintToolHeightRange: m_current_tool = ImGui::HeightRangeIcon; break;
|
||||
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
|
||||
// BBS
|
||||
bool on_number_key_down(int number);
|
||||
bool on_key_down_select_tool_type(int keyCode);
|
||||
bool on_tool_shortcut(Shortcut shortcut) override;
|
||||
|
||||
protected:
|
||||
// BBS
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoMove.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
//BBS: GUI refactor
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
@@ -61,7 +62,7 @@ bool GLGizmoMove3D::on_init()
|
||||
m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 };
|
||||
m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 };
|
||||
|
||||
m_shortcut_key = WXK_CONTROL_M;
|
||||
m_shortcut = Shortcut::GizmoMove;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,15 +131,12 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoPainterBase::render_cursor()
|
||||
std::vector<Transform3d> GLGizmoPainterBase::mesh_trafo_matrices() const
|
||||
{
|
||||
// First check that the mouse pointer is on an object.
|
||||
const ModelObject* mo = m_c->selection_info()->model_object();
|
||||
const Selection& selection = m_parent.get_selection();
|
||||
const ModelInstance* mi = mo->instances[selection.get_instance_idx()];
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
// Precalculate transformations of individual meshes.
|
||||
std::vector<Transform3d> trafo_matrices;
|
||||
for (const ModelVolume* mv : mo->volumes) {
|
||||
if (mv->is_model_part())
|
||||
@@ -154,6 +151,26 @@ void GLGizmoPainterBase::render_cursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
return trafo_matrices;
|
||||
}
|
||||
|
||||
bool GLGizmoPainterBase::render_follows_cursor() const
|
||||
{
|
||||
// The brush is drawn only where the cursor meets the model. update_raycast_cache() keeps the
|
||||
// answer for render_cursor().
|
||||
if (m_c->selection_info() == nullptr || m_c->selection_info()->model_object() == nullptr)
|
||||
return false;
|
||||
update_raycast_cache(m_parent.get_local_mouse_position(), wxGetApp().plater()->get_camera(), mesh_trafo_matrices());
|
||||
return m_rr.mesh_id != -1;
|
||||
}
|
||||
|
||||
void GLGizmoPainterBase::render_cursor()
|
||||
{
|
||||
// First check that the mouse pointer is on an object.
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
// Precalculate transformations of individual meshes.
|
||||
const std::vector<Transform3d> trafo_matrices = mesh_trafo_matrices();
|
||||
// Raycast and return if there's no hit.
|
||||
update_raycast_cache(m_parent.get_local_mouse_position(), camera, trafo_matrices);
|
||||
if (m_rr.mesh_id == -1)
|
||||
|
||||
@@ -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
|
||||
@@ -318,6 +320,8 @@ private:
|
||||
std::vector<ProjectedHeightRange> get_projected_height_range(const Vec2d& mouse_position, double resolution, const std::vector<const ModelVolume*>& part_volumes, const std::vector<Transform3d>& trafo_matrices) const;
|
||||
|
||||
bool is_mesh_point_clipped(const Vec3d& point, const Transform3d& trafo) const;
|
||||
// World transforms of the model parts, in mo->volumes order.
|
||||
std::vector<Transform3d> mesh_trafo_matrices() const;
|
||||
void update_raycast_cache(const Vec2d& mouse_position,
|
||||
const Camera& camera,
|
||||
const std::vector<Transform3d>& trafo_matrices) const;
|
||||
@@ -370,6 +374,7 @@ protected:
|
||||
virtual PainterGizmoType get_painter_type() const = 0;
|
||||
|
||||
bool on_is_activable() const override;
|
||||
bool render_follows_cursor() const override;
|
||||
bool on_is_selectable() const override;
|
||||
void on_load(cereal::BinaryInputArchive& ar) override;
|
||||
void on_save(cereal::BinaryOutputArchive& ar) const override {}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp"
|
||||
@@ -555,7 +556,7 @@ bool GLGizmoRotate3D::on_init()
|
||||
for (unsigned int i = 0; i < 3; ++i)
|
||||
m_gizmos[i].set_highlight_color(AXES_COLOR[i]);
|
||||
|
||||
m_shortcut_key = WXK_CONTROL_R;
|
||||
m_shortcut = Shortcut::GizmoRotate;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoScale.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
|
||||
#include <glad/gl.h>
|
||||
@@ -135,7 +136,7 @@ bool GLGizmoScale3D::on_init()
|
||||
|
||||
// BBS
|
||||
m_grabbers[4].enabled = false;
|
||||
m_shortcut_key = WXK_CONTROL_S;
|
||||
m_shortcut = Shortcut::GizmoScale;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
@@ -28,7 +29,7 @@ void GLGizmoSeam::on_shutdown()
|
||||
|
||||
bool GLGizmoSeam::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_P;
|
||||
m_shortcut = Shortcut::GizmoSeam;
|
||||
|
||||
const wxString ctrl = GUI::shortkey_ctrl_prefix();
|
||||
const wxString alt = GUI::shortkey_alt_prefix();
|
||||
@@ -86,19 +87,12 @@ void GLGizmoSeam::render_painter_gizmo()
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
// BBS
|
||||
bool GLGizmoSeam::on_key_down_select_tool_type(int keyCode) {
|
||||
switch (keyCode)
|
||||
{
|
||||
case 'S':
|
||||
m_current_tool = ImGui::SphereButtonIcon;
|
||||
break;
|
||||
case 'C':
|
||||
m_current_tool = ImGui::CircleButtonIcon;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
bool GLGizmoSeam::on_tool_shortcut(Shortcut shortcut)
|
||||
{
|
||||
switch (shortcut) {
|
||||
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
|
||||
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ public:
|
||||
|
||||
void render_painter_gizmo() override;
|
||||
|
||||
//BBS
|
||||
bool on_key_down_select_tool_type(int keyCode);
|
||||
bool on_tool_shortcut(Shortcut shortcut) override;
|
||||
|
||||
protected:
|
||||
// BBS
|
||||
|
||||
@@ -34,7 +34,6 @@ GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& ic
|
||||
|
||||
bool GLGizmoSlaSupports::on_init()
|
||||
{
|
||||
m_shortcut_key = WXK_CONTROL_L;
|
||||
|
||||
m_desc["head_diameter"] = _L("Head diameter") + ": ";
|
||||
m_desc["lock_supports"] = _L("Lock supports under new islands");
|
||||
|
||||
@@ -261,7 +261,6 @@ bool GLGizmoText::on_init()
|
||||
//m_avail_font_names = init_occt_fonts();
|
||||
update_font_texture();
|
||||
m_scale = m_imgui->get_font_size();
|
||||
m_shortcut_key = WXK_CONTROL_T;
|
||||
|
||||
m_grabbers.push_back(Grabber());
|
||||
|
||||
|
||||
@@ -410,6 +410,10 @@ void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal, bool ver
|
||||
|
||||
void ObjectClipper::set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos)
|
||||
{
|
||||
// Called every frame by GLGizmoCut3D::on_render(), usually with the plane already set.
|
||||
if (m_clp && *m_clp == ClippingPlane(cpl_normal, cpl_offset) && m_clp_ratio == pos)
|
||||
return;
|
||||
|
||||
m_clp.reset(new ClippingPlane(cpl_normal, cpl_offset));
|
||||
m_clp_ratio = pos;
|
||||
get_pool()->get_canvas()->set_as_dirty();
|
||||
|
||||
@@ -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"
|
||||
@@ -36,6 +37,8 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
#include <wx/glcanvas.h>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -234,8 +237,10 @@ bool GLGizmosManager::init()
|
||||
#ifdef SLIC3R_CAD
|
||||
// Registered last: Primitive and Sketch are the final entries before Undefined, so
|
||||
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
|
||||
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
|
||||
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
|
||||
if (wxGetApp().is_enable_cad_feature()) {
|
||||
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
|
||||
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
|
||||
}
|
||||
#endif
|
||||
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
|
||||
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
|
||||
@@ -482,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;
|
||||
}
|
||||
@@ -498,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
|
||||
@@ -629,6 +637,7 @@ void GLGizmosManager::render_painter_assemble_view() const
|
||||
m_assemble_view_data->model_objects_clipper()->render_cut();
|
||||
}
|
||||
|
||||
// The icon bar, drawn with GL.
|
||||
void GLGizmosManager::render_overlay()
|
||||
{
|
||||
if (!m_enabled)
|
||||
@@ -637,7 +646,27 @@ void GLGizmosManager::render_overlay()
|
||||
if (m_icons_texture_dirty)
|
||||
generate_icons_texture();
|
||||
|
||||
do_render_overlay();
|
||||
do_render_overlay(true);
|
||||
}
|
||||
|
||||
// The open gizmo's settings panel, ImGui.
|
||||
void GLGizmosManager::render_overlay_input_window()
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
do_render_overlay(false);
|
||||
}
|
||||
|
||||
size_t GLGizmosManager::get_overlay_state_hash() const
|
||||
{
|
||||
size_t hash = 0;
|
||||
boost::hash_combine(hash, m_enabled);
|
||||
boost::hash_combine(hash, (int)m_hover);
|
||||
boost::hash_combine(hash, (int)m_current);
|
||||
boost::hash_combine(hash, (int)m_highlight.first);
|
||||
boost::hash_combine(hash, m_highlight.second);
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::string GLGizmosManager::get_tooltip() const
|
||||
@@ -831,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))
|
||||
@@ -858,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':
|
||||
//{
|
||||
@@ -907,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();
|
||||
|
||||
@@ -1052,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)
|
||||
@@ -1220,7 +1213,9 @@ void GLGizmosManager::render_arrow(const GLCanvas3D& parent, EType highlighted_t
|
||||
|
||||
//BBS: GUI refactor: GLToolbar&&Gizmo adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-top
|
||||
void GLGizmosManager::do_render_overlay() const
|
||||
// draw_icons selects the icon bar (GL) or the open gizmo's input window (ImGui), placed by the same
|
||||
// layout walk.
|
||||
void GLGizmosManager::do_render_overlay(bool draw_icons) const
|
||||
{
|
||||
const std::vector<size_t> selectable_idxs = get_selectable_idxs();
|
||||
if (selectable_idxs.empty())
|
||||
@@ -1259,7 +1254,8 @@ void GLGizmosManager::do_render_overlay() const
|
||||
}
|
||||
float top_y = 1.0f;
|
||||
|
||||
render_background(top_x, top_y, top_x + width, top_y - height, border_w, border_h);
|
||||
if (draw_icons)
|
||||
render_background(top_x, top_y, top_x + width, top_y - height, border_w, border_h);
|
||||
|
||||
top_x += border_w;
|
||||
top_y -= border_h;
|
||||
@@ -1296,7 +1292,8 @@ void GLGizmosManager::do_render_overlay() const
|
||||
const float v_top = v_offset + sprite_id * dv;
|
||||
const float v_bottom = v_top + dv - v_offset;
|
||||
|
||||
GLTexture::render_sub_texture(icons_texture_id, top_x, top_x + icons_size_x, top_y - icons_size_y, top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } });
|
||||
if (draw_icons)
|
||||
GLTexture::render_sub_texture(icons_texture_id, top_x, top_x + icons_size_x, top_y - icons_size_y, top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } });
|
||||
if (idx == m_current
|
||||
// Orca: Show Svg dialog at the same place as emboss gizmo
|
||||
|| (m_current == Svg && idx == Emboss)) {
|
||||
@@ -1304,7 +1301,8 @@ void GLGizmosManager::do_render_overlay() const
|
||||
//render_input_window uses a different coordination(imgui)
|
||||
//1. no need to scale by camera zoom, set {0,0} at left-up corner for imgui
|
||||
//gizmo->render_input_window(width, 0.5f * cnv_h - zoomed_top_y * zoom, toolbar_top);
|
||||
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
|
||||
if (!draw_icons)
|
||||
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
|
||||
|
||||
is_render_current = true;
|
||||
}
|
||||
@@ -1312,7 +1310,7 @@ void GLGizmosManager::do_render_overlay() const
|
||||
}
|
||||
|
||||
// BBS simplify gizmo is not a selected gizmo and need to render input window
|
||||
if (!is_render_current && m_current != Undefined) {
|
||||
if (!draw_icons && !is_render_current && m_current != Undefined) {
|
||||
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
|
||||
}
|
||||
}
|
||||
@@ -1344,7 +1342,8 @@ GLGizmoBase* GLGizmosManager::get_current() const
|
||||
|
||||
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
|
||||
{
|
||||
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get();
|
||||
// m_gizmos ends before the enum does when the CAD gizmos are not registered.
|
||||
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
|
||||
}
|
||||
|
||||
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -296,6 +299,9 @@ public:
|
||||
void render_painter_assemble_view() const;
|
||||
|
||||
void render_overlay();
|
||||
void render_overlay_input_window();
|
||||
// Hash of the state render_overlay() draws from: enabled, hover, current and highlight.
|
||||
size_t get_overlay_state_hash() const;
|
||||
|
||||
void render_arrow(const GLCanvas3D& parent, EType highlighted_type) const;
|
||||
|
||||
@@ -329,7 +335,7 @@ private:
|
||||
|
||||
void render_background(float left, float top, float right, float bottom, float border_w, float border_h) const;
|
||||
|
||||
void do_render_overlay() const;
|
||||
void do_render_overlay(bool draw_icons) const;
|
||||
|
||||
bool generate_icons_texture();
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -590,11 +590,39 @@ void ImGuiWrapper::new_frame()
|
||||
// BBL: end copy & paste
|
||||
}
|
||||
|
||||
void ImGuiWrapper::render()
|
||||
ImDrawData* ImGuiWrapper::end_frame()
|
||||
{
|
||||
ImGui::Render();
|
||||
render_draw_data(ImGui::GetDrawData());
|
||||
m_new_frame_open = false;
|
||||
return ImGui::GetDrawData();
|
||||
}
|
||||
|
||||
void ImGuiWrapper::render(ImDrawData* draw_data)
|
||||
{
|
||||
render_draw_data(draw_data);
|
||||
}
|
||||
|
||||
ImGuiID ImGuiWrapper::draw_data_signature(const ImDrawData* draw_data)
|
||||
{
|
||||
ImGuiID hash = 0;
|
||||
if (draw_data == nullptr)
|
||||
return hash;
|
||||
|
||||
for (int i = 0; i < draw_data->CmdListsCount; ++i) {
|
||||
const ImDrawList* list = draw_data->CmdLists[i];
|
||||
hash = ImHashData(list->VtxBuffer.Data, list->VtxBuffer.Size * sizeof(ImDrawVert), hash);
|
||||
hash = ImHashData(list->IdxBuffer.Data, list->IdxBuffer.Size * sizeof(ImDrawIdx), hash);
|
||||
// ImDrawCmd has padding, and a hovered ImageButton3() differs only in TextureId.
|
||||
for (const ImDrawCmd& cmd : list->CmdBuffer) {
|
||||
hash = ImHashData(&cmd.ClipRect, sizeof(cmd.ClipRect), hash);
|
||||
hash = ImHashData(&cmd.TextureId, sizeof(cmd.TextureId), hash);
|
||||
hash = ImHashData(&cmd.VtxOffset, sizeof(cmd.VtxOffset), hash);
|
||||
hash = ImHashData(&cmd.IdxOffset, sizeof(cmd.IdxOffset), hash);
|
||||
hash = ImHashData(&cmd.ElemCount, sizeof(cmd.ElemCount), hash);
|
||||
hash = ImHashData(&cmd.UserCallback, sizeof(cmd.UserCallback), hash);
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
ImVec2 ImGuiWrapper::calc_text_size(std::string_view text,
|
||||
|
||||
@@ -98,7 +98,11 @@ public:
|
||||
const ImWchar *get_glyph_ranges() const { return m_glyph_ranges; } // language specific
|
||||
|
||||
void new_frame();
|
||||
void render();
|
||||
// Ends the frame and returns its draw data without drawing it.
|
||||
ImDrawData* end_frame();
|
||||
void render(ImDrawData* draw_data);
|
||||
// Hash of every draw list's vertices, indices and commands.
|
||||
static ImGuiID draw_data_signature(const ImDrawData* draw_data);
|
||||
|
||||
float scaled(float x) const { return x * m_font_size; }
|
||||
ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); }
|
||||
|
||||
@@ -6,358 +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")},
|
||||
//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") },
|
||||
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
|
||||
|
||||
};
|
||||
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);
|
||||
@@ -366,5 +327,227 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const ShortcutsItem& s
|
||||
return main_page;
|
||||
}
|
||||
|
||||
void KBShortcutsDialog::edit_shortcut(Shortcut shortcut)
|
||||
{
|
||||
ShortcutCaptureDialog dlg(this, shortcut);
|
||||
if (dlg.ShowModal() != wxID_OK)
|
||||
return;
|
||||
const wxString question = wxString::Format(_L("%s is assigned to %s. Reassign it to %s?"),
|
||||
join_keys(to_wx(dlg.chord().display_parts())), shortcut_names(dlg.conflicts()), _(shortcut_info(shortcut).name));
|
||||
if (!take_chord_from(shortcut, dlg.conflicts(), question))
|
||||
return;
|
||||
wxGetApp().shortcuts().bind(shortcut, dlg.chord());
|
||||
apply_bindings();
|
||||
}
|
||||
|
||||
void KBShortcutsDialog::reset_shortcut(Shortcut shortcut)
|
||||
{
|
||||
const std::vector<Shortcut> conflicts = wxGetApp().shortcuts().conflicts(shortcut, shortcut_info(shortcut).default_chord);
|
||||
const wxString question = wxString::Format(_L("The default %s is assigned to %s. Reassign it to %s?"),
|
||||
join_keys(to_wx(shortcut_info(shortcut).default_chord.display_parts())), shortcut_names(conflicts), _(shortcut_info(shortcut).name));
|
||||
if (!take_chord_from(shortcut, conflicts, question))
|
||||
return;
|
||||
wxGetApp().shortcuts().reset(shortcut);
|
||||
apply_bindings();
|
||||
}
|
||||
|
||||
bool KBShortcutsDialog::take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question)
|
||||
{
|
||||
if (conflicts.empty())
|
||||
return true;
|
||||
MessageDialog confirm(this, question, _(shortcut_info(shortcut).name), wxICON_QUESTION | wxOK | wxCANCEL);
|
||||
if (confirm.ShowModal() != wxID_OK)
|
||||
return false;
|
||||
for (Shortcut other : conflicts)
|
||||
wxGetApp().shortcuts().bind(other, KeyChord{});
|
||||
return true;
|
||||
}
|
||||
|
||||
void KBShortcutsDialog::apply_bindings()
|
||||
{
|
||||
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
|
||||
std::set<wxWindow*> pages;
|
||||
for (const EditableRow& row : m_editable_rows) {
|
||||
const int chord_width = set_chord_labels(row.modifiers, row.key, to_wx(shortcuts.binding(row.shortcut).display_parts()));
|
||||
row.description->SetLabel(_(shortcut_info(row.shortcut).name));
|
||||
row.description->Wrap(m_row_text_width - chord_width);
|
||||
row.reset->Show(shortcuts.is_customized(row.shortcut));
|
||||
pages.insert(row.key->GetParent());
|
||||
}
|
||||
for (wxWindow* page : pages)
|
||||
page->Layout();
|
||||
wxGetApp().on_shortcuts_changed();
|
||||
}
|
||||
|
||||
int KBShortcutsDialog::set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts)
|
||||
{
|
||||
const wxString last = parts.empty() ? wxString() : parts.back();
|
||||
if (!parts.empty())
|
||||
parts.pop_back();
|
||||
modifiers->SetLabel(parts.empty() ? wxString() : join_keys(parts) + " + ");
|
||||
modifiers->Show(!parts.empty());
|
||||
key->SetLabel(last);
|
||||
const int key_width = last.length() == 1 ? m_key_slot : key->GetBestSize().x;
|
||||
key->SetMinSize(wxSize(key_width, -1));
|
||||
return (parts.empty() ? 0 : modifiers->GetBestSize().x) + key_width;
|
||||
}
|
||||
|
||||
void KBShortcutsDialog::open_mouse_preferences(const char* preference)
|
||||
{
|
||||
// Opened from Preferences > Control, the settings are right behind this dialog.
|
||||
if (auto preferences = dynamic_cast<PreferencesDialog*>(GetParent()); preferences != nullptr) {
|
||||
// Runs once this dialog has closed and the focus is back in Preferences.
|
||||
preferences->CallAfter([preferences, preference] { preferences->select_tab(PreferencesTab::Control, preference); });
|
||||
EndModal(wxID_OK);
|
||||
return;
|
||||
}
|
||||
wxGetApp().open_preferences(PreferencesTab::Control, preference);
|
||||
// A language change rebuilds the main frame, taking this dialog with it.
|
||||
if (GetParent() != wxGetApp().mainframe) {
|
||||
EndModal(wxID_CANCEL);
|
||||
return;
|
||||
}
|
||||
for (const PreferenceRow& row : m_preference_rows)
|
||||
row.description->SetLabel(_(mouse_action(row.preference)));
|
||||
}
|
||||
|
||||
ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut)
|
||||
: DPIDialog(parent, wxID_ANY, _(shortcut_info(shortcut).name), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
|
||||
, m_shortcut(shortcut)
|
||||
{
|
||||
SetBackgroundColour(*wxWHITE);
|
||||
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use.
|
||||
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
|
||||
const wxString advice = global_key_advice();
|
||||
const wxString typing = _L("Global shortcuts also apply while typing.");
|
||||
const wxString rule = _L("A key that types a character cannot be a global shortcut.");
|
||||
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms.");
|
||||
m_rejection = rule + "\n" + advice;
|
||||
|
||||
// Wide enough for each sentence on a line of its own where the translation allows, within limits.
|
||||
int width = FromDIP(450);
|
||||
for (const wxString& sentence : { typing, rule, advice }) {
|
||||
int extent = 0;
|
||||
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
|
||||
width = std::max(width, extent);
|
||||
}
|
||||
width = std::min(width, FromDIP(550));
|
||||
|
||||
auto prompt = new Label(this, wxGetApp().normal_font(), wxString::Format(_L("Press the new shortcut for\n\"%s\""), _(shortcut_info(shortcut).name)), LB_AUTO_WRAP);
|
||||
prompt->SetMinSize(wxSize(width, -1));
|
||||
sizer->Add(prompt, 0, wxALL, FromDIP(20));
|
||||
|
||||
// Keyboard focus stays on this box so the buttons never receive the key presses.
|
||||
const wxColour box_colour = StateColor::darkModeColorFor(*wxWHITE);
|
||||
StaticBox* capture = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxSize(width, FromDIP(60)), wxWANTS_CHARS);
|
||||
capture->SetCornerRadius(FromDIP(4));
|
||||
capture->SetBorderColorNormal(StateColor::darkModeColorFor(wxColour("#009688"))); // the focused-input colour, since the box always has the focus
|
||||
capture->SetBackgroundColorNormal(box_colour);
|
||||
capture->SetBackgroundColour(box_colour);
|
||||
wxBoxSizer* capture_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_chord_label = new wxStaticText(capture, wxID_ANY, join_keys(to_wx(wxGetApp().shortcuts().binding(shortcut).display_parts())));
|
||||
m_chord_label->SetFont(::Label::Head_14);
|
||||
m_chord_label->SetBackgroundColour(box_colour);
|
||||
capture_sizer->AddStretchSpacer();
|
||||
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
|
||||
capture_sizer->AddStretchSpacer();
|
||||
capture->SetSizer(capture_sizer);
|
||||
capture->Bind(wxEVT_KEY_DOWN, &ShortcutCaptureDialog::on_key, this);
|
||||
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
|
||||
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
|
||||
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
|
||||
|
||||
m_status = new Label(this, wxGetApp().normal_font(), m_hint, LB_AUTO_WRAP);
|
||||
m_status->SetMinSize(wxSize(width, 3 * m_status->GetCharHeight())); // room for three lines, so the dialog keeps its size while keys are tried
|
||||
m_status_colour = m_status->GetForegroundColour();
|
||||
sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(20));
|
||||
|
||||
auto dlg_btns = new DialogButtons(this, {"Unbind", "OK", "Cancel"}, "", 1 /*left_aligned*/);
|
||||
dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
m_chord = KeyChord{};
|
||||
m_conflicts.clear();
|
||||
EndModal(wxID_OK);
|
||||
});
|
||||
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
m_ok = dlg_btns->GetOK();
|
||||
m_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); });
|
||||
m_ok->Enable(false);
|
||||
sizer->Add(dlg_btns, 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
|
||||
SetSizerAndFit(sizer);
|
||||
CenterOnParent();
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
capture->CallAfter([capture]() { capture->SetFocus(); });
|
||||
}
|
||||
|
||||
void ShortcutCaptureDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
{
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ShortcutCaptureDialog::on_key(wxKeyEvent& evt)
|
||||
{
|
||||
if (!evt.HasAnyModifiers()) {
|
||||
if (evt.GetKeyCode() == WXK_ESCAPE) {
|
||||
EndModal(wxID_CANCEL);
|
||||
return;
|
||||
}
|
||||
if (evt.GetKeyCode() == WXK_RETURN || evt.GetKeyCode() == WXK_NUMPAD_ENTER) {
|
||||
if (m_ok->IsEnabled())
|
||||
EndModal(wxID_OK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const KeyChord chord = KeyChord::from_event(evt);
|
||||
if (!chord.valid())
|
||||
return;
|
||||
if (chord.needs_char_event()) {
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
record(chord);
|
||||
}
|
||||
|
||||
void ShortcutCaptureDialog::on_char(wxKeyEvent& evt)
|
||||
{
|
||||
const KeyChord chord = KeyChord::from_event(evt);
|
||||
if (chord.is_punctuation())
|
||||
record(chord);
|
||||
}
|
||||
|
||||
void ShortcutCaptureDialog::record(const KeyChord& chord)
|
||||
{
|
||||
m_chord = chord;
|
||||
m_chord_label->SetLabel(join_keys(to_wx(chord.display_parts())));
|
||||
m_chord_label->GetParent()->Layout();
|
||||
|
||||
auto reject = [this](const wxString& reason) {
|
||||
m_status->SetForegroundColour(ERROR_COLOUR);
|
||||
m_status->SetLabel(reason);
|
||||
m_conflicts.clear();
|
||||
m_ok->Enable(false);
|
||||
};
|
||||
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
|
||||
if (global && !chord.is_menu_accelerator()) {
|
||||
reject(m_rejection);
|
||||
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
|
||||
reject(wxString::Format(_L("Already used as a step of %s."), _(shortcut_info(*owner).name)));
|
||||
} else {
|
||||
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
|
||||
m_status->SetForegroundColour(m_status_colour);
|
||||
if (m_conflicts.empty())
|
||||
m_status->SetLabel(m_hint);
|
||||
else
|
||||
m_status->SetLabel(wxString::Format(_L("Already assigned to %s. Press OK to reassign it."), shortcut_names(m_conflicts)));
|
||||
m_ok->Enable(true);
|
||||
}
|
||||
m_status->Refresh(); // a colour change alone does not repaint
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -3,53 +3,120 @@
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include <wx/simplebook.h>
|
||||
|
||||
class Button;
|
||||
class Label;
|
||||
class TabCtrl;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class Select
|
||||
{
|
||||
public:
|
||||
int m_index;
|
||||
wxWindow *m_tab_button;
|
||||
wxWindow *m_tab_text;
|
||||
};
|
||||
WX_DECLARE_HASH_MAP(int, Select *, wxIntegerHash, wxIntegerEqual, SelectHash);
|
||||
|
||||
// Lists every shortcut per context and lets the user rebind the assignable ones.
|
||||
class KBShortcutsDialog : public DPIDialog
|
||||
{
|
||||
typedef std::pair<std::string, std::string> Shortcut;
|
||||
typedef std::vector<Shortcut> Shortcuts;
|
||||
typedef std::pair<std::pair<wxString, wxString>, Shortcuts> ShortcutsItem;
|
||||
typedef std::vector<ShortcutsItem> ShortcutsVec;
|
||||
// A key the user cannot rebind.
|
||||
struct FixedKey
|
||||
{
|
||||
std::vector<wxString> keys; // modifier names and the key, shown joined with "+"
|
||||
const char* description; // untranslated
|
||||
};
|
||||
// A mouse button whose camera action is chosen in Preferences.
|
||||
struct MouseAction
|
||||
{
|
||||
wxString button;
|
||||
const char* preference; // AppConfig key of the action
|
||||
};
|
||||
struct Row
|
||||
{
|
||||
std::variant<Shortcut, FixedKey, MouseAction> content;
|
||||
ShortcutSection section;
|
||||
};
|
||||
struct Page
|
||||
{
|
||||
wxString title;
|
||||
wxString caption; // when the page's keys apply
|
||||
ShortcutContext context;
|
||||
std::vector<Row> rows;
|
||||
};
|
||||
struct EditableRow
|
||||
{
|
||||
Shortcut shortcut;
|
||||
wxStaticText* description;
|
||||
wxStaticText* modifiers;
|
||||
wxStaticText* key;
|
||||
ScalableButton* reset;
|
||||
};
|
||||
struct PreferenceRow
|
||||
{
|
||||
const char* preference;
|
||||
wxStaticText* description;
|
||||
};
|
||||
|
||||
ShortcutsVec m_full_shortcuts;
|
||||
ScalableBitmap m_logo_bmp;
|
||||
wxStaticBitmap* m_header_bitmap;
|
||||
std::vector<wxPanel*> m_pages;
|
||||
std::vector<Page> m_pages;
|
||||
std::vector<EditableRow> m_editable_rows;
|
||||
std::vector<PreferenceRow> m_preference_rows;
|
||||
// Row geometry, measured once and shared by every page.
|
||||
wxSize m_edit_size; // an edit or reset icon
|
||||
int m_buttons_width = 0; // every row's buttons column, so the right-aligned keys share an edge
|
||||
int m_row_text_width = 0; // what a description and its chord share; the description wraps at the rest
|
||||
int m_key_slot = 0; // width of the widest single key, the column single keys line up in
|
||||
|
||||
TabCtrl* m_tabs;
|
||||
wxSimplebook* m_simplebook;
|
||||
|
||||
public:
|
||||
KBShortcutsDialog();
|
||||
wxWindow* create_button(int id, wxString text);
|
||||
void OnSelectTabel(wxCommandEvent &event);
|
||||
wxPanel *m_panel_selects;
|
||||
wxBoxSizer *m_sizer_right;
|
||||
wxSimplebook *m_simplebook;
|
||||
wxBoxSizer * m_sizer_body;
|
||||
SelectHash m_hash_selector;
|
||||
KBShortcutsDialog(wxWindow* parent, ShortcutContext page); // opens on the page of that context
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
|
||||
private:
|
||||
void fill_shortcuts();
|
||||
wxPanel* create_header(wxWindow* parent, const wxFont& bold_font);
|
||||
wxPanel* create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font);
|
||||
void fill_pages();
|
||||
wxPanel* create_page(wxWindow* parent, const Page& page);
|
||||
void edit_shortcut(Shortcut shortcut);
|
||||
void reset_shortcut(Shortcut shortcut);
|
||||
// Asks question before unbinding conflicts; false when the user declined.
|
||||
bool take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question);
|
||||
void apply_bindings(); // refreshes the rows and pushes the change to the rest of the app
|
||||
// Puts a chord on a row's two labels, a single key in the shared column, and returns the width the chord takes.
|
||||
int set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts);
|
||||
void open_mouse_preferences(const char* preference);
|
||||
};
|
||||
|
||||
// Records one key chord for a shortcut, warning about the shortcuts it would take the chord from.
|
||||
class ShortcutCaptureDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut);
|
||||
|
||||
// Valid after ShowModal() returned wxID_OK; an invalid chord means "unbind".
|
||||
const KeyChord& chord() const { return m_chord; }
|
||||
const std::vector<Shortcut>& conflicts() const { return m_conflicts; }
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
private:
|
||||
void on_key(wxKeyEvent& evt);
|
||||
void on_char(wxKeyEvent& evt);
|
||||
void record(const KeyChord& chord);
|
||||
|
||||
Shortcut m_shortcut;
|
||||
KeyChord m_chord;
|
||||
std::vector<Shortcut> m_conflicts;
|
||||
wxStaticText* m_chord_label;
|
||||
wxString m_hint; // what m_status shows while there is nothing to warn about
|
||||
wxString m_rejection; // what it shows for a key a Global shortcut cannot use
|
||||
Label* m_status;
|
||||
wxColour m_status_colour;
|
||||
Button* m_ok;
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
#include "KeyChord.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int BINDABLE_MODIFIERS = wxMOD_CONTROL | wxMOD_SHIFT | wxMOD_ALT | wxMOD_RAW_CONTROL;
|
||||
|
||||
struct KeyName
|
||||
{
|
||||
int key;
|
||||
const char* name; // canonical name, as wx parses it
|
||||
const char* alias; // accepted when parsing; nullptr when there is none
|
||||
const char* label; // translation key for KeyChord::display(); nullptr when name is it
|
||||
};
|
||||
|
||||
constexpr std::array<KeyName, 15> special_keys{{
|
||||
{ WXK_BACK, L_CONTEXT("Backspace", "Keyboard Shortcut"), "Back", nullptr },
|
||||
{ WXK_TAB, L_CONTEXT("Tab", "Keyboard Shortcut"), nullptr, nullptr },
|
||||
{ WXK_RETURN, L_CONTEXT("Enter", "Keyboard Shortcut"), "Return", nullptr },
|
||||
{ WXK_ESCAPE, L_CONTEXT("Esc", "Keyboard Shortcut"), "Escape", nullptr },
|
||||
{ WXK_SPACE, L_CONTEXT("Space", "Keyboard Shortcut"), nullptr, nullptr },
|
||||
{ WXK_DELETE, L_CONTEXT("Del", "Keyboard Shortcut"), "Delete", nullptr },
|
||||
{ WXK_INSERT, L_CONTEXT("Ins", "Keyboard Shortcut"), "Insert", nullptr },
|
||||
{ WXK_HOME, L_CONTEXT("Home", "Keyboard Shortcut"), nullptr, nullptr },
|
||||
{ WXK_END, L_CONTEXT("End", "Keyboard Shortcut"), nullptr, nullptr },
|
||||
{ WXK_PAGEUP, L_CONTEXT("PgUp", "Keyboard Shortcut"), "PageUp", nullptr },
|
||||
{ WXK_PAGEDOWN, L_CONTEXT("PgDn", "Keyboard Shortcut"), "PageDown", nullptr },
|
||||
// Displayed as "Arrow Left" and so on, which is what the catalogs translate.
|
||||
{ WXK_LEFT, "Left", nullptr, L_CONTEXT("Arrow Left", "Keyboard Shortcut") },
|
||||
{ WXK_RIGHT, "Right", nullptr, L_CONTEXT("Arrow Right", "Keyboard Shortcut") },
|
||||
{ WXK_UP, "Up", nullptr, L_CONTEXT("Arrow Up", "Keyboard Shortcut") },
|
||||
{ WXK_DOWN, "Down", nullptr, L_CONTEXT("Arrow Down", "Keyboard Shortcut") },
|
||||
}};
|
||||
|
||||
bool equals_ignoring_case(const std::string& a, const char* b)
|
||||
{
|
||||
if (b == nullptr)
|
||||
return false;
|
||||
size_t i = 0;
|
||||
for (; i < a.size() && b[i] != '\0'; ++i)
|
||||
if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i])))
|
||||
return false;
|
||||
return i == a.size() && b[i] == '\0';
|
||||
}
|
||||
|
||||
bool is_letter(int key) { return key >= 'A' && key <= 'Z'; }
|
||||
bool is_digit(int key) { return key >= '0' && key <= '9'; }
|
||||
bool is_printable(int key) { return key > ' ' && key < 127; }
|
||||
bool is_symbol(int key) { return is_printable(key) && !std::isalnum(key); }
|
||||
bool is_function_key(int key) { return key >= WXK_F1 && key <= WXK_F24; }
|
||||
|
||||
bool is_special(int key)
|
||||
{
|
||||
if (is_function_key(key))
|
||||
return true;
|
||||
return std::any_of(special_keys.begin(), special_keys.end(), [key](const KeyName& k) { return k.key == key; });
|
||||
}
|
||||
|
||||
std::string special_key_name(int key)
|
||||
{
|
||||
if (is_function_key(key))
|
||||
return "F" + std::to_string(key - WXK_F1 + 1);
|
||||
for (const KeyName& k : special_keys)
|
||||
if (k.key == key)
|
||||
return k.name;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string special_key_label(int key)
|
||||
{
|
||||
for (const KeyName& k : special_keys)
|
||||
if (k.key == key)
|
||||
return _u8L_CONTEXT(k.label != nullptr ? k.label : k.name, "Keyboard Shortcut");
|
||||
return special_key_name(key);
|
||||
}
|
||||
|
||||
int parse_key(const std::string& text)
|
||||
{
|
||||
if (text.size() == 1) {
|
||||
const int key = static_cast<unsigned char>(text[0]);
|
||||
return is_printable(key) ? std::toupper(key) : WXK_NONE;
|
||||
}
|
||||
for (const KeyName& k : special_keys)
|
||||
if (equals_ignoring_case(text, k.name) || equals_ignoring_case(text, k.alias))
|
||||
return k.key;
|
||||
if ((text[0] == 'F' || text[0] == 'f') && text.size() <= 3 && std::all_of(text.begin() + 1, text.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); })) {
|
||||
const int n = std::stoi(text.substr(1));
|
||||
if (n >= 1 && n <= 24)
|
||||
return WXK_F1 + n - 1;
|
||||
}
|
||||
return WXK_NONE;
|
||||
}
|
||||
|
||||
int parse_modifier(const std::string& text)
|
||||
{
|
||||
if (equals_ignoring_case(text, "Ctrl") || equals_ignoring_case(text, "Control") || equals_ignoring_case(text, "Cmd") || equals_ignoring_case(text, "Command"))
|
||||
return wxMOD_CONTROL;
|
||||
if (equals_ignoring_case(text, "Shift"))
|
||||
return wxMOD_SHIFT;
|
||||
if (equals_ignoring_case(text, "Alt") || equals_ignoring_case(text, "Option"))
|
||||
return wxMOD_ALT;
|
||||
if (equals_ignoring_case(text, "RawCtrl"))
|
||||
return wxMOD_RAW_CONTROL;
|
||||
return wxMOD_NONE;
|
||||
}
|
||||
|
||||
// Numpad keys act as their main-keyboard counterparts, so one binding covers both.
|
||||
int fold_numpad(int key)
|
||||
{
|
||||
if (key >= WXK_NUMPAD0 && key <= WXK_NUMPAD9)
|
||||
return '0' + (key - WXK_NUMPAD0);
|
||||
switch (key) {
|
||||
case WXK_NUMPAD_ENTER: return WXK_RETURN;
|
||||
case WXK_NUMPAD_SPACE: return WXK_SPACE;
|
||||
case WXK_NUMPAD_TAB: return WXK_TAB;
|
||||
case WXK_NUMPAD_HOME: return WXK_HOME;
|
||||
case WXK_NUMPAD_END: return WXK_END;
|
||||
case WXK_NUMPAD_PAGEUP: return WXK_PAGEUP;
|
||||
case WXK_NUMPAD_PAGEDOWN: return WXK_PAGEDOWN;
|
||||
case WXK_NUMPAD_LEFT: return WXK_LEFT;
|
||||
case WXK_NUMPAD_RIGHT: return WXK_RIGHT;
|
||||
case WXK_NUMPAD_UP: return WXK_UP;
|
||||
case WXK_NUMPAD_DOWN: return WXK_DOWN;
|
||||
case WXK_NUMPAD_INSERT: return WXK_INSERT;
|
||||
case WXK_NUMPAD_DELETE: return WXK_DELETE;
|
||||
case WXK_NUMPAD_ADD: return '+';
|
||||
case WXK_NUMPAD_SUBTRACT: return '-';
|
||||
case WXK_NUMPAD_MULTIPLY: return '*';
|
||||
case WXK_NUMPAD_DIVIDE: return '/';
|
||||
case WXK_NUMPAD_DECIMAL: return '.';
|
||||
case WXK_NUMPAD_EQUAL: return '=';
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifiers in the order the text forms list them; wxMOD_RAW_CONTROL is wxMOD_CONTROL off macOS.
|
||||
#ifdef __APPLE__
|
||||
constexpr std::array<int, 4> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT, wxMOD_RAW_CONTROL };
|
||||
#else
|
||||
constexpr std::array<int, 3> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT };
|
||||
#endif
|
||||
|
||||
const char* canonical_modifier_prefix(int modifier)
|
||||
{
|
||||
if (modifier == wxMOD_CONTROL)
|
||||
return "Ctrl+";
|
||||
if (modifier == wxMOD_SHIFT)
|
||||
return "Shift+";
|
||||
if (modifier == wxMOD_ALT)
|
||||
return "Alt+";
|
||||
return "RawCtrl+";
|
||||
}
|
||||
|
||||
template<typename Prefix>
|
||||
std::string join_modifiers(int modifiers, Prefix prefix)
|
||||
{
|
||||
std::string out;
|
||||
for (int modifier : MODIFIER_ORDER)
|
||||
if (modifiers & modifier)
|
||||
out += prefix(modifier);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool KeyChord::is_punctuation() const { return is_symbol(key) && modifiers == wxMOD_NONE; }
|
||||
|
||||
bool KeyChord::needs_char_event() const { return is_symbol(key) && (modifiers & ~wxMOD_SHIFT) == 0; }
|
||||
|
||||
bool KeyChord::is_menu_accelerator() const
|
||||
{
|
||||
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
|
||||
}
|
||||
|
||||
std::string KeyChord::to_string() const
|
||||
{
|
||||
if (!valid())
|
||||
return {};
|
||||
std::string out = join_modifiers(modifiers, canonical_modifier_prefix);
|
||||
if (is_printable(key))
|
||||
out += char(key);
|
||||
else
|
||||
out += special_key_name(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<KeyChord> KeyChord::parse(const std::string& text)
|
||||
{
|
||||
if (text.empty())
|
||||
return std::nullopt;
|
||||
|
||||
// The key is whatever follows the last separator; a trailing '+' is the '+' key itself.
|
||||
size_t key_start = text.size() - 1;
|
||||
if (text.back() != '+') {
|
||||
const size_t sep = text.rfind('+');
|
||||
key_start = sep == std::string::npos ? 0 : sep + 1;
|
||||
}
|
||||
KeyChord chord;
|
||||
chord.key = parse_key(text.substr(key_start));
|
||||
if (chord.key == WXK_NONE)
|
||||
return std::nullopt;
|
||||
|
||||
const std::string prefix = key_start == 0 ? std::string() : text.substr(0, key_start - 1);
|
||||
size_t begin = 0;
|
||||
while (begin < prefix.size()) {
|
||||
size_t end = prefix.find('+', begin);
|
||||
if (end == std::string::npos)
|
||||
end = prefix.size();
|
||||
const int modifier = parse_modifier(prefix.substr(begin, end - begin));
|
||||
if (modifier == wxMOD_NONE)
|
||||
return std::nullopt;
|
||||
chord.modifiers |= modifier;
|
||||
begin = end + 1;
|
||||
}
|
||||
if (is_letter(chord.key) || is_digit(chord.key) || !is_printable(chord.key) || chord.modifiers == wxMOD_NONE)
|
||||
return chord;
|
||||
// Shift is folded into the character for punctuation, so "Shift+/" is not accepted.
|
||||
return (chord.modifiers & wxMOD_SHIFT) ? std::nullopt : std::optional<KeyChord>(chord);
|
||||
}
|
||||
|
||||
std::string KeyChord::display() const
|
||||
{
|
||||
std::string out;
|
||||
for (const std::string& part : display_parts())
|
||||
out += (out.empty() ? "" : "+") + part;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> KeyChord::display_parts() const
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
if (!valid())
|
||||
return parts;
|
||||
for (int modifier : MODIFIER_ORDER)
|
||||
if (modifiers & modifier)
|
||||
parts.push_back(modifier_name(modifier));
|
||||
parts.push_back(is_printable(key) ? std::string(1, char(key)) : special_key_label(key));
|
||||
return parts;
|
||||
}
|
||||
|
||||
std::string KeyChord::modifier_prefix(int modifier)
|
||||
{
|
||||
if (modifier == wxMOD_CONTROL)
|
||||
return shortkey_ctrl_prefix();
|
||||
if (modifier == wxMOD_SHIFT)
|
||||
return _u8L("Shift+");
|
||||
if (modifier == wxMOD_ALT)
|
||||
return shortkey_alt_prefix();
|
||||
#ifdef __APPLE__
|
||||
if (modifier == wxMOD_RAW_CONTROL)
|
||||
return u8"⌃+";
|
||||
#endif
|
||||
return {};
|
||||
}
|
||||
|
||||
// The catalogue holds the "Ctrl+" prefixes, so the bare name is the prefix without its "+"
|
||||
// and any space before it ("Strg +" in German).
|
||||
std::string KeyChord::modifier_name(int modifier)
|
||||
{
|
||||
std::string name = modifier_prefix(modifier);
|
||||
if (!name.empty() && name.back() == '+')
|
||||
name.pop_back();
|
||||
while (!name.empty() && name.back() == ' ')
|
||||
name.pop_back();
|
||||
return name;
|
||||
}
|
||||
|
||||
wxAcceleratorEntry KeyChord::to_accelerator_entry(int command) const
|
||||
{
|
||||
int flags = wxACCEL_NORMAL;
|
||||
if (modifiers & wxMOD_CONTROL)
|
||||
flags |= wxACCEL_CTRL;
|
||||
if (modifiers & wxMOD_SHIFT)
|
||||
flags |= wxACCEL_SHIFT;
|
||||
if (modifiers & wxMOD_ALT)
|
||||
flags |= wxACCEL_ALT;
|
||||
#ifdef __APPLE__
|
||||
if (modifiers & wxMOD_RAW_CONTROL)
|
||||
flags |= wxACCEL_RAW_CTRL;
|
||||
#endif
|
||||
return wxAcceleratorEntry(flags, key, command);
|
||||
}
|
||||
|
||||
KeyChord KeyChord::from_event(const wxKeyEvent& evt)
|
||||
{
|
||||
KeyChord chord;
|
||||
chord.modifiers = evt.GetModifiers() & BINDABLE_MODIFIERS;
|
||||
int key = fold_numpad(evt.GetKeyCode());
|
||||
|
||||
if (evt.GetEventType() == wxEVT_CHAR) {
|
||||
if (key >= 1 && key <= 26 && evt.ControlDown())
|
||||
key = 'A' + key - 1; // Ctrl+letter arrives as the control character
|
||||
else if (is_symbol(key))
|
||||
chord.modifiers &= ~wxMOD_SHIFT; // the character already reflects Shift
|
||||
}
|
||||
if (key >= 'a' && key <= 'z')
|
||||
key -= 'a' - 'A';
|
||||
|
||||
if (is_printable(key) || is_special(key))
|
||||
chord.key = key;
|
||||
return chord;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <wx/accel.h>
|
||||
#include <wx/defs.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class wxKeyEvent;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// One key press: a key code as wxEVT_KEY_DOWN reports it (letters upper-case, numpad keys
|
||||
// folded onto their main-keyboard equivalents) plus the wxMOD_* modifiers held with it.
|
||||
// Printable punctuation is stored as the character it produces, so "+" means the key that
|
||||
// types "+" on the user's layout.
|
||||
struct KeyChord
|
||||
{
|
||||
int key = WXK_NONE;
|
||||
int modifiers = wxMOD_NONE;
|
||||
|
||||
bool valid() const { return key != WXK_NONE; }
|
||||
bool operator==(const KeyChord& other) const { return key == other.key && modifiers == other.modifiers; }
|
||||
bool operator!=(const KeyChord& other) const { return !(*this == other); }
|
||||
|
||||
// Bare printable keys other than letters and digits are matched on wxEVT_CHAR, because
|
||||
// only the char event knows which character a key produces under the active layout.
|
||||
bool is_punctuation() const;
|
||||
// True for a printable non-alphanumeric key pressed with nothing but Shift, which only the
|
||||
// char event that follows can resolve.
|
||||
bool needs_char_event() const;
|
||||
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
|
||||
// swallowing typing in text fields.
|
||||
bool is_menu_accelerator() const;
|
||||
|
||||
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
|
||||
std::string to_string() const;
|
||||
static std::optional<KeyChord> parse(const std::string& text);
|
||||
|
||||
// Text for menus, tooltips and the shortcuts dialog, with translated modifier names and the
|
||||
// command and option glyphs on macOS.
|
||||
std::string display() const;
|
||||
// The pieces display() joins with "+": the modifier names, then the key name.
|
||||
std::vector<std::string> display_parts() const;
|
||||
// Translated text of one wxMOD_* modifier, as a "Ctrl+" prefix or the bare "Ctrl" name.
|
||||
static std::string modifier_prefix(int modifier);
|
||||
static std::string modifier_name(int modifier);
|
||||
|
||||
wxAcceleratorEntry to_accelerator_entry(int command) const;
|
||||
|
||||
// Builds the chord a key event describes, or an invalid chord for pure modifier presses and
|
||||
// keys outside the bindable set. wxEVT_CHAR events are normalized to the key codes
|
||||
// wxEVT_KEY_DOWN reports for letters, digits and special keys.
|
||||
static KeyChord from_event(const wxKeyEvent& evt);
|
||||
};
|
||||
|
||||
struct KeyChordHash
|
||||
{
|
||||
size_t operator()(const KeyChord& chord) const { return std::hash<long long>()((static_cast<long long>(chord.modifiers) << 32) | unsigned(chord.key)); }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
+297
-210
@@ -1,6 +1,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
|
||||
#include <wx/panel.h>
|
||||
#include <wx/textentry.h>
|
||||
#include <wx/notebook.h>
|
||||
#include <wx/listbook.h>
|
||||
#include <wx/simplebook.h>
|
||||
@@ -46,13 +47,16 @@
|
||||
// BBS
|
||||
#include "PartPlate.hpp"
|
||||
#include "Preferences.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
#include "Widgets/StaticBox.hpp"
|
||||
#include "BindDialog.hpp"
|
||||
#include "../Utils/MacDarkMode.hpp"
|
||||
#include "../Utils/NetworkAgentFactory.hpp"
|
||||
#include "../Utils/PrintHost.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "UnsavedChangesDialog.hpp"
|
||||
#include "PublishSettingsDialog.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
@@ -107,6 +111,31 @@ enum class ERescaleTarget
|
||||
SettingsDialog
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other
|
||||
// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has
|
||||
// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and
|
||||
// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page.
|
||||
bool focus_keeps_space(wxWindow* focus)
|
||||
{
|
||||
if (!focus)
|
||||
return false;
|
||||
if (dynamic_cast<wxTextEntryBase*>(focus))
|
||||
return true; // typing a space into a text field
|
||||
if (dynamic_cast<wxWebView*>(focus))
|
||||
return true; // web content scrolls and hosts its own text fields
|
||||
if (dynamic_cast<::Button*>(focus))
|
||||
return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl)
|
||||
if (dynamic_cast<StaticBox*>(focus))
|
||||
return true; // custom composites (ComboBox, SpinInput, ...) activate with Space and are wxWindow
|
||||
if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus))
|
||||
return true; // stock button/checkbox/choice/list/etc. keep Space
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef __WXGTK__
|
||||
// A thin transparent panel placed at a window edge to handle resize.
|
||||
// Works regardless of underlying content (GLCanvas3D, wxWebView, etc.)
|
||||
@@ -282,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))
|
||||
@@ -702,53 +720,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
}
|
||||
return;}
|
||||
#endif
|
||||
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) {
|
||||
@@ -769,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) {
|
||||
@@ -2722,13 +2785,31 @@ static const wxString sep = " - ";
|
||||
static const wxString sep = "\t";
|
||||
#endif
|
||||
|
||||
static wxMenu* generate_help_menu()
|
||||
wxString MainFrame::shortcut_label(const wxString& label, Shortcut shortcut, bool accelerator)
|
||||
{
|
||||
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
|
||||
if (accelerator) {
|
||||
const std::string accel = shortcuts.accelerator(shortcut);
|
||||
if (!accel.empty())
|
||||
return label + " " + from_u8(accel);
|
||||
}
|
||||
const std::string text = shortcuts.display(shortcut);
|
||||
return text.empty() ? label : label + sep + from_u8(text);
|
||||
}
|
||||
|
||||
void MainFrame::update_shortcut_labels()
|
||||
{
|
||||
for (const ShortcutMenuItem& entry : m_shortcut_menu_items)
|
||||
entry.item->SetItemLabel(shortcut_label(entry.label, entry.shortcut, entry.accelerator));
|
||||
}
|
||||
|
||||
wxMenu* MainFrame::generate_help_menu()
|
||||
{
|
||||
wxMenu* helpMenu = new wxMenu();
|
||||
|
||||
// shortcut key
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Keyboard Shortcuts") + sep + "&?", _L("Show the list of keyboard shortcuts"),
|
||||
[](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||
append_shortcut_item(helpMenu, Shortcut::KeyboardShortcuts, false, _L("Keyboard Shortcuts"), _L("Show the list of keyboard shortcuts"),
|
||||
[](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(ShortcutContext::Global); });
|
||||
// Show Beginner's Tutorial
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Setup Wizard"), _L("Setup Wizard"), [](wxCommandEvent &) {wxGetApp().ShowUserGuide();});
|
||||
|
||||
@@ -2810,29 +2891,28 @@ static void add_common_publish_menu_items(wxMenu* publish_menu, MainFrame* mainF
|
||||
#endif
|
||||
}
|
||||
|
||||
static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, std::function<bool(void)> can_change_view)
|
||||
void MainFrame::add_common_view_menu_items(wxMenu* view_menu, std::function<bool(void)> can_change_view)
|
||||
{
|
||||
// The camera control accelerators are captured by GLCanvas3D::on_char().
|
||||
append_menu_item(view_menu, wxID_ANY, _L("Default View") + "\t" + ctrl + "0", _L("Default View"), [mainFrame](wxCommandEvent&) {
|
||||
mainFrame->select_view("plate");
|
||||
mainFrame->plater()->get_current_canvas3D()->zoom_to_bed();
|
||||
append_shortcut_item(view_menu, Shortcut::ViewDefault, true, _L("Default View"), _L("Default View"), [this](wxCommandEvent&) {
|
||||
select_view("plate");
|
||||
plater()->get_current_canvas3D()->zoom_to_bed();
|
||||
},
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
//view_menu->AppendSeparator();
|
||||
//TRN To be shown in the main menu View->Top
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Top", "Camera View") + "\t" + ctrl + "1", _L("Top View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("top"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewTop, true, _L_CONTEXT("Top", "Camera View"), _L("Top View"), [this](wxCommandEvent&) { select_view("top"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
//TRN To be shown in the main menu View->Bottom
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Bottom", "Camera View") + "\t" + ctrl + "2", _L("Bottom View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("bottom"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Front", "Camera View") + "\t" + ctrl + "3", _L("Front View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("front"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Rear", "Camera View") + "\t" + ctrl + "4", _L("Rear View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("rear"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Left", "Camera View") + "\t" + ctrl + "5", _L("Left View"),[mainFrame](wxCommandEvent &) {mainFrame->select_view("left"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Right", "Camera View") + "\t" + ctrl + "6", _L("Right View"),[mainFrame](wxCommandEvent &) { mainFrame->select_view("right"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewBottom, true, _L_CONTEXT("Bottom", "Camera View"), _L("Bottom View"), [this](wxCommandEvent&) { select_view("bottom"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewFront, true, _L_CONTEXT("Front", "Camera View"), _L("Front View"), [this](wxCommandEvent&) { select_view("front"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewRear, true, _L_CONTEXT("Rear", "Camera View"), _L("Rear View"), [this](wxCommandEvent&) { select_view("rear"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewLeft, true, _L_CONTEXT("Left", "Camera View"), _L("Left View"), [this](wxCommandEvent &) { select_view("left"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
append_shortcut_item(view_menu, Shortcut::ViewRight, true, _L_CONTEXT("Right", "Camera View"), _L("Right View"), [this](wxCommandEvent &) { select_view("right"); },
|
||||
"", nullptr, [can_change_view]() { return can_change_view(); }, this);
|
||||
}
|
||||
|
||||
void MainFrame::init_menubar_as_editor()
|
||||
@@ -2851,17 +2931,17 @@ void MainFrame::init_menubar_as_editor()
|
||||
[this] { return m_plater != nullptr && wxGetApp().app_config->get("app", "single_instance") == "false"; }, this);
|
||||
#endif
|
||||
// New Project
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("New Project") + "\t" + ctrl + "N", _L("Start a new project"),
|
||||
append_shortcut_item(fileMenu, Shortcut::NewProject, true, _L("New Project"), _L("Start a new project"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->new_project(); }, "", nullptr,
|
||||
[this](){return can_start_new_project(); }, this);
|
||||
// Open Project
|
||||
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Open Project") + dots + "\t" + ctrl + "O", _L("Open a project file"),
|
||||
append_shortcut_item(fileMenu, Shortcut::OpenProject, true, _L("Open Project") + dots, _L("Open a project file"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->load_project(); }, "menu_open", nullptr,
|
||||
[this](){return can_open_project(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Open Project") + dots + "\t" + ctrl + "O", _L("Open a project file"),
|
||||
append_shortcut_item(fileMenu, Shortcut::OpenProject, true, _L("Open Project") + dots, _L("Open a project file"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->load_project(); }, "", nullptr,
|
||||
[this](){return can_open_project(); }, this);
|
||||
#endif
|
||||
@@ -2888,21 +2968,21 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// BBS: close save project
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Save Project") + "\t" + ctrl + "S", _L("Save current project to file"),
|
||||
append_shortcut_item(fileMenu, Shortcut::SaveProject, true, _L("Save Project"), _L("Save current project to file"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(); }, "menu_save", nullptr,
|
||||
[this](){return m_plater != nullptr && can_save(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Save Project") + "\t" + ctrl + "S", _L("Save current project to file"),
|
||||
append_shortcut_item(fileMenu, Shortcut::SaveProject, true, _L("Save Project"), _L("Save current project to file"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(); }, "", nullptr,
|
||||
[this](){return m_plater != nullptr && can_save(); }, this);
|
||||
#endif
|
||||
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Save Project as") + dots + "\t" + ctrl + shift + "S", _L("Save current project as"),
|
||||
append_shortcut_item(fileMenu, Shortcut::SaveProjectAs, true, _L("Save Project as") + dots, _L("Save current project as"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(true); }, "menu_save", nullptr,
|
||||
[this](){return m_plater != nullptr && can_save_as(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Save Project as") + dots + "\t" + ctrl + shift + "S", _L("Save current project as"),
|
||||
append_shortcut_item(fileMenu, Shortcut::SaveProjectAs, true, _L("Save Project as") + dots, _L("Save current project as"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->save_project(true); }, "", nullptr,
|
||||
[this](){return m_plater != nullptr && can_save_as(); }, this);
|
||||
#endif
|
||||
@@ -2912,11 +2992,11 @@ void MainFrame::init_menubar_as_editor()
|
||||
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
|
||||
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
append_shortcut_item(fileMenu, Shortcut::Publish3mf, true, _L("Publish 3MF") + dots, _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "menu_publish", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
append_shortcut_item(fileMenu, Shortcut::Publish3mf, true, _L("Publish 3MF") + dots, _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#endif
|
||||
@@ -2927,13 +3007,13 @@ void MainFrame::init_menubar_as_editor()
|
||||
// BBS
|
||||
wxMenu *import_menu = new wxMenu();
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"),
|
||||
append_shortcut_item(import_menu, Shortcut::ImportModel, true, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots, _L("Load a model"),
|
||||
[this](wxCommandEvent&) { if (m_plater) {
|
||||
m_plater->add_file();
|
||||
} }, "menu_import", nullptr,
|
||||
[this](){return can_add_models(); }, this);
|
||||
#else
|
||||
append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"),
|
||||
append_shortcut_item(import_menu, Shortcut::ImportModel, true, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots, _L("Load a model"),
|
||||
[this](wxCommandEvent&) { if (m_plater) { m_plater->add_model(); } }, "", nullptr,
|
||||
[this](){return can_add_models(); }, this);
|
||||
#endif
|
||||
@@ -2965,7 +3045,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->export_core_3mf(); }, "menu_export_sliced_file", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
// BBS export .gcode.3mf
|
||||
append_menu_item(export_menu, wxID_ANY, _L("Export plate sliced file") + dots + "\t" + ctrl + "G", _L("Export current sliced file"),
|
||||
append_shortcut_item(export_menu, Shortcut::ExportSlicedFile, true, _L("Export plate sliced file") + dots, _L("Export current sliced file"),
|
||||
[this](wxCommandEvent&) { if (m_plater) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); }, "menu_export_sliced_file", nullptr,
|
||||
[this](){return can_export_gcode(); }, this);
|
||||
|
||||
@@ -3015,37 +3095,37 @@ void MainFrame::init_menubar_as_editor()
|
||||
};
|
||||
#ifndef __APPLE__
|
||||
// BBS undo
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Undo") + "\t" + ctrl + "Z",
|
||||
append_shortcut_item(editMenu, Shortcut::Undo, true, _L("Undo"),
|
||||
_L("Undo"), [this](wxCommandEvent&) { m_plater->undo(); },
|
||||
"menu_undo", nullptr, [this](){return m_plater->can_undo(); }, this);
|
||||
// BBS redo
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Redo") + "\t" + ctrl + "Y",
|
||||
append_shortcut_item(editMenu, Shortcut::Redo, true, _L("Redo"),
|
||||
_L("Redo"), [this](wxCommandEvent&) { m_plater->redo(); },
|
||||
"menu_redo", nullptr, [this](){return m_plater->can_redo(); }, this);
|
||||
editMenu->AppendSeparator();
|
||||
// BBS Cut TODO
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Cut") + "\t" + ctrl + "X",
|
||||
append_shortcut_item(editMenu, Shortcut::Cut, true, _L("Cut"),
|
||||
_L("Cut selection to clipboard"), [this](wxCommandEvent&) {m_plater->cut_selection_to_clipboard(); },
|
||||
"menu_cut", nullptr, [this]() {return m_plater->can_copy_to_clipboard(); }, this);
|
||||
// BBS Copy
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Copy") + "\t" + ctrl + "C",
|
||||
append_shortcut_item(editMenu, Shortcut::Copy, true, _L("Copy"),
|
||||
_L("Copy selection to clipboard"), [this](wxCommandEvent&) { m_plater->copy_selection_to_clipboard(); },
|
||||
"menu_copy", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this);
|
||||
// BBS Paste
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Paste") + "\t" + ctrl + "V",
|
||||
append_shortcut_item(editMenu, Shortcut::Paste, true, _L("Paste"),
|
||||
_L("Paste clipboard"), [this](wxCommandEvent&) { m_plater->paste_from_clipboard(); },
|
||||
"menu_paste", nullptr, [this](){return m_plater->can_paste_from_clipboard(); }, this);
|
||||
// BBS Delete selected
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Delete Selected") + "\t" + _L_CONTEXT("Del", "Keyboard Shortcut"),
|
||||
append_shortcut_item(editMenu, Shortcut::DeleteSelected, true, _L("Delete Selected"),
|
||||
_L("Deletes the current selection"),[this](wxCommandEvent&) { m_plater->remove_selected(); },
|
||||
"menu_remove", nullptr, [this](){return can_delete(); }, this);
|
||||
//BBS: delete all
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Delete All") + "\t" + ctrl + "D",
|
||||
append_shortcut_item(editMenu, Shortcut::DeleteAll, true, _L("Delete All"),
|
||||
_L("Deletes all objects"),[this](wxCommandEvent&) { m_plater->delete_all_objects_from_model(); },
|
||||
"menu_remove", nullptr, [this](){return can_delete_all(); }, this);
|
||||
editMenu->AppendSeparator();
|
||||
// BBS Clone Selected
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Clone Selected") /*+ "\t" + ctrl + "M"*/,
|
||||
append_shortcut_item(editMenu, Shortcut::CloneSelected, true, _L("Clone Selected"),
|
||||
_L("Clone copies of selections"),[this](wxCommandEvent&) {
|
||||
m_plater->clone_selection();
|
||||
},
|
||||
@@ -3059,7 +3139,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
editMenu->AppendSeparator();
|
||||
#else
|
||||
// BBS undo
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Undo") + sep + ctrl_t + "Z",
|
||||
append_shortcut_item(editMenu, Shortcut::Undo, false, _L("Undo"),
|
||||
_L("Undo"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3071,7 +3151,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
m_plater->undo(); },
|
||||
"", nullptr, [this](){return m_plater->can_undo(); }, this);
|
||||
// BBS redo
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Redo") + sep + ctrl_t + "Y",
|
||||
append_shortcut_item(editMenu, Shortcut::Redo, false, _L("Redo"),
|
||||
_L("Redo"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3084,7 +3164,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
"", nullptr, [this](){return m_plater->can_redo(); }, this);
|
||||
editMenu->AppendSeparator();
|
||||
// BBS Cut TODO
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Cut") + sep + ctrl_t + "X",
|
||||
append_shortcut_item(editMenu, Shortcut::Cut, false, _L("Cut"),
|
||||
_L("Cut selection to clipboard"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3096,7 +3176,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
m_plater->cut_selection_to_clipboard(); },
|
||||
"", nullptr, [this]() {return m_plater->can_copy_to_clipboard(); }, this);
|
||||
// BBS Copy
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Copy") + sep + ctrl_t + "C",
|
||||
append_shortcut_item(editMenu, Shortcut::Copy, false, _L("Copy"),
|
||||
_L("Copy selection to clipboard"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3108,7 +3188,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
m_plater->copy_selection_to_clipboard(); },
|
||||
"", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this);
|
||||
// BBS Paste
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Paste") + sep + ctrl_t + "V",
|
||||
append_shortcut_item(editMenu, Shortcut::Paste, false, _L("Paste"),
|
||||
_L("Paste clipboard"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3121,14 +3201,14 @@ void MainFrame::init_menubar_as_editor()
|
||||
"", nullptr, [this](){return m_plater->can_paste_from_clipboard(); }, this);
|
||||
#if 0
|
||||
// BBS Delete selected
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Delete Selected") + "\t" + _L_CONTEXT("Backspace", "Keyboard Shortcut"),
|
||||
append_shortcut_item(editMenu, Shortcut::DeleteSelected, true, _L("Delete Selected"),
|
||||
_L("Deletes the current selection"),[this](wxCommandEvent&) {
|
||||
m_plater->remove_selected();
|
||||
},
|
||||
"", nullptr, [this](){return can_delete(); }, this);
|
||||
#endif
|
||||
//BBS: delete all
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Delete All") + "\t" + ctrl + "D",
|
||||
append_shortcut_item(editMenu, Shortcut::DeleteAll, true, _L("Delete All"),
|
||||
_L("Deletes all objects"),[this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3141,7 +3221,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
"", nullptr, [this](){return can_delete_all(); }, this);
|
||||
editMenu->AppendSeparator();
|
||||
// BBS Clone Selected
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Clone Selected") + "\t" + ctrl + "K",
|
||||
append_shortcut_item(editMenu, Shortcut::CloneSelected, true, _L("Clone Selected"),
|
||||
_L("Clone copies of selections"),[this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3164,7 +3244,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
#endif
|
||||
|
||||
// BBS Select All
|
||||
append_menu_item(editMenu, wxID_ANY, _L("Select All") + sep + ctrl_t + "A",
|
||||
append_shortcut_item(editMenu, Shortcut::SelectAll, false, _L("Select All"),
|
||||
_L("Selects all objects"), [this, handle_key_event](wxCommandEvent&) {
|
||||
wxKeyEvent e;
|
||||
e.SetEventType(wxEVT_KEY_DOWN);
|
||||
@@ -3216,7 +3296,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxMenu* viewMenu = nullptr;
|
||||
if (m_plater) {
|
||||
viewMenu = new wxMenu();
|
||||
add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this));
|
||||
add_common_view_menu_items(viewMenu, std::bind(&MainFrame::can_change_view, this));
|
||||
viewMenu->AppendSeparator();
|
||||
|
||||
//BBS perspective view
|
||||
@@ -3247,13 +3327,14 @@ void MainFrame::init_menubar_as_editor()
|
||||
[]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
|
||||
|
||||
viewMenu->AppendSeparator();
|
||||
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &G-code Window") + sep + "C", _L("Show G-code window in Preview scene."),
|
||||
wxMenuItem* gcode_window = append_menu_check_item(viewMenu, wxID_ANY, shortcut_label(_L("Show &G-code Window"), Shortcut::ToggleGcodeWindow, true), _L("Show G-code window in Preview scene."),
|
||||
[this](wxCommandEvent &) {
|
||||
wxGetApp().toggle_show_gcode_window();
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
},
|
||||
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
|
||||
[]() { return wxGetApp().show_gcode_window(); }, this);
|
||||
m_shortcut_menu_items.push_back({ gcode_window, Shortcut::ToggleGcodeWindow, _L("Show &G-code Window"), true });
|
||||
|
||||
append_menu_check_item(
|
||||
viewMenu, wxID_ANY, _L("Show 3D Navigator"), _L("Show 3D navigator in Prepare and Preview scene."),
|
||||
@@ -3281,9 +3362,10 @@ void MainFrame::init_menubar_as_editor()
|
||||
this);
|
||||
|
||||
viewMenu->AppendSeparator();
|
||||
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &Labels") + "\t" + ctrl + "E", _L("Show object labels in 3D scene."),
|
||||
wxMenuItem* show_labels = append_menu_check_item(viewMenu, wxID_ANY, shortcut_label(_L("Show &Labels"), Shortcut::ShowLabels, true), _L("Show object labels in 3D scene."),
|
||||
[this](wxCommandEvent&) { m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown()); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this,
|
||||
[this]() { return m_plater->is_view3D_shown(); }, [this]() { return m_plater->are_view3D_labels_shown(); }, this);
|
||||
m_shortcut_menu_items.push_back({ show_labels, Shortcut::ShowLabels, _L("Show &Labels"), true });
|
||||
|
||||
append_menu_check_item(viewMenu, wxID_ANY, _L("Show &Overhang"), _L("Show object overhang highlight in 3D scene."),
|
||||
[this](wxCommandEvent &) {
|
||||
@@ -3328,8 +3410,6 @@ void MainFrame::init_menubar_as_editor()
|
||||
//auto preference_item = new wxMenuItem(parent_menu, OrcaSlicerMenuPreferences + bambu_studio_id_base, _L("Preferences") + "\t" + ctrl + ",", "");
|
||||
#else
|
||||
wxMenu* parent_menu = m_topbar->GetTopMenu();
|
||||
auto preference_item = new wxMenuItem(parent_menu, ConfigMenuPreferences + config_id_base, _L("Preferences") + "\t" + ctrl + "P", "");
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -3338,12 +3418,17 @@ 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_shortcut_item(
|
||||
parent_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
//parent_menu->Insert(1, preference_item);
|
||||
#endif
|
||||
// Help menu
|
||||
@@ -3357,8 +3442,8 @@ void MainFrame::init_menubar_as_editor()
|
||||
m_topbar->AddDropDownSubMenu(viewMenu, _L("View"));
|
||||
//BBS add Preference
|
||||
|
||||
append_menu_item(
|
||||
m_topbar->GetTopMenu(), wxID_ANY, _L("Preferences") + "\t" + ctrl + "P", "",
|
||||
append_shortcut_item(
|
||||
m_topbar->GetTopMenu(), Shortcut::Preferences, true, _L("Preferences"), "",
|
||||
[](wxCommandEvent &) {
|
||||
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
|
||||
wxGetApp().open_preferences();
|
||||
@@ -3368,7 +3453,13 @@ void MainFrame::init_menubar_as_editor()
|
||||
auto top_menu = m_topbar->GetTopMenu();
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
append_shortcut_item(
|
||||
top_menu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
|
||||
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
top_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
|
||||
[this](wxCommandEvent &) {
|
||||
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
|
||||
@@ -3414,88 +3505,51 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// Temperature
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Max Volumetric Speed
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Pressure Advance
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Flow rate (Wizard Dialog)
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_plater) return;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Retraction
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Cornering
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Input Shaping (with submenu)
|
||||
auto input_shaping_menu = new wxMenu();
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
|
||||
|
||||
// VFA
|
||||
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// help
|
||||
@@ -3506,6 +3560,10 @@ 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_shortcut_item(
|
||||
fileMenu, Shortcut::SpeedDial, false, _L("Open speed dial..."), "",
|
||||
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
|
||||
"", nullptr, []() { return true; }, this);
|
||||
append_menu_item(
|
||||
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
|
||||
[this](wxCommandEvent&) {
|
||||
@@ -3552,89 +3610,52 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// Temperature
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Max Volumetric Speed
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Pressure Advance
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Flowrate (with submenu)
|
||||
// ORCA: Flow rate (Wizard Dialog)
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_plater) return;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Retraction
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Cornering
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
|
||||
// Input Shaping (with submenu)
|
||||
auto input_shaping_menu = new wxMenu();
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
append_menu_item(
|
||||
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
|
||||
[this](wxCommandEvent&) {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
},
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
|
||||
"", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
|
||||
|
||||
// VFA
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
|
||||
[this](wxCommandEvent&) {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
}, "", nullptr,
|
||||
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
|
||||
[this]() {return m_plater->is_view3D_shown();; }, this);
|
||||
// help
|
||||
append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"),
|
||||
@@ -3743,7 +3764,7 @@ void MainFrame::init_menubar_as_gcodeviewer()
|
||||
wxMenu* viewMenu = nullptr;
|
||||
if (m_plater != nullptr) {
|
||||
viewMenu = new wxMenu();
|
||||
add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this));
|
||||
add_common_view_menu_items(viewMenu, std::bind(&MainFrame::can_change_view, this));
|
||||
}
|
||||
|
||||
// helpmenu
|
||||
@@ -4408,6 +4429,72 @@ void MainFrame::technology_changed()
|
||||
m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings"));
|
||||
}
|
||||
|
||||
// Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle:
|
||||
// the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are
|
||||
// cached members reused across launches; cornering/input-shaping build a fresh transient dialog.
|
||||
// Call while the Prepare (3D) panel is shown.
|
||||
void MainFrame::run_calibration(CalibKind calib_kind)
|
||||
{
|
||||
switch (calib_kind) {
|
||||
case CalibKind::Temperature: {
|
||||
if (!m_temp_calib_dlg)
|
||||
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_temp_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::MaxVolumetric: {
|
||||
if (!m_vol_test_dlg)
|
||||
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_vol_test_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::PressureAdvance: {
|
||||
if (!m_pa_calib_dlg)
|
||||
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_pa_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::FlowRatio: {
|
||||
if (!m_plater)
|
||||
break;
|
||||
if (!m_flow_rate_calib_dlg)
|
||||
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_flow_rate_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::Retraction: {
|
||||
if (!m_retraction_calib_dlg)
|
||||
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_retraction_calib_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
case CalibKind::Cornering: {
|
||||
auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::InputShapingFreq: {
|
||||
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::InputShapingDamp: {
|
||||
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
dlg->ShowModal();
|
||||
dlg->Destroy();
|
||||
break;
|
||||
}
|
||||
case CalibKind::VFA: {
|
||||
if (!m_vfa_test_dlg)
|
||||
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
|
||||
m_vfa_test_dlg->ShowModal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Called after the Preferences dialog is closed and the program settings are saved.
|
||||
|
||||
@@ -74,6 +74,8 @@ class DesignPanel;
|
||||
class MainFrame;
|
||||
class WebViewPanel;
|
||||
class ParamsDialog;
|
||||
enum class Shortcut : uint8_t;
|
||||
struct KeyChord;
|
||||
#ifdef __WXGTK__
|
||||
class ResizeEdgePanel;
|
||||
#endif
|
||||
@@ -113,6 +115,20 @@ protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
};
|
||||
|
||||
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
|
||||
enum class CalibKind : int
|
||||
{
|
||||
Temperature,
|
||||
MaxVolumetric,
|
||||
PressureAdvance,
|
||||
FlowRatio,
|
||||
Retraction,
|
||||
Cornering,
|
||||
InputShapingFreq,
|
||||
InputShapingDamp,
|
||||
VFA
|
||||
};
|
||||
|
||||
class MainFrame : public DPIFrame
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
@@ -181,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) {}
|
||||
@@ -340,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);
|
||||
@@ -360,6 +400,11 @@ public:
|
||||
|
||||
void technology_changed();
|
||||
|
||||
// Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle:
|
||||
// the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards
|
||||
// are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel
|
||||
// is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
|
||||
void run_calibration(CalibKind calib_kind);
|
||||
|
||||
//BBS
|
||||
void load_url(wxString url);
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
#include "NativeCommands.hpp"
|
||||
|
||||
#include "calib_dlg.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "DailyTips.hpp"
|
||||
#include "GCodeViewer.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "IMSlider.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "NetworkTestDialog.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "PluginsDialog.hpp"
|
||||
#include "PlateSettingsDialog.hpp"
|
||||
#include "DeviceCore/DevManager.h"
|
||||
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <wx/utils.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no
|
||||
// editable project - so gate every plate op on FFF + the normal editor.
|
||||
bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); }
|
||||
|
||||
AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; }
|
||||
|
||||
// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and
|
||||
// the notebook page label matches. A no-op when the 3D panel is already shown.
|
||||
void ensure_3d_view(Plater* plater)
|
||||
{
|
||||
if (plater && !plater->is_view3D_shown()) {
|
||||
plater->select_view_3D("3D");
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREPARE);
|
||||
}
|
||||
}
|
||||
|
||||
// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so
|
||||
// a launch from another tab doesn't report a spuriously empty selection.
|
||||
AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*))
|
||||
{
|
||||
if (!plater)
|
||||
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
|
||||
ensure_3d_view(plater);
|
||||
if (!ok(plater))
|
||||
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
|
||||
op(plater);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
// Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already
|
||||
// switched to Preview (which may request a slice); if a slicer result is present the slider is
|
||||
// repositioned immediately, otherwise the jump is a no-op until the user re-slices.
|
||||
void go_to_layer(Plater* plater, const std::string& param)
|
||||
{
|
||||
if (!plater)
|
||||
return;
|
||||
double pct = 50.0;
|
||||
try {
|
||||
pct = std::stod(param);
|
||||
} catch (const std::exception&) {}
|
||||
pct = std::clamp(pct, 0.0, 100.0);
|
||||
|
||||
GLCanvas3D* canvas = plater->get_current_canvas3D();
|
||||
if (!canvas)
|
||||
return;
|
||||
GCodeViewer& viewer = canvas->get_gcode_viewer();
|
||||
IMSlider* layers = viewer.get_layers_slider();
|
||||
IMSlider* moves = viewer.get_moves_slider();
|
||||
if (!layers || layers->GetMaxValue() <= 0)
|
||||
return;
|
||||
|
||||
const double max = double(layers->GetMaxValue());
|
||||
const int target = int(std::lround(pct / 100.0 * max));
|
||||
layers->SetHigherValue(target);
|
||||
if (layers->is_one_layer())
|
||||
layers->SetLowerValue(target);
|
||||
layers->set_as_dirty();
|
||||
if (moves) {
|
||||
moves->SetHigherValue(moves->GetMaxValue());
|
||||
moves->set_as_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Select a named camera view. Plater::select_view dispatches to the current panel.
|
||||
AppActionRunResult view_command(Plater* plater, const std::string& dir)
|
||||
{
|
||||
if (plater)
|
||||
plater->select_view(dir);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
// Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the
|
||||
// Calibration menu (which caches most of the wizard dialogs).
|
||||
AppActionRunResult calib_command(CalibKind kind)
|
||||
{
|
||||
MainFrame* mf = wxGetApp().mainframe;
|
||||
if (!mf)
|
||||
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
|
||||
ensure_3d_view(wxGetApp().plater());
|
||||
mf->run_calibration(kind);
|
||||
return {AppActionRunResult::Level::Success};
|
||||
}
|
||||
|
||||
constexpr const char* kCommandPrefix = "orca_command";
|
||||
|
||||
// Thin AppAction wrapper for one catalog entry: identity and presentation come from the catalog,
|
||||
// run() routes back to it. The id is keyed by the stable catalog key (not the display title), so a
|
||||
// rename or a UI-language switch never re-keys the action.
|
||||
struct CommandAction : AppAction
|
||||
{
|
||||
std::string command_key;
|
||||
|
||||
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
|
||||
|
||||
explicit CommandAction(const NativeCommand& c)
|
||||
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
|
||||
, command_key(c.key)
|
||||
{
|
||||
this->kind = AppActionKind::Command;
|
||||
this->group = c.group;
|
||||
this->input = c.input;
|
||||
this->icon = c.icon;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<NativeCommand> build_command_catalog()
|
||||
{
|
||||
std::vector<NativeCommand> out;
|
||||
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
|
||||
std::string input = {}, std::string icon = {}) {
|
||||
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
|
||||
};
|
||||
// Presentation-first overload: keeps the tile icon next to the title/group it belongs to.
|
||||
auto add_with_icon = [&](std::string key, std::string title, std::string group, std::string icon,
|
||||
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) {
|
||||
add(std::move(key), std::move(title), std::move(group), std::move(runner), std::move(input), std::move(icon));
|
||||
};
|
||||
|
||||
// ---- Slice & Export ----
|
||||
add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->reslice();
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREVIEW);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
add_with_icon(
|
||||
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer",
|
||||
[](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->select_view_3D("Preview", false);
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf)
|
||||
mf->select_tab(TAB_ID_PREVIEW);
|
||||
go_to_layer(plater, param);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
},
|
||||
"percent");
|
||||
|
||||
// "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab
|
||||
// id back as `param` (same contract as go_to_layer's percent).
|
||||
add(
|
||||
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
|
||||
[](const std::string& param) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty())
|
||||
mf->select_tab(from_u8(param));
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
},
|
||||
"tab");
|
||||
|
||||
add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->load_project();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->save_project(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) {
|
||||
wxGetApp().open_preferences();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Mode ----
|
||||
add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comSimple);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comAdvanced);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
wxGetApp().set_mode(comExpert);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
|
||||
add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) {
|
||||
GUI_App& app = wxGetApp();
|
||||
const bool on = !app.app_config->get_bool("developer_mode");
|
||||
app.app_config->set_bool("developer_mode", on);
|
||||
app.app_config->save();
|
||||
app.update_mode();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success, on ? _L("Developer mode enabled.") : _L("Developer mode disabled.")};
|
||||
});
|
||||
|
||||
// ---- Export pipeline ----
|
||||
add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_core_3mf();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_gcode_3mf(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Calibration ----
|
||||
// The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's
|
||||
// light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green).
|
||||
add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature",
|
||||
[](const std::string&) { return calib_command(CalibKind::Temperature); });
|
||||
add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed",
|
||||
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
|
||||
add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
|
||||
add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
|
||||
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
|
||||
add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction",
|
||||
[](const std::string&) { return calib_command(CalibKind::Retraction); });
|
||||
add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision",
|
||||
[](const std::string&) { return calib_command(CalibKind::Cornering); });
|
||||
add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
|
||||
add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
|
||||
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
|
||||
add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
|
||||
|
||||
// ---- View ----
|
||||
// Titles are built with _u8L here (not via a variable) so xgettext can extract them.
|
||||
for (auto [key, dir, title] :
|
||||
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")},
|
||||
{"view_bottom", "bottom", _u8L("View: Bottom")},
|
||||
{"view_front", "front", _u8L("View: Front")},
|
||||
{"view_rear", "rear", _u8L("View: Rear")},
|
||||
{"view_left", "left", _u8L("View: Left")},
|
||||
{"view_right", "right", _u8L("View: Right")},
|
||||
{"view_iso", "iso", _u8L("View: Isometric")}}) {
|
||||
std::string k = key, d = dir;
|
||||
add(k, title, _u8L("View"),
|
||||
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
|
||||
}
|
||||
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
plater->select_view("plate");
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->zoom_to_bed();
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->zoom_to_bed();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->get_camera().select_next_type();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->reset_window_layout();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Object ----
|
||||
add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
|
||||
});
|
||||
add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
|
||||
});
|
||||
add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
|
||||
});
|
||||
add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
|
||||
});
|
||||
add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
|
||||
});
|
||||
add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
|
||||
});
|
||||
add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
|
||||
});
|
||||
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
|
||||
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
|
||||
});
|
||||
add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
|
||||
});
|
||||
add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) {
|
||||
return object_op(
|
||||
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
|
||||
});
|
||||
add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
|
||||
});
|
||||
add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
|
||||
});
|
||||
|
||||
// ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object)
|
||||
auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) {
|
||||
add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
if (ObjectList* list = wxGetApp().obj_list())
|
||||
list->load_generic_subobject(type_name, ModelVolumeType::INVALID);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
};
|
||||
add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube");
|
||||
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder");
|
||||
add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere");
|
||||
add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone");
|
||||
add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc");
|
||||
add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus");
|
||||
add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
if (GLCanvas3D* canvas = plater->canvas3D())
|
||||
canvas->clear_popup_menu_position();
|
||||
MenuFactory::add_text_volume(ModelVolumeType::INVALID);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater) {
|
||||
ensure_3d_view(plater);
|
||||
if (GLCanvas3D* canvas = plater->canvas3D())
|
||||
canvas->clear_popup_menu_position();
|
||||
MenuFactory::add_svg_volume(ModelVolumeType::INVALID);
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Add Handy models ---- (the Add > Add Handy models submenu)
|
||||
const std::vector<MenuFactory::HandyModel>& handy = MenuFactory::handy_models();
|
||||
for (std::size_t i = 0; i < handy.size(); ++i) {
|
||||
add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"),
|
||||
[i](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
ensure_3d_view(plater);
|
||||
MenuFactory::load_handy_model(i);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Plate ----
|
||||
add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_add_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
|
||||
plater->add_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_add_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
|
||||
plater->duplicate_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
if (!plater->can_delete_plate())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
|
||||
plater->delete_plate();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlate* curr = plater->get_partplate_list().get_curr_plate();
|
||||
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
|
||||
dlg.set_plate_name(from_u8(curr->get_plate_name()));
|
||||
if (dlg.ShowModal() == wxID_YES)
|
||||
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlateList& plates = plater->get_partplate_list();
|
||||
const int index = plates.get_curr_plate_index();
|
||||
plater->take_snapshot("lock partplate");
|
||||
plates.lock_plate(index, !plates.is_locked(index));
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (!is_fff_plater(plater))
|
||||
return plate_unavailable();
|
||||
PartPlateList& plates = plater->get_partplate_list();
|
||||
const int count = plates.get_plate_count();
|
||||
if (count <= 0)
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("No plates available.")};
|
||||
int index = 0;
|
||||
try {
|
||||
index = std::stoi(param);
|
||||
} catch (const std::exception&) {}
|
||||
index = std::clamp(index, 0, count - 1);
|
||||
plater->select_plate(index, false);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Printer / device connection ----
|
||||
add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
if (dev && dev->get_selected_machine() && plater) {
|
||||
plater->sidebar().sync_ams_list();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")};
|
||||
});
|
||||
|
||||
// ---- Presets / cloud ----
|
||||
add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) {
|
||||
wxGetApp().open_presetbundledialog();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) {
|
||||
if (!wxGetApp().is_user_login())
|
||||
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
|
||||
wxGetApp().restart_sync_user_preset();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Import ----
|
||||
add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
#ifdef __APPLE__
|
||||
plater->add_model();
|
||||
#else
|
||||
plater->add_file();
|
||||
#endif
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->import_zip_archive();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->load_config_file();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Export extras ----
|
||||
add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, false, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_stl(false, false, true, FT_DRC);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->export_toolpaths_to_obj();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) {
|
||||
if (MainFrame* mf = wxGetApp().mainframe)
|
||||
mf->export_config();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- 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(ShortcutContext::Global);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
|
||||
wxGetApp().ShowUserGuide();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) {
|
||||
Slic3r::GUI::desktop_open_datadir_folder();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) {
|
||||
wxGetApp().troubleshoot();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) {
|
||||
NetworkTestDialog dlg(wxGetApp().mainframe);
|
||||
dlg.ShowModal();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
plater->get_dailytips()->open();
|
||||
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
|
||||
canvas->set_as_dirty();
|
||||
}
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) {
|
||||
wxGetApp().check_new_version_sf(true, 1);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
|
||||
Slic3r::GUI::about();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) {
|
||||
wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) {
|
||||
wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
// ---- Plugins ----
|
||||
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().open_plugins_dialog();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().refresh_plugins();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
open_plugin_hub();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) {
|
||||
wxGetApp().install_local_plugin();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<NativeCommand>& catalog_storage()
|
||||
{
|
||||
static std::vector<NativeCommand> commands = build_command_catalog();
|
||||
return commands;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<NativeCommand>& NativeCommands::catalog()
|
||||
{
|
||||
return catalog_storage();
|
||||
}
|
||||
|
||||
void NativeCommands::rebuild_catalog()
|
||||
{
|
||||
// build_command_catalog() re-runs _u8L under the current locale, so replacing the storage
|
||||
// refreshes every translated title/group after a language switch.
|
||||
catalog_storage() = build_command_catalog();
|
||||
}
|
||||
|
||||
std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command)
|
||||
{
|
||||
return std::make_unique<CommandAction>(command);
|
||||
}
|
||||
|
||||
AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param)
|
||||
{
|
||||
GUI_App& app = wxGetApp();
|
||||
if (app.is_closing())
|
||||
return {};
|
||||
for (const NativeCommand& c : catalog())
|
||||
if (c.key == key)
|
||||
return c.runner(param);
|
||||
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ActionRegistry.hpp" // for AppAction / AppActionRunResult
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A built-in speed-dial command: identity + how to run it. make_action() wraps a value as a thin
|
||||
// AppAction for the registry, so this catalog is the single source of truth for the behaviour
|
||||
// (runner => an owner method), the presentation (title/group/input), and the tile pictogram
|
||||
// (icon = an SVG base name under resources/images, "" for no icon).
|
||||
struct NativeCommand
|
||||
{
|
||||
std::string key;
|
||||
std::string title;
|
||||
std::string group;
|
||||
std::string input; // "percent"/"tab" or "" for immediate run
|
||||
std::string icon; // SVG base name, or "" to render a blank tile
|
||||
std::function<AppActionRunResult(const std::string& param)> runner;
|
||||
};
|
||||
|
||||
namespace NativeCommands {
|
||||
// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a
|
||||
// live UI language switch so the translated titles/groups match the new locale. UI thread only.
|
||||
const std::vector<NativeCommand>& catalog();
|
||||
|
||||
// Rebuilds the catalog in the current locale. UI thread only.
|
||||
void rebuild_catalog();
|
||||
|
||||
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
|
||||
AppActionRunResult run(const std::string& key, const std::string& param = {});
|
||||
|
||||
// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity,
|
||||
// presentation and behaviour as the single source of truth; ActionRegistry only stores and
|
||||
// dispatches the result. UI thread only.
|
||||
std::unique_ptr<AppAction> make_action(const NativeCommand& command);
|
||||
} // namespace NativeCommands
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Widgets/Label.hpp"
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
|
||||
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
|
||||
|
||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
||||
{
|
||||
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
|
||||
Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
|
||||
btn->SetCornerRadius(0);
|
||||
|
||||
if (bmp_name.empty() && bmp.IsOk())
|
||||
btn->SetIcon(bmp);
|
||||
|
||||
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
|
||||
// original spacing between a tab's icon and its caption.
|
||||
{
|
||||
wxClientDC dc(btn);
|
||||
dc.SetFont(btn->GetFont());
|
||||
int space_w = 0;
|
||||
dc.GetTextExtent(" ", &space_w, nullptr);
|
||||
btn->SetIconSpacing(5 + space_w);
|
||||
}
|
||||
|
||||
int em = em_unit(this);
|
||||
//BBS set size for button
|
||||
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
@@ -190,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
|
||||
Slic3r::GUI::wxGetApp().UpdateDarkUI(btn);
|
||||
m_pageButtons.insert(m_pageButtons.begin() + n, btn);
|
||||
m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA
|
||||
m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name);
|
||||
m_buttons_sizer->Insert(n, new wxSizerItem(btn));
|
||||
m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1);
|
||||
m_sizer->Layout();
|
||||
@@ -209,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
|
||||
Button* btn = m_pageButtons[n];
|
||||
m_pageButtons.erase(m_pageButtons.begin() + n);
|
||||
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
|
||||
m_pageIcons.erase(m_pageIcons.begin() + n);
|
||||
m_buttons_sizer->Remove(n);
|
||||
#if __WXOSX__
|
||||
RemoveChild(btn);
|
||||
@@ -245,7 +258,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
|
||||
int em = em_unit(this);
|
||||
Button* btn = m_pageButtons[n];
|
||||
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n]));
|
||||
btn->SetLabel(compact ? "" : m_pageLabels[n]);
|
||||
}
|
||||
|
||||
wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
@@ -254,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
return btn->GetLabel();
|
||||
}
|
||||
|
||||
// ORCA
|
||||
wxString ButtonsListCtrl::GetPageLabel(size_t n) const
|
||||
{
|
||||
return n < m_pageLabels.size() ? m_pageLabels[n] : wxString();
|
||||
}
|
||||
|
||||
// ORCA
|
||||
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,14 @@ public:
|
||||
void SetPageText(size_t n, const wxString& strText);
|
||||
void SetCompact(size_t n, bool compact); // ORCA
|
||||
wxString GetPageText(size_t n) const;
|
||||
// ORCA: the full page label, unaffected by SetCompact() blanking the button text.
|
||||
wxString GetPageLabel(size_t n) const;
|
||||
// Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap).
|
||||
const std::string& GetPageIcon(size_t n) const
|
||||
{
|
||||
static const std::string empty;
|
||||
return n < m_pageIcons.size() ? m_pageIcons[n] : empty;
|
||||
}
|
||||
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
|
||||
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
|
||||
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
|
||||
@@ -47,6 +55,7 @@ private:
|
||||
int m_btn_margin;
|
||||
int m_line_margin;
|
||||
std::vector<wxString> m_pageLabels; // ORCA
|
||||
std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty
|
||||
wxWindow* m_overflow_button{nullptr}; // ORCA
|
||||
};
|
||||
|
||||
@@ -241,6 +250,20 @@ public:
|
||||
return GetBtnsListCtrl()->GetPageText(n);
|
||||
}
|
||||
|
||||
// ORCA: the real page label. GetPageText() returns the button label, which SetCompact() blanks.
|
||||
wxString GetPageLabel(size_t n) const
|
||||
{
|
||||
wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page"));
|
||||
return GetBtnsListCtrl()->GetPageLabel(n);
|
||||
}
|
||||
|
||||
// Resource icon name the page was inserted with; empty for pages added with a wxBitmap.
|
||||
std::string GetPageIcon(size_t n) const
|
||||
{
|
||||
wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page"));
|
||||
return GetBtnsListCtrl()->GetPageIcon(n);
|
||||
}
|
||||
|
||||
virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "ConfigExceptions.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "SettingsIndex.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "OG_CustomCtrl.hpp"
|
||||
@@ -244,6 +245,25 @@ void OptionsGroup::append_line(const Line& line)
|
||||
{
|
||||
m_lines.emplace_back(line);
|
||||
|
||||
// Feed the searcher the row's wiki path (Line::label_path, for the Speed Dial's "open wiki"
|
||||
// affordance) and the label the row actually draws, so a setting action is named like the page.
|
||||
if (m_use_custom_ctrl) {
|
||||
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
|
||||
const Preset::Type type = static_cast<Preset::Type>(config_type());
|
||||
const bool multi = line.get_options().size() > 1;
|
||||
for (const auto& opt : line.get_options()) {
|
||||
if (!line.label_path.empty())
|
||||
index.set_path(opt.opt_id, type, line.label_path);
|
||||
// Mirror the sub-label OG_CustomCtrl draws for a multi-option row, so the palette
|
||||
// names each field like the page does.
|
||||
const std::string& leaf_src = opt.opt.label;
|
||||
const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ?
|
||||
_L_CONTEXT(leaf_src, "Layers") :
|
||||
_(leaf_src);
|
||||
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi));
|
||||
}
|
||||
}
|
||||
|
||||
if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty()))
|
||||
return;
|
||||
|
||||
@@ -650,7 +670,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
|
||||
m_opt_map.emplace(opt_id, pair);
|
||||
|
||||
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
|
||||
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category());
|
||||
wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
|
||||
|
||||
return Option(*m_config->def()->get(opt_key), opt_id);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,10 @@ protected:
|
||||
virtual void back_to_initial_value(const std::string& opt_key) {}
|
||||
virtual void back_to_sys_value(const std::string& opt_key) {}
|
||||
|
||||
// Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to
|
||||
// register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup.
|
||||
virtual int config_type() const { return -1; }
|
||||
|
||||
public:
|
||||
static wxString get_url(const std::string& path_end);
|
||||
static bool launch_browser(const std::string& path_end);
|
||||
@@ -273,7 +277,7 @@ public:
|
||||
OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {}
|
||||
|
||||
const wxString& config_category() const throw() { return m_config_category; }
|
||||
int config_type() const throw() { return m_config_type; }
|
||||
int config_type() const throw() override { return m_config_type; }
|
||||
const t_opt_map& opt_map() const throw() { return m_opt_map; }
|
||||
|
||||
void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; }
|
||||
|
||||
@@ -324,7 +324,9 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
|
||||
wxID_ANY,
|
||||
wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxVSCROLL) // hide hori-bar will cause hidden field mis-position
|
||||
wxVSCROLL // hide hori-bar will cause hidden field mis-position
|
||||
| wxTAB_TRAVERSAL // Allows for traversal via tab key
|
||||
)
|
||||
{
|
||||
// ShowScrollBar(GetHandle(), SB_BOTH, FALSE);
|
||||
Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) {
|
||||
|
||||
@@ -1116,19 +1116,9 @@ void PartPlate::render_plate_name_texture()
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
}
|
||||
|
||||
void PartPlate::show_tooltip(const std::string tooltip)
|
||||
void PartPlate::set_hover_tooltip(const std::string& tooltip)
|
||||
{
|
||||
const auto scale = m_plater->get_current_canvas3D()->get_scale();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale});
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale);
|
||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0});
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(tooltip.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar(2);
|
||||
m_partplate_list->m_hover_tooltip = tooltip;
|
||||
}
|
||||
|
||||
void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
@@ -1153,21 +1143,21 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
if (!only_name) {
|
||||
if (hover_id == 1) {
|
||||
render_icon_texture(m_del_icon.model, m_partplate_list->m_del_hovered_texture);
|
||||
show_tooltip(_u8L("Remove current plate (if not last one)"));
|
||||
set_hover_tooltip(_u8L("Remove current plate (if not last one)"));
|
||||
}
|
||||
else
|
||||
render_icon_texture(m_del_icon.model, m_partplate_list->m_del_texture);
|
||||
|
||||
if (hover_id == 2) {
|
||||
render_icon_texture(m_orient_icon.model, m_partplate_list->m_orient_hovered_texture);
|
||||
show_tooltip(_u8L("Auto orient objects on current plate"));
|
||||
set_hover_tooltip(_u8L("Auto orient objects on current plate"));
|
||||
}
|
||||
else
|
||||
render_icon_texture(m_orient_icon.model, m_partplate_list->m_orient_texture);
|
||||
|
||||
if (hover_id == 3) {
|
||||
render_icon_texture(m_arrange_icon.model, m_partplate_list->m_arrange_hovered_texture);
|
||||
show_tooltip(_u8L("Arrange objects on current plate"));
|
||||
set_hover_tooltip(_u8L("Arrange objects on current plate"));
|
||||
}
|
||||
else
|
||||
render_icon_texture(m_arrange_icon.model, m_partplate_list->m_arrange_texture);
|
||||
@@ -1176,12 +1166,12 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
if (this->is_locked()) {
|
||||
render_icon_texture(m_lock_icon.model,
|
||||
m_partplate_list->m_locked_hovered_texture);
|
||||
show_tooltip(_u8L("Unlock current plate"));
|
||||
set_hover_tooltip(_u8L("Unlock current plate"));
|
||||
}
|
||||
else {
|
||||
render_icon_texture(m_lock_icon.model,
|
||||
m_partplate_list->m_lockopen_hovered_texture);
|
||||
show_tooltip(_u8L("Lock current plate"));
|
||||
set_hover_tooltip(_u8L("Lock current plate"));
|
||||
}
|
||||
} else {
|
||||
if (this->is_locked())
|
||||
@@ -1195,21 +1185,21 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
if (dual_bbl) {
|
||||
if (hover_id == PLATE_FILAMENT_MAP_ID){
|
||||
render_icon_texture(m_plate_filament_map_icon.model, m_partplate_list->m_plate_set_filament_map_hovered_texture);
|
||||
show_tooltip(_u8L("Filament grouping"));
|
||||
set_hover_tooltip(_u8L("Filament grouping"));
|
||||
} else
|
||||
render_icon_texture(m_plate_filament_map_icon.model, m_partplate_list->m_plate_set_filament_map_texture);
|
||||
}
|
||||
|
||||
if (hover_id == 6) {
|
||||
render_icon_texture(m_plate_name_edit_icon.model, m_partplate_list->m_plate_name_edit_hovered_texture);
|
||||
show_tooltip(_u8L("Edit current plate name"));
|
||||
set_hover_tooltip(_u8L("Edit current plate name"));
|
||||
}
|
||||
else
|
||||
render_icon_texture(m_plate_name_edit_icon.model, m_partplate_list->m_plate_name_edit_texture);
|
||||
|
||||
if (hover_id == 7) {
|
||||
render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_hovered_texture);
|
||||
show_tooltip(_u8L("Move plate to the front"));
|
||||
set_hover_tooltip(_u8L("Move plate to the front"));
|
||||
} else
|
||||
render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture);
|
||||
|
||||
@@ -1222,7 +1212,7 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
else
|
||||
render_icon_texture(m_plate_settings_icon.model, m_partplate_list->m_plate_settings_changed_hovered_texture);
|
||||
|
||||
show_tooltip(_u8L("Customize current plate"));
|
||||
set_hover_tooltip(_u8L("Customize current plate"));
|
||||
} else {
|
||||
if (!has_plate_settings)
|
||||
render_icon_texture(m_plate_settings_icon.model, m_partplate_list->m_plate_settings_texture);
|
||||
@@ -6035,6 +6025,24 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
}
|
||||
}
|
||||
|
||||
void PartPlateList::render_hover_tooltip() const
|
||||
{
|
||||
if (m_hover_tooltip.empty())
|
||||
return;
|
||||
|
||||
const auto scale = m_plater->get_current_canvas3D()->get_scale();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale});
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale);
|
||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0});
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(m_hover_tooltip.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
/*int PartPlateList::select_plate_by_hover_id(int hover_id)
|
||||
{
|
||||
int index = hover_id / PartPlate::GRABBER_COUNT;
|
||||
|
||||
@@ -197,7 +197,7 @@ private:
|
||||
// void render_left_arrow(const ColorRGBA render_color, bool use_lighting) const;
|
||||
// void render_right_arrow(const ColorRGBA render_color, bool use_lighting) const;
|
||||
void render_icon_texture(GLModel &buffer, GLTexture &texture);
|
||||
void show_tooltip(const std::string tooltip);
|
||||
void set_hover_tooltip(const std::string& tooltip);
|
||||
void render_icons(bool bottom, bool only_name = false, int hover_id = -1);
|
||||
void render_only_numbers(bool bottom);
|
||||
void render_plate_name_texture();
|
||||
@@ -640,6 +640,8 @@ class PartPlateList : public ObjectBase
|
||||
bool render_bedtype_logo = true;
|
||||
bool render_plate_settings = true;
|
||||
bool render_cali_logo = true;
|
||||
// Tooltip of the plate icon the last scene pass drew hovered; the canvas overlay shows it.
|
||||
std::string m_hover_tooltip;
|
||||
|
||||
bool m_is_dark = false;
|
||||
|
||||
@@ -860,6 +862,8 @@ public:
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void render_hover_tooltip() const;
|
||||
void clear_hover_tooltip() { m_hover_tooltip.clear(); }
|
||||
void register_raycasters_for_picking(GLCanvas3D& canvas)
|
||||
{
|
||||
for (auto plate : m_plate_list)
|
||||
|
||||
@@ -265,6 +265,43 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
|
||||
return sizer;
|
||||
};
|
||||
|
||||
|
||||
auto ultimaker_generate_creds = [=](wxWindow* parent) {
|
||||
auto sizer = create_sizer_with_btn(parent, &m_printhost_generate_creds_btn, "ultimaker_generate_creds", _L("Generate API Key"));
|
||||
|
||||
m_printhost_generate_creds_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
|
||||
std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
|
||||
if (!host) {
|
||||
const wxString text = _L("Could not get a valid Printer Host reference");
|
||||
show_error(this, text);
|
||||
return;
|
||||
}
|
||||
|
||||
wxString msg = "generate_auth_creds";
|
||||
bool result;
|
||||
{
|
||||
// Show a wait cursor during the connection test, as it is blocking UI.
|
||||
wxBusyCursor wait;
|
||||
// Send request to printer for api key and such
|
||||
result = host->test(msg); // using test with special input because I don't want to create the generate_auth_creds func for every printer
|
||||
|
||||
// Prompt user to approve access on the machine.
|
||||
show_info(this, "API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again.\n"+msg, "API Key created.");
|
||||
|
||||
|
||||
}
|
||||
if (result)
|
||||
show_info(this, host->get_test_ok_msg(), _L("Success!"));
|
||||
else
|
||||
show_error(this, host->get_test_failed_msg(msg));
|
||||
|
||||
update();
|
||||
});
|
||||
|
||||
return sizer;
|
||||
};
|
||||
|
||||
|
||||
auto print_host_logout = [&](wxWindow* parent) {
|
||||
auto sizer = create_sizer_with_btn(parent, &m_printhost_logout_btn, "", _L("Log Out"));
|
||||
|
||||
@@ -303,6 +340,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
|
||||
Line host_line = m_optgroup->create_single_option_line(option);
|
||||
host_line.append_widget(printhost_browse);
|
||||
host_line.append_widget(print_host_test);
|
||||
host_line.append_widget(ultimaker_generate_creds);
|
||||
host_line.append_widget(print_host_logout);
|
||||
m_optgroup->append_line(host_line);
|
||||
|
||||
@@ -606,8 +644,7 @@ void PhysicalPrinterDialog::update(bool printer_change)
|
||||
m_optgroup->hide_field("bbl_use_print_host_webui");
|
||||
m_optgroup->enable_field("printhost_cafile");
|
||||
m_optgroup->enable_field("printhost_ssl_ignore_revoke");
|
||||
if (m_printhost_cafile_browse_btn)
|
||||
m_printhost_cafile_browse_btn->Enable();
|
||||
if (m_printhost_cafile_browse_btn) { m_printhost_cafile_browse_btn->Enable(); }
|
||||
|
||||
// hide pre-configured address, in case user switched to a different host type
|
||||
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
|
||||
@@ -704,7 +741,24 @@ void PhysicalPrinterDialog::update(bool printer_change)
|
||||
m_optgroup->hide_field("printhost_authorization_type");
|
||||
} else {
|
||||
m_optgroup->hide_field("flashforge_serial_number");
|
||||
}
|
||||
}
|
||||
|
||||
if (opt->value == htUltiMaker) {
|
||||
m_optgroup->hide_field("printhost_apikey");
|
||||
m_optgroup->hide_field("printhost_authorization_type");
|
||||
m_optgroup->hide_field("bbl_use_print_host_webui");
|
||||
m_optgroup->hide_field("printhost_cafile");
|
||||
m_optgroup->show_field("printhost_user");
|
||||
m_optgroup->show_field("printhost_password");
|
||||
m_optgroup->enable_field("print_host");
|
||||
m_optgroup->show_field("print_host_webui");
|
||||
if (m_printhost_cafile_browse_btn) {
|
||||
m_printhost_cafile_browse_btn->Disable();
|
||||
}
|
||||
if (m_printhost_generate_creds_btn) {
|
||||
m_printhost_generate_creds_btn->Enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false);
|
||||
@@ -720,6 +774,10 @@ void PhysicalPrinterDialog::update(bool printer_change)
|
||||
m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword);
|
||||
}
|
||||
|
||||
// The "Generate API Key" button is only meaningful for UltiMaker printers.
|
||||
if (m_printhost_generate_creds_btn)
|
||||
m_printhost_generate_creds_btn->Show(tech == ptFFF && m_config->opt_enum<PrintHostType>("host_type") == htUltiMaker);
|
||||
|
||||
m_optgroup->show_field("printhost_port", supports_multiple_printers);
|
||||
m_printhost_port_browse_btn->Show(supports_multiple_printers);
|
||||
|
||||
@@ -787,6 +845,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
|
||||
m_printhost_browse_btn->Rescale();
|
||||
m_printhost_test_btn->Rescale();
|
||||
m_printhost_generate_creds_btn->Rescale();
|
||||
m_printhost_logout_btn->Rescale();
|
||||
if (m_printhost_cafile_browse_btn)
|
||||
m_printhost_cafile_browse_btn->Rescale();
|
||||
|
||||
@@ -31,6 +31,7 @@ class PhysicalPrinterDialog : public DPIDialog
|
||||
|
||||
Button* m_printhost_browse_btn {nullptr};
|
||||
Button* m_printhost_test_btn {nullptr};
|
||||
Button* m_printhost_generate_creds_btn {nullptr};
|
||||
Button* m_printhost_logout_btn {nullptr};
|
||||
Button* m_printhost_cafile_browse_btn {nullptr};
|
||||
Button* m_printhost_port_browse_btn {nullptr};
|
||||
|
||||
+148
-108
@@ -83,6 +83,7 @@
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#ifdef __WXGTK__
|
||||
#include "LinuxDisplayBackend.hpp"
|
||||
@@ -149,7 +150,6 @@
|
||||
#include "Widgets/RadioGroup.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/StaticGroup.hpp"
|
||||
|
||||
#include "GUI_ObjectTable.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
@@ -476,14 +476,9 @@ enum class ActionButtonType : int {
|
||||
abSendGCode
|
||||
};
|
||||
|
||||
// Background for the extruder-group title chip and its edit buttons, matching the StaticGroup
|
||||
// interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour.
|
||||
// Background for the extruder-group title chip and its edit buttons
|
||||
static wxColour extruder_group_chip_bg()
|
||||
{
|
||||
#ifdef __WXOSX__
|
||||
if (!wxGetApp().dark_mode())
|
||||
return wxColour("#F7F7F7");
|
||||
#endif
|
||||
return StateColor::darkModeColorFor(*wxWHITE);
|
||||
}
|
||||
|
||||
@@ -498,38 +493,39 @@ public:
|
||||
SetBackgroundColour(extruder_group_chip_bg());
|
||||
auto sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
|
||||
|
||||
m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
m_label->SetFont(Label::Body_13);
|
||||
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
m_label->SetFont(Label::Body_12);
|
||||
m_label->SetForegroundColour(label_color);
|
||||
|
||||
m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
m_brace_left->SetFont(Label::Body_13);
|
||||
m_brace_left->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
m_brace_left->SetFont(Label::Body_12);
|
||||
m_brace_left->SetForegroundColour(label_color);
|
||||
m_brace_left->Hide();
|
||||
|
||||
m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
m_count->SetFont(Label::Body_13.Bold());
|
||||
m_count->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
m_count->SetFont(Label::Body_12.Bold());
|
||||
m_count->SetForegroundColour(label_color);
|
||||
m_count->Hide();
|
||||
|
||||
m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
m_brace_right->SetFont(Label::Body_13);
|
||||
m_brace_right->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
m_brace_right->SetFont(Label::Body_12);
|
||||
m_brace_right->SetForegroundColour(label_color);
|
||||
m_brace_right->Hide();
|
||||
|
||||
m_hover_btn = new ScalableButton(this, wxID_ANY, "dot");
|
||||
m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1));
|
||||
m_hover_btn = new ScalableButton(this, wxID_ANY, "edit_12px", wxEmptyString, FromDIP(wxSize(12,12)), wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 12);
|
||||
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
|
||||
m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
|
||||
if (m_enabled && m_hover_on_click)
|
||||
m_hover_on_click();
|
||||
});
|
||||
|
||||
sizer->Add(m_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_brace_left, 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_count, 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_label , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
|
||||
sizer->Add(m_brace_left , 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_count , 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_brace_right, 0, wxALIGN_CENTER_VERTICAL);
|
||||
sizer->Add(m_hover_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
|
||||
sizer->Add(m_hover_btn , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
|
||||
|
||||
// No SetSizerAndFit: that would record the count-hidden width as an explicit min size,
|
||||
// which outranks best size in sizer allocation, so once the count is shown any ancestor
|
||||
@@ -541,7 +537,7 @@ public:
|
||||
void EnableEdit(bool enable)
|
||||
{
|
||||
m_enabled = enable;
|
||||
m_hover_btn->SetBitmap_(enable ? "edit" : "dot");
|
||||
//m_hover_btn->SetBitmap_(enable ? "edit_12px" : "dot"); // it causes crash if icon sizes not matches
|
||||
}
|
||||
|
||||
void SetOnHoverClick(std::function<void()> on_click) { m_hover_on_click = std::move(on_click); }
|
||||
@@ -552,11 +548,13 @@ public:
|
||||
m_count->Hide();
|
||||
m_brace_left->Hide();
|
||||
m_brace_right->Hide();
|
||||
m_hover_btn->Hide();
|
||||
} else {
|
||||
m_count->SetLabel(wxString::Format("%d", count));
|
||||
m_count->Show();
|
||||
m_brace_left->Show();
|
||||
m_brace_right->Show();
|
||||
m_hover_btn->Show();
|
||||
}
|
||||
UpdateSizing();
|
||||
}
|
||||
@@ -567,15 +565,19 @@ public:
|
||||
UpdateSizing();
|
||||
}
|
||||
|
||||
void Rescale() { m_hover_btn->msw_rescale(); }
|
||||
void Rescale() {
|
||||
m_hover_btn->msw_rescale();
|
||||
UpdateSizing();
|
||||
}
|
||||
|
||||
// Re-apply the chip colours on a live light/dark switch (they are set once at construction).
|
||||
void sys_color_changed()
|
||||
{
|
||||
SetBackgroundColour(extruder_group_chip_bg());
|
||||
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
|
||||
m_label->SetForegroundColour(label_color);
|
||||
for (wxStaticText *t : {m_brace_left, m_count, m_brace_right})
|
||||
t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
t->SetForegroundColour(label_color);
|
||||
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
|
||||
Refresh();
|
||||
}
|
||||
@@ -602,11 +604,12 @@ private:
|
||||
bool m_enabled{false};
|
||||
};
|
||||
|
||||
struct ExtruderGroup : StaticGroup
|
||||
struct ExtruderGroup : StaticBox
|
||||
{
|
||||
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
|
||||
wxStaticBoxSizer *sizer = nullptr;
|
||||
wxBoxSizer * sizer = nullptr;
|
||||
HoverLabel * hover_label = nullptr;
|
||||
wxStaticText* ams_label{nullptr};
|
||||
ScalableButton * btn_edit = nullptr;
|
||||
ComboBox * combo_diameter = nullptr;
|
||||
ComboBox * combo_flow = nullptr;
|
||||
@@ -646,8 +649,10 @@ struct ExtruderGroup : StaticGroup
|
||||
{
|
||||
if (hover_label)
|
||||
hover_label->Rescale();
|
||||
if (btn_edit)
|
||||
if (btn_edit){
|
||||
btn_edit->msw_rescale();
|
||||
btn_edit->SetMinSize(ams_label->GetSize());
|
||||
}
|
||||
btn_up->msw_rescale();
|
||||
btn_down->msw_rescale();
|
||||
combo_diameter->Rescale();
|
||||
@@ -862,9 +867,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
||||
|
||||
// double
|
||||
extruder_dual_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
extruder_dual_sizer->Add(left_extruder->sizer, 1, wxEXPAND, 0);
|
||||
extruder_dual_sizer->Add(left_extruder, 1, wxEXPAND, 0);
|
||||
extruder_dual_sizer->AddSpacer(FromDIP(4));
|
||||
extruder_dual_sizer->Add(right_extruder->sizer, 1, wxEXPAND, 0);
|
||||
extruder_dual_sizer->Add(right_extruder, 1, wxEXPAND, 0);
|
||||
|
||||
// Filament Track Switch status icon, floated over the extruder AMS area (positioned in
|
||||
// update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip.
|
||||
@@ -879,7 +884,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
||||
}
|
||||
|
||||
// single
|
||||
extruder_single_sizer = single_extruder->sizer;
|
||||
extruder_single_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
extruder_single_sizer->Add(single_extruder, 1, wxEXPAND, 0);
|
||||
|
||||
wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
||||
extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
||||
@@ -1257,7 +1264,7 @@ public:
|
||||
|
||||
Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {
|
||||
wxPaintDC dc(this);
|
||||
dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color
|
||||
dc.SetPen(StateColor::darkModeColorFor(wxColour("#009688"))); // ORCA match popup border color
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0);
|
||||
});
|
||||
@@ -1311,29 +1318,25 @@ public:
|
||||
};
|
||||
|
||||
ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title)
|
||||
: StaticGroup(parent, wxID_ANY, wxString())
|
||||
: StaticBox(parent)
|
||||
{
|
||||
SetFont(Label::Body_10);
|
||||
SetForegroundColour(wxColour("#CECECE"));
|
||||
SetBorderColor(wxColour("#EEEEEE"));
|
||||
SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes
|
||||
ShowBadge(true);
|
||||
SetTopMargin(FromDIP(7)); // ORCA
|
||||
|
||||
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit
|
||||
// button) instead of being painted on the border by StaticGroup.
|
||||
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit button)
|
||||
hover_label = new HoverLabel(this, title);
|
||||
hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title
|
||||
|
||||
// Nozzle
|
||||
wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter"));
|
||||
label_diameter->SetFont(Label::Body_14);
|
||||
label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1});
|
||||
auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
this->combo_diameter = combo_diameter;
|
||||
wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow"));
|
||||
label_flow->SetFont(Label::Body_14);
|
||||
label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1});
|
||||
combo_diameter->SetToolTip(_L("Diameter"));
|
||||
|
||||
// Flow
|
||||
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
combo_flow->GetDropDown().SetUseContentWidth(true);
|
||||
combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) {
|
||||
@@ -1350,51 +1353,75 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
}
|
||||
});
|
||||
this->combo_flow = combo_flow;
|
||||
combo_flow->SetToolTip(_L("Flow"));
|
||||
|
||||
// AMS
|
||||
wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS"));
|
||||
label_ams->SetFont(Label::Body_14);
|
||||
label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
//label_ams->SetMinSize({FromDIP(70), -1});
|
||||
auto ams_panel = new wxPanel(this, wxID_ANY);
|
||||
ams_panel->SetBackgroundColour(*wxWHITE);
|
||||
|
||||
ams_label = new wxStaticText(ams_panel, wxID_ANY, _L("AMS"));
|
||||
ams_label->SetFont(Label::Body_14);
|
||||
ams_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
|
||||
// AMS not installed message
|
||||
ams_not_installed_msg = new wxStaticText(ams_panel, wxID_ANY, _L("Not installed"));
|
||||
ams_not_installed_msg->SetFont(Label::Body_14);
|
||||
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
|
||||
if (index >= 0) {
|
||||
btn_edit = new ScalableButton(this, wxID_ANY, "dot");
|
||||
btn_edit = new ScalableButton(ams_panel, wxID_ANY, "edit");
|
||||
btn_edit->SetMinSize(ams_label->GetSize());
|
||||
btn_edit->SetBackgroundColour(extruder_group_chip_bg());
|
||||
btn_edit->Hide();
|
||||
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) {
|
||||
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index, combo_diameter](auto &evt) {
|
||||
PopupWindow *window = new AMSCountPopupWindow(this, index);
|
||||
auto size = GetSize();
|
||||
auto pos = ClientToScreen({0, size.y + 12});
|
||||
auto size = GetSize();
|
||||
auto pos = ClientToScreen({0, size.y - FromDIP(8) - combo_diameter->GetSize().y});
|
||||
size.SetWidth(size.GetWidth() + FromDIP(10));
|
||||
window->Position(pos, {0, 0});
|
||||
window->Popup();
|
||||
});
|
||||
|
||||
auto hovered = std::make_shared<wxWindow *>();
|
||||
for (wxWindow *w : std::initializer_list<wxWindow *>{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); });
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } });
|
||||
for (wxWindow *w : std::initializer_list<wxWindow *>{this, btn_edit, ams_not_installed_msg, ams_label, ams_panel}) {
|
||||
// ORCA using CallAfter fixes crash on linux while clicking edit button
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
|
||||
*hovered = w;
|
||||
this->CallAfter([this]() {
|
||||
btn_edit->Show();
|
||||
ams_label->Hide();
|
||||
hsizer_ams->Layout();
|
||||
});
|
||||
});
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
|
||||
if (*hovered == w) {
|
||||
*hovered = nullptr;
|
||||
this->CallAfter([this]() {
|
||||
btn_edit->Hide();
|
||||
ams_label->Show();
|
||||
hsizer_ams->Layout();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// AMS not installed message
|
||||
ams_not_installed_msg = new wxStaticText(this, wxID_ANY, _L("Not installed"));
|
||||
ams_not_installed_msg->SetFont(Label::Body_14);
|
||||
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
|
||||
// AMS group
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
ams[i] = new AMSPreview(this, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS);
|
||||
ams[i] = new AMSPreview(ams_panel, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS);
|
||||
ams[i]->Close();
|
||||
}
|
||||
|
||||
hsizer_ams = new wxBoxSizer(wxHORIZONTAL);
|
||||
hsizer_ams->SetMinSize(0, ams[0]->GetMinHeight());
|
||||
hsizer_ams->Add(label_ams, 0, wxALIGN_CENTER);
|
||||
hsizer_ams->Add(ams_label, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
|
||||
if (btn_edit)
|
||||
hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2));
|
||||
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER);
|
||||
hsizer_ams->Add(btn_edit, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
|
||||
hsizer_ams->Add(ams_not_installed_msg, 1, wxALIGN_CENTER);
|
||||
|
||||
btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
|
||||
ams_panel->SetSizer(hsizer_ams);
|
||||
|
||||
btn_up = new ScalableButton(ams_panel, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
|
||||
btn_up->SetBackgroundColour(*wxWHITE);
|
||||
btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
|
||||
if (page_cur > 0)
|
||||
@@ -1402,7 +1429,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
update_ams();
|
||||
});
|
||||
btn_up->Hide();
|
||||
btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
|
||||
btn_down = new ScalableButton(ams_panel, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
|
||||
btn_down->SetBackgroundColour(*wxWHITE);
|
||||
btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
|
||||
if (page_cur + 1 < page_num)
|
||||
@@ -1411,31 +1438,24 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
});
|
||||
btn_down->Hide();
|
||||
|
||||
wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL);
|
||||
hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER);
|
||||
hsizer_diameter->Add(combo_diameter, 1, wxEXPAND);
|
||||
wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL);
|
||||
hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER);
|
||||
hsizer_nozzle->Add(combo_flow, 1, wxEXPAND);
|
||||
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
|
||||
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
|
||||
hsizer->Add(combo_flow , 1);
|
||||
|
||||
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
|
||||
if (index < 0) {
|
||||
label_ams->Hide();
|
||||
ams_not_installed_msg->Hide();
|
||||
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
|
||||
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8));
|
||||
hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8));
|
||||
hsizer->AddSpacer(FromDIP(2)); // Avoid badge
|
||||
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2));
|
||||
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2));
|
||||
this->sizer = vsizer;
|
||||
ams_panel->Hide();
|
||||
} else {
|
||||
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
|
||||
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2));
|
||||
vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
|
||||
vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
|
||||
vsizer->Add(hsizer_nozzle, 0, wxEXPAND | wxALL, FromDIP(2));
|
||||
this->sizer = vsizer;
|
||||
vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
|
||||
vsizer->AddSpacer(FromDIP(2));
|
||||
}
|
||||
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
|
||||
|
||||
SetSizer(vsizer);
|
||||
Layout();
|
||||
|
||||
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
|
||||
}
|
||||
|
||||
@@ -1506,7 +1526,7 @@ void ExtruderGroup::update_ams()
|
||||
}
|
||||
}
|
||||
|
||||
sizer->Layout();
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1)
|
||||
@@ -6423,6 +6443,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
|
||||
return p->searcher;
|
||||
}
|
||||
|
||||
Search::SettingsIndex& Sidebar::settings_index()
|
||||
{
|
||||
return p->searcher.index();
|
||||
}
|
||||
|
||||
std::string& Sidebar::get_search_line()
|
||||
{
|
||||
return p->searcher.search_string();
|
||||
@@ -6804,6 +6829,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;
|
||||
@@ -7571,10 +7597,8 @@ 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_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
|
||||
if (this->q->is_view3D_shown())
|
||||
wxGetApp().open_speed_dial();
|
||||
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(); });
|
||||
@@ -7652,7 +7676,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();
|
||||
@@ -8113,10 +8137,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);
|
||||
|
||||
@@ -12870,17 +12891,8 @@ void Plater::priv::on_action_add(SimpleEvent&)
|
||||
//BBS: add plate from toolbar
|
||||
void Plater::priv::on_action_add_plate(SimpleEvent&)
|
||||
{
|
||||
if (q != nullptr) {
|
||||
take_snapshot("add partplate");
|
||||
this->partplate_list.create_plate();
|
||||
int new_plate = this->partplate_list.get_plate_count() - 1;
|
||||
this->partplate_list.select_plate(new_plate);
|
||||
update();
|
||||
|
||||
// BBS set default view
|
||||
//q->get_camera().select_view("topfront");
|
||||
q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
|
||||
}
|
||||
if (q != nullptr)
|
||||
q->add_plate();
|
||||
}
|
||||
|
||||
//BBS: remove plate from toolbar
|
||||
@@ -22005,6 +22017,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
|
||||
return ret;
|
||||
}
|
||||
|
||||
//BBS: add an empty plate and switch to it (mirrors the toolbar's Add Plate).
|
||||
int Plater::add_plate()
|
||||
{
|
||||
if (!p->can_add_plate())
|
||||
return -1;
|
||||
take_snapshot("add partplate");
|
||||
int new_plate = p->partplate_list.create_plate();
|
||||
if (new_plate < 0)
|
||||
return new_plate;
|
||||
p->partplate_list.select_plate(new_plate);
|
||||
update();
|
||||
|
||||
// BBS set default view
|
||||
//get_camera().select_view("topfront");
|
||||
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
|
||||
return new_plate;
|
||||
}
|
||||
|
||||
int Plater::duplicate_plate(int plate_index)
|
||||
{
|
||||
int index = plate_index, ret;
|
||||
@@ -22462,6 +22492,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)) {}
|
||||
|
||||
@@ -283,6 +283,7 @@ public:
|
||||
std::vector<std::string>& types,
|
||||
std::vector<size_t>* config_indices = nullptr);
|
||||
Search::OptionsSearcher& get_searcher();
|
||||
Search::SettingsIndex& settings_index();
|
||||
std::string& get_search_line();
|
||||
void update_printer_thumbnail();
|
||||
|
||||
@@ -781,6 +782,9 @@ public:
|
||||
void apply_background_progress();
|
||||
//BBS: select the plate by hover_id
|
||||
int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false);
|
||||
//BBS: add an empty plate and switch to it (the toolbar's Add Plate). Returns the new
|
||||
// plate index, or -1 when the plate cap is reached.
|
||||
int add_plate();
|
||||
//BBS: delete the plate, index= -1 means the current plate
|
||||
int delete_plate(int plate_index = -1);
|
||||
int duplicate_plate(int plate_index = -1);
|
||||
@@ -952,6 +956,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)); }
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/uri.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -55,6 +56,14 @@ wxString web_base_url()
|
||||
return wxString("file://") + from_u8(dir) + "/";
|
||||
}
|
||||
|
||||
// Whether a loaded document is the plugin HTML's own base URL. The web view reports the URL it
|
||||
// parsed, so any fragment the page navigated to is ignored and the escaping it applies to what the
|
||||
// resources path holds (a space, a non-ASCII character) is undone first.
|
||||
bool is_content_url(const wxString& url)
|
||||
{
|
||||
return wxURI::Unescape(url.BeforeFirst('#')) == web_base_url();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
@@ -89,6 +98,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
// 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_NAVIGATED, &PluginWebDialog::on_navigated, this, wv->GetId());
|
||||
}
|
||||
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
|
||||
}
|
||||
@@ -139,19 +149,37 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
|
||||
|
||||
void PluginWebDialog::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_plugin_content();
|
||||
// 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 (is_content_url(event.GetURL())) {
|
||||
if (m_own_page_load)
|
||||
m_own_page_load = false;
|
||||
else if (loaded && m_content_navigated)
|
||||
load_plugin_content();
|
||||
}
|
||||
if (loaded)
|
||||
m_content_navigated = false;
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void PluginWebDialog::on_navigated(wxWebViewEvent& event)
|
||||
{
|
||||
m_content_navigated = is_content_url(event.GetURL());
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void PluginWebDialog::load_plugin_content()
|
||||
{
|
||||
if (m_content_loaded)
|
||||
return;
|
||||
m_content_loaded = true;
|
||||
if (wxWebView* wv = browser())
|
||||
if (wxWebView* wv = browser()) {
|
||||
m_own_page_load = true;
|
||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
||||
}
|
||||
}
|
||||
|
||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
||||
|
||||
@@ -64,6 +64,7 @@ protected:
|
||||
|
||||
private:
|
||||
void on_bootstrap_event(wxWebViewEvent& event);
|
||||
void on_navigated(wxWebViewEvent& event);
|
||||
void load_plugin_content();
|
||||
void on_close_window(wxCloseEvent& event);
|
||||
void fire_submit(const nlohmann::json& data);
|
||||
@@ -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;
|
||||
|
||||
+185
-122
@@ -126,15 +126,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
|
||||
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
|
||||
{
|
||||
std::vector<PluginDescriptor> cloud_entries;
|
||||
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
return cloud_entries;
|
||||
}
|
||||
|
||||
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
|
||||
{
|
||||
descriptor.plugin_root.clear();
|
||||
@@ -150,41 +141,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
std::vector<std::string> not_found, unauthorized;
|
||||
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
|
||||
current_cloud_metadata_snapshot();
|
||||
|
||||
manager.rescan_plugins();
|
||||
|
||||
if (!fetch_cloud) {
|
||||
manager.update_cloud_metadata(current_cloud_metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
|
||||
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
std::string to_string(PluginUpdateStatus status);
|
||||
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
|
||||
|
||||
@@ -448,6 +404,176 @@ bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& s
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── Dialog-independent plugin actions (also used by the speed dial) ───────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a
|
||||
// network round-trip (kUseCurrentCloudMeta).
|
||||
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
|
||||
{
|
||||
std::vector<PluginDescriptor> cloud_entries;
|
||||
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
return cloud_entries;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud)
|
||||
{
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
std::vector<std::string> not_found, unauthorized;
|
||||
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
|
||||
current_cloud_metadata_snapshot();
|
||||
|
||||
manager.rescan_plugins();
|
||||
|
||||
if (!fetch_cloud) {
|
||||
manager.update_cloud_metadata(current_cloud_metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
|
||||
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
void open_plugin_hub()
|
||||
{
|
||||
std::string cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
if (wxGetApp().getAgent()) {
|
||||
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
|
||||
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
|
||||
cloud_base_url = orca_agent->get_cloud_base_url();
|
||||
}
|
||||
|
||||
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
|
||||
cloud_base_url.pop_back();
|
||||
if (cloud_base_url.empty())
|
||||
cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
|
||||
}
|
||||
|
||||
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message)
|
||||
{
|
||||
message.clear();
|
||||
if (package_file.empty())
|
||||
return false;
|
||||
|
||||
// ---- pre-flight (main thread): validate + inspect + overwrite prompt ----
|
||||
const wxString package_name = from_u8(package_file.filename().string());
|
||||
|
||||
std::string extension = package_file.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
if (extension != ".py" && extension != ".whl") {
|
||||
message = _L("Select a .py or .whl plugin package.");
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor plugin_descriptor;
|
||||
bool existing_installation = false;
|
||||
std::string error;
|
||||
try {
|
||||
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what();
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existing_installation) {
|
||||
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
wxMessageDialog dialog(parent,
|
||||
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
|
||||
plugin_name),
|
||||
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
|
||||
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
|
||||
if (dialog.ShowModal() != wxID_OK) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file
|
||||
<< " plugin=" << plugin_descriptor.name;
|
||||
return false; // cancelled: message stays empty so callers stay silent
|
||||
}
|
||||
}
|
||||
|
||||
// ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ----
|
||||
bool installed = false;
|
||||
{
|
||||
struct Result
|
||||
{
|
||||
std::mutex mutex;
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
};
|
||||
auto state = std::make_shared<Result>();
|
||||
|
||||
detail::run_wait_with_progress(
|
||||
[state, package_file]() {
|
||||
std::string error;
|
||||
bool ok = false;
|
||||
try {
|
||||
ok = PluginManager::instance().install_plugin(package_file, error);
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
}
|
||||
if (ok) {
|
||||
// Reflect the new package in discovery/cloud metadata without blocking the caller.
|
||||
try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->ok = ok;
|
||||
state->error = std::move(error);
|
||||
},
|
||||
parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
|
||||
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{});
|
||||
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
installed = state->ok;
|
||||
error = std::move(state->error);
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error;
|
||||
message = _L("Failed to install plugin package. See the log for details.");
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file;
|
||||
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
message = wxString::Format(_L("Installed \"%s\"."), installed_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
|
||||
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
|
||||
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
|
||||
@@ -819,78 +945,31 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
|
||||
{
|
||||
if (package_path.empty())
|
||||
return false;
|
||||
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path;
|
||||
std::string error;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path;
|
||||
|
||||
const boost::filesystem::path package_file(package_path);
|
||||
const wxString package_name = from_u8(package_file.filename().string());
|
||||
wxString message;
|
||||
const bool installed = install_local_plugin_package(package_file, this, message);
|
||||
// The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it
|
||||
// once, after both have closed (the speed-dial path parents to the mainframe instead).
|
||||
restore_z_order();
|
||||
|
||||
std::string extension = package_file.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
if (extension != ".py" && extension != ".whl") {
|
||||
show_status(_L("Select a .py or .whl plugin package."), "info");
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor plugin_descriptor;
|
||||
bool existing_installation = false;
|
||||
auto report_inspection_failure = [&]() {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
|
||||
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
|
||||
// The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
|
||||
if (message.IsEmpty()) {
|
||||
send_plugins();
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
|
||||
return report_inspection_failure();
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
return report_inspection_failure();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
return report_inspection_failure();
|
||||
}
|
||||
|
||||
if (existing_installation) {
|
||||
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
wxMessageDialog dialog(
|
||||
this,
|
||||
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
|
||||
plugin_name),
|
||||
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
|
||||
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
|
||||
const int overwrite_rc = dialog.ShowModal();
|
||||
restore_z_order();
|
||||
if (overwrite_rc != wxID_OK) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
|
||||
<< " plugin=" << plugin_descriptor.name;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool installed = false;
|
||||
try {
|
||||
installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); },
|
||||
_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
|
||||
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME);
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
|
||||
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
|
||||
show_status(message, "warn");
|
||||
send_plugins();
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
|
||||
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
|
||||
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
|
||||
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
|
||||
show_status(message, "success");
|
||||
prompt_for_missing_plugins();
|
||||
send_plugins();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1086,23 +1165,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
|
||||
}
|
||||
|
||||
void PluginsDialog::open_plugin_hub()
|
||||
{
|
||||
std::string cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
if (wxGetApp().getAgent()) {
|
||||
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
|
||||
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
|
||||
cloud_base_url = orca_agent->get_cloud_base_url();
|
||||
}
|
||||
|
||||
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
|
||||
cloud_base_url.pop_back();
|
||||
if (cloud_base_url.empty())
|
||||
cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
|
||||
}
|
||||
void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
|
||||
|
||||
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
|
||||
{
|
||||
|
||||
+183
-124
@@ -25,6 +25,8 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
class wxTimer;
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -35,6 +37,184 @@ enum class PluginCapabilityType;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial:
|
||||
// they never require the webview dialog to be open.
|
||||
|
||||
// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI
|
||||
// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins().
|
||||
void refresh_plugin_metadata_blocking(bool fetch_cloud);
|
||||
|
||||
// Opens the Cloud plugin hub in the default browser. No dialog needed.
|
||||
void open_plugin_hub();
|
||||
|
||||
// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it
|
||||
// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns
|
||||
// the overwrite prompt and the progress dialog. On success `message` carries the localized
|
||||
// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason.
|
||||
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message);
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Shared worker + modal-progress machinery: pulse a progress dialog while `run` executes on a
|
||||
// detached worker, then run `on_finish` back on the UI thread. `alive`, when non-null, gates both
|
||||
// the pulse and `on_finish` so a worker outliving its dialog can't touch freed windows; pass null
|
||||
// for a dialog-independent caller. `restore` runs after the progress dialog is destroyed and before
|
||||
// `on_finish`, so a webview host can re-raise itself. `finish_after_dialog_destroyed` still calls
|
||||
// `on_finish` (without touching the dialog) when the host died, so a waiting loop can exit.
|
||||
template<typename Run, typename OnFinish>
|
||||
void run_off_thread_with_progress(Run&& run,
|
||||
OnFinish&& on_finish,
|
||||
wxWindow* parent,
|
||||
const wxString& title,
|
||||
const wxString& message,
|
||||
int maximum,
|
||||
int style,
|
||||
std::shared_ptr<std::atomic<bool>> alive,
|
||||
bool finish_after_dialog_destroyed,
|
||||
std::function<void()> restore)
|
||||
{
|
||||
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, parent, style);
|
||||
wxTimer* timer = new wxTimer();
|
||||
|
||||
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||
if ((!alive || alive->load(std::memory_order_acquire)) && progress)
|
||||
progress->Pulse(message);
|
||||
});
|
||||
|
||||
timer->Start(100);
|
||||
|
||||
std::thread([alive,
|
||||
progress,
|
||||
timer,
|
||||
run = std::forward<Run>(run),
|
||||
on_finish = std::forward<OnFinish>(on_finish),
|
||||
finish_after_dialog_destroyed,
|
||||
restore = std::move(restore)]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
|
||||
}
|
||||
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
wxTheApp->CallAfter([alive,
|
||||
progress,
|
||||
timer,
|
||||
on_finish = std::move(on_finish),
|
||||
finish_after_dialog_destroyed,
|
||||
restore = std::move(restore)]() mutable {
|
||||
timer->Stop();
|
||||
delete timer;
|
||||
|
||||
if (!alive || alive->load(std::memory_order_acquire)) {
|
||||
progress->Destroy();
|
||||
if (restore)
|
||||
restore();
|
||||
on_finish();
|
||||
} else if (finish_after_dialog_destroyed) {
|
||||
on_finish();
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// Wait for a worker behind a progress dialog, returning its result (or rethrowing). The waiting
|
||||
// loop stays responsive because it pumps the event loop the worker posts its completion into.
|
||||
template<typename Run>
|
||||
std::invoke_result_t<std::decay_t<Run>&> run_wait_with_progress(Run&& run,
|
||||
wxWindow* parent,
|
||||
const wxString& title,
|
||||
const wxString& message,
|
||||
int maximum,
|
||||
int style,
|
||||
std::shared_ptr<std::atomic<bool>> alive,
|
||||
std::function<void()> restore)
|
||||
{
|
||||
using Result = std::invoke_result_t<std::decay_t<Run>&>;
|
||||
|
||||
bool finished = false;
|
||||
wxEventLoop loop;
|
||||
auto on_finish = [&finished, &loop]() {
|
||||
finished = true;
|
||||
if (loop.IsRunning())
|
||||
loop.Exit();
|
||||
};
|
||||
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_off_thread_with_progress(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
} else {
|
||||
using StoredResult = std::decay_t<Result>;
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_off_thread_with_progress(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
StoredResult result = run();
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->result.emplace(std::move(result));
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (state->result)
|
||||
result.emplace(std::move(*state->result));
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
return std::move(*result);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
@@ -111,53 +291,8 @@ private:
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button
|
||||
bool finish_after_dialog_destroyed = false)
|
||||
{
|
||||
const auto alive = m_alive;
|
||||
ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style);
|
||||
wxTimer* timer = new wxTimer();
|
||||
|
||||
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||
if (alive->load(std::memory_order_acquire) && progress)
|
||||
progress->Pulse(message);
|
||||
});
|
||||
|
||||
timer->Start(100);
|
||||
|
||||
std::thread([this,
|
||||
alive,
|
||||
progress,
|
||||
timer,
|
||||
run = std::forward<Run>(run),
|
||||
on_finish = std::forward<OnFinish>(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
|
||||
}
|
||||
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
wxTheApp->CallAfter([this,
|
||||
alive,
|
||||
progress,
|
||||
timer,
|
||||
on_finish = std::move(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
timer->Stop();
|
||||
delete timer;
|
||||
|
||||
if (alive->load(std::memory_order_acquire)) {
|
||||
progress->Destroy();
|
||||
restore_z_order();
|
||||
on_finish();
|
||||
} else if (finish_after_dialog_destroyed) {
|
||||
on_finish();
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style,
|
||||
m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); });
|
||||
}
|
||||
|
||||
template<typename Run>
|
||||
@@ -167,83 +302,7 @@ private:
|
||||
int maximum = 100,
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||
{
|
||||
using Result = std::invoke_result_t<std::decay_t<Run>&>;
|
||||
|
||||
bool finished = false;
|
||||
wxEventLoop loop;
|
||||
auto on_finish = [&finished, &loop]() {
|
||||
finished = true;
|
||||
if (loop.IsRunning())
|
||||
loop.Exit();
|
||||
};
|
||||
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_with_dialog(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
} else {
|
||||
using StoredResult = std::decay_t<Result>;
|
||||
struct WaitState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<WaitState>();
|
||||
run_with_dialog(
|
||||
[run = std::forward<Run>(run), state]() mutable {
|
||||
try {
|
||||
StoredResult result = run();
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->result.emplace(std::move(result));
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
|
||||
std::optional<StoredResult> result;
|
||||
std::exception_ptr exception;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (state->result)
|
||||
result.emplace(std::move(*state->result));
|
||||
exception = state->exception;
|
||||
}
|
||||
if (exception)
|
||||
std::rethrow_exception(exception);
|
||||
return std::move(*result);
|
||||
}
|
||||
return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); });
|
||||
}
|
||||
|
||||
std::function<void()> m_open_terminal_dlg_fn;
|
||||
|
||||
@@ -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++) {
|
||||
@@ -1005,6 +1007,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
|
||||
checkbox->SetToolTip(tip);
|
||||
|
||||
if (param == "sync_user_preset") { m_sync_user_preset_checkbox = checkbox; }
|
||||
if (param == SETTING_OPENGL_SKIP_IDENTICAL_FRAMES) { m_skip_identical_frames_checkbox = checkbox; }
|
||||
|
||||
m_sizer->Add(checkbox, 0, wxALIGN_CENTER);
|
||||
|
||||
@@ -1031,6 +1034,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false");
|
||||
}
|
||||
else if (param == SETTING_OPENGL_SCENE_CACHE) {
|
||||
if (m_skip_identical_frames_checkbox) m_skip_identical_frames_checkbox->Enable(checkbox->GetValue());
|
||||
}
|
||||
else if (param == "stealth_mode") {
|
||||
bool enabled = app_config->get_stealth_mode();
|
||||
if (enabled) wxGetApp().on_stealth_mode_enter();
|
||||
@@ -1518,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();
|
||||
|
||||
@@ -1595,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);
|
||||
@@ -1726,6 +1745,21 @@ 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 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);
|
||||
|
||||
auto item_speed_dial_recents = create_item_spinctrl(
|
||||
_L("Recent actions"),
|
||||
"",
|
||||
_L("actions"),
|
||||
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
|
||||
SETTING_SPEED_DIAL_RECENT_COUNT,
|
||||
SPEED_DIAL_RECENT_COUNT_MIN,
|
||||
SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
g_sizer->Add(item_speed_dial_recents);
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
|
||||
_L("With this option enabled, the Design tab is shown, where models can be built and edited "
|
||||
@@ -1770,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);
|
||||
@@ -1840,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);
|
||||
|
||||
@@ -1864,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);
|
||||
@@ -1944,9 +1986,32 @@ void PreferencesDialog::create_items()
|
||||
);
|
||||
g_sizer->Add(item_fps_cap);
|
||||
|
||||
auto item_scene_cache = create_item_checkbox(
|
||||
_L("Reuse the 3D scene while idle"),
|
||||
_L("Skips redrawing the 3D scene when only the mouse cursor moves over the viewport,\n"
|
||||
"and reuses the previous frame's scene instead. Reduces GPU load.\n"
|
||||
"Disable it if the viewport shows stale or missing contents.\n\n"
|
||||
"Takes effect immediately."),
|
||||
SETTING_OPENGL_SCENE_CACHE
|
||||
);
|
||||
g_sizer->Add(item_scene_cache);
|
||||
|
||||
auto item_skip_identical_frames = create_item_checkbox(
|
||||
_L("Skip unchanged frames"),
|
||||
_L("Skips drawing a frame altogether when it would be identical to the one already on screen.\n"
|
||||
"Only applies to frames that reuse the 3D scene, so it needs Reuse the 3D scene while idle.\n"
|
||||
"Disable it if a hover highlight, tooltip or animation stops updating.\n\n"
|
||||
"Takes effect immediately."),
|
||||
SETTING_OPENGL_SKIP_IDENTICAL_FRAMES
|
||||
);
|
||||
g_sizer->Add(item_skip_identical_frames);
|
||||
if (m_skip_identical_frames_checkbox) m_skip_identical_frames_checkbox->Enable(app_config->get_bool(SETTING_OPENGL_SCENE_CACHE));
|
||||
|
||||
auto item_fps_overlay = create_item_checkbox(
|
||||
_L("Show FPS overlay"),
|
||||
_L("Displays current viewport FPS in the top-right corner."),
|
||||
_L("Displays rendering counts in the top-right corner of the viewport.") + "\n" +
|
||||
_L("FPS: frames presented to the screen per second.") + "\n" +
|
||||
_L("3D: frames per second that redrew the 3D scene."),
|
||||
SETTING_OPENGL_SHOW_FPS_OVERLAY
|
||||
);
|
||||
g_sizer->Add(item_fps_overlay);
|
||||
@@ -1989,7 +2054,7 @@ void PreferencesDialog::create_items()
|
||||
//////////////////////////
|
||||
//// ONLINE TAB
|
||||
/////////////////////////////////////
|
||||
m_pref_tabs->AppendItem(_L("Online"));
|
||||
m_tab_index[PreferencesTab::Online] = m_pref_tabs->AppendItem(_L("Online"));
|
||||
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
|
||||
g_sizer = f_sizers.back();
|
||||
g_sizer->AddGrowableCol(0, 1);
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace Slic3r { namespace GUI {
|
||||
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
|
||||
#define DESIGN_LEFT_MARGIN 25
|
||||
|
||||
// The tabs other dialogs open Preferences on.
|
||||
enum class PreferencesTab { General, Control, Graphics, Online };
|
||||
|
||||
class PreferencesDialog : public DPIDialog
|
||||
{
|
||||
private:
|
||||
@@ -38,6 +41,7 @@ protected:
|
||||
wxBoxSizer * m_sizer_body;
|
||||
wxScrolledWindow* m_parent;
|
||||
TabCtrl* m_pref_tabs;
|
||||
std::map<PreferencesTab, int> m_tab_index; // position of each tab in m_pref_tabs
|
||||
|
||||
// bool m_settings_layout_changed {false};
|
||||
bool m_seq_top_layer_only_changed{false};
|
||||
@@ -60,6 +64,8 @@ public:
|
||||
|
||||
~PreferencesDialog();
|
||||
|
||||
void select_tab(PreferencesTab tab, const std::string& option = {}); // and scrolls a combobox option's row into view, focused
|
||||
|
||||
wxString m_backup_interval_time;
|
||||
wxTimer m_filament_height_timer;
|
||||
|
||||
@@ -71,6 +77,7 @@ public:
|
||||
::CheckBox * m_dark_mode_ckeckbox = {nullptr};
|
||||
::CheckBox * m_sync_user_preset_checkbox = {nullptr};
|
||||
::CheckBox * m_bambu_cloud_checkbox = {nullptr};
|
||||
::CheckBox * m_skip_identical_frames_checkbox = {nullptr};
|
||||
::TextInput *m_backup_interval_textinput = {nullptr};
|
||||
::SpinInput *m_dim_previous_layers_brightness_input = {nullptr};
|
||||
::ComboBox * m_network_version_combo = {nullptr};
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "ConfigValueFormatter.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
#include "Widgets/StaticLine.hpp"
|
||||
@@ -39,6 +41,48 @@
|
||||
namespace Slic3r { namespace GUI {
|
||||
namespace {
|
||||
|
||||
// Orca's bitmap checkbox has the established teal checked state on every platform. Keep the
|
||||
// label separate so it stays clickable like a native wxCheckBox, while the control itself
|
||||
// remains accessible by keyboard.
|
||||
wxStaticText* add_checkbox_label(wxWindow* parent,
|
||||
wxBoxSizer* sizer,
|
||||
::CheckBox* check,
|
||||
const wxString& label,
|
||||
const wxString& tooltip,
|
||||
int label_width = 0)
|
||||
{
|
||||
check->SetToolTip(tooltip);
|
||||
sizer->Add(check, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, parent->FromDIP(2));
|
||||
|
||||
auto* text = new wxStaticText(parent, wxID_ANY, label);
|
||||
text->SetFont(Label::Body_14);
|
||||
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
if (label_width > 0) {
|
||||
text->SetMinSize(wxSize(label_width, -1));
|
||||
text->SetMaxSize(wxSize(label_width, -1));
|
||||
text->Wrap(label_width);
|
||||
}
|
||||
text->SetToolTip(tooltip);
|
||||
text->SetCursor(wxCURSOR_HAND);
|
||||
const auto toggle = [check]() {
|
||||
if (!check->IsEnabled())
|
||||
return;
|
||||
check->SetValue(!check->GetValue());
|
||||
wxCommandEvent event(wxEVT_TOGGLEBUTTON, check->GetId());
|
||||
event.SetEventObject(check);
|
||||
check->GetEventHandler()->ProcessEvent(event);
|
||||
};
|
||||
text->Bind(wxEVT_LEFT_DOWN, [toggle](wxMouseEvent& event) {
|
||||
if (!event.LeftDClick())
|
||||
toggle();
|
||||
});
|
||||
text->Bind(wxEVT_LEFT_DCLICK, [toggle](wxMouseEvent&) {
|
||||
toggle();
|
||||
});
|
||||
sizer->Add(text, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, parent->FromDIP(5));
|
||||
return text;
|
||||
}
|
||||
|
||||
// Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level
|
||||
// bindings (e.g. MainFrame's recent-files wxID_FILE1.. range).
|
||||
enum {
|
||||
@@ -649,6 +693,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
|
||||
m_outer_tabs->SetBackgroundColour(GetBackgroundColour());
|
||||
|
||||
m_outer_host = new wxPanel(this, wxID_ANY);
|
||||
#ifdef __WINDOWS__
|
||||
m_outer_host->SetDoubleBuffered(true);
|
||||
#endif
|
||||
m_outer_host->SetBackgroundColour(GetBackgroundColour());
|
||||
m_outer_host_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_outer_host->SetSizer(m_outer_host_sizer);
|
||||
@@ -688,24 +735,22 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
|
||||
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
// Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right).
|
||||
auto make_link = [this](const wxString& label, const char* url) {
|
||||
wxStaticText* link = new wxStaticText(this, wxID_ANY, label);
|
||||
link->SetFont(Label::Body_13);
|
||||
link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA));
|
||||
link->SetCursor(wxCURSOR_HAND);
|
||||
link->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent&) { wxLaunchDefaultBrowser(url, wxBROWSER_NEW_WINDOW); });
|
||||
return link;
|
||||
};
|
||||
wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
links_sizer->Add(make_link(_L("Publish 3MF Wiki"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, wxALIGN_LEFT);
|
||||
links_sizer->Add(make_link(_L("Publish 3MF Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg"), 0,
|
||||
wxTOP | wxALIGN_LEFT, FromDIP(4));
|
||||
auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html");
|
||||
auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg");
|
||||
links_sizer->Add(wiki_link , 0, wxALIGN_LEFT);
|
||||
links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4));
|
||||
|
||||
wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL);
|
||||
footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL);
|
||||
footer->AddStretchSpacer();
|
||||
footer->Add(dlg_btns, 0, wxALIGN_CENTER_VERTICAL);
|
||||
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxBOTTOM | wxEXPAND, FromDIP(10));
|
||||
auto* footer_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
|
||||
footer_line->SetBackgroundColour(wxColour("#CECECE"));
|
||||
footer_line->SetMinSize(wxSize(-1, 1));
|
||||
footer_line->SetMaxSize(wxSize(-1, 1));
|
||||
w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
|
||||
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10));
|
||||
|
||||
SetSizerAndFit(w_sizer);
|
||||
fit_to_content(); // initial size only; the dialog is resizable
|
||||
@@ -718,14 +763,14 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
|
||||
|
||||
// Size the window to its content: width follows the widest tab strip so no filament tab is
|
||||
// hidden (TabCtrl::relayout hides overflowing buttons), height scales proportionally. Both
|
||||
// are floored at the 600x500 base and capped at hard DIP limits - deliberately not the whole
|
||||
// are floored at the 530x530 base and capped at hard DIP limits - deliberately not the whole
|
||||
// display - with one last-resort clamp so the dialog can never open larger than the screen.
|
||||
// Also owns the resize floor: the window cannot be resized below what the tabs need, so
|
||||
// shrinking never re-hides a filament tab.
|
||||
void PublishSettingsDialog::fit_to_content()
|
||||
{
|
||||
static const wxSize BASE{600, 500};
|
||||
static const wxSize CAP{1300, 850};
|
||||
static const wxSize BASE{530, 530}; // base size in DIP, the minimum the dialog can shrink to
|
||||
static const wxSize CAP{1300, 850}; // hard cap in DIP, the maximum the dialog can grow to
|
||||
|
||||
int strip = m_outer_tabs->GetFullSize();
|
||||
for (const SectionGroup& section : m_sections) {
|
||||
@@ -793,6 +838,8 @@ void PublishSettingsDialog::build_option_model()
|
||||
return false;
|
||||
value = get_string_value(opt_id, full);
|
||||
unit = _(def->sidetext);
|
||||
if (unit == "%" && value.EndsWith("%"))
|
||||
unit.clear();
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1005,13 +1052,19 @@ void PublishSettingsDialog::build_option_model()
|
||||
// stays valid even if the vector is reallocated later.
|
||||
for (size_t c = 0; c < m_categories.size(); ++c)
|
||||
if (m_categories[c].enable_check != nullptr)
|
||||
m_categories[c].enable_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_enable_toggle(c); });
|
||||
m_categories[c].enable_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
|
||||
on_enable_toggle(c);
|
||||
event.Skip();
|
||||
});
|
||||
|
||||
// Wire the "Full Publish" checkboxes (physical slots): toggling one disables/enables the
|
||||
// material's rows.
|
||||
for (size_t c = 0; c < m_categories.size(); ++c)
|
||||
if (m_categories[c].full_check != nullptr)
|
||||
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
|
||||
m_categories[c].full_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
|
||||
on_full_toggle(c);
|
||||
event.Skip();
|
||||
});
|
||||
|
||||
// No filter is active at startup: every row matches until the user types.
|
||||
for (Row& row : m_rows)
|
||||
@@ -1074,6 +1127,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
|
||||
section.mixed_tabs->Hide();
|
||||
}
|
||||
section.page_host = new wxPanel(section.page, wxID_ANY);
|
||||
#ifdef __WINDOWS__
|
||||
section.page_host->SetDoubleBuffered(true);
|
||||
#endif
|
||||
section.page_host->SetBackgroundColour(GetBackgroundColour());
|
||||
section.page_host_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
section.page_host->SetSizer(section.page_host_sizer);
|
||||
@@ -1136,10 +1192,9 @@ size_t PublishSettingsDialog::category_index_for(
|
||||
if (is_mixed) {
|
||||
// No chip/title: the lone "Enable" checkbox tops the page.
|
||||
auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable"));
|
||||
category.enable_check->SetFont(Label::Body_13);
|
||||
category.enable_check->SetToolTip(_L("Publish this mixed filament and enable + Full Publish its component filaments"));
|
||||
enable_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
category.enable_check = new ::CheckBox(category.page, wxID_ANY);
|
||||
category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
|
||||
_L("Publish this mixed filament and enable + Full Publish its component filaments"));
|
||||
page_sizer->Add(enable_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
|
||||
} else {
|
||||
// Line 1: [chip] [title] [Enable]. The Enable checkbox gates the whole slot: while
|
||||
@@ -1152,19 +1207,19 @@ size_t PublishSettingsDialog::category_index_for(
|
||||
category.title_label = new wxStaticText(category.page, wxID_ANY, title);
|
||||
category.title_label->SetFont(Label::Head_14);
|
||||
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable"));
|
||||
category.enable_check->SetFont(Label::Body_13);
|
||||
category.enable_check->SetToolTip(_L("Publish this filament slot in the 3MF file"));
|
||||
header_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
category.enable_check = new ::CheckBox(category.page, wxID_ANY);
|
||||
category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
|
||||
_L("Publish this filament slot in the 3MF file"));
|
||||
header_sizer->Add(enable_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
|
||||
|
||||
// Line 2: the "Full Publish" toggle, on its own line below the title (hidden until
|
||||
// the slot is enabled), aligned with the colour chip above it.
|
||||
auto* full_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish"));
|
||||
category.full_check->SetFont(Label::Body_13);
|
||||
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file"));
|
||||
full_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
category.full_check = new ::CheckBox(category.page, wxID_ANY);
|
||||
category.full_label = add_checkbox_label(category.page, full_sizer, category.full_check, _L("Full Publish"),
|
||||
_L("Embed the entire filament of this slot in the 3MF file"));
|
||||
category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
|
||||
}
|
||||
}
|
||||
@@ -1180,7 +1235,7 @@ size_t PublishSettingsDialog::category_index_for(
|
||||
category.info->SetFont(Label::Body_13);
|
||||
category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10));
|
||||
category.info->Hide();
|
||||
page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4));
|
||||
page_sizer->Add(category.scroll, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(4));
|
||||
// A material slot starts disabled: its rows (and its Full Publish line) stay hidden until
|
||||
// "Enable" is checked.
|
||||
if (section == Section::Material)
|
||||
@@ -1241,7 +1296,7 @@ size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const
|
||||
sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
|
||||
auto* wrap = new wxBoxSizer(wxVERTICAL);
|
||||
wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
|
||||
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22));
|
||||
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
}
|
||||
category.subs.push_back(std::move(sub));
|
||||
return category.subs.size() - 1;
|
||||
@@ -1271,15 +1326,18 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||||
const size_t row_index = m_rows.size();
|
||||
m_rows.push_back(std::move(row));
|
||||
Row& current = m_rows[row_index];
|
||||
current.check = new wxCheckBox(category.scroll, wxID_ANY, label);
|
||||
current.check->SetFont(Label::Body_13);
|
||||
current.check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { refresh_tab_indicators(); });
|
||||
current.check = new ::CheckBox(category.scroll, wxID_ANY);
|
||||
current.check->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& event) {
|
||||
refresh_tab_indicators();
|
||||
event.Skip();
|
||||
});
|
||||
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
current.check_label = add_checkbox_label(category.scroll, row_sizer, current.check, label + ":", wxEmptyString,
|
||||
24 * wxGetApp().em_unit());
|
||||
// The value is read-only text (incl. the Type row: the published type is the slot's
|
||||
// normalized type, not author-editable).
|
||||
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
|
||||
current.value_label->SetFont(Label::Body_13);
|
||||
current.value_label->SetFont(Label::Body_14);
|
||||
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
|
||||
if (kind == RowKind::Color && !value.IsEmpty()) {
|
||||
@@ -1290,14 +1348,14 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||||
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
}
|
||||
}
|
||||
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
row_sizer->Add(current.value_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
if (!unit.IsEmpty()) {
|
||||
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
|
||||
current.unit_label->SetFont(Label::Body_13);
|
||||
current.unit_label->SetFont(Label::Body_14);
|
||||
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
|
||||
}
|
||||
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38));
|
||||
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
category.rows.push_back(row_index);
|
||||
category.subs[subcategory_index].rows.push_back(row_index);
|
||||
}
|
||||
@@ -1306,8 +1364,10 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index)
|
||||
{
|
||||
Category& cat = m_categories[category_index];
|
||||
const bool full = cat.full_check->GetValue();
|
||||
for (size_t r : cat.rows)
|
||||
for (size_t r : cat.rows) {
|
||||
m_rows[r].check->Enable(!full);
|
||||
m_rows[r].check_label->Enable(!full);
|
||||
}
|
||||
refresh_tab_indicators();
|
||||
}
|
||||
|
||||
@@ -1501,7 +1561,7 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV
|
||||
void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
|
||||
{
|
||||
// Rebase on the dialog's body font so clearing bold restores the exact original font.
|
||||
row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
|
||||
row.check_label->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::save_scroll_position(Category& category)
|
||||
@@ -1749,7 +1809,7 @@ void PublishSettingsDialog::select_all(bool value)
|
||||
for (Category& cat : m_categories)
|
||||
if (cat.section == Section::Material && cat.enable_check != nullptr)
|
||||
cat.enable_check->SetValue(value);
|
||||
// wxCheckBox::SetValue does not emit wxEVT_CHECKBOX, so re-run the enable handlers to
|
||||
// CheckBox::SetValue does not emit wxEVT_TOGGLEBUTTON, so re-run the enable handlers to
|
||||
// propagate mixed-slot components and refresh visibility as if the user had clicked.
|
||||
for (size_t c = 0; c < m_categories.size(); ++c)
|
||||
if (m_categories[c].section == Section::Material)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
// Widgets/StaticLine.hpp).
|
||||
class TextInput;
|
||||
class StaticLine;
|
||||
class CheckBox;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -97,7 +98,8 @@ private:
|
||||
size_t inner_index{0};
|
||||
bool dirty{false}; // matches a dirty base key: pre-checked + bold
|
||||
bool matches_filter{false}; // survives the active filter (computed by apply_filter)
|
||||
wxCheckBox* check{nullptr};
|
||||
::CheckBox* check{nullptr};
|
||||
wxStaticText* check_label{nullptr};
|
||||
wxStaticText* value_label{nullptr};
|
||||
wxStaticText* unit_label{nullptr};
|
||||
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
|
||||
@@ -131,11 +133,13 @@ private:
|
||||
// header row is hidden. For physical slots the Full Publish toggle sits on a second
|
||||
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
|
||||
// publishing the mix definition, so no Full Publish widget exists at all.
|
||||
wxCheckBox* enable_check{nullptr};
|
||||
::CheckBox* enable_check{nullptr};
|
||||
wxStaticText* enable_label{nullptr};
|
||||
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
|
||||
// "Full Publish": while checked, the whole slot preset is serialized and its rows
|
||||
// (incl. Color/Type) are disabled.
|
||||
wxCheckBox* full_check{nullptr};
|
||||
::CheckBox* full_check{nullptr};
|
||||
wxStaticText* full_label{nullptr};
|
||||
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
|
||||
// the slot's gradient/ratio definition as a whole.
|
||||
bool is_mixed{false};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "SceneCache.hpp"
|
||||
|
||||
#include "3DScene.hpp"
|
||||
#include "GLModel.hpp"
|
||||
#include "GLShader.hpp"
|
||||
#include "GLTexture.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
|
||||
#include <glad/gl.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
void SceneCache::capture(Key key)
|
||||
{
|
||||
m_valid = false;
|
||||
|
||||
if (wxGetApp().get_shader("flat_texture") == nullptr)
|
||||
return;
|
||||
|
||||
GLTexture::copy_from_framebuffer(m_texture_id, m_texture_size, key.size[0], key.size[1], GL_NEAREST);
|
||||
|
||||
m_key = std::move(key);
|
||||
m_valid = true;
|
||||
}
|
||||
|
||||
void SceneCache::render(GLModel& quad)
|
||||
{
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat_texture");
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
|
||||
shader->start_using();
|
||||
shader->set_uniform("view_model_matrix", Transform3d::Identity());
|
||||
shader->set_uniform("projection_matrix", Transform3d::Identity());
|
||||
shader->set_uniform("uniform_texture", 0);
|
||||
|
||||
glsafe(::glActiveTexture(GL_TEXTURE0));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_texture_id));
|
||||
quad.render();
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
shader->stop_using();
|
||||
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
}
|
||||
|
||||
void SceneCache::reset()
|
||||
{
|
||||
m_valid = false;
|
||||
if (m_texture_id != 0) {
|
||||
glsafe(::glDeleteTextures(1, &m_texture_id));
|
||||
m_texture_id = 0;
|
||||
}
|
||||
m_texture_size = { { 0, 0 } };
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class GLModel;
|
||||
|
||||
// The last 3D scene pass, kept as a texture for frames that only rebuild the overlay.
|
||||
class SceneCache
|
||||
{
|
||||
public:
|
||||
// Scene pass inputs that change without any frame request.
|
||||
struct Key
|
||||
{
|
||||
std::array<unsigned int, 2> size{ { 0, 0 } };
|
||||
Transform3d view_matrix{ Transform3d::Identity() };
|
||||
Transform3d projection_matrix{ Transform3d::Identity() };
|
||||
// Hovered volumes that draw their sinking contour, hovered plate icons, hovered gizmo grabber.
|
||||
std::vector<int> sinking_hover_volume_idxs;
|
||||
std::vector<int> hover_plate_icon_idxs;
|
||||
int gizmo_hover_id{ -1 };
|
||||
bool render_preview{ true };
|
||||
|
||||
bool operator == (const Key& other) const {
|
||||
return size == other.size && render_preview == other.render_preview &&
|
||||
gizmo_hover_id == other.gizmo_hover_id &&
|
||||
sinking_hover_volume_idxs == other.sinking_hover_volume_idxs &&
|
||||
hover_plate_icon_idxs == other.hover_plate_icon_idxs &&
|
||||
view_matrix.isApprox(other.view_matrix) &&
|
||||
projection_matrix.isApprox(other.projection_matrix);
|
||||
}
|
||||
};
|
||||
|
||||
// Copies the bound read framebuffer, sized by key.size, and remembers key.
|
||||
void capture(Key key);
|
||||
bool matches(const Key& key) const { return m_valid && m_key == key; }
|
||||
// Draws the last capture over the whole viewport, on the given full screen quad.
|
||||
void render(GLModel& quad);
|
||||
void invalidate() { m_valid = false; }
|
||||
// Frees the texture.
|
||||
void reset();
|
||||
|
||||
private:
|
||||
unsigned int m_texture_id{ 0 };
|
||||
std::array<unsigned int, 2> m_texture_size{ { 0, 0 } };
|
||||
Key m_key;
|
||||
bool m_valid{ false };
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user