Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCampbell Barton <ideasman42@gmail.com>2019-08-31 20:16:58 +0300
committerCampbell Barton <ideasman42@gmail.com>2019-08-31 20:36:00 +0300
commita5b1231de790e4cbf0d4dd62e30afcbc5d8a95f5 (patch)
treec8d8ff04d05ec7811cb808c1df534e933af6fbcd
parent284e4d18be64268ae788dd21c8e6beff7c4cd61f (diff)
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.
-rw-r--r--release/scripts/presets/keyconfig/keymap_data/blender_default.py1
-rw-r--r--release/scripts/startup/bl_operators/wm.py292
-rw-r--r--release/scripts/startup/bl_ui/space_topbar.py2
3 files changed, 295 insertions, 0 deletions
diff --git a/release/scripts/presets/keyconfig/keymap_data/blender_default.py b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
index f789da6f3fa..ae2d2952f85 100644
--- a/release/scripts/presets/keyconfig/keymap_data/blender_default.py
+++ b/release/scripts/presets/keyconfig/keymap_data/blender_default.py
@@ -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'}),
])
diff --git a/release/scripts/startup/bl_operators/wm.py b/release/scripts/startup/bl_operators/wm.py
index bf1c79e766f..f4724ee9709 100644
--- a/release/scripts/startup/bl_operators/wm.py
+++ b/release/scripts/startup/bl_operators/wm.py
@@ -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,
)
diff --git a/release/scripts/startup/bl_ui/space_topbar.py b/release/scripts/startup/bl_ui/space_topbar.py
index d0ad9a8f31e..836ec53e2e5 100644
--- a/release/scripts/startup/bl_ui/space_topbar.py
+++ b/release/scripts/startup/bl_ui/space_topbar.py
@@ -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).