From 0ab6f0b24d3d2c08226cd56ed2576e407b8fd617 Mon Sep 17 00:00:00 2001 From: doc-code Date: Mon, 2 Sep 2024 15:46:21 +0200 Subject: [PATCH] Create legacy version (tested on all LTS versions from 2.83 to 3.6) --- legacy/__init__.py | 68 ++++ legacy/classes/draw.py | 39 ++ legacy/classes/ots.py | 350 ++++++++++++++++++ legacy/classes/splash.py | 116 ++++++ release-legacy.sh | 1 + .../legacy/custom-templates-legacy-1.3.1.zip | Bin 0 -> 6950 bytes 6 files changed, 574 insertions(+) create mode 100644 legacy/__init__.py create mode 100644 legacy/classes/draw.py create mode 100644 legacy/classes/ots.py create mode 100644 legacy/classes/splash.py create mode 100644 release-legacy.sh create mode 100644 releases/1.x.x/legacy/custom-templates-legacy-1.3.1.zip diff --git a/legacy/__init__.py b/legacy/__init__.py new file mode 100644 index 0000000..deabd07 --- /dev/null +++ b/legacy/__init__.py @@ -0,0 +1,68 @@ +# 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 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_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", + "name": "Custom Templates", + "tagline": "Add your own .blend files as template options for new projects", + "blender": (2, 80, 0), + "location": "File > New, File > Defaults, Splash Screen", + "category": "System", + "support": "COMMUNITY", +} + +classes = [WM_MT_splash, + TemplateItem, + CT_OT_export_templates, + CT_OT_import_templates, + CT_OT_splash_custom, + CT_OT_splash_default, + CT_OT_open_preferences, + CT_MT_splash_mode, + 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; + +def register(): + global og_splash + og_splash = bpy.types.WM_MT_splash; + for c in classes: + bpy.utils.register_class(c) + bpy.types.TOPBAR_MT_file_new.append(draw_file_new_templates) + bpy.types.TOPBAR_MT_file_defaults.append(draw_file_default_operators) + +def unregister(): + for c in reversed(classes): + bpy.utils.unregister_class(c) + bpy.utils.register_class(og_splash) + bpy.types.TOPBAR_MT_file_new.remove(draw_file_new_templates) + bpy.types.TOPBAR_MT_file_defaults.remove(draw_file_default_operators) + +if __name__ == "__main__": + register() diff --git a/legacy/classes/draw.py b/legacy/classes/draw.py new file mode 100644 index 0000000..6092a60 --- /dev/null +++ b/legacy/classes/draw.py @@ -0,0 +1,39 @@ +from .. import __name__ as base_package +from .ots import name_from_path +import bpy + +def draw_file_new_templates(self, context): + layout = self.layout + + prefs = context.preferences.addons[base_package].preferences + if len(prefs.projects) > 0: + layout.separator() + draw_templates(layout, context) + +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] + 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.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 template") + if bpy.data.filepath != "": + layout.operator("ct.add_template_popup", + text="Use current file as template") diff --git a/legacy/classes/ots.py b/legacy/classes/ots.py new file mode 100644 index 0000000..79d9669 --- /dev/null +++ b/legacy/classes/ots.py @@ -0,0 +1,350 @@ +from .. import __name__ as base_package +import os +import bpy +import json +from bpy.types import Operator, PropertyGroup, AddonPreferences +from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty + +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 + if self.path and self.path.startswith('//'): + self.path = bpy.path.abspath(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', update=on_path_update) + +class CustomTemplatesPreferences(AddonPreferences): + bl_idname = base_package + + 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=f"Your custom templates ({len(self.projects)})") + row = layout.row() + row.template_list("UI_UL_list", "custom_templates", + self, "projects", self, "active_template_index", rows=6, maxrows=6) + + col = row.column(align=True) + 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() + col.operator("ct.move_up", icon='TRIA_UP', text="") + col.operator("ct.move_down", icon='TRIA_DOWN', text="") + + if self.projects: + project = self.projects[self.active_template_index] + 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.") + + if not self.override_splash or len(self.projects) == 0: + 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.") + +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.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") + + 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] + 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 len(context.preferences.addons[base_package].preferences.projects) > 0 + +class CT_OT_import_templates(bpy.types.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 = context.preferences.addons[base_package].preferences + + 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 = context.preferences.addons[base_package].preferences + 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 = context.preferences.addons[base_package].preferences + 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 len(context.preferences.addons[base_package].preferences.projects) > 0 + +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 = context.preferences.addons[base_package].preferences + 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 len(context.preferences.addons[base_package].preferences.projects) > 0 + +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 = context.preferences.addons[base_package].preferences + 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 len(context.preferences.addons[base_package].preferences.projects) > 0 + +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" + + name: StringProperty(name="Project Name") + + def execute(self, context): + prefs = context.preferences.addons[base_package].preferences + 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 = context.preferences.addons[base_package].preferences + 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(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" + 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'} diff --git a/legacy/classes/splash.py b/legacy/classes/splash.py new file mode 100644 index 0000000..821d7ab --- /dev/null +++ b/legacy/classes/splash.py @@ -0,0 +1,116 @@ +# Ref WM_MT_splash https://projects.blender.org/blender/blender/src/commit/5a9fe638dedb179050c4929ea8fcdec80d221af2/scripts/startup/bl_operators/wm.py#L3296 +# Ref TOPBAR_MT_file_new https://projects.blender.org/blender/blender/src/commit/5a9fe638dedb179050c4929ea8fcdec80d221af2/scripts/startup/bl_ui/space_topbar.py#L286 +from .. import __name__ as base_package +from .draw import draw_templates +import bpy +# Clone of the WM_MT_splash (the section under the image of the Splash) +# It's edited (only in #Templates part) to add a menu in the New File section +# to switch between blender's original templates and Custom Templates. +# If you have no custom templates, the default templates are displayed +class WM_MT_splash(bpy.types.Menu): + bl_label="Splash" + + def draw(self, context): + layout = self.layout + prefs = context.preferences.addons[base_package].preferences + layout.operator_context = 'EXEC_DEFAULT' + layout.emboss = 'PULLDOWN_MENU' + split = layout.split() + + # Templates + col1 = split.column() + ct_split = col1.split(factor=0.9) + colA = ct_split.column() + if prefs.override_splash and len(prefs.projects) > 0: + colA.label(text="Custom Templates") + col1.operator_context = 'INVOKE_DEFAULT' + draw_templates(col1, context, True) + else: + colA.label(text="New File") + # Call original code + bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True) + colB = ct_split.column() + colB.menu("CT_MT_splash_mode", icon='DOWNARROW_HLT', text="") + + # Recent + col2 = split.column() + col2_title = col2.row() + + found_recent = col2.template_recent_files() + + if found_recent: + col2_title.label(text="Recent Files") + else: + # Links if no recent files. + col2_title.label(text="Getting Started") + + col2.operator("wm.url_open_preset", text="Manual", icon='URL').type = 'MANUAL' + col2.operator("wm.url_open", text="Tutorials", icon='URL').url = "https://www.blender.org/tutorials/" + col2.operator("wm.url_open", text="Support", icon='URL').url = "https://www.blender.org/support/" + col2.operator("wm.url_open", text="User Communities", icon='URL').url = "https://www.blender.org/community/" + col2.operator("wm.url_open_preset", text="Blender Website", icon='URL').type = 'BLENDER' + + layout.separator() + + split = layout.split() + + col1 = split.column() + sub = col1.row() + sub.operator_context = 'INVOKE_DEFAULT' + sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER') + col1.operator("wm.recover_last_session", icon='RECOVER_LAST') + + col2 = split.column() + + col2.operator("wm.url_open_preset", text="Donate", icon='FUND').type = 'FUND' + col2.operator("wm.url_open_preset", text="What's New", icon='URL').type = 'RELEASE_NOTES' + + layout.separator() + + if (not bpy.app.online_access) and bpy.app.online_access_override: + self.layout.label(text="Running in Offline Mode", icon='INTERNET_OFFLINE') + + layout.separator() + +class CT_MT_splash_mode(bpy.types.Menu): + bl_idname = "CT_MT_splash_mode" + bl_label = "Custom Templates Switch" + bl_description = "Swtich between Blender's default and your Custom Templates" + + def draw(self, context): + layout = self.layout + prefs = context.preferences.addons[base_package].preferences + def_check = 'NONE' + ct_check = 'NONE' + if prefs.override_splash: + ct_check = "CHECKMARK" + else: + def_check = "CHECKMARK" + + layout.operator("ct.open_preferences", text="Manage templates") + layout.separator() + layout.operator("ct.splash_custom", text="Use Custom Templates", icon=ct_check) + layout.operator("ct.splash_default", text="Use Blender's Default", icon=def_check) + +class CT_OT_splash_default(bpy.types.Operator): + bl_idname = "ct.splash_default" + bl_label = "Display default Blender's templates" + bl_description = "Use Blender's default templates in the Splash Screen" + + def execute(self, context): + prefs = context.preferences.addons[base_package].preferences + prefs.override_splash = False + context.preferences.is_dirty = True + return {'FINISHED'} + +class CT_OT_splash_custom(bpy.types.Operator): + bl_idname = "ct.splash_custom" + bl_label = "Display your custom templates" + bl_description = "Use your custom templates in the Splash Screen (limited to first 5)" + + def execute(self, context): + prefs = context.preferences.addons[base_package].preferences + prefs.override_splash = True + context.preferences.is_dirty = True + return {'FINISHED'} + \ No newline at end of file diff --git a/release-legacy.sh b/release-legacy.sh new file mode 100644 index 0000000..3742418 --- /dev/null +++ b/release-legacy.sh @@ -0,0 +1 @@ +zip -9 -r ./releases/1.x.x/legacy/custom-templates-legacy-1.3.1.zip ./legacy/ diff --git a/releases/1.x.x/legacy/custom-templates-legacy-1.3.1.zip b/releases/1.x.x/legacy/custom-templates-legacy-1.3.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..458959d31eb5a390c2b67f114594cf987c5dc28e GIT binary patch literal 6950 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 T;sgMo{Je2~9{CaJuc!Y5zdLd4 literal 0 HcmV?d00001