gp_toolbox/OP_layer_manager.py

358 lines
11 KiB
Python

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, 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(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')
## --- 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')
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)
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
def unregister():
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)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)