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,6 +187,143 @@ class OUTLINER_OT_list_dependencies_of_datablock(
return sorted(dependencies, key=lambda u: (str(type(u)), u.name))
### Remap Users
class RemapTarget(bpy.types.PropertyGroup):
pass
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, width=800)
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:

This loop is looking for a VIEW_3D but the report says it is checking if an outliner is present. Assuming the outliner is what you are looking for wouldn't it be more explicit to do...

for area in context.screen.areas:
    if area.type == 'OUTLINER':
        break
else:
    self.report(
        {'ERROR'}, "Error: This operation requires an Outliner to be present."
    )
    return {'CANCELLED'}
This loop is looking for a `VIEW_3D` but the report says it is checking if an outliner is present. Assuming the outliner is what you are looking for wouldn't it be more explicit to do... ```python for area in context.screen.areas: if area.type == 'OUTLINER': break else: self.report( {'ERROR'}, "Error: This operation requires an Outliner to be present." ) return {'CANCELLED'} ```
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'}
### ID utilities
# (ID Python type, identifier string, database name)
ID_INFO = [
(types.WindowManager, 'WINDOWMANAGER', 'window_managers'),
@ -238,11 +414,9 @@ def get_datablock_icon(id) -> str:
return icon
class RemapTarget(bpy.types.PropertyGroup):
pass
def get_id_storage(id_type):
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
@ -250,201 +424,30 @@ def get_id_storage(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, width=800)
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'}
### 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
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'
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},
)