27

How can I make a Interface which is similar to the Material-Interface within Blender via Python?

enter image description here

It should look like these. Could you guide me to the right direction? I don't think it's in bpy.ops, right?

Thank you very much!

p2or
  • 15,860
  • 10
  • 83
  • 143
Hamburml
  • 957
  • 1
  • 11
  • 25

1 Answers1

47

The example given is called UIList. The following code is based on the Modifier UIList of this answer. Once registered, scene objects can be added, removed, printed or selected via UI Elements (added an index value for debugging purposes):

custom_uilist.py

# ##### 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": "object-uilist-dev", "description": "", "author": "p2or", "version": (0, 1), "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)

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 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':
            info = 'Item "%s" removed from list' % (scn.custom[idx].name)
            scn.custom_index -= 1
            scn.custom.remove(idx)
            self.report({'INFO'}, info)

    if self.action == 'ADD':
        if context.object:
            item = scn.custom.add()
            item.name = context.object.name
            item.obj_type = context.object.type
            item.obj_id = len(scn.custom)
            scn.custom_index = len(scn.custom)-1
            info = '"%s" added to list' % (item.name)
            self.report({'INFO'}, info)
        else:
            self.report({'INFO'}, "Nothing selected in the Viewport")
    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):        
            item = scn.custom[i]
            print ("Name:", item.name,"-",item.obj_type,item.obj_id)
    else:
        for item in scn.custom:
            print ("Name:", item.name,"-",item.obj_type,item.obj_id)
    return{'FINISHED'}


class CUSTOM_OT_clearList(Operator): """Clear all items of the list""" bl_idname = "custom.clear_list" bl_label = "Clear List" bl_description = "Clear all items of the list" 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):
        context.scene.custom.clear()
        self.report({'INFO'}, "All items removed")
    else:
        self.report({'INFO'}, "Nothing to remove")
    return{'FINISHED'}


class CUSTOM_OT_removeDuplicates(Operator): """Remove all duplicates""" bl_idname = "custom.remove_duplicates" bl_label = "Remove Duplicates" bl_description = "Remove all duplicates" bl_options = {'INTERNAL'}

def find_duplicates(self, context):
    """find all duplicates by name"""
    name_lookup = {}
    for c, i in enumerate(context.scene.custom):
        name_lookup.setdefault(i.name, []).append(c)
    duplicates = set()
    for name, indices in name_lookup.items():
        for i in indices[1:]:
            duplicates.add(i)
    return sorted(list(duplicates))

@classmethod
def poll(cls, context):
    return bool(context.scene.custom)

def execute(self, context):
    scn = context.scene
    removed_items = []
    # Reverse the list before removing the items
    for i in self.find_duplicates(context)[::-1]:
        scn.custom.remove(i)
        removed_items.append(i)
    if removed_items:
        scn.custom_index = len(scn.custom)-1
        info = ', '.join(map(str, removed_items))
        self.report({'INFO'}, "Removed indices: %s" % (info))
    else:
        self.report({'INFO'}, "No duplicates")
    return{'FINISHED'}

def invoke(self, context, event):
    return context.window_manager.invoke_confirm(self, event)


class CUSTOM_OT_selectItems(Operator): """Select Items in the Viewport""" bl_idname = "custom.select_items" bl_label = "Select Item(s) in Viewport" bl_description = "Select Items in the Viewport" bl_options = {'REGISTER', 'UNDO'}

select_all: BoolProperty(
    default=False,
    name="Select all Items of List",
    options={'SKIP_SAVE'})

@classmethod
def poll(cls, context):
    return bool(context.scene.custom)

def execute(self, context):
    scn = context.scene
    idx = scn.custom_index

    try:
        item = scn.custom[idx]
    except IndexError:
        self.report({'INFO'}, "Nothing selected in the list")
        return{'CANCELLED'}

    obj_error = False
    bpy.ops.object.select_all(action='DESELECT')
    if not self.select_all:
        obj = scn.objects.get(scn.custom[idx].name, None)
        if not obj: 
            obj_error = True
        else:
            obj.select_set(True)
            info = '"%s" selected in Viewport' % (obj.name)
    else:
        selected_items = []
        unique_objs = set([i.name for i in scn.custom])
        for i in unique_objs:
            obj = scn.objects.get(i, None)
            if obj:
                obj.select_set(True)
                selected_items.append(obj.name)

        if not selected_items: 
            obj_error = True
        else:
            missing_items = unique_objs.difference(selected_items)
            if not missing_items:
                info = '"%s" selected in Viewport' \
                    % (', '.join(map(str, selected_items)))
            else:
                info = 'Missing items: "%s"' \
                    % (', '.join(map(str, missing_items)))
    if obj_error: 
        info = "Nothing to select, object removed from scene"
    self.report({'INFO'}, info)    
    return{'FINISHED'}


-------------------------------------------------------------------

Drawing

-------------------------------------------------------------------

class CUSTOM_UL_items(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): split = layout.split(factor=0.3) split.label(text="Index: %d" % (index)) custom_icon = "OUTLINER_OB_%s" % item.obj_type #split.prop(item, "name", text="", emboss=False, translate=False, icon=custom_icon) split.label(text=item.name, icon=custom_icon) # avoids renaming the item by accident

def invoke(self, context, event):
    pass   

class CUSTOM_PT_objectList(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_label = "Custom Object List Demo"

def draw(self, context):
    layout = self.layout
    scn = bpy.context.scene

    rows = 2
    row = layout.row()
    row.template_list("CUSTOM_UL_items", "", scn, "custom", scn, "custom_index", rows=rows)

    col = row.column(align=True)
    col.operator("custom.list_action", icon='ZOOM_IN', text="").action = 'ADD'
    col.operator("custom.list_action", icon='ZOOM_OUT', text="").action = 'REMOVE'
    col.separator()
    col.operator("custom.list_action", icon='TRIA_UP', text="").action = 'UP'
    col.operator("custom.list_action", icon='TRIA_DOWN', text="").action = 'DOWN'

    row = layout.row()
    col = row.column(align=True)
    row = col.row(align=True)
    row.operator("custom.print_items", icon="LINENUMBERS_ON") #LINENUMBERS_OFF, ANIM
    row = col.row(align=True)
    row.operator("custom.select_items", icon="VIEW3D", text="Select Item")
    row.operator("custom.select_items", icon="GROUP", text="Select all Items").select_all = True
    row = col.row(align=True)
    row.operator("custom.clear_list", icon="X")
    row.operator("custom.remove_duplicates", icon="GHOST_ENABLED")


-------------------------------------------------------------------

Collection

-------------------------------------------------------------------

class CUSTOM_objectCollection(PropertyGroup): #name: StringProperty() -> Instantiated by default obj_type: StringProperty() obj_id: IntProperty()

-------------------------------------------------------------------

Register & Unregister

-------------------------------------------------------------------

classes = ( CUSTOM_OT_actions, CUSTOM_OT_printItems, CUSTOM_OT_clearList, CUSTOM_OT_removeDuplicates, CUSTOM_OT_selectItems, CUSTOM_UL_items, CUSTOM_PT_objectList, CUSTOM_objectCollection, )

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_objectCollection)
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()

Note: Another version of this using object pointers (see below) can be found here: https://gist.github.com/p2or/5acad9e29ddb071096f9f004ae6cace7


As of 2.79 we can have real references to objects per ID by using a PointerProperty, which basically allows to display the actual data, access and edit the attributes on the fly.

Following funky example is a simple demo on how to create a custom UIList of materials. You can create new materials, display and edit their properties (name, color etc.) directly within the list. There is also an operator to add all materials of the current blend-file to the list.

material_uilist.py

# ##### 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()

Same principle applies to all other types. Using the PointerProperty for e.g. objects in the scene, makes it pretty comfortable to edit, add and remove them safely without any additional checks.

Gists


For Blender 2.7x have a look into the revisions of this answer.

p2or
  • 15,860
  • 10
  • 83
  • 143
  • 3
    Wow, Thanks! This looks heavier than I thought. – Hamburml May 05 '15 at 17:46
  • @poor nice answer ! where I can find the types like UL_items looked in the doc but couldn't find it ? – Chebhou May 05 '15 at 17:53
  • I don't understand Chebhou's question. The UL_items are created, aren't they? class UL_items(UIList): - So the blender python api doesn't have docs about it? – Hamburml May 05 '15 at 18:22
  • @Michael template_list() uses a predefined UI templates one of them is UL_items which happen to be used for the material list ( AFIK ), so let's wait for poor – Chebhou May 05 '15 at 18:29
  • 5
    Great example. I had a similar question but was missing the check-box. Finally this turns to be surprisingly simple and depends on the type of the "sub"-properties of class CustomProp. The above example contains enough examples to continue from there. Thank you very much for this answer it helped a lot. – Monster Dec 23 '15 at 12:08
  • Been suggested that scene.custom.clear() is a more efficient way to clear all. – batFINGER Jul 19 '18 at 12:25
  • Thanks @batFINGER I'm going to update the answer. You are right, Collection.clear() wasn't available at this time. However, I think the manual way helps to understand the concept. Would be great having a 'demo' for both, let me know if something useful comes into your mind. – p2or Jul 19 '18 at 12:50
  • Another big change is having pointers to ID objects in a collection. Could be some "on remove" action a per item basis. (Cheers for edit on other q too) – batFINGER Jul 19 '18 at 12:59
  • Good idea @batFINGER. I try to update the answer in the next days. (You're welcome, Cheers!) – p2or Jul 19 '18 at 13:12
  • Something like that? @batFINGER Updated the original one and added 2 new examples on how using the pointerprop. Feel free to edit the post, if there is something I forgot. – p2or Jul 24 '18 at 18:52
  • Looks good. Finishing off an edit with gvim addon, where among other things writing files to tmp folder for internal texts (As you can see went a different way from traditional collection prop (behaves like one in py console)) If it was a CP tho: If I cleared the temp texts collection , would want to del text["_tmpfile"] from each textblock. (this is what gave idea for suggesting ID props) – batFINGER Jul 24 '18 at 20:05
  • Quite interesting how that works. Did you already tried the tempfile module? Are you going to release that? @batFINGER – p2or Jul 25 '18 at 18:23
  • An attempt to emulate blender collection behaviour (the pointer prop is bloody handy). Yep started with tempfile module. Changed to utilize the per session temp folder blender auto creates. Release? waiting on 2.8 ( and me working out how to script it. ), eg how the per workspace addon rego works, .. the toolbar, new version of bgl ,,, gawain. So many questions. Nothing near the 2.4x to 2.5 hullabaloo. – batFINGER Jul 26 '18 at 05:15
  • I tried the part where you add operators. I needed only ADD and REMOVE. But only ADD shows up!!! What am I doing wrong? – N. Wells Mar 13 '23 at 12:47