AnimCupboard: ID Management Pie #127
@ -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_pie
|
||||
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_pie,
|
||||
)
|
||||
|
||||
|
||||
|
@ -0,0 +1,444 @@
|
||||
import bpy
|
||||
from bpy import types
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
from bpy.props import StringProperty, CollectionProperty
|
||||
from bpy_extras import id_map_utils
|
||||
|
||||
import os
|
||||
from ..utils import hotkeys
|
||||
from .relink_overridden_asset import OUTLINER_OT_relink_overridden_asset
|
||||
|
||||
|
||||
### 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[-1]
|
||||
|
||||
@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')
|
||||
# ^
|
||||
|
||||
id = self.get_id(context)
|
||||
id_type = ID_CLASS_TO_IDENTIFIER.get(type(id))
|
||||
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)
|
||||
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()
|
||||
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[-1]
|
||||
|
||||
@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 "Listing datablocks that reference this:"
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
# 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
|
||||
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
|
||||
):
|
||||
"""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 "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)
|
||||
if not dependencies:
|
||||
return []
|
||||
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):
|
||||
# Prepare the ID selector.
|
||||
remap_targets = context.scene.remap_targets
|
||||
remap_targets.clear()
|
||||
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
|
||||
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
|
||||
|
||||
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 = 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) == type(source_id):
|
||||
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 = 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:")
|
||||
row = split.row()
|
||||
row.prop(self, 'id_name_source', text="", icon=id_icon)
|
||||
row.enabled = False
|
||||
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
col.label(text="Will now reference this instead: ")
|
||||
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="Datablock",
|
||||
icon=id_icon,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
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)
|
||||
assert source_id and target_id, "Error: Failed to find source or target."
|
||||
|
||||
source_id.user_remap(target_id)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
### ID utilities
|
||||
# (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 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}
|
||||
# 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 'NONE'
|
||||
icon = ID_TYPE_STR_TO_ICON.get(identifier_str)
|
||||
if not icon:
|
||||
return 'NONE'
|
||||
return icon
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
return getattr(bpy.data, storage)
|
||||
|
||||
|
||||
def get_id(id_name: str, id_type: str, lib_path="") -> bpy.types.ID:
|
||||
storage = get_id_storage(id_type)
|
||||
if lib_path and lib_path != 'Local Data':
|
||||
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."""
|
||||
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
|
||||
|
||||
|
||||
registry = [
|
||||
IDMAN_MT_relationship_pie,
|
||||
OUTLINER_OT_list_users_of_datablock,
|
||||
OUTLINER_OT_list_dependencies_of_datablock,
|
||||
RemapTarget,
|
||||
OUTLINER_OT_remap_users,
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
hotkeys.register_hotkey(
|
||||
bl_idname='wm.call_menu_pie',
|
||||
km_name='Outliner',
|
||||
key_id='Y',
|
||||
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)
|
188
scripts-blender/addons/anim_cupboard/utils/hotkeys.py
Normal file
188
scripts-blender/addons/anim_cupboard/utils/hotkeys.py
Normal file
@ -0,0 +1,188 @@
|
||||
from typing import List, Dict, Tuple, Optional, Type
|
||||
import bpy
|
||||
from bpy.types import KeyMapItem, Operator
|
||||
|
||||
|
||||
def get_enum_values(bpy_type, enum_prop_name: str) -> 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)
|
Loading…
Reference in New Issue
Block a user