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.
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 ?
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
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.
@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?
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)
@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:
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.
Hey Kaio, if I want to run a custom function that stores the tooltip contents (description, shortcut, python command) into a global variable when any tooltip appears (not just for predefined items), how would I do that please?
Alternatively If I could just store the ID of the hovered item that would also be great. Maybe it’s possible to intercept the ID somehow from the python function that calls the tooltip?