Merge branch 'main' into feat/plugin-lifecycle-evts

This commit is contained in:
Ian Chua
2026-09-22 14:21:36 +08:00
1052 changed files with 140050 additions and 5263 deletions
+24
View File
@@ -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;
+10
View File
@@ -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;
+3 -1
View File
@@ -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
View File
@@ -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));
+1
View File
@@ -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
+72 -10
View File
@@ -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() == ';') {
+5 -3
View File
@@ -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_ */
+10 -9
View File
@@ -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;
+7 -3
View File
@@ -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
+15
View File
@@ -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);
+2
View File
@@ -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);
+22 -3
View File
@@ -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.");
+1 -1
View File
@@ -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
View File
@@ -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);
+12 -2
View File
@@ -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
+17 -5
View File
@@ -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();
+523 -60
View File
@@ -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
+137 -25
View File
@@ -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
+1 -1
View File
@@ -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();
});
+31 -14
View File
@@ -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)
+10 -2
View File
@@ -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);
File diff suppressed because it is too large Load Diff
+65 -4
View File
@@ -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;
+2
View File
@@ -90,6 +90,8 @@ std::pair<bool, std::string> GLShadersManager::init()
, { "ENABLE_ENVIRONMENT_MAP"sv }
#endif // ENABLE_ENVIRONMENT_MAP
);
// used to render objects as translucent, edge weighted surfaces in the X-Ray view
valid &= append_shader("xray", { prefix + "xray.vs", prefix + "xray.fs" });
// used to render variable layers heights in 3d editor
valid &= append_shader("variable_layer_height", { prefix + "variable_layer_height.vs", prefix + "variable_layer_height.fs" });
// used to render highlight contour around selected triangles inside the multi-material gizmo
+23
View File
@@ -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
View File
@@ -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);
+47 -15
View File
@@ -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()) {
+8 -2
View File
@@ -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
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "BindDialog.hpp"
#include "DeviceManager.hpp"
#include "HMS.hpp"
@@ -1120,6 +1121,8 @@ GUI_App::GUI_App()
{
//app config initializes early becasuse it is used in instance checking in OrcaSlicer.cpp
this->init_app_config();
m_shortcuts = std::make_unique<ShortcutRegistry>();
m_shortcuts->load(*app_config);
this->init_download_path();
// Note: the WebView2 runtime check (init_webview_runtime) used to run here, but
// the constructor executes before wxWidgets is fully initialized and before the
@@ -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)
+18 -2
View File
@@ -70,6 +70,9 @@ namespace GUI{
class RemovableDriveManager;
class OtherInstanceMessageHandler;
class ShortcutRegistry;
enum class ShortcutContext : uint8_t;
enum class PreferencesTab;
class MainFrame;
class Sidebar;
class ObjectSettings;
@@ -286,6 +289,7 @@ private:
std::unique_ptr<RemovableDriveManager> m_removable_drive_manager;
std::unique_ptr<ImGuiWrapper> m_imgui;
std::unique_ptr<ShortcutRegistry> m_shortcuts;
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
@@ -483,7 +487,7 @@ public:
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
void import_model(wxWindow *parent, wxArrayString& input_files) const;
@@ -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
View File
@@ -6,6 +6,7 @@
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_App.hpp"
#include "Shortcuts.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "ObjectDataViewModel.hpp"
@@ -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,
+18
View File
@@ -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;
+51 -80
View File
@@ -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();
+8 -1
View File
@@ -40,6 +40,9 @@ typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI {
enum class Shortcut : uint8_t;
struct ObjectVolumeID {
ModelObject* object{ nullptr };
ModelVolume* volume{ nullptr };
@@ -270,7 +273,11 @@ public:
void extruder_editing();
#ifndef __WXOSX__
void key_event(wxKeyEvent& event);
#else
// wxDataViewCtrl never sees key events on macOS, so the bindings are installed as accelerators.
void update_shortcut_accelerators();
#endif /* __WXOSX__ */
bool dispatch_shortcut(Shortcut shortcut);
void copy();
void paste();
@@ -482,8 +489,8 @@ public:
private:
#ifdef __WXOSX__
// void OnChar(wxKeyEvent& event);
wxAcceleratorTable m_accel;
wxWindowID m_shortcut_id_base;
#endif /* __WXOSX__ */
void OnContextMenu(wxDataViewEvent &event);
void list_manipulation(const wxPoint& mouse_pos, bool evt_context_menu = false);
-24
View File
@@ -280,8 +280,6 @@ bool Preview::init(wxWindow* parent, Bed3D& bed, Model* model)
m_canvas->enable_assemble_view_toolbar(false);
// sizer, m_canvas_widget
m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_layers_slider_from_canvas, this);
wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0);
@@ -505,28 +503,6 @@ void Preview::update_layers_slider_mode()
m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder, can_change_color);
}
void Preview::update_layers_slider_from_canvas(wxKeyEvent &event)
{
if (event.HasModifiers()) {
event.Skip();
return;
}
const auto key = event.GetKeyCode();
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
IMSlider *m_moves_slider = m_canvas->get_gcode_viewer().get_moves_slider();
if (key == 'L') {
if(!m_layers_slider->switch_one_layer_mode())
event.Skip();
m_canvas->set_as_dirty();
}
/*else if (key == WXK_SHIFT)
m_layers_slider->UseDefaultColors(false);*/
else
event.Skip();
}
void Preview::update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range)
{
IMSlider *m_layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
-1
View File
@@ -171,7 +171,6 @@ private:
void update_layers_slider(const std::vector<double>& layers_z, bool keep_z_range = false);
void update_layers_slider_mode();
void update_layers_slider_from_canvas(wxKeyEvent &event);
//BBS: add only gcode mode
void load_print_as_fff(bool keep_z_range = false, bool only_gcode = false);
};
+2
View File
@@ -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__
+17
View File
@@ -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();
+3 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoAssembly.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoAssembly::on_init()
{
GLGizmoMeasure::on_init();
m_shortcut_key = WXK_CONTROL_Y;
m_shortcut = Shortcut::GizmoAssembly;
return true;
}
@@ -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;
+3 -6
View File
@@ -4,6 +4,7 @@
#include <glad/gl.h>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_Colors.hpp"
@@ -299,7 +300,6 @@ GLGizmoBase::GLGizmoBase(GLCanvas3D &parent, const std::string &icon_filename, u
: m_parent(parent)
, m_group_id(-1)
, m_state(Off)
, m_shortcut_key(NO_SHORTCUT_KEY_VALUE)
, m_icon_filename(icon_filename)
, m_sprite_id(sprite_id)
, m_imgui(wxGetApp().imgui())
@@ -515,11 +515,8 @@ void GLGizmoBase::render_input_window(float x, float y, float bottom_limit)
std::string GLGizmoBase::get_name(bool include_shortcut) const
{
int key = get_shortcut_key();
std::string out = on_get_name();
if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z)
out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]";
return out;
const std::string name = on_get_name();
return include_shortcut && m_shortcut.has_value() ? wxGetApp().shortcuts().with_key(name, *m_shortcut) : name;
}
} // namespace GUI
+6 -5
View File
@@ -11,6 +11,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include <cereal/archives/binary.hpp>
#include <optional>
#include <wx/event.h>
@@ -29,6 +30,7 @@ namespace GUI {
class ImGuiWrapper;
enum class Shortcut : uint8_t;
class GLCanvas3D;
enum class CommonGizmosDataID;
class CommonGizmosDataPool;
@@ -72,9 +74,6 @@ public:
NegZ = 1 << 5,
};
// Represents NO key(button on keyboard) value
static const int NO_SHORTCUT_KEY_VALUE = 0;
protected:
struct Grabber
{
@@ -138,7 +137,7 @@ protected:
int m_group_id; // TODO: remove only for rotate
EState m_state;
int m_shortcut_key;
std::optional<Shortcut> m_shortcut; // the registry entry that opens this gizmo
std::string m_icon_filename;
unsigned int m_sprite_id;
int m_hover_id{ -1 };
@@ -169,7 +168,7 @@ public:
EState get_state() const { return m_state; }
void set_state(EState state) { m_state = state; on_set_state(); }
int get_shortcut_key() const { return m_shortcut_key; }
std::optional<Shortcut> shortcut() const { return m_shortcut; }
const std::string& get_icon_filename() const { return m_icon_filename; }
@@ -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;
+2 -1
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -46,7 +47,7 @@ bool GLGizmoBrimEars::on_init()
{
m_new_point_head_radius = get_brim_default_radius();
m_shortcut_key = WXK_CONTROL_E;
m_shortcut = Shortcut::GizmoBrimEars;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -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;
+2 -1
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/format.hpp"
@@ -1310,7 +1311,7 @@ void GLGizmoCut3D::render_cut_line()
bool GLGizmoCut3D::on_init()
{
m_grabbers.emplace_back();
m_shortcut_key = WXK_CONTROL_C;
m_shortcut = Shortcut::GizmoCut;
// initiate info shortcuts
const wxString ctrl = GUI::shortkey_ctrl_prefix();
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoEmboss.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/MainFrame.hpp" // to update title when add text
@@ -727,7 +728,7 @@ bool GLGizmoEmboss::on_init()
m_rotate_gizmo.set_highlight_color(gray_color);
// NOTE: It has special handling in GLGizmosManager::handle_shortcut
m_shortcut_key = WXK_CONTROL_T;
m_shortcut = Shortcut::GizmoEmboss;
m_shortcuts = {
{_L("Drag"), _L("Position on surface")}
+10 -20
View File
@@ -8,6 +8,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -78,7 +79,7 @@ std::string GLGizmoFdmSupports::on_get_name() const
bool GLGizmoFdmSupports::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_L;
m_shortcut = Shortcut::GizmoFdmSupports;
m_desc["perform"] = _L("Apply");
m_desc["on_overhangs_only"] = _L("On highlighted overhangs only");
@@ -149,25 +150,14 @@ void GLGizmoFdmSupports::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoFdmSupports::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoFdmSupports::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -26,8 +26,7 @@ public:
state_ready
};
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoFlatten.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
@@ -54,7 +55,7 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
bool GLGizmoFlatten::on_init()
{
m_shortcut_key = WXK_CONTROL_F;
m_shortcut = Shortcut::GizmoFlatten;
return true;
}
+2 -1
View File
@@ -6,6 +6,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
@@ -31,7 +32,7 @@ std::string GLGizmoFuzzySkin::on_get_name() const
bool GLGizmoFuzzySkin::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_shortcut = Shortcut::GizmoFuzzySkin;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
-1
View File
@@ -25,7 +25,6 @@ GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filenam
bool GLGizmoHollow::on_init()
{
m_shortcut_key = WXK_CONTROL_H;
m_desc["enable"] = _(L("Hollow this object"));
m_desc["preview"] = _(L("Preview hollowed and drilled model"));
m_desc["offset"] = _(L("Offset")) + ": ";
+63 -29
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -449,7 +450,7 @@ bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_po
bool GLGizmoMeasure::on_init()
{
m_shortcut_key = WXK_CONTROL_U;
m_shortcut = Shortcut::GizmoMeasure;
const wxString shift = GUI::shortkey_shift_prefix();
@@ -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;
+6
View File
@@ -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;
+2 -1
View File
@@ -1,5 +1,6 @@
#include "GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "libslic3r/MeshBoolean.hpp"
@@ -104,7 +105,7 @@ bool GLGizmoMeshBoolean::on_mouse(const wxMouseEvent &mouse_event)
bool GLGizmoMeshBoolean::on_init()
{
m_shortcut_key = WXK_CONTROL_B;
m_shortcut = Shortcut::GizmoMeshBoolean;
return true;
}
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Plater.hpp"
@@ -90,7 +91,7 @@ void GLGizmoMmuSegmentation::init_extruders_data()
bool GLGizmoMmuSegmentation::on_init()
{
// BBS
m_shortcut_key = WXK_CONTROL_N;
m_shortcut = Shortcut::GizmoMmuSegmentation;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -209,30 +210,16 @@ bool GLGizmoMmuSegmentation::on_number_key_down(int number)
return true;
}
bool GLGizmoMmuSegmentation::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'F':
m_current_tool = ImGui::FillButtonIcon;
break;
case 'T':
m_current_tool = ImGui::TriangleButtonIcon;
break;
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
case 'H':
m_current_tool = ImGui::HeightRangeIcon;
break;
case 'G':
m_current_tool = ImGui::GapFillIcon;
break;
default:
return false;
break;
bool GLGizmoMmuSegmentation::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolFill: m_current_tool = ImGui::FillButtonIcon; break;
case Shortcut::PaintToolTriangle: m_current_tool = ImGui::TriangleButtonIcon; break;
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
case Shortcut::PaintToolHeightRange: m_current_tool = ImGui::HeightRangeIcon; break;
case Shortcut::PaintToolGapFill: m_current_tool = ImGui::GapFillIcon; break;
default: return false;
}
return true;
}
@@ -82,7 +82,7 @@ public:
// BBS
bool on_number_key_down(int number);
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoMove.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
//BBS: GUI refactor
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/AppConfig.hpp"
@@ -61,7 +62,7 @@ bool GLGizmoMove3D::on_init()
m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 };
m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 };
m_shortcut_key = WXK_CONTROL_M;
m_shortcut = Shortcut::GizmoMove;
return true;
}
+21 -4
View File
@@ -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 {}
+2 -1
View File
@@ -3,6 +3,7 @@
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp"
@@ -555,7 +556,7 @@ bool GLGizmoRotate3D::on_init()
for (unsigned int i = 0; i < 3; ++i)
m_gizmos[i].set_highlight_color(AXES_COLOR[i]);
m_shortcut_key = WXK_CONTROL_R;
m_shortcut = Shortcut::GizmoRotate;
return true;
}
+2 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoScale.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <glad/gl.h>
@@ -135,7 +136,7 @@ bool GLGizmoScale3D::on_init()
// BBS
m_grabbers[4].enabled = false;
m_shortcut_key = WXK_CONTROL_S;
m_shortcut = Shortcut::GizmoScale;
return true;
}
+8 -14
View File
@@ -5,6 +5,7 @@
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
@@ -28,7 +29,7 @@ void GLGizmoSeam::on_shutdown()
bool GLGizmoSeam::on_init()
{
m_shortcut_key = WXK_CONTROL_P;
m_shortcut = Shortcut::GizmoSeam;
const wxString ctrl = GUI::shortkey_ctrl_prefix();
const wxString alt = GUI::shortkey_alt_prefix();
@@ -86,19 +87,12 @@ void GLGizmoSeam::render_painter_gizmo()
glsafe(::glDisable(GL_BLEND));
}
// BBS
bool GLGizmoSeam::on_key_down_select_tool_type(int keyCode) {
switch (keyCode)
{
case 'S':
m_current_tool = ImGui::SphereButtonIcon;
break;
case 'C':
m_current_tool = ImGui::CircleButtonIcon;
break;
default:
return false;
break;
bool GLGizmoSeam::on_tool_shortcut(Shortcut shortcut)
{
switch (shortcut) {
case Shortcut::PaintToolSphere: m_current_tool = ImGui::SphereButtonIcon; break;
case Shortcut::PaintToolCircle: m_current_tool = ImGui::CircleButtonIcon; break;
default: return false;
}
return true;
}
+1 -2
View File
@@ -12,8 +12,7 @@ public:
void render_painter_gizmo() override;
//BBS
bool on_key_down_select_tool_type(int keyCode);
bool on_tool_shortcut(Shortcut shortcut) override;
protected:
// BBS
@@ -34,7 +34,6 @@ GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& ic
bool GLGizmoSlaSupports::on_init()
{
m_shortcut_key = WXK_CONTROL_L;
m_desc["head_diameter"] = _L("Head diameter") + ": ";
m_desc["lock_supports"] = _L("Lock supports under new islands");
-1
View File
@@ -261,7 +261,6 @@ bool GLGizmoText::on_init()
//m_avail_font_names = init_occt_fonts();
update_font_texture();
m_scale = m_imgui->get_font_size();
m_shortcut_key = WXK_CONTROL_T;
m_grabbers.push_back(Grabber());
+4
View File
@@ -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();
+67 -68
View File
@@ -4,6 +4,7 @@
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -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
+8 -2
View File
@@ -263,7 +263,10 @@ public:
EType get_gizmo_from_name(const std::string& gizmo_name) const;
bool is_running() const;
bool handle_shortcut(int key);
// Opens the gizmo bound to a Plater-context shortcut; false when no gizmo has it or it cannot open now.
bool open_gizmo_by_shortcut(Shortcut shortcut);
// Lets the current gizmo consume the delete key; false when it did not.
bool on_delete_key();
bool is_dragging() const;
@@ -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();
+2 -2
View File
@@ -5,6 +5,7 @@
#include "GUI_ObjectList.hpp"
#include "GLCanvas3D.hpp"
#include "MainFrame.hpp"
#include "Preferences.hpp"
#include "Tab.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
@@ -444,9 +445,8 @@ void HintDatabase::load_hints_from_file(const boost::filesystem::path& path)
// open preferences
}
else if (dict["hypertext_type"] == "preferences") {
std::string page = dict["hypertext_preferences_page"];
std::string item = dict["hypertext_preferences_item"];
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [page, item]() { wxGetApp().open_preferences(1, page); } };// 1 is to modify
HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, img_url, [item]() { wxGetApp().open_preferences(PreferencesTab::Control, item); } };
m_loaded_hints.emplace_back(hint_data);
}
else if (dict["hypertext_type"] == "plater") {
+30 -2
View File
@@ -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,
+5 -1
View File
@@ -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); }
+491 -308
View File
@@ -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
+95 -28
View File
@@ -3,53 +3,120 @@
#include <wx/wx.h>
#include <map>
#include <variant>
#include <vector>
#include "GUI_Utils.hpp"
#include "Shortcuts.hpp"
#include "wxExtensions.hpp"
#include <wx/simplebook.h>
class Button;
class Label;
class TabCtrl;
namespace Slic3r {
namespace GUI {
class Select
{
public:
int m_index;
wxWindow *m_tab_button;
wxWindow *m_tab_text;
};
WX_DECLARE_HASH_MAP(int, Select *, wxIntegerHash, wxIntegerEqual, SelectHash);
// Lists every shortcut per context and lets the user rebind the assignable ones.
class KBShortcutsDialog : public DPIDialog
{
typedef std::pair<std::string, std::string> Shortcut;
typedef std::vector<Shortcut> Shortcuts;
typedef std::pair<std::pair<wxString, wxString>, Shortcuts> ShortcutsItem;
typedef std::vector<ShortcutsItem> ShortcutsVec;
// A key the user cannot rebind.
struct FixedKey
{
std::vector<wxString> keys; // modifier names and the key, shown joined with "+"
const char* description; // untranslated
};
// A mouse button whose camera action is chosen in Preferences.
struct MouseAction
{
wxString button;
const char* preference; // AppConfig key of the action
};
struct Row
{
std::variant<Shortcut, FixedKey, MouseAction> content;
ShortcutSection section;
};
struct Page
{
wxString title;
wxString caption; // when the page's keys apply
ShortcutContext context;
std::vector<Row> rows;
};
struct EditableRow
{
Shortcut shortcut;
wxStaticText* description;
wxStaticText* modifiers;
wxStaticText* key;
ScalableButton* reset;
};
struct PreferenceRow
{
const char* preference;
wxStaticText* description;
};
ShortcutsVec m_full_shortcuts;
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_header_bitmap;
std::vector<wxPanel*> m_pages;
std::vector<Page> m_pages;
std::vector<EditableRow> m_editable_rows;
std::vector<PreferenceRow> m_preference_rows;
// Row geometry, measured once and shared by every page.
wxSize m_edit_size; // an edit or reset icon
int m_buttons_width = 0; // every row's buttons column, so the right-aligned keys share an edge
int m_row_text_width = 0; // what a description and its chord share; the description wraps at the rest
int m_key_slot = 0; // width of the widest single key, the column single keys line up in
TabCtrl* m_tabs;
wxSimplebook* m_simplebook;
public:
KBShortcutsDialog();
wxWindow* create_button(int id, wxString text);
void OnSelectTabel(wxCommandEvent &event);
wxPanel *m_panel_selects;
wxBoxSizer *m_sizer_right;
wxSimplebook *m_simplebook;
wxBoxSizer * m_sizer_body;
SelectHash m_hash_selector;
KBShortcutsDialog(wxWindow* parent, ShortcutContext page); // opens on the page of that context
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void fill_shortcuts();
wxPanel* create_header(wxWindow* parent, const wxFont& bold_font);
wxPanel* create_page(wxWindow* parent, const ShortcutsItem& shortcuts, const wxFont& font, const wxFont& bold_font);
void fill_pages();
wxPanel* create_page(wxWindow* parent, const Page& page);
void edit_shortcut(Shortcut shortcut);
void reset_shortcut(Shortcut shortcut);
// Asks question before unbinding conflicts; false when the user declined.
bool take_chord_from(Shortcut shortcut, const std::vector<Shortcut>& conflicts, const wxString& question);
void apply_bindings(); // refreshes the rows and pushes the change to the rest of the app
// Puts a chord on a row's two labels, a single key in the shared column, and returns the width the chord takes.
int set_chord_labels(wxStaticText* modifiers, wxStaticText* key, std::vector<wxString> parts);
void open_mouse_preferences(const char* preference);
};
// Records one key chord for a shortcut, warning about the shortcuts it would take the chord from.
class ShortcutCaptureDialog : public DPIDialog
{
public:
ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut);
// Valid after ShowModal() returned wxID_OK; an invalid chord means "unbind".
const KeyChord& chord() const { return m_chord; }
const std::vector<Shortcut>& conflicts() const { return m_conflicts; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void on_key(wxKeyEvent& evt);
void on_char(wxKeyEvent& evt);
void record(const KeyChord& chord);
Shortcut m_shortcut;
KeyChord m_chord;
std::vector<Shortcut> m_conflicts;
wxStaticText* m_chord_label;
wxString m_hint; // what m_status shows while there is nothing to warn about
wxString m_rejection; // what it shows for a key a Global shortcut cannot use
Label* m_status;
wxColour m_status_colour;
Button* m_ok;
};
} // namespace GUI
+314
View File
@@ -0,0 +1,314 @@
#include "KeyChord.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cctype>
namespace Slic3r { namespace GUI {
namespace {
constexpr int BINDABLE_MODIFIERS = wxMOD_CONTROL | wxMOD_SHIFT | wxMOD_ALT | wxMOD_RAW_CONTROL;
struct KeyName
{
int key;
const char* name; // canonical name, as wx parses it
const char* alias; // accepted when parsing; nullptr when there is none
const char* label; // translation key for KeyChord::display(); nullptr when name is it
};
constexpr std::array<KeyName, 15> special_keys{{
{ WXK_BACK, L_CONTEXT("Backspace", "Keyboard Shortcut"), "Back", nullptr },
{ WXK_TAB, L_CONTEXT("Tab", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_RETURN, L_CONTEXT("Enter", "Keyboard Shortcut"), "Return", nullptr },
{ WXK_ESCAPE, L_CONTEXT("Esc", "Keyboard Shortcut"), "Escape", nullptr },
{ WXK_SPACE, L_CONTEXT("Space", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_DELETE, L_CONTEXT("Del", "Keyboard Shortcut"), "Delete", nullptr },
{ WXK_INSERT, L_CONTEXT("Ins", "Keyboard Shortcut"), "Insert", nullptr },
{ WXK_HOME, L_CONTEXT("Home", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_END, L_CONTEXT("End", "Keyboard Shortcut"), nullptr, nullptr },
{ WXK_PAGEUP, L_CONTEXT("PgUp", "Keyboard Shortcut"), "PageUp", nullptr },
{ WXK_PAGEDOWN, L_CONTEXT("PgDn", "Keyboard Shortcut"), "PageDown", nullptr },
// Displayed as "Arrow Left" and so on, which is what the catalogs translate.
{ WXK_LEFT, "Left", nullptr, L_CONTEXT("Arrow Left", "Keyboard Shortcut") },
{ WXK_RIGHT, "Right", nullptr, L_CONTEXT("Arrow Right", "Keyboard Shortcut") },
{ WXK_UP, "Up", nullptr, L_CONTEXT("Arrow Up", "Keyboard Shortcut") },
{ WXK_DOWN, "Down", nullptr, L_CONTEXT("Arrow Down", "Keyboard Shortcut") },
}};
bool equals_ignoring_case(const std::string& a, const char* b)
{
if (b == nullptr)
return false;
size_t i = 0;
for (; i < a.size() && b[i] != '\0'; ++i)
if (std::tolower(static_cast<unsigned char>(a[i])) != std::tolower(static_cast<unsigned char>(b[i])))
return false;
return i == a.size() && b[i] == '\0';
}
bool is_letter(int key) { return key >= 'A' && key <= 'Z'; }
bool is_digit(int key) { return key >= '0' && key <= '9'; }
bool is_printable(int key) { return key > ' ' && key < 127; }
bool is_symbol(int key) { return is_printable(key) && !std::isalnum(key); }
bool is_function_key(int key) { return key >= WXK_F1 && key <= WXK_F24; }
bool is_special(int key)
{
if (is_function_key(key))
return true;
return std::any_of(special_keys.begin(), special_keys.end(), [key](const KeyName& k) { return k.key == key; });
}
std::string special_key_name(int key)
{
if (is_function_key(key))
return "F" + std::to_string(key - WXK_F1 + 1);
for (const KeyName& k : special_keys)
if (k.key == key)
return k.name;
return {};
}
std::string special_key_label(int key)
{
for (const KeyName& k : special_keys)
if (k.key == key)
return _u8L_CONTEXT(k.label != nullptr ? k.label : k.name, "Keyboard Shortcut");
return special_key_name(key);
}
int parse_key(const std::string& text)
{
if (text.size() == 1) {
const int key = static_cast<unsigned char>(text[0]);
return is_printable(key) ? std::toupper(key) : WXK_NONE;
}
for (const KeyName& k : special_keys)
if (equals_ignoring_case(text, k.name) || equals_ignoring_case(text, k.alias))
return k.key;
if ((text[0] == 'F' || text[0] == 'f') && text.size() <= 3 && std::all_of(text.begin() + 1, text.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); })) {
const int n = std::stoi(text.substr(1));
if (n >= 1 && n <= 24)
return WXK_F1 + n - 1;
}
return WXK_NONE;
}
int parse_modifier(const std::string& text)
{
if (equals_ignoring_case(text, "Ctrl") || equals_ignoring_case(text, "Control") || equals_ignoring_case(text, "Cmd") || equals_ignoring_case(text, "Command"))
return wxMOD_CONTROL;
if (equals_ignoring_case(text, "Shift"))
return wxMOD_SHIFT;
if (equals_ignoring_case(text, "Alt") || equals_ignoring_case(text, "Option"))
return wxMOD_ALT;
if (equals_ignoring_case(text, "RawCtrl"))
return wxMOD_RAW_CONTROL;
return wxMOD_NONE;
}
// Numpad keys act as their main-keyboard counterparts, so one binding covers both.
int fold_numpad(int key)
{
if (key >= WXK_NUMPAD0 && key <= WXK_NUMPAD9)
return '0' + (key - WXK_NUMPAD0);
switch (key) {
case WXK_NUMPAD_ENTER: return WXK_RETURN;
case WXK_NUMPAD_SPACE: return WXK_SPACE;
case WXK_NUMPAD_TAB: return WXK_TAB;
case WXK_NUMPAD_HOME: return WXK_HOME;
case WXK_NUMPAD_END: return WXK_END;
case WXK_NUMPAD_PAGEUP: return WXK_PAGEUP;
case WXK_NUMPAD_PAGEDOWN: return WXK_PAGEDOWN;
case WXK_NUMPAD_LEFT: return WXK_LEFT;
case WXK_NUMPAD_RIGHT: return WXK_RIGHT;
case WXK_NUMPAD_UP: return WXK_UP;
case WXK_NUMPAD_DOWN: return WXK_DOWN;
case WXK_NUMPAD_INSERT: return WXK_INSERT;
case WXK_NUMPAD_DELETE: return WXK_DELETE;
case WXK_NUMPAD_ADD: return '+';
case WXK_NUMPAD_SUBTRACT: return '-';
case WXK_NUMPAD_MULTIPLY: return '*';
case WXK_NUMPAD_DIVIDE: return '/';
case WXK_NUMPAD_DECIMAL: return '.';
case WXK_NUMPAD_EQUAL: return '=';
default: return key;
}
}
// Modifiers in the order the text forms list them; wxMOD_RAW_CONTROL is wxMOD_CONTROL off macOS.
#ifdef __APPLE__
constexpr std::array<int, 4> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT, wxMOD_RAW_CONTROL };
#else
constexpr std::array<int, 3> MODIFIER_ORDER{ wxMOD_CONTROL, wxMOD_SHIFT, wxMOD_ALT };
#endif
const char* canonical_modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return "Ctrl+";
if (modifier == wxMOD_SHIFT)
return "Shift+";
if (modifier == wxMOD_ALT)
return "Alt+";
return "RawCtrl+";
}
template<typename Prefix>
std::string join_modifiers(int modifiers, Prefix prefix)
{
std::string out;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
out += prefix(modifier);
return out;
}
} // namespace
bool KeyChord::is_punctuation() const { return is_symbol(key) && modifiers == wxMOD_NONE; }
bool KeyChord::needs_char_event() const { return is_symbol(key) && (modifiers & ~wxMOD_SHIFT) == 0; }
bool KeyChord::is_menu_accelerator() const
{
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
}
std::string KeyChord::to_string() const
{
if (!valid())
return {};
std::string out = join_modifiers(modifiers, canonical_modifier_prefix);
if (is_printable(key))
out += char(key);
else
out += special_key_name(key);
return out;
}
std::optional<KeyChord> KeyChord::parse(const std::string& text)
{
if (text.empty())
return std::nullopt;
// The key is whatever follows the last separator; a trailing '+' is the '+' key itself.
size_t key_start = text.size() - 1;
if (text.back() != '+') {
const size_t sep = text.rfind('+');
key_start = sep == std::string::npos ? 0 : sep + 1;
}
KeyChord chord;
chord.key = parse_key(text.substr(key_start));
if (chord.key == WXK_NONE)
return std::nullopt;
const std::string prefix = key_start == 0 ? std::string() : text.substr(0, key_start - 1);
size_t begin = 0;
while (begin < prefix.size()) {
size_t end = prefix.find('+', begin);
if (end == std::string::npos)
end = prefix.size();
const int modifier = parse_modifier(prefix.substr(begin, end - begin));
if (modifier == wxMOD_NONE)
return std::nullopt;
chord.modifiers |= modifier;
begin = end + 1;
}
if (is_letter(chord.key) || is_digit(chord.key) || !is_printable(chord.key) || chord.modifiers == wxMOD_NONE)
return chord;
// Shift is folded into the character for punctuation, so "Shift+/" is not accepted.
return (chord.modifiers & wxMOD_SHIFT) ? std::nullopt : std::optional<KeyChord>(chord);
}
std::string KeyChord::display() const
{
std::string out;
for (const std::string& part : display_parts())
out += (out.empty() ? "" : "+") + part;
return out;
}
std::vector<std::string> KeyChord::display_parts() const
{
std::vector<std::string> parts;
if (!valid())
return parts;
for (int modifier : MODIFIER_ORDER)
if (modifiers & modifier)
parts.push_back(modifier_name(modifier));
parts.push_back(is_printable(key) ? std::string(1, char(key)) : special_key_label(key));
return parts;
}
std::string KeyChord::modifier_prefix(int modifier)
{
if (modifier == wxMOD_CONTROL)
return shortkey_ctrl_prefix();
if (modifier == wxMOD_SHIFT)
return _u8L("Shift+");
if (modifier == wxMOD_ALT)
return shortkey_alt_prefix();
#ifdef __APPLE__
if (modifier == wxMOD_RAW_CONTROL)
return u8"⌃+";
#endif
return {};
}
// The catalogue holds the "Ctrl+" prefixes, so the bare name is the prefix without its "+"
// and any space before it ("Strg +" in German).
std::string KeyChord::modifier_name(int modifier)
{
std::string name = modifier_prefix(modifier);
if (!name.empty() && name.back() == '+')
name.pop_back();
while (!name.empty() && name.back() == ' ')
name.pop_back();
return name;
}
wxAcceleratorEntry KeyChord::to_accelerator_entry(int command) const
{
int flags = wxACCEL_NORMAL;
if (modifiers & wxMOD_CONTROL)
flags |= wxACCEL_CTRL;
if (modifiers & wxMOD_SHIFT)
flags |= wxACCEL_SHIFT;
if (modifiers & wxMOD_ALT)
flags |= wxACCEL_ALT;
#ifdef __APPLE__
if (modifiers & wxMOD_RAW_CONTROL)
flags |= wxACCEL_RAW_CTRL;
#endif
return wxAcceleratorEntry(flags, key, command);
}
KeyChord KeyChord::from_event(const wxKeyEvent& evt)
{
KeyChord chord;
chord.modifiers = evt.GetModifiers() & BINDABLE_MODIFIERS;
int key = fold_numpad(evt.GetKeyCode());
if (evt.GetEventType() == wxEVT_CHAR) {
if (key >= 1 && key <= 26 && evt.ControlDown())
key = 'A' + key - 1; // Ctrl+letter arrives as the control character
else if (is_symbol(key))
chord.modifiers &= ~wxMOD_SHIFT; // the character already reflects Shift
}
if (key >= 'a' && key <= 'z')
key -= 'a' - 'A';
if (is_printable(key) || is_special(key))
chord.key = key;
return chord;
}
}} // namespace Slic3r::GUI
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <wx/accel.h>
#include <wx/defs.h>
#include <functional>
#include <optional>
#include <string>
#include <vector>
class wxKeyEvent;
namespace Slic3r { namespace GUI {
// One key press: a key code as wxEVT_KEY_DOWN reports it (letters upper-case, numpad keys
// folded onto their main-keyboard equivalents) plus the wxMOD_* modifiers held with it.
// Printable punctuation is stored as the character it produces, so "+" means the key that
// types "+" on the user's layout.
struct KeyChord
{
int key = WXK_NONE;
int modifiers = wxMOD_NONE;
bool valid() const { return key != WXK_NONE; }
bool operator==(const KeyChord& other) const { return key == other.key && modifiers == other.modifiers; }
bool operator!=(const KeyChord& other) const { return !(*this == other); }
// Bare printable keys other than letters and digits are matched on wxEVT_CHAR, because
// only the char event knows which character a key produces under the active layout.
bool is_punctuation() const;
// True for a printable non-alphanumeric key pressed with nothing but Shift, which only the
// char event that follows can resolve.
bool needs_char_event() const;
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields.
bool is_menu_accelerator() const;
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
std::string to_string() const;
static std::optional<KeyChord> parse(const std::string& text);
// Text for menus, tooltips and the shortcuts dialog, with translated modifier names and the
// command and option glyphs on macOS.
std::string display() const;
// The pieces display() joins with "+": the modifier names, then the key name.
std::vector<std::string> display_parts() const;
// Translated text of one wxMOD_* modifier, as a "Ctrl+" prefix or the bare "Ctrl" name.
static std::string modifier_prefix(int modifier);
static std::string modifier_name(int modifier);
wxAcceleratorEntry to_accelerator_entry(int command) const;
// Builds the chord a key event describes, or an invalid chord for pure modifier presses and
// keys outside the bindable set. wxEVT_CHAR events are normalized to the key codes
// wxEVT_KEY_DOWN reports for letters, digits and special keys.
static KeyChord from_event(const wxKeyEvent& evt);
};
struct KeyChordHash
{
size_t operator()(const KeyChord& chord) const { return std::hash<long long>()((static_cast<long long>(chord.modifiers) << 32) | unsigned(chord.key)); }
};
}} // namespace Slic3r::GUI
+297 -210
View File
@@ -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.
+45
View File
@@ -74,6 +74,8 @@ class DesignPanel;
class MainFrame;
class WebViewPanel;
class ParamsDialog;
enum class Shortcut : uint8_t;
struct KeyChord;
#ifdef __WXGTK__
class ResizeEdgePanel;
#endif
@@ -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);
+669
View File
@@ -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
+43
View File
@@ -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
+21 -2
View File
@@ -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)
{
+23
View File
@@ -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;
+21 -1
View File
@@ -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);
}
+5 -1
View File
@@ -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; }
+3 -1
View File
@@ -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) {
+29 -21
View File
@@ -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;
+5 -1
View File
@@ -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)
+62 -3
View File
@@ -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();
+1
View File
@@ -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
View File
@@ -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)) {}
+7
View File
@@ -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)); }
+34 -6
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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(&not_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(&not_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
View File
@@ -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;
+70 -5
View File
@@ -18,6 +18,7 @@
#include "NetworkTestDialog.hpp"
#include "Widgets/StaticLine.hpp"
#include "Widgets/RadioGroup.hpp"
#include "Shortcuts.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "NetworkPluginDialog.hpp"
@@ -285,6 +286,7 @@ std::tuple<wxBoxSizer*, ComboBox*> PreferencesDialog::create_item_combobox_base(
auto combobox = new ::ComboBox(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, DESIGN_LARGE_COMBOBOX_SIZE, 0, nullptr, wxCB_READONLY);
combobox->GetDropDown().SetUseContentWidth(true);
combobox->SetToolTip(tip);
combobox->SetName(param); // select_tab() finds the row by this name
std::vector<wxString>::iterator iter;
for (iter = vlist.begin(); iter != vlist.end(); iter++) {
@@ -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);
+7
View File
@@ -29,6 +29,9 @@ namespace Slic3r { namespace GUI {
#define DESIGN_INPUT_SIZE wxSize(FromDIP(120), -1)
#define DESIGN_LEFT_MARGIN 25
// The tabs other dialogs open Preferences on.
enum class PreferencesTab { General, Control, Graphics, Online };
class PreferencesDialog : public DPIDialog
{
private:
@@ -38,6 +41,7 @@ protected:
wxBoxSizer * m_sizer_body;
wxScrolledWindow* m_parent;
TabCtrl* m_pref_tabs;
std::map<PreferencesTab, int> m_tab_index; // position of each tab in m_pref_tabs
// bool m_settings_layout_changed {false};
bool m_seq_top_layer_only_changed{false};
@@ -60,6 +64,8 @@ public:
~PreferencesDialog();
void select_tab(PreferencesTab tab, const std::string& option = {}); // and scrolls a combobox option's row into view, focused
wxString m_backup_interval_time;
wxTimer m_filament_height_timer;
@@ -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};
+102 -42
View File
@@ -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)
+7 -3
View File
@@ -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};
+62
View File
@@ -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
+56
View File
@@ -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