How to add modal modifiers?

blender_modal_modifier
(I’m currently running Shrink/Fatten in this screenshot screenshot, at the bottom: Confirm, Cancel, Snap Invert, Snap Toggle, Precision Mode)

I don’t know what these are called. Modifiers? Is it possible to add them to my own addon’s modal operators’ UI so my modifier keys appear at the bottom of the window? Any examples?

edit: for clarity

Thanks but I know how to add the key detection. I don’t know how to get it to appear in the UI at the bottom of the window when the modal is running.

There’s https://docs.blender.org/api/current/bpy.types.WorkSpace.html#bpy.types.WorkSpace.status_text_set_internal but not sure about an example.

Thanks, that is sounds like it might be what I’m looking for. I’ll look through other addons and see if I can find anything that uses it.

Got curious how would it work with the icons and turned out status_text_set is just a Python defined method that works with STATUSBAR_HT_header.draw.

class WorkSpace(_types.ID):
    __slots__ = ()

    def status_text_set(self, text):
        """
        Set the status text or None to clear,
        When text is a function, this will be called with the (header, context) arguments.
        """
        from bl_ui.space_statusbar import STATUSBAR_HT_header
        draw_fn = getattr(STATUSBAR_HT_header, "_draw_orig", None)
        if draw_fn is None:
            draw_fn = STATUSBAR_HT_header._draw_orig = STATUSBAR_HT_header.draw

        if not (text is None or isinstance(text, str)):
            draw_fn = text
            text = None

        self.status_text_set_internal(text)
        STATUSBAR_HT_header.draw = draw_fn

Basically, it allows you to set your own draw() method for the STATUSBAR_HT_header and create a more fancy status with icons:

bpy.context.workspace.status_text_set(lambda self, context: self.layout.label(text="hi", icon="KEY_TAB"))

image

I did find this code in the modules/bpy_types.py but what is C in this example?

C is the console shortcut for bpy.context. (it shows when you open a Python Console Editor, in last line)

PYTHON INTERACTIVE CONSOLE 3.11.9 (main, Oct 15 2024, 19:17:05) [MSC v.1929 64 bit (AMD64)]

Builtin Modules:       bpy, bpy.data, bpy.ops, bpy.props, bpy.types, bpy.context, bpy.utils, bgl, gpu, blf, mathutils
Convenience Imports:   from mathutils import *; from math import *
Convenience Variables: C = bpy.context, D = bpy.data

Thanks, I guess I’ve just always ignored that text in the Python console and typed bpy.context because I totally forgot it was even there.

Thanks @Andrej too!

I wonder if there’s a way to reset that bar, because after using it, it doesn’t go back when the modal exits, leaving edit mode or even using another modal that uses it internally. Setting context to None just makes the bar blank forever.

Ahh bpy.context.workspace.status_text_set(None) does it.

The text parameter is the one which is supposed to be None… (not the context)
(the example from Andrej isn’t probably the best to clarify the status_text_set function…)

You could also check the startup\bl_operators\wm.py to see how that function can be used.
(or the function itself, in modules\bpy_types.py)

>>> help(bpy.context.workspace.status_text_set)
Help on method status_text_set in module bpy_types:

status_text_set(text) method of bpy_types.WorkSpace instance
    Set the status text or None to clear,
    When text is a function, this will be called with the (header, context) arguments.

I guess the last piece to this puzzle would be to invert the icon when my modifier is active if you have any ideas.

You probably have some internal ‘state’ in your modal operator, right?

If that’s the case, you could use pass a function that generates the correct status_text (based on the internal state) to the status_text_set.

The issue is I can’t get it to look like Blender with the icon inverted next to the text:
blender_modifier_on

Width and Clamp are active in the Bevel operator here.

Closest I’ve been able to get with mine is using a prop but it looks ugly:
blender_modifier_mine

(icon_only looks the same just without the text)

Ohh… Now I understand your problem.

I don’t think the inverted parameter is exposed to the Python API…
The C function that uses it is ed_workspace_status_item (blender\editors\screen\area.cc), and it’s an attribute of bke::WorkSpaceStatusItem, which I believe is not included in the API.

I would need to dig a bit further, but probably some changes to the source are required.

Ahh! Well, maybe we’ll see that in the future then. I’m 99% there thanks to your clarifications. I’ll think I’ll just change the text for now.

There are two ways to provide icon to UI - icon and icon_value. Perhaps there is a way to offset icon_value of the button icon to “invert” it.

You can use the patch below and see the system console for actual icon values used for each element - so you’ll be able to see what value is used for normal and inverted icons, maybe it will give some clues.

from bl_ui.space_statusbar import STATUSBAR_HT_header

def clever_draw(self, context):
    self.layout.label(text="test")
    STATUSBAR_HT_header._draw_orig(self, context)
    print(self.layout.introspect())

I did look into icon_value but couldn’t figure out what it was used for. The docs for it are useless too.

I don’t think I have anywhere to plug in your introspection code because there aren’t any included Python operators that use status_text_set. I can’t run this in my operator because then it’s out of context when running a different operator so the introspection just errors.

edit: Actually, I figured out how to plug it in without reference to my class but the output doesn’t appear to change when Clamp is turned on or off in the Bevel operator. It doesn’t show icon information either.

Console output:

[{'type': 'LAYOUT_ROOT', 'items': [{'type': 10240, 'draw_string': 'test', 'tip': ''}, {'type': 'LAYOUT_ROW', 'items': [{'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Confirm', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Cancel', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Width Type', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Width', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Segments', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Profile', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Clamp', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Harden', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Seam', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Sharp', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Affect (Edges) ', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Outer (Sharp) ', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Inner (Sharp) ', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Profile Type (Superellipse) ', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': 'Intersection (Grid Fill) ', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}]}, {'type': 28672, 'draw_string': '', 'tip': ''}, {'type': 28672, 'draw_string': '', 'tip': ''}, {'type': 'LAYOUT_ROW', 'items': []}, {'type': 'LAYOUT_ROW', 'items': [{'type': 10240, 'draw_string': 'Cube | Verts:4/42 | Edges:4/63 | Faces:1/31 | Tris:64 | Objects:1/3 | Memory: 27.8 MiB | VRAM: 5.1/6.0 GiB', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '|', 'tip': ''}, {'type': 27648, 'draw_string': '', 'tip': ''}, {'type': 10240, 'draw_string': '4.3.2', 'tip': ''}]}]}]

Oh, sorry, for some reason I assumed icon_value was part of introspection output.

There is a way to get icon_value for known icon as described in here https://projects.blender.org/blender/blender/issues/97909#issue-14039 but not sure how to get it for the inverted icon to compare.

Well according to that link it should be -1, 0 or 1 but none of them invert the icon. In my case, I’m using EVENT_C which has no “next” icon according to the Icon Viewer addon. It is interesting that I don’t get like D or B though either.

After reading the responses to that bug report it seems like it’s kind of a rabbit hole and no one wants to make a decision on the “right” way to handle icons.

Leveraging(abusing?) the emboss and invert_checkbox parameters, I’ve got:

False:
my_mod_true

True:
my_mod_false

I don’t like the poor contrast but it is probably the best I can get until they expose the inverted parameter to Python like Secrop mentioned.