diff --git a/GUI/src/app/pages/generator/generator.component.html b/GUI/src/app/pages/generator/generator.component.html
index 11d2123557..9e401883db 100644
--- a/GUI/src/app/pages/generator/generator.component.html
+++ b/GUI/src/app/pages/generator/generator.component.html
@@ -13,17 +13,17 @@
{{section.text}}
0" class="subheaderLabel" [innerHTML]="section.subheader">
-
+
- 0 ? 'hint' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="clockwise">{{setting.text}}
- {{setting.text}}
- {{setting.text}}
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise">
{{option.text}}
@@ -32,8 +32,8 @@ {{section.text}}
- 0" class="comboBoxLabel" [ngClass]="{'disabled': !global.generator_settingsVisibilityMap[setting.name], 'oneLineComboBox': getColumnCount(refEl) > 2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)), 'oLCBLabelFirst': getColumnCount(refEl) > 2 && setting.no_line_break, 'oLCBLabelSecond': getColumnCount(refEl) > 2 && itemIndex > 0 && section.settings[itemIndex-1].no_line_break}">{{setting.text}} ({{global.generator_customColorMap[setting.name]}})
- 2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)), 'oLCBLabelFirst': getColumnCount(refEl) > 2 && setting.no_line_break, 'oLCBLabelSecond': getColumnCount(refEl) > 2 && itemIndex > 0 && section.settings[itemIndex-1].no_line_break}">{{setting.text}}
+ 0" class="comboBoxLabel" [ngClass]="{'disabled': !settingIsEnabled(setting.name), 'oneLineComboBox': getColumnCount(refEl) > 2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)), 'oLCBLabelFirst': getColumnCount(refEl) > 2 && setting.no_line_break, 'oLCBLabelSecond': getColumnCount(refEl) > 2 && itemIndex > 0 && section.settings[itemIndex-1].no_line_break}">{{setting.text}} ({{global.generator_customColorMap[setting.name]}})
+ 2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)), 'oLCBLabelFirst': getColumnCount(refEl) > 2 && setting.no_line_break, 'oLCBLabelSecond': getColumnCount(refEl) > 2 && itemIndex > 0 && section.settings[itemIndex-1].no_line_break}">{{setting.text}}
{{section.text}}
2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)), 'select-colors' : getColumnCount(refEl) === 1}"
- [disabled]="!global.generator_settingsVisibilityMap[setting.name]"
+ [disabled]="!settingIsEnabled(setting.name)"
[(selected)]="global.generator_settingsMap[setting.name]"
(selectedChange)="checkVisibility($event, setting, findOption(setting.options, $event), refColorPicker)"
[nbPopover]="tooltipComponent"
@@ -71,7 +71,7 @@ {{section.text}}
2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break))}"
- [disabled]="!global.generator_settingsVisibilityMap[setting.name]"
+ [disabled]="!settingIsEnabled(setting.name)"
[(selected)]="global.generator_settingsMap[setting.name]"
(selectedChange)="checkVisibility($event, setting, findOption(setting.options, $event))"
[nbPopover]="tooltipComponent"
@@ -96,7 +96,7 @@ {{section.text}}
2 && (setting.no_line_break || (itemIndex > 0 && section.settings[itemIndex-1].no_line_break)),
'oLCBLabelFirst': getColumnCount(refEl) > 2 && setting.no_line_break,
'oLCBLabelSecond': getColumnCount(refEl) > 2 && itemIndex > 0 && section.settings[itemIndex-1].no_line_break
@@ -106,7 +106,7 @@ {{section.text}}
2 && setting.no_line_break}"
- [disabled]="!global.generator_settingsVisibilityMap[setting.name]"
+ [disabled]="!settingIsEnabled(setting.name)"
[(selected)]="global.generator_settingsMap[setting.name]"
(selectedChange)="checkVisibility($event, setting, findOption(setting.options, $event))"
[nbPopover]="tooltipComponent"
@@ -130,95 +130,95 @@ {{section.text}}
- {{setting.text}}:
- {{setting.text}}
- {{setting.text}}:
+ {{setting.text}}
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="counterclockwise">
- 0 ? 'hint' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="counterclockwise">
- {{setting.text}}:
- {{setting.text}}
- {{setting.text}}:
+ {{setting.text}}
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="counterclockwise">
- {{setting.text}}
- {{setting.text}}
+ {{setting.text}}
+ {{setting.text}}
- {{setting.text}}:
- {{setting.text}}
- {{setting.text}}:
+ {{setting.text}}
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="counterclockwise">
-
+
- {{setting.text}}
+ {{setting.text}}
- 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise">
-
+
-
-
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise" ngfDrop [(file)]="global.generator_settingsMap[setting.name]">
-
-
+ 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise">
-
-
+
+
- {{setting.text}}
+ {{setting.text}}
- 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise">
-
+
- 0 ? 'hint' : 'noop'" nbPopoverPlacement="top" nbPopoverAdjustment="clockwise" (dragover)="onDirectoryDragOverWeb($event, setting)" (drop)="onDirectoryDropWeb($event, setting)">
-
+
-
+
-
+
{{section.text}}
[nbPopoverPlacement]="'top'"
[nbPopoverAdjustment]="'vertical'" />
-
-
-
+
+
+
- 0 ? setting.tags : null" [tooltipComponent]="tooltipComponent" tooltip="tooltip" [nbPopover]="tooltipComponent" [nbPopoverContext]="{tooltip: setting.tooltip}" [nbPopoverTrigger]="setting.tooltip && setting.tooltip.length > 0 ? 'click' : 'noop'" nbPopoverPlacement="right" nbPopoverAdjustment="counterclockwise">
diff --git a/GUI/src/app/pages/generator/generator.component.ts b/GUI/src/app/pages/generator/generator.component.ts
index 00814d2750..748a5f11a3 100644
--- a/GUI/src/app/pages/generator/generator.component.ts
+++ b/GUI/src/app/pages/generator/generator.component.ts
@@ -753,7 +753,7 @@ export class GeneratorComponent implements OnInit {
return;
//Check setting is enabled first
- if (!this.global.generator_settingsVisibilityMap[setting.name])
+ if (!this.settingIsEnabled(setting.name))
return;
event.dataTransfer.dropEffect = 'link'; //Change cursor to link icon when in input area
@@ -771,7 +771,7 @@ export class GeneratorComponent implements OnInit {
return;
//Check setting is enabled first
- if (!this.global.generator_settingsVisibilityMap[setting.name])
+ if (!this.settingIsEnabled(setting.name))
return;
let items = event.dataTransfer.items;
@@ -1075,13 +1075,29 @@ export class GeneratorComponent implements OnInit {
return typeof (variable);
}
+ settingIsEnabled(setting_name: string) {
+ return this.global.generator_settingsVisibilityMap[setting_name];
+ }
+
+ settingIsFullyHidden(setting: any) {
+ return !this.settingIsEnabled(setting.name) && setting.hide_when_disabled;
+ }
+
+ getSettingCurrentState(setting: any) {
+ return {
+ "value": this.global.generator_settingsMap[setting.name],
+ "visible": !this.settingIsFullyHidden(setting),
+ "enabled": this.settingIsEnabled(setting.name),
+ };
+ }
+
getNextVisibleSetting(settings: any, startingIndex: number) {
if (settings.length > startingIndex) {
for (let i = startingIndex; i < settings.length; i++) {
let setting = settings[i];
- if (this.global.generator_settingsVisibilityMap[setting.name] || !setting.hide_when_disabled)
+ if (!this.settingIsFullyHidden(setting))
return setting;
}
}
@@ -1182,7 +1198,7 @@ export class GeneratorComponent implements OnInit {
this.triggerTabVisibility(targetSetting, targetValue);
}
- if ("controls_visibility_setting" in targetSetting) {
+ if ("controls_visibility_setting" in targetSetting || 'conditionally_controls_setting' in targetSetting) {
triggeredChange = this.triggerSettingVisibility(targetSetting, targetValue, triggeredChange);
}
@@ -1248,11 +1264,11 @@ export class GeneratorComponent implements OnInit {
let enabledChildren = false;
//If a setting gets disabled, re-enable all the settings that this setting caused to deactivate. The later full check will fix any potential issues
- if (targetValue == false && this.global.generator_settingsVisibilityMap[setting.name] == true) {
+ if (targetValue == false && this.settingIsEnabled(setting.name) == true) {
enabledChildren = this.clearDeactivationsOfSetting(setting);
}
- if ((targetValue == true && this.global.generator_settingsVisibilityMap[setting.name] == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
+ if ((targetValue == true && this.settingIsEnabled(setting.name) == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
triggeredChange = true;
this.global.generator_settingsVisibilityMap[setting.name] = targetValue;
@@ -1263,7 +1279,19 @@ export class GeneratorComponent implements OnInit {
return triggeredChange;
}
+ // targetSetting = The current option of the setting to process.
+ // targetValue = 'true' if the settings this option controls should be enabled, 'false' if they should be disabled.
+ // (Note: This is passed in 'checkVisibility' as "option != value", in other words: "This option is NOT the option the setting is being changed to".)
+ // triggeredChange = Set to 'true' to force this function to return 'true', suggesting a change occurred regardless of how things processed.
+ // Otherwise, the function will return 'true' if a dependent setting's state was altered, otherwise it will return 'false'.
private triggerSettingVisibility(targetSetting: any, targetValue: boolean, triggeredChange: boolean) {
+ // Resolve logic that could conditionally update this setting.
+ let conditionalSettingUpdates = this.getConditionallyChangedSettingsForOption(targetSetting);
+
+ // NOTE: We are treating any setting under "controls_visibility_setting" as one
+ // that should be disabled by the current option. Could be worth renaming...
+ let settingsDisabled = []; // Setting names in here are being disabled and take priority over any changes made by conditional logic
+ if (targetSetting["controls_visibility_setting"] != null) {
targetSetting["controls_visibility_setting"].split(",").forEach(setting => {
//Ignore settings that don't exist in this specific app
@@ -1272,19 +1300,118 @@ export class GeneratorComponent implements OnInit {
let enabledChildren = false;
- if (targetValue == false && this.global.generator_settingsVisibilityMap[setting] == true) {
+ // We are about to disable this setting.
+ // If this is currently enabled, attempt to re-enable any settings that it
+ // may be disabling on its own. If it's disabled, it shouldn't also disable other settings.
+ if (targetValue == false && this.settingIsEnabled(setting)) {
enabledChildren = this.clearDeactivationsOfSetting(this.global.findSettingByName(setting));
+ settingsDisabled.push(setting);
}
- if ((targetValue == true && this.global.generator_settingsVisibilityMap[setting] == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
+ // We are about to enable this setting.
+ // If this setting is currently disabled, note that we are triggering a change.
+ // Alternatively, if disabling this setting causes any other settings to be
+ // enabled due to it being disabled, then also note that we are triggering a change.
+ if ((targetValue == true && !this.settingIsEnabled(setting)) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
triggeredChange = true;
+ // targetValue = true => This setting will be enabled.
+ // targetValue = false => This setting will be disabled.
this.global.generator_settingsVisibilityMap[setting] = targetValue;
});
+ }
+
+ // If a setting won't be forcibly disabled, allow conditions to update the setting
+ for (let settingName in conditionalSettingUpdates) {
+ if (!settingsDisabled.includes(settingName)) {
+ this.global.generator_settingsMap[settingName] = conditionalSettingUpdates[settingName]['value'];
+ this.global.generator_settingsVisibilityMap[settingName] = conditionalSettingUpdates[settingName]['enabled'];
+ // TODO: Revisit for 'visible' when/if the "visibility" and "enabled" logic are more decoupled and we have more direct control. (See 'settingIsEnabled' and 'settingIsFullyHidden')
+ triggeredChange = true;
+ }
+ }
return triggeredChange;
}
+ private getConditionallyChangedSettingsForOption(settingOption: any) {
+ let conditionalSettingUpdates = {};
+ if (settingOption["conditionally_controls_setting"] != null) {
+ settingOption["conditionally_controls_setting"].forEach(setting => {
+
+ let dependentSetting = this.global.findSettingByName(setting);
+ if (dependentSetting.conditional_controls != null) {
+ let targetSettingState = this.getTargetSettingStateFromConditions(dependentSetting);
+ let currentSettingState = this.getSettingCurrentState(dependentSetting);
+
+ // If any part of the setting would change, save the new setting state for later
+ if (currentSettingState['value'] != targetSettingState['value'] ||
+ currentSettingState['enabled'] != targetSettingState['enabled'] ||
+ currentSettingState['visible'] != targetSettingState['visible']
+ ) {
+ conditionalSettingUpdates[dependentSetting.name] = targetSettingState;
+ }
+ }
+ });
+ }
+ return conditionalSettingUpdates;
+ }
+
+
+ private getTargetSettingStateFromConditions(setting: any) {
+ // Start with the current state as the target
+ // If no conditions change the target state, then we effectively just return the current state
+ let targetSettingState = this.getSettingCurrentState(setting);
+
+ // There may be multiple combinations of conditions that may alter this setting.
+ // We'll check each one, and if one of them passes we'll use that to determine the setting's state.
+ let settingConditions = setting.conditional_controls;
+ let conditionHasDisabled = false;
+ for (let conditionName in settingConditions) {
+ var conditionToTest = settingConditions[conditionName];
+ let conditionList = conditionToTest['conditions'];
+ let conditionsPassed = [];
+ for (let i = 0; i < conditionList.length; i++) {
+ let condition = conditionList[i];
+ let partialConditionPassed = false;
+ // Only one of these conditional settings has to match the given value
+ for (let conditionalSettingName in condition) {
+ // If the conditional setting is currently set to the conditional value...
+ if (condition[conditionalSettingName] == this.global.generator_settingsMap[conditionalSettingName]) {
+ partialConditionPassed = true;
+ break;
+ }
+ }
+
+ conditionsPassed.push(partialConditionPassed);
+ };
+
+ // If one full condition passed, we'll use that condition's target state
+ if (!conditionsPassed.includes(false)) {
+ // TODO: Define priority rules so we know what should take precedent.
+ // - Option 1: First that passes has priority => just early exit
+ // - Option 2: Last that passes has priority => could result in mixed data sets if "condition1" sets some of the state and later "condition 3" sets other parts
+ // - Option 3: Manually define priority inside the blob => basically option 1 with extra logic. But what if two options have the same priority? First or last wins?
+ // If the condition sets one of these keys, we'll use that value. Otherwise use the current value.
+ targetSettingState['value'] = conditionToTest['value'] != null ? conditionToTest['value'] : targetSettingState['value'];
+ targetSettingState['enabled'] = conditionToTest['enabled'] != null ? conditionToTest['enabled'] : targetSettingState['enabled'];
+ targetSettingState['visible'] = conditionToTest['visible'] != null ? conditionToTest['visible'] : targetSettingState['visible'];
+ if (targetSettingState['enabled'] == false) {
+ conditionHasDisabled = true;
+ }
+ break; // First condition that passes wins and takes priority
+ }
+ }
+
+ // The setting is currently disabled, but no conditions are attempting to disable it.
+ // Let's re-enable it and the old "disable" logic can take priority if needed.
+ if (!conditionHasDisabled && targetSettingState['enabled'] == false) {
+ targetSettingState['enabled'] = true;
+ }
+
+ return targetSettingState;
+ }
+
clearDeactivationsOfSetting(setting: any) {
let enabledChildren = false;
@@ -1319,7 +1446,7 @@ export class GeneratorComponent implements OnInit {
this.global.getGlobalVar('generatorSettingsArray').forEach(tab => tab.sections.forEach(section => section.settings.forEach(checkSetting => {
- if (skipSetting && checkSetting.name === skipSetting || !this.global.generator_settingsVisibilityMap[checkSetting.name]) //Disabled settings can not alter visibility anymore
+ if (skipSetting && checkSetting.name === skipSetting || !this.settingIsEnabled(checkSetting.name)) //Disabled settings can not alter visibility anymore
return;
if (checkSetting["type"] === "Checkbutton" || checkSetting["type"] === "Radiobutton" || checkSetting["type"] === "Combobox" || checkSetting["type"] === "SearchBox") {
diff --git a/GUI/src/app/providers/GUIGlobal.ts b/GUI/src/app/providers/GUIGlobal.ts
index a6c82ca209..d7ceb1d527 100644
--- a/GUI/src/app/providers/GUIGlobal.ts
+++ b/GUI/src/app/providers/GUIGlobal.ts
@@ -469,6 +469,15 @@ export class GUIGlobal implements OnDestroy {
async parseGeneratorGUISettings(guiSettings, userSettings) {
const isRGBHex = /[0-9A-Fa-f]{6}/;
+ /*
+ !! DEBUGGING ONLY !!
+ This value can be accessed via the browser console as simply 'SettingsListZOOTR_Angular'
+ and is only intended for debugging. The current list of settings as they appear in the UI
+ will be accessible via this variable. Their values will be objects representing their current
+ state and metadata as is relevant, including a direct JSON-decoded object of that settings'
+ data from the generated settings list JSON file.
+ */
+ globalThis.SettingsListZOOTR_Angular = {};
//Intialize settings maps
for (let tabIndex = 0; tabIndex < guiSettings.settingsArray.length; tabIndex++) {
@@ -528,6 +537,20 @@ export class GUIGlobal implements OnDestroy {
this.generator_settingsVisibilityMap[setting.name] = true;
+ // Bind a property as a function that returns an object representing this setting for easy debugging
+ // By using the setting name as the property name, it allows for auto-complete and fuzzy searching/suggestions
+ // This works by binding a property to a getter function that has the current 'this' value bound to the function context
+ Object.defineProperty(globalThis.SettingsListZOOTR_Angular, setting.name, {
+ get: () => {
+ return {
+ // Object representing the current state of this setting. Add more values as you see fit.
+ enabled: this.generator_settingsVisibilityMap[setting.name],
+ value: this.generator_settingsMap[setting.name],
+ _json: this.findSettingByName(setting.name),
+ };
+ },
+ });
+
if (setting.type == "SearchBox" && userSettings && setting.name in userSettings) { //Special parsing for SearchBox data
let valueArray = [];
diff --git a/Notes/GUI/architecture.md b/Notes/GUI/architecture.md
index b1e8850da3..cd8e27f831 100644
--- a/Notes/GUI/architecture.md
+++ b/Notes/GUI/architecture.md
@@ -49,6 +49,12 @@ utils/settings_list.json contains every single setting of the GUI as an array of
4. controls-visibility-tab: What tab(s) to disable when this option is selected. Multiple tabs can be separated by comma and are addressed by their internal name (see mapping.json structure)
5. controls-visibility-section: What section(s) to disable when this option is selected
6. controls-visibility-setting: What specific setting(s) to disable when this option is selected
+* conditional-controls → An object of "conditions" that can alter this setting's visibility, enabled state, and value based on the state of other settings. This object can contain multiple items that each define their own conditions and target state for the setting. The first condition that passes will take priority in altering the setting and the other conditions will not be evaluated. If this setting would be disabled by some other logic (eg. 'controls_visibility_setting'), that will take priority over ALL condition-based logic. When this happens, the setting state will not be changed even if a passing condition would normally do so. The format of a condition object is as follows:
+ * "key" -> {object}: "key" has no functional purpose and is purely for human readability and debugging purposes. "object" contains key/value pairs that define the behavior of the condition.
+ * value: If the condition passes, the setting will be changed to this value.
+ * enabled: If the condition passes, `True` will enable the setting and `False` will disable it.
+ * conditions: A list of "partial condition" objects that determine if this condition passes or not. All "partial conditions" must pass for the full condition to also pass. (This provides `AND` logic)
+ * Each partial condition contains "key" -> "value" pairs in the format of "setting_name" -> "setting_value". If at least one of these pairs matches the current state of the settings, the partial condition will pass. Otherwise it will fail. (This provides `OR` logic)
### The settings_mapping.json structure
@@ -81,6 +87,7 @@ The settings array follows that defines the settings that should appear in this
* controls-visibility-tab → What tab(s) to disable when this setting is enabled, used for Checkbuttons. Multiple tabs can be separated by comma and are addressed by their internal name
* controls-visibility-section → What section(s) to disable when this setting is enabled
* controls-visibility-setting → What specific setting(s) to disable when this setting is enabled
+* conditional-controls → List of setting/value pairs this setting may be dependent on to determine what its current state should be (eg. disabled, specific value, etc.)
* hide-when-disabled → If this setting should be completely hidden when it gets disabled, not just greyed out. Used on the website to make the difference between generator and patcher more distinct
* min → The minimum numeric value allowed. Used for Scales and Numberinputs
* max → The maximum numeric value allowed. Used for Scales and Numberinputs. Can differ between Electron and website (e.g. multi world limit)
diff --git a/SettingTypes.py b/SettingTypes.py
index 9a6ac734b2..53f2f0ed83 100644
--- a/SettingTypes.py
+++ b/SettingTypes.py
@@ -10,7 +10,7 @@
class SettingInfo:
def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Optional[str], shared: bool,
choices: Optional[dict | list] = None, default: Any = None, disabled_default: Any = None,
- disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None,
+ disable: Optional[dict] = None, conditional_controls: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None,
cosmetic: bool = False) -> None:
self.type: type = setting_type # type of the setting's value, used to properly convert types to setting strings
self.shared: bool = shared # whether the setting is one that should be shared, used in converting settings to a string
@@ -19,6 +19,7 @@ def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Option
self.gui_type: Optional[str] = gui_type
self.gui_tooltip: Optional[str] = "" if gui_tooltip is None else gui_tooltip
self.gui_params: dict[str, Any] = {} if gui_params is None else gui_params # additional parameters that the randomizer uses for the gui
+ self.conditional_controls: Optional[dict] = conditional_controls # dictionary of settings and values that can enable this setting
self.disable: Optional[dict] = disable # dictionary of settings this setting disabled
self.dependency = None # lambda that determines if this is disabled. Generated later
@@ -100,10 +101,10 @@ def create_dependency(self, disabling_setting: 'SettingInfo', option, negative:
class SettingInfoNone(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], gui_tooltip: Optional[str] = None,
- gui_params: Optional[dict] = None) -> None:
+ gui_params: Optional[dict] = None, conditional_controls: Optional[dict] = None) -> None:
super().__init__(setting_type=type(None), gui_text=gui_text, gui_type=gui_type, shared=False, choices=None,
- default=None, disabled_default=None, disable=None, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=False)
+ default=None, disabled_default=None, conditional_controls=conditional_controls,
+ disable=None, gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=False)
def __get__(self, obj, obj_type=None) -> None:
raise Exception(f"{self.name} is not a setting and cannot be retrieved.")
@@ -114,16 +115,16 @@ def __set__(self, obj, value: str) -> None:
class SettingInfoBool(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, default: Optional[bool] = None,
- disabled_default: Optional[bool] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None,
- gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ disabled_default: Optional[bool] = None, disable: Optional[dict] = None, conditional_controls: Optional[dict] = None,
+ gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
choices = {
True: 'checked',
False: 'unchecked',
}
super().__init__(setting_type=bool, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices,
- default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ default=default, disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
def __get__(self, obj, obj_type=None) -> bool:
value = super().__get__(obj, obj_type)
@@ -140,11 +141,11 @@ def __set__(self, obj, value: bool) -> None:
class SettingInfoStr(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool = False,
choices: Optional[dict | list] = None, default: Optional[str] = None,
- disabled_default: Optional[str] = None, disable: Optional[dict] = None,
+ disabled_default: Optional[str] = None, disable: Optional[dict] = None, conditional_controls: Optional[dict] = None,
gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(setting_type=str, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices,
- default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ default=default, disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
def __get__(self, obj, obj_type=None) -> str:
value = super().__get__(obj, obj_type)
@@ -161,11 +162,11 @@ def __set__(self, obj, value: str) -> None:
class SettingInfoInt(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool,
choices: Optional[dict | list] = None, default: Optional[int] = None,
- disabled_default: Optional[int] = None, disable: Optional[dict] = None,
+ disabled_default: Optional[int] = None, disable: Optional[dict] = None, conditional_controls: Optional[dict] = None,
gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(setting_type=int, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices,
- default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ default=default, disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
def __get__(self, obj, obj_type=None) -> int:
value = super().__get__(obj, obj_type)
@@ -182,11 +183,11 @@ def __set__(self, obj, value: int) -> None:
class SettingInfoList(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool,
choices: Optional[dict | list] = None, default: Optional[list] = None,
- disabled_default: Optional[list] = None, disable: Optional[dict] = None,
+ disabled_default: Optional[list] = None, disable: Optional[dict] = None, conditional_controls: Optional[dict] = None,
gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(setting_type=list, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices,
- default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ default=default, disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
def __get__(self, obj, obj_type=None) -> list:
value = super().__get__(obj, obj_type)
@@ -203,11 +204,11 @@ def __set__(self, obj, value: list) -> None:
class SettingInfoDict(SettingInfo):
def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool,
choices: Optional[dict | list] = None, default: Optional[dict] = None,
- disabled_default: Optional[dict] = None, disable: Optional[dict] = None,
+ disabled_default: Optional[dict] = None, disable: Optional[dict] = None, conditional_controls: Optional[dict] = None,
gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(setting_type=dict, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices,
- default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ default=default, disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
def __get__(self, obj, obj_type=None) -> dict:
value = super().__get__(obj, obj_type)
@@ -223,83 +224,83 @@ def __set__(self, obj, value: dict) -> None:
class Button(SettingInfoNone):
def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None,
- gui_params: Optional[dict] = None) -> None:
- super().__init__(gui_text=gui_text, gui_type="Button", gui_tooltip=gui_tooltip, gui_params=gui_params)
+ gui_params: Optional[dict] = None, conditional_controls: Optional[dict] = None) -> None:
+ super().__init__(gui_text=gui_text, gui_type="Button", gui_tooltip=gui_tooltip, gui_params=gui_params, conditional_controls=conditional_controls)
class Textbox(SettingInfoNone):
def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None,
- gui_params: Optional[dict] = None) -> None:
- super().__init__(gui_text=gui_text, gui_type="Textbox", gui_tooltip=gui_tooltip, gui_params=gui_params)
+ gui_params: Optional[dict] = None, conditional_controls: Optional[dict] = None) -> None:
+ super().__init__(gui_text=gui_text, gui_type="Textbox", gui_tooltip=gui_tooltip, gui_params=gui_params, conditional_controls=conditional_controls)
class Checkbutton(SettingInfoBool):
def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None,
- disabled_default: Optional[bool] = None, default: bool = False, shared: bool = False,
- gui_params: Optional[dict] = None, cosmetic: bool = False):
+ disabled_default: Optional[bool] = None, conditional_controls: Optional[dict] = None, default: bool = False,
+ shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False):
super().__init__(gui_text=gui_text, gui_type='Checkbutton', shared=shared, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Combobox(SettingInfoStr):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str],
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Radiobutton(SettingInfoStr):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str],
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Radiobutton', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Fileinput(SettingInfoStr):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None,
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Fileinput', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Directoryinput(SettingInfoStr):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None,
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Directoryinput', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Textinput(SettingInfoStr):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None,
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Textinput', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class ComboboxInt(SettingInfoInt):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[int],
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Scale(SettingInfoInt):
def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: int, maximum: int, step: int = 1,
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
choices = {
i: str(i) for i in range(minimum, maximum+1, step)
}
@@ -311,15 +312,15 @@ def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: int
gui_params['step'] = step
super().__init__(gui_text=gui_text, gui_type='Scale', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class Numberinput(SettingInfoInt):
def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: Optional[int] = None,
maximum: Optional[int] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None,
- disabled_default: Optional[int] = None, shared: bool = False, gui_params: Optional[dict] = None,
- cosmetic: bool = False) -> None:
+ disabled_default: Optional[int] = None, conditional_controls: Optional[dict] = None,
+ shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
if gui_params is None:
gui_params = {}
if minimum is not None:
@@ -328,23 +329,25 @@ def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: Opt
gui_params['max'] = maximum
super().__init__(gui_text=gui_text, gui_type='Numberinput', shared=shared, choices=None, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class MultipleSelect(SettingInfoList):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list],
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None,
+ cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='MultipleSelect', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
class SearchBox(SettingInfoList):
def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list],
gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None,
- shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None:
+ conditional_controls: Optional[dict] = None, shared: bool = False, gui_params: Optional[dict] = None,
+ cosmetic: bool = False) -> None:
super().__init__(gui_text=gui_text, gui_type='SearchBox', shared=shared, choices=choices, default=default,
- disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip,
- gui_params=gui_params, cosmetic=cosmetic)
+ disabled_default=disabled_default, disable=disable, conditional_controls=conditional_controls,
+ gui_tooltip=gui_tooltip, gui_params=gui_params, cosmetic=cosmetic)
diff --git a/SettingsList.py b/SettingsList.py
index 0b556c85a2..786f1225f2 100644
--- a/SettingsList.py
+++ b/SettingsList.py
@@ -5642,9 +5642,9 @@ class UnmappedSettingError(Exception):
if info.disable is not None:
for option, disabling in info.disable.items():
- negative = False
+ negative = False # If this option is enabled, the "disabling" settings will be disabled
if isinstance(option, str) and option[0] == '!':
- negative = True
+ negative = True # If this option is NOT enabled, the "disabling" settings will be disabled
option = option[1:]
for setting_name in disabling.get('settings', []):
SettingInfos.setting_infos[setting_name].create_dependency(info, option, negative)
diff --git a/SettingsToJson.py b/SettingsToJson.py
index 0dddae3ef0..e6423beca8 100755
--- a/SettingsToJson.py
+++ b/SettingsToJson.py
@@ -15,6 +15,7 @@
setting_keys: list[str] = ['hide_when_disabled', 'min', 'max', 'size', 'max_length', 'file_types', 'no_line_break', 'function', 'option_remove', 'dynamic']
types_with_options: list[str] = ['Checkbutton', 'Radiobutton', 'Combobox', 'SearchBox', 'MultipleSelect']
+conditional_control_dependencies: dict[str, dict] = {}
def remove_trailing_lines(text: str) -> str:
while text.endswith('
'):
@@ -53,6 +54,11 @@ def add_disable_option_to_json(disable_option: dict[str, Any], option_json: dict
option_json['controls_visibility_tab'] += ',' + ','.join(disable_option['tabs'])
+def mark_conditional_control_option_for_setting(setting_name: str, conditional_settings: dict[str, Any]) -> None:
+ if setting_name not in conditional_control_dependencies:
+ conditional_control_dependencies[setting_name] = conditional_settings
+
+
def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> Optional[dict[str, Any]]:
try:
setting_info = SettingInfos.setting_infos[setting]
@@ -83,6 +89,11 @@ def get_setting_json(setting: str, web_version: bool, as_array: bool = False) ->
if setting_info.disable is not None:
setting_disable = copy.deepcopy(setting_info.disable)
+ # If this setting can be conditionally enabled, we will need to revisit it once the full JSON has been built
+ if setting_info.conditional_controls is not None:
+ mark_conditional_control_option_for_setting(setting_info.name, setting_info.conditional_controls)
+ setting_json['conditional_controls'] = setting_info.conditional_controls
+
version_specific_keys = []
for key, value in setting_info.gui_params.items():
@@ -260,6 +271,9 @@ def create_settings_list_json(path: str, web_version: bool = False) -> None:
output_json['cosmeticsObj'][tab['name']] = tab_json_object
output_json['cosmeticsArray'].append(tab_json_array)
+ # Resolve conditional visibility settings now that the full json is available
+ resolve_conditional_control_dependencies(output_json)
+
for d in hint_dist_files():
with open(d, 'r') as dist_file:
dist = json.load(dist_file)
@@ -273,6 +287,51 @@ def create_settings_list_json(path: str, web_version: bool = False) -> None:
json.dump(output_json, f)
+def resolve_conditional_control_dependencies(output_json: dict) -> None:
+ for dependent_setting_name, dependent_conditions in conditional_control_dependencies.items():
+ for condition_name, condition_details in dependent_conditions.items():
+ for conditional_settings in condition_details['conditions']:
+ for setting_name, setting_value in conditional_settings.items():
+ # Handle the setting in the "object" portion of the json
+ setting_obj_json = find_setting_obj_json_in_output(setting_name, output_json)
+ if setting_obj_json is not None:
+ setting_obj_option_json = setting_obj_json['options'][setting_value]
+ if setting_obj_option_json is not None:
+ if 'conditionally_controls_setting' not in setting_obj_option_json:
+ setting_obj_option_json['conditionally_controls_setting'] = [dependent_setting_name]
+ elif dependent_setting_name not in setting_obj_option_json['conditionally_controls_setting']:
+ setting_obj_option_json['conditionally_controls_setting'].append(dependent_setting_name)
+
+ # Handle the setting in the "array" portion of the json
+ setting_array_json = find_setting_array_json_in_output(setting_name, output_json)
+ if setting_array_json is not None:
+ for setting_array_option_json in setting_array_json['options']:
+ if setting_array_option_json['name'] is setting_value:
+ if 'conditionally_controls_setting' not in setting_array_option_json:
+ setting_array_option_json['conditionally_controls_setting'] = [dependent_setting_name]
+ elif dependent_setting_name not in setting_array_option_json['conditionally_controls_setting']:
+ setting_array_option_json['conditionally_controls_setting'].append(dependent_setting_name)
+ break # bail early(...bad idea?)
+
+
+def find_setting_obj_json_in_output(setting_name: str, output_json: dict[str, dict[str, dict]]) -> dict:
+ for setting_tab_name, setting_tab_json in output_json['settingsObj'].items():
+ for setting_section_name, setting_section_json in setting_tab_json['sections'].items():
+ setting_json = setting_section_json['settings'].get(setting_name)
+ if setting_json is not None:
+ return setting_json
+ return None
+
+
+def find_setting_array_json_in_output(setting_name: str, output_json: dict[str, dict[str, dict]]) -> dict:
+ for setting_tab_json in output_json['settingsArray']:
+ for setting_section_json in setting_tab_json['sections']:
+ for setting_json in setting_section_json['settings']:
+ if setting_name == setting_json['name']:
+ return setting_json
+ return None
+
+
def get_setting_details(setting_key: str, web_version: bool) -> None:
setting_json_object = get_setting_json(setting_key, web_version, as_array=False)
setting_json_array = get_setting_json(setting_key, web_version, as_array=True)