Is there any way to override UILayout.prop tooltip? I have a collection on boolean properties I want to display in a node. BoolVectorProperty isn't really applicable because I want to be able to change the size of the array dynamically.

My issue is that when displaying a compact grid, there's no way to tell the user the index of a button. That is, I would like to override the tooltip of a UILayout.prop widget. If I set the text-kwarg to something nonempty, icon_only seems to be ignored.

My current code:

class SequencerPosition(bpy.types.PropertyGroup):
    value: bpy.props.BoolProperty(name="Value")

class TriggerSequencer(Node):
    bl_idname = 'TriggerSequencerNode'
    bl_label = 'Trigger Sequencer'
    bl_width_min = 70.0

    sequence: bpy.props.CollectionProperty(type=SequencerPosition, name="Sequence")

    def init(self, context):
        for i in range(16):
            self.sequence.add()

    def draw_buttons(self, context, layout):
        grid = layout.grid_flow(row_major=True)
        for i, pg in enumerate(self.sequence):
            grid.prop(pg, 'value', icon_only=True)


results in this node:



The tooltips are rather unhelpful, just saying "Value."
Dynamically re-annotate a pointer property


I forgot to select one

Example below.  Note I've used Scene properties to illustrate the concept, as it's simpler to put together using simple panel template code and column flow to layout, than making custom nodes.  Pretty sure the same concepts apply.

The Scene.foo_int property unregisters re-annotates and re registers the propertygroup class.  The init call is emulated by calling the rereg update method.

import bpy
from bpy.props import BoolProperty, PointerProperty, IntProperty


class SequencerPosition(bpy.types.PropertyGroup):
    __annotations__ = {}
    pass


def rereg(self, context):
    if getattr(SequencerPosition, "is_registered", False):
        bpy.utils.unregister_class(SequencerPosition)
    SequencerPosition.__annotations__.clear()
    for i in range(self.foo_int):

        SequencerPosition.__annotations__[f"value{i}"] = BoolProperty(
            name=f"Value{i}",
            description=f"Do something with value {i}")

    bpy.utils.register_class(SequencerPosition)      
    bpy.types.Scene.foo = PointerProperty(type=SequencerPosition)  




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
        scene = context.scene
        layout.prop(scene, "foo_int")
        grid = layout.column_flow(columns=8)
        for anno in SequencerPosition.__annotations__.keys():
            grid.prop(scene.foo, anno, icon_only=True)

def register():
    bpy.utils.register_class(HelloWorldPanel)
    bpy.types.Scene.foo_int = IntProperty(min=0, max=32, update=rereg)

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


if __name__ == "__main__":
    rereg(bpy.context.scene, bpy.context)
    register()



  Oh, and this won't really work if I have multiple instances of the
  node, right?


Yes that's a catch on the dynamic registering, however can annotate the node with a maximum number, and only display so many based on a node int property

to demonstrate:



I've annotated the CustomNode with a max of 32 and used the int to set the layout

def draw_buttons(self, context, layout):
    layout.label(text="Node settings")
    layout.prop(self, "my_int_prop")
    grid = layout.grid_flow(row_major=True, columns=4)
    for i in range(self.my_int_prop):
        grid.prop(self, f'value{i}', icon_only=True)