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 lock_curves
from .operators import bake_anim_across_armatures from .operators import bake_anim_across_armatures
from .operators import relink_overridden_asset from .operators import relink_overridden_asset
from .operators import id_management_ops from .operators import id_management_pie
from . import easy_constraints from . import easy_constraints
from . import warn_about_broken_libraries from . import warn_about_broken_libraries
from . import bone_selection_sets from . import bone_selection_sets
@ -31,7 +31,7 @@ modules = (
warn_about_broken_libraries, warn_about_broken_libraries,
bone_selection_sets, bone_selection_sets,
relink_overridden_asset, 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 from .relink_overridden_asset import OUTLINER_OT_relink_overridden_asset
def get_library_icon(library: bpy.types.Library): ### Pie Menu UI
if not library: class IDMAN_MT_relationship_pie(bpy.types.Menu):
return 'NONE' # bl_label is displayed at the center of the pie menu
icon = 'LIBRARY_DATA_DIRECT' bl_label = 'Datablock Relationships'
filepath = os.path.abspath(bpy.path.abspath(library.filepath)) bl_idname = 'IDMAN_MT_relationship_pie'
if not os.path.exists(filepath):
icon = 'LIBRARY_DATA_BROKEN' @staticmethod
return icon def get_id(context) -> Optional[bpy.types.ID]:
if context.area.type == 'OUTLINER' and len(context.selected_ids) > 0:
return context.selected_ids[0]
def get_library_by_filepath(filepath: str):
for lib in bpy.data.libraries: @classmethod
if lib.filepath == filepath: def poll(cls, context):
return lib 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: class RelationshipOperatorMixin:
datablock_name: StringProperty() datablock_name: StringProperty()
datablock_storage: 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)) 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:
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 Python type, identifier string, database name)
ID_INFO = [ ID_INFO = [
(types.WindowManager, 'WINDOWMANAGER', 'window_managers'), (types.WindowManager, 'WINDOWMANAGER', 'window_managers'),
@ -238,11 +414,9 @@ def get_datablock_icon(id) -> str:
return icon return icon
class RemapTarget(bpy.types.PropertyGroup): def get_id_storage(id_type) -> "bpy.data.something":
pass """Return the database of a certain ID Type, for example if you pass in an
Object, this will return bpy.data.objects."""
def get_id_storage(id_type):
storage = ID_TYPE_TO_STORAGE.get(id_type) storage = ID_TYPE_TO_STORAGE.get(id_type)
assert storage and hasattr(bpy.data, storage), ( assert storage and hasattr(bpy.data, storage), (
"Error: Storage not found for id type: " + id_type "Error: Storage not found for id type: " + id_type
@ -250,201 +424,30 @@ def get_id_storage(id_type):
return getattr(bpy.data, storage) return getattr(bpy.data, storage)
class OUTLINER_OT_remap_users(bpy.types.Operator): ### Library utilities
"""A wrapper around Blender's built-in Remap Users operator""" def get_library_icon(library: bpy.types.Library) -> str:
"""Return the library or the broken library icon, as appropriate."""
bl_idname = "outliner.remap_users" if not library:
bl_label = "Remap Users" return 'NONE'
bl_options = {'INTERNAL', 'UNDO'} icon = 'LIBRARY_DATA_DIRECT'
filepath = os.path.abspath(bpy.path.abspath(library.filepath))
def update_library_path(self, context): if not os.path.exists(filepath):
def get_source_id(): icon = 'LIBRARY_DATA_BROKEN'
# WTF??? When I try to access this through self, it says it doesn't exist... so I had to duplicate it!? TODO return icon
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'}
class IDMAN_MT_relationship_pie(bpy.types.Menu): def get_library_by_filepath(filepath: str):
# bl_label is displayed at the center of the pie menu for lib in bpy.data.libraries:
bl_label = 'Datablock Relationships' if lib.filepath == filepath:
bl_idname = 'IDMAN_MT_relationship_pie' return lib
@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'
registry = [ registry = [
IDMAN_MT_relationship_pie,
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,
OUTLINER_OT_remap_users,
RemapTarget, RemapTarget,
OUTLINER_OT_remap_users,
] ]
@ -453,15 +456,6 @@ 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',
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}, op_kwargs={'name': IDMAN_MT_relationship_pie.bl_idname},
) )