3
$\begingroup$

I have an operator that draw its own panel (via invoke_props_dialog) when invoked to let user set properties which are then applied to the selected object, but the settings are not remember from one call to another when I use the operator's own panel. It works though when using the Tool Shelf Redo feature (bottom of the left panel in the 3D view).

Here's a sample code example to illustrate this issue. It adds a Test Panel with a Test button to the Data properties.

# add a Test Panel with a Test button to the Data properties.
import bpy

class DATA_PT_test(bpy.types.Panel):
    bl_label = "Test Panel"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "data"

    def draw(self, context):
        self.layout.operator("object.test_invoke")

class OBJECT_OT_test_invoke(bpy.types.Operator):
    bl_idname = "object.test_invoke"
    bl_label = "Test"
    bl_options = {'REGISTER', 'UNDO'}

    width = bpy.props.IntProperty(name="Width", 
            default=10, min=0, max=100)
    height = bpy.props.IntProperty(name="Height", 
            default=20, min=0, max=100)

    def execute(self, context):
        self.report({'INFO'}, "w:{}, h:{}".format(self.width, self.height))
        return {'FINISHED'}

    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self)

def register():
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Detailed steps:

  • Use the operator button: it uses its default settings.
  • Change the settings, apply them, then re-use the button: the last-used settings are not remembered, it still uses the default one.
  • Use the operator button, modify the settings in the Tool Shelf, use the button again: the setting from the Tool Shelf are remembered. (It does the same when using the Redo Last F6 panel to modify the settings).

For info, I already know some hacky ways to make it work but that's not really the point. I'd like to know if something is wrong or missing in this code, if it sounds like a Blender API bug, or if it's simply not an expected feature.

I'm using last official Blender 2.69.0 (r60995). I have the feeling it worked with previous releases (only from 2.62 though) but didn't feel like reinstalling just to try...

$\endgroup$
3
  • $\begingroup$ I've just reinstalled 2.62 to be sure, and this version works as expected. There's also a "Reset" button that appears in the panel. This was really more convenient. Anybody know the reason of this regression? $\endgroup$
    – jigebren
    Jan 25, 2014 at 20:11
  • $\begingroup$ Add 'PRESET' to bl_options and use Restore Defaults in the preset dropdown that will now appear in the Redo panel. $\endgroup$
    – CodeManX
    Jan 25, 2014 at 20:31
  • $\begingroup$ Just found out that it's a standard operator, that can be added to the redo panel in draw() like this: layout.operator("wm.operator_defaults") $\endgroup$
    – CodeManX
    Jan 25, 2014 at 23:25

2 Answers 2

3
$\begingroup$

1) The dialog is supposed to start with the default settings. A new operator instance is created as you open it, and the default values are taken from the property definitions. The redo panel works differently, it revokes what the last operator did, but keeps the values in the UI and re-executes with new settings.

One could say, the redo panel relates to the last operator, whereas your dialog is part of a new operator.


2) Operators remember their settings to allow for redo / repeat functionality, see Operator Design.

The operator log is available to python:

bpy.context.window_manager.operators

You could read the value used for a translation like Operator.properties.value. But I wouldn't recommend to iterate the log in reverse direction until you find an instance of your operator, just to read the last used settings.

How about this generic solution instead, which stores the settings at python class level:

Use a static variable (dictionary) to store the operator settings on execute(), and load them on invoke() into self.properties. These two methods can be defined together with the static dict as separate class, and be mixed-in into the operator class:

import bpy

class OperatorSettings:

    # Static member
    _settings = {}

    def save_settings(self): 
        for d in dir(self.properties):
            if d in {'bl_rna', 'rna_type'}:
                continue
            try:
                self.__class__._settings[d] = self.properties[d]
            except KeyError:
                # catches __doc__ etc.
                continue

    def load_settings(self):
        # what exception could occur here??
        for d in self.__class__._settings:
            self.properties[d] = self.__class__._settings[d]


class DATA_PT_test(bpy.types.Panel):
    bl_label = "Test Panel"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "data"

    def draw(self, context):
        self.layout.operator("object.test_invoke")


class OBJECT_OT_test_invoke(bpy.types.Operator, OperatorSettings):
    bl_idname = "object.test_invoke"
    bl_label = "Test"
    bl_options = {'REGISTER'}

    width = bpy.props.IntProperty(name="Width", 
            default=10, min=0, max=100)
    height = bpy.props.IntProperty(name="Height", 
            default=20, min=0, max=100)

    def execute(self, context):
        self.report({'INFO'}, "w:{}, h:{}".format(self.width, self.height))
        self.save_settings()
        return {'FINISHED'}

    def invoke(self, context, event):
        self.load_settings()
        wm = context.window_manager
        return wm.invoke_props_dialog(self)

def register():
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Should work for base types (int, float, str), but will probably fail for CollectionProperty as it is. But should be possible if you implement (de-)serialization functions for such propert types.

If you add non-property class variables to the operator, make sure to exclude them in save_settings(), or add a type check for all members - see the bl_rna property for that:

self.properties.bl_rna.properties['...'].type # e.g. 'INT'

BTW: I removed 'UNDO' from bl_options 'cause every change in the redo panel causes a re-execution of the operator. This is not the case for the dialog, the operator is run once on confirmation. Such dialogs should be used if an operator takes a long time to finish, to avoid irresponsive UI. So it's probably not supposed to have the redo functionality enabled.


3) There's another approach:

Register global properties (e.g. on bpy.types.Object or bpy.types.WindowManager) and add them to a panel. Let the user set them before (s)he clicks an Execute button. Pass the global properties to the operator. Global properties are remembered, so it works similar to the redo panel.

Example:

import bpy

class PrintSomeString(bpy.types.Operator):
    bl_label = "Print Some String"
    bl_idname = "object.print_some_string"
    bl_options = {'REGISTER', 'UNDO'}

    print_this = bpy.props.StringProperty(name="Print this")

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


class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

    def draw(self, context):
        layout = self.layout
        layout.prop(context.window_manager, "some_string")

        props = layout.operator(PrintSomeString.bl_idname)
        props.print_this = context.window_manager.some_string

        props = layout.operator(PrintSomeString.bl_idname, text="Print welcome message")
        props.print_this = "Welcome to Blender!"

def register():
    bpy.utils.register_module(__name__)
    bpy.types.WindowManager.some_string = bpy.props.StringProperty(name="Some String")


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.WindowManager.some_string


if __name__ == "__main__":
    register()

Paste into Text Editor and Run Script. See Object tab in Properties Editor.

$\endgroup$
12
  • $\begingroup$ Thanks for the detailed and informative answer, though it doesn't really answer the question. $\endgroup$
    – jigebren
    Jan 25, 2014 at 19:55
  • $\begingroup$ The redo panel was quite convenient, that's why I kept 'UNDO'. In fact I'll probably keep the Tool Shelf only and remove the invoke_props_dialog(), but I couldn't find a way to automatically open the Tool Shelf in case it's not opened yet (to guide the end-user). $\endgroup$
    – jigebren
    Jan 25, 2014 at 20:03
  • $\begingroup$ Edited post to explain a bit why it's not remembered for dialogs and added another possible solution (depends on exact goal if it's suitable). $\endgroup$
    – CodeManX
    Jan 25, 2014 at 20:29
  • $\begingroup$ Point 1) is not true. The dialog does read the last-values (you can see it uses the one set in the Tools Shelf instead of the default ones). Where it fails IMO is at saving them. That why I presume this behaviour is unintended... $\endgroup$
    – jigebren
    Jan 25, 2014 at 23:33
  • $\begingroup$ Point 2) I actually planned to use: "bpy.data.window_managers['WinMan'].operators[-1].bl_rna" to check if this is actually the same operator. Then I could get the last values form here. But it sounded a bit hacky anyhow, and probably undocumented. $\endgroup$
    – jigebren
    Jan 25, 2014 at 23:34
0
$\begingroup$

From what I understood it was actually an API bug. So the answer is, yes an operator should reuse its settings, even with popup dialog.

The whole bug report can be found here.

$\endgroup$

You must log in to answer this question.

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