Page MenuHome

Redrawing an UIList on Property Dialog crashes Blender 2.8x
Open, Confirmed, MediumPublic

Description

System Information
Operating system: Windows 7
Graphics card: GTX 970

Blender Version
Broken: 2.80, 6bab905c9d40, blender2.8, 2019-02-24
Worked: 2.7x

Short description of error
Redrawing an UIList on a Property Dialog crashes Blender 2.8x

Exact steps for others to reproduce the error

Following simple example creates a dialog that allows to display previously selected images from disk via UIList using a PropertyGroup to store them.

  1. Run the script to test call Image List operator
  2. Press Add Files to List
  3. Select an arbitrary Image

The code runs without any issues in 2.7x. However in 2.8, after selecting an image from disk (coming back from the File Browser) Blender crashes because of supplying row.template_list(...) within the draw method. When removing that line the dialog comes back as expected, but without the list of course :). I'm not sure what the issue is. I even don't get any valuable error when running blender in debug mode, just crashes instantly.

For debugging I also added the same UIList to the Toolbar of the Text Editor, which works as expected.

2.8x (Broken)

import bpy
from bpy.props import IntProperty, CollectionProperty, StringProperty 
from bpy.types import Operator, Panel, UIList
from bpy_extras.io_utils import ImportHelper
import os
  
# -------------------------------------------------------------
#   Operators
# -------------------------------------------------------------

class ImageSelectOperator(Operator, ImportHelper):
    """Select Images within the File Browser"""
    bl_idname = "custom.image_select"
    bl_label = "Select Image(s)"
    bl_options = {'PRESET'}

    filter_glob: StringProperty(
            default="*.exr;*.tiff;*.jpeg;*.jpg;*.pic;*.png",
            options={'HIDDEN'},
            )

    files: CollectionProperty(type=bpy.types.PropertyGroup)

    def execute(self, context):
        scn = context.scene
        folder = (os.path.dirname(self.filepath))

        # Iterate through the selected files
        for i in self.files:
            path_to_file = (os.path.join(folder, i.name))   
            item = scn.custom.add()
            item.id = len(scn.custom)
            item.name = i.name
            item.path = path_to_file
        
        self.report({'INFO'}, "Files added to the List")
        
        # Call the file browser
        bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')
        
        return {'FINISHED'}
    
    
class UIListActions(Operator):
    bl_idname = "custom.list_action"
    bl_label = "List Action"

    action: bpy.props.EnumProperty(
        items=(
            ('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 == 'REMOVE':
                info = 'Item %s removed from list' % (scn.custom[scn.custom_index].name)
                scn.custom_index -= 1
                if scn.custom_index < 0: scn.custom_index = 0
                self.report({'INFO'}, info)
                scn.custom.remove(idx)
                
        if self.action == 'ADD':
            bpy.ops.custom.image_select('INVOKE_DEFAULT')            
            scn.custom_index = (len(scn.custom))

        return {"FINISHED"}

# -------------------------------------------------------------
#   Drawing
# -------------------------------------------------------------

class ULItems(UIList):

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        split = layout.split(factor=0.7)
        split.prop(item, "name", text="", emboss=False, translate=False, icon='IMAGE')
        split.label(text="Index: %d" % (index))

    def invoke(self, context, event):
        pass   


class PopupDialog(Operator):
    """Displays the Popup Dialog"""
    bl_idname = "custom.popup_dialog"
    bl_label = "Image List"
    bl_options = {'REGISTER'}

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=400)

    def check(self, context):
        return True
    
    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene
        
        try:
            current_item = (scn.custom[scn.custom_index].name)
        except IndexError:
            current_item = ""
        
        row = layout.row()
        
        """
        Crashes Blender 2.8x
        """
        row.template_list("ULItems", "", scn, "custom", scn, "custom_index", rows=2)
                
        row = layout.row()
        col = layout.column(align=True)
        rowsub = col.row(align=True)
        rowsub.operator("custom.list_action", icon='ZOOM_IN', text="Add Files to List").action = 'ADD'
        rowsub.operator("custom.list_action", icon='ZOOM_OUT', text="Remove File").action = 'REMOVE'
        
        row = layout.row(align=True)
        row.label(text="Active Item is: {}".format(current_item))
        

# DEBUGGING
class UIListPanelExample(Panel):
    """Creates a Panel in the Text Editor"""
    bl_idname = 'OBJECT_PT_my_panel'
    bl_space_type = "TEXT_EDITOR"
    bl_region_type = "UI"
    bl_label = "Custom Image Files"
    
    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene
        
        try:
            current_item = (scn.custom[scn.custom_index].name)
        except IndexError:
            current_item = ""
        
        row = layout.row()
        row.template_list("ULItems", "", scn, "custom", scn, "custom_index", rows=2)
                
        row = layout.row()
        col = layout.column(align=True)
        rowsub = col.row(align=True)
        rowsub.operator("custom.list_action", icon='ZOOM_IN', text="Add Files to List").action = 'ADD'
        rowsub.operator("custom.list_action", icon='ZOOM_OUT', text="Remove File").action = 'REMOVE'
        
        row = layout.row(align=True)
        row.label(text="Active Item is: {}".format(current_item))


class CustomProps(bpy.types.PropertyGroup):
    #name: StringProperty() -> Initiated by default
    id: IntProperty()
    path: StringProperty()
    
# -------------------------------------------------------------------
#   Register & Unregister
# -------------------------------------------------------------------

classes = (
    CustomProps,
    UIListActions,
    ImageSelectOperator,
    ULItems,
    UIListPanelExample, # DEBUGGING
    PopupDialog
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.custom = CollectionProperty(type=CustomProps)
    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()

    bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')

2.7x (Working)

import bpy
from bpy.props import IntProperty, CollectionProperty, StringProperty 
from bpy.types import Operator, Panel, UIList
from bpy_extras.io_utils import ImportHelper
import os
  
# -------------------------------------------------------------
#   Operators
# -------------------------------------------------------------

class ImageSelectOperator(Operator, ImportHelper):
    """Select Images within the File Browser"""
    bl_idname = "custom.image_select"
    bl_label = "Select Image(s)"
    bl_options = {'PRESET'}

    filter_glob = StringProperty(
            default="*.exr;*.tiff;*.jpeg;*.jpg;*.pic;*.png",
            options={'HIDDEN'},
            )

    files = CollectionProperty(type=bpy.types.PropertyGroup)

    def execute(self, context):
        scn = context.scene
        folder = (os.path.dirname(self.filepath))

        # Iterate through the selected files
        for i in self.files:
            path_to_file = (os.path.join(folder, i.name))   
            item = scn.custom.add()
            item.id = len(scn.custom)
            item.name = i.name
            item.path = path_to_file
        
        self.report({'INFO'}, "Files added to the List")
        
        # Call the file browser
        bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')
        
        return {'FINISHED'}
    
    
class UIListActions(Operator):
    bl_idname = "custom.list_action"
    bl_label = "List Action"

    action = bpy.props.EnumProperty(
        items=(
            ('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 == 'REMOVE':
                info = 'Item %s removed from list' % (scn.custom[scn.custom_index].name)
                scn.custom_index -= 1
                if scn.custom_index < 0: scn.custom_index = 0
                self.report({'INFO'}, info)
                scn.custom.remove(idx)
                
        if self.action == 'ADD':
            bpy.ops.custom.image_select('INVOKE_DEFAULT')            
            scn.custom_index = (len(scn.custom))

        return {"FINISHED"}

# -------------------------------------------------------------
#   Drawing
# -------------------------------------------------------------

class ULItems(UIList):

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        split = layout.split(percentage=0.7)
        split.prop(item, "name", text="", emboss=False, translate=False, icon='IMAGE_COL')
        split.label(text="Index: %d" % (index))

    def invoke(self, context, event):
        pass   


class PopupDialog(Operator):
    """Displays the Popup Dialog"""
    bl_idname = "custom.popup_dialog"
    bl_label = "Image List"
    bl_options = {'REGISTER'}

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=400)

    def check(self, context):
        return True

    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene
        
        try:
            current_item = (scn.custom[scn.custom_index].name)
        except IndexError:
            current_item = ""
        
        row = layout.row()
        row.template_list("ULItems", "", scn, "custom", scn, "custom_index", rows=2)
                
        row = layout.row()
        col = layout.column(align=True)
        rowsub = col.row(align=True)
        rowsub.operator("custom.list_action", icon='ZOOM_IN', text="Add Files to List").action = 'ADD'
        rowsub.operator("custom.list_action", icon='ZOOM_OUT', text="Remove File").action = 'REMOVE'
        
        row = layout.row(align=True)
        row.label(text="Active Item is: {}".format(current_item))
        

# DEBUGGING
class UIListPanelExample(Panel):
    """Creates a Panel in the Text Editor"""
    bl_idname = 'OBJECT_PT_my_panel'
    bl_space_type = "TEXT_EDITOR"
    bl_region_type = "UI"
    bl_label = "Custom Image Files"
    
    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene
        
        try:
            current_item = (scn.custom[scn.custom_index].name)
        except IndexError:
            current_item = ""
        
        row = layout.row()
        row.template_list("ULItems", "", scn, "custom", scn, "custom_index", rows=2)
                
        row = layout.row()
        col = layout.column(align=True)
        rowsub = col.row(align=True)
        rowsub.operator("custom.list_action", icon='ZOOM_IN', text="Add Files to List").action = 'ADD'
        rowsub.operator("custom.list_action", icon='ZOOM_OUT', text="Remove File").action = 'REMOVE'
        
        row = layout.row(align=True)
        row.label(text="Active Item is: {}".format(current_item))


class CustomProps(bpy.types.PropertyGroup):
    #name: StringProperty() -> Initiated by default
    id = IntProperty()
    path = StringProperty()
    
# -------------------------------------------------------------------
#   Register & Unregister
# -------------------------------------------------------------------

classes = (
    CustomProps,
    UIListActions,
    ImageSelectOperator,
    ULItems,
    UIListPanelExample, # DEBUGGING
    PopupDialog
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.custom = CollectionProperty(type=CustomProps)
    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()

    bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')

Cheers,
Christian

Details

Type
Bug

Event Timeline

Jacques Lucke (JacquesLucke) triaged this task as Confirmed, Medium priority.

I can reproduce it. Can you try to remove everything from that script that is not necessary to reproduce the bug? That would make debugging a bit easier.

Hey @Jacques Lucke (JacquesLucke),

of course. Just wasn't sure what's easier to debug. I tried to strip it down as best as I could:

import bpy
from bpy.props import CollectionProperty, IntProperty
from bpy.types import Operator, Panel, UIList
from bpy_extras.io_utils import ImportHelper
import os
  
# -------------------------------------------------------------
#   Operators
# -------------------------------------------------------------

class FileSelectOperator(Operator, ImportHelper):
    bl_idname = "custom.file_select"
    bl_label = "Select File"
    bl_options = {'PRESET'}
        
    def execute(self, context):
        scn = context.scene
        fpath, fname = os.path.split(self.filepath)
        item = scn.custom.add()
        item.id = len(scn.custom)
        item.name = fname
        self.report({'INFO'}, "{} added to the List".format(fname))
        bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')
        return {'FINISHED'}
    
# -------------------------------------------------------------
#   UI List
# -------------------------------------------------------------

class UIListActionAdd(Operator):
    bl_idname = "custom.list_action"
    bl_label = "List Action"

    def invoke(self, context, event):
        scn = context.scene
        idx = scn.custom_index
        bpy.ops.custom.image_select('INVOKE_DEFAULT')
        scn.custom_index = (len(scn.custom))
        return {"FINISHED"}

class ULItems(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        layout.row().prop(item, "name", text="", emboss=False, translate=False, icon='IMAGE')
           
class PopupDialog(Operator):
    bl_idname = "custom.popup_dialog"
    bl_label = "Image List"
    bl_options = {'REGISTER'}

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=400)

    def check(self, context):
        return True
    
    def draw(self, context):
        scn = context.scene
        self.layout.row().template_list("ULItems", "", scn, "custom", scn, "custom_index", rows=2) 
        self.layout.row().operator(FileSelectOperator.bl_idname, icon='ZOOM_IN', text="Add Files to List")

class CustomProps(bpy.types.PropertyGroup):
    #name: StringProperty() -> instantiated by default
    id: IntProperty()

# -------------------------------------------------------------------
#   Register & Unregister
# -------------------------------------------------------------------

classes = (
    FileSelectOperator,
    UIListActionAdd,
    ULItems,
    PopupDialog,
    CustomProps
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.custom = CollectionProperty(type=CustomProps)
    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()
    # test call
    bpy.ops.custom.popup_dialog('INVOKE_DEFAULT')

I Hope that's better. Just let me know if I can do anything else.

Cheers,
Christian

CTX_wm_region(C) returns NULL in uiTemplateList(..) directly after the file selector is closed.
When I skip the function in this case, everything goes back to normal on the next redraw.

@Campbell Barton (campbellbarton), I'm not sure at which level this has to be fixed...