]> git.lizzy.rs Git - minetest.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
Cleanup global namespace pollution in builtin (#9451)
[minetest.git] / builtin / mainmenu / dlg_settings_advanced.lua
1 --Minetest
2 --Copyright (C) 2015 PilzAdam
3 --
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
8 --
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 --GNU Lesser General Public License for more details.
13 --
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 local FILENAME = "settingtypes.txt"
19
20 local CHAR_CLASSES = {
21         SPACE = "[%s]",
22         VARIABLE = "[%w_%-%.]",
23         INTEGER = "[+-]?[%d]",
24         FLOAT = "[+-]?[%d%.]",
25         FLAGS = "[%w_%-%.,]",
26 }
27
28 local function flags_to_table(flags)
29         return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split
30 end
31
32 -- returns error message, or nil
33 local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
34         -- comment
35         local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
36         if comment then
37                 if settings.current_comment == "" then
38                         settings.current_comment = comment
39                 else
40                         settings.current_comment = settings.current_comment .. "\n" .. comment
41                 end
42                 return
43         end
44
45         -- clear current_comment so only comments directly above a setting are bound to it
46         -- but keep a local reference to it for variables in the current line
47         local current_comment = settings.current_comment
48         settings.current_comment = ""
49
50         -- empty lines
51         if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
52                 return
53         end
54
55         -- category
56         local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
57         if category then
58                 table.insert(settings, {
59                         name = category,
60                         level = stars:len() + base_level,
61                         type = "category",
62                 })
63                 return
64         end
65
66         -- settings
67         local first_part, name, readable_name, setting_type = line:match("^"
68                         -- this first capture group matches the whole first part,
69                         --  so we can later strip it from the rest of the line
70                         .. "("
71                                 .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
72                                 .. CHAR_CLASSES.SPACE .. "*"
73                                 .. "%(([^%)]*)%)"  -- readable name
74                                 .. CHAR_CLASSES.SPACE .. "*"
75                                 .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
76                                 .. CHAR_CLASSES.SPACE .. "*"
77                         .. ")")
78
79         if not first_part then
80                 return "Invalid line"
81         end
82
83         if name:match("secure%.[.]*") and not allow_secure then
84                 return "Tried to add \"secure.\" setting"
85         end
86
87         if readable_name == "" then
88                 readable_name = nil
89         end
90         local remaining_line = line:sub(first_part:len() + 1)
91
92         if setting_type == "int" then
93                 local default, min, max = remaining_line:match("^"
94                                 -- first int is required, the last 2 are optional
95                                 .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*"
96                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*"
97                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)"
98                                 .. "$")
99
100                 if not default or not tonumber(default) then
101                         return "Invalid integer setting"
102                 end
103
104                 min = tonumber(min)
105                 max = tonumber(max)
106                 table.insert(settings, {
107                         name = name,
108                         readable_name = readable_name,
109                         type = "int",
110                         default = default,
111                         min = min,
112                         max = max,
113                         comment = current_comment,
114                 })
115                 return
116         end
117
118         if setting_type == "string"
119                         or setting_type == "key" or setting_type == "v3f" then
120                 local default = remaining_line:match("^(.*)$")
121
122                 if not default then
123                         return "Invalid string setting"
124                 end
125                 if setting_type == "key" and not read_all then
126                         -- ignore key type if read_all is false
127                         return
128                 end
129
130                 table.insert(settings, {
131                         name = name,
132                         readable_name = readable_name,
133                         type = setting_type,
134                         default = default,
135                         comment = current_comment,
136                 })
137                 return
138         end
139
140         if setting_type == "noise_params_2d"
141                         or setting_type == "noise_params_3d" then
142                 local default = remaining_line:match("^(.*)$")
143
144                 if not default then
145                         return "Invalid string setting"
146                 end
147
148                 local values = {}
149                 local ti = 1
150                 local index = 1
151                 for match in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
152                         index = default:find("[+-]?[%d.-e]+", index) + match:len()
153                         table.insert(values, match)
154                         ti = ti + 1
155                         if ti > 9 then
156                                 break
157                         end
158                 end
159                 index = default:find("[^, ]", index)
160                 local flags = ""
161                 if index then
162                         flags = default:sub(index)
163                         default = default:sub(1, index - 3) -- Make sure no flags in single-line format
164                 end
165                 table.insert(values, flags)
166
167                 table.insert(settings, {
168                         name = name,
169                         readable_name = readable_name,
170                         type = setting_type,
171                         default = default,
172                         default_table = {
173                                 offset = values[1],
174                                 scale = values[2],
175                                 spread = {
176                                         x = values[3],
177                                         y = values[4],
178                                         z = values[5]
179                                 },
180                                 seed = values[6],
181                                 octaves = values[7],
182                                 persistence = values[8],
183                                 lacunarity = values[9],
184                                 flags = values[10]
185                         },
186                         values = values,
187                         comment = current_comment,
188                         noise_params = true,
189                         flags = flags_to_table("defaults,eased,absvalue")
190                 })
191                 return
192         end
193
194         if setting_type == "bool" then
195                 if remaining_line ~= "false" and remaining_line ~= "true" then
196                         return "Invalid boolean setting"
197                 end
198
199                 table.insert(settings, {
200                         name = name,
201                         readable_name = readable_name,
202                         type = "bool",
203                         default = remaining_line,
204                         comment = current_comment,
205                 })
206                 return
207         end
208
209         if setting_type == "float" then
210                 local default, min, max = remaining_line:match("^"
211                                 -- first float is required, the last 2 are optional
212                                 .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*"
213                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*"
214                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)"
215                                 .."$")
216
217                 if not default or not tonumber(default) then
218                         return "Invalid float setting"
219                 end
220
221                 min = tonumber(min)
222                 max = tonumber(max)
223                 table.insert(settings, {
224                         name = name,
225                         readable_name = readable_name,
226                         type = "float",
227                         default = default,
228                         min = min,
229                         max = max,
230                         comment = current_comment,
231                 })
232                 return
233         end
234
235         if setting_type == "enum" then
236                 local default, values = remaining_line:match("^"
237                                 -- first value (default) may be empty (i.e. is optional)
238                                 .. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*"
239                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)"
240                                 .. "$")
241
242                 if not default or values == "" then
243                         return "Invalid enum setting"
244                 end
245
246                 table.insert(settings, {
247                         name = name,
248                         readable_name = readable_name,
249                         type = "enum",
250                         default = default,
251                         values = values:split(",", true),
252                         comment = current_comment,
253                 })
254                 return
255         end
256
257         if setting_type == "path" or setting_type == "filepath" then
258                 local default = remaining_line:match("^(.*)$")
259
260                 if not default then
261                         return "Invalid path setting"
262                 end
263
264                 table.insert(settings, {
265                         name = name,
266                         readable_name = readable_name,
267                         type = setting_type,
268                         default = default,
269                         comment = current_comment,
270                 })
271                 return
272         end
273
274         if setting_type == "flags" then
275                 local default, possible = remaining_line:match("^"
276                                 -- first value (default) may be empty (i.e. is optional)
277                                 -- this is implemented by making the last value optional, and
278                                 -- swapping them around if it turns out empty.
279                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*"
280                                 .. "(" .. CHAR_CLASSES.FLAGS .. "*)"
281                                 .. "$")
282
283                 if not default or not possible then
284                         return "Invalid flags setting"
285                 end
286
287                 if possible == "" then
288                         possible = default
289                         default = ""
290                 end
291
292                 table.insert(settings, {
293                         name = name,
294                         readable_name = readable_name,
295                         type = "flags",
296                         default = default,
297                         possible = flags_to_table(possible),
298                         comment = current_comment,
299                 })
300                 return
301         end
302
303         return "Invalid setting type \"" .. setting_type .. "\""
304 end
305
306 local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
307         -- store this helper variable in the table so it's easier to pass to parse_setting_line()
308         result.current_comment = ""
309
310         local line = file:read("*line")
311         while line do
312                 local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
313                 if error_msg then
314                         core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
315                 end
316                 line = file:read("*line")
317         end
318
319         result.current_comment = nil
320 end
321
322 -- read_all: whether to ignore certain setting types for GUI or not
323 -- parse_mods: whether to parse settingtypes.txt in mods and games
324 local function parse_config_file(read_all, parse_mods)
325         local settings = {}
326
327         do
328                 local builtin_path = core.get_builtin_path() .. FILENAME
329                 local file = io.open(builtin_path, "r")
330                 if not file then
331                         core.log("error", "Can't load " .. FILENAME)
332                         return settings
333                 end
334
335                 parse_single_file(file, builtin_path, read_all, settings, 0, true)
336
337                 file:close()
338         end
339
340         if parse_mods then
341                 -- Parse games
342                 local games_category_initialized = false
343                 local index = 1
344                 local game = pkgmgr.get_game(index)
345                 while game do
346                         local path = game.path .. DIR_DELIM .. FILENAME
347                         local file = io.open(path, "r")
348                         if file then
349                                 if not games_category_initialized then
350                                         fgettext_ne("Games") -- not used, but needed for xgettext
351                                         table.insert(settings, {
352                                                 name = "Games",
353                                                 level = 0,
354                                                 type = "category",
355                                         })
356                                         games_category_initialized = true
357                                 end
358
359                                 table.insert(settings, {
360                                         name = game.name,
361                                         level = 1,
362                                         type = "category",
363                                 })
364
365                                 parse_single_file(file, path, read_all, settings, 2, false)
366
367                                 file:close()
368                         end
369
370                         index = index + 1
371                         game = pkgmgr.get_game(index)
372                 end
373
374                 -- Parse mods
375                 local mods_category_initialized = false
376                 local mods = {}
377                 get_mods(core.get_modpath(), mods)
378                 for _, mod in ipairs(mods) do
379                         local path = mod.path .. DIR_DELIM .. FILENAME
380                         local file = io.open(path, "r")
381                         if file then
382                                 if not mods_category_initialized then
383                                         fgettext_ne("Mods") -- not used, but needed for xgettext
384                                         table.insert(settings, {
385                                                 name = "Mods",
386                                                 level = 0,
387                                                 type = "category",
388                                         })
389                                         mods_category_initialized = true
390                                 end
391
392                                 table.insert(settings, {
393                                         name = mod.name,
394                                         level = 1,
395                                         type = "category",
396                                 })
397
398                                 parse_single_file(file, path, read_all, settings, 2, false)
399
400                                 file:close()
401                         end
402                 end
403         end
404
405         return settings
406 end
407
408 local function filter_settings(settings, searchstring)
409         if not searchstring or searchstring == "" then
410                 return settings, -1
411         end
412
413         -- Setup the keyword list
414         local keywords = {}
415         for word in searchstring:lower():gmatch("%S+") do
416                 table.insert(keywords, word)
417         end
418
419         local result = {}
420         local category_stack = {}
421         local current_level = 0
422         local best_setting = nil
423         for _, entry in pairs(settings) do
424                 if entry.type == "category" then
425                         -- Remove all settingless categories
426                         while #category_stack > 0 and entry.level <= current_level do
427                                 table.remove(category_stack, #category_stack)
428                                 if #category_stack > 0 then
429                                         current_level = category_stack[#category_stack].level
430                                 else
431                                         current_level = 0
432                                 end
433                         end
434
435                         -- Push category onto stack
436                         category_stack[#category_stack + 1] = entry
437                         current_level = entry.level
438                 else
439                         -- See if setting matches keywords
440                         local setting_score = 0
441                         for k = 1, #keywords do
442                                 local keyword = keywords[k]
443
444                                 if string.find(entry.name:lower(), keyword, 1, true) then
445                                         setting_score = setting_score + 1
446                                 end
447
448                                 if entry.readable_name and
449                                                 string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then
450                                         setting_score = setting_score + 1
451                                 end
452
453                                 if entry.comment and
454                                                 string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then
455                                         setting_score = setting_score + 1
456                                 end
457                         end
458
459                         -- Add setting to results if match
460                         if setting_score > 0 then
461                                 -- Add parent categories
462                                 for _, category in pairs(category_stack) do
463                                         result[#result + 1] = category
464                                 end
465                                 category_stack = {}
466
467                                 -- Add setting
468                                 result[#result + 1] = entry
469                                 entry.score = setting_score
470
471                                 if not best_setting or
472                                                 setting_score > result[best_setting].score then
473                                         best_setting = #result
474                                 end
475                         end
476                 end
477         end
478         return result, best_setting or -1
479 end
480
481 local full_settings = parse_config_file(false, true)
482 local search_string = ""
483 local settings = full_settings
484 local selected_setting = 1
485
486 local function get_current_value(setting)
487         local value = core.settings:get(setting.name)
488         if value == nil then
489                 value = setting.default
490         end
491         return value
492 end
493
494 local function get_current_np_group(setting)
495         local value = core.settings:get_np_group(setting.name)
496         local t = {}
497         if value == nil then
498                 t = setting.values
499         else
500                 table.insert(t, value.offset)
501                 table.insert(t, value.scale)
502                 table.insert(t, value.spread.x)
503                 table.insert(t, value.spread.y)
504                 table.insert(t, value.spread.z)
505                 table.insert(t, value.seed)
506                 table.insert(t, value.octaves)
507                 table.insert(t, value.persistence)
508                 table.insert(t, value.lacunarity)
509                 table.insert(t, value.flags)
510         end
511         return t
512 end
513
514 local function get_current_np_group_as_string(setting)
515         local value = core.settings:get_np_group(setting.name)
516         local t
517         if value == nil then
518                 t = setting.default
519         else
520                 t = value.offset .. ", " ..
521                         value.scale .. ", (" ..
522                         value.spread.x .. ", " ..
523                         value.spread.y .. ", " ..
524                         value.spread.z .. "), " ..
525                         value.seed .. ", " ..
526                         value.octaves .. ", " ..
527                         value.persistence .. ", " ..
528                         value.lacunarity
529                 if value.flags ~= "" then
530                         t = t .. ", " .. value.flags
531                 end
532         end
533         return t
534 end
535
536 local checkboxes = {} -- handle checkboxes events
537
538 local function create_change_setting_formspec(dialogdata)
539         local setting = settings[selected_setting]
540         -- Final formspec will be created at the end of this function
541         -- Default values below, may be changed depending on setting type
542         local width = 10
543         local height = 3.5
544         local description_height = 3
545         local formspec = ""
546
547         -- Setting-specific formspec elements
548         if setting.type == "bool" then
549                 local selected_index = 1
550                 if core.is_yes(get_current_value(setting)) then
551                         selected_index = 2
552                 end
553                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
554                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
555                                 .. selected_index .. "]"
556                 height = height + 1.25
557
558         elseif setting.type == "enum" then
559                 local selected_index = 0
560                 formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;"
561                 for index, value in ipairs(setting.values) do
562                         -- translating value is not possible, since it's the value
563                         --  that we set the setting to
564                         formspec = formspec ..  core.formspec_escape(value) .. ","
565                         if get_current_value(setting) == value then
566                                 selected_index = index
567                         end
568                 end
569                 if #setting.values > 0 then
570                         formspec = formspec:sub(1, -2) -- remove trailing comma
571                 end
572                 formspec = formspec .. ";" .. selected_index .. "]"
573                 height = height + 1.25
574
575         elseif setting.type == "path" or setting.type == "filepath" then
576                 local current_value = dialogdata.selected_path
577                 if not current_value then
578                         current_value = get_current_value(setting)
579                 end
580                 formspec = "field[0.28," .. height + 0.15 .. ";8,1;te_setting_value;;"
581                                 .. core.formspec_escape(current_value) .. "]"
582                                 .. "button[8," .. height - 0.15 .. ";2,1;btn_browser_"
583                                 .. setting.type .. ";" .. fgettext("Browse") .. "]"
584                 height = height + 1.15
585
586         elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
587                 local t = get_current_np_group(setting)
588                 local dimension = 3
589                 if setting.type == "noise_params_2d" then
590                         dimension = 2
591                 end
592
593                 -- More space for 3x3 fields
594                 description_height = description_height - 1.5
595                 height = height - 1.5
596
597                 local fields = {}
598                 local function add_field(x, name, label, value)
599                         fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format(
600                                 x, height, name, label, core.formspec_escape(value or "")
601                         )
602                 end
603                 -- First row
604                 height = height + 0.3
605                 add_field(0.3, "te_offset", fgettext("Offset"), t[1])
606                 add_field(3.6, "te_scale",  fgettext("Scale"),  t[2])
607                 add_field(6.9, "te_seed",   fgettext("Seed"),   t[6])
608                 height = height + 1.1
609
610                 -- Second row
611                 add_field(0.3, "te_spreadx", fgettext("X spread"), t[3])
612                 if dimension == 3 then
613                         add_field(3.6, "te_spready", fgettext("Y spread"), t[4])
614                 else
615                         fields[#fields + 1] = "label[4," .. height - 0.2 .. ";" ..
616                                         fgettext("2D Noise") .. "]"
617                 end
618                 add_field(6.9, "te_spreadz", fgettext("Z spread"), t[5])
619                 height = height + 1.1
620
621                 -- Third row
622                 add_field(0.3, "te_octaves", fgettext("Octaves"),     t[7])
623                 add_field(3.6, "te_persist", fgettext("Persistance"), t[8])
624                 add_field(6.9, "te_lacun",   fgettext("Lacunarity"),  t[9])
625                 height = height + 1.1
626
627
628                 local enabled_flags = flags_to_table(t[10])
629                 local flags = {}
630                 for _, name in ipairs(enabled_flags) do
631                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
632                         flags[name] = true
633                 end
634                 for _, name in ipairs(setting.flags) do
635                         local checkbox_name = "cb_" .. name
636                         local is_enabled = flags[name] == true -- to get false if nil
637                         checkboxes[checkbox_name] = is_enabled
638                 end
639                 -- Flags
640                 formspec = table.concat(fields)
641                                 .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;"
642                                 .. fgettext("defaults") .. ";" -- defaults
643                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
644                                 .. "checkbox[5," .. height - 0.6 .. ";cb_eased;"
645                                 .. fgettext("eased") .. ";" -- eased
646                                 .. tostring(flags["eased"] == true) .. "]"
647                                 .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;"
648                                 .. fgettext("absvalue") .. ";" -- absvalue
649                                 .. tostring(flags["absvalue"] == true) .. "]"
650                 height = height + 1
651
652         elseif setting.type == "v3f" then
653                 local val = get_current_value(setting)
654                 local v3f = {}
655                 for line in val:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
656                         table.insert(v3f, line)
657                 end
658
659                 height = height + 0.3
660                 formspec = formspec
661                                 .. "field[0.3," .. height .. ";3.3,1;te_x;"
662                                 .. fgettext("X") .. ";" -- X
663                                 .. core.formspec_escape(v3f[1] or "") .. "]"
664                                 .. "field[3.6," .. height .. ";3.3,1;te_y;"
665                                 .. fgettext("Y") .. ";" -- Y
666                                 .. core.formspec_escape(v3f[2] or "") .. "]"
667                                 .. "field[6.9," .. height .. ";3.3,1;te_z;"
668                                 .. fgettext("Z") .. ";" -- Z
669                                 .. core.formspec_escape(v3f[3] or "") .. "]"
670                 height = height + 1.1
671
672         elseif setting.type == "flags" then
673                 local current_flags = flags_to_table(get_current_value(setting))
674                 local flags = {}
675                 for _, name in ipairs(current_flags) do
676                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
677                         if name:sub(1, 2) == "no" then
678                                 flags[name:sub(3)] = false
679                         else
680                                 flags[name] = true
681                         end
682                 end
683                 local flags_count = #setting.possible / 2
684                 local max_height = math.ceil(flags_count / 2) / 2
685
686                 -- More space for flags
687                 description_height = description_height - 1
688                 height = height - 1
689
690                 local fields = {} -- To build formspec
691                 local j = 1
692                 for _, name in ipairs(setting.possible) do
693                         if name:sub(1, 2) ~= "no" then
694                                 local x = 0.5
695                                 local y = height + j / 2 - 0.75
696                                 if j - 1 >= flags_count / 2 then -- 2nd column
697                                         x = 5
698                                         y = y - max_height
699                                 end
700                                 j = j + 1;
701                                 local checkbox_name = "cb_" .. name
702                                 local is_enabled = flags[name] == true -- to get false if nil
703                                 checkboxes[checkbox_name] = is_enabled
704
705                                 fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
706                                         x, y, checkbox_name, name, tostring(is_enabled)
707                                 )
708                         end
709                 end
710                 formspec = table.concat(fields)
711                 height = height + max_height + 0.25
712
713         else
714                 -- TODO: fancy input for float, int
715                 local text = get_current_value(setting)
716                 if dialogdata.error_message and dialogdata.entered_text then
717                         text = dialogdata.entered_text
718                 end
719                 formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;"
720                                 .. core.formspec_escape(text) .. "]"
721                 height = height + 1.15
722         end
723
724         -- Box good, textarea bad. Calculate textarea size from box.
725         local function create_textfield(size, label, text, bg_color)
726                 local textarea = {
727                         x = size.x + 0.3,
728                         y = size.y,
729                         w = size.w + 0.25,
730                         h = size.h * 1.16 + 0.12
731                 }
732                 return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
733                         size.x, size.y, size.w, size.h, bg_color or "#000",
734                         textarea.x, textarea.y, textarea.w, textarea.h,
735                         core.formspec_escape(label), core.formspec_escape(text)
736                 )
737
738         end
739
740         -- When there's an error: Shrink description textarea and add error below
741         if dialogdata.error_message then
742                 local error_box = {
743                         x = 0,
744                         y = description_height - 0.4,
745                         w = width - 0.25,
746                         h = 0.5
747                 }
748                 formspec = formspec ..
749                         create_textfield(error_box, "", dialogdata.error_message, "#600")
750                 description_height = description_height - 0.75
751         end
752
753         -- Get description field
754         local description_box = {
755                 x = 0,
756                 y = 0.2,
757                 w = width - 0.25,
758                 h = description_height
759         }
760
761         local setting_name = setting.name
762         if setting.readable_name then
763                 setting_name = fgettext_ne(setting.readable_name) ..
764                         " (" .. setting.name .. ")"
765         end
766
767         local comment_text
768         if setting.comment == "" then
769                 comment_text = fgettext_ne("(No description of setting given)")
770         else
771                 comment_text = fgettext_ne(setting.comment)
772         end
773
774         return (
775                 "size[" .. width .. "," .. height + 0.25 .. ",true]" ..
776                 create_textfield(description_box, setting_name, comment_text) ..
777                 formspec ..
778                 "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
779                         fgettext("Save") .. "]" ..
780                 "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
781                         fgettext("Cancel") .. "]"
782         )
783 end
784
785 local function handle_change_setting_buttons(this, fields)
786         local setting = settings[selected_setting]
787         if fields["btn_done"] or fields["key_enter"] then
788                 if setting.type == "bool" then
789                         local new_value = fields["dd_setting_value"]
790                         -- Note: new_value is the actual (translated) value shown in the dropdown
791                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
792
793                 elseif setting.type == "enum" then
794                         local new_value = fields["dd_setting_value"]
795                         core.settings:set(setting.name, new_value)
796
797                 elseif setting.type == "int" then
798                         local new_value = tonumber(fields["te_setting_value"])
799                         if not new_value or math.floor(new_value) ~= new_value then
800                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
801                                 this.data.entered_text = fields["te_setting_value"]
802                                 core.update_formspec(this:get_formspec())
803                                 return true
804                         end
805                         if setting.min and new_value < setting.min then
806                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
807                                 this.data.entered_text = fields["te_setting_value"]
808                                 core.update_formspec(this:get_formspec())
809                                 return true
810                         end
811                         if setting.max and new_value > setting.max then
812                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
813                                 this.data.entered_text = fields["te_setting_value"]
814                                 core.update_formspec(this:get_formspec())
815                                 return true
816                         end
817                         core.settings:set(setting.name, new_value)
818
819                 elseif setting.type == "float" then
820                         local new_value = tonumber(fields["te_setting_value"])
821                         if not new_value then
822                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
823                                 this.data.entered_text = fields["te_setting_value"]
824                                 core.update_formspec(this:get_formspec())
825                                 return true
826                         end
827                         if setting.min and new_value < setting.min then
828                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
829                                 this.data.entered_text = fields["te_setting_value"]
830                                 core.update_formspec(this:get_formspec())
831                                 return true
832                         end
833                         if setting.max and new_value > setting.max then
834                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
835                                 this.data.entered_text = fields["te_setting_value"]
836                                 core.update_formspec(this:get_formspec())
837                                 return true
838                         end
839                         core.settings:set(setting.name, new_value)
840
841                 elseif setting.type == "flags" then
842                         local values = {}
843                         for _, name in ipairs(setting.possible) do
844                                 if name:sub(1, 2) ~= "no" then
845                                         if checkboxes["cb_" .. name] then
846                                                 table.insert(values, name)
847                                         else
848                                                 table.insert(values, "no" .. name)
849                                         end
850                                 end
851                         end
852
853                         checkboxes = {}
854
855                         local new_value = table.concat(values, ", ")
856                         core.settings:set(setting.name, new_value)
857
858                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
859                         local np_flags = {}
860                         for _, name in ipairs(setting.flags) do
861                                 if checkboxes["cb_" .. name] then
862                                         table.insert(np_flags, name)
863                                 end
864                         end
865
866                         checkboxes = {}
867
868                         if setting.type == "noise_params_2d" then
869                                  fields["te_spready"] = fields["te_spreadz"]
870                         end
871                         local new_value = {
872                                 offset = fields["te_offset"],
873                                 scale = fields["te_scale"],
874                                 spread = {
875                                         x = fields["te_spreadx"],
876                                         y = fields["te_spready"],
877                                         z = fields["te_spreadz"]
878                                 },
879                                 seed = fields["te_seed"],
880                                 octaves = fields["te_octaves"],
881                                 persistence = fields["te_persist"],
882                                 lacunarity = fields["te_lacun"],
883                                 flags = table.concat(np_flags, ", ")
884                         }
885                         core.settings:set_np_group(setting.name, new_value)
886
887                 elseif setting.type == "v3f" then
888                         local new_value = "("
889                                         .. fields["te_x"] .. ", "
890                                         .. fields["te_y"] .. ", "
891                                         .. fields["te_z"] .. ")"
892                         core.settings:set(setting.name, new_value)
893
894                 else
895                         local new_value = fields["te_setting_value"]
896                         core.settings:set(setting.name, new_value)
897                 end
898                 core.settings:write()
899                 this:delete()
900                 return true
901         end
902
903         if fields["btn_cancel"] then
904                 this:delete()
905                 return true
906         end
907
908         if fields["btn_browser_path"] then
909                 core.show_path_select_dialog("dlg_browse_path",
910                         fgettext_ne("Select directory"), false)
911         end
912
913         if fields["btn_browser_filepath"] then
914                 core.show_path_select_dialog("dlg_browse_path",
915                         fgettext_ne("Select file"), true)
916         end
917
918         if fields["dlg_browse_path_accepted"] then
919                 this.data.selected_path = fields["dlg_browse_path_accepted"]
920                 core.update_formspec(this:get_formspec())
921         end
922
923         if setting.type == "flags"
924                         or setting.type == "noise_params_2d"
925                         or setting.type == "noise_params_3d" then
926                 for name, value in pairs(fields) do
927                         if name:sub(1, 3) == "cb_" then
928                                 checkboxes[name] = value == "true"
929                         end
930                 end
931         end
932
933         return false
934 end
935
936 local function create_settings_formspec(tabview, _, tabdata)
937         local formspec = "size[12,5.4;true]" ..
938                         "tablecolumns[color;tree;text,width=28;text]" ..
939                         "tableoptions[background=#00000000;border=false]" ..
940                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
941                         "field_close_on_enter[search_string;false]" ..
942                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
943                         "table[0,0.8;12,3.5;list_settings;"
944
945         local current_level = 0
946         for _, entry in ipairs(settings) do
947                 local name
948                 if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
949                         name = fgettext_ne(entry.readable_name)
950                 else
951                         name = entry.name
952                 end
953
954                 if entry.type == "category" then
955                         current_level = entry.level
956                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
957
958                 elseif entry.type == "bool" then
959                         local value = get_current_value(entry)
960                         if core.is_yes(value) then
961                                 value = fgettext("Enabled")
962                         else
963                                 value = fgettext("Disabled")
964                         end
965                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
966                                         .. value .. ","
967
968                 elseif entry.type == "key" then --luacheck: ignore
969                         -- ignore key settings, since we have a special dialog for them
970
971                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
972                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
973                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
974
975                 else
976                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
977                                         .. core.formspec_escape(get_current_value(entry)) .. ","
978                 end
979         end
980
981         if #settings > 0 then
982                 formspec = formspec:sub(1, -2) -- remove trailing comma
983         end
984         formspec = formspec .. ";" .. selected_setting .. "]" ..
985                         "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
986                         "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
987                         "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
988                         "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
989                                         .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
990
991         return formspec
992 end
993
994 local function handle_settings_buttons(this, fields, tabname, tabdata)
995         local list_enter = false
996         if fields["list_settings"] then
997                 selected_setting = core.get_table_index("list_settings")
998                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
999                         -- Directly toggle booleans
1000                         local setting = settings[selected_setting]
1001                         if setting and setting.type == "bool" then
1002                                 local current_value = get_current_value(setting)
1003                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
1004                                 core.settings:write()
1005                                 return true
1006                         else
1007                                 list_enter = true
1008                         end
1009                 else
1010                         return true
1011                 end
1012         end
1013
1014         if fields.search or fields.key_enter_field == "search_string" then
1015                 if search_string == fields.search_string then
1016                         if selected_setting > 0 then
1017                                 -- Go to next result on enter press
1018                                 local i = selected_setting + 1
1019                                 local looped = false
1020                                 while i > #settings or settings[i].type == "category" do
1021                                         i = i + 1
1022                                         if i > #settings then
1023                                                 -- Stop infinte looping
1024                                                 if looped then
1025                                                         return false
1026                                                 end
1027                                                 i = 1
1028                                                 looped = true
1029                                         end
1030                                 end
1031                                 selected_setting = i
1032                                 core.update_formspec(this:get_formspec())
1033                                 return true
1034                         end
1035                 else
1036                         -- Search for setting
1037                         search_string = fields.search_string
1038                         settings, selected_setting = filter_settings(full_settings, search_string)
1039                         core.update_formspec(this:get_formspec())
1040                 end
1041                 return true
1042         end
1043
1044         if fields["btn_edit"] or list_enter then
1045                 local setting = settings[selected_setting]
1046                 if setting and setting.type ~= "category" then
1047                         local edit_dialog = dialog_create("change_setting",
1048                                         create_change_setting_formspec, handle_change_setting_buttons)
1049                         edit_dialog:set_parent(this)
1050                         this:hide()
1051                         edit_dialog:show()
1052                 end
1053                 return true
1054         end
1055
1056         if fields["btn_restore"] then
1057                 local setting = settings[selected_setting]
1058                 if setting and setting.type ~= "category" then
1059                         core.settings:remove(setting.name)
1060                         core.settings:write()
1061                         core.update_formspec(this:get_formspec())
1062                 end
1063                 return true
1064         end
1065
1066         if fields["btn_back"] then
1067                 this:delete()
1068                 return true
1069         end
1070
1071         if fields["cb_tech_settings"] then
1072                 core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
1073                 core.settings:write()
1074                 core.update_formspec(this:get_formspec())
1075                 return true
1076         end
1077
1078         return false
1079 end
1080
1081 function create_adv_settings_dlg()
1082         local dlg = dialog_create("settings_advanced",
1083                                 create_settings_formspec,
1084                                 handle_settings_buttons,
1085                                 nil)
1086
1087                                 return dlg
1088 end
1089
1090 -- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'.
1091 -- For RUN_IN_PLACE the generated files may appear in the 'bin' folder.
1092 -- See comment and alternative line at the end of 'generate_from_settingtypes.lua'.
1093
1094 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM..
1095 --              "generate_from_settingtypes.lua"))(parse_config_file(true, false))