--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 full_settings = settingtypes.parse_config_file(false, true) local info_icon_path = core.formspec_escape(defaulttexturedir .. "settings_info.png") local reset_icon_path = core.formspec_escape(defaulttexturedir .. "settings_reset.png") local gettext = fgettext_ne local all_pages = {} local page_by_id = {} local filtered_pages = all_pages local filtered_page_by_id = page_by_id 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 change_keys = { query_text = "Change keys", 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("Change keys")), 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 = gettext("Accessibility"), content = { "font_size", "chat_font_size", "gui_scaling", "hud_scaling", "show_nametag_backgrounds", { heading = gettext("Chat") }, "console_height", "console_alpha", "console_color", { heading = gettext("Controls") }, "autojump", "safe_dig_and_place", { heading = gettext("Movement") }, "arm_inertia", "view_bobbing_amount", "fall_bobbing_amount", }, }) 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 gettext("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 = {}, } if page.title:sub(1, 5) ~= "Hide:" then page = add_page(page) end elseif entry.level == 2 then ensure_page_started() page.content[#page.content + 1] = { heading = gettext(entry.readable_name or entry.name), } end else ensure_page_started() page.content[#page.content + 1] = entry.name end end end load_settingtypes() table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys) do local content = page_by_id.graphics_and_audio_shaders.content local idx = table.indexof(content, "enable_dynamic_shadows") table.insert(content, idx, shadows_component) end 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 -- 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 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 = {} if query == "" or query == nil then filtered_pages = all_pages filtered_page_by_id = page_by_id return filtered_pages[1].id end 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 #content > 0 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 build_page_components(page) local retval = {} local j = 1 for i, content in ipairs(page.content) do if content == false then -- false is used to disable components conditionally (ie: Android specific) j = j - 1 elseif type(content) == "string" then local setting = get_setting_info(content) assert(setting, "Unknown setting: " .. content) local component_func = component_funcs[setting.type] assert(component_func, "Unknown setting type: " .. setting.type) retval[j] = component_func(setting) elseif content.get_formspec then retval[j] = content elseif content.heading then retval[j] = component_funcs.heading(content.heading) else error("Unknown content in page: " .. dump(content)) end j = j + 1 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 "most_used" local page = filtered_page_by_id[page_id] local extra_h = 1 -- not included in tabsize.height local tabsize = { width = TOUCHSCREEN_GUI and 16.5 or 15.5, height = TOUCHSCREEN_GUI and (10 - extra_h) or 12, } local scrollbar_w = TOUCHSCREEN_GUI and 0.6 or 0.4 local left_pane_width = TOUCHSCREEN_GUI and 4.5 or 4.25 local search_width = left_pane_width + scrollbar_w - (0.75 * 2) local technical_names_w = TOUCHSCREEN_GUI and 6 or 5 local show_technical_names = core.settings:get_bool("show_technical_names") formspec_show_hack = not formspec_show_hack local fs = { "formspec_version[6]", "size[", tostring(tabsize.width), ",", tostring(tabsize.height + extra_h), "]", TOUCHSCREEN_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,", tostring(tabsize.height + 0.2), ";3,0.8;back;", fgettext("Back"), "]", ("box[%f,%f;%f,0.8;#0000008C]"):format( tabsize.width - technical_names_w, tabsize.height + 0.2, technical_names_w), ("checkbox[%f,%f;show_technical_names;%s;%s]"):format( tabsize.width - technical_names_w + 0.25, tabsize.height + 0.6, fgettext("Show technical names"), tostring(show_technical_names)), "field[0.25,0.25;", tostring(search_width), ",0.75;search_query;;", core.formspec_escape(dialogdata.query or ""), "]", "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, 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, 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 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) 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 return true end if comp.setting and fields["reset_" .. i] then core.settings:remove(comp.setting.name) return true end end return false end function create_settings_dlg() local dlg = dialog_create("dlg_settings", get_formspec, buttonhandler, nil) dlg.data.page_id = update_filtered_pages("") return dlg end