280 lines
10 KiB
Python

# 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 <https://www.gnu.org/licenses/>.
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[__package__].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[__package__].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[__package__].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[__package__].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[__package__].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[__package__].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 = __package__
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[__package__].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()