From 69a7948575e683fb45f2902604c15230e7c70f32 Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Mon, 11 Nov 2024 16:09:55 +0100 Subject: [PATCH] Doc: Py API: Add more info about `UNDO` operator option. Essentially, any operator modifying Blender data should enable this `UNDO` option, else bad things (corruption, crashes...) are likely to happen. * Added a new Operator example to explain this topic. * Updated some existing Operator examples that were not correct anymore. * Added a new small section in the gotchas page linking to it. * Added also short reminder about this in the `UNDO` 'tooltip' description itself. Related to #77557. --- .../examples/bpy.types.Operator.1.py | 84 ++++++++++--------- .../examples/bpy.types.Operator.2.py | 69 ++++++++------- .../examples/bpy.types.Operator.3.py | 54 +++++++----- .../examples/bpy.types.Operator.4.py | 47 ++++------- .../examples/bpy.types.Operator.5.py | 77 +++++++---------- .../examples/bpy.types.Operator.6.py | 76 ++++++++++++----- .../examples/bpy.types.Operator.7.py | 45 ++++++++++ doc/python_api/examples/bpy.types.Operator.py | 4 +- doc/python_api/rst/info_gotcha.rst | 9 ++ source/blender/makesrna/intern/rna_wm.cc | 7 +- 10 files changed, 275 insertions(+), 197 deletions(-) create mode 100644 doc/python_api/examples/bpy.types.Operator.7.py diff --git a/doc/python_api/examples/bpy.types.Operator.1.py b/doc/python_api/examples/bpy.types.Operator.1.py index dbdf009a3be..d02562e5089 100644 --- a/doc/python_api/examples/bpy.types.Operator.1.py +++ b/doc/python_api/examples/bpy.types.Operator.1.py @@ -1,61 +1,63 @@ """ -Invoke Function -+++++++++++++++ +.. _operator_modifying_blender_data_undo: -:class:`Operator.invoke` is used to initialize the operator from the context -at the moment the operator is called. -invoke() is typically used to assign properties which are then used by -execute(). -Some operators don't have an execute() function, removing the ability to be -repeated from a script or macro. +Modifying Blender Data & Undo ++++++++++++++++++++++++++++++ -This example shows how to define an operator which gets mouse input to -execute a function and that this operator can be invoked or executed from -the python api. +Any operator modifying Blender data should enable the ``'UNDO'`` option. +This will make Blender automatically create an undo step when the operator +finishes its ``execute`` (or ``invoke``, see below) functions, and returns +``{'FINISHED'}``. + +Otherwise, no undo step will be created, which will at best corrupt the +undo stack and confuse the user (since modifications done by the operator +may either not be undoable, or be undone together with other edits done +before). In many cases, this can even lead to data corruption and crashes. + +Note that when an operator returns ``{'CANCELLED'}``, no undo step will be +created. This means that if an error occurs *after* modifying some data +already, it is better to return ``{'FINISHED'}``, unless it is possible to +fully undo the changes before returning. + +.. note:: + + Most examples in this page do not do any edit to Blender data, which is + why it is safe to keep the default ``bl_options`` value for these operators. + +.. note:: + + In some complex cases, the automatic undo step created on operator exit may + not be enough. For example, if the operator does mode switching, or calls + other operators that should create an extra undo step, etc. + + Such manual undo push is possible using the :class:`bpy.ops.ed.undo_push` + function. Be careful though, this is considered an advanced feature and + requires some understanding of the actual undo system in Blender code. -Also notice this operator defines its own properties, these are different -to typical class properties because blender registers them with the -operator, to use as arguments when called, saved for operator undo/redo and -automatically added into the user interface. """ import bpy -class SimpleMouseOperator(bpy.types.Operator): - """ This operator shows the mouse location, - this string is used for the tooltip and API docs - """ - bl_idname = "wm.mouse_position" - bl_label = "Invoke Mouse Operator" - - x: bpy.props.IntProperty() - y: bpy.props.IntProperty() +class DataEditOperator(bpy.types.Operator): + bl_idname = "object.data_edit" + bl_label = "Data Editing Operator" + # The default value is only 'REGISTER', 'UNDO' is mandatory when Blender data is modified + # (and does require 'REGISTER' as well). + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - # rather than printing, use the report function, - # this way the message appears in the header, - self.report({'INFO'}, "Mouse coords are {:d} {:d}".format(self.x, self.y)) + context.object.location.x += 1.0 return {'FINISHED'} - def invoke(self, context, event): - self.x = event.mouse_x - self.y = event.mouse_y - return self.execute(context) - # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator(SimpleMouseOperator.bl_idname, text="Simple Mouse Operator") + self.layout.operator(DataEditOperator.bl_idname, text="Blender Data Editing Operator") -# Register and add to the view menu (required to also use F3 search "Simple Mouse Operator" for quick access) -bpy.utils.register_class(SimpleMouseOperator) +# Register. +bpy.utils.register_class(DataEditOperator) bpy.types.VIEW3D_MT_view.append(menu_func) # Test call to the newly defined operator. -# Here we call the operator and invoke it, meaning that the settings are taken -# from the mouse. -bpy.ops.wm.mouse_position('INVOKE_DEFAULT') - -# Another test call, this time call execute() directly with pre-defined settings. -bpy.ops.wm.mouse_position('EXEC_DEFAULT', x=20, y=66) +bpy.ops.object.data_edit() diff --git a/doc/python_api/examples/bpy.types.Operator.2.py b/doc/python_api/examples/bpy.types.Operator.2.py index 2a14d6b00f5..dbdf009a3be 100644 --- a/doc/python_api/examples/bpy.types.Operator.2.py +++ b/doc/python_api/examples/bpy.types.Operator.2.py @@ -1,52 +1,61 @@ """ -Calling a File Selector -+++++++++++++++++++++++ -This example shows how an operator can use the file selector. +Invoke Function ++++++++++++++++ -Notice the invoke function calls a window manager method and returns -``{'RUNNING_MODAL'}``, this means the file selector stays open and the operator does not -exit immediately after invoke finishes. +:class:`Operator.invoke` is used to initialize the operator from the context +at the moment the operator is called. +invoke() is typically used to assign properties which are then used by +execute(). +Some operators don't have an execute() function, removing the ability to be +repeated from a script or macro. -The file selector runs the operator, calling :class:`Operator.execute` when the -user confirms. +This example shows how to define an operator which gets mouse input to +execute a function and that this operator can be invoked or executed from +the python api. -The :class:`Operator.poll` function is optional, used to check if the operator -can run. +Also notice this operator defines its own properties, these are different +to typical class properties because blender registers them with the +operator, to use as arguments when called, saved for operator undo/redo and +automatically added into the user interface. """ import bpy -class ExportSomeData(bpy.types.Operator): - """Test exporter which just writes hello world""" - bl_idname = "export.some_data" - bl_label = "Export Some Data" +class SimpleMouseOperator(bpy.types.Operator): + """ This operator shows the mouse location, + this string is used for the tooltip and API docs + """ + bl_idname = "wm.mouse_position" + bl_label = "Invoke Mouse Operator" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - @classmethod - def poll(cls, context): - return context.object is not None + x: bpy.props.IntProperty() + y: bpy.props.IntProperty() def execute(self, context): - file = open(self.filepath, 'w') - file.write("Hello World " + context.object.name) + # rather than printing, use the report function, + # this way the message appears in the header, + self.report({'INFO'}, "Mouse coords are {:d} {:d}".format(self.x, self.y)) return {'FINISHED'} def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + self.x = event.mouse_x + self.y = event.mouse_y + return self.execute(context) # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator_context = 'INVOKE_DEFAULT' - self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator") + self.layout.operator(SimpleMouseOperator.bl_idname, text="Simple Mouse Operator") -# Register and add to the file selector (required to also use F3 search "Text Export Operator" for quick access) -bpy.utils.register_class(ExportSomeData) -bpy.types.TOPBAR_MT_file_export.append(menu_func) +# Register and add to the view menu (required to also use F3 search "Simple Mouse Operator" for quick access) +bpy.utils.register_class(SimpleMouseOperator) +bpy.types.VIEW3D_MT_view.append(menu_func) +# Test call to the newly defined operator. +# Here we call the operator and invoke it, meaning that the settings are taken +# from the mouse. +bpy.ops.wm.mouse_position('INVOKE_DEFAULT') -# test call -bpy.ops.export.some_data('INVOKE_DEFAULT') +# Another test call, this time call execute() directly with pre-defined settings. +bpy.ops.wm.mouse_position('EXEC_DEFAULT', x=20, y=66) diff --git a/doc/python_api/examples/bpy.types.Operator.3.py b/doc/python_api/examples/bpy.types.Operator.3.py index 264b72f27c6..2a14d6b00f5 100644 --- a/doc/python_api/examples/bpy.types.Operator.3.py +++ b/doc/python_api/examples/bpy.types.Operator.3.py @@ -1,40 +1,52 @@ """ -Dialog Box -++++++++++ +Calling a File Selector ++++++++++++++++++++++++ +This example shows how an operator can use the file selector. -This operator uses its :class:`Operator.invoke` function to call a popup. +Notice the invoke function calls a window manager method and returns +``{'RUNNING_MODAL'}``, this means the file selector stays open and the operator does not +exit immediately after invoke finishes. + +The file selector runs the operator, calling :class:`Operator.execute` when the +user confirms. + +The :class:`Operator.poll` function is optional, used to check if the operator +can run. """ import bpy -class DialogOperator(bpy.types.Operator): - bl_idname = "object.dialog_operator" - bl_label = "Simple Dialog Operator" +class ExportSomeData(bpy.types.Operator): + """Test exporter which just writes hello world""" + bl_idname = "export.some_data" + bl_label = "Export Some Data" - my_float: bpy.props.FloatProperty(name="Some Floating Point") - my_bool: bpy.props.BoolProperty(name="Toggle Option") - my_string: bpy.props.StringProperty(name="String Value") + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + @classmethod + def poll(cls, context): + return context.object is not None def execute(self, context): - message = "Popup Values: {:f}, {:d}, '{:s}'".format( - self.my_float, self.my_bool, self.my_string, - ) - self.report({'INFO'}, message) + file = open(self.filepath, 'w') + file.write("Hello World " + context.object.name) return {'FINISHED'} def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator(DialogOperator.bl_idname, text="Dialog Operator") + self.layout.operator_context = 'INVOKE_DEFAULT' + self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator") -# Register and add to the object menu (required to also use F3 search "Dialog Operator" for quick access) -bpy.utils.register_class(DialogOperator) -bpy.types.VIEW3D_MT_object.append(menu_func) +# Register and add to the file selector (required to also use F3 search "Text Export Operator" for quick access) +bpy.utils.register_class(ExportSomeData) +bpy.types.TOPBAR_MT_file_export.append(menu_func) -# Test call. -bpy.ops.object.dialog_operator('INVOKE_DEFAULT') + +# test call +bpy.ops.export.some_data('INVOKE_DEFAULT') diff --git a/doc/python_api/examples/bpy.types.Operator.4.py b/doc/python_api/examples/bpy.types.Operator.4.py index d33f6a6113d..264b72f27c6 100644 --- a/doc/python_api/examples/bpy.types.Operator.4.py +++ b/doc/python_api/examples/bpy.types.Operator.4.py @@ -1,55 +1,40 @@ """ -Custom Drawing -++++++++++++++ +Dialog Box +++++++++++ -By default operator properties use an automatic user interface layout. -If you need more control you can create your own layout with a -:class:`Operator.draw` function. - -This works like the :class:`Panel` and :class:`Menu` draw functions, its used -for dialogs and file selectors. +This operator uses its :class:`Operator.invoke` function to call a popup. """ import bpy -class CustomDrawOperator(bpy.types.Operator): - bl_idname = "object.custom_draw" - bl_label = "Simple Modal Operator" +class DialogOperator(bpy.types.Operator): + bl_idname = "object.dialog_operator" + bl_label = "Simple Dialog Operator" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - my_float: bpy.props.FloatProperty(name="Float") + my_float: bpy.props.FloatProperty(name="Some Floating Point") my_bool: bpy.props.BoolProperty(name="Toggle Option") my_string: bpy.props.StringProperty(name="String Value") def execute(self, context): - print("Test", self) + message = "Popup Values: {:f}, {:d}, '{:s}'".format( + self.my_float, self.my_bool, self.my_string, + ) + self.report({'INFO'}, message) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self) - def draw(self, context): - layout = self.layout - col = layout.column() - col.label(text="Custom Interface!") - - row = col.row() - row.prop(self, "my_float") - row.prop(self, "my_bool") - - col.prop(self, "my_string") - # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator(CustomDrawOperator.bl_idname, text="Custom Draw Operator") + self.layout.operator(DialogOperator.bl_idname, text="Dialog Operator") -# Register and add to the object menu (required to also use F3 search "Custom Draw Operator" for quick access). -bpy.utils.register_class(CustomDrawOperator) +# Register and add to the object menu (required to also use F3 search "Dialog Operator" for quick access) +bpy.utils.register_class(DialogOperator) bpy.types.VIEW3D_MT_object.append(menu_func) -# test call -bpy.ops.object.custom_draw('INVOKE_DEFAULT') +# Test call. +bpy.ops.object.dialog_operator('INVOKE_DEFAULT') diff --git a/doc/python_api/examples/bpy.types.Operator.5.py b/doc/python_api/examples/bpy.types.Operator.5.py index 6f912f863f1..d33f6a6113d 100644 --- a/doc/python_api/examples/bpy.types.Operator.5.py +++ b/doc/python_api/examples/bpy.types.Operator.5.py @@ -1,72 +1,55 @@ """ -.. _modal_operator: +Custom Drawing +++++++++++++++ -Modal Execution -+++++++++++++++ +By default operator properties use an automatic user interface layout. +If you need more control you can create your own layout with a +:class:`Operator.draw` function. -This operator defines a :class:`Operator.modal` function that will keep being -run to handle events until it returns ``{'FINISHED'}`` or ``{'CANCELLED'}``. - -Modal operators run every time a new event is detected, such as a mouse click -or key press. Conversely, when no new events are detected, the modal operator -will not run. Modal operators are especially useful for interactive tools, an -operator can have its own state where keys toggle options as the operator runs. -Grab, Rotate, Scale, and Fly-Mode are examples of modal operators. - -:class:`Operator.invoke` is used to initialize the operator as being active -by returning ``{'RUNNING_MODAL'}``, initializing the modal loop. - -Notice ``__init__()`` and ``__del__()`` are declared. -For other operator types they are not useful but for modal operators they will -be called before the :class:`Operator.invoke` and after the operator finishes. +This works like the :class:`Panel` and :class:`Menu` draw functions, its used +for dialogs and file selectors. """ import bpy -class ModalOperator(bpy.types.Operator): - bl_idname = "object.modal_operator" +class CustomDrawOperator(bpy.types.Operator): + bl_idname = "object.custom_draw" bl_label = "Simple Modal Operator" - def __init__(self): - print("Start") + filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def __del__(self): - print("End") + my_float: bpy.props.FloatProperty(name="Float") + my_bool: bpy.props.BoolProperty(name="Toggle Option") + my_string: bpy.props.StringProperty(name="String Value") def execute(self, context): - context.object.location.x = self.value / 100.0 + print("Test", self) return {'FINISHED'} - def modal(self, context, event): - if event.type == 'MOUSEMOVE': # Apply - self.value = event.mouse_x - self.execute(context) - elif event.type == 'LEFTMOUSE': # Confirm - return {'FINISHED'} - elif event.type in {'RIGHTMOUSE', 'ESC'}: # Cancel - # Revert all changes that have been made - context.object.location.x = self.init_loc_x - return {'CANCELLED'} - - return {'RUNNING_MODAL'} - def invoke(self, context, event): - self.init_loc_x = context.object.location.x - self.value = event.mouse_x - self.execute(context) + wm = context.window_manager + return wm.invoke_props_dialog(self) - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + def draw(self, context): + layout = self.layout + col = layout.column() + col.label(text="Custom Interface!") + + row = col.row() + row.prop(self, "my_float") + row.prop(self, "my_bool") + + col.prop(self, "my_string") # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator(ModalOperator.bl_idname, text="Modal Operator") + self.layout.operator(CustomDrawOperator.bl_idname, text="Custom Draw Operator") -# Register and add to the object menu (required to also use F3 search "Modal Operator" for quick access). -bpy.utils.register_class(ModalOperator) +# Register and add to the object menu (required to also use F3 search "Custom Draw Operator" for quick access). +bpy.utils.register_class(CustomDrawOperator) bpy.types.VIEW3D_MT_object.append(menu_func) # test call -bpy.ops.object.modal_operator('INVOKE_DEFAULT') +bpy.ops.object.custom_draw('INVOKE_DEFAULT') diff --git a/doc/python_api/examples/bpy.types.Operator.6.py b/doc/python_api/examples/bpy.types.Operator.6.py index cf556c63ab8..f3f2a53b3c7 100644 --- a/doc/python_api/examples/bpy.types.Operator.6.py +++ b/doc/python_api/examples/bpy.types.Operator.6.py @@ -1,45 +1,75 @@ """ -Enum Search Popup -+++++++++++++++++ +.. _modal_operator: -You may want to have an operator prompt the user to select an item -from a search field, this can be done using :class:`bpy.types.Operator.invoke_search_popup`. +Modal Execution ++++++++++++++++ + +This operator defines a :class:`Operator.modal` function that will keep being +run to handle events until it returns ``{'FINISHED'}`` or ``{'CANCELLED'}``. + +Modal operators run every time a new event is detected, such as a mouse click +or key press. Conversely, when no new events are detected, the modal operator +will not run. Modal operators are especially useful for interactive tools, an +operator can have its own state where keys toggle options as the operator runs. +Grab, Rotate, Scale, and Fly-Mode are examples of modal operators. + +:class:`Operator.invoke` is used to initialize the operator as being active +by returning ``{'RUNNING_MODAL'}``, initializing the modal loop. + +Notice ``__init__()`` and ``__del__()`` are declared. +For other operator types they are not useful but for modal operators they will +be called before the :class:`Operator.invoke` and after the operator finishes. """ import bpy -from bpy.props import EnumProperty -class SearchEnumOperator(bpy.types.Operator): - bl_idname = "object.search_enum_operator" - bl_label = "Search Enum Operator" - bl_property = "my_search" +class ModalOperator(bpy.types.Operator): + bl_idname = "object.modal_operator" + bl_label = "Simple Modal Operator" + bl_options = {'REGISTER', 'UNDO'} - my_search: EnumProperty( - name="My Search", - items=( - ('FOO', "Foo", ""), - ('BAR', "Bar", ""), - ('BAZ', "Baz", ""), - ), - ) + def __init__(self): + super().__init__() + print("Start") + + def __del__(self): + super().__del__() + print("End") def execute(self, context): - self.report({'INFO'}, "Selected:" + self.my_search) + context.object.location.x = self.value / 100.0 return {'FINISHED'} + def modal(self, context, event): + if event.type == 'MOUSEMOVE': # Apply + self.value = event.mouse_x + self.execute(context) + elif event.type == 'LEFTMOUSE': # Confirm + return {'FINISHED'} + elif event.type in {'RIGHTMOUSE', 'ESC'}: # Cancel + # Revert all changes that have been made + context.object.location.x = self.init_loc_x + return {'CANCELLED'} + + return {'RUNNING_MODAL'} + def invoke(self, context, event): - context.window_manager.invoke_search_popup(self) + self.init_loc_x = context.object.location.x + self.value = event.mouse_x + self.execute(context) + + context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} # Only needed if you want to add into a dynamic menu. def menu_func(self, context): - self.layout.operator(SearchEnumOperator.bl_idname, text="Search Enum Operator") + self.layout.operator(ModalOperator.bl_idname, text="Modal Operator") -# Register and add to the object menu (required to also use F3 search "Search Enum Operator" for quick access) -bpy.utils.register_class(SearchEnumOperator) +# Register and add to the object menu (required to also use F3 search "Modal Operator" for quick access). +bpy.utils.register_class(ModalOperator) bpy.types.VIEW3D_MT_object.append(menu_func) # test call -bpy.ops.object.search_enum_operator('INVOKE_DEFAULT') +bpy.ops.object.modal_operator('INVOKE_DEFAULT') diff --git a/doc/python_api/examples/bpy.types.Operator.7.py b/doc/python_api/examples/bpy.types.Operator.7.py new file mode 100644 index 00000000000..cf556c63ab8 --- /dev/null +++ b/doc/python_api/examples/bpy.types.Operator.7.py @@ -0,0 +1,45 @@ +""" +Enum Search Popup ++++++++++++++++++ + +You may want to have an operator prompt the user to select an item +from a search field, this can be done using :class:`bpy.types.Operator.invoke_search_popup`. +""" +import bpy +from bpy.props import EnumProperty + + +class SearchEnumOperator(bpy.types.Operator): + bl_idname = "object.search_enum_operator" + bl_label = "Search Enum Operator" + bl_property = "my_search" + + my_search: EnumProperty( + name="My Search", + items=( + ('FOO', "Foo", ""), + ('BAR', "Bar", ""), + ('BAZ', "Baz", ""), + ), + ) + + def execute(self, context): + self.report({'INFO'}, "Selected:" + self.my_search) + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.invoke_search_popup(self) + return {'RUNNING_MODAL'} + + +# Only needed if you want to add into a dynamic menu. +def menu_func(self, context): + self.layout.operator(SearchEnumOperator.bl_idname, text="Search Enum Operator") + + +# Register and add to the object menu (required to also use F3 search "Search Enum Operator" for quick access) +bpy.utils.register_class(SearchEnumOperator) +bpy.types.VIEW3D_MT_object.append(menu_func) + +# test call +bpy.ops.object.search_enum_operator('INVOKE_DEFAULT') diff --git a/doc/python_api/examples/bpy.types.Operator.py b/doc/python_api/examples/bpy.types.Operator.py index 19cd08b018f..a0dd149dcb3 100644 --- a/doc/python_api/examples/bpy.types.Operator.py +++ b/doc/python_api/examples/bpy.types.Operator.py @@ -9,9 +9,7 @@ user input. The function should return ``{'FINISHED'}`` or ``{'CANCELLED'}``, the latter meaning that operator execution was aborted without making any changes, and -saving an undo entry isn't neccesary. If an error is detected after some changes -have already been made, use the ``{'FINISHED'}`` return code, or the behavior -of undo will be confusing for the user. +that no undo step will created (see next example for more info about undo). .. note:: diff --git a/doc/python_api/rst/info_gotcha.rst b/doc/python_api/rst/info_gotcha.rst index 155035879aa..a59375f0fa0 100644 --- a/doc/python_api/rst/info_gotcha.rst +++ b/doc/python_api/rst/info_gotcha.rst @@ -737,6 +737,15 @@ interactively by the user is the only way to make sure that the script doesn't b guarantee of any kind that it will be safe and consistent. Use it at your own risk. +Modifying Blender Data & Undo +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In general, when Blender data is modified, there should always be an undo step created for it. +Otherwise, there will be issues, ranging from invalid/broken undo stack, to crashes on undo/redo. + +This is especially true when modifying Blender data :ref:`in operators `. + + Undo & Library Data ^^^^^^^^^^^^^^^^^^^ diff --git a/source/blender/makesrna/intern/rna_wm.cc b/source/blender/makesrna/intern/rna_wm.cc index 24794db6394..2d6539c6b23 100644 --- a/source/blender/makesrna/intern/rna_wm.cc +++ b/source/blender/makesrna/intern/rna_wm.cc @@ -520,7 +520,12 @@ const EnumPropertyItem rna_enum_operator_type_flag_items[] = { 0, "Register", "Display in the info window and support the redo toolbar panel"}, - {OPTYPE_UNDO, "UNDO", 0, "Undo", "Push an undo event (needed for operator redo)"}, + {OPTYPE_UNDO, + "UNDO", + 0, + "Undo", + "Push an undo event when the operator returns `FINISHED` (needed for operator redo, " + "mandatory if the operator modifies Blender data)"}, {OPTYPE_UNDO_GROUPED, "UNDO_GROUPED", 0,