minetest/builtin/mainmenu/settings/dlg_settings.lua
2024-10-03 11:37:04 +02:00

760 lines
22 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--Minetest
--Copyright (C) 2022 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local component_funcs = dofile(core.get_mainmenu_path() .. DIR_DELIM ..
"settings" .. DIR_DELIM .. "components.lua")
local shadows_component = dofile(core.get_mainmenu_path() .. DIR_DELIM ..
"settings" .. DIR_DELIM .. "shadows_component.lua")
local loaded = false
local full_settings
local info_icon_path = core.formspec_escape(defaulttexturedir .. "settings_info.png")
local reset_icon_path = core.formspec_escape(defaulttexturedir .. "settings_reset.png")
local all_pages = {}
local page_by_id = {}
local filtered_pages = all_pages
local filtered_page_by_id = page_by_id
local function get_setting_info(name)
for _, entry in ipairs(full_settings) do
if entry.type ~= "category" and entry.name == name then
return entry
end
end
return nil
end
local function add_page(page)
assert(type(page.id) == "string")
assert(type(page.title) == "string")
assert(page.section == nil or type(page.section) == "string")
assert(type(page.content) == "table")
assert(not page_by_id[page.id], "Page " .. page.id .. " already registered")
all_pages[#all_pages + 1] = page
page_by_id[page.id] = page
return page
end
local function load_settingtypes()
local page = nil
local section = nil
local function ensure_page_started()
if not page then
page = add_page({
id = (section or "general"):lower():gsub(" ", "_"),
title = section or fgettext_ne("General"),
section = section,
content = {},
})
end
end
for _, entry in ipairs(full_settings) do
if entry.type == "category" then
if entry.level == 0 then
section = entry.name
page = nil
elseif entry.level == 1 then
page = {
id = ((section and section .. "_" or "") .. entry.name):lower():gsub(" ", "_"),
title = entry.readable_name or entry.name,
section = section,
content = {},
}
page = add_page(page)
elseif entry.level == 2 then
ensure_page_started()
page.content[#page.content + 1] = {
heading = fgettext_ne(entry.readable_name or entry.name),
}
end
else
ensure_page_started()
page.content[#page.content + 1] = entry.name
end
end
end
local function load()
if loaded then
return
end
loaded = true
full_settings = settingtypes.parse_config_file(false, true)
local change_keys = {
query_text = "Controls",
requires = {
touch_controls = false,
},
get_formspec = function(self, avail_w)
local btn_w = math.min(avail_w, 3)
return ("button[0,0;%f,0.8;btn_change_keys;%s]"):format(btn_w, fgettext("Controls")), 0.8
end,
on_submit = function(self, fields)
if fields.btn_change_keys then
core.show_keys_menu()
end
end,
}
add_page({
id = "accessibility",
title = fgettext_ne("Accessibility"),
content = {
"language",
{ heading = fgettext_ne("General") },
"font_size",
"chat_font_size",
"gui_scaling",
"hud_scaling",
"show_nametag_backgrounds",
{ heading = fgettext_ne("Chat") },
"console_height",
"console_alpha",
"console_color",
{ heading = fgettext_ne("Controls") },
"autojump",
"safe_dig_and_place",
{ heading = fgettext_ne("Movement") },
"arm_inertia",
"view_bobbing_amount",
"fall_bobbing_amount",
},
})
load_settingtypes()
table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys)
do
local content = page_by_id.graphics_and_audio_effects.content
local idx = table.indexof(content, "enable_dynamic_shadows")
table.insert(content, idx, shadows_component)
idx = table.indexof(content, "enable_auto_exposure") + 1
local note = component_funcs.note(fgettext_ne("(The game will need to enable automatic exposure as well)"))
note.requires = get_setting_info("enable_auto_exposure").requires
table.insert(content, idx, note)
idx = table.indexof(content, "enable_volumetric_lighting") + 1
note = component_funcs.note(fgettext_ne("(The game will need to enable volumetric lighting as well)"))
note.requires = get_setting_info("enable_volumetric_lighting").requires
table.insert(content, idx, note)
end
-- These must not be translated, as they need to show in the local
-- language no matter the user's current language.
-- This list must be kept in sync with src/unsupported_language_list.txt.
get_setting_info("language").option_labels = {
[""] = fgettext_ne("(Use system language)"),
--ar = " [ar]", blacklisted
be = "Беларуская [be]",
bg = "Български [bg]",
ca = "Català [ca]",
cs = "Česky [cs]",
cy = "Cymraeg [cy]",
da = "Dansk [da]",
de = "Deutsch [de]",
--dv = " [dv]", blacklisted
el = "Ελληνικά [el]",
en = "English [en]",
eo = "Esperanto [eo]",
es = "Español [es]",
et = "Eesti [et]",
eu = "Euskara [eu]",
fi = "Suomi [fi]",
fil = "Wikang Filipino [fil]",
fr = "Français [fr]",
gd = "Gàidhlig [gd]",
gl = "Galego [gl]",
--he = " [he]", blacklisted
--hi = " [hi]", blacklisted
hu = "Magyar [hu]",
id = "Bahasa Indonesia [id]",
it = "Italiano [it]",
ja = "日本語 [ja]",
jbo = "Lojban [jbo]",
kk = "Қазақша [kk]",
--kn = " [kn]", blacklisted
ko = "한국어 [ko]",
ky = "Kırgızca / Кыргызча [ky]",
lt = "Lietuvių [lt]",
lv = "Latviešu [lv]",
mn = "Монгол [mn]",
mr = "मराठी [mr]",
ms = "Bahasa Melayu [ms]",
--ms_Arab = " [ms_Arab]", blacklisted
nb = "Norsk Bokmål [nb]",
nl = "Nederlands [nl]",
nn = "Norsk Nynorsk [nn]",
oc = "Occitan [oc]",
pl = "Polski [pl]",
pt = "Português [pt]",
pt_BR = "Português do Brasil [pt_BR]",
ro = "Română [ro]",
ru = "Русский [ru]",
sk = "Slovenčina [sk]",
sl = "Slovenščina [sl]",
sr_Cyrl = "Српски [sr_Cyrl]",
sr_Latn = "Srpski (Latinica) [sr_Latn]",
sv = "Svenska [sv]",
sw = "Kiswahili [sw]",
--th = " [th]", blacklisted
tr = "Türkçe [tr]",
tt = "Tatarça [tt]",
uk = "Українська [uk]",
vi = "Tiếng Việt [vi]",
zh_CN = "中文 (简体) [zh_CN]",
zh_TW = "正體中文 (繁體) [zh_TW]",
}
end
-- See if setting matches keywords
local function get_setting_match_weight(entry, query_keywords)
local setting_score = 0
for _, keyword in ipairs(query_keywords) do
if string.find(entry.name:lower(), keyword, 1, true) then
setting_score = setting_score + 1
end
if entry.readable_name and
string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then
setting_score = setting_score + 1
end
if entry.comment and
string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then
setting_score = setting_score + 1
end
end
return setting_score
end
local function filter_page_content(page, query_keywords)
if #query_keywords == 0 then
return page.content, 0
end
local retval = {}
local i = 1
local max_weight = 0
for _, content in ipairs(page.content) do
if type(content) == "string" then
local setting = get_setting_info(content)
assert(setting, "Unknown setting: " .. content)
local weight = get_setting_match_weight(setting, query_keywords)
if weight > 0 then
max_weight = math.max(max_weight, weight)
retval[i] = content
i = i + 1
end
elseif type(content) == "table" and content.query_text then
for _, keyword in ipairs(query_keywords) do
if string.find(fgettext(content.query_text), keyword, 1, true) then
max_weight = math.max(max_weight, 1)
retval[i] = content
i = i + 1
break
end
end
end
end
return retval, max_weight
end
local function update_filtered_pages(query)
filtered_pages = {}
filtered_page_by_id = {}
local query_keywords = {}
for word in query:lower():gmatch("%S+") do
table.insert(query_keywords, word)
end
local best_page = nil
local best_page_weight = -1
for _, page in ipairs(all_pages) do
local content, page_weight = filter_page_content(page, query_keywords)
if page_has_contents(page, content) then
local new_page = table.copy(page)
new_page.content = content
filtered_pages[#filtered_pages + 1] = new_page
filtered_page_by_id[new_page.id] = new_page
if page_weight > best_page_weight then
best_page = new_page
best_page_weight = page_weight
end
end
end
return best_page and best_page.id or nil
end
local function check_requirements(name, requires)
if requires == nil then
return true
end
local video_driver = core.get_active_driver()
local shaders_support = video_driver == "opengl" or video_driver == "opengl3" or video_driver == "ogles2"
local special = {
android = PLATFORM == "Android",
desktop = PLATFORM ~= "Android",
shaders_support = shaders_support,
shaders = core.settings:get_bool("enable_shaders") and shaders_support,
opengl = video_driver == "opengl",
gles = video_driver:sub(1, 5) == "ogles",
}
for req_key, req_value in pairs(requires) do
if special[req_key] == nil then
local required_setting = get_setting_info(req_key)
if required_setting == nil then
core.log("warning", "Unknown setting " .. req_key .. " required by " .. name)
end
local actual_value = core.settings:get_bool(req_key,
required_setting and core.is_yes(required_setting.default))
if actual_value ~= req_value then
return false
end
elseif special[req_key] ~= req_value then
return false
end
end
return true
end
function page_has_contents(page, actual_content)
local is_advanced =
page.id:sub(1, #"client_and_server") == "client_and_server" or
page.id:sub(1, #"mapgen") == "mapgen" or
page.id:sub(1, #"advanced") == "advanced"
local show_advanced = core.settings:get_bool("show_advanced")
if is_advanced and not show_advanced then
return false
end
for _, item in ipairs(actual_content) do
if item == false or item.heading then --luacheck: ignore
-- skip
elseif type(item) == "string" then
local setting = get_setting_info(item)
assert(setting, "Unknown setting: " .. item)
if check_requirements(setting.name, setting.requires) then
return true
end
elseif item.get_formspec then
if check_requirements(item.id, item.requires) then
return true
end
else
error("Unknown content in page: " .. dump(item))
end
end
return false
end
local function build_page_components(page)
-- Filter settings based on requirements
local content = {}
local last_heading
for _, item in ipairs(page.content) do
if item == false then --luacheck: ignore
-- skip
elseif item.heading then
last_heading = item
else
local name, requires
if type(item) == "string" then
local setting = get_setting_info(item)
assert(setting, "Unknown setting: " .. item)
name = setting.name
requires = setting.requires
elseif item.get_formspec then
name = item.id
requires = item.requires
else
error("Unknown content in page: " .. dump(item))
end
if check_requirements(name, requires) then
if last_heading then
content[#content + 1] = last_heading
last_heading = nil
end
content[#content + 1] = item
end
end
end
-- Create components
local retval = {}
for i, item in ipairs(content) do
if type(item) == "string" then
local setting = get_setting_info(item)
local component_func = component_funcs[setting.type]
assert(component_func, "Unknown setting type: " .. setting.type)
retval[i] = component_func(setting)
elseif item.get_formspec then
retval[i] = item
elseif item.heading then
retval[i] = component_funcs.heading(item.heading)
end
end
return retval
end
--- Creates a scrollbaroptions for a scroll_container
--
-- @param visible_l the length of the scroll_container and scrollbar
-- @param total_l length of the scrollable area
-- @param scroll_factor as passed to scroll_container
local function make_scrollbaroptions_for_scroll_container(visible_l, total_l, scroll_factor)
assert(total_l >= visible_l)
local max = total_l - visible_l
local thumb_size = (visible_l / total_l) * max
return ("scrollbaroptions[min=0;max=%f;thumbsize=%f]"):format(max / scroll_factor, thumb_size / scroll_factor)
end
local formspec_show_hack = false
local function get_formspec(dialogdata)
local page_id = dialogdata.page_id or "accessibility"
local page = filtered_page_by_id[page_id]
local extra_h = 1 -- not included in tabsize.height
local tabsize = {
width = core.settings:get_bool("touch_gui") and 16.5 or 15.5,
height = core.settings:get_bool("touch_gui") and (10 - extra_h) or 12,
}
local scrollbar_w = core.settings:get_bool("touch_gui") and 0.6 or 0.4
local left_pane_width = core.settings:get_bool("touch_gui") and 4.5 or 4.25
local left_pane_padding = 0.25
local search_width = left_pane_width + scrollbar_w - (0.75 * 2)
local back_w = 3
local checkbox_w = (tabsize.width - back_w - 2*0.2) / 2
local show_technical_names = core.settings:get_bool("show_technical_names")
local show_advanced = core.settings:get_bool("show_advanced")
formspec_show_hack = not formspec_show_hack
local fs = {
"formspec_version[6]",
"size[", tostring(tabsize.width), ",", tostring(tabsize.height + extra_h), "]",
core.settings:get_bool("touch_gui") and "padding[0.01,0.01]" or "",
"bgcolor[#0000]",
-- HACK: this is needed to allow resubmitting the same formspec
formspec_show_hack and " " or "",
"box[0,0;", tostring(tabsize.width), ",", tostring(tabsize.height), ";#0000008C]",
("button[0,%f;%f,0.8;back;%s]"):format(
tabsize.height + 0.2, back_w, fgettext("Back")),
("box[%f,%f;%f,0.8;#0000008C]"):format(
back_w + 0.2, tabsize.height + 0.2, checkbox_w),
("checkbox[%f,%f;show_technical_names;%s;%s]"):format(
back_w + 2*0.2, tabsize.height + 0.6,
fgettext("Show technical names"), tostring(show_technical_names)),
("box[%f,%f;%f,0.8;#0000008C]"):format(
back_w + 2*0.2 + checkbox_w, tabsize.height + 0.2, checkbox_w),
("checkbox[%f,%f;show_advanced;%s;%s]"):format(
back_w + 3*0.2 + checkbox_w, tabsize.height + 0.6,
fgettext("Show advanced settings"), tostring(show_advanced)),
"field[0.25,0.25;", tostring(search_width), ",0.75;search_query;;",
core.formspec_escape(dialogdata.query or ""), "]",
"field_enter_after_edit[search_query;true]",
"container[", tostring(search_width + 0.25), ", 0.25]",
"image_button[0,0;0.75,0.75;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
"image_button[0.75,0;0.75,0.75;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";search_clear;]",
"tooltip[search;", fgettext("Search"), "]",
"tooltip[search_clear;", fgettext("Clear"), "]",
"container_end[]",
"scroll_container[0.25,1.25;", tostring(left_pane_width), ",",
tostring(tabsize.height - 1.5), ";leftscroll;vertical;0.1]",
"style_type[button;border=false;bgcolor=#3333]",
"style_type[button:hover;border=false;bgcolor=#6663]",
}
local y = 0
local last_section = nil
for _, other_page in ipairs(filtered_pages) do
if other_page.section ~= last_section then
fs[#fs + 1] = ("label[0.1,%f;%s]"):format(
y + 0.41, core.colorize("#ff0", fgettext(other_page.section)))
last_section = other_page.section
y = y + 0.82
end
fs[#fs + 1] = ("box[0,%f;%f,0.8;%s]"):format(
y, left_pane_width-left_pane_padding, other_page.id == page_id and "#467832FF" or "#3339")
fs[#fs + 1] = ("button[0,%f;%f,0.8;page_%s;%s]")
:format(y, left_pane_width-left_pane_padding, other_page.id, fgettext(other_page.title))
y = y + 0.82
end
if #filtered_pages == 0 then
fs[#fs + 1] = "label[0.1,0.41;"
fs[#fs + 1] = fgettext("No results")
fs[#fs + 1] = "]"
end
fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height - 1.25 then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height - 1.5, y, 0.1)
fs[#fs + 1] = ("scrollbar[%f,1.25;%f,%f;vertical;leftscroll;%f]"):format(
left_pane_width + 0.25, scrollbar_w, tabsize.height - 1.5, dialogdata.leftscroll or 0)
end
fs[#fs + 1] = "style_type[button;border=;bgcolor=]"
if not dialogdata.components then
dialogdata.components = page and build_page_components(page) or {}
end
local right_pane_width = tabsize.width - left_pane_width - 0.375 - 2*scrollbar_w - 0.25
fs[#fs + 1] = ("scroll_container[%f,0;%f,%f;rightscroll;vertical;0.1]"):format(
tabsize.width - right_pane_width - scrollbar_w, right_pane_width, tabsize.height)
y = 0.25
for i, comp in ipairs(dialogdata.components) do
fs[#fs + 1] = ("container[0,%f]"):format(y)
local avail_w = right_pane_width - 0.25
if not comp.full_width then
avail_w = avail_w - 1.4
end
if comp.max_w then
avail_w = math.min(avail_w, comp.max_w)
end
local comp_fs, used_h = comp:get_formspec(avail_w)
fs[#fs + 1] = comp_fs
fs[#fs + 1] = "style_type[image_button;border=false;padding=]"
local show_reset = comp.resettable and comp.setting
local show_info = comp.info_text and comp.info_text ~= ""
if show_reset or show_info then
-- ensure there's enough space for reset/info
used_h = math.max(used_h, 0.5)
end
local info_reset_y = used_h / 2 - 0.25
if show_reset then
local default = comp.setting.default
local reset_tooltip = default and
fgettext("Reset setting to default ($1)", tostring(default)) or
fgettext("Reset setting to default")
fs[#fs + 1] = ("image_button[%f,%f;0.5,0.5;%s;%s;]"):format(
right_pane_width - 1.4, info_reset_y, reset_icon_path, "reset_" .. i)
fs[#fs + 1] = ("tooltip[%s;%s]"):format("reset_" .. i, reset_tooltip)
end
if show_info then
local info_x = right_pane_width - 0.75
fs[#fs + 1] = ("image[%f,%f;0.5,0.5;%s]"):format(info_x, info_reset_y, info_icon_path)
fs[#fs + 1] = ("tooltip[%f,%f;0.5,0.5;%s]"):format(info_x, info_reset_y, fgettext(comp.info_text))
end
fs[#fs + 1] = "style_type[image_button;border=;padding=]"
fs[#fs + 1] = "container_end[]"
if used_h > 0 then
y = y + used_h + 0.25
end
end
fs[#fs + 1] = "scroll_container_end[]"
if y >= tabsize.height then
fs[#fs + 1] = make_scrollbaroptions_for_scroll_container(tabsize.height, y + 0.375, 0.1)
fs[#fs + 1] = ("scrollbar[%f,0;%f,%f;vertical;rightscroll;%f]"):format(
tabsize.width - scrollbar_w, scrollbar_w, tabsize.height, dialogdata.rightscroll or 0)
end
return table.concat(fs, "")
end
-- On Android, closing the app via the "Recents screen" won't result in a clean
-- exit, discarding any setting changes made by the user.
-- To avoid that, we write the settings file in more cases on Android.
function write_settings_early()
if PLATFORM == "Android" then
core.settings:write()
end
end
local function buttonhandler(this, fields)
local dialogdata = this.data
dialogdata.leftscroll = core.explode_scrollbar_event(fields.leftscroll).value or dialogdata.leftscroll
dialogdata.rightscroll = core.explode_scrollbar_event(fields.rightscroll).value or dialogdata.rightscroll
dialogdata.query = fields.search_query
if fields.back then
this:delete()
return true
end
if fields.show_technical_names ~= nil then
local value = core.is_yes(fields.show_technical_names)
core.settings:set_bool("show_technical_names", value)
write_settings_early()
return true
end
if fields.show_advanced ~= nil then
local value = core.is_yes(fields.show_advanced)
core.settings:set_bool("show_advanced", value)
write_settings_early()
end
-- touch_controls is a checkbox in a setting component. We handle this
-- setting differently so we can hide/show pages using the next if-statement
if fields.touch_controls ~= nil then
local value = core.is_yes(fields.touch_controls)
core.settings:set_bool("touch_controls", value)
write_settings_early()
end
if fields.show_advanced ~= nil or fields.touch_controls ~= nil then
local suggested_page_id = update_filtered_pages(dialogdata.query)
dialogdata.components = nil
if not filtered_page_by_id[dialogdata.page_id] then
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = suggested_page_id
end
return true
end
if fields.search or fields.key_enter_field == "search_query" then
dialogdata.components = nil
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = update_filtered_pages(dialogdata.query)
return true
end
if fields.search_clear then
dialogdata.query = ""
dialogdata.components = nil
dialogdata.leftscroll = 0
dialogdata.rightscroll = 0
dialogdata.page_id = update_filtered_pages("")
return true
end
for _, page in ipairs(all_pages) do
if fields["page_" .. page.id] then
dialogdata.page_id = page.id
dialogdata.components = nil
dialogdata.rightscroll = 0
return true
end
end
for i, comp in ipairs(dialogdata.components) do
if comp.on_submit and comp:on_submit(fields, this) then
write_settings_early()
-- Clear components so they regenerate
dialogdata.components = nil
return true
end
if comp.setting and fields["reset_" .. i] then
core.settings:remove(comp.setting.name)
write_settings_early()
-- Clear components so they regenerate
dialogdata.components = nil
return true
end
end
return false
end
local function eventhandler(event)
if event == "DialogShow" then
-- Don't show the "MINETEST" header behind the dialog.
mm_game_theme.set_engine(true)
return true
end
if event == "FullscreenChange" then
-- Refresh the formspec to keep the fullscreen checkbox up to date.
ui.update()
return true
end
return false
end
function create_settings_dlg()
load()
local dlg = dialog_create("dlg_settings", get_formspec, buttonhandler, eventhandler)
dlg.data.page_id = update_filtered_pages("")
return dlg
end