Is there a built‑in handler, a msgbus subscription, or a different technique that can notify me when a UV layer is removed from a mesh?
Ideally, I would also like to know the index and name of the removed layer.
I’ve tried implementing something like this until I realized that operator class for bpy.ops.mesh.uv_texture_remove() doesn’t exist.
Python
import bpy
original_uv_remove = None
class MESH_OT_uv_layer_remove_override(bpy.types.Operator):
bl_idname = "mesh.uv_layer_remove"
bl_label = "Remove UV Map"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj and obj.type == 'MESH' and obj.data.uv_layers.active)
def execute(self, context):
obj = context.active_object
active_layer = obj.data.uv_layers.active
if active_layer:
index = obj.data.uv_layers.find(active_layer.name)
print(f"Removing UV layer: index={index}, name='{active_layer.name}'")
else:
self.report({'WARNING'}, "No active UV layer found")
return {'CANCELLED'}
return original_uv_remove.execute(self, context)
def register_override():
global original_uv_remove
original_uv_remove = bpy.types.MESH_OT_uv_layer_remove
bpy.utils.unregister_class(original_uv_remove)
bpy.utils.register_class(MESH_OT_uv_layer_remove_override)
def unregister_override():
bpy.utils.unregister_class(MESH_OT_uv_layer_remove_override)
bpy.utils.register_class(original_uv_remove)
if __name__ == "__main__":
register_override()
To me it looked like there were two approaches to this problem with only one of them working in the end.
First approach (does not work as intended):
This method uses bpy.msgbus to listen for changes to a custom IntProperty on bpy.types.Object . The property’s value is driven by a driver that evaluates len(obj.data.uv_layers) . The fatal flaw is that the driver does not automatically re‑evaluate after a UV layer is added or removed , so no change is registered and the message bus never triggers.
In theory this approach would have been ideal, because it would also detect programmatic UV layer changes, but due to the driver evaluation issue it is useless in practice.
import bpy
target_object = None
def on_uv_layer_count_changed():
print("something")
def register_msgbus_for_object(obj):
global target_object
target_object = obj
bpy.types.Object.uv_layer_count = bpy.props.IntProperty(name="UV Layer Count", default=0)
subscribe_to = obj.path_resolve("uv_layer_count", False)
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=obj,
args=(),
notify=on_uv_layer_count_changed,
options=set(["PERSISTENT"])
)
add_driver_to_uv_layer_count(obj)
def add_driver_to_uv_layer_count(obj):
# Ensure the object has the custom RNA property (it exists on all objects now)
# But we need to make sure the driver's variable points to obj.data.uv_layers
if not obj.animation_data:
obj.animation_data_create()
data_path = 'uv_layer_count' # RNA property, not IDProperty
fcurve = None
for fc in obj.animation_data.drivers:
if fc.data_path == data_path:
fcurve = fc
break
if not fcurve:
fcurve = obj.animation_data.drivers.new(data_path)
driver = fcurve.driver
driver.type = 'SCRIPTED'
#driver.variables.clear()
var = driver.variables.new()
var.name = "uv_layer_count"
var.targets[0].id = obj
var.targets[0].data_path = "data.uv_layers"
driver.expression = "len(uv_layer_count)"
# Force initial update
obj.uv_layer_count = len(obj.data.uv_layers)
def cleanup_msgbus(obj):
bpy.msgbus.clear_by_owner(obj)
if obj.animation_data:
for fc in obj.animation_data.drivers:
if fc.data_path == 'uv_layer_count':
obj.animation_data.drivers.remove(fc)
break
if __name__ == "__main__":
register_msgbus_for_object(bpy.context.object)
Second approach (works, but with limitations):
This method periodically checks the operator history (bpy.context.window_manager.operators ) to see if bpy.ops.mesh.uv_texture_remove() or bpy.ops.mesh.uv_texture_add() has been called. When a relevant operator is found, it triggers a callback for the selected object.
The obvious drawback is that it only detects UV layer removals or additions performed manually by the user (through the UI or a direct operator call). It cannot detect programmatic changes to the UV layer list made via Python scripts that bypass the dedicated operators.
import bpy
class UVLayerChangeDetector:
def __init__(self):
self.last_operator_count = 0
self.operator_cache = []
def check_for_changes(self):
# Get the current operator history
current_operators = bpy.context.window_manager.operators
current_count = len(current_operators)
if current_count > self.last_operator_count:
# Get only the new operators
new_ops = current_operators[self.last_operator_count:current_count]
for op in new_ops:
# Check for UV texture removal or addition
if op.bl_idname == "MESH_OT_uv_texture_remove":
print(f"UV layer removal detected! Operator called: {op.bl_idname}")
self.on_uv_layer_removed()
elif op.bl_idname == "MESH_OT_uv_texture_add":
print(f"UV layer addition detected!")
self.on_uv_layer_added()
# Update our cache
self.last_operator_count = current_count
self.operator_cache = [op.bl_idname for op in current_operators]
return 0.125
def on_uv_layer_removed(self):
if bpy.context.active_object and bpy.context.active_object.type == 'MESH':
current_count = len(bpy.context.active_object.data.uv_layers)
print(f"Current UV layer count: {current_count}")
def on_uv_layer_added(self):
# Your custom logic for when UV layers are added
print("UV layer was added!")
detector = UVLayerChangeDetector()
bpy.app.timers.register(detector.check_for_changes)
As of right now I will not be marking this as the solution, since I’m confident that more experienced community members will step in with their own improved takes on this as well as some advice.
EDIT:
No additional input from others after couple of days. Marking this as the solution.