Bpy.ops.outliner does not work after selecting through script

I am trying to use an outliner operation after selecting an object via bpy. For this I used a context override.

Why does this script not delete the Cube the first time it is run? It only works when the cube is selected before the script is run.

Does the outliner need to be refreshed after selecting the cube?

I need to use a bpy.ops.outliner operation. The delete op is just a placeholder for testing purposes. My specific usecase is adding a library override via bpy.ops.outliner.id_operation. The alternative bpy.ops.object.make_override_library() seems to be buggy in 2.9

import bpy

bpy.data.objects["Cube"].select_set(True)

for window in bpy.context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'OUTLINER':
            override = bpy.context.copy()
            override["area"] = area
            bpy.ops.outliner.delete(override)

Thanks!

Hi!

tl;dr The outliner can’t see the new selection until after the script has run.

Outliner operators work on proxy data called tree elements, which is separate from bmain objects. Tree synchronization happens only before the outliner is getting ready to redraw, which will only be at the end of the script. In other words, when you select an object and call bpy.ops.outliner.delete(), the outliner doesn’t know the new selection yet.

One workaround is to use a timer to call the delete operator outside of the main script execution. The timer fires instantly, but still after the script has been executed and the internal outliner tree has been synchronized. The downside is the outliner selection may blip for a split second.

import bpy

ob = bpy.data.objects["Cube"]
ob.select_set(True)

def outliner_delete_delayed(override):
    if ob in set(bpy.data.objects):
        bpy.ops.outliner.delete(override, 'INVOKE_DEFAULT')
        return 0.0

for window in bpy.context.window_manager.windows:
    screen = window.screen

    for area in screen.areas:
        if area.type == 'OUTLINER':
            override = bpy.context.copy()
            override["area"] = area
            bpy.app.timers.register(lambda: outliner_delete_delayed(override))
            break
2 Likes

@iceythe Thank you for your answer! Really saved my time and I was able to get ahead with my tool. Here is it in action!

What’s the use for this though, why not just use bpy.ops.object.delete() or even simpler bpy.data.objects.remove(obj)?

PS Found a way to do it synchronously

import bpy

bpy.data.objects["Cube"].select_set(True)

def get_outliner_area():
    screen = bpy.context.screen
    for area in screen.areas:
        if area.type == "OUTLINER":
            return area

area = get_outliner_area()
area.tag_redraw()
bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)

with bpy.context.temp_override(area=area):
    bpy.ops.outliner.delete()

@Andrej It turns out the outliner delete (global delete) and the 3D viewport default delete (SHIFT X vs X) have different implications, which are not immediately obvious. If you’re using library overrides the two operations will have different outcomes.

See https://projects.blender.org/blender/blender/issues/90228#issuecomment-201222

Quote from Philipp Oeser :

The regular Delete {key X} in the 3dView will not delete from each and every usage.
There is also Delete Global {key Shift X} which will do exactly that. And this is also what happens from the Outliner.
This has caused a bit of confusion before, but is not considered a bug (since it is working as intended).
In the context of overrides, they can only stay intact if the linked object is still “on board” as well, if this is deleted globally, everything is made local [ since you cant have an override of… nothing :slight_smile: ]

1 Like