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

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormeta-androcto <meta.androcto1@gmail.com>2019-08-11 08:54:11 +0300
committermeta-androcto <meta.androcto1@gmail.com>2019-08-11 08:54:11 +0300
commit802904cf43da3deed2094520e17a79d8020d372a (patch)
treee2d3f20be12d63cd1b7ab7457a04f09979111b60 /materials_utils
parent1bba102d652cea832ae7da0d1d476739d2e1d8da (diff)
materials_utils: return to release: T67990 T63750 01d80b8f602f
Diffstat (limited to 'materials_utils')
-rw-r--r--materials_utils/__init__.py179
-rw-r--r--materials_utils/enum_values.py32
-rw-r--r--materials_utils/functions.py604
-rw-r--r--materials_utils/menus.py216
-rw-r--r--materials_utils/operators.py525
5 files changed, 1556 insertions, 0 deletions
diff --git a/materials_utils/__init__.py b/materials_utils/__init__.py
new file mode 100644
index 00000000..40f976d7
--- /dev/null
+++ b/materials_utils/__init__.py
@@ -0,0 +1,179 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# Based on 2010 version by MichaelW
+# (c) 2016 meta-androcto, parts based on work by Saidenka, lijenstina
+# Materials Utils: by MichaleW, meta-androcto, lijenstina,
+# (some code thanks to: CoDEmanX, SynaGl0w, ideasman42)
+# Link to base names: Sybren, Texture renamer: Yadoob
+# Ported from 2.6/2.7 to 2.8x by Christopher Hindefjord (chrishinde) 2019
+
+bl_info = {
+ "name": "Material Utils",
+ "author": "MichaleW, ChrisHinde",
+ "version": (1, 0, 6),
+ "blender": (2, 80, 0),
+ "location": "View3D > Shift + Q key",
+ "description": "Menu of material tools (assign, select..) in the 3D View",
+ "warning": "",
+ "wiki_url": "https://github.com/ChrisHinde/MaterialUtilities",
+ "category": "Material"
+}
+
+"""
+This script has several functions and operators, grouped for convenience:
+
+* assign material:
+ offers the user a list of ALL the materials in the blend file and an
+ additional "new" entry the chosen material will be assigned to all the
+ selected objects in object mode.
+
+ in edit mode the selected polygons get the selected material applied.
+
+ if the user chose "new" the new material can be renamed using the
+ "last operator" section of the toolbox.
+
+
+* select by material
+ in object mode this offers the user a menu of all materials in the blend
+ file any objects using the selected material will become selected, any
+ objects without the material will be removed from selection.
+
+ in edit mode: the menu offers only the materials attached to the current
+ object. It will select the polygons that use the material and deselect those
+ that do not.
+
+* clean material slots
+ for all selected objects any empty material slots or material slots with
+ materials that are not used by the mesh polygons or splines will be removed.
+
+* remove material slots
+ removes all material slots of the active (or selected) object(s).
+
+* replace materials
+ lets your replace one material by another. Optionally for all objects in
+ the blend, otherwise for selected editable objects only. An additional
+ option allows you to update object selection, to indicate which objects
+ were affected and which not.
+
+* set fake user
+ enable/disable fake user for materials. You can chose for which materials
+ it shall be set, materials of active / selected / objects in current scene
+ or used / unused / all materials.
+
+"""
+
+
+import bpy
+
+from .enum_values import *
+from .functions import *
+from .operators import *
+from .menus import *
+
+# All classes used by Material Utilities, that need to be registred
+classes = (
+ VIEW3D_OT_materialutilities_assign_material_object,
+ VIEW3D_OT_materialutilities_assign_material_edit,
+ VIEW3D_OT_materialutilities_select_by_material_name,
+ VIEW3D_OT_materialutilities_copy_material_to_others,
+
+ VIEW3D_OT_materialutilities_clean_material_slots,
+ VIEW3D_OT_materialutilities_remove_material_slot,
+ VIEW3D_OT_materialutilities_remove_all_material_slots,
+
+ VIEW3D_OT_materialutilities_replace_material,
+ VIEW3D_OT_materialutilities_fake_user_set,
+ VIEW3D_OT_materialutilities_change_material_link,
+
+ MATERIAL_OT_materialutilities_merge_base_names,
+
+ MATERIAL_OT_materialutilities_material_slot_move,
+
+ VIEW3D_MT_materialutilities_assign_material,
+ VIEW3D_MT_materialutilities_select_by_material,
+
+ VIEW3D_MT_materialutilities_clean_slots,
+ VIEW3D_MT_materialutilities_specials,
+
+ VIEW3D_MT_materialutilities_main,
+)
+
+
+# This allows you to right click on a button and link to the manual
+def materialutilities_manual_map():
+ print("ManMap")
+ url_manual_prefix = "https://github.com/ChrisHinde/MaterialUtilities"
+ url_manual_map = []
+ #url_manual_mapping = ()
+ #("bpy.ops.view3d.materialutilities_*", ""),
+ #("bpy.ops.view3d.materialutilities_assign_material_edit", ""),
+ #("bpy.ops.view3d.materialutilities_select_by_material_name", ""),)
+
+ for cls in classes:
+ if issubclass(cls, bpy.types.Operator):
+ url_manual_map.append(("bpy.ops." + cls.bl_idname, ""))
+
+ url_manual_mapping = tuple(url_manual_map)
+ #print(url_manual_mapping)
+ return url_manual_prefix, url_manual_mapping
+
+mu_classes_register, mu_classes_unregister = bpy.utils.register_classes_factory(classes)
+
+
+def register():
+ """Register the classes of Material Utilities together with the default shortcut (Shift+Q)"""
+ mu_classes_register()
+
+ bpy.types.VIEW3D_MT_object_context_menu.append(materialutilities_specials_menu)
+
+ bpy.types.MATERIAL_MT_context_menu.prepend(materialutilities_menu_move)
+ bpy.types.MATERIAL_MT_context_menu.append(materialutilities_menu_functions)
+
+ kc = bpy.context.window_manager.keyconfigs.addon
+ if kc:
+ km = kc.keymaps.new(name = "3D View", space_type = "VIEW_3D")
+ kmi = km.keymap_items.new('wm.call_menu', 'Q', 'PRESS', ctrl = False, shift = True)
+ kmi.properties.name = VIEW3D_MT_materialutilities_main.bl_idname
+
+ bpy.utils.register_manual_map(materialutilities_manual_map)
+
+
+def unregister():
+ """Unregister the classes of Material Utilities together with the default shortcut for the menu"""
+ mu_classes_unregister()
+
+ bpy.utils.unregister_manual_map(materialutilities_manual_map)
+
+ bpy.types.VIEW3D_MT_object_context_menu.remove(materialutilities_specials_menu)
+
+ bpy.types.MATERIAL_MT_context_menu.remove(materialutilities_menu_move)
+ bpy.types.MATERIAL_MT_context_menu.remove(materialutilities_menu_functions)
+
+ kc = bpy.context.window_manager.keyconfigs.addon
+ if kc:
+ km = kc.keymaps["3D View"]
+ for kmi in km.keymap_items:
+ if kmi.idname == 'wm.call_menu':
+ if kmi.properties.name == VIEW3D_MT_materialutilities_main.bl_idname:
+ km.keymap_items.remove(kmi)
+ break
+
+
+if __name__ == "__main__":
+ register()
diff --git a/materials_utils/enum_values.py b/materials_utils/enum_values.py
new file mode 100644
index 00000000..f7f8f65b
--- /dev/null
+++ b/materials_utils/enum_values.py
@@ -0,0 +1,32 @@
+import bpy
+
+
+mu_override_type_enums = [
+ ('OVERRIDE_ALL', "Override all assigned slots",
+ "Remove any current material slots, and assign the current material"),
+ ('OVERRIDE_SLOTS', 'Assign material to each slot',
+ 'Keep the material slots, but assign the selected material in each slot'),
+ ('APPEND_MATERIAL', 'Append Material',
+ 'Add the material in a new slot, and assign it to the whole object')
+]
+
+mu_fake_user_set_enums = (('ON', "On", "Enable fake user"),
+ ('OFF', "Off", "Disable fake user"),
+ ('TOGGLE', "Toggle", "Toggle fake user"))
+mu_fake_user_materials_enums = (('ACTIVE', "Active object", "Materials of active object only"),
+ ('SELECTED', "Selected objects", "Materials of selected objects"),
+ ('SCENE', "Scene objects", "Materials of objects in current scene"),
+ ('USED', "Used", "All materials used by objects"),
+ ('UNUSED', "Unused", "Currently unused materials"),
+ ('ALL', "All", "All materials in this blend file"))
+
+mu_link_to_enums = (('DATA', "Data", "Link the materials to the data"),
+ ('OBJECT', "Object", "Link the materials to the object"),
+ ('TOGGLE', "Toggle", "Toggle what the materials are currently linked to"))
+mu_link_affect_enums = (('ACTIVE', "Active object", "Materials of active object only"),
+ ('SELECTED', "Selected objects", "Materials of selected objects"),
+ ('SCENE', "Scene objects", "Materials of objects in current scene"),
+ ('ALL', "All", "All materials in this blend file"))
+
+mu_material_slot_move_enums = (('TOP', "Top", "Move slot to the top"),
+ ('BOTTOM', "Bottom", "Move slot to the bottom"))
diff --git a/materials_utils/functions.py b/materials_utils/functions.py
new file mode 100644
index 00000000..6c8c72bd
--- /dev/null
+++ b/materials_utils/functions.py
@@ -0,0 +1,604 @@
+import bpy
+
+# -----------------------------------------------------------------------------
+# utility functions
+
+def mu_assign_material_slots(object, material_list):
+ """Given an object and a list of material names removes all material slots from the object
+ adds new ones for each material in the material list, adds the materials to the slots as well."""
+
+ scene = bpy.context.scene
+ active_object = bpy.context.active_object
+ bpy.context.view_layer.objects.active = object
+
+ for s in object.material_slots:
+ bpy.ops.object.material_slot_remove()
+
+ # re-add them and assign material
+ i = 0
+ for mat in material_list:
+ material = bpy.data.materials[mat]
+ object.data.materials.append(material)
+ i += 1
+
+ # restore active object:
+ bpy.context.view_layer.objects.active = active_object
+
+def mu_assign_to_data(object, material, index, edit_mode, all = True):
+ """Assign the material to the object data (polygons/splines)"""
+
+ if object.type == 'MESH':
+ # now assign the material to the mesh
+ mesh = object.data
+ if all:
+ for poly in mesh.polygons:
+ poly.material_index = index
+ else:
+ for poly in mesh.polygons:
+ if poly.select:
+ poly.material_index = index
+
+ mesh.update()
+
+ elif object.type in {'CURVE', 'SURFACE', 'TEXT'}:
+ bpy.ops.object.mode_set(mode = 'EDIT') # This only works in Edit mode
+
+ # If operator was run in Object mode
+ if not edit_mode:
+ # Select everything in Edit mode
+ bpy.ops.curve.select_all(action = 'SELECT')
+
+ bpy.ops.object.material_slot_assign() # Assign material of the current slot to selection
+
+ if not edit_mode:
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+
+def mu_new_material_name(material):
+ return material
+
+
+def mu_clear_materials(object):
+ #obj.data.materials.clear()
+
+ for mat in object.material_slots:
+ bpy.ops.object.material_slot_remove()
+
+
+def mu_assign_material(self, material_name = "Default", override_type = 'APPEND_MATERIAL', link_override = 'KEEP'):
+ """Assign the defined material to selected polygons/objects"""
+
+ # get active object so we can restore it later
+ active_object = bpy.context.active_object
+
+ edit_mode = False
+ all_polygons = True
+ if (not active_object is None) and active_object.mode == 'EDIT':
+ edit_mode = True
+ all_polygons = False
+ bpy.ops.object.mode_set()
+
+ # check if material exists, if it doesn't then create it
+ found = False
+ for material in bpy.data.materials:
+ if material.name == material_name:
+ target = material
+ found = True
+ break
+
+ if not found:
+ target = bpy.data.materials.new(mu_new_material_name(material_name))
+ target.use_nodes = True # When do we not want nodes today?
+
+
+ index = 0
+ objects = bpy.context.selected_editable_objects
+
+ for obj in objects:
+ # Apparently selected_editable_objects includes objects as cameras etc
+ if not obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
+ continue
+
+ # set the active object to our object
+ scene = bpy.context.scene
+ bpy.context.view_layer.objects.active = obj
+
+ if link_override == 'KEEP':
+ if len(obj.material_slots) > 0:
+ link = obj.material_slots[0].link
+ else:
+ link = 'DATA'
+ else:
+ link = link_override
+
+ # If we should override all current material slots
+ if override_type == 'OVERRIDE_ALL' or obj.type == 'META':
+
+ # If there's more than one slot, Clear out all the material slots
+ if len(obj.material_slots) > 1:
+ mu_clear_materials(obj)
+
+ # If there's no slots left/never was one, add a slot
+ if len(obj.material_slots) == 0:
+ bpy.ops.object.material_slot_add()
+
+ # Assign the material to that slot
+ obj.material_slots[0].link = link
+ obj.material_slots[0].material = target
+
+ if obj.type == 'META':
+ self.report({'INFO'}, "Meta balls only support one material, all other materials overriden!")
+
+ # If we should override each material slot
+ elif override_type == 'OVERRIDE_SLOTS':
+ i = 0
+ # go through each slot
+ for material in obj.material_slots:
+ # assign the target material to current slot
+ if not link_override == 'KEEP':
+ obj.material_slots[i].link = link
+ obj.material_slots[i].material = target
+ i += 1
+
+ # if we should keep the material slots and just append the selected material (if not already assigned)
+ elif override_type == 'APPEND_MATERIAL':
+ found = False
+ i = 0
+ material_slots = obj.material_slots
+
+ if (obj.data.users > 1) and (len(material_slots) >= 1 and material_slots[0].link == 'OBJECT'):
+ self.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' +
+ 'Unwanted results might happen!')
+
+ # check material slots for material_name materia
+ for material in material_slots:
+ if material.name == material_name:
+ found = True
+ index = i
+
+ # make slot active
+ obj.active_material_index = i
+ break
+ i += 1
+
+ if not found:
+ # In Edit mode, or if there's not a slot, append the assigned material
+ # If we're overriding, there's currently no materials at all, so after this there will be 1
+ # If not, this adds another slot with the assigned material
+
+ index = len(obj.material_slots)
+ bpy.ops.object.material_slot_add()
+ obj.material_slots[index].link = link
+ obj.material_slots[index].material = target
+ obj.active_material_index = index
+
+ mu_assign_to_data(obj, target, index, edit_mode, all_polygons)
+
+ # We shouldn't risk unsetting the active object
+ if not active_object is None:
+ # restore the active object
+ bpy.context.view_layer.objects.active = active_object
+
+ if edit_mode:
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ return {'FINISHED'}
+
+
+def mu_select_by_material_name(self, find_material_name, extend_selection = False):
+ """Searches through all objects, or the polygons/curves of the current object
+ to find and select objects/data with the desired material"""
+
+ # in object mode selects all objects with material find_material_name
+ # in edit mode selects all polygons with material find_material_name
+
+ find_material = bpy.data.materials.get(find_material_name)
+
+ if find_material is None:
+ self.report({'INFO'}, "The material " + find_material_name + " doesn't exists!")
+ return {'CANCELLED'}
+
+ # check for edit_mode
+ edit_mode = False
+ found_material = False
+
+ scene = bpy.context.scene
+
+ # set selection mode to polygons
+ scene.tool_settings.mesh_select_mode = False, False, True
+
+ active_object = bpy.context.active_object
+
+ if (not active_object is None) and (active_object.mode == 'EDIT'):
+ edit_mode = True
+
+ if not edit_mode:
+ objects = bpy.context.visible_objects
+
+ for obj in objects:
+ if obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
+ mat_slots = obj.material_slots
+ for material in mat_slots:
+ if material.material == find_material:
+ obj.select_set(state = True)
+
+ found_material = True
+
+ # the active object may not have the material!
+ # set it to one that does!
+ bpy.context.view_layer.objects.active = obj
+ break
+ else:
+ if not extend_selection:
+ obj.select_set(state=False)
+
+ #deselect non-meshes
+ elif not extend_selection:
+ obj.select_set(state=False)
+
+ if not found_material:
+ self.report({'INFO'}, "No objects found with the material " +
+ find_material_name + "!")
+ return {'FINISHED'}
+
+ else:
+ # it's edit_mode, so select the polygons
+
+ if active_object.type == 'MESH':
+ # if not extending the selection, deselect all first
+ # (Without this, edges/faces were still selected
+ # while the faces were deselcted)
+ if not extend_selection:
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+
+ objects = bpy.context.selected_editable_objects
+
+ for obj in objects:
+ print("Obj:" + obj.name)
+ bpy.context.view_layer.objects.active = obj
+
+ if obj.type == 'MESH':
+ bpy.ops.object.mode_set()
+
+ mat_slots = obj.material_slots
+
+ # same material can be on multiple slots
+ slot_indeces = []
+ i = 0
+ for material in mat_slots:
+ if material.material == find_material:
+ slot_indeces.append(i)
+ i += 1
+
+ mesh = obj.data
+
+ for poly in mesh.polygons:
+ if poly.material_index in slot_indeces:
+ poly.select = True
+ found_material = True
+ elif not extend_selection:
+ poly.select = False
+
+ mesh.update()
+
+ bpy.ops.object.mode_set(mode = 'EDIT')
+
+
+ elif obj.type in {'CURVE', 'SURFACE'}:
+ # For Curve objects, there can only be one material per spline
+ # and thus each spline is linked to one material slot.
+ # So to not have to care for different data structures
+ # for different curve types, we use the material slots
+ # and the built in selection methods
+ # (Technically, this should work for meshes as well)
+
+ mat_slots = obj.material_slots
+
+ i = 0
+ for material in mat_slots:
+ bpy.context.active_object.active_material_index = i
+
+ if material.material == find_material:
+ bpy.ops.object.material_slot_select()
+ found_material = True
+ elif not extend_selection:
+ bpy.ops.object.material_slot_deselect()
+
+ i += 1
+
+ else:
+ # Some object types are not supported
+ # mostly because don't really support selecting by material (like Font/Text objects)
+ # ore that they don't support multiple materials/are just "weird" (i.e. Meta balls)
+ self.report({'WARNING'}, "The type '" +
+ obj.type +
+ "' isn't supported in Edit mode by Material Utilities!")
+ #return {'CANCELLED'}
+
+ bpy.context.view_layer.objects.active = active_object
+
+ if not found_material:
+ self.report({'INFO'}, "Material " + find_material_name + " isn't assigned to anything!")
+
+ return {'FINISHED'}
+
+
+def mu_copy_material_to_others(self):
+ """Copy the material to of the current object to the other seleceted all_objects"""
+ # Currently uses the built-in method
+ # This could be extended to work in edit mode as well
+
+ #active_object = context.active_object
+
+ bpy.ops.object.material_slot_copy()
+
+ return {'FINISHED'}
+
+
+def mu_cleanmatslots(self):
+ """Clean the material slots of the seleceted objects"""
+
+ # check for edit mode
+ edit_mode = False
+ active_object = bpy.context.active_object
+ if active_object.mode == 'EDIT':
+ edit_mode = True
+ bpy.ops.object.mode_set()
+
+ objects = bpy.context.selected_editable_objects
+
+ for obj in objects:
+ used_mat_index = [] # we'll store used materials indices here
+ assigned_materials = []
+ material_list = []
+ material_names = []
+
+ materials = obj.material_slots.keys()
+
+ if obj.type == 'MESH':
+ # check the polygons on the mesh to build a list of used materials
+ mesh = obj.data
+
+ for poly in mesh.polygons:
+ # get the material index for this face...
+ material_index = poly.material_index
+
+ if material_index >= len(materials):
+ poly.select = True
+ self.report({'ERROR'},
+ "A poly with an invalid material was found, this should not happen! Canceling!")
+ return {'CANCELLED'}
+
+ # indices will be lost: Store face mat use by name
+ current_mat = materials[material_index]
+ assigned_materials.append(current_mat)
+
+ # check if index is already listed as used or not
+ found = False
+ for mat in used_mat_index:
+ if mat == material_index:
+ found = True
+
+ if not found:
+ # add this index to the list
+ used_mat_index.append(material_index)
+
+ # re-assign the used materials to the mesh and leave out the unused
+ for u in used_mat_index:
+ material_list.append(materials[u])
+ # we'll need a list of names to get the face indices...
+ material_names.append(materials[u])
+
+ mu_assign_material_slots(obj, material_list)
+
+ # restore face indices:
+ i = 0
+ for poly in mesh.polygons:
+ material_index = material_names.index(assigned_materials[i])
+ poly.material_index = material_index
+ i += 1
+
+ elif obj.type in {'CURVE', 'SURFACE'}:
+
+ splines = obj.data.splines
+
+ for spline in splines:
+ # Get the material index of this spline
+ material_index = spline.material_index
+
+ # indices will be last: Store material use by name
+ current_mat = materials[material_index]
+ assigned_materials.append(current_mat)
+
+ # check if indek is already listed as used or not
+ found = False
+ for mat in used_mat_index:
+ if mat == material_index:
+ found = True
+
+ if not found:
+ # add this index to the list
+ used_mat_index.append(material_index)
+
+ # re-assigned the used materials to the curve and leave out the unused
+ for u in used_mat_index:
+ material_list.append(materials[u])
+ # we'll need a list of names to get the face indices
+ material_names.append(materials[u])
+
+ mu_assign_material_slots(obj, material_list)
+
+ # restore spline indices
+ i = 0
+ for spline in splines:
+ material_index = material_names.index(assigned_materials[i])
+ spline.material_index = material_index
+ i += 1
+
+ else:
+ # Some object types are not supported
+ self.report({'WARNING'},
+ "The type '" + obj.type + "' isn't currently supported " +
+ "for Material slots cleaning by Material Utilities!")
+
+ if edit_mode:
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ return {'FINISHED'}
+
+def mu_remove_material(self, for_active_object = False):
+ """Remove the active material slot from selected object(s)"""
+
+ if for_active_object:
+ bpy.ops.object.material_slot_remove()
+ else:
+ last_active = bpy.context.active_object
+ objects = bpy.context.selected_editable_objects
+
+ for obj in objects:
+ bpy.context.view_layer.objects.active = obj
+ bpy.ops.object.material_slot_remove()
+
+ bpy.context.view_layer.objects.active = last_active
+
+ return {'FINISHED'}
+
+def mu_remove_all_materials(self, for_active_object = False):
+ """Remove all material slots from selected object(s)"""
+
+ if for_active_object:
+ obj = bpy.context.active_object
+
+ # Clear out the material slots
+ obj.data.materials.clear()
+
+ else:
+ last_active = bpy.context.active_object
+ objects = bpy.context.selected_editable_objects
+
+ for obj in objects:
+ obj.data.materials.clear()
+
+ bpy.context.view_layer.objects.active = last_active
+
+ return {'FINISHED'}
+
+
+def mu_replace_material(material_a, material_b, all_objects=False, update_selection=False):
+ """Replace one material with another material"""
+
+ # material_a is the name of original material
+ # material_b is the name of the material to replace it with
+ # 'all' will replace throughout the blend file
+
+ mat_org = bpy.data.materials.get(material_a)
+ mat_rep = bpy.data.materials.get(material_b)
+
+ if mat_org != mat_rep and None not in (mat_org, mat_rep):
+ # Store active object
+ scn = bpy.context.scene
+
+ if all_objects:
+ objs = bpy.data.objects
+ else:
+ objs = bpy.context.selected_editable_objects
+
+ for obj in objs:
+ if obj.type == 'MESH':
+ match = False
+
+ for mat in obj.material_slots:
+ if mat.material == mat_org:
+ mat.material = mat_rep
+
+ # Indicate which objects were affected
+ if update_selection:
+ obj.select_set(state = True)
+ match = True
+
+ if update_selection and not match:
+ obj.select_set(state = False)
+
+ return {'FINISHED'}
+
+
+def mu_set_fake_user(self, fake_user, materials):
+ """Set the fake user flag for the objects material"""
+
+ if materials == 'ALL':
+ mats = (mat for mat in bpy.data.materials if mat.library is None)
+ elif materials == 'UNUSED':
+ mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0)
+ else:
+ mats = []
+ if materials == 'ACTIVE':
+ objs = [bpy.context.active_object]
+ elif materials == 'SELECTED':
+ objs = bpy.context.selected_objects
+ elif materials == 'SCENE':
+ objs = bpy.context.scene.objects
+ else: # materials == 'USED'
+ objs = bpy.data.objects
+ # Maybe check for users > 0 instead?
+
+ mats = (mat for ob in objs
+ if hasattr(ob.data, "materials")
+ for mat in ob.data.materials
+ if mat.library is None)
+
+ if fake_user == 'TOGGLE':
+ done_mats = []
+ for mat in mats:
+ if not mat.name in done_mats:
+ mat.use_fake_user = not mat.use_fake_user
+ done_mats.append(mat.name)
+ else:
+ fake_user_val = fake_user == 'ON'
+ for mat in mats:
+ mat.use_fake_user = fake_user_val
+
+ for area in bpy.context.screen.areas:
+ if area.type in ('PROPERTIES', 'NODE_EDITOR'):
+ area.tag_redraw()
+
+ return {'FINISHED'}
+
+
+def mu_change_material_link(self, link, affect, override_data_material = False):
+ """Change what the materials are linked to (Object or Data), while keeping materials assigned"""
+
+ objects = []
+
+ if affect == "ACTIVE":
+ objects = [bpy.context.active_object]
+ elif affect == "SELECTED":
+ objects = bpy.context.selected_objects
+ elif affect == "SCENE":
+ objects = bpy.context.scene.objects
+ elif affect == "ALL":
+ objects = bpy.data.objects
+
+ for object in objects:
+ index = 0
+ for slot in object.material_slots:
+ present_material = slot.material
+
+ if link == 'TOGGLE':
+ slot.link = ('DATA' if slot.link == 'OBJECT' else 'OBJECT')
+ else:
+ slot.link = link
+
+ if slot.link == 'OBJECT':
+ override_data_material = True
+ elif slot.material is None:
+ override_data_material = True
+ elif not override_data_material:
+ self.report({'INFO'},
+ 'The object Data for object ' + object.name_full + ' already had a material assigned ' +
+ 'to slot #' + str(index) + ' (' + slot.material.name + '), it was not overriden!')
+
+ if override_data_material:
+ slot.material = present_material
+
+ index = index + 1
+
+ return {'FINISHED'}
diff --git a/materials_utils/menus.py b/materials_utils/menus.py
new file mode 100644
index 00000000..faf6da0c
--- /dev/null
+++ b/materials_utils/menus.py
@@ -0,0 +1,216 @@
+import bpy
+
+from .functions import *
+from .operators import *
+
+# -----------------------------------------------------------------------------
+# menu classes
+
+class VIEW3D_MT_materialutilities_assign_material(bpy.types.Menu):
+ """Menu for choosing which material should be assigned to current selection"""
+ # The menu is filled programmatically with available materials
+
+ bl_idname = "VIEW3D_MT_materialutilities_assign_material"
+ bl_label = "Assign Material"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ bl_id = VIEW3D_OT_materialutilities_assign_material_object.bl_idname
+ obj = context.object
+
+ if (not obj is None) and obj.mode == 'EDIT':
+ bl_id = VIEW3D_OT_materialutilities_assign_material_edit.bl_idname
+
+ for material_name, material in bpy.data.materials.items():
+ layout.operator(bl_id,
+ text = material_name,
+ icon_value = material.preview.icon_id).material_name = material_name
+
+ layout.operator(bl_id,
+ text = "Add New Material",
+ icon = 'ADD').material_name = "Unnamed material"
+
+
+class VIEW3D_MT_materialutilities_clean_slots(bpy.types.Menu):
+ """Menu for cleaning up the material slots"""
+
+ bl_idname = "VIEW3D_MT_materialutilities_clean_slots"
+ bl_label = "Clean Slots"
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.label
+ layout.operator(VIEW3D_OT_materialutilities_clean_material_slots.bl_idname,
+ text = "Clean Material Slots",
+ icon = 'X')
+ layout.separator()
+ layout.operator(VIEW3D_OT_materialutilities_remove_material_slot.bl_idname,
+ text = "Remove Active Material Slot",
+ icon = 'REMOVE')
+ layout.operator(VIEW3D_OT_materialutilities_remove_all_material_slots.bl_idname,
+ text = "Remove All Material Slots",
+ icon = 'CANCEL')
+
+
+class VIEW3D_MT_materialutilities_select_by_material(bpy.types.Menu):
+ """Menu for choosing which material should be used for selection"""
+ # The menu is filled programmatically with available materials
+
+ bl_idname = "VIEW3D_MT_materialutilities_select_by_material"
+ bl_label = "Select by Material"
+
+ def draw(self, context):
+ layout = self.layout
+
+ obj = context.object
+ layout.label
+
+ if obj is None or obj.mode == 'OBJECT':
+ #show all used materials in entire blend file
+ for material_name, material in bpy.data.materials.items():
+ # There's no point in showing materials with 0 users
+ # (It will still show materials with fake user though)
+ if material.users > 0:
+ layout.operator(VIEW3D_OT_materialutilities_select_by_material_name.bl_idname,
+ text = material_name,
+ icon_value = material.preview.icon_id
+ ).material_name = material_name
+
+ elif obj.mode == 'EDIT':
+ objects = context.selected_editable_objects
+ materials_added = []
+
+ for obj in objects:
+ #show only the materials on this object
+ material_slots = obj.material_slots
+ for material_slot in material_slots:
+ material = material_slot.material
+
+ # Don't add a material that's already in the menu
+ if material.name in materials_added:
+ continue
+
+ layout.operator(VIEW3D_OT_materialutilities_select_by_material_name.bl_idname,
+ text = material.name,
+ icon_value = material.preview.icon_id
+ ).material_name = material.name
+
+ materials_added.append(material.name)
+
+class VIEW3D_MT_materialutilities_specials(bpy.types.Menu):
+ """Spcials menu for Material Utilities"""
+
+ bl_idname = "VIEW3D_MT_materialutilities_specials"
+ bl_label = "Specials"
+
+ def draw(self, context):
+ layout = self.layout
+
+ #layout.operator(VIEW3D_OT_materialutilities_set_new_material_name.bl_idname, icon = "SETTINGS")
+
+ #layout.separator()
+
+ layout.operator(MATERIAL_OT_materialutilities_merge_base_names.bl_idname,
+ text = "Merge Base Names",
+ icon = "GREASEPENCIL")
+
+
+class VIEW3D_MT_materialutilities_main(bpy.types.Menu):
+ """Main menu for Material Utilities"""
+
+ bl_idname = "VIEW3D_MT_materialutilities_main"
+ bl_label = "Material Utilities"
+
+ def draw(self, context):
+ obj = context.object
+
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.menu(VIEW3D_MT_materialutilities_assign_material.bl_idname,
+ icon = 'ADD')
+ layout.menu(VIEW3D_MT_materialutilities_select_by_material.bl_idname,
+ icon = 'VIEWZOOM')
+ layout.separator()
+
+ layout.operator(VIEW3D_OT_materialutilities_copy_material_to_others.bl_idname,
+ text = 'Copy Materials to Selected',
+ icon = 'COPY_ID')
+
+ layout.separator()
+
+ layout.menu(VIEW3D_MT_materialutilities_clean_slots.bl_idname,
+ icon = 'NODE_MATERIAL')
+
+ layout.separator()
+ layout.operator(VIEW3D_OT_materialutilities_replace_material.bl_idname,
+ text = 'Replace Material',
+ icon = 'OVERLAY')
+
+ layout.operator(VIEW3D_OT_materialutilities_fake_user_set.bl_idname,
+ text = 'Set Fake User',
+ icon = 'FAKE_USER_OFF')
+
+ layout.operator(VIEW3D_OT_materialutilities_change_material_link.bl_idname,
+ text = 'Change Material Link',
+ icon = 'LINKED')
+ layout.separator()
+
+ layout.menu(VIEW3D_MT_materialutilities_specials.bl_idname,
+ icon = 'SOLO_ON')
+
+
+
+def materialutilities_specials_menu(self, contxt):
+ self.layout.separator()
+ self.layout.menu(VIEW3D_MT_materialutilities_main.bl_idname)
+
+
+def materialutilities_menu_move(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.operator(MATERIAL_OT_materialutilities_material_slot_move.bl_idname,
+ icon = 'TRIA_UP_BAR',
+ text = 'Move Slot to the Top').movement = 'TOP'
+ layout.operator(MATERIAL_OT_materialutilities_material_slot_move.bl_idname,
+ icon = 'TRIA_DOWN_BAR',
+ text = 'Move Slot to the Bottom').movement = 'BOTTOM'
+ layout.separator()
+
+def materialutilities_menu_functions(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.separator()
+
+ layout.menu(VIEW3D_MT_materialutilities_assign_material.bl_idname,
+ icon = 'ADD')
+ layout.menu(VIEW3D_MT_materialutilities_select_by_material.bl_idname,
+ icon = 'VIEWZOOM')
+ layout.separator()
+
+ layout.separator()
+
+ layout.menu(VIEW3D_MT_materialutilities_clean_slots.bl_idname,
+ icon = 'NODE_MATERIAL')
+
+ layout.separator()
+ layout.operator(VIEW3D_OT_materialutilities_replace_material.bl_idname,
+ text = 'Replace Material',
+ icon = 'OVERLAY')
+
+ layout.operator(VIEW3D_OT_materialutilities_fake_user_set.bl_idname,
+ text = 'Set Fake User',
+ icon = 'FAKE_USER_OFF')
+
+ layout.operator(VIEW3D_OT_materialutilities_change_material_link.bl_idname,
+ text = 'Change Material Link',
+ icon = 'LINKED')
+ layout.separator()
+
+ layout.menu(VIEW3D_MT_materialutilities_specials.bl_idname,
+ icon = 'SOLO_ON')
diff --git a/materials_utils/operators.py b/materials_utils/operators.py
new file mode 100644
index 00000000..6bb451e6
--- /dev/null
+++ b/materials_utils/operators.py
@@ -0,0 +1,525 @@
+import bpy
+
+from bpy.types import Operator
+from bpy.props import StringProperty, BoolProperty, EnumProperty
+
+
+from .enum_values import *
+from .functions import *
+
+# -----------------------------------------------------------------------------
+# operator classes
+
+class VIEW3D_OT_materialutilities_assign_material_edit(bpy.types.Operator):
+ """Assign a material to the current selection"""
+
+ bl_idname = "view3d.materialutilities_assign_material_edit"
+ bl_label = "Assign Material (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ material_name: StringProperty(
+ name = 'Material Name',
+ description = 'Name of Material to assign to current selection',
+ default = "",
+ maxlen = 63
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.active_object is not None
+
+ def execute(self, context):
+ material_name = self.material_name
+ return mu_assign_material(self, material_name, 'APPEND_MATERIAL')
+
+
+
+class VIEW3D_OT_materialutilities_assign_material_object(bpy.types.Operator):
+ """Assign a material to the current selection
+ (See the operator panel [F9] for more options)"""
+
+ bl_idname = "view3d.materialutilities_assign_material_object"
+ bl_label = "Assign Material (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ material_name: StringProperty(
+ name = 'Material Name',
+ description = 'Name of Material to assign to current selection',
+ default = "Unnamed Material",
+ maxlen = 63
+ )
+ override_type: EnumProperty(
+ name = 'Assignment method',
+ description = '',
+ items = mu_override_type_enums
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return len(context.selected_editable_objects) > 0
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop_search(self, "material_name", bpy.data, "materials")
+
+ layout.prop(self, "override_type")
+
+ def execute(self, context):
+ material_name = self.material_name
+ override_type = self.override_type
+ result = mu_assign_material(self, material_name, override_type)
+ return result
+
+
+class VIEW3D_OT_materialutilities_select_by_material_name(bpy.types.Operator):
+ """Select geometry that has the chosen material assigned to it
+ (See the operator panel [F9] for more options)"""
+
+ bl_idname = "view3d.materialutilities_select_by_material_name"
+ bl_label = "Select By Material Name (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ extend_selection: BoolProperty(
+ name = 'Extend Selection',
+ description = 'Keeps the current selection and adds faces with the material to the selection'
+ )
+ material_name: StringProperty(
+ name = 'Material Name',
+ description = 'Name of Material to find and Select',
+ maxlen = 63
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return len(context.visible_objects) > 0
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop_search(self, "material_name", bpy.data, "materials")
+
+ layout.prop(self, "extend_selection", icon = "SELECT_EXTEND")
+
+ def execute(self, context):
+ material_name = self.material_name
+ ext = self.extend_selection
+ return mu_select_by_material_name(self, material_name, ext)
+
+
+class VIEW3D_OT_materialutilities_copy_material_to_others(bpy.types.Operator):
+ """Copy the material(s) of the active object to the other selected objects"""
+
+ bl_idname = "view3d.materialutilities_copy_material_to_others"
+ bl_label = "Copy material(s) to others (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
+
+ def execute(self, context):
+ return mu_copy_material_to_others(self)
+
+
+class VIEW3D_OT_materialutilities_clean_material_slots(bpy.types.Operator):
+ """Removes any material slots from the selected objects that are not used"""
+
+ bl_idname = "view3d.materialutilities_clean_material_slots"
+ bl_label = "Clean Material Slots (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ return len(context.selected_editable_objects) > 0
+
+ def execute(self, context):
+ return mu_cleanmatslots(self)
+
+
+class VIEW3D_OT_materialutilities_remove_material_slot(bpy.types.Operator):
+ """Remove the active material slot from selected object(s)
+ (See the operator panel [F9] for more options)"""
+
+ bl_idname = "view3d.materialutilities_remove_material_slot"
+ bl_label = "Remove Active Material Slot (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ only_active: BoolProperty(
+ name = 'Only active object',
+ description = 'Only remove the active material slot for the active object ' +
+ '(otherwise do it for every selected object)'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")
+
+ def execute(self, context):
+ return mu_remove_material(self, self.only_active)
+
+class VIEW3D_OT_materialutilities_remove_all_material_slots(bpy.types.Operator):
+ """Remove all material slots from selected object(s)
+ (See the operator panel [F9] for more options)"""
+
+ bl_idname = "view3d.materialutilities_remove_all_material_slots"
+ bl_label = "Remove All Material Slots (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ only_active: BoolProperty(
+ name = 'Only active object',
+ description = 'Only remove the material slots for the active object ' +
+ '(otherwise do it for every selected object)'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return (context.active_object is not None) and (context.active_object.mode != 'EDIT')
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "only_active", icon = "PIVOT_ACTIVE")
+
+ def execute(self, context):
+ return mu_remove_all_materials(self, self.only_active)
+
+
+class VIEW3D_OT_materialutilities_replace_material(bpy.types.Operator):
+ """Replace a material by name"""
+ bl_idname = "view3d.materialutilities_replace_material"
+ bl_label = "Replace Material (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ matorg: StringProperty(
+ name = "Original",
+ description = "Material to find and replace",
+ maxlen = 63,
+ )
+ matrep: StringProperty(name="Replacement",
+ description = "Material that will be used instead of the Original material",
+ maxlen = 63,
+ )
+ all_objects: BoolProperty(
+ name = "All Objects",
+ description = "Replace for all objects in this blend file (otherwise only selected objects)",
+ default = True,
+ )
+ update_selection: BoolProperty(
+ name = "Update Selection",
+ description = "Select affected objects and deselect unaffected",
+ default = True,
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.prop_search(self, "matorg", bpy.data, "materials")
+ layout.prop_search(self, "matrep", bpy.data, "materials")
+ layout.separator()
+
+ layout.prop(self, "all_objects", icon = "BLANK1")
+ layout.prop(self, "update_selection", icon = "SELECT_INTERSECT")
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ return mu_replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection)
+
+
+class VIEW3D_OT_materialutilities_fake_user_set(bpy.types.Operator):
+ """Enable/disable fake user for materials"""
+
+ bl_idname = "view3d.materialutilities_fake_user_set"
+ bl_label = "Set Fake User (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ fake_user: EnumProperty(
+ name = "Fake User",
+ description = "Turn fake user on or off",
+ items = mu_fake_user_set_enums,
+ default = 'TOGGLE'
+ )
+
+ materials: EnumProperty(
+ name = "Materials",
+ description = "Which materials of objects to affect",
+ items = mu_fake_user_materials_enums,
+ default = 'UNUSED'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return (context.active_object is not None)
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "fake_user", expand = True)
+ layout.separator()
+
+ layout.prop(self, "materials")
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ return mu_set_fake_user(self, self.fake_user, self.materials)
+
+
+class VIEW3D_OT_materialutilities_change_material_link(bpy.types.Operator):
+ """Link the materials to Data or Object, while keepng materials assigned"""
+
+ bl_idname = "view3d.materialutilities_change_material_link"
+ bl_label = "Change Material Linking (Material Utilities)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ override: BoolProperty(
+ name = "Override Data material",
+ description = "Override the materials assigned to the object data/mesh when switching to 'Linked to Data'\n" +
+ "(WARNING: This will override the materials of other linked objects, " +
+ "which have the materials linked to Data)",
+ default = False,
+ )
+ link_to: EnumProperty(
+ name = "Link",
+ description = "What should the material be linked to",
+ items = mu_link_to_enums,
+ default = 'OBJECT'
+ )
+
+ affect: EnumProperty(
+ name = "Materials",
+ description = "Which materials of objects to affect",
+ items = mu_link_affect_enums,
+ default = 'SELECTED'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return (context.active_object is not None)
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.prop(self, "link_to", expand = True)
+ layout.separator()
+
+ layout.prop(self, "affect")
+ layout.separator()
+
+ layout.prop(self, "override", icon = "DECORATE_OVERRIDE")
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ return mu_change_material_link(self, self.link_to, self.affect, self.override)
+
+class MATERIAL_OT_materialutilities_merge_base_names(bpy.types.Operator):
+ """Merges materials that has the same base names but ends with .xxx (.001, .002 etc)"""
+
+ bl_idname = "material.materialutilities_merge_base_names"
+ bl_label = "Merge Base Names"
+ bl_description = "Merge materials that has the same base names but ends with .xxx (.001, .002 etc)"
+
+ material_base_name: StringProperty(
+ name = "Material Base Name",
+ default = "",
+ description = 'Base name for materials to merge ' +
+ '(e.g. "Material" is the base name of "Material.001", "Material.002" etc.)'
+ )
+ is_auto: BoolProperty(
+ name = "Auto Merge",
+ description = "Find all available duplicate materials and Merge them"
+ )
+
+ is_not_undo = False
+ material_error = [] # collect mat for warning messages
+
+
+ def replace_name(self):
+ """If the user chooses a material like 'Material.042', clean it up to get a base name ('Material')"""
+
+ # use the chosen material as a base one, check if there is a name
+ self.check_no_name = (False if self.material_base_name in {""} else True)
+
+ # No need to do this if it's already "clean"
+ # (Also lessens the potential of error given about the material with the Base name)
+ if '.' not in self.material_base_name:
+ return
+
+ if self.check_no_name is True:
+ for mat in bpy.data.materials:
+ name = mat.name
+
+ if name == self.material_base_name:
+ try:
+ base, suffix = name.rsplit('.', 1)
+
+ # trigger the exception
+ num = int(suffix, 10)
+ self.material_base_name = base
+ mat.name = self.material_base_name
+ return
+ except ValueError:
+ if name not in self.material_error:
+ self.material_error.append(name)
+ return
+
+ return
+
+ def split_name(self, material):
+ """Split the material name into a base and a suffix"""
+
+ name = material.name
+
+ # No need to do this if it's already "clean"/there is no suffix
+ if '.' not in name:
+ return name, None
+
+ base, suffix = name.rsplit('.', 1)
+
+ try:
+ # trigger the exception
+ num = int(suffix, 10)
+ except ValueError:
+ # Not a numeric suffix
+ # Don't report on materials not actually included in the merge!
+ if ((self.is_auto or base == self.material_base_name)
+ and (name not in self.material_error)):
+ self.material_error.append(name)
+ return name, None
+
+ if self.is_auto is False:
+ if base == self.material_base_name:
+ return base, suffix
+ else:
+ return name, None
+
+ return base, suffix
+
+ def fixup_slot(self, slot):
+ """Fix material slots that was assigned to materials now removed"""
+
+ if not slot.material:
+ return
+
+ base, suffix = self.split_name(slot.material)
+ if suffix is None:
+ return
+
+ try:
+ base_mat = bpy.data.materials[base]
+ except KeyError:
+ print("\n[Materials Utilities Specials]\nLink to base names\nError:"
+ "Base material %r not found\n" % base)
+ return
+
+ slot.material = base_mat
+
+ def main_loop(self, context):
+ """Loops through all objects and material slots to make sure they are assigned to the right material"""
+
+ for obj in context.scene.objects:
+ for slot in obj.material_slots:
+ self.fixup_slot(slot)
+
+ @classmethod
+ def poll(self, context):
+ return len(context.selected_editable_objects) > 0
+
+ def draw(self, context):
+ layout = self.layout
+
+ box_1 = layout.box()
+ box_1.prop_search(self, "material_base_name", bpy.data, "materials")
+ box_1.enabled = not self.is_auto
+ layout.separator()
+
+ layout.prop(self, "is_auto", text = "Auto Rename/Replace", icon = "SYNTAX_ON")
+
+ def invoke(self, context, event):
+ self.is_not_undo = True
+ return context.window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ # Reset Material errors, otherwise we risk reporting errors erroneously..
+ self.material_error = []
+
+ if not self.is_auto:
+ self.replace_name()
+
+ if self.check_no_name:
+ self.main_loop(context)
+ else:
+ self.report({'WARNING'}, "No Material Base Name given!")
+
+ self.is_not_undo = False
+ return {'CANCELLED'}
+
+ self.main_loop(context)
+
+ if self.material_error:
+ materials = ", ".join(self.material_error)
+
+ if len(self.material_error) == 1:
+ waswere = " was"
+ suff_s = ""
+ else:
+ waswere = " were"
+ suff_s = "s"
+
+ self.report({'WARNING'}, materials + waswere + " not removed or set as Base" + suff_s)
+
+ self.is_not_undo = False
+ return {'FINISHED'}
+
+class MATERIAL_OT_materialutilities_material_slot_move(bpy.types.Operator):
+ """Move the active material slot"""
+
+ bl_idname = "material.materialutilities_slot_move"
+ bl_label = "Move Slot"
+ bl_description = "Move the material slot"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ movement: EnumProperty(
+ name = "Move",
+ description = "How to move the material slot",
+ items = mu_material_slot_move_enums
+ )
+
+ @classmethod
+ def poll(self, context):
+ # would prefer to access sely.movement here, but can'-'t..
+ obj = context.active_object
+ if not obj:
+ return False
+ if (obj.active_material_index < 0) or (len(obj.material_slots) <= 1):
+ return False
+ return True
+
+ def execute(self, context):
+ active_object = context.active_object
+ active_material = context.object.active_material
+
+ if self.movement == 'TOP':
+ dir = 'UP'
+
+ steps = active_object.active_material_index
+ else:
+ dir = 'DOWN'
+
+ last_slot_index = len(active_object.material_slots) - 1
+ steps = last_slot_index - active_object.active_material_index
+
+ if steps == 0:
+ self.report({'WARNING'}, active_material.name + " already at " + self.movement.lower() + '!')
+ else:
+ for i in range(steps):
+ bpy.ops.object.material_slot_move(direction = dir)
+
+ self.report({'INFO'}, active_material.name + ' moved to ' + self.movement.lower())
+
+ return {'FINISHED'}