1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-26 09:35:28 +00:00

Merge branch 'settings_interface' into 'master'

Settings interface

See merge request OpenMW/openmw!1731
This commit is contained in:
psi29a 2022-05-08 13:45:38 +00:00
commit 39a48d3275
14 changed files with 677 additions and 22 deletions

View File

@ -44,11 +44,10 @@ namespace LuaUi
void Content::insert(size_t index, const sol::table& table)
{
size_t size = mOrdered.size();
if (size < index)
if (mOrdered.size() < index)
throw std::logic_error("Can't have gaps in UI Content.");
mOrdered.insert(mOrdered.begin() + index, table);
for (size_t i = index; i < size; ++i)
for (size_t i = index; i < mOrdered.size(); ++i)
{
sol::optional<std::string> name = mOrdered[i]["name"];
if (name.has_value())

View File

@ -55,8 +55,7 @@ namespace LuaUi
{
WidgetExtension* ext = children[i];
sol::table newLayout = content.at(i);
if (ext->widget()->getTypeName() == widgetType(newLayout)
&& ext->getLayout() == newLayout)
if (ext->widget()->getTypeName() == widgetType(newLayout))
{
updateWidget(ext, newLayout);
}

View File

@ -71,6 +71,7 @@ namespace LuaUi
w->forcePosition(childPosition);
primary(size) += static_cast<int>(growFactor * getGrow(w));
w->forceSize(size);
w->updateCoord();
primary(childPosition) += primary(size);
w->updateCoord();
}

View File

@ -68,3 +68,4 @@ $DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/ai.lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera.lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/mwui/init.lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/settings/player.lua

View File

@ -27,6 +27,7 @@ Lua API reference
interface_ai
interface_camera
interface_mwui
interface_settings
iterables
@ -94,12 +95,19 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid
**Interfaces of built-in scripts**
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`AI <Interface AI>` | by local scripts | | Control basic AI of NPCs and creatures. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
.. list-table::
:widths: 20 20 60
* - Interface
- Can be used
- Description
* - :ref:`AI <Interface AI>`
- by local scripts
- Control basic AI of NPCs and creatures.
* - :ref:`Camera <Interface Camera>`
- by player scripts
- | Allows to alter behavior of the built-in camera script
| without overriding the script completely.
* - :ref:`Settings <Interface Settings>`
- by player and global scripts
- Save, display and track changes of setting values.

View File

@ -0,0 +1,6 @@
Interface Settings
==================
.. raw:: html
:file: generated_html/scripts_omw_settings_player.html

View File

@ -460,15 +460,22 @@ The order in which the scripts are started is important. So if one mod should ov
**Interfaces of built-in scripts**
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`AI <Interface AI>` | by local scripts | | Control basic AI of NPCs and creatures. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
.. list-table::
:widths: 20 20 60
* - Interface
- Can be used
- Description
* - :ref:`AI <Interface AI>`
- by local scripts
- Control basic AI of NPCs and creatures.
* - :ref:`Camera <Interface Camera>`
- by player scripts
- | Allows to alter behavior of the built-in camera script
| without overriding the script completely.
* - :ref:`Settings <Interface Settings>`
- by player and global scripts
- Save, display and track changes of setting values.
Event system
============

View File

@ -24,7 +24,7 @@ Properties
- ui.ALIGNMENT (Start)
- Where to align the children in the main axis.
* - arrange
- ui.ALIGNMETN (Start)
- ui.ALIGNMENT (Start)
- How to arrange the children in the cross axis.
External

View File

@ -17,6 +17,10 @@ set(LUA_BUILTIN_FILES
scripts/omw/console/player.lua
scripts/omw/console/global.lua
scripts/omw/console/local.lua
scripts/omw/settings/player.lua
scripts/omw/settings/global.lua
scripts/omw/settings/common.lua
scripts/omw/settings/render.lua
l10n/Calendar/en.yaml

View File

@ -4,3 +4,5 @@ PLAYER: scripts/omw/console/player.lua
GLOBAL: scripts/omw/console/global.lua
CUSTOM: scripts/omw/console/local.lua
PLAYER: scripts/omw/mwui/init.lua
GLOBAL: scripts/omw/settings/global.lua
PLAYER: scripts/omw/settings/player.lua

View File

@ -0,0 +1,135 @@
local storage = require('openmw.storage')
local contextSection = storage.playerSection or storage.globalSection
local groupSectionKey = 'OmwSettingGroups'
local groupSection = contextSection(groupSectionKey)
groupSection:removeOnExit()
local function validateSettingOptions(options)
if type(options) ~= 'table' then
error('Setting options must be a table')
end
if type(options.key) ~= 'string' then
error('Setting must have a key')
end
if type(options.permanentStorage) ~= 'boolean' then
error('Setting must have a permanentStorage flag')
end
if type(options.renderer) ~= 'string' then
error('Setting must have a renderer')
end
if type(options.name) ~= 'string' then
error('Setting must have a name localization key')
end
if type(options.description) ~= 'string' then
error('Setting must have a descripiton localization key')
end
end
local function validateGroupOptions(options)
if type(options) ~= 'table' then
error('Group options must be a table')
end
if type(options.key) ~= 'string' then
error('Group must have a key')
end
local conventionPrefix = "Settings"
if options.key:sub(1, conventionPrefix:len()) ~= conventionPrefix then
print(("Group key %s doesn't start with %s"):format(options.key, conventionPrefix))
end
if type(options.page) ~= 'string' then
error('Group must belong to a page')
end
if type(options.order) ~= 'number' and type(options.order) ~= 'nil' then
error('Group order must be a number')
end
if type(options.l10n) ~= 'string' then
error('Group must have a localization context')
end
if type(options.name) ~= 'string' then
error('Group must have a name localization key')
end
if type(options.description) ~= 'string' then
error('Group must have a description localization key')
end
if type(options.settings) ~= 'table' then
error('Group must have a table of settings')
end
for _, opt in ipairs(options.settings) do
validateSettingOptions(opt)
end
end
local function registerSetting(options)
return {
key = options.key,
permanentStorage = options.permanentStorage,
default = options.default,
renderer = options.renderer,
argument = options.argument,
name = options.name,
description = options.description,
}
end
local function registerGroup(options)
validateGroupOptions(options)
if groupSection:get(options.key) then
error(('Group with key %s was already registered'):format(options.key))
end
local group = {
key = options.key,
page = options.page,
order = options.order or 0,
l10n = options.l10n,
name = options.name,
description = options.description,
settings = {},
}
local valueSection = contextSection(options.key)
for _, opt in ipairs(options.settings) do
local setting = registerSetting(opt)
if group.settings[setting.key] then
error(('Duplicate setting key %s'):format(options.key))
end
group.settings[setting.key] = setting
if not valueSection:get(setting.key) then
valueSection:set(setting.key, setting.default)
end
end
groupSection:set(group.key, group)
end
return {
getSection = function(global, key)
return (global and storage.globalSection or storage.playerSection)(key)
end,
setGlobalEvent = 'OMWSettingsGlobalSet',
groupSectionKey = groupSectionKey,
onLoad = function(saved)
if not saved then return end
for groupKey, settings in pairs(saved) do
local section = contextSection(groupKey)
for key, value in pairs(settings) do
section:set(key, value)
end
end
end,
onSave = function()
local saved = {}
for groupKey, group in pairs(groupSection:asTable()) do
local section = contextSection(groupKey)
saved[groupKey] = {}
for key, value in pairs(section:asTable()) do
if not group.settings[key].permanentStorage then
saved[groupKey][key] = value
end
end
end
groupSection:reset()
return saved
end,
registerGroup = registerGroup,
}

View File

@ -0,0 +1,19 @@
local storage = require('openmw.storage')
local common = require('scripts.omw.settings.common')
return {
interfaceName = 'Settings',
interface = {
registerGroup = common.registerGroup,
},
engineHandlers = {
onLoad = common.onLoad,
onSave = common.onSave,
},
eventHandlers = {
[common.setGlobalEvent] = function(e)
storage.globalSection(e.groupKey):set(e.settingKey, e.value)
end,
},
}

View File

@ -0,0 +1,160 @@
local ui = require('openmw.ui')
local async = require('openmw.async')
local util = require('openmw.util')
local common = require('scripts.omw.settings.common')
local render = require('scripts.omw.settings.render')
render.registerRenderer('text', function(value, set, arg)
return {
type = ui.TYPE.TextEdit,
props = {
size = util.vector2(arg and arg.size or 150, 30),
text = value,
textColor = util.color.rgb(1, 1, 1),
textSize = 15,
textAlignV = ui.ALIGNMENT.End,
},
events = {
textChanged = async:callback(function(s) set(s) end),
},
}
end)
---
-- @type PageOptions
-- @field #string key A unique key
-- @field #string l10n A localization context (an argument of core.l10n)
-- @field #string name A key from the localization context
-- @field #string description A key from the localization context
---
-- @type GroupOptions
-- @field #string key A unique key, starts with "Settings" by convention
-- @field #string l10n A localization context (an argument of core.l10n)
-- @field #string name A key from the localization context
-- @field #string description A key from the localization context
-- @field #string page Key of a page which will contain this group
-- @field #number order Groups within the same page are sorted by this number, or their key for equal values.
-- Defaults to 0.
-- @field #list<#SettingOptions> settings A [iterables#List](iterables.html#List) of #SettingOptions
---
-- @type SettingOptions
-- @field #string key A unique key
-- @field #string name A key from the localization context
-- @field #string description A key from the localization context
-- @field default A default value
-- @field #string renderer A renderer key
-- @field argument An argument for the renderer
-- @field #boolean permanentStorage Whether the setting should is stored in permanent storage, or in the save file
return {
interfaceName = 'Settings',
---
-- @module Settings
-- @usage
-- -- In a player script
-- local storage = require('openmw.storage')
-- local I = require('openmw.interfaces')
-- I.Settings.registerGroup({
-- key = 'SettingsPlayerMyMod',
-- page = 'MyPage',
-- l10n = 'mymod',
-- name = 'modName',
-- description = 'modDescription',
-- settings = {
-- {
-- key = 'Greeting',
-- renderer = 'text',
-- name = 'greetingName',
-- description = 'greetingDescription',
-- default = 'Hello, world!',
-- argument = {
-- size = 200,
-- },
-- },
-- },
-- })
-- local playerSettings = storage.playerSection('SettingsPlayerMyMod')
-- -- access a setting page registered by a global script
-- local globalSettings = storage.globalSection('SettingsGlobalMyMod')
interface = {
---
-- @field [parent=#Settings] #string version
version = 0,
---
-- @function [parent=#Settings] registerPage Register a page to be displayed in the settings menu,
-- only available in player scripts
-- @param #PageOptions options
-- @usage
-- I.Settings.registerPage({
-- key = 'MyModName',
-- l10n = 'MyModName',
-- name = 'MyModName',
-- description = 'MyModDescription',
-- })---
registerPage = render.registerPage,
---
-- @function [parent=#Settings] registerRenderer Register a renderer,
-- only avaialable in player scripts
-- @param #string key
-- @param #function renderer A renderer function, receives setting's value,
-- a function to change it and an argument from the setting options
-- @usage
-- I.Settings.registerRenderer('text', function(value, set, arg)
-- return {
-- type = ui.TYPE.TextEdit,
-- props = {
-- size = util.vector2(arg and arg.size or 150, 30),
-- text = value,
-- textColor = util.color.rgb(1, 1, 1),
-- textSize = 15,
-- textAlignV = ui.ALIGNMENT.End,
-- },
-- events = {
-- textChanged = async:callback(function(s) set(s) end),
-- },
-- }
-- end)
registerRenderer = render.registerRenderer,
---
-- @function [parent=#Settings] registerGroup Register a group to be attached to a page,
-- available both in player and global scripts
-- @param #GroupOptions options
-- @usage
-- I.Settings.registerGroup {
-- key = 'SettingsTest',
-- page = 'test',
-- l10n = 'test',
-- name = 'Player',
-- description = 'Player settings group',
-- settings = {
-- {
-- key = 'Greeting',
-- saveOnly = true,
-- default = 'Hi',
-- renderer = 'text',
-- argument = {
-- size = 200,
-- },
-- name = 'Text Input',
-- description = 'Short text input',
-- },
-- {
-- key = 'Key',
-- saveOnly = false,
-- default = input.KEY.LeftAlt,
-- renderer = 'keybind',
-- name = 'Key',
-- description = 'Bind Key',
-- },
-- }
-- }
registerGroup = common.registerGroup,
},
engineHandlers = {
onLoad = common.onLoad,
onSave = common.onSave,
},
}

View File

@ -0,0 +1,314 @@
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 common = require('scripts.omw.settings.common')
local renderers = {}
local function registerRenderer(name, renderFunction)
renderers[name] = renderFunction
end
local pages = {}
local groups = {}
local pageOptions = {}
local padding = function(size)
return {
props = {
size = util.vector2(size, size),
}
}
end
local smallPadding = padding(10)
local bigPadding = padding(25)
local pageHeader = {
props = {
textColor = util.color.rgb(1, 1, 1),
textSize = 30,
},
}
local groupHeader = {
props = {
textColor = util.color.rgb(1, 1, 1),
textSize = 25,
},
}
local normal = {
props = {
textColor = util.color.rgb(1, 1, 1),
textSize = 20,
},
}
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)
if global then
core.sendGlobalEvent(common.setGlobalEvent, {
groupKey = group.key,
settingKey = setting.key,
value = value,
})
else
storage.playerSection(group.key):set(setting.key, value)
end
end
local l10n = core.l10n(group.l10n)
return {
name = setting.key,
type = ui.TYPE.Flex,
content = ui.content {
{
type = ui.TYPE.Flex,
props = {
horizontal = true,
align = ui.ALIGNMENT.Start,
arrange = ui.ALIGNMENT.End,
},
content = ui.content {
{
type = ui.TYPE.Text,
template = normal,
props = {
text = l10n(setting.name),
},
},
smallPadding,
renderFunction(value, set, setting.argument),
smallPadding,
{
type = ui.TYPE.Text,
template = normal,
props = {
text = 'Reset',
},
events = {
mouseClick = async:callback(function()
set(setting.default)
end),
},
},
},
},
},
}
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 layout = {
name = groupLayoutName(group.key, global),
type = ui.TYPE.Flex,
content = ui.content {
{
type = ui.TYPE.Flex,
props = {
horizontal = true,
align = ui.ALIGNMENT.Start,
arrange = ui.ALIGNMENT.End,
},
content = ui.content {
{
name = 'name',
type = ui.TYPE.Text,
template = groupHeader,
props = {
text = l10n(group.name),
},
},
smallPadding,
{
name = 'description',
type = ui.TYPE.Text,
template = normal,
props = {
text = l10n(group.description),
},
},
},
},
smallPadding,
{
name = 'settings',
type = ui.TYPE.Flex,
content = ui.content{},
},
bigPadding,
},
}
local settingsContent = layout.content.settings.content
local valueSection = common.getSection(global, group.key)
for _, setting in pairs(group.settings) do
settingsContent:add(renderSetting(group, setting, valueSection:get(setting.key), global))
end
return layout
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 = {}
local l10n = core.l10n(page.l10n)
table.insert(hints, l10n(page.name))
table.insert(hints, l10n(page.description))
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))
table.insert(hints, l10n(group.description))
for _, setting in pairs(group.settings) do
table.insert(hints, l10n(setting.name))
table.insert(hints, l10n(setting.description))
end
end
return table.concat(hints, ' ')
end
local function renderPage(page)
local l10n = core.l10n(page.l10n)
local layout = {
name = page.key,
type = ui.TYPE.Flex,
content = ui.content {
smallPadding,
{
type = ui.TYPE.Flex,
props = {
horizontal = true,
align = ui.ALIGNMENT.Start,
arrange = ui.ALIGNMENT.End,
},
content = ui.content {
{
name = 'name',
type = ui.TYPE.Text,
template = pageHeader,
props = {
text = l10n(page.name),
},
},
smallPadding,
{
name = 'description',
type = ui.TYPE.Text,
template = normal,
props = {
text = l10n(page.description),
},
},
},
},
bigPadding,
{
name = 'groups',
type = ui.TYPE.Flex,
content = ui.content {},
},
},
}
local groupsContent = layout.content.groups.content
local pageGroups = groups[page.key]
local sortedGroups = {}
for i, v in ipairs(pageGroups) do sortedGroups[i] = v end
table.sort(sortedGroups, pageGroupComparator)
for _, pageGroup in ipairs(sortedGroups) do
local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key)
groupsContent:add(renderGroup(group, pageGroup.global))
end
return {
name = l10n(page.name),
element = ui.create(layout),
searchHints = generateSearchHints(page),
}
end
local function onSettingChanged(global)
return async:callback(function(groupKey, settingKey)
local group = common.getSection(global, common.groupSectionKey):get(groupKey)
if not pageOptions[group.page] then return end
local element = pageOptions[group.page].element
local groupLayout = element.layout.content.groups.content[groupLayoutName(group.key, global)]
local settingsLayout = groupLayout.content.settings
local value = common.getSection(global, group.key):get(settingKey)
settingsLayout.content[settingKey] = renderSetting(group, group.settings[settingKey], value, global)
element:update()
end)
end
local function onGroupRegistered(global, key)
local group = common.getSection(global, common.groupSectionKey):get(key)
groups[group.page] = groups[group.page] or {}
local pageGroup = {
key = group.key,
global = global,
order = group.order,
}
table.insert(groups[group.page], pageGroup)
common.getSection(global, group.key):subscribe(onSettingChanged(global))
if not pages[group.page] then return end
local options = renderPage(pages[group.page])
pageOptions[group.page].element:destroy()
for k, v in pairs(options) do
pageOptions[group.page][k] = v
end
end
local globalGroups = storage.globalSection(common.groupSectionKey)
for groupKey in pairs(globalGroups:asTable()) do
onGroupRegistered(true, groupKey)
end
globalGroups:subscribe(async:callback(function(_, key)
if key then onGroupRegistered(true, key) end
end))
storage.playerSection(common.groupSectionKey):subscribe(async:callback(function(_, key)
if key then onGroupRegistered(false, key) 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 type(options.description) ~= 'string' then
error('Page must have a description')
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] = renderPage(page)
ui.registerSettingsPage(pageOptions[page.key])
end
return {
registerPage = registerPage,
registerRenderer = registerRenderer,
}