From 1750818ef03d4213e4966708a259f57c7b8eec57 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 10 Jul 2023 16:36:15 +0200 Subject: [PATCH 01/11] AnimCupboard: Relationship viewer & pie menu --- .../addons/anim_cupboard/__init__.py | 4 +- .../operators/id_management_ops.py | 268 ++++++++++++++++++ .../addons/anim_cupboard/utils/hotkeys.py | 188 ++++++++++++ 3 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 scripts-blender/addons/anim_cupboard/operators/id_management_ops.py create mode 100644 scripts-blender/addons/anim_cupboard/utils/hotkeys.py diff --git a/scripts-blender/addons/anim_cupboard/__init__.py b/scripts-blender/addons/anim_cupboard/__init__.py index 85d929f1..5c66dc72 100644 --- a/scripts-blender/addons/anim_cupboard/__init__.py +++ b/scripts-blender/addons/anim_cupboard/__init__.py @@ -7,6 +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 . import easy_constraints from . import warn_about_broken_libraries from . import bone_selection_sets @@ -29,7 +30,8 @@ modules = ( easy_constraints, warn_about_broken_libraries, bone_selection_sets, - relink_overridden_asset + relink_overridden_asset, + id_management_ops, ) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py new file mode 100644 index 00000000..8dc29129 --- /dev/null +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py @@ -0,0 +1,268 @@ +import bpy +from bpy import types +from typing import List, Tuple, Dict, Optional +from bpy.props import StringProperty +from bpy_extras import id_map_utils + +import os +from ..utils import hotkeys + + +class RelationshipOperatorMixin: + datablock_name: StringProperty() + datablock_storage: StringProperty() + library_filepath: StringProperty() + + def get_datablock(self, context) -> Optional[bpy.types.ID]: + if self.datablock_name and self.datablock_storage: + storage = getattr(bpy.data, self.datablock_storage) + lib_path = self.library_filepath or None + return storage.get((self.datablock_name, lib_path)) + elif context.area.type == 'OUTLINER' and len(context.selected_ids) > 0: + return context.selected_ids[0] + + @classmethod + def poll(cls, context): + return context.area.type == 'OUTLINER' and len(context.selected_ids) > 0 + + def invoke(self, context, _event): + return context.window_manager.invoke_props_dialog(self, width=600) + + def get_datablocks_to_display(self, id: bpy.types.ID) -> List[bpy.types.ID]: + raise NotImplementedError + + def get_label(self): + return "Showing users of datablock:" + + def draw(self, context): + layout = self.layout + layout.use_property_decorate = False + layout.use_property_split = True + + datablock = self.get_datablock(context) + if not datablock: + layout.alert = True + layout.label( + text=f"Failed to find datablock: {self.datablock_storage}, {self.datablock_name}, {self.library_filepath}") + return + + row = layout.row() + split = row.split() + row = split.row() + row.alignment = 'RIGHT' + row.label(text=self.get_label()) + id_row = split.row(align=True) + name_row = id_row.row() + name_row.enabled = False + name_row.prop(datablock, 'name', + icon=get_datablock_icon(datablock), text="") + fake_user_row = id_row.row() + fake_user_row.prop(datablock, 'use_fake_user', text="") + + layout.separator() + + for user in self.get_datablocks_to_display(datablock): + if user == datablock: + # Scenes are users of themself for technical reasons, + # I think it's confusing to display that. + continue + row = layout.row() + name_row = row.row() + name_row.enabled = False + name_row.prop(user, 'name', icon=get_datablock_icon(user), text="") + op_row = row.row() + op = op_row.operator(type(self).bl_idname, + text="", icon='LOOP_FORWARDS') + op.datablock_name = user.name + storage = ID_CLASS_TO_STORAGE.get(type(user)) + if not storage: + print("Error: Can't find storage: ", type(user)) + op.datablock_storage = storage + if user.library: + op.library_filepath = user.library.filepath + icon = 'LIBRARY_DATA_DIRECT' + filepath = os.path.abspath( + bpy.path.abspath(user.library.filepath)) + if not os.path.exists(filepath): + icon = 'LIBRARY_DATA_BROKEN' + name_row.prop(user.library, 'filepath', icon=icon, text="") + + def execute(self, context): + return {'FINISHED'} + + +class OUTLINER_OT_list_users_of_datablock(RelationshipOperatorMixin, bpy.types.Operator): + """Show list of users of this datablock""" + bl_idname = "object.list_datablock_users" + bl_label = "List Datablock Users" + + datablock_name: StringProperty() + datablock_storage: StringProperty() + library_filepath: StringProperty() + + def get_datablocks_to_display(self, datablock: bpy.types.ID) -> List[bpy.types.ID]: + user_map = bpy.data.user_map() + users = user_map[datablock] + return sorted(users, key=lambda u: (str(type(u)), u.name)) + + +class OUTLINER_OT_list_dependencies_of_datablock(RelationshipOperatorMixin, bpy.types.Operator): + """Show list of dependencies of this datablock""" + bl_idname = "object.list_datablock_dependencies" + bl_label = "List Datablock Dependencies" + + def get_label(self): + return "Showing dependencies of datablock:" + + def get_datablocks_to_display(self, datablock: bpy.types.ID) -> List[bpy.types.ID]: + dependencies = id_map_utils.get_id_reference_map().get(datablock) + if not dependencies: + return [] + 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 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 'QUESTION' + icon = ID_TYPE_STR_TO_ICON.get(identifier_str) + if not icon: + return 'QUESTION' + 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 = 'MT_relationships' + + 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') + # ^ + pie.operator('outliner.better_purge', icon='TRASH') + # V + pie.operator('outliner.id_operation', icon='FILE_REFRESH', + text="Remap Users").type = 'REMAP' + + # Dict[str, Tuple[str, str]]: + if isinstance(bpy_type, Operator): + try: + enum_items = bpy_type.__annotations__[ + enum_prop_name].keywords['items'] + return {e[0]: (e[1], e[2]) for e in enum_items} + except: + return + + enum_items = bpy_type.bl_rna.properties[enum_prop_name].enum_items + return {e.identifier: (e.name, e.description) for e in enum_items} + + +def is_valid_key_id(key_id: str) -> bool: + all_valid_key_identifiers = get_enum_values(KeyMapItem, 'type') + is_valid = key_id in all_valid_key_identifiers + if not is_valid: + print("All valid key identifiers and names:") + print("\n".join(list(all_valid_key_identifiers.items()))) + print( + f'\nShortcut error: "{key_id}" is not a valid key identifier. Must be one of the above.') + return is_valid + + +def is_valid_event_type(event_type: str) -> bool: + all_valid_event_types = get_enum_values(KeyMapItem, 'value') + is_valid = event_type in all_valid_event_types + if not is_valid: + print("All valid event names:") + print("\n".join(list(all_valid_event_types.keys()))) + print( + f'\nShortcut Error: "{event_type}" is not a valid event type. Must be one of the above.') + return is_valid + + +def get_all_keymap_names() -> List[str]: + return bpy.context.window_manager.keyconfigs.default.keymaps.keys() + + +def is_valid_keymap_name(km_name: str) -> bool: + all_km_names = get_all_keymap_names() + is_valid = km_name in all_km_names + if not is_valid: + print("All valid keymap names:") + print("\n".join(all_km_names)) + print( + f'\nShortcut Error: "{km_name}" is not a valid keymap name. Must be one of the above.') + return is_valid + + +def get_space_type_of_keymap(km_name: str) -> str: + return bpy.context.window_manager.keyconfigs.default.keymaps[km_name].space_type + + +def find_operator_class_by_bl_idname(bl_idname: str) -> Type[Operator]: + for cl in Operator.__subclasses__(): + if cl.bl_idname == bl_idname: + return cl + + +def find_keymap_item_by_trigger( + keymap, + bl_idname: str, + key_id: str, + ctrl=False, + alt=False, + shift=False, + oskey=False +) -> Optional[KeyMapItem]: + + for kmi in keymap.keymap_items: + if ( + kmi.idname == bl_idname and + kmi.type == key_id and + kmi.ctrl == ctrl and + kmi.alt == alt and + kmi.shift == shift and + kmi.oskey == oskey + ): + return kmi + + +def find_keymap_item_by_op_kwargs( + keymap, + bl_idname: str, + op_kwargs={} +) -> Optional[KeyMapItem]: + + for kmi in keymap.keymap_items: + if kmi.idname != bl_idname: + continue + + op_class = find_operator_class_by_bl_idname(bl_idname) + + if set(kmi.properties.keys()) != set(op_kwargs.keys()): + continue + + any_mismatch = False + for prop_name in kmi.properties.keys(): + # Check for enum string + enum_dict = get_enum_values(op_class, prop_name) + if enum_dict: + value = enum_dict[op_kwargs[prop_name]] + else: + value = kmi.properties[prop_name] + + if value != op_kwargs[prop_name]: + any_mismatch = True + break + + if any_mismatch: + continue + + return kmi + + +def register_hotkey( + *, + bl_idname: str, + km_name='Window', + key_id: str, + + event_type='PRESS', + + any=False, + ctrl=False, + alt=False, + shift=False, + oskey=False, + key_modifier='NONE', + direction='ANY', + repeat=False, + + op_kwargs={} +): + wm = bpy.context.window_manager + kc = wm.keyconfigs.addon + if not kc: + # This happens when running Blender in background mode. + return + + if not is_valid_keymap_name(km_name): + return + if not is_valid_key_id(key_id): + return + if not is_valid_event_type(event_type): + return + + # If this keymap already exists, new() will return the existing one, which is confusing but ideal. + km = kc.keymaps.new( + name=km_name, space_type=get_space_type_of_keymap(km_name)) + + kmi_existing = find_keymap_item_by_trigger( + km, + bl_idname=bl_idname, + key_id=key_id, + ctrl=ctrl, + alt=alt, + shift=shift, + oskey=oskey + ) + if kmi_existing: + return + + kmi = km.keymap_items.new( + bl_idname, + type=key_id, + value=event_type, + + any=any, + ctrl=ctrl, + alt=alt, + shift=shift, + oskey=oskey, + key_modifier=key_modifier, + + direction=direction, + repeat=repeat, + ) + + for key in op_kwargs: + value = op_kwargs[key] + setattr(kmi.properties, key, value) -- 2.30.2 From 999c96138fe8c944235c224427123d8a283ae88f Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Fri, 14 Jul 2023 18:48:55 +0200 Subject: [PATCH 02/11] Don't display unavailable operators --- .../operators/id_management_ops.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py index 8dc29129..c5f1ab4a 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py @@ -6,6 +6,7 @@ from bpy_extras import id_map_utils import os from ..utils import hotkeys +from .relink_overridden_asset import OUTLINER_OT_relink_overridden_asset class RelationshipOperatorMixin: @@ -61,6 +62,11 @@ class RelationshipOperatorMixin: layout.separator() + datablocks = self.get_datablocks_to_display(datablock) + if not datablocks: + layout.label(text="There are none.") + return + for user in self.get_datablocks_to_display(datablock): if user == datablock: # Scenes are users of themself for technical reasons, @@ -226,18 +232,23 @@ class IDMAN_MT_relationship_pie(bpy.types.Menu): # > pie.operator( OUTLINER_OT_list_dependencies_of_datablock.bl_idname, icon='LOOP_FORWARDS') - # ^ - pie.operator('outliner.better_purge', icon='TRASH') # V + pie.operator('outliner.better_purge', icon='TRASH') + # ^ + pie.operator('outliner.id_operation', icon='FILE_REFRESH', text="Remap Users").type = 'REMAP' - # + id = OUTLINER_OT_relink_overridden_asset.get_id(context) + if id: + pie.operator('object.relink_overridden_asset', + icon='LIBRARY_DATA_OVERRIDE') - pie.operator('outliner.liboverride_troubleshoot_operation', icon='UV_SYNC_SELECT', - text="Resync Override Hierarchy").type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE' + # <^ + 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 = [ -- 2.30.2 From 375d1137fa50241a3df079067568eb05ce964e97 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Fri, 14 Jul 2023 20:25:00 +0200 Subject: [PATCH 03/11] Relationship Pie: Implement custom Remap Users UX --- .../operators/id_management_ops.py | 362 +++++++++++++----- 1 file changed, 276 insertions(+), 86 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py index c5f1ab4a..428531ad 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py @@ -1,7 +1,7 @@ import bpy from bpy import types 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 import os @@ -9,6 +9,22 @@ 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 + + class RelationshipOperatorMixin: datablock_name: StringProperty() datablock_storage: StringProperty() @@ -44,7 +60,8 @@ class RelationshipOperatorMixin: if not datablock: layout.alert = True 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 row = layout.row() @@ -55,8 +72,7 @@ class RelationshipOperatorMixin: id_row = split.row(align=True) name_row = id_row.row() name_row.enabled = False - name_row.prop(datablock, 'name', - icon=get_datablock_icon(datablock), text="") + name_row.prop(datablock, 'name', icon=get_datablock_icon(datablock), text="") fake_user_row = id_row.row() fake_user_row.prop(datablock, 'use_fake_user', text="") @@ -77,8 +93,7 @@ class RelationshipOperatorMixin: name_row.enabled = False name_row.prop(user, 'name', icon=get_datablock_icon(user), text="") op_row = row.row() - op = op_row.operator(type(self).bl_idname, - text="", icon='LOOP_FORWARDS') + op = op_row.operator(type(self).bl_idname, text="", icon='LOOP_FORWARDS') op.datablock_name = user.name storage = ID_CLASS_TO_STORAGE.get(type(user)) if not storage: @@ -86,19 +101,22 @@ class RelationshipOperatorMixin: op.datablock_storage = storage if user.library: op.library_filepath = user.library.filepath - icon = 'LIBRARY_DATA_DIRECT' - filepath = os.path.abspath( - bpy.path.abspath(user.library.filepath)) - if not os.path.exists(filepath): - icon = 'LIBRARY_DATA_BROKEN' - name_row.prop(user.library, 'filepath', icon=icon, text="") + name_row.prop( + user.library, + 'filepath', + icon=get_library_icon(user.library), + text="", + ) def execute(self, context): 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""" + bl_idname = "object.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)) -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""" + bl_idname = "object.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_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'), + (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'), ] @@ -184,23 +200,27 @@ def get_datablock_icon_map() -> Dict[str, str]: """ 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', - }) + 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} @@ -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: identifier_str = ID_CLASS_TO_IDENTIFIER.get(type(id)) if not identifier_str: - return 'QUESTION' + return 'NONE' icon = ID_TYPE_STR_TO_ICON.get(identifier_str) if not icon: - return 'QUESTION' + return 'NONE' 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): # bl_label is displayed at the center of the pie menu 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): 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_users_of_datablock.bl_idname, icon='LOOP_BACK') # > 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 pie.operator('outliner.better_purge', icon='TRASH') # ^ - pie.operator('outliner.id_operation', icon='FILE_REFRESH', - text="Remap Users").type = 'REMAP' + 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') + 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' + pie.operator( + 'outliner.liboverride_troubleshoot_operation', + icon='UV_SYNC_SELECT', + text="Resync Override Hierarchy", + ).type = 'OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE' registry = [ OUTLINER_OT_list_users_of_datablock, OUTLINER_OT_list_dependencies_of_datablock, IDMAN_MT_relationship_pie, + OUTLINER_OT_remap_users, + RemapTarget, ] @@ -263,9 +453,7 @@ def register(): bl_idname='wm.call_menu_pie', km_name='Outliner', key_id='Y', - event_type='PRESS', - any=False, ctrl=False, alt=False, @@ -274,6 +462,8 @@ def register(): 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}, ) + + bpy.types.Scene.remap_targets = CollectionProperty(type=RemapTarget) + bpy.types.Scene.remap_target_libraries = CollectionProperty(type=RemapTarget) -- 2.30.2 From b4eded6cff78860af38b9741b0c4bdc5e82d59ed Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Fri, 14 Jul 2023 20:25:30 +0200 Subject: [PATCH 04/11] wider. --- .../addons/anim_cupboard/operators/id_management_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py index 428531ad..c636f525 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py @@ -43,7 +43,7 @@ class RelationshipOperatorMixin: return context.area.type == 'OUTLINER' and len(context.selected_ids) > 0 def invoke(self, context, _event): - return context.window_manager.invoke_props_dialog(self, width=600) + return context.window_manager.invoke_props_dialog(self, width=800) def get_datablocks_to_display(self, id: bpy.types.ID) -> List[bpy.types.ID]: raise NotImplementedError -- 2.30.2 From 5de2af1eb940fef199903074ac5edec1f70b634f Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Fri, 14 Jul 2023 20:30:07 +0200 Subject: [PATCH 05/11] more wider --- .../addons/anim_cupboard/operators/id_management_ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py index c636f525..7c33fc87 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py @@ -43,7 +43,7 @@ class RelationshipOperatorMixin: return context.area.type == 'OUTLINER' and len(context.selected_ids) > 0 def invoke(self, context, _event): - return context.window_manager.invoke_props_dialog(self, width=800) + return context.window_manager.invoke_props_dialog(self, width=600) def get_datablocks_to_display(self, id: bpy.types.ID) -> List[bpy.types.ID]: raise NotImplementedError @@ -327,7 +327,7 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): self.library_path = "Local Data" - return context.window_manager.invoke_props_dialog(self) + return context.window_manager.invoke_props_dialog(self, width=800) def draw(self, context): layout = self.layout -- 2.30.2 From ba73eec495584dd56aad94f759dbca536929cb89 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 17 Jul 2023 18:29:42 +0200 Subject: [PATCH 06/11] Rename to id_management_pie.py and clean up --- .../addons/anim_cupboard/__init__.py | 4 +- ...management_ops.py => id_management_pie.py} | 424 +++++++++--------- 2 files changed, 211 insertions(+), 217 deletions(-) rename scripts-blender/addons/anim_cupboard/operators/{id_management_ops.py => id_management_pie.py} (96%) diff --git a/scripts-blender/addons/anim_cupboard/__init__.py b/scripts-blender/addons/anim_cupboard/__init__.py index 5c66dc72..6f0aa375 100644 --- a/scripts-blender/addons/anim_cupboard/__init__.py +++ b/scripts-blender/addons/anim_cupboard/__init__.py @@ -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, ) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py similarity index 96% rename from scripts-blender/addons/anim_cupboard/operators/id_management_ops.py rename to scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index 7c33fc87..a88cf462 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_ops.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -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: + 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}, ) -- 2.30.2 From 6e489eb1f6251e9d499d63799f5227af310d77aa Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 17 Jul 2023 19:09:01 +0200 Subject: [PATCH 07/11] More artist-friendly UI strings --- .../anim_cupboard/operators/id_management_pie.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index a88cf462..12fdff7f 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -88,7 +88,7 @@ class RelationshipOperatorMixin: raise NotImplementedError def get_label(self): - return "Showing users of datablock:" + return "Listing datablocks that reference this:" def draw(self, context): layout = self.layout @@ -178,7 +178,7 @@ class OUTLINER_OT_list_dependencies_of_datablock( bl_label = "List Datablock Dependencies" def get_label(self): - return "Showing dependencies of datablock:" + return "Listing datablocks that are referenced by this:" def get_datablocks_to_display(self, datablock: bpy.types.ID) -> List[bpy.types.ID]: dependencies = id_map_utils.get_id_reference_map().get(datablock) @@ -281,14 +281,14 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): id = self.get_source_id() id_icon = get_datablock_icon(id) split = row.split() - split.row().label(text="Remap Users of ID:") + split.row().label(text="Anything that was referencing this:") 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: ") + col.label(text="Will now reference this instead: ") if len(scene.remap_target_libraries) > 1: col.prop_search( self, @@ -302,7 +302,7 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): 'id_name_target', scene, 'remap_targets', - text="Remap To", + text="Datablock", icon=id_icon, ) -- 2.30.2 From 2f120ba69cc4519e9334a01be2e25f4a6af0d793 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 18 Jul 2023 13:03:37 +0200 Subject: [PATCH 08/11] Handle unknown ID types for future Also work around issue 110239 --- .../operators/id_management_pie.py | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index 12fdff7f..198af183 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -37,17 +37,18 @@ class IDMAN_MT_relationship_pie(bpy.types.Menu): 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 + if id_type: + remap = pie.operator( + 'outliner.remap_users', icon='FILE_REFRESH', text="Remap Users" + ) + remap.id_type = id_type + remap.id_name_source = id.name + if id.library: + remap.library_path_source = id.library.filepath + else: + pie.label(text="Cannot remap unknwon ID type: " + str(type(id))) # ^> id = OUTLINER_OT_relink_overridden_asset.get_id(context) @@ -200,17 +201,10 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): 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() + source_id = get_id(self.id_name_source, self.id_type, self.library_path_source) for id in get_id_storage(self.id_type): if id == source_id: continue @@ -220,18 +214,6 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): 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", @@ -259,10 +241,10 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): remap_target_libraries.clear() local = remap_target_libraries.add() local.name = "Local Data" - source_id_type = type(self.get_source_id()) + source_id = get_id(self.id_name_source, self.id_type, self.library_path_source) for lib in bpy.data.libraries: for id in lib.users_id: - if type(id) == source_id_type: + if type(id) == type(source_id): lib_entry = remap_target_libraries.add() lib_entry.name = lib.filepath break @@ -278,7 +260,7 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): scene = context.scene row = layout.row() - id = self.get_source_id() + id = get_id(self.id_name_source, self.id_type, self.library_path_source) id_icon = get_datablock_icon(id) split = row.split() split.row().label(text="Anything that was referencing this:") @@ -316,8 +298,8 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): ) return {'CANCELLED'} - source_id = self.get_source_id() - target_id = self.get_target_id() + source_id = get_id(self.id_name_source, self.id_type, self.library_path_source) + target_id = get_id(self.id_name_target, self.id_type, self.library_path) source_id.user_remap(target_id) return {'FINISHED'} @@ -424,6 +406,13 @@ def get_id_storage(id_type) -> "bpy.data.something": return getattr(bpy.data, storage) +def get_id(id_name: str, id_type: str, lib_path=""): + storage = get_id_storage(id_type) + if lib_path: + return storage.get((id_name, lib_path)) + return storage.get((id_name, None)) + + ### Library utilities def get_library_icon(library: bpy.types.Library) -> str: """Return the library or the broken library icon, as appropriate.""" -- 2.30.2 From 3f601f7a9f80a958233e76552b8cbed33abbc9d3 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 18 Jul 2023 15:49:38 +0200 Subject: [PATCH 09/11] Use last selected ID instead of first --- .../addons/anim_cupboard/operators/id_management_pie.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index 198af183..1eb427d5 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -18,7 +18,7 @@ class IDMAN_MT_relationship_pie(bpy.types.Menu): @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] + return context.selected_ids[-1] @classmethod def poll(cls, context): @@ -76,7 +76,7 @@ class RelationshipOperatorMixin: lib_path = self.library_filepath or None return storage.get((self.datablock_name, lib_path)) elif context.area.type == 'OUTLINER' and len(context.selected_ids) > 0: - return context.selected_ids[0] + return context.selected_ids[-1] @classmethod def poll(cls, context): -- 2.30.2 From b7fc968c952b8d126cb79ee2fe909a22c11726f8 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 18 Jul 2023 17:00:51 +0200 Subject: [PATCH 10/11] Fix an error --- .../anim_cupboard/operators/id_management_pie.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index 1eb427d5..c6f59e86 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -300,6 +300,13 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): source_id = get_id(self.id_name_source, self.id_type, self.library_path_source) target_id = get_id(self.id_name_target, self.id_type, self.library_path) + if not target_id: + self.report( + {'ERROR'}, + f'Failed to find ID: {self.id_name_target}, {self.id_type}, lib: {self.library_path}', + ) + return {'CANCELLED'} + source_id.user_remap(target_id) return {'FINISHED'} @@ -406,9 +413,9 @@ def get_id_storage(id_type) -> "bpy.data.something": return getattr(bpy.data, storage) -def get_id(id_name: str, id_type: str, lib_path=""): +def get_id(id_name: str, id_type: str, lib_path="") -> bpy.types.ID: storage = get_id_storage(id_type) - if lib_path: + if lib_path and lib_path != 'Local Data': return storage.get((id_name, lib_path)) return storage.get((id_name, None)) -- 2.30.2 From 1a245cc7c2b486e4e6a9c2b0f9603040232c7263 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Wed, 19 Jul 2023 14:40:56 +0200 Subject: [PATCH 11/11] Remove some unnecessary code --- .../operators/id_management_pie.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py index c6f59e86..72399e64 100644 --- a/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py +++ b/scripts-blender/addons/anim_cupboard/operators/id_management_pie.py @@ -289,26 +289,11 @@ class OUTLINER_OT_remap_users(bpy.types.Operator): ) 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 = get_id(self.id_name_source, self.id_type, self.library_path_source) target_id = get_id(self.id_name_target, self.id_type, self.library_path) - if not target_id: - self.report( - {'ERROR'}, - f'Failed to find ID: {self.id_name_target}, {self.id_type}, lib: {self.library_path}', - ) - return {'CANCELLED'} + assert source_id and target_id, "Error: Failed to find source or target." source_id.user_remap(target_id) - return {'FINISHED'} -- 2.30.2