264 lines
9.2 KiB
Python
264 lines
9.2 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import bpy
|
|
from bpy.props import StringProperty, CollectionProperty
|
|
from ..id_types import get_id, get_id_storage_by_type_str, get_datablock_icon, get_library_icon
|
|
|
|
|
|
class BLENLOG_OT_report_fake_users(bpy.types.Operator):
|
|
"""Report Fake User IDs. Ignores Text and Brush IDs"""
|
|
|
|
bl_idname = "blenlog.report_fake_users"
|
|
bl_label = "Report Fake Users"
|
|
bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
user_map = bpy.data.user_map()
|
|
|
|
blenlog = context.scene.blender_log
|
|
|
|
category = "Fake User ID"
|
|
blenlog.clear_category(category)
|
|
|
|
for id, users in user_map.items():
|
|
if id.library or id.override_library:
|
|
continue
|
|
if id.id_type not in {'BRUSH', 'TEXT'} and id.use_fake_user:
|
|
blenlog.add(
|
|
name=f"{id.id_type}: {id.name} (Users: {len(users)})",
|
|
category=category,
|
|
description="Datablocks with fake users can cause further referenced datablocks to linger in the file. It is recommended not to use fake users, in order to keep files clear of trash data.",
|
|
icon='FAKE_USER_ON',
|
|
operator=BLENLOG_OT_clear_fake_user.bl_idname,
|
|
op_kwargs={'id_name': id.name, 'id_type': id.id_type},
|
|
op_icon='FAKE_USER_OFF',
|
|
)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class BLENLOG_OT_remap_users(bpy.types.Operator):
|
|
"""Remap users of an ID to another of the same type"""
|
|
|
|
bl_idname = "blenlog.remap_users"
|
|
bl_label = "Remap Users"
|
|
bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
|
|
|
|
redundant_id: StringProperty()
|
|
id_type: StringProperty()
|
|
preserved_id: StringProperty()
|
|
|
|
def execute(self, context):
|
|
redundant_id = get_id(self.redundant_id, self.id_type)
|
|
if not redundant_id:
|
|
self.report({'ERROR'}, f"ID no longer exists: {self.redundant_id}.")
|
|
return {'CANCELLED'}
|
|
preserved_id = get_id(self.preserved_id, self.id_type)
|
|
if not preserved_id:
|
|
self.report({'ERROR'}, f"ID no longer exists: {self.preserved_id}.")
|
|
return {'CANCELLED'}
|
|
|
|
redundant_id.user_remap(preserved_id)
|
|
redundant_id.use_fake_user = False
|
|
self.report({'INFO'}, f"{self.redundant_id} has been replaced with {self.preserved_id}")
|
|
|
|
context.scene.blender_log.remove_active()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
### Remap Users
|
|
class RemapTarget(bpy.types.PropertyGroup):
|
|
pass
|
|
|
|
|
|
# TODO: This code also exists in the 3D Viewport Pie Menus add-on. Keep code in sync at least?
|
|
class BLENLOG_OT_remap_users_ui(bpy.types.Operator):
|
|
"""Remap users of a selected ID to any other ID of the same type"""
|
|
|
|
bl_idname = "outliner.remap_users"
|
|
bl_label = "Remap Users"
|
|
bl_options = {'INTERNAL', 'UNDO'}
|
|
|
|
def update_library_path(self, context):
|
|
# Prepare the ID selector.
|
|
blenlog_remap_targets = context.scene.blenlog_remap_targets
|
|
blenlog_remap_targets.clear()
|
|
source_id = get_id(self.id_name_source, self.id_type, self.library_path_source)
|
|
for id in get_id_storage_by_type_str(self.id_type)[0]:
|
|
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 = blenlog_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 blenlog_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.
|
|
blenlog_remap_target_libs = context.scene.blenlog_remap_target_libs
|
|
blenlog_remap_target_libs.clear()
|
|
local = blenlog_remap_target_libs.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 = blenlog_remap_target_libs.add()
|
|
lib_entry.name = lib.filepath
|
|
break
|
|
|
|
self.library_path = "Local Data"
|
|
if source_id.name[-4] == ".":
|
|
storage = get_id_storage_by_type_str(self.id_type)[0]
|
|
suggestion = storage.get(source_id.name[:-4])
|
|
if suggestion:
|
|
self.id_name_target = suggestion.name
|
|
if suggestion.library:
|
|
self.library_path = suggestion.library.filepath
|
|
|
|
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.blenlog_remap_target_libs) > 1:
|
|
col.prop_search(
|
|
self,
|
|
'library_path',
|
|
scene,
|
|
'blenlog_remap_target_libs',
|
|
icon=get_library_icon(self.library_path),
|
|
)
|
|
col.prop_search(
|
|
self,
|
|
'id_name_target',
|
|
scene,
|
|
'blenlog_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'}
|
|
|
|
|
|
class BLENLOG_OT_clear_fake_user(bpy.types.Operator):
|
|
"""Clear the fake user flag of an ID."""
|
|
|
|
bl_idname = "blenlog.clear_fake_user"
|
|
bl_label = "Clear Fake User"
|
|
bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
|
|
|
|
id_name: StringProperty()
|
|
id_type: StringProperty()
|
|
|
|
def execute(self, context):
|
|
id = get_id(self.id_name, self.id_type)
|
|
if not id:
|
|
self.report({'INFO'}, f"{self.id_type} {self.id_name} had already been removed.")
|
|
else:
|
|
id.use_fake_user = False
|
|
self.report(
|
|
{'INFO'}, f"{self.id_type} {self.id_name} no longer marked with a fake user."
|
|
)
|
|
|
|
context.scene.blender_log.remove_active()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class BLENLOG_OT_report_missing_IDs(bpy.types.Operator):
|
|
"""Report Fake User IDs. Ignores Text and Brush IDs"""
|
|
|
|
bl_idname = "blenlog.report_missing_ids"
|
|
bl_label = "Report Missing IDs"
|
|
bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
user_map = bpy.data.user_map()
|
|
|
|
blenlog = context.scene.blender_log
|
|
|
|
category = "Missing ID"
|
|
blenlog.clear_category(category)
|
|
|
|
for id, users in user_map.items():
|
|
|
|
if id.library or id.override_library:
|
|
continue
|
|
if id.id_type not in {'BRUSH', 'TEXT'} and id.use_fake_user:
|
|
blenlog.add(
|
|
name=f"{id.id_type}: {id.name} (Users: {len(users)})",
|
|
category=category,
|
|
description="A linked ID was being referenced locally, and then removed from its library. The missing ID can be remapped to another.",
|
|
icon='LIBRARY_DATA_BROKEN',
|
|
operator=BLENLOG_OT_clear_fake_user.bl_idname,
|
|
op_kwargs={'id_name': id.name, 'id_type': id.id_type},
|
|
op_icon='FAKE_USER_OFF',
|
|
)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
registry = [
|
|
BLENLOG_OT_report_fake_users,
|
|
BLENLOG_OT_remap_users,
|
|
RemapTarget,
|
|
BLENLOG_OT_remap_users_ui,
|
|
BLENLOG_OT_clear_fake_user,
|
|
]
|
|
|
|
|
|
def register():
|
|
bpy.types.Scene.blenlog_remap_targets = CollectionProperty(type=RemapTarget)
|
|
bpy.types.Scene.blenlog_remap_target_libs = CollectionProperty(type=RemapTarget)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.blenlog_remap_targets
|
|
del bpy.types.Scene.blenlog_remap_target_libs
|