diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a79c51..950b1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog +1.5.4 + +- feat: Layer manager + - select/set layer prefix + - select/set layer color +- code: refactor name builder function + 1.5.3 - feat: layer aquick-prefix for layer using pref separator diff --git a/OP_layer_manager.py b/OP_layer_manager.py new file mode 100644 index 0000000..69b8cc1 --- /dev/null +++ b/OP_layer_manager.py @@ -0,0 +1,335 @@ +import bpy +import re +from bpy.types import Operator +from bpy.props import StringProperty, BoolProperty, EnumProperty + +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 + +def layer_name_build(layer, prefix='', prefix2='', desc=''): + '''GET a layer and infos to build name + can take one or two prefix and description/name of the layer) + ''' + + global pattern + + prefs = get_addon_prefs() + sep = prefs.separator + name = layer.info + + pattern = pattern.replace('_', sep) # set separator + + res = re.search(pattern, name) + p1, p2, p3 = res.group(1), res.group(2), res.group(3) + + ## empty instead of None + p1 = '' if p1 is None else p1 + p2 = '' if p2 is None else p2 + p3 = '' if p3 is None else p3 + + if prefix: + if prefix == 'prefixkillcode': + p1 = '' + else: + p1 = prefix.upper() + sep + + if prefix2: + p2 = prefix2.upper() + sep + if desc: + p3 = desc + + new = f'{p1}{p2}{p3}' + + 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'}) + + 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 not l.select: + continue + layer_name_build(l, prefix=self.prefix, prefix2=self.prefix2, desc=self.desc) + 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): + global pattern + 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): + prefs = get_addon_prefs() + sep = prefs.separator + if not target: + return + scene = bpy.context.scene + gpl = bpy.context.object.data.layers + + if scene.gp_rename_selected: + lays = [l for l in gpl if l.info != 'background' and l.select] + else: + lays = [l for l in gpl if l.info != 'background'] + + ct = 0 + for l in lays: + if scene.gp_rename_prefix: + old = l.info + splited = l.info.split(sep) + prefix = splited[0] + #print(1, splited) + new_prefix = prefix.replace(target, replacement) + #print(2, splited) + if prefix != new_prefix: + splited[0] = new_prefix + #print(3, splited) + l.info = sep.join(splited) + print('rename:', old, '-->', l.info) + ct+= 1 + + else: + old = l.info + new = l.info.replace(target, replacement) + if old != new: + print('rename:', old, '-->', l.info) + l.info = 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 layer only", description="Affect only selected layers", default=True) + prefix: BoolProperty(name="Prefix only", description="Affect only prefix of name (full name if not '_' in it)", default=True) + + def execute(self, context): + count = replace_layer_name(self.find, self.replace, selected_only=self.select, prefix_only=self.prefix) + 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 + #layout.separator() + #layout.label('search/replace GPlayers') + layout.prop(self, "selected") + layout.prop(self, "prefix") + layout.prop(self, "find")#, text = 'find') + layout.prop(self, "replace")#, text = 'replace') + + +## --- UI layer panel--- + +def layer_name_builder(self, context): + '''appended to DATA_PT_gpencil_layers''' + + prefs = get_addon_prefs() + if not prefs.prefixes: + return + + layout = self.layout + # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'} + # layout.separator() + col = layout.column() + + all_prefixes = prefs.prefixes.split(',') + line_limit = 8 + + ## 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 + # row = layout.row(align=True) + # for task in prefs.prefixes: # 'PO', 'AN' + # row.operator("me.set_layer_name", text=task).prefix2 = task + + row = col.row(align=True) + row.prop(context.scene.gptoolprops, 'layer_name', text='') + row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name + + +## --- 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') + + +classes=( + GPTB_OT_rename_gp_layer, + GPTB_OT_layer_name_build, + 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) + bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header) + +def unregister(): + bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header) + bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder) + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/OP_layer_name_builder.py b/OP_layer_name_builder.py deleted file mode 100644 index 282f5fb..0000000 --- a/OP_layer_name_builder.py +++ /dev/null @@ -1,116 +0,0 @@ -import bpy -import re -from bpy.types import Operator -from bpy.props import StringProperty, BoolProperty - - -from .utils import get_addon_prefs - - -# --- OPS --- - -## multi-prefix solution (Caps letters) -class PROJ_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'}) - - def execute(self, context): - prefs = get_addon_prefs() - sep = prefs.separator - ob = context.object - gpl = ob.data.layers - act = gpl.active - if not act: - self.report({'ERROR'}, 'no layer active') - return {"CANCELLED"} - - name = act.info - - # 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 = pattern.replace('_', sep) # set separator - - res = re.search(pattern, name) - p1, p2, p3 = res.group(1), res.group(2), res.group(3) - - ## empty instead of None - p1 = '' if p1 is None else p1 - p2 = '' if p2 is None else p2 - p3 = '' if p3 is None else p3 - - if self.prefix: - if self.prefix == 'prefixkillcode': - p1 = '' - else: - p1 = self.prefix.upper() + sep - - if self.prefix2: - p2 = self.prefix2.upper() + sep - if self.desc: - p3 = self.desc - - new = f'{p1}{p2}{p3}' - - act.info = new - - return {"FINISHED"} - -## --- UI --- - -def layer_name_builder(self, context): - prefs = get_addon_prefs() - if not prefs.prefixes: - return - - layout = self.layout - # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'} - # layout.separator() - col = layout.column() - - all_prefixes = prefs.prefixes.split(',') - line_limit = 8 - - ## 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 - # row = layout.row(align=True) - # for task in prefs.prefixes: # 'PO', 'AN' - # row.operator("me.set_layer_name", text=task).prefix2 = task - - row = col.row(align=True) - row.prop(context.scene.gptoolprops, 'layer_name', text='') - row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name - - -classes=( -PROJ_OT_layer_name_build, -) - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder) - -def unregister(): - bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder) - for cls in reversed(classes): - bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/__init__.py b/__init__.py index fba1bf9..c72ee14 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (1, 5, 3), +"version": (1, 5, 4), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -48,7 +48,7 @@ from . import OP_copy_paste from . import OP_realign from . import OP_depth_move from . import OP_key_duplicate_send -from . import OP_layer_name_builder +from . import OP_layer_manager from . import OP_eraser_brush from . import TOOL_eraser_brush from . import handler_draw_cam @@ -538,7 +538,7 @@ def register(): OP_realign.register() OP_depth_move.register() OP_key_duplicate_send.register() - OP_layer_name_builder.register() + OP_layer_manager.register() OP_eraser_brush.register() TOOL_eraser_brush.register() handler_draw_cam.register() @@ -567,7 +567,7 @@ def unregister(): handler_draw_cam.unregister() OP_eraser_brush.unregister() TOOL_eraser_brush.unregister() - OP_layer_name_builder.unregister() + OP_layer_manager.unregister() OP_key_duplicate_send.unregister() OP_depth_move.unregister() OP_realign.unregister()