From ac75e37c8e1291f8a46ebed89fd2f99ad68c7aff Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Mon, 11 Nov 2024 14:57:29 +0100 Subject: [PATCH 1/2] Fix #130113: Copying Object ID never copies its preview. This was added 7 years ago as 'safe' preservation of previous behavior, when ID copying was refactored and more control was added over its behavior. However, it was never removed since then, even though `NO_PREVIEW` flag has been part of the `LOCALIZE` copying behavior since many years. So time to remove this enforced bahevior and use the API as designed. If this causes new issues, they will have to be fixed in code calling the ID copy API (most likely by simply adding the `NO_PREVIEW` flag to the copy options). --- source/blender/blenkernel/intern/object.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/blender/blenkernel/intern/object.cc b/source/blender/blenkernel/intern/object.cc index a6d5bdfccb0..7d27882eebf 100644 --- a/source/blender/blenkernel/intern/object.cc +++ b/source/blender/blenkernel/intern/object.cc @@ -262,9 +262,7 @@ static void object_copy_data(Main *bmain, ob_dst->avs = ob_src->avs; ob_dst->mpath = animviz_copy_motionpath(ob_src->mpath); - /* Do not copy object's preview - * (mostly due to the fact renderers create temp copy of objects). */ - if ((flag & LIB_ID_COPY_NO_PREVIEW) == 0 && false) { /* XXX TODO: temp hack. */ + if ((flag & LIB_ID_COPY_NO_PREVIEW) == 0) { BKE_previewimg_id_copy(&ob_dst->id, &ob_src->id); } else { From 69a7948575e683fb45f2902604c15230e7c70f32 Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Mon, 11 Nov 2024 16:09:55 +0100 Subject: [PATCH 2/2] 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,