2026-01-01
This commit is contained in:
@@ -6,6 +6,7 @@ import subprocess
|
||||
|
||||
# Import ghost buster functionality
|
||||
from ..ops.ghost_buster import GhostBuster, GhostDetector, ResyncEnforce
|
||||
from ..utils import compat
|
||||
|
||||
# Regular expression to match numbered suffixes like .001, .002, _001, _0001, etc.
|
||||
NUMBERED_SUFFIX_PATTERN = re.compile(r'(.*?)[._](\d{3,})$')
|
||||
@@ -91,6 +92,31 @@ def register_dataremap_properties():
|
||||
default=False
|
||||
)
|
||||
|
||||
# Search filter properties for each data type
|
||||
bpy.types.Scene.dataremap_search_images = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Images",
|
||||
description="Filter images by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_materials = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Materials",
|
||||
description="Filter materials by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_fonts = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Fonts",
|
||||
description="Filter fonts by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_worlds = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Worlds",
|
||||
description="Filter worlds by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
# Dictionary to store excluded groups
|
||||
if not hasattr(bpy.types.Scene, "excluded_remap_groups"):
|
||||
bpy.types.Scene.excluded_remap_groups = {}
|
||||
@@ -859,6 +885,21 @@ def draw_drag_selectable_checkbox(layout, context, data_type, group_key):
|
||||
op.group_key = group_key
|
||||
op.data_type = data_type
|
||||
|
||||
def search_matches_group(group, search_string):
|
||||
"""Check if search string matches group base name or any item in group"""
|
||||
if not search_string:
|
||||
return True
|
||||
search_lower = search_string.lower()
|
||||
base_name, items = group
|
||||
# Check base name
|
||||
if search_lower in base_name.lower():
|
||||
return True
|
||||
# Check all item names in group
|
||||
for item in items:
|
||||
if search_lower in item.name.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
# Update the UI code to use the custom draw function
|
||||
def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
"""Draw the list of duplicate data items with drag-selectable checkboxes and click to rename"""
|
||||
@@ -881,6 +922,13 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
if hasattr(context.scene, sort_prop_name):
|
||||
select_row.prop(context.scene, sort_prop_name, text="Sort by Selected")
|
||||
|
||||
# Add search filter
|
||||
search_row = box_dup.row()
|
||||
search_row.label(text="", icon='VIEWZOOM')
|
||||
search_prop_name = f"dataremap_search_{data_type}"
|
||||
if hasattr(context.scene, search_prop_name):
|
||||
search_row.prop(context.scene, search_prop_name, text="")
|
||||
|
||||
box_dup.separator(factor=0.5)
|
||||
|
||||
# Initialize the expanded groups dictionary if it doesn't exist
|
||||
@@ -890,6 +938,15 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
# Get the groups and possibly sort them
|
||||
group_items = list(data_groups.items())
|
||||
|
||||
# Filter by search string if provided
|
||||
search_prop_name = f"dataremap_search_{data_type}"
|
||||
search_string = ""
|
||||
if hasattr(context.scene, search_prop_name):
|
||||
search_string = getattr(context.scene, search_prop_name)
|
||||
|
||||
if search_string:
|
||||
group_items = [group for group in group_items if search_matches_group(group, search_string)]
|
||||
|
||||
# Sort by selection if enabled
|
||||
sort_prop_name = f"dataremap_sort_{data_type}"
|
||||
if hasattr(context.scene, sort_prop_name) and getattr(context.scene, sort_prop_name):
|
||||
@@ -1443,14 +1500,11 @@ def register():
|
||||
register_dataremap_properties()
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
# Unregister properties
|
||||
try:
|
||||
unregister_dataremap_properties()
|
||||
|
||||
+82
-4
@@ -3,6 +3,7 @@ from bpy.types import Panel, Operator, PropertyGroup # type: ignore
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty, PointerProperty, CollectionProperty # type: ignore
|
||||
import os
|
||||
import re
|
||||
from ..utils import compat
|
||||
|
||||
class REMOVE_EXT_OT_summary_dialog(bpy.types.Operator):
|
||||
"""Show remove extensions operation summary"""
|
||||
@@ -257,6 +258,13 @@ class BST_PathProperties(PropertyGroup):
|
||||
default=True
|
||||
) # type: ignore
|
||||
|
||||
# Search filter for images
|
||||
search_filter: StringProperty(
|
||||
name="Search Filter",
|
||||
description="Filter images by name (case-insensitive)",
|
||||
default=""
|
||||
) # type: ignore
|
||||
|
||||
# Smart pathing properties
|
||||
smart_base_path: StringProperty(
|
||||
name="Base Path",
|
||||
@@ -594,6 +602,61 @@ class BST_OT_select_active_images(Operator):
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Operator to select all images with absolute paths
|
||||
class BST_OT_select_absolute_images(Operator):
|
||||
bl_idname = "bst.select_absolute_images"
|
||||
bl_label = "Select Absolute Images"
|
||||
bl_description = "Select all images with absolute file paths"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
selected_count = 0
|
||||
|
||||
# Iterate through all images
|
||||
for img in bpy.data.images:
|
||||
# Skip images that shouldn't be checked
|
||||
if (img.source == 'GENERATED' or # Procedurally generated
|
||||
img.source == 'VIEWER' or # Render Result, Viewer Node, etc.
|
||||
img.name in ['Render Result', 'Viewer Node']): # Special Blender images
|
||||
continue
|
||||
|
||||
# Check if image has a file path
|
||||
if not img.filepath and not img.filepath_raw:
|
||||
continue
|
||||
|
||||
# Check both filepath and filepath_raw for absolute paths
|
||||
is_absolute = False
|
||||
|
||||
# Check filepath
|
||||
if img.filepath:
|
||||
# Skip Blender relative paths (starting with //)
|
||||
if not img.filepath.startswith('//'):
|
||||
# Convert to absolute path and check
|
||||
abs_path = bpy.path.abspath(img.filepath)
|
||||
if abs_path and os.path.isabs(abs_path):
|
||||
is_absolute = True
|
||||
|
||||
# Check filepath_raw if filepath wasn't absolute
|
||||
if not is_absolute and img.filepath_raw:
|
||||
# Skip Blender relative paths (starting with //)
|
||||
if not img.filepath_raw.startswith('//'):
|
||||
# Convert to absolute path and check
|
||||
abs_path = bpy.path.abspath(img.filepath_raw)
|
||||
if abs_path and os.path.isabs(abs_path):
|
||||
is_absolute = True
|
||||
|
||||
# Select image if it has an absolute path
|
||||
if is_absolute:
|
||||
img.bst_selected = True
|
||||
selected_count += 1
|
||||
|
||||
if selected_count > 0:
|
||||
self.report({'INFO'}, f"Selected {selected_count} images with absolute paths")
|
||||
else:
|
||||
self.report({'INFO'}, "No images with absolute paths found")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Add a class for renaming datablocks
|
||||
class BST_OT_rename_datablock(Operator):
|
||||
"""Click to rename datablock"""
|
||||
@@ -1482,7 +1545,8 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
|
||||
# Get addon preferences
|
||||
addon_name = __package__.split('.')[0]
|
||||
prefs = context.preferences.addons.get(addon_name).preferences
|
||||
addon_entry = context.preferences.addons.get(addon_name)
|
||||
prefs = addon_entry.preferences if addon_entry else None
|
||||
|
||||
row = box.row(align=True)
|
||||
row.enabled = any_selected
|
||||
@@ -1495,7 +1559,8 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
|
||||
# Right side: checkbox
|
||||
col = split.column()
|
||||
col.prop(prefs, "automat_common_outside_blend", text="", icon='FOLDER_REDIRECT')
|
||||
if prefs:
|
||||
col.prop(prefs, "automat_common_outside_blend", text="", icon='FOLDER_REDIRECT')
|
||||
|
||||
# Bulk operations section
|
||||
box = layout.box()
|
||||
@@ -1521,10 +1586,16 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.select_material_images", text="Material Images")
|
||||
row.operator("bst.select_active_images", text="Active Images")
|
||||
row.operator("bst.select_absolute_images", text="Absolute Images", icon='FOLDER_REDIRECT')
|
||||
|
||||
# Sorting option
|
||||
row = box.row()
|
||||
row.prop(path_props, "sort_by_selected", text="Sort by Selected")
|
||||
|
||||
# Search filter
|
||||
row = box.row()
|
||||
row.label(text="", icon='VIEWZOOM')
|
||||
row.prop(path_props, "search_filter", text="")
|
||||
|
||||
box.separator()
|
||||
|
||||
@@ -1539,6 +1610,12 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
# Use original order
|
||||
sorted_images = bpy.data.images
|
||||
|
||||
# Filter by search string if provided
|
||||
search_filter = path_props.search_filter
|
||||
if search_filter:
|
||||
search_lower = search_filter.lower()
|
||||
sorted_images = [img for img in sorted_images if search_lower in img.name.lower()]
|
||||
|
||||
for img in sorted_images:
|
||||
# Add bst_selected attribute if it doesn't exist
|
||||
if not hasattr(img, "bst_selected"):
|
||||
@@ -1590,6 +1667,7 @@ classes = (
|
||||
BST_OT_toggle_path_edit,
|
||||
BST_OT_select_material_images,
|
||||
BST_OT_select_active_images,
|
||||
BST_OT_select_absolute_images,
|
||||
BST_OT_rename_datablock,
|
||||
BST_OT_toggle_image_selection,
|
||||
BST_OT_reuse_material_path,
|
||||
@@ -1609,7 +1687,7 @@ classes = (
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
# Register properties
|
||||
bpy.types.Scene.bst_path_props = PointerProperty(type=BST_PathProperties)
|
||||
@@ -1633,7 +1711,7 @@ def unregister():
|
||||
|
||||
# Unregister classes
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
+3
-5
@@ -7,6 +7,7 @@ from ..ops.delete_single_keyframe_actions import DeleteSingleKeyframeActions
|
||||
from ..ops.find_material_users import FindMaterialUsers, MATERIAL_USERS_OT_summary_dialog
|
||||
from ..ops.remove_unused_material_slots import RemoveUnusedMaterialSlots
|
||||
from ..ops.convert_relations_to_constraint import ConvertRelationsToConstraint
|
||||
from ..utils import compat
|
||||
|
||||
class BulkSceneGeneral(bpy.types.Panel):
|
||||
"""Bulk Scene General Panel"""
|
||||
@@ -76,7 +77,7 @@ classes = (
|
||||
# Registration
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
# Register the window manager property for the checkbox
|
||||
bpy.types.WindowManager.bst_no_subdiv_only_selected = bpy.props.BoolProperty(
|
||||
name="Selected Only",
|
||||
@@ -92,10 +93,7 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
# Unregister the window manager property
|
||||
if hasattr(bpy.types.WindowManager, "bst_no_subdiv_only_selected"):
|
||||
del bpy.types.WindowManager.bst_no_subdiv_only_selected
|
||||
|
||||
+3
-5
@@ -5,6 +5,7 @@ import os
|
||||
from enum import Enum
|
||||
import colorsys # Add colorsys for RGB to HSV conversion
|
||||
from ..ops.select_diffuse_nodes import select_diffuse_nodes # Import the specific function
|
||||
from ..utils import compat
|
||||
|
||||
# Material processing status enum
|
||||
class MaterialStatus(Enum):
|
||||
@@ -1014,7 +1015,7 @@ classes = (
|
||||
# Registration
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
# Register properties
|
||||
register_viewport_properties()
|
||||
@@ -1027,7 +1028,4 @@ def unregister():
|
||||
pass
|
||||
# Unregister classes
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
Reference in New Issue
Block a user