How do I detect if the active tool changes? I need to update a property when the active tool changes. This seems to work ,but you have to change mode for it to be detected.
import bpy
from bpy.app.handlers import persistent
@persistent
def toolUpdated(scene):
tools = bpy.context.workspace.tools
active_tool = tools.from_space_view3d_mode(bpy.context.mode).idname
depsgraph = bpy.context.evaluated_depsgraph_get()
for update in depsgraph.updates:
if update.id.original != active_tool:
print(active_tool)
def register():
bpy.app.handlers.depsgraph_update_post.append(toolUpdated)
def unregister():
bpy.app.handlers.depsgraph_update_post.remove(toolUpdated)
if __name__ == "__main__":
register()
not 100% sure without doing some tests, but I donât believe changing the active tool will cause the depsgraph to update. if thatâs the case, then your handler isnât running until something forces the depsgraph update to happen. have you tried using a timer and caching the active tool in a local variable instead?
I mean, the same thing you have but with a timer instead of depsgraph_update (which by the way, I just verified does not get triggered when you change tools- looks like a timer is the way to go here):
import bpy
from bpy.app.handlers import persistent
last_tool = None
@persistent
def tool_updated():
global last_tool
tools = bpy.data.workspaces['Scripting'].tools # i did this in the scripting workspace and just forced it to use the correct one rather than relying on context to be correct. you can do whatever here.
active_tool = tools.from_space_view3d_mode(bpy.context.mode).idname
if active_tool != last_tool:
print(f"Tool was changed from {last_tool} to {active_tool}")
last_tool = active_tool
# check again once every second.
return 0.5
bpy.app.timers.register(tool_updated)
Output:
Tool was changed from None to builtin.move
Tool was changed from builtin.move to builtin.select
Tool was changed from builtin.select to builtin.move
Edit:
This is related ,but unrelated. Iâm trying to create a poll that only gets textures from my previews. It eventually fails and displays textures from multiple brushes even though the preview is correct. The only way I know to updated it, is clear the previews when changes are made to active tool or brush. Is there a better way to do this?
def poll_image_items(self, object):
preview_textures = []
# Get preview items
preview_items = preview_collections_textures["main"]
for item in preview_items:
preview_textures.append(item)
# Check for texture image type and see if texture uses the same image as previews
return object.type == 'IMAGE' and object.image.name in preview_textures
it looks like preview_collections_textures["main"] continues to add textures unless cleared each time, instead of just the previews for current brush.
you canât get a reference to your workspace this way. if you had bpy.context.workspace you wouldnât need to get it from bpy.data.workspaces, itâs the same object. Once you have a reference to the workspace object, you can get its tools attribute. If you already have an object reference you donât need to dig through bpy.data to get it (which you dontâ, hence the NoneType error). The context the timer is run from has no reference to the current workspace, so youâll need to get it another way.
If youâre interested in seeing exactly what you have available to you via a Timerâs context, all you have to do is output a copy of bpy.context from inside a timer:
import bpy
from pprint import pprint as pp
def context_check():
pp(bpy.context.copy())
return None
bpy.app.timers.register(context_check)
Since itâs for a addon, I donât see how I could define which workspace. Not being able to get current workspace is becoming a pain, LOL. I came up with this and it seems to work for the modes ,but it fails as soon as you change in another workspace.
active_sculpt_tool = None
last_sculpt_tool = None
active_vertex_tool = None
last_vertex_tool = None
active_texture_tool = None
last_texture_tool = None
tool_changed = False
@persistent
def modeToolUpdated():
global active_sculpt_tool
global last_sculpt_tool
global active_vertex_tool
global last_vertex_tool
global active_texture_tool
global last_texture_tool
global tool_changed
for w in bpy.data.workspaces:
sculpt_tool = w.tools.from_space_view3d_mode(mode='SCULPT')
if sculpt_tool:
active_sculpt_tool = sculpt_tool.idname
if active_sculpt_tool is not None:
if active_sculpt_tool != last_sculpt_tool:
print(f"(Tool: {last_sculpt_tool}) was changed in (Workspace: {w.name}) to {active_sculpt_tool}")
last_sculpt_tool = active_sculpt_tool
tool_changed = True
vertex_tool = w.tools.from_space_view3d_mode(mode='PAINT_VERTEX')
if vertex_tool:
active_vertex_tool = vertex_tool.idname
if active_vertex_tool is not None:
if active_vertex_tool != last_vertex_tool:
print(f"(Tool: {last_vertex_tool}) was changed in (Workspace: {w.name}) to {active_vertex_tool}")
last_vertex_tool = active_vertex_tool
tool_changed = True
texture_tool = w.tools.from_space_view3d_mode(mode='PAINT_TEXTURE')
if texture_tool:
active_texture_tool = texture_tool.idname
if active_texture_tool is not None:
if active_texture_tool != last_texture_tool:
print(f"(Tool: {last_texture_tool}) was changed in (Workspace: {w.name}) to {active_texture_tool}")
last_texture_tool = active_texture_tool
tool_changed = True
if tool_changed:
print("Tool got changed, update previews")
'''for pcoll in preview_collections_textures.values():
bpy.utils.previews.remove(pcoll)
pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = ""
pcoll.my_previews = ()
preview_collections_textures["main"] = pcoll'''
tool_changed = False
# check again once every second.
return 0.05
yeah I dunno man, youâre trying to do something Blender doesnât directly support. You have an imperfect workaround, might be the best youâre going to get. If you want to get really hacky you can start a modal operator on startup and just run it in the background the entire time Blender is running.
starting a modal operator on startup without user intervention is itself a hack, and requires a few hacks to make it work- just so weâre clear itâs hacks all the way down from here on out, but it does work.
import bpy
modal_op_running = False
class APP_OT_modal_startup(bpy.types.Operator):
bl_idname = "app.modal_startup"
bl_label = "This modal operator starts when blender starts."
bl_options = {'INTERNAL'} # hide it from the operator search so users don't accidentally run it.
def modal(self, context, event):
global modal_op_running
if not modal_op_running:
return {'FINISHED'}
# do your stuff here.
# FYI - modal gets fired a LOT. you should throttle your updates (check current time against last update time, skip this update if it's below some threshold, etc)
return {'PASS_THROUGH'}
def execute(self, context):
global modal_op_running
if modal_op_running:
print(f"Something tried to start the op but it was already running")
return {'FINISHED'}
modal_op_running = True
print("Started the global modal operator!")
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def get_view3d_override():
# HACK- we can't fire a modal operator from a timer because it needs a context to run in.
# the easiest way to do that is to just find the first view3d area we can and build an override out of it.
for window in bpy.context.window_manager.windows:
for area in [a for a in window.screen.areas if a.type == 'VIEW_3D']:
for region in [r for r in area.regions if r.type == 'WINDOW']:
return {
'window': window,
'screen': window.screen,
'area': area,
'region': region,
'scene': bpy.context.scene
}
return None
def start():
# HACK: we have to do this in a timer because we might not have a window manager from which to create a context override yet. Once Blender is fully loaded, the modal operator will start.
try:
print("trying to start the modal op...")
result = bpy.ops.app.modal_startup(get_view3d_override())
print("Modal operator startup was successful, shutting down the timer.")
return None
except Exception as e:
print(f"Could not start the modal operator! {str(e)}")
print("Trying again in 2 seconds.")
return 2.0
def register():
bpy.utils.register_class(APP_OT_modal_startup)
if not bpy.app.timers.is_registered(start):
bpy.app.timers.register(start)
print("Starting the modal op using a timer callback.")
def unregister():
if bpy.app.timers.is_registered(start):
bpy.app.timers.unregister(start)
bpy.utils.unregister_class(APP_OT_modal_startup)
Also, it will stop running if you open a new scene, so youâll need a post_load app handler to re-register the startup timer. If you do something like this you need to be aware that youâre well off the reservation and weird shit can and will happen. As an example, if you try to reload scripts it will fail because you canât reload scripts while a modal operator is running.
This seem like a better solution. Itâs kind of a mystery code, cause I couldnât figure out where you got that code info ,but then started looking at the space_toolsystem_common.py file.
Just realized there might be a cleaner way of getting callbacks for active tools using the msgbus. Since workspace tools arenât rna properties themselves, figured itâs possible to monitor the bpy_prop_collection which changes with the tool.
The handle is the workspace itself, so shouldnât have to worry about keeping a reference.
The subscription lasts until a new file is loaded, so add a load_post callback which reapplies it.
Note this doesnât proactively subscribe to workspaces added afterwards.
Might need a separate callback for that
import bpy
def rna_callback(workspace):
idname = workspace.tools[-1].idname
print(idname)
def subscribe(workspace):
bpy.msgbus.subscribe_rna(
key=ws.path_resolve("tools", False),
owner=workspace,
args=(workspace,),
notify=rna_callback)
if __name__ == "__main__":
ws = bpy.context.workspace
subscribe(bpy.context.workspace)
# Subscribe to all workspaces:
if 0:
for ws in bpy.data.workspaces:
subscribe(bpy.context.workspace)
# Clear all workspace subscriptions
if 0:
for ws in bpy.data.workspaces:
bpy.msgbus.clear_by_owner(ws)
Would it be possible to do something similar for tools / actions / operators that have been invoked by menus like the rightclick menu or radial menus ?