|
| 1 | +# Feature Flags |
| 2 | + |
| 3 | +NVDA makes judicious use of feature flags to enable and disable features that are in early development. |
| 4 | +The following are provided to streamline the creation of new feature flags: |
| 5 | +- A config spec type |
| 6 | +- A GUI control type |
| 7 | + |
| 8 | +## Background |
| 9 | +When providing a feature flag it is important to understand the importance of providing a "default" state. |
| 10 | +A boolean feature, must have 3 states selectable by the user: |
| 11 | +- `True` |
| 12 | +- `False` |
| 13 | +- `Default` (NVDA developer recommendation) |
| 14 | + |
| 15 | +This allows a choice between the following use-cases to be made at any point in time: |
| 16 | +- **Explicitly opt-in** to the feature, regardless of the default behavior. |
| 17 | +An early adopter may choose to do this to test the feature and provide feedback. |
| 18 | +- **Explicitly opt-out** of the feature, regardless of the default behavior. |
| 19 | +A user may find the pre-existing behavior acceptable, and wants the maximum delay to adopt the new feature. |
| 20 | +They may be prioritising stability, or anticipating this feature flag receives a permanent home in NVDA settings. |
| 21 | +- **Explicitly choose the default** (NVDA developer recommended) behavior. |
| 22 | +Noting, that in this case it is important that the user must be able to select one of the other options first, |
| 23 | +and return to the default behavior at any point in time. |
| 24 | + |
| 25 | +This should be possible while still allowing developers to change the behaviour |
| 26 | +of the default option. |
| 27 | +The development process might require that initially NVDA is released with |
| 28 | +the feature disabled by default. |
| 29 | +In this case only testers, or the most curious users are expected to opt-in temporarily. |
| 30 | +As the feature improves, bugs are fixed, edge cases are handled, and the UX is improved, |
| 31 | +developers may wish to change the behavior of the default option to enable the feature. |
| 32 | +This change shouldn't affect those who have already explicitly opted out of the feature. |
| 33 | +Only those who maybe haven't tried the feature, because they were using the prior default behaviour, or |
| 34 | +those who have tried and found the feature to be unstable and decided they would wait for it to become |
| 35 | +stable. |
| 36 | + |
| 37 | +## Feature Flag Enum |
| 38 | +To aid static typing in NVDA, `enum` classes are used. |
| 39 | +`BoolFlag` is provided, the majority of feature flags are expected to use this. |
| 40 | +However, if more values are required (E.G. `AllowUiaInMSWord` has options `WHEN_NECESSARY`, `WHERE_SUITABLE`, `ALWAYS`, in addition to `DEFAULT`), then a new `enum` class can be defined. |
| 41 | +Adding the enum class to the `featureFlagEnums.py` file will automatically expose it for use in the config spec (see the next section). |
| 42 | +Example new `enum` class: |
| 43 | + |
| 44 | +```python |
| 45 | + |
| 46 | +class AllowUiaInMSWordFlag(DisplayStringEnum): |
| 47 | + """Feature flag for UIA in MS Word. |
| 48 | + The explicit DEFAULT option allows developers to differentiate between a value set that happens to be |
| 49 | + the current default, and a value that has been returned to the "default" explicitly. |
| 50 | + """ |
| 51 | + |
| 52 | + @property |
| 53 | + def _displayStringLabels(self): |
| 54 | + """ These labels will be used in the GUI when displaying the options. |
| 55 | + """ |
| 56 | + # To prevent duplication, self.DEFAULT is not included here. |
| 57 | + return { |
| 58 | + # Translators: Label for an option in NVDA settings. |
| 59 | + self.WHEN_NECESSARY: _("Only when necessary"), |
| 60 | + # Translators: Label for an option in NVDA settings. |
| 61 | + self.WHERE_SUITABLE: _("Where suitable"), |
| 62 | + # Translators: Label for an option in NVDA settings. |
| 63 | + self.ALWAYS: _("ALWAYS"), |
| 64 | + } |
| 65 | + |
| 66 | + DEFAULT = enum.auto() |
| 67 | + WHEN_NECESSARY = enum.auto() |
| 68 | + WHERE_SUITABLE = enum.auto() |
| 69 | + ALWAYS = enum.auto() |
| 70 | +``` |
| 71 | + |
| 72 | +## Config Spec |
| 73 | +In `configSpec.py` specify the new config key, ideally in the category that is most relevant to the feature. |
| 74 | +Placing it in a category rather than a catch-all feature flags category, allows for the option to become |
| 75 | +permanent without having to write config upgrade code to move it from section to another. |
| 76 | + |
| 77 | +```ini |
| 78 | +[virtualBuffers] |
| 79 | + newOptionForUsers = featureFlag(optionsEnum="BoolFlag", behaviourOfDefault="disabled") |
| 80 | + anotherOptionForUsers = featureFlag(optionsEnum="AllowUiaInMSWordFlag", behaviourOfDefault="WHERE_SUITABLE") |
| 81 | +``` |
| 82 | + |
| 83 | +The `featureFlag` type is a custom spec type. |
| 84 | +It will produce a `config.FeatureFlag` class instance when the key is accessed. |
| 85 | +```python |
| 86 | +newFlagValue: config.FeatureFlag = config.conf["virtualBuffers"]["newOptionForUsers"] |
| 87 | + |
| 88 | +# BoolFlag converts to bool automatically, taking into account 'behaviorOfDefault' |
| 89 | +if newFlagValue: |
| 90 | + print("The new option is enabled") |
| 91 | + |
| 92 | +anotherFlagValue: config.FeatureFlag = config.conf["virtualBuffers"]["anotherOptionForUsers"] |
| 93 | + |
| 94 | +# Other "optionsEnum" types can compare with the value, the 'behaviorOfDefault' is taken into account. |
| 95 | +if flagValue == AllowUiaInMSWordFlag.ALWAYS: |
| 96 | + print("Another option is enabled") |
| 97 | +``` |
| 98 | + |
| 99 | +## GUI |
| 100 | +A control (`gui.nvdaControls.FeatureFlagCombo`) is provided to simplify exposing the feature flag to the user. |
| 101 | + |
| 102 | +### Usage: |
| 103 | +Note the comments in the example: |
| 104 | +- `creation` |
| 105 | +- `is default` |
| 106 | +- `reset to default value` |
| 107 | +- `save GUI value to config` |
| 108 | + |
| 109 | + |
| 110 | +```python |
| 111 | +import collections |
| 112 | +from gui import nvdaControls, guiHelper |
| 113 | +import config |
| 114 | +import wx |
| 115 | + |
| 116 | +sHelper = guiHelper.BoxSizerHelper(self, sizer=wx.BoxSizer(wx.HORIZONTAL)) |
| 117 | + |
| 118 | +# Translators: Explanation for the group name |
| 119 | +label = _("Virtual Buffers") |
| 120 | +vbufSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) |
| 121 | +vbufGroup = guiHelper.BoxSizerHelper(vbufSizer, sizer=vbufSizer) |
| 122 | +sHelper.addItem(vbufGroup) |
| 123 | + |
| 124 | +# creation |
| 125 | +self.newOptionForUsersCombo: nvdaControls.FeatureFlagCombo = vbufGroup.addLabeledControl( |
| 126 | + labelText=_( |
| 127 | + # Translators: Explanation of what the control does and where it is used. |
| 128 | + "New option for users" |
| 129 | + ), |
| 130 | + wxCtrlClass=nvdaControls.FeatureFlagCombo, |
| 131 | + keyPath=["virtualBuffers", "newOptionForUsers"], # The path of keys, see config spec. |
| 132 | + conf=config.conf, # The configObj instance, allows getting / setting the value |
| 133 | +) |
| 134 | +... |
| 135 | +# is default |
| 136 | +# Checking if the user has a saved (non-default) value |
| 137 | +self.loadChromeVbufWhenBusyCombo.isValueConfigSpecDefault() |
| 138 | +... |
| 139 | +# reset to default value: |
| 140 | +self.loadChromeVbufWhenBusyCombo.resetToConfigSpecDefault() |
| 141 | +... |
| 142 | +# save GUI value to config: |
| 143 | +self.loadChromeVbufWhenBusyCombo.saveCurrentValueToConf() |
| 144 | +``` |
| 145 | + |
| 146 | +## User Guide Documentation |
| 147 | +Refer to [User Guide Standards](./userGuideStandards.md#feature-settings) |
0 commit comments