Compare commits

..

95 Commits
v1.0.2 ... main

Author SHA1 Message Date
04ffbc52a2 Update README_EXT 2024-09-23 03:02:00 +02:00
215aba3866 Update .zip(s) for v1.5.0 2024-09-23 02:36:26 +02:00
70709ef27c Update READMEs 2024-09-23 02:31:27 +02:00
65695286a5 Update release-legacy.sh version 2024-09-23 02:31:17 +02:00
af0c68a09c Add TEST_WORKFLOW dev testing helper 2024-09-23 02:29:15 +02:00
3c9ac708a9 Update CHANGELOG for v1.5.0 2024-09-23 02:07:36 +02:00
a8c30de813 Update affected Preview Images 2024-09-23 02:07:04 +02:00
05e8eab320 Update legacy to v1.5.0 2024-09-23 02:06:50 +02:00
06bf637da9 Start from: Avoid add to templates if already present 2024-09-23 00:31:40 +02:00
7636f7611b File > New > Start from, Splash Screen 5+ support, Splash menu update, code refactor/re-organized, update v to 1.5.0 2024-09-23 00:26:53 +02:00
5fb7fa1a9b Update README_EXT 2024-09-07 19:15:29 +02:00
doc-code
c4878e1209 Simplified README in favor of Wiki 2024-09-04 13:48:46 +02:00
doc-code
52650546b5 Update README 2024-09-03 23:42:16 +02:00
doc-code
78d25f4682 Update release-legacy script for 1.4.0 2024-09-03 23:40:16 +02:00
doc-code
1360818b57 Update README_EXT for v1.4.0 2024-09-03 23:37:30 +02:00
doc-code
181cb5cff9 Update Preview Image 2024-09-03 23:34:30 +02:00
doc-code
6f1bf8be6d Preview Image: Add Workspace from templates 2024-09-03 23:33:25 +02:00
doc-code
38947bbc7d Upload .zip for v.1.4.0 2024-09-03 23:32:43 +02:00
doc-code
ae3e42263e Upload legacy .zip for v1.4.0 2024-09-03 23:30:19 +02:00
doc-code
3c2e31948e Update CHANGELOG for v1.4.0 2024-09-03 23:29:57 +02:00
doc-code
bec320f707 Update legacy to v.1.4.0 (and update splash screen to match Blender <=3.6 splash screen version) 2024-09-03 23:27:22 +02:00
doc-code
600efc1ea6 Update to v1.4.0 2024-09-03 23:27:14 +02:00
doc-code
28bc4dac8f Add Workspace from Template feature, +refactor 2024-09-03 23:21:50 +02:00
079487299b Refactor README 2024-09-02 16:20:16 +02:00
b79f008d23 Update README_EXT for legacy version 2024-09-02 16:17:50 +02:00
125c9d6b91 Update README for legacy versions 2024-09-02 15:50:25 +02:00
0ab6f0b24d Create legacy version (tested on all LTS versions from 2.83 to 3.6) 2024-09-02 15:46:21 +02:00
706b86a975 Update CHANGELOG 2024-09-01 16:05:34 +02:00
2f24258c06 Upload .zip for v1.3.1 2024-09-01 16:01:00 +02:00
7f8a074ccf Update CHANGELOG to v1.3.1 2024-09-01 15:46:25 +02:00
a867ebb73d Update manifest to v1.3.1 2024-09-01 15:46:12 +02:00
2965688087 Convert relative path to absolute on selection (fix relative path with .blend file loaded) 2024-09-01 15:45:24 +02:00
doc-code
3fc8da77d2 Update README_EXT 2024-09-01 04:05:13 +02:00
doc-code
28ed1a909a Update README for v1.3.0 2024-09-01 03:49:41 +02:00
doc-code
0476c641ea Update CHANGELOG for v1.3.0 2024-09-01 03:48:45 +02:00
doc-code
2580b0aae4 Upload .zip for v1.3.0 2024-09-01 03:48:34 +02:00
doc-code
477d526443 Update preview image 2024-09-01 03:48:07 +02:00
doc-code
a7ffba8082 Update manifest to v1.3.0, update files permission reason 2024-09-01 03:47:57 +02:00
doc-code
f80b32a127 Features: add from folder, clear current templates, auto-naming; +refactor 2024-09-01 03:45:29 +02:00
doc-code
c85b88d939 Update README 2024-08-31 23:45:58 +02:00
doc-code
2fdb853b8c Add README_EXT.md (readme for extensions.blender.org) 2024-08-31 19:54:18 +02:00
doc-code
f372dd05c8 Add menu to README 2024-08-31 19:51:17 +02:00
doc-code
ae8fd3c3d2 Re-oranized and simplified README 2024-08-31 19:39:28 +02:00
doc-code
45fc4ebf78 Update README using preview image instead of featured 2024-08-31 14:03:39 +02:00
doc-code
dcc0a75c97 Update .gitignore to remove new images projects and utils files 2024-08-31 14:02:33 +02:00
doc-code
929f2f5e26 Update Featured and Preview Images 2024-08-31 14:02:10 +02:00
doc-code
090b6f7775 Update to v1.2.6 2024-08-28 19:27:09 +02:00
doc-code
439aac2702 Fix preference persistence by marking preferences as dirty, Used poll to disable operators instead of hiding them 2024-08-28 19:26:46 +02:00
doc-code
814b755bb7 Minor README update 2024-08-28 11:56:13 +02:00
doc-code
e71ab3ecba Update preferences image to match current UI 2024-08-28 11:49:41 +02:00
doc-code
119717bfbb Update CHANGELOG to v.1.2.5 2024-08-27 12:52:04 +02:00
doc-code
ad2b549ea2 Fix persistence of preferences when changing override_splash from splash 2024-08-27 12:50:41 +02:00
doc-code
eb8b4aac82 Update to v1.2.4 2024-08-26 23:09:06 +02:00
doc-code
a0203dc56a Update version to v1.2.4 2024-08-26 23:08:19 +02:00
doc-code
727b535cba Naming refactor 2024-08-26 23:07:28 +02:00
doc-code
98aa91d3e5 Update featured image 2024-08-26 16:29:35 +02:00
doc-code
6b87e447d0 Update Preview Images to reflect v1.2.3 little UI updates 2024-08-26 16:22:58 +02:00
doc-code
55ceb10701 Upload new .zip for v1.2.3 2024-08-26 16:03:13 +02:00
doc-code
a911ba7994 Update CHANGELOG explaining v1.2.3 2024-08-26 16:02:59 +02:00
doc-code
68e7b18fb2 Update .toml to 1.2.3 and add User Interface tag, as suggested 2024-08-26 16:02:40 +02:00
doc-code
06bde7a6b3 Fix splash screen issue on disabling add-on, by re-registering the original splash class, in unregister phase 2024-08-26 16:02:06 +02:00
doc-code
2992585cd5 Prefs: Removed warning message for splash screen issue, remove [-] button with no templates, Fix index access issue importing shorter list 2024-08-26 16:01:26 +02:00
doc-code
ba9e026291 Removed custom separator in favor of simple separator 2024-08-26 16:00:08 +02:00
doc-code
f6c8730062 Removed messages after switching mode for splash screen 2024-08-26 15:59:44 +02:00
fb3898c25f Update CHANGELOG to v1.2.2 2024-08-19 00:24:21 +02:00
8e7c93a829 Update to v1.2.2 2024-08-19 00:22:07 +02:00
b8289b9ac3 Add files permission for import/export feature, Update to 1.2.2 2024-08-19 00:21:56 +02:00
31fa1c2b4d Update CHAGNELOG to v.1.2.1 2024-08-18 23:03:12 +02:00
5a4a336e5a Update to 1.2.1 2024-08-18 22:59:27 +02:00
78c384c850 Remove a couple of unuseful lines 2024-08-18 22:59:13 +02:00
3dc5dca176 Update preview images 2024-08-18 21:09:35 +02:00
b79c7862b1 Update CHANGELOG for v1.2.0 2024-08-18 20:22:30 +02:00
7fb69dcf9c Build .zip for v1.2.0 2024-08-18 20:12:35 +02:00
b0ced5013a Update version to v1.2.0 2024-08-18 20:12:14 +02:00
2a60a876bf Fix 'Use current as template' bpy missing import 2024-08-18 20:11:55 +02:00
c9cb3c576b Update README for v1.2.0 with Splash Screen override 2024-08-18 20:09:41 +02:00
bfcbd2e266 Create new feature and preview images 2024-08-18 20:03:12 +02:00
2f0eaaef13 Minor refactor 2024-08-18 17:00:04 +02:00
bab5a57099 Implement custom splash screen, and updates for functions moving 2024-08-18 16:57:43 +02:00
d6fa3dd76b Update preferences to add override_splash prop 2024-08-18 16:57:06 +02:00
d3ed9be3b9 Move draw functions to file 2024-08-18 16:56:44 +02:00
a906c8b948 Implement override of WM_MT_splash (a clone, adding menu for switching between default and custom templates) 2024-08-18 16:56:33 +02:00
797e720fa0 Update Preview and Featured image 2024-08-17 23:42:12 +02:00
12d173b2ce Update README for import/export and refactor 2024-08-17 18:29:18 +02:00
2531be905c Update featured and preview images 2024-08-17 17:46:26 +02:00
2ce9613e74 Upload .zip for v1.1.0 2024-08-17 17:46:13 +02:00
db42669de1 Add label in preferences 2024-08-17 17:45:55 +02:00
ec3af34472 Update CHANGELOG for v1.1.0 2024-08-17 17:45:47 +02:00
47cd466a6d Remove manifest comments, Update version to v1.1.0 2024-08-17 17:07:53 +02:00
f7eea625fd Implement json import/export of custom templates (in preferences and File > Defaults > Import/Export) 2024-08-17 17:07:23 +02:00
8f5034fbf6 Separate classe in new file, reduce register/unregister code length 2024-08-17 16:47:09 +02:00
56daf65b80 Update README structure and add details 2024-08-17 14:57:43 +02:00
b5d9bdb9f0 Refactor README 2024-08-17 14:41:34 +02:00
ab648889dc Update the Featured Image 2024-08-17 14:40:44 +02:00
df63af65b1 Refactor CHANGELOG 2024-08-17 14:20:03 +02:00
45 changed files with 1525 additions and 325 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
*.xcf
*.blend*
/images/utils/**

View File

@ -1,22 +0,0 @@
## v1.0.2
Swich from `zip` to `blender -c extension built` for building the .zip file (now without useless /addon folder in the zip).
*This update does not impact on functionalities*
## v1.0.1
Update preferences identifier from `__name__` to `__package__` as documented [here](https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#user-preferences-and-package)
*This update does not impact on functionalities*
## v1.0.0
This is the first official version of the extension.
Current features:
- Add-on preferences with the list of custom templates (each with display name & path)
- The list of Custom Templates is added to the `File > New` menu
- The add-on functions are added to the `File > Defaults` menu, which are:
1) Manage Templates (simple link to the Custom Templates add-on preferences)
2) Select new custom template (open a popup requesting name and path and add it to the preferences
3) Use current file as template (open a popup requesting name, and using the path of the current .blend file and add it to the preferences. Note: This function is only visible when a saved .blend file is currently loaded)

107
CHANGELOG.md Normal file
View File

@ -0,0 +1,107 @@
## v1.5.0
### New Features
- New option: **`File > New > Start from...`** (opt-in from preferences)
*Select any .blend file to use as template. Optionally, you save the template in your list, on the fly.*
- Splash Screen now supports 5+ templates with a submenu
---
- Minor UI update
*Now use a single checkbox for switching templates in Splash Screen menu.*
## v1.4.0
### New Feature
- **Add workspace** from your Custom Templates
*Usually, while adding a new workspace, you can choose between the workspaces of the 5 default blender templates.*
*Now you can also add workspaces from your Custom Templates!*
Right-click one of your workspaces and look for the `Add from Custom Templates` menu.
- Minor preferences UI update
*Legacy update*
- *Update Splash Screen UI for legacy to match Blender <=3.6*
## v1.3.1
- Fix file path selector
*Always use absolute path when selecting, even when a .blend file is loaded.*
## v1.3.0
### New Features
- Add templates from folder
*Automatically add the `.blend` files from a given folder and auto-name them with the clean file name.*
*(with optional recursion depth parameter, default 1)*
- Clear current templates (with safety confirm)
*Easily remove all the current templates from the add-on preferences menu.*
- Auto-naming from the path (when adding from folder, or selecting the path before setting the name)
- Added current number of templates in the add-on preferences
- Update `File > Default` menu with new functions:
1) Clear current templates
2) Add from folder functions
## v1.2.6
- Fix preferences persistence issues on restart
- Used poll function to disable operators instead of hiding them
## v1.2.5
- Fix override splash preference persistence when updating from splash
*by marking preferences as dirty, after altering the value.*
## v1.2.3 - 1.2.4
- Fix Splash Screen issue after disabling the add-on
- Remove some `INFO` messages
- Fix index access problems in a couple of situations
- Update classes id prefix (shorter and removing build warnings for menus naming)
- Minor UI updates
## v1.2.2
Add files permission in manifest *(for import/export)*
## v1.2.1
Minor code refactor
## v1.2.0
- Implement Splash Screen 'New File' list override:
1) Add new preference for Splash Screen Override
2) Add little menu for switching between Blender's default and Custom Templates list right from the splash screen.
*The add-on will only show your first 5 templates (while `File > New` always shows all your templates)*
*If you have no custom templates, the default Blender's one will be used.*
## v1.1.0
- Implement Import/Export functionality (to and from json file)
- Re-organized source code, and reduce size where possibile
*This update does not impact on previous functionalities*
## v1.0.2
Swich from `zip` to `blender -c extension built` for building the .zip file (now without useless /addon folder in the zip).
*This update does not impact on functionalities*
## v1.0.1
Update preferences identifier from `__name__` to `__package__` as documented [here](https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#user-preferences-and-package)
*This update does not impact on functionalities*
## v1.0.0
This is the first official version of the extension.
Current features:
- Add-on preferences with the list of custom templates (each with display name & path)
- The list of Custom Templates is added to the `File > New` menu
- The add-on functions are added to the `File > Defaults` menu, which are:
1) Manage Templates (simple link to the Custom Templates add-on preferences)
2) Select new custom template (open a popup requesting name and path and add it to the preferences
3) Use current file as template (open a popup requesting name, and using the path of the current .blend file and add it to the preferences. Note: This function is only visible when a saved .blend file is currently loaded)

View File

@ -1,51 +1,28 @@
# Custom Templates - Add-On
# Custom Templates
### Have you ever wanted to customize new projects?
## The Freedom to *Template*
Custom Templates Add-On allows you to use your own .blend files as template options for new projects.
**Custom Templates** allows you to use your own .blend files as template options for new projects.
![File > New menu with the add-on custom buttons](./images/Preview%20Image.png)
---
### Why this add-on?
- [Getting Started](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki/Getting-Started)
- [Why this add-on?](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki/Why+this+add-on%3F.-)
- [Support](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki/Support)
- [Changelog](./CHANGELOG.md)
- [Copyright](#copyright)
Blender allows you to save you own .blend file to use on startup.
---
This is great, but wouldn't it be nice to be able to create your own templates?
![File > New menu with the add-on custom buttons](./images/Preview%20Image%20-%20v2.png)
With **Custom Templates** you can setup as many .blend file as you like, as templates.
Your templates will be avaliable in the Splash Screen and the `File > New` menu.
### Preferences
### Changelog
In the add-on preferences, you can manage the list of your custom template files.
You can add, remove and reorder templates.
Each template has it's Name, and Path to the .blend file.
![Custom Templates Preferences](./images/Addons_Preferences.png)
*Note that, as of now, using wrong file paths will NOT show any warning and you will get an error while opening the template.*
*Be sure to use only valid .blend files as path.*
### How to use
You can always open the add-on preferences using the button `File > Defaults > Manage templates`.
Instead of using preferences, you can select a new template file using `File > Defaults > Select new custom template`, or, if you have a .blend file open, you can use `File > Defaults > Use current as new template`.
*This functions will open a popup where you can also set the Name for the new template*
![File > Defaults menu with the add-on custom buttons](./images/File__Defaults.png)
### Support
As of today, the add-on has been only tested on Blender 4.2.0.
*It may or may not work on previous version.*
Visit the [CHANGELOG](./CHANGELOG.md) file to see the changes over new version.
### Copyright
This add-on is developed under GPL-3.0 license by Francesco Bellini from 2024.
*See the [LICENSE](./LICENSE) file.*
#### Changelog
Visit the [CHANGELOG](./CHANGELOG) file to se the changes over new version.

53
README_EXT.md Normal file
View File

@ -0,0 +1,53 @@
# The Freedom to *Template*
Blender allows you to save you own .blend file to use on startup.
This is great, but wouldn't it be nice to be able to create **your own templates**?
**Custom Templates** allows you to use your own .blend files as template options for new projects.
> Your templates will be added in the `File > New` menu and in the Splash Screen *(according to preferences)*.
*You can follow this [steps to get started](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki/Getting-Started).*
---
*Instead of being limited to the usual options Blender offers to start a new project (General, 2D Animation, Sculpting, VFX, Video Editing), you will be able to configure your own list of .blend files.*
---
Create your own templates or find the ones that best suit your needs online.
*Customize workspaces, preset your usual materials, define additional windows or anything else you would like to have from the beginning.*
*The only limit is your immagination!*
---
#### Features
- Show your templates in `File > New` menu and in the Splash Screen
- Option `File > New > Start from...`:
Pick any .blend file as template (optional)
*While selecting, you can optionally add the template.*
*Activate this option from add-on preferences.*
- Import/Export templates from/to JSON file
- Add, remove, update, reorder your templates from add-on preferences
- Add templates from folder (lookup `.blend` files in folder)
- Select new template
- Use current file as template
- Clear current templates
- Add workspace from templates *(right-click on a workspace and use `Add from Custom Templates` menu)*
##### Legacy Support
For previous Blender versions, you can install the [legacy version](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki/Support#legacy-version).
*Tested on all LTS versions from 2.83 to 3.6 (remember to use compatible templates).*
---
*For more details checkout the project's [Wiki](https://projects.blender.org/Francesco-Bellini/custom_templates_addon/wiki).*
---
#### Make your new projects easier from the start

View File

@ -13,267 +13,65 @@
# 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
from bpy.types import TOPBAR_MT_file_new, TOPBAR_MT_file_defaults, TOPBAR_MT_workspace_menu
from .src.funcs import draw_file_new_templates, draw_file_default_operators, draw_ws_menu_add
from .src.splash import WM_MT_splash
from .src.menus import CT_MT_splash_mode, CT_MT_templates_menu, CT_MT_workspace_add, CT_MT_more_templates
from .src.ops import CT_OT_export_templates, CT_OT_start_from, CT_OT_import_templates, 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, CT_OT_template_workspaces, CT_OT_add_workspace
from .src.prefs import CustomTemplatesPreferences, TemplateItem
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",
"location": "File > New, File > Defaults, Splash Screen",
"category": "System",
"support": "COMMUNITY",
"blender_manifest": "blender_manifest.toml"
}
classes = [WM_MT_splash,
CT_MT_workspace_add,
CT_MT_splash_mode,
CT_MT_templates_menu,
CT_MT_more_templates,
CT_OT_add_workspace,
CT_OT_start_from,
CT_OT_template_workspaces,
CT_OT_export_templates,
CT_OT_import_templates,
CT_OT_open_preferences,
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,
TemplateItem,
CustomTemplatesPreferences]
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")
og_splash = None
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)
global og_splash
og_splash = bpy.types.WM_MT_splash
for c in classes:
bpy.utils.register_class(c)
TOPBAR_MT_file_new.append(draw_file_new_templates)
TOPBAR_MT_file_defaults.append(draw_file_default_operators)
TOPBAR_MT_workspace_menu.append(draw_ws_menu_add)
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)
for c in reversed(classes):
bpy.utils.unregister_class(c)
bpy.utils.register_class(og_splash)
TOPBAR_MT_file_new.remove(draw_file_new_templates)
TOPBAR_MT_file_defaults.remove(draw_file_default_operators)
TOPBAR_MT_workspace_menu.remove(draw_ws_menu_add)
if __name__ == "__main__":
register()
registers()

View File

@ -1,27 +1,14 @@
schema_version = "1.0.0"
id = "custom_templates"
version = "1.0.2"
version = "1.5.0"
name = "Custom Templates"
tagline = "Use your own .blend files as template options for new projects"
maintainer = "Francesco Bellini <doc.open.dev@gmail.com>"
type = "add-on"
# Repository on projects.blender.org
website = "https://projects.blender.org/Francesco-Bellini/custom_templates_addon"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
tags = ["System"]
tags = ["System", "User Interface"]
blender_version_min = "4.2.0"
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
license = [
"SPDX:GPL-3.0-or-later",
]
# Optional: required by some licenses.
copyright = [
"2024 Francesco Bellini",
]
license = ["SPDX:GPL-3.0-or-later"]
copyright = ["2024 Francesco Bellini"]
[permissions]
files = "JSON Import/Export of templates list / Add templates from folder"

77
addon/src/funcs.py Normal file
View File

@ -0,0 +1,77 @@
import os
import bpy
from .. import __package__ as base_package
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
def pref():
return bpy.context.preferences.addons[base_package].preferences
def has_templates():
return len(pref().projects) > 0
def draw_file_new_templates(self, context):
layout = self.layout
if has_templates():
layout.separator()
draw_templates(layout, context)
def draw_templates(layout, context, splash_mode=False):
prefs = pref()
for i in range(min(5 if len(prefs.projects) <= 5 else 4, 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
if splash_mode and len(prefs.projects) > 5:
layout.menu("CT_MT_more_templates", text="More...", icon="FILE_NEW")
if not splash_mode and prefs.start_from and context.area.type == 'TOPBAR':
layout.separator()
layout.operator("ct.start_from", icon="FILE_FOLDER")
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")
def draw_ws_menu_add(self, context):
layout = self.layout
if has_templates():
layout.separator()
layout.menu("CT_MT_workspace_add", text="Add from Custom Templates", icon="WORKSPACE")

60
addon/src/menus.py Normal file
View File

@ -0,0 +1,60 @@
from .. import __package__ as base_package
import os
import bpy
import json
from bpy.types import Menu
from .funcs import pref, name_from_path
class CT_MT_templates_menu(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_MT_splash_mode(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 = pref()
layout.operator("ct.open_preferences", text="Manage templates")
layout.separator()
layout.prop(prefs, "override_splash")
class CT_MT_more_templates(Menu):
bl_idname = "CT_MT_more_templates"
bl_label = "Other Custom Templates"
bl_description = "Show the other templates in the splash screen"
def draw(self, context):
layout = self.layout
prefs = pref()
for t in prefs.projects[4:]:
layout.operator(
"wm.read_homefile", text=(t.name if t.name else name_from_path(t.path)), icon="FILE_NEW").filepath = t.path
class CT_MT_workspace_add(Menu):
bl_label = "Add workspace from Custom Templates"
def draw(self, context):
layout = self.layout
templates = pref().projects
layout.label(text="Add workspace from Custom Templates", icon="ADD")
layout.separator()
for i in range(len(templates)):
t = templates[i]
layout.operator("CT_OT_template_workspaces", text=t.name).index = i

328
addon/src/ops.py Normal file
View File

@ -0,0 +1,328 @@
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'}

60
addon/src/prefs.py Normal file
View File

@ -0,0 +1,60 @@
import bpy
from bpy.types import PropertyGroup, AddonPreferences
from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty
from .funcs import on_path_update
from .. import __package__ as base_package
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="Use Custom Templates", description="Override Splash Screen's 'New File' list with your Custom Templates")
start_from: BoolProperty(
default=False, name="Add option: File > New > Start from...", description="Show an option in File > New menu to start a new project from any .blend file")
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")
b = layout.box()
b.label(text="Splash Screen", icon="SETTINGS")
b.prop(self, "override_splash")
box = b.box()
if self.override_splash and self.projects:
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.", icon=("ERROR" if not self.projects else "NONE"))
b = layout.box()
b.label(text="Extra", icon="SETTINGS")
b.prop(self, "start_from")

73
addon/src/splash.py Normal file
View File

@ -0,0 +1,73 @@
# 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
import bpy
from .funcs import draw_templates, pref
from .. import __package__ as base_package
# 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 = pref()
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:
# Call original code
colA.label(text="New File")
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()

27
dev/TEST_WORKFLOW.md Normal file
View File

@ -0,0 +1,27 @@
# Workflow for testing new versions
- Install new version, and ensure no errors/previous setting preserved
- Disable/Re-enable add-on for preferences reset
- Ensure `File > New`/Splash Screen empty
- Ensure operators are disabled (if they need templates)
- Add single template from prefs (select path, name should auto complete from filename)
- Ensure operators are enabled (if they need templates)
- Add from folder (the same of the previous template, but with others)
*The folder templates (with optional nested levels) should be added, excluding aready present ones.*
- Ensure `File > New`/Splash Screen match
- Ensure `File > New`/Splash Screen works opening
- Test the Splash Screen Switch (optionally checking preference persistence across restart)
- Export the list to JSON file
- Clear all templates (from settings)
- Import the exported file
- Ensure template list match previous
- Test reordering/removing
- Enable Extra option `File > New > Start From...`
- Ensure it works (optionally checking add templates)
- Ensure it doesn't show up with ctrl + N (which would not work properly because of context)
- Test `File > Defaults > Select new template`
- Test `File > Defaults > Use current as template`
- Test `File > Defaults > Manage templates`/same as Splash Screen
- Test `Workspace menu > Add from Custom Templates` feature
- Ensure `File > New`/Splash Screen match
- Nice, version ready!

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

78
legacy/__init__.py Normal file
View File

@ -0,0 +1,78 @@
# 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 bpy
from bpy.types import TOPBAR_MT_file_new, TOPBAR_MT_file_defaults, TOPBAR_MT_workspace_menu
from .src.funcs import draw_file_new_templates, draw_file_default_operators, draw_ws_menu_add
from .src.splash import WM_MT_splash
from .src.menus import CT_MT_splash_mode, CT_MT_templates_menu, CT_MT_workspace_add, CT_MT_more_templates
from .src.ops import CT_OT_export_templates, CT_OT_start_from, CT_OT_import_templates, 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, CT_OT_template_workspaces, CT_OT_add_workspace
from .src.prefs import CustomTemplatesPreferences, TemplateItem
bl_info = {
"id": "custom_templates",
"name": "Custom Templates",
"tagline": "Add your own .blend files as template options for new projects",
"version": (1, 5, 0),
"blender": (2, 83, 0),
"location": "File > New, File > Defaults, Splash Screen",
"category": "System",
"support": "COMMUNITY",
"blender_manifest": "blender_manifest.toml"
}
classes = [WM_MT_splash,
CT_MT_workspace_add,
CT_MT_splash_mode,
CT_MT_templates_menu,
CT_MT_more_templates,
CT_OT_add_workspace,
CT_OT_start_from,
CT_OT_template_workspaces,
CT_OT_export_templates,
CT_OT_import_templates,
CT_OT_open_preferences,
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,
TemplateItem,
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)
TOPBAR_MT_file_new.append(draw_file_new_templates)
TOPBAR_MT_file_defaults.append(draw_file_default_operators)
TOPBAR_MT_workspace_menu.append(draw_ws_menu_add)
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)
bpy.utils.register_class(og_splash)
TOPBAR_MT_file_new.remove(draw_file_new_templates)
TOPBAR_MT_file_defaults.remove(draw_file_default_operators)
TOPBAR_MT_workspace_menu.remove(draw_ws_menu_add)
if __name__ == "__main__":
registers()

77
legacy/src/funcs.py Normal file
View File

@ -0,0 +1,77 @@
import os
import bpy
from .. import __name__ as base_package
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
def pref():
return bpy.context.preferences.addons[base_package].preferences
def has_templates():
return len(pref().projects) > 0
def draw_file_new_templates(self, context):
layout = self.layout
if has_templates():
layout.separator()
draw_templates(layout, context)
def draw_templates(layout, context, splash_mode=False):
prefs = pref()
for i in range(min(5 if len(prefs.projects) <= 5 else 4, 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
if splash_mode and len(prefs.projects) > 5:
layout.menu("CT_MT_more_templates", text="More...", icon="FILE_NEW")
if not splash_mode and prefs.start_from and context.area.type == 'TOPBAR':
layout.separator()
layout.operator("ct.start_from", icon="FILE_FOLDER")
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")
def draw_ws_menu_add(self, context):
layout = self.layout
if has_templates():
layout.separator()
layout.menu("CT_MT_workspace_add", text="Add from Custom Templates", icon="WORKSPACE")

60
legacy/src/menus.py Normal file
View File

@ -0,0 +1,60 @@
from .. import __name__ as base_package
import os
import bpy
import json
from bpy.types import Menu
from .funcs import pref, name_from_path
class CT_MT_templates_menu(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_MT_splash_mode(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 = pref()
layout.operator("ct.open_preferences", text="Manage templates")
layout.separator()
layout.prop(prefs, "override_splash")
class CT_MT_more_templates(Menu):
bl_idname = "CT_MT_more_templates"
bl_label = "Other Custom Templates"
bl_description = "Show the other templates in the splash screen"
def draw(self, context):
layout = self.layout
prefs = pref()
for t in prefs.projects[4:]:
layout.operator(
"wm.read_homefile", text=(t.name if t.name else name_from_path(t.path)), icon="FILE_NEW").filepath = t.path
class CT_MT_workspace_add(Menu):
bl_label = "Add workspace from Custom Templates"
def draw(self, context):
layout = self.layout
templates = pref().projects
layout.label(text="Add workspace from Custom Templates", icon="ADD")
layout.separator()
for i in range(len(templates)):
t = templates[i]
layout.operator("CT_OT_template_workspaces", text=t.name).index = i

328
legacy/src/ops.py Normal file
View File

@ -0,0 +1,328 @@
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 __name__ 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'}

60
legacy/src/prefs.py Normal file
View File

@ -0,0 +1,60 @@
import bpy
from bpy.types import PropertyGroup, AddonPreferences
from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty
from .funcs import on_path_update
from .. import __name__ as base_package
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="Use Custom Templates", description="Override Splash Screen's 'New File' list with your Custom Templates")
start_from: BoolProperty(
default=False, name="Add option: File > New > Start from...", description="Show an option in File > New menu to start a new project from any .blend file")
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")
b = layout.box()
b.label(text="Splash Screen", icon="SETTINGS")
b.prop(self, "override_splash")
box = b.box()
if self.override_splash and self.projects:
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.", icon=("ERROR" if not self.projects else "NONE"))
b = layout.box()
b.label(text="Extra", icon="SETTINGS")
b.prop(self, "start_from")

69
legacy/src/splash.py Normal file
View File

@ -0,0 +1,69 @@
# 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
import bpy
from .funcs import draw_templates, pref
from .. import __name__ as base_package
# 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
colx = split.column()
ct_split = colx.split(factor=0.9)
colA = ct_split.column()
if prefs.override_splash and len(prefs.projects) > 0:
colA.label(text="Custom Templates")
colx.operator_context = 'INVOKE_DEFAULT'
draw_templates(colx, context, True)
else:
# Call original code
colA.label(text="New File")
bpy.types.TOPBAR_MT_file_new.draw_ex(colx, 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_preset", text="Blender Website", icon='URL').type = 'BLENDER'
col2.operator("wm.url_open_preset", text="Credits", icon='URL').type = 'CREDITS'
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="Release Notes", icon='URL').type = 'RELEASE_NOTES'
col2.operator("wm.url_open_preset", text="Development Fund", icon='FUND').type = 'FUND'
layout.separator()
layout.separator()

1
release-legacy.sh Normal file
View File

@ -0,0 +1 @@
zip -9 -r ./releases/1.x.x/legacy/custom-templates-legacy-1.5.0.zip ./legacy/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.