# SPDX-License-Identifier: GPL-2.0-or-later """ Pose Library - GUI definition. """ import bpy from bpy.types import ( AssetHandle, Context, Panel, UIList, WindowManager, WorkSpace, ) from bpy_extras import asset_utils class PoseLibraryPanel: @classmethod def pose_library_panel_poll(cls, context: Context) -> bool: return bool( context.object and context.object.mode == 'POSE' ) @classmethod def poll(cls, context: Context) -> bool: return cls.pose_library_panel_poll(context); class VIEW3D_PT_pose_library(PoseLibraryPanel, Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Animation" bl_label = "Pose Library" def draw(self, context: Context) -> None: layout = self.layout row = layout.row(align=True) row.operator("poselib.create_pose_asset").activate_new_action = False if bpy.types.POSELIB_OT_restore_previous_action.poll(context): row.operator("poselib.restore_previous_action", text="", icon='LOOP_BACK') row.operator("poselib.copy_as_asset", icon="COPYDOWN", text="") wm = context.window_manager layout.prop(wm, "poselib_flipped") if hasattr(layout, "template_asset_view"): workspace = context.workspace activate_op_props, drag_op_props = layout.template_asset_view( "pose_assets", workspace, "asset_library_ref", wm, "pose_assets", workspace, "active_pose_asset_index", filter_id_types={"filter_action"}, activate_operator="poselib.apply_pose_asset", drag_operator="poselib.blend_pose_asset", ) drag_op_props.release_confirm = True drag_op_props.flipped = wm.poselib_flipped activate_op_props.flipped = wm.poselib_flipped def pose_library_list_item_context_menu(self: UIList, context: Context) -> None: def is_pose_asset_view() -> bool: # Important: Must check context first, or the menu is added for every kind of list. list = getattr(context, "ui_list", None) if not list or list.bl_idname != "UI_UL_asset_view" or list.list_id != "pose_assets": return False if not context.asset_handle: return False return True def is_pose_library_asset_browser() -> bool: asset_library_ref = getattr(context, "asset_library_ref", None) if not asset_library_ref: return False asset = getattr(context, "asset_file_handle", None) if not asset: return False return bool(asset.id_type == 'ACTION') if not is_pose_asset_view() and not is_pose_library_asset_browser(): return layout = self.layout wm = context.window_manager layout.separator() layout.operator("poselib.apply_pose_asset", text="Apply Pose") old_op_ctx = layout.operator_context layout.operator_context = 'INVOKE_DEFAULT' props = layout.operator("poselib.blend_pose_asset", text="Blend Pose") props.flipped = wm.poselib_flipped layout.operator_context = old_op_ctx props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones") props.select = True props = layout.operator("poselib.pose_asset_select_bones", text="Deselect Pose Bones") props.select = False layout.separator() if is_pose_asset_view(): layout.operator("asset.open_containing_blend_file") class ASSETBROWSER_PT_pose_library_usage(PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel): bl_region_type = "TOOLS" bl_label = "Pose Library" asset_categories = {'ANIMATIONS'} @classmethod def poll(cls, context: Context) -> bool: return ( cls.pose_library_panel_poll(context) and cls.asset_browser_panel_poll(context) ) def draw(self, context: Context) -> None: layout = self.layout wm = context.window_manager col = layout.column(align=True) col.prop(wm, "poselib_flipped") props = col.operator("poselib.apply_pose_asset", text="Apply") props.flipped = wm.poselib_flipped props = col.operator("poselib.blend_pose_asset", text="Interactive Blend") props.flipped = wm.poselib_flipped row = col.row(align=True) props = row.operator("poselib.pose_asset_select_bones", text="Select", icon="BONE_DATA") props.flipped = wm.poselib_flipped props.select = True props = row.operator("poselib.pose_asset_select_bones", text="Deselect") props.flipped = wm.poselib_flipped props.select = False class ASSETBROWSER_PT_pose_library_editing(PoseLibraryPanel, asset_utils.AssetBrowserPanel, Panel): bl_region_type = "TOOL_PROPS" bl_label = "Pose Library" asset_categories = {'ANIMATIONS'} @classmethod def poll(cls, context: Context) -> bool: return ( cls.pose_library_panel_poll(context) and cls.asset_browser_panel_poll(context) ) def draw(self, context: Context) -> None: layout = self.layout col = layout.column(align=True) col.enabled = bpy.types.ASSET_OT_assign_action.poll(context) col.label(text="Activate & Edit") col.operator("asset.assign_action") # Creation col = layout.column(align=True) col.enabled = bpy.types.POSELIB_OT_paste_asset.poll(context) col.label(text="Create Pose Asset") col.operator("poselib.paste_asset", icon="PASTEDOWN") class DOPESHEET_PT_asset_panel(PoseLibraryPanel, Panel): bl_space_type = "DOPESHEET_EDITOR" bl_region_type = "UI" bl_label = "Create Pose Asset" bl_category = "Pose Library" def draw(self, context: Context) -> None: layout = self.layout col = layout.column(align=True) row = col.row(align=True) row.operator("poselib.create_pose_asset").activate_new_action = True if bpy.types.POSELIB_OT_restore_previous_action.poll(context): row.operator("poselib.restore_previous_action", text="", icon='LOOP_BACK') col.operator("poselib.copy_as_asset", icon="COPYDOWN") layout.operator("poselib.convert_old_poselib") ### Messagebus subscription to monitor asset library changes. _msgbus_owner = object() def _on_asset_library_changed() -> None: """Update areas when a different asset library is selected.""" refresh_area_types = {'DOPESHEET_EDITOR', 'VIEW_3D'} for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type not in refresh_area_types: continue area.tag_redraw() def register_message_bus() -> None: bpy.msgbus.subscribe_rna( key=(bpy.types.FileAssetSelectParams, "asset_library_ref"), owner=_msgbus_owner, args=(), notify=_on_asset_library_changed, options={'PERSISTENT'}, ) def unregister_message_bus() -> None: bpy.msgbus.clear_by_owner(_msgbus_owner) @bpy.app.handlers.persistent def _on_blendfile_load_pre(none, other_none) -> None: # The parameters are required, but both are None. unregister_message_bus() @bpy.app.handlers.persistent def _on_blendfile_load_post(none, other_none) -> None: # The parameters are required, but both are None. register_message_bus() classes = ( ASSETBROWSER_PT_pose_library_editing, ASSETBROWSER_PT_pose_library_usage, DOPESHEET_PT_asset_panel, VIEW3D_PT_pose_library, ) _register, _unregister = bpy.utils.register_classes_factory(classes) def register() -> None: _register() WorkSpace.active_pose_asset_index = bpy.props.IntProperty( name="Active Pose Asset", # TODO explain which list the index belongs to, or how it can be used to get the pose. description="Per workspace index of the active pose asset" ) # Register for window-manager. This is a global property that shouldn't be # written to files. WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle) bpy.types.UI_MT_list_item_context_menu.prepend(pose_library_list_item_context_menu) bpy.types.ASSETBROWSER_MT_context_menu.prepend(pose_library_list_item_context_menu) register_message_bus() bpy.app.handlers.load_pre.append(_on_blendfile_load_pre) bpy.app.handlers.load_post.append(_on_blendfile_load_post) def unregister() -> None: _unregister() unregister_message_bus() del WorkSpace.active_pose_asset_index del WindowManager.pose_assets bpy.types.UI_MT_list_item_context_menu.remove(pose_library_list_item_context_menu) bpy.types.ASSETBROWSER_MT_context_menu.remove(pose_library_list_item_context_menu)