local menu = require('openmw.menu') local ui = require('openmw.ui') local util = require('openmw.util') local async = require('openmw.async') local core = require('openmw.core') local storage = require('openmw.storage') local I = require('openmw.interfaces') local auxUi = require('openmw_aux.ui') local common = require('scripts.omw.settings.common') common.getSection(false, common.groupSectionKey):setLifeTime(storage.LIFE_TIME.GameSession) -- need to :reset() on reloadlua as well as game session end common.getSection(false, common.groupSectionKey):reset() local renderers = {} local function registerRenderer(name, renderFunction) renderers[name] = renderFunction end require('scripts.omw.settings.renderers')(registerRenderer) local interfaceL10n = core.l10n('Interface') local pages = {} local groups = {} local pageOptions = {} local groupElements = {} local interval = { template = I.MWUI.templates.interval } local growingIntreval = { template = I.MWUI.templates.interval, external = { grow = 1, }, } local spacer = { props = { size = util.vector2(0, 10), }, } local bigSpacer = { props = { size = util.vector2(0, 50), }, } local stretchingLine = { template = I.MWUI.templates.horizontalLine, external = { stretch = 1, }, } local spacedLines = function(count) local content = {} table.insert(content, spacer) table.insert(content, stretchingLine) for _ = 2, count do table.insert(content, interval) table.insert(content, stretchingLine) end table.insert(content, spacer) return { type = ui.TYPE.Flex, external = { stretch = 1, }, content = ui.content(content), } end local function interlaceSeparator(layouts, separator) local result = {} result[1] = layouts[1] for i = 2, #layouts do table.insert(result, separator) table.insert(result, layouts[i]) end return result end local function setSettingValue(global, groupKey, settingKey, value) if global then core.sendGlobalEvent(common.setGlobalEvent, { groupKey = groupKey, settingKey = settingKey, value = value, }) else storage.playerSection(groupKey):set(settingKey, value) end end local function renderSetting(group, setting, value, global) local renderFunction = renderers[setting.renderer] if not renderFunction then error(('Setting %s of %s has unknown renderer %s'):format(setting.key, group.key, setting.renderer)) end local set = function(value) setSettingValue(global, group.key, setting.key, value) end local l10n = core.l10n(group.l10n) local titleLayout = { type = ui.TYPE.Flex, content = ui.content { { template = I.MWUI.templates.textHeader, props = { text = l10n(setting.name), }, }, }, } if setting.description then titleLayout.content:add(interval) titleLayout.content:add { template = I.MWUI.templates.textParagraph, props = { text = l10n(setting.description), size = util.vector2(300, 0), }, } end local argument = common.getArgumentSection(global, group.key):get(setting.key) local ok, rendererResult = pcall(renderFunction, value, set, argument) if not ok then print(string.format('Setting %s renderer "%s" error: %s', setting.key, setting.renderer, rendererResult)) end return { name = setting.key, type = ui.TYPE.Flex, props = { horizontal = true, arrange = ui.ALIGNMENT.Center, }, external = { stretch = 1, }, content = ui.content { titleLayout, growingIntreval, ok and rendererResult or {}, -- TODO: display error? }, } end local groupLayoutName = function(key, global) return ('%s%s'):format(global and 'global_' or 'player_', key) end local function renderGroup(group, global) local l10n = core.l10n(group.l10n) local valueSection = common.getSection(global, group.key) local settingLayouts = {} local sortedSettings = {} for _, setting in pairs(group.settings) do sortedSettings[setting.order] = setting end for _, setting in ipairs(sortedSettings) do table.insert(settingLayouts, renderSetting(group, setting, valueSection:getCopy(setting.key), global)) end local settingsContent = ui.content(interlaceSeparator(settingLayouts, spacedLines(1))) local resetButtonLayout = { template = I.MWUI.templates.box, events = { mouseClick = async:callback(function() for _, setting in pairs(group.settings) do setSettingValue(global, group.key, setting.key, setting.default) end end), }, content = ui.content { { template = I.MWUI.templates.padding, content = ui.content { { template = I.MWUI.templates.textNormal, props = { text = interfaceL10n('Reset') }, }, }, }, }, } local titleLayout = { type = ui.TYPE.Flex, external = { stretch = 1, }, content = ui.content { { template = I.MWUI.templates.textHeader, props = { text = l10n(group.name), textSize = 20, }, } }, } if group.description then titleLayout.content:add(interval) titleLayout.content:add { template = I.MWUI.templates.textParagraph, props = { text = l10n(group.description), size = util.vector2(300, 0), }, } end return { name = groupLayoutName(group.key, global), type = ui.TYPE.Flex, external = { stretch = 1, }, content = ui.content { { type = ui.TYPE.Flex, props = { horizontal = true, arrange = ui.ALIGNMENT.Center, }, external = { stretch = 1, }, content = ui.content { titleLayout, growingIntreval, resetButtonLayout, }, }, spacedLines(2), { name = 'settings', type = ui.TYPE.Flex, content = settingsContent, external = { stretch = 1, }, }, }, } end local function pageGroupComparator(a, b) return a.order < b.order or ( a.order == b.order and a.key < b.key ) end local function generateSearchHints(page) local hints = {} do local l10n = core.l10n(page.l10n) table.insert(hints, l10n(page.name)) if page.description then table.insert(hints, l10n(page.description)) end end local pageGroups = groups[page.key] for _, pageGroup in pairs(pageGroups) do local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key) local l10n = core.l10n(group.l10n) table.insert(hints, l10n(group.name)) if group.description then table.insert(hints, l10n(group.description)) end for _, setting in pairs(group.settings) do table.insert(hints, l10n(setting.name)) if setting.description then table.insert(hints, l10n(setting.description)) end end end return table.concat(hints, ' ') end local function renderPage(page, options) local l10n = core.l10n(page.l10n) local sortedGroups = {} for _, group in pairs(groups[page.key]) do table.insert(sortedGroups, group) end table.sort(sortedGroups, pageGroupComparator) local groupLayouts = {} for _, pageGroup in ipairs(sortedGroups) do local group = common.getSection(pageGroup.global, common.groupSectionKey):getCopy(pageGroup.key) if not group then error(string.format('%s group "%s" was not found', pageGroup.global and 'Global' or 'Player', pageGroup.key)) end local groupElement = groupElements[page.key][group.key] if not groupElement or not groupElement.layout then groupElement = ui.create(renderGroup(group, pageGroup.global)) end if groupElement.layout == nil then error(string.format('Destroyed group element for %s %s', page.key, group.key)) end groupElements[page.key][group.key] = groupElement table.insert(groupLayouts, groupElement) end local groupsLayout = { name = 'groups', type = ui.TYPE.Flex, external = { stretch = 1, }, content = ui.content(interlaceSeparator(groupLayouts, bigSpacer)), } local titleLayout = { type = ui.TYPE.Flex, external = { stretch = 1, }, content = ui.content { { template = I.MWUI.templates.textHeader, props = { text = l10n(page.name), textSize = 22, }, }, spacedLines(3), }, } if page.description then titleLayout.content:add { template = I.MWUI.templates.textParagraph, props = { text = l10n(page.description), size = util.vector2(300, 0), }, } end local layout = { name = page.key, type = ui.TYPE.Flex, props = { position = util.vector2(10, 10), }, content = ui.content { titleLayout, bigSpacer, groupsLayout, bigSpacer, }, } options.name = l10n(page.name) options.searchHints = generateSearchHints(page) if options.element then options.element.layout = layout options.element:update() else options.element = ui.create(layout) end end local function onSettingChanged(global) return async:callback(function(groupKey, settingKey) local group = common.getSection(global, common.groupSectionKey):getCopy(groupKey) if not group or not pageOptions[group.page] then return end local groupElement = groupElements[group.page][group.key] if not settingKey then if groupElement then groupElement.layout = renderGroup(group) groupElement:update() else renderPage(pages[group.page], pageOptions[group.page]) end return end local value = common.getSection(global, group.key):getCopy(settingKey) local settingsContent = groupElement.layout.content.settings.content auxUi.deepDestroy(settingsContent[settingKey]) -- support setting renderers which return UI elements settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) groupElement:update() end) end local function onGroupRegistered(global, key) local group = common.getSection(global, common.groupSectionKey):get(key) if not group then return end groups[group.page] = groups[group.page] or {} groupElements[group.page] = groupElements[group.page] or {} local pageGroup = { key = group.key, global = global, order = group.order, } if not groups[group.page][pageGroup.key] then common.getSection(global, group.key):subscribe(onSettingChanged(global)) common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey) if settingKey == nil then return end local group = common.getSection(global, common.groupSectionKey):get(group.key) if not group or not pageOptions[group.page] then return end local value = common.getSection(global, group.key):getCopy(settingKey) local element = groupElements[group.page][group.key] local settingsContent = element.layout.content.settings.content settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global) element:update() end)) end groups[group.page][pageGroup.key] = pageGroup if not pages[group.page] then return end pageOptions[group.page] = pageOptions[group.page] or {} renderPage(pages[group.page], pageOptions[group.page]) end local function updateGroups(global) local groupSection = common.getSection(global, common.groupSectionKey) for groupKey in pairs(groupSection:asTable()) do onGroupRegistered(global, groupKey) end groupSection:subscribe(async:callback(function(_, key) if key then onGroupRegistered(global, key) else for groupKey in pairs(groupSection:asTable()) do onGroupRegistered(global, groupKey) end end end)) end local updatePlayerGroups = function() updateGroups(false) end local updateGlobalGroups = function() updateGroups(true) end local menuGroups = {} local menuPages = {} local function resetPlayerGroups() local playerGroupsSection = storage.playerSection(common.groupSectionKey) for pageKey, page in pairs(groups) do for groupKey in pairs(page) do if not menuGroups[groupKey] then if groupElements[pageKey][groupKey] then groupElements[pageKey][groupKey]:destroy() groupElements[pageKey][groupKey] = nil end page[groupKey] = nil playerGroupsSection:set(groupKey, nil) end end local options = pageOptions[pageKey] if options then if not menuPages[pageKey] then if options.element then auxUi.deepDestroy(options.element) options.element = nil end ui.removeSettingsPage(options) pageOptions[pageKey] = nil else renderPage(pages[pageKey], options) end end end end local function registerPage(options) if type(options) ~= 'table' then error('Page options must be a table') end if type(options.key) ~= 'string' then error('Page must have a key') end if type(options.l10n) ~= 'string' then error('Page must have a localization context') end if type(options.name) ~= 'string' then error('Page must have a name') end if options.description ~= nil and type(options.description) ~= 'string' then error('Page description key must be a string') end local page = { key = options.key, l10n = options.l10n, name = options.name, description = options.description, } pages[page.key] = page groups[page.key] = groups[page.key] or {} pageOptions[page.key] = pageOptions[page.key] or {} renderPage(page, pageOptions[page.key]) ui.registerSettingsPage(pageOptions[page.key]) end updatePlayerGroups() if menu.getState() == menu.STATE.Running then -- handle reloadlua correctly updateGlobalGroups() end return { interfaceName = 'Settings', interface = { version = 1, registerPage = function(options) registerPage(options) menuPages[options.key] = true end, registerRenderer = registerRenderer, registerGroup = function(options) if not options.permanentStorage then error('Menu scripts are only allowed to register setting groups with permanentStorage = true') end common.registerGroup(options) menuGroups[options.key] = true end, updateRendererArgument = common.updateRendererArgument, }, engineHandlers = { onStateChanged = function() if menu.getState() == menu.STATE.Running then updatePlayerGroups() updateGlobalGroups() elseif menu.getState() == menu.STATE.NoGame then resetPlayerGroups() end end, }, eventHandlers = { [common.registerPageEvent] = function(options) registerPage(options) end, } }