From 11a6b694d8d6b664fe363f589d34304d22737845 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Tue, 19 May 2026 21:49:46 +0100 Subject: [PATCH] Copy/paste works more intuitively in macos color dialog. Make sure crash backup updates when color data is changed --- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 86 +++++++++++++ .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 7 ++ src/slic3r/GUI/MainFrame.cpp | 5 +- src/slic3r/Utils/MacDarkMode.hpp | 2 + src/slic3r/Utils/MacDarkMode.mm | 114 +++++++++++++++--- 5 files changed, 198 insertions(+), 16 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index df6db774bca..a2e388f8149 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -14,6 +14,7 @@ #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Tab.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/TextureMapping.hpp" #include "libslic3r/ColorSolver.hpp" @@ -70,6 +71,47 @@ constexpr float SlopeAutoPaintMaxAngleDeg = 180.f; constexpr float ProjectionTextRasterBaseFontSize = 200.f; constexpr int ProjectionTextRasterMaxDimension = 2048; constexpr int ProjectionTextRasterMinFontSize = 18; + +static bool queue_color_data_crash_backup(ModelObject *object) +{ + if (object == nullptr || + object->get_model() == nullptr || + !object->get_model()->is_need_backup() || + object->volumes.empty() || + object->instances.empty()) + return false; + + Slic3r::save_object_mesh(*object); + return true; +} + +static ModelObject *find_model_object_by_id(const ObjectID &object_id) +{ + for (ModelObject *object : wxGetApp().model().objects) + if (object != nullptr && object->id() == object_id) + return object; + return nullptr; +} + +static void remember_color_data_crash_backup_object(std::vector &object_ids, const ModelObject *object) +{ + if (object == nullptr) + return; + + const ObjectID object_id = object->id(); + if (std::find(object_ids.begin(), object_ids.end(), object_id) == object_ids.end()) + object_ids.emplace_back(object_id); +} + +static void queue_color_data_crash_backups(std::vector &object_ids) +{ + bool queued = false; + for (const ObjectID &object_id : object_ids) + queued |= queue_color_data_crash_backup(find_model_object_by_id(object_id)); + object_ids.clear(); + if (queued) + Slic3r::backup_soon(); +} } struct ManagedRegionColorSource @@ -8226,6 +8268,8 @@ public: m_background_clear->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { clear_background_color(); }); } + bool changed() const { return m_changed; } + private: struct Row { @@ -8578,6 +8622,7 @@ private: void refresh_object_after_change() { + m_changed = true; refresh_managed_color_data_object(m_canvas, m_object); notify_object_changed(); m_canvas.set_as_dirty(); @@ -8620,6 +8665,7 @@ private: std::function m_on_object_changed; wxPanel *m_background_picker = nullptr; wxButton *m_background_clear = nullptr; + bool m_changed = false; std::vector m_rows; std::vector m_raw_image_texture_info_windows; wxStaticText *m_raw_image_texture_status = nullptr; @@ -8634,6 +8680,8 @@ void open_color_data_management_dialog(wxWindow *parent, GLCanvas3D &canvas, Mod ColorDataManagementDialog dialog(parent, canvas, object, std::move(on_object_changed)); dialog.ShowModal(); + if (dialog.changed() && queue_color_data_crash_backup(object)) + Slic3r::backup_soon(); } void GLGizmoMmuSegmentation::init_extruders_data(const std::vector &extruder_colors) @@ -10807,6 +10855,8 @@ void GLGizmoMmuSegmentation::convert_selected_object_vertex_colors_to_texture_ma wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); } m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + if (queue_color_data_crash_backup(object)) + Slic3r::backup_soon(); } void GLGizmoMmuSegmentation::convert_selected_object_image_texture_to_texture_mapping_colors() @@ -10878,6 +10928,8 @@ void GLGizmoMmuSegmentation::convert_selected_object_image_texture_to_texture_ma wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); } m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + if (queue_color_data_crash_backup(object)) + Slic3r::backup_soon(); } void GLGizmoMmuSegmentation::convert_selected_regions_to_vertex_colors() @@ -10958,6 +11010,8 @@ void GLGizmoMmuSegmentation::finish_selected_regions_color_data_conversion(Model wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); } m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + if (queue_color_data_crash_backup(&object)) + Slic3r::backup_soon(); m_parent.render(); } @@ -11119,6 +11173,14 @@ void GLGizmoTrueColorPainting::on_shutdown() m_parent.toggle_model_objects_visibility(true); } +void GLGizmoTrueColorPainting::on_set_state() +{ + GLGizmoPainterBase::on_set_state(); + + if (get_state() == Off) + backup_changed_rgb_data_objects(); +} + PainterGizmoType GLGizmoTrueColorPainting::get_painter_type() const { return PainterGizmoType::TRUE_COLOR; @@ -11845,6 +11907,7 @@ void GLGizmoTrueColorPainting::convert_selected_object_image_texture_to_rgb_data void GLGizmoTrueColorPainting::refresh_selected_object_after_rgb_change(ModelObject *object) { + remember_changed_rgb_data_object(object); update_selected_object_color_state(); init_model_triangle_selectors(); m_parent.update_volumes_colors_by_extruder(); @@ -11859,6 +11922,16 @@ void GLGizmoTrueColorPainting::refresh_selected_object_after_rgb_change(ModelObj m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } +void GLGizmoTrueColorPainting::remember_changed_rgb_data_object(ModelObject *object) +{ + remember_color_data_crash_backup_object(m_changed_rgb_data_object_ids, object); +} + +void GLGizmoTrueColorPainting::backup_changed_rgb_data_objects() +{ + queue_color_data_crash_backups(m_changed_rgb_data_object_ids); +} + bool GLGizmoTrueColorPainting::pick_color_from_model(const Vec2d &mouse_position) { ColorRGBA color; @@ -12639,6 +12712,7 @@ void GLGizmoTrueColorPainting::refresh_selected_object_after_background_color_ch if (object == nullptr) return; + remember_changed_rgb_data_object(object); cancel_rgb_data_preview_conversion(); m_preview_rgb_data_volume_ids.clear(); m_preview_rgb_data_by_volume.clear(); @@ -12952,6 +13026,7 @@ void GLGizmoImageProjection::on_set_state() } else if (get_state() == Off) { m_parent.enable_picking(true); m_parent.toggle_model_objects_visibility(true); + backup_projected_objects(); } } @@ -13762,6 +13837,7 @@ bool GLGizmoImageProjection::project_image_to_selected_object() } refresh_projected_object(object); + remember_projected_object(object); m_projection_mode_initialized = true; m_projection_mode_object_id = object->id(); if (converting_raw_to_rgba_image && !selected_object_has_raw_atlas_texture_data()) @@ -14442,6 +14518,16 @@ void GLGizmoImageProjection::refresh_projected_object(ModelObject *object) m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } +void GLGizmoImageProjection::remember_projected_object(ModelObject *object) +{ + remember_color_data_crash_backup_object(m_projected_object_ids, object); +} + +void GLGizmoImageProjection::backup_projected_objects() +{ + queue_color_data_crash_backups(m_projected_object_ids); +} + void GLMmSegmentationGizmo3DScene::release_geometry() { if (this->vertices_VBO_id) { glsafe(::glDeleteBuffers(1, &this->vertices_VBO_id)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 31d09c69d8d..ef36a498fe8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -309,6 +309,7 @@ private: void update_from_model_object(bool first_update = false) override; void on_opening() override; void on_shutdown() override; + void on_set_state() override; PainterGizmoType get_painter_type() const override; void init_model_triangle_selectors(); @@ -322,6 +323,8 @@ private: void convert_selected_object_vertex_colors_to_rgb_data(); void convert_selected_object_image_texture_to_rgb_data(); void refresh_selected_object_after_rgb_change(ModelObject *object); + void remember_changed_rgb_data_object(ModelObject *object); + void backup_changed_rgb_data_objects(); void update_triangle_selectors_color(); void update_rgb_data_preview_conversion(); void start_rgb_data_preview_conversion(ModelObject &object); @@ -391,6 +394,7 @@ private: bool m_color_picker_active = false; bool m_brush_stroke_active = false; ObjectID m_selected_color_state_object_id; + std::vector m_changed_rgb_data_object_ids; std::vector> m_brush_stroke_points_by_volume; std::vector m_preview_rgb_data_volume_ids; std::vector> m_preview_rgb_data_by_volume; @@ -477,6 +481,8 @@ private: bool project_to_image_texture(ModelObject *object); bool project_to_rgb_data(ModelObject *object); void refresh_projected_object(ModelObject *object); + void remember_projected_object(ModelObject *object); + void backup_projected_objects(); ProjectionMode m_projection_mode = ProjectionMode::RGBData; bool m_projection_mode_initialized = false; @@ -512,6 +518,7 @@ private: bool m_apply_transparency_as_background = false; bool m_pass_through_model = false; bool m_convert_existing_colors_to_raw_offsets = true; + std::vector m_projected_object_ids; }; } // namespace Slic3r diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c7c756875d2..2c8fd7f2f85 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2888,6 +2888,9 @@ void MainFrame::init_menubar_as_editor() // BBS Copy append_menu_item(editMenu, wxID_ANY, _L("Copy") + sep + ctrl_t + "C", _L("Copy selection to clipboard"), [this, handle_key_event](wxCommandEvent&) { + if (mac_copy_to_text_control()) { + return; + } wxKeyEvent e; e.SetEventType(wxEVT_KEY_DOWN); e.SetControlDown(true); @@ -2896,7 +2899,7 @@ void MainFrame::init_menubar_as_editor() return; } m_plater->copy_selection_to_clipboard(); }, - "", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this); + "", nullptr, [this](){return mac_can_copy_to_text_control() || m_plater->can_copy_to_clipboard(); }, this); // BBS Paste append_menu_item(editMenu, wxID_ANY, _L("Paste") + sep + ctrl_t + "V", _L("Paste clipboard"), [this, handle_key_event](wxCommandEvent&) { diff --git a/src/slic3r/Utils/MacDarkMode.hpp b/src/slic3r/Utils/MacDarkMode.hpp index b8a6321ce12..8401fec78b4 100644 --- a/src/slic3r/Utils/MacDarkMode.hpp +++ b/src/slic3r/Utils/MacDarkMode.hpp @@ -9,7 +9,9 @@ namespace GUI { #if __APPLE__ extern bool mac_dark_mode(); extern double mac_max_scaling_factor(); +extern bool mac_can_copy_to_text_control(); extern bool mac_can_paste_to_text_control(); +extern bool mac_copy_to_text_control(); extern bool mac_paste_to_text_control(); extern void mac_install_text_paste_shortcut(); extern void set_miniaturizable(void * window); diff --git a/src/slic3r/Utils/MacDarkMode.mm b/src/slic3r/Utils/MacDarkMode.mm index 101b11d35a0..3750140f263 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -42,36 +42,95 @@ double mac_max_scaling_factor() return scaling; } -static id mac_text_paste_target() +static id mac_text_target_for_action(SEL action) { - id target = [NSApp targetForAction:@selector(paste:) to:nil from:nil]; + id target = [NSApp targetForAction:action to:nil from:nil]; if (target != nil && [target isKindOfClass:[NSText class]]) return target; - return nil; + + id responder = [[NSApp keyWindow] firstResponder]; + return [responder isKindOfClass:[NSText class]] ? responder : nil; +} + +static id mac_text_paste_target() +{ + return mac_text_target_for_action(@selector(paste:)); +} + +static id mac_text_copy_target() +{ + return mac_text_target_for_action(@selector(copy:)); +} + +static bool mac_text_target_has_selection(id target) +{ + return [target respondsToSelector:@selector(selectedRange)] && [target selectedRange].length > 0; +} + +static bool mac_string_is_six_hex(NSString *string) +{ + if ([string length] != 6) + return false; + + NSCharacterSet *non_hex = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"] invertedSet]; + return [string rangeOfCharacterFromSet:non_hex].location == NSNotFound; +} + +static bool mac_string_is_hex_field_value(NSString *string) +{ + NSUInteger length = [string length]; + if (length < 6) + return false; + + NSCharacterSet *non_hex = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"] invertedSet]; + return [string rangeOfCharacterFromSet:non_hex].location == NSNotFound; +} + +static bool mac_key_window_is_color_panel() +{ + return [[NSApp keyWindow] isKindOfClass:[NSColorPanel class]]; } static NSString *mac_color_panel_hex_string() { - NSWindow *window = [NSApp keyWindow]; - if (![window isKindOfClass:[NSColorPanel class]]) + if (!mac_key_window_is_color_panel()) return nil; NSString *string = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - if (![string hasPrefix:@"#"]) - return nil; - NSString *value = [string substringFromIndex:1]; - if ([value length] != 6) - return nil; - - NSCharacterSet *non_hex = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"] invertedSet]; - if ([value rangeOfCharacterFromSet:non_hex].location != NSNotFound) + NSString *value = [string hasPrefix:@"#"] ? [string substringFromIndex:1] : string; + if (!mac_string_is_six_hex(value)) return nil; return value; } +static NSString *mac_color_panel_hex_target_value(id target) +{ + if (!mac_key_window_is_color_panel() || ![target respondsToSelector:@selector(string)]) + return nil; + + NSString *string = [target string]; + return mac_string_is_hex_field_value(string) ? string : nil; +} + +static void mac_select_hex_text_target(id target) +{ + if (![target respondsToSelector:@selector(string)] || ![target respondsToSelector:@selector(setSelectedRange:)]) + return; + + NSString *string = mac_color_panel_hex_target_value(target); + if (string != nil) + [target setSelectedRange:NSMakeRange(0, [string length])]; +} + +bool mac_can_copy_to_text_control() +{ + id target = mac_text_copy_target(); + return target != nil && (mac_text_target_has_selection(target) || mac_color_panel_hex_target_value(target) != nil); +} + bool mac_can_paste_to_text_control() { NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; @@ -80,6 +139,25 @@ bool mac_can_paste_to_text_control() return mac_text_paste_target() != nil; } +bool mac_copy_to_text_control() +{ + id target = mac_text_copy_target(); + if (target == nil) + return false; + + if (!mac_text_target_has_selection(target)) { + NSString *color_panel_hex = mac_color_panel_hex_target_value(target); + if (color_panel_hex == nil) + return false; + + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + return [pasteboard setString:color_panel_hex forType:NSPasteboardTypeString] == YES; + } + + return [NSApp sendAction:@selector(copy:) to:nil from:nil] == YES; +} + bool mac_paste_to_text_control() { id target = mac_text_paste_target(); @@ -88,6 +166,7 @@ bool mac_paste_to_text_control() NSString *color_panel_hex = mac_color_panel_hex_string(); if (color_panel_hex != nil && [target respondsToSelector:@selector(insertText:)]) { + mac_select_hex_text_target(target); [target insertText:color_panel_hex]; return true; } @@ -106,8 +185,13 @@ void mac_install_text_paste_shortcut() if ((flags & NSEventModifierFlagCommand) != 0 && (flags & (NSEventModifierFlagShift | NSEventModifierFlagControl | NSEventModifierFlagOption)) == 0) { NSString *characters = [event charactersIgnoringModifiers]; - if ([characters length] == 1 && [[characters lowercaseString] isEqualToString:@"v"] && mac_paste_to_text_control()) - return nil; + if ([characters length] == 1) { + NSString *key = [characters lowercaseString]; + if ([key isEqualToString:@"c"] && mac_copy_to_text_control()) + return nil; + if ([key isEqualToString:@"v"] && mac_paste_to_text_control()) + return nil; + } } return event; }];