AnimCupboard: ID Management Pie #127
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user
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...