How to execute a custom function when a button is hovered (mouse over)?

Hi, everyone. This is my first question here.
After a lot of struggle and research I came this far of making an addon that will show an animated gif of a brush in a tiny window like this.
ezgif.com-video-to-gif (1)

The thing is, I only want to show this preview when I hover a brush button in sculpting mode.
For example, this preview is showing the ‘Draw’ brush. But when I hover the ‘Elastic Deform’ brush I would show animation of another gif file. The problem is that I don’t know how to invoke the ‘hover’ event of those buttons. I tried looking for some keywords in the Blender API or even check on the source code of blender but I couldn’t find anything related to the brush buttons in sculpt mode (like the one that shows a tooltip for example). Could anyone point me out where I find this hover of mouse over function ?

2 Likes

The tooltip from mouse hovers comes from, a python call which goes to bl_ui.space_toolsystem_common.description_from_id

Each tool definition has a field called description. When this is a callable, it is automatically called when the mouse hovers over the tool.

This prints the name of the brushes in console on hover:

import bpy
from bl_ui.space_toolsystem_toolbar import VIEW3D_PT_tools_active, ToolDef, _defs_sculpt

# Main view3d tools dict
tools = VIEW3D_PT_tools_active._tools

sculpt_tools = tools['SCULPT']

# Custom tooltip must have 3 parameters and return a string
# Called when tool is hovered
def tooltip(context, tool, keymap):
    print(tool.label)
    return ""

# Get all tooldefs for builtin brushes
brushes = list(_defs_sculpt.generate_from_brushes(bpy.context))

# Change 'description' of sculpt tools into a callable
for idx, tool in enumerate(brushes):
    new_tool_dict = tool._asdict()
    new_tool_dict['description'] = tooltip
    new_tool = ToolDef(*new_tool_dict.values())
    brushes[idx] = new_tool


def brush_tooldefs_get(context):
    return tuple(brushes)

# sculpt_tools first item is a callable that generates a
# tuple of tooldefs. Replace it with new_brushes
sculpt_tools[0] = brush_tooldefs_get
2 Likes

@kaio Thank you for you response! I have a question about this code. In line 21 is really a tooltip variable or just tool?

    new_tool_dict['description'] = tooltip

I only found the function with the name tooltip. I this this variable is refered to the enumerated ‘tool’ variable, right?

Check out space_toolsystem_common.py in scripts\startup\bl_ui where you can find the ToolDef class.

A tooldef is a named tuple with fields which blender reads to generate the ui part of an active tool.

In the example above I’m converting the content of a tooldef into a dictionary so that I can change what the description field reads.
When blender reads the field, it first checks if the second part of description is a callable (a function), and if so proceed to call it.

This happens at line 983 in the same file

    # Custom description.
    description = item.description
    if description is not None:
        if callable(description):
            km = _keymap_from_item(context, item)
            return description(context, item, km)
        return tip_(description)

A named tuple field is just a string, and its counterpart can be either a string or a function. In the example I set it to the new function called tooltip.

3 Likes

@kaio Thank you very much for your help. After quite some time I was able to do what I was trying to. I would like to ask if you know any 'mouseOut ’ event in blender similar to the tooltip?

What I did so far is not yet finished and I’m using a timer to hide the animated widget. I think it would be preferable to be on a mouse_out event of the brush button. Do you (or anyone who might read this) know anything similar to that?

This is how my addon is so far:

ezgif.com-video-to-gif (2)

Thank you very much!

1 Like

There isn’t a mouse event for changing region, but you can block an animation by using a dummy operator which is set to only run in 3d view’s WINDOW region, and in its poll method set a flag to stop the animation from playing.

The keymap for the operator below is set to 3D View and uses mouse movements to poll. You can put a condition in your timed animation and have it stop if the condition becomes True.

The operator will always fail poll, so there shouldn’t be any overhead.

import bpy


# A timed function that runs until blocked
def animation():
    animation.block = getattr(animation, "block", False)

    if not animation.block:
        print("playing")
        return 0.1


class VIEW3D_OT_poll_region(bpy.types.Operator):
    bl_idname = "view3d.poll_region"
    bl_label = "Poll Region"
    bl_options = {'INTERNAL'}

    @classmethod
    def poll(cls, context):
        
        if not animation.block:
            print("Start blocking animation")
            animation.block = True
        return False

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


if __name__ == "__main__":
    bpy.utils.register_class(VIEW3D_OT_poll_region)

    # Reset animation block each time timer runs
    animation.block = False
    bpy.app.timers.register(animation)

    kc = bpy.context.window_manager.keyconfigs.addon
    km = kc.keymaps.get("3D View")

    if not km:
        km = kc.keymaps.new("3D View", space_type="VIEW_3D")

    kmi = km.keymap_items.get("view3d.poll_region")
    if not kmi:
        kmi = km.keymap_items.new("view3d.poll_region", 'MOUSEMOVE', 'ANY', head=1)
2 Likes

@kaio thank you so much for your help. I also struggled a lot after your last response but I was able to finish the addon, here is how it looks now:

tutorial

I wouldn’t have done without yout help. Thanks again.
I also published it into my github page. I believe this is not very useful because it is focused to new users and I think they would not know how to install it. But I would like to leave this idea in case the Blender developers want to implement this as a native tool for new users.

Github Project

2 Likes

No problem. That’s pretty impressive!

1 Like