Skip to content

Instantly share code, notes, and snippets.

@Shaun-Fong
Created March 12, 2025 09:02
Show Gist options
  • Select an option

  • Save Shaun-Fong/9489473c04f52132f7d0ee81f0476df1 to your computer and use it in GitHub Desktop.

Select an option

Save Shaun-Fong/9489473c04f52132f7d0ee81f0476df1 to your computer and use it in GitHub Desktop.
Blender Action Manager
import bpy
import os
bl_info = {
"name": "Action Manager",
"author": "ShaunFong",
"version": (1, 0, 0),
"location": "View3D > UI > Action Manager",
"description": "Manage and append actions from external .blend files and apply them to selected armatures.",
"category": "Animation",
}
class ActionNameProperty(bpy.types.PropertyGroup): # Define a PropertyGroup for Action Names
name: bpy.props.StringProperty(name="Action Name")
class BlendFileData(bpy.types.PropertyGroup): # Define a custom PropertyGroup for Blend File Data
blend_file_name: bpy.props.StringProperty(name="Blend File Name")
actions: bpy.props.CollectionProperty(type=ActionNameProperty, name="Actions") # Store actions as CollectionProperty of ActionNameProperty
is_expanded: bpy.props.BoolProperty(name="Expanded", default=False) # Add is_expanded property to BlendFileData
class ACTIONMANAGER_PT_MainPanel(bpy.types.Panel):
bl_label = "Action Manager"
bl_idname = "ACTIONMANAGER_PT_main"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Action Manager"
def draw(self, context):
layout = self.layout
scene = context.scene
# Folder input (auto-loads actions when changed)
layout.prop(scene, "action_manager_folder", text="Folder")
layout.operator("actionmanager.refresh_actions", text="Refresh Actions", icon='FILE_REFRESH')
actions_by_blendfile = scene.action_manager_actions_data # Get action data
if not actions_by_blendfile:
layout.label(text="No actions loaded.", icon='INFO')
return
for blend_file_data in actions_by_blendfile:
box = layout.box()
row = box.row(align=True)
row.prop(blend_file_data, 'is_expanded', text="", emboss=True, icon_only=False, expand=False)
row.label(text=blend_file_data.blend_file_name, icon='FILE_BLEND') # Access property directly
open_blend_op = row.operator("actionmanager.open_blend_file", text="", icon='FILE_FOLDER', emboss=True)
open_blend_op.blend_file_name = blend_file_data.blend_file_name # Access property directly
if blend_file_data.is_expanded: # Access property directly
col = box.column(align=True)
for action_name_prop in blend_file_data.actions: # Iterate through CollectionProperty of ActionNameProperty
action_name = action_name_prop.name # Access StringProperty's name
row = col.row(align=True)
row.label(text=action_name, icon='ACTION')
op = row.operator("actionmanager.append_action", text="Load", icon='IMPORT', emboss=True)
op.action_name = action_name
op.blend_file_name = blend_file_data.blend_file_name # Access property directly
class ACTIONMANAGER_OT_LoadActions(bpy.types.Operator):
bl_idname = "actionmanager.load_actions"
bl_label = "Load Actions"
def execute(self, context):
scene = context.scene
folder_path = scene.action_manager_folder
if not folder_path or not os.path.isdir(folder_path):
scene.action_manager_folder = ""
scene.action_manager_actions_data.clear()
self.report({'WARNING'}, "Invalid folder path. Actions list cleared.")
return {'CANCELLED'}
scene.action_manager_actions_data.clear()
actions_found = False
blend_files_data = []
for root, _, files in os.walk(folder_path):
for file_name in files:
if file_name.endswith(".blend"):
file_path = os.path.join(root, file_name)
blend_file_base_name = os.path.splitext(file_name)[0]
blend_file_actions_names = []
with bpy.data.libraries.load(file_path, link=False) as (data_from, data_to):
if not data_from.actions:
continue
for action_name in data_from.actions:
blend_file_actions_names.append(action_name)
actions_found = True
if blend_file_actions_names:
new_blend_file_data = scene.action_manager_actions_data.add()
new_blend_file_data.blend_file_name = blend_file_base_name
for action_name in blend_file_actions_names: # Populate actions CollectionProperty correctly
action_prop = new_blend_file_data.actions.add() # Add ActionNameProperty item
action_prop.name = action_name # Set the name property of ActionNameProperty
new_blend_file_data.is_expanded = False
if not actions_found:
self.report({'WARNING'}, "No actions found in selected folder.")
return {'CANCELLED'}
return {'FINISHED'}
class ACTIONMANAGER_OT_AppendAction(bpy.types.Operator):
bl_idname = "actionmanager.append_action"
bl_label = "Append and Apply Action"
action_name: bpy.props.StringProperty(name="Action Name")
blend_file_name: bpy.props.StringProperty(name="Blend File Name")
def execute(self, context):
blend_file_path = ""
folder_path = context.scene.action_manager_folder
blend_file_name = self.blend_file_name
action_name = self.action_name
for root, _, files in os.walk(folder_path):
for file_name in files:
if file_name == blend_file_name + ".blend":
blend_file_path = os.path.join(root, file_name)
break
if blend_file_path:
break
if not blend_file_path:
self.report({'ERROR'}, f"Blend file '{blend_file_name}.blend' not found in folder.")
return {'CANCELLED'}
try:
with bpy.data.libraries.load(blend_file_path, link=False) as (data_from, data_to):
if action_name in data_from.actions:
data_to.actions = [action_name]
else:
self.report({'ERROR'}, f"Action '{action_name}' not found in {blend_file_name}.blend")
return {'CANCELLED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
obj = context.object
if obj and obj.type == 'ARMATURE':
obj.animation_data_create()
obj.animation_data.action = bpy.data.actions.get(action_name)
self.report({'INFO'}, f"Applied action: {action_name} from {blend_file_name} to {obj.name}")
else:
self.report({'WARNING'}, "No armature selected. Action appended but not applied.")
return {'FINISHED'}
class ACTIONMANAGER_OT_RefreshActions(bpy.types.Operator):
bl_idname = "actionmanager.refresh_actions"
bl_label = "Refresh Actions"
def execute(self, context):
scene = context.scene
scene.action_manager_actions_data.clear()
load_result = bpy.ops.actionmanager.load_actions()
if load_result == {'CANCELLED'}:
self.report({'WARNING'}, "No actions found in selected folder.")
else:
self.report({'INFO'}, "Action list refreshed.")
return {'FINISHED'}
class ACTIONMANAGER_OT_OpenBlendFile(bpy.types.Operator):
bl_idname = "actionmanager.open_blend_file"
bl_label = "Open Blend File"
blend_file_name: bpy.props.StringProperty(name="Blend File Name")
def execute(self, context):
folder_path = context.scene.action_manager_folder
blend_file_name = self.blend_file_name
blend_file_path = ""
if not folder_path or not os.path.isdir(folder_path):
self.report({'WARNING'}, "Invalid folder path set in Action Manager.")
return {'CANCELLED'}
for root, _, files in os.walk(folder_path):
for file_name in files:
if file_name == blend_file_name + ".blend":
blend_file_path = os.path.join(root, file_name)
break
if blend_file_path:
break
if blend_file_path:
try:
bpy.ops.wm.open_mainfile(filepath=blend_file_path)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Could not open blend file: {blend_file_name}.blend\n{str(e)}")
return {'CANCELLED'}
else:
self.report({'WARNING'}, f"Blend file '{blend_file_name}.blend' not found in folder.")
return {'CANCELLED'}
def register():
# Removed registration of ActionListItem and ACTIONMANAGER_UL_ActionList
bpy.utils.register_class(ActionNameProperty) # Register ActionNameProperty
bpy.utils.register_class(BlendFileData) # Register BlendFileData PropertyGroup
bpy.utils.register_class(ACTIONMANAGER_PT_MainPanel)
bpy.utils.register_class(ACTIONMANAGER_OT_LoadActions)
bpy.utils.register_class(ACTIONMANAGER_OT_AppendAction)
bpy.utils.register_class(ACTIONMANAGER_OT_RefreshActions)
bpy.utils.register_class(ACTIONMANAGER_OT_OpenBlendFile)
bpy.types.Scene.action_manager_folder = bpy.props.StringProperty(
name="Folder Path", subtype='DIR_PATH',
update=lambda self, context: bpy.ops.actionmanager.load_actions()
)
# Changed to a CollectionProperty of BlendFileData PropertyGroup
bpy.types.Scene.action_manager_actions_data = bpy.props.CollectionProperty(type=BlendFileData, name="Action Data")
def unregister():
bpy.utils.unregister_class(ActionNameProperty) # Unregister ActionNameProperty
bpy.utils.unregister_class(BlendFileData) # Unregister BlendFileData PropertyGroup
bpy.utils.unregister_class(ACTIONMANAGER_PT_MainPanel)
bpy.utils.unregister_class(ACTIONMANAGER_OT_LoadActions)
bpy.utils.unregister_class(ACTIONMANAGER_OT_AppendAction)
bpy.utils.unregister_class(ACTIONMANAGER_OT_RefreshActions)
bpy.utils.unregister_class(ACTIONMANAGER_OT_OpenBlendFile)
del bpy.types.Scene.action_manager_folder
del bpy.types.Scene.action_manager_actions_data
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment