import os import bpy import json from bpy.types import Operator from bpy.props import StringProperty, IntProperty, BoolProperty from .funcs import pref, has_templates, name_from_path, already_present, on_path_update from .. import __package__ as base_package class CT_OT_export_templates(Operator): bl_idname = "ct.export_templates" bl_label = "Export custom templates" bl_description = "Export the current list of templates to JSON file" filepath: StringProperty( subtype="FILE_PATH", description="Select the path for the exported file", default="templates.json") def execute(self, context): prefs = pref() with open(self.filepath, 'w') as f: projects = [{"name": project.name, "path": project.path} for project in prefs.projects] json.dump(projects, f, indent=4) self.report({'INFO'}, f"Templates exported to {self.filepath}") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} @classmethod def poll(cls, context): return has_templates() class CT_OT_import_templates(Operator): bl_idname = "ct.import_templates" bl_label = "Import custom templates" bl_description = "Import a list of templates from JSON file (note that this will override the current templates list)" filepath: StringProperty( subtype="FILE_PATH", description="Select the .json file to load") def execute(self, context): prefs = pref() if os.path.exists(self.filepath): with open(self.filepath, 'r') as f: projects = json.load(f) prefs.projects.clear() for project in projects: item = prefs.projects.add() item.name = project["name"] item.path = project["path"] prefs.active_template_index = 0 context.preferences.is_dirty = True self.report({'INFO'}, f"Projects imported from {self.filepath}") else: self.report( {'WARNING'}, f"Import cancelled: path not found ({self.filepath})") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} class CT_OT_add(Operator): bl_idname = "ct.add" bl_label = "Add Template" bl_description = "Add new template" def execute(self, context): prefs = pref() prefs.projects.add() prefs.active_template_index = len(prefs.projects) - 1 context.preferences.is_dirty = True return {'FINISHED'} class CT_OT_remove(Operator): bl_idname = "ct.remove" bl_label = "Remove Template" bl_description = "Remove selected template" def execute(self, context): prefs = pref() prefs.projects.remove(prefs.active_template_index) prefs.active_template_index = min( max(0, prefs.active_template_index - 1), len(prefs.projects) - 1) context.preferences.is_dirty = True return {'FINISHED'} @classmethod def poll(cls, context): return has_templates() class CT_OT_move_up(Operator): bl_idname = "ct.move_up" bl_label = "Move Up" bl_description = "Move the selected template up in the list" def execute(self, context): prefs = pref() index = prefs.active_template_index if index > 0: prefs.projects.move(index, index - 1) prefs.active_template_index -= 1 context.preferences.is_dirty = True else: self.report({'WARNING'}, "Template is already at the top") return {'FINISHED'} @classmethod def poll(cls, context): return has_templates() class CT_OT_move_down(Operator): bl_idname = "ct.move_down" bl_label = "Move Down" bl_description = "Move the selected template down in the list" def execute(self, context): prefs = pref() index = prefs.active_template_index if index < len(prefs.projects) - 1: prefs.projects.move(index, index + 1) prefs.active_template_index += 1 context.preferences.is_dirty = True else: self.report({'WARNING'}, "Template is already at the bottom") return {'FINISHED'} @classmethod def poll(cls, context): return has_templates() class CT_OT_clear(Operator): bl_idname = "ct.clear" bl_label = "Clear Templates" bl_description = "Clear the current list of templates (remove all templates)" def execute(self, context): prefs = pref() prefs.projects.clear() prefs.active_template_index = 0 context.preferences.is_dirty = True return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @classmethod def poll(cls, context): return has_templates() class CT_OT_add_template_popup(Operator): bl_idname = "ct.add_template_popup" bl_label = "Use as template" bl_description = "Use the current .blend file to create a new template occurency" name: StringProperty(name="Project Name") def execute(self, context): prefs = pref() if not already_present(self, prefs, bpy.data.filepath): if bpy.data.filepath: new_project = prefs.projects.add() new_project.name = self.name new_project.path = bpy.data.filepath self.name = '' context.preferences.is_dirty = True else: self.report({'ERROR'}, "Current file is not saved on disk.") return {'FINISHED'} def invoke(self, context, event): if bpy.data.filepath: self.name = name_from_path(bpy.data.filepath) return context.window_manager.invoke_props_dialog(self) class CT_OT_select_template_popup(Operator): bl_idname = "ct.select_template_popup" bl_label = "Select a new custom template" bl_description = "Create a new template by selecting an existing .blend file" path: StringProperty(name="Template Path", subtype="FILE_PATH", update=on_path_update) name: StringProperty(name="Template Name") def execute(self, context): prefs = pref() if not already_present(self, prefs, self.path): template = prefs.projects.add() template.name = self.name template.path = self.path context.preferences.is_dirty = True self.name = '' self.path = '' return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) class CT_OT_add_templates_from_folder(Operator): bl_idname = "ct.add_templates_from_folder" bl_label = "Add Templates from Folder" bl_description = "Add templates from a specified folder containing .blend files" directory: StringProperty( subtype="DIR_PATH", description="Select the folder containing .blend files") depth: IntProperty( name="Depth", description="Depth of recursion (default 1)", default=1, min=1) def execute(self, context): prefs = pref() if os.path.exists(self.directory): blend_files = [] for root, dirs, files in os.walk(self.directory): # Calculate the current depth current_depth = root[len(self.directory):].count(os.sep) if current_depth < self.depth: for file in files: if file.endswith('.blend'): blend_files.append(os.path.join(root, file)) if not blend_files: self.report( {'WARNING'}, "No .blend files found in the specified directory") return {'CANCELLED'} new_t = 0 for path in blend_files: if not already_present(self, prefs, path, False): new_t += 1 item = prefs.projects.add() item.name = name_from_path(path) item.path = path context.preferences.is_dirty = True self.report( {'INFO'}, f"{new_t} new templates added from {self.directory} ({len(blend_files) - new_t} already present)") else: self.report( {'WARNING'}, f"Import cancelled: directory not found ({self.directory})") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} class CT_OT_open_preferences(Operator): bl_idname = "ct.open_preferences" bl_label = "Open Custom Templates Preferences" bl_description = "Open the preferences for the Custom Templates add-on" def execute(self, context): bpy.ops.screen.userpref_show('INVOKE_DEFAULT') context.preferences.active_section = 'ADDONS' context.window_manager.addon_search = "Custom Templates" return {'FINISHED'} class CT_OT_add_workspace(Operator): bl_idname = "ct.add_workspace" bl_label = "Add this workspace from your template" bl_description = "Add to the current project, the selected workspace from your Custom Template" workspace: StringProperty(name="workspace") path: StringProperty(name="path") def execute(self, context): bpy.ops.workspace.append_activate(idname=self.workspace, filepath=self.path) return {'FINISHED'} class CT_OT_template_workspaces(Operator): bl_idname = "ct.template_workspaces" bl_label = "Add workspace from this template" bl_description = "Click to select one of the workspaces from this Custom Template" index: IntProperty(name='index', default=0) def draw_ws(self, s, context): layout = s.layout template = pref().projects[self.index] with bpy.data.libraries.load(template.path) as (data, _): for w in data.workspaces: op = layout.operator("ct.add_workspace", text=w) op.workspace = w op.path = template.path def execute(self, context): return {'RUNNING_MODAL'} def invoke(self, context, event): wm = context.window_manager template = pref().projects[self.index] wm.popup_menu(self.draw_ws, title=f"Workspaces from '{template.name}'", icon="ADD") return {'RUNNING_MODAL'} class CT_OT_start_from(Operator): bl_idname = "ct.start_from" bl_label = "Start from..." bl_description = "Use any selected .blend file as template (and optionally add it to the list)" filepath: StringProperty( subtype="FILE_PATH", description="Select the .json file to load", update=on_path_update) add: BoolProperty(default=False, name="Add template", description="Add this file in your templates") name: StringProperty(name="Name", description="The name for this template (if empty, file name will be used)") def execute(self, context): prefs = pref() if self.filepath: if self.add and not already_present(self, prefs, self.filepath): template = prefs.projects.add() template.name = self.name if self.name else name_from_path(self.filepath) template.path = self.filepath context.preferences.is_dirty = True bpy.ops.wm.read_homefile(filepath=self.filepath) else: self.report({'WARNING'}, "This option can only be used from File > New menu") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'}