From d0ef66ddff58065dbda08d32c06ef3157ff6487d Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 14 Nov 2023 18:14:01 +0200 Subject: [PATCH] Drivers: implement fallback values for RNA path based variables. As discussed in #105407, it can be useful to support returning a fallback value specified by the user instead of failing the driver if a driver variable cannot resolve its RNA path. This especially applies to context variables referencing custom properties, since when the object with the driver is linked into another scene, the custom property can easily not exist there. This patch adds an optional fallback value setting to properties based on RNA path (including ordinary Single Property variables due to shared code and similarity). When enabled, RNA path lookup failures (including invalid array index) cause the fallback value to be used instead of marking the driver invalid. A flag is added to track when this happens for UI use. It is also exposed to python for lint type scripts. When the fallback value is used, the input field containing the property RNA path that failed to resolve is highlighted in red (identically to the case without a fallback), and the driver can be included in the With Errors filter of the Drivers editor. However, the channel name is not underlined in red, because the driver as a whole evaluates successfully. Pull Request: https://projects.blender.org/blender/blender/pulls/110135 --- scripts/startup/bl_ui/space_graph.py | 7 + source/blender/blenkernel/BKE_fcurve_driver.h | 2 + .../blenkernel/intern/fcurve_driver.cc | 38 ++++ .../animation/anim_channels_defines.cc | 2 +- .../blender/editors/animation/anim_filter.cc | 10 +- .../editors/space_graph/graph_buttons.cc | 24 +- source/blender/makesdna/DNA_action_types.h | 3 + source/blender/makesdna/DNA_anim_types.h | 16 +- source/blender/makesrna/intern/rna_action.cc | 10 + source/blender/makesrna/intern/rna_fcurve.cc | 22 ++ .../blender/python/intern/bpy_rna_driver.cc | 5 + tests/python/CMakeLists.txt | 7 + tests/python/bl_animation_drivers.py | 211 ++++++++++++++++++ 13 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 tests/python/bl_animation_drivers.py diff --git a/scripts/startup/bl_ui/space_graph.py b/scripts/startup/bl_ui/space_graph.py index 7df04028b55..0bb4bbacd70 100644 --- a/scripts/startup/bl_ui/space_graph.py +++ b/scripts/startup/bl_ui/space_graph.py @@ -93,6 +93,7 @@ class GRAPH_PT_filters(DopesheetFilterPopoverBase, Panel): def draw(self, context): layout = self.layout + st = context.space_data DopesheetFilterPopoverBase.draw_generic_filters(context, layout) layout.separator() @@ -100,6 +101,12 @@ class GRAPH_PT_filters(DopesheetFilterPopoverBase, Panel): layout.separator() DopesheetFilterPopoverBase.draw_standard_filters(context, layout) + if st.mode == 'DRIVERS': + layout.separator() + col = layout.column(align=True) + col.label(text="Drivers:") + col.prop(st.dopesheet, "show_driver_fallback_as_error") + class GRAPH_PT_snapping(Panel): bl_space_type = 'GRAPH_EDITOR' diff --git a/source/blender/blenkernel/BKE_fcurve_driver.h b/source/blender/blenkernel/BKE_fcurve_driver.h index 5bca2abb1e3..c2787dde064 100644 --- a/source/blender/blenkernel/BKE_fcurve_driver.h +++ b/source/blender/blenkernel/BKE_fcurve_driver.h @@ -138,6 +138,8 @@ float driver_get_variable_value(const struct AnimationEvalContext *anim_eval_con typedef enum eDriverVariablePropertyResult { /** The property reference has been succesfully resolved and can be accessed. */ DRIVER_VAR_PROPERTY_SUCCESS, + /** Evaluation should use the fallback value. */ + DRIVER_VAR_PROPERTY_FALLBACK, /** The target property could not be resolved. */ DRIVER_VAR_PROPERTY_INVALID, /** The property was resolved (output parameters are set), diff --git a/source/blender/blenkernel/intern/fcurve_driver.cc b/source/blender/blenkernel/intern/fcurve_driver.cc index 4cfc79501bf..f7c02eabe9a 100644 --- a/source/blender/blenkernel/intern/fcurve_driver.cc +++ b/source/blender/blenkernel/intern/fcurve_driver.cc @@ -146,6 +146,21 @@ bool driver_get_target_property(const DriverTargetContext *driver_target_context return true; } +/** + * Checks if the fallback value can be used, and if so, sets dtar flags to signal its usage. + * The caller is expected to immediately return the fallback value if this returns true. + */ +static bool dtar_try_use_fallback(DriverTarget *dtar) +{ + if ((dtar->options & DTAR_OPTION_USE_FALLBACK) == 0) { + return false; + } + + dtar->flag &= ~DTAR_FLAG_INVALID; + dtar->flag |= DTAR_FLAG_FALLBACK_USED; + return true; +} + /** * Helper function to obtain a value using RNA from the specified source * (for evaluating drivers). @@ -160,6 +175,8 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context, return 0.0f; } + dtar->flag &= ~DTAR_FLAG_FALLBACK_USED; + /* Get property to resolve the target from. * Naming is a bit confusing, but this is what is exposed as "Prop" or "Context Property" in * interface. */ @@ -184,6 +201,10 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context, if (!RNA_path_resolve_property_full( &property_ptr, dtar->rna_path, &value_ptr, &value_prop, &index)) { + if (dtar_try_use_fallback(dtar)) { + return dtar->fallback_value; + } + /* Path couldn't be resolved. */ if (G.debug & G_DEBUG) { CLOG_ERROR(&LOG, @@ -200,6 +221,10 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context, if (RNA_property_array_check(value_prop)) { /* Array. */ if (index < 0 || index >= RNA_property_array_length(&value_ptr, value_prop)) { + if (dtar_try_use_fallback(dtar)) { + return dtar->fallback_value; + } + /* Out of bounds. */ if (G.debug & G_DEBUG) { CLOG_ERROR(&LOG, @@ -272,6 +297,8 @@ eDriverVariablePropertyResult driver_get_variable_property( return DRIVER_VAR_PROPERTY_INVALID; } + dtar->flag &= ~DTAR_FLAG_FALLBACK_USED; + /* Get RNA-pointer for the data-block given in target. */ const DriverTargetContext driver_target_context = driver_target_context_from_animation_context( anim_eval_context); @@ -295,6 +322,13 @@ eDriverVariablePropertyResult driver_get_variable_property( /* OK. */ } else { + if (dtar_try_use_fallback(dtar)) { + ptr = PointerRNA_NULL; + *r_prop = nullptr; + *r_index = -1; + return DRIVER_VAR_PROPERTY_FALLBACK; + } + /* Path couldn't be resolved. */ if (G.debug & G_DEBUG) { CLOG_ERROR(&LOG, @@ -319,6 +353,10 @@ eDriverVariablePropertyResult driver_get_variable_property( /* Verify the array index and apply fallback if appropriate. */ if (prop && RNA_property_array_check(prop)) { if ((index < 0 && !allow_no_index) || index >= RNA_property_array_length(&ptr, prop)) { + if (dtar_try_use_fallback(dtar)) { + return DRIVER_VAR_PROPERTY_FALLBACK; + } + /* Out of bounds. */ if (G.debug & G_DEBUG) { CLOG_ERROR(&LOG, diff --git a/source/blender/editors/animation/anim_channels_defines.cc b/source/blender/editors/animation/anim_channels_defines.cc index 566a2aba370..19c66cd485a 100644 --- a/source/blender/editors/animation/anim_channels_defines.cc +++ b/source/blender/editors/animation/anim_channels_defines.cc @@ -4559,7 +4559,7 @@ static bool achannel_is_being_renamed(const bAnimContext *ac, return false; } -/** Check if the animation channel name should be underlined in red due to fatal errors. */ +/** Check if the animation channel name should be underlined in red due to errors. */ static bool achannel_is_broken(const bAnimListElem *ale) { switch (ale->type) { diff --git a/source/blender/editors/animation/anim_filter.cc b/source/blender/editors/animation/anim_filter.cc index f0e482afce4..c7811d5ebb4 100644 --- a/source/blender/editors/animation/anim_filter.cc +++ b/source/blender/editors/animation/anim_filter.cc @@ -1212,7 +1212,7 @@ static bool skip_fcurve_with_name( * * \return true if F-Curve has errors/is disabled */ -static bool fcurve_has_errors(const FCurve *fcu) +static bool fcurve_has_errors(const FCurve *fcu, bDopeSheet *ads) { /* F-Curve disabled (path evaluation error). */ if (fcu->flag & FCURVE_DISABLED) { @@ -1238,6 +1238,12 @@ static bool fcurve_has_errors(const FCurve *fcu) if (dtar->flag & DTAR_FLAG_INVALID) { return true; } + + if ((dtar->flag & DTAR_FLAG_FALLBACK_USED) && + (ads->filterflag2 & ADS_FILTER_DRIVER_FALLBACK_AS_ERROR)) + { + return true; + } } DRIVER_TARGETS_LOOPER_END; } @@ -1305,7 +1311,7 @@ static FCurve *animfilter_fcurve_next(bDopeSheet *ads, /* error-based filtering... */ if ((ads) && (ads->filterflag & ADS_FILTER_ONLY_ERRORS)) { /* skip if no errors... */ - if (fcurve_has_errors(fcu) == false) { + if (!fcurve_has_errors(fcu, ads)) { continue; } } diff --git a/source/blender/editors/space_graph/graph_buttons.cc b/source/blender/editors/space_graph/graph_buttons.cc index 9417537a1a0..533728ff483 100644 --- a/source/blender/editors/space_graph/graph_buttons.cc +++ b/source/blender/editors/space_graph/graph_buttons.cc @@ -740,6 +740,20 @@ static bool graph_panel_drivers_poll(const bContext *C, PanelType * /*pt*/) return graph_panel_context(C, nullptr, nullptr); } +static void graph_panel_driverVar_fallback(uiLayout *layout, + const DriverTarget *dtar, + PointerRNA *dtar_ptr) +{ + if (dtar->options & DTAR_OPTION_USE_FALLBACK) { + uiLayout *row = uiLayoutRow(layout, true); + uiItemR(row, dtar_ptr, "use_fallback_value", UI_ITEM_NONE, "", ICON_NONE); + uiItemR(row, dtar_ptr, "fallback_value", UI_ITEM_NONE, nullptr, ICON_NONE); + } + else { + uiItemR(layout, dtar_ptr, "use_fallback_value", UI_ITEM_NONE, nullptr, ICON_NONE); + } +} + /* settings for 'single property' driver variable type */ static void graph_panel_driverVar__singleProp(uiLayout *layout, ID *id, DriverVar *dvar) { @@ -761,12 +775,15 @@ static void graph_panel_driverVar__singleProp(uiLayout *layout, ID *id, DriverVa /* rna path */ col = uiLayoutColumn(layout, true); - uiLayoutSetRedAlert(col, (dtar->flag & DTAR_FLAG_INVALID)); + uiLayoutSetRedAlert(col, (dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED))); uiTemplatePathBuilder(col, &dtar_ptr, "data_path", &root_ptr, CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path")); + + /* Default value. */ + graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr); } } @@ -904,13 +921,16 @@ static void graph_panel_driverVar__contextProp(uiLayout *layout, ID *id, DriverV /* Target Path */ { uiLayout *col = uiLayoutColumn(layout, true); - uiLayoutSetRedAlert(col, (dtar->flag & DTAR_FLAG_INVALID)); + uiLayoutSetRedAlert(col, (dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED))); uiTemplatePathBuilder(col, &dtar_ptr, "data_path", nullptr, CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path")); } + + /* Default value. */ + graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr); } /* ----------------------------------------------------------------- */ diff --git a/source/blender/makesdna/DNA_action_types.h b/source/blender/makesdna/DNA_action_types.h index a1fac99e157..84cf5f62898 100644 --- a/source/blender/makesdna/DNA_action_types.h +++ b/source/blender/makesdna/DNA_action_types.h @@ -827,6 +827,9 @@ typedef enum eDopeSheet_FilterFlag2 { ADS_FILTER_NOHAIR = (1 << 3), ADS_FILTER_NOPOINTCLOUD = (1 << 4), ADS_FILTER_NOVOLUME = (1 << 5), + + /** Include working drivers with variables using their fallback values into Only Show Errors. */ + ADS_FILTER_DRIVER_FALLBACK_AS_ERROR = (1 << 6), } eDopeSheet_FilterFlag2; /* DopeSheet general flags */ diff --git a/source/blender/makesdna/DNA_anim_types.h b/source/blender/makesdna/DNA_anim_types.h index 0e4fa90e127..0f45ff46eab 100644 --- a/source/blender/makesdna/DNA_anim_types.h +++ b/source/blender/makesdna/DNA_anim_types.h @@ -312,13 +312,15 @@ typedef struct DriverTarget { /** Rotation channel calculation type. */ char rotation_mode; - char _pad[7]; + char _pad[5]; /** * Flags for the validity of the target * (NOTE: these get reset every time the types change). */ short flag; + /** Single-bit user-visible toggles (not reset on type change) from eDriverTarget_Options. */ + short options; /** Type of ID-block that this target can use. */ int idtype; @@ -327,9 +329,16 @@ typedef struct DriverTarget { * This is a value of enumerator #eDriverTarget_ContextProperty. */ int context_property; - int _pad1; + /* Fallback value to use with DTAR_OPTION_USE_FALLBACK. */ + float fallback_value; } DriverTarget; +/** Driver Target options. */ +typedef enum eDriverTarget_Options { + /** Use the fallback value when the target is invalid (rna_path cannot be resolved). */ + DTAR_OPTION_USE_FALLBACK = (1 << 0), +} eDriverTarget_Options; + /** Driver Target flags. */ typedef enum eDriverTarget_Flag { /** used for targets that use the pchan_name instead of RNA path @@ -346,6 +355,9 @@ typedef enum eDriverTarget_Flag { /** error flags */ DTAR_FLAG_INVALID = (1 << 4), + + /** the fallback value was actually used */ + DTAR_FLAG_FALLBACK_USED = (1 << 5), } eDriverTarget_Flag; /* Transform Channels for Driver Targets */ diff --git a/source/blender/makesrna/intern/rna_action.cc b/source/blender/makesrna/intern/rna_action.cc index fddf3f048eb..4bbb93dfa54 100644 --- a/source/blender/makesrna/intern/rna_action.cc +++ b/source/blender/makesrna/intern/rna_action.cc @@ -655,6 +655,16 @@ static void rna_def_dopesheet(BlenderRNA *brna) prop, "Display Movie Clips", "Include visualization of movie clip related animation data"); RNA_def_property_ui_icon(prop, ICON_TRACKER, 0); RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr); + + prop = RNA_def_property(srna, "show_driver_fallback_as_error", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "filterflag2", ADS_FILTER_DRIVER_FALLBACK_AS_ERROR); + RNA_def_property_ui_text( + prop, + "Variable Fallback As Error", + "Include drivers that relied on any fallback values for their evaluation " + "in the Only Show Errors filter, even if the driver evaluation succeeded"); + RNA_def_property_ui_icon(prop, ICON_RNA, 0); + RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr); } static void rna_def_action_group(BlenderRNA *brna) diff --git a/source/blender/makesrna/intern/rna_fcurve.cc b/source/blender/makesrna/intern/rna_fcurve.cc index 5b49911c8d6..3ded98004dd 100644 --- a/source/blender/makesrna/intern/rna_fcurve.cc +++ b/source/blender/makesrna/intern/rna_fcurve.cc @@ -1974,6 +1974,28 @@ static void rna_def_drivertarget(BlenderRNA *brna) RNA_def_property_ui_text( prop, "Context Property", "Type of a context-dependent data-block to access property from"); RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data"); + + prop = RNA_def_property(srna, "use_fallback_value", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "options", DTAR_OPTION_USE_FALLBACK); + RNA_def_property_ui_text(prop, + "Use Fallback", + "Use the fallback value if the data path can't be resolved, instead of " + "failing to evaluate the driver"); + RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data"); + + prop = RNA_def_property(srna, "fallback_value", PROP_FLOAT, PROP_NONE); + RNA_def_property_float_sdna(prop, nullptr, "fallback_value"); + RNA_def_property_ui_text( + prop, "Fallback", "The value to use if the data path can't be resolved"); + RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data"); + + prop = RNA_def_property(srna, "is_fallback_used", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", DTAR_FLAG_FALLBACK_USED); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text( + prop, + "Is Fallback Used", + "Indicates that the most recent variable evaluation used the fallback value"); } static void rna_def_drivervar(BlenderRNA *brna) diff --git a/source/blender/python/intern/bpy_rna_driver.cc b/source/blender/python/intern/bpy_rna_driver.cc index 33dab4b7f2d..8db248cb3cd 100644 --- a/source/blender/python/intern/bpy_rna_driver.cc +++ b/source/blender/python/intern/bpy_rna_driver.cc @@ -12,6 +12,8 @@ #include "MEM_guardedalloc.h" +#include "DNA_anim_types.h" + #include "BLI_utildefines.h" #include "BKE_fcurve_driver.h" @@ -55,6 +57,9 @@ PyObject *pyrna_driver_get_variable_value(const AnimationEvalContext *anim_eval_ /* object & property */ return pyrna_prop_to_py(&ptr, prop); + case DRIVER_VAR_PROPERTY_FALLBACK: + return PyFloat_FromDouble(dtar->fallback_value); + case DRIVER_VAR_PROPERTY_INVALID: case DRIVER_VAR_PROPERTY_INVALID_INDEX: /* can't resolve path, pass */ diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 203494e58c2..6df6b903a0c 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -364,6 +364,13 @@ add_blender_test( --python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_armature.py ) +add_blender_test( + bl_animation_drivers + --python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_drivers.py + -- + --testdir "${TEST_SRC_DIR}/animation" +) + add_blender_test( bl_animation_fcurves --python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_fcurves.py diff --git a/tests/python/bl_animation_drivers.py b/tests/python/bl_animation_drivers.py new file mode 100644 index 00000000000..caca840f194 --- /dev/null +++ b/tests/python/bl_animation_drivers.py @@ -0,0 +1,211 @@ +# SPDX-FileCopyrightText: 2020-2023 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import unittest +import bpy +import pathlib +import sys +from rna_prop_ui import rna_idprop_quote_path + +""" +blender -b -noaudio --factory-startup --python tests/python/bl_animation_drivers.py -- --testdir /path/to/lib/tests/animation +""" + + +class AbstractEmptyDriverTest: + def setUp(self): + super().setUp() + bpy.ops.wm.read_homefile(use_factory_startup=True) + self.obj = bpy.data.objects['Cube'] + + def assertPropValue(self, prop_name, value): + self.assertEqual(self.obj[prop_name], value) + + +def _make_context_driver(obj, prop_name, ctx_type, ctx_path, index=None, fallback=None, force_python=False): + obj[prop_name] = 0 + fcu = obj.driver_add(rna_idprop_quote_path(prop_name), -1) + drv = fcu.driver + + if force_python: + # Expression that requires full python interpreter + drv.type = 'SCRIPTED' + drv.expression = '[var][0]' + else: + drv.type = 'SUM' + + var = drv.variables.new() + var.name = "var" + var.type = 'CONTEXT_PROP' + tgt = var.targets[0] + tgt.context_property = ctx_type + tgt.data_path = rna_idprop_quote_path(ctx_path) + (f"[{index}]" if index is not None else "") + + if fallback is not None: + tgt.use_fallback_value = True + tgt.fallback_value = fallback + + return fcu + + +def _is_fallback_used(fcu): + return fcu.driver.variables[0].targets[0].is_fallback_used + + +class ContextSceneDriverTest(AbstractEmptyDriverTest, unittest.TestCase): + """ Ensure keying things by name or with a keying set adds the right keys. """ + + def setUp(self): + super().setUp() + bpy.context.scene["test_property"] = 123 + + def test_context_valid(self): + fcu = _make_context_driver( + self.obj, 'test_valid', 'ACTIVE_SCENE', 'test_property') + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertPropValue('test_valid', 123) + + def test_context_invalid(self): + fcu = _make_context_driver( + self.obj, 'test_invalid', 'ACTIVE_SCENE', 'test_property_bad') + bpy.context.view_layer.update() + self.assertFalse(fcu.driver.is_valid) + + def test_context_fallback(self): + fcu = _make_context_driver( + self.obj, 'test_fallback', 'ACTIVE_SCENE', 'test_property_bad', fallback=321) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertTrue(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback', 321) + + def test_context_fallback_valid(self): + fcu = _make_context_driver( + self.obj, 'test_fallback_valid', 'ACTIVE_SCENE', 'test_property', fallback=321) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertFalse(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback_valid', 123) + + def test_context_fallback_python(self): + fcu = _make_context_driver( + self.obj, 'test_fallback_py', 'ACTIVE_SCENE', 'test_property_bad', fallback=321, force_python=True) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertTrue(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback_py', 321) + + +class ContextSceneArrayDriverTest(AbstractEmptyDriverTest, unittest.TestCase): + """ Ensure keying things by name or with a keying set adds the right keys. """ + + def setUp(self): + super().setUp() + bpy.context.scene["test_property"] = [123, 456] + + def test_context_valid(self): + fcu = _make_context_driver( + self.obj, 'test_valid', 'ACTIVE_SCENE', 'test_property', index=0) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertPropValue('test_valid', 123) + + def test_context_invalid(self): + fcu = _make_context_driver( + self.obj, 'test_invalid', 'ACTIVE_SCENE', 'test_property', index=2) + bpy.context.view_layer.update() + self.assertFalse(fcu.driver.is_valid) + + def test_context_fallback(self): + fcu = _make_context_driver( + self.obj, 'test_fallback', 'ACTIVE_SCENE', 'test_property', index=2, fallback=321) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertTrue(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback', 321) + + def test_context_fallback_valid(self): + fcu = _make_context_driver( + self.obj, 'test_fallback_valid', 'ACTIVE_SCENE', 'test_property', index=0, fallback=321) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertFalse(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback_valid', 123) + + def test_context_fallback_python(self): + fcu = _make_context_driver( + self.obj, 'test_fallback_py', 'ACTIVE_SCENE', 'test_property', index=2, fallback=321, force_python=True) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertTrue(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback_py', 321) + + +def _select_view_layer(index): + bpy.context.window.view_layer = bpy.context.scene.view_layers[index] + + +class ContextViewLayerDriverTest(AbstractEmptyDriverTest, unittest.TestCase): + """ Ensure keying things by name or with a keying set adds the right keys. """ + + def setUp(self): + super().setUp() + bpy.ops.scene.view_layer_add(type='COPY') + scene = bpy.context.scene + scene.view_layers[0]['test_property'] = 123 + scene.view_layers[1]['test_property'] = 456 + _select_view_layer(0) + + def test_context_valid(self): + fcu = _make_context_driver( + self.obj, 'test_valid', 'ACTIVE_VIEW_LAYER', 'test_property') + + _select_view_layer(0) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertPropValue('test_valid', 123) + + _select_view_layer(1) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertPropValue('test_valid', 456) + + def test_context_fallback(self): + del bpy.context.scene.view_layers[1]['test_property'] + + fcu = _make_context_driver( + self.obj, 'test_fallback', 'ACTIVE_VIEW_LAYER', 'test_property', fallback=321) + + _select_view_layer(0) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertFalse(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback', 123) + + _select_view_layer(1) + bpy.context.view_layer.update() + self.assertTrue(fcu.driver.is_valid) + self.assertTrue(_is_fallback_used(fcu)) + self.assertPropValue('test_fallback', 321) + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main()