# Custom Templates - Blender Add-On # Copyright (C) 2024 Francesco Bellini # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import bpy from bpy.types import AddonPreferences, PropertyGroup, Operator from bpy.props import CollectionProperty, IntProperty, StringProperty bl_info = { "id": "custom_templates", "name": "Custom Templates", "tagline": "Add your own .blend files as template options for new projects", "blender": (4, 2, 0), "location": "File > New & File > Defaults", "category": "System", "support": "COMMUNITY", "blender_manifest": "blender_manifest.toml" } 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') class OT_SelectTemplatePopup(Operator): bl_idname = "wm.select_template_popup" bl_label = "Select a new custom template" bl_description = "Create a new template occurency by selecting an existing .blend file" project_name: StringProperty(name="Project Name") project_path: StringProperty(name="Project Path", subtype="FILE_PATH") def execute(self, context): prefs = context.preferences.addons[__name__].preferences for p in prefs.projects: if p.path == self.project_path: already_present = True self.report( {'WARNING'}, f'Selected file is already in the templates list as "{p.name}".') return {'FINISHED'} new_project = prefs.projects.add() new_project.name = self.project_name new_project.path = self.project_path self.report( {'INFO'}, f"Template '{self.project_name}' selected and added successfully!") return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) class OT_AddTemplatePopup(Operator): bl_idname = "wm.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") def execute(self, context): prefs = context.preferences.addons[__name__].preferences current_file_path = bpy.data.filepath for p in prefs.projects: if p.path == current_file_path: already_present = True self.report( {'WARNING'}, f'Current file is already in the templates list as "{p.name}".') return {'FINISHED'} if current_file_path: new_project = prefs.projects.add() new_project.name = self.project_name new_project.path = current_file_path self.report( {'INFO'}, f"Template '{self.project_name}' added successfully!") else: self.report({'ERROR'}, "Current file is not saved on disk.") return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) class OT_AddTemplateItem(Operator): bl_idname = "custom_templates.add" bl_label = "Add Template" bl_description = "Add new template" def execute(self, context): prefs = context.preferences.addons[__name__].preferences prefs.projects.add() prefs.active_template_index = len(prefs.projects) - 1 self.report({'INFO'}, f"Empty template added") return {'FINISHED'} class OT_RemoveTemplateItem(Operator): bl_idname = "custom_templates.remove" bl_label = "Remove Template" bl_description = "Remove selected template" def execute(self, context): prefs = context.preferences.addons[__name__].preferences self.report( {'INFO'}, f'Template "{prefs.projects[prefs.active_template_index].name}" removed{" (`"+prefs.projects[prefs.active_template_index].path+"`)" if prefs.projects[prefs.active_template_index].path != "" else "" }') prefs.projects.remove(prefs.active_template_index) prefs.active_template_index = min( max(0, prefs.active_template_index - 1), len(prefs.projects) - 1) return {'FINISHED'} class OT_MoveUpTemplateItem(bpy.types.Operator): bl_idname = "custom_templates.move_up" bl_label = "Move Up" bl_description = "Move the selected template up in the list" def execute(self, context): prefs = context.preferences.addons[__name__].preferences index = prefs.active_template_index if index > 0: prefs.projects.move(index, index - 1) prefs.active_template_index -= 1 self.report({'INFO'}, f"Templates list re-ordered") else: self.report({'WARNING'}, "Template is already at the top") return {'FINISHED'} class OT_MoveDownTemplateItem(bpy.types.Operator): bl_idname = "custom_templates.move_down" bl_label = "Move Down" bl_description = "Move the selected template down in the list" def execute(self, context): prefs = context.preferences.addons[__name__].preferences index = prefs.active_template_index if index < len(prefs.projects) - 1: prefs.projects.move(index, index + 1) prefs.active_template_index += 1 self.report({'INFO'}, f"Templates list re-ordered") else: self.report({'WARNING'}, "Template is already at the bottom") return {'FINISHED'} class OT_OpenAddonPreferences(bpy.types.Operator): bl_idname = "custom_templates.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'} # Add-On Preferences class CustomTemplatesPreferences(AddonPreferences): bl_idname = __name__ projects: CollectionProperty(type=TemplateItem) active_template_index: IntProperty( description="Index of the selected template") def draw(self, context): layout = self.layout layout.label( text="Here you can setup your own .blend files that will be shown in the `File > New` menu.") layout.label( text="You can also use `File > Defaults > Select new custom template` and `File > Defaults > Use current as new template` to update this preferences.") row = layout.row() row.template_list("UI_UL_list", "custom_templates", self, "projects", self, "active_template_index") col = row.column(align=True) col.operator("custom_templates.add", icon='ADD', text="") col.operator("custom_templates.remove", icon='REMOVE', text="") col.operator("custom_templates.move_up", icon='TRIA_UP', text="") col.operator("custom_templates.move_down", icon='TRIA_DOWN', text="") if self.projects: project = self.projects[self.active_template_index] layout.prop(project, "name") layout.prop(project, "path") def draw_addon_separator(layout): layout.separator() layout.label(text="Custom Templates") layout.separator() def draw_new_menu(self, context): layout = self.layout prefs = context.preferences.addons[__name__].preferences if len(prefs.projects) > 0: draw_addon_separator(layout) for project in prefs.projects: layout.operator( "wm.read_homefile", text=project.name).filepath = project.path # File > Defaults > Add-On Buttons def draw_add_template(self, context): layout = self.layout draw_addon_separator(layout) # Manage Template layout.operator("custom_templates.open_preferences", text="Manage templates") # Select new custom template layout.operator("wm.select_template_popup", text="Select new custom template") if bpy.data.filepath != "": # Use current as new template (only with an active saved .blend file opened) layout.operator("wm.add_template_popup", text="Use current as new template") def register(): bpy.utils.register_class(TemplateItem) bpy.utils.register_class(OT_MoveUpTemplateItem) bpy.utils.register_class(OT_MoveDownTemplateItem) bpy.utils.register_class(OT_AddTemplateItem) bpy.utils.register_class(OT_RemoveTemplateItem) bpy.utils.register_class(OT_AddTemplatePopup) bpy.utils.register_class(OT_SelectTemplatePopup) bpy.utils.register_class(OT_OpenAddonPreferences) bpy.utils.register_class(CustomTemplatesPreferences) bpy.types.TOPBAR_MT_file_new.remove(draw_new_menu) bpy.types.TOPBAR_MT_file_new.remove(draw_add_template) bpy.types.TOPBAR_MT_file_new.append(draw_new_menu) bpy.types.TOPBAR_MT_file_defaults.append(draw_add_template) def unregister(): bpy.utils.unregister_class(CustomTemplatesPreferences) bpy.utils.unregister_class(OT_MoveUpTemplateItem) bpy.utils.unregister_class(OT_MoveDownTemplateItem) bpy.utils.unregister_class(OT_AddTemplateItem) bpy.utils.unregister_class(OT_AddTemplatePopup) bpy.utils.unregister_class(OT_RemoveTemplateItem) bpy.utils.unregister_class(OT_SelectTemplatePopup) bpy.utils.unregister_class(OT_OpenAddonPreferences) bpy.utils.unregister_class(TemplateItem) bpy.types.TOPBAR_MT_file_new.remove(draw_new_menu) bpy.types.TOPBAR_MT_file_defaults.remove(draw_add_template) if __name__ == "__main__": register()