import bpy import os import re from . import fn from bpy.props import (StringProperty, BoolProperty, EnumProperty, CollectionProperty) from .constant import TECH_PASS_KEYWORDS class RT_OT_outputs_search_and_replace(bpy.types.Operator): bl_idname = "rt.outputs_search_and_replace" bl_label = "Search And Replace Outputs Paths" bl_description = "Search/Replace texts in output path and slots" bl_options = {'REGISTER', 'UNDO'} ## Search and replace options find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'HIDDEN'}, subtype='NONE') replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'HIDDEN'}, subtype='NONE') use_regex: BoolProperty(name="Regex", description="Use regular expression (advanced), equivalent to python re.sub()", default=False) target : EnumProperty( name="Target Fields", description="Fields to search and replace in outputs", items=( ('path', "Base Paths", "search and replace in output node paths"), ('slots', "File Slots", "search and replace in output node file slots (also Layer slots for multilayers outputs)"), ('all', "All", "search and replace in both paths and slots"), ), default='all' ) section : EnumProperty( name="Section", description="Search-replace only on a specific section of the text field based on a separator (if no separator exists, skip the field)", items=( ('full', "Full", "Affect the whole string, No specific section"), ('suffix', "Suffix", "Affect only the part after the last separator"), ('prefix', "Prefix", "Affect only part before the first separator"), ), default='full' ) separator: StringProperty(name="Separator", description="Separator for prefix/suffix", default='/') selected_node_only: BoolProperty(name="Selected Nodes Only", description="Affect only selected file output nodes", default=True) @classmethod def poll(cls, context): return context.scene.node_tree and context.scene.node_tree.nodes def rename(self, source): if not self.find: return old = source if self.use_regex: # Directly replace using regex new = re.sub(self.find, self.replace, source) if old != new: return new ## Regex usage exemple to add as hint: # search (separate in 3 groups) : (.*/)(.*?_)(.*) # replace (keep only group 1 and 3) : \1\3 # -- # source: "ViewLayer_DiffCol/ViewLayer_DiffCol_" # result: "ViewLayer_DiffCol/DiffCol_" return if self.section == 'full': new = source.replace(self.find, self.replace) if old != new: return new if not self.separator in source: # Only if separator exists return if self.section == 'prefix': splited = source.split(self.separator) prefix = splited[0] if not prefix: return new_prefix = prefix.replace(self.find, self.replace) if prefix != new_prefix: splited[0] = new_prefix return self.separator.join(splited) elif self.section == 'suffix': splited = source.rsplit(self.separator, 1) suffix = splited[-1] if not suffix: return new_suffix = suffix.replace(self.find, self.replace) if suffix != new_suffix: splited[-1] = new_suffix return self.separator.join(splited) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=430) def draw(self, context): layout = self.layout # col = layout.column(align=False) # col.prop(self, "separator") # col.prop(self, "selected_node_only") row = layout.row(align=False) row.prop(self, "target", text='Fields', expand=False) row.prop(self, "selected_node_only", text="Selected File Outputs Only") row = layout.row() row_c= row.row() row_c.prop(self, "use_regex", text="Use Regex") row_b= layout.row() row_b.prop(self, "section") row_b.active = not self.use_regex subrow_b = row_b.row(align=True) subrow_b.active = self.section != 'full' subrow_b.alignment = 'RIGHT' subrow_b.label(text='Separator:') subrow_b.prop(self, "separator", text='') layout.prop(self, "find") layout.prop(self, "replace") def execute(self, context): ## Get the collection prop from data path ## any node_tree type (here we specifically want compo nodes) # node_tree = bpy.context.space_data.edit_tree # if not node_tree or not node_tree.nodes: # return ## Checked in poll node_tree = context.scene.node_tree file_outputs = [f for f in node_tree.nodes if f.type == 'OUTPUT_FILE'] if self.selected_node_only: file_outputs = [f for f in file_outputs if f.select] if not file_outputs: self.report({'ERROR'}, 'No file output nodes found') return {'CANCELLED'} print() count = 0 for file_out in file_outputs: ## Get the target prop if self.target in ('path', 'all'): current_base_path = file_out.base_path new_base_path = self.rename(current_base_path) if new_base_path is not None and current_base_path != new_base_path: print(f"\n{file_out.name}: base path: {current_base_path} to {new_base_path}") file_out.base_path = new_base_path count += 1 if self.target in ('slots', 'all'): for slot in file_out.file_slots: current_slot_path = slot.path new_slot_path = self.rename(current_slot_path) if new_slot_path is not None and current_slot_path != new_slot_path: print(f"{file_out.name}: file slot: {current_slot_path} to {new_slot_path}") slot.path = new_slot_path count += 1 ## Layers slots for layer in file_out.layer_slots: current_layer_path = layer.name new_layer_path = self.rename(current_layer_path) if new_layer_path is not None and current_layer_path != new_layer_path: print(f"{file_out.name}: layer slot: {current_layer_path} to {new_layer_path}") layer.name = new_layer_path count += 1 if count: self.report({'INFO'}, f"{str(count)} field(s) renamed") else: self.report({'WARNING'}, 'Nothing changed') return{'FINISHED'} classes = ( RT_OT_outputs_search_and_replace, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)