Files
blender-portable-repo/scripts/addons/pose_shape_keys/ui.py
T
2026-03-17 14:58:51 -06:00

306 lines
11 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
from bpy.types import Panel, UIList, Menu
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
from bpy.props import EnumProperty
from .ui_list import draw_ui_list
from .ops import get_deforming_armature, poll_correct_pose_key_pose, get_active_pose_key
from .prefs import get_addon_prefs
from .props import update_posekey_index
class MESH_PT_pose_keys(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'data'
bl_options = {'DEFAULT_CLOSED'}
bl_label = "Pose Shape Keys"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH'
def draw(self, context):
obj = context.object
mesh = obj.data
layout = self.layout.column()
layout.row().prop(mesh, 'shape_key_ui_type', expand=True)
if mesh.shape_key_ui_type == 'DEFAULT':
return DATA_PT_shape_keys.draw(self, context)
arm_ob = get_deforming_armature(obj)
if not arm_ob:
layout.alert = True
layout.label(text="Object must be deformed by an Armature to use Pose Keys.")
return
if mesh.shape_keys and not mesh.shape_keys.use_relative:
layout.alert = True
layout.label("Relative Shape Keys must be enabled!")
return
list_row = layout.row()
groups_col = list_row.column()
draw_ui_list(
groups_col,
context,
class_name='POSEKEYS_UL_pose_keys',
list_context_path='object.data.pose_keys',
active_idx_context_path='object.data.active_pose_key_index',
menu_class_name='MESH_MT_pose_key_utils',
add_op_name='object.posekey_add',
)
layout.use_property_split = True
layout.use_property_decorate = False
if len(mesh.pose_keys) == 0:
return
active_posekey = get_active_pose_key(context.object)
if not active_posekey:
return
action_split = layout.row().split(factor=0.4, align=True)
action_split.alignment = 'RIGHT'
action_split.label(text="Action")
row = action_split.row(align=True)
icon = 'FORWARD'
if active_posekey.action:
icon = 'FILE_REFRESH'
row.operator('object.posekey_auto_init', text="", icon=icon)
row.prop(active_posekey, 'action', text="")
layout.prop(active_posekey, 'frame')
layout.separator()
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
layout.separator()
row = layout.row(align=True)
text = "Save Posed Mesh"
if active_posekey.storage_object:
text = "Overwrite Posed Mesh"
row.operator('object.posekey_save', text=text, icon="FILE_TICK")
row.prop(active_posekey, 'storage_object', text="")
row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF')
class POSEKEYS_UL_pose_keys(UIList):
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
pose_key = item
if self.layout_type != 'DEFAULT':
# Other layout types not supported by this UIList.
return
split = layout.row().split(factor=0.7, align=True)
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
name_row = split.row()
if pose_key.has_error:
# name_row.alert = True
icon = 'ERROR'
if not pose_key.name:
split = name_row.split()
name_row = split.row()
split.label(text="Unnamed!", icon='ERROR')
name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon)
class MESH_MT_pose_key_utils(Menu):
bl_label = "Pose Key Utilities"
def draw(self, context):
layout = self.layout
layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME')
layout.operator('object.posekey_push_all', icon='WORLD')
layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES')
layout.operator('object.posekey_copy_data', icon='PASTEDOWN')
class MESH_PT_shape_key_subpanel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'data'
bl_options = {'DEFAULT_CLOSED'}
bl_label = "Shape Key Slots"
bl_parent_id = "MESH_PT_pose_keys"
@classmethod
def poll(cls, context):
obj = context.object
if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'):
return False
try:
return poll_correct_pose_key_pose(cls, context, demand_pose=False)
except AttributeError:
# Happens any time that function tries to set a poll message,
# since panels don't have poll messages, lol.
return False
def draw(self, context):
obj = context.object
mesh = obj.data
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
idx = context.object.data.active_pose_key_index
active_posekey = get_active_pose_key(context.object)
if not active_posekey:
return
layout.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT")
draw_ui_list(
layout,
context,
class_name='POSEKEYS_UL_target_shape_keys',
list_context_path=f'object.data.pose_keys[{idx}].target_shapes',
active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index',
add_op_name='object.posekey_shape_add',
remove_op_name='object.posekey_shape_remove',
)
if len(active_posekey.target_shapes) == 0:
return
active_target = active_posekey.active_target
selector_row = layout.row()
if not mesh.shape_keys:
return
icon = 'SHAPEKEY_DATA'
key_block = context.object.data.shape_keys.key_blocks.get(active_target.shape_key_name)
if not key_block:
# selector_row.alert = True
icon = 'ERROR'
selector_row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks', icon=icon)
if not active_target.key_block:
add_shape_op = selector_row.operator('object.posekey_shape_add', icon='ADD', text="")
add_shape_op.create_slot=False
sk = active_target.key_block
if not sk:
return
addon_prefs = get_addon_prefs(context)
icon = 'HIDE_OFF' if addon_prefs.show_shape_key_info else 'HIDE_ON'
selector_row.prop(addon_prefs, 'show_shape_key_info', text="", icon=icon)
if addon_prefs.show_shape_key_info:
layout.prop(active_target, 'mirror_x')
split = layout.split(factor=0.1)
split.row()
col = split.column()
col.row().prop(sk, 'value')
row = col.row(align=True)
row.prop(sk, 'slider_min', text="Range")
row.prop(sk, 'slider_max', text="")
col.prop_search(sk, "vertex_group", obj, "vertex_groups", text="Vertex Mask")
col.row().prop(sk, 'relative_key')
class POSEKEYS_UL_target_shape_keys(UIList):
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
obj = context.object
pose_key_target = item
key_block = pose_key_target.key_block
if self.layout_type != 'DEFAULT':
# Other layout types not supported by this UIList.
return
split = layout.row().split(factor=0.7, align=True)
name_row = split.row()
icon = 'SHAPEKEY_DATA'
if pose_key_target.has_error:
icon = 'ERROR'
name_row.prop(pose_key_target, 'name', text="", emboss=False, icon=icon)
value_row = split.row(align=True)
value_row.emboss = 'NONE_OR_STATUS'
if not key_block:
return
if (
key_block.mute
or (obj.mode == 'EDIT' and not (obj.use_shape_key_edit_mode and obj.type == 'MESH'))
or (obj.show_only_shape_key and key_block != obj.active_shape_key)
):
name_row.active = value_row.active = False
value_row.operator('object.posekey_magic_driver', text="", icon='DECORATE_DRIVER').shapekey_name = key_block.name
value_row.prop(key_block, "value", text="")
mute_row = split.row()
mute_row.alignment = 'RIGHT'
mute_row.prop(key_block, 'mute', emboss=False, text="")
@classmethod
def shape_key_panel_new_poll(cls, context):
engine = context.engine
obj = context.object
return obj and obj.type in {'LATTICE', 'CURVE', 'SURFACE'} and (engine in cls.COMPAT_ENGINES)
registry = [
POSEKEYS_UL_pose_keys,
POSEKEYS_UL_target_shape_keys,
MESH_PT_pose_keys,
MESH_PT_shape_key_subpanel,
MESH_MT_pose_key_utils,
]
def register():
bpy.types.Mesh.shape_key_ui_type = EnumProperty(
name="Shape Key List Type",
items=[
('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"),
(
'POSE_KEYS',
'Pose Shape Keys',
"Organize shape keys into a higher-level concept called Pose Keys. These can store vertex positions and push one shape to multiple shape keys at once, relative to existing deformation",
),
],
update=update_posekey_index,
)
for panel in bpy.types.Panel.__subclasses__():
if hasattr(panel, 'bl_parent_id') and panel.bl_parent_id == 'DATA_PT_shape_keys':
panel.bl_parent_id = 'MESH_PT_pose_keys'
try:
bpy.utils.unregister_class(panel)
bpy.utils.register_class(panel)
except RuntimeError:
# Class was already unregistered, leave it unregistered.
pass
DATA_PT_shape_keys.replacement = 'MESH_PT_pose_keys' # This is used by GeoNodeShapeKeys add-on to register to the correct parent panel. Could be used by any other add-on I guess.
DATA_PT_shape_keys.old_poll = DATA_PT_shape_keys.poll
DATA_PT_shape_keys.poll = shape_key_panel_new_poll
def unregister():
for panel in bpy.types.Panel.__subclasses__():
if hasattr(panel, 'bl_parent_id') and panel.bl_parent_id == 'MESH_PT_pose_keys':
panel.bl_parent_id = 'DATA_PT_shape_keys'
try:
bpy.utils.unregister_class(panel)
bpy.utils.register_class(panel)
except RuntimeError:
# Class was already unregistered, leave it unregistered.
pass
del bpy.types.Mesh.shape_key_ui_type
DATA_PT_shape_keys.poll = DATA_PT_shape_keys.old_poll
DATA_PT_shape_keys.replacement = None