Skip to content

Instantly share code, notes, and snippets.

@p2or
Last active August 18, 2023 20:58
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save p2or/30b8b30c89871b8ae5c97803107fd494 to your computer and use it in GitHub Desktop.
Save p2or/30b8b30c89871b8ae5c97803107fd494 to your computer and use it in GitHub Desktop.
# ##### 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 #####
bl_info = {
"name": "material-pointer-uilist-dev",
"description": "",
"author": "p2or",
"version": (0, 2),
"blender": (2, 80, 0),
"location": "Text Editor",
"warning": "", # used for warning icon and text in addons panel
"wiki_url": "",
"tracker_url": "",
"category": "Development"
}
import bpy
from bpy.props import (IntProperty,
BoolProperty,
StringProperty,
CollectionProperty,
PointerProperty)
from bpy.types import (Operator,
Panel,
PropertyGroup,
UIList)
# -------------------------------------------------------------------
# Operators
# -------------------------------------------------------------------
class CUSTOM_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "custom.list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER'}
action: bpy.props.EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", "")))
def random_color(self):
from mathutils import Color
from random import random
return Color((random(), random(), random()))
def invoke(self, context, event):
scn = context.scene
idx = scn.custom_index
try:
item = scn.custom[idx]
except IndexError:
pass
else:
if self.action == 'DOWN' and idx < len(scn.custom) - 1:
item_next = scn.custom[idx+1].name
scn.custom.move(idx, idx+1)
scn.custom_index += 1
info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
self.report({'INFO'}, info)
elif self.action == 'UP' and idx >= 1:
item_prev = scn.custom[idx-1].name
scn.custom.move(idx, idx-1)
scn.custom_index -= 1
info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
self.report({'INFO'}, info)
elif self.action == 'REMOVE':
item = scn.custom[scn.custom_index]
mat = item.material
if mat:
mat_obj = bpy.data.materials.get(mat.name, None)
if mat_obj:
bpy.data.materials.remove(mat_obj, do_unlink=True)
info = 'Item %s removed from scene' % (item)
scn.custom.remove(idx)
if scn.custom_index == 0:
scn.custom_index = 0
else:
scn.custom_index -= 1
self.report({'INFO'}, info)
if self.action == 'ADD':
item = scn.custom.add()
item.id = len(scn.custom)
item.material = bpy.data.materials.new(name="Material")
item.name = item.material.name
col = self.random_color()
item.material.diffuse_color = (col.r, col.g, col.b, 1.0)
scn.custom_index = (len(scn.custom)-1)
info = '%s added to list' % (item.name)
self.report({'INFO'}, info)
return {"FINISHED"}
class CUSTOM_OT_addBlendMaterials(Operator):
"""Add all materials of the current Blend-file to the UI list"""
bl_idname = "custom.add_bmaterials"
bl_label = "Add all available Materials"
bl_description = "Add all available materials to the UI list"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(bpy.data.materials)
def execute(self, context):
scn = context.scene
for mat in bpy.data.materials:
if not context.scene.custom.get(mat.name):
item = scn.custom.add()
item.id = len(scn.custom)
item.material = mat
item.name = item.material.name
scn.custom_index = (len(scn.custom)-1)
info = '%s added to list' % (item.name)
self.report({'INFO'}, info)
return{'FINISHED'}
class CUSTOM_OT_printItems(Operator):
"""Print all items and their properties to the console"""
bl_idname = "custom.print_items"
bl_label = "Print Items to Console"
bl_description = "Print all items and their properties to the console"
bl_options = {'REGISTER', 'UNDO'}
reverse_order: BoolProperty(
default=False,
name="Reverse Order")
@classmethod
def poll(cls, context):
return bool(context.scene.custom)
def execute(self, context):
scn = context.scene
if self.reverse_order:
for i in range(scn.custom_index, -1, -1):
mat = scn.custom[i].material
print ("Material:", mat,"-",mat.name, mat.diffuse_color)
else:
for item in scn.custom:
mat = item.material
print ("Material:", mat,"-",mat.name, mat.diffuse_color)
return{'FINISHED'}
class CUSTOM_OT_clearList(Operator):
"""Clear all items of the list and remove from scene"""
bl_idname = "custom.clear_list"
bl_label = "Clear List and Remove Materials"
bl_description = "Clear all items of the list and remove from scene"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
return bool(context.scene.custom)
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def execute(self, context):
if bool(context.scene.custom):
# Remove materials from the scene
for i in context.scene.custom:
if i.material:
mat_obj = bpy.data.materials.get(i.material.name, None)
if mat_obj:
info = 'Item %s removed from scene' % (i.material.name)
bpy.data.materials.remove(mat_obj, do_unlink=True)
# Clear the list
context.scene.custom.clear()
self.report({'INFO'}, "All materials removed from scene")
else:
self.report({'INFO'}, "Nothing to remove")
return{'FINISHED'}
# -------------------------------------------------------------------
# Drawing
# -------------------------------------------------------------------
class CUSTOM_UL_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
mat = item.material
if self.layout_type in {'DEFAULT', 'COMPACT'}:
split = layout.split(factor=0.3)
split.label(text="Index: %d" % (index))
# static method UILayout.icon returns the integer value of the icon ID
# "computed" for the given RNA object.
split.prop(mat, "name", text="", emboss=False, icon_value=layout.icon(mat))
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon_value=layout.icon(mat))
def invoke(self, context, event):
pass
class CUSTOM_PT_materialList(Panel):
"""Adds a custom panel to the TEXT_EDITOR"""
bl_idname = 'TEXT_PT_my_panel'
bl_space_type = "TEXT_EDITOR"
bl_region_type = "UI"
bl_category = "Dev"
bl_label = "Custom Material List Demo"
def draw(self, context):
layout = self.layout
scn = bpy.context.scene
rows = 2
row = layout.row()
row.template_list("CUSTOM_UL_items", "custom_def_list", scn, "custom",
scn, "custom_index", rows=rows)
col = row.column(align=True)
col.operator(CUSTOM_OT_actions.bl_idname, icon='ADD', text="").action = 'ADD'
col.operator(CUSTOM_OT_actions.bl_idname, icon='REMOVE', text="").action = 'REMOVE'
col.separator()
col.operator(CUSTOM_OT_actions.bl_idname, icon='TRIA_UP', text="").action = 'UP'
col.operator(CUSTOM_OT_actions.bl_idname, icon='TRIA_DOWN', text="").action = 'DOWN'
row = layout.row()
row.template_list("CUSTOM_UL_items", "custom_grid_list", scn, "custom",
scn, "custom_index", rows=2, type='GRID')
row = layout.row()
row.operator(CUSTOM_OT_addBlendMaterials.bl_idname, icon="NODE_MATERIAL")
row = layout.row()
col = row.column(align=True)
row = col.row(align=True)
row.operator(CUSTOM_OT_printItems.bl_idname, icon="LINENUMBERS_ON")
row = col.row(align=True)
row.operator(CUSTOM_OT_clearList.bl_idname, icon="X")
# -------------------------------------------------------------------
# Collection
# -------------------------------------------------------------------
class CUSTOM_PG_materialCollection(PropertyGroup):
#name: StringProperty() -> Instantiated by default
material: PointerProperty(
name="Material",
type=bpy.types.Material)
# -------------------------------------------------------------------
# Register & Unregister
# -------------------------------------------------------------------
classes = (
CUSTOM_OT_actions,
CUSTOM_OT_addBlendMaterials,
CUSTOM_OT_printItems,
CUSTOM_OT_clearList,
CUSTOM_UL_items,
CUSTOM_PT_materialList,
CUSTOM_PG_materialCollection
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
# Custom scene properties
bpy.types.Scene.custom = CollectionProperty(type=CUSTOM_PG_materialCollection)
bpy.types.Scene.custom_index = IntProperty()
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.custom
del bpy.types.Scene.custom_index
if __name__ == "__main__":
register()
@DB3D
Copy link

DB3D commented Nov 13, 2019

hello @p2or
is it possible to have multiples selected slots at the same time with this GUI ?

@p2or
Copy link
Author

p2or commented Nov 13, 2019

Hi @DB3D,

I guess that's not really a python thing you can control at the moment. Have a look at the Shape Keys list to see what's possible to implement. There is a Bool Property to set the actual state for each item and you can select multiple shape key values at once to edit them simultaneously, which should possible for a custom list as well (by default).

Cheers,
Christian

@ikakupa
Copy link

ikakupa commented Mar 22, 2021

Hello @p2or,
how can I update list according to selection (to view only the materials of selected object)? Without any "add or "remove" operators.

Thanks!

@p2or
Copy link
Author

p2or commented Mar 23, 2021

Hi @ikakupa,

that's actually pretty easy to do since this is the behavior of the regular material list. Right-click the list (Material Properties), choose 'Edit Source' and just copy-paste the draw method of the CYCLES_PT_context_material class to your custom panel. Demo based on 'UI Panel Simple' template that comes with Blender:

import bpy
         

class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

    def draw(self, context):
        layout = self.layout

        mat = context.material
        ob = context.object
        slot = context.material_slot
        space = context.space_data

        if ob:
            is_sortable = len(ob.material_slots) > 1
            rows = 1
            if (is_sortable):
                rows = 4

            row = layout.row()

            row.template_list("MATERIAL_UL_matslots", "", ob, "material_slots", ob, "active_material_index", rows=rows)

            col = row.column(align=True)
            col.operator("object.material_slot_add", icon='ADD', text="")
            col.operator("object.material_slot_remove", icon='REMOVE', text="")

            col.menu("MATERIAL_MT_context_menu", icon='DOWNARROW_HLT', text="")

            if is_sortable:
                col.separator()

                col.operator("object.material_slot_move", icon='TRIA_UP', text="").direction = 'UP'
                col.operator("object.material_slot_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
            '''
            if ob.mode == 'EDIT':
                row = layout.row(align=True)
                row.operator("object.material_slot_assign", text="Assign")
                row.operator("object.material_slot_select", text="Select")
                row.operator("object.material_slot_deselect", text="Deselect")
            '''
        split = layout.split(factor=0.65)
        
        if ob:
            split.template_ID(ob, "active_material", new="material.new")
            row = split.row()

            if slot:
                row.prop(slot, "link", text="")
            else:
                row.label()
        elif mat:
            split.template_ID(space, "pin_id")
            split.separator()



def register():
    bpy.utils.register_class(HelloWorldPanel)


def unregister():
    bpy.utils.unregister_class(HelloWorldPanel)


if __name__ == "__main__":
    register()

Cheers,
Christian

@ikakupa
Copy link

ikakupa commented Mar 23, 2021

Thank you for your answer. Sorry, I misspelled object/objectS in my last comment. I want to view materials (and edit their name) of multiple selected objects simultaneously. I tried to change 'UI Panel Simple' template to achieve this, sadly my py skills are not good enough.

Objects are not selected:
image

Objects are selected:
https://pasteboard.co/JTVzC2Vs.jpg
image

Thanks,
Irakli

@p2or
Copy link
Author

p2or commented Mar 23, 2021

Hi @ikakupa,

aha, need to investigate, not sure. I don't think there is an easy way to get that due to the nature of the UI list. I suggest ask a question on https://blender.stackexchange.com/ or https://devtalk.blender.org/ if you need a solution rather quick.

Cheers,
Christian

@ikakupa
Copy link

ikakupa commented Mar 23, 2021

Thank you Christian, I am going to give it a try and post it on Devtalk.

@squindt
Copy link

squindt commented Jan 26, 2022

Hi @ikakupa,

is it possible to show only "non-greasepencil-materials" in the list?
I tried it with a simple "if material.is_grease_pencil: return" in the draw_item-function, but then I get empty slots in the list...
image

Thank you very much,
Sergej

@p2or
Copy link
Author

p2or commented Jan 26, 2022

Hi @squindt,

If you don't want to show grease pencil materials I wouldn't even add them to the PropertyGroup:

class CUSTOM_OT_addBlendMaterials(Operator):
    """Add all materials of the current Blend-file to the UI list"""
    bl_idname = "custom.add_bmaterials"
    bl_label = "Add all available Materials"
    bl_description = "Add all available materials to the UI list"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return len(bpy.data.materials)
    
    def execute(self, context):
        scn = context.scene
        materials = [m for m in bpy.data.materials if not m.is_grease_pencil]
        for mat in materials:
            if not context.scene.custom.get(mat.name):
                item = scn.custom.add()
                ...

Cheers,
Christian

@squindt
Copy link

squindt commented Jan 26, 2022

Sorry @p2or,

I forgot to mention, that I am trying to populate the list without an operator but automatically by referencing "bpy.data" as the "dataptr" and "materials" as propname arguments in the template_list-function. That way every new material which is created is automatically in the custom list, but unfortunately also the grease pencil materials. Is there a possibility to prevent this?

Thank you very much for the fast response! :)
Sergej

@p2or
Copy link
Author

p2or commented Jan 26, 2022

Hi @squindt,

see e.g. How to populate UIList with all material slot in scene? @batFINGER's code displays all materials used the scene (even using pointers), simple and clean IMHO.

Cheers, Christian

@squindt
Copy link

squindt commented Jan 27, 2022

Hi @p2or,

thank you very much! That is exactly what I need!
I saw this example of @batFINGER before, but didn't consider it as a solution for my problem because in his demo-GIF he pushed a button to repopulate the list after he created new materials. Now I tried it myself and the materials got added automatically, but I don't understand why :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment