5
$\begingroup$

I'm using Blender 3.0 and I would like to dynamically change an operator's tooltip based on conditions that happen inside the operator's poll() function but I can't get it to work. I have tried to follow the example from the 2.81 Release Notes.

Why I want to do this: my understanding of the poll function is that is is meant to be used to disable the operator's button if certain conditions aren't met. If there are multiple potential conditions that could fail, I would like a way to inform the user what specifically isn't met so that they can correct the problem. The sample code below shows only some of the sanity checks I would like to perform. Yes, I could move this into the execute() and then skip execution if the conditions aren't met, but then what's the point of the poll() function?

class MYCOMPANY_OT_ExportSingleObject(bpy.types.Operator):
    bl_label = "Export"
    bl_idname = "mycompany.exportsingleobject"
    myTooltip: bpy.props.StringProperty()

    @classmethod
    def description(cls, context, properties):
        ## myTooltip is always empty here
        return "Tooltip is [ " + properties.myTooltip + " ]"

    @classmethod
    def poll(cls, context):
        cls.myTooltip = "Tooltip: Set from poll()"

        ## This prints the correct ID and the updated tooltip
        print("[ " + cls.bl_idname + " ] has tooltip [ " + cls.myTooltip +" ]")

        ## As an aside, cls.report() doesn't seem to do anything.  I left it in in case someone also knows if/how to make it work in poll()
        if context.active_object is None:
            cls.report( {"WARNING"}, "No object selected.")
            return False

        if context.active_object.type != "MESH":
            cls.report( {"WARNING"}, "Can only export objects of type MESH. [ "+context.active_object.name+" ] is of type [ "+context.active_object.type+" ]")
            return False

        is_enabled, is_loaded = addon_utils.check("io_scene_fbx")
        if not (is_enabled and is_loaded):
            cls.report(
                {"WARNING"},
                "Add-on for FBX format is not enabled." +
                "Go to 'Edit → Preferences → Add-ons' and enable 'Import-Export: FBX format'")
            return False

        return True

    def execute(self, context):
        ## Omitted
$\endgroup$
2
  • 2
    $\begingroup$ Instead of that, put cls.poll_message_set("reason") calls before you return False in your poll function. Example $\endgroup$
    – scurest
    Commented Mar 18, 2022 at 22:52
  • $\begingroup$ Excellent! This is exactly what I was looking for. As a bit of a tangent I'm a little annoyed that the Blender documentation isn't sorted alphabetically, or grouped more intelligently. I admit to missing this on a first read. :( docs.blender.org/api/current/… EDIT: I guess I'm also support to ask you to post this as an "answer" so that I can mark is a complete. Thank you again. $\endgroup$
    – Spurkey
    Commented Mar 21, 2022 at 19:20

1 Answer 1

2
$\begingroup$

If you wish to use the approach from 2.81:

For various obscure reasons having to do with the C magic behind Blender's Property Definitions you can't directly do what you want. You have to follow the guideline from the release notes.

If you're willing to introduce a class with file scope and an instance of that class, you can accomplish your goal, however. This requires cooperation between the operator class and the panel class, but here's a proof of concept you can adapt:

import bpy
from bpy.types import Operator
from bpy.types import Panel
from bpy.props import StringProperty

class _shared_message:
    def __init__(self, text):
        self.text = text
        
_message = _shared_message("Hi")

class TLA_OT_operator(Operator):
    bl_idname = "demo.operator"
    bl_label = "I'm a silly Operator"
    bl_options = {"REGISTER", "UNDO"}

    message : StringProperty(default = "unset")

    @classmethod
    def description(cls, context, properties):
        return properties.message

    @classmethod
    def poll(cls, context):
        _message.text = "HUH"
        return context.mode == "OBJECT"

    def execute(self, context):
        self.report({'INFO'},  f"execute()")
        return {'FINISHED'}


class TLA_PT_sidebar(Panel):
    """Display test button"""
    bl_label = "DEMO"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "TLA"

    def draw(self, context):
        col = self.layout.column(align=True)
        prop = col.operator(TLA_OT_operator.bl_idname)
        prop.message = _message.text
 
classes = [
    TLA_OT_operator,
    TLA_PT_sidebar,
]

def register():
    for c in classes:
        bpy.utils.register_class(c)


def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)


if __name__ == '__main__':
    register()

A global text string, encased in a class, is shared between the two classes. It's modified in the operator poll function and is used to set the argument value for the description function in the panel's draw function, as described in the release notes.

Better Way from comments:

The credit for the below goes to scurest in a comment, and they should get the 'accept' for the question. I'm recording the code that works using their technique as an alternative to my answer. It really is the better way from the above paragraph.

class TLA_OT_operator(Operator):
    """ Tooltip for success case """
    bl_idname = "demo.operator"
    bl_label = "I'm a silly Operator"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        if context.mode != "OBJECT":
            cls.poll_message_set("HUH")
        return context.mode == "OBJECT"

    def execute(self, context):
        self.report({'INFO'}, f"execute()")
        return {'FINISHED'}

class TLA_PT_sidebar(Panel):
    """Display test button"""
    bl_label = "DEMO"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "TLA"

    def draw(self, context):
        col = self.layout.column(align=True)
        prop = col.operator(TLA_OT_operator.bl_idname)

You can remove the myToolTip string and the description class method but you do need to give your operator class a default tooltip by giving it a documentation string.

$\endgroup$
2
  • $\begingroup$ I tried this out and unfortunately it doesn't quite work as expected. I tried this in Blender 3.0.1 and it appears that for whatever reason Blender calls the Panel's draw() before it calls the Operator's poll() meaning that the very first time the user mouses-over the button, the tooltip is incorrect (using your example it says "Hi"). The user has to mouse-over the button a second time to get the correct tooltip which is not intuitive. I'm sure there's a way to force a call to draw(), but I think the "correct" way to go about this is the poll_message_set() function proposed by @scurest $\endgroup$
    – Spurkey
    Commented Mar 21, 2022 at 19:08
  • 1
    $\begingroup$ Sure, poll_message_function will work and you should get scurest to write an answer and accept it, but note that poll_message_function only generates the tooltip for the failure case. You still need a tooltip for the success case. $\endgroup$ Commented Mar 21, 2022 at 19:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .