From 550ef48fe170f67a7f3058f0c361bbbd18254897 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Tue, 3 Jan 2023 13:24:01 +0100 Subject: [PATCH 01/20] Add function for check whether transformation contain reflection Extend RayCast hit by square distance Use distance to distiguish closest place on surface when move origin Move origin after job (only on success) --- src/libslic3r/Point.hpp | 8 +++ src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 65 +++++++++++--------- src/slic3r/GUI/Jobs/EmbossJob.cpp | 81 ++++++++++++++++--------- src/slic3r/Utils/RaycastManager.cpp | 25 ++------ src/slic3r/Utils/RaycastManager.hpp | 5 +- 5 files changed, 105 insertions(+), 79 deletions(-) diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 32dcb82d05..7ddbcb20bf 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -136,6 +136,14 @@ inline std::string to_string(const Vec3d &pt) { return std::string("[") + floa std::vector transform(const std::vector& points, const Transform3f& t); Pointf3s transform(const Pointf3s& points, const Transform3d& t); +/// +/// Check whether transformation matrix contains odd number of mirroring. +/// NOTE: In code is sometime function named is_left_handed +/// +/// Transformation to check +/// Is positive determinant +inline bool has_reflection(const Transform3d &transform) { return transform.matrix().determinant() < 0; } + template using Vec = Eigen::Matrix; class Point : public Vec2crd diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index ecbfd5a8c0..11c1db2672 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -1120,6 +1120,17 @@ static inline void execute_job(std::shared_ptr j) }); } +namespace priv { +/// +/// Calculate translation of text volume onto surface of model +/// +/// Text +/// AABB trees of object. Actualize object containing text +/// Transformation of actual instance +/// Offset of volume in volume coordinate +std::optional calc_surface_offset(const ModelVolume &volume, RaycastManager &raycast_manager, const Selection &selection); +} // namespace priv + bool GLGizmoEmboss::process() { // no volume is selected -> selection from right panel @@ -1160,6 +1171,13 @@ bool GLGizmoEmboss::process() if (fix_3mf.has_value()) text_tr = text_tr * fix_3mf->inverse(); + // when it is new applying of use surface than move origin onto surfaca + if (!m_volume->text_configuration->style.prop.use_surface) { + auto offset = priv::calc_surface_offset(*m_volume, m_raycast_manager, m_parent.get_selection()); + if (offset.has_value()) + text_tr *= Eigen::Translation(*offset); + } + bool is_outside = m_volume->is_model_part(); // check that there is not unexpected volume type assert(is_outside || m_volume->is_negative_volume() || @@ -2231,18 +2249,7 @@ void GLGizmoEmboss::draw_delete_style_button() { } } -namespace priv { -/// -/// Transform origin of Text volume onto surface of model. -/// -/// Text -/// AABB trees of object -/// Transformation of actual instance -/// True when transform otherwise false -bool transform_on_surface(ModelVolume &volume, RaycastManager &raycast_manager, const Selection &selection); -} // namespace priv - - +// FIX IT: it should not change volume position before successfull change void GLGizmoEmboss::fix_transformation(const FontProp &from, const FontProp &to) { @@ -2264,10 +2271,6 @@ void GLGizmoEmboss::fix_transformation(const FontProp &from, float t_move = t_move_opt.has_value() ? *t_move_opt : .0f; do_translate(Vec3d::UnitZ() * (t_move - f_move)); } - - // when start using surface than move volume origin onto surface - if (!from.use_surface && to.use_surface) - priv::transform_on_surface(*m_volume, m_raycast_manager, m_parent.get_selection()); } void GLGizmoEmboss::draw_style_list() { @@ -2887,8 +2890,7 @@ void GLGizmoEmboss::do_rotate(float relative_z_angle) m_parent.do_rotate(snapshot_name); } -bool priv::transform_on_surface(ModelVolume &volume, RaycastManager &raycast_manager, const Selection &selection) -{ +std::optional priv::calc_surface_offset(const ModelVolume &volume, RaycastManager &raycast_manager, const Selection &selection) { // Move object on surface auto cond = RaycastManager::SkipVolume({volume.id().id}); raycast_manager.actualize(volume.get_object(), &cond); @@ -2901,26 +2903,31 @@ bool priv::transform_on_surface(ModelVolume &volume, RaycastManager &raycast_man // ray in direction of text projection(from volume zero to z-dir) std::optional hit_opt = raycast_manager.unproject(point, direction, &cond); + // start point lay on surface could appear slightly behind surface + std::optional hit_opt_opposit = raycast_manager.unproject(point, -direction, &cond); + if (!hit_opt.has_value() || + (hit_opt_opposit.has_value() && hit_opt->squared_distance > hit_opt_opposit->squared_distance)) + hit_opt = hit_opt_opposit; + + // Try to find closest point when no hit object in emboss direction if (!hit_opt.has_value()) hit_opt = raycast_manager.closest(point); + // It should NOT appear. Closest point always exists. if (!hit_opt.has_value()) - return false; - const RaycastManager::Hit &hit = *hit_opt; + return {}; + // It is no neccesary to move with origin by very small value + if (hit_opt->squared_distance < EPSILON) + return {}; + + const RaycastManager::Hit &hit = *hit_opt; Transform3d hit_tr = raycast_manager.get_transformation(hit.tr_key); Vec3d hit_world = hit_tr * hit.position.cast(); Vec3d offset_world = hit_world - point; // vector in world // TIP: It should be close to only z move Vec3d offset_volume = to_world.inverse().linear() * offset_world; - - // when try to use surface on just loaded text from 3mf - auto fix = volume.text_configuration->fix_3mf_tr; - if (fix.has_value()) - offset_volume = fix->linear() * offset_volume; - - volume.set_transformation(volume.get_matrix() * Eigen::Translation(offset_volume)); - return true; + return offset_volume; } void GLGizmoEmboss::draw_advanced() @@ -2975,8 +2982,6 @@ void GLGizmoEmboss::draw_advanced() // there should be minimal embossing depth if (font_prop.emboss < 0.1) font_prop.emboss = 1; - - priv::transform_on_surface(*m_volume, m_raycast_manager, m_parent.get_selection()); } process(); } diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index bdedc53ee2..ce98e972b1 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -63,7 +63,8 @@ static TriangleMesh create_default_mesh(); /// /// New mesh data /// Text configuration, ... -static void update_volume(TriangleMesh &&mesh, const DataUpdate &data); +/// Transformation of volume +static void update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d *tr = nullptr); /// /// Add new volume to object @@ -316,16 +317,8 @@ void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { // doesn't care about exception when process was canceled by user if (canceled) return; if (priv::process(eptr)) return; - - // TODO: Find better way to Not center volume data when add !!! - TriangleMesh mesh = m_result; // Part1: copy - priv::create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.text_tr, m_input); - - // Part2: update volume data - //auto vol = wxGetApp().plater()->model().objects[m_input.object_idx]->volumes.back(); - //UpdateJob::update_volume(vol, std::move(mesh), m_input.text_configuration, m_input.volume_name); } ///////////////// @@ -358,7 +351,11 @@ void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) } if (canceled) return; if (priv::process(eptr)) return; - priv::update_volume(std::move(m_result), m_input); + + // when start using surface it is wanted to move text origin on surface of model + // also when repeteadly move above surface result position should match + Transform3d *tr = &m_input.text_tr; + priv::update_volume(std::move(m_result), m_input, tr); } //////////////////////////// @@ -535,31 +532,37 @@ void UpdateJob::update_volume(ModelVolume *volume, canvas->reload_scene(refresh_immediately); } -void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data) +void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d* tr) { // for sure that some object will be created - if (mesh.its.empty()) - return priv::create_message("Empty mesh can't be created."); + if (mesh.its.empty()) + return create_message("Empty mesh can't be created."); Plater *plater = wxGetApp().plater(); GLCanvas3D *canvas = plater->canvas3D(); // Check emboss gizmo is still open GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) return; + if (manager.get_current_type() != GLGizmosManager::Emboss) + return; std::string snap_name = GUI::format(_L("Text: %1%"), data.text_configuration.text); Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction); ModelVolume *volume = get_volume(plater->model().objects, data.volume_id); + // could appear when user delete edited volume if (volume == nullptr) return; - // apply fix matrix made by store to .3mf - const auto &tc = volume->text_configuration; - assert(tc.has_value()); - if (tc.has_value() && tc->fix_3mf_tr.has_value()) - volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse()); + if (tr) { + volume->set_transformation(*tr); + } else { + // apply fix matrix made by store to .3mf + const auto &tc = volume->text_configuration; + assert(tc.has_value()); + if (tc.has_value() && tc->fix_3mf_tr.has_value()) + volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse()); + } UpdateJob::update_volume(volume, std::move(mesh), data.text_configuration, data.volume_name); } @@ -726,30 +729,48 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 biggest_count = its.vertices.size(); biggest = &s; } - s_to_itss[&s - &sources.front()] = itss.size(); + size_t source_index = &s - &sources.front(); + size_t its_index = itss.size(); + s_to_itss[source_index] = its_index; itss.emplace_back(std::move(its)); } if (itss.empty()) throw JobException(_u8L("There is no volume in projection direction.").c_str()); - Transform3d tr_inv = biggest->tr.inverse(); + Transform3d tr_inv = biggest->tr.inverse(); + Transform3d cut_projection_tr = tr_inv * input2.text_tr; + // Cut surface in reflected system? + bool use_reflection = Slic3r::has_reflection(cut_projection_tr); + if (use_reflection) + cut_projection_tr *= Eigen::Scaling(-1., 1., 1.); + size_t itss_index = s_to_itss[biggest - &sources.front()]; BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); for (const SurfaceVolumeData::ModelSource &s : sources) { - if (&s == biggest) continue; size_t itss_index = s_to_itss[&s - &sources.front()]; if (itss_index == std::numeric_limits::max()) continue; - Transform3d tr = s.tr * tr_inv; + Transform3d tr; + if (&s == biggest) { + if (!use_reflection) + continue; + // add reflection for biggest source + tr = Eigen::Scaling(-1., 1., 1.); + } else { + tr = s.tr * tr_inv; + if (use_reflection) + tr *= Eigen::Scaling(-1., 1., 1.); + } + + bool fix_reflected = true; indexed_triangle_set &its = itss[itss_index]; - its_transform(its, tr); + its_transform(its, tr, fix_reflected); BoundingBoxf3 bb = bounding_box(its); mesh_bb.merge(bb); } // tr_inv = transformation of mesh inverted - Transform3d cut_projection_tr = tr_inv * input2.text_tr; - Transform3d emboss_tr = cut_projection_tr.inverse(); - BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); + Transform3d emboss_tr = cut_projection_tr.inverse(); + BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension); @@ -761,9 +782,13 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 // !! Projection needs to transform cut OrthoProject3d projection = create_emboss_projection(input2.is_outside, fp.emboss, emboss_tr, cut); - indexed_triangle_set new_its = cut2model(cut, projection); assert(!new_its.empty()); + if (use_reflection) { + // when cut was made in reflected system it must be converted back + Transform3d tr(Eigen::Scaling(-1., 1., 1.)); + its_transform(new_its, tr, true); + } if (was_canceled()) return {}; return TriangleMesh(std::move(new_its)); diff --git a/src/slic3r/Utils/RaycastManager.cpp b/src/slic3r/Utils/RaycastManager.cpp index afaf053a82..4667be3e9c 100644 --- a/src/slic3r/Utils/RaycastManager.cpp +++ b/src/slic3r/Utils/RaycastManager.cpp @@ -73,23 +73,10 @@ void RaycastManager::actualize(const ModelObject *object, const ISkip *skip) m_transformations.erase(m_transformations.begin() + i); } -namespace priv { -struct HitWithDistance : public RaycastManager::Hit -{ - double squared_distance; - HitWithDistance(double squared_distance, - const Hit::Key &key, - const SurfacePoint &surface_point) - : Hit(key, surface_point.position, surface_point.normal) - , squared_distance(squared_distance) - {} -}; -} - std::optional RaycastManager::unproject( const Vec2d &mouse_pos, const Camera &camera, const ISkip *skip) const { - std::optional closest; + std::optional closest; for (const auto &item : m_transformations) { const TrKey &key = item.first; size_t volume_id = key.second; @@ -112,7 +99,7 @@ std::optional RaycastManager::unproject( if (closest.has_value() && closest->squared_distance < squared_distance) continue; - closest = priv::HitWithDistance(squared_distance, key, surface_point); + closest = Hit(key, surface_point, squared_distance); } //if (!closest.has_value()) return {}; @@ -121,7 +108,7 @@ std::optional RaycastManager::unproject( std::optional RaycastManager::unproject(const Vec3d &point, const Vec3d &direction, const ISkip *skip) const { - std::optional closest; + std::optional closest; for (const auto &item : m_transformations) { const TrKey &key = item.first; size_t volume_id = key.second; @@ -144,14 +131,14 @@ std::optional RaycastManager::unproject(const Vec3d &point, closest->squared_distance < squared_distance) continue; SurfacePoint surface_point(hit.position().cast(), hit.normal().cast()); - closest = priv::HitWithDistance(squared_distance, key, surface_point); + closest = Hit(key, surface_point, squared_distance); } } return closest; } std::optional RaycastManager::closest(const Vec3d &point, const ISkip *skip) const { - std::optional closest; + std::optional closest; for (const auto &item : m_transformations) { const TrKey &key = item.first; size_t volume_id = key.second; @@ -172,7 +159,7 @@ std::optional RaycastManager::closest(const Vec3d &point, c if (closest.has_value() && closest->squared_distance < squared_distance) continue; SurfacePoint surface_point(p,n); - closest = priv::HitWithDistance(squared_distance, key, surface_point); + closest = Hit(key, surface_point, squared_distance); } return closest; } diff --git a/src/slic3r/Utils/RaycastManager.hpp b/src/slic3r/Utils/RaycastManager.hpp index e1e72e8459..a29977150c 100644 --- a/src/slic3r/Utils/RaycastManager.hpp +++ b/src/slic3r/Utils/RaycastManager.hpp @@ -67,8 +67,9 @@ public: { using Key = TrKey; Key tr_key; - Hit(Key tr_key, Vec3f position, Vec3f normal) - : SurfacePoint(position, normal), tr_key(tr_key) + double squared_distance; + Hit(const Key& tr_key, const SurfacePoint& surface_point, double squared_distance) + : SurfacePoint(surface_point), tr_key(tr_key), squared_distance(squared_distance) {} }; From bf0cef30e0f30831142a0ce77b6f2f7dfd2024ef Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 4 Jan 2023 15:56:41 +0100 Subject: [PATCH 02/20] Fixed build when tech ENABLE_WORLD_COORDINATE is disabled --- src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index ecbfd5a8c0..5db01ff872 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -274,8 +274,12 @@ void GLGizmoEmboss::create_volume(ModelVolumeType volume_type) Vec3d offset(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()); // offset += 0.5 * mesh_bb.size(); // No size of text volume at this position offset -= vol->get_instance_offset(); +#if ENABLE_WORLD_COORDINATE Transform3d tr = vol->get_instance_transformation().get_matrix_no_offset().inverse(); - Vec3d offset_tr = tr * offset; +#else + Transform3d tr = vol->get_instance_transformation().get_matrix(true).inverse(); +#endif // ENABLE_WORLD_COORDINATE + Vec3d offset_tr = tr * offset; Transform3d volume_trmat = tr.translate(offset_tr); priv::start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); } @@ -531,7 +535,7 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) Transform3d volume_trmat = gl_volume->get_instance_transformation().get_matrix().inverse() * *m_temp_transformation; - gl_volume->set_volume_transformation(volume_trmat); + gl_volume->set_volume_transformation(Geometry::Transformation(volume_trmat)); m_parent.toggle_model_objects_visibility(true); // Apply temporary position m_temp_transformation = {}; @@ -1277,7 +1281,7 @@ bool priv::apply_camera_dir(const Camera &camera, GLCanvas3D &canvas) { vol_rot * Eigen::Translation(offset_inv); //Transform3d res = vol_tr * vol_rot; - vol->set_volume_transformation(res); + vol->set_volume_transformation(Geometry::Transformation(res)); priv::get_model_volume(vol, sel.get_model()->objects)->set_transformation(res); return true; } From bfb60875fa8c58c6609a22dca04c198d7c6e8289 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 4 Jan 2023 17:39:45 +0100 Subject: [PATCH 03/20] CutGizmo: Allow to cut by line when only one cut part is selected. + Updated splashscreen --- resources/icons/splashscreen.jpg | Bin 98793 -> 145267 bytes src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/icons/splashscreen.jpg b/resources/icons/splashscreen.jpg index e42888c5c7ec39911f954f3d19dea1e2a3cf15fb..1019cdf2beda9b1ffd4f9846b23430a53b381468 100644 GIT binary patch delta 140094 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

7`p`0< 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�)1>~&g2*A=t@mSX67{W0ONe7Y4Qd4E6sYyUU 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 z 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 delta 93199 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 Z$lvnz_CaOX7(Nrj9gm5eKk@I!{{zI!0jB@} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 5f000bb034..3e5822cfd3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -2323,13 +2323,16 @@ void GLGizmoCut3D::process_selection_rectangle(CutConnectors &connectors) bool GLGizmoCut3D::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) { - if (is_dragging() || m_connector_mode == CutConnectorMode::Auto || (!m_keep_upper || !m_keep_lower)) + if (is_dragging() || m_connector_mode == CutConnectorMode::Auto) return false; if ( m_hover_id < 0 && shift_down && ! m_connectors_editing && (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Moving) ) return process_cut_line(action, mouse_position); + if (!m_keep_upper || !m_keep_lower) + return false; + if (!m_connectors_editing) { if (0 && action == SLAGizmoEventType::LeftDown) { // disable / enable current contour From be73967ac0b7fb76df71672e35483a39c698d4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Miku=C5=A1?= Date: Thu, 5 Jan 2023 10:18:51 +0100 Subject: [PATCH 04/20] Fix wrong inequality sign in surface.bridge_angle check --- src/libslic3r/SupportMaterial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index d46094f7fb..9965c499f7 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -1322,7 +1322,7 @@ namespace SupportMaterialInternal { // remove the entire bridges and only support the unsupported edges //FIXME the brided regions are already collected as layerm.bridged. Use it? for (const Surface &surface : layerm.fill_surfaces()) - if (surface.surface_type == stBottomBridge && surface.bridge_angle < 0.0) + if (surface.surface_type == stBottomBridge && surface.bridge_angle >= 0.0) polygons_append(bridges, surface.expolygon); //FIXME add the gap filled areas. Extrude the gaps with a bridge flow? // Remove the unsupported ends of the bridges from the bridged areas. From 86f04ac15923ea02d87a822c5f05d648f375ca88 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 5 Jan 2023 10:42:49 +0100 Subject: [PATCH 05/20] Fix for #9144 - Project not marked as changed (asterisk in window title) --- src/slic3r/GUI/ProjectDirtyStateManager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp index d30e5d5109..af35d8de78 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -42,7 +42,11 @@ void ProjectDirtyStateManager::update_from_presets() void ProjectDirtyStateManager::update_from_preview() { - m_custom_gcode_per_print_z_dirty = m_initial_custom_gcode_per_print_z != wxGetApp().model().custom_gcode_per_print_z; + const bool is_dirty = m_initial_custom_gcode_per_print_z != wxGetApp().model().custom_gcode_per_print_z; + if (m_custom_gcode_per_print_z_dirty != is_dirty) { + m_custom_gcode_per_print_z_dirty = is_dirty; + wxGetApp().mainframe->update_title(); + } } void ProjectDirtyStateManager::reset_after_save() From 2ede6686768aadf412e8b44c721b179ebd1e8cec Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Jan 2023 11:52:10 +0100 Subject: [PATCH 06/20] Added debug imgui window to show the content of selected volumes' matrices --- src/libslic3r/Technologies.hpp | 2 + src/slic3r/GUI/GLCanvas3D.cpp | 4 ++ src/slic3r/GUI/Selection.cpp | 100 +++++++++++++++++++++++++++++++++ src/slic3r/GUI/Selection.hpp | 4 ++ 4 files changed, 110 insertions(+) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 87406d724d..920a101fa4 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -55,6 +55,8 @@ #define ENABLE_RELOAD_FROM_DISK_REWORK (1 && ENABLE_2_6_0_ALPHA1) // Enable editing volumes transformation in world coordinates and instances in local coordinates #define ENABLE_WORLD_COORDINATE (1 && ENABLE_2_6_0_ALPHA1) +// Shows an imgui dialog containing the matrices of the selected volumes +#define ENABLE_WORLD_COORDINATE_DEBUG (0 && ENABLE_WORLD_COORDINATE) // Enable alternative version of file_wildcards() #define ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR (1 && ENABLE_2_6_0_ALPHA1) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 088451b79f..4530350249 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5653,6 +5653,10 @@ void GLCanvas3D::_render_selection() if (!m_gizmos.is_running()) m_selection.render(scale_factor); + +#if ENABLE_WORLD_COORDINATE_DEBUG + m_selection.render_debug_window(); +#endif // ENABLE_WORLD_COORDINATE_DEBUG } void GLCanvas3D::_render_sequential_clearance() diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index d7b1297827..f70963485b 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -2666,6 +2666,106 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GL glsafe(::glDisable(GL_BLEND)); } +#if ENABLE_WORLD_COORDINATE_DEBUG +void Selection::render_debug_window() const +{ + if (m_list.empty()) + return; + + if (get_first_volume()->is_wipe_tower) + return; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Selection matrices"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + auto volume_name = [this](size_t id) { + const GLVolume& v = *(*m_volumes)[id]; + return m_model->objects[v.object_idx()]->volumes[v.volume_idx()]->name; + }; + + static size_t current_cmb_idx = 0; + static size_t current_vol_idx = *m_list.begin(); + + if (m_list.find(current_vol_idx) == m_list.end()) + current_vol_idx = *m_list.begin(); + + if (ImGui::BeginCombo("Volumes", volume_name(current_vol_idx).c_str())) { + size_t count = 0; + for (unsigned int id : m_list) { + const GLVolume& v = *(*m_volumes)[id]; + const bool is_selected = (current_cmb_idx == count); + if (ImGui::Selectable(volume_name(id).c_str(), is_selected)) { + current_cmb_idx = count; + current_vol_idx = id; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + ++count; + } + ImGui::EndCombo(); + } + + const std::vector methods = { "computeRotationScaling", "computeScalingRotation" }; + static size_t current_method_idx = 0; + if (ImGui::BeginCombo("Decomposition method", methods[current_method_idx].c_str())) { + size_t count = 0; + for (const std::string& method : methods) { + const bool is_selected = (current_method_idx == count); + if (ImGui::Selectable(method.c_str(), is_selected)) + current_method_idx = count; + if (is_selected) + ImGui::SetItemDefaultFocus(); + ++count; + } + ImGui::EndCombo(); + } + + const GLVolume& v = *get_volume(current_vol_idx); + + auto add_matrix = [&imgui](const std::string& name, const Transform3d& m, unsigned int size) { + ImGui::BeginGroup(); + imgui.text(name); + if (ImGui::BeginTable(name.c_str(), size, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInner)) { + for (unsigned int r = 0; r < size; ++r) { + ImGui::TableNextRow(); + for (unsigned int c = 0; c < size; ++c) { + ImGui::TableSetColumnIndex(c); + imgui.text(std::to_string(m(r, c))); + } + } + ImGui::EndTable(); + } + ImGui::EndGroup(); + }; + + auto add_matrices_set = [add_matrix](const std::string& name, const Transform3d& m, size_t method) { + static unsigned int counter = 0; + ++counter; + if (ImGui::CollapsingHeader(name.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + add_matrix("Full", m, 4); + + Matrix3d rotation; + Matrix3d scale; + if (method == 0) + m.computeRotationScaling(&rotation, &scale); + else + m.computeScalingRotation(&scale, &rotation); + + ImGui::SameLine(); + add_matrix("Rotation component", Transform3d(rotation), 3); + ImGui::SameLine(); + add_matrix("Scale component", Transform3d(scale), 3); + } + }; + + add_matrices_set("World", v.world_matrix(), current_method_idx); + add_matrices_set("Instance", v.get_instance_transformation().get_matrix(), current_method_idx); + add_matrices_set("Volume", v.get_volume_transformation().get_matrix(), current_method_idx); + + imgui.end(); +} +#endif // ENABLE_WORLD_COORDINATE_DEBUG + #ifndef NDEBUG static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) { diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index 029a70a252..d2c483c6e6 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -446,6 +446,10 @@ public: // returns the list of idxs of the volumes contained in the given list but not in the selection std::vector get_unselected_volume_idxs_from(const std::vector& volume_idxs) const; +#if ENABLE_WORLD_COORDINATE_DEBUG + void render_debug_window() const; +#endif // ENABLE_WORLD_COORDINATE_DEBUG + private: void update_valid(); void update_type(); From b08263ffbd1d9fd7704f7d84291f52267c67a44a Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Thu, 5 Jan 2023 12:04:03 +0100 Subject: [PATCH 07/20] Fix translation --- src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index 11c1db2672..dca86d537c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -2364,7 +2364,7 @@ void GLGizmoEmboss::draw_style_list() { if (selected_style_index.has_value() && is_modified) { wxString title = _L("Style modification will be lost."); const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; - wxString message = GUI::format_wxstr(_L("Changing style to '%1%' will discard actual style modification.\n\n Would you like to continue anyway?"), style.name); + wxString message = GUI::format_wxstr(_L("Changing style to '%1%' will discard current style modification.\n\n Would you like to continue anyway?"), style.name); MessageDialog not_loaded_style_message(nullptr, message, title, wxICON_WARNING | wxYES|wxNO); if (not_loaded_style_message.ShowModal() != wxID_YES) selected_style_index.reset(); From c01453c2c7ebfa756486db5692fd38816ba9d711 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Thu, 5 Jan 2023 12:04:26 +0100 Subject: [PATCH 08/20] Fix dragging of mirrored object --- src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index dca86d537c..06ba0619f0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -605,12 +605,20 @@ void GLGizmoEmboss::on_render() { glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); } + bool is_left_handed = has_reflection(*m_temp_transformation); + if (is_left_handed) + glsafe(::glFrontFace(GL_CW)); + glsafe(::glEnable(GL_DEPTH_TEST)); gl_volume.model.set_color(color); gl_volume.model.render(); glsafe(::glDisable(GL_DEPTH_TEST)); - if (is_transparent) glsafe(::glDisable(GL_BLEND)); + // set it back to pevious state + if (is_left_handed) + glsafe(::glFrontFace(GL_CCW)); + if (is_transparent) + glsafe(::glDisable(GL_BLEND)); shader->stop_using(); } From d883c5e6688ae864ba37584aef1bce287d87b345 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Jan 2023 12:32:54 +0100 Subject: [PATCH 09/20] Follow-up of 2ede6686768aadf412e8b44c721b179ebd1e8cec - Tweaks to imgui dialog --- src/slic3r/GUI/Selection.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index f70963485b..2e92c8b6b0 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -2676,7 +2676,7 @@ void Selection::render_debug_window() const return; ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Selection matrices"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + imgui.begin(std::string("Selection matrices"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize); auto volume_name = [this](size_t id) { const GLVolume& v = *(*m_volumes)[id]; @@ -2705,20 +2705,8 @@ void Selection::render_debug_window() const ImGui::EndCombo(); } - const std::vector methods = { "computeRotationScaling", "computeScalingRotation" }; - static size_t current_method_idx = 0; - if (ImGui::BeginCombo("Decomposition method", methods[current_method_idx].c_str())) { - size_t count = 0; - for (const std::string& method : methods) { - const bool is_selected = (current_method_idx == count); - if (ImGui::Selectable(method.c_str(), is_selected)) - current_method_idx = count; - if (is_selected) - ImGui::SetItemDefaultFocus(); - ++count; - } - ImGui::EndCombo(); - } + static int current_method_idx = 0; + ImGui::Combo("Decomposition method", ¤t_method_idx, "computeRotationScaling\0computeScalingRotation\0"); const GLVolume& v = *get_volume(current_vol_idx); From e70c4849ba2e9e6e56f5299c10a92f502d44e71b Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 5 Jan 2023 12:33:19 +0100 Subject: [PATCH 10/20] Removed unused code --- src/slic3r/GUI/GUI_ObjectManipulation.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index a0336c6853..0ab4f166b0 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -931,9 +931,6 @@ void ObjectManipulation::update_reset_buttons_visibility() if (selection.is_single_full_instance()) { #if ENABLE_WORLD_COORDINATE - const Geometry::Transformation& trafo = volume->get_instance_transformation(); - rotation = trafo.get_rotation_matrix(); - scale = trafo.get_scaling_factor_matrix(); const Selection::IndicesList& idxs = selection.get_volume_idxs(); for (unsigned int id : idxs) { const Geometry::Transformation world_trafo(selection.get_volume(id)->world_matrix()); From ce38e57ec4c831c1966de602bdc31d2677c0d31c Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 5 Jan 2023 15:00:37 +0100 Subject: [PATCH 11/20] Downloader feature - Downloads from Printables.com Custom URL Registration: - Windows - writes to registers. - Linux - desktop integration file. - Macos - info.plist.in creates registration and is controlled only via app config. Registration is first made in Config Wizard. Or is triggered from Preferences. Path to downloads folder can be set. URL link starts new instance of PS which sends data to running instance via SingleInstance structures if exists. New progress notification is introduced with pause and stop buttons. Downloader writes downloaded data by chunks. Support for zip files is introduced. Zip files can be opened, downloaded or drag'n'droped in PS. Archive dialog is opened. Then if more than 1 project is selected, only geometry is loaded. Opening of 3mf project now supports openning project in new PS instance. --- resources/icons/notification_open.svg | 63 + resources/icons/notification_open_hover.svg | 64 + resources/icons/notification_pause.svg | 75 + resources/icons/notification_pause_hover.svg | 75 + resources/icons/notification_play.svg | 75 + resources/icons/notification_play_hover.svg | 75 + src/PrusaSlicer.cpp | 16 +- src/imgui/imconfig.h | 8 +- src/libslic3r/PrintConfig.cpp | 4 + src/platform/osx/Info.plist.in | 11 + src/slic3r/CMakeLists.txt | 6 + src/slic3r/GUI/ConfigWizard.cpp | 6483 +++++++++-------- src/slic3r/GUI/ConfigWizard_private.hpp | 55 +- src/slic3r/GUI/DesktopIntegrationDialog.cpp | 154 +- src/slic3r/GUI/DesktopIntegrationDialog.hpp | 7 +- src/slic3r/GUI/Downloader.cpp | 245 + src/slic3r/GUI/Downloader.hpp | 99 + src/slic3r/GUI/DownloaderFileGet.cpp | 322 + src/slic3r/GUI/DownloaderFileGet.hpp | 44 + src/slic3r/GUI/FileArchiveDialog.cpp | 363 + src/slic3r/GUI/FileArchiveDialog.hpp | 118 + src/slic3r/GUI/GUI_App.cpp | 6721 +++++++++--------- src/slic3r/GUI/GUI_App.hpp | 9 + src/slic3r/GUI/GUI_Init.hpp | 3 + src/slic3r/GUI/ImGuiWrapper.cpp | 7 +- src/slic3r/GUI/InstanceCheck.cpp | 20 + src/slic3r/GUI/InstanceCheck.hpp | 3 +- src/slic3r/GUI/NotificationManager.cpp | 334 +- src/slic3r/GUI/NotificationManager.hpp | 68 +- src/slic3r/GUI/Plater.cpp | 487 +- src/slic3r/GUI/Plater.hpp | 6 +- src/slic3r/GUI/Preferences.cpp | 113 +- src/slic3r/GUI/Preferences.hpp | 10 +- src/slic3r/Utils/Http.cpp | 16 +- src/slic3r/Utils/Http.hpp | 7 +- src/slic3r/Utils/Process.cpp | 23 +- src/slic3r/Utils/Process.hpp | 4 +- 37 files changed, 9616 insertions(+), 6577 deletions(-) create mode 100644 resources/icons/notification_open.svg create mode 100644 resources/icons/notification_open_hover.svg create mode 100644 resources/icons/notification_pause.svg create mode 100644 resources/icons/notification_pause_hover.svg create mode 100644 resources/icons/notification_play.svg create mode 100644 resources/icons/notification_play_hover.svg create mode 100644 src/slic3r/GUI/Downloader.cpp create mode 100644 src/slic3r/GUI/Downloader.hpp create mode 100644 src/slic3r/GUI/DownloaderFileGet.cpp create mode 100644 src/slic3r/GUI/DownloaderFileGet.hpp create mode 100644 src/slic3r/GUI/FileArchiveDialog.cpp create mode 100644 src/slic3r/GUI/FileArchiveDialog.hpp diff --git a/resources/icons/notification_open.svg b/resources/icons/notification_open.svg new file mode 100644 index 0000000000..a83138a887 --- /dev/null +++ b/resources/icons/notification_open.svg @@ -0,0 +1,63 @@ + +image/svg+xml + + + + + + diff --git a/resources/icons/notification_open_hover.svg b/resources/icons/notification_open_hover.svg new file mode 100644 index 0000000000..7747280886 --- /dev/null +++ b/resources/icons/notification_open_hover.svg @@ -0,0 +1,64 @@ + +image/svg+xml + + + + + + diff --git a/resources/icons/notification_pause.svg b/resources/icons/notification_pause.svg new file mode 100644 index 0000000000..dc0d613116 --- /dev/null +++ b/resources/icons/notification_pause.svg @@ -0,0 +1,75 @@ + +image/svg+xml + + + + + + + diff --git a/resources/icons/notification_pause_hover.svg b/resources/icons/notification_pause_hover.svg new file mode 100644 index 0000000000..6654f3775e --- /dev/null +++ b/resources/icons/notification_pause_hover.svg @@ -0,0 +1,75 @@ + +image/svg+xml + + + + + + + diff --git a/resources/icons/notification_play.svg b/resources/icons/notification_play.svg new file mode 100644 index 0000000000..5aa80cd94f --- /dev/null +++ b/resources/icons/notification_play.svg @@ -0,0 +1,75 @@ + +image/svg+xml + + + + + + + diff --git a/resources/icons/notification_play_hover.svg b/resources/icons/notification_play_hover.svg new file mode 100644 index 0000000000..f0d07fc123 --- /dev/null +++ b/resources/icons/notification_play_hover.svg @@ -0,0 +1,75 @@ + +image/svg+xml + + + + + + + diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 918d2ca3c2..1ee4b0d347 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -113,6 +113,9 @@ int CLI::run(int argc, char **argv) std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() && std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() && std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end(); + bool start_downloader = false; + bool delete_after_load = false; + std::string download_url; bool start_as_gcodeviewer = #ifdef _WIN32 false; @@ -221,6 +224,11 @@ int CLI::run(int argc, char **argv) } if (!start_as_gcodeviewer) { for (const std::string& file : m_input_files) { + if (boost::starts_with(file, "prusaslicer://")) { + start_downloader = true; + download_url = file; + continue; + } if (!boost::filesystem::exists(file)) { boost::nowide::cerr << "No such file: " << file << std::endl; exit(1); @@ -478,6 +486,9 @@ int CLI::run(int argc, char **argv) // Models are repaired by default. //for (auto &model : m_models) // model.repair(); + + } else if (opt_key == "delete-after-load") { + delete_after_load = true; } else { boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; return 1; @@ -663,9 +674,12 @@ int CLI::run(int argc, char **argv) params.extra_config = std::move(m_extra_config); params.input_files = std::move(m_input_files); params.start_as_gcodeviewer = start_as_gcodeviewer; + params.start_downloader = start_downloader; + params.download_url = download_url; + params.delete_after_load = delete_after_load; #if ENABLE_GL_CORE_PROFILE - params.opengl_version = opengl_version; #if ENABLE_OPENGL_DEBUG_OPTION + params.opengl_version = opengl_version; params.opengl_debug = opengl_debug; #endif // ENABLE_OPENGL_DEBUG_OPTION #endif // ENABLE_GL_CORE_PROFILE diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index 6dabace5b5..651df6183c 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -158,6 +158,12 @@ namespace ImGui const wchar_t SliderFloatEditBtnIcon = 0x2604; const wchar_t SliderFloatEditBtnPressedIcon = 0x2605; const wchar_t ClipboardBtnIcon = 0x2606; + const wchar_t PlayButton = 0x2618; + const wchar_t PlayHoverButton = 0x2619; + const wchar_t PauseButton = 0x261A; + const wchar_t PauseHoverButton = 0x261B; + const wchar_t OpenButton = 0x261C; + const wchar_t OpenHoverButton = 0x261D; const wchar_t LegendTravel = 0x2701; const wchar_t LegendWipe = 0x2702; @@ -173,8 +179,8 @@ namespace ImGui const wchar_t LegendToolMarker = 0x2712; const wchar_t WarningMarkerSmall = 0x2713; const wchar_t ExpandBtn = 0x2714; - const wchar_t CollapseBtn = 0x2715; const wchar_t InfoMarkerSmall = 0x2716; + const wchar_t CollapseBtn = 0x2715; // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index fd752873d3..af2ce3b66d 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4632,6 +4632,10 @@ CLITransformConfigDef::CLITransformConfigDef() def->label = L("Scale to Fit"); def->tooltip = L("Scale to fit the given volume."); def->set_default_value(new ConfigOptionPoint3(Vec3d(0,0,0))); + + def = this->add("delete-after-load", coString); + def->label = L("Delete files after loading"); + def->tooltip = L("Delete files after loading."); } CLIMiscConfigDef::CLIMiscConfigDef() diff --git a/src/platform/osx/Info.plist.in b/src/platform/osx/Info.plist.in index d3c9136049..60f0920167 100644 --- a/src/platform/osx/Info.plist.in +++ b/src/platform/osx/Info.plist.in @@ -110,6 +110,17 @@ Alternate + CFBundleURLTypes + + + CFBundleURLName + PrusaSlicer Downloads + CFBundleURLSchemes + + prusaslicer + + + LSMinimumSystemVersion 10.12 NSPrincipalClass diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 4fa51c4868..236aedf66f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -232,6 +232,12 @@ set(SLIC3R_GUI_SOURCES GUI/DesktopIntegrationDialog.hpp GUI/HintNotification.cpp GUI/HintNotification.hpp + GUI/FileArchiveDialog.cpp + GUI/FileArchiveDialog.hpp + GUI/Downloader.cpp + GUI/Downloader.hpp + GUI/DownloaderFileGet.cpp + GUI/DownloaderFileGet.hpp Utils/AppUpdater.cpp Utils/AppUpdater.hpp Utils/Http.cpp diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 204067a951..ef6f53bf08 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1,3123 +1,3360 @@ -// FIXME: extract absolute units -> em - -#include "ConfigWizard_private.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE - -#include "libslic3r/Platform.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Config.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Model.hpp" -#include "libslic3r/Color.hpp" -#include "GUI.hpp" -#include "GUI_App.hpp" -#include "GUI_Utils.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Field.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" -#include "format.hpp" -#include "MsgDialog.hpp" -#include "UnsavedChangesDialog.hpp" - -#if defined(__linux__) && defined(__WXGTK3__) -#define wxLinux_gtk3 true -#else -#define wxLinux_gtk3 false -#endif //defined(__linux__) && defined(__WXGTK3__) - -namespace Slic3r { -namespace GUI { - - -using Config::Snapshot; -using Config::SnapshotDB; - - -// Configuration data structures extensions needed for the wizard - -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) -{ - this->preset_bundle = std::make_unique(); - this->is_in_resources = ais_in_resources; - this->is_prusa_bundle = ais_prusa_bundle; - - std::string path_string = source_path.string(); - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( - path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); - auto first_vendor = preset_bundle->vendors.begin(); - if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; - return false; - } - if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; - return false; - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; - this->vendor_profile = &first_vendor->second; - return true; -} - -Bundle::Bundle(Bundle &&other) - : preset_bundle(std::move(other.preset_bundle)) - , vendor_profile(other.vendor_profile) - , is_in_resources(other.is_in_resources) - , is_prusa_bundle(other.is_prusa_bundle) -{ - other.vendor_profile = nullptr; -} - -BundleMap BundleMap::load() -{ - BundleMap res; - - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); - const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - auto prusa_bundle_rsrc = false; - if (! boost::filesystem::exists(prusa_bundle_path)) { - prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - prusa_bundle_rsrc = true; - } - { - Bundle prusa_bundle; - if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) - res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); - } - - // Load the other bundles in the datadir/vendor directory - // and then additionally from resources/profiles. - bool is_in_resources = false; - for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { - for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part - - // Don't load this bundle if we've already loaded it. - if (res.find(id) != res.end()) { continue; } - - Bundle bundle; - if (bundle.load(dir_entry.path(), is_in_resources)) - res.emplace(std::move(id), std::move(bundle)); - } - } - - is_in_resources = true; - } - - return res; -} - -Bundle& BundleMap::prusa_bundle() -{ - auto it = find(PresetBundle::PRUSA_BUNDLE); - if (it == end()) { - throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); - } - - return it->second; -} - -const Bundle& BundleMap::prusa_bundle() const -{ - return const_cast(this)->prusa_bundle(); -} - - -// Printer model picker GUI control - -struct PrinterPickerEvent : public wxEvent -{ - std::string vendor_id; - std::string model_id; - std::string variant_name; - bool enable; - - PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) - : wxEvent(winid, eventType) - , vendor_id(std::move(vendor_id)) - , model_id(std::move(model_id)) - , variant_name(std::move(variant_name)) - , enable(enable) - {} - - virtual wxEvent *Clone() const - { - return new PrinterPickerEvent(*this); - } -}; - -wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); - -const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) - : wxPanel(parent) - , vendor_id(vendor.id) - , width(0) -{ - wxGetApp().UpdateDarkUI(this); - const auto &models = vendor.models; - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - const auto font_title = GetFont().MakeBold().Scaled(1.3f); - const auto font_name = GetFont().MakeBold(); - const auto font_alt_nozzle = GetFont().Scaled(0.9f); - - // wxGrid appends widgets by rows, but we need to construct them in columns. - // These vectors are used to hold the elements so that they can be appended in the right order. - std::vector titles; - std::vector bitmaps; - std::vector variants_panels; - - int max_row_width = 0; - int current_row_width = 0; - - bool is_variants = false; - - for (const auto &model : models) { - if (! filter(model)) { continue; } - - wxBitmap bitmap; - int bitmap_width = 0; - auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { - if (wxFileExists(bitmap_file)) { - bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); - bitmap_width = bitmap.GetWidth(); - return true; - } - return false; - }; - if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") - % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") - % vendor.id - % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); - } - } - auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(font_name); - const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); - title->Wrap(wrap_width); - - current_row_width += wrap_width; - if (titles.size() % max_cols == max_cols - 1) { - max_row_width = std::max(max_row_width, current_row_width); - current_row_width = 0; - } - - titles.push_back(title); - - auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); - bitmaps.push_back(bitmap_widget); - - auto *variants_panel = new wxPanel(this); - wxGetApp().UpdateDarkUI(variants_panel); - auto *variants_sizer = new wxBoxSizer(wxVERTICAL); - variants_panel->SetSizer(variants_sizer); - const auto model_id = model.id; - - for (size_t i = 0; i < model.variants.size(); i++) { - const auto &variant = model.variants[i]; - - const auto label = model.technology == ptFFF - ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) - : from_u8(model.name); - - if (i == 1) { - auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); - alt_label->SetFont(font_alt_nozzle); - variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); - is_variants = true; - } - - auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); - i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - - const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); - cbox->SetValue(enabled); - - variants_sizer->Add(cbox, 0, wxBOTTOM, 3); - - cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { - on_checkbox(cbox, event.IsChecked()); - }); - } - - variants_panels.push_back(variants_panel); - } - - width = std::max(max_row_width, current_row_width); - - const size_t cols = std::min(max_cols, titles.size()); - - auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); - - if (titles.size() > 0) { - const size_t odd_items = titles.size() % cols; - - for (size_t i = 0; i < titles.size() - odd_items; i += cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } - - // Add separator space to multiliners - if (titles.size() > cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } - } - } - if (odd_items > 0) { - const size_t rem = titles.size() - odd_items; - - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } - } - } - - auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); - if (! title.IsEmpty()) { - auto *title_widget = new wxStaticText(this, wxID_ANY, title); - title_widget->SetFont(font_title); - title_sizer->Add(title_widget); - } - title_sizer->AddStretchSpacer(); - - if (titles.size() > 1 || is_variants) { - // It only makes sense to add the All / None buttons if there's multiple printers - // All Standard button is added when there are more variants for at least one printer - auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - if (is_variants) - sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); - sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); - sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); - if (is_variants) - title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(sel_all_std); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - // fill button indexes used later for buttons rescaling - if (is_variants) - m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; - else { - sel_all_std->Destroy(); - m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; - } - } - - sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); - sizer->Add(printer_grid); - - SetSizer(sizer); -} - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) - : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) -{} - -void PrinterPicker::select_all(bool select, bool alternates) -{ - for (const auto &cb : cboxes) { - if (cb->GetValue() != select) { - cb->SetValue(select); - on_checkbox(cb, select); - } - } - - if (! select) { alternates = false; } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue() != alternates) { - cb->SetValue(alternates); - on_checkbox(cb, alternates); - } - } -} - -void PrinterPicker::select_one(size_t i, bool select) -{ - if (i < cboxes.size() && cboxes[i]->GetValue() != select) { - cboxes[i]->SetValue(select); - on_checkbox(cboxes[i], select); - } -} - -bool PrinterPicker::any_selected() const -{ - for (const auto &cb : cboxes) { - if (cb->GetValue()) { return true; } - } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue()) { return true; } - } - - return false; -} - -std::set PrinterPicker::get_selected_models() const -{ - std::set ret_set; - - for (const auto& cb : cboxes) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - for (const auto& cb : cboxes_alt) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - return ret_set; -} - -void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) -{ - PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); - AddPendingEvent(evt); -} - - -// Wizard page base - -ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) - : wxPanel(parent->p->hscroll) - , parent(parent) - , shortname(std::move(shortname)) - , indent(indent) -{ - wxGetApp().UpdateDarkUI(this); - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - const auto font = GetFont().MakeBold().Scaled(1.5); - text->SetFont(font); - sizer->Add(text, 0, wxALIGN_LEFT, 0); - sizer->AddSpacer(10); - - content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1, wxEXPAND); - - SetSizer(sizer); - - // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 - // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages - if (!wxLinux_gtk3) - this->Hide(); - - Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { - this->Layout(); - event.Skip(); - }); -} - -ConfigWizardPage::~ConfigWizardPage() {} - -wxStaticText* ConfigWizardPage::append_text(wxString text) -{ - auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - widget->Wrap(WRAP_WIDTH); - widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); - append(widget); - return widget; -} - -void ConfigWizardPage::append_spacer(int space) -{ - // FIXME: scaling - content->AddSpacer(space); -} - -// Wizard pages - -PageWelcome::PageWelcome(ConfigWizard *parent) - : ConfigWizardPage(parent, from_u8((boost::format( -#ifdef __APPLE__ - _utf8(L("Welcome to the %s Configuration Assistant")) -#else - _utf8(L("Welcome to the %s Configuration Wizard")) -#endif - ) % SLIC3R_APP_NAME).str()), _L("Welcome")) - , welcome_text(append_text(from_u8((boost::format( - _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) - % SLIC3R_APP_NAME - % _utf8(ConfigWizard::name())).str()) - )) - , cbox_reset(append( - new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) - )) - , cbox_integrate(append( - new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) - )) -{ - welcome_text->Hide(); - cbox_reset->Hide(); - cbox_integrate->Hide(); -} - -void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) -{ - const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; - welcome_text->Show(data_empty); - cbox_reset->Show(!data_empty); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - if (!DesktopIntegrationDialog::is_integrated()) - cbox_integrate->Show(true); - else - cbox_integrate->Hide(); -#else - cbox_integrate->Hide(); -#endif -} - - -PagePrinters::PagePrinters(ConfigWizard *parent, - wxString title, - wxString shortname, - const VendorProfile &vendor, - unsigned indent, - Technology technology) - : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) - , technology(technology) - , install(false) // only used for 3rd party vendors -{ - enum { - COL_SIZE = 200, - }; - - AppConfig *appconfig = &this->wizard_p()->appconfig_new; - - const auto families = vendor.families(); - for (const auto &family : families) { - const auto filter = [&](const VendorProfile::PrinterModel &model) { - return ((model.technology == ptFFF && technology & T_FFF) - || (model.technology == ptSLA && technology & T_SLA)) - && model.family == family; - }; - - if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { - continue; - } - - const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - - picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { - appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); - wizard_p()->on_printer_pick(this, evt); - }); - - append(new StaticLine(this)); - - append(picker); - printer_pickers.push_back(picker); - has_printers = true; - } - -} - -void PagePrinters::select_all(bool select, bool alternates) -{ - for (auto picker : printer_pickers) { - picker->select_all(select, alternates); - } -} - -int PagePrinters::get_width() const -{ - return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, - [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); -} - -bool PagePrinters::any_selected() const -{ - for (const auto *picker : printer_pickers) { - if (picker->any_selected()) { return true; } - } - - return false; -} - -std::set PagePrinters::get_selected_models() -{ - std::set ret_set; - - for (const auto *picker : printer_pickers) - { - std::set tmp_models = picker->get_selected_models(); - ret_set.insert(tmp_models.begin(), tmp_models.end()); - } - - return ret_set; -} - -void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) -{ - if (is_primary_printer_page - && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0 - && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { - printer_pickers[0]->select_one(0, true); - } -} - - -const std::string PageMaterials::EMPTY; - -PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) - : ConfigWizardPage(parent, std::move(title), std::move(shortname)) - , materials(materials) - , list_printer(new StringList(this, wxLB_MULTIPLE)) - , list_type(new StringList(this)) - , list_vendor(new StringList(this)) - , list_profile(new PresetList(this)) -{ - append_spacer(VERTICAL_SPACING); - - const int em = parent->em_unit(); - const int list_h = 30*em; - - - list_printer->SetMinSize(wxSize(23*em, list_h)); - list_type->SetMinSize(wxSize(13*em, list_h)); - list_vendor->SetMinSize(wxSize(13*em, list_h)); - list_profile->SetMinSize(wxSize(23*em, list_h)); - - - - grid = new wxFlexGridSizer(4, em/2, em); - grid->AddGrowableCol(3, 1); - grid->AddGrowableRow(1, 1); - - grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); - grid->Add(new wxStaticText(this, wxID_ANY, list1name)); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); - - grid->Add(list_printer, 0, wxEXPAND); - grid->Add(list_type, 0, wxEXPAND); - grid->Add(list_vendor, 0, wxEXPAND); - grid->Add(list_profile, 1, wxEXPAND); - - auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); - btn_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(list_printer); - wxGetApp().UpdateDarkUI(list_type); - wxGetApp().UpdateDarkUI(list_vendor); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - - append(grid, 1, wxEXPAND); - - append_spacer(VERTICAL_SPACING); - - html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, - wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); - append(html_window, 0, wxEXPAND); - - list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); - }); - list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - - list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); - list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); - - sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); - sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); - /* - Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); - - list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); - list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); - list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); - */ - reload_presets(); - set_compatible_printers_html_window(std::vector(), false); -} -void PageMaterials::on_paint() -{ -} -void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) -{ - const wxClientDC dc(list_profile); - const wxPoint pos = evt.GetLogicalPosition(dc); - int item = list_profile->HitTest(pos); - on_material_hovered(item); -} -void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) -{} -void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) -{ - on_material_hovered(-1); -} -void PageMaterials::reload_presets() -{ - clear(); - - list_printer->append(_L("(All)"), &EMPTY); - //list_printer->SetLabelMarkup("bald"); - for (const Preset* printer : materials->printers) { - list_printer->append(printer->name, &printer->name); - } - sort_list_data(list_printer, true, false); - if (list_printer->GetCount() > 0) { - list_printer->SetSelection(0); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - update_lists(0, 0, 0); - } - - presets_loaded = true; -} - -void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) -{ - const auto bgr_clr = -#if defined(__APPLE__) - html_window->GetParent()->GetBackgroundColour(); -#else -#if defined(_WIN32) - wxGetApp().get_window_default_clr(); -#else - wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); -#endif -#endif - const auto text_clr = wxGetApp().get_label_clr_default(); - const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); - wxString text; - if (all_printers) { - wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "
" - "
" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line - ); - } else { - wxString second_line; - if (!printer_names.empty()) - second_line = (materials->technology == T_FFF ? - _L("Only the following installed printers are compatible with the selected filaments") : - _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line); - for (size_t i = 0; i < printer_names.size(); ++i) - { - text += wxString::Format("", boost::nowide::widen(printer_names[i])); - if (i % 3 == 2) { - text += wxString::Format( - "" - ""); - } - } - text += wxString::Format( - "" - "
%s
" - "
" - "
" - "" - "" - ); - } - - wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); - const int fs = font.GetPointSize(); - int size[] = { fs,fs,fs,fs,fs,fs,fs }; - html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - html_window->SetPage(text); -} - -void PageMaterials::clear_compatible_printers_label() -{ - set_compatible_printers_html_window(std::vector(), false); -} - -void PageMaterials::on_material_hovered(int sel_material) -{ - -} - -void PageMaterials::on_material_highlighted(int sel_material) -{ - if (sel_material == last_hovered_item) - return; - if (sel_material == -1) { - clear_compatible_printers_label(); - return; - } - last_hovered_item = sel_material; - std::vector tabs; - tabs.push_back(std::string()); - tabs.push_back(std::string()); - tabs.push_back(std::string()); - //selected material string - std::string material_name = list_profile->get_data(sel_material); - // get material preset - const std::vector matching_materials = materials->get_presets_by_alias(material_name); - if (matching_materials.empty()) - { - clear_compatible_printers_label(); - return; - } - //find matching printers - std::vector names; - for (const Preset* printer : materials->printers) { - for (const Preset* material : matching_materials) { - if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { - names.push_back(printer->name); - break; - } - } - } - set_compatible_printers_html_window(names, names.size() == materials->printers.size()); -} - -void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - wxArrayInt sel_printers; - int sel_printers_count = list_printer->GetSelections(sel_printers); - - // Does our wxWidgets version support operator== for wxArrayInt ? - // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 -#if wxCHECK_VERSION(3, 1, 1) - if (sel_printers != sel_printers_prev) { -#else - auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { - if (arr_first.GetCount() != arr_second.GetCount()) - return false; - for (size_t i = 0; i < arr_first.GetCount(); i++) - if (arr_first[i] != arr_second[i]) - return false; - return true; - }; - if (!are_equal(sel_printers, sel_printers_prev)) { -#endif - - // Refresh type list - list_type->Clear(); - list_type->append(_L("(All)"), &EMPTY); - if (sel_printers_count > 0) { - // If all is selected with other printers - // unselect "all" or all printers depending on last value - if (sel_printers[0] == 0 && sel_printers_count > 1) { - if (last_selected_printer == 0) { - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - } else { - list_printer->SetSelection(0, false); - sel_printers_count = list_printer->GetSelections(sel_printers); - } - } - if (sel_printers[0] != 0) { - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - } else { - //clear selection except "ALL" - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - sel_printers_count = list_printer->GetSelections(sel_printers); - - materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - sort_list_data(list_type, true, true); - } - - sel_printers_prev = sel_printers; - sel_type = 0; - sel_type_prev = wxNOT_FOUND; - list_type->SetSelection(sel_type); - list_profile->Clear(); - } - - if (sel_type != sel_type_prev) { - // Refresh vendor list - - // XXX: The vendor list is created with quadratic complexity here, - // but the number of vendors is going to be very small this shouldn't be a problem. - - list_vendor->Clear(); - list_vendor->append(_L("(All)"), &EMPTY); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - // find printer preset - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { - const std::string& vendor = this->materials->get_vendor(p); - if (list_vendor->find(vendor) == wxNOT_FOUND) { - list_vendor->append(vendor, &vendor); - } - }); - } - sort_list_data(list_vendor, true, false); - } - - sel_type_prev = sel_type; - sel_vendor = 0; - sel_vendor_prev = wxNOT_FOUND; - list_vendor->SetSelection(sel_vendor); - list_profile->Clear(); - } - - if (sel_vendor != sel_vendor_prev) { - // Refresh material list - - list_profile->Clear(); - clear_compatible_printers_label(); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - const std::string& vendor = list_vendor->get_data(sel_vendor); - // finst printer preset - std::vector to_list; - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - - materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { - const std::string& section = materials->appconfig_section(); - bool checked = wizard_p()->appconfig_new.has(section, p->name); - bool was_checked = false; - - int cur_i = list_profile->find(p->alias); - if (cur_i == wxNOT_FOUND) { - cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); - to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); - } - else { - was_checked = list_profile->IsChecked(cur_i); - to_list[cur_i].checked = checked || was_checked; - } - list_profile->Check(cur_i, checked || was_checked); - - /* Update preset selection in config. - * If one preset from aliases bundle is selected, - * than mark all presets with this aliases as selected - * */ - if (checked && !was_checked) - wizard_p()->update_presets_in_config(section, p->alias, true); - else if (!checked && was_checked) - wizard_p()->appconfig_new.set(section, p->name, "1"); - }); - } - sort_list_data(list_profile, to_list); - } - - sel_vendor_prev = sel_vendor; - } - wxGetApp().UpdateDarkUI(list_profile); -} - -void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) -{ -// get data from list -// sort data -// first should be -// then prusa profiles -// then the rest -// in alphabetical order - - std::vector> prusa_profiles; - std::vector> other_profiles; - for (int i = 0 ; i < list->size(); ++i) { - const std::string& data = list->get_data(i); - if (data == EMPTY) // do not sort item - continue; - if (!material_type_ordering && data.find("Prusa") != std::string::npos) - prusa_profiles.push_back(data); - else - other_profiles.push_back(data); - } - if(material_type_ordering) { - - const ConfigOptionDef* def = print_config_def.get("filament_type"); - std::vectorenum_values = def->enum_values; - size_t end_of_sorted = 0; - for (size_t vals = 0; vals < enum_values.size(); vals++) { - for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) - { - // find instead compare because PET vs PETG - if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { - //swap - if(profs != end_of_sorted) { - std::reference_wrapper aux = other_profiles[end_of_sorted]; - other_profiles[end_of_sorted] = other_profiles[profs]; - other_profiles[profs] = aux; - } - end_of_sorted++; - break; - } - } - } - } else { - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - } - - list->Clear(); - if (add_All_item) - list->append(_L("(All)"), &EMPTY); - for (const auto& item : prusa_profiles) - list->append(item, &const_cast(item.get())); - for (const auto& item : other_profiles) - list->append(item, &const_cast(item.get())); -} - -void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) -{ - // sort data - // then prusa profiles - // then the rest - // in alphabetical order - std::vector prusa_profiles; - std::vector other_profiles; - //for (int i = 0; i < data.size(); ++i) { - for (const auto& item : data) { - const std::string& name = item.name; - if (name.find("Prusa") != std::string::npos) - prusa_profiles.emplace_back(item); - else - other_profiles.emplace_back(item); - } - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - list->Clear(); - for (size_t i = 0; i < prusa_profiles.size(); ++i) { - list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); - list->Check(i, prusa_profiles[i].checked); - } - for (size_t i = 0; i < other_profiles.size(); ++i) { - list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); - list->Check(i + prusa_profiles.size(), other_profiles[i].checked); - } -} - -void PageMaterials::select_material(int i) -{ - const bool checked = list_profile->IsChecked(i); - - const std::string& alias_key = list_profile->get_data(i); - wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); -} - -void PageMaterials::select_all(bool select) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - for (unsigned i = 0; i < list_profile->GetCount(); i++) { - const bool current = list_profile->IsChecked(i); - if (current != select) { - list_profile->Check(i, select); - select_material(i); - } - } -} - -void PageMaterials::clear() -{ - list_printer->Clear(); - list_type->Clear(); - list_vendor->Clear(); - list_profile->Clear(); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - presets_loaded = false; -} - -void PageMaterials::on_activate() -{ - if (! presets_loaded) { - wizard_p()->update_materials(materials->technology); - reload_presets(); - } - first_paint = true; -} - - -const char *PageCustom::default_profile_name = "My Settings"; - -PageCustom::PageCustom(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) -{ - cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); - auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); - - wxBoxSizer* profile_name_sizer = new wxBoxSizer(wxVERTICAL); - profile_name_editor = new SavePresetDialog::Item{ this, profile_name_sizer, default_profile_name }; - profile_name_editor->Enable(false); - - cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { - profile_name_editor->Enable(custom_wanted()); - wizard_p()->on_custom_setup(custom_wanted()); - }); - - append(cb_custom); - append(label); - append(profile_name_sizer); -} - -PageUpdate::PageUpdate(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) - , version_check(true) - , preset_update(true) -{ - const AppConfig *app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); - box_slic3r->SetValue(app_config->get("notify_release") != "none"); - append(box_slic3r); - append_text(wxString::Format(_L( - "If enabled, %s checks for new application versions online. When a new version becomes available, " - "a notification is displayed at the next application startup (never during program usage). " - "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); - - append_spacer(VERTICAL_SPACING); - - auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); - box_presets->SetValue(app_config->get("preset_update") == "1"); - append(box_presets); - append_text(wxString::Format(_L( - "If enabled, %s downloads updates of built-in system presets in the background." - "These updates are downloaded into a separate temporary location." - "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); - const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); - auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); - label_bold->SetFont(boldfont); - label_bold->Wrap(WRAP_WIDTH); - append(label_bold); - append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); - - box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); - box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); -} - -PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) - , full_pathnames(false) -{ - auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); - box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); - append(box_pathnames); - append_text(_L( - "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" - "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." - )); - - box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); -} - -#ifdef _WIN32 -PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) -{ - cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); - cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); -// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); - - append(cb_3mf); - append(cb_stl); -// append(cb_gcode); -} -#endif // _WIN32 - -PageMode::PageMode(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) -{ - append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" - "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " - "The other two offer progressively more sophisticated fine-tuning, " - "they are suitable for advanced and expert users, respectively.")); - - radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); - radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); - radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); - - std::string mode { "simple" }; - wxGetApp().app_config->get("", "view_mode", mode); - - if (mode == "advanced") { radio_advanced->SetValue(true); } - else if (mode == "expert") { radio_expert->SetValue(true); } - else { radio_simple->SetValue(true); } - - append(radio_simple); - append(radio_advanced); - append(radio_expert); - - append_text("\n" + _L("The size of the object can be specified in inches")); - check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); - check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); - append(check_inch); - - on_activate(); -} - -void PageMode::serialize_mode(AppConfig *app_config) const -{ - std::string mode = ""; - - if (radio_simple->GetValue()) { mode = "simple"; } - if (radio_advanced->GetValue()) { mode = "advanced"; } - if (radio_expert->GetValue()) { mode = "expert"; } - - app_config->set("view_mode", mode); - app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); -} - -PageVendors::PageVendors(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) -{ - const AppConfig &appconfig = this->wizard_p()->appconfig_new; - - append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); - - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - for (const auto &pair : wizard_p()->bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); - }); - - const auto &vendors = appconfig.vendors(); - const bool enabled = vendors.find(pair.first) != vendors.end(); - if (enabled) { - cbox->SetValue(true); - - auto pages = wizard_p()->pages_3rdparty.find(vendor->id); - wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); - - for (PagePrinters* page : { pages->second.first, pages->second.second }) - if (page) page->install = true; - } - - append(cbox); - } -} - -PageFirmware::PageFirmware(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) - , gcode_opt(*print_config_def.get("gcode_flavor")) - , gcode_picker(nullptr) -{ - append_text(_L("Choose the type of firmware used by your printer.")); - append_text(_(gcode_opt.tooltip)); - - wxArrayString choices; - choices.Alloc(gcode_opt.enum_labels.size()); - for (const auto &label : gcode_opt.enum_labels) { - choices.Add(label); - } - - gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); - wxGetApp().UpdateDarkUI(gcode_picker); - const auto &enum_values = gcode_opt.enum_values; - auto needle = enum_values.cend(); - if (gcode_opt.default_value) { - needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); - } - if (needle != enum_values.cend()) { - gcode_picker->SetSelection(needle - enum_values.cbegin()); - } else { - gcode_picker->SetSelection(0); - } - - append(gcode_picker); -} - -void PageFirmware::apply_custom_config(DynamicPrintConfig &config) -{ - auto sel = gcode_picker->GetSelection(); - if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { - auto *opt = new ConfigOptionEnum(static_cast(sel)); - config.set_key_value("gcode_flavor", opt); - } -} - -static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) -{ - e.Skip(); - wxString str = ctrl->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - if (val == 0.0) - val = def_value; - ctrl->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - // On Windows, this SetFocus creates an invisible marker. - //ctrl->SetFocus(); - } - else if (was_replaced) - ctrl->SetValue(double_to_string(val)); -} - -class DiamTextCtrl : public wxTextCtrl -{ -public: - DiamTextCtrl(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxBORDER_SIMPLE; -#else - long style = 0; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); - wxGetApp().UpdateDarkUI(this); - } - ~DiamTextCtrl() {} -}; - -PageBedShape::PageBedShape(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) - , shape_panel(new BedShapePanel(this)) -{ - append_text(_L("Set the shape of your printer's bed.")); - - shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), - *wizard_p()->custom_config->option("bed_custom_texture"), - *wizard_p()->custom_config->option("bed_custom_model")); - - append(shape_panel); -} - -void PageBedShape::apply_custom_config(DynamicPrintConfig &config) -{ - const std::vector& points = shape_panel->get_shape(); - const std::string& custom_texture = shape_panel->get_custom_texture(); - const std::string& custom_model = shape_panel->get_custom_model(); - config.set_key_value("bed_shape", new ConfigOptionPoints(points)); - config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); - config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); -} - -PageBuildVolume::PageBuildVolume(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1) - , build_volume(new DiamTextCtrl(this)) -{ - append_text(_L("Set verctical size of your printer.")); - - wxString value = "200"; - build_volume->SetValue(value); - - build_volume->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { - double def_value = 200.0; - double max_value = 1200.0; - e.Skip(); - wxString str = build_volume->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - val = def_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (val < 0.0) { - val = def_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (val > max_value) { - val = max_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (was_replaced) - build_volume->SetValue(double_to_string(val)); - }, build_volume->GetId()); - - auto* sizer_volume = new wxFlexGridSizer(3, 5, 5); - auto* text_volume = new wxStaticText(this, wxID_ANY, _L("Max print height:")); - auto* unit_volume = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_volume->AddGrowableCol(0, 1); - sizer_volume->Add(text_volume, 0, wxALIGN_CENTRE_VERTICAL); - sizer_volume->Add(build_volume); - sizer_volume->Add(unit_volume, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_volume); -} - -void PageBuildVolume::apply_custom_config(DynamicPrintConfig& config) -{ - double val = 0.0; - build_volume->GetValue().ToDouble(&val); - auto* opt_volume = new ConfigOptionFloat(val); - config.set_key_value("max_print_height", opt_volume); -} - -PageDiameters::PageDiameters(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) - , diam_nozzle(new DiamTextCtrl(this)) - , diam_filam (new DiamTextCtrl(this)) -{ - auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); - wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); - diam_nozzle->SetValue(value); - - auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); - value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); - diam_filam->SetValue(value); - - diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); - diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); - - append_text(_L("Enter the diameter of your printer's hot end nozzle.")); - - auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); - auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); - auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_nozzle->AddGrowableCol(0, 1); - sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - sizer_nozzle->Add(diam_nozzle); - sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_nozzle); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the diameter of your filament.")); - append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); - - auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); - auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); - auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_filam->AddGrowableCol(0, 1); - sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(diam_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_filam); -} - -void PageDiameters::apply_custom_config(DynamicPrintConfig &config) -{ - double val = 0.0; - diam_nozzle->GetValue().ToDouble(&val); - auto *opt_nozzle = new ConfigOptionFloats(1, val); - config.set_key_value("nozzle_diameter", opt_nozzle); - - val = 0.0; - diam_filam->GetValue().ToDouble(&val); - auto * opt_filam = new ConfigOptionFloats(1, val); - config.set_key_value("filament_diameter", opt_filam); - - auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { - char buf[64]; // locales don't matter here (sprintf/atof) - sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); - config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); - }; - - set_extrusion_width("support_material_extrusion_width", 0.35); - set_extrusion_width("top_infill_extrusion_width", 0.40); - set_extrusion_width("first_layer_extrusion_width", 0.42); - - set_extrusion_width("extrusion_width", 0.45); - set_extrusion_width("perimeter_extrusion_width", 0.45); - set_extrusion_width("external_perimeter_extrusion_width", 0.45); - set_extrusion_width("infill_extrusion_width", 0.45); - set_extrusion_width("solid_infill_extrusion_width", 0.45); -} - -class SpinCtrlDouble: public wxSpinCtrlDouble -{ -public: - SpinCtrlDouble(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; -#else - long style = wxSP_ARROW_KEYS; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this->GetText()); -#endif - this->Refresh(); - } - ~SpinCtrlDouble() {} -}; - -PageTemperatures::PageTemperatures(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) - , spin_extr(new SpinCtrlDouble(this)) - , spin_bed (new SpinCtrlDouble(this)) -{ - spin_extr->SetIncrement(5.0); - const auto &def_extr = *print_config_def.get("temperature"); - spin_extr->SetRange(def_extr.min, def_extr.max); - auto *default_extr = def_extr.get_default_value(); - spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); - - spin_bed->SetIncrement(5.0); - const auto &def_bed = *print_config_def.get("bed_temperature"); - spin_bed->SetRange(def_bed.min, def_bed.max); - auto *default_bed = def_bed.get_default_value(); - spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); - - append_text(_L("Enter the temperature needed for extruding your filament.")); - append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); - - auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); - auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); - auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_extr->AddGrowableCol(0, 1); - sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); - sizer_extr->Add(spin_extr); - sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_extr); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); - append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); - - auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); - auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); - auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_bed->AddGrowableCol(0, 1); - sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); - sizer_bed->Add(spin_bed); - sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_bed); -} - -void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) -{ - auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("temperature", opt_extr); - auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("first_layer_temperature", opt_extr1st); - auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("bed_temperature", opt_bed); - auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("first_layer_bed_temperature", opt_bed1st); -} - - -// Index - -ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) - : wxPanel(parent) - , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) - , bullet_black(ScalableBitmap(parent, "bullet_black.png")) - , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) - , bullet_white(ScalableBitmap(parent, "bullet_white.png")) - , item_active(NO_ITEM) - , item_hover(NO_ITEM) - , last_page((size_t)-1) -{ -#ifndef __WXOSX__ - SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX -#endif //__WXOSX__ - SetMinSize(bg.GetSize()); - - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); - Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); - Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); - - Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { - if (item_hover != -1) { - item_hover = -1; - Refresh(); - } - evt.Skip(); - }); - - Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { - if (item_hover >= 0) { go_to(item_hover); } - }); -} - -wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); - -void ConfigWizardIndex::add_page(ConfigWizardPage *page) -{ - last_page = items.size(); - items.emplace_back(Item { page->shortname, page->indent, page }); - Refresh(); -} - -void ConfigWizardIndex::add_label(wxString label, unsigned indent) -{ - items.emplace_back(Item { std::move(label), indent, nullptr }); - Refresh(); -} - -ConfigWizardPage* ConfigWizardIndex::active_page() const -{ - if (item_active >= items.size()) { return nullptr; } - - return items[item_active].page; -} - -void ConfigWizardIndex::go_prev() -{ - // Search for a preceiding item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active; i > 0; i--) { - if (items[i - 1].page != nullptr) { - go_to(i - 1); - return; - } - } -} - -void ConfigWizardIndex::go_next() -{ - // Search for a next item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active + 1; i < items.size(); i++) { - if (items[i].page != nullptr) { - go_to(i); - return; - } - } -} - -// This one actually performs the go-to op -void ConfigWizardIndex::go_to(size_t i) -{ - if (i != item_active - && i < items.size() - && items[i].page != nullptr) { - auto *new_active = items[i].page; - auto *former_active = active_page(); - if (former_active != nullptr) { - former_active->Hide(); - } - - item_active = i; - new_active->Show(); - - wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); - AddPendingEvent(evt); - - Refresh(); - - new_active->on_activate(); - } -} - -void ConfigWizardIndex::go_to(const ConfigWizardPage *page) -{ - if (page == nullptr) { return; } - - for (size_t i = 0; i < items.size(); i++) { - if (items[i].page == page) { - go_to(i); - return; - } - } -} - -void ConfigWizardIndex::clear() -{ - auto *former_active = active_page(); - if (former_active != nullptr) { former_active->Hide(); } - - items.clear(); - item_active = NO_ITEM; -} - -void ConfigWizardIndex::on_paint(wxPaintEvent & evt) -{ - const auto size = GetClientSize(); - if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } - - wxPaintDC dc(this); - - const auto bullet_w = bullet_black.GetWidth(); - const auto bullet_h = bullet_black.GetHeight(); - const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; - const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; - const int yinc = item_height(); - - int index_width = 0; - - unsigned y = 0; - for (size_t i = 0; i < items.size(); i++) { - const Item& item = items[i]; - unsigned x = em_w/2 + item.indent * em_w; - - if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { - dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); - } - else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } - else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } - - x += + bullet_w + em_w/2; - const auto text_size = dc.GetTextExtent(item.label); - dc.SetTextForeground(wxGetApp().get_label_clr_default()); - dc.DrawText(item.label, x, y + yoff_text); - - y += yinc; - index_width = std::max(index_width, (int)x + text_size.x); - } - - //draw logo - if (int y = size.y - bg.GetHeight(); y>=0) { - dc.DrawBitmap(bg.get_bitmap(), 0, y, false); - index_width = std::max(index_width, bg.GetWidth() + em_w / 2); - } - - if (GetMinSize().x < index_width) { - CallAfter([this, index_width]() { - SetMinSize(wxSize(index_width, GetMinSize().y)); - Refresh(); - }); - } -} - -void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) -{ - const wxClientDC dc(this); - const wxPoint pos = evt.GetLogicalPosition(dc); - - const ssize_t item_hover_new = pos.y / item_height(); - - if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { - item_hover = item_hover_new; - Refresh(); - } - - evt.Skip(); -} - -void ConfigWizardIndex::msw_rescale() -{ - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - SetMinSize(bg.GetSize()); - - Refresh(); -} - - -// Materials - -const std::string Materials::UNKNOWN = "(Unknown)"; - -void Materials::push(const Preset *preset) -{ - presets.emplace_back(preset); - types.insert(technology & T_FFF - ? Materials::get_filament_type(preset) - : Materials::get_material_type(preset)); -} - -void Materials::add_printer(const Preset* preset) -{ - printers.insert(preset); -} - -void Materials::clear() -{ - presets.clear(); - types.clear(); - printers.clear(); - compatibility_counter.clear(); -} - -const std::string& Materials::appconfig_section() const -{ - return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; -} - -const std::string& Materials::get_type(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); -} - -const std::string& Materials::get_vendor(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); -} - -const std::string& Materials::get_filament_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_type"); - if (opt != nullptr && opt->values.size() > 0) { - return opt->values[0]; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_filament_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -const std::string& Materials::get_material_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_type"); - if (opt != nullptr) { - return opt->value; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_material_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -// priv - -static const std::unordered_map> legacy_preset_map {{ - { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, - { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, - { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, -}}; - -void ConfigWizard::priv::load_pages() -{ - wxWindowUpdateLocker freeze_guard(q); - (void)freeze_guard; - - const ConfigWizardPage *former_active = index->active_page(); - - index->clear(); - - index->add_page(page_welcome); - - // Printers - if (!only_sla_mode) - index->add_page(page_fff); - index->add_page(page_msla); - if (!only_sla_mode) { - index->add_page(page_vendors); - for (const auto &pages : pages_3rdparty) { - for ( PagePrinters* page : { pages.second.first, pages.second.second }) - if (page && page->install) - index->add_page(page); - } - - index->add_page(page_custom); - if (page_custom->custom_wanted()) { - index->add_page(page_firmware); - index->add_page(page_bed); - index->add_page(page_bvolume); - index->add_page(page_diams); - index->add_page(page_temps); - } - - // Filaments & Materials - if (any_fff_selected) { index->add_page(page_filaments); } - } - if (any_sla_selected) { index->add_page(page_sla_materials); } - - // there should to be selected at least one printer - btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); - - index->add_page(page_update); - index->add_page(page_reload_from_disk); -#ifdef _WIN32 - index->add_page(page_files_association); -#endif // _WIN32 - index->add_page(page_mode); - - index->go_to(former_active); // Will restore the active item/page if possible - - q->Layout(); -// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig - q->Refresh(); -} - -void ConfigWizard::priv::init_dialog_size() -{ - // Clamp the Wizard size based on screen dimensions - - const auto idx = wxDisplay::GetFromWindow(q); - wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); - - const auto disp_rect = display.GetClientArea(); - wxRect window_rect( - disp_rect.x + disp_rect.width / 20, - disp_rect.y + disp_rect.height / 20, - 9*disp_rect.width / 10, - 9*disp_rect.height / 10); - - const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution - if (width_hint < window_rect.width) { - window_rect.x += (window_rect.width - width_hint) / 2; - window_rect.width = width_hint; - } - - q->SetSize(window_rect); -} - -void ConfigWizard::priv::load_vendors() -{ - bundles = BundleMap::load(); - - // Load up the set of vendors / models / variants the user has had enabled up till now - AppConfig *app_config = wxGetApp().app_config; - if (! app_config->legacy_datadir()) { - appconfig_new.set_vendors(*app_config); - } else { - // In case of legacy datadir, try to guess the preference based on the printer preset files that are present - const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; - for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) - if (Slic3r::is_ini_file(dir_entry)) { - auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); - if (needle == legacy_preset_map.end()) { continue; } - - const auto &model = needle->second.first; - const auto &variant = needle->second.second; - appconfig_new.set_variant("PrusaResearch", model, variant, true); - } - } - - // Initialize the is_visible flag in printer Presets - for (auto &pair : bundles) { - pair.second.preset_bundle->load_installed_printers(appconfig_new); - } - - // Copy installed filaments and SLA material names from app_config to appconfig_new - // while resolving current names of profiles, which were renamed in the meantime. - for (PrinterTechnology technology : { ptFFF, ptSLA }) { - const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; - std::map section_new; - if (app_config->has_section(section_name)) { - const std::map §ion_old = app_config->get_section(section_name); - for (const auto& material_name_and_installed : section_old) - if (material_name_and_installed.second == "1") { - // Material is installed. Resolve it in bundles. - size_t num_found = 0; - const std::string &material_name = material_name_and_installed.first; - for (auto &bundle : bundles) { - const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); - const Preset *preset = materials.find_preset(material_name); - if (preset == nullptr) { - // Not found. Maybe the material preset is there, bu it was was renamed? - const std::string *new_name = materials.get_preset_name_renamed(material_name); - if (new_name != nullptr) - preset = materials.find_preset(*new_name); - } - if (preset != nullptr) { - // Materal preset was found, mark it as installed. - section_new[preset->name] = "1"; - ++ num_found; - } - } - if (num_found == 0) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; - else if (num_found > 1) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; - } - } - appconfig_new.set_section(section_name, section_new); - }; -} - -void ConfigWizard::priv::add_page(ConfigWizardPage *page) -{ - const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; - hscroll_sizer->Add(page, proportion, wxEXPAND); - all_pages.push_back(page); -} - -void ConfigWizard::priv::enable_next(bool enable) -{ - btn_next->Enable(enable); - btn_finish->Enable(enable); -} - -void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) -{ - switch (start_page) { - case ConfigWizard::SP_PRINTERS: - index->go_to(page_fff); - btn_next->SetFocus(); - break; - case ConfigWizard::SP_FILAMENTS: - index->go_to(page_filaments); - btn_finish->SetFocus(); - break; - case ConfigWizard::SP_MATERIALS: - index->go_to(page_sla_materials); - btn_finish->SetFocus(); - break; - default: - index->go_to(page_welcome); - btn_next->SetFocus(); - break; - } -} - -void ConfigWizard::priv::create_3rdparty_pages() -{ - for (const auto &pair : bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - bool is_fff_technology = false; - bool is_sla_technology = false; - - for (auto& model: vendor->models) - { - if (!is_fff_technology && model.technology == ptFFF) - is_fff_technology = true; - if (!is_sla_technology && model.technology == ptSLA) - is_sla_technology = true; - } - - PagePrinters* pageFFF = nullptr; - PagePrinters* pageSLA = nullptr; - - if (is_fff_technology) { - pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); - add_page(pageFFF); - } - - if (is_sla_technology) { - pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); - add_page(pageSLA); - } - - pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); - } -} - -void ConfigWizard::priv::set_run_reason(RunReason run_reason) -{ - this->run_reason = run_reason; - for (auto &page : all_pages) { - page->set_run_reason(run_reason); - } -} - -void ConfigWizard::priv::update_materials(Technology technology) -{ - if (any_fff_selected && (technology & T_FFF)) { - filaments.clear(); - aliases_fff.clear(); - // Iterate filaments in all bundles - for (const auto &pair : bundles) { - for (const auto &filament : pair.second.preset_bundle->filaments) { - // Check if filament is already added - if (filaments.containts(&filament)) - continue; - // Iterate printers in all bundles - for (const auto &printer : pair.second.preset_bundle->printers) { - if (!printer.is_visible || printer.printer_technology() != ptFFF) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } - filaments.add_printer(&printer); - } - } - - } - } - // count compatible printers - for (const auto& preset : filaments.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < filaments.presets.size(); ++i) { - if (preset->alias == filaments.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : filaments.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - filaments.compatibility_counter.emplace_back(preset->alias, counter); - } - } - - if (any_sla_selected && (technology & T_SLA)) { - sla_materials.clear(); - aliases_sla.clear(); - - // Iterate SLA materials in all bundles - for (const auto &pair : bundles) { - for (const auto &material : pair.second.preset_bundle->sla_materials) { - // Check if material is already added - if (sla_materials.containts(&material)) - continue; - // Iterate printers in all bundles - // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. - for (const auto& printer : pair.second.preset_bundle->printers) { - if(!printer.is_visible || printer.printer_technology() != ptSLA) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - // Check if material is already added - if(!sla_materials.containts(&material)) { - sla_materials.push(&material); - if (!material.alias.empty()) - aliases_sla[material.alias].insert(material.name); - } - sla_materials.add_printer(&printer); - } - } - } - } - // count compatible printers - for (const auto& preset : sla_materials.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < sla_materials.presets.size(); ++i) { - if(preset->alias == sla_materials.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : sla_materials.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - sla_materials.compatibility_counter.emplace_back(preset->alias, counter); - } - } -} - -void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) -{ - custom_printer_selected = custom_wanted; - load_pages(); -} - -void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) -{ - if (check_sla_selected() != any_sla_selected || - check_fff_selected() != any_fff_selected) { - any_fff_selected = check_fff_selected(); - any_sla_selected = check_sla_selected(); - - load_pages(); - } - - // Update the is_visible flag on relevant printer profiles - for (auto &pair : bundles) { - if (pair.first != evt.vendor_id) { continue; } - - for (auto &preset : pair.second.preset_bundle->printers) { - if (preset.config.opt_string("printer_model") == evt.model_id - && preset.config.opt_string("printer_variant") == evt.variant_name) { - preset.is_visible = evt.enable; - } - } - - // When a printer model is picked, but there is no material installed compatible with this printer model, - // install default materials for selected printer model silently. - check_and_install_missing_materials(page->technology, evt.model_id); - } - - if (page->technology & T_FFF) { - page_filaments->clear(); - } else if (page->technology & T_SLA) { - page_sla_materials->clear(); - } -} - -void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) -{ - PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - for (const std::string& material : printer_model.default_materials) - appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); -} - -void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) -{ - PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - const std::string &appconfig_section = page_materials->materials->appconfig_section(); - - // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. - // Filament is selected on same page for all printers of same technology. - /* - auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) - { - const std::string vendor_id = page_printers->get_vendor_id(); - for (auto& pair : bundles) - if (pair.first == vendor_id) - for (const VendorProfile::PrinterModel *printer_model : printer_models) - for (const std::string &material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - }; - - PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; - select_default_materials_for_printer_page(page_printers, technology); - - for (const auto& printer : pages_3rdparty) - { - page_printers = technology & T_FFF ? printer.second.first : printer.second.second; - if (page_printers) - select_default_materials_for_printer_page(page_printers, technology); - } - */ - - // Iterate printer_models and select default materials. If none available -> msg to user. - std::vector models_without_default; - for (const VendorProfile::PrinterModel* printer_model : printer_models) { - if (printer_model->default_materials.empty()) { - models_without_default.emplace_back(printer_model); - } else { - for (const std::string& material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - } - } - - if (!models_without_default.empty()) { - std::string printer_names = "\n\n"; - for (const VendorProfile::PrinterModel* printer_model : models_without_default) { - printer_names += printer_model->name + "\n"; - } - printer_names += "\n\n"; - std::string message = (technology & T_FFF ? - GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : - GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); - MessageDialog msg(q, message, _L("Notice"), wxOK); - msg.ShowModal(); - } - - update_materials(technology); - ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); -} - -void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) -{ - auto it = pages_3rdparty.find(vendor->id); - wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); - - for (PagePrinters* page : { it->second.first, it->second.second }) - if (page) { - if (page->install && !install) - page->select_all(false); - page->install = install; - // if some 3rd vendor is selected, select first printer for them - if (install) - page->printer_pickers[0]->select_one(0, true); - page->Layout(); - } - - load_pages(); -} - -bool ConfigWizard::priv::on_bnt_finish() -{ - wxBusyCursor wait; - /* When Filaments or Sla Materials pages are activated, - * materials for this pages are automaticaly updated and presets are reloaded. - * - * But, if _Finish_ button was clicked without activation of those pages - * (for example, just some printers were added/deleted), - * than last changes wouldn't be updated for filaments/materials. - * SO, do that before close of Wizard - */ - update_materials(T_ANY); - if (any_fff_selected) - page_filaments->reload_presets(); - if (any_sla_selected) - page_sla_materials->reload_presets(); - - // theres no need to check that filament is selected if we have only custom printer - if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; - // check, that there is selected at least one filament/material - return check_and_install_missing_materials(T_ANY); -} - -// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed -// for each Printer preset of each Printer Model installed. -// -// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. -// Otherwise the user is quieried whether to install the missing default materials or not. -// -// Return true if the tested Printer Models already had materials installed. -// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these -// respective Printer Models or not. -bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) -{ - // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, - // which is compatible with it. - const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) - { - const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); - std::set printer_models_without_material; - for (const auto &pair : bundles) { - const PresetCollection &materials = pair.second.preset_bundle->materials(technology); - for (const auto &printer : pair.second.preset_bundle->printers) { - if (printer.is_visible && printer.printer_technology() == technology) { - const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); - assert(printer_model != nullptr); - if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && - printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { - bool has_material = false; - for (const auto& preset : appconfig_presets) { - if (preset.second == "1") { - const Preset *material = materials.find_preset(preset.first, false); - if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - has_material = true; - break; - } - } - } - if (! has_material) - printer_models_without_material.insert(printer_model); - } - } - } - } - assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); - return printer_models_without_material; - }; - - const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) - { - //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); - MessageDialog msg(q, message, _L("Notice"), wxYES_NO); - if (msg.ShowModal() == wxID_YES) - select_default_materials_for_printer_models(technology, printer_models); - }; - - const auto printer_model_list = [](const std::set &printer_models) -> wxString { - wxString out; - for (const VendorProfile::PrinterModel *printer_model : printer_models) { - wxString name = from_u8(printer_model->name); - out += "\t\t"; - out += name; - out += "\n"; - } - return out; - }; - - if (any_fff_selected && (technology & T_FFF)) { - std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following FFF printer models have no filament selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default filaments for these FFF printer models?"), - printer_models_without_material, - T_FFF); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); - return false; - } - } - - if (any_sla_selected && (technology & T_SLA)) { - std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following SLA printer models have no materials selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default SLA materials for these printer models?"), - printer_models_without_material, - T_SLA); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); - return false; - } - } - - return true; -} - -static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) -{ - auto get_aliases = [](const std::map& data) { - std::set old_aliases; - for (auto item : data) { - const std::string& name = item.first; - size_t pos = name.find("@"); - old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); - } - return old_aliases; - }; - - std::set old_aliases = get_aliases(old_data); - std::set new_aliases = get_aliases(new_data); - std::set diff; - std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); - - return diff; -} - -static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) -{ - std::set diff = get_new_added_presets(old_data, new_data); - if (diff.empty()) - return std::string(); - return *diff.begin(); -} - -bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) -{ - wxString header, caption = _L("Configuration is edited in ConfigWizard"); - const auto enabled_vendors = appconfig_new.vendors(); - const auto enabled_vendors_old = app_config->vendors(); - - bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); - PrinterTechnology preferred_pt = ptAny; - auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { - const auto config = enabled_vendors.find(bundle_name); - PrinterTechnology pt = ptAny; - if (config != enabled_vendors.end()) { - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0) { - pt = model.technology; - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - - if (const auto model_it_old = config_old->second.find(model.id); - model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - } - } - } - return pt; - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); - preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) - continue; - else if (preferred_pt == ptAny) - preferred_pt = pt; - if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) - break; - } - } - - if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) - return false; - - bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); - if (check_unsaved_preset_changes) - header = _L("All user presets will be deleted."); - int act_btns = ActionButtons::KEEP; - if (!check_unsaved_preset_changes) - act_btns |= ActionButtons::SAVE; - - // Install bundles from resources if needed: - std::vector install_bundles; - for (const auto &pair : bundles) { - if (! pair.second.is_in_resources) { continue; } - - if (pair.second.is_prusa_bundle) { - // Always install Prusa bundle, because it has a lot of filaments/materials - // likely to be referenced by other profiles. - install_bundles.emplace_back(pair.first); - continue; - } - - const auto vendor = enabled_vendors.find(pair.first); - if (vendor == enabled_vendors.end()) { continue; } - - size_t size_sum = 0; - for (const auto &model : vendor->second) { size_sum += model.second.size(); } - - if (size_sum > 0) { - // This vendor needs to be installed - install_bundles.emplace_back(pair.first); - } - } - if (!check_unsaved_preset_changes) - if ((check_unsaved_preset_changes = install_bundles.size() > 0)) - header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); - -#ifdef __linux__ - // Desktop integration on Linux - if (page_welcome->integrate_desktop()) - DesktopIntegrationDialog::perform_desktop_integration(); -#endif - - // Decide whether to create snapshot based on run_reason and the reset profile checkbox - bool snapshot = true; - Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; - switch (run_reason) { - case ConfigWizard::RR_DATA_EMPTY: - snapshot = false; - break; - case ConfigWizard::RR_DATA_LEGACY: - snapshot = true; - break; - case ConfigWizard::RR_DATA_INCOMPAT: - // In this case snapshot has already been taken by - // PresetUpdater with the appropriate reason - snapshot = false; - break; - case ConfigWizard::RR_USER: - snapshot = page_welcome->reset_user_profile(); - snapshot_reason = Snapshot::SNAPSHOT_USER; - break; - } - - if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) - return false; - - if (check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - - if (install_bundles.size() > 0) { - // Install bundles from resources. - // Don't create snapshot - we've already done that above if applicable. - if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) - return false; - } else { - BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; - } - - if (page_welcome->reset_user_profile()) { - BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; - preset_bundle->reset(true); - } - - std::string preferred_model; - std::string preferred_variant; - auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { - const auto config = enabled_vendors.find(bundle_name); - if (config == enabled_vendors.end()) - return std::string(); - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0 && - preferred_pt == model.technology) { - variant = *model_it->second.begin(); - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end()) - return model.id; - const auto model_it_old = config_old->second.find(model.id); - if (model_it_old == config_old->second.end()) - return model.id; - else if (model_it_old->second != model_it->second) { - for (const auto& var : model_it->second) - if (model_it_old->second.find(var) == model_it_old->second.end()) { - variant = var; - return model.id; - } - } - } - } - if (!variant.empty()) - variant.clear(); - return std::string(); - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); - preferred_model.empty()) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); - !preferred_model.empty()) - break; - } - } - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !preferred_model.empty())) { - header = _L("A new Printer was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { - header = _L("Some Printers were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - - std::string first_added_filament, first_added_sla_material; - auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { - if (appconfig_new.has_section(section_name)) { - // get first of new added preset names - const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); - first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); - } - }; - get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); - get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { - header = !first_added_filament.empty() ? - _L("A new filament was installed and it will be activated.") : - _L("A new SLA material was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else { - auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { - return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); - }; - bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); - bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); - if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { - header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - } - - // apply materials in app_config - for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) - app_config->set_section(section_name, appconfig_new.get_section(section_name)); - - app_config->set_vendors(appconfig_new); - - app_config->set("notify_release", page_update->version_check ? "all" : "none"); - app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); - -#ifdef _WIN32 - app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); - app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); -// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); - - if (wxGetApp().is_editor()) { - if (page_files_association->associate_3mf()) - wxGetApp().associate_3mf_files(); - if (page_files_association->associate_stl()) - wxGetApp().associate_stl_files(); - } -// else { -// if (page_files_association->associate_gcode()) -// wxGetApp().associate_gcode_files(); -// } -#endif // _WIN32 - - page_mode->serialize_mode(app_config); - - if (check_unsaved_preset_changes) - preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, - {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); - - if (!only_sla_mode && page_custom->custom_wanted() && page_custom->is_valid_profile_name()) { - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) - return false; - - page_firmware->apply_custom_config(*custom_config); - page_bed->apply_custom_config(*custom_config); - page_bvolume->apply_custom_config(*custom_config); - page_diams->apply_custom_config(*custom_config); - page_temps->apply_custom_config(*custom_config); - - copy_bed_model_and_texture_if_needed(*custom_config); - - const std::string profile_name = page_custom->profile_name(); - preset_bundle->load_config_from_wizard(profile_name, *custom_config); - } - - // Update the selections from the compatibilty. - preset_bundle->export_selections(*app_config); - - return true; -} -void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) -{ - const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; - - auto update = [this, add](const std::string& s, const std::string& key) { - assert(! s.empty()); - if (add) - appconfig_new.set(s, key, "1"); - else - appconfig_new.erase(s, key); - }; - - // add or delete presets had a same alias - auto it = aliases.find(alias_key); - if (it != aliases.end()) - for (const std::string& name : it->second) - update(section, name); -} - -bool ConfigWizard::priv::check_fff_selected() -{ - bool ret = page_fff->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.first) // FFF page - ret |= printer.second.first->any_selected(); - return ret; -} - -bool ConfigWizard::priv::check_sla_selected() -{ - bool ret = page_msla->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.second) // SLA page - ret |= printer.second.second->any_selected(); - return ret; -} - - -// Public - -ConfigWizard::ConfigWizard(wxWindow *parent) - : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , p(new priv(this)) -{ - this->SetFont(wxGetApp().normal_font()); - - p->load_vendors(); - p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ - "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", - })); - - p->index = new ConfigWizardIndex(this); - - auto *vsizer = new wxBoxSizer(wxVERTICAL); - auto *topsizer = new wxBoxSizer(wxHORIZONTAL); - auto* hline = new StaticLine(this); - p->btnsizer = new wxBoxSizer(wxHORIZONTAL); - - // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. - // Later, we compare that to the size of the current screen and set minimum width based on that (see below). - p->hscroll = new wxScrolledWindow(this); - p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); - p->hscroll->SetSizer(p->hscroll_sizer); - - topsizer->Add(p->index, 0, wxEXPAND); - topsizer->AddSpacer(INDEX_MARGIN); - topsizer->Add(p->hscroll, 1, wxEXPAND); - - p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); - p->btnsizer->Add(p->btn_sel_all); - - p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); - p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); - p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); - p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac - p->btnsizer->AddStretchSpacer(); - p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - - wxGetApp().UpdateDarkUI(p->btn_sel_all); - wxGetApp().UpdateDarkUI(p->btn_prev); - wxGetApp().UpdateDarkUI(p->btn_next); - wxGetApp().UpdateDarkUI(p->btn_finish); - wxGetApp().UpdateDarkUI(p->btn_cancel); - - const auto prusa_it = p->bundles.find("PrusaResearch"); - wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); - const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; - - p->add_page(p->page_welcome = new PageWelcome(this)); - - - p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); - p->only_sla_mode = !p->page_fff->has_printers; - if (!p->only_sla_mode) { - p->add_page(p->page_fff); - p->page_fff->is_primary_printer_page = true; - } - - - p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); - p->add_page(p->page_msla); - if (p->only_sla_mode) { - p->page_msla->is_primary_printer_page = true; - } - - if (!p->only_sla_mode) { - // Pages for 3rd party vendors - p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors - p->add_page(p->page_vendors = new PageVendors(this)); - p->add_page(p->page_custom = new PageCustom(this)); - p->custom_printer_selected = p->page_custom->custom_wanted(); - } - - p->any_sla_selected = p->check_sla_selected(); - p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); - - p->update_materials(T_ANY); - if (!p->only_sla_mode) - p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, - _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); - - p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, - _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); - - - p->add_page(p->page_update = new PageUpdate(this)); - p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); -#ifdef _WIN32 - p->add_page(p->page_files_association = new PageFilesAssociation(this)); -#endif // _WIN32 - p->add_page(p->page_mode = new PageMode(this)); - p->add_page(p->page_firmware = new PageFirmware(this)); - p->add_page(p->page_bed = new PageBedShape(this)); - p->add_page(p->page_bvolume = new PageBuildVolume(this)); - p->add_page(p->page_diams = new PageDiameters(this)); - p->add_page(p->page_temps = new PageTemperatures(this)); - - p->load_pages(); - p->index->go_to(size_t{0}); - - vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); - vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); - vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); - - SetSizer(vsizer); - SetSizerAndFit(vsizer); - - // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); - - on_window_geometry(this, [this]() { - p->init_dialog_size(); - }); - - p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); - - p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - // check, that there is selected at least one filament/material - ConfigWizardPage* active_page = this->p->index->active_page(); - if (// Leaving the filaments or SLA materials page and - (active_page == p->page_filaments || active_page == p->page_sla_materials) && - // some Printer models had no filament or SLA material selected. - ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) - // In that case don't leave the page and the function above queried the user whether to install default materials. - return; - this->p->index->go_next(); - }); - - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - if (p->on_bnt_finish()) - this->EndModal(wxID_OK); - }); - - p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { - p->any_sla_selected = true; - p->load_pages(); - p->page_fff->select_all(true, false); - p->page_msla->select_all(true, false); - p->index->go_to(p->page_mode); - }); - - p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { - const bool is_last = p->index->active_is_last(); - p->btn_next->Show(! is_last); - if (is_last) - p->btn_finish->SetFocus(); - - Layout(); - }); - - if (wxLinux_gtk3) - this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { - ConfigWizardPage* active_page = p->index->active_page(); - if (!active_page) - return; - for (auto page : p->all_pages) - if (page != active_page) - page->Hide(); - // update best size for the dialog after hiding of the non-active pages - vsizer->SetSizeHints(this); - // set initial dialog size - p->init_dialog_size(); - }); -} - -ConfigWizard::~ConfigWizard() {} - -bool ConfigWizard::run(RunReason reason, StartPage start_page) -{ - BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; - - GUI_App &app = wxGetApp(); - - p->set_run_reason(reason); - p->set_start_page(start_page); - - if (ShowModal() == wxID_OK) { - bool apply_keeped_changes = false; - if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) - return false; - - if (apply_keeped_changes) - app.apply_keeped_preset_modifications(); - - app.app_config->set_legacy_datadir(false); - app.update_mode(); - app.obj_manipul()->update_ui_from_settings(); - BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; - return true; - } else { - BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; - return false; - } -} - -const wxString& ConfigWizard::name(const bool from_menu/* = false*/) -{ - // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. - // Note: Don't call _() macro here. - // This function just return the current name according to the OS. - // Translation is implemented inside GUI_App::add_config_menu() -#if __APPLE__ - static const wxString config_wizard_name = L("Configuration Assistant"); - static const wxString config_wizard_name_menu = L("Configuration &Assistant"); -#else - static const wxString config_wizard_name = L("Configuration Wizard"); - static const wxString config_wizard_name_menu = L("Configuration &Wizard"); -#endif - return from_menu ? config_wizard_name_menu : config_wizard_name; -} - -void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) -{ - p->index->msw_rescale(); - - const int em = em_unit(); - - msw_buttons_rescale(this, em, { wxID_APPLY, - wxID_CANCEL, - p->btn_sel_all->GetId(), - p->btn_next->GetId(), - p->btn_prev->GetId() }); - - for (auto printer_picker: p->page_fff->printer_pickers) - msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); - - p->init_dialog_size(); - - Refresh(); -} - -void ConfigWizard::on_sys_color_changed() -{ - wxGetApp().UpdateDlgDarkUI(this); - Refresh(); -} - -} -} +// FIXME: extract absolute units -> em + +#include "ConfigWizard_private.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#endif // WIN32 + +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE + +#include "libslic3r/Platform.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/libslic3r.h" +#include "libslic3r/Model.hpp" +#include "libslic3r/Color.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Utils.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "Field.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" +#include "format.hpp" +#include "MsgDialog.hpp" +#include "UnsavedChangesDialog.hpp" +#include "slic3r/Utils/AppUpdater.hpp" + +#if defined(__linux__) && defined(__WXGTK3__) +#define wxLinux_gtk3 true +#else +#define wxLinux_gtk3 false +#endif //defined(__linux__) && defined(__WXGTK3__) + +namespace Slic3r { +namespace GUI { + + +using Config::Snapshot; +using Config::SnapshotDB; + + +// Configuration data structures extensions needed for the wizard + +bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) +{ + this->preset_bundle = std::make_unique(); + this->is_in_resources = ais_in_resources; + this->is_prusa_bundle = ais_prusa_bundle; + + std::string path_string = source_path.string(); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( + path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + UNUSED(config_substitutions); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); + auto first_vendor = preset_bundle->vendors.begin(); + if (first_vendor == preset_bundle->vendors.end()) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; + return false; + } + if (presets_loaded == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + return false; + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + this->vendor_profile = &first_vendor->second; + return true; +} + +Bundle::Bundle(Bundle &&other) + : preset_bundle(std::move(other.preset_bundle)) + , vendor_profile(other.vendor_profile) + , is_in_resources(other.is_in_resources) + , is_prusa_bundle(other.is_prusa_bundle) +{ + other.vendor_profile = nullptr; +} + +BundleMap BundleMap::load() +{ + BundleMap res; + + const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + auto prusa_bundle_rsrc = false; + if (! boost::filesystem::exists(prusa_bundle_path)) { + prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + prusa_bundle_rsrc = true; + } + { + Bundle prusa_bundle; + if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) + res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); + } + + // Load the other bundles in the datadir/vendor directory + // and then additionally from resources/profiles. + bool is_in_resources = false; + for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { + for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { + if (Slic3r::is_ini_file(dir_entry)) { + std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part + + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } + + Bundle bundle; + if (bundle.load(dir_entry.path(), is_in_resources)) + res.emplace(std::move(id), std::move(bundle)); + } + } + + is_in_resources = true; + } + + return res; +} + +Bundle& BundleMap::prusa_bundle() +{ + auto it = find(PresetBundle::PRUSA_BUNDLE); + if (it == end()) { + throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); + } + + return it->second; +} + +const Bundle& BundleMap::prusa_bundle() const +{ + return const_cast(this)->prusa_bundle(); +} + + +// Printer model picker GUI control + +struct PrinterPickerEvent : public wxEvent +{ + std::string vendor_id; + std::string model_id; + std::string variant_name; + bool enable; + + PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) + : wxEvent(winid, eventType) + , vendor_id(std::move(vendor_id)) + , model_id(std::move(model_id)) + , variant_name(std::move(variant_name)) + , enable(enable) + {} + + virtual wxEvent *Clone() const + { + return new PrinterPickerEvent(*this); + } +}; + +wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); + +const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) + : wxPanel(parent) + , vendor_id(vendor.id) + , width(0) +{ + wxGetApp().UpdateDarkUI(this); + const auto &models = vendor.models; + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + const auto font_title = GetFont().MakeBold().Scaled(1.3f); + const auto font_name = GetFont().MakeBold(); + const auto font_alt_nozzle = GetFont().Scaled(0.9f); + + // wxGrid appends widgets by rows, but we need to construct them in columns. + // These vectors are used to hold the elements so that they can be appended in the right order. + std::vector titles; + std::vector bitmaps; + std::vector variants_panels; + + int max_row_width = 0; + int current_row_width = 0; + + bool is_variants = false; + + for (const auto &model : models) { + if (! filter(model)) { continue; } + + wxBitmap bitmap; + int bitmap_width = 0; + auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { + if (wxFileExists(bitmap_file)) { + bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); + bitmap_width = bitmap.GetWidth(); + return true; + } + return false; + }; + if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") + % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") + % vendor.id + % model.id; + load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); + } + } + auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(font_name); + const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); + title->Wrap(wrap_width); + + current_row_width += wrap_width; + if (titles.size() % max_cols == max_cols - 1) { + max_row_width = std::max(max_row_width, current_row_width); + current_row_width = 0; + } + + titles.push_back(title); + + auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); + bitmaps.push_back(bitmap_widget); + + auto *variants_panel = new wxPanel(this); + wxGetApp().UpdateDarkUI(variants_panel); + auto *variants_sizer = new wxBoxSizer(wxVERTICAL); + variants_panel->SetSizer(variants_sizer); + const auto model_id = model.id; + + for (size_t i = 0; i < model.variants.size(); i++) { + const auto &variant = model.variants[i]; + + const auto label = model.technology == ptFFF + ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) + : from_u8(model.name); + + if (i == 1) { + auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); + alt_label->SetFont(font_alt_nozzle); + variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); + is_variants = true; + } + + auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); + i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); + + const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); + cbox->SetValue(enabled); + + variants_sizer->Add(cbox, 0, wxBOTTOM, 3); + + cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { + on_checkbox(cbox, event.IsChecked()); + }); + } + + variants_panels.push_back(variants_panel); + } + + width = std::max(max_row_width, current_row_width); + + const size_t cols = std::min(max_cols, titles.size()); + + auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); + + if (titles.size() > 0) { + const size_t odd_items = titles.size() % cols; + + for (size_t i = 0; i < titles.size() - odd_items; i += cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } + + // Add separator space to multiliners + if (titles.size() > cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } + } + } + if (odd_items > 0) { + const size_t rem = titles.size() - odd_items; + + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } + } + } + + auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); + if (! title.IsEmpty()) { + auto *title_widget = new wxStaticText(this, wxID_ANY, title); + title_widget->SetFont(font_title); + title_sizer->Add(title_widget); + } + title_sizer->AddStretchSpacer(); + + if (titles.size() > 1 || is_variants) { + // It only makes sense to add the All / None buttons if there's multiple printers + // All Standard button is added when there are more variants for at least one printer + auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + if (is_variants) + sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); + sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); + sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); + if (is_variants) + title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(sel_all_std); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + // fill button indexes used later for buttons rescaling + if (is_variants) + m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; + else { + sel_all_std->Destroy(); + m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; + } + } + + sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); + sizer->Add(printer_grid); + + SetSizer(sizer); +} + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) + : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) +{} + +void PrinterPicker::select_all(bool select, bool alternates) +{ + for (const auto &cb : cboxes) { + if (cb->GetValue() != select) { + cb->SetValue(select); + on_checkbox(cb, select); + } + } + + if (! select) { alternates = false; } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue() != alternates) { + cb->SetValue(alternates); + on_checkbox(cb, alternates); + } + } +} + +void PrinterPicker::select_one(size_t i, bool select) +{ + if (i < cboxes.size() && cboxes[i]->GetValue() != select) { + cboxes[i]->SetValue(select); + on_checkbox(cboxes[i], select); + } +} + +bool PrinterPicker::any_selected() const +{ + for (const auto &cb : cboxes) { + if (cb->GetValue()) { return true; } + } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue()) { return true; } + } + + return false; +} + +std::set PrinterPicker::get_selected_models() const +{ + std::set ret_set; + + for (const auto& cb : cboxes) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + for (const auto& cb : cboxes_alt) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + return ret_set; +} + +void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) +{ + PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); + AddPendingEvent(evt); +} + + +// Wizard page base + +ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) + : wxPanel(parent->p->hscroll) + , parent(parent) + , shortname(std::move(shortname)) + , indent(indent) +{ + wxGetApp().UpdateDarkUI(this); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + const auto font = GetFont().MakeBold().Scaled(1.5); + text->SetFont(font); + sizer->Add(text, 0, wxALIGN_LEFT, 0); + sizer->AddSpacer(10); + + content = new wxBoxSizer(wxVERTICAL); + sizer->Add(content, 1, wxEXPAND); + + SetSizer(sizer); + + // There is strange layout on Linux with GTK3, + // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 + // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages + if (!wxLinux_gtk3) + this->Hide(); + + Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { + this->Layout(); + event.Skip(); + }); +} + +ConfigWizardPage::~ConfigWizardPage() {} + +wxStaticText* ConfigWizardPage::append_text(wxString text) +{ + auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + widget->Wrap(WRAP_WIDTH); + widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); + append(widget); + return widget; +} + +void ConfigWizardPage::append_spacer(int space) +{ + // FIXME: scaling + content->AddSpacer(space); +} + +// Wizard pages + +PageWelcome::PageWelcome(ConfigWizard *parent) + : ConfigWizardPage(parent, from_u8((boost::format( +#ifdef __APPLE__ + _utf8(L("Welcome to the %s Configuration Assistant")) +#else + _utf8(L("Welcome to the %s Configuration Wizard")) +#endif + ) % SLIC3R_APP_NAME).str()), _L("Welcome")) + , welcome_text(append_text(from_u8((boost::format( + _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) + % SLIC3R_APP_NAME + % _utf8(ConfigWizard::name())).str()) + )) + , cbox_reset(append( + new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) + )) + , cbox_integrate(append( + new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) + )) +{ + welcome_text->Hide(); + cbox_reset->Hide(); + cbox_integrate->Hide(); +} + +void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) +{ + const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; + welcome_text->Show(data_empty); + cbox_reset->Show(!data_empty); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + if (!DesktopIntegrationDialog::is_integrated()) + cbox_integrate->Show(true); + else + cbox_integrate->Hide(); +#else + cbox_integrate->Hide(); +#endif +} + + +PagePrinters::PagePrinters(ConfigWizard *parent, + wxString title, + wxString shortname, + const VendorProfile &vendor, + unsigned indent, + Technology technology) + : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) + , technology(technology) + , install(false) // only used for 3rd party vendors +{ + enum { + COL_SIZE = 200, + }; + + AppConfig *appconfig = &this->wizard_p()->appconfig_new; + + const auto families = vendor.families(); + for (const auto &family : families) { + const auto filter = [&](const VendorProfile::PrinterModel &model) { + return ((model.technology == ptFFF && technology & T_FFF) + || (model.technology == ptSLA && technology & T_SLA)) + && model.family == family; + }; + + if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { + continue; + } + + const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); + auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); + + picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { + appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + wizard_p()->on_printer_pick(this, evt); + }); + + append(new StaticLine(this)); + + append(picker); + printer_pickers.push_back(picker); + has_printers = true; + } + +} + +void PagePrinters::select_all(bool select, bool alternates) +{ + for (auto picker : printer_pickers) { + picker->select_all(select, alternates); + } +} + +int PagePrinters::get_width() const +{ + return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, + [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); +} + +bool PagePrinters::any_selected() const +{ + for (const auto *picker : printer_pickers) { + if (picker->any_selected()) { return true; } + } + + return false; +} + +std::set PagePrinters::get_selected_models() +{ + std::set ret_set; + + for (const auto *picker : printer_pickers) + { + std::set tmp_models = picker->get_selected_models(); + ret_set.insert(tmp_models.begin(), tmp_models.end()); + } + + return ret_set; +} + +void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) +{ + if (is_primary_printer_page + && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) + && printer_pickers.size() > 0 + && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { + printer_pickers[0]->select_one(0, true); + } +} + + +const std::string PageMaterials::EMPTY; + +PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) + : ConfigWizardPage(parent, std::move(title), std::move(shortname)) + , materials(materials) + , list_printer(new StringList(this, wxLB_MULTIPLE)) + , list_type(new StringList(this)) + , list_vendor(new StringList(this)) + , list_profile(new PresetList(this)) +{ + append_spacer(VERTICAL_SPACING); + + const int em = parent->em_unit(); + const int list_h = 30*em; + + + list_printer->SetMinSize(wxSize(23*em, list_h)); + list_type->SetMinSize(wxSize(13*em, list_h)); + list_vendor->SetMinSize(wxSize(13*em, list_h)); + list_profile->SetMinSize(wxSize(23*em, list_h)); + + + + grid = new wxFlexGridSizer(4, em/2, em); + grid->AddGrowableCol(3, 1); + grid->AddGrowableRow(1, 1); + + grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); + grid->Add(new wxStaticText(this, wxID_ANY, list1name)); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); + + grid->Add(list_printer, 0, wxEXPAND); + grid->Add(list_type, 0, wxEXPAND); + grid->Add(list_vendor, 0, wxEXPAND); + grid->Add(list_profile, 1, wxEXPAND); + + auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); + btn_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(list_printer); + wxGetApp().UpdateDarkUI(list_type); + wxGetApp().UpdateDarkUI(list_vendor); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(btn_sizer, 0, wxALIGN_RIGHT); + + append(grid, 1, wxEXPAND); + + append_spacer(VERTICAL_SPACING); + + html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, + wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); + append(html_window, 0, wxEXPAND); + + list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); + }); + list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + + list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); + + sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); + sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); + /* + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); + + list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); + list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); + list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); + */ + reload_presets(); + set_compatible_printers_html_window(std::vector(), false); +} +void PageMaterials::on_paint() +{ +} +void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) +{ + const wxClientDC dc(list_profile); + const wxPoint pos = evt.GetLogicalPosition(dc); + int item = list_profile->HitTest(pos); + on_material_hovered(item); +} +void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) +{} +void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) +{ + on_material_hovered(-1); +} +void PageMaterials::reload_presets() +{ + clear(); + + list_printer->append(_L("(All)"), &EMPTY); + //list_printer->SetLabelMarkup("bald"); + for (const Preset* printer : materials->printers) { + list_printer->append(printer->name, &printer->name); + } + sort_list_data(list_printer, true, false); + if (list_printer->GetCount() > 0) { + list_printer->SetSelection(0); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + update_lists(0, 0, 0); + } + + presets_loaded = true; +} + +void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) +{ + const auto bgr_clr = +#if defined(__APPLE__) + html_window->GetParent()->GetBackgroundColour(); +#else +#if defined(_WIN32) + wxGetApp().get_window_default_clr(); +#else + wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); +#endif +#endif + const auto text_clr = wxGetApp().get_label_clr_default(); + const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); + const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); + wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); + wxString text; + if (all_printers) { + wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "
" + "
" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line + ); + } else { + wxString second_line; + if (!printer_names.empty()) + second_line = (materials->technology == T_FFF ? + _L("Only the following installed printers are compatible with the selected filaments") : + _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line); + for (size_t i = 0; i < printer_names.size(); ++i) + { + text += wxString::Format("", boost::nowide::widen(printer_names[i])); + if (i % 3 == 2) { + text += wxString::Format( + "" + ""); + } + } + text += wxString::Format( + "" + "
%s
" + "
" + "
" + "" + "" + ); + } + + wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html_window->SetPage(text); +} + +void PageMaterials::clear_compatible_printers_label() +{ + set_compatible_printers_html_window(std::vector(), false); +} + +void PageMaterials::on_material_hovered(int sel_material) +{ + +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + if (sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::vector tabs; + tabs.push_back(std::string()); + tabs.push_back(std::string()); + tabs.push_back(std::string()); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + std::vector names; + for (const Preset* printer : materials->printers) { + for (const Preset* material : matching_materials) { + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + names.push_back(printer->name); + break; + } + } + } + set_compatible_printers_html_window(names, names.size() == materials->printers.size()); +} + +void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + // Does our wxWidgets version support operator== for wxArrayInt ? + // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 +#if wxCHECK_VERSION(3, 1, 1) + if (sel_printers != sel_printers_prev) { +#else + auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { + if (arr_first.GetCount() != arr_second.GetCount()) + return false; + for (size_t i = 0; i < arr_first.GetCount(); i++) + if (arr_first[i] != arr_second[i]) + return false; + return true; + }; + if (!are_equal(sel_printers, sel_printers_prev)) { +#endif + + // Refresh type list + list_type->Clear(); + list_type->append(_L("(All)"), &EMPTY); + if (sel_printers_count > 0) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (last_selected_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (sel_printers[0] != 0) { + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } else { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + sel_printers_count = list_printer->GetSelections(sel_printers); + + materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + sort_list_data(list_type, true, true); + } + + sel_printers_prev = sel_printers; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_L("(All)"), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + sort_list_data(list_vendor, true, false); + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // finst printer preset + std::vector to_list; + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + + materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { + const std::string& section = materials->appconfig_section(); + bool checked = wizard_p()->appconfig_new.has(section, p->name); + bool was_checked = false; + + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) { + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); + to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); + } + else { + was_checked = list_profile->IsChecked(cur_i); + to_list[cur_i].checked = checked || was_checked; + } + list_profile->Check(cur_i, checked || was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + sort_list_data(list_profile, to_list); + } + + sel_vendor_prev = sel_vendor; + } + wxGetApp().UpdateDarkUI(list_profile); +} + +void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) +{ +// get data from list +// sort data +// first should be +// then prusa profiles +// then the rest +// in alphabetical order + + std::vector> prusa_profiles; + std::vector> other_profiles; + for (int i = 0 ; i < list->size(); ++i) { + const std::string& data = list->get_data(i); + if (data == EMPTY) // do not sort item + continue; + if (!material_type_ordering && data.find("Prusa") != std::string::npos) + prusa_profiles.push_back(data); + else + other_profiles.push_back(data); + } + if(material_type_ordering) { + + const ConfigOptionDef* def = print_config_def.get("filament_type"); + std::vectorenum_values = def->enum_values; + size_t end_of_sorted = 0; + for (size_t vals = 0; vals < enum_values.size(); vals++) { + for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) + { + // find instead compare because PET vs PETG + if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { + //swap + if(profs != end_of_sorted) { + std::reference_wrapper aux = other_profiles[end_of_sorted]; + other_profiles[end_of_sorted] = other_profiles[profs]; + other_profiles[profs] = aux; + } + end_of_sorted++; + break; + } + } + } + } else { + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + } + + list->Clear(); + if (add_All_item) + list->append(_L("(All)"), &EMPTY); + for (const auto& item : prusa_profiles) + list->append(item, &const_cast(item.get())); + for (const auto& item : other_profiles) + list->append(item, &const_cast(item.get())); +} + +void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) +{ + // sort data + // then prusa profiles + // then the rest + // in alphabetical order + std::vector prusa_profiles; + std::vector other_profiles; + //for (int i = 0; i < data.size(); ++i) { + for (const auto& item : data) { + const std::string& name = item.name; + if (name.find("Prusa") != std::string::npos) + prusa_profiles.emplace_back(item); + else + other_profiles.emplace_back(item); + } + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + list->Clear(); + for (size_t i = 0; i < prusa_profiles.size(); ++i) { + list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); + list->Check(i, prusa_profiles[i].checked); + } + for (size_t i = 0; i < other_profiles.size(); ++i) { + list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); + list->Check(i + prusa_profiles.size(), other_profiles[i].checked); + } +} + +void PageMaterials::select_material(int i) +{ + const bool checked = list_profile->IsChecked(i); + + const std::string& alias_key = list_profile->get_data(i); + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); +} + +void PageMaterials::select_all(bool select) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); + if (current != select) { + list_profile->Check(i, select); + select_material(i); + } + } +} + +void PageMaterials::clear() +{ + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + presets_loaded = false; +} + +void PageMaterials::on_activate() +{ + if (! presets_loaded) { + wizard_p()->update_materials(materials->technology); + reload_presets(); + } + first_paint = true; +} + + +const char *PageCustom::default_profile_name = "My Settings"; + +PageCustom::PageCustom(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) +{ + cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); + auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); + + wxBoxSizer* profile_name_sizer = new wxBoxSizer(wxVERTICAL); + profile_name_editor = new SavePresetDialog::Item{ this, profile_name_sizer, default_profile_name }; + profile_name_editor->Enable(false); + + cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { + profile_name_editor->Enable(custom_wanted()); + wizard_p()->on_custom_setup(custom_wanted()); + }); + + append(cb_custom); + append(label); + append(profile_name_sizer); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) + , version_check(true) + , preset_update(true) +{ + const AppConfig *app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); + box_slic3r->SetValue(app_config->get("notify_release") != "none"); + append(box_slic3r); + append_text(wxString::Format(_L( + "If enabled, %s checks for new application versions online. When a new version becomes available, " + "a notification is displayed at the next application startup (never during program usage). " + "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); + + append_spacer(VERTICAL_SPACING); + + auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); + box_presets->SetValue(app_config->get("preset_update") == "1"); + append(box_presets); + append_text(wxString::Format(_L( + "If enabled, %s downloads updates of built-in system presets in the background." + "These updates are downloaded into a separate temporary location." + "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); + const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(WRAP_WIDTH); + append(label_bold); + append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); +} + +namespace DownloaderUtils +{ +#ifdef _WIN32 + + wxString get_downloads_path() + { + wxString ret; + PWSTR path = NULL; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); + if (SUCCEEDED(hr)) { + ret = wxString(path); + } + CoTaskMemFree(path); + return ret; + } + +#elif __APPLE__ + wxString get_downloads_path() + { + // call objective-c implementation + return wxString::FromUTF8(get_downloads_path_mac()); + } +#else + wxString get_downloads_path() + { + wxString command = "xdg-user-dir DOWNLOAD"; + wxArrayString output; + GUI::desktop_execute_get_result(command, output); + if (output.GetCount() > 0) { + return output[0]; + } + return wxString(); + } + +#endif + +Worker::Worker(wxWindow* parent) +: wxBoxSizer(wxHORIZONTAL) +, m_parent(parent) +{ + m_input_path = new wxTextCtrl(m_parent, wxID_ANY); + set_path_name(get_app_config()->get("url_downloader_dest")); + + auto* path_label = new wxStaticText(m_parent, wxID_ANY, _L("Download path") + ":"); + + this->Add(path_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + this->Add(m_input_path, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); + + auto* button_path = new wxButton(m_parent, wxID_ANY, _L("Browse")); + this->Add(button_path, 0, wxEXPAND | wxTOP | wxLEFT, 5); + button_path->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + boost::filesystem::path chosen_dest(boost::nowide::narrow(m_input_path->GetValue())); + + wxDirDialog dialog(m_parent, L("Choose folder:"), chosen_dest.string() ); + if (dialog.ShowModal() == wxID_OK) + this->m_input_path->SetValue(dialog.GetPath()); + }); + + for (wxSizerItem* item : this->GetChildren()) + if (item->IsWindow()) { + wxWindow* win = item->GetWindow(); + wxGetApp().UpdateDarkUI(win); + } +} + +void Worker::set_path_name(wxString path) +{ + if (path.empty()) + path = boost::nowide::widen(get_app_config()->get("url_downloader_dest")); + + if (path.empty()) { + // What should be default path? Each system has Downloads folder, that could be good one. + // Other would be program location folder - not so good: access rights, apple bin is inside bundle... + // default_path = boost::dll::program_location().parent_path().string(); + path = get_downloads_path(); + } + + m_input_path->SetValue(path); +} + +void Worker::set_path_name(const std::string& name) +{ + if (!m_input_path) + return; + + set_path_name(boost::nowide::widen(name)); +} + +} // DownLoader + +PageDownloader::PageDownloader(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Downloads from URL"), _L("Downloads")) +{ + const AppConfig* app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + append_spacer(VERTICAL_SPACING); + + auto* box_allow_downloads = new wxCheckBox(this, wxID_ANY, _L("Allow build-in downloader")); + // TODO: Do we want it like this? The downloader is allowed for very first time the wizard is run. + bool box_allow_value = (app_config->has("downloader_url_registered") ? app_config->get("downloader_url_registered") == "1" : true); + box_allow_downloads->SetValue(box_allow_value); + append(box_allow_downloads); + + append_text(wxString::Format(_L( + "If enabled, %s registers to start on custom URL on www.printables.com." + " You will be able to use button with %s logo to open models in this %s." + " The model will be downloaded into folder you choose bellow." + ), SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); + +#ifdef __linux__ + append_text(wxString::Format(_L( + "On Linux systems the process of registration also creates desktop integration files for this version of application." + ))); +#endif + + box_allow_downloads->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->downloader->allow(event.IsChecked()); }); + + downloader = new DownloaderUtils::Worker(this); + append(downloader); + downloader->allow(box_allow_value); +} + +bool PageDownloader::on_finish_downloader() const +{ + return downloader->on_finish(); +} + +bool DownloaderUtils::Worker::perform_register() +{ + //boost::filesystem::path chosen_dest/*(path_text_ctrl->GetValue());*/(boost::nowide::narrow(path_text_ctrl->GetValue())); + boost::filesystem::path chosen_dest (GUI::format(path_name())); + boost::system::error_code ec; + if (chosen_dest.empty() || !boost::filesystem::is_directory(chosen_dest, ec) || ec) { + std::string err_msg = GUI::format("%1%\n\n%2%",_L("Chosen directory for downloads does not Exists.") ,chosen_dest.string()); + BOOST_LOG_TRIVIAL(error) << err_msg; + show_error(m_parent, err_msg); + return false; + } + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Directory for downloads: " << chosen_dest.string(); + wxGetApp().app_config->set("url_downloader_dest", chosen_dest.string()); +#ifdef _WIN32 + // Registry key creation for "prusaslicer://" URL + + boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); + // the path to binary needs to be correctly saved in string with respect to localized characters + wxString wbinary = wxString::FromUTF8(binary_path.string()); + std::string binary_string = (boost::format("%1%") % wbinary).str(); + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << binary_string; + + //std::string key_string = "\"" + binary_string + "\" \"-u\" \"%1\""; + //std::string key_string = "\"" + binary_string + "\" \"%1\""; + std::string key_string = "\"" + binary_string + "\" \"--single-instance\" \"%1\""; + + wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\prusaslicer"); + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_first.Exists()) { + key_first.Create(false); + } + key_first.SetValue("URL Protocol", ""); + + if (!key_full.Exists()) { + key_full.Create(false); + } + //key_full = "\"C:\\Program Files\\Prusa3D\\PrusaSlicer\\prusa-slicer-console.exe\" \"%1\""; + key_full = key_string; +#elif __APPLE__ + // Apple registers for custom url in info.plist thus it has to be already registered since build. + // The url will always trigger opening of prusaslicer and we have to check that user has allowed it. (GUI_App::MacOpenURL is the triggered method) +#else + // the performation should be called later during desktop integration + perform_registration_linux = true; +#endif + return true; +} + +void DownloaderUtils::Worker::deregister() +{ +#ifdef _WIN32 + std::string key_string = ""; + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_full.Exists()) { + return; + } + key_full = key_string; +#elif __APPLE__ + // TODO +#else + BOOST_LOG_TRIVIAL(debug) << "DesktopIntegrationDialog::undo_downloader_registration"; + DesktopIntegrationDialog::undo_downloader_registration(); + perform_registration_linux = false; +#endif +} + +bool DownloaderUtils::Worker::on_finish() { + AppConfig* app_config = wxGetApp().app_config; + bool ac_value = app_config->get("downloader_url_registered") == "1"; + BOOST_LOG_TRIVIAL(debug) << "PageDownloader::on_finish_downloader ac_value " << ac_value << " downloader_checked " << downloader_checked; + if (ac_value && downloader_checked) { + // already registered but we need to do it again + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (!ac_value && downloader_checked) { + // register + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (ac_value && !downloader_checked) { + // deregister, downloads are banned now + deregister(); + app_config->set("downloader_url_registered", "0"); + } /*else if (!ac_value && !downloader_checked) { + // not registered and we dont want to do it + // do not deregister as other instance might be registered + } */ + return true; +} + + +PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) + , full_pathnames(false) +{ + auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); + box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); + append(box_pathnames); + append_text(_L( + "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" + "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." + )); + + box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); +} + +#ifdef _WIN32 +PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) +{ + cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); + cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); +// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); + + append(cb_3mf); + append(cb_stl); +// append(cb_gcode); +} +#endif // _WIN32 + +PageMode::PageMode(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) +{ + append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" + "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively.")); + + radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); + radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); + radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); + + std::string mode { "simple" }; + wxGetApp().app_config->get("", "view_mode", mode); + + if (mode == "advanced") { radio_advanced->SetValue(true); } + else if (mode == "expert") { radio_expert->SetValue(true); } + else { radio_simple->SetValue(true); } + + append(radio_simple); + append(radio_advanced); + append(radio_expert); + + append_text("\n" + _L("The size of the object can be specified in inches")); + check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); + check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); + append(check_inch); + + on_activate(); +} + +void PageMode::serialize_mode(AppConfig *app_config) const +{ + std::string mode = ""; + + if (radio_simple->GetValue()) { mode = "simple"; } + if (radio_advanced->GetValue()) { mode = "advanced"; } + if (radio_expert->GetValue()) { mode = "expert"; } + + app_config->set("view_mode", mode); + app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); +} + +PageVendors::PageVendors(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) +{ + const AppConfig &appconfig = this->wizard_p()->appconfig_new; + + append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); + + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); + }); + + const auto &vendors = appconfig.vendors(); + const bool enabled = vendors.find(pair.first) != vendors.end(); + if (enabled) { + cbox->SetValue(true); + + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; + } + + append(cbox); + } +} + +PageFirmware::PageFirmware(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) + , gcode_opt(*print_config_def.get("gcode_flavor")) + , gcode_picker(nullptr) +{ + append_text(_L("Choose the type of firmware used by your printer.")); + append_text(_(gcode_opt.tooltip)); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + wxGetApp().UpdateDarkUI(gcode_picker); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + auto sel = gcode_picker->GetSelection(); + if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { + auto *opt = new ConfigOptionEnum(static_cast(sel)); + config.set_key_value("gcode_flavor", opt); + } +} + +static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) +{ + e.Skip(); + wxString str = ctrl->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + if (val == 0.0) + val = def_value; + ctrl->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + // On Windows, this SetFocus creates an invisible marker. + //ctrl->SetFocus(); + } + else if (was_replaced) + ctrl->SetValue(double_to_string(val)); +} + +class DiamTextCtrl : public wxTextCtrl +{ +public: + DiamTextCtrl(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxBORDER_SIMPLE; +#else + long style = 0; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); + wxGetApp().UpdateDarkUI(this); + } + ~DiamTextCtrl() {} +}; + +PageBedShape::PageBedShape(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) + , shape_panel(new BedShapePanel(this)) +{ + append_text(_L("Set the shape of your printer's bed.")); + + shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), + *wizard_p()->custom_config->option("bed_custom_texture"), + *wizard_p()->custom_config->option("bed_custom_model")); + + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const std::vector& points = shape_panel->get_shape(); + const std::string& custom_texture = shape_panel->get_custom_texture(); + const std::string& custom_model = shape_panel->get_custom_model(); + config.set_key_value("bed_shape", new ConfigOptionPoints(points)); + config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); + config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); +} + +PageBuildVolume::PageBuildVolume(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1) + , build_volume(new DiamTextCtrl(this)) +{ + append_text(_L("Set verctical size of your printer.")); + + wxString value = "200"; + build_volume->SetValue(value); + + build_volume->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + double def_value = 200.0; + double max_value = 1200.0; + e.Skip(); + wxString str = build_volume->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val < 0.0) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val > max_value) { + val = max_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (was_replaced) + build_volume->SetValue(double_to_string(val)); + }, build_volume->GetId()); + + auto* sizer_volume = new wxFlexGridSizer(3, 5, 5); + auto* text_volume = new wxStaticText(this, wxID_ANY, _L("Max print height:")); + auto* unit_volume = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_volume->AddGrowableCol(0, 1); + sizer_volume->Add(text_volume, 0, wxALIGN_CENTRE_VERTICAL); + sizer_volume->Add(build_volume); + sizer_volume->Add(unit_volume, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_volume); +} + +void PageBuildVolume::apply_custom_config(DynamicPrintConfig& config) +{ + double val = 0.0; + build_volume->GetValue().ToDouble(&val); + auto* opt_volume = new ConfigOptionFloat(val); + config.set_key_value("max_print_height", opt_volume); +} + +PageDiameters::PageDiameters(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) + , diam_nozzle(new DiamTextCtrl(this)) + , diam_filam (new DiamTextCtrl(this)) +{ + auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); + wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + diam_nozzle->SetValue(value); + + auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); + value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + diam_filam->SetValue(value); + + diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); + diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); + + append_text(_L("Enter the diameter of your printer's hot end nozzle.")); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); + auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_nozzle->AddGrowableCol(0, 1); + sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + sizer_nozzle->Add(diam_nozzle); + sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the diameter of your filament.")); + append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); + auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_filam->AddGrowableCol(0, 1); + sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(diam_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_filam); +} + +void PageDiameters::apply_custom_config(DynamicPrintConfig &config) +{ + double val = 0.0; + diam_nozzle->GetValue().ToDouble(&val); + auto *opt_nozzle = new ConfigOptionFloats(1, val); + config.set_key_value("nozzle_diameter", opt_nozzle); + + val = 0.0; + diam_filam->GetValue().ToDouble(&val); + auto * opt_filam = new ConfigOptionFloats(1, val); + config.set_key_value("filament_diameter", opt_filam); + + auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { + char buf[64]; // locales don't matter here (sprintf/atof) + sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); + config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); + }; + + set_extrusion_width("support_material_extrusion_width", 0.35); + set_extrusion_width("top_infill_extrusion_width", 0.40); + set_extrusion_width("first_layer_extrusion_width", 0.42); + + set_extrusion_width("extrusion_width", 0.45); + set_extrusion_width("perimeter_extrusion_width", 0.45); + set_extrusion_width("external_perimeter_extrusion_width", 0.45); + set_extrusion_width("infill_extrusion_width", 0.45); + set_extrusion_width("solid_infill_extrusion_width", 0.45); +} + +class SpinCtrlDouble: public wxSpinCtrlDouble +{ +public: + SpinCtrlDouble(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; +#else + long style = wxSP_ARROW_KEYS; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this->GetText()); +#endif + this->Refresh(); + } + ~SpinCtrlDouble() {} +}; + +PageTemperatures::PageTemperatures(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) + , spin_extr(new SpinCtrlDouble(this)) + , spin_bed (new SpinCtrlDouble(this)) +{ + spin_extr->SetIncrement(5.0); + const auto &def_extr = *print_config_def.get("temperature"); + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = def_extr.get_default_value(); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5.0); + const auto &def_bed = *print_config_def.get("bed_temperature"); + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = def_bed.get_default_value(); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_L("Enter the temperature needed for extruding your filament.")); + append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); + append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) + : wxPanel(parent) + , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) + , bullet_black(ScalableBitmap(parent, "bullet_black.png")) + , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) + , bullet_white(ScalableBitmap(parent, "bullet_white.png")) + , item_active(NO_ITEM) + , item_hover(NO_ITEM) + , last_page((size_t)-1) +{ +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif //__WXOSX__ + SetMinSize(bg.GetSize()); + + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); + Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); + + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { + if (item_hover != -1) { + item_hover = -1; + Refresh(); + } + evt.Skip(); + }); + + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { + if (item_hover >= 0) { go_to(item_hover); } + }); +} + +wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +void ConfigWizardIndex::add_page(ConfigWizardPage *page) +{ + last_page = items.size(); + items.emplace_back(Item { page->shortname, page->indent, page }); + Refresh(); +} + +void ConfigWizardIndex::add_label(wxString label, unsigned indent) +{ + items.emplace_back(Item { std::move(label), indent, nullptr }); + Refresh(); +} + +ConfigWizardPage* ConfigWizardIndex::active_page() const +{ + if (item_active >= items.size()) { return nullptr; } + + return items[item_active].page; +} + +void ConfigWizardIndex::go_prev() +{ + // Search for a preceiding item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active; i > 0; i--) { + if (items[i - 1].page != nullptr) { + go_to(i - 1); + return; + } + } +} + +void ConfigWizardIndex::go_next() +{ + // Search for a next item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active + 1; i < items.size(); i++) { + if (items[i].page != nullptr) { + go_to(i); + return; + } + } +} + +// This one actually performs the go-to op +void ConfigWizardIndex::go_to(size_t i) +{ + if (i != item_active + && i < items.size() + && items[i].page != nullptr) { + auto *new_active = items[i].page; + auto *former_active = active_page(); + if (former_active != nullptr) { + former_active->Hide(); + } + + item_active = i; + new_active->Show(); + + wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); + AddPendingEvent(evt); + + Refresh(); + + new_active->on_activate(); + } +} + +void ConfigWizardIndex::go_to(const ConfigWizardPage *page) +{ + if (page == nullptr) { return; } + + for (size_t i = 0; i < items.size(); i++) { + if (items[i].page == page) { + go_to(i); + return; + } + } +} + +void ConfigWizardIndex::clear() +{ + auto *former_active = active_page(); + if (former_active != nullptr) { former_active->Hide(); } + + items.clear(); + item_active = NO_ITEM; +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + const auto size = GetClientSize(); + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } + + wxPaintDC dc(this); + + const auto bullet_w = bullet_black.GetWidth(); + const auto bullet_h = bullet_black.GetHeight(); + const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; + const int yinc = item_height(); + + int index_width = 0; + + unsigned y = 0; + for (size_t i = 0; i < items.size(); i++) { + const Item& item = items[i]; + unsigned x = em_w/2 + item.indent * em_w; + + if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { + dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); + } + else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } + else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } + + x += + bullet_w + em_w/2; + const auto text_size = dc.GetTextExtent(item.label); + dc.SetTextForeground(wxGetApp().get_label_clr_default()); + dc.DrawText(item.label, x, y + yoff_text); + + y += yinc; + index_width = std::max(index_width, (int)x + text_size.x); + } + + //draw logo + if (int y = size.y - bg.GetHeight(); y>=0) { + dc.DrawBitmap(bg.get_bitmap(), 0, y, false); + index_width = std::max(index_width, bg.GetWidth() + em_w / 2); + } + + if (GetMinSize().x < index_width) { + CallAfter([this, index_width]() { + SetMinSize(wxSize(index_width, GetMinSize().y)); + Refresh(); + }); + } +} + +void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) +{ + const wxClientDC dc(this); + const wxPoint pos = evt.GetLogicalPosition(dc); + + const ssize_t item_hover_new = pos.y / item_height(); + + if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { + item_hover = item_hover_new; + Refresh(); + } + + evt.Skip(); +} + +void ConfigWizardIndex::msw_rescale() +{ + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + SetMinSize(bg.GetSize()); + + Refresh(); +} + + +// Materials + +const std::string Materials::UNKNOWN = "(Unknown)"; + +void Materials::push(const Preset *preset) +{ + presets.emplace_back(preset); + types.insert(technology & T_FFF + ? Materials::get_filament_type(preset) + : Materials::get_material_type(preset)); +} + +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + +void Materials::clear() +{ + presets.clear(); + types.clear(); + printers.clear(); + compatibility_counter.clear(); +} + +const std::string& Materials::appconfig_section() const +{ + return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; +} + +const std::string& Materials::get_type(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); +} + +const std::string& Materials::get_vendor(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); +} + +const std::string& Materials::get_filament_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_type"); + if (opt != nullptr && opt->values.size() > 0) { + return opt->values[0]; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_filament_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +const std::string& Materials::get_material_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_type"); + if (opt != nullptr) { + return opt->value; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_material_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +// priv + +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + +void ConfigWizard::priv::load_pages() +{ + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); + + index->clear(); + + index->add_page(page_welcome); + + // Printers + if (!only_sla_mode) + index->add_page(page_fff); + index->add_page(page_msla); + if (!only_sla_mode) { + index->add_page(page_vendors); + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && page->install) + index->add_page(page); + } + + index->add_page(page_custom); + if (page_custom->custom_wanted()) { + index->add_page(page_firmware); + index->add_page(page_bed); + index->add_page(page_bvolume); + index->add_page(page_diams); + index->add_page(page_temps); + } + + // Filaments & Materials + if (any_fff_selected) { index->add_page(page_filaments); } + } + if (any_sla_selected) { index->add_page(page_sla_materials); } + + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); + + index->add_page(page_update); + index->add_page(page_downloader); + index->add_page(page_reload_from_disk); +#ifdef _WIN32 + index->add_page(page_files_association); +#endif // _WIN32 + index->add_page(page_mode); + + index->go_to(former_active); // Will restore the active item/page if possible + + q->Layout(); +// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig + q->Refresh(); +} + +void ConfigWizard::priv::init_dialog_size() +{ + // Clamp the Wizard size based on screen dimensions + + const auto idx = wxDisplay::GetFromWindow(q); + wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); + + const auto disp_rect = display.GetClientArea(); + wxRect window_rect( + disp_rect.x + disp_rect.width / 20, + disp_rect.y + disp_rect.height / 20, + 9*disp_rect.width / 10, + 9*disp_rect.height / 10); + + const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution + if (width_hint < window_rect.width) { + window_rect.x += (window_rect.width - width_hint) / 2; + window_rect.width = width_hint; + } + + q->SetSize(window_rect); +} + +void ConfigWizard::priv::load_vendors() +{ + bundles = BundleMap::load(); + + // Load up the set of vendors / models / variants the user has had enabled up till now + AppConfig *app_config = wxGetApp().app_config; + if (! app_config->legacy_datadir()) { + appconfig_new.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_new.set_variant("PrusaResearch", model, variant, true); + } + } + + // Initialize the is_visible flag in printer Presets + for (auto &pair : bundles) { + pair.second.preset_bundle->load_installed_printers(appconfig_new); + } + + // Copy installed filaments and SLA material names from app_config to appconfig_new + // while resolving current names of profiles, which were renamed in the meantime. + for (PrinterTechnology technology : { ptFFF, ptSLA }) { + const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + std::map section_new; + if (app_config->has_section(section_name)) { + const std::map §ion_old = app_config->get_section(section_name); + for (const auto& material_name_and_installed : section_old) + if (material_name_and_installed.second == "1") { + // Material is installed. Resolve it in bundles. + size_t num_found = 0; + const std::string &material_name = material_name_and_installed.first; + for (auto &bundle : bundles) { + const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); + const Preset *preset = materials.find_preset(material_name); + if (preset == nullptr) { + // Not found. Maybe the material preset is there, bu it was was renamed? + const std::string *new_name = materials.get_preset_name_renamed(material_name); + if (new_name != nullptr) + preset = materials.find_preset(*new_name); + } + if (preset != nullptr) { + // Materal preset was found, mark it as installed. + section_new[preset->name] = "1"; + ++ num_found; + } + } + if (num_found == 0) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; + else if (num_found > 1) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; + } + } + appconfig_new.set_section(section_name, section_new); + }; +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); + all_pages.push_back(page); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) +{ + switch (start_page) { + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; + } +} + +void ConfigWizard::priv::create_3rdparty_pages() +{ + for (const auto &pair : bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } + + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); + } +} + +void ConfigWizard::priv::set_run_reason(RunReason run_reason) +{ + this->run_reason = run_reason; + for (auto &page : all_pages) { + page->set_run_reason(run_reason); + } +} + +void ConfigWizard::priv::update_materials(Technology technology) +{ + if (any_fff_selected && (technology & T_FFF)) { + filaments.clear(); + aliases_fff.clear(); + // Iterate filaments in all bundles + for (const auto &pair : bundles) { + for (const auto &filament : pair.second.preset_bundle->filaments) { + // Check if filament is already added + if (filaments.containts(&filament)) + continue; + // Iterate printers in all bundles + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer(&printer); + } + } + + } + } + // count compatible printers + for (const auto& preset : filaments.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < filaments.presets.size(); ++i) { + if (preset->alias == filaments.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : filaments.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + filaments.compatibility_counter.emplace_back(preset->alias, counter); + } + } + + if (any_sla_selected && (technology & T_SLA)) { + sla_materials.clear(); + aliases_sla.clear(); + + // Iterate SLA materials in all bundles + for (const auto &pair : bundles) { + for (const auto &material : pair.second.preset_bundle->sla_materials) { + // Check if material is already added + if (sla_materials.containts(&material)) + continue; + // Iterate printers in all bundles + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { + sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); + } + sla_materials.add_printer(&printer); + } + } + } + } + // count compatible printers + for (const auto& preset : sla_materials.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < sla_materials.presets.size(); ++i) { + if(preset->alias == sla_materials.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : sla_materials.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + sla_materials.compatibility_counter.emplace_back(preset->alias, counter); + } + } +} + +void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) +{ + custom_printer_selected = custom_wanted; + load_pages(); +} + +void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) +{ + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); + + load_pages(); + } + + // Update the is_visible flag on relevant printer profiles + for (auto &pair : bundles) { + if (pair.first != evt.vendor_id) { continue; } + + for (auto &preset : pair.second.preset_bundle->printers) { + if (preset.config.opt_string("printer_model") == evt.model_id + && preset.config.opt_string("printer_variant") == evt.variant_name) { + preset.is_visible = evt.enable; + } + } + + // When a printer model is picked, but there is no material installed compatible with this printer model, + // install default materials for selected printer model silently. + check_and_install_missing_materials(page->technology, evt.model_id); + } + + if (page->technology & T_FFF) { + page_filaments->clear(); + } else if (page->technology & T_SLA) { + page_sla_materials->clear(); + } +} + +void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) +{ + PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + for (const std::string& material : printer_model.default_materials) + appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); +} + +void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) +{ + PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + const std::string &appconfig_section = page_materials->materials->appconfig_section(); + + // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. + // Filament is selected on same page for all printers of same technology. + /* + auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) + { + const std::string vendor_id = page_printers->get_vendor_id(); + for (auto& pair : bundles) + if (pair.first == vendor_id) + for (const VendorProfile::PrinterModel *printer_model : printer_models) + for (const std::string &material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + }; + + PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; + select_default_materials_for_printer_page(page_printers, technology); + + for (const auto& printer : pages_3rdparty) + { + page_printers = technology & T_FFF ? printer.second.first : printer.second.second; + if (page_printers) + select_default_materials_for_printer_page(page_printers, technology); + } + */ + + // Iterate printer_models and select default materials. If none available -> msg to user. + std::vector models_without_default; + for (const VendorProfile::PrinterModel* printer_model : printer_models) { + if (printer_model->default_materials.empty()) { + models_without_default.emplace_back(printer_model); + } else { + for (const std::string& material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + } + } + + if (!models_without_default.empty()) { + std::string printer_names = "\n\n"; + for (const VendorProfile::PrinterModel* printer_model : models_without_default) { + printer_names += printer_model->name + "\n"; + } + printer_names += "\n\n"; + std::string message = (technology & T_FFF ? + GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : + GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); + MessageDialog msg(q, message, _L("Notice"), wxOK); + msg.ShowModal(); + } + + update_materials(technology); + ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); +} + +void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) +{ + auto it = pages_3rdparty.find(vendor->id); + wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); + + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + // if some 3rd vendor is selected, select first printer for them + if (install) + page->printer_pickers[0]->select_one(0, true); + page->Layout(); + } + + load_pages(); +} + +bool ConfigWizard::priv::on_bnt_finish() +{ + wxBusyCursor wait; + + if (!page_downloader->on_finish_downloader()) { + index->go_to(page_downloader); + return false; + } + /* When Filaments or Sla Materials pages are activated, + * materials for this pages are automaticaly updated and presets are reloaded. + * + * But, if _Finish_ button was clicked without activation of those pages + * (for example, just some printers were added/deleted), + * than last changes wouldn't be updated for filaments/materials. + * SO, do that before close of Wizard + */ + update_materials(T_ANY); + if (any_fff_selected) + page_filaments->reload_presets(); + if (any_sla_selected) + page_sla_materials->reload_presets(); + + // theres no need to check that filament is selected if we have only custom printer + if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; + // check, that there is selected at least one filament/material + return check_and_install_missing_materials(T_ANY); +} + +// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed +// for each Printer preset of each Printer Model installed. +// +// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. +// Otherwise the user is quieried whether to install the missing default materials or not. +// +// Return true if the tested Printer Models already had materials installed. +// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these +// respective Printer Models or not. +bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) +{ + // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, + // which is compatible with it. + const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) + { + const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); + std::set printer_models_without_material; + for (const auto &pair : bundles) { + const PresetCollection &materials = pair.second.preset_bundle->materials(technology); + for (const auto &printer : pair.second.preset_bundle->printers) { + if (printer.is_visible && printer.printer_technology() == technology) { + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + assert(printer_model != nullptr); + if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && + printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { + bool has_material = false; + for (const auto& preset : appconfig_presets) { + if (preset.second == "1") { + const Preset *material = materials.find_preset(preset.first, false); + if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + } + } + if (! has_material) + printer_models_without_material.insert(printer_model); + } + } + } + } + assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); + return printer_models_without_material; + }; + + const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) + { + //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); + MessageDialog msg(q, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_YES) + select_default_materials_for_printer_models(technology, printer_models); + }; + + const auto printer_model_list = [](const std::set &printer_models) -> wxString { + wxString out; + for (const VendorProfile::PrinterModel *printer_model : printer_models) { + wxString name = from_u8(printer_model->name); + out += "\t\t"; + out += name; + out += "\n"; + } + return out; + }; + + if (any_fff_selected && (technology & T_FFF)) { + std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following FFF printer models have no filament selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default filaments for these FFF printer models?"), + printer_models_without_material, + T_FFF); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); + return false; + } + } + + if (any_sla_selected && (technology & T_SLA)) { + std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following SLA printer models have no materials selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default SLA materials for these printer models?"), + printer_models_without_material, + T_SLA); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); + return false; + } + } + + return true; +} + +static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) +{ + auto get_aliases = [](const std::map& data) { + std::set old_aliases; + for (auto item : data) { + const std::string& name = item.first; + size_t pos = name.find("@"); + old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); + } + return old_aliases; + }; + + std::set old_aliases = get_aliases(old_data); + std::set new_aliases = get_aliases(new_data); + std::set diff; + std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); + + return diff; +} + +static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) +{ + std::set diff = get_new_added_presets(old_data, new_data); + if (diff.empty()) + return std::string(); + return *diff.begin(); +} + +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) +{ + wxString header, caption = _L("Configuration is edited in ConfigWizard"); + const auto enabled_vendors = appconfig_new.vendors(); + const auto enabled_vendors_old = app_config->vendors(); + + bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + PrinterTechnology preferred_pt = ptAny; + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + const auto config = enabled_vendors.find(bundle_name); + PrinterTechnology pt = ptAny; + if (config != enabled_vendors.end()) { + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0) { + pt = model.technology; + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + + if (const auto model_it_old = config_old->second.find(model.id); + model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + } + } + } + return pt; + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); + preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) + continue; + else if (preferred_pt == ptAny) + preferred_pt = pt; + if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) + break; + } + } + + if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) + return false; + + bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); + if (check_unsaved_preset_changes) + header = _L("All user presets will be deleted."); + int act_btns = ActionButtons::KEEP; + if (!check_unsaved_preset_changes) + act_btns |= ActionButtons::SAVE; + + // Install bundles from resources if needed: + std::vector install_bundles; + for (const auto &pair : bundles) { + if (! pair.second.is_in_resources) { continue; } + + if (pair.second.is_prusa_bundle) { + // Always install Prusa bundle, because it has a lot of filaments/materials + // likely to be referenced by other profiles. + install_bundles.emplace_back(pair.first); + continue; + } + + const auto vendor = enabled_vendors.find(pair.first); + if (vendor == enabled_vendors.end()) { continue; } + + size_t size_sum = 0; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + + if (size_sum > 0) { + // This vendor needs to be installed + install_bundles.emplace_back(pair.first); + } + } + if (!check_unsaved_preset_changes) + if ((check_unsaved_preset_changes = install_bundles.size() > 0)) + header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); + +#ifdef __linux__ + // Desktop integration on Linux + BOOST_LOG_TRIVIAL(debug) << "ConfigWizard::priv::apply_config integrate_desktop" << page_welcome->integrate_desktop() << " perform_registration_linux " << page_downloader->downloader->get_perform_registration_linux(); + if (page_welcome->integrate_desktop() || page_downloader->downloader->get_perform_registration_linux()) + DesktopIntegrationDialog::perform_desktop_integration(page_downloader->downloader->get_perform_registration_linux()); +#endif + + // Decide whether to create snapshot based on run_reason and the reset profile checkbox + bool snapshot = true; + Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; + switch (run_reason) { + case ConfigWizard::RR_DATA_EMPTY: + snapshot = false; + break; + case ConfigWizard::RR_DATA_LEGACY: + snapshot = true; + break; + case ConfigWizard::RR_DATA_INCOMPAT: + // In this case snapshot has already been taken by + // PresetUpdater with the appropriate reason + snapshot = false; + break; + case ConfigWizard::RR_USER: + snapshot = page_welcome->reset_user_profile(); + snapshot_reason = Snapshot::SNAPSHOT_USER; + break; + } + + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) + return false; + + if (check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + + if (install_bundles.size() > 0) { + // Install bundles from resources. + // Don't create snapshot - we've already done that above if applicable. + if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) + return false; + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; + } + + if (page_welcome->reset_user_profile()) { + BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; + preset_bundle->reset(true); + } + + std::string preferred_model; + std::string preferred_variant; + auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { + const auto config = enabled_vendors.find(bundle_name); + if (config == enabled_vendors.end()) + return std::string(); + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0 && + preferred_pt == model.technology) { + variant = *model_it->second.begin(); + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end()) + return model.id; + const auto model_it_old = config_old->second.find(model.id); + if (model_it_old == config_old->second.end()) + return model.id; + else if (model_it_old->second != model_it->second) { + for (const auto& var : model_it->second) + if (model_it_old->second.find(var) == model_it_old->second.end()) { + variant = var; + return model.id; + } + } + } + } + if (!variant.empty()) + variant.clear(); + return std::string(); + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); + preferred_model.empty()) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); + !preferred_model.empty()) + break; + } + } + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !preferred_model.empty())) { + header = _L("A new Printer was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { + header = _L("Some Printers were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + + std::string first_added_filament, first_added_sla_material; + auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { + if (appconfig_new.has_section(section_name)) { + // get first of new added preset names + const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); + first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); + } + }; + get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); + get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { + header = !first_added_filament.empty() ? + _L("A new filament was installed and it will be activated.") : + _L("A new SLA material was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else { + auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); + }; + bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); + bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); + if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { + header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + } + + // apply materials in app_config + for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) + app_config->set_section(section_name, appconfig_new.get_section(section_name)); + + app_config->set_vendors(appconfig_new); + + app_config->set("notify_release", page_update->version_check ? "all" : "none"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); + +#ifdef _WIN32 + app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); + app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); +// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); + + if (wxGetApp().is_editor()) { + if (page_files_association->associate_3mf()) + wxGetApp().associate_3mf_files(); + if (page_files_association->associate_stl()) + wxGetApp().associate_stl_files(); + } +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } +#endif // _WIN32 + + page_mode->serialize_mode(app_config); + + if (check_unsaved_preset_changes) + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); + + if (!only_sla_mode && page_custom->custom_wanted() && page_custom->is_valid_profile_name()) { + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) + return false; + + page_firmware->apply_custom_config(*custom_config); + page_bed->apply_custom_config(*custom_config); + page_bvolume->apply_custom_config(*custom_config); + page_diams->apply_custom_config(*custom_config); + page_temps->apply_custom_config(*custom_config); + + copy_bed_model_and_texture_if_needed(*custom_config); + + const std::string profile_name = page_custom->profile_name(); + preset_bundle->load_config_from_wizard(profile_name, *custom_config); + } + + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); + + return true; +} +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + assert(! s.empty()); + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + + +// Public + +ConfigWizard::ConfigWizard(wxWindow *parent) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , p(new priv(this)) +{ + this->SetFont(wxGetApp().normal_font()); + + p->load_vendors(); + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto* hline = new StaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. + // Later, we compare that to the size of the current screen and set minimum width based on that (see below). + p->hscroll = new wxScrolledWindow(this); + p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); + p->hscroll->SetSizer(p->hscroll_sizer); + + topsizer->Add(p->index, 0, wxEXPAND); + topsizer->AddSpacer(INDEX_MARGIN); + topsizer->Add(p->hscroll, 1, wxEXPAND); + + p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); + p->btnsizer->Add(p->btn_sel_all); + + p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); + p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); + p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); + p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + wxGetApp().UpdateDarkUI(p->btn_sel_all); + wxGetApp().UpdateDarkUI(p->btn_prev); + wxGetApp().UpdateDarkUI(p->btn_next); + wxGetApp().UpdateDarkUI(p->btn_finish); + wxGetApp().UpdateDarkUI(p->btn_cancel); + + const auto prusa_it = p->bundles.find("PrusaResearch"); + wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); + const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; + + p->add_page(p->page_welcome = new PageWelcome(this)); + + + p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); + p->only_sla_mode = !p->page_fff->has_printers; + if (!p->only_sla_mode) { + p->add_page(p->page_fff); + p->page_fff->is_primary_printer_page = true; + } + + + p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); + p->add_page(p->page_msla); + if (p->only_sla_mode) { + p->page_msla->is_primary_printer_page = true; + } + + if (!p->only_sla_mode) { + // Pages for 3rd party vendors + p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_custom = new PageCustom(this)); + p->custom_printer_selected = p->page_custom->custom_wanted(); + } + + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); + + p->update_materials(T_ANY); + if (!p->only_sla_mode) + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, + _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); + + p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, + _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); + + + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_downloader = new PageDownloader(this)); + p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); +#ifdef _WIN32 + p->add_page(p->page_files_association = new PageFilesAssociation(this)); +#endif // _WIN32 + p->add_page(p->page_mode = new PageMode(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_bvolume = new PageBuildVolume(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + + p->load_pages(); + p->index->go_to(size_t{0}); + + vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + SetSizer(vsizer); + SetSizerAndFit(vsizer); + + // We can now enable scrolling on hscroll + p->hscroll->SetScrollRate(30, 30); + + on_window_geometry(this, [this]() { + p->init_dialog_size(); + }); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if (// Leaving the filaments or SLA materials page and + (active_page == p->page_filaments || active_page == p->page_sla_materials) && + // some Printer models had no filament or SLA material selected. + ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) + // In that case don't leave the page and the function above queried the user whether to install default materials. + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (p->on_bnt_finish()) + this->EndModal(wxID_OK); + }); + + p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { + p->any_sla_selected = true; + p->load_pages(); + p->page_fff->select_all(true, false); + p->page_msla->select_all(true, false); + p->index->go_to(p->page_mode); + }); + + p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { + const bool is_last = p->index->active_is_last(); + p->btn_next->Show(! is_last); + if (is_last) + p->btn_finish->SetFocus(); + + Layout(); + }); + + if (wxLinux_gtk3) + this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + ConfigWizardPage* active_page = p->index->active_page(); + if (!active_page) + return; + for (auto page : p->all_pages) + if (page != active_page) + page->Hide(); + // update best size for the dialog after hiding of the non-active pages + vsizer->SetSizeHints(this); + // set initial dialog size + p->init_dialog_size(); + }); +} + +ConfigWizard::~ConfigWizard() {} + +bool ConfigWizard::run(RunReason reason, StartPage start_page) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; + + GUI_App &app = wxGetApp(); + + p->set_run_reason(reason); + p->set_start_page(start_page); + + if (ShowModal() == wxID_OK) { + bool apply_keeped_changes = false; + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + return false; + + if (apply_keeped_changes) + app.apply_keeped_preset_modifications(); + + app.app_config->set_legacy_datadir(false); + app.update_mode(); + app.obj_manipul()->update_ui_from_settings(); + BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; + return true; + } else { + BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; + return false; + } +} + +const wxString& ConfigWizard::name(const bool from_menu/* = false*/) +{ + // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. + // Note: Don't call _() macro here. + // This function just return the current name according to the OS. + // Translation is implemented inside GUI_App::add_config_menu() +#if __APPLE__ + static const wxString config_wizard_name = L("Configuration Assistant"); + static const wxString config_wizard_name_menu = L("Configuration &Assistant"); +#else + static const wxString config_wizard_name = L("Configuration Wizard"); + static const wxString config_wizard_name_menu = L("Configuration &Wizard"); +#endif + return from_menu ? config_wizard_name_menu : config_wizard_name; +} + +void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) +{ + p->index->msw_rescale(); + + const int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_APPLY, + wxID_CANCEL, + p->btn_sel_all->GetId(), + p->btn_next->GetId(), + p->btn_prev->GetId() }); + + for (auto printer_picker: p->page_fff->printer_pickers) + msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); + + p->init_dialog_size(); + + Refresh(); +} + +void ConfigWizard::on_sys_color_changed() +{ + wxGetApp().UpdateDlgDarkUI(this); + Refresh(); +} + +} +} diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index c1e848a63a..a8ac09d9b1 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -392,10 +392,56 @@ struct PageUpdate: ConfigWizardPage { bool version_check; bool preset_update; + wxTextCtrl* path_text_ctrl; PageUpdate(ConfigWizard *parent); }; +namespace DownloaderUtils { + wxString get_downloads_path(); + +class Worker : public wxBoxSizer +{ + wxWindow* m_parent {nullptr}; + wxTextCtrl* m_input_path {nullptr}; + bool downloader_checked {false}; +#ifdef __linux__ + bool perform_registration_linux { false }; +#endif // __linux__ + + bool perform_register(); + void deregister(); + +public: + Worker(wxWindow* parent); + ~Worker(){} + + void allow(bool allow_) { downloader_checked = allow_; } + bool is_checked() const { return downloader_checked; } + wxString path_name() const { return m_input_path ? m_input_path->GetValue() : wxString(); } + + void set_path_name(wxString name); + void set_path_name(const std::string& name); + + bool on_finish(); + +#ifdef __linux__ + bool get_perform_registration_linux() { return perform_registration_linux; } +#endif // __linux__ +}; + +} + + +struct PageDownloader : ConfigWizardPage +{ + DownloaderUtils::Worker* downloader{ nullptr }; + + PageDownloader(ConfigWizard* parent); + + bool on_finish_downloader() const ; +}; + struct PageReloadFromDisk : ConfigWizardPage { bool full_pathnames; @@ -583,7 +629,8 @@ struct ConfigWizard::priv PageMaterials *page_filaments = nullptr; PageMaterials *page_sla_materials = nullptr; PageCustom *page_custom = nullptr; - PageUpdate *page_update = nullptr; + PageUpdate* page_update = nullptr; + PageDownloader* page_downloader = nullptr; PageReloadFromDisk *page_reload_from_disk = nullptr; #ifdef _WIN32 PageFilesAssociation* page_files_association = nullptr; @@ -631,9 +678,9 @@ struct ConfigWizard::priv bool apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes); // #ys_FIXME_alise void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add); -#ifdef __linux__ - void perform_desktop_integration() const; -#endif +//#ifdef __linux__ +// void perform_desktop_integration() const; +//#endif bool check_fff_selected(); // Used to decide whether to display Filaments page bool check_sla_selected(); // Used to decide whether to display SLA Materials page diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp index 7f99a505c6..26a8f60e5c 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.cpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp @@ -218,10 +218,9 @@ bool DesktopIntegrationDialog::integration_possible() { return true; } -void DesktopIntegrationDialog::perform_desktop_integration() +void DesktopIntegrationDialog::perform_desktop_integration(bool perform_downloader) { - BOOST_LOG_TRIVIAL(debug) << "performing desktop integration"; - + BOOST_LOG_TRIVIAL(debug) << "performing desktop integration. With downloader integration: " << perform_downloader; // Path to appimage const char *appimage_env = std::getenv("APPIMAGE"); std::string excutable_path; @@ -287,7 +286,7 @@ void DesktopIntegrationDialog::perform_desktop_integration() std::string target_dir_icons; std::string target_dir_desktop; - + // slicer icon // iterate thru target_candidates to find icons folder for (size_t i = 0; i < target_candidates.size(); ++i) { @@ -300,20 +299,20 @@ void DesktopIntegrationDialog::perform_desktop_integration() break; // success else target_dir_icons.clear(); // copying failed - // if all failed - try creating default home folder - if (i == target_candidates.size() - 1) { - // create $HOME/.local/share - create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs); - // copy icon - target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir()); - std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir()); - std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix); - if (!contains_path_dir(target_dir_icons, "icons") - || !copy_icon(icon_path, dest_path)) { - // every attempt failed - icon wont be present - target_dir_icons.clear(); - } - } + } + // if all failed - try creating default home folder + if (i == target_candidates.size() - 1) { + // create $HOME/.local/share + create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs); + // copy icon + target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir()); + std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir()); + std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix); + if (!contains_path_dir(target_dir_icons, "icons") + || !copy_icon(icon_path, dest_path)) { + // every attempt failed - icon wont be present + target_dir_icons.clear(); + } } } if(target_dir_icons.empty()) { @@ -324,25 +323,25 @@ void DesktopIntegrationDialog::perform_desktop_integration() // desktop file // iterate thru target_candidates to find applications folder - for (size_t i = 0; i < target_candidates.size(); ++i) - { + + std::string desktop_file = GUI::format( + "[Desktop Entry]\n" + "Name=PrusaSlicer%1%\n" + "GenericName=3D Printing Software\n" + "Icon=PrusaSlicer%2%\n" + "Exec=\"%3%\" %%F\n" + "Terminal=false\n" + "Type=Application\n" + "MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n" + "Categories=Graphics;3DGraphics;Engineering;\n" + "Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n" + "StartupNotify=false\n" + "StartupWMClass=prusa-slicer\n", name_suffix, version_suffix, excutable_path); + + for (size_t i = 0; i < target_candidates.size(); ++i) { if (contains_path_dir(target_candidates[i], "applications")) { target_dir_desktop = target_candidates[i]; // Write slicer desktop file - std::string desktop_file = GUI::format( - "[Desktop Entry]\n" - "Name=PrusaSlicer%1%\n" - "GenericName=3D Printing Software\n" - "Icon=PrusaSlicer%2%\n" - "Exec=\"%3%\" %%F\n" - "Terminal=false\n" - "Type=Application\n" - "MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n" - "Categories=Graphics;3DGraphics;Engineering;\n" - "Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n" - "StartupNotify=false\n" - "StartupWMClass=prusa-slicer\n", name_suffix, version_suffix, excutable_path); - std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix); if (create_desktop_file(path, desktop_file)){ BOOST_LOG_TRIVIAL(debug) << "PrusaSlicer.desktop file installation success."; @@ -352,24 +351,24 @@ void DesktopIntegrationDialog::perform_desktop_integration() BOOST_LOG_TRIVIAL(debug) << "Attempt to PrusaSlicer.desktop file installation failed. failed path: " << target_candidates[i]; target_dir_desktop.clear(); } - // if all failed - try creating default home folder - if (i == target_candidates.size() - 1) { - // create $HOME/.local/share - create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications"); - // create desktop file - target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir()); - std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix); - if (contains_path_dir(target_dir_desktop, "applications")) { - if (!create_desktop_file(path, desktop_file)) { - // Desktop file not written - end desktop integration - BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file"; - return; - } - } else { - // Desktop file not written - end desktop integration - BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed because the application directory was not found."; + } + // if all failed - try creating default home folder + if (i == target_candidates.size() - 1) { + // create $HOME/.local/share + create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications"); + // create desktop file + target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir()); + std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix); + if (contains_path_dir(target_dir_desktop, "applications")) { + if (!create_desktop_file(path, desktop_file)) { + // Desktop file not written - end desktop integration + BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file"; return; } + } else { + // Desktop file not written - end desktop integration + BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed because the application directory was not found."; + return; } } } @@ -398,7 +397,7 @@ void DesktopIntegrationDialog::perform_desktop_integration() } // Desktop file - std::string desktop_file = GUI::format( + std::string desktop_file_viewer = GUI::format( "[Desktop Entry]\n" "Name=Prusa Gcode Viewer%1%\n" "GenericName=3D Printing Software\n" @@ -410,9 +409,8 @@ void DesktopIntegrationDialog::perform_desktop_integration() "Categories=Graphics;3DGraphics;\n" "Keywords=3D;Printing;Slicer;\n" "StartupNotify=false\n", name_suffix, version_suffix, excutable_path); - std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix); - if (create_desktop_file(desktop_path, desktop_file)) + if (create_desktop_file(desktop_path, desktop_file_viewer)) // save path to desktop file app_config->set("desktop_integration_app_viewer_path", desktop_path); else { @@ -421,6 +419,37 @@ void DesktopIntegrationDialog::perform_desktop_integration() } } + if (perform_downloader) + { + std::string desktop_file_downloader = GUI::format( + "[Desktop Entry]\n" + "Name=PrusaSlicer URL Protocol%1%\n" + "Exec=\"%3%\" --single-instance %%u\n" + "Icon=PrusaSlicer%4%\n" + "Terminal=false\n" + "Type=Application\n" + "MimeType=x-scheme-handler/prusaslicer;\n" + "StartupNotify=false\n" + , name_suffix, version_suffix, excutable_path, version_suffix); + + // desktop file for downloader as part of main app + std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerURLProtocol%2%.desktop", target_dir_desktop, version_suffix); + if (create_desktop_file(desktop_path, desktop_file_downloader)) { + // save path to desktop file + app_config->set("desktop_integration_URL_path", desktop_path); + // finish registration on mime type + std::string command = GUI::format("xdg-mime default PrusaSlicerURLProtocol%1%.desktop x-scheme-handler/prusaslicer", version_suffix); + BOOST_LOG_TRIVIAL(debug) << "system command: " << command; + int r = system(command.c_str()); + BOOST_LOG_TRIVIAL(debug) << "system result: " << r; + + } else { + BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create URL Protocol desktop file"; + show_error(nullptr, _L("Performing desktop integration failed - could not create URL Protocol desktop file.")); + return; + } + } + wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess); } void DesktopIntegrationDialog::undo_desktop_intgration() @@ -453,9 +482,26 @@ void DesktopIntegrationDialog::undo_desktop_intgration() std::remove(path.c_str()); } } + // URL Protocol + path = std::string(app_config->get("desktop_integration_URL_path")); + if (!path.empty()) { + BOOST_LOG_TRIVIAL(debug) << "removing " << path; + std::remove(path.c_str()); + } wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess); } +void DesktopIntegrationDialog::undo_downloader_registration() +{ + const AppConfig *app_config = wxGetApp().app_config; + std::string path = std::string(app_config->get("desktop_integration_URL_path")); + if (!path.empty()) { + BOOST_LOG_TRIVIAL(debug) << "removing " << path; + std::remove(path.c_str()); + } + // There is no need to undo xdg-mime default command. It is done automatically when desktop file is deleted. +} + DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) : wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) { @@ -481,7 +527,7 @@ DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent) wxButton *btn_perform = new wxButton(this, wxID_ANY, _L("Perform")); btn_szr->Add(btn_perform, 0, wxALL, 10); - btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(); EndModal(wxID_ANY); }); + btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(false); EndModal(wxID_ANY); }); if (can_undo){ wxButton *btn_undo = new wxButton(this, wxID_ANY, _L("Undo")); diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.hpp b/src/slic3r/GUI/DesktopIntegrationDialog.hpp index 74a0a68f99..08c984083b 100644 --- a/src/slic3r/GUI/DesktopIntegrationDialog.hpp +++ b/src/slic3r/GUI/DesktopIntegrationDialog.hpp @@ -26,9 +26,14 @@ public: // Creates Desktop files and icons for both PrusaSlicer and GcodeViewer. // Stores paths into App Config. // Rewrites if files already existed. - static void perform_desktop_integration(); + // if perform_downloader: + // Creates Destktop files for PrusaSlicer downloader feature + // Regiters PrusaSlicer to start on prusaslicer:// URL + static void perform_desktop_integration(bool perform_downloader); // Deletes Desktop files and icons for both PrusaSlicer and GcodeViewer at paths stored in App Config. static void undo_desktop_intgration(); + + static void undo_downloader_registration(); private: }; diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp new file mode 100644 index 0000000000..157233076d --- /dev/null +++ b/src/slic3r/GUI/Downloader.cpp @@ -0,0 +1,245 @@ +#include "Downloader.hpp" +#include "GUI_App.hpp" +#include "NotificationManager.hpp" + +#include +#include + +namespace Slic3r { +namespace GUI { + +namespace { +void open_folder(const std::string& path) +{ + // Code taken from NotificationManager.cpp + + // Execute command to open a file explorer, platform dependent. + // FIXME: The const_casts aren't needed in wxWidgets 3.1, remove them when we upgrade. + +#ifdef _WIN32 + const wxString widepath = from_u8(path); + const wchar_t* argv[] = { L"explorer", widepath.GetData(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#elif __APPLE__ + const char* argv[] = { "open", path.data(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); +#else + const char* argv[] = { "xdg-open", path.data(), nullptr }; + + // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + } + else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, nullptr); + } +#endif +} + +std::string filename_from_url(const std::string& url) +{ + // TODO: can it be done with curl? + size_t slash = url.find_last_of("/"); + if (slash == std::string::npos && slash != url.size() - 1) + return std::string(); + return url.substr(slash + 1, url.size() - slash + 1); +} +} + +Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : m_id(ID) + , m_filename(filename_from_url(url)) + , m_dest_folder(dest_folder) +{ + assert(boost::filesystem::is_directory(dest_folder)); + m_final_path = dest_folder / m_filename; + m_file_get = std::make_shared(ID, std::move(url), m_filename, evt_handler, dest_folder); +} + +void Download::start() +{ + m_state = DownloadState::DownloadOngoing; + m_file_get->get(); +} +void Download::cancel() +{ + m_state = DownloadState::DownloadStopped; + m_file_get->cancel(); +} +void Download::pause() +{ + //assert(m_state == DownloadState::DownloadOngoing); + // if instead of assert - it can happen that user clicks on pause several times before the pause happens + if (m_state != DownloadState::DownloadOngoing) + return; + m_state = DownloadState::DownloadPaused; + m_file_get->pause(); +} +void Download::resume() +{ + //assert(m_state == DownloadState::DownloadPaused); + if (m_state != DownloadState::DownloadPaused) + return; + m_state = DownloadState::DownloadOngoing; + m_file_get->resume(); +} + + +Downloader::Downloader() + : wxEvtHandler() +{ + //Bind(EVT_DWNLDR_FILE_COMPLETE, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_PROGRESS, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_ERROR, [](const wxCommandEvent& evt) {}); + //Bind(EVT_DWNLDR_FILE_NAME_CHANGE, [](const wxCommandEvent& evt) {}); + + Bind(EVT_DWNLDR_FILE_COMPLETE, &Downloader::on_complete, this); + Bind(EVT_DWNLDR_FILE_PROGRESS, &Downloader::on_progress, this); + Bind(EVT_DWNLDR_FILE_ERROR, &Downloader::on_error, this); + Bind(EVT_DWNLDR_FILE_NAME_CHANGE, &Downloader::on_name_change, this); + Bind(EVT_DWNLDR_FILE_PAUSED, &Downloader::on_paused, this); + Bind(EVT_DWNLDR_FILE_CANCELED, &Downloader::on_canceled, this); +} + +void Downloader::start_download(const std::string& full_url) +{ + assert(m_initialized); + + // TODO: There is a misterious slash appearing in recieved msg on windows +#ifdef _WIN32 + if (!boost::starts_with(full_url, "prusaslicer://open/?file=")) { +#else + if (!boost::starts_with(full_url, "prusaslicer://open?file=")) { +#endif + BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; + // TODO: show error? + return; + } + size_t id = get_next_id(); + // TODO: still same mistery +#ifdef _WIN32 + std::string escaped_url = FileGet::escape_url(full_url.substr(25)); +#else + std::string escaped_url = FileGet::escape_url(full_url.substr(24)); +#endif + // TODO: enable after testing + /* + if (!boost::starts_with(escaped_url, "https://media.printables.com/")) { + BOOST_LOG_TRIVIAL(error) << "Download won't start. Download URL doesn't point to https://media.printables.com : " << escaped_url; + // TODO: show error? + return; + } + */ + std::string text(escaped_url); + m_downloads.emplace_back(std::make_unique(id, std::move(escaped_url), this, m_dest_folder)); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->push_download_URL_progress_notification(id, m_downloads.back()->get_filename(), std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2)); + m_downloads.back()->start(); + BOOST_LOG_TRIVIAL(debug) << "started download"; +} + +void Downloader::on_progress(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + float percent = (float)std::stoi(boost::nowide::narrow(event.GetString())) / 100.f; + //BOOST_LOG_TRIVIAL(error) << "progress " << id << ": " << percent; + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + BOOST_LOG_TRIVIAL(trace) << "Download "<< id << ": " << percent; + ntf_mngr->set_download_URL_progress(id, percent); +} +void Downloader::on_error(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + set_download_state(event.GetInt(), DownloadState::DownloadError); + BOOST_LOG_TRIVIAL(error) << "Download error: " << event.GetString(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_error(id, boost::nowide::narrow(event.GetString())); +} +void Downloader::on_complete(wxCommandEvent& event) +{ + // TODO: is this always true? : + // here we open the file itself, notification should get 1.f progress from on progress. + set_download_state(event.GetInt(), DownloadState::DownloadDone); + wxArrayString paths; + paths.Add(event.GetString()); + wxGetApp().plater()->load_files(paths); +} +bool Downloader::user_action_callback(DownloaderUserAction action, int id) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + switch (action) { + case DownloadUserCanceled: + m_downloads[i]->cancel(); + return true; + case DownloadUserPaused: + m_downloads[i]->pause(); + return true; + case DownloadUserContinued: + m_downloads[i]->resume(); + return true; + case DownloadUserOpenedFolder: + open_folder(m_downloads[i]->get_dest_folder()); + return true; + default: + return false; + } + } + } + return false; +} + +void Downloader::on_name_change(wxCommandEvent& event) +{ + +} + +void Downloader::on_paused(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_paused(id); +} + +void Downloader::on_canceled(wxCommandEvent& event) +{ + size_t id = event.GetInt(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_canceled(id); +} + +void Downloader::set_download_state(int id, DownloadState state) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + m_downloads[i]->set_state(state); + return; + } + } +} + +} +} \ No newline at end of file diff --git a/src/slic3r/GUI/Downloader.hpp b/src/slic3r/GUI/Downloader.hpp new file mode 100644 index 0000000000..84a9a95697 --- /dev/null +++ b/src/slic3r/GUI/Downloader.hpp @@ -0,0 +1,99 @@ +#ifndef slic3r_Downloader_hpp_ +#define slic3r_Downloader_hpp_ + +#include "DownloaderFileGet.hpp" +#include +#include + +namespace Slic3r { +namespace GUI { + +class NotificationManager; + +enum DownloadState +{ + DownloadPending = 0, + DownloadOngoing, + DownloadStopped, + DownloadDone, + DownloadError, + DownloadPaused, + DownloadStateUnknown +}; + +enum DownloaderUserAction +{ + DownloadUserCanceled, + DownloadUserPaused, + DownloadUserContinued, + DownloadUserOpenedFolder +}; + +class Download { +public: + Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + void start(); + void cancel(); + void pause(); + void resume(); + + int get_id() const { return m_id; } + boost::filesystem::path get_final_path() const { return m_final_path; } + std::string get_filename() const { return m_filename; } + DownloadState get_state() const { return m_state; } + void set_state(DownloadState state) { m_state = state; } + std::string get_dest_folder() { return m_dest_folder.string(); } +private: + const int m_id; + std::string m_filename; + boost::filesystem::path m_final_path; + boost::filesystem::path m_dest_folder; + std::shared_ptr m_file_get; + DownloadState m_state { DownloadState::DownloadPending }; +}; + +class Downloader : public wxEvtHandler { +public: + Downloader(); + + bool get_initialized() { return m_initialized; } + void init(const boost::filesystem::path& dest_folder) + { + m_dest_folder = dest_folder; + m_initialized = true; + } + void start_download(const std::string& full_url); + // cancel = false -> just pause + bool user_action_callback(DownloaderUserAction action, int id); +private: + bool m_initialized { false }; + + std::vector> m_downloads; + boost::filesystem::path m_dest_folder; + + size_t m_next_id { 0 }; + size_t get_next_id() { return ++m_next_id; } + + void on_progress(wxCommandEvent& event); + void on_error(wxCommandEvent& event); + void on_complete(wxCommandEvent& event); + void on_name_change(wxCommandEvent& event); + void on_paused(wxCommandEvent& event); + void on_canceled(wxCommandEvent& event); + + void set_download_state(int id, DownloadState state); + /* + bool is_in_state(int id, DownloadState state) const; + DownloadState get_download_state(int id) const; + bool cancel_download(int id); + bool pause_download(int id); + bool resume_download(int id); + bool delete_download(int id); + wxString get_path_of(int id) const; + wxString get_folder_path_of(int id) const; + */ +}; + +} +} +#endif \ No newline at end of file diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp new file mode 100644 index 0000000000..10142c7c12 --- /dev/null +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -0,0 +1,322 @@ +#include "DownloaderFileGet.hpp" + +#include +#include +#include +#include +#include +#include + +#include "format.hpp" + +namespace Slic3r { +namespace GUI { + +const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024; +const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024; + +std::string FileGet::escape_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + int decodelen; + char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +namespace { +unsigned get_current_pid() +{ +#ifdef WIN32 + return GetCurrentProcessId(); +#else + return ::getpid(); +#endif +} +} + +// int = DOWNLOAD ID; string = file path +wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +// int = DOWNLOAD ID; string = error msg +wxDEFINE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent); +// int = DOWNLOAD ID; string = progress percent +wxDEFINE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent); +// int = DOWNLOAD ID; string = name +wxDEFINE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent); +// int = DOWNLOAD ID; +wxDEFINE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent); +// int = DOWNLOAD ID; +wxDEFINE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent); + +struct FileGet::priv +{ + const int m_id; + std::string m_url; + std::string m_filename; + std::thread m_io_thread; + wxEvtHandler* m_evt_handler; + boost::filesystem::path m_dest_folder; + boost::filesystem::path m_tmp_path; // path when ongoing download + std::atomic_bool m_cancel { false }; + std::atomic_bool m_pause { false }; + std::atomic_bool m_stopped { false }; // either canceled or paused - download is not running + size_t m_written { 0 }; + size_t m_absolute_size { 0 }; + priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); + + void get_perform(); +}; + +FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : m_id(ID) + , m_url(std::move(url)) + , m_filename(filename) + , m_evt_handler(evt_handler) + , m_dest_folder(dest_folder) +{ +} + +void FileGet::priv::get_perform() +{ + assert(m_evt_handler); + assert(!m_url.empty()); + assert(!m_filename.empty()); + assert(boost::filesystem::is_directory(m_dest_folder)); + + m_stopped = false; + + // open dest file + if (m_written == 0) + { + boost::filesystem::path dest_path = m_dest_folder / m_filename; + std::string extension = boost::filesystem::extension(dest_path); + std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size()); + std::string final_filename = just_filename; + + size_t version = 0; + while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download"))) + { + ++version; + final_filename = just_filename + "(" + std::to_string(version) + ")"; + } + m_filename = final_filename + extension; + + m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download"); + + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE); + evt->SetString(boost::nowide::widen(m_filename)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + } + + boost::filesystem::path dest_path = m_dest_folder / m_filename; + + wxString temp_path_wstring(m_tmp_path.wstring()); + + std::cout << "dest_path: " << dest_path.string() << std::endl; + std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl; + + BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path); + + FILE* file; + // open file for writting + if (m_written == 0) + file = fopen(temp_path_wstring.c_str(), "wb"); + else + file = fopen(temp_path_wstring.c_str(), "a"); + + assert(file != NULL); + + std:: string range_string = std::to_string(m_written) + "-"; + + size_t written_previously = m_written; + size_t written_this_session = 0; + Http::get(m_url) + .size_limit(DOWNLOAD_SIZE_LIMIT) //more? + .set_range(range_string) + .on_progress([&](Http::Progress progress, bool& cancel) { + // to prevent multiple calls into following ifs (m_cancel / m_pause) + if (m_stopped){ + cancel = true; + return; + } + if (m_cancel) { + m_stopped = true; + fclose(file); + // remove canceled file + std::remove(m_tmp_path.string().c_str()); + m_written = 0; + cancel = true; + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + // TODO: send canceled event? + } + if (m_pause) { + m_stopped = true; + fclose(file); + cancel = true; + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + if (m_absolute_size < progress.dltotal) { + m_absolute_size = progress.dltotal; + } + + if (progress.dlnow != 0) { + if (progress.dlnow - written_this_session > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) { + try + { + std::string part_for_write = progress.buffer.substr(written_this_session, progress.dlnow); + fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); + } + catch (const std::exception& e) + { + // fclose(file); do it? + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString(e.what()); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + cancel = true; + return; + } + written_this_session = progress.dlnow; + m_written = written_previously + written_this_session; + } + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS); + int percent_total = (written_previously + progress.dlnow) * 100 / m_absolute_size; + evt->SetString(std::to_string(percent_total)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + } + + }) + .on_error([&](std::string body, std::string error, unsigned http_status) { + if (file != NULL) + fclose(file); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString(error); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + }) + .on_complete([&](std::string body, unsigned /* http_status */) { + + // TODO: perform a body size check + // + //size_t body_size = body.size(); + //if (body_size != expected_size) { + // return; + //} + try + { + /* + if (m_written < body.size()) + { + // this code should never be entered. As there should be on_progress call after last bit downloaded. + std::string part_for_write = body.substr(m_written); + fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); + } + */ + fclose(file); + boost::filesystem::rename(m_tmp_path, dest_path); + } + catch (const std::exception& /*e*/) + { + //TODO: report? + //error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); + evt->SetString("Failed to write and move."); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + return; + } + + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_COMPLETE); + evt->SetString(dest_path.wstring()); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + }) + .perform_sync(); + +} + +FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) + : p(new priv(ID, std::move(url), filename, evt_handler, dest_folder)) +{} + +FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {} + +FileGet::~FileGet() +{ + if (p && p->m_io_thread.joinable()) { + p->m_cancel = true; + p->m_io_thread.join(); + } +} + +void FileGet::get() +{ + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); + } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); +} + +void FileGet::cancel() +{ + if(p && p->m_stopped) { + if (p->m_io_thread.joinable()) { + p->m_cancel = true; + p->m_io_thread.join(); + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED); + evt->SetInt(p->m_id); + p->m_evt_handler->QueueEvent(evt); + } + } + + if (p) + p->m_cancel = true; + +} + +void FileGet::pause() +{ + if (p) { + p->m_pause = true; + } +} +void FileGet::resume() +{ + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); + } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); +} +} +} diff --git a/src/slic3r/GUI/DownloaderFileGet.hpp b/src/slic3r/GUI/DownloaderFileGet.hpp new file mode 100644 index 0000000000..38ddd9af02 --- /dev/null +++ b/src/slic3r/GUI/DownloaderFileGet.hpp @@ -0,0 +1,44 @@ +#ifndef slic3r_DownloaderFileGet_hpp_ +#define slic3r_DownloaderFileGet_hpp_ + +#include "../Utils/Http.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { +class FileGet : public std::enable_shared_from_this { +private: + struct priv; +public: + FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder); + FileGet(FileGet&& other); + ~FileGet(); + + void get(); + void cancel(); + void pause(); + void resume(); + static std::string escape_url(const std::string& url); +private: + std::unique_ptr p; +}; +// int = DOWNLOAD ID; string = file path +wxDECLARE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent); +// int = DOWNLOAD ID; string = error msg +wxDECLARE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent); +// int = DOWNLOAD ID; string = progress percent +wxDECLARE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent); +// int = DOWNLOAD ID; string = name +wxDECLARE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent); +// int = DOWNLOAD ID; +wxDECLARE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent); +// int = DOWNLOAD ID; +wxDECLARE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent); +} +} +#endif diff --git a/src/slic3r/GUI/FileArchiveDialog.cpp b/src/slic3r/GUI/FileArchiveDialog.cpp new file mode 100644 index 0000000000..2813887dff --- /dev/null +++ b/src/slic3r/GUI/FileArchiveDialog.cpp @@ -0,0 +1,363 @@ +#include "FileArchiveDialog.hpp" + +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "GUI.hpp" +#include "MainFrame.hpp" +#include "ExtraRenderers.hpp" +#include "format.hpp" +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +ArchiveViewModel::ArchiveViewModel(wxWindow* parent) + :m_parent(parent) +{} +ArchiveViewModel::~ArchiveViewModel() +{} + +std::shared_ptr ArchiveViewModel::AddFile(std::shared_ptr parent, const wxString& name, bool container) +{ + std::shared_ptr node = std::make_shared(ArchiveViewNode(name)); + node->set_container(container); + + if (parent.get() != nullptr) { + parent->get_children().push_back(node); + node->set_parent(parent); + parent->set_is_folder(true); + } else { + m_top_children.emplace_back(node); + } + + wxDataViewItem child = wxDataViewItem((void*)node.get()); + wxDataViewItem parent_item= wxDataViewItem((void*)parent.get()); + ItemAdded(parent_item, child); + + if (parent) + m_ctrl->Expand(parent_item); + return node; +} + +wxString ArchiveViewModel::GetColumnType(unsigned int col) const +{ + if (col == 0) + return "bool"; + return "string";//"DataViewBitmapText"; +} + +void ArchiveViewModel::Rescale() +{ + // There should be no pictures rendered +} + +void ArchiveViewModel::Delete(const wxDataViewItem& item) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + assert(node->get_parent() != nullptr); + for (std::shared_ptr child : node->get_children()) + { + Delete(wxDataViewItem((void*)child.get())); + } + delete [] node; +} +void ArchiveViewModel::Clear() +{ +} + +wxDataViewItem ArchiveViewModel::GetParent(const wxDataViewItem& item) const +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + return wxDataViewItem((void*)node->get_parent().get()); +} +unsigned int ArchiveViewModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const +{ + if (!parent.IsOk()) { + for (std::shared_ptrchild : m_top_children) { + array.push_back(wxDataViewItem((void*)child.get())); + } + return m_top_children.size(); + } + + ArchiveViewNode* node = static_cast(parent.GetID()); + for (std::shared_ptr child : node->get_children()) { + array.push_back(wxDataViewItem((void*)child.get())); + } + return node->get_children().size(); +} + +void ArchiveViewModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + if (col == 0) { + variant = node->get_toggle(); + } else { + variant = node->get_name(); + } +} + +void ArchiveViewModel::untoggle_folders(const wxDataViewItem& item) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + node->set_toggle(false); + if (node->get_parent().get() != nullptr) + untoggle_folders(wxDataViewItem((void*)node->get_parent().get())); +} + +bool ArchiveViewModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) +{ + assert(item.IsOk()); + ArchiveViewNode* node = static_cast(item.GetID()); + if (col == 0) { + node->set_toggle(variant.GetBool()); + // if folder recursivelly check all children + for (std::shared_ptr child : node->get_children()) { + SetValue(variant, wxDataViewItem((void*)child.get()), col); + } + if(!variant.GetBool() && node->get_parent()) + untoggle_folders(wxDataViewItem((void*)node->get_parent().get())); + } else { + node->set_name(variant.GetString()); + } + m_parent->Refresh(); + return true; +} +bool ArchiveViewModel::IsEnabled(const wxDataViewItem& item, unsigned int col) const +{ + // As of now, all items are always enabled. + // Returning false for col 1 would gray out text. + return true; +} + +bool ArchiveViewModel::IsContainer(const wxDataViewItem& item) const +{ + if(!item.IsOk()) + return true; + ArchiveViewNode* node = static_cast(item.GetID()); + return node->is_container(); +} + +ArchiveViewCtrl::ArchiveViewCtrl(wxWindow* parent, wxSize size) + : wxDataViewCtrl(parent, wxID_ANY, wxDefaultPosition, size, wxDV_VARIABLE_LINE_HEIGHT | wxDV_ROW_LINES +#ifdef _WIN32 + | wxBORDER_SIMPLE +#endif + ) + //, m_em_unit(em_unit(parent)) +{ + wxGetApp().UpdateDVCDarkUI(this); + + m_model = new ArchiveViewModel(parent); + this->AssociateModel(m_model); + m_model->SetAssociatedControl(this); +} + +ArchiveViewCtrl::~ArchiveViewCtrl() +{ + if (m_model) { + m_model->Clear(); + m_model->DecRef(); + } +} + +FileArchiveDialog::FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector& selected_paths) + : DPIDialog(parent_window, wxID_ANY, _(L("Archive preview")), wxDefaultPosition, + wxSize(45 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit()), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX) + , m_selected_paths (selected_paths) +{ + int em = em_unit(); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + + m_avc = new ArchiveViewCtrl(this, wxSize(60 * em, 30 * em)); + m_avc->AppendToggleColumn(L"\u2714", 0, wxDATAVIEW_CELL_ACTIVATABLE, 6 * em); + m_avc->AppendTextColumn("filename", 1); + + + std::vector> stack; + + std::function >&, size_t)> reduce_stack = [] (std::vector>& stack, size_t size) { + if (size == 0) { + stack.clear(); + return; + } + while (stack.size() > size) + stack.pop_back(); + }; + // recursively stores whole structure of file onto function stack and synchoronize with stack object. + std::function>&)> adjust_stack = [&adjust_stack, &reduce_stack, &avc = m_avc](const boost::filesystem::path& const_file, std::vector>& stack)->size_t { + boost::filesystem::path file(const_file); + size_t struct_size = file.has_parent_path() ? adjust_stack(file.parent_path(), stack) : 0; + + if (stack.size() > struct_size && (file.has_extension() || file.filename().string() != stack[struct_size]->get_name())) + { + reduce_stack(stack, struct_size); + } + if (!file.has_extension() && stack.size() == struct_size) + stack.push_back(avc->get_model()->AddFile((stack.empty() ? std::shared_ptr(nullptr) : stack.back()), GUI::format_wxstr(file.filename().string()), true)); // filename string to wstring? + return struct_size + 1; + }; + + const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp)", std::regex::icase); + mz_uint num_entries = mz_zip_reader_get_num_files(archive); + mz_zip_archive_file_stat stat; + std::vector filtered_entries; + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(archive, i, &stat)) { + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = GUI::format(wname); + //std::replace(name.begin(), name.end(), '\\', '/'); + boost::filesystem::path path(name); + if (!path.has_extension()) + continue; + // filter out MACOS specific hidden files + if (boost::algorithm::starts_with(path.string(), "__MACOSX")) + continue; + filtered_entries.emplace_back(std::move(path)); + } + } + // sorting files will help adjust_stack function to not create multiple same folders + std::sort(filtered_entries.begin(), filtered_entries.end(), [](const boost::filesystem::path& p1, const boost::filesystem::path& p2){ return p1.string() > p2.string(); }); + for (const boost::filesystem::path& path : filtered_entries) + { + std::shared_ptr parent(nullptr); + + adjust_stack(path, stack); + if (!stack.empty()) + parent = stack.back(); + if (std::regex_match(path.extension().string(), pattern_drop)) { // this leaves out non-compatible files + m_avc->get_model()->AddFile(parent, GUI::format_wxstr(path.filename().string()), false)->set_fullpath(/*std::move(path)*/path); // filename string to wstring? + } + } + wxBoxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxButton* btn_all = new wxButton(this, wxID_ANY, "All"); + btn_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_all_button(); }); + btn_sizer->Add(btn_all, 0, wxLeft); + + wxButton* btn_none = new wxButton(this, wxID_ANY, "None"); + btn_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_none_button(); }); + btn_sizer->Add(btn_none, 0, wxLeft); + + btn_sizer->AddStretchSpacer(); + wxButton* btn_run = new wxButton(this, wxID_OK, "Open"); + btn_run->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_open_button(); }); + btn_sizer->Add(btn_run, 0, wxRIGHT); + + wxButton* cancel_btn = new wxButton(this, wxID_CANCEL, "Cancel"); + cancel_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { this->EndModal(wxID_CANCEL); }); + btn_sizer->Add(cancel_btn, 0, wxRIGHT); + + topSizer->Add(m_avc, 1, wxEXPAND | wxALL, 10); + topSizer->Add(btn_sizer, 0, wxEXPAND | wxALL, 10); + this->SetMinSize(wxSize(80 * em, 30 * em)); + this->SetSizer(topSizer); +} + +void FileArchiveDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + int em = em_unit(); + BOOST_LOG_TRIVIAL(error) << "on_dpi_changed"; + //msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id }); + //for (auto btn : { m_save_btn, m_transfer_btn, m_discard_btn }) + // if (btn) btn->msw_rescale(); + + const wxSize& size = wxSize(70 * em, 30 * em); + SetMinSize(size); + + //m_tree->Rescale(em); + + Fit(); + Refresh(); +} + +void FileArchiveDialog::on_open_button() +{ + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&paths = m_selected_paths, &deep_fill](ArchiveViewNode* node){ + if (node == nullptr) + return; + if (node->get_children().empty()) { + if (node->get_toggle()) + paths.emplace_back(node->get_fullpath()); + } else { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + } + this->EndModal(wxID_OK); +} + +void FileArchiveDialog::on_all_button() +{ + + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&deep_fill](ArchiveViewNode* node) { + if (node == nullptr) + return; + node->set_toggle(true); + if (!node->get_children().empty()) { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + // Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes. + // It should be enough to call ValueChanged for top items. + m_avc->get_model()->ValueChanged(item, 0); + } + + Refresh(); +} + +void FileArchiveDialog::on_none_button() +{ + wxDataViewItemArray top_items; + m_avc->get_model()->GetChildren(wxDataViewItem(nullptr), top_items); + + std::function deep_fill = [&deep_fill](ArchiveViewNode* node) { + if (node == nullptr) + return; + node->set_toggle(false); + if (!node->get_children().empty()) { + for (std::shared_ptr child : node->get_children()) + deep_fill(child.get()); + } + }; + + for (const auto& item : top_items) + { + ArchiveViewNode* node = static_cast(item.GetID()); + deep_fill(node); + // Fix for linux, where Refresh or Update wont help to redraw toggle checkboxes. + // It should be enough to call ValueChanged for top items. + m_avc->get_model()->ValueChanged(item, 0); + } + + this->Refresh(); +} + +} // namespace GUI +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/FileArchiveDialog.hpp b/src/slic3r/GUI/FileArchiveDialog.hpp new file mode 100644 index 0000000000..22a9ecedfa --- /dev/null +++ b/src/slic3r/GUI/FileArchiveDialog.hpp @@ -0,0 +1,118 @@ +#ifndef slic3r_GUI_FileArchiveDialog_hpp_ +#define slic3r_GUI_FileArchiveDialog_hpp_ + +#include "GUI_Utils.hpp" +#include "libslic3r/miniz_extension.hpp" + +#include +#include +#include +#include "wxExtensions.hpp" + +namespace Slic3r { +namespace GUI { + +class ArchiveViewCtrl; + +class ArchiveViewNode +{ +public: + ArchiveViewNode(const wxString& name) : m_name(name) {} + + std::vector>& get_children() { return m_children; } + void set_parent(std::shared_ptr parent) { m_parent = parent; } + // On Linux, get_parent cannot just return size of m_children. ItemAdded would than crash. + std::shared_ptr get_parent() const { return m_parent; } + bool is_container() const { return m_container; } + void set_container(bool is_container) { m_container = is_container; } + wxString get_name() const { return m_name; } + void set_name(const wxString& name) { m_name = name; } + bool get_toggle() const { return m_toggle; } + void set_toggle(bool toggle) { m_toggle = toggle; } + bool get_is_folder() const { return m_folder; } + void set_is_folder(bool is_folder) { m_folder = is_folder; } + void set_fullpath(boost::filesystem::path path) { m_fullpath = path; } + boost::filesystem::path get_fullpath() const { return m_fullpath; } + +private: + wxString m_name; + std::shared_ptr m_parent { nullptr }; + std::vector> m_children; + + bool m_toggle { false }; + bool m_folder { false }; + boost::filesystem::path m_fullpath; + bool m_container { false }; +}; + +class ArchiveViewModel : public wxDataViewModel +{ +public: + ArchiveViewModel(wxWindow* parent); + ~ArchiveViewModel(); + + /* wxDataViewItem AddFolder(wxDataViewItem& parent, wxString name); + wxDataViewItem AddFile(wxDataViewItem& parent, wxString name);*/ + + std::shared_ptr AddFile(std::shared_ptr parent,const wxString& name, bool container); + + wxString GetColumnType(unsigned int col) const override; + unsigned int GetColumnCount() const override { return 2; } + + void Rescale(); + void Delete(const wxDataViewItem& item); + void Clear(); + + wxDataViewItem GetParent(const wxDataViewItem& item) const override; + unsigned int GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const override; + + void SetAssociatedControl(ArchiveViewCtrl* ctrl) { m_ctrl = ctrl; } + + void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override; + bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override; + + void untoggle_folders(const wxDataViewItem& item); + + bool IsEnabled(const wxDataViewItem& item, unsigned int col) const override; + bool IsContainer(const wxDataViewItem& item) const override; + // Is the container just a header or an item with all columns + // In our case it is an item with all columns + bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; } + +protected: + wxWindow* m_parent { nullptr }; + ArchiveViewCtrl* m_ctrl { nullptr }; + std::vector> m_top_children; +}; + +class ArchiveViewCtrl : public wxDataViewCtrl +{ + public: + ArchiveViewCtrl(wxWindow* parent, wxSize size); + ~ArchiveViewCtrl(); + + ArchiveViewModel* get_model() const {return m_model; } +protected: + ArchiveViewModel* m_model; +}; + + +class FileArchiveDialog : public DPIDialog +{ +public: + FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* archive, std::vector& selected_paths); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + + void on_open_button(); + void on_all_button(); + void on_none_button(); + + std::vector& m_selected_paths; + ArchiveViewCtrl* m_avc; +}; + +} // namespace GU +} // namespace Slic3r +#endif // slic3r_GUI_FileArchiveDialog_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index ebb4a7ef22..42ba4295f2 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1,3336 +1,3385 @@ -#include "libslic3r/Technologies.hpp" -#include "GUI_App.hpp" -#include "GUI_Init.hpp" -#include "GUI_ObjectList.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_Factories.hpp" -#include "format.hpp" -#include "I18N.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/I18N.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Color.hpp" - -#include "GUI.hpp" -#include "GUI_Utils.hpp" -#include "3DScene.hpp" -#include "MainFrame.hpp" -#include "Plater.hpp" -#include "GLCanvas3D.hpp" - -#include "../Utils/PresetUpdater.hpp" -#include "../Utils/PrintHost.hpp" -#include "../Utils/Process.hpp" -#include "../Utils/MacDarkMode.hpp" -#include "../Utils/AppUpdater.hpp" -#include "../Utils/WinRegistry.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "ConfigSnapshotDialog.hpp" -#include "FirmwareDialog.hpp" -#include "Preferences.hpp" -#include "Tab.hpp" -#include "SysInfoDialog.hpp" -#include "KBShortcutsDialog.hpp" -#include "UpdateDialogs.hpp" -#include "Mouse3DController.hpp" -#include "RemovableDriveManager.hpp" -#include "InstanceCheck.hpp" -#include "NotificationManager.hpp" -#include "UnsavedChangesDialog.hpp" -#include "SavePresetDialog.hpp" -#include "PrintHostDialogs.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" - -#include "BitmapCache.hpp" -#include "Notebook.hpp" - -#ifdef __WXMSW__ -#include -#include -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE -#endif -#ifdef _WIN32 -#include -#endif - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -#include -#include -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -// Needed for forcing menu icons back under gtk2 and gtk3 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - #include -#endif - -using namespace std::literals; - -namespace Slic3r { -namespace GUI { - -class MainFrame; - -class SplashScreen : public wxSplashScreen -{ -public: - SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) - : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, -#ifdef __APPLE__ - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP -#else - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR -#endif // !__APPLE__ - ) - { - wxASSERT(bitmap.IsOk()); - -// int init_dpi = get_dpi_for_window(this); - this->SetPosition(pos); - // The size of the SplashScreen can be hanged after its moving to another display - // So, update it from a bitmap size - this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); - this->CenterOnScreen(); -// int new_dpi = get_dpi_for_window(this); - -// m_scale = (float)(new_dpi) / (float)(init_dpi); - m_main_bitmap = bitmap; - -// scale_bitmap(m_main_bitmap, m_scale); - - // init constant texts and scale fonts - init_constant_text(); - - // this font will be used for the action string - m_action_font = m_constant_text.credits_font.Bold(); - - // draw logo and constant info text - Decorate(m_main_bitmap); - } - - void SetText(const wxString& text) - { - set_bitmap(m_main_bitmap); - if (!text.empty()) { - wxBitmap bitmap(m_main_bitmap); - - wxMemoryDC memDC; - memDC.SelectObject(bitmap); - - memDC.SetFont(m_action_font); - memDC.SetTextForeground(wxColour(237, 107, 33)); - memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); - - memDC.SelectObject(wxNullBitmap); - set_bitmap(bitmap); -#ifdef __WXOSX__ - // without this code splash screen wouldn't be updated under OSX - wxYield(); -#endif - } - } - - static wxBitmap MakeBitmap(wxBitmap bmp) - { - if (!bmp.IsOk()) - return wxNullBitmap; - - // create dark grey background for the splashscreen - // It will be 5/3 of the weight of the bitmap - int width = lround((double)5 / 3 * bmp.GetWidth()); - int height = bmp.GetHeight(); - - wxImage image(width, height); - unsigned char* imgdata_ = image.GetData(); - for (int i = 0; i < width * height; ++i) { - *imgdata_++ = 51; - *imgdata_++ = 51; - *imgdata_++ = 51; - } - - wxBitmap new_bmp(image); - - wxMemoryDC memDC; - memDC.SelectObject(new_bmp); - memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); - - return new_bmp; - } - - void Decorate(wxBitmap& bmp) - { - if (!bmp.IsOk()) - return; - - // draw text to the box at the left of the splashscreen. - // this box will be 2/5 of the weight of the bitmap, and be at the left. - int width = lround(bmp.GetWidth() * 0.4); - - // load bitmap for logo - BitmapCache bmp_cache; - int logo_size = lround(width * 0.25); - wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); - - wxCoord margin = int(m_scale * 20); - - wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); - banner_rect.Deflate(margin, 2 * margin); - - // use a memory DC to draw directly onto the bitmap - wxMemoryDC memDc(bmp); - - // draw logo - memDc.DrawBitmap(logo_bmp, margin, margin, true); - - // draw the (white) labels inside of our black box (at the left of the splashscreen) - memDc.SetTextForeground(wxColour(255, 255, 255)); - - memDc.SetFont(m_constant_text.title_font); - memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - - int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); - banner_rect.SetTop(banner_rect.GetTop() + title_height); - banner_rect.SetHeight(banner_rect.GetHeight() - title_height); - - memDc.SetFont(m_constant_text.version_font); - memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); - - memDc.SetFont(m_constant_text.credits_font); - memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); - int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); - int text_height = memDc.GetTextExtent("text").GetY(); - - // calculate position for the dynamic text - int logo_and_header_height = margin + logo_size + title_height + version_height; - m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); - } - -private: - wxBitmap m_main_bitmap; - wxFont m_action_font; - int m_action_line_y_position; - float m_scale {1.0}; - - struct ConstantText - { - wxString title; - wxString version; - wxString credits; - - wxFont title_font; - wxFont version_font; - wxFont credits_font; - - void init(wxFont init_font) - { - // title - title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; - - // dynamically get the version to display - version = _L("Version") + " " + std::string(SLIC3R_VERSION); - - // credits infornation - credits = title + " " + - _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + - _L("Developed by Prusa Research.") + "\n\n" + - title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + - _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + - _L("Artwork model by Leslie Ing"); - - title_font = version_font = credits_font = init_font; - } - } - m_constant_text; - - void init_constant_text() - { - m_constant_text.init(get_default_font(this)); - - // As default we use a system font for current display. - // Scale fonts in respect to banner width - - int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins - - float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); - scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); - - float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); - scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); - - // The width of the credits information string doesn't respect to the banner width some times. - // So, scale credits_font in the respect to the longest string width - int longest_string_width = word_wrap_string(m_constant_text.credits); - float font_scale = (float)text_banner_width / longest_string_width; - scale_font(m_constant_text.credits_font, font_scale); - } - - void set_bitmap(wxBitmap& bmp) - { - m_window->SetBitmap(bmp); - m_window->Refresh(); - m_window->Update(); - } - - void scale_bitmap(wxBitmap& bmp, float scale) - { - if (scale == 1.0) - return; - - wxImage image = bmp.ConvertToImage(); - if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) - return; - - int width = int(scale * image.GetWidth()); - int height = int(scale * image.GetHeight()); - image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); - - bmp = wxBitmap(std::move(image)); - } - - void scale_font(wxFont& font, float scale) - { -#ifdef __WXMSW__ - // Workaround for the font scaling in respect to the current active display, - // not for the primary display, as it's implemented in Font.cpp - // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp - // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) - wxNativeFontInfo nfi= *font.GetNativeFontInfo(); - float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); - nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); - nfi.pointSize = pointSizeNew; - font = wxFont(nfi); -#else - font.Scale(scale); -#endif //__WXMSW__ - } - - // wrap a string for the strings no longer then 55 symbols - // return extent of the longest string - int word_wrap_string(wxString& input) - { - size_t line_len = 55;// count of symbols in one line - int idx = -1; - size_t cur_len = 0; - - wxString longest_sub_string; - auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { - if (cur_len > longest_sub_str.Len()) - longest_sub_str = input.SubString(i - cur_len + 1, i); - }; - - for (size_t i = 0; i < input.Len(); i++) - { - cur_len++; - if (input[i] == ' ') - idx = i; - if (input[i] == '\n') - { - get_longest_sub_string(longest_sub_string, cur_len, i); - idx = -1; - cur_len = 0; - } - if (cur_len >= line_len && idx >= 0) - { - get_longest_sub_string(longest_sub_string, cur_len, i); - input[idx] = '\n'; - cur_len = i - static_cast(idx); - } - } - - return GetTextExtent(longest_sub_string).GetX(); - } -}; - - -#ifdef __linux__ -bool static check_old_linux_datadir(const wxString& app_name) { - // If we are on Linux and the datadir does not exist yet, look into the old - // location where the datadir was before version 2.3. If we find it there, - // tell the user that he might wanna migrate to the new location. - // (https://github.com/prusa3d/PrusaSlicer/issues/2911) - // To be precise, the datadir should exist, it is created when single instance - // lock happens. Instead of checking for existence, check the contents. - - namespace fs = boost::filesystem; - - std::string new_path = Slic3r::data_dir(); - - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - std::string default_path = (dir + "/" + app_name).ToUTF8().data(); - - if (new_path != default_path) { - // This happens when the user specifies a custom --datadir. - // Do not show anything in that case. - return true; - } - - fs::path data_dir = fs::path(new_path); - if (! fs::is_directory(data_dir)) - return true; // This should not happen. - - int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); - - if (file_count <= 1) { // just cache dir with an instance lock - std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - - if (fs::is_directory(old_path)) { - wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " - "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" - "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " - "an old %1% configuration directory was detected in \n%3%.\n\n" - "Consider moving the contents of the old directory to the new location in order to access " - "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " - "location again.\n\n" - "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); - wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); - dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); - if (dlg.ShowModal() != wxID_NO) - return false; - } - } else { - // If the new directory exists, be silent. The user likely already saw the message. - } - return true; -} -#endif - -#ifdef _WIN32 -#if 0 // External Updater is replaced with AppUpdater.cpp -static bool run_updater_win() -{ - // find updater exe - boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; - // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst - std::string msg; - bool res = create_process(path_updater, L"/silent", msg); - if (!res) - BOOST_LOG_TRIVIAL(error) << msg; - return res; -} -#endif // 0 -#endif // _WIN32 - -struct FileWildcards { - std::string_view title; - std::vector file_extensions; -}; - - - -static const FileWildcards file_wildcards_by_type[FT_SIZE] = { - /* FT_STL */ { "STL files"sv, { ".stl"sv } }, - /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, - /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, - /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, - /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, - /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, - /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, - /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, - - /* FT_INI */ { "INI files"sv, { ".ini"sv } }, - /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, - - /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, - - /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, -}; - -#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR -wxString file_wildcards(FileType file_type) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - - // Generate cumulative first item - for (const std::string_view& ext : data.file_extensions) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } - else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); - - // Adds an item for each of the extensions - if (data.file_extensions.size() > 1) { - for (const std::string_view& ext : data.file_extensions) { - title = "*"; - title += ext; - ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); - } - } - - return ret; -} -#else -// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. -// The function accepts a custom extension parameter. If the parameter is provided, the custom extension -// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips -// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). -wxString file_wildcards(FileType file_type, const std::string &custom_extension) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - std::string custom_ext_lower; - - if (! custom_extension.empty()) { - // Generate an extension into the title mask and into the list of extensions. - custom_ext_lower = boost::to_lower_copy(custom_extension); - const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); - if (custom_ext_lower == custom_extension) { - // Add a lower case version. - title = std::string("*") + custom_ext_lower; - mask = title; - // Add an upper case version. - mask += ";*"; - mask += custom_ext_upper; - } else if (custom_ext_upper == custom_extension) { - // Add an upper case version. - title = std::string("*") + custom_ext_upper; - mask = title; - // Add a lower case version. - mask += ";*"; - mask += custom_ext_lower; - } else { - // Add the mixed case version only. - title = std::string("*") + custom_extension; - mask = title; - } - } - - for (const std::string_view &ext : data.file_extensions) - // Only add an extension if it was not added first as the custom extension. - if (ext != custom_ext_lower) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); -} -#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR - -static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) -static void register_win32_dpi_event() -{ - enum { WM_DPICHANGED_ = 0x02e0 }; - - wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { - const int dpi = wParam & 0xffff; - const auto rect = reinterpret_cast(lParam); - const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); - - DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); - win->GetEventHandler()->AddPendingEvent(evt); - - return true; - }); -} -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; - -static void register_win32_device_notification_event() -{ - wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; - switch (wParam) { - case DBT_DEVICEARRIVAL: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { -// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - case DBT_DEVICEREMOVECOMPLETE: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) -// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - default: - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - wchar_t sPath[MAX_PATH]; - if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { - struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); - if (! SHGetPathFromIDList(pidl, sPath)) { - BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; - return false; - } - } - switch (lParam) { - case SHCNE_MEDIAINSERTED: - { - //printf("SHCNE_MEDIAINSERTED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - break; - } - case SHCNE_MEDIAREMOVED: - { - //printf("SHCNE_MEDIAREMOVED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - break; - } - default: -// printf("Unknown\n"); - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); -// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { - if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { - RAWINPUT raw; - UINT rawSize = sizeof(RAWINPUT); - ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); - if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) - return true; - } - return false; - }); - - wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - COPYDATASTRUCT* copy_data_structure = { 0 }; - copy_data_structure = (COPYDATASTRUCT*)lParam; - if (copy_data_structure->dwData == 1) { - LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; - Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); - } - return true; - }); -} -#endif // WIN32 - -static void generic_exception_handle() -{ - // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage - // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 - // - // wxLogError typically goes around exception handling and display an error dialog some time - // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. - // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates - // errors if multiple have been collected and displays just one error message for all of them. - // Otherwise we would get multiple error messages for one missing png, for example. - // - // If a custom error message window (or some other solution) were to be used, it would be necessary - // to turn off wxLogError() usage in wx APIs, most notably in wxImage - // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a - - try { - throw; - } catch (const std::bad_alloc& ex) { - // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) - // and terminate the app so it is at least certain to happen now. - wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); - std::terminate(); - } catch (const boost::io::bad_format_string& ex) { - wxString errmsg = _L("PrusaSlicer has encountered a localization error. " - "Please report to PrusaSlicer team, what language was active and in which scenario " - "this issue happened. Thank you.\n\nThe application will now terminate."); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - std::terminate(); - throw; - } catch (const std::exception& ex) { - wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - throw; - } -} - -void GUI_App::post_init() -{ - assert(initialized()); - if (! this->initialized()) - throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); - - if (this->is_gcode_viewer()) { - if (! this->init_params->input_files.empty()) - this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); - } - else { - if (! this->init_params->preset_substitutions.empty()) - show_substitutions_info(this->init_params->preset_substitutions); - -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - this->gui->mainframe->load_config(m_print_config); -#endif - if (! this->init_params->load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - this->mainframe->load_config_file(this->init_params->load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!this->init_params->input_files.empty()) { - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { - // Update application titlebar when opening a project file - const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) - this->plater()->set_project_filename(from_u8(filename)); - } - } - if (! this->init_params->extra_config.empty()) - this->mainframe->load_config(this->init_params->extra_config); - } - - // show "Did you know" notification - if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) - plater_->get_notification_manager()->push_hint_notification(true); - - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; - CallAfter([this] { - bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - this->app_version_check(false); - if (! cw_showed) { - // The CallAfter is needed as well, without it, GL extensions did not show. - // Also, we only want to show this when the wizard does not, so the new user - // sees something else than "we want something" on the first start. - show_send_system_info_dialog_if_needed(); - } - }); - } - - // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. - app_config->set("version", SLIC3R_VERSION); - app_config->save(); - -#ifdef _WIN32 - // Sets window property to mainframe so other instances can indentify it. - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 -} - -IMPLEMENT_APP(GUI_App) - -GUI_App::GUI_App(EAppMode mode) - : wxApp() - , m_app_mode(mode) - , m_em_unit(10) - , m_imgui(new ImGuiWrapper()) - , m_removable_drive_manager(std::make_unique()) - , m_other_instance_message_handler(std::make_unique()) -{ - //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp - this->init_app_config(); - // init app downloader after path to datadir is set - m_app_updater = std::make_unique(); -} - -GUI_App::~GUI_App() -{ - if (app_config != nullptr) - delete app_config; - - if (preset_bundle != nullptr) - delete preset_bundle; - - if (preset_updater != nullptr) - delete preset_updater; -} - -// If formatted for github, plaintext with OpenGL extensions enclosed into

. -// Otherwise HTML formatted for the system info dialog. -std::string GUI_App::get_gl_info(bool for_github) -{ - return OpenGLManager::get_gl_info().to_string(for_github); -} - -wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) -{ -#if ENABLE_GL_CORE_PROFILE -#if ENABLE_OPENGL_DEBUG_OPTION - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), - init_params != nullptr ? init_params->opengl_debug : false); -#else - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); -#endif // ENABLE_OPENGL_DEBUG_OPTION -#else - return m_opengl_mgr.init_glcontext(canvas); -#endif // ENABLE_GL_CORE_PROFILE -} - -bool GUI_App::init_opengl() -{ -#ifdef __linux__ - bool status = m_opengl_mgr.init_gl(); - m_opengl_initialized = true; - return status; -#else - return m_opengl_mgr.init_gl(); -#endif -} - -// gets path to PrusaSlicer.ini, returns semver from first line comment -static boost::optional parse_semver_from_ini(std::string path) -{ - std::ifstream stream(path); - std::stringstream buffer; - buffer << stream.rdbuf(); - std::string body = buffer.str(); - size_t start = body.find("PrusaSlicer "); - if (start == std::string::npos) - return boost::none; - body = body.substr(start + 12); - size_t end = body.find_first_of(" \n"); - if (end < body.size()) - body.resize(end); - return Semver::parse(body); -} - -void GUI_App::init_app_config() -{ - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. - -// SetAppName(SLIC3R_APP_KEY); - SetAppName(SLIC3R_APP_KEY "-alpha"); -// SetAppName(SLIC3R_APP_KEY "-beta"); - - -// SetAppDisplayName(SLIC3R_APP_NAME); - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - - if (data_dir().empty()) { - #ifndef __linux__ - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - #else - // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. - // https://github.com/prusa3d/PrusaSlicer/issues/2911 - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); - #endif - } else { - m_datadir_redefined = true; - } - - if (!app_config) - app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); - - // load settings - m_app_conf_exists = app_config->exists(); - if (m_app_conf_exists) { - std::string error = app_config->load(); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - } -} - -// returns old config path to copy from if such exists, -// returns an empty string if such config path does not exists or if it cannot be loaded. -std::string GUI_App::check_older_app_config(Semver current_version, bool backup) -{ - std::string older_data_dir_path; - - // If the config folder is redefined - do not check - if (m_datadir_redefined) - return {}; - - // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); - - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); - - Semver last_semver = current_version; - for (const auto& candidate : candidates) { - if (boost::filesystem::exists(candidate)) { - // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); - if (other_semver && *other_semver > last_semver) { - last_semver = *other_semver; - older_data_dir_path = candidate.parent_path().string(); - } - } - } - if (older_data_dir_path.empty()) - return {}; - BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; - // ask about using older data folder - - InfoDialog msg(nullptr - , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) - , backup ? - format_wxstr(_L( - "The active configuration was created by %1% %2%," - "\nwhile a newer configuration was found in %3%" - "\ncreated by %1% %4%." - "\n\nShall the newer configuration be imported?" - "\nIf so, your active configuration will be backed up before importing the new configuration." - ) - , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) - : format_wxstr(_L( - "An existing configuration was found in %3%" - "\ncreated by %1% %2%." - "\n\nShall this configuration be imported?" - ) - , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) - , true, wxYES_NO); - - if (backup) { - msg.SetButtonLabel(wxID_YES, _L("Import")); - msg.SetButtonLabel(wxID_NO, _L("Don't import")); - } - - if (msg.ShowModal() == wxID_YES) { - std::string snapshot_id; - if (backup) { - const Config::Snapshot* snapshot{ nullptr }; - if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", - _u8L("Continue and import newer configuration?"), &snapshot)) - return {}; - if (snapshot) { - // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. - snapshot_id = snapshot->id; - assert(! snapshot_id.empty()); - app_config->set("on_snapshot", snapshot_id); - } else - BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; - } - - // load app config from older file - std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - if (!snapshot_id.empty()) - app_config->set("on_snapshot", snapshot_id); - m_app_conf_exists = true; - return older_data_dir_path; - } - return {}; -} - -void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) -{ - BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; - m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); -} - -bool GUI_App::OnInit() -{ - try { - return on_init_inner(); - } catch (const std::exception&) { - generic_exception_handle(); - return false; - } -} - -bool GUI_App::on_init_inner() -{ - // Set initialization of image handlers before any UI actions - See GH issue #7469 - wxInitAllImageHandlers(); - -#if defined(_WIN32) && ! defined(_WIN64) - // Win32 32bit build. - if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - RichMessageDialog dlg(nullptr, - _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." - "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." - "\nDo you wish to continue?"), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - if (dlg.ShowModal() != wxID_YES) - return false; - } -#endif // _WIN64 - - // Forcing back menu icons under gtk2 and gtk3. Solution is based on: - // https://docs.gtk.org/gtk3/class.Settings.html - // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb - // TODO: Find workaround for GTK4 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); -#endif - - // Verify resources path - const wxString resources_dir = from_u8(Slic3r::resources_dir()); - wxCHECK_MSG(wxDirExists(resources_dir), false, - wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - -#ifdef __linux__ - if (! check_old_linux_datadir(GetAppName())) { - std::cerr << "Quitting, user chose to move their data to new location." << std::endl; - return false; - } -#endif - - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. -// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); - // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible - // performance when working on high resolution multi-display setups. -// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); - -// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. - // Like here, before the show InfoDialog in check_older_app_config() - - // If load_language() fails, the application closes. - load_language(wxString(), true); -#ifdef _MSW_DARK_MODE - bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; - bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); -#endif - // initialize label colors and fonts - init_ui_colours(); - init_fonts(); - - std::string older_data_dir_path; - if (m_app_conf_exists) { - if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) - // Only copying configuration if it was saved with a newer slicer than the one currently running. - older_data_dir_path = check_older_app_config(app_config->orig_version(), true); - } else { - // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. - older_data_dir_path = check_older_app_config(Semver(), false); - } - -#ifdef _MSW_DARK_MODE - // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed - if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; - init_dark_color_mode != new_dark_color_mode) { - NppDarkMode::SetDarkMode(new_dark_color_mode); - init_ui_colours(); - update_ui_colours_from_appconfig(); - } - if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - init_sys_menu_enabled != new_sys_menu_enabled) - NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); -#endif - - if (is_editor()) { - std::string msg = Http::tls_global_init(); - std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); - bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - - if (!msg.empty() && !ssl_accept) { - RichMessageDialog - dlg(nullptr, - wxString::Format(_L("%s\nDo you want to continue?"), msg), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - dlg.ShowCheckBox(_L("Remember my choice")); - if (dlg.ShowModal() != wxID_YES) return false; - - app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); - app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); - } - } - - SplashScreen* scrn = nullptr; - if (app_config->get("show_splash_screen") == "1") { - // make a bitmap with dark grey banner on the left side - wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); - - // Detect position (display) to show the splash screen - // Now this position is equal to the mainframe position - wxPoint splashscreen_pos = wxDefaultPosition; - bool default_splashscreen_pos = true; - if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { - auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); - default_splashscreen_pos = metrics == boost::none; - if (!default_splashscreen_pos) - splashscreen_pos = metrics->get_rect().GetPosition(); - } - - if (!default_splashscreen_pos) { - // workaround for crash related to the positioning of the window on secondary monitor - get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); - get_app_config()->save(); - } - - // create splash screen with updated bmp - scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), - wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); - - if (!default_splashscreen_pos) - // revert "restore_win_position" value if application wasn't crashed - get_app_config()->set("restore_win_position", "1"); -#ifndef __linux__ - wxYield(); -#endif - scrn->SetText(_L("Loading configuration")+ dots); - } - - preset_bundle = new PresetBundle(); - - // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory - // supplied as argument to --datadir; in that case we should still run the wizard - preset_bundle->setup_directories(); - - if (! older_data_dir_path.empty()) { - preset_bundle->import_newer_configs(older_data_dir_path); - app_config->save(); - } - - if (is_editor()) { -#ifdef __WXMSW__ - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#endif // __WXMSW__ - - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); - Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { - std::string evt_string = into_u8(evt.GetString()); - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { - auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) - , _u8L("See Releases page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } - ); - } - } - }); - Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { - //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); - }); - - Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); - if(!evt.GetString().IsEmpty()) - show_error(nullptr, evt.GetString()); - }); - - Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { - show_error(nullptr, evt.GetString()); - }); - } - else { -#ifdef __WXMSW__ - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#endif // __WXMSW__ - } - - // Suppress the '- default -' presets. - preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); - try { - // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. - // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force - // installation of a compatible system preset, thus nullifying the system preset substitutions. - init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); - } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); - } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - register_win32_dpi_event(); -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - register_win32_device_notification_event(); -#endif // WIN32 - - // Let the libslic3r know the callback, which will translate messages on demand. - Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); - - // application frame - if (scrn && is_editor()) - scrn->SetText(_L("Preparing settings tabs") + dots); - - mainframe = new MainFrame(); - // hide settings tabs after first Layout - if (is_editor()) - mainframe->select_tab(size_t(0)); - - sidebar().obj_list()->init_objects(); // propagate model objects to object list -// update_mode(); // !!! do that later - SetTopWindow(mainframe); - - plater_->init_notification_manager(); - - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - - if (is_gcode_viewer()) { - mainframe->update_layout(); - if (plater_ != nullptr) - // ensure the selected technology is ptFFF - plater_->set_printer_technology(ptFFF); - } - else - load_current_presets(); - - // Save the active profiles as a "saved into project". - update_saved_preset_from_current_preset(); - - if (plater_ != nullptr) { - // Save the names of active presets and project specific config into ProjectDirtyStateManager. - plater_->reset_project_dirty_initial_presets(); - // Update Project dirty state, update application title bar. - plater_->update_project_dirty_from_presets(); - } - - mainframe->Show(true); - - obj_list()->set_min_height(); - - update_mode(); // update view mode after fix of the object_list size - -#ifdef __APPLE__ - other_instance_message_handler()->bring_instance_forward(); -#endif //__APPLE__ - - Bind(wxEVT_IDLE, [this](wxIdleEvent& event) - { - if (! plater_) - return; - - this->obj_manipul()->update_if_dirty(); - - // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT - // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. -#ifdef __linux__ - if (! m_post_initialized && m_opengl_initialized) { -#else - if (! m_post_initialized) { -#endif - m_post_initialized = true; -#ifdef WIN32 - this->mainframe->register_win32_callbacks(); -#endif - this->post_init(); - } - - if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") - app_config->save(); - }); - - m_initialized = true; - - if (const std::string& crash_reason = app_config->get("restore_win_position"); - boost::starts_with(crash_reason,"crashed")) - { - wxString preferences_item = _L("Restore window position on start"); - InfoDialog dialog(nullptr, - _L("PrusaSlicer started after a crash"), - format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" - "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" - "More precise reason for the crash: \"%1%\".\n" - "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" - "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " - "Otherwise, the application will most likely crash again next time."), - "" + from_u8(crash_reason) + "", - "#2939", - "#5573", - "" + preferences_item + ""), - true, wxYES_NO); - - dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); - dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); - - auto answer = dialog.ShowModal(); - if (answer == wxID_YES) - app_config->set("restore_win_position", "0"); - else if (answer == wxID_NO) - app_config->set("restore_win_position", "1"); - app_config->save(); - } - - return true; -} - -unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) -{ - double r = colour.Red(); - double g = colour.Green(); - double b = colour.Blue(); - - return std::round(std::sqrt( - r * r * .241 + - g * g * .691 + - b * b * .068 - )); -} - -bool GUI_App::dark_mode() -{ -#if __APPLE__ - // The check for dark mode returns false positive on 10.12 and 10.13, - // which allowed setting dark menu bar and dock area, which is - // is detected as dark mode. We must run on at least 10.14 where the - // proper dark mode was first introduced. - return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); -#else - if (wxGetApp().app_config->has("dark_color_mode")) - return wxGetApp().app_config->get("dark_color_mode") == "1"; - return check_dark_mode(); -#endif -} - -const wxColour GUI_App::get_label_default_clr_system() -{ - return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); -} - -const wxColour GUI_App::get_label_default_clr_modified() -{ - return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); -} - -const std::vector GUI_App::get_mode_default_palette() -{ - return { "#7DF028", "#FFDC00", "#E70000" }; -} - -void GUI_App::init_ui_colours() -{ - m_color_label_modified = get_label_default_clr_modified(); - m_color_label_sys = get_label_default_clr_system(); - m_mode_palette = get_mode_default_palette(); - - bool is_dark_mode = dark_mode(); -#ifdef _WIN32 - m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); - m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); - m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); - m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif - m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); -} - -void GUI_App::update_ui_colours_from_appconfig() -{ - // load label colors - if (app_config->has("label_clr_sys")) { - auto str = app_config->get("label_clr_sys"); - if (!str.empty()) - m_color_label_sys = wxColour(str); - } - - if (app_config->has("label_clr_modified")) { - auto str = app_config->get("label_clr_modified"); - if (!str.empty()) - m_color_label_modified = wxColour(str); - } - - // load mode markers colors - if (app_config->has("mode_palette")) { - const auto colors = app_config->get("mode_palette"); - if (!colors.empty()) { - m_mode_palette.clear(); - if (!unescape_strings_cstyle(colors, m_mode_palette)) - m_mode_palette = get_mode_default_palette(); - } - } -} - -void GUI_App::update_label_colours() -{ - for (Tab* tab : tabs_list) - tab->update_label_colours(); -} - -#ifdef _WIN32 -static bool is_focused(HWND hWnd) -{ - HWND hFocusedWnd = ::GetFocus(); - return hFocusedWnd && hWnd == hFocusedWnd; -} - -static bool is_default(wxWindow* win) -{ - wxTopLevelWindow* tlw = find_toplevel_parent(win); - if (!tlw) - return false; - - return win == tlw->GetDefaultItem(); -} -#endif - -void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) -{ -#ifdef _WIN32 - bool is_focused_button = false; - bool is_default_button = false; - if (wxButton* btn = dynamic_cast(window)) { - if (!(btn->GetWindowStyle() & wxNO_BORDER)) { - btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); - highlited = true; - } - // button marking - { - auto mark_button = [this, btn, highlited](const bool mark) { - if (btn->GetLabel().IsEmpty()) - btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); - else - btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); - btn->Refresh(); - btn->Update(); - }; - - // hovering - btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); - // focusing - btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - - is_focused_button = is_focused(btn->GetHWND()); - is_default_button = is_default(btn); - if (is_focused_button || is_default_button) - mark_button(is_focused_button); - } - } - else if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); - - if (!just_font) - window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - if (!is_focused_button && !is_default_button) - window->SetForegroundColour(m_color_label_default); -#endif -} - -// recursive function for scaling fonts for all controls in Window -#ifdef _WIN32 -static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) -{ - bool is_btn = dynamic_cast(window) != nullptr; - if (!(just_buttons_update && !is_btn)) - wxGetApp().UpdateDarkUI(window, is_btn); - - auto children = window->GetChildren(); - for (auto child : children) { - update_dark_children_ui(child); - } -} -#endif - -// Note: Don't use this function for Dialog contains ScalableButtons -void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) -{ -#ifdef _WIN32 - update_dark_children_ui(dlg, just_buttons_update); -#endif -} -void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) -{ -#ifdef _WIN32 - UpdateDarkUI(dvc, highlited ? dark_mode() : false); -#ifdef _MSW_DARK_MODE - dvc->RefreshHeaderDarkMode(&m_normal_font); -#endif //_MSW_DARK_MODE - if (dvc->HasFlag(wxDV_ROW_LINES)) - dvc->SetAlternateRowColour(m_color_highlight_default); - if (dvc->GetBorder() != wxBORDER_SIMPLE) - dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); -#endif -} - -void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(parent); - - auto children = parent->GetChildren(); - for (auto child : children) { - if (dynamic_cast(child)) - child->SetForegroundColour(m_color_label_default); - } -#endif -} - -void GUI_App::init_fonts() -{ - m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); - m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - -#ifdef __WXMAC__ - m_small_font.SetPointSize(11); - m_bold_font.SetPointSize(13); -#endif /*__WXMAC__*/ - - // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as - // DEFAULT in wxGtk. Use the TELETYPE family as a work-around - m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::update_fonts(const MainFrame *main_frame) -{ - /* Only normal and bold fonts are used for an application rescale, - * because of under MSW small and normal fonts are the same. - * To avoid same rescaling twice, just fill this values - * from rescaled MainFrame - */ - if (main_frame == nullptr) - main_frame = this->mainframe; - m_normal_font = main_frame->normal_font(); - m_small_font = m_normal_font; - m_bold_font = main_frame->normal_font().Bold(); - m_link_font = m_bold_font.Underlined(); - m_em_unit = main_frame->em_unit(); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::set_label_clr_modified(const wxColour& clr) -{ - if (m_color_label_modified == clr) - return; - m_color_label_modified = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_modified", str); - app_config->save(); -} - -void GUI_App::set_label_clr_sys(const wxColour& clr) -{ - if (m_color_label_sys == clr) - return; - m_color_label_sys = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_sys", str); - app_config->save(); -} - -const std::string& GUI_App::get_mode_btn_color(int mode_id) -{ - assert(0 <= mode_id && size_t(mode_id) < m_mode_palette.size()); - return m_mode_palette[mode_id]; -} - -std::vector GUI_App::get_mode_palette() -{ - return { wxColor(m_mode_palette[0]), - wxColor(m_mode_palette[1]), - wxColor(m_mode_palette[2]) }; -} - -void GUI_App::set_mode_palette(const std::vector& palette) -{ - bool save = false; - - for (size_t mode = 0; mode < palette.size(); ++mode) { - const wxColour& clr = palette[mode]; - std::string color_str = clr == wxTransparentColour ? std::string("") : encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - if (m_mode_palette[mode] != color_str) { - m_mode_palette[mode] = color_str; - save = true; - } - } - - if (save) { - mainframe->update_mode_markers(); - app_config->set("mode_palette", escape_strings_cstyle(m_mode_palette)); - app_config->save(); - } -} - -bool GUI_App::tabs_as_menu() const -{ - return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); -} - -wxSize GUI_App::get_min_size() const -{ - return wxSize(76*m_em_unit, 49 * m_em_unit); -} - -float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit*0.1f; -#endif // __APPLE__ - - const std::string& use_val = app_config->get("use_custom_toolbar_size"); - const std::string& val = app_config->get("custom_toolbar_size"); - const std::string& auto_val = app_config->get("auto_toolbar_size"); - - if (val.empty() || auto_val.empty() || use_val.empty()) - return icon_sc; - - int int_val = use_val == "0" ? 100 : atoi(val.c_str()); - // correct value in respect to auto_toolbar_size - int_val = std::min(atoi(auto_val.c_str()), int_val); - - if (is_limited && int_val < 50) - int_val = 50; - - return 0.01f * int_val * icon_sc; -} - -void GUI_App::set_auto_toolbar_icon_scale(float scale) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit * 0.1f; -#endif // __APPLE__ - - long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); - std::string val = std::to_string(int_val); - - app_config->set("auto_toolbar_size", val); -} - -// check user printer_presets for the containing information about "Print Host upload" -void GUI_App::check_printer_presets() -{ - std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); - if (preset_names.empty()) - return; - - wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; - for (const std::string& preset_name : preset_names) - msg_text += "\n \"" + from_u8(preset_name) + "\","; - msg_text.RemoveLast(); - msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" - "Settings will be available in physical printers settings.") + "\n\n" + - _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" - "Note: This name can be changed later from the physical printers settings"); - - //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - - preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); -} - -void GUI_App::recreate_GUI(const wxString& msg_name) -{ - m_is_recreating_gui = true; - - mainframe->shutdown(); - - wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); - dlg.Pulse(); - dlg.Update(10, _L("Recreating") + dots); - - MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(); - if (is_editor()) - // hide settings tabs after first Layout - mainframe->select_tab(size_t(0)); - // Propagate model objects to object list. - sidebar().obj_list()->init_objects(); - SetTopWindow(mainframe); - - dlg.Update(30, _L("Recreating") + dots); - old_main_frame->Destroy(); - - dlg.Update(80, _L("Loading of current presets") + dots); - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - load_current_presets(); - mainframe->Show(true); - - dlg.Update(90, _L("Loading of a mode view") + dots); - - obj_list()->set_min_height(); - update_mode(); - - // #ys_FIXME_delete_after_testing Do we still need this ? -// CallAfter([]() { -// // Run the config wizard, don't offer the "reset user profile" checkbox. -// config_wizard_startup(true); -// }); - - m_is_recreating_gui = false; -} - -void GUI_App::system_info() -{ - SysInfoDialog dlg; - dlg.ShowModal(); -} - -void GUI_App::keyboard_shortcuts() -{ - KBShortcutsDialog dlg; - dlg.ShowModal(); -} - -// static method accepting a wxWindow object as first parameter -bool GUI_App::catch_error(std::function cb, - // wxMessageDialog* message_dialog, - const std::string& err /*= ""*/) -{ - if (!err.empty()) { - if (cb) - cb(); - // if (message_dialog) - // message_dialog->(err, "Error", wxOK | wxICON_ERROR); - show_error(/*this*/nullptr, err); - return true; - } - return false; -} - -// static method accepting a wxWindow object as first parameter -void fatal_error(wxWindow* parent) -{ - show_error(parent, ""); - // exit 1; // #ys_FIXME -} - -#ifdef _WIN32 - -#ifdef _MSW_DARK_MODE -static void update_scrolls(wxWindow* window) -{ - wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); - while (node) - { - wxWindow* win = node->GetData(); - if (dynamic_cast(win) || - dynamic_cast(win) || - dynamic_cast(win)) - NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); - - update_scrolls(win); - node = node->GetNext(); - } -} -#endif //_MSW_DARK_MODE - - -#ifdef _MSW_DARK_MODE -void GUI_App::force_menu_update() -{ - NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); -} -#endif //_MSW_DARK_MODE - -void GUI_App::force_colors_update() -{ -#ifdef _MSW_DARK_MODE - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) - NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); - NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); - NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); -#endif //_MSW_DARK_MODE - m_force_colors_update = true; -} -#endif //_WIN32 - -// Called after the Preferences dialog is closed and the program settings are saved. -// Update the UI based on the current preferences. -void GUI_App::update_ui_from_settings() -{ - update_label_colours(); -#ifdef _WIN32 - // Upadte UI colors before Update UI from settings - if (m_force_colors_update) { - m_force_colors_update = false; - mainframe->force_color_changed(); - mainframe->diff_dialog.force_color_changed(); - mainframe->preferences_dialog->force_color_changed(); - mainframe->printhost_queue_dlg()->force_color_changed(); -#ifdef _MSW_DARK_MODE - update_scrolls(mainframe); - if (mainframe->is_dlg_layout()) { - // update for tabs bar - UpdateDarkUI(&mainframe->m_settings_dialog); - mainframe->m_settings_dialog.Fit(); - mainframe->m_settings_dialog.Refresh(); - // update scrollbars - update_scrolls(&mainframe->m_settings_dialog); - } -#endif //_MSW_DARK_MODE - } -#endif - mainframe->update_ui_from_settings(); -} - -void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) -{ - const std::string name = into_u8(window->GetName()); - - window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { - window_pos_save(window, name); - event.Skip(); - }); - - window_pos_restore(window, name, default_maximized); - - on_window_geometry(window, [=]() { - window_pos_sanitize(window); - }); -} - -void GUI_App::load_project(wxWindow *parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (3MF/AMF):"), - app_config->get_last_dir(), "", - file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const -{ - input_files.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), - from_u8(app_config->get_last_dir()), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - dialog.GetPaths(input_files); -} - -void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), - app_config->get_last_dir(), "", - file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Changing of an application language") + dots); - return true; - } else { - return false; - } -} - -#ifdef __linux__ -static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, - const wxLanguageInfo* system_language) -{ - constexpr size_t max_len = 50; - char path[max_len] = ""; - std::vector locales; - const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); - - // Call locale -a so we can parse the output to get the list of available locales - // We expect lines such as "en_US.utf8". Pick ones starting with the language code - // we are switching to. Lines with different formatting will be removed later. - FILE* fp = popen("locale -a", "r"); - if (fp != NULL) { - while (fgets(path, max_len, fp) != NULL) { - std::string line(path); - line = line.substr(0, line.find('\n')); - if (boost::starts_with(line, lang_prefix)) - locales.push_back(line); - } - pclose(fp); - } - - // locales now contain all candidates for this language. - // Sort them so ones containing anything about UTF-8 are at the end. - std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) - { - auto has_utf8 = [](const std::string & s) { - auto S = boost::to_upper_copy(s); - return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; - }; - return ! has_utf8(a) && has_utf8(b); - }); - - // Remove the suffix behind a dot, if there is one. - for (std::string& s : locales) - s = s.substr(0, s.find(".")); - - // We just hope that dear Linux "locale -a" returns country codes - // in ISO 3166-1 alpha-2 code (two letter) format. - // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - // To be sure, remove anything not looking as expected - // (any number of lowercase letters, underscore, two uppercase letters). - locales.erase(std::remove_if(locales.begin(), - locales.end(), - [](const std::string& s) { - return ! std::regex_match(s, - std::regex("^[a-z]+_[A-Z]{2}$")); - }), - locales.end()); - - if (system_language) { - // Is there a candidate matching a country code of a system language? Move it to the end, - // while maintaining the order of matches, so that the best match ends up at the very end. - std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); - int cnt = locales.size(); - for (int i = 0; i < cnt; ++i) - if (locales[i].find(system_country) != std::string::npos) { - locales.emplace_back(std::move(locales[i])); - locales[i].clear(); - } - } - - // Now try them one by one. - for (auto it = locales.rbegin(); it != locales.rend(); ++ it) - if (! it->empty()) { - const std::string &locale = *it; - const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); - if (wxLocale::IsAvailable(lang->Language)) - return lang; - } - return language; -} -#endif - -int GUI_App::GetSingleChoiceIndex(const wxString& message, - const wxString& caption, - const wxArrayString& choices, - int initialSelection) -{ -#ifdef _WIN32 - wxSingleChoiceDialog dialog(nullptr, message, caption, choices); - wxGetApp().UpdateDlgDarkUI(&dialog); - - dialog.SetSelection(initialSelection); - return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; -#else - return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); -#endif -} - -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); - app_config->save(); - return true; - } - } - - return false; -} - -// Load gettext translation files and activate them at the start of the application, -// based on the "translation_language" key stored in the application config. -bool GUI_App::load_language(wxString language, bool initial) -{ - if (initial) { - // There is a static list of lookup path prefixes in wxWidgets. Add ours. - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); - // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. - language = app_config->get("translation_language"); - if (! language.empty()) - BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; - - // Get the system language. - { - const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); - if (lang_system != wxLANGUAGE_UNKNOWN) { - m_language_info_system = wxLocale::GetLanguageInfo(lang_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); - } - } - { - // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. - wxLocale temp_locale; -#ifdef __WXOSX__ - // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: - // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. - // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. - // But wxWidgets guys work on it. - temp_locale.Init(wxLANGUAGE_ENGLISH); -#else - temp_locale.Init(); -#endif // __WXOSX__ - // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). - wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); - // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer - // and try to match them with the system specific "preferred languages". - // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). - // The last parameter gets added to the list of detected dictionaries. This is a workaround - // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. - wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - if (! best_language.IsEmpty()) { - m_language_info_best = wxLocale::FindLanguageInfo(best_language); - BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); - } - #ifdef __linux__ - wxString lc_all; - if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { - // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. - // Disregard the "best" suggestion in case LC_ALL is provided. - m_language_info_best = nullptr; - } - #endif - } - } - - const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); - if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { - // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). - language_info = nullptr; - BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); - } - - if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { - BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); - language_info = nullptr; - } - - if (language_info == nullptr) { - // PrusaSlicer does not support the Right to Left languages yet. - if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_system; - if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_best; - if (language_info == nullptr) - language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); - - // Alternate language code. - wxLanguage language_dict = wxLanguage(language_info->Language); - if (language_info->CanonicalName.BeforeFirst('_') == "sk") { - // Slovaks understand Czech well. Give them the Czech translation. - language_dict = wxLANGUAGE_CZECH; - BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; - } - - // Select language for locales. This language may be different from the language of the dictionary. - if (language_info == m_language_info_best || language_info == m_language_info_system) { - // The current language matches user's default profile exactly. That's great. - } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { - // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. - // This allows a Swiss guy to use a German dictionary without forcing him to German locales. - language_info = m_language_info_best; - } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) - language_info = m_language_info_system; - -#ifdef __linux__ - // If we can't find this locale , try to use different one for the language - // instead of just reporting that it is impossible to switch. - if (! wxLocale::IsAvailable(language_info->Language)) { - std::string original_lang = into_u8(language_info->CanonicalName); - language_info = linux_get_existing_locale_language(language_info, m_language_info_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") - % original_lang % language_info->CanonicalName.ToUTF8().data(); - } -#endif - - if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; -#if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; -#endif - if (initial) - message + "\n\nApplication will close."; - wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); - if (initial) - std::exit(EXIT_FAILURE); - else - return false; - } - - // Release the old locales, create new locales. - //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. - m_wxLocale.release(); - m_wxLocale = Slic3r::make_unique(); - m_wxLocale->Init(language_info->Language); - // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) - // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. - wxTranslations::Get()->SetLanguage(language_dict); - { - // UKR Localization specific workaround till the wxWidgets doesn't fixed: - // From wxWidgets 3.1.6 calls setlocation(0, wxInfoLanguage->LocaleTag), see (https://github.com/prusa3d/wxWidgets/commit/deef116a09748796711d1e3509965ee208dcdf0b#diff-7de25e9a71c4dce61bbf76492c589623d5b93fd1bb105ceaf0662075d15f4472), - // where LocaleTag is a Tag of locale in BCP 47 - like notation. - // For Ukrainian Language LocaleTag == "uk". - // But setlocale(0, "uk") returns "English_United Kingdom.1252" instead of "uk", - // and, as a result, locales are set to English_United Kingdom - - if (language_info->CanonicalName == "uk") - setlocale(0, language_info->GetCanonicalWithRegion().data()); - } - m_wxLocale->AddCatalog(SLIC3R_APP_KEY); - m_imgui->set_language(into_u8(language_info->CanonicalName)); - //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. - //wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); - return true; -} - -Tab* GUI_App::get_tab(Preset::Type type) -{ - for (Tab* tab: tabs_list) - if (tab->type() == type) - return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab - return nullptr; -} - -ConfigOptionMode GUI_App::get_mode() -{ - if (!app_config->has("view_mode")) - return comSimple; - - const auto mode = app_config->get("view_mode"); - return mode == "expert" ? comExpert : - mode == "simple" ? comSimple : comAdvanced; -} - -void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) -{ - const std::string mode_str = mode == comExpert ? "expert" : - mode == comSimple ? "simple" : "advanced"; - app_config->set("view_mode", mode_str); - app_config->save(); - update_mode(); -} - -// Update view mode according to selected menu -void GUI_App::update_mode() -{ - sidebar().update_mode(); - -#ifdef _WIN32 //_MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif - - for (auto tab : tabs_list) - tab->update_mode(); - - plater()->update_menus(); - plater()->canvas3D()->update_gizmos_on_off_state(); -} - -void GUI_App::add_config_menu(wxMenuBar *menu) -{ - auto local_menu = new wxMenu(); - wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); - - const auto config_wizard_name = _(ConfigWizard::name(true)); - const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); - // Cmd+, is standard on OS X - what about other operating systems? - if (is_editor()) { - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); - local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); - local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); - local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - //if (DesktopIntegrationDialog::integration_possible()) - local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); -#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - local_menu->AppendSeparator(); - } - local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + -#ifdef __APPLE__ - "\tCtrl+,", -#else - "\tCtrl+P", -#endif - _L("Application preferences")); - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); - if (is_editor()) { - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); - // TODO: for when we're able to flash dictionaries - // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); - } - - local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { - switch (event.GetId() - config_id_base) { - case ConfigMenuWizard: - run_wizard(ConfigWizard::RR_USER); - break; - case ConfigMenuUpdateConf: - check_updates(true); - break; - case ConfigMenuUpdateApp: - app_version_check(true); - break; -#ifdef __linux__ - case ConfigMenuDesktopIntegration: - show_desktop_integration_dialog(); - break; -#endif - case ConfigMenuTakeSnapshot: - // Take a configuration snapshot. - if (wxString action_name = _L("Taking a configuration snapshot"); - check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { - wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); - UpdateDlgDarkUI(&dlg); - - // set current normal font for dialog children, - // because of just dlg.SetFont(normal_font()) has no result; - for (auto child : dlg.GetChildren()) - child->SetFont(normal_font()); - - if (dlg.ShowModal() == wxID_OK) - if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( - *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); - snapshot != nullptr) - app_config->set("on_snapshot", snapshot->id); - } - break; - case ConfigMenuSnapshots: - if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { - std::string on_snapshot; - if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - on_snapshot = app_config->get("on_snapshot"); - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); - dlg.ShowModal(); - if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && - ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", - GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) - break; - try { - app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system - // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users - // are known to be creative and mess with the config files in various ways. - if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); - ! all_substitutions.empty()) - show_substitutions_info(all_substitutions); - - // Load the currently selected preset into the GUI, update the preset selection box. - load_current_presets(); - } catch (std::exception &ex) { - GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); - } - } - } - break; - case ConfigMenuPreferences: - { - open_preferences(); - break; - } - case ConfigMenuLanguage: - { - /* Before change application language, let's check unsaved changes on 3D-Scene - * and draw user's attention to the application restarting after a language change - */ - { - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Language selection"); - //wxMessageDialog dialog(nullptr, - MessageDialog dialog(nullptr, - _L("Switching the language will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxOK | wxCANCEL); - if (dialog.ShowModal() == wxID_CANCEL) - return; - } - - switch_language(); - break; - } - case ConfigMenuFlashFirmware: - FirmwareDialog::run(mainframe); - break; - default: - break; - } - }); - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -} - -void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - mainframe->preferences_dialog->show(highlight_option, tab_name); - - if (mainframe->preferences_dialog->recreate_GUI()) - recreate_GUI(_L("Restart application") + dots); - -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (mainframe->preferences_dialog->seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - this->plater_->refresh_print(); - -#ifdef _WIN32 - if (is_editor()) { - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); - } - else { - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); - } -#endif // _WIN32 - - if (mainframe->preferences_dialog->settings_layout_changed()) { - // hide full main_sizer for mainFrame - mainframe->GetSizer()->Show(false); - mainframe->update_layout(); - mainframe->select_tab(size_t(0)); - } -} - -bool GUI_App::has_unsaved_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) - return true; - } - return false; -} - -bool GUI_App::has_current_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - return true; - } - return false; -} - -void GUI_App::update_saved_preset_from_current_preset() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->update_saved_preset_from_current_preset(); - } -} - -std::vector GUI_App::get_active_preset_collections() const -{ - std::vector ret; - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) - ret.push_back(tab->get_presets()); - return ret; -} - -// To notify the user whether he is aware that some preset changes will be lost, -// UnsavedChangesDialog: "Discard / Save / Cancel" -// This is called when: -// - Close Application & Current project isn't saved -// - Load Project & Current project isn't saved -// - Undo / Redo with change of print technologie -// - Loading snapshot -// - Loading config_file/bundle -// UnsavedChangesDialog: "Don't save / Save / Cancel" -// This is called when: -// - Exporting config_bundle -// - Taking snapshot -bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) -{ - if (has_current_preset_changes()) { - const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = ActionButtons::SAVE; - if (dont_save_insted_of_discard) - act_buttons |= ActionButtons::DONT_SAVE; - UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - if (dlg.save_preset()) // save selected changes - { - for (const std::pair& nt : dlg.get_names_and_types()) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - load_current_presets(false); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); - } - } - - return true; -} - -void GUI_App::apply_keeped_preset_modifications() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->apply_config_from_cache(); - } - load_current_presets(false); -} - -// This is called when creating new project or load another project -// OR close ConfigWizard -// to ask the user what should we do with unsaved changes for presets. -// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" -// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed -bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) -{ - if (has_current_preset_changes()) { - bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; - - const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; - UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - auto reset_modifications = [this, is_called_from_configwizard]() { - if (is_called_from_configwizard) - return; // no need to discared changes. It will be done fromConfigWizard closing - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - tab->m_presets->discard_current_changes(); - } - load_current_presets(false); - }; - - if (dlg.discard()) - reset_modifications(); - else // save selected changes - { - const auto& preset_names_and_types = dlg.get_names_and_types(); - if (dlg.save_preset()) { - for (const std::pair& nt : preset_names_and_types) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); - if (!is_called_from_configwizard) - text += "\n\n" + _L("For new project all modifications will be reseted"); - - MessageDialog(nullptr, text).ShowModal(); - reset_modifications(); - } - else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { - // execute this part of code only if not all modifications are keeping to the new project - // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected - for (const std::pair& nt : preset_names_and_types) { - Preset::Type type = nt.second; - Tab* tab = get_tab(type); - std::vector selected_options = dlg.get_selected_options(type); - if (type == Preset::TYPE_PRINTER) { - auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); - if (it != selected_options.end()) { - // erase "extruders_count" option from the list - selected_options.erase(it); - // cache the extruders count - static_cast(tab)->cache_extruder_cnt(); - } - } - tab->cache_config_diff(selected_options); - if (!is_called_from_configwizard) - tab->m_presets->discard_current_changes(); - } - if (is_called_from_configwizard) - *postponed_apply_of_keeped_changes = true; - else - apply_keeped_preset_modifications(); - } - } - } - - return true; -} - -bool GUI_App::can_load_project() -{ - int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); - if (saved_project == wxID_CANCEL || - (plater()->is_project_dirty() && saved_project == wxID_NO && - !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) - return false; - return true; -} - -bool GUI_App::check_print_host_queue() -{ - wxString dirty; - std::vector> jobs; - // Get ongoing jobs from dialog - mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); - if (jobs.empty()) - return true; - // Show dialog - wxString job_string = wxString(); - for (const auto& job : jobs) { - job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); - } - wxString message; - message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); - //wxMessageDialog dialog(mainframe, - MessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - if (dialog.ShowModal() == wxID_YES) - return true; - - // TODO: If already shown, bring forward - mainframe->m_printhost_queue_dlg->Show(); - return false; -} - -bool GUI_App::checked_tab(Tab* tab) -{ - bool ret = true; - if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) - ret = false; - return ret; -} - -// Update UI / Tabs to reflect changes in the currently loaded presets -void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) -{ - // check printer_presets for the containing information about "Print Host upload" - // and create physical printer from it, if any exists - if (check_printer_presets_) - check_printer_presets(); - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - this->plater()->set_printer_technology(printer_technology); - for (Tab *tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) { - if (tab->type() == Preset::TYPE_PRINTER) { - static_cast(tab)->update_pages(); - // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). - this->plater()->force_print_bed_update(); - } - tab->load_current_preset(); - } -} - -bool GUI_App::OnExceptionInMainLoop() -{ - generic_exception_handle(); - return false; -} - -#ifdef __APPLE__ -// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run -// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App -// to a G-code viewer. -void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) -{ - size_t num_gcodes = 0; - for (const wxString &filename : fileNames) - if (is_gcode_file(into_u8(filename))) - ++ num_gcodes; - if (fileNames.size() == num_gcodes) { - // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, - // just G-codes were passed. Switch to G-code viewer mode. - m_app_mode = EAppMode::GCodeViewer; - unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); - if(app_config != nullptr) - delete app_config; - app_config = nullptr; - init_app_config(); - } - wxApp::OSXStoreOpenFiles(fileNames); -} -// wxWidgets override to get an event on open files. -void GUI_App::MacOpenFiles(const wxArrayString &fileNames) -{ - std::vector files; - std::vector gcode_files; - std::vector non_gcode_files; - for (const auto& filename : fileNames) { - if (is_gcode_file(into_u8(filename))) - gcode_files.emplace_back(filename); - else { - files.emplace_back(into_u8(filename)); - non_gcode_files.emplace_back(filename); - } - } - if (m_app_mode == EAppMode::GCodeViewer) { - // Running in G-code viewer. - // Load the first G-code into the G-code viewer. - // Or if no G-codes, send other files to slicer. - if (! gcode_files.empty()) { - if (m_post_initialized) - this->plater()->load_gcode(gcode_files.front()); - else - this->init_params->input_files = { into_u8(gcode_files.front()) }; - } - if (!non_gcode_files.empty()) - start_new_slicer(non_gcode_files, true); - } else { - if (! files.empty()) { - if (m_post_initialized) { - wxArrayString input_files; - for (size_t i = 0; i < non_gcode_files.size(); ++i) - input_files.push_back(non_gcode_files[i]); - this->plater()->load_files(input_files); - } else { - for (const auto &f : non_gcode_files) - this->init_params->input_files.emplace_back(into_u8(f)); - } - } - for (const wxString &filename : gcode_files) - start_new_gcodeviewer(&filename); - } -} -#endif /* __APPLE */ - -Sidebar& GUI_App::sidebar() -{ - return plater_->sidebar(); -} - -ObjectManipulation* GUI_App::obj_manipul() -{ - // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) - return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; -} - -ObjectSettings* GUI_App::obj_settings() -{ - return sidebar().obj_settings(); -} - -ObjectList* GUI_App::obj_list() -{ - return sidebar().obj_list(); -} - -ObjectLayers* GUI_App::obj_layers() -{ - return sidebar().obj_layers(); -} - -Plater* GUI_App::plater() -{ - return plater_; -} - -const Plater* GUI_App::plater() const -{ - return plater_; -} - -Model& GUI_App::model() -{ - return plater_->model(); -} -wxBookCtrlBase* GUI_App::tab_panel() const -{ - return mainframe->m_tabpanel; -} - -NotificationManager * GUI_App::notification_manager() -{ - return plater_->get_notification_manager(); -} - -GalleryDialog* GUI_App::gallery_dialog() -{ - return mainframe->gallery_dialog(); -} - -// extruders count from selected printer preset -int GUI_App::extruders_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_selected_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -// extruders count from edited printer preset -int GUI_App::extruders_edited_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_edited_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -wxString GUI_App::current_language_code_safe() const -{ - // Translate the language code to a code, for which Prusa Research maintains translations. - const std::map mapping { - { "cs", "cs_CZ", }, - { "sk", "cs_CZ", }, - { "de", "de_DE", }, - { "es", "es_ES", }, - { "fr", "fr_FR", }, - { "it", "it_IT", }, - { "ja", "ja_JP", }, - { "ko", "ko_KR", }, - { "pl", "pl_PL", }, - { "uk", "uk_UA", }, - { "zh", "zh_CN", }, - { "ru", "ru_RU", }, - }; - wxString language_code = this->current_language_code().BeforeFirst('_'); - auto it = mapping.find(language_code); - if (it != mapping.end()) - language_code = it->second; - else - language_code = "en_US"; - return language_code; -} - -void GUI_App::open_web_page_localized(const std::string &http_address) -{ - open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); -} - -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. -bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) -{ - if (model_has_multi_part_objects(model())) { - show_info(nullptr, - _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - if (model_has_connectors(model())) { - show_info(nullptr, - _L("SLA technology doesn't support cut with connectors") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - return true; -} - -bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) -{ - wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - - if (reason == ConfigWizard::RR_USER) { - if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) - return false; - } - - auto wizard = new ConfigWizard(mainframe); - const bool res = wizard->run(reason, start_page); - - if (res) { - load_current_presets(); - - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); - } - - return res; -} - -void GUI_App::show_desktop_integration_dialog() -{ -#ifdef __linux__ - //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - DesktopIntegrationDialog dialog(mainframe); - dialog.ShowModal(); -#endif //__linux__ -} - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -void GUI_App::gcode_thumbnails_debug() -{ - const std::string BEGIN_MASK = "; thumbnail begin"; - const std::string END_MASK = "; thumbnail end"; - std::string gcode_line; - bool reading_image = false; - unsigned int width = 0; - unsigned int height = 0; - - wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK) - return; - - std::string in_filename = into_u8(dialog.GetPath()); - std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); - - boost::nowide::ifstream in_file(in_filename.c_str()); - std::vector rows; - std::string row; - if (in_file.good()) - { - while (std::getline(in_file, gcode_line)) - { - if (in_file.good()) - { - if (boost::starts_with(gcode_line, BEGIN_MASK)) - { - reading_image = true; - gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); - std::string::size_type x_pos = gcode_line.find('x'); - std::string width_str = gcode_line.substr(0, x_pos); - width = (unsigned int)::atoi(width_str.c_str()); - std::string height_str = gcode_line.substr(x_pos + 1); - height = (unsigned int)::atoi(height_str.c_str()); - row.clear(); - } - else if (reading_image && boost::starts_with(gcode_line, END_MASK)) - { - std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; - boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); - if (out_file.good()) - { - std::string decoded; - decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); - decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); - - out_file.write(decoded.c_str(), decoded.size()); - out_file.close(); - } - - reading_image = false; - width = 0; - height = 0; - rows.clear(); - } - else if (reading_image) - row += gcode_line.substr(2); - } - } - - in_file.close(); - } -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - WindowMetrics metrics = WindowMetrics::from_window(window); - app_config->set(config_key, metrics.serialize()); - app_config->save(); -} - -void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - if (! app_config->has(config_key)) { - window->Maximize(default_maximized); - return; - } - - auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); - if (! metrics) { - window->Maximize(default_maximized); - return; - } - - const wxRect& rect = metrics->get_rect(); - - if (app_config->get("restore_win_position") == "1") { - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); - app_config->save(); - window->SetPosition(rect.GetPosition()); - - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); - app_config->save(); - window->SetSize(rect.GetSize()); - - // revert "restore_win_position" value if application wasn't crashed - app_config->set("restore_win_position", "1"); - app_config->save(); - } - else - window->CenterOnScreen(); - - window->Maximize(metrics->get_maximized()); -} - -void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) -{ - /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); - wxRect display; - if (display_idx == wxNOT_FOUND) { - display = wxDisplay(0u).GetClientArea(); - window->Move(display.GetTopLeft()); - } else { - display = wxDisplay(display_idx).GetClientArea(); - } - - auto metrics = WindowMetrics::from_window(window); - metrics.sanitize_for_display(display); - if (window->GetScreenRect() != metrics.get_rect()) { - window->SetSize(metrics.get_rect()); - } -} - -bool GUI_App::config_wizard_startup() -{ - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - run_wizard(ConfigWizard::RR_DATA_EMPTY); - return true; - } else if (get_app_config()->legacy_datadir()) { - // Looks like user has legacy pre-vendorbundle data directory, - // explain what this is and run the wizard - - MsgDataLegacy dlg; - dlg.ShowModal(); - - run_wizard(ConfigWizard::RR_DATA_LEGACY); - return true; - } - return false; -} - -bool GUI_App::check_updates(const bool verbose) -{ - PresetUpdater::UpdateResult updater_result; - try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (verbose && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } - } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); - } - // Applicaiton will continue. - return true; -} - -bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) -{ - bool launch = true; - - // warning dialog containes a "Remember my choice" checkbox - std::string option_key = "suppress_hyperlinks"; - if (force_remember_choice || app_config->get(option_key).empty()) { - if (app_config->get(option_key).empty()) { - RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - auto answer = dialog.ShowModal(); - launch = answer == wxID_YES; - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - app_config->set(option_key, answer == wxID_NO ? "1" : "0"); - } - } - if (launch) - launch = app_config->get(option_key) != "1"; - } - // warning dialog doesn't containe a "Remember my choice" checkbox - // and will be shown only when "Suppress to open hyperlink in browser" is ON. - else if (app_config->get(option_key) == "1") { - MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - launch = dialog.ShowModal() == wxID_YES; - } - - return launch && wxLaunchDefaultBrowser(url, flags); -} - -// static method accepting a wxWindow object as first parameter -// void warning_catcher{ -// my($self, $message_dialog) = @_; -// return sub{ -// my $message = shift; -// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; -// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); -// $message_dialog -// ? $message_dialog->(@params) -// : Wx::MessageDialog->new($self, @params)->ShowModal; -// }; -// } - -// Do we need this function??? -// void GUI_App::notify(message) { -// auto frame = GetTopWindow(); -// // try harder to attract user attention on OS X -// if (!frame->IsActive()) -// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); -// -// // There used to be notifier using a Growl application for OSX, but Growl is dead. -// // The notifier also supported the Linux X D - bus notifications, but that support was broken. -// //TODO use wxNotificationMessage ? -// } - - -#ifdef __WXMSW__ -void GUI_App::associate_3mf_files() -{ - associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_stl_files() -{ - associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_gcode_files() -{ - associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); -} -#endif // __WXMSW__ - -void GUI_App::on_version_read(wxCommandEvent& evt) -{ - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { - return; - } - if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { - return; - } - // notification - /* - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - */ - // updater - // read triggered_by_user that was set when calling GUI_App::app_version_check - app_updater(m_app_updater->get_triggered_by_user()); -} - -void GUI_App::app_updater(bool from_user) -{ - DownloadAppData app_data = m_app_updater->get_app_data(); - - if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) - { - BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; - MsgNoAppUpdates no_update_dialog; - no_update_dialog.ShowModal(); - return; - - } - - assert(!app_data.url.empty()); - assert(!app_data.target_path.empty()); - - // dialog with new version info - AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); - auto dialog_result = dialog.ShowModal(); - // checkbox "do not show again" - if (dialog.disable_version_check()) { - app_config->set("notify_release", "none"); - } - // Doesn't wish to update - if (dialog_result != wxID_OK) { - return; - } - // dialog with new version download (installer or app dependent on system) including path selection - AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); - dialog_result = dwnld_dlg.ShowModal(); - // Doesn't wish to download - if (dialog_result != wxID_OK) { - return; - } - app_data.target_path =dwnld_dlg.get_download_path(); - - // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); - app_data.start_after = dwnld_dlg.run_after_download(); - m_app_updater->set_app_data(std::move(app_data)); - m_app_updater->sync_download(); -} - -void GUI_App::app_version_check(bool from_user) -{ - if (from_user) { - if (m_app_updater->get_download_ongoing()) { - MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); - if (msgdlg.ShowModal() != wxID_YES) - return; - } - } - std::string version_check_url = app_config->version_check_url(); - m_app_updater->sync_version(version_check_url, from_user); -} - -} // GUI -} //Slic3r +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_Factories.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" +#include "../Utils/WinRegistry.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "ConfigSnapshotDialog.hpp" +#include "FirmwareDialog.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" +#include "Downloader.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" + +#ifdef __WXMSW__ +#include +#include +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE +#endif +#ifdef _WIN32 +#include +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + +// int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + // The size of the SplashScreen can be hanged after its moving to another display + // So, update it from a bitmap size + this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); + this->CenterOnScreen(); +// int new_dpi = get_dpi_for_window(this); + +// m_scale = (float)(new_dpi) / (float)(init_dpi); + m_main_bitmap = bitmap; + +// scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("Version") + " " + std::string(SLIC3R_VERSION); + + // credits infornation + credits = title + " " + + _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + + _L("Developed by Prusa Research.") + "\n\n" + + title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + + _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + + _L("Artwork model by Leslie Ing"); + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + + if (fs::is_directory(old_path)) { + wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " + "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" + "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " + "an old %1% configuration directory was detected in \n%3%.\n\n" + "Consider moving the contents of the old directory to the new location in order to access " + "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " + "location again.\n\n" + "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); + wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); + if (dlg.ShowModal() != wxID_NO) + return false; + } + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +#ifdef _WIN32 +#if 0 // External Updater is replaced with AppUpdater.cpp +static bool run_updater_win() +{ + // find updater exe + boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + std::string msg; + bool res = create_process(path_updater, L"/silent", msg); + if (!res) + BOOST_LOG_TRIVIAL(error) << msg; + return res; +} +#endif // 0 +#endif // _WIN32 + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + + + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, + /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, +}; + +#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR +wxString file_wildcards(FileType file_type) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + + // Generate cumulative first item + for (const std::string_view& ext : data.file_extensions) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } + else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); + + // Adds an item for each of the extensions + if (data.file_extensions.size() > 1) { + for (const std::string_view& ext : data.file_extensions) { + title = "*"; + title += ext; + ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); + } + } + + return ret; +} +#else +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} +#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a + + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + std::terminate(); + } catch (const boost::io::bad_format_string& ex) { + wxString errmsg = _L("PrusaSlicer has encountered a localization error. " + "Please report to PrusaSlicer team, what language was active and in which scenario " + "this issue happened. Thank you.\n\nThe application will now terminate."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + std::terminate(); + throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + throw; + } +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (this->is_gcode_viewer()) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else if (this->init_params->start_downloader) { + start_download(this->init_params->download_url); + } else { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { + const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); + if (!res.empty() && this->init_params->input_files.size() == 1) { + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + if (boost::algorithm::iends_with(filename, ".amf") || + boost::algorithm::iends_with(filename, ".amf.xml") || + boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(from_u8(filename)); + } + if (this->init_params->delete_after_load) { + for (const std::string& p : this->init_params->input_files) { + boost::system::error_code ec; + boost::filesystem::remove(boost::filesystem::path(p), ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + } + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + } + + // show "Did you know" notification + if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) + plater_->get_notification_manager()->push_hint_notification(true); + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + if (! this->check_updates(false)) + // Configuration is not compatible and reconfigure was refused by the user. Application is closing. + return; + CallAfter([this] { + bool cw_showed = this->config_wizard_startup(); + this->preset_updater->sync(preset_bundle); + this->app_version_check(false); + if (! cw_showed) { + // The CallAfter is needed as well, without it, GL extensions did not show. + // Also, we only want to show this when the wizard does not, so the new user + // sees something else than "we want something" on the first start. + show_send_system_info_dialog_if_needed(); + } + }); + } + + // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +IMPLEMENT_APP(GUI_App) + +GUI_App::GUI_App(EAppMode mode) + : wxApp() + , m_app_mode(mode) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) + , m_downloader(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); +} + +GUI_App::~GUI_App() +{ + if (app_config != nullptr) + delete app_config; + + if (preset_bundle != nullptr) + delete preset_bundle; + + if (preset_updater != nullptr) + delete preset_updater; +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ +#if ENABLE_GL_CORE_PROFILE +#if ENABLE_OPENGL_DEBUG_OPTION + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), + init_params != nullptr ? init_params->opengl_debug : false); +#else + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); +#endif // ENABLE_OPENGL_DEBUG_OPTION +#else + return m_opengl_mgr.init_glcontext(canvas); +#endif // ENABLE_GL_CORE_PROFILE +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("PrusaSlicer "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + +// SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); + + +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); + #endif + } else { + m_datadir_redefined = true; + } + + if (!app_config) + app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + } +} + +// returns old config path to copy from if such exists, +// returns an empty string if such config path does not exists or if it cannot be loaded. +std::string GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + std::string older_data_dir_path; + + // If the config folder is redefined - do not check + if (m_datadir_redefined) + return {}; + + // find other version app config (alpha / beta / release) + std::string config_path = app_config->config_path(); + boost::filesystem::path parent_file_path(config_path); + std::string filename = parent_file_path.filename().string(); + parent_file_path.remove_filename().remove_filename(); + + std::vector candidates; + + if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); + if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); + if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + + Semver last_semver = current_version; + for (const auto& candidate : candidates) { + if (boost::filesystem::exists(candidate)) { + // parse + boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + if (other_semver && *other_semver > last_semver) { + last_semver = *other_semver; + older_data_dir_path = candidate.parent_path().string(); + } + } + } + if (older_data_dir_path.empty()) + return {}; + BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; + // ask about using older data folder + + InfoDialog msg(nullptr + , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) + , backup ? + format_wxstr(_L( + "The active configuration was created by %1% %2%," + "\nwhile a newer configuration was found in %3%" + "\ncreated by %1% %4%." + "\n\nShall the newer configuration be imported?" + "\nIf so, your active configuration will be backed up before importing the new configuration." + ) + , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) + : format_wxstr(_L( + "An existing configuration was found in %3%" + "\ncreated by %1% %2%." + "\n\nShall this configuration be imported?" + ) + , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) + , true, wxYES_NO); + + if (backup) { + msg.SetButtonLabel(wxID_YES, _L("Import")); + msg.SetButtonLabel(wxID_NO, _L("Don't import")); + } + + if (msg.ShowModal() == wxID_YES) { + std::string snapshot_id; + if (backup) { + const Config::Snapshot* snapshot{ nullptr }; + if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", + _u8L("Continue and import newer configuration?"), &snapshot)) + return {}; + if (snapshot) { + // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. + snapshot_id = snapshot->id; + assert(! snapshot_id.empty()); + app_config->set("on_snapshot", snapshot_id); + } else + BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; + } + + // load app config from older file + std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + if (!snapshot_id.empty()) + app_config->set("on_snapshot", snapshot_id); + m_app_conf_exists = true; + return older_data_dir_path; + } + return {}; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception&) { + generic_exception_handle(); + return false; + } +} + +bool GUI_App::on_init_inner() +{ + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); + +#if defined(_WIN32) && ! defined(_WIN64) + // Win32 32bit build. + if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { + RichMessageDialog dlg(nullptr, + _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." + "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." + "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." + "\nDo you wish to continue?"), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif + // initialize label colors and fonts + init_ui_colours(); + init_fonts(); + + std::string older_data_dir_path; + if (m_app_conf_exists) { + if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) + // Only copying configuration if it was saved with a newer slicer than the one currently running. + older_data_dir_path = check_older_app_config(app_config->orig_version(), true); + } else { + // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. + older_data_dir_path = check_older_app_config(Semver(), false); + } + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; + init_dark_color_mode != new_dark_color_mode) { + NppDarkMode::SetDarkMode(new_dark_color_mode); + init_ui_colours(); + update_ui_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif + + if (is_editor()) { + std::string msg = Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + } + } + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") { + // make a bitmap with dark grey banner on the left side + wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + bool default_splashscreen_pos = true; + if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + default_splashscreen_pos = metrics == boost::none; + if (!default_splashscreen_pos) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + if (!default_splashscreen_pos) { + // workaround for crash related to the positioning of the window on secondary monitor + get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); + get_app_config()->save(); + } + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); + + if (!default_splashscreen_pos) + // revert "restore_win_position" value if application wasn't crashed + get_app_config()->set("restore_win_position", "1"); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + if (! older_data_dir_path.empty()) { + preset_bundle->import_newer_configs(older_data_dir_path); + app_config->save(); + } + + if (is_editor()) { +#ifdef __WXMSW__ + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); + Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { + std::string evt_string = into_u8(evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { + auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); + this->plater_->get_notification_manager()->push_notification( notif_type + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) + , _u8L("See Releases page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } + ); + } + } + }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + if(!evt.GetString().IsEmpty()) + show_error(nullptr, evt.GetString()); + }); + + Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { + show_error(nullptr, evt.GetString()); + }); + + } + else { +#ifdef __WXMSW__ + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); +#endif // __WXMSW__ + } + + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } catch (const std::exception &ex) { + show_error(nullptr, ex.what()); + } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + // application frame + if (scrn && is_editor()) + scrn->SetText(_L("Preparing settings tabs") + dots); + + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) + mainframe->select_tab(size_t(0)); + + sidebar().obj_list()->init_objects(); // propagate model objects to object list +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + // Save the active profiles as a "saved into project". + update_saved_preset_from_current_preset(); + + if (plater_ != nullptr) { + // Save the names of active presets and project specific config into ProjectDirtyStateManager. + plater_->reset_project_dirty_initial_presets(); + // Update Project dirty state, update application title bar. + plater_->update_project_dirty_from_presets(); + } + + mainframe->Show(true); + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + if (! plater_) + return; + + this->obj_manipul()->update_if_dirty(); + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +#ifdef __linux__ + if (! m_post_initialized && m_opengl_initialized) { +#else + if (! m_post_initialized) { +#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + } + + if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") + app_config->save(); + }); + + m_initialized = true; + + if (const std::string& crash_reason = app_config->get("restore_win_position"); + boost::starts_with(crash_reason,"crashed")) + { + wxString preferences_item = _L("Restore window position on start"); + InfoDialog dialog(nullptr, + _L("PrusaSlicer started after a crash"), + format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" + "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" + "More precise reason for the crash: \"%1%\".\n" + "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" + "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " + "Otherwise, the application will most likely crash again next time."), + "" + from_u8(crash_reason) + "", + "#2939", + "#5573", + "" + preferences_item + ""), + true, wxYES_NO); + + dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); + dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); + + auto answer = dialog.ShowModal(); + if (answer == wxID_YES) + app_config->set("restore_win_position", "0"); + else if (answer == wxID_NO) + app_config->set("restore_win_position", "1"); + app_config->save(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + if (wxGetApp().app_config->has("dark_color_mode")) + return wxGetApp().app_config->get("dark_color_mode") == "1"; + return check_dark_mode(); +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +const std::vector GUI_App::get_mode_default_palette() +{ + return { "#7DF028", "#FFDC00", "#E70000" }; +} + +void GUI_App::init_ui_colours() +{ + m_color_label_modified = get_label_default_clr_modified(); + m_color_label_sys = get_label_default_clr_system(); + m_mode_palette = get_mode_default_palette(); + + bool is_dark_mode = dark_mode(); +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + +void GUI_App::update_ui_colours_from_appconfig() +{ + // load label colors + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (!str.empty()) + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (!str.empty()) + m_color_label_modified = wxColour(str); + } + + // load mode markers colors + if (app_config->has("mode_palette")) { + const auto colors = app_config->get("mode_palette"); + if (!colors.empty()) { + m_mode_palette.clear(); + if (!unescape_strings_cstyle(colors, m_mode_palette)) + m_mode_palette = get_mode_default_palette(); + } + } +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ +#ifdef _WIN32 + bool is_focused_button = false; + bool is_default_button = false; + if (wxButton* btn = dynamic_cast(window)) { + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + // button marking + { + auto mark_button = [this, btn, highlited](const bool mark) { + if (btn->GetLabel().IsEmpty()) + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + else + btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + if (is_focused_button || is_default_button) + mark_button(is_focused_button); + } + } + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); + } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + + if (!just_font) + window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + if (!is_focused_button && !is_default_button) + window->SetForegroundColour(m_color_label_default); +#endif +} + +// recursive function for scaling fonts for all controls in Window +#ifdef _WIN32 +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + bool is_btn = dynamic_cast(window) != nullptr; + if (!(just_buttons_update && !is_btn)) + wxGetApp().UpdateDarkUI(window, is_btn); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} +#endif + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) +{ +#ifdef _WIN32 + update_dark_children_ui(dlg, just_buttons_update); +#endif +} +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef _WIN32 + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + dvc->RefreshHeaderDarkMode(&m_normal_font); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = main_frame->normal_font(); + m_small_font = m_normal_font; + m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_modified", str); + app_config->save(); +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_sys", str); + app_config->save(); +} + +const std::string& GUI_App::get_mode_btn_color(int mode_id) +{ + assert(0 <= mode_id && size_t(mode_id) < m_mode_palette.size()); + return m_mode_palette[mode_id]; +} + +std::vector GUI_App::get_mode_palette() +{ + return { wxColor(m_mode_palette[0]), + wxColor(m_mode_palette[1]), + wxColor(m_mode_palette[2]) }; +} + +void GUI_App::set_mode_palette(const std::vector& palette) +{ + bool save = false; + + for (size_t mode = 0; mode < palette.size(); ++mode) { + const wxColour& clr = palette[mode]; + std::string color_str = clr == wxTransparentColour ? std::string("") : encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + if (m_mode_palette[mode] != color_str) { + m_mode_palette[mode] = color_str; + save = true; + } + } + + if (save) { + mainframe->update_mode_markers(); + app_config->set("mode_palette", escape_strings_cstyle(m_mode_palette)); + app_config->save(); + } +} + +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit*0.1f; +#endif // __APPLE__ + + const std::string& use_val = app_config->get("use_custom_toolbar_size"); + const std::string& val = app_config->get("custom_toolbar_size"); + const std::string& auto_val = app_config->get("auto_toolbar_size"); + + if (val.empty() || auto_val.empty() || use_val.empty()) + return icon_sc; + + int int_val = use_val == "0" ? 100 : atoi(val.c_str()); + // correct value in respect to auto_toolbar_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("auto_toolbar_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" + "Settings will be available in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + +void GUI_App::recreate_GUI(const wxString& msg_name) +{ + m_is_recreating_gui = true; + + mainframe->shutdown(); + + wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Recreating") + dots); + + MainFrame *old_main_frame = mainframe; + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(0)); + // Propagate model objects to object list. + sidebar().obj_list()->init_objects(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Recreating") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading of current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + + dlg.Update(90, _L("Loading of a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + m_is_recreating_gui = false; +} + +void GUI_App::system_info() +{ + SysInfoDialog dlg; + dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef _WIN32 + +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} +#endif //_WIN32 + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); +#ifdef _WIN32 + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); + mainframe->preferences_dialog->force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); +#ifdef _MSW_DARK_MODE + update_scrolls(mainframe); + if (mainframe->is_dlg_layout()) { + // update for tabs bar + UpdateDarkUI(&mainframe->m_settings_dialog); + mainframe->m_settings_dialog.Fit(); + mainframe->m_settings_dialog.Refresh(); + // update scrollbars + update_scrolls(&mainframe->m_settings_dialog); + } +#endif //_MSW_DARK_MODE + } +#endif + mainframe->update_ui_from_settings(); +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + window_pos_save(window, name); + event.Skip(); + }); + + window_pos_restore(window, name, default_maximized); + + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3MF/AMF):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Changing of an application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + if (system_language) { + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); + int cnt = locales.size(); + for (int i = 0; i < cnt; ++i) + if (locales[i].find(system_country) != std::string::npos) { + locales.emplace_back(std::move(locales[i])); + locales[i].clear(); + } + } + + // Now try them one by one. + for (auto it = locales.rbegin(); it != locales.rend(); ++ it) + if (! it->empty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "translation_language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("translation_language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; + + // Get the system language. + { + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + } + } + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; +#ifdef __WXOSX__ + // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: + // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. + // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. + // But wxWidgets guys work on it. + temp_locale.Init(wxLANGUAGE_ENGLISH); +#else + temp_locale.Init(); +#endif // __WXOSX__ + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (! best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); + } + #ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } + #endif + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; + } + + // Select language for locales. This language may be different from the language of the dictionary. + if (language_info == m_language_info_best || language_info == m_language_info_system) { + // The current language matches user's default profile exactly. That's great. + } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + language_info = m_language_info_best; + } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + language_info = m_language_info_system; + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language)) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + { + // UKR Localization specific workaround till the wxWidgets doesn't fixed: + // From wxWidgets 3.1.6 calls setlocation(0, wxInfoLanguage->LocaleTag), see (https://github.com/prusa3d/wxWidgets/commit/deef116a09748796711d1e3509965ee208dcdf0b#diff-7de25e9a71c4dce61bbf76492c589623d5b93fd1bb105ceaf0662075d15f4472), + // where LocaleTag is a Tag of locale in BCP 47 - like notation. + // For Ukrainian Language LocaleTag == "uk". + // But setlocale(0, "uk") returns "English_United Kingdom.1252" instead of "uk", + // and, as a result, locales are set to English_United Kingdom + + if (language_info->CanonicalName == "uk") + setlocale(0, language_info->GetCanonicalWithRegion().data()); + } + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("view_mode")) + return comSimple; + + const auto mode = app_config->get("view_mode"); + return mode == "expert" ? comExpert : + mode == "simple" ? comSimple : comAdvanced; +} + +void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + const std::string mode_str = mode == comExpert ? "expert" : + mode == comSimple ? "simple" : "advanced"; + app_config->set("view_mode", mode_str); + app_config->save(); + update_mode(); +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + +#ifdef _WIN32 //_MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + + plater()->update_menus(); + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::add_config_menu(wxMenuBar *menu) +{ + auto local_menu = new wxMenu(); + wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); + + const auto config_wizard_name = _(ConfigWizard::name(true)); + const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); + // Cmd+, is standard on OS X - what about other operating systems? + if (is_editor()) { + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); + local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + //if (DesktopIntegrationDialog::integration_possible()) + local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); +#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + local_menu->AppendSeparator(); + } + local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _L("Application preferences")); + wxMenu* mode_menu = nullptr; + if (is_editor()) { + local_menu->AppendSeparator(); + mode_menu = new wxMenu(); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); + + local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); + } + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); + if (is_editor()) { + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); + // TODO: for when we're able to flash dictionaries + // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); + } + + local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { + switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + run_wizard(ConfigWizard::RR_USER); + break; + case ConfigMenuUpdateConf: + check_updates(true); + break; + case ConfigMenuUpdateApp: + app_version_check(true); + break; +#ifdef __linux__ + case ConfigMenuDesktopIntegration: + show_desktop_integration_dialog(); + break; +#endif + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + if (wxString action_name = _L("Taking a configuration snapshot"); + check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { + wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); + UpdateDlgDarkUI(&dlg); + + // set current normal font for dialog children, + // because of just dlg.SetFont(normal_font()) has no result; + for (auto child : dlg.GetChildren()) + child->SetFont(normal_font()); + + if (dlg.ShowModal() == wxID_OK) + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); + } + break; + case ConfigMenuSnapshots: + if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) + on_snapshot = app_config->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); + dlg.ShowModal(); + if (!dlg.snapshot_to_activate().empty()) { + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; + try { + app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + + // Load the currently selected preset into the GUI, update the preset selection box. + load_current_presets(); + } catch (std::exception &ex) { + GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); + } + } + } + break; + case ConfigMenuPreferences: + { + open_preferences(); + break; + } + case ConfigMenuLanguage: + { + /* Before change application language, let's check unsaved changes on 3D-Scene + * and draw user's attention to the application restarting after a language change + */ + { + // the dialog needs to be destroyed before the call to switch_language() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Language selection"); + //wxMessageDialog dialog(nullptr, + MessageDialog dialog(nullptr, + _L("Switching the language will trigger application restart.\n" + "You will lose content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxOK | wxCANCEL); + if (dialog.ShowModal() == wxID_CANCEL) + return; + } + + switch_language(); + break; + } + case ConfigMenuFlashFirmware: + FirmwareDialog::run(mainframe); + break; + default: + break; + } + }); + + using std::placeholders::_1; + + if (mode_menu != nullptr) { + auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); + } + + menu->Append(local_menu, _L("&Configuration")); +} + +void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + mainframe->preferences_dialog->show(highlight_option, tab_name); + + if (mainframe->preferences_dialog->recreate_GUI()) + recreate_GUI(_L("Restart application") + dots); + +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (mainframe->preferences_dialog->seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); + +#ifdef _WIN32 + if (is_editor()) { + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); + } + else { + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); + } +#endif // _WIN32 + + if (mainframe->preferences_dialog->settings_layout_changed()) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + } +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector GUI_App::get_active_preset_collections() const +{ + std::vector ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) + ret.push_back(tab->get_presets()); + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; + int act_buttons = ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= ActionButtons::DONT_SAVE; + UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + for (const std::pair& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; + UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + if (is_called_from_configwizard) + return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const std::pair& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); + if (!is_called_from_configwizard) + text += "\n\n" + _L("For new project all modifications will be reseted"); + + MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const std::pair& nt : preset_names_and_types) { + Preset::Type type = nt.second; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + tab->cache_config_diff(selected_options); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); + if (saved_project == wxID_CANCEL || + (plater()->is_project_dirty() && saved_project == wxID_NO && + !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) + return false; + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + } +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + } + wxApp::OSXStoreOpenFiles(fileNames); +} +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) { + if (m_post_initialized) + this->plater()->load_gcode(gcode_files.front()); + else + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) + input_files.push_back(non_gcode_files[i]); + this->plater()->load_files(input_files); + } else { + for (const auto &f : non_gcode_files) + this->init_params->input_files.emplace_back(into_u8(f)); + } + } + for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename); + } +} + +void GUI_App::MacOpenURL(const wxString& url) +{ + if (app_config && app_config->get("downloader_url_registered") != "1") + { + BOOST_LOG_TRIVIAL(error) << "Recieved command to open URL, but it is not allowed in app configuration. URL: " << url; + return; + } + start_download(boost::nowide::narrow(url)); +} + +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +ObjectManipulation* GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} +wxBookCtrlBase* GUI_App::tab_panel() const +{ + return mainframe->m_tabpanel; +} + +NotificationManager* GUI_App::notification_manager() +{ + return plater_->get_notification_manager(); +} + +GalleryDialog* GUI_App::gallery_dialog() +{ + return mainframe->gallery_dialog(); +} + +Downloader* GUI_App::downloader() +{ + return m_downloader.get(); +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_multi_part_objects(model())) { + show_info(nullptr, + _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + if (model_has_connectors(model())) { + show_info(nullptr, + _L("SLA technology doesn't support cut with connectors") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + return true; +} + +bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) +{ + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + + if (reason == ConfigWizard::RR_USER) { + if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + return false; + } + + auto wizard = new ConfigWizard(mainframe); + const bool res = wizard->run(reason, start_page); + + if (res) { + load_current_presets(); + + // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() + if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + window->Maximize(default_maximized); + return; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return; + } + + const wxRect& rect = metrics->get_rect(); + + if (app_config->get("restore_win_position") == "1") { + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); + app_config->save(); + window->SetPosition(rect.GetPosition()); + + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); + app_config->save(); + window->SetSize(rect.GetSize()); + + // revert "restore_win_position" value if application wasn't crashed + app_config->set("restore_win_position", "1"); + app_config->save(); + } + else + window->CenterOnScreen(); + + window->Maximize(metrics->get_maximized()); +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + run_wizard(ConfigWizard::RR_DATA_EMPTY); + return true; + } else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + } + return false; +} + +bool GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } + // Applicaiton will continue. + return true; +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + if (force_remember_choice || app_config->get(option_key).empty()) { + if (app_config->get(option_key).empty()) { + RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + app_config->set(option_key, answer == wxID_NO ? "1" : "0"); + } + } + if (launch) + launch = app_config->get(option_key) != "1"; + } + // warning dialog doesn't containe a "Remember my choice" checkbox + // and will be shown only when "Suppress to open hyperlink in browser" is ON. + else if (app_config->get(option_key) == "1") { + MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + launch = dialog.ShowModal() == wxID_YES; + } + + return launch && wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +void GUI_App::associate_3mf_files() +{ + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_stl_files() +{ + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_gcode_files() +{ + associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); +} +#endif // __WXMSW__ + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + // read triggered_by_user that was set when calling GUI_App::app_version_check + app_updater(m_app_updater->get_triggered_by_user()); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) including path selection + AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + app_data.target_path =dwnld_dlg.get_download_path(); + + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check(bool from_user) +{ + if (from_user) { + if (m_app_updater->get_download_ongoing()) { + MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + } + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url, from_user); +} + +void GUI_App::start_download(std::string url) +{ + if (!plater_) { + BOOST_LOG_TRIVIAL(error) << "Could not start URL download: plater is nullptr."; + return; + } + //lets always init so if the download dest folder was changed, new dest is used + boost::filesystem::path dest_folder(app_config->get("url_downloader_dest")); + if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { + std::string msg = _utf8("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return; + } + m_downloader->init(dest_folder); + m_downloader->start_download(url); +} + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 39d713dba8..81382d092c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -46,6 +46,7 @@ class ObjectList; class ObjectLayers; class Plater; class NotificationManager; +class Downloader; struct GUI_InitParams; class GalleryDialog; @@ -165,6 +166,7 @@ private: std::unique_ptr m_other_instance_message_handler; std::unique_ptr m_app_updater; std::unique_ptr m_single_instance_checker; + std::unique_ptr m_downloader; std::string m_instance_hash_string; size_t m_instance_hash_int; @@ -292,6 +294,7 @@ public: void OSXStoreOpenFiles(const wxArrayString &files) override; // wxWidgets override to get an event on open files. void MacOpenFiles(const wxArrayString &fileNames) override; + void MacOpenURL(const wxString& url) override; #endif /* __APPLE */ Sidebar& sidebar(); @@ -304,6 +307,7 @@ public: Model& model(); NotificationManager * notification_manager(); GalleryDialog * gallery_dialog(); + Downloader* downloader(); // Parameters extracted from the command line to be passed to GUI after initialization. GUI_InitParams* init_params { nullptr }; @@ -358,6 +362,10 @@ public: void associate_gcode_files(); #endif // __WXMSW__ + + // URL download - PrusaSlicer gets system call to open prusaslicer:// URL which should contain address of download + void start_download(std::string url); + private: bool on_init_inner(); void init_app_config(); @@ -380,6 +388,7 @@ private: void app_version_check(bool from_user); bool m_datadir_redefined { false }; + }; DECLARE_APP(GUI_App) diff --git a/src/slic3r/GUI/GUI_Init.hpp b/src/slic3r/GUI/GUI_Init.hpp index 878edc1d97..14d0c4e280 100644 --- a/src/slic3r/GUI/GUI_Init.hpp +++ b/src/slic3r/GUI/GUI_Init.hpp @@ -30,6 +30,9 @@ struct GUI_InitParams std::vector input_files; bool start_as_gcodeviewer; + bool start_downloader; + bool delete_after_load; + std::string download_url; #if ENABLE_GL_CORE_PROFILE std::pair opengl_version; #if ENABLE_OPENGL_DEBUG_OPTION diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index f64666d7bb..20913f66db 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -96,7 +96,12 @@ static const std::map font_icons_large = { {ImGui::DocumentationButton , "notification_documentation" }, {ImGui::DocumentationHoverButton, "notification_documentation_hover"}, {ImGui::InfoMarker , "notification_info" }, - + {ImGui::PlayButton , "notification_play" }, + {ImGui::PlayHoverButton , "notification_play_hover" }, + {ImGui::PauseButton , "notification_pause" }, + {ImGui::PauseHoverButton , "notification_pause_hover" }, + {ImGui::OpenButton , "notification_open" }, + {ImGui::OpenHoverButton , "notification_open_hover" }, }; static const std::map font_icons_extra_large = { diff --git a/src/slic3r/GUI/InstanceCheck.cpp b/src/slic3r/GUI/InstanceCheck.cpp index 81d5e01feb..4831b90845 100644 --- a/src/slic3r/GUI/InstanceCheck.cpp +++ b/src/slic3r/GUI/InstanceCheck.cpp @@ -373,6 +373,7 @@ bool instance_check(int argc, char** argv, bool app_config_single_instance) namespace GUI { wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); +wxDEFINE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent); wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler) @@ -501,12 +502,20 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message) } std::vector paths; + std::vector downloads; // Skip the first argument, it is the path to the slicer executable. auto it = args.begin(); for (++ it; it != args.end(); ++ it) { boost::filesystem::path p = MessageHandlerInternal::get_path(*it); if (! p.string().empty()) paths.emplace_back(p); +// TODO: There is a misterious slash appearing in recieved msg on windows +#ifdef _WIN32 + else if (it->rfind("prusaslicer://open/?file=", 0) == 0) +#else + else if (it->rfind("prusaslicer://open?file=", 0) == 0) +#endif + downloads.emplace_back(*it); } if (! paths.empty()) { //wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here? @@ -514,6 +523,10 @@ void OtherInstanceMessageHandler::handle_message(const std::string& message) wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector(std::move(paths)))); //} } + if (!downloads.empty()) + { + wxPostEvent(m_callback_evt_handler, StartDownloadOtherInstanceEvent(GUI::EVT_START_DOWNLOAD_OTHER_INSTANCE, std::vector(std::move(downloads)))); + } } #ifdef __APPLE__ @@ -545,6 +558,9 @@ namespace MessageHandlerDBusInternal " " " " " " + " " + " " + " " " " " "; @@ -553,6 +569,7 @@ namespace MessageHandlerDBusInternal dbus_connection_send(connection, reply, NULL); dbus_message_unref(reply); } + //method AnotherInstance receives message from another PrusaSlicer instance static void handle_method_another_instance(DBusConnection *connection, DBusMessage *request) { @@ -587,6 +604,9 @@ namespace MessageHandlerDBusInternal } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("AnotherInstance", member_name)) { handle_method_another_instance(connection, message); return DBUS_HANDLER_RESULT_HANDLED; + } else if (0 == strcmp(our_interface.c_str(), interface_name) && 0 == strcmp("Introspect", member_name)) { + respond_to_introspect(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } diff --git a/src/slic3r/GUI/InstanceCheck.hpp b/src/slic3r/GUI/InstanceCheck.hpp index 10ccf7b925..54ec12c31f 100644 --- a/src/slic3r/GUI/InstanceCheck.hpp +++ b/src/slic3r/GUI/InstanceCheck.hpp @@ -43,8 +43,9 @@ class MainFrame; #endif // __linux__ using LoadFromOtherInstanceEvent = Event>; +using StartDownloadOtherInstanceEvent = Event>; wxDECLARE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); - +wxDECLARE_EVENT(EVT_START_DOWNLOAD_OTHER_INSTANCE, StartDownloadOtherInstanceEvent); using InstanceGoToFrontEvent = SimpleEvent; wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 1138163fb7..e6f9d952a6 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -947,6 +947,7 @@ void NotificationManager::ProgressBarNotification::render_bar(ImGuiWrapper& imgu imgui.text(text.c_str()); } } + //------ProgressBarWithCancelNotification---------------- void NotificationManager::ProgressBarWithCancelNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) @@ -1060,6 +1061,263 @@ void NotificationManager::ProgressBarWithCancelNotification::render_bar(ImGuiWra ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); imgui.text(text.c_str()); } + + + + + + + + + + + + + +//------URLDownloadNotification---------------- + +void NotificationManager::URLDownloadNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (m_percentage < 0.f || m_percentage >= 1.f) { + render_close_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + if (m_percentage >= 1.f) + render_open_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + } else + render_pause_cancel_buttons_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); +} +void NotificationManager::URLDownloadNotification::render_close_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CloseNotifButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CloseNotifHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + close(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + close(); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::URLDownloadNotification::render_pause_cancel_buttons_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + + render_cancel_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + render_pause_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); +} +void NotificationManager::URLDownloadNotification::render_pause_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + std::wstring button_text; + button_text = (m_download_paused ? ImGui::PlayButton : ImGui::PauseButton); + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y), + ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y), + true)) + { + button_text = (m_download_paused ? ImGui::PlayHoverButton : ImGui::PauseHoverButton); + } + + ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.f, win_size.y)) + { + trigger_user_action_callback(m_download_paused ? DownloaderUserAction::DownloadUserContinued : DownloaderUserAction::DownloadUserPaused); + } + ImGui::PopStyleColor(5); +} + +void NotificationManager::URLDownloadNotification::render_open_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + std::wstring button_text; + button_text = ImGui::OpenButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y), + ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y), + true)) + { + button_text = ImGui::OpenHoverButton; + } + + ImVec2 button_pic_size = ImGui::CalcTextSize(boost::nowide::narrow(button_text).c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.f, win_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserOpenedFolder); + } + ImGui::PopStyleColor(5); +} + +void NotificationManager::URLDownloadNotification::render_cancel_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CancelButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CancelHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + trigger_user_action_callback(DownloaderUserAction::DownloadUserCanceled); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::URLDownloadNotification::trigger_user_action_callback(DownloaderUserAction action) +{ + if (m_user_action_callback) { + if (m_user_action_callback(action, m_download_id)) {} + } +} + + +void NotificationManager::URLDownloadNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + std::string text; + if (m_percentage < 0.f) { + text = _u8L("ERROR") + ": " + m_error_message; + } else if (m_percentage >= 1.f) { + text = _u8L("COMPLETED"); + } else { + std::stringstream stream; + stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%"; + text = stream.str(); + } + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); + imgui.text(text.c_str()); +} + +void NotificationManager::URLDownloadNotification::count_spaces() +{ + ProgressBarNotification::count_spaces(); + m_window_width_offset = m_line_height * 6; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + //------PrintHostUploadNotification---------------- void NotificationManager::PrintHostUploadNotification::init() @@ -2162,7 +2420,6 @@ void NotificationManager::upload_job_notification_show_error(int id, const std:: } } } - void NotificationManager::push_download_progress_notification(const std::string& text, std::function cancel_callback) { // If already exists, change text and reset progress @@ -2194,6 +2451,81 @@ void NotificationManager::set_download_progress_percentage(float percentage) } } +void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback) +{ + // If already exists + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload && dynamic_cast(notification.get())->get_download_id() == id) { + return; + } + } + // push new one + NotificationData data{ NotificationType::URLDownload, NotificationLevel::ProgressBarNotificationLevel, 5, _utf8("Download:") + " " + text }; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, id, user_action_callback), 0); +} + +void NotificationManager::set_download_URL_progress(size_t id, float percentage) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + // if this changes the percentage, it should be shown now + float percent_b4 = ntf->get_percentage(); + ntf->set_percentage(percentage); + ntf->set_paused(false); + if (ntf->get_percentage() != percent_b4) + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + +void NotificationManager::set_download_URL_paused(size_t id) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + ntf->set_paused(true); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + +void NotificationManager::set_download_URL_canceled(size_t id) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + ntf->close(); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} +void NotificationManager::set_download_URL_error(size_t id, const std::string& text) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + float percent_b4 = ntf->get_percentage(); + ntf->set_percentage(-1.f); + ntf->set_error_message(text); + if (ntf->get_percentage() != percent_b4) + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} + void NotificationManager::init_slicing_progress_notification(std::function cancel_callback) { for (std::unique_ptr& notification : m_pop_notifications) { diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 3d0e3fbeeb..7cd77a3048 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -7,6 +7,7 @@ #include "Event.hpp" #include "I18N.hpp" #include "Jobs/ProgressIndicator.hpp" +#include "Downloader.hpp" #include #include @@ -117,6 +118,8 @@ enum class NotificationType NetfabbFinished, // Short meesage to fill space between start and finish of export ExportOngoing, + // Progressbar of download from prusaslicer:// url + URLDownload }; class NotificationManager @@ -215,6 +218,12 @@ public: // Download App progress void push_download_progress_notification(const std::string& text, std::function cancel_callback); void set_download_progress_percentage(float percentage); + // Download URL progress notif + void push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback); + void set_download_URL_progress(size_t id, float percentage); + void set_download_URL_paused(size_t id); + void set_download_URL_canceled(size_t id); + void set_download_URL_error(size_t id, const std::string& text); // slicing progress void init_slicing_progress_notification(std::function cancel_callback); void set_slicing_progress_began(); @@ -505,6 +514,62 @@ private: long m_hover_time{ 0 }; }; + class URLDownloadNotification : public ProgressBarNotification + { + public: + URLDownloadNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, size_t download_id, std::function user_action_callback) + //: ProgressBarWithCancelNotification(n, id_provider, evt_handler, cancel_callback) + : ProgressBarNotification(n, id_provider, evt_handler) + , m_download_id(download_id) + , m_user_action_callback(user_action_callback) + { + } + void set_percentage(float percent) override + { + m_percentage = percent; + if (m_percentage >= 1.f) { + m_notification_start = GLCanvas3D::timestamp_now(); + m_state = EState::Shown; + } else + m_state = EState::NotFading; + } + size_t get_download_id() { return m_download_id; } + void set_user_action_callback(std::function user_action_callback) { m_user_action_callback = user_action_callback; } + void set_paused(bool paused) { m_download_paused = paused; } + void set_error_message(const std::string& message) { m_error_message = message; } + bool compare_text(const std::string& text) const override { return false; }; + protected: + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_close_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_pause_cancel_buttons_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_open_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_cancel_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_pause_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_bar(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void trigger_user_action_callback(DownloaderUserAction action); + + void count_spaces() override; + + size_t m_download_id; + std::function m_user_action_callback; + bool m_download_paused {false}; + std::string m_error_message; + }; + class PrintHostUploadNotification : public ProgressBarNotification { public: @@ -819,7 +884,8 @@ private: NotificationType::PlaterWarning, NotificationType::ProgressBar, NotificationType::PrintHostUpload, - NotificationType::SimplifySuggestion + NotificationType::SimplifySuggestion, + NotificationType::URLDownload }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 54de6cd56c..ee8e5a8422 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #ifdef _WIN32 #include #include @@ -50,6 +51,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/miniz_extension.hpp" #include "GUI.hpp" #include "GUI_App.hpp" @@ -98,6 +100,7 @@ #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification #include "Gizmos/GLGizmoCut.hpp" +#include "FileArchiveDialog.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -2231,6 +2234,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) wxGetApp().mainframe->Raise(); this->q->load_files(input_files); }); + + this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [this](StartDownloadOtherInstanceEvent& evt) { + BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event."; + wxGetApp().mainframe->Raise(); + for (size_t i = 0; i < evt.data.size(); ++i) { + wxGetApp().start_download(evt.data[i]); + } + + }); + this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { bring_instance_forward(); }); @@ -2446,6 +2459,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ int answer_convert_from_meters = wxOK_DEFAULT; int answer_convert_from_imperial_units = wxOK_DEFAULT; + bool in_temp = false; + const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + size_t input_files_size = input_files.size(); for (size_t i = 0; i < input_files_size; ++i) { #ifdef _WIN32 @@ -2456,6 +2472,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute. const auto &path = input_files[i]; #endif // _WIN32 + in_temp = (path.parent_path() == temp_path); const auto filename = path.filename(); if (progress_dlg) { progress_dlg->Update(static_cast(100.0f * static_cast(i) / static_cast(input_files.size())), _L("Loading file") + ": " + from_path(filename)); @@ -2536,7 +2553,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ q->update_filament_colors_in_full_config(); is_project_file = true; } - wxGetApp().app_config->update_config_dir(path.parent_path().string()); + if (!in_temp) + wxGetApp().app_config->update_config_dir(path.parent_path().string()); } } else { @@ -2695,10 +2713,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); } - if (load_model) { + if (load_model && !in_temp) { wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... -// statusbar()->set_status_text(_L("Loaded")); + // statusbar()->set_status_text(_L("Loaded")); } // automatic selection of added objects @@ -2909,9 +2927,10 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type) } std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string(); - + std::string temp_dir = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + wxFileDialog dlg(q, dlg_title, - is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), from_path(output_file.filename()), + out_dir == temp_dir ? from_u8(wxGetApp().app_config->get("last_output_path")) : (is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path())), from_path(output_file.filename()), wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (dlg.ShowModal() != wxID_OK) @@ -4626,7 +4645,10 @@ void Plater::priv::set_project_filename(const wxString& filename) m_project_filename = from_path(full_path); wxGetApp().mainframe->update_title(); - if (!filename.empty()) + const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + bool in_temp = (temp_path == full_path.parent_path().make_preferred()); + + if (!filename.empty() && !in_temp) wxGetApp().mainframe->add_to_recent_projects(filename); } @@ -5359,6 +5381,11 @@ Print& Plater::fff_print() { return p->fff_print; } const SLAPrint& Plater::sla_print() const { return p->sla_print; } SLAPrint& Plater::sla_print() { return p->sla_print; } +bool Plater::is_project_temp() const +{ + return false; +} + void Plater::new_project() { if (int saved_project = p->save_project_if_dirty(_L("Creating a new project while the current project is modified.")); saved_project == wxID_CANCEL) @@ -5563,18 +5590,408 @@ std::vector Plater::load_files(const std::vector& input_fil return p->load_files(paths, load_model, load_config, imperial_units); } -enum class LoadType : unsigned char + +class LoadProjectsDialog : public DPIDialog { - Unknown, - OpenProject, - LoadGeometry, - LoadConfig + int m_action{ 0 }; + bool m_all { false }; + wxComboBox* m_combo_project { nullptr }; + wxComboBox* m_combo_config { nullptr }; +public: + enum class LoadProjectOption : unsigned char + { + Unknown, + AllGeometry, + AllNewWindow, + OneProject, + OneConfig + }; + + LoadProjectsDialog(const std::vector& paths); + + int get_action() const { return m_action + 1; } + bool get_all() const { return m_all; } + int get_selected() const + { + if (m_combo_project && m_combo_project->IsEnabled()) + return m_combo_project->GetSelection(); + else if (m_combo_config && m_combo_config->IsEnabled()) + return m_combo_config->GetSelection(); + else + return -1; + } +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; }; +LoadProjectsDialog::LoadProjectsDialog(const std::vector& paths) + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, + from_u8((boost::format(_utf8(L("%s - Multiple projects file"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE) +{ + SetFont(wxGetApp().normal_font()); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + bool contains_projects = !paths.empty(); + bool instances_allowed = wxGetApp().app_config->get("single_instance") != "1"; + if (contains_projects) + main_sizer->Add(new wxStaticText(this, wxID_ANY, + get_wraped_wxString(_L("There are several files being loaded, including Project files.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10); + else + main_sizer->Add(new wxStaticText(this, wxID_ANY, + get_wraped_wxString(_L("There are several files being loaded.") + "\n" + _L("Select an action to apply to all files."))), 0, wxEXPAND | wxALL, 10); + + wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action")); + if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT); + action_stb->SetFont(wxGetApp().normal_font()); + + if (contains_projects) { + wxArrayString filenames; + for (const fs::path& path : paths) { + filenames.push_back(from_u8(path.filename().string())); + } + m_combo_project = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY); + m_combo_project->SetValue(filenames.front()); + m_combo_project->Enable(false); + + m_combo_config = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, filenames, wxCB_READONLY); + m_combo_config->SetValue(filenames.front()); + m_combo_config->Enable(false); + } + wxStaticBoxSizer* stb_sizer = new wxStaticBoxSizer(action_stb, wxVERTICAL); + int id = 0; + + // all geometry + wxRadioButton* btn = new wxRadioButton(this, wxID_ANY, _L("Import geometry"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + btn->SetValue(id == m_action); + btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) { + m_action = id; + if (contains_projects) { + m_combo_project->Enable(false); + m_combo_config->Enable(false); + } + }); + stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5); + id++; + // all new window + if (instances_allowed) { + btn = new wxRadioButton(this, wxID_ANY, _L("Start new PrusaSlicer instance"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + btn->SetValue(id == m_action); + btn->Bind(wxEVT_RADIOBUTTON, [this, id, contains_projects](wxCommandEvent&) { + m_action = id; + if (contains_projects) { + m_combo_project->Enable(false); + m_combo_config->Enable(false); + } + }); + stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5); + } + id++; // IMPORTANT TO ALWAYS UP THE ID EVEN IF OPTION IS NOT ADDED! + if (contains_projects) { + // one project + btn = new wxRadioButton(this, wxID_ANY, _L("Select one to load as project"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + btn->SetValue(false); + btn->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { + m_action = id; + m_combo_project->Enable(true); + m_combo_config->Enable(false); + }); + stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5); + stb_sizer->Add(m_combo_project, 0, wxEXPAND | wxTOP, 5); + // one config + id++; + btn = new wxRadioButton(this, wxID_ANY, _L("Select one to load config only"), wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + btn->SetValue(id == m_action); + btn->Bind(wxEVT_RADIOBUTTON, [this, id, instances_allowed](wxCommandEvent&) { + m_action = id; + if (instances_allowed) + m_combo_project->Enable(false); + m_combo_config->Enable(true); + }); + stb_sizer->Add(btn, 0, wxEXPAND | wxTOP, 5); + stb_sizer->Add(m_combo_config, 0, wxEXPAND | wxTOP, 5); + } + + + main_sizer->Add(stb_sizer, 1, wxEXPAND | wxRIGHT | wxLEFT, 10); + wxBoxSizer* bottom_sizer = new wxBoxSizer(wxHORIZONTAL); + bottom_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxLEFT, 5); + main_sizer->Add(bottom_sizer, 0, wxEXPAND | wxALL, 10); + SetSizer(main_sizer); + main_sizer->SetSizeHints(this); + + // Update DarkUi just for buttons + wxGetApp().UpdateDlgDarkUI(this, true); +} + +void LoadProjectsDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + const int em = em_unit(); + SetMinSize(wxSize(65 * em, 30 * em)); + Fit(); + Refresh(); +} + + + + +bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) +{ + //std::vector unzipped_paths; + std::vector non_project_paths; + std::vector project_paths; + try + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, archive_path.string())) { + std::string err_msg = GUI::format(_utf8("Loading of a zip archive on path %1% has failed."), archive_path.string()); + throw Slic3r::FileIOError(err_msg); + } + + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + + mz_zip_archive_file_stat stat; + + std::vector selected_paths; + + FileArchiveDialog dlg(static_cast(wxGetApp().mainframe), &archive, selected_paths); + if (dlg.ShowModal() == wxID_OK) + { + std::string archive_path_string = archive_path.string(); + archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); + + fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data()); + + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = GUI::format(wname); + fs::path archive_path(name); + for (const auto& path : selected_paths) { + if (path == archive_path) { + try + { + std::replace(name.begin(), name.end(), '\\', '/'); + // rename if file exists + std::string filename = path.filename().string(); + std::string extension = boost::filesystem::extension(path); + std::string just_filename = filename.substr(0, filename.size() - extension.size()); + std::string final_filename = just_filename; + + size_t version = 0; + while (fs::exists(archive_dir / (final_filename + extension))) + { + ++version; + final_filename = just_filename + "(" + std::to_string(version) + ")"; + } + filename = final_filename + extension; + fs::path final_path = archive_dir / filename; + + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2% "), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + continue; + } + fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(buffer.c_str(), buffer.size()); + file.close(); + if (!fs::exists(final_path)) { + wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string()); + BOOST_LOG_TRIVIAL(error) << error_log; + show_error(nullptr, error_log); + continue; + } + BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path; + + if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) { + non_project_paths.emplace_back(final_path); + continue; + } + // if 3mf - read archive headers to find project file + if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) || + (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) { + non_project_paths.emplace_back(final_path); + continue; + } + + project_paths.emplace_back(final_path); + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + throw Slic3r::FileIOError(e.what()); + } + } + } + } + } + close_zip_reader(&archive); + if (non_project_paths.size() + project_paths.size() != selected_paths.size()) + BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " + << selected_paths.size() + << " Decopressed files: " + << non_project_paths.size() + project_paths.size(); + } else { + close_zip_reader(&archive); + return false; + } + + } + catch (const Slic3r::FileIOError& e) { + // zip reader should be already closed or not even opened + GUI::show_error(this, e.what()); + return false; + } + // none selected + if (project_paths.empty() && non_project_paths.empty()) + { + return false; + } +#if 0 + // 1 project, 0 models - behave like drag n drop + if (project_paths.size() == 1 && non_project_paths.empty()) + { + wxArrayString aux; + aux.Add(from_u8(project_paths.front().string())); + load_files(aux); + //load_files(project_paths, true, true); + boost::system::error_code ec; + fs::remove(project_paths.front(), ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + return true; + } + // 1 model (or more and other instances are not allowed), 0 projects - open geometry + if (project_paths.empty() && (non_project_paths.size() == 1 || wxGetApp().app_config->get("single_instance") == "1")) + { + load_files(non_project_paths, true, false); + boost::system::error_code ec; + fs::remove(non_project_paths.front(), ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + return true; + } + + bool delete_after = true; + + LoadProjectsDialog dlg(project_paths); + if (dlg.ShowModal() == wxID_OK) { + LoadProjectsDialog::LoadProjectOption option = static_cast(dlg.get_action()); + switch (option) + { + case LoadProjectsDialog::LoadProjectOption::AllGeometry: { + load_files(project_paths, true, false); + load_files(non_project_paths, true, false); + break; + } + case LoadProjectsDialog::LoadProjectOption::AllNewWindow: { + delete_after = false; + for (const fs::path& path : project_paths) { + wxString f = from_path(path); + start_new_slicer(&f, false); + } + for (const fs::path& path : non_project_paths) { + wxString f = from_path(path); + start_new_slicer(&f, false); + } + break; + } + case LoadProjectsDialog::LoadProjectOption::OneProject: { + int pos = dlg.get_selected(); + assert(pos >= 0 && pos < project_paths.size()); + if (wxGetApp().can_load_project()) + load_project(from_path(project_paths[pos])); + project_paths.erase(project_paths.begin() + pos); + load_files(project_paths, true, false); + load_files(non_project_paths, true, false); + break; + } + case LoadProjectsDialog::LoadProjectOption::OneConfig: { + int pos = dlg.get_selected(); + assert(pos >= 0 && pos < project_paths.size()); + std::vector aux; + aux.push_back(project_paths[pos]); + load_files(aux, false, true); + project_paths.erase(project_paths.begin() + pos); + load_files(project_paths, true, false); + load_files(non_project_paths, true, false); + break; + } + case LoadProjectsDialog::LoadProjectOption::Unknown: + default: + assert(false); + break; + } + } + + if (!delete_after) + return true; +#else + // 1 project file and some models - behave like drag n drop of 3mf and then load models + if (project_paths.size() == 1) + { + wxArrayString aux; + aux.Add(from_u8(project_paths.front().string())); + bool loaded3mf = load_files(aux, true); + load_files(non_project_paths, true, false); + boost::system::error_code ec; + if (loaded3mf) { + fs::remove(project_paths.front(), ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + return true; + } + + // load all projects and all models as geometry + load_files(project_paths, true, false); + load_files(non_project_paths, true, false); +#endif // 0 + + + for (const fs::path& path : project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + for (const fs::path& path : non_project_paths) { + // Delete file from temp file (path variable), it will stay only in app memory. + boost::system::error_code ec; + fs::remove(path, ec); + if (ec) + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + + return true; +} + class ProjectDropDialog : public DPIDialog { int m_action { 0 }; public: + enum class LoadType : unsigned char + { + Unknown, + OpenProject, + LoadGeometry, + LoadConfig, + OpenWindow + }; ProjectDropDialog(const std::string& filename); int get_action() const { return m_action + 1; } @@ -5590,17 +6007,21 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename) { SetFont(wxGetApp().normal_font()); + bool single_instance_only = wxGetApp().app_config->get("single_instance") == "1"; wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - - const wxString choices[] = { _L("Open as project"), - _L("Import geometry only"), - _L("Import config only") }; + wxArrayString choices; + choices.reserve(4); + choices.Add(_L("Open as project")); + choices.Add(_L("Import geometry only")); + choices.Add(_L("Import config only")); + if (!single_instance_only) + choices.Add(_L("Start new PrusaSlicer instance")); main_sizer->Add(new wxStaticText(this, wxID_ANY, get_wraped_wxString(_L("Select an action to apply to the file") + ": " + from_u8(filename))), 0, wxEXPAND | wxALL, 10); m_action = std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")), - static_cast(LoadType::OpenProject), static_cast(LoadType::LoadConfig)) - 1; + static_cast(LoadType::OpenProject), single_instance_only? static_cast(LoadType::LoadConfig) : static_cast(LoadType::OpenWindow)) - 1; wxStaticBox* action_stb = new wxStaticBox(this, wxID_ANY, _L("Action")); if (!wxOSX) action_stb->SetBackgroundStyle(wxBG_STYLE_PAINT); @@ -5642,9 +6063,9 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } -bool Plater::load_files(const wxArrayString& filenames) +bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/*=false*/) { - const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp)", std::regex::icase); + const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|prusa|step|stp|zip)", std::regex::icase); const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase); std::vector paths; @@ -5688,53 +6109,61 @@ bool Plater::load_files(const wxArrayString& filenames) for (std::vector::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) { std::string filename = (*it).filename().string(); if (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")) { - LoadType load_type = LoadType::Unknown; + ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown; if (!model().objects.empty()) { if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) || (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) - load_type = LoadType::LoadGeometry; + load_type = ProjectDropDialog::LoadType::LoadGeometry; else { if (wxGetApp().app_config->get("show_drop_project_dialog") == "1") { ProjectDropDialog dlg(filename); if (dlg.ShowModal() == wxID_OK) { int choice = dlg.get_action(); - load_type = static_cast(choice); + load_type = static_cast(choice); wxGetApp().app_config->set("drop_project_action", std::to_string(choice)); } } else - load_type = static_cast(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")), - static_cast(LoadType::OpenProject), static_cast(LoadType::LoadConfig))); + load_type = static_cast(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")), + static_cast(ProjectDropDialog::LoadType::OpenProject), static_cast(ProjectDropDialog::LoadType::LoadConfig))); } } else - load_type = LoadType::OpenProject; + load_type = ProjectDropDialog::LoadType::OpenProject; - if (load_type == LoadType::Unknown) + if (load_type == ProjectDropDialog::LoadType::Unknown) return false; switch (load_type) { - case LoadType::OpenProject: { + case ProjectDropDialog::LoadType::OpenProject: { if (wxGetApp().can_load_project()) load_project(from_path(*it)); break; } - case LoadType::LoadGeometry: { + case ProjectDropDialog::LoadType::LoadGeometry: { Plater::TakeSnapshot snapshot(this, _L("Import Object")); load_files({ *it }, true, false); break; } - case LoadType::LoadConfig: { + case ProjectDropDialog::LoadType::LoadConfig: { load_files({ *it }, false, true); break; } - case LoadType::Unknown : { + case ProjectDropDialog::LoadType::OpenWindow: { + wxString f = from_path(*it); + start_new_slicer(&f, false, delete_after_load); + return false; // did not load anything to this instance + } + case ProjectDropDialog::LoadType::Unknown : { assert(false); break; } } return true; + } else if (boost::algorithm::iends_with(filename, ".zip")) { + return preview_zip_archive(*it); + } } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 34b1abc5ad..e254428b56 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -152,6 +152,8 @@ public: void render_project_state_debug_window() const; #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW + bool is_project_temp() const; + Sidebar& sidebar(); const Model& model() const; Model& model(); @@ -175,9 +177,11 @@ public: // To be called when providing a list of files to the GUI slic3r on command line. std::vector load_files(const std::vector& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false); // to be called on drag and drop - bool load_files(const wxArrayString& filenames); + bool load_files(const wxArrayString& filenames, bool delete_after_load = false); void check_selected_presets_visibility(PrinterTechnology loaded_printer_technology); + bool preview_zip_archive(const boost::filesystem::path& input_file); + const wxString& get_last_loaded_gcode() const { return m_last_loaded_gcode; } void update(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 719f0bc162..3c5c11a6bb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -10,6 +10,16 @@ #include "ButtonsDescription.hpp" #include "OG_CustomCtrl.hpp" #include "GLCanvas3D.hpp" +#include "ConfigWizard_private.hpp" + +#include + +#ifdef WIN32 +#include +#endif // WIN32 +#ifdef __linux__ +#include "DesktopIntegrationDialog.hpp" +#endif //__linux__ namespace Slic3r { @@ -80,6 +90,14 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; if (wxGetApp().is_editor()) { + auto app_config = get_app_config(); + + downloader->set_path_name(app_config->get("url_downloader_dest")); + downloader->allow(!app_config->has("downloader_url_registered") || app_config->get("downloader_url_registered") == "1"); + + for (const std::string& opt_key : {"suppress_hyperlinks", "downloader_url_registered"}) + m_optgroup_other->set_value(opt_key, app_config->get(opt_key) == "1"); + // update colors for color pickers of the labels update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); @@ -166,6 +184,25 @@ static void append_enum_option( std::shared_ptr optgroup, wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); } +static void append_string_option(std::shared_ptr optgroup, + const std::string& opt_key, + const std::string& label, + const std::string& tooltip, + const std::string& def_val, + ConfigOptionMode mode = comSimple) +{ + ConfigOptionDef def = { opt_key, coString }; + def.label = label; + def.tooltip = tooltip; + def.mode = mode; + def.set_default_value(new ConfigOptionString{ def_val }); + Option option(def, opt_key); + optgroup->append_single_option_line(option); + + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); +} + static void append_preferences_option_to_searcer(std::shared_ptr optgroup, const std::string& opt_key, const wxString& label) @@ -423,9 +460,9 @@ void PreferencesDialog::build() return; } - if (opt_key == "suppress_hyperlinks") +/* if (opt_key == "suppress_hyperlinks") m_values[opt_key] = boost::any_cast(value) ? "1" : ""; - else + else*/ m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; }; @@ -440,14 +477,14 @@ void PreferencesDialog::build() L("Show sidebar collapse/expand button"), L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"), app_config->get("show_collapse_button") == "1"); - +/* append_bool_option(m_optgroup_gui, "suppress_hyperlinks", L("Suppress to open hyperlink in browser"), L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), app_config->get("suppress_hyperlinks") == "1"); - +*/ append_bool_option(m_optgroup_gui, "color_mapinulation_panel", L("Use colors for axes values in Manipulation panel"), L("If enabled, the axes names and axes values will be colorized according to the axes colors. " @@ -514,6 +551,37 @@ void PreferencesDialog::build() create_settings_text_color_widget(); create_settings_mode_color_widget(); + m_optgroup_other = create_options_tab(_L("Other"), tabs); + m_optgroup_other->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + + if (auto it = m_values.find(opt_key); it != m_values.end() && opt_key != "url_downloader_dest") { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + + if (opt_key == "suppress_hyperlinks") + m_values[opt_key] = boost::any_cast(value) ? "1" : ""; + else + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + + append_bool_option(m_optgroup_other, "suppress_hyperlinks", + L("Suppress to open hyperlink in browser"), + L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), + //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " + // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), + app_config->get("suppress_hyperlinks") == "1"); + + append_bool_option(m_optgroup_other, "downloader_url_registered", + L("Allow downloads from Printables.com"), + L("If enabled, PrusaSlicer will allow to download from Printables.com"), + app_config->get("downloader_url_registered") == "1"); + + activate_options_tab(m_optgroup_other); + + create_downloader_path_sizer(); + #if ENABLE_ENVIRONMENT_MAP // Add "Render" tab m_optgroup_render = create_options_tab(L("Render"), tabs); @@ -587,7 +655,7 @@ std::vector PreferencesDialog::optgroups() { std::vector out; out.reserve(4); - for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() + for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get(), m_optgroup_other.get() #ifdef _WIN32 , m_optgroup_dark_mode.get() #endif // _WIN32 @@ -614,6 +682,16 @@ void PreferencesDialog::update_ctrls_alignment() void PreferencesDialog::accept(wxEvent&) { + if (const auto it = m_values.find("downloader_url_registered"); it != m_values.end()) + downloader->allow(it->second == "1"); + if (!downloader->on_finish()) + return; + +#ifdef __linux__ + if( downloader->get_perform_registration_linux()) + DesktopIntegrationDialog::perform_desktop_integration(true); +#endif // __linux__ + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; for (const std::string& option : options_to_recreate_GUI) { @@ -637,7 +715,7 @@ void PreferencesDialog::accept(wxEvent&) } } - auto app_config = get_app_config(); + auto app_config = get_app_config(); m_seq_top_layer_only_changed = false; if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end()) @@ -738,7 +816,7 @@ void PreferencesDialog::revert(wxEvent&) continue; } - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui, m_optgroup_other #ifdef _WIN32 , m_optgroup_dark_mode #endif // _WIN32 @@ -970,6 +1048,25 @@ void PreferencesDialog::create_settings_mode_color_widget() append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); } +void PreferencesDialog::create_downloader_path_sizer() +{ + wxWindow* parent = m_optgroup_other->parent(); + + wxString title = L("Download path"); + std::string opt_key = "url_downloader_dest"; + m_blinkers[opt_key] = new BlinkingBitmap(parent); + + downloader = new DownloaderUtils::Worker(parent); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); + sizer->Add(downloader, 1, wxALIGN_CENTER_VERTICAL); + + m_optgroup_other->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); + + append_preferences_option_to_searcer(m_optgroup_other, opt_key, title); +} + void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) { if (m_blinkers.find(opt_key) != m_blinkers.end()) @@ -978,7 +1075,7 @@ void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) return; } - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui, m_optgroup_other #ifdef _WIN32 , m_optgroup_dark_mode #endif // _WIN32 diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index 8e11c7375b..15a9576921 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -28,19 +28,24 @@ namespace GUI { class ConfigOptionsGroup; class OG_CustomCtrl; +namespace DownloaderUtils { + class Worker; +} + class PreferencesDialog : public DPIDialog { std::map m_values; std::shared_ptr m_optgroup_general; std::shared_ptr m_optgroup_camera; std::shared_ptr m_optgroup_gui; + std::shared_ptr m_optgroup_other; #ifdef _WIN32 std::shared_ptr m_optgroup_dark_mode; #endif //_WIN32 #if ENABLE_ENVIRONMENT_MAP std::shared_ptr m_optgroup_render; #endif // ENABLE_ENVIRONMENT_MAP - wxSizer* m_icon_size_sizer; + wxSizer* m_icon_size_sizer {nullptr}; wxSlider* m_icon_size_slider {nullptr}; wxRadioButton* m_rb_old_settings_layout_mode {nullptr}; wxRadioButton* m_rb_new_settings_layout_mode {nullptr}; @@ -54,6 +59,8 @@ class PreferencesDialog : public DPIDialog wxColourPickerCtrl* m_mode_advanced { nullptr }; wxColourPickerCtrl* m_mode_expert { nullptr }; + DownloaderUtils::Worker* downloader{ nullptr }; + wxBookCtrlBase* tabs {nullptr}; bool isOSX {false}; @@ -88,6 +95,7 @@ protected: void create_settings_mode_widget(); void create_settings_text_color_widget(); void create_settings_mode_color_widget(); + void create_downloader_path_sizer(); void init_highlighter(const t_config_option_key& opt_key); std::vector optgroups(); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index a6ad54b32d..c0aa68c8ce 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -144,6 +144,7 @@ struct Http::priv void set_post_body(const fs::path &path); void set_post_body(const std::string &body); void set_put_body(const fs::path &path); + void set_range(const std::string& range); std::string curl_error(CURLcode curlcode); std::string body_size_error(); @@ -225,7 +226,7 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o bool cb_cancel = false; if (self->progressfn) { - Progress progress(dltotal, dlnow, ultotal, ulnow); + Progress progress(dltotal, dlnow, ultotal, ulnow, self->buffer); self->progressfn(progress, cb_cancel); } @@ -313,6 +314,11 @@ void Http::priv::set_put_body(const fs::path &path) } } +void Http::priv::set_range(const std::string& range) +{ + ::curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str()); +} + std::string Http::priv::curl_error(CURLcode curlcode) { return (boost::format("%1%:\n%2%\n[Error %3%]") @@ -370,7 +376,7 @@ void Http::priv::http_perform() if (res == CURLE_ABORTED_BY_CALLBACK) { if (cancel) { // The abort comes from the request being cancelled programatically - Progress dummyprogress(0, 0, 0, 0); + Progress dummyprogress(0, 0, 0, 0, std::string()); bool cancel = true; if (progressfn) { progressfn(dummyprogress, cancel); } } else { @@ -438,6 +444,12 @@ Http& Http::size_limit(size_t sizeLimit) return *this; } +Http& Http::set_range(const std::string& range) +{ + if (p) { p->set_range(range); } + return *this; +} + Http& Http::header(std::string name, const std::string &value) { if (!p) { return * this; } diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 2f458582d2..a99b6164b1 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -21,9 +21,10 @@ public: size_t dlnow; // Bytes downloaded so far size_t ultotal; // Total bytes to upload size_t ulnow; // Bytes uploaded so far + const std::string& buffer; // reference to buffer containing all data - Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) : - dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow) + Progress(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow, const std::string& buffer) : + dltotal(dltotal), dlnow(dlnow), ultotal(ultotal), ulnow(ulnow), buffer(buffer) {} }; @@ -65,6 +66,8 @@ public: // Sets a maximum size of the data that can be received. // A value of zero sets the default limit, which is is 5MB. Http& size_limit(size_t sizeLimit); + // range of donloaded bytes. example: curl_easy_setopt(curl, CURLOPT_RANGE, "0-199"); + Http& set_range(const std::string& range); // Sets a HTTP header field. Http& header(std::string name, const std::string &value); // Removes a header field. diff --git a/src/slic3r/Utils/Process.cpp b/src/slic3r/Utils/Process.cpp index a12fd6647a..5de84e91aa 100644 --- a/src/slic3r/Utils/Process.cpp +++ b/src/slic3r/Utils/Process.cpp @@ -33,7 +33,7 @@ enum class NewSlicerInstanceType { // Start a new Slicer process instance either in a Slicer mode or in a G-code mode. // Optionally load a 3MF, STL or a G-code on start. -static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const std::vector paths_to_open, bool single_instance) +static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const std::vector paths_to_open, bool single_instance, bool delete_after_load) { #ifdef _WIN32 wxString path; @@ -49,6 +49,9 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance } if (instance_type == NewSlicerInstanceType::Slicer && single_instance) args.emplace_back(L"--single-instance"); + if(delete_after_load && !paths_to_open.empty()) + args.emplace_back(L"--delete-after-load=1"); + args.emplace_back(nullptr); BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << into_u8(path) << "\""; // Don't call with wxEXEC_HIDE_CONSOLE, PrusaSlicer in GUI mode would just show the splash screen. It would not open the main window though, it would @@ -77,6 +80,8 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance } if (instance_type == NewSlicerInstanceType::Slicer && single_instance) args.emplace_back("--single-instance"); + if (delete_after_load && !paths_to_open.empty()) + args.emplace_back("--delete-after-load=1"); boost::process::spawn(bin_path, args); // boost::process::spawn() sets SIGCHLD to SIGIGN for the child process, thus if a child PrusaSlicer spawns another // subprocess and the subrocess dies, the child PrusaSlicer will not receive information on end of subprocess @@ -121,6 +126,8 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance } if (instance_type == NewSlicerInstanceType::Slicer && single_instance) args.emplace_back("--single-instance"); + if (delete_after_load && !paths_to_open.empty()) + args.emplace_back("--delete-after-load=1"); args.emplace_back(nullptr); BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << args[0] << "\""; if (wxExecute(const_cast(args.data()), wxEXEC_ASYNC | wxEXEC_MAKE_GROUP_LEADER) <= 0) @@ -129,26 +136,26 @@ static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance #endif // Linux or Unix #endif // Win32 } -static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString* path_to_open, bool single_instance) +static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString* path_to_open, bool single_instance, bool delete_after_load) { std::vector paths; if (path_to_open != nullptr) paths.emplace_back(path_to_open->wc_str()); - start_new_slicer_or_gcodeviewer(instance_type, paths, single_instance); + start_new_slicer_or_gcodeviewer(instance_type, paths, single_instance, delete_after_load); } -void start_new_slicer(const wxString *path_to_open, bool single_instance) +void start_new_slicer(const wxString *path_to_open, bool single_instance/*=false*/, bool delete_after_load/*=false*/) { - start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open, single_instance); + start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open, single_instance, delete_after_load); } -void start_new_slicer(const std::vector& files, bool single_instance) +void start_new_slicer(const std::vector& files, bool single_instance/*=false*/, bool delete_after_load/*=false*/) { - start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, files, single_instance); + start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, files, single_instance, delete_after_load); } void start_new_gcodeviewer(const wxString *path_to_open) { - start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open, false); + start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open, false, false); } void start_new_gcodeviewer_open_file(wxWindow *parent) diff --git a/src/slic3r/Utils/Process.hpp b/src/slic3r/Utils/Process.hpp index 494b222eb3..7cba1c2632 100644 --- a/src/slic3r/Utils/Process.hpp +++ b/src/slic3r/Utils/Process.hpp @@ -11,8 +11,8 @@ namespace Slic3r { namespace GUI { // Start a new slicer instance, optionally with a file to open. -void start_new_slicer(const wxString *path_to_open = nullptr, bool single_instance = false); -void start_new_slicer(const std::vector& files, bool single_instance = false); +void start_new_slicer(const wxString *path_to_open = nullptr, bool single_instance = false, bool delete_after_load = false); +void start_new_slicer(const std::vector& files, bool single_instance = false, bool delete_after_load = false); // Start a new G-code viewer instance, optionally with a file to open. void start_new_gcodeviewer(const wxString *path_to_open = nullptr); From fa67b8f5c384c55bb353061cbc04acfa40532b64 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 5 Jan 2023 15:45:24 +0100 Subject: [PATCH 12/20] Miniz: Get filename from Extra Field. Co-authored-by: lane.wei --- src/miniz/miniz.c | 34 ++++++++++++++++++++++++++++ src/miniz/miniz.h | 3 +++ src/slic3r/GUI/FileArchiveDialog.cpp | 15 ++++++++---- src/slic3r/GUI/Plater.cpp | 10 ++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/miniz/miniz.c b/src/miniz/miniz.c index 0d6fc7d9bc..09794bea02 100644 --- a/src/miniz/miniz.c +++ b/src/miniz/miniz.c @@ -7922,6 +7922,40 @@ mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, cha return n + 1; } +mz_uint mz_zip_reader_get_filename_from_extra(mz_zip_archive* pZip, mz_uint file_index, char* buffer, mz_uint extra_buf_size) +{ + if (extra_buf_size == 0) + return 0; + mz_uint nf; + mz_uint ne; + const mz_uint8* p = mz_zip_get_cdh(pZip, file_index); + if (!p) + { + if (extra_buf_size) + buffer[0] = '\0'; + mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); + return 0; + } + nf = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); + ne = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); + + int copy = 0; + char const* p_nf = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + nf; + char const* e = p_nf + ne + 1; + while (p_nf + 4 < e) { + mz_uint16 len = ((mz_uint16)p_nf[2]) | ((mz_uint16)p_nf[3] << 8); + if (p_nf[0] == '\x75' && p_nf[1] == '\x70' && len >= 5 && p_nf + 4 + len < e && p_nf[4] == '\x01') { + mz_uint length = MZ_MIN(len - 5, extra_buf_size - 1); + memcpy(buffer, p_nf + 9, length); + return length; + } + else { + p_nf += 4 + len; + } + } + return 0; +} + mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat) { return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL); diff --git a/src/miniz/miniz.h b/src/miniz/miniz.h index 8fe0461adb..62d579f9eb 100644 --- a/src/miniz/miniz.h +++ b/src/miniz/miniz.h @@ -1166,6 +1166,9 @@ mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, cha int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); int mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index); +/* Retrieves the filename of an archive file entry from EXTRA ID. */ +mz_uint mz_zip_reader_get_filename_from_extra(mz_zip_archive * pZip, mz_uint file_index, char* buffer, mz_uint extra_buf_size); + /* Returns detailed information about an archive file entry. */ mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat); diff --git a/src/slic3r/GUI/FileArchiveDialog.cpp b/src/slic3r/GUI/FileArchiveDialog.cpp index 2813887dff..2b861692a4 100644 --- a/src/slic3r/GUI/FileArchiveDialog.cpp +++ b/src/slic3r/GUI/FileArchiveDialog.cpp @@ -212,10 +212,17 @@ FileArchiveDialog::FileArchiveDialog(wxWindow* parent_window, mz_zip_archive* ar std::vector filtered_entries; for (mz_uint i = 0; i < num_entries; ++i) { if (mz_zip_reader_file_stat(archive, i, &stat)) { - wxString wname = boost::nowide::widen(stat.m_filename); - std::string name = GUI::format(wname); - //std::replace(name.begin(), name.end(), '\\', '/'); - boost::filesystem::path path(name); + std::string extra(1024, 0); + boost::filesystem::path path; + size_t extra_size = mz_zip_reader_get_filename_from_extra(archive, i, extra.data(), extra.size()); + if (extra_size > 0) { + path = boost::filesystem::path(extra.substr(0, extra_size)); + } else { + wxString wname = boost::nowide::widen(stat.m_filename); + std::string name = GUI::format(wname); + path = boost::filesystem::path(name); + } + assert(!path.empty()); if (!path.has_extension()) continue; // filter out MACOS specific hidden files diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ee8e5a8422..ed280697ac 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5769,6 +5769,16 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) wxString wname = boost::nowide::widen(stat.m_filename); std::string name = GUI::format(wname); fs::path archive_path(name); + + std::string extra(1024, 0); + size_t extra_size = mz_zip_reader_get_filename_from_extra(&archive, i, extra.data(), extra.size()); + if (extra_size > 0) { + archive_path = fs::path(extra.substr(0, extra_size)); + name = archive_path.string(); + } + + if (archive_path.empty()) + continue; for (const auto& path : selected_paths) { if (path == archive_path) { try From 50da412251f38ba4abf1e7de817792e3f2a794b3 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Thu, 5 Jan 2023 16:42:54 +0100 Subject: [PATCH 13/20] sermoonv1 resources https://github.com/prusa3d/PrusaSlicer-settings/pull/177 --- resources/profiles/Creality/sermoonv1.svg | 4 ++++ resources/profiles/Creality/sermoonv1_bed.stl | Bin 0 -> 35884 bytes 2 files changed, 4 insertions(+) create mode 100644 resources/profiles/Creality/sermoonv1.svg create mode 100644 resources/profiles/Creality/sermoonv1_bed.stl diff --git a/resources/profiles/Creality/sermoonv1.svg b/resources/profiles/Creality/sermoonv1.svg new file mode 100644 index 0000000000..16ea62ae66 --- /dev/null +++ b/resources/profiles/Creality/sermoonv1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/profiles/Creality/sermoonv1_bed.stl b/resources/profiles/Creality/sermoonv1_bed.stl new file mode 100644 index 0000000000000000000000000000000000000000..b9ea60a0f3c54580678790473421e7dca0881599 GIT binary patch literal 35884 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 PP|fyy%o3oPQ2T!Yi|PSf literal 0 HcmV?d00001 From dd225513b9df119f7d42eacdb29edb2204f6aeee Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Thu, 5 Jan 2023 17:43:50 +0100 Subject: [PATCH 14/20] Fix of vertex reduction. Before fix exists case when after reduction exist triangle with opposite direction of vertices(CW vs CCW) --- src/libslic3r/CutSurface.cpp | 65 ++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp index 375b6b7a0c..4d56b84729 100644 --- a/src/libslic3r/CutSurface.cpp +++ b/src/libslic3r/CutSurface.cpp @@ -78,6 +78,8 @@ using EI = CGAL::SM_Edge_index; using FI = CGAL::SM_Face_index; using P3 = CGAL::Epick::Point_3; +inline Vec3d to_vec3d(const P3 &p) { return Vec3d(p.x(),p.y(),p.z()); } + /// /// Convert triangle mesh model to CGAL Surface_mesh /// Filtrate out opposite triangles @@ -1573,8 +1575,52 @@ void priv::create_reduce_map(ReductionMap &reduction_map, const CutMesh &mesh) assert(!is_reducible_vertex(left)); VI &vi = reduction_map[erase]; // check if it is first add - if (!vi.is_valid()) - reduction_map[erase] = left; + if (vi.is_valid()) + return; + + // check that all triangles after reduction has 'erase' and 'left' vertex + // on same side of opposite line of vertex in triangle + Vec3d v_erase = to_vec3d(mesh.point(erase)); + Vec3d v_left = to_vec3d(mesh.point(left)); + for (FI fi : mesh.faces_around_target(hi)) { + if (!fi.is_valid()) + continue; + // get vertices of rest + VI vi_a, vi_b; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + if (!vi.is_valid()) + continue; + if (vi == erase) + continue; + if (!vi_a.is_valid()) + vi_a = vi; + else { + assert(!vi_b.is_valid()); + vi_b = vi; + } + } + assert(vi_b.is_valid()); + // do not check triangle, which will be removed + if (vi_a == left || vi_b == left) + continue; + + Vec3d v_a = to_vec3d(mesh.point(vi_a)); + Vec3d v_b = to_vec3d(mesh.point(vi_b)); + // Vectors of triangle edges + Vec3d v_ab = v_b - v_a; + Vec3d v_ae = v_erase - v_a; + Vec3d v_al = v_left - v_a; + + Vec3d n1 = v_ab.cross(v_ae); + Vec3d n2 = v_ab.cross(v_al); + // check that normal has same direction + if ((n1.x() > 0 != n2.x() > 0) || + (n1.y() > 0 != n2.y() > 0) || + (n1.z() > 0 != n2.z() > 0)) + return; // this reduction will create CCW triangle + } + + reduction_map[erase] = left; // I have no better rule than take the first // for decide which reduction will be better // But it could be use only one of them @@ -2521,7 +2567,7 @@ bool priv::clip_cut(SurfacePatch &cut, CutMesh clipper) BoundingBoxf3 priv::bounding_box(const CutAOI &cut, const CutMesh &mesh) { const P3& p_from_cut = mesh.point(mesh.target(mesh.halfedge(cut.first.front()))); - Vec3d min(p_from_cut.x(), p_from_cut.y(), p_from_cut.z()); + Vec3d min = to_vec3d(p_from_cut); Vec3d max = min; for (FI fi : cut.first) { for(VI vi: mesh.vertices_around_face(mesh.halfedge(fi))){ @@ -2537,9 +2583,8 @@ BoundingBoxf3 priv::bounding_box(const CutAOI &cut, const CutMesh &mesh) { BoundingBoxf3 priv::bounding_box(const CutMesh &mesh) { - const P3 &p_from_cut = *mesh.points().begin(); - Vec3d min(p_from_cut.x(), p_from_cut.y(), p_from_cut.z()); - Vec3d max = min; + Vec3d min = to_vec3d(*mesh.points().begin()); + Vec3d max = min; for (VI vi : mesh.vertices()) { const P3 &p = mesh.point(vi); for (size_t i = 0; i < 3; ++i) { @@ -2806,7 +2851,7 @@ bool priv::is_patch_inside_of_model(const SurfacePatch &patch, { // TODO: Solve model with hole in projection direction !!! const P3 &a = patch.mesh.point(VI(0)); - Vec3d a_(a.x(), a.y(), a.z()); + Vec3d a_ = to_vec3d(a); Vec3d b_ = projection.project(a_); P3 b(b_.x(), b_.y(), b_.z()); @@ -3396,7 +3441,7 @@ Polygons priv::unproject_loops(const SurfacePatch &patch, const Project &project pts.reserve(l.size()); for (VI vi : l) { const P3 &p3 = patch.mesh.point(vi); - Vec3d p(p3.x(), p3.y(), p3.z()); + Vec3d p = to_vec3d(p3); double depth; std::optional p2_opt = projection.unproject(p, &depth); if (depth_range[0] > depth) depth_range[0] = depth; // min @@ -3597,7 +3642,7 @@ void priv::store(const CutMesh &mesh, const FaceTypeMap &face_type_map, const st default: color = CGAL::Color{0, 0, 255}; // blue } } - CGAL::IO::write_OFF(off_file, mesh); + CGAL::IO::write_OFF(off_file, mesh, CGAL::parameters::face_color_map(face_colors)); mesh_.remove_property_map(face_colors); } @@ -3624,7 +3669,7 @@ void priv::store(const CutMesh &mesh, const ReductionMap &reduction_map, const s vertex_colors[reduction_to] = CGAL::Color{0, 0, 255}; } - CGAL::IO::write_OFF(off_file, mesh); + CGAL::IO::write_OFF(off_file, mesh, CGAL::parameters::vertex_color_map(vertex_colors)); mesh_.remove_property_map(vertex_colors); } From 244ca5ed446aa76c61f7b40cff0929ab7e949942 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Thu, 5 Jan 2023 19:52:56 +0100 Subject: [PATCH 15/20] Use surface for reflected text. --- src/libslic3r/CutSurface.cpp | 16 ++++++++++-- src/slic3r/GUI/Jobs/EmbossJob.cpp | 43 ++++++++++++++++--------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp index 4d56b84729..88e3bba751 100644 --- a/src/libslic3r/CutSurface.cpp +++ b/src/libslic3r/CutSurface.cpp @@ -3471,9 +3471,21 @@ ExPolygon priv::to_expoly(const SurfacePatch &patch, const Project &projection, // should not be used when no opposit triangle are counted so should not create overlaps ClipperLib::PolyFillType fill_type = ClipperLib::PolyFillType::pftEvenOdd; ExPolygons expolys = Slic3r::union_ex(polys, fill_type); - assert(expolys.size() == 1); + if (expolys.size() == 1) + return expolys.front(); + + // It should be one expolygon + assert(false); + if (expolys.empty()) return {}; - return expolys.front(); + // find biggest + const ExPolygon *biggest = &expolys.front(); + for (size_t index = 1; index < expolys.size(); ++index) { + const ExPolygon *current = &expolys[index]; + if (biggest->contour.size() < current->contour.size()) + biggest = current; + } + return *biggest; } SurfaceCut priv::patch2cut(SurfacePatch &patch) diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index ce98e972b1..86517d0ead 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -739,28 +739,16 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 Transform3d tr_inv = biggest->tr.inverse(); Transform3d cut_projection_tr = tr_inv * input2.text_tr; - // Cut surface in reflected system? - bool use_reflection = Slic3r::has_reflection(cut_projection_tr); - if (use_reflection) - cut_projection_tr *= Eigen::Scaling(-1., 1., 1.); size_t itss_index = s_to_itss[biggest - &sources.front()]; BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); for (const SurfaceVolumeData::ModelSource &s : sources) { size_t itss_index = s_to_itss[&s - &sources.front()]; if (itss_index == std::numeric_limits::max()) continue; - Transform3d tr; - if (&s == biggest) { - if (!use_reflection) - continue; - // add reflection for biggest source - tr = Eigen::Scaling(-1., 1., 1.); - } else { - tr = s.tr * tr_inv; - if (use_reflection) - tr *= Eigen::Scaling(-1., 1., 1.); - } + if (&s == biggest) + continue; + Transform3d tr = s.tr * tr_inv; bool fix_reflected = true; indexed_triangle_set &its = itss[itss_index]; its_transform(its, tr, fix_reflected); @@ -775,8 +763,27 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension); + bool is_text_reflected = Slic3r::has_reflection(input2.text_tr); + if (is_text_reflected) { + // revert order of points in expolygons + // CW --> CCW + for (ExPolygon &shape : shapes) { + shape.contour.reverse(); + for (Slic3r::Polygon &hole : shape.holes) + hole.reverse(); + } + } + // Use CGAL to cut surface from triangle mesh SurfaceCut cut = cut_surface(shapes, itss, cut_projection, projection_ratio); + + if (is_text_reflected) { + for (SurfaceCut::Contour &c : cut.contours) + std::reverse(c.begin(), c.end()); + for (Vec3i &t : cut.indices) + std::swap(t[0], t[1]); + } + if (cut.empty()) throw JobException(_u8L("There is no valid surface for text projection.").c_str()); if (was_canceled()) return {}; @@ -784,12 +791,6 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 OrthoProject3d projection = create_emboss_projection(input2.is_outside, fp.emboss, emboss_tr, cut); indexed_triangle_set new_its = cut2model(cut, projection); assert(!new_its.empty()); - if (use_reflection) { - // when cut was made in reflected system it must be converted back - Transform3d tr(Eigen::Scaling(-1., 1., 1.)); - its_transform(new_its, tr, true); - } - if (was_canceled()) return {}; return TriangleMesh(std::move(new_its)); } From 221770cc94a532d4dd337e0d919d164369226743 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Fri, 6 Jan 2023 12:00:58 +0100 Subject: [PATCH 16/20] Remove convexHull calculation for circular beds when arranging Extremely slow --- src/libslic3r/Arrange.cpp | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index a6aecd205c..e200b7edba 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -425,24 +425,11 @@ template<> std::function AutoArranger::get_objfn() { auto bincenter = m_bin.center(); return [this, bincenter](const Item &item) { - + auto result = objfunc(item, bincenter); - + double score = std::get<0>(result); - - auto isBig = [this](const Item& itm) { - return itm.area() / m_bin_area > BIG_ITEM_TRESHOLD ; - }; - - if(isBig(item)) { - auto mp = m_merged_pile; - mp.push_back(item.transformedShape()); - auto chull = sl::convexHull(mp); - double miss = Placer::overfit(chull, m_bin); - if(miss < 0) miss = 0; - score += miss*miss; - } - + return score; }; } From fcacc7042c9605e79ae2ff556c904ce46e15a498 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 6 Jan 2023 13:27:59 +0100 Subject: [PATCH 17/20] Class Selection - Added method to calculate bounding box aligned to current selected reference system --- src/slic3r/GUI/Selection.cpp | 263 ++++++++++++++++++++++++++--------- src/slic3r/GUI/Selection.hpp | 16 ++- 2 files changed, 208 insertions(+), 71 deletions(-) diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 2e92c8b6b0..bd48e14c12 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -801,6 +801,131 @@ const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() } return *m_full_unscaled_instance_local_bounding_box; } + +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +const std::pair& Selection::get_bounding_box_in_current_reference_system() const +{ + static int last_coordinates_type = -1; + + assert(!is_empty()); + + ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (m_mode == Instance && coordinates_type == ECoordinatesType::Local) + coordinates_type = ECoordinatesType::World; + + if (last_coordinates_type != int(coordinates_type)) + const_cast>*>(&m_bounding_box_in_current_reference_system)->reset(); + + if (!m_bounding_box_in_current_reference_system.has_value()) { + last_coordinates_type = int(coordinates_type); + + BoundingBoxf3 original_box; + Transform3d trafo; + + // + // calculate box aligned to current reference system + // + switch (coordinates_type) + { + case ECoordinatesType::World: + { + original_box = get_bounding_box(); + trafo = Transform3d::Identity(); + break; + } + case ECoordinatesType::Instance: { + for (unsigned int id : m_list) { + const GLVolume& v = *get_volume(id); + original_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + trafo = get_first_volume()->get_instance_transformation().get_matrix(); + break; + } + case ECoordinatesType::Local: { + assert(is_single_volume_or_modifier()); + const GLVolume& v = *get_first_volume(); + original_box = v.bounding_box(); + trafo = v.world_matrix(); + break; + } + } + + // + // calculate box size in world coordinates + // + auto point_to_Vec4d = [](const Vec3d& p) { return Vec4d(p.x(), p.y(), p.z(), 1.0); }; + auto Vec4d_to_Vec3d = [](const Vec4d& v) { return Vec3d(v.x(), v.y(), v.z()); }; + + auto apply_transform = [](const std::vector& original, const Transform3d& trafo, bool normalize) { + std::vector transformed(original.size()); + for (size_t i = 0; i < original.size(); ++i) { + transformed[i] = trafo * original[i]; + if (normalize) + transformed[i].normalize(); + } + return transformed; + }; + + auto calc_box_size = [point_to_Vec4d, Vec4d_to_Vec3d, apply_transform](const BoundingBoxf3& box, const Transform3d& trafo) { + Geometry::Transformation transformation(trafo); + + // box aligned to current reference system + std::vector homo_vertices = { + point_to_Vec4d({ box.min.x(), box.min.y(), box.min.z() }), + point_to_Vec4d({ box.max.x(), box.min.y(), box.min.z() }), + point_to_Vec4d({ box.max.x(), box.max.y(), box.min.z() }), + point_to_Vec4d({ box.min.x(), box.max.y(), box.min.z() }), + point_to_Vec4d({ box.min.x(), box.min.y(), box.max.z() }), + point_to_Vec4d({ box.max.x(), box.min.y(), box.max.z() }), + point_to_Vec4d({ box.max.x(), box.max.y(), box.max.z() }), + point_to_Vec4d({ box.min.x(), box.max.y(), box.max.z() }) + }; + + // box vertices in world coordinates + std::vector transformed_homo_vertices = apply_transform(homo_vertices, trafo, false); + + // project back to current reference system + const std::vector homo_axes = { Vec4d::UnitX(), Vec4d::UnitY(), Vec4d::UnitZ() }; + std::vector transformed_homo_axes = apply_transform(homo_axes, Geometry::Transformation(trafo).get_matrix_no_scaling_factor(), true); + std::vector transformed_axes(transformed_homo_axes.size()); + for (size_t i = 0; i < transformed_homo_axes.size(); ++i) { + transformed_axes[i] = Vec4d_to_Vec3d(transformed_homo_axes[i]); + } + + Vec3d min = { DBL_MAX, DBL_MAX, DBL_MAX }; + Vec3d max = { -DBL_MAX, -DBL_MAX, -DBL_MAX }; + + for (const Vec4d& v_homo : transformed_homo_vertices) { + const Vec3d v = Vec4d_to_Vec3d(v_homo); + for (int i = 0; i < 3; ++i) { + const double dot_i = v.dot(transformed_axes[i]); + min(i) = std::min(min(i), dot_i); + max(i) = std::max(max(i), dot_i); + } + } + + // return size + const Vec3d size = max - min; + return size; + }; + + const Vec3d box_size = calc_box_size(original_box, trafo); + const std::vector box_center = { point_to_Vec4d(original_box.center()) }; + std::vector transformed_box_center = apply_transform(box_center, trafo, false); + + // + // return box centered at 0, 0, 0 + // + const Vec3d half_box_size = 0.5 * box_size; + BoundingBoxf3 out_box(-half_box_size, half_box_size); + Geometry::Transformation out_trafo(trafo); + out_trafo.set_offset(Vec4d_to_Vec3d(transformed_box_center[0])); + *const_cast>*>(&m_bounding_box_in_current_reference_system) = { out_box, out_trafo.get_matrix_no_scaling_factor() }; + } + + return *m_bounding_box_in_current_reference_system; +} +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE void Selection::setup_cache() @@ -2669,88 +2794,88 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GL #if ENABLE_WORLD_COORDINATE_DEBUG void Selection::render_debug_window() const { - if (m_list.empty()) - return; + if (m_list.empty()) + return; - if (get_first_volume()->is_wipe_tower) - return; + if (get_first_volume()->is_wipe_tower) + return; - ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.begin(std::string("Selection matrices"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize); + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Selection matrices"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize); - auto volume_name = [this](size_t id) { - const GLVolume& v = *(*m_volumes)[id]; - return m_model->objects[v.object_idx()]->volumes[v.volume_idx()]->name; - }; + auto volume_name = [this](size_t id) { + const GLVolume& v = *(*m_volumes)[id]; + return m_model->objects[v.object_idx()]->volumes[v.volume_idx()]->name; + }; - static size_t current_cmb_idx = 0; - static size_t current_vol_idx = *m_list.begin(); + static size_t current_cmb_idx = 0; + static size_t current_vol_idx = *m_list.begin(); - if (m_list.find(current_vol_idx) == m_list.end()) - current_vol_idx = *m_list.begin(); + if (m_list.find(current_vol_idx) == m_list.end()) + current_vol_idx = *m_list.begin(); - if (ImGui::BeginCombo("Volumes", volume_name(current_vol_idx).c_str())) { - size_t count = 0; - for (unsigned int id : m_list) { - const GLVolume& v = *(*m_volumes)[id]; - const bool is_selected = (current_cmb_idx == count); - if (ImGui::Selectable(volume_name(id).c_str(), is_selected)) { - current_cmb_idx = count; - current_vol_idx = id; - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - ++count; - } - ImGui::EndCombo(); - } - - static int current_method_idx = 0; - ImGui::Combo("Decomposition method", ¤t_method_idx, "computeRotationScaling\0computeScalingRotation\0"); - - const GLVolume& v = *get_volume(current_vol_idx); - - auto add_matrix = [&imgui](const std::string& name, const Transform3d& m, unsigned int size) { - ImGui::BeginGroup(); - imgui.text(name); - if (ImGui::BeginTable(name.c_str(), size, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInner)) { - for (unsigned int r = 0; r < size; ++r) { - ImGui::TableNextRow(); - for (unsigned int c = 0; c < size; ++c) { - ImGui::TableSetColumnIndex(c); - imgui.text(std::to_string(m(r, c))); + if (ImGui::BeginCombo("Volumes", volume_name(current_vol_idx).c_str())) { + size_t count = 0; + for (unsigned int id : m_list) { + const GLVolume& v = *(*m_volumes)[id]; + const bool is_selected = (current_cmb_idx == count); + if (ImGui::Selectable(volume_name(id).c_str(), is_selected)) { + current_cmb_idx = count; + current_vol_idx = id; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + ++count; } - } - ImGui::EndTable(); + ImGui::EndCombo(); } - ImGui::EndGroup(); - }; - auto add_matrices_set = [add_matrix](const std::string& name, const Transform3d& m, size_t method) { - static unsigned int counter = 0; - ++counter; - if (ImGui::CollapsingHeader(name.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { - add_matrix("Full", m, 4); + static int current_method_idx = 0; + ImGui::Combo("Decomposition method", ¤t_method_idx, "computeRotationScaling\0computeScalingRotation\0"); - Matrix3d rotation; - Matrix3d scale; - if (method == 0) - m.computeRotationScaling(&rotation, &scale); - else - m.computeScalingRotation(&scale, &rotation); + const GLVolume& v = *get_volume(current_vol_idx); - ImGui::SameLine(); - add_matrix("Rotation component", Transform3d(rotation), 3); - ImGui::SameLine(); - add_matrix("Scale component", Transform3d(scale), 3); - } - }; + auto add_matrix = [&imgui](const std::string& name, const Transform3d& m, unsigned int size) { + ImGui::BeginGroup(); + imgui.text(name); + if (ImGui::BeginTable(name.c_str(), size, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInner)) { + for (unsigned int r = 0; r < size; ++r) { + ImGui::TableNextRow(); + for (unsigned int c = 0; c < size; ++c) { + ImGui::TableSetColumnIndex(c); + imgui.text(std::to_string(m(r, c))); + } + } + ImGui::EndTable(); + } + ImGui::EndGroup(); + }; - add_matrices_set("World", v.world_matrix(), current_method_idx); - add_matrices_set("Instance", v.get_instance_transformation().get_matrix(), current_method_idx); - add_matrices_set("Volume", v.get_volume_transformation().get_matrix(), current_method_idx); + auto add_matrices_set = [add_matrix](const std::string& name, const Transform3d& m, size_t method) { + static unsigned int counter = 0; + ++counter; + if (ImGui::CollapsingHeader(name.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + add_matrix("Full", m, 4); - imgui.end(); + Matrix3d rotation; + Matrix3d scale; + if (method == 0) + m.computeRotationScaling(&rotation, &scale); + else + m.computeScalingRotation(&scale, &rotation); + + ImGui::SameLine(); + add_matrix("Rotation component", Transform3d(rotation), 3); + ImGui::SameLine(); + add_matrix("Scale component", Transform3d(scale), 3); + } + }; + + add_matrices_set("World", v.world_matrix(), current_method_idx); + add_matrices_set("Instance", v.get_instance_transformation().get_matrix(), current_method_idx); + add_matrices_set("Volume", v.get_volume_transformation().get_matrix(), current_method_idx); + + imgui.end(); } #endif // ENABLE_WORLD_COORDINATE_DEBUG diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index d2c483c6e6..cb2c461ef1 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -242,6 +242,11 @@ private: // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. // Modifiers are taken in account std::optional m_full_unscaled_instance_local_bounding_box; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + // Bounding box aligned to the axis of the currently selected reference system (World/Object/Part) + // and transform to place and orient it in world coordinates + std::optional> m_bounding_box_in_current_reference_system; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE #if ENABLE_RENDER_SELECTION_CENTER @@ -385,10 +390,14 @@ public: // Bounding box of a single full instance selection, in world coordinates. // Modifiers are taken in account const BoundingBoxf3& get_full_scaled_instance_bounding_box() const; - // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. // Modifiers are taken in account const BoundingBoxf3& get_full_unscaled_instance_local_bounding_box() const; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + // Returns the bounding box aligned to the axis of the currently selected reference system (World/Object/Part) + // and the transform to place and orient it in world coordinates + const std::pair& get_bounding_box_in_current_reference_system() const; +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE void setup_cache(); @@ -464,7 +473,10 @@ private: m_bounding_box.reset(); m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); m_full_unscaled_instance_bounding_box.reset(); m_full_scaled_instance_bounding_box.reset(); - m_full_unscaled_instance_local_bounding_box.reset();; + m_full_unscaled_instance_local_bounding_box.reset(); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + m_bounding_box_in_current_reference_system.reset(); +//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ } #else void set_bounding_boxes_dirty() { m_bounding_box.reset(); m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); } From 49fdf013196a8fa53852779f3d9b1eaffb85e4ce Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 6 Jan 2023 13:41:43 +0100 Subject: [PATCH 18/20] Tech ENABLE_WORLD_COORDINATE - Fixed size of Move Gizmo in 3D scene --- src/slic3r/GUI/Gizmos/GLGizmoMove.cpp | 32 ++++----------------------- src/slic3r/GUI/Gizmos/GLGizmoMove.hpp | 1 - src/slic3r/GUI/Selection.cpp | 5 ++--- src/slic3r/GUI/Selection.hpp | 6 ----- 4 files changed, 6 insertions(+), 38 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 69fcc54147..7656226b59 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -149,7 +149,10 @@ void GLGizmoMove3D::on_render() glsafe(::glEnable(GL_DEPTH_TEST)); #if ENABLE_WORLD_COORDINATE - calc_selection_box_and_center(); + const Selection& selection = m_parent.get_selection(); + const auto& [box, box_trafo] = selection.get_bounding_box_in_current_reference_system(); + m_bounding_box = box; + m_center = box_trafo.translation(); const Transform3d base_matrix = local_transform(m_parent.get_selection()); for (int i = 0; i < 3; ++i) { m_grabbers[i].matrix = base_matrix; @@ -363,33 +366,6 @@ Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const } return ret; } - -void GLGizmoMove3D::calc_selection_box_and_center() -{ - const Selection& selection = m_parent.get_selection(); - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - m_bounding_box = selection.get_bounding_box(); - m_center = m_bounding_box.center(); - } - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_bounding_box = v.transformed_convex_hull_bounding_box( - v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); - m_center = v.world_matrix() * m_bounding_box.center(); - } - else { - m_bounding_box.reset(); - const Selection::IndicesList& ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *selection.get_volume(id); - m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); - m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); - m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); - } -} #endif // ENABLE_WORLD_COORDINATE } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index 5f1d562e9c..cd92d74721 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -67,7 +67,6 @@ private: double calc_projection(const UpdateData& data) const; #if ENABLE_WORLD_COORDINATE Transform3d local_transform(const Selection& selection) const; - void calc_selection_box_and_center(); #endif // ENABLE_WORLD_COORDINATE }; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index bd48e14c12..6d57541617 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -802,7 +802,6 @@ const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() return *m_full_unscaled_instance_local_bounding_box; } -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ const std::pair& Selection::get_bounding_box_in_current_reference_system() const { static int last_coordinates_type = -1; @@ -925,7 +924,6 @@ const std::pair& Selection::get_bounding_box_in_curr return *m_bounding_box_in_current_reference_system; } -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE void Selection::setup_cache() @@ -961,7 +959,8 @@ void Selection::translate(const Vec3d& displacement, TransformationType transfor else { if (transformation_type.local()) { const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); - v.set_volume_offset(vol_trafo.get_offset() + vol_trafo.get_rotation_matrix() * displacement); + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + v.set_volume_offset(vol_trafo.get_offset() + inst_trafo.get_scaling_factor_matrix().inverse() * vol_trafo.get_rotation_matrix() * displacement); } else { Vec3d relative_disp = displacement; diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index cb2c461ef1..67b28ac785 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -242,11 +242,9 @@ private: // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. // Modifiers are taken in account std::optional m_full_unscaled_instance_local_bounding_box; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // Bounding box aligned to the axis of the currently selected reference system (World/Object/Part) // and transform to place and orient it in world coordinates std::optional> m_bounding_box_in_current_reference_system; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE #if ENABLE_RENDER_SELECTION_CENTER @@ -393,11 +391,9 @@ public: // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. // Modifiers are taken in account const BoundingBoxf3& get_full_unscaled_instance_local_bounding_box() const; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // Returns the bounding box aligned to the axis of the currently selected reference system (World/Object/Part) // and the transform to place and orient it in world coordinates const std::pair& get_bounding_box_in_current_reference_system() const; -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ #endif // ENABLE_WORLD_COORDINATE void setup_cache(); @@ -474,9 +470,7 @@ private: m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); m_full_unscaled_instance_bounding_box.reset(); m_full_scaled_instance_bounding_box.reset(); m_full_unscaled_instance_local_bounding_box.reset(); -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ m_bounding_box_in_current_reference_system.reset(); -//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ } #else void set_bounding_boxes_dirty() { m_bounding_box.reset(); m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); } From 0f302a71064a7a181235d11e87cccadde8a7a0a2 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 6 Jan 2023 14:00:49 +0100 Subject: [PATCH 19/20] Tech ENABLE_WORLD_COORDINATE - Fixed rendering of selection bounding box --- src/slic3r/GUI/Selection.cpp | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 6d57541617..fb7fa51700 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1848,27 +1848,7 @@ void Selection::render(float scale_factor) m_scale_factor = scale_factor; // render cumulative bounding box of selected volumes #if ENABLE_WORLD_COORDINATE - BoundingBoxf3 box; - Transform3d trafo; - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - box = get_bounding_box(); - trafo = Transform3d::Identity(); - } - else if (coordinates_type == ECoordinatesType::Local && is_single_volume_or_modifier()) { - const GLVolume& v = *get_first_volume(); - box = v.bounding_box(); - trafo = v.world_matrix(); - } - else { - const Selection::IndicesList& ids = get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *get_volume(id); - box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - trafo = get_first_volume()->get_instance_transformation().get_matrix(); - } - + const auto [box, trafo] = get_bounding_box_in_current_reference_system(); render_bounding_box(box, trafo, ColorRGB::WHITE()); #else render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); From 74b391218d617d874ab65923b4b61414d8b3afa8 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 6 Jan 2023 14:35:54 +0100 Subject: [PATCH 20/20] Tech ENABLE_WORLD_COORDINATE - Fixed size of selection shown in sidebar panel --- src/slic3r/GUI/GUI_ObjectManipulation.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 0ab4f166b0..94ab206962 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -714,13 +714,14 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_position = volume->get_instance_offset(); m_new_scale_label_string = L("Scale"); m_new_scale = Vec3d(100.0, 100.0, 100.0); + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); #else if (m_world_coordinates) { m_new_scale = m_new_size.cwiseQuotient(selection.get_unscaled_instance_bounding_box().size()) * 100.0; + m_new_size = selection.get_scaled_instance_bounding_box().size(); #endif // ENABLE_WORLD_COORDINATE m_new_rotate_label_string = L("Rotate"); m_new_rotation = Vec3d::Zero(); - m_new_size = selection.get_scaled_instance_bounding_box().size(); } else { #if ENABLE_WORLD_COORDINATE @@ -729,11 +730,12 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_position = Vec3d::Zero(); m_new_rotation = Vec3d::Zero(); m_new_scale = Vec3d(100.0, 100.0, 100.0); + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); #else m_new_rotation = volume->get_instance_rotation() * (180.0 / M_PI); + m_new_size = volume->get_instance_scaling_factor().cwiseProduct(wxGetApp().model().objects[volume->object_idx()]->raw_mesh_bounding_box().size()); #endif // ENABLE_WORLD_COORDINATE m_new_scale = volume->get_instance_scaling_factor() * 100.0; - m_new_size = volume->get_instance_scaling_factor().cwiseProduct(wxGetApp().model().objects[volume->object_idx()]->raw_mesh_bounding_box().size()); } m_new_enabled = true; @@ -743,7 +745,11 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_position = box.center(); m_new_rotation = Vec3d::Zero(); m_new_scale = Vec3d(100.0, 100.0, 100.0); - m_new_size = box.size(); +#if ENABLE_WORLD_COORDINATE + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); +#else + m_new_size = box.size(); +#endif // ENABLE_WORLD_COORDINATE m_new_rotate_label_string = L("Rotate"); m_new_scale_label_string = L("Scale"); m_new_enabled = true; @@ -766,7 +772,7 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_scale_label_string = L("Scale"); m_new_scale = Vec3d(100.0, 100.0, 100.0); m_new_rotation = Vec3d::Zero(); - m_new_size = volume->transformed_convex_hull_bounding_box(trafo.get_matrix()).size(); + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); } else if (is_local_coordinates()) { m_new_move_label_string = L("Translate"); @@ -774,7 +780,7 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_position = Vec3d::Zero(); m_new_rotation = Vec3d::Zero(); m_new_scale = volume->get_volume_scaling_factor() * 100.0; - m_new_size = volume->get_volume_scaling_factor().cwiseProduct(volume->bounding_box().size()); + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); } else { #endif // ENABLE_WORLD_COORDINATE @@ -783,8 +789,8 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_rotation = Vec3d::Zero(); #if ENABLE_WORLD_COORDINATE m_new_scale_label_string = L("Scale"); - m_new_size = volume->transformed_convex_hull_bounding_box(volume->get_volume_transformation().get_matrix()).size(); m_new_scale = Vec3d(100.0, 100.0, 100.0); + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); } #else m_new_scale = volume->get_volume_scaling_factor() * 100.0; @@ -797,7 +803,11 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_move_label_string = L("Translate"); m_new_rotate_label_string = L("Rotate"); m_new_scale_label_string = L("Scale"); +#if ENABLE_WORLD_COORDINATE + m_new_size = selection.get_bounding_box_in_current_reference_system().first.size(); +#else m_new_size = selection.get_bounding_box().size(); +#endif // ENABLE_WORLD_COORDINATE m_new_enabled = true; } else {