Extra Material List - Blender Addon

teaser.png

Have you ever struggled with the tiny pop-up list in the material Node Editor? So have I!

Therefore, I made an addon that enables to pop-up an extra material list with specified number of rows and columns. Optionally, you can display all materials in a plain list.

Moreover, there is a button to remove material and node group duplicates (ending with .001, .002, etc), which might occur after appending assets from external files.

Features

  • Two display options (preview and plain list)
  • Display object and world materials
  • Eliminate duplicates for node groups and materials
  • Located in Node Editor - Tools panel (T)
ExtraMaterialList_alpha.png

Download

blender-addons/extra-material-list/0.2/ExtraMaterialList.py (Source)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
#-------------------------------------------------------------------------------
#                     Extra Material List - Addon for Blender
#
# - Two display options (preview and plain list)
# - Display object and world materials
# - Eliminate duplicates for node groups and materials
#
# Version: 0.2
# Revised: 11.08.2017
# Author: Miki (meshlogic)
#-------------------------------------------------------------------------------
bl_info = {
    "name": "Extra Material List",
    "author": "Miki (meshlogic)",
    "category": "Node",
    "description": "An alternative object/world material list for Node Editor.",
    "location": "Node Editor > Tools > Material List",
    "version": (0, 2),
    "blender": (2, 79, 0)
}

import bpy
from bpy.props import *
from bpy.types import Menu, Operator, Panel, UIList
from bpy.app.handlers import persistent


#-------------------------------------------------------------------------------
# UI PANEL - Extra Material List
#-------------------------------------------------------------------------------
class ExtraMaterialList_PT(Panel):
    bl_space_type = 'NODE_EDITOR'
    bl_region_type = 'TOOLS'
    bl_category = "Material List"
    bl_label = "Extra Material List"

    #--- Available only for "shading nodes" render
    @classmethod
    def poll(cls, context):
        cs = context.scene
        return cs.render.use_shading_nodes

    #--- Draw Panel
    def draw(self, context):
        layout = self.layout
        cs = context.scene
        sdata = context.space_data
        props = cs.extra_material_list

        #--- Shader tree and type selection
        row = layout.row()
        row.alignment = 'CENTER'
        row.prop(sdata, "tree_type", text="", expand=True)
        row.prop(sdata, "shader_type", text="", expand=True)

        #--- Proceed only for OBJECT/WORLD shader node trees
        if sdata.tree_type != 'ShaderNodeTree' or (sdata.shader_type != 'OBJECT' and sdata.shader_type != 'WORLD'):
            return

        #--- List style buttons
        row = layout.row()
        row.prop(props, "style", expand=True)

        #-----------------------------------------------------------------------
        # PREVIEW Style
        #-----------------------------------------------------------------------
        if props.style == 'PREVIEW':

            #--- Num. of rows & cols for the preview list
            row = layout.row()
            split = row.split(percentage=0.6)
            col = split.column(True)
            col.prop(props, "rows")
            col.prop(props, "cols")

            #--- Object materials
            if sdata.shader_type == 'OBJECT':

                # List of all scene materials
                mat_list = bpy.data.materials

                # Current active material
                if hasattr(sdata.id_from, "active_material"):
                    mat = sdata.id_from.active_material
                else:
                    return

                # Navigation button PREV
                sub = split.column()
                sub.scale_y = 2
                sub.operator("extra_material_list.nav", text="", icon='BACK').dir = 'PREV'
                sub.enabled = enable_prev_button(mat, mat_list)

                # Navigation button NEXT
                sub = split.column()
                sub.scale_y = 2
                sub.operator("extra_material_list.nav", text="", icon='FORWARD').dir = 'NEXT'
                sub.enabled = enable_next_button(mat, mat_list)

                # Preview list
                layout.template_ID_preview(
                    sdata.id_from, "active_material",
                    new = "material.new",
                    rows = props.rows, cols = props.cols
                )

            #--- World materials
            elif sdata.shader_type == 'WORLD':

                # List of all scene worlds
                world_list = bpy.data.worlds

                # Current active world
                world = context.scene.world

                # Navigation button PREV
                sub = split.column()
                sub.scale_y = 2
                sub.operator("extra_material_list.nav", text="", icon='BACK').dir = 'PREV'
                sub.enabled = enable_prev_button(world, world_list)

                # Navigation button NEXT
                sub = split.column()
                sub.scale_y = 2
                sub.operator("extra_material_list.nav", text="", icon='FORWARD').dir = 'NEXT'
                sub.enabled = enable_next_button(world, world_list)

                # Preview list
                layout.template_ID_preview(
                    cs, "world",
                    new = "world.new",
                    rows = props.rows, cols = props.cols
                )

            layout.separator()

        #-----------------------------------------------------------------------
        # LIST Style
        #-----------------------------------------------------------------------
        elif props.style == 'LIST':

            #--- Object materials
            if sdata.shader_type == 'OBJECT':
                layout.template_list(
                    "extra_material_list.material_list", "",
                    bpy.data, "materials",
                    props, "material_id",
                    rows = len(bpy.data.materials)
                )

            #--- World materials
            elif sdata.shader_type == 'WORLD':
                layout.template_list(
                    "extra_material_list.material_list", "",
                    bpy.data, "worlds",
                    props, "world_id",
                    rows = len(bpy.data.worlds)
                )

            #--- Show icons prop
            row = layout.row()
            row.prop(props, "show_icons")

        #-----------------------------------------------------------------------
        # ELIMINATE Duplicates
        #-----------------------------------------------------------------------
        row = layout.row()
        row.label("Eliminate Duplicates:", icon='RADIO')
        row = layout.row(True)
        row.operator("extra_material_list.eliminate_nodegroups", text="Node Groups")
        row.operator("extra_material_list.eliminate_materials", text="Materials")


#-------------------------------------------------------------------------------
# Functions to decide if enable/disable navigation buttons
#-------------------------------------------------------------------------------
def enable_prev_button(item, item_list):
    if item != None and len(item_list) > 0:
        return item != item_list[0]
    else:
        return False

def enable_next_button(item, item_list):
    if item != None and len(item_list) > 0:
        return item != item_list[-1]
    else:
        return False


#-------------------------------------------------------------------------------
# CUSTOM TEMPLATE_LIST FOR MATERIALS
#-------------------------------------------------------------------------------
class ExtraMaterialList_UL(UIList):
    bl_idname = "extra_material_list.material_list"

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        props = bpy.context.scene.extra_material_list

        # Material name and icon
        row = layout.row(True)
        if props.show_icons:
            row.prop(item, "name", text="", emboss=False, icon_value=icon)
        else:
            row.prop(item, "name", text="", emboss=False, icon_value=0)

        # Material status (fake user, zero users)
        row = row.row(True)
        row.alignment = 'RIGHT'

        if item.use_fake_user:
            row.label("F")
        else:
            if item.users == 0:
                row.label("0")


#--- Update the active material when you select another item in the template_list
def update_active_material(self, context):
    try:
        id = bpy.context.scene.extra_material_list.material_id
        if id < len(bpy.data.materials):
            mat = bpy.data.materials[id]
            bpy.context.object.active_material = mat
    except:
        pass

#--- Update the active world shader when you select another item in the template_list
def update_active_world(self, context):
    try:
        id = bpy.context.scene.extra_material_list.world_id
        if id < len(bpy.data.worlds):
            world = bpy.data.worlds[id]
            bpy.context.scene.world = world
    except:
        pass


#-------------------------------------------------------------------------------
# ELIMINATE MATERIAL DUPLICATES
#-------------------------------------------------------------------------------
class ExtraMaterialList_PT_EliminateMaterials(Operator):
    bl_idname = "extra_material_list.eliminate_materials"
    bl_label = "Eliminate Material Duplicates"
    bl_description = "Eliminate material duplicates (ending with .001, .002, etc) and replace them with the original material if found."

    def execute(self, context):
        print("\nEliminate Material Duplicates:")
        mats = bpy.data.materials

        #--- Search for mat. slots in all objects
        for obj in bpy.data.objects:
            for slot in obj.material_slots:

                # Get the material name as 3-tuple (base, separator, extension)
                (base, sep, ext) = slot.name.rpartition('.')

                # Replace the numbered duplicate with the original if found
                if ext.isnumeric():
                    if base in mats:
                        print("  For object '%s' replace '%s' with '%s'" % (obj.name, slot.name, base))
                        slot.material = mats.get(base)

        return{'FINISHED'}


#-------------------------------------------------------------------------------
# ELIMINATE NODE GROUP DUPLICATES
#-------------------------------------------------------------------------------
class ExtraMaterialList_PT_EliminateNodeGroups(Operator):
    bl_idname = "extra_material_list.eliminate_nodegroups"
    bl_label = "Eliminate Node Group Duplicates"
    bl_description = "Eliminate node group duplicates (ending with .001, .002, etc) and replace them with the original node group if found."

    #--- Eliminate node group duplicate with the original group found
    def eliminate(self, node):
        node_groups = bpy.data.node_groups

        # Get the node group name as 3-tuple (base, separator, extension)
        (base, sep, ext) = node.node_tree.name.rpartition('.')

        # Replace the numbered duplicate with original if found
        if ext.isnumeric():
            if base in node_groups:
                print("  Replace '%s' with '%s'" % (node.node_tree.name, base))
                node.node_tree.use_fake_user = False
                node.node_tree = node_groups.get(base)

    #--- Execute
    def execute(self, context):
        print("\nEliminate Node Group Duplicates:")

        mats = list(bpy.data.materials)
        worlds = list(bpy.data.worlds)
        node_groups = bpy.data.node_groups

        #--- Search for duplicates in the actual node groups
        for group in node_groups:
            for node in group.nodes:
                if node.type == 'GROUP':
                    self.eliminate(node)

        #--- Search for duplicates in materials
        for mat in mats + worlds:
            if mat.use_nodes:
                for node in mat.node_tree.nodes:
                    if node.type == 'GROUP':
                        self.eliminate(node)

        return{'FINISHED'}


#-------------------------------------------------------------------------------
# NAVIGATION OPERATOR
#-------------------------------------------------------------------------------
class ExtraMaterialList_PT_Nav(Operator):
    bl_idname = "extra_material_list.nav"
    bl_label = "Nav"
    bl_description = "Navigation button"

    dir = EnumProperty(
        items = [
            ('NEXT', "PREV", "PREV"),
            ('PREV', "PREV", "PREV")
        ],
        name = "dir",
        default = 'NEXT')

    def execute(self, context):
        sdata = context.space_data

        #--- Navigate in object materials
        if sdata.shader_type == 'OBJECT':

            # List of all scene materials
            mat_list = list(bpy.data.materials)

            # Get index of the current active material
            mat = sdata.id_from.active_material
            if mat in mat_list:
                id = mat_list.index(mat)
            else:
                return{'FINISHED'}

            # Navigate
            if self.dir == 'NEXT':
                if id+1 < len(mat_list):
                    sdata.id_from.active_material = mat_list[id+1]

            if self.dir == 'PREV':
                if id > 0:
                    sdata.id_from.active_material = mat_list[id-1]

        #--- Navigate in worlds
        elif sdata.shader_type == 'WORLD':

            # List of all scene worlds
            world_list = list(bpy.data.worlds)

            # Get index of the current active world
            world = context.scene.world
            if world in world_list:
                id = world_list.index(world)
            else:
                return{'FINISHED'}

            # Navigate
            if self.dir == 'NEXT':
                if id+1 < len(world_list):
                    context.scene.world = world_list[id+1]

            if self.dir == 'PREV':
                if id > 0:
                    context.scene.world = world_list[id-1]

        return{'FINISHED'}


#-------------------------------------------------------------------------------
# CUSTOM HANDLER (scene_update_post)
# - This handler is invoked after the scene updates
# - Keeps template_list synced with the active material
#-------------------------------------------------------------------------------
@persistent
def update_material_list(context):
    try:
        props = bpy.context.scene.extra_material_list

        #--- Update world list
        try:
            world = bpy.context.scene.world
            if world != None:
                id = bpy.data.worlds.find(world.name)
                if id != -1 and id != props.world_id:
                    props.world_id = id
        except:
            pass

        #--- Update material list
        try:
            mat = bpy.context.object.active_material
            if mat != None:
                id = bpy.data.materials.find(mat.name)
                if id != -1 and id != props.material_id:
                    props.material_id = id
        except:
            pass
    except:
        pass


#-------------------------------------------------------------------------------
# CUSTOM SCENE PROPS
#-------------------------------------------------------------------------------
class ExtraMaterialList_Props(bpy.types.PropertyGroup):

    style = EnumProperty(
        items = [
            ('PREVIEW', "Preview", "", 0),
            ('LIST', "List", "", 1),
        ],
        default = 'PREVIEW',
        name = "Style",
        description = "Material list style")

    rows = IntProperty(
        name = "Rows",
        description = "Num. of rows in the preview list",
        default = 5, min = 1, max = 15)

    cols = IntProperty(
        name = "Cols",
        description = "Num. of columns in the preview list",
        default = 10, min = 1, max = 30)

    # Index of the active material in the template_list
    material_id = IntProperty(
        default = 0,
        update = update_active_material)

    # Index of the active world in the template_list
    world_id = IntProperty(
        default = 0,
        update = update_active_world)

    show_icons = BoolProperty(
        name = "Show material icons",
        default = False)


#-------------------------------------------------------------------------------
# REGISTER/UNREGISTER ADDON CLASSES
#-------------------------------------------------------------------------------
def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.extra_material_list = PointerProperty(type=ExtraMaterialList_Props)
    bpy.app.handlers.scene_update_post.append(update_material_list)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.extra_material_list
    bpy.app.handlers.scene_update_post.remove(update_material_list)

if __name__ == "__main__":
    register()

Comments

Comments powered by Disqus