3
$\begingroup$

I am fairly experienced with both Python and Blender, but using them together is still new to me, and so I am not entirely clear on how everything referneces each other.

To avoid confusion, I have a script that is executed when clicked from the "space" menu:

At the moment, I have three variables hardcoded in, that I would like to be changable by the user. This means that rather than executing the addon's function when "Slow" is clicked, I instead want a menu to be brought up, with sliders I can change the variables with, much like "Bake Action":

I then want it to execute when I press "OK".

However, I don't know where to put the actual code. At the moment it is just in the file that adds "Slow" to the menu, but if I add another menu, should I put the code in the second one, and the first just triggers the second?

Or should it be seperate from both, but then how would the sliders change the values?

Currently the values are coded like this:

        bpy.types.Scene.firstSlowFrame= bpy.props.IntProperty(
            name = "First Slow Frame:", # as you want it to appear on the slider
            default = 40, # optional
            description = "This is the first frame that will be in slow motion"
            )

        bpy.types.Scene.lastSlowFrame= bpy.props.IntProperty(
            name = "Last Slow Frame:", # as you want it to appear on the slider
            default = 60, # optional
            description = "This is the last frame that will be in slow motion"
            )

        bpy.types.Scene.scaleFactor= bpy.props.IntProperty(
            name = "Slow Factor:", # as you want it to appear on the slider
            default = 1, # optional
            description = "This is how much it will be slowed by (1 is nothing, 2 is half as fast"
            )

Since I read this is the way to put them in sliders, however I tried keeping my code in the first and making a panel that used them in sliders and it didn't work.

Overall, I am looking for someone to explain the proper way of using multiple menus, and where various bits of code should go for what I described to happen.

Thank you

$\endgroup$

2 Answers 2

2
$\begingroup$

Okay, so it took me a little while (and a lot of searching online), but it was this link (https://wiki.blender.org/index.php/Dev:Py/Scripts/Cookbook/Code_snippets/Interface#A_popup_dialog) that showed me what I needed.

What I have done is definitely a hack, and there are more elegant solutions, but if you are just looking for a way that works, then this will help a lot. (Warning: Lots of code)

So it works by having three classes, which call each other in turn.

The "Clicked On" Operator

The first is an operator that simply exists to invoke the dialog box when clicked in the "search menu".

class slowClick(bpy.types.Operator):
bl_idname = "slowclick.move_operator"
bl_label = "Slow"

def execute(self, context):
    # Default can be used since I only have a single dialog
    bpy.ops.object.dialog_operator('INVOKE_DEFAULT')

    return {'FINISHED'}

The Dialog Box

The second is the dialog box itself. This is invoked in the line bpy.ops.object.dialog_operator('INVOKE_DEFAULT'), and then displayed.

The actual dialog box is built somewhat counter-intuitively. Rather than putting in sliders using the standard layout.row().prop, all you actually do is define the variables, and on creation they can be changed. In my program that looked like this:

# create the sliders (and checkbox) in the dialog box so the user can change where the slo-mo starts and ends
firstSlowFrameP = bpy.props.IntProperty(
    name="First Slow Frame:",  # as you want it to appear on the slider
    default=40,  # optional
    description="This is the first frame that will be in slow motion"
)

globalSelect = bpy.props.BoolProperty(
    name="Global",  # affect all objects in scene
    default=False,
    description="If true, all available objects will be affected"
)

As with normal panels, how you define the property defines what kind of item it will be (e.g. slider for 'Int' and checkbox for 'Bool'), and at the bottom it has an "OK" button.

def execute(self, context):
    # first thing is to globalise the variables needed, and yes I know this isn't the best way
    global firstSlowFrame, globalSelect

    # reassign the variables based on the user's input on the sliders
    firstSlowFrame = self.firstSlowFrameP
    globalSelect = self.globalSelect

    # this actually calls the operator that does the editing
    bpy.ops.slow.move_operator()
    return {'FINISHED'}

def invoke(self, context, event):
    # call itself and run
    wm = context.window_manager
    return wm.invoke_props_dialog(self)

So the def execute() details what happens when the "OK" button is pressed. It globalises all the variables that were changed in the slider, so they can be accessed by the main and final function.

(The def invoke() just describes its creation when invoked)

The Main Function

Finally we get to the actual function. I won't put the main code here (If you want the addon go to How can I slow down or animate time?), but this is just your standard operator:

class Slow(bpy.types.Operator):
    bl_idname = "slow.move_operator"
    bl_label = "OK"

    # get the global variables since this is in a class
    global firstSlowFrame, globalSelect

    #PROGRAM

All you have to do is get the variables that were changed by the sliders, and it all works nicely.

(Technically, if you press "space" and type "OK" you will get this and it will probably not work since the variables don't exist yet, so at the very beginning of the program I would define a "default" value for each that will be used if the user accidentally bypasses the whole dialog box.)

Example

This is a template that can be used, feel free to use it, modify it, I doubt I invented this hack so I mean credit me if you want but I'm really not fussed, just hope this can help someone.

import bpy
import math
from bpy.props import *

scn = bpy.context.scene

# initialising the variables that will be in the dialog
intVariable = 10
strVariable = "Hello"
boolVariable = True


# This is the operator that is clicked by the user, triggering the dialog box
class ToBeClicked(bpy.types.Operator):
    bl_idname = "toBeClicked.move_operator"
    bl_label = "WHATEVER YOU WANT YOUR USER TO TYPE IN"

    def execute(self, context):
        # Default can be used since I only have a single dialog
        bpy.ops.object.dialog_operator('INVOKE_DEFAULT')

        return {'FINISHED'}


class DialogOperator(bpy.types.Operator):
    bl_idname = "object.dialog_operator"
    bl_label = "Dialog Operator"

    # create the sliders (and checkbox) in the dialog box 
    intVariableProperty = bpy.props.IntProperty(
        name="Integer Variable:",  # as you want it to appear on the slider
        default=10,  # optional
        description="This is an integer"  # what you get when you hover over
    )

    strVariableProperty = bpy.props.StringProperty(
        name="String Variable:",  # as you want it to appear on the bar
        default="Hello",  # optional
        description="This is a string"  # what you get when you hover over
    )

    boolVariableProperty = bpy.props.BoolProperty(
        name="Boolean Variable:",  # as you want it to appear on the checkbox
        default=True,
        description="This is a boolean checkbox"  # what you get when you hover over
    )

    # This is what happens when "OK is pressed
    def execute(self, context):
        # first thing is to globalise the variables needed, and yes I know this isn't the best way
        global intVariable, strVariable, boolVariable

        # this is just for troubleshooting
        print("Dialog Runs")
        print(self.intVariableProperty, self.strVariableProperty, self.boolVariableProperty)

        # reassign the variables based on the user's input on the sliders
        intVariable = self.intVariableProperty
        strVariable = self.strVariableProperty
        boolVariable = self.boolVariableProperty

        # this actually calls the operator that does the editing
        bpy.ops.slow.move_operator()
        return {'FINISHED'}

    # This is just saying that when it is invoked, how to display it
    def invoke(self, context, event):
        # call itself and run
        wm = context.window_manager
        return wm.invoke_props_dialog(self)


# This is the actual program that does something with the variables
class Main(bpy.types.Operator):
    bl_idname = "main.move_operator"
    bl_label = "OK"

    # get the global variables since this is in a class
    global intVariable, strVariable, boolVariable

    # This just makes sure that everything can be applied
    # if (CONDITION):
    # PROGRAM


    return {'FINISHED'}

    # This can give an error if you want certain conditions met
    # else:
    # self.report({'ERROR'}, 'There is no animation')
    # return {'CANCELLED'}


# registers everything
def register():
    bpy.utils.register_class(Main)
    bpy.utils.register_class(ToBeClicked)
    bpy.utils.register_class(DialogOperator)


if __name__ == "__main__":
    register()

I know this is really very long, in answer to a very unimportant question, but it took me a while to solve this and I'm pretty pleased with it, so I hope other people can find it just as useful.

Feel free to add corrections in the comments!

$\endgroup$
2
$\begingroup$

It's been almost 2 years but I don't know if you realized what you were doing is roundabout and wrong. Since you didn't post any update, I assume either you didn't, or you didn't care to post them.

execute() function is called when ran through python ex: bpy.ops.op.path()
invoke() function is called when the operator is clicked and essentially everywhere outside of python, except explicitly declared; ex: bpy.ops.op.path('INVOKE_DEFAULT')
Otherwise, python assumes bpy.ops.op.path('EXEC_DEFAULT')

if no invoke is declared, operator defaults to execute.
If no execute is declared, operator defaults to invoke.

import bpy


class OPERATOR_OT_name(bpy.types.Operator):
    bl_description = "Displayed on hover"
    bl_idname = 'ops.path'
    bl_label = "Found in Search"
    bl_options = {'REGISTER', 'UNDO'}
        # enum set in {'REGISTER', 'UNDO', 'UNDO_GROUPED', 'BLOCKING', 'MACRO', 'GRAB_CURSOR', 'PRESET', 'INTERNAL'}
    bl_undo_group = ""  # Unused without 'UNDO_GROUPED'.
        # This is used when you want to have a different name displayed in Undo from the Label

    @classmethod
    def poll(self, context):
        return True

    def invoke(self, context, event):
        print("I have been called with an event, now I will draw myself")
        wm = context.window_manager  # there are some other functions here
        return wm.invoke_props_dialog(self)
        # This return means to exit and bring a popup with an OK button
        # To call execute function instead, you use: return self.execute(context)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, 'string', text="")
        layout.prop(self, 'bool')
        layout.prop(self, 'inemene')

    def execute(self, context):
        print("I will actually run by blazing the execs, then return to nothing")
        print("String", self.string)
        print("bool", self.bool)
        print("Ee nee Me nee My nee mo", self.inemene)
        return {'FINISHED'}

    string = bpy.props.StringProperty(name="Ambience", default="{Insert text here}")
    bool = bpy.props.BoolProperty(name="Always Truther", options={'SKIP_SAVE'})
    inemene = bpy.props.FloatProperty(description="No display name given, so displayed text is lowercased")

    # The order of the 4 def functions, the 3 variables, and the 5 bl_ vars don't matter, I just put them that way.
    # However, for the desired process, this is essentially the order that Blender/Python will internally read them  
        # (except the vars, those go up with the bl_ vars)
    # Also, the poll isn't needed (because it doesn't check anything), 
        # and some of the bl_ vars aren't needed, I just chose to include them both.


def register():
    bpy.utils.register_class(OPERATOR_OT_name)


def unregister():
    bpy.utils.unregister_class(OPERATOR_OT_name)

# This section is used for when you want to debug the script inside Blender's text editor, using Live Edit 
if __name__ == "__main__":
    register()

When you need to read the event values (like key-presses, mouse location, etc), you use the invoke function and send it to vars in self, not global vars.
global vars are for when you need to store properties outside of operators and classes/function during the blender session.


Since you control the operators, you could also send properties to a context copy.
In the layout, it's done with layout.context_pointer_set('var_name', data_pointer) (data_pointer is strict on what type of things can be sent)
note: for UI buttons, not this wm.invoke function

In ops, it's used like

context_to_dict = context.copy()  # Creates a duplicate as dict()
context_to_dict['custom_var'] = "Whatever"
bpy.ops.op.path(context_to_dict, 'INVOKE_DEFAULT', self_var="A registered variable for self")

In the operator called, the custom variables will be found in context. To poll for whether or not they were declared, you just use hasattr(context, "var") or getattr(context, "var", "value to default to if not found")

$\endgroup$
1
  • $\begingroup$ When I use the layout.context_pointer_set() in the panel, and then the operator uses invoke_props_dialog() in an invoke() function, it then looses the context I have set in the UI in the execute function. How can I pass it on? I tried copying the context in the invoke function, but it didn't work. $\endgroup$
    – Rooster
    Commented Oct 15 at 17:13

You must log in to answer this question.

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