mirror of
				https://github.com/OpenMW/openmw.git
				synced 2025-11-04 05:26:39 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			540 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			540 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
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,
 | 
						|
    }
 | 
						|
}
 |