Setting “layout.alert” (or alternatively display a message) in the 3D UI based on a bool property

I’m asking your help to solve a problem I had when trying to design a custom UI.

I would like an operator to have a red background (layout.alert) or display a message, all based on the status of a boolean I’m setting (or resetting) in update functions inside properties.

Part of the code is the following:

from bpy.props import (StringProperty,
                       BoolProperty,
                       IntProperty,
                       FloatProperty,
                       FloatVectorProperty,
                       EnumProperty,
                       PointerProperty,
                       )
from bpy.types import (Panel,
                       Menu,
                       Operator,
                       PropertyGroup,
                       )
from bpy.utils import register_class, unregister_class


# ------------------------------------------------------------------------
#    Scene Properties
# ------------------------------------------------------------------------


class IgnitProperties(bpy.types.PropertyGroup):
    bool_ND1:  bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND1')
    )
    bool_ND2: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND2')
    )
    bool_ND4: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND4')
    )
    bool_ND5: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND5')
    )
    bool_ND6: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND6')
    )
    bool_ND7: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_ND7')
    )
    bool_Need_update: bpy.props.BoolProperty(
        update=lambda self, context: common_update(self, context, 'bool_Need_update')
    )


def common_update(self, context, origin):
    if origin == 'bool_ND1':
        bpy.data.objects["ND1"].hide_viewport = not getattr(self, origin)
    if origin == 'bool_ND2':
        bpy.data.objects["ND2"].hide_viewport = not getattr(self, origin)
    if origin == 'bool_ND4':
        bpy.data.objects["ND4"].hide_viewport = not getattr(self, origin)
    if origin == 'bool_ND5':
        bpy.data.objects["ND5"].hide_viewport = not getattr(self, origin)
    if origin == 'bool_ND6':
        bpy.data.objects["ND6"].hide_viewport = not getattr(self, origin)
    if origin == 'bool_ND7':
        bpy.data.objects["ND7"].hide_viewport = not getattr(self, origin)
    if origin == 'Mis_1':
        bpy.data.scenes["Scene"]["Misura_1"] = getattr(self, origin)
        bool_Need_update = True
    if origin == 'Mis_2':
        bpy.data.scenes["Scene"]["Misura_2"] = getattr(self, origin)
        bool_Need_update = True
    if origin == 'Mis_3':
        bpy.data.scenes["Scene"]["Misura_3"] = getattr(self, origin)
        bool_Need_update = True
    if origin == 'Mis_4':
        bpy.data.scenes["Scene"]["Misura_4"] = getattr(self, origin)
        bool_Need_update = True
    if origin == 'Mis_E':
        bpy.data.scenes["Scene"]["Misura_E"] = getattr(self, origin)
        bool_Need_update = True
    if origin == 'Mis_P':
        bpy.data.scenes["Scene"]["Misura_P"] = getattr(self, origin)
    if origin == 'Mis_Rot':
        bpy.data.scenes["Scene"]["Centro_Rotazione"] = getattr(self, origin)
        bool_Need_update = True

class MyProperties(PropertyGroup):

    Mis_1: IntProperty(
        name = "Misura 1",
        description = "Misura 1",
        default = 20,
        min = 0,
        max = 100,
        update=lambda self, context: common_update (self, context, 'Mis_1')
        )

    Mis_2: IntProperty(
        name = "Misura 2",
        description="Misura 2",
        default = 20,
        min = 0,
        max = 100,
        update=lambda self, context: common_update (self, context, 'Mis_2')
        )

    Mis_3: IntProperty(
        name = "Misura 3",
        description="Misura 3",
        default = 20,
        min = 0,
        max = 100,
        update=lambda self, context: common_update (self, context, 'Mis_3')
        )

    Mis_4: IntProperty(
        name = "Misura 4",
        description="Misura 4",
        default = 20,
        min = 0,
        max = 100,
        update=lambda self, context: common_update (self, context, 'Mis_4')
        )

    Mis_E: IntProperty(
        name = "Misura E",
        description="Misura E",
        default = 0,
        min = 0,
        max = 100,
        update=lambda self, context: common_update (self, context, 'Mis_E')
        )

    Mis_P: IntProperty(
        name = "Misura P",
        description="Misura P",
        default = 110,
        min = 100,
        max = 400,
        update=lambda self, context: common_update (self, context, 'Mis_P')
        )

    Mis_Rot: IntProperty(
        name = "Misura_Rot",
        description="Misura Rot",
        default = 0,
        min = 0,
        max = 60,
        update=lambda self, context: common_update (self, context, 'Mis_Rot')
        )

class TEST_OT_test_op(Operator):
    bl_idname = 'test.test_op'
    bl_label = 'Test'
    bl_description = 'Test'
    bl_options = {'REGISTER', 'UNDO'}
 
    action: EnumProperty(
        items=[
            ('Aggiorna', 'clear scene', 'clear scene'),
            ('Apri', 'add cube', 'add cube'),
            ('Chiudi', 'add sphere', 'add sphere')
        ]
    )
 
    def execute(self, context):
        if self.action == 'Aggiorna':
            self.aggiorna(context=context)
            self.aggiorna_driver(context=context)
        elif self.action == 'Apri':
            self.apri(context=context)
            self.aggiorna_driver(context=context)
        elif self.action == 'Chiudi':
            self.chiudi(context=context)
            self.aggiorna_driver(context=context)
        return {'FINISHED'}
 
    @staticmethod
    def aggiorna_driver(context):
        bpy.data.objects["Attacco ND1"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND1"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND1"].animation_data.drivers[0].driver.expression [:-1]
        bpy.data.objects["Attacco ND2"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND2"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND2"].animation_data.drivers[0].driver.expression [:-1]
        bpy.data.objects["Attacco ND4"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND4"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND4"].animation_data.drivers[0].driver.expression [:-1]
        bpy.data.objects["Attacco ND5"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND5"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND5"].animation_data.drivers[0].driver.expression [:-1]
        bpy.data.objects["Attacco ND6"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND6"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND6"].animation_data.drivers[0].driver.expression [:-1]
        bpy.data.objects["Attacco ND7"].animation_data.drivers[0].driver.expression += " "
        bpy.data.objects["Attacco ND7"].animation_data.drivers[0].driver.expression = 
        bpy.data.objects["Attacco ND7"].animation_data.drivers[0].driver.expression [:-1]
        bool_Need_update = False

    @staticmethod
    def aggiorna(context):
        bpy.context.scene.frame_current += 1
        bpy.context.scene.frame_current -= 1

    @staticmethod
    def apri(context):
        bpy.context.scene.frame_current = 1

    @staticmethod
    def chiudi(context):
        bpy.context.scene.frame_current = 180
        
class CALDIM_PT_panel(bpy.types.Panel):
    bl_label = "CALDIM"
    bl_category = "CAlDIM"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    
    def draw(self, context):
        layout = self.layout
        scene = context.scene
        mytool = scene.my_tool
        mypanel = scene.ignit_panel
        layout.use_property_split = True
        layout.label(text="Inserire le misure in mm")
        row = layout.row()
        flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True)
        col = flow.column()
        col.prop(scene.my_tool, "Mis_1")
        col.prop(scene.my_tool, "Mis_2")
        col.prop(scene.my_tool, "Mis_3")
        col.prop(scene.my_tool, "Mis_4")
        col.prop(scene.my_tool, "Mis_E")
        col.prop(scene.my_tool, "Mis_P")
        col.prop(scene.my_tool, "Mis_Rot")
        layout.prop(scene.ignit_panel, "bool_ND1", text='Visibilità ND 1')
        layout.prop(scene.ignit_panel, "bool_ND2", text='Visibilità ND 2')
        layout.prop(scene.ignit_panel, "bool_ND4", text='Visibilità ND 4')
        layout.prop(scene.ignit_panel, "bool_ND5", text='Visibilità ND 5')
        layout.prop(scene.ignit_panel, "bool_ND6", text='Visibilità ND 6')
        layout.prop(scene.ignit_panel, "bool_ND7", text='Visibilità ND 7')
        layout.label()
        if mypanel.bool_Need_update:
            layout.alert = True
        layout.operator('test.test_op', text='Aggiorna').action = 'Aggiorna'
        layout.label()
        layout.alert = False
        layout.operator('test.test_op', text='Apri').action = 'Apri'
        layout.operator('test.test_op', text='Chiudi').action = 'Chiudi'

classes = (CALDIM_PT_panel,MyProperties, IgnitProperties, TEST_OT_test_op)

def register():
    for cls in classes:
        register_class(cls)
    bpy.types.Scene.my_tool = PointerProperty(type=MyProperties)
    bpy.types.Scene.ignit_panel = bpy.props.PointerProperty(type=IgnitProperties)

def unregister():
    for cls in classes:
        unregister_class(cls)
    del bpy.types.Scene.ignit_panel
    del bpy.types.Scene.my_tool
    
if __name__ == "__main__":
    register()

Basically, I set the bool_Need_update whenever one of the “Misura” values (1 to 4, E and Rot) are updated, and reset it into the aggiorna_driver function, after all the scripted drivers have been updated.

Don’t know if it’s the right way to do it, but in the draw function I added the line if mypanel.bool_Need_update: layout.alert = True but it seems to have no effect. On the other hand, if I add the instruction “layout.alert = True” it works correctly. I found this answer on stackexchange which seems to use the same approach, but has the drawback of using a checkbox to drive the appearance (in fact the modification) of a message, which is not what I want (the “Please update” warning should appear only when the bool_Need_update boolean is set, not based on the checkbox status).

Any hint? I can upload the blender file somewhere should it be needed.

Thanks to anyone who will be willing to help.

Though I can’t figure out all of the details of your implementation. In essence to implement the ‘dirty pattern’ in blender you can do it like this:

import bpy

def update_function(self, context):
    print("updating")
    context.scene.my_tool.need_update = True
    return

class MySettings(bpy.types.PropertyGroup):
    need_update : bpy.props.BoolProperty()
    something : bpy.props.StringProperty(update=update_function)

class TEST_OT_test_op(bpy.types.Operator):
    bl_idname = 'test.test_op'
    bl_label = 'Test'
    bl_description = 'Test'
    def execute(self, context):
        context.scene.my_tool.something = "" # now it invokes update again
        context.scene.my_tool.need_update = False # but now update status is reset
        return {'FINISHED'}

class CALDIM_PT_panel(bpy.types.Panel):
    bl_label = "CALDIM"
    bl_category = "CAlDIM"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    
    def draw(self, context):
        layout = self.layout
        layout.label(text='Need Update: '+str(context.scene.my_tool.need_update))
        layout.row()

        layout.label(text='Change This')
        layout.prop(context.scene.my_tool, 'something', text='')
        
        if context.scene.my_tool.need_update:
            layout.operator('test.test_op', text='Update')

def register():
    bpy.utils.register_class(CALDIM_PT_panel)
    bpy.utils.register_class(TEST_OT_test_op)
    
    
    # register the class
    bpy.utils.register_class(MySettings)
    # instantiate the class inside another type (1 scene : 1 property)
    bpy.types.Scene.my_tool = bpy.props.PointerProperty(type = MySettings)

def unregister():
    bpy.utils.unregister_class(CALDIM_PT_panel)
    bpy.utils.unregister_class(TEST_OT_test_op)
    
    
    # unregister the class
    bpy.utils.unregister_class(MySettings)
    # throw away the instance
    del bpy.types.Scene.my_tool

if __name__ == "__main__":
    try:unregister()
    except:pass
    register()