from os import error import bpy import re from bpy.types import Operator from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.app.handlers import persistent from .utils import get_addon_prefs # --- OPS --- # PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator # pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter # pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator # pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six # pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix # pattern = r'^(?P[A-Z]{1,6}_)?(?P[A-Z]{1,6}_)?(?P.*?)(?P_[A-Z]{2})?$' # named # pattern = r'^(?P-\s)?(?P[A-Z]{2}_)?(?P[A-Z]{1,6}_)?(?P.*?)(?P_[A-Z]{2})?$' # group start ' - ' # PATTERN = r'^(?P-\s)?(?P[A-Z]{2}_)?(?P[A-Z]{1,6}_)?(?P.*?)(?P_[A-Z]{2})?(?P\.\d{3})?$' # numering PATTERN = r'^(?P-\s)?(?P[A-Z]{2}_)?(?P.*?)(?P_[A-Z]{2})?(?P\.\d{3})?$' # numering def layer_name_build(layer, prefix='', desc='', suffix=''): '''GET a layer and argumen to build and assign name''' prefs = get_addon_prefs() sep = prefs.separator name = layer.info pattern = PATTERN.replace('_', sep) # set separator res = re.search(pattern, name.strip()) # prefix -> tag # prefix2 -> tag2 # desc -> name # suffix -> sfix grp = '' if res.group('grp') is None else res.group('grp') tag = '' if res.group('tag') is None else res.group('tag') # tag2 = '' if res.group('tag2') is None else res.group('tag2') name = '' if res.group('name') is None else res.group('name') sfix = '' if res.group('sfix') is None else res.group('sfix') inc = '' if res.group('inc') is None else res.group('inc') if grp: grp = ' ' + grp # name is strip(), so grp first spaces are gones. if prefix: if prefix == 'prefixkillcode': tag = '' else: tag = prefix.upper().strip() + sep # if prefix2: # tag2 = prefix2.upper().strip() + sep if desc: name = desc if suffix: if suffix == 'suffixkillcode': sfix = '' else: sfix = sep + suffix.upper().strip() # check if name is available without the increment ending new = f'{grp}{tag}{name}{sfix}' layer.info = new """ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''): '''GET a layer and infos to build name Can take one or two prefix and description/name of the layer) ''' prefs = get_addon_prefs() sep = prefs.separator name = layer.info pattern = pattern.replace('_', sep) # set separator res = re.search(pattern, name.strip()) p1 = '' if res.group(1) is None else res.group(1) p2 = '' if res.group(2) is None else res.group(2) p3 = '' if res.group(3) is None else res.group(3) p4 = '' if res.group(4) is None else res.group(4) if prefix: if prefix == 'prefixkillcode': p1 = '' else: p1 = prefix.upper().strip() + sep if prefix2: p2 = prefix2.upper().strip() + sep if desc: p3 = desc if suffix: if suffix == 'suffixkillcode': p4 = '' else: p4 = sep + suffix.upper().strip() new = f'{p1}{p2}{p3}{p4}' layer.info = new """ ## multi-prefix solution (Caps letters) class GPTB_OT_layer_name_build(Operator): bl_idname = "gp.layer_name_build" bl_label = "Layer Name Build" bl_description = "Change prefix of layer name" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return True prefix : StringProperty(default='', options={'SKIP_SAVE'}) # prefix2 : StringProperty(default='', options={'SKIP_SAVE'}) desc : StringProperty(default='', options={'SKIP_SAVE'}) suffix : StringProperty(default='', options={'SKIP_SAVE'}) def execute(self, context): ob = context.object gpl = ob.data.layers act = gpl.active if not act: self.report({'ERROR'}, 'no layer active') return {"CANCELLED"} layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix) ## Deactivate multi-selection on layer ! ## somethimes it affect a random layer that is still considered selected # for l in gpl: # if l.select or l == act: # layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix) return {"FINISHED"} def grp_toggle(l, mode='TOGGLE'): '''take mode in (TOGGLE, GROUP, UNGROUP) ''' grp_item_id = ' - ' res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info) if not res and mode in ('TOGGLE', 'GROUP'): # No gpr : add group prefix after stripping all space and dash l.info = grp_item_id + l.info.lstrip(' -') elif res and mode in ('TOGGLE', 'UNGROUP'): # found : delete group prefix l.info = res.group(2) class GPTB_OT_layer_group_toggle(Operator): bl_idname = "gp.layer_group_toggle" bl_label = "Group Toggle" bl_description = "Group or ungroup a layer" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return True # group : StringProperty(default='', options={'SKIP_SAVE'}) def execute(self, context): ob = context.object gpl = ob.data.layers act = gpl.active if not act: self.report({'ERROR'}, 'no layer active') return {"CANCELLED"} for l in gpl: if l.select or l == act: grp_toggle(l) return {"FINISHED"} class GPTB_OT_layer_new_group(Operator): bl_idname = "gp.layer_new_group" bl_label = "New Group" bl_description = "Create a group from active layer" bl_options = {"REGISTER", "UNDO"} def execute(self, context): ob = context.object gpl = ob.data.layers act = gpl.active if not act: self.report({'ERROR'}, 'no layer active') return {"CANCELLED"} res = re.search(PATTERN, act.info) if not res: self.report({'ERROR'}, 'Could not create a group name, create a layer manually') return {"CANCELLED"} name = res.group('name').strip(' -') if not name: self.report({'ERROR'}, f'No name found in {act.info}') return {"CANCELLED"} if name in [l.info.strip(' -') for l in gpl]: self.report({'WARNING'}, f'Name already exists: {act.info}') return {"FINISHED"} grp_toggle(act, mode='GROUP') n = gpl.new(name, set_active=False) n.use_onion_skinning = n.use_lights = False n.hide = True n.opacity = 0 return {"FINISHED"} #-## SELECTION MANAGEMENT ##-# def activate_channel_group_color(context): if not context.preferences.edit.use_anim_channel_group_colors: context.preferences.edit.use_anim_channel_group_colors = True def refresh_areas(): for area in bpy.context.screen.areas: area.tag_redraw() class GPTB_OT_select_set_same_prefix(Operator): bl_idname = "gp.select_same_prefix" bl_label = "Select Same Prefix" bl_description = "Select layers that have the same prefix as active\nSet with ctrl+clic" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'}, items=( ('SELECT', "Select", "Select layer with same prefix as active"), ('SET', "Set", "Set prefix on selected layer to the same as active"), ), ) def invoke(self, context, event): if event.ctrl: self.mode = 'SET' return self.execute(context) def execute(self, context): prefs = get_addon_prefs() sep = prefs.separator # '_' gpl = context.object.data.layers act = gpl.active res = re.search(PATTERN, act.info) if not res: # self.report({'ERROR'}, f'Error scanning {act.info}') return {"CANCELLED"} namespace = res.group(1) if not namespace: self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}') return {"CANCELLED"} if self.mode == 'SELECT': ## with split # namespace = act.info.split(sep,1)[0] # namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl] ## with reg namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl] gpl.foreach_set('select', namespace_bool_list) elif self.mode == 'SET': for l in gpl: if not l.select or l == act: continue layer_name_build(l, prefix=namespace.strip(sep)) refresh_areas() return {"FINISHED"} class GPTB_OT_select_set_same_color(Operator): bl_idname = "gp.select_same_color" bl_label = "Select Same Color" bl_description = "Select layers that have the same color as active\nSet with ctrl+clic" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'}, items=( ('SELECT', "Select", "Select layer with same prefix as active"), ('SET', "Set", "Set prefix on selected layer to the same as active"), ), ) def invoke(self, context, event): if event.ctrl: self.mode = 'SET' return self.execute(context) def execute(self, context): gpl = context.object.data.layers act = gpl.active if self.mode == 'SELECT': same_color_bool = [l.channel_color == act.channel_color for l in gpl] gpl.foreach_set('select', same_color_bool) elif self.mode == 'SET': activate_channel_group_color(context) for l in gpl: if not l.select or l == act: continue l.channel_color = act.channel_color refresh_areas() return {"FINISHED"} def replace_layer_name(target, replacement, selected_only=True, prefix_only=True, regex=False): prefs = get_addon_prefs() sep = prefs.separator if not target: return gpl = bpy.context.object.data.layers if selected_only: lays = [l for l in gpl if l.select] # exclude : l.info != 'background' else: lays = [l for l in gpl] # exclude : if l.info != 'background' ct = 0 for l in lays: old = l.info if regex: new = re.sub(target, replacement, l.info) if old != new: l.info = new print('rename:', old, '-->', new) ct += 1 continue if prefix_only: if not sep in l.info: # only if separator exists continue splited = l.info.split(sep) prefix = splited[0] new_prefix = prefix.replace(target, replacement) if prefix != new_prefix: splited[0] = new_prefix l.info = sep.join(splited) print('rename:', old, '-->', l.info) ct += 1 else: new = l.info.replace(target, replacement) if old != new: l.info = new print('rename:', old, '-->', new) ct += 1 return ct class GPTB_OT_rename_gp_layer(Operator): '''rename GP layers based on a search and replace''' bl_idname = "gp.rename_gp_layers" bl_label = "Rename Gp Layers" bl_description = "Search/Replace string in all GP layers" @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE') replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE') selected: BoolProperty(name="Selected Only", description="Affect only selected layers", default=False) prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skip layer without separator in name)", default=False) use_regex: BoolProperty(name="Regex", description="use regular expression (advanced), equivalent to python re.sub()", default=False) def execute(self, context): count = replace_layer_name(self.find, self.replace, selected_only=self.selected, prefix_only=self.prefix, regex=self.use_regex) if count: mess = str(count) + ' layers renamed' self.report({'INFO'}, mess) else: self.report({'WARNING'}, 'No text found !') return{'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout row = layout.row() row_a= row.row() row_a.prop(self, "selected") row_b= row.row() row_b.prop(self, "prefix") row_c= row.row() row_c.prop(self, "use_regex") row_b.active = not self.use_regex layout.prop(self, "find") layout.prop(self, "replace") ## --- UI layer panel--- def layer_name_builder_ui(self, context): '''appended to DATA_PT_gpencil_layers''' prefs = get_addon_prefs() if not prefs.show_prefix_buttons: return if not prefs.prefixes and not prefs.suffixes: return layout = self.layout # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'} # layout.separator() col = layout.column() all_prefixes = prefs.prefixes.split(',') all_suffixes = prefs.suffixes.split(',') line_limit = 8 if prefs.prefixes: ## first prefix for i, prefix in enumerate(all_prefixes): if i % line_limit == 0: row = col.row(align=True) row.operator("gp.layer_name_build", text=prefix.upper() ).prefix = prefix row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode' ## secondary prefix ? ## name (description) row = col.row(align=True) row.prop(context.scene.gptoolprops, 'layer_name', text='') row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW') row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE') ## no need for desc ops, already trigerred from update # row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name if prefs.suffixes: for i, suffix in enumerate(all_suffixes): if i % line_limit == 0: row = col.row(align=True) row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode' ## --- UI dopesheet --- def gpencil_dopesheet_header(self, context): '''to append in DOPESHEET_HT_header''' layout = self.layout st = context.space_data if st.mode != 'GPENCIL': return row = layout.row(align=True) # row.operator('gp.active_channel_color_to_selected', text='', icon='RESTRICT_COLOR_ON') row.operator('gp.select_same_prefix', text='', icon='SYNTAX_OFF') # SORTALPHA, SMALL_CAPS row.operator('gp.select_same_color', text='', icon='RESTRICT_COLOR_ON') ## --- UI context menu --- def gpencil_layer_dropdown_menu(self, context): '''to append in GPENCIL_MT_layer_context_menu''' self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE') ## handler and msgbus def obj_layer_name_callback(): '''assign layer name properties so user an tweak it''' ob = bpy.context.object if not ob or ob.type != 'GPENCIL': return if not ob.data.layers.active: return ## Set selection to active object ot avoid un-sync selection on Layers stack ## (happen when an objet is selected but not active with 'lock object mode') for l in ob.data.layers: l.select = l == ob.data.layers.active res = re.search(PATTERN, ob.data.layers.active.info.strip()) if not res: return if not res.group('name'): return # print('grp:', res.group('grp')) # print('tag:', res.group('tag')) # print('name:', res.group('name')) # print('sfix:', res.group('sfix')) # print('inc:', res.group('inc')) bpy.context.scene.gptoolprops['layer_name'] = res.group('name') @persistent def subscribe_handler(dummy): subscribe_to = (bpy.types.GreasePencilLayers, "active_index") bpy.msgbus.subscribe_rna( key=subscribe_to, # owner of msgbus subcribe (for clearing later) # owner=handle, owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime... # Args passed to callback function (tuple) args=(), # Callback function for property update notify=obj_layer_name_callback, options={'PERSISTENT'}, ) classes=( GPTB_OT_rename_gp_layer, GPTB_OT_layer_name_build, GPTB_OT_layer_group_toggle, GPTB_OT_layer_new_group, GPTB_OT_select_set_same_prefix, GPTB_OT_select_set_same_color, ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui) bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header) bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu) bpy.app.handlers.load_post.append(subscribe_handler) # need to restart after first activation def unregister(): bpy.app.handlers.load_post.remove(subscribe_handler) bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu) bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header) bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui) for cls in reversed(classes): bpy.utils.unregister_class(cls) # delete layer index trigger bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)