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
Showing only changes of commit 375d1137fa - Show all commits

View File

@ -1,7 +1,7 @@
import bpy import bpy
from bpy import types from bpy import types
from typing import List, Tuple, Dict, Optional from typing import List, Tuple, Dict, Optional
from bpy.props import StringProperty from bpy.props import StringProperty, CollectionProperty
from bpy_extras import id_map_utils from bpy_extras import id_map_utils
import os import os
@ -9,6 +9,22 @@ from ..utils import hotkeys
from .relink_overridden_asset import OUTLINER_OT_relink_overridden_asset 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
class RelationshipOperatorMixin: class RelationshipOperatorMixin:
datablock_name: StringProperty() datablock_name: StringProperty()
datablock_storage: StringProperty() datablock_storage: StringProperty()
@ -44,7 +60,8 @@ class RelationshipOperatorMixin:
if not datablock: if not datablock:
layout.alert = True layout.alert = True
layout.label( layout.label(
text=f"Failed to find datablock: {self.datablock_storage}, {self.datablock_name}, {self.library_filepath}") text=f"Failed to find datablock: {self.datablock_storage}, {self.datablock_name}, {self.library_filepath}"
)
return return
row = layout.row() row = layout.row()
@ -55,8 +72,7 @@ class RelationshipOperatorMixin:
id_row = split.row(align=True) id_row = split.row(align=True)
name_row = id_row.row() name_row = id_row.row()
name_row.enabled = False name_row.enabled = False
name_row.prop(datablock, 'name', name_row.prop(datablock, 'name', icon=get_datablock_icon(datablock), text="")
icon=get_datablock_icon(datablock), text="")
fake_user_row = id_row.row() fake_user_row = id_row.row()
fake_user_row.prop(datablock, 'use_fake_user', text="") fake_user_row.prop(datablock, 'use_fake_user', text="")
@ -77,8 +93,7 @@ class RelationshipOperatorMixin:
name_row.enabled = False name_row.enabled = False
name_row.prop(user, 'name', icon=get_datablock_icon(user), text="") name_row.prop(user, 'name', icon=get_datablock_icon(user), text="")
op_row = row.row() op_row = row.row()
op = op_row.operator(type(self).bl_idname, op = op_row.operator(type(self).bl_idname, text="", icon='LOOP_FORWARDS')
text="", icon='LOOP_FORWARDS')
op.datablock_name = user.name op.datablock_name = user.name
storage = ID_CLASS_TO_STORAGE.get(type(user)) storage = ID_CLASS_TO_STORAGE.get(type(user))
if not storage: if not storage:
@ -86,19 +101,22 @@ class RelationshipOperatorMixin:
op.datablock_storage = storage op.datablock_storage = storage
if user.library: if user.library:
op.library_filepath = user.library.filepath op.library_filepath = user.library.filepath
icon = 'LIBRARY_DATA_DIRECT' name_row.prop(
filepath = os.path.abspath( user.library,
bpy.path.abspath(user.library.filepath)) 'filepath',
if not os.path.exists(filepath): icon=get_library_icon(user.library),
icon = 'LIBRARY_DATA_BROKEN' text="",
name_row.prop(user.library, 'filepath', icon=icon, text="") )
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
class OUTLINER_OT_list_users_of_datablock(RelationshipOperatorMixin, bpy.types.Operator): class OUTLINER_OT_list_users_of_datablock(
RelationshipOperatorMixin, bpy.types.Operator
):
"""Show list of users of this datablock""" """Show list of users of this datablock"""
bl_idname = "object.list_datablock_users" bl_idname = "object.list_datablock_users"
bl_label = "List Datablock Users" bl_label = "List Datablock Users"
@ -112,8 +130,11 @@ class OUTLINER_OT_list_users_of_datablock(RelationshipOperatorMixin, bpy.types.O
return sorted(users, key=lambda u: (str(type(u)), u.name)) return sorted(users, key=lambda u: (str(type(u)), u.name))
class OUTLINER_OT_list_dependencies_of_datablock(RelationshipOperatorMixin, bpy.types.Operator): class OUTLINER_OT_list_dependencies_of_datablock(
RelationshipOperatorMixin, bpy.types.Operator
):
"""Show list of dependencies of this datablock""" """Show list of dependencies of this datablock"""
bl_idname = "object.list_datablock_dependencies" bl_idname = "object.list_datablock_dependencies"
bl_label = "List Datablock Dependencies" bl_label = "List Datablock Dependencies"
@ -129,51 +150,46 @@ class OUTLINER_OT_list_dependencies_of_datablock(RelationshipOperatorMixin, bpy.
# (ID Python type, identifier string, database name) # (ID Python type, identifier string, database name)
ID_INFO = [ ID_INFO = [
(types.WindowManager, 'WINDOWMANAGER', 'window_managers'), (types.WindowManager, 'WINDOWMANAGER', 'window_managers'),
(types.Scene, 'SCENE', 'scenes'), (types.Scene, 'SCENE', 'scenes'),
(types.World, 'WORLD', 'worlds'), (types.World, 'WORLD', 'worlds'),
(types.Collection, 'COLLECTION', 'collections'), (types.Collection, 'COLLECTION', 'collections'),
(types.Armature, 'ARMATURE', 'armatures'),
(types.Armature, 'ARMATURE', 'armatures'), (types.Mesh, 'MESH', 'meshes'),
(types.Mesh, 'MESH', 'meshes'), (types.Camera, 'CAMERA', 'cameras'),
(types.Camera, 'CAMERA', 'cameras'), (types.Lattice, 'LATTICE', 'lattices'),
(types.Lattice, 'LATTICE', 'lattices'), (types.Light, 'LIGHT', 'lights'),
(types.Light, 'LIGHT', 'lights'), (types.Speaker, 'SPEAKER', 'speakers'),
(types.Speaker, 'SPEAKER', 'speakers'), (types.Volume, 'VOLUME', 'volumes'),
(types.Volume, 'VOLUME', 'volumes'), (types.GreasePencil, 'GREASEPENCIL', 'grease_pencils'),
(types.GreasePencil, 'GREASEPENCIL', 'grease_pencils'), (types.Curve, 'CURVE', 'curves'),
(types.Curve, 'CURVE', 'curves'), (types.LightProbe, 'LIGHT_PROBE', 'lightprobes'),
(types.LightProbe, 'LIGHT_PROBE', 'lightprobes'), (types.MetaBall, 'METABALL', 'metaballs'),
(types.Object, 'OBJECT', 'objects'),
(types.MetaBall, 'METABALL', 'metaballs'), (types.Action, 'ACTION', 'actions'),
(types.Object, 'OBJECT', 'objects'), (types.Key, 'KEY', 'shape_keys'),
(types.Action, 'ACTION', 'actions'), (types.Sound, 'SOUND', 'sounds'),
(types.Key, 'KEY', 'shape_keys'), (types.Material, 'MATERIAL', 'materials'),
(types.Sound, 'SOUND', 'sounds'), (types.NodeTree, 'NODETREE', 'node_groups'),
(types.GeometryNodeTree, 'GEOMETRY', 'node_groups'),
(types.Material, 'MATERIAL', 'materials'), (types.ShaderNodeTree, 'SHADER', 'node_groups'),
(types.NodeTree, 'NODETREE', 'node_groups'), (types.Image, 'IMAGE', 'images'),
(types.GeometryNodeTree, 'GEOMETRY', 'node_groups'), (types.Mask, 'MASK', 'masks'),
(types.ShaderNodeTree, 'SHADER', 'node_groups'), (types.FreestyleLineStyle, 'LINESTYLE', 'linestyles'),
(types.Image, 'IMAGE', 'images'), (types.Library, 'LIBRARY', 'libraries'),
(types.VectorFont, 'FONT', 'fonts'),
(types.Mask, 'MASK', 'masks'), (types.CacheFile, 'CACHE_FILE', 'cache_files'),
(types.FreestyleLineStyle, 'LINESTYLE', 'linestyles'), (types.PointCloud, 'POINT_CLOUD', 'pointclouds'),
(types.Library, 'LIBRARY', 'libraries'), (types.Curves, 'HAIR_CURVES', 'hair_curves'),
(types.VectorFont, 'FONT', 'fonts'), (types.Text, 'TEXT', 'texts'),
(types.CacheFile, 'CACHE_FILE', 'cache_files'), (types.ParticleSettings, 'PARTICLE', 'particles'),
(types.PointCloud, 'POINT_CLOUD', 'pointclouds'), (types.Palette, 'PALETTE', 'palettes'),
(types.Curves, 'HAIR_CURVES', 'hair_curves'), (types.PaintCurve, 'PAINT_CURVE', 'paint_curves'),
(types.Text, 'TEXT', 'texts'), (types.MovieClip, 'MOVIECLIP', 'movieclips'),
(types.ParticleSettings, 'PARTICLE', 'particles'), (types.WorkSpace, 'WORKSPACE', 'workspaces'),
(types.Palette, 'PALETTE', 'palettes'), (types.Screen, 'SCREEN', 'screens'),
(types.PaintCurve, 'PAINT_CURVE', 'paint_curves'), (types.Brush, 'BRUSH', 'brushes'),
(types.MovieClip, 'MOVIECLIP', 'movieclips'), (types.Texture, 'TEXTURE', 'textures'),
(types.WorkSpace, 'WORKSPACE', 'workspaces'),
(types.Screen, 'SCREEN', 'screens'),
(types.Brush, 'BRUSH', 'brushes'),
(types.Texture, 'TEXTURE', 'textures'),
] ]
@ -184,23 +200,27 @@ def get_datablock_icon_map() -> Dict[str, str]:
""" """
enum_items = types.DriverTarget.bl_rna.properties['id_type'].enum_items enum_items = types.DriverTarget.bl_rna.properties['id_type'].enum_items
icon_map = {typ.identifier: typ.icon for typ in enum_items} icon_map = {typ.identifier: typ.icon for typ in enum_items}
icon_map.update({ icon_map.update(
'SCREEN': 'RESTRICT_VIEW_OFF', {
'METABALL': 'OUTLINER_OB_META', 'SCREEN': 'RESTRICT_VIEW_OFF',
'CACHE_FILE': 'MOD_MESHDEFORM', 'METABALL': 'OUTLINER_OB_META',
'POINT_CLOUD': 'OUTLINER_OB_POINTCLOUD', 'CACHE_FILE': 'MOD_MESHDEFORM',
'HAIR_CURVES': 'OUTLINER_OB_CURVES', 'POINT_CLOUD': 'OUTLINER_OB_POINTCLOUD',
'PAINT_CURVE': 'FORCE_CURVE', 'HAIR_CURVES': 'OUTLINER_OB_CURVES',
'MOVIE_CLIP': 'FILE_MOVIE', 'PAINT_CURVE': 'FORCE_CURVE',
'GEOMETRY': 'GEOMETRY_NODES', 'MOVIE_CLIP': 'FILE_MOVIE',
'SHADER': 'NODETREE', 'GEOMETRY': 'GEOMETRY_NODES',
}) 'SHADER': 'NODETREE',
}
)
return icon_map return icon_map
# Map datablock identifier strings to their icon. # Map datablock identifier strings to their icon.
ID_TYPE_STR_TO_ICON: Dict[str, str] = get_datablock_icon_map() 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. # Map Python ID classes to their string representation.
ID_CLASS_TO_IDENTIFIER: Dict[type, str] = {tup[0]: tup[1] for tup in ID_INFO} ID_CLASS_TO_IDENTIFIER: Dict[type, str] = {tup[0]: tup[1] for tup in ID_INFO}
@ -211,50 +231,220 @@ ID_CLASS_TO_STORAGE: Dict[type, str] = {tup[0]: tup[2] for tup in ID_INFO}
def get_datablock_icon(id) -> str: def get_datablock_icon(id) -> str:
identifier_str = ID_CLASS_TO_IDENTIFIER.get(type(id)) identifier_str = ID_CLASS_TO_IDENTIFIER.get(type(id))
if not identifier_str: if not identifier_str:
return 'QUESTION' return 'NONE'
icon = ID_TYPE_STR_TO_ICON.get(identifier_str) icon = ID_TYPE_STR_TO_ICON.get(identifier_str)
if not icon: if not icon:
return 'QUESTION' return 'NONE'
return icon return icon
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"""
bl_idname = "outliner.remap_users"
bl_label = "Remap Users"
bl_options = {'INTERNAL', 'UNDO'}
def update_library_path(self, context):
def get_source_id():
# WTF??? When I try to access this through self, it says it doesn't exist... so I had to duplicate it!? TODO
storage = get_id_storage(self.id_type)
if self.library_path_source:
return storage.get((self.id_name_source, self.library_path_source))
return storage.get((self.id_name_source, None))
# Prepare the ID selector.
remap_targets = context.scene.remap_targets
remap_targets.clear()
source_id = get_source_id()
for id in get_id_storage(self.id_type):
if id == source_id:
continue
if (self.library_path == 'Local Data' and not id.library) or (
id.library and (self.library_path == id.library.filepath)
):
id_entry = remap_targets.add()
id_entry.name = id.name
def get_source_id(self):
storage = get_id_storage(self.id_type)
if self.library_path_source:
return storage.get((self.id_name_source, self.library_path_source))
return storage.get((self.id_name_source, None))
def get_target_id(self):
storage = get_id_storage(self.id_type)
if self.library_path != 'Local Data':
return storage.get((self.id_name_target, self.library_path))
return storage.get((self.id_name_target, None))
library_path: StringProperty(
name="Library",
description="Library path, if we want to remap to a linked ID",
update=update_library_path,
)
id_type: StringProperty(description="ID type, eg. 'OBJECT' or 'MESH'")
library_path_source: StringProperty()
id_name_source: StringProperty(
name="Source ID Name", description="Name of the ID we're remapping the users of"
)
id_name_target: StringProperty(
name="Target ID Name", description="Name of the ID we're remapping users to"
)
def invoke(self, context, _event):
# Populate the remap_targets string list with possible options based on
# what was passed to the operator.
assert (
self.id_type and self.id_name_source
), "Error: UI must provide ID and ID type to this operator."
# Prepare the library selector.
remap_target_libraries = context.scene.remap_target_libraries
remap_target_libraries.clear()
local = remap_target_libraries.add()
local.name = "Local Data"
source_id_type = type(self.get_source_id())
for lib in bpy.data.libraries:
for id in lib.users_id:
if type(id) == source_id_type:
lib_entry = remap_target_libraries.add()
lib_entry.name = lib.filepath
break
self.library_path = "Local Data"
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
row = layout.row()
id = self.get_source_id()
id_icon = get_datablock_icon(id)
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.enabled=False
layout.separator()
col = layout.column()
col.label(text="Remap users to: ")
if len(scene.remap_target_libraries) > 1:
col.prop_search(
self,
'library_path',
scene,
'remap_target_libraries',
icon=get_library_icon(get_library_by_filepath(self.library_path)),
)
col.prop_search(
self,
'id_name_target',
scene,
'remap_targets',
text="Remap To",
icon=id_icon
)
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
break
else:
self.report(
{'ERROR'}, "Error: This operation requires an Outliner to be present."
)
return {'CANCELLED'}
source_id = self.get_source_id()
target_id = self.get_target_id()
source_id.user_remap(target_id)
return {'FINISHED'}
class IDMAN_MT_relationship_pie(bpy.types.Menu): class IDMAN_MT_relationship_pie(bpy.types.Menu):
# bl_label is displayed at the center of the pie menu # bl_label is displayed at the center of the pie menu
bl_label = 'Datablock Relationships' bl_label = 'Datablock Relationships'
bl_idname = 'MT_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): def draw(self, context):
layout = self.layout layout = self.layout
pie = layout.menu_pie() pie = layout.menu_pie()
# < # <
pie.operator(OUTLINER_OT_list_users_of_datablock.bl_idname, pie.operator(OUTLINER_OT_list_users_of_datablock.bl_idname, icon='LOOP_BACK')
icon='LOOP_BACK')
# > # >
pie.operator( pie.operator(
OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS') OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS'
)
# V # V
pie.operator('outliner.better_purge', icon='TRASH') pie.operator('outliner.better_purge', icon='TRASH')
# ^ # ^
pie.operator('outliner.id_operation', icon='FILE_REFRESH', remap = pie.operator(
text="Remap Users").type = 'REMAP' '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) id = OUTLINER_OT_relink_overridden_asset.get_id(context)
if id: if id:
pie.operator('object.relink_overridden_asset', pie.operator('object.relink_overridden_asset', icon='LIBRARY_DATA_OVERRIDE')
icon='LIBRARY_DATA_OVERRIDE')
# <^ # <^
if id and id.override_library: if id and id.override_library:
pie.operator('outliner.liboverride_troubleshoot_operation', icon='UV_SYNC_SELECT', pie.operator(
text="Resync Override Hierarchy").type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE' 'outliner.liboverride_troubleshoot_operation',
icon='UV_SYNC_SELECT',
text="Resync Override Hierarchy",
).type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE'
registry = [ registry = [
OUTLINER_OT_list_users_of_datablock, OUTLINER_OT_list_users_of_datablock,
OUTLINER_OT_list_dependencies_of_datablock, OUTLINER_OT_list_dependencies_of_datablock,
IDMAN_MT_relationship_pie, IDMAN_MT_relationship_pie,
OUTLINER_OT_remap_users,
RemapTarget,
] ]
@ -263,9 +453,7 @@ def register():
bl_idname='wm.call_menu_pie', bl_idname='wm.call_menu_pie',
km_name='Outliner', km_name='Outliner',
key_id='Y', key_id='Y',
event_type='PRESS', event_type='PRESS',
any=False, any=False,
ctrl=False, ctrl=False,
alt=False, alt=False,
@ -274,6 +462,8 @@ def register():
key_modifier='NONE', key_modifier='NONE',
direction='ANY', direction='ANY',
repeat=False, repeat=False,
op_kwargs={'name': IDMAN_MT_relationship_pie.bl_idname},
op_kwargs={'name': IDMAN_MT_relationship_pie.bl_idname}
) )
bpy.types.Scene.remap_targets = CollectionProperty(type=RemapTarget)
bpy.types.Scene.remap_target_libraries = CollectionProperty(type=RemapTarget)