AnimCupboard: ID Management Pie #127

Merged
Demeter Dzadik merged 11 commits from Mets/blender-studio-pipeline:AnimCupboard-relationship-viewer into main 2023-07-19 14:43:14 +02:00
2 changed files with 211 additions and 217 deletions
Showing only changes of commit ba73eec495 - Show all commits

View File

@ -7,7 +7,7 @@ from .operators import select_similar_curves
from .operators import lock_curves
from .operators import bake_anim_across_armatures
from .operators import relink_overridden_asset
from .operators import id_management_ops
from .operators import id_management_pie
from . import easy_constraints
from . import warn_about_broken_libraries
from . import bone_selection_sets
@ -31,7 +31,7 @@ modules = (
warn_about_broken_libraries,
bone_selection_sets,
relink_overridden_asset,
id_management_ops,
id_management_pie,
)

View File

@ -9,22 +9,61 @@ from ..utils import hotkeys
from .relink_overridden_asset import OUTLINER_OT_relink_overridden_asset
def get_library_icon(library: bpy.types.Library):
if not library:
return 'NONE'
icon = 'LIBRARY_DATA_DIRECT'
filepath = os.path.abspath(bpy.path.abspath(library.filepath))
if not os.path.exists(filepath):
icon = 'LIBRARY_DATA_BROKEN'
return icon
def get_library_by_filepath(filepath: str):
for lib in bpy.data.libraries:
if lib.filepath == filepath:
return lib
### Pie Menu UI
class IDMAN_MT_relationship_pie(bpy.types.Menu):
# bl_label is displayed at the center of the pie menu
bl_label = 'Datablock Relationships'
bl_idname = 'IDMAN_MT_relationship_pie'
@staticmethod
def get_id(context) -> Optional[bpy.types.ID]:
if context.area.type == 'OUTLINER' and len(context.selected_ids) > 0:
return context.selected_ids[0]
@classmethod
def poll(cls, context):
return cls.get_id(context)
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
# <
pie.operator(OUTLINER_OT_list_users_of_datablock.bl_idname, icon='LOOP_BACK')
# >
pie.operator(
OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS'
)
# V
pie.operator('outliner.better_purge', icon='TRASH')
# ^
remap = pie.operator(
'outliner.remap_users', icon='FILE_REFRESH', text="Remap Users"
)
id = self.get_id(context)
id_type = ID_CLASS_TO_IDENTIFIER.get(type(id))
if not id_type:
pass # TODO
remap.id_type = id_type
remap.id_name_source = id.name
if id.library:
remap.library_path_source = id.library.filepath
# ^>
id = OUTLINER_OT_relink_overridden_asset.get_id(context)
if id:
pie.operator('object.relink_overridden_asset', icon='LIBRARY_DATA_OVERRIDE')
# <^
if id and id.override_library:
pie.operator(
'outliner.liboverride_troubleshoot_operation',
icon='UV_SYNC_SELECT',
text="Resync Override Hierarchy",
).type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE'
### Relationship visualization operators
class RelationshipOperatorMixin:
datablock_name: StringProperty()
datablock_storage: StringProperty()
@ -148,108 +187,11 @@ class OUTLINER_OT_list_dependencies_of_datablock(
return sorted(dependencies, key=lambda u: (str(type(u)), u.name))
# (ID Python type, identifier string, database name)
ID_INFO = [
(types.WindowManager, 'WINDOWMANAGER', 'window_managers'),
(types.Scene, 'SCENE', 'scenes'),
(types.World, 'WORLD', 'worlds'),
(types.Collection, 'COLLECTION', 'collections'),
(types.Armature, 'ARMATURE', 'armatures'),
(types.Mesh, 'MESH', 'meshes'),
(types.Camera, 'CAMERA', 'cameras'),
(types.Lattice, 'LATTICE', 'lattices'),
(types.Light, 'LIGHT', 'lights'),
(types.Speaker, 'SPEAKER', 'speakers'),
(types.Volume, 'VOLUME', 'volumes'),
(types.GreasePencil, 'GREASEPENCIL', 'grease_pencils'),
(types.Curve, 'CURVE', 'curves'),
(types.LightProbe, 'LIGHT_PROBE', 'lightprobes'),
(types.MetaBall, 'METABALL', 'metaballs'),
(types.Object, 'OBJECT', 'objects'),
(types.Action, 'ACTION', 'actions'),
(types.Key, 'KEY', 'shape_keys'),
(types.Sound, 'SOUND', 'sounds'),
(types.Material, 'MATERIAL', 'materials'),
(types.NodeTree, 'NODETREE', 'node_groups'),
(types.GeometryNodeTree, 'GEOMETRY', 'node_groups'),
(types.ShaderNodeTree, 'SHADER', 'node_groups'),
(types.Image, 'IMAGE', 'images'),
(types.Mask, 'MASK', 'masks'),
(types.FreestyleLineStyle, 'LINESTYLE', 'linestyles'),
(types.Library, 'LIBRARY', 'libraries'),
(types.VectorFont, 'FONT', 'fonts'),
(types.CacheFile, 'CACHE_FILE', 'cache_files'),
(types.PointCloud, 'POINT_CLOUD', 'pointclouds'),
(types.Curves, 'HAIR_CURVES', 'hair_curves'),
(types.Text, 'TEXT', 'texts'),
(types.ParticleSettings, 'PARTICLE', 'particles'),
(types.Palette, 'PALETTE', 'palettes'),
(types.PaintCurve, 'PAINT_CURVE', 'paint_curves'),
(types.MovieClip, 'MOVIECLIP', 'movieclips'),
(types.WorkSpace, 'WORKSPACE', 'workspaces'),
(types.Screen, 'SCREEN', 'screens'),
(types.Brush, 'BRUSH', 'brushes'),
(types.Texture, 'TEXTURE', 'textures'),
]
def get_datablock_icon_map() -> Dict[str, str]:
"""Create a mapping from datablock type identifiers to their icon.
We can get most of the icons from the Driver Type selector enum,
the rest we have to enter manually.
"""
enum_items = types.DriverTarget.bl_rna.properties['id_type'].enum_items
icon_map = {typ.identifier: typ.icon for typ in enum_items}
icon_map.update(
{
'SCREEN': 'RESTRICT_VIEW_OFF',
'METABALL': 'OUTLINER_OB_META',
'CACHE_FILE': 'MOD_MESHDEFORM',
'POINT_CLOUD': 'OUTLINER_OB_POINTCLOUD',
'HAIR_CURVES': 'OUTLINER_OB_CURVES',
'PAINT_CURVE': 'FORCE_CURVE',
'MOVIE_CLIP': 'FILE_MOVIE',
'GEOMETRY': 'GEOMETRY_NODES',
'SHADER': 'NODETREE',
}
)
return icon_map
# Map datablock identifier strings to their icon.
ID_TYPE_STR_TO_ICON: Dict[str, str] = get_datablock_icon_map()
# Map datablock identifier strings to the string of their bpy.data database name.
ID_TYPE_TO_STORAGE: Dict[type, str] = {tup[1]: tup[2] for tup in ID_INFO}
# Map Python ID classes to their string representation.
ID_CLASS_TO_IDENTIFIER: Dict[type, str] = {tup[0]: tup[1] for tup in ID_INFO}
# Map Python ID classes to the string of their bpy.data database name.
ID_CLASS_TO_STORAGE: Dict[type, str] = {tup[0]: tup[2] for tup in ID_INFO}
def get_datablock_icon(id) -> str:
identifier_str = ID_CLASS_TO_IDENTIFIER.get(type(id))
if not identifier_str:
return 'NONE'
icon = ID_TYPE_STR_TO_ICON.get(identifier_str)
if not icon:
return 'NONE'
return icon
### Remap Users
class RemapTarget(bpy.types.PropertyGroup):
pass
def get_id_storage(id_type):
storage = ID_TYPE_TO_STORAGE.get(id_type)
assert storage and hasattr(bpy.data, storage), (
"Error: Storage not found for id type: " + id_type
)
return getattr(bpy.data, storage)
class OUTLINER_OT_remap_users(bpy.types.Operator):
"""A wrapper around Blender's built-in Remap Users operator"""
@ -341,12 +283,7 @@ class OUTLINER_OT_remap_users(bpy.types.Operator):
split = row.split()
split.row().label(text="Remap Users of ID:")
row = split.row()
row.prop(
self,
'id_name_source',
text="",
icon=id_icon
)
row.prop(self, 'id_name_source', text="", icon=id_icon)
row.enabled = False
layout.separator()
@ -366,7 +303,7 @@ class OUTLINER_OT_remap_users(bpy.types.Operator):
scene,
'remap_targets',
text="Remap To",
icon=id_icon
icon=id_icon,
)
def execute(self, context):
@ -386,65 +323,131 @@ class OUTLINER_OT_remap_users(bpy.types.Operator):
return {'FINISHED'}
class IDMAN_MT_relationship_pie(bpy.types.Menu):
# bl_label is displayed at the center of the pie menu
bl_label = 'Datablock Relationships'
bl_idname = 'IDMAN_MT_relationship_pie'
### ID utilities
# (ID Python type, identifier string, database name)
ID_INFO = [
(types.WindowManager, 'WINDOWMANAGER', 'window_managers'),
(types.Scene, 'SCENE', 'scenes'),
(types.World, 'WORLD', 'worlds'),
(types.Collection, 'COLLECTION', 'collections'),
(types.Armature, 'ARMATURE', 'armatures'),
(types.Mesh, 'MESH', 'meshes'),
(types.Camera, 'CAMERA', 'cameras'),
(types.Lattice, 'LATTICE', 'lattices'),
(types.Light, 'LIGHT', 'lights'),
(types.Speaker, 'SPEAKER', 'speakers'),
(types.Volume, 'VOLUME', 'volumes'),
(types.GreasePencil, 'GREASEPENCIL', 'grease_pencils'),
(types.Curve, 'CURVE', 'curves'),
(types.LightProbe, 'LIGHT_PROBE', 'lightprobes'),
(types.MetaBall, 'METABALL', 'metaballs'),
(types.Object, 'OBJECT', 'objects'),
(types.Action, 'ACTION', 'actions'),
(types.Key, 'KEY', 'shape_keys'),
(types.Sound, 'SOUND', 'sounds'),
(types.Material, 'MATERIAL', 'materials'),
(types.NodeTree, 'NODETREE', 'node_groups'),
(types.GeometryNodeTree, 'GEOMETRY', 'node_groups'),
(types.ShaderNodeTree, 'SHADER', 'node_groups'),
(types.Image, 'IMAGE', 'images'),
(types.Mask, 'MASK', 'masks'),
(types.FreestyleLineStyle, 'LINESTYLE', 'linestyles'),
(types.Library, 'LIBRARY', 'libraries'),
(types.VectorFont, 'FONT', 'fonts'),
(types.CacheFile, 'CACHE_FILE', 'cache_files'),
(types.PointCloud, 'POINT_CLOUD', 'pointclouds'),
(types.Curves, 'HAIR_CURVES', 'hair_curves'),
(types.Text, 'TEXT', 'texts'),
(types.ParticleSettings, 'PARTICLE', 'particles'),
(types.Palette, 'PALETTE', 'palettes'),
(types.PaintCurve, 'PAINT_CURVE', 'paint_curves'),
(types.MovieClip, 'MOVIECLIP', 'movieclips'),
(types.WorkSpace, 'WORKSPACE', 'workspaces'),
(types.Screen, 'SCREEN', 'screens'),
(types.Brush, 'BRUSH', 'brushes'),
(types.Texture, 'TEXTURE', 'textures'),
]
@staticmethod
def get_id(context) -> Optional[bpy.types.ID]:
if context.area.type == 'OUTLINER' and len(context.selected_ids) > 0:
return context.selected_ids[0]
@classmethod
def poll(cls, context):
return cls.get_id(context)
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
# <
pie.operator(OUTLINER_OT_list_users_of_datablock.bl_idname, icon='LOOP_BACK')
# >
pie.operator(
OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS'
def get_datablock_icon_map() -> Dict[str, str]:
"""Create a mapping from datablock type identifiers to their icon.
We can get most of the icons from the Driver Type selector enum,
the rest we have to enter manually.
"""
enum_items = types.DriverTarget.bl_rna.properties['id_type'].enum_items
icon_map = {typ.identifier: typ.icon for typ in enum_items}
icon_map.update(
{
'SCREEN': 'RESTRICT_VIEW_OFF',
'METABALL': 'OUTLINER_OB_META',
'CACHE_FILE': 'MOD_MESHDEFORM',
'POINT_CLOUD': 'OUTLINER_OB_POINTCLOUD',
'HAIR_CURVES': 'OUTLINER_OB_CURVES',
'PAINT_CURVE': 'FORCE_CURVE',
'MOVIE_CLIP': 'FILE_MOVIE',
'GEOMETRY': 'GEOMETRY_NODES',
'SHADER': 'NODETREE',
}
)
# V
pie.operator('outliner.better_purge', icon='TRASH')
# ^
remap = pie.operator(
'outliner.remap_users', icon='FILE_REFRESH', text="Remap Users"
return icon_map
# Map datablock identifier strings to their icon.
ID_TYPE_STR_TO_ICON: Dict[str, str] = get_datablock_icon_map()
# Map datablock identifier strings to the string of their bpy.data database name.
ID_TYPE_TO_STORAGE: Dict[type, str] = {tup[1]: tup[2] for tup in ID_INFO}
# Map Python ID classes to their string representation.
ID_CLASS_TO_IDENTIFIER: Dict[type, str] = {tup[0]: tup[1] for tup in ID_INFO}
# Map Python ID classes to the string of their bpy.data database name.
ID_CLASS_TO_STORAGE: Dict[type, str] = {tup[0]: tup[2] for tup in ID_INFO}
def get_datablock_icon(id) -> str:
identifier_str = ID_CLASS_TO_IDENTIFIER.get(type(id))
if not identifier_str:
return 'NONE'
icon = ID_TYPE_STR_TO_ICON.get(identifier_str)
if not icon:
return 'NONE'
return icon
def get_id_storage(id_type) -> "bpy.data.something":
"""Return the database of a certain ID Type, for example if you pass in an
Object, this will return bpy.data.objects."""
storage = ID_TYPE_TO_STORAGE.get(id_type)
assert storage and hasattr(bpy.data, storage), (
"Error: Storage not found for id type: " + id_type
)
id = self.get_id(context)
id_type = ID_CLASS_TO_IDENTIFIER.get(type(id))
if not id_type:
pass # TODO
remap.id_type = id_type
remap.id_name_source = id.name
if id.library:
remap.library_path_source = id.library.filepath
return getattr(bpy.data, storage)
# ^>
id = OUTLINER_OT_relink_overridden_asset.get_id(context)
if id:
pie.operator('object.relink_overridden_asset', icon='LIBRARY_DATA_OVERRIDE')
# <^
if id and id.override_library:
pie.operator(
'outliner.liboverride_troubleshoot_operation',
icon='UV_SYNC_SELECT',
text="Resync Override Hierarchy",
).type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE'
### Library utilities
def get_library_icon(library: bpy.types.Library) -> str:
"""Return the library or the broken library icon, as appropriate."""
if not library:
return 'NONE'
icon = 'LIBRARY_DATA_DIRECT'
filepath = os.path.abspath(bpy.path.abspath(library.filepath))
if not os.path.exists(filepath):
icon = 'LIBRARY_DATA_BROKEN'
return icon
def get_library_by_filepath(filepath: str):
for lib in bpy.data.libraries:
if lib.filepath == filepath:
return lib
registry = [
IDMAN_MT_relationship_pie,
OUTLINER_OT_list_users_of_datablock,
OUTLINER_OT_list_dependencies_of_datablock,
IDMAN_MT_relationship_pie,
OUTLINER_OT_remap_users,
RemapTarget,
OUTLINER_OT_remap_users,
]
@ -453,15 +456,6 @@ def register():
bl_idname='wm.call_menu_pie',
km_name='Outliner',
key_id='Y',
event_type='PRESS',
any=False,
ctrl=False,
alt=False,
shift=False,
oskey=False,
key_modifier='NONE',
direction='ANY',
repeat=False,
op_kwargs={'name': IDMAN_MT_relationship_pie.bl_idname},
)