import bpy from bpy.types import Operator from bpy.props import StringProperty, BoolProperty, EnumProperty import os import json import time # Import utility functions from main module from ..presets import ( get_preset_path, get_persistent_preset_path, get_addon_version, get_blend_file_presets, save_blend_file_preset, delete_blend_file_preset, refresh_presets_sync, ensure_presets_available ) from ..utils import bl_info class TEXT_TEXTURE_OT_save_preset(Operator): """Save current settings as preset""" bl_idname = "text_texture.save_preset" bl_label = "Save Preset" bl_description = "Save current settings as preset" bl_options = {'REGISTER', 'UNDO'} overwrite: bpy.props.BoolProperty( name="Overwrite", description="Overwrite existing preset", default=False ) preset_name: bpy.props.StringProperty( name="Preset Name", description="Name of preset to save/overwrite", default="" ) def execute(self, context): props = context.scene.text_texture_props # Use provided preset name or fallback to input field preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip() if not preset_name: self.report({'ERROR'}, "Please enter a preset name") return {'CANCELLED'} # When overwriting from button, update the input field for consistency if self.preset_name and self.preset_name.strip(): props.new_preset_name = preset_name # Validate preset name characters (avoid filesystem issues) import re # Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters # This pattern supports international characters like ö, ü, ñ, etc. if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE): self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility") return {'CANCELLED'} # Additional check for filesystem-unsafe characters invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\'] if any(char in preset_name for char in invalid_chars): self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}") return {'CANCELLED'} # Check for duplicate names preset_file = os.path.join(get_preset_path(), f"{preset_name}.json") if os.path.exists(preset_file) and not self.overwrite: # Suggest alternative names base_name = preset_name counter = 1 suggested_name = f"{base_name}_{counter}" while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")): counter += 1 suggested_name = f"{base_name}_{counter}" self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.") return {'CANCELLED'} # DETAILED LOGGING: Current shader properties before saving print(f"[PRESET SAVE DEBUG] ==================== SAVING PRESET: '{preset_name}' ====================") print(f"[PRESET SAVE DEBUG] Current shader properties from props:") print(f"[PRESET SAVE DEBUG] disable_shadows: {props.disable_shadows}") print(f"[PRESET SAVE DEBUG] disable_reflections: {props.disable_reflections}") print(f"[PRESET SAVE DEBUG] disable_backfacing: {props.disable_backfacing}") print(f"[PRESET SAVE DEBUG] shader_type: {props.shader_type}") print(f"[PRESET SAVE DEBUG] shader_connection: {props.shader_connection}") print(f"[PRESET SAVE DEBUG] use_alpha: {props.use_alpha}") print(f"[PRESET SAVE DEBUG] emission_strength: {props.emission_strength}") print(f"[PRESET SAVE DEBUG] auto_assign_material: {props.auto_assign_material}") preset_data = { 'text': props.text, 'texture_width': props.texture_width, 'texture_height': props.texture_height, 'font_size': props.font_size, 'padding': props.padding, 'text_color': list(props.text_color), 'background_transparent': props.background_transparent, 'background_color': list(props.background_color), 'font_path': props.font_path, 'custom_font_path': props.custom_font_path, 'use_custom_font': props.use_custom_font, 'text_align': props.text_align, 'prepend_text': props.prepend_text, 'append_text': props.append_text, 'prepend_margin': props.prepend_margin, 'append_margin': props.append_margin, 'prepend_append_layout': props.prepend_append_layout, 'shader_type': props.shader_type, 'shader_connection': props.shader_connection, 'use_alpha': props.use_alpha, 'emission_strength': props.emission_strength, 'auto_assign_material': props.auto_assign_material, 'disable_shadows': props.disable_shadows, 'disable_reflections': props.disable_reflections, 'disable_backfacing': props.disable_backfacing, 'generate_normal_map': props.generate_normal_map, 'normal_map_strength': props.normal_map_strength, 'normal_map_blur_radius': props.normal_map_blur_radius, 'normal_map_invert': props.normal_map_invert, 'enable_multiline': props.enable_multiline, 'line_height': props.line_height, 'text_alignment': props.text_alignment, 'auto_wrap': props.auto_wrap, 'wrap_width': props.wrap_width, 'max_lines': props.max_lines, 'line_spacing_pixels': props.line_spacing_pixels, 'enable_stroke': props.enable_stroke, 'stroke_width': props.stroke_width, 'stroke_position': props.stroke_position, 'stroke_color': list(props.stroke_color), 'enable_shadow': props.enable_shadow, 'shadow_offset_x': props.shadow_offset_x, 'shadow_offset_y': props.shadow_offset_y, 'shadow_blur': props.shadow_blur, 'shadow_color': list(props.shadow_color), 'shadow_spread': getattr(props, 'shadow_spread', 1.0), 'enable_glow': props.enable_glow, 'glow_blur': props.glow_blur, 'glow_color': list(props.glow_color), 'enable_blur': props.enable_blur, 'blur_radius': props.blur_radius, 'image_overlays': [] } # DETAILED LOGGING: Verify shader properties in preset_data print(f"[PRESET SAVE DEBUG] Shader properties in preset_data dictionary:") print(f"[PRESET SAVE DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") print(f"[PRESET SAVE DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") print(f"[PRESET SAVE DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") # Save overlay data for overlay in props.image_overlays: overlay_data = { 'name': overlay.name, 'image_path': overlay.image_path, 'x_position': overlay.x_position, 'y_position': overlay.y_position, 'scale': overlay.scale, 'rotation': overlay.rotation, 'z_index': overlay.z_index, 'enabled': overlay.enabled } preset_data['image_overlays'].append(overlay_data) try: # Primary: Save to blend file (new system) print(f"[PRESET SAVE DEBUG] Attempting to save to blend file...") blend_success = save_blend_file_preset(preset_name, preset_data) print(f"[PRESET SAVE DEBUG] Blend file save result: {blend_success}") # ALWAYS save to persistent storage for cross-blend-file sharing and addon update survival persistent_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json") print(f"[PRESET SAVE DEBUG] Attempting to save to persistent file: {persistent_file}") persistent_success = True try: with open(persistent_file, 'w') as f: json.dump(preset_data, f, indent=2) print(f"[PRESET SAVE DEBUG] Successfully saved preset '{preset_name}' to persistent storage") # VERIFY: Read back the persistent file to confirm shader properties were saved print(f"[PRESET SAVE DEBUG] Verifying persistent file contents...") with open(persistent_file, 'r') as f: saved_data = json.load(f) print(f"[PRESET SAVE DEBUG] Persistent file shader properties verification:") print(f"[PRESET SAVE DEBUG] disable_shadows: {saved_data.get('disable_shadows', 'NOT FOUND')}") print(f"[PRESET SAVE DEBUG] disable_reflections: {saved_data.get('disable_reflections', 'NOT FOUND')}") print(f"[PRESET SAVE DEBUG] disable_backfacing: {saved_data.get('disable_backfacing', 'NOT FOUND')}") except Exception as persistent_error: print(f"[PRESET SAVE DEBUG] Warning: Failed to save persistent preset file: {persistent_error}") persistent_success = False # Also save to legacy location for backward compatibility (optional) legacy_success = True try: legacy_file = os.path.join(get_preset_path(), f"{preset_name}.json") with open(legacy_file, 'w') as f: json.dump(preset_data, f, indent=2) print(f"[PRESET SAVE DEBUG] Also saved to legacy location for backward compatibility") except Exception as legacy_error: print(f"[PRESET SAVE DEBUG] Note: Could not save to legacy location: {legacy_error}") legacy_success = False if not blend_success and not persistent_success: self.report({'ERROR'}, f"Failed to save preset to both blend file and persistent storage") return {'CANCELLED'} # Check if preset already exists in the collection and update it existing_preset = None for preset in props.presets: if preset.name == preset_name: existing_preset = preset break if not existing_preset: preset = props.presets.add() preset.name = preset_name # Priority: Blend file > Persistent file preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE' else: # Update source based on where it was successfully saved existing_preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE' # Don't clear the input field - keep the name for easy re-saving # props.new_preset_name = "New Preset" # Provide clear feedback storage_info = "" if blend_success and persistent_success: storage_info = " (saved to blend file + persistent storage)" elif blend_success: storage_info = " (saved to blend file)" elif persistent_success: storage_info = " (saved to persistent storage)" if self.overwrite: self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully{storage_info}") else: self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully{storage_info}") except (OSError, IOError, json.JSONEncodeError) as e: self.report({'ERROR'}, f"Failed to save preset: {str(e)}") return {'CANCELLED'} except Exception as e: self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} def invoke(self, context, event): # Check if preset exists and prompt for overwrite props = context.scene.text_texture_props preset_name = props.new_preset_name.strip() if preset_name and preset_name != "New Preset": preset_file = os.path.join(get_preset_path(), f"{preset_name}.json") if os.path.exists(preset_file): return context.window_manager.invoke_confirm(self, event) return self.execute(context) def draw(self, context): layout = self.layout props = context.scene.text_texture_props preset_name = props.new_preset_name.strip() layout.label(text=f"Overwrite existing preset '{preset_name}'?") class TEXT_TEXTURE_OT_save_preset_enter(Operator): """Save preset when Enter key is pressed in name field""" bl_idname = "text_texture.save_preset_enter" bl_label = "Save Preset (Enter)" bl_description = "Save current settings as preset when Enter is pressed" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): # Delegate to the main save preset operator return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT') class TEXT_TEXTURE_OT_load_preset(Operator): """Load preset""" bl_idname = "text_texture.load_preset" bl_label = "Load Preset" bl_description = "Load selected preset" bl_options = {'REGISTER', 'UNDO'} preset_name: StringProperty() def execute(self, context): props = context.scene.text_texture_props # DEFENSIVE PROGRAMMING: Ensure presets are available before loading if not ensure_presets_available(): self.report({'ERROR'}, "Failed to ensure presets are loaded - cannot load preset") return {'CANCELLED'} # DETAILED LOGGING: Current shader properties before loading print(f"[PRESET LOAD DEBUG] ==================== LOADING PRESET: '{self.preset_name}' ====================") print(f"[PRESET LOAD DEBUG] Current shader properties BEFORE loading:") print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows}") print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections}") print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing}") print(f"[PRESET LOAD DEBUG] shader_type: {props.shader_type}") print(f"[PRESET LOAD DEBUG] shader_connection: {props.shader_connection}") try: # Three-tier loading priority: Blend File > Persistent File > Legacy File blend_presets = get_blend_file_presets() preset_data = None source = 'BLEND_FILE' print(f"[PRESET LOAD DEBUG] Available blend file presets: {list(blend_presets.keys())}") if self.preset_name in blend_presets: preset_data = blend_presets[self.preset_name] source = 'BLEND_FILE' print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from blend file") print(f"[PRESET LOAD DEBUG] Blend file preset shader properties:") print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") else: # Try persistent storage next persistent_file = os.path.join(get_persistent_preset_path(), f"{self.preset_name}.json") print(f"[PRESET LOAD DEBUG] Preset not in blend file, trying persistent file: {persistent_file}") if os.path.exists(persistent_file): with open(persistent_file, 'r') as f: preset_data = json.load(f) source = 'PERSISTENT_FILE' print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from persistent file") print(f"[PRESET LOAD DEBUG] Persistent file preset shader properties:") print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") else: # Fallback to legacy local file legacy_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") print(f"[PRESET LOAD DEBUG] Preset not in persistent storage, trying legacy file: {legacy_file}") if os.path.exists(legacy_file): with open(legacy_file, 'r') as f: preset_data = json.load(f) source = 'LOCAL_FILE' print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from legacy file") print(f"[PRESET LOAD DEBUG] Legacy file preset shader properties:") print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") else: print(f"[PRESET LOAD DEBUG] Legacy file does not exist: {legacy_file}") self.report({'ERROR'}, f"Preset not found in any storage location: {self.preset_name}") return {'CANCELLED'} if not preset_data: print(f"[PRESET LOAD DEBUG] No preset data found!") self.report({'ERROR'}, f"No data found for preset: {self.preset_name}") return {'CANCELLED'} props.is_updating = True # Skip loading text to prevent overwriting current text # props.text = preset_data.get('text', props.text) props.texture_width = preset_data.get('texture_width', props.texture_width) props.texture_height = preset_data.get('texture_height', props.texture_height) props.font_size = preset_data.get('font_size', props.font_size) props.padding = preset_data.get('padding', props.padding) props.text_color = preset_data.get('text_color', props.text_color) props.background_transparent = preset_data.get('background_transparent', props.background_transparent) props.background_color = preset_data.get('background_color', props.background_color) props.font_path = preset_data.get('font_path', props.font_path) props.custom_font_path = preset_data.get('custom_font_path', props.custom_font_path) props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font) props.text_align = preset_data.get('text_align', props.text_align) props.prepend_text = preset_data.get('prepend_text', props.prepend_text) props.append_text = preset_data.get('append_text', props.append_text) props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin) props.append_margin = preset_data.get('append_margin', props.append_margin) props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout) # DETAILED LOGGING: Shader property loading with before/after values print(f"[PRESET LOAD DEBUG] Loading shader properties...") old_shader_type = props.shader_type props.shader_type = preset_data.get('shader_type', props.shader_type) print(f"[PRESET LOAD DEBUG] shader_type: {old_shader_type} -> {props.shader_type}") old_shader_connection = props.shader_connection props.shader_connection = preset_data.get('shader_connection', props.shader_connection) print(f"[PRESET LOAD DEBUG] shader_connection: {old_shader_connection} -> {props.shader_connection}") old_use_alpha = props.use_alpha props.use_alpha = preset_data.get('use_alpha', props.use_alpha) print(f"[PRESET LOAD DEBUG] use_alpha: {old_use_alpha} -> {props.use_alpha}") old_emission_strength = props.emission_strength props.emission_strength = preset_data.get('emission_strength', props.emission_strength) print(f"[PRESET LOAD DEBUG] emission_strength: {old_emission_strength} -> {props.emission_strength}") old_auto_assign = props.auto_assign_material props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material) print(f"[PRESET LOAD DEBUG] auto_assign_material: {old_auto_assign} -> {props.auto_assign_material}") # CRITICAL SHADER PROPERTIES - Most detailed logging print(f"[PRESET LOAD DEBUG] ==================== CRITICAL SHADER PROPERTIES ====================") old_disable_shadows = props.disable_shadows new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows) props.disable_shadows = new_disable_shadows print(f"[PRESET LOAD DEBUG] disable_shadows:") print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_shadows} (type: {type(old_disable_shadows)})") print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_shadows', 'NOT FOUND')} (type: {type(preset_data.get('disable_shadows', 'N/A'))})") print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_shadows} (type: {type(props.disable_shadows)})") old_disable_reflections = props.disable_reflections new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections) props.disable_reflections = new_disable_reflections print(f"[PRESET LOAD DEBUG] disable_reflections:") print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_reflections} (type: {type(old_disable_reflections)})") print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_reflections', 'NOT FOUND')} (type: {type(preset_data.get('disable_reflections', 'N/A'))})") print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_reflections} (type: {type(props.disable_reflections)})") old_disable_backfacing = props.disable_backfacing new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing) props.disable_backfacing = new_disable_backfacing print(f"[PRESET LOAD DEBUG] disable_backfacing:") print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_backfacing} (type: {type(old_disable_backfacing)})") print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_backfacing', 'NOT FOUND')} (type: {type(preset_data.get('disable_backfacing', 'N/A'))})") print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") print(f"[PRESET LOAD DEBUG] ================================================================") props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map) props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength) props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius) props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert) # Load multiline settings props.enable_multiline = preset_data.get('enable_multiline', props.enable_multiline) props.line_height = preset_data.get('line_height', props.line_height) props.text_alignment = preset_data.get('text_alignment', props.text_alignment) props.auto_wrap = preset_data.get('auto_wrap', props.auto_wrap) props.wrap_width = preset_data.get('wrap_width', props.wrap_width) props.max_lines = preset_data.get('max_lines', props.max_lines) props.line_spacing_pixels = preset_data.get('line_spacing_pixels', props.line_spacing_pixels) # Load stroke settings props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke) props.stroke_width = preset_data.get('stroke_width', props.stroke_width) props.stroke_position = preset_data.get('stroke_position', props.stroke_position) props.stroke_color = preset_data.get('stroke_color', props.stroke_color) # Load shadow/glow settings props.enable_shadow = preset_data.get('enable_shadow', props.enable_shadow) props.shadow_offset_x = preset_data.get('shadow_offset_x', props.shadow_offset_x) props.shadow_offset_y = preset_data.get('shadow_offset_y', props.shadow_offset_y) props.shadow_blur = preset_data.get('shadow_blur', props.shadow_blur) props.shadow_color = preset_data.get('shadow_color', props.shadow_color) # Handle new shadow_spread property with backward compatibility if hasattr(props, 'shadow_spread'): props.shadow_spread = preset_data.get('shadow_spread', 1.0) props.enable_glow = preset_data.get('enable_glow', props.enable_glow) props.glow_blur = preset_data.get('glow_blur', props.glow_blur) props.glow_color = preset_data.get('glow_color', props.glow_color) # Load blur settings props.enable_blur = preset_data.get('enable_blur', props.enable_blur) props.blur_radius = preset_data.get('blur_radius', props.blur_radius) # Load text fitting settings props.enable_text_fitting = preset_data.get('enable_text_fitting', props.enable_text_fitting) props.text_fitting_mode = preset_data.get('text_fitting_mode', props.text_fitting_mode) props.text_fitting_margin = preset_data.get('text_fitting_margin', props.text_fitting_margin) props.min_font_size = preset_data.get('min_font_size', props.min_font_size) props.max_font_size = preset_data.get('max_font_size', props.max_font_size) # Load overlay data props.image_overlays.clear() overlay_data_list = preset_data.get('image_overlays', []) for overlay_data in overlay_data_list: overlay = props.image_overlays.add() overlay.name = overlay_data.get('name', 'Overlay') overlay.image_path = overlay_data.get('image_path', '') overlay.x_position = overlay_data.get('x_position', 0.5) overlay.y_position = overlay_data.get('y_position', 0.5) overlay.scale = overlay_data.get('scale', 1.0) overlay.rotation = overlay_data.get('rotation', 0.0) overlay.z_index = overlay_data.get('z_index', 1) overlay.enabled = overlay_data.get('enabled', True) props.is_updating = False # FINAL VERIFICATION: Check what the properties actually are after loading print(f"[PRESET LOAD DEBUG] ==================== FINAL VERIFICATION ====================") print(f"[PRESET LOAD DEBUG] Properties AFTER loading preset '{self.preset_name}':") print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows} (type: {type(props.disable_shadows)})") print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections} (type: {type(props.disable_reflections)})") print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") print(f"[PRESET LOAD DEBUG] ================================================================") # Always generate preview after loading preset # Import preview generation function from .. import generate_preview generate_preview(context) source_info = { 'BLEND_FILE': "from blend file", 'PERSISTENT_FILE': "from persistent storage", 'LOCAL_FILE': "from legacy storage" }.get(source, "from unknown source") self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}") except (OSError, IOError) as e: props.is_updating = False self.report({'ERROR'}, f"Failed to read preset file: {str(e)}") return {'CANCELLED'} except json.JSONDecodeError as e: props.is_updating = False self.report({'ERROR'}, f"Invalid preset file format: {str(e)}") return {'CANCELLED'} except Exception as e: props.is_updating = False self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} class TEXT_TEXTURE_OT_delete_preset(Operator): """Delete preset""" bl_idname = "text_texture.delete_preset" bl_label = "Delete Preset" bl_description = "Delete selected preset" bl_options = {'REGISTER', 'UNDO'} preset_name: StringProperty() def execute(self, context): props = context.scene.text_texture_props try: # Delete from both blend file and local storage blend_deleted = delete_blend_file_preset(self.preset_name) preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") local_deleted = False if os.path.exists(preset_file): os.remove(preset_file) local_deleted = True if not blend_deleted and not local_deleted: self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage") # Remove from UI list for i, preset in enumerate(props.presets): if preset.name == self.preset_name: props.presets.remove(i) break delete_info = [] if blend_deleted: delete_info.append("blend file") if local_deleted: delete_info.append("local storage") location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)" self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}") except (OSError, IOError) as e: self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}") return {'CANCELLED'} except Exception as e: self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} def invoke(self, context, event): # Add confirmation dialog for better UX return context.window_manager.invoke_confirm(self, event) def draw(self, context): layout = self.layout layout.label(text=f"Delete preset '{self.preset_name}'?") layout.label(text="This action cannot be undone.", icon='ERROR') class TEXT_TEXTURE_OT_refresh_presets(Operator): """Refresh presets from blend file and local storage""" bl_idname = "text_texture.refresh_presets" bl_label = "Refresh Presets" bl_description = "Reload presets from blend file and local storage" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: # Use the reliable synchronous refresh system success = refresh_presets_sync() if success: props = context.scene.text_texture_props total_presets = len(props.presets) # Count by source for detailed reporting blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE']) persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE']) local_count = len([p for p in props.presets if p.source == 'LOCAL_FILE']) self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)") else: self.report({'WARNING'}, "Preset refresh completed with issues - check console for details") # Force UI redraw for area in context.screen.areas: area.tag_redraw() return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}") import traceback traceback.print_exc() return {'CANCELLED'} class TEXT_TEXTURE_OT_export_presets(Operator): """Export all presets to a file for backup""" bl_idname = "text_texture.export_presets" bl_label = "Export Presets" bl_description = "Export all presets to a file for backup" bl_options = {'REGISTER', 'UNDO'} filepath: StringProperty( name="File Path", description="Path to save preset backup file", subtype='FILE_PATH', default="text_texture_presets_backup.json" ) def execute(self, context): try: props = context.scene.text_texture_props # Collect all presets from all sources all_presets = {} # Get blend file presets blend_presets = get_blend_file_presets() for name, data in blend_presets.items(): all_presets[name] = { "data": data, "source": "BLEND_FILE" } # Get persistent presets persistent_dir = get_persistent_preset_path() if os.path.exists(persistent_dir): for filename in os.listdir(persistent_dir): if filename.endswith('.json'): preset_name = os.path.splitext(filename)[0] if preset_name not in all_presets: # Don't override blend file presets try: with open(os.path.join(persistent_dir, filename), 'r') as f: preset_data = json.load(f) all_presets[preset_name] = { "data": preset_data, "source": "PERSISTENT_FILE" } except (json.JSONDecodeError, OSError) as e: print(f"[TTG] Error reading persistent preset {preset_name}: {e}") # Get legacy presets legacy_dir = get_preset_path() if os.path.exists(legacy_dir) and legacy_dir != persistent_dir: for filename in os.listdir(legacy_dir): if filename.endswith('.json'): preset_name = os.path.splitext(filename)[0] if preset_name not in all_presets: # Don't override higher priority presets try: with open(os.path.join(legacy_dir, filename), 'r') as f: preset_data = json.load(f) all_presets[preset_name] = { "data": preset_data, "source": "LOCAL_FILE" } except (json.JSONDecodeError, OSError) as e: print(f"[TTG] Error reading legacy preset {preset_name}: {e}") if not all_presets: self.report({'WARNING'}, "No presets found to export") return {'CANCELLED'} # Create export data with metadata export_data = { "format_version": "1.0", "addon_version": bl_info["version"], "export_timestamp": time.time(), "presets": all_presets } # Write to file with open(self.filepath, 'w') as f: json.dump(export_data, f, indent=2) self.report({'INFO'}, f"✅ Exported {len(all_presets)} presets to {os.path.basename(self.filepath)}") return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed to export presets: {str(e)}") return {'CANCELLED'} def invoke(self, context, event): # Set default filename with timestamp import datetime timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.filepath = f"text_texture_presets_backup_{timestamp}.json" context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} class TEXT_TEXTURE_OT_import_presets(Operator): """Import presets from a backup file""" bl_idname = "text_texture.import_presets" bl_label = "Import Presets" bl_description = "Import presets from a backup file" bl_options = {'REGISTER', 'UNDO'} filepath: StringProperty( name="File Path", description="Path to preset backup file", subtype='FILE_PATH' ) import_mode: EnumProperty( name="Import Mode", description="How to handle existing presets with same names", items=[ ('SKIP', 'Skip Existing', 'Skip presets that already exist'), ('OVERWRITE', 'Overwrite', 'Overwrite existing presets'), ('RENAME', 'Rename New', 'Rename imported presets if they conflict'), ], default='SKIP' ) import_to_blend: BoolProperty( name="Import to Blend File", description="Import presets to blend file (recommended for portability)", default=True ) def execute(self, context): try: if not os.path.exists(self.filepath): self.report({'ERROR'}, "Backup file not found") return {'CANCELLED'} props = context.scene.text_texture_props # Read backup file with open(self.filepath, 'r') as f: import_data = json.load(f) # Validate format if "presets" not in import_data: self.report({'ERROR'}, "Invalid backup file format") return {'CANCELLED'} imported_presets = import_data["presets"] if not imported_presets: self.report({'WARNING'}, "No presets found in backup file") return {'CANCELLED'} # Get existing presets to check for conflicts existing_blend_presets = get_blend_file_presets() existing_preset_names = set(existing_blend_presets.keys()) # Also check persistent storage persistent_dir = get_persistent_preset_path() if os.path.exists(persistent_dir): for filename in os.listdir(persistent_dir): if filename.endswith('.json'): existing_preset_names.add(os.path.splitext(filename)[0]) imported_count = 0 skipped_count = 0 errors = [] for preset_name, preset_info in imported_presets.items(): preset_data = preset_info.get("data", {}) # Handle conflicts final_name = preset_name if preset_name in existing_preset_names: if self.import_mode == 'SKIP': skipped_count += 1 continue elif self.import_mode == 'RENAME': counter = 1 base_name = preset_name while final_name in existing_preset_names: final_name = f"{base_name}_imported_{counter}" counter += 1 # OVERWRITE mode uses original name try: # Import to blend file or persistent storage based on setting if self.import_to_blend: success = save_blend_file_preset(final_name, preset_data) if not success: raise Exception("Failed to save to blend file") else: # Save to persistent storage persistent_file = os.path.join(get_persistent_preset_path(), f"{final_name}.json") with open(persistent_file, 'w') as f: json.dump(preset_data, f, indent=2) imported_count += 1 # Add to UI list if not already present existing_preset = None for preset in props.presets: if preset.name == final_name: existing_preset = preset break if not existing_preset: new_preset = props.presets.add() new_preset.name = final_name new_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE' else: existing_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE' except Exception as e: errors.append(f"{preset_name}: {str(e)}") # Report results if imported_count > 0: storage_location = "blend file" if self.import_to_blend else "persistent storage" self.report({'INFO'}, f"✅ Imported {imported_count} presets to {storage_location}") if skipped_count > 0: self.report({'INFO'}, f"⏭️ Skipped {skipped_count} existing presets") else: self.report({'WARNING'}, "No presets were imported") if errors: self.report({'WARNING'}, f"Errors importing {len(errors)} presets - check console") for error in errors[:5]: # Show first 5 errors print(f"[TTG] Import error: {error}") return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed to import presets: {str(e)}") return {'CANCELLED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def draw(self, context): layout = self.layout layout.prop(self, "import_mode") layout.prop(self, "import_to_blend") class TEXT_TEXTURE_OT_show_migration_report(Operator): """Show migration report with backup and upgrade information""" bl_idname = "text_texture.show_migration_report" bl_label = "Show Migration Report" bl_description = "Display information about preset migration and backups" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context): layout = self.layout layout.label(text="Migration Report", icon='INFO') layout.separator() layout.label(text="This addon has been upgraded to the new preset system.") layout.label(text="Your existing presets have been preserved.") layout.separator() layout.label(text="Features:", icon='CHECKMARK') layout.label(text="• Presets saved in blend files") layout.label(text="• Cross-file preset sharing") layout.label(text="• Automatic backup system") layout.separator() # Export all operator classes for wildcard imports __all__ = [ 'TEXT_TEXTURE_OT_save_preset', 'TEXT_TEXTURE_OT_save_preset_enter', 'TEXT_TEXTURE_OT_load_preset', 'TEXT_TEXTURE_OT_delete_preset', 'TEXT_TEXTURE_OT_refresh_presets', 'TEXT_TEXTURE_OT_export_presets', 'TEXT_TEXTURE_OT_import_presets', 'TEXT_TEXTURE_OT_show_migration_report', ]