import "pkg:/source/api/ApiClient.bs"
import "pkg:/source/api/apiPool.bs"
import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/deviceCapabilities.bs"
import "pkg:/source/utils/globals.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/translate.bs"
import "pkg:/source/utils/translateLocale.bs"
sub init()
m.log = new log.Logger("Settings")
m.top.overhangTitle = translate(translationKeys.LabelSettings)
m.top.isOptionsAvailable = false
m.userLocation = []
m.themeColorsChanged = false
m.languageChanged = false
m.customColorBackup = invalid
' Load available languages for the language picker
languagesJson = ReadAsciiFile("pkg:/locale/languages.json")
m.availableLanguages = ParseJson(languagesJson)
if not isValid(m.availableLanguages) then m.availableLanguages = []
' Normalize language entries so downstream code can safely access all fields
for each lang in m.availableLanguages
if not isValid(lang.name) then lang.name = ""
if not isValid(lang.nativeName) then lang.nativeName = ""
if not isValid(lang.code) then lang.code = ""
end for
versionLabel = m.top.findNode("versionLabel")
versionLabel.text = "v" + m.global.app.version
m.settingsMenu = m.top.findNode("settingsMenu")
m.settingDetail = m.top.findNode("settingDetail")
m.settingDesc = m.top.findNode("settingDesc")
m.path = m.top.findNode("path")
m.top.findNode("rightPanelBackground").blendColor = m.global.constants.colorBackgroundSecondary
m.boolSetting = m.top.findNode("boolSetting")
m.integerSetting = m.top.findNode("integerSetting")
m.radioSetting = m.top.findNode("radioSetting")
m.hexSetting = m.top.findNode("hexSetting")
m.alphaSetting = m.top.findNode("alphaSetting")
m.languagePicker = m.top.findNode("languagePicker")
m.integerSetting.observeField("submit", "onKeyGridSubmit")
m.integerSetting.observeField("escape", "onKeyGridEscape")
m.hexSetting.observeField("submit", "onHexKeyGridSubmit")
m.hexSetting.observeField("escape", "onHexKeyGridEscape")
m.hexSetting.observeField("reset", "onHexResetRequested")
m.alphaSetting.observeField("submit", "onAlphaKeyGridSubmit")
m.alphaSetting.observeField("escape", "onAlphaKeyGridEscape")
m.alphaSetting.observeField("reset", "onAlphaResetRequested")
m.languagePicker.observeField("selectedCode", "onLanguagePickerSelected")
m.languagePicker.languages = m.availableLanguages
m.suppressLanguagePicker = false
m.settingsMenu.setFocus(true)
m.settingsMenu.observeField("itemFocused", "settingFocused")
m.settingsMenu.observeField("itemSelected", "settingSelected")
m.boolSetting.observeField("checkedItem", "boolSettingChanged")
m.radioSetting.observeField("checkedItem", "radioSettingChanged")
' Load Configuration Tree
m.configTree = GetConfigTree()
LoadMenu({ children: m.configTree })
end sub
sub onKeyGridSubmit()
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
newValue = m.integerSetting.text
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, newValue)
m.settingsMenu.setFocus(true)
end sub
sub onKeyGridEscape()
if m.integerSetting.escape = "left" or m.integerSetting.escape = "back"
m.settingsMenu.setFocus(true)
end if
end sub
sub onHexKeyGridSubmit()
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
newValue = UCase(m.hexSetting.text)
' Validate hex color input
if not isValidHexColor(newValue)
m.global.sceneManager.callFunc("userMessage", translate(translationKeys.ErrorInvalidColor), translate(translationKeys.LabelPleaseEnterExactly6HexCharacters09A))
return
end if
' Track if theme color changed (requires UI refresh on exit)
if selectedSetting.settingName.left(12) = "uiThemeColor"
m.themeColorsChanged = true
end if
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, newValue)
m.settingsMenu.setFocus(true)
end sub
sub onHexKeyGridEscape()
if m.hexSetting.escape = "left" or m.hexSetting.escape = "back"
m.settingsMenu.setFocus(true)
end if
end sub
' Called when reset button is selected in HexKeyboard
sub onHexResetRequested()
m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelResetSetting), translate(translationKeys.MessageAreYouSureThisWillReset), [translate(translationKeys.ButtonCancel), translate(translationKeys.ButtonReset)])
m.global.sceneManager.observeFieldScoped("isDataReturned", "onResetDialogResponse")
end sub
' Handle user response from reset confirmation dialog
sub onResetDialogResponse()
m.global.sceneManager.unobserveFieldScoped("isDataReturned")
returnData = m.global.sceneManager.returnData
if not isValid(returnData) then return
' User selected "Cancel" or closed dialog
if returnData.indexSelected <> 1
m.hexSetting.findNode("resetButton").setFocus(true)
return
end if
' User confirmed reset - get default value from config tree
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
configEntry = findConfigTreeKey(selectedSetting.settingName, m.configTree)
if not isValid(configEntry) or not isValid(configEntry.default)
m.log.warn("Could not find default value for setting", selectedSetting.settingName)
m.hexSetting.findNode("resetButton").setFocus(true)
return
end if
defaultValue = configEntry.default
' Track if theme color changed (requires UI refresh on exit)
if selectedSetting.settingName.left(12) = "uiThemeColor"
m.themeColorsChanged = true
end if
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, defaultValue)
' Update the UI
m.hexSetting.text = defaultValue
m.hexSetting.findNode("resetButton").setFocus(true)
end sub
sub onAlphaKeyGridSubmit()
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
newValue = LCase(m.alphaSetting.text)
' Validate alpha input is exactly 3 letters (a-z only)
isValidCode = Len(newValue) = 3
if isValidCode
for i = 0 to 2
charCode = Asc(Mid(newValue, i + 1, 1))
if charCode < 97 or charCode > 122
isValidCode = false
exit for
end if
end for
end if
if not isValidCode
m.global.sceneManager.callFunc("userMessage", translate(translationKeys.ErrorInvalidLanguageCode), translate(translationKeys.MessagePleaseEnterExactly3LettersEG))
return
end if
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, newValue)
m.settingsMenu.setFocus(true)
end sub
sub onAlphaKeyGridEscape()
if m.alphaSetting.escape = "left" or m.alphaSetting.escape = "back"
m.settingsMenu.setFocus(true)
end if
end sub
' Called when reset button is selected in AlphaKeyboard
sub onAlphaResetRequested()
m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelResetSetting), translate(translationKeys.MessageAreYouSureThisWillReset), [translate(translationKeys.ButtonCancel), translate(translationKeys.ButtonReset)])
m.global.sceneManager.observeFieldScoped("isDataReturned", "onAlphaResetDialogResponse")
end sub
' Handle user response from alpha reset confirmation dialog
sub onAlphaResetDialogResponse()
m.global.sceneManager.unobserveFieldScoped("isDataReturned")
returnData = m.global.sceneManager.returnData
if not isValid(returnData) then return
' User selected "Cancel" or closed dialog
if returnData.indexSelected <> 1
m.alphaSetting.findNode("resetButton").setFocus(true)
return
end if
' User confirmed reset - get default value from config tree
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
configEntry = findConfigTreeKey(selectedSetting.settingName, m.configTree)
if not isValid(configEntry) or not isValid(configEntry.default)
m.log.warn("Could not find default value for setting", selectedSetting.settingName)
m.alphaSetting.findNode("resetButton").setFocus(true)
return
end if
defaultValue = configEntry.default
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, defaultValue)
' Update the UI
m.alphaSetting.text = defaultValue
m.alphaSetting.findNode("resetButton").setFocus(true)
end sub
sub LoadMenu(configSection)
if not isValid(configSection.children)
' Load parent menu
m.userLocation.pop()
configSection = m.userLocation.peek()
else
if m.userLocation.Count() > 0 then m.userLocation.peek().selectedIndex = m.settingsMenu.itemFocused
m.userLocation.push(configSection)
end if
result = CreateObject("roSGNode", "ContentNode")
' Build filtered children list (respecting visibleWhen conditions)
filteredChildren = []
for each item in configSection.children
if isValid(item.visibleWhen) and not isSettingVisible(item.visibleWhen)
continue for
end if
filteredChildren.push(item)
end for
configSection.filteredChildren = filteredChildren
for each item in filteredChildren
listItem = result.CreateChild("ContentNode")
listItem.title = translate(item.titleKey)
listItem.Description = translate(item.descriptionKey)
listItem.id = item.id
end for
' Inject "Reset User Settings" action at root level only
if m.userLocation.Count() = 1
resetItem = {
title: "Reset User Settings",
description: "Reset all settings to their default values. Your login session will not be affected.",
titleKey: translationKeys.LabelResetUserSettings,
descriptionKey: translationKeys.SettingResetAllSettingsToTheirDefault,
type: "action",
actionId: "resetUserSettings"
}
filteredChildren.push(resetItem)
listItem = result.CreateChild("ContentNode")
listItem.title = translate(resetItem.titleKey)
listItem.Description = translate(resetItem.descriptionKey)
listItem.id = "resetUserSettings"
end if
m.settingsMenu.content = result
if isValid(configSection.selectedIndex) and configSection.selectedIndex > -1
m.settingsMenu.jumpToItem = configSection.selectedIndex
end if
' Set Path display
m.path.text = ""
for each level in m.userLocation
if isValid(level.title)
if m.path.text = ""
m.path.text = translate(level.titleKey)
else
m.path.text += " / " + translate(level.titleKey)
end if
end if
end for
end sub
sub settingFocused()
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
m.settingDesc.text = translate(selectedSetting.descriptionKey)
' Hide Settings
m.boolSetting.visible = false
m.integerSetting.visible = false
m.radioSetting.visible = false
m.hexSetting.visible = false
m.alphaSetting.visible = false
m.languagePicker.visible = false
userSettings = m.global.user.settings
if not isValid(selectedSetting.type)
return
else if selectedSetting.type = "bool"
m.boolSetting.visible = true
if userSettings[selectedSetting.settingName] = true
m.boolSetting.checkedItem = 1
else
m.boolSetting.checkedItem = 0
end if
else if selectedSetting.type = "integer"
integerValue = userSettings[selectedSetting.settingName].ToStr()
if isValid(integerValue)
m.integerSetting.text = integerValue
end if
m.integerSetting.visible = true
else if LCase(selectedSetting.type) = "radio"
selectedValue = userSettings[selectedSetting.settingName]
radioContent = CreateObject("roSGNode", "ContentNode")
m.radioSetting.checkedItem = 0
itemIndex = 0
for each item in m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused].options
listItem = radioContent.CreateChild("ContentNode")
listItem.title = translate(item.titleKey)
listItem.id = item.id
if selectedValue = item.id
m.radioSetting.checkedItem = itemIndex
end if
itemIndex++
end for
m.radioSetting.content = radioContent
m.radioSetting.jumpToItem = m.radioSetting.checkedItem
m.radioSetting.visible = true
else if selectedSetting.type = "text"
' Text input type (used for hex color codes)
textValue = userSettings[selectedSetting.settingName]
if isValid(textValue)
m.hexSetting.text = textValue
else
m.hexSetting.text = ""
end if
m.hexSetting.visible = true
else if selectedSetting.type = "alpha"
' Alpha text input type (used for language codes)
textValue = userSettings[selectedSetting.settingName]
if isValid(textValue)
m.alphaSetting.text = textValue
else
m.alphaSetting.text = ""
end if
m.alphaSetting.visible = true
else if selectedSetting.type = "languagePicker"
selectedValue = userSettings[selectedSetting.settingName]
if not isValid(selectedValue) then selectedValue = ""
m.suppressLanguagePicker = true
m.languagePicker.selectedCode = selectedValue
m.suppressLanguagePicker = false
m.languagePicker.visible = true
else if selectedSetting.type = "action"
' Action items show description only, no setting controls
else
m.log.warn("Unknown setting type", selectedSetting.type)
end if
end sub
sub settingSelected()
selectedItem = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
' Handle action items (e.g., Reset User Settings)
if isValid(selectedItem.actionId) and selectedItem.actionId = "resetUserSettings"
confirmResetUserSettings()
return
end if
if isValid(selectedItem.type) ' Show setting
if selectedItem.type = "bool"
m.boolSetting.setFocus(true)
else if selectedItem.type = "integer"
m.integerSetting.setFocus(true)
else if selectedItem.type = "radio"
m.radioSetting.setFocus(true)
else if selectedItem.type = "text"
m.hexSetting.setFocus(true)
else if selectedItem.type = "alpha"
m.alphaSetting.setFocus(true)
else if selectedItem.type = "languagePicker"
m.languagePicker.findNode("keyboard").setFocus(true)
end if
else if isValid(selectedItem.children) and selectedItem.children.Count() > 0 ' Show sub menu
LoadMenu(selectedItem)
m.settingsMenu.setFocus(true)
else
return
end if
m.settingDesc.text = m.settingsMenu.content.GetChild(m.settingsMenu.itemFocused).Description
end sub
sub boolSettingChanged()
if not isValid(m.boolSetting.focusedChild) then return
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
if m.boolSetting.checkedItem
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, "true")
' Special handling for globalRememberMe - set active_user in global registry
if selectedSetting.settingName = "globalRememberMe"
setSetting("active_user", m.global.user.id)
end if
else
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, "false")
' Special handling for globalRememberMe - remove active_user from global registry
if selectedSetting.settingName = "globalRememberMe"
unsetSetting("active_user")
end if
end if
end sub
sub radioSettingChanged()
if not isValid(m.radioSetting.focusedChild) then return
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
' Language picker is handled by onLanguagePickerSelected(), not the radio control
if selectedSetting.type = "languagePicker" then return
selectedOption = selectedSetting.options[m.radioSetting.checkedItem]
newValue = selectedOption.id
' Backup custom colors before applying a preset (so user can switch back)
if isValid(selectedOption.presetValues)
currentTheme = m.global.user.settings.uiTheme
if currentTheme = "custom" or currentTheme = ""
backupCustomColors()
end if
else if newValue = "custom" and isValid(m.customColorBackup)
' Switching to Custom - restore backed up colors
restoreCustomColors()
end if
' Update node field - observer handles registry persistence
user.settings.Save(selectedSetting.settingName, newValue)
' Apply preset values if the selected option has them
if isValid(selectedOption.presetValues)
for each presetKey in selectedOption.presetValues
user.settings.Save(presetKey, selectedOption.presetValues[presetKey])
end for
m.themeColorsChanged = true
end if
' Refresh menu if sibling items have visibleWhen referencing this setting
if hasDependentVisibility(selectedSetting.settingName)
refreshCurrentMenu()
end if
end sub
' Evaluate a visibleWhen condition against current user settings
' @param {object} condition - { settingName: string, value: string }
' @return {boolean} - true if the setting matches the required value
function isSettingVisible(condition as object) as boolean
if not isValid(condition) or not isValid(condition.settingName) or not isValid(condition.value)
return true
end if
currentValue = m.global.user.settings[condition.settingName]
return currentValue = condition.value
end function
' Check if any sibling items in the current menu depend on a setting for visibility
function hasDependentVisibility(settingName as string) as boolean
currentSection = m.userLocation.peek()
for each item in currentSection.children
if isValid(item.visibleWhen) and item.visibleWhen.settingName = settingName
return true
end if
end for
return false
end function
' Refresh the current menu level in-place (re-filter visibleWhen and re-render)
' Rebuilds filtered children and content without touching the navigation stack
sub refreshCurrentMenu()
currentSection = m.userLocation.peek()
savedIndex = m.settingsMenu.itemFocused
' Rebuild filtered children list
filteredChildren = []
for each item in currentSection.children
if isValid(item.visibleWhen) and not isSettingVisible(item.visibleWhen)
continue for
end if
filteredChildren.push(item)
end for
currentSection.filteredChildren = filteredChildren
' Rebuild content nodes
result = CreateObject("roSGNode", "ContentNode")
for each item in filteredChildren
listItem = result.CreateChild("ContentNode")
listItem.title = translate(item.titleKey)
listItem.Description = translate(item.descriptionKey)
listItem.id = item.id
end for
' Re-inject reset button at root level
if m.userLocation.Count() = 1
resetItem = {
title: "Reset User Settings",
description: "Reset all settings to their default values. Your login session will not be affected.",
titleKey: translationKeys.LabelResetUserSettings,
descriptionKey: translationKeys.SettingResetAllSettingsToTheirDefault,
type: "action",
actionId: "resetUserSettings"
}
filteredChildren.push(resetItem)
listItem = result.CreateChild("ContentNode")
listItem.title = translate(resetItem.titleKey)
listItem.Description = translate(resetItem.descriptionKey)
listItem.id = "resetUserSettings"
end if
m.settingsMenu.content = result
' Restore focus, clamping to new list bounds
if savedIndex >= filteredChildren.Count()
savedIndex = filteredChildren.Count() - 1
end if
if savedIndex >= 0
m.settingsMenu.jumpToItem = savedIndex
end if
end sub
' Backup current custom color values before applying a preset
sub backupCustomColors()
userSettings = m.global.user.settings
m.customColorBackup = {
uiThemeColorPrimary: userSettings.uiThemeColorPrimary,
uiThemeColorSecondary: userSettings.uiThemeColorSecondary,
uiThemeColorBackgroundPrimary: userSettings.uiThemeColorBackgroundPrimary,
uiThemeColorBackgroundSecondary: userSettings.uiThemeColorBackgroundSecondary,
uiThemeColorTextPrimary: userSettings.uiThemeColorTextPrimary,
uiThemeColorTextSecondary: userSettings.uiThemeColorTextSecondary,
uiThemeColorTextDisabled: userSettings.uiThemeColorTextDisabled
}
end sub
' Restore custom color values from backup
sub restoreCustomColors()
if not isValid(m.customColorBackup) then return
for each key in m.customColorBackup
user.settings.Save(key, m.customColorBackup[key])
end for
m.themeColorsChanged = true
end sub
' Check if the current color values differ from the backed-up custom colors
' @return {boolean} - true if any color has changed from the backup
function hasCustomColorsChanged() as boolean
if not isValid(m.customColorBackup) then return false
userSettings = m.global.user.settings
for each key in m.customColorBackup
if userSettings[key] <> m.customColorBackup[key]
return true
end if
end for
return false
end function
' Show confirmation dialog before resetting all user settings
sub confirmResetUserSettings()
m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelResetUserSettings), translate(translationKeys.MessageAreYouSureYouWantTo2), [translate(translationKeys.ButtonCancel), translate(translationKeys.ButtonReset)])
m.global.sceneManager.observeFieldScoped("isDataReturned", "onResetUserSettingsResponse")
end sub
' Handle user response from reset user settings confirmation dialog
sub onResetUserSettingsResponse()
m.global.sceneManager.unobserveFieldScoped("isDataReturned")
returnData = m.global.sceneManager.returnData
if not isValid(returnData) or returnData.indexSelected <> 1
' User selected "Cancel" or closed dialog
m.settingsMenu.setFocus(true)
return
end if
executeResetUserSettings()
end sub
' Reset all user settings to defaults from settings.json
' Preserves session keys (authToken, username, serverId, primaryImageTag, LastRunVersion)
sub executeResetUserSettings()
localUser = m.global.user
userId = localUser.id
if not isValid(userId) or userId = ""
m.log.warn("Cannot reset settings - user ID is invalid")
m.settingsMenu.setFocus(true)
return
end if
m.log.info("Resetting all user settings to defaults")
' Disable auto-sync to prevent observers from firing during cleanup
localUser.settings.callFunc("disableAutoSync")
' Delete all non-session keys from user registry
sessionKeys = ["authToken", "username", "serverId", "primaryImageTag", "LastRunVersion"]
allRegistryData = RegistryReadAll(userId)
keysToDelete = getSettingKeysToDelete(allRegistryData, sessionKeys)
for each key in keysToDelete
registryDelete(key, userId)
end for
' Reload all defaults from settings.json onto the settings node
user.settings.SaveDefaults()
' globalRememberMe was reset to false — clear auto-login state
unsetSetting("active_user")
' Clear per-library display settings
localUser.settings.displaySettings = {}
' Re-enable auto-sync for future changes
localUser.settings.callFunc("enableAutoSync")
' Theme colors were reset to defaults - flag for refresh on exit
m.themeColorsChanged = true
m.log.info("User settings reset complete", { deletedKeys: keysToDelete.Count() })
' Show success feedback (toast lives on JRScene, persists across scene transitions)
m.top.getScene().callFunc("showToast", translate(translationKeys.LabelSettingsResetToDefaults), "success")
' Exit settings and reload home with fresh theme colors
performSettingsExit()
end sub
' JRScreen hook that gets ran when the screen is shown.
sub onScreenShown()
' Clear backdrop on settings screens
m.global.sceneManager.callFunc("setBackgroundImage", "")
overhang = m.top.getScene().findNode("overhang")
if not isValid(overhang) then return
overhang.isLogoVisible = true
overhang.currentUser = m.global.user.name
' Ensure settings menu has focus every time this screen is shown.
' init() calls setFocus before the node is in the scene graph (silently fails),
' and SceneManager's fallback focuses the Group, not settingsMenu — breaking
' all BACK key handling since onKeyEvent checks settingsMenu.focusedChild.
if isValid(m.settingsMenu)
m.settingsMenu.setFocus(true)
end if
end sub
' JRScreen hook that gets ran as needed.
' Assumes settings were changed and they affect the device profile.
' Posts a new device profile to the server via fire-and-forget side effect.
sub onScreenHidden()
SubmitSideEffect(GetApi().BuildPostSessionCapabilitiesRequest(getDeviceCapabilities()))
end sub
' Returns true if any of the data entry forms are in focus
function isFormInFocus() as boolean
if isValid(m.settingDetail.focusedChild) or m.radioSetting.hasFocus() or m.boolSetting.hasFocus() or m.integerSetting.hasFocus() or m.hexSetting.hasFocus()
return true
end if
return false
end function
' Exit settings screen, refreshing theme colors only if they changed
' Shows confirmation dialog if custom colors would be permanently lost
sub exitSettingsAndReloadHome()
' Check if custom colors will be lost (user switched from Custom to a preset)
if isValid(m.customColorBackup) and m.global.user.settings.uiTheme <> "custom" and hasCustomColorsChanged()
m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelReplaceCustomColors), translate(translationKeys.MessageYourCustomColorsWillBeReplaced), [translate(translationKeys.LabelYes), translate(translationKeys.LabelNo)])
m.global.sceneManager.observeFieldScoped("isDataReturned", "onExitConfirmResponse")
return
end if
performSettingsExit()
end sub
' Handle user response from exit confirmation dialog
sub onExitConfirmResponse()
m.global.sceneManager.unobserveFieldScoped("isDataReturned")
returnData = m.global.sceneManager.returnData
if not isValid(returnData) or returnData.indexSelected <> 0
' User selected "No" or closed dialog - restore custom colors and stay
restoreCustomColors()
user.settings.Save("uiTheme", "custom")
refreshCurrentMenu()
m.settingsMenu.setFocus(true)
return
end if
' User confirmed - clear backup and exit
m.customColorBackup = invalid
performSettingsExit()
end sub
' Handle language selection from LanguagePicker component
sub onLanguagePickerSelected()
' Guard against being triggered during init or programmatic selectedCode changes
if m.suppressLanguagePicker then return
if not isValid(m.userLocation) or m.userLocation.count() = 0 then return
newLocale = m.languagePicker.selectedCode
selectedSetting = m.userLocation.peek().filteredChildren[m.settingsMenu.itemFocused]
' Save to user registry (empty string = automatic)
user.settings.Save(selectedSetting.settingName, newLocale)
' Resolve and load the new locale
if newLocale = ""
resolvedLocale = resolveTranslationLocale(true)
else
resolvedLocale = newLocale
end if
if resolvedLocale <> m.global.translationLocale
loadTranslations(resolvedLocale)
m.languageChanged = true
end if
end sub
' Perform the actual settings exit (apply theme changes and navigate)
sub performSettingsExit()
if m.themeColorsChanged or m.languageChanged
if m.themeColorsChanged
applyThemeColorOverrides(m.global.user.settings)
m.global.sceneManager.callFunc("refreshThemeColors")
end if
m.global.sceneManager.callFunc("reloadHome")
else
m.global.sceneManager.callFunc("popScene")
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
if (key = "back" or key = "left") and isValid(m.settingsMenu.focusedChild) and m.userLocation.Count() > 1
LoadMenu({})
return true
else if (key = "back" or key = "left") and isFormInFocus()
m.settingsMenu.setFocus(true)
return true
else if key = "back" and isValid(m.settingsMenu.focusedChild) and m.userLocation.Count() = 1
' Exiting Settings - apply theme colors and reload home screen
exitSettingsAndReloadHome()
return true
end if
if key = "options"
exitSettingsAndReloadHome()
return true
end if
if key = "right"
settingSelected()
end if
if key = "up" and isValid(m.settingsMenu.focusedChild) and m.settingsMenu.itemFocused = 0
m.settingsMenu.jumpToItem = m.settingsMenu.content.getChildCount() - 1
return true
end if
if key = "down" and isValid(m.settingsMenu.focusedChild)
if m.settingsMenu.itemFocused = m.settingsMenu.content.getChildCount() - 1
m.settingsMenu.jumpToItem = 0
return true
end if
end if
return false
end function
' onDestroy: Full teardown releasing all resources before component removal
' Called automatically by SceneManager.popScene() / clearScenes()
sub onDestroy()
m.log.verbose("onDestroy")
' Unobserve all child node observers
m.integerSetting.unobserveField("submit")
m.integerSetting.unobserveField("escape")
m.hexSetting.unobserveField("submit")
m.hexSetting.unobserveField("escape")
m.hexSetting.unobserveField("reset")
m.alphaSetting.unobserveField("submit")
m.alphaSetting.unobserveField("escape")
m.alphaSetting.unobserveField("reset")
m.languagePicker.unobserveField("selectedCode")
m.settingsMenu.unobserveField("itemFocused")
m.settingsMenu.unobserveField("itemSelected")
m.boolSetting.unobserveField("checkedItem")
m.radioSetting.unobserveField("checkedItem")
' Unobserve scoped observer on sceneManager (may be active if reset dialog was open)
m.global.sceneManager.unobserveFieldScoped("isDataReturned")
' Clear node references
m.settingsMenu = invalid
m.settingDetail = invalid
m.settingDesc = invalid
m.path = invalid
m.boolSetting = invalid
m.integerSetting = invalid
m.radioSetting = invalid
m.hexSetting = invalid
m.alphaSetting = invalid
m.languagePicker = invalid
' Clear data structures
m.userLocation = invalid
m.configTree = invalid
m.customColorBackup = invalid
end sub