forked from bartvdbraak/blender
WM: batch rename operator
Matches rename active, supports objects, bones, sequence strips & nodes. Support chaining actions, these can be extended, initially support: - set/prefix/suffix - search replace - stripping characters - capitalization.
This commit is contained in:
parent
284e4d18be
commit
a5b1231de7
@ -409,6 +409,7 @@ def km_window(params):
|
||||
items.extend([
|
||||
("wm.doc_view_manual_ui_context", {"type": 'F1', "value": 'PRESS'}, None),
|
||||
op_panel("TOPBAR_PT_name", {"type": 'F2', "value": 'PRESS'}, [("keep_open", False)]),
|
||||
("wm.batch_rename", {"type": 'F2', "value": 'PRESS', "alt": True}, None),
|
||||
("wm.search_menu", {"type": 'F3', "value": 'PRESS'}, None),
|
||||
op_menu("TOPBAR_MT_file_context_menu", {"type": 'F4', "value": 'PRESS'}),
|
||||
])
|
||||
|
@ -25,6 +25,7 @@ from bpy.types import (
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
@ -1174,6 +1175,7 @@ rna_vector_subtype_items = (
|
||||
('QUATERNION', "Quaternion Rotation", "Quaternion rotation (affects NLA blending)"),
|
||||
)
|
||||
|
||||
|
||||
class WM_OT_properties_edit(Operator):
|
||||
bl_idname = "wm.properties_edit"
|
||||
bl_label = "Edit Property"
|
||||
@ -1763,6 +1765,294 @@ class WM_OT_toolbar(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BatchRenameAction(bpy.types.PropertyGroup):
|
||||
# category: StringProperty()
|
||||
type: EnumProperty(
|
||||
name="Operation",
|
||||
items=(
|
||||
('SET', "Set Name", "Set a new name or prefix/suffix the existing one"),
|
||||
('STRIP', "Strip Characters", "Strip leading/trailing text from the name"),
|
||||
('REPLACE', "Find/Replace", "Replace text in the name"),
|
||||
('CASE', "Change Case", "Change case of each name"),
|
||||
),
|
||||
)
|
||||
|
||||
# We could split these into sub-properties, however it's not so important.
|
||||
|
||||
# type: 'SET'.
|
||||
set_name: StringProperty(name="Name")
|
||||
set_method: EnumProperty(
|
||||
name="Method",
|
||||
items=(
|
||||
('NEW', "New", ""),
|
||||
('PREFIX', "Prefix", ""),
|
||||
('SUFFIX', "Suffix", ""),
|
||||
),
|
||||
default='SUFFIX',
|
||||
)
|
||||
|
||||
# type: 'STRIP'.
|
||||
strip_chars: EnumProperty(
|
||||
name="Strip Characters",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(
|
||||
('SPACE', "Spaces", ""),
|
||||
('DIGIT', "Digits", ""),
|
||||
('PUNCT', "Punctuation", ""),
|
||||
),
|
||||
)
|
||||
|
||||
# type: 'STRIP'.
|
||||
strip_part: EnumProperty(
|
||||
name="Strip Part",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(
|
||||
('START', "Start", ""),
|
||||
('END', "End", ""),
|
||||
),
|
||||
)
|
||||
|
||||
# type: 'REPLACE'.
|
||||
replace_src: StringProperty(name="Find")
|
||||
replace_dst: StringProperty(name="Replace")
|
||||
replace_match_case: BoolProperty(name="Match Case")
|
||||
|
||||
# type: 'CASE'.
|
||||
case_method: EnumProperty(
|
||||
name="Case",
|
||||
items=(
|
||||
('UPPER', "Upper Case", ""),
|
||||
('LOWER', "Lower Case", ""),
|
||||
('TITLE', "Title Caps", ""),
|
||||
),
|
||||
)
|
||||
|
||||
# Weak, add/remove as properties.
|
||||
op_add: BoolProperty()
|
||||
op_remove: BoolProperty()
|
||||
|
||||
|
||||
class WM_OT_batch_rename(Operator):
|
||||
bl_idname = "wm.batch_rename"
|
||||
bl_label = "Batch Rename"
|
||||
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
data_source: EnumProperty(
|
||||
name="Source",
|
||||
items=(
|
||||
('SELECT', "Selected", ""),
|
||||
('ALL', "All", ""),
|
||||
),
|
||||
)
|
||||
|
||||
actions: CollectionProperty(type=BatchRenameAction)
|
||||
|
||||
@staticmethod
|
||||
def _data_from_context(context, only_selected):
|
||||
|
||||
mode = context.mode
|
||||
scene = context.scene
|
||||
space = context.space_data
|
||||
space_type = None if (space is None) else space.type
|
||||
|
||||
data = None
|
||||
if space_type == 'SEQUENCE_EDITOR':
|
||||
data = (
|
||||
# TODO, we don't have access to seqbasep, this won't work when inside metas.
|
||||
[seq for seq in context.scene.sequence_editor.sequences_all if seq.select]
|
||||
if only_selected else
|
||||
context.scene.sequence_editor.sequences_all,
|
||||
"name",
|
||||
"Strip(s)",
|
||||
)
|
||||
elif space_type == 'NODE_EDITOR':
|
||||
data = (
|
||||
context.selected_nodes
|
||||
if only_selected else
|
||||
list(space.node_tree.nodes),
|
||||
"name",
|
||||
"Node(s)",
|
||||
)
|
||||
else:
|
||||
if mode == 'POSE' or (mode == 'WEIGHT_PAINT' and context.pose_object):
|
||||
data = (
|
||||
[pchan.bone for pchan in context.selected_pose_bones]
|
||||
if only_selected else
|
||||
[pchan.bone for ob in context.objects_in_mode_unique_data for pbone in ob.pose.bones],
|
||||
"name",
|
||||
"Bone(s)",
|
||||
)
|
||||
elif mode == 'EDIT_ARMATURE':
|
||||
data = (
|
||||
context.selected_editable_bones
|
||||
if only_selected else
|
||||
[ebone for ob in context.objects_in_mode_unique_data for ebone in ob.data.edit_bones],
|
||||
"name",
|
||||
"Edit Bone(s)",
|
||||
)
|
||||
else:
|
||||
data = (
|
||||
context.selected_editable_objects
|
||||
if only_selected else
|
||||
[ob for ob in bpy.data.objects if ob.library is None],
|
||||
"name",
|
||||
"Object(s)",
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _apply_actions(actions, name):
|
||||
import string
|
||||
import re
|
||||
|
||||
for action in actions:
|
||||
ty = action.type
|
||||
if ty == 'SET':
|
||||
text = action.set_name
|
||||
method = action.set_method
|
||||
if method == 'NEW':
|
||||
name = text
|
||||
elif method == 'PREFIX':
|
||||
name = text + name
|
||||
elif method == 'SUFFIX':
|
||||
name = name + text
|
||||
else:
|
||||
assert(0)
|
||||
|
||||
elif ty == 'STRIP':
|
||||
chars = action.strip_chars
|
||||
chars_strip = (
|
||||
"{:s}{:s}{:s}"
|
||||
).format(
|
||||
string.punctuation if 'PUNCT' in chars else "",
|
||||
string.digits if 'DIGIT' in chars else "",
|
||||
" " if 'SPACE' in chars else "",
|
||||
)
|
||||
part = action.strip_part
|
||||
if 'START' in part:
|
||||
name = name.lstrip(chars_strip)
|
||||
if 'END' in part:
|
||||
name = name.rstrip(chars_strip)
|
||||
|
||||
elif ty == 'REPLACE':
|
||||
if action.replace_match_case:
|
||||
name = name.replace(
|
||||
action.replace_src,
|
||||
action.replace_dst,
|
||||
)
|
||||
else:
|
||||
name = re.sub(
|
||||
re.escape(action.replace_src),
|
||||
re.escape(action.replace_dst),
|
||||
name,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
elif ty == 'CASE':
|
||||
method = action.case_method
|
||||
if method == 'UPPER':
|
||||
name = name.upper()
|
||||
elif method == 'LOWER':
|
||||
name = name.lower()
|
||||
elif method == 'TITLE':
|
||||
name = name.title()
|
||||
else:
|
||||
assert(0)
|
||||
else:
|
||||
assert(0)
|
||||
return name
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row()
|
||||
row.label(text="Rename {:d} {:s}".format(len(self._data[0]), self._data[2]))
|
||||
row.prop(self, "data_source", expand=True)
|
||||
|
||||
for action in self.actions:
|
||||
box = layout.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
row.prop(action, "type", text="")
|
||||
row.prop(action, "op_add", text="", icon='ADD')
|
||||
row.prop(action, "op_remove", text="", icon='REMOVE')
|
||||
|
||||
ty = action.type
|
||||
if ty == 'SET':
|
||||
box.prop(action, "set_method")
|
||||
box.prop(action, "set_name")
|
||||
elif ty == 'STRIP':
|
||||
box.row().prop(action, "strip_chars")
|
||||
box.row().prop(action, "strip_part")
|
||||
elif ty == 'REPLACE':
|
||||
box.row().prop(action, "replace_src")
|
||||
box.row().prop(action, "replace_dst")
|
||||
box.row().prop(action, "replace_match_case")
|
||||
elif ty == 'CASE':
|
||||
box.row().prop(action, "case_method", expand=True)
|
||||
|
||||
def check(self, context):
|
||||
changed = False
|
||||
for i, action in enumerate(self.actions):
|
||||
if action.op_add:
|
||||
action.op_add = False
|
||||
self.actions.add()
|
||||
if i + 2 != len(self.actions):
|
||||
self.actions.move(len(self.actions) - 1, i + 1)
|
||||
changed = True
|
||||
break
|
||||
if action.op_remove:
|
||||
action.op_remove = False
|
||||
if len(self.actions) > 1:
|
||||
self.actions.remove(i)
|
||||
changed = True
|
||||
break
|
||||
|
||||
if self._data_source_prev != self.data_source:
|
||||
only_selected = self.data_source == 'SELECT'
|
||||
self._data = self._data_from_context(context, only_selected)
|
||||
self._data_source_prev = self.data_source
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
def execute(self, context):
|
||||
seq, attr, descr = self._data
|
||||
|
||||
actions = self.actions
|
||||
|
||||
total_len = 0
|
||||
change_len = 0
|
||||
for item in seq:
|
||||
name_src = getattr(item, attr)
|
||||
name_dst = self._apply_actions(actions, name_src)
|
||||
if name_src != name_dst:
|
||||
setattr(item, attr, name_dst)
|
||||
change_len += 1
|
||||
total_len += 1
|
||||
|
||||
self.report({'INFO'}, "Renamed {:d} of {:d} {:s}".format(total_len, change_len, descr))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
only_selected = self.data_source == 'SELECT'
|
||||
data = self._data_from_context(context, only_selected)
|
||||
self._data_source_prev = self.data_source
|
||||
|
||||
if data is None:
|
||||
self.report({'ERROR'}, "No usable items in this context")
|
||||
return {'CANCELLED'}
|
||||
|
||||
self._data = data
|
||||
|
||||
if not self.actions:
|
||||
self.actions.add()
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
|
||||
class WM_MT_splash(Menu):
|
||||
bl_label = "Splash"
|
||||
|
||||
@ -1979,5 +2269,7 @@ classes = (
|
||||
WM_OT_tool_set_by_id,
|
||||
WM_OT_tool_set_by_index,
|
||||
WM_OT_toolbar,
|
||||
BatchRenameAction,
|
||||
WM_OT_batch_rename,
|
||||
WM_MT_splash,
|
||||
)
|
||||
|
@ -505,6 +505,8 @@ class TOPBAR_MT_edit(Menu):
|
||||
props.name = "TOPBAR_PT_name"
|
||||
props.keep_open = False
|
||||
|
||||
layout.operator("wm.batch_rename")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Should move elsewhere (impacts outliner & 3D view).
|
||||
|
Loading…
Reference in New Issue
Block a user