Fix #105009: AnimAll: Error when inserting key on string attribute
[blender-addons.git] / pose_library / gui.py
bloba12340fa7791bfd731e08801a2d15867eca85f6b
1 # SPDX-FileCopyrightText: 2021-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 Pose Library - GUI definition.
7 """
9 import bpy
10 from bpy.types import (
11 AssetHandle,
12 AssetRepresentation,
13 Context,
14 Menu,
15 Panel,
16 UILayout,
17 UIList,
18 WindowManager,
19 WorkSpace,
21 from bl_ui_utils.layout import operator_context
24 class PoseLibraryPanel:
25 @classmethod
26 def pose_library_panel_poll(cls, context: Context) -> bool:
27 return context.mode == 'POSE'
29 @classmethod
30 def poll(cls, context: Context) -> bool:
31 return cls.pose_library_panel_poll(context)
34 class VIEW3D_AST_pose_library(bpy.types.AssetShelf):
35 bl_space_type = "VIEW_3D"
36 # We have own keymap items to add custom drag behavior (pose blending), disable the default
37 # asset dragging.
38 bl_options = {'NO_ASSET_DRAG'}
40 @classmethod
41 def poll(cls, context: Context) -> bool:
42 return PoseLibraryPanel.poll(context)
44 @classmethod
45 def asset_poll(cls, asset: AssetRepresentation) -> bool:
46 return asset.id_type == 'ACTION'
48 @classmethod
49 def draw_context_menu(cls, _context: Context, _asset: AssetRepresentation, layout: UILayout):
50 # Make sure these operator properties match those used in `VIEW3D_PT_pose_library_legacy`.
51 layout.operator("poselib.apply_pose_asset", text="Apply Pose").flipped = False
52 layout.operator("poselib.apply_pose_asset", text="Apply Pose Flipped").flipped = True
54 with operator_context(layout, 'INVOKE_DEFAULT'):
55 layout.operator("poselib.blend_pose_asset", text="Blend Pose")
57 layout.separator()
58 props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones")
59 props.select = True
60 props = layout.operator("poselib.pose_asset_select_bones", text="Deselect Pose Bones")
61 props.select = False
63 layout.separator()
64 layout.operator("asset.open_containing_blend_file")
67 class VIEW3D_PT_pose_library_legacy(PoseLibraryPanel, Panel):
68 bl_space_type = "VIEW_3D"
69 bl_region_type = "UI"
70 bl_category = "Animation"
71 bl_label = "Pose Library"
73 def draw(self, _context: Context) -> None:
74 layout = self.layout
75 layout.label(text="The pose library moved.", icon='INFO')
76 sub = layout.column(align=True)
77 sub.label(text="Pose assets are now available")
78 sub.label(text="in the asset shelf.")
79 layout.operator("screen.region_toggle", text="Toggle Asset Shelf").region_type = 'ASSET_SHELF'
82 def pose_library_list_item_context_menu(self: UIList, context: Context) -> None:
83 def is_pose_asset_view() -> bool:
84 # Important: Must check context first, or the menu is added for every kind of list.
85 list = getattr(context, "ui_list", None)
86 if not list or list.bl_idname != "UI_UL_asset_view" or list.list_id != "pose_assets":
87 return False
88 if not context.active_file:
89 return False
90 return True
92 def is_pose_library_asset_browser() -> bool:
93 asset_library_ref = getattr(context, "asset_library_reference", None)
94 if not asset_library_ref:
95 return False
96 asset = getattr(context, "asset", None)
97 if not asset:
98 return False
99 return bool(asset.id_type == 'ACTION')
101 if not is_pose_asset_view() and not is_pose_library_asset_browser():
102 return
104 layout = self.layout
106 layout.separator()
108 # Make sure these operator properties match those used in `VIEW3D_PT_pose_library_legacy`.
109 layout.operator("poselib.apply_pose_asset", text="Apply Pose").flipped = False
110 layout.operator("poselib.apply_pose_asset", text="Apply Pose Flipped").flipped = True
112 with operator_context(layout, 'INVOKE_DEFAULT'):
113 layout.operator("poselib.blend_pose_asset", text="Blend Pose")
115 layout.separator()
116 props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones")
117 props.select = True
118 props = layout.operator("poselib.pose_asset_select_bones", text="Deselect Pose Bones")
119 props.select = False
121 if not is_pose_asset_view():
122 layout.separator()
123 layout.operator("asset.assign_action")
125 layout.separator()
126 if is_pose_asset_view():
127 layout.operator("asset.open_containing_blend_file")
129 props.select = False
132 class DOPESHEET_PT_asset_panel(PoseLibraryPanel, Panel):
133 bl_space_type = "DOPESHEET_EDITOR"
134 bl_region_type = "UI"
135 bl_label = "Create Pose Asset"
136 bl_category = "Action"
138 def draw(self, context: Context) -> None:
139 layout = self.layout
140 col = layout.column(align=True)
141 row = col.row(align=True)
142 row.operator("poselib.create_pose_asset").activate_new_action = True
143 if bpy.types.POSELIB_OT_restore_previous_action.poll(context):
144 row.operator("poselib.restore_previous_action", text="", icon='LOOP_BACK')
145 col.operator("poselib.copy_as_asset", icon="COPYDOWN")
147 layout.operator("poselib.convert_old_poselib")
150 def pose_library_list_item_asset_menu(self: UIList, context: Context) -> None:
151 layout = self.layout
152 layout.menu("ASSETBROWSER_MT_asset")
155 class ASSETBROWSER_MT_asset(Menu):
156 bl_label = "Asset"
158 @classmethod
159 def poll(cls, context):
160 from bpy_extras.asset_utils import SpaceAssetInfo
162 return SpaceAssetInfo.is_asset_browser_poll(context)
164 def draw(self, context: Context) -> None:
165 layout = self.layout
167 layout.operator("poselib.paste_asset", icon='PASTEDOWN')
168 layout.separator()
169 layout.operator("poselib.create_pose_asset").activate_new_action = False
172 ### Messagebus subscription to monitor asset library changes.
173 _msgbus_owner = object()
176 def _on_asset_library_changed() -> None:
177 """Update areas when a different asset library is selected."""
178 refresh_area_types = {'DOPESHEET_EDITOR', 'VIEW_3D'}
179 for win in bpy.context.window_manager.windows:
180 for area in win.screen.areas:
181 if area.type not in refresh_area_types:
182 continue
184 area.tag_redraw()
187 def register_message_bus() -> None:
188 bpy.msgbus.subscribe_rna(
189 key=(bpy.types.FileAssetSelectParams, "asset_library_reference"),
190 owner=_msgbus_owner,
191 args=(),
192 notify=_on_asset_library_changed,
193 options={'PERSISTENT'},
197 def unregister_message_bus() -> None:
198 bpy.msgbus.clear_by_owner(_msgbus_owner)
201 @bpy.app.handlers.persistent
202 def _on_blendfile_load_pre(none, other_none) -> None:
203 # The parameters are required, but both are None.
204 unregister_message_bus()
207 @bpy.app.handlers.persistent
208 def _on_blendfile_load_post(none, other_none) -> None:
209 # The parameters are required, but both are None.
210 register_message_bus()
213 classes = (
214 DOPESHEET_PT_asset_panel,
215 VIEW3D_PT_pose_library_legacy,
216 ASSETBROWSER_MT_asset,
217 VIEW3D_AST_pose_library,
220 _register, _unregister = bpy.utils.register_classes_factory(classes)
223 def register() -> None:
224 _register()
226 WorkSpace.active_pose_asset_index = bpy.props.IntProperty(
227 name="Active Pose Asset",
228 # TODO explain which list the index belongs to, or how it can be used to get the pose.
229 description="Per workspace index of the active pose asset",
231 # Register for window-manager. This is a global property that shouldn't be
232 # written to files.
233 WindowManager.pose_assets = bpy.props.CollectionProperty(type=AssetHandle)
235 bpy.types.UI_MT_list_item_context_menu.prepend(pose_library_list_item_context_menu)
236 bpy.types.ASSETBROWSER_MT_context_menu.prepend(pose_library_list_item_context_menu)
237 bpy.types.ASSETBROWSER_MT_editor_menus.append(pose_library_list_item_asset_menu)
239 register_message_bus()
240 bpy.app.handlers.load_pre.append(_on_blendfile_load_pre)
241 bpy.app.handlers.load_post.append(_on_blendfile_load_post)
244 def unregister() -> None:
245 _unregister()
247 unregister_message_bus()
249 del WorkSpace.active_pose_asset_index
250 del WindowManager.pose_assets
252 bpy.types.UI_MT_list_item_context_menu.remove(pose_library_list_item_context_menu)
253 bpy.types.ASSETBROWSER_MT_context_menu.remove(pose_library_list_item_context_menu)
254 bpy.types.ASSETBROWSER_MT_editor_menus.remove(pose_library_list_item_asset_menu)