From f80b32a127b6fd20f090d7ebe4a096c51d91dca0 Mon Sep 17 00:00:00 2001 From: doc-code Date: Sun, 1 Sep 2024 03:45:29 +0200 Subject: [PATCH] Features: add from folder, clear current templates, auto-naming; +refactor --- addon/__init__.py | 6 +- addon/classes/draw.py | 27 +++--- addon/classes/ots.py | 197 ++++++++++++++++++++++++++++++------------ 3 files changed, 164 insertions(+), 66 deletions(-) diff --git a/addon/__init__.py b/addon/__init__.py index d025f45..6816c34 100644 --- a/addon/__init__.py +++ b/addon/__init__.py @@ -16,7 +16,7 @@ import bpy from .classes.draw import draw_file_new_templates, draw_file_default_operators from .classes.splash import WM_MT_splash, CT_MT_splash_mode, CT_OT_splash_custom, CT_OT_splash_default -from .classes.ots import CustomTemplatesPreferences, TemplateItem, CT_OT_export_templates, CT_OT_import_templates, CT_MT_export, CT_OT_select_template_popup, CT_OT_add_template_popup, CT_OT_add, CT_OT_remove, CT_OT_move_down, CT_OT_move_up, CT_OT_open_preferences +from .classes.ots import CustomTemplatesPreferences, TemplateItem, CT_OT_export_templates, CT_OT_import_templates, CT_MT_templates_menu, CT_OT_select_template_popup, CT_OT_add_template_popup, CT_OT_add, CT_OT_remove, CT_OT_move_down, CT_OT_move_up, CT_OT_open_preferences, CT_OT_add_templates_from_folder, CT_OT_clear bl_info = { "id": "custom_templates", @@ -37,13 +37,15 @@ classes = [WM_MT_splash, CT_OT_splash_default, CT_OT_open_preferences, CT_MT_splash_mode, - CT_MT_export, + CT_MT_templates_menu, CT_OT_move_up, CT_OT_move_down, CT_OT_add, CT_OT_remove, + CT_OT_clear, CT_OT_add_template_popup, CT_OT_select_template_popup, + CT_OT_add_templates_from_folder, CustomTemplatesPreferences] og_splash = None; diff --git a/addon/classes/draw.py b/addon/classes/draw.py index 70ac856..75cbe26 100644 --- a/addon/classes/draw.py +++ b/addon/classes/draw.py @@ -1,4 +1,5 @@ from .. import __package__ as base_package +from .ots import name_from_path import bpy def draw_file_new_templates(self, context): @@ -8,25 +9,31 @@ def draw_file_new_templates(self, context): if len(prefs.projects) > 0: layout.separator() draw_templates(layout, context) - -def draw_templates(layout, context, splash_mode = False): + +def draw_templates(layout, context, splash_mode=False): prefs = context.preferences.addons[base_package].preferences for i in range(min(5, len(prefs.projects)) if splash_mode else len(prefs.projects)): project = prefs.projects[i] - layout.operator( - "wm.read_homefile", text=project.name, icon="FILE_NEW" if splash_mode else "NONE").filepath = project.path - + if project.path: + layout.operator( + "wm.read_homefile", text=(project.name if project.name else name_from_path(project.path)), icon="FILE_NEW" if splash_mode else "NONE").filepath = project.path + def draw_file_default_operators(self, context): layout = self.layout layout.separator() layout.operator("ct.open_preferences", text="Manage templates") - layout.menu("CT_MT_export", text="Import/Export") + layout.operator("ct.add_templates_from_folder", + text="Add from folder", icon="ADD") + layout.operator("ct.clear", text="Clear current templates", icon="TRASH") + layout.separator() + layout.operator("ct.import_templates", + text="Import templates", icon="IMPORT") + layout.operator("ct.export_templates", + text="Export templates", icon="EXPORT") layout.separator() layout.operator("ct.select_template_popup", - text="Select new custom template") + text="Select new template") if bpy.data.filepath != "": - # Only with an active saved .blend file layout.operator("ct.add_template_popup", - text="Use current as new template") - \ No newline at end of file + text="Use current file as template") diff --git a/addon/classes/ots.py b/addon/classes/ots.py index 65aa024..4bf38fb 100644 --- a/addon/classes/ots.py +++ b/addon/classes/ots.py @@ -5,33 +5,54 @@ import json from bpy.types import Operator, PropertyGroup, AddonPreferences from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty -# Preferences +def already_present(self, prefs, path, report=True): + for p in prefs.projects: + if p.path == path: + if report: + self.report( + {'WARNING'}, f'Current file is already in the templates list as "{p.name}".') + return True + return False + +def name_from_path(path): + if path: + return os.path.splitext(os.path.basename(path))[0] + else: + return "" + +def on_path_update(self, context): + if not self.name and self.path: + self.name = name_from_path(self.path) + context.preferences.is_dirty = True + class TemplateItem(PropertyGroup): name: StringProperty( name="Name", description="Display name for this template") path: StringProperty( - name="Path", description="Path to the .blend file for this template", subtype='FILE_PATH') + name="Path", description="Path to the .blend file for this template", subtype='FILE_PATH', update=on_path_update) -override_splash_text = "Override Splash Screen's 'New File' list" class CustomTemplatesPreferences(AddonPreferences): bl_idname = base_package - - override_splash: BoolProperty(default=True, name=override_splash_text, description=override_splash_text) + + override_splash: BoolProperty( + default=True, name="Override Splash Screen Templates", description="Override Splash Screen's 'New File' list with your Custom Templates") projects: CollectionProperty(type=TemplateItem) active_template_index: IntProperty( description="Index of the selected template") - + def draw(self, context): layout = self.layout - - layout.label(text="Your custom templates:") + + layout.label( + text=f"Your custom templates ({len(self.projects)})") row = layout.row() row.template_list("UI_UL_list", "custom_templates", self, "projects", self, "active_template_index") col = row.column(align=True) - col.menu("CT_MT_export", icon='DOWNARROW_HLT', text="") + col.menu("CT_MT_templates_menu", icon='DOWNARROW_HLT', text="") col.separator() + col.operator("ct.add_templates_from_folder", text="", icon="FILE_FOLDER") col.operator("ct.add", icon='ADD', text="") col.operator("ct.remove", icon='REMOVE', text="") col.separator() @@ -40,42 +61,49 @@ class CustomTemplatesPreferences(AddonPreferences): if self.projects: project = self.projects[self.active_template_index] - layout.prop(project, "name") layout.prop(project, "path") - + layout.prop(project, "name") + layout.prop(self, "override_splash") box = layout.box() if self.override_splash and len(self.projects) == 0: box.label(text="There are currently no templates.") elif self.override_splash: - box.label(text="Custom templates will be shown in the Splash Screen.") - + box.label(text="Custom templates will be shown in the Splash Screen.") + if not self.override_splash or len(self.projects) == 0: - box.label(text="The default Blender list will be shown in the Splash Screen.") - + box.label( + text="The default Blender list will be shown in the Splash Screen.") + if len(self.projects) > 5: - box.label(text="Note: Only the first 5 templates in the list will be shown in the splash screen.") + box.label( + text="Note: Only the first 5 templates in the list will be shown in the splash screen.") -class CT_MT_export(bpy.types.Menu): - bl_label = "Import/Export custom templates" - bl_description = "Allows you to save al load your custom templates (using json format)" +class CT_MT_templates_menu(bpy.types.Menu): + bl_label = "Manage your custom templates" + bl_description = "Import, export, add from folder (with controllable recursion depth), clear current templates" def draw(self, context): layout = self.layout - layout.operator("ct.export_templates", text="Export templates") - layout.operator("ct.import_templates", text="Import templates") + layout.operator("ct.add_templates_from_folder", text="Add from folder", icon="ADD") + layout.operator("ct.clear", text="Clear current templates", icon="TRASH") + layout.separator() + layout.operator("ct.import_templates", text="Import templates", icon="IMPORT") + layout.operator("ct.export_templates", text="Export templates", icon="EXPORT") class CT_OT_export_templates(bpy.types.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") + filepath: StringProperty( + subtype="FILE_PATH", description="Select the path for the exported file", default="templates.json") def execute(self, context): prefs = context.preferences.addons[base_package].preferences with open(self.filepath, 'w') as f: - projects = [{"name": project.name, "path": project.path} for project in prefs.projects] + 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}") @@ -84,7 +112,7 @@ class CT_OT_export_templates(bpy.types.Operator): def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - + @classmethod def poll(cls, context): return len(context.preferences.addons[base_package].preferences.projects) > 0 @@ -94,7 +122,8 @@ class CT_OT_import_templates(bpy.types.Operator): 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") + filepath: StringProperty( + subtype="FILE_PATH", description="Select the .json file to load") def execute(self, context): prefs = context.preferences.addons[base_package].preferences @@ -113,14 +142,14 @@ class CT_OT_import_templates(bpy.types.Operator): self.report({'INFO'}, f"Projects imported from {self.filepath}") else: - self.report({'WARNING'}, f"Import cancelled: path not found ({self.filepath})") + 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'} -# Templates list oeprators class CT_OT_add(Operator): bl_idname = "ct.add" bl_label = "Add Template" @@ -145,7 +174,7 @@ class CT_OT_remove(Operator): max(0, prefs.active_template_index - 1), len(prefs.projects) - 1) context.preferences.is_dirty = True return {'FINISHED'} - + @classmethod def poll(cls, context): return len(context.preferences.addons[base_package].preferences.projects) > 0 @@ -166,7 +195,7 @@ class CT_OT_move_up(Operator): else: self.report({'WARNING'}, "Template is already at the top") return {'FINISHED'} - + @classmethod def poll(cls, context): return len(context.preferences.addons[base_package].preferences.projects) > 0 @@ -187,65 +216,125 @@ class CT_OT_move_down(Operator): else: self.report({'WARNING'}, "Template is already at the bottom") return {'FINISHED'} - + @classmethod def poll(cls, context): return len(context.preferences.addons[base_package].preferences.projects) > 0 -# Popups of File > Defaults -def already_present(self, prefs, path): - for p in prefs.projects: - if p.path == path: - already_present = True - self.report( - {'WARNING'}, f'Current file is already in the templates list as "{p.name}".') - return True - return False +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 = context.preferences.addons[base_package].preferences + 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 len(context.preferences.addons[base_package].preferences.projects) > 0 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" - project_name: StringProperty(name="Project Name") + name: StringProperty(name="Project Name") def execute(self, context): prefs = context.preferences.addons[base_package].preferences - current_file_path = bpy.data.filepath - if not already_present(self, prefs, current_file_path): - if current_file_path: + if not already_present(self, prefs, bpy.data.filepath): + if bpy.data.filepath: new_project = prefs.projects.add() - new_project.name = self.project_name - new_project.path = current_file_path + 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 occurency by selecting an existing .blend file" + bl_description = "Create a new template by selecting an existing .blend file" - project_name: StringProperty(name="Project Name") - project_path: StringProperty(name="Project Path", subtype="FILE_PATH") + path: StringProperty(name="Template Path", subtype="FILE_PATH", update=on_path_update) + name: StringProperty(name="Template Name") def execute(self, context): prefs = context.preferences.addons[base_package].preferences - if not already_present(self, prefs, self.project_path): - new_project = prefs.projects.add() - new_project.name = self.project_name - new_project.path = self.project_path + 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) - -# Open Preferences + +class CT_OT_add_templates_from_folder(bpy.types.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 = context.preferences.addons[base_package].preferences + + 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"